Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
# DevOps W07 In-Class Exercise Template

This repository contains a full-stack application with a SvelteKit client and a Spring Boot server. It demonstrates modern web application architecture and DevOps practices.
This repository contains a full-stack canteen application with a SvelteKit client, Spring Boot server, and LLM recommendation service. It demonstrates modern web application architecture and DevOps practices.

## Project Overview

This project includes:
- **Client**: SvelteKit with TypeScript, TailwindCSS, and reusable UI components.
- **Server**: Spring Boot Java application with RESTful APIs.
- **DevOps**: Dockerized services, CI/CD pipelines, and production-ready deployment configurations.
- **Client**: SvelteKit with TypeScript, TailwindCSS, and reusable UI components for browsing canteen meals.
- **Server**: Spring Boot Java application with RESTful APIs, gRPC communication, and PostgreSQL integration.
- **LLM Service**: Python FastAPI service for generating meal recommendations using AI.
- **Database**: PostgreSQL for storing user preferences and application data.
- **DevOps**: Dockerized services, CI/CD pipelines, Helm charts, and production-ready deployment configurations.

## Prerequisites

- Node.js (v22 or later)
- Java JDK 21+
- Python 3.x
- Gradle
- Docker and Docker Compose
- Git
- Kubernetes and Helm (for Kubernetes deployment)

## Setup Instructions

Expand Down Expand Up @@ -48,8 +52,25 @@ cd w07-solution
./gradlew build
```

### LLM Service Setup

1. Navigate to the `llm` directory:
```bash
cd llm
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```

## Running the Application

### Start the Database

```bash
docker compose up database -d
```

### Start the Client

```bash
Expand All @@ -66,21 +87,39 @@ cd server
```
The server API will be available at [http://localhost:8080](http://localhost:8080).

### Start the LLM Service

```bash
cd llm
python main.py
```
The LLM service will be available at [http://localhost:5000](http://localhost:5000).

## Development Workflow

### Client Development

- Built with SvelteKit and TypeScript for a modern, reactive UI.
- TailwindCSS for styling.
- Components and routes are organized in the `src` directory.
- Features meal browsing, favoriting, and user preferences.

### Server Development

- Built with Spring Boot for scalable and maintainable server services.
- Includes gRPC communication with the LLM service.
- PostgreSQL integration for user preferences storage.
- RESTful APIs for canteen data and user management.
- Gradle is used for dependency management and building.
- Source code is in the `src/main/java` directory.
- Tests are in the `src/test/java` directory.

### LLM Service Development

- Built with FastAPI for AI-powered meal recommendations.
- Integrates with external LLM APIs for generating personalized suggestions.
- Source code is in the `llm` directory.

## Building for Production

### Client Build
Expand All @@ -103,13 +142,31 @@ The project includes Docker configurations for containerized deployment.

### Build and Run with Docker Compose

1. Build and start the services:
1. Build and start all services:
```bash
docker compose up --build
```
2. Access the application:
- Client: [http://localhost:3000](http://localhost:3000)
- Server: [http://localhost:8080](http://localhost:8080)
- LLM Service: [http://localhost:5000](http://localhost:5000)
- Database: PostgreSQL on port 5432

## Kubernetes Deployment

The project includes Helm charts for Kubernetes deployment.

### Deploy with Helm

1. Update the `tumid` value in [`helm/canteen-app/values.yaml`](helm/canteen-app/values.yaml):
```yaml
tumid: your-tum-id
```

2. Install the Helm chart:
```bash
helm install canteen ./helm/canteen-app
```

## CI/CD Pipeline

Expand All @@ -122,18 +179,31 @@ The project includes GitHub Actions workflows for:
```
├── client/ # SvelteKit client
│ ├── src/ # Source code
│ ├── public/ # Static assets
│ ├── static/ # Static assets
│ └── package.json # Client dependencies
├── server/ # Spring Boot server
│ ├── src/ # Source code
│ ├── src/ # Source code including gRPC services
│ ├── build.gradle # Gradle build file
│ └── Dockerfile # Server Dockerfile
├── llm/ # Python LLM service
│ ├── main.py # FastAPI application
│ ├── requirements.txt # Python dependencies
│ └── Dockerfile # LLM service Dockerfile
├── helm/ # Kubernetes Helm charts
│ └── canteen-app/ # Main application chart
├── docs/ # API documentation (Bruno collection)
├── compose.yml # Docker Compose for local development
└── .github/workflows/ # CI/CD workflows
```

## API Documentation

API documentation is available in the [`docs/CanteenApp Bruno`](docs/CanteenApp%20Bruno) directory as a Bruno collection for testing endpoints.

## License

This project is licensed under the MIT License.
3 changes: 3 additions & 0 deletions client/src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { env } from '$env/dynamic/public';

export let BaseURL = env.PUBLIC_API_URL || "http://localhost:8080/api";
1 change: 1 addition & 0 deletions client/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
export { getCookie, setCookie } from './utils';
6 changes: 6 additions & 0 deletions client/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@ export type Meal = {
name: string;
dish_type: string;
labels: string[];
favorite: boolean;
};

export type UserPreferences = {
username: string;
favoriteMeals: string[];
};
13 changes: 13 additions & 0 deletions client/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function getCookie(name: string): string | null {
if (typeof document === 'undefined') return null;
return document.cookie
.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1] || null;
}

export function setCookie(name: string, value: string, days: number) {
if (typeof document === 'undefined') return;
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
}
69 changes: 2 additions & 67 deletions client/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<script lang="ts">
import {onMount} from "svelte";
import "../app.css";
import Icon from "@iconify/svelte";
import {env} from '$env/dynamic/public';

import FoodCard from './FoodCard.svelte';
import type {Meal} from '$lib/types';
Expand All @@ -11,70 +9,7 @@
let {data}: PageProps = $props();

// For more information on runes and reactivity, see: https://svelte.dev/docs/svelte/what-are-runes
let meals: Meal[] = data.meals;

// Note: The data loading is now done in the +page.ts file (compared to the W03 exercise)
// and passed to this component as props. This is a more efficient way to load data in SvelteKit.

// Track favorites
let favorites: string[] = []; //list of favorite meal names

//get the username from the cookie
let username: string | null = null;

onMount(async () => {
let username = getCookie('username');
// If the username exists, fetch the list of favorites from the backend
if (username) {
const response = await fetch(`/preferences/${username}`);
const data = await response.json();
favorites = data.favorites || []; // Load favorites from the backend
} else {
//store the name in the cookie
const name = prompt("Welcome! What's your name?");
if (name) {
setCookie('username', name, 365);
username = name;
}
}
});


function setCookie(name: string, value: string, days: number) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
}

function getCookie(name: string): string | null {
return document.cookie
.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1] || null;
}

// function callback from FoodCard.svelte
const handleFavoriteChange = (mealName: string) => {
// Toggle the favorite meal status
if (favorites.includes(mealName)) {
favorites = favorites.filter(fav => fav !== mealName); // Remove from favorites
} else {
favorites.push(mealName); // Add to favorites
}

// Send updated favorites list to the server
fetch('/preferences', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: username,
favorites: favorites,
}),
});
};


let meals: Meal[] = $state(data.meals);
</script>

<main>
Expand All @@ -90,7 +25,7 @@
{:else}
<div class="food-grid">
{#each meals as meal}
<FoodCard {meal} {favorites} onFavoriteChange={handleFavoriteChange}/>
<FoodCard {meal}/>
{/each}
</div>
{/if}
Expand Down
38 changes: 32 additions & 6 deletions client/src/routes/+page.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import type { PageLoad } from './$types';
import { env } from '$env/dynamic/public';
import { BaseURL} from '$lib/env';
import type { Meal, UserPreferences } from '$lib/types';
import { getCookie, setCookie } from '$lib';

export const ssr = false;

let baseUrl = env.PUBLIC_API_URL || "http://localhost:8080/api";

//get the username from the cookie
let username: string | null = null;

export const load: PageLoad = async ({ fetch }) => {
const res = await fetch(`${baseUrl}/mensa-garching/today`);
const meals = await res.json();
username = getCookie('username');

return { meals };
// Prompt for username if not found in cookie
if (!username) {
username = prompt('Please enter your username:');
if (username) {
setCookie('username', username, 30); // Store for 30 days
} else {
// Handle case where user cancels or enters empty string
username = 'anonymous';
}
}

const res = await fetch(`${BaseURL}/mensa-garching/today`);
const meals: Meal[] = await res.json();

const res2 = await fetch(`${BaseURL}/preferences/${username}`);
const preferences: UserPreferences = await res2.json();

console.log('Meals:', meals);
console.log('Preferences:', preferences);

// Set the boolean favorite property for each meal
meals.forEach((meal: any) => {
meal.favorite = preferences.favoriteMeals.includes(meal.name);
});

return { meals};
};
Loading