333 lines
11 KiB
Markdown
333 lines
11 KiB
Markdown
# Teknisk beskrivning av Recipe App
|
||
|
||
> Se [README.md](README.md) för användarinformation och kom-igång-guide.
|
||
|
||
## Översikt
|
||
|
||
Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och inköpsplanering. Systemet är byggt med Next.js (frontend), NestJS (backend), Prisma ORM och MariaDB. Applikationen är containeriserad med Docker och använder Caddy som reverse proxy.
|
||
|
||
---
|
||
|
||
## Versionsinformation
|
||
|
||
| Delsystem | Teknik | Version |
|
||
|-------------|----------------|-----------------|
|
||
| Frontend | Next.js | 16.2 |
|
||
| | React | 19.2 |
|
||
| | TypeScript | 5.4.5 |
|
||
| | Node | 22.x (via @types/node 22.15.29) |
|
||
| Backend | NestJS | 10.3 |
|
||
| | Prisma | 6.12.0 |
|
||
| | TypeScript | 5.4.5 |
|
||
| | Node | 22.x (via @types/node 22.15.29) |
|
||
| Databas | MariaDB | 11 |
|
||
| Proxy | Caddy | 2.x |
|
||
| Container | Docker | 24+ |
|
||
|
||
---
|
||
|
||
## Frontend
|
||
|
||
- **Framework:** Next.js 16.2 (App Router, server + client components)
|
||
- **Språk:** TypeScript 5.4.5
|
||
- **UI:** React 19.2, ingen CSS-ramverk (ren CSS-in-JS och inline-stilar)
|
||
- **Bygg:** Standalone output, körs i Docker-container
|
||
- **API-anrop:** Fetch mot backend och Next.js API routes
|
||
- **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden
|
||
|
||
### Funktioner (frontend)
|
||
|
||
- **Inventarielista:**
|
||
- Sök, filtrera och sortera hemmavaror (namn, plats, bäst före, A–Ö)
|
||
- Lägg till, redigera, konsumera och ta bort varor
|
||
- Konsumtionshistorik med enheter
|
||
- **Recept:**
|
||
- Lista, skapa, redigera och ta bort recept
|
||
- Jämför recept mot hemmavaror (räcker/saknas/enhetskonflikt)
|
||
- Visar instruktioner och saknade ingredienser för valt recept
|
||
- Sidebar med snabblista över recept
|
||
- **Admin: Produkter:**
|
||
- Sök och sortera produkter (A–Ö, senast tillagda)
|
||
- Redigera canonical name
|
||
- Merge preview för produktnamn
|
||
- **Felhantering:**
|
||
- Svenska felmeddelanden, tydliga varningar vid valideringsfel
|
||
- **Responsiv design:**
|
||
- Fungerar på mobil, surfplatta och desktop
|
||
|
||
---
|
||
|
||
## Backend
|
||
|
||
- **Framework:** NestJS 10.3
|
||
- **Språk:** TypeScript 5.4.5
|
||
- **Databas:** MariaDB 11 (via Prisma 6.12.0)
|
||
- **API:** REST, validering med class-validator
|
||
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
|
||
- **Hälsokontroll:** /health endpoint med status, uptime, DB-latens
|
||
- **Bygg:** Körs i Docker-container, byggs med nest build
|
||
|
||
### Funktioner (backend)
|
||
|
||
- **Inventarie-API:**
|
||
- CRUD för hemmavaror
|
||
- Konsumtionshistorik (med enheter)
|
||
- Sortering och filtrering (plats, bäst före, namn)
|
||
- **Recept-API:**
|
||
- CRUD för recept och ingredienser
|
||
- Preview mot hemmavaror (räcker/saknas/enhetskonflikt)
|
||
- **Produkt-API:**
|
||
- CRUD för produkter
|
||
- Merge preview och canonical name
|
||
- **Felhantering:**
|
||
- Svenska felmeddelanden, 400/503 status
|
||
- **Hälsokontroll:**
|
||
- /health endpoint (200/503, DB-status, uptime)
|
||
|
||
---
|
||
|
||
## Infrastruktur & DevOps
|
||
|
||
- **Docker Compose:** Orkestrerar frontend, backend, databas och proxy
|
||
- **Caddy:** Reverse proxy, hanterar Next.js API routes och backend
|
||
- **Miljövariabler:** Hanterar DB-url, ports etc
|
||
- **Backup-script:** backup_recipe_app.sh
|
||
|
||
---
|
||
|
||
## Viktiga filer & mappar
|
||
|
||
- `frontend/app/` – Next.js app directory (pages, komponenter)
|
||
- `backend/src/` – NestJS API (controllers, services, modules)
|
||
- `backend/prisma/schema.prisma` – Prisma datamodell
|
||
- `compose.yml` – Docker Compose setup
|
||
- `Caddyfile` – Proxyregler
|
||
|
||
---
|
||
|
||
## Funktionell översikt
|
||
|
||
### Hemmavaror
|
||
- Lägg till, redigera, ta bort och konsumera varor
|
||
- Sök, filtrera (plats), sortera (bäst före, namn)
|
||
- Konsumtionshistorik med enheter
|
||
|
||
### Recept
|
||
- Skapa, redigera, ta bort recept
|
||
- Jämför mot hemmavaror (räcker/saknas/enhetskonflikt)
|
||
- Visar instruktioner och saknade ingredienser
|
||
- **Importera recept från Markdown** (se nedan)
|
||
|
||
### Produkter (Admin)
|
||
- Sök, sortera, redigera canonical name
|
||
- Merge preview
|
||
|
||
### Hälsa & Fel
|
||
- /health endpoint (status, uptime, DB)
|
||
- Svenska felmeddelanden i hela systemet
|
||
|
||
---
|
||
|
||
## Exempel på API-endpoints
|
||
|
||
- `GET /api/inventory` – Lista hemmavaror
|
||
- `POST /api/inventory` – Lägg till vara
|
||
- `PATCH /api/inventory/:id` – Uppdatera vara
|
||
- `DELETE /api/inventory/:id` – Ta bort vara
|
||
- `GET /api/recipes` – Lista recept
|
||
- `POST /api/recipes` – Skapa recept
|
||
- `PATCH /api/recipes/:id` – Uppdatera recept
|
||
- `DELETE /api/recipes/:id` – Ta bort recept
|
||
- `POST /api/recipes/parse-markdown` – Tolka Markdown-recept (se nedan)
|
||
- `GET /api/products` – Lista produkter
|
||
- `PATCH /api/products/:id` – Uppdatera produkt
|
||
- `GET /health` – Hälsokontroll
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Receptimport via Markdown
|
||
|
||
### Syfte
|
||
|
||
Användaren kan importera ett recept skrivet i ett enkelt Markdown-format istället för att fylla i formularet manuellt. Systemet tolkar texten, föreslår matchande produkter från databasen och låter användaren granska och bekräfta innan receptet sparas.
|
||
|
||
### Markdown-format
|
||
|
||
```markdown
|
||
# Receptnamn
|
||
|
||
Valfri beskrivning av receptet.
|
||
|
||
## Ingredienser
|
||
- 500 g köttfärs
|
||
- 1 st lök
|
||
- 2 msk tomatpuré
|
||
- 1 dl grädde (vispgrädde)
|
||
|
||
## Tillvägagångssätt
|
||
Stek löken i lite smör. Tillsats köttfärsen...
|
||
```
|
||
|
||
Regler:
|
||
- Rad med `#` tolkas som receptnamn
|
||
- Text mellan `#`-rubriken och `## Ingredienser` tolkas som beskrivning
|
||
- Rader under `## Ingredienser` med mönstret `- ANTAL ENHET NAMN` tolkas som ingredienser
|
||
- Text i parentes efter ingrediensnamnet (`(vispgrädde)`) sparas som anteckning
|
||
- Text under `## Tillvägagångssätt` (eller `## Instruktioner`) tolkas som instruktioner
|
||
|
||
### Arkitektur
|
||
|
||
```
|
||
Användaren Frontend Backend Bibliotek
|
||
(klistrar in MD) → /recipes/import → POST /api/ → recipe-document-
|
||
ImportRecipePage recipes/ converter/
|
||
parse-markdown parseRecipeMarkdown()
|
||
↑
|
||
Granskar förslag
|
||
Väljer produkter
|
||
↓
|
||
POST /api/recipes (befintlig endpoint)
|
||
```
|
||
|
||
### Komponenterna
|
||
|
||
#### `recipe-document-converter/` (fristående TypeScript-bibliotek)
|
||
|
||
Ett eget npm-paket som inte har externa beroenden. Det enda som exporteras är:
|
||
|
||
```typescript
|
||
parseRecipeMarkdown(markdown: string): ParsedRecipe
|
||
```
|
||
|
||
Returnerar:
|
||
```typescript
|
||
type ParsedRecipe = {
|
||
name: string;
|
||
description?: string;
|
||
instructions?: string;
|
||
ingredients: Array<{
|
||
rawName: string; // fråntext, t.ex. "köttfärs"
|
||
quantity: number; // t.ex. 500
|
||
unit: string; // t.ex. "g"
|
||
note?: string; // text i parentes, t.ex. "nötfärs"
|
||
}>;
|
||
};
|
||
```
|
||
|
||
Biblioteket kompileras i ett separat Docker-byggsteg och länkas till backend via `"recipe-document-converter": "file:../recipe-document-converter"` i `backend/package.json`.
|
||
|
||
#### Backend — `POST /api/recipes/parse-markdown`
|
||
|
||
Endpoint som tar emot `{ markdown: string }` och returnerar det tolkade receptet åtsamman med produktmatchförslag för varje ingrediens.
|
||
|
||
Matchningslogik:
|
||
1. Anropar `parseRecipeMarkdown()` från biblioteket
|
||
2. Hämtar alla aktiva produkter ur databasen
|
||
3. Jämför varje ingrediensnamn mot `product.canonicalName` / `product.normalizedName` med tre metoder i ordning:
|
||
- **Exakt match** (efter normalisering) → 100 poäng
|
||
- **Delsträngsmatch** → 70 poäng
|
||
- **Levenshtein-likhet** → 0–100 poäng (filtreras under 40)
|
||
4. Returnerar upp till 5 förslag per ingrediens, sorterade efter poäng
|
||
|
||
Svar:
|
||
```json
|
||
{
|
||
"name": "Köttfärssås",
|
||
"description": "En klassisk...",
|
||
"instructions": "Stek löken...",
|
||
"ingredients": [
|
||
{
|
||
"rawName": "köttfärs",
|
||
"quantity": 500,
|
||
"unit": "g",
|
||
"suggestions": [
|
||
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
|
||
{ "productId": 34, "productName": "Blandfärs", "score": 55 }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### Frontend — `/recipes/import`
|
||
|
||
En 3-stegsvy (client component):
|
||
|
||
| Steg | Innehåll |
|
||
|------|----------|
|
||
| 1. Klistra in | Textarea för Markdown + "Tolka recept"-knapp |
|
||
| 2. Granska | Redigerbara fält för namn/beskrivning/instruktioner; varje ingrediens har en dropdown med föreslagna produkter överst, sedan alla produkter |
|
||
| 3. Spara | Knapp som POSTar till befintlig `POST /api/recipes` |
|
||
|
||
Ingrediensräder med ingen matchning markeras visuellt (gul ram) så att användaren ser att de behöver väljas manuellt. Receptet sparas inte förrän minst en ingrediens har en vald produkt.
|
||
|
||
Flöde i Next.js:
|
||
```
|
||
/recipes/import
|
||
└─ ImportRecipePage.tsx (client component, 3-stegsflödet)
|
||
|
||
/api/parse-markdown-proxy
|
||
└─ route.ts (POST-proxy till backend, omgår CORS)
|
||
```
|
||
|
||
### Docker-bygget
|
||
|
||
Backend-Dockerfilen använder nu projektets rot (`.`) som byggkontext. Converter-biblioteket kompileras i en separat stage:
|
||
|
||
```dockerfile
|
||
# Stage 1: Bygg converter-biblioteket
|
||
FROM node:22-alpine AS converter-build
|
||
WORKDIR /converter
|
||
COPY recipe-document-converter/package.json ./
|
||
RUN npm install
|
||
COPY recipe-document-converter/src ./src
|
||
COPY recipe-document-converter/tsconfig.json ./
|
||
RUN npm run build
|
||
|
||
# Stage 2: Installera backend-beroenden (converter kopieras in)
|
||
FROM node:22-alpine AS deps
|
||
WORKDIR /app
|
||
COPY --from=converter-build /converter /recipe-document-converter
|
||
...
|
||
```
|
||
|
||
Bygga om backend efter ändringar:
|
||
```bash
|
||
docker compose build recipe-api
|
||
```
|
||
|
||
### Relevanta filer
|
||
|
||
| Fil | Syfte |
|
||
|-----|-------|
|
||
| `recipe-document-converter/src/parser.ts` | Markdown-parser |
|
||
| `recipe-document-converter/src/index.ts` | Biblioteksexport |
|
||
| `backend/src/recipes/dto/parse-markdown.dto.ts` | Inkommande DTO |
|
||
| `backend/src/recipes/recipes.controller.ts` | Nytt endpoint |
|
||
| `backend/src/recipes/recipes.service.ts` | Matchningslogik |
|
||
| `frontend/app/recipes/import/ImportRecipePage.tsx` | 3-stegsvy |
|
||
| `frontend/app/api/parse-markdown-proxy/route.ts` | Proxy-route |
|
||
|
||
---
|
||
|
||
## Säkerhet
|
||
|
||
- Ingen auth i grundutförande (kan enkelt byggas på)
|
||
- Validering av all input (class-validator)
|
||
- Felmeddelanden på svenska
|
||
|
||
---
|
||
|
||
## Utbyggbarhet
|
||
|
||
- Lätt att lägga till fler fält, filter och funktioner
|
||
- Kan utökas med auth, shoppinglistor, delning m.m.
|
||
|
||
---
|
||
|
||
## Kontakt
|
||
|
||
För frågor, kontakta utvecklaren.
|