Requires Docker Desktop.
cp .env.example .env # then set LOGOS_KEY=... to enable the LLM (Optional step, otw canned responses)
docker compose up --buildOpen http://localhost:8081.
Also started by compose: Prometheus and
Grafana (admin / bytebite), and the
Swagger UI.
| URL | Login | |
|---|---|---|
| App (Kubernetes) | https://team-bytebite.stud.k8s.aet.cit.tum.de | admin@bytebite.dev / password |
| Swagger UI | https://team-bytebite.stud.k8s.aet.cit.tum.de/swagger-ui.html | — |
| Grafana | https://team-bytebite.stud.k8s.aet.cit.tum.de/grafana | admin / bytebite |
The app also deploys to an Azure VM at http://<public_ip>:8081, with the Swagger UI on
:8080/swagger-ui.html, Prometheus on :9090 and Grafana on :3000. The IP is assigned by
Terraform, get it from the GitHub Actions result.
Both deployments run automatically on every merge to main.
Cooking a new meal often starts with inspiration from a blog, a social media post, or a handwritten note. However, the transition from "finding a recipe" to "having the ingredients" is filled with friction. Users often have to manually read through long descriptions, identify specific ingredients, estimate quantities, and then rewrite them into a categorized list suitable for a grocery store layout.
ByteBite solves this by removing the manual labor of list-making. It addresses the user's need for efficiency and accuracy, ensuring no ingredient is overlooked and reducing the time spent planning meals.
The core of the application is an intelligent parser that transforms unconcrete recipes into clearly structured lists with quantity estimations, allowing an improved shopping and cooking experience. Key features include:
- Recipe Extraction: Paste a full recipe text (including stories or instructions), and the app extracts only the necessary ingredients. Alternatively, paste the name of a recipe and the app will generate a full ingredients list.
- Intelligent Categorization: Ingredients are automatically grouped by grocery store aisles (e.g., Produce, Dairy, Spices, Meat).
- Dietary Filtering & Substitution: Users can state preferences (Vegan, Vegetarian, Gluten Free, Lactose Free). The app flags "red flag" ingredients and suggests a safe alternative for each.
- Cross-Recipe Merging: Combine several recipes into one shopping list. Duplicates are summed, and ingredients that mean the same thing under different names are merged into a single entry.
- The Busy Professional: Someone who wants to cook healthy meals but lacks the time to manually plan grocery trips.
- The Home Cook: Enthusiasts who love trying new recipes from diverse sources but find the organization part tedious.
- Students on a Budget: Users who need to ensure they only buy exactly what they need for a specific set of meals to avoid food waste.
Unlike traditional apps that rely on rigid "If/Then" logic or specific input formatting, ByteBite uses Generative AI (LLMs) to leverage its knowledge of countless recipes. Each of the following would be impractical to implement with pattern matching or a fixed ingredient database:
- Extraction from Unstructured Text: The input is free-form, a dish name, a tidy ingredient list, or a rambling blog post. The model decides what is an ingredient and what is the author's story about their grandmother. No parser, delimiter, or expected format.
- Knowledge-Based Generation: Given only a dish name, the model produces the ingredients a typical recipe requires, drawing on what it already knows about that dish. There is no recipe database behind this.
- Semantic Categorization: Ingredients are assigned to a store aisle by meaning, not by lookup, "fresh basil" goes to Produce while "dried basil" goes to Spices, and "canned tomatoes" to Pantry while "fresh tomatoes" go to Produce.
- Dietary Substitution: Ingredients that violate a stated restriction are flagged, and the model proposes a substitute that fits the dish rather than a generic swap, lactose-free yogurt for heavy cream in a pan sauce.
- Synonym-Aware Merging: When several recipes are combined, the model recognizes that "cilantro" and "coriander" are the same purchase and sums them, while keeping "garlic clove" and "garlic powder" apart. It converts mismatched units before adding quantities.
Unit conversion to metric and quantity estimation for vague amounts ("salt to taste") also run through the model.
- User Action: Jason finds a 2,000-word blog post about "The Best Sunday Roast." He copies the entire text, including the author's life story, and pastes it into ByteBite.
- App Action: The AI ignores the anecdotes about the author's grandmother and generates a clean, metric list: "1500 g Beef Brisket" (Meat), "4 piece Carrots" (Produce), "Fresh Rosemary, N/A" (Produce), each already sorted into its aisle.
- User Action: Mark wants to cook Chicken Piccata, but he is lactose intolerant. He enters the dish and selects the Lactose Free filter before generating.
- App Action: The list comes back with heavy cream marked as restricted and shown alongside a suggested swap, lactose free yogurt, so Mark can see at a glance which item to replace and what to buy instead.
- User Action: A user saves three recipes for the week, Tacos, Stir-fry, and Salad, then selects all three and merges them into a single grocery list.
- App Action: The AI combines the three ingredient lists into one. Limes appear in all three recipes, so their quantities are added into a single entry instead of three. It also recognizes that the "coriander" in the Stir-fry and the "cilantro" in the Tacos are the same purchase and merges those too. The result is one aisle-sorted list with nothing bought twice.
The project is split across three students, each owning one application area and one operations area.
| Student | Application | Operations |
|---|---|---|
| Jonathan | GenAI: FastAPI service, prompt design, LLM providers | Azure deployment: Terraform and Ansible |
| Malik | Server: API gateway, user service, grocery service | Monitoring: Prometheus, Grafana dashboards and alerting |
| Tim | Client: React frontend | Kubernetes deployment |
These are main responsibilities, not exclusive ownership. The areas overlap in practice, and everyone contributed outside their own column. The exact task distribution is tracked on the project board.
team-bytebite/
├── client/ # React + Vite frontend
├── gen-ai/ # Python FastAPI AI generation service
├── server/ # Java Spring Boot microservices
│ ├── api-gateway/ # Public entrypoint, routes requests to backend services
│ ├── user-service/ # User domain service
│ └── grocery-service/ # Grocery and recipe domain service
├── databases/ # Database image definitions and init schemas
├── helm/ # Helm chart for the Kubernetes deployment
├── infra/ # Terraform (Azure VM) + Ansible (configure & deploy)
├── monitoring/ # Prometheus scrape config, Grafana dashboards and alerts
└── documentation/ # Architecture diagrams (DrawIO + exported images)
Each directory has its own README with the details specific to it.
Diagrams live in documentation/, as both editable .drawio sources and exported
images:
- Component Diagram, how the services fit together
- Class Diagram, the domain model
- DB Schema Diagram, the user and grocery databases
- Use Case Diagram, what users can do
The user-facing web application. Provides a dish name input and displays the generated shopping list. Communicates with the backend via REST.
The public backend entrypoint. Receives frontend API requests and forwards them to the owning backend service.
Owns user-related data and connects to the user database.
Owns recipes, grocery lists, and grocery items. Connects to the grocery database and calls the gen-ai service when ingredient generation is needed.
The AI generation service. Receives a dish name from the server and returns a shopping list with all required ingredients using LLM integrations.
Each service has its own detailed setup instructions in its respective directory's README. A short summary also follows here.
Requires Java 21, Node 22, and Python 3.12. Each service runs in its own terminal.
1. Gen-AI (port 8000), create gen-ai/.env with LOGOS_KEY=...; add OPENAI_API_KEY=sk-... if you want to use the OpenAI switch. A local, offline option via LM Studio is also available, see gen-ai/README.md.
cd gen-ai
python -m venv .venv
.venv/Scripts/Activate.ps1 # Windows
pip install -r requirements.txt
uvicorn main:app --reload2. User Service (port 8083)
cd server/user-service
./mvnw spring-boot:run3. Grocery Service (port 8082)
cd server/grocery-service
./mvnw spring-boot:run4. API Gateway (port 8080)
cd server/api-gateway
./mvnw spring-boot:run5. Client (port 5173)
cd client
npm install
npm run devWhen the backend services are running, the API gateway exposes the aggregated Swagger UI at:
http://localhost:8080/swagger-ui.html
The UI includes the User Service, Grocery Service, and Gen AI Service OpenAPI definitions. The raw specs are available through the gateway at /v3/api-docs/user-service, /v3/api-docs/grocery-service, and /v3/api-docs/gen-ai.
On the deployed environments the same UI lives at:
| Deployment | Swagger UI |
|---|---|
| Kubernetes | https://team-bytebite.stud.k8s.aet.cit.tum.de/swagger-ui.html |
| Azure VM | http://<public_ip>:8080/swagger-ui.html |
Every service is tested, and no test needs a running backend, database, or API key.
Java (JUnit), unit and lightweight integration tests:
api-gateway: JWT gateway filter behavior, protected-route rejection, and trustedX-User-*header injection.user-service: registration/login validation, password hashing behavior, current-user lookup, and JWT signing/verification.grocery-service: grocery item mapping, list create/update behavior, merge behavior around Gen AI responses/failures, and controller HTTP behavior.
cd server/api-gateway && ./mvnw test
cd server/user-service && ./mvnw test
cd server/grocery-service && ./mvnw testClient (Vitest + React Testing Library), unit tests for the API↔view-model mappers, component
tests driving real user interactions, and integration tests over the whole App with fetch
mocked at the network boundary. See client/README.md.
cd client && npm testGen-AI (pytest), both endpoints and their fallback paths, provider selection, prompt
construction, and JSON recovery, with the LLM client stubbed. pytest.ini
enforces 85% coverage of main.py. See gen-ai/README.md.
cd gen-ai && pip install -r requirements-dev.txt && pytestAll three suites run in CI on every push.
Three GitHub Actions workflows, in .github/workflows/:
| Workflow | Trigger | What it does |
|---|---|---|
| Test, Build and Push Images | every push, any branch | Runs the Java, client, and gen-ai test suites in parallel. Only if all three pass does it build the Docker images. Images are pushed to GHCR on main only. |
| Deploy to Kubernetes | green build of main |
helm upgrade --install to the AET cluster. |
| Provision and Deploy | green build of main |
terraform apply for the Azure VM, then the Ansible playbook to deploy onto it. |
So a failing test on any branch blocks the image build, and merging to main deploys to both
targets automatically. Both deploy workflows can also be run on demand from the Actions tab; a
manual run does not rerun the tests.
Every service is linted in CI (Test, Build and Push Images) as a blocking gate.
A lint failure fails the build and prevents images from being pushed or deployed.
| Service | Tool | Run locally |
|---|---|---|
client |
ESLint | cd client && npm run lint |
gen-ai |
Ruff | cd gen-ai && ruff check . |
api-gateway, user-service, grocery-service |
Spotless (google-java-format) | cd server/<service> && ./mvnw spotless:check |
For the Java services, auto-format any violations with ./mvnw spotless:apply.
Ruff config lives in gen-ai/pyproject.toml; ESLint config in client/eslint.config.js.
Every service is linted in CI (Test, Build and Push Images) as a blocking gate —
a lint failure fails the build and prevents images from being pushed or deployed.
| Service | Tool | Run locally |
|---|---|---|
client |
ESLint | cd client && npm run lint |
gen-ai |
Ruff | cd gen-ai && ruff check . |
api-gateway, user-service, grocery-service |
Spotless (google-java-format) | cd server/<service> && ./mvnw spotless:check |
For the Java services, auto-format any violations with ./mvnw spotless:apply.
Ruff config lives in gen-ai/pyproject.toml; ESLint config in client/eslint.config.js.
Requires Docker Desktop running.
Copy .env.example to .env and fill in LOGOS_KEY first. Compose reads it and
passes it to gen-ai. Without it, gen-ai still runs but serves a canned example ingredient list.
docker compose up --build
docker compose down # To take down laterDrop --build on subsequent starts if nothing has changed. To stop: docker compose down.
docker compose up also starts Prometheus and Grafana. Prometheus scrapes metrics
from all backend services. The Spring services expose metrics at
/actuator/prometheus (via Spring Boot Actuator + Micrometer) and gen-ai exposes
them at /metrics.
Open the Prometheus UI at http://localhost:9090, check http://localhost:9090/targets
to confirm every service is UP. The scrape configuration lives in
monitoring/prometheus.yml.
Open Grafana at http://localhost:3000 and log in with admin / bytebite, unless you override
GRAFANA_ADMIN_USER and GRAFANA_ADMIN_PASSWORD. Grafana is provisioned with the Prometheus
datasource and a ByteBite / ByteBite Overview dashboard from
monitoring/grafana.
Grafana also provisions a ByteBite service down alert. It evaluates the
Prometheus up metric every 30 seconds and fires when any scraped ByteBite
target stays down for at least 1 minute. In Grafana, open Alerting to see
the rule state and active firing alerts.
Requires a local Kubernetes cluster running via Docker Desktop.
kubectl config use-context docker-desktop
kubectl create namespace team-bytebite
helm upgrade --install bytebite ./helm/bytebite -f ./helm/bytebite/values-local.yaml --namespace team-bytebite --set genai.logosKey="lg-..." --atomic
helm uninstall bytebite --namespace team-bytebite # To take down laterOpen http://localhost:80
The local Helm values also expose monitoring services when supported by your local cluster:
- Prometheus: http://localhost:9090
- Grafana: http://localhost:3000 (
admin/bytebite)
Prerequisite: The team-bytebite namespace must exist in the cluster.
Deployment is automated via GitHub Actions:
- Automatic: every push to
maintriggers the build workflow, which triggers the deploy workflow on success. - Manual: go to Actions → Deploy to Kubernetes → Run workflow to manually start the deploy workflow.
Alternatively, you can do manual deployment with Helm:
(Requires helm and a valid kubeconfig)
kubectl config use-context stud
helm upgrade --install bytebite ./helm/bytebite --namespace team-bytebite --set genai.logosKey="lg-..." --atomic
helm uninstall bytebite --namespace team-bytebite # To take down laterThe app is available at https://team-bytebite.stud.k8s.aet.cit.tum.de
Grafana is available at https://team-bytebite.stud.k8s.aet.cit.tum.de/grafana
when monitoring.enabled and monitoring.grafana.ingress.enabled are true.
For evaluation, both the deployed app and Grafana come with a ready-to-use login:
| Service | URL | Username | Password |
|---|---|---|---|
| ByteBite app | https://team-bytebite.stud.k8s.aet.cit.tum.de | admin@bytebite.dev |
password |
| Grafana | https://team-bytebite.stud.k8s.aet.cit.tum.de/grafana | admin |
bytebite |
The app account is seeded by databases/user-db/init.sql; you can
also self-register a new account. The Grafana password is supplied at deploy time via the
GRAFANA_ADMIN_PASSWORD GitHub Actions secret, and the chart falls back to the same
admin / bytebite used everywhere else.
A second, independent deployment target: a single Azure VM running the same compose.yaml stack.
Infrastructure and configuration are split across two tools.
| Step | Tool | What it does |
|---|---|---|
| Provision | Terraform | Resource group, network, NSG, public IP, Ubuntu VM, SSH keypair. State lives remotely in Azure Storage. Writes an Ansible inventory + SSH key on apply. |
| Configure & deploy | Ansible | Installs Docker, copies compose.yaml + monitoring config + a rendered .env, logs into GHCR, and runs docker compose up -d --pull=always. Images are pulled, never built on the VM. |
.github/workflows/deploy-azure.yml (Provision and Deploy)
runs both steps in one job, automatically after a green build of main and on demand via
Run workflow. terraform apply is idempotent, so an unchanged infrastructure is a no-op and only
the Ansible deploy does work.
To run it by hand, see infra/terraform/README.md followed by infra/ansible/README.md, Terraform hands its generated inventory and SSH key straight to Ansible.
After a deploy, the app is at http://<public_ip>:8081, Prometheus at http://<public_ip>:9090,
and Grafana at http://<public_ip>:3000.