Files
recipe-app/TEKNISK_BESKRIVNING.md
T
Nils-Johan Gynther 4f183df711 feat: Implement quick import feature for recipes
- Added QuickImportController and QuickImportService to handle recipe imports from URLs and file paths.
- Created QuickImportModule to encapsulate the quick import functionality.
- Developed frontend ImportFilePage for users to upload files or enter URLs for recipe import.
- Integrated API proxy to communicate with the backend for quick import requests.
- Implemented WriteRecipePage for users to manually input recipes with Markdown support.
- Added page routing for the new import and write recipe functionalities.
2026-04-12 07:41:18 +02:00

713 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 matplanering. 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 (@types/node 22.15.29) |
| Backend | NestJS | 10.3 |
| | Prisma | 6.12.0 |
| | TypeScript | 5.4.5 |
| | Node | 22.x (@types/node 22.15.29) |
| Databas | MariaDB | 11 |
| Proxy | Caddy | 2.x |
| Container | Docker | 24+ |
| Converter | Node.js (TypeScript) | Noll externa beroenden |
---
## 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
### Frontend-sidor och komponenter
| Sida | Fil | Funktionalitet |
|------|-----|---|
| **Hem** | `app/page.tsx` | Startsida |
| **Navigering** | `app/Navigation.tsx` | Huvudmeny |
| **Inventorie** | `app/inventory/page.tsx` | Lista, filtrera, sortera varor |
| | `InventoryList.tsx` | Ritning av inventarieföremål |
| | `InventoryForm.tsx` | Skapa nytt inventarieföremål |
| | `InventoryEditForm.tsx` | Redigera inventarieföremål |
| | `InventoryConsumeForm.tsx` | Konsumera (brukat) inventarieföremål |
| | `InventoryConsumptionHistory.tsx` | Visa konsumtionshistorik |
| | `ProductForm.tsx` | Välja produkt för inventarieföremål |
| | `actions.ts` | Server actions för inventarie |
| **Recept** | `app/recipes/page.tsx` | Lista recept |
| | `RecipePreview.tsx` | Receptförhandsvisning med inventariestatus |
| **Lägg till recept** | `app/recipes/create/page.tsx` | Meny för receptskaping (val mellan två vägar) |
| **Skriv in recept** | `app/recipes/write/page.tsx` | Startpunkt för Markdown-inmatning |
| | `app/recipes/write/WriteRecipePage.tsx` | Komponenter för receptskapande (Markdown-baserat, 3-steg) |
| **Importera från fil** | `app/recipes/import/page.tsx` | Startpunkt för fil/länk-import |
| | `app/recipes/import/ImportFilePage.tsx` | Komponenter för fil-/länk-import (PDF, URL, etc) |
| **Recipe detail** | `app/recipes/[id]/` | Enskilt recept (detaljer, redigering) |
| **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel |
| | `AdminProductList.tsx` | Lista produkter, sök, sortera |
| | `NameForm.tsx` | Redigera produktnamn |
| | `CanonicalNameForm.tsx` | Redigera canonical name |
| | `MergePreviewForm.tsx` | Förhandsgranska merge |
### API-proxy routes (Next.js)
| Route | Metod | Syfte |
|-------|-------|-------|
| `/api/parse-markdown-proxy` | POST | Proxies `POST /api/recipes/parse-markdown` (Markdown-tolkning för skriv-in-recept) |
| `/api/inventory-history-proxy` | GET | Proxies konsumtionshistorik |
| `/api/recipe-preview-proxy` | GET | Proxies receptförhandsvisning |
| `/api/admin/merge-preview-proxy` | GET | Proxies produktmerge-preview |
| `/api/products` | GET | Lista/proxies produkter |
### Frontend utbyggbarhet
- Svenska felmeddelanden via `lib/error-handler.ts` (`parseErrorResponse`)
- Centraliserad API-access via `lib/api.ts` (`fetchJson`)
- Typade inventory/recipe data i `features/inventory/types.ts`
---
## Backend (NestJS)
- **Framework:** NestJS 10.3
- **Språk:** TypeScript 5.4.5
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
- **API:** REST, validering med class-validator
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
- **Hälsokontroll:** /health endpoints
- **Bygg:** `nest build`, körs i Docker-container
### Backend-moduler och strukturen läsa
```
backend/src/
├── app.module.ts # Root module
├── main.ts # Startpunkt (port 8080)
├── common/
│ ├── filters/
│ │ └── global-exception.filter.ts # Centraliserad felhantering
│ └── utils/
│ └── normalize-name.ts # Namnormalisering
├── health/
│ ├── health.controller.ts # GET /health, /health/db
│ ├── health.service.ts # Hälsotillstånd-logik
│ └── health.module.ts
├── inventory/
│ ├── inventory.controller.ts # CRUD endpoints
│ ├── inventory.service.ts # CRUD + konsumtion
│ ├── inventory.module.ts
│ └── dto/
│ ├── create-inventory.dto.ts
│ ├── update-inventory.dto.ts
│ └── consume-inventory.dto.ts
├── prisma/
│ ├── prisma.service.ts # PrismaClient wrapper
│ └── prisma.module.ts
├── products/
│ ├── products.controller.ts # CRUD, merge, duplicates
│ ├── products.service.ts # Produktlogik
│ ├── products.module.ts
│ └── dto/
│ ├── create-product.dto.ts
│ ├── update-product.dto.ts
│ ├── merge-products.dto.ts
│ └── update-canonical-name.dto.ts
├── quick-import/ # NYT: Snabbimport-modul
│ ├── quick-import.controller.ts # POST /api/quick-import
│ ├── quick-import.service.ts # ICA-skrapning, PDF-stöd
│ └── quick-import.module.ts # Module definition
└── recipes/
├── recipes.controller.ts # Recept endpoints
├── recipes.service.ts # Recept + Markdown-parsing
├── recipes.module.ts
└── dto/
├── create-recipe.dto.ts
├── parse-markdown.dto.ts
└── create-recipe-ingredient.dto.ts
```
### Backend-funktioner
**Health API:**
- Övergripande systemstatus (uptime, service info)
- Databasspecifik hälsokontroll (responseTime, connection test)
- Returnerar statusCode 200 eller 503
**Inventarie-API:**
- CRUD för inventarieföremål (produktreferens, kvantitet, enhet, plats, märke, bäst före, mm)
- Konsumtionshistorik-tracking (registrera brukat amount och kommentar)
- Sortering: efter plats, bäst före-datum, namn (A–Ö)
- Filtrera utgående varor
- **Enhetskonvertering:** Stöd för viktenheter (g/kg), volymenheter (ml/dl), portionsenheter (tsk/msk)
- Normalisering av enheter (t.ex. "tesked" → "tsk", "gram" → "g")
- Konverteringsregler per enhet-typ
- Kan endast konvertera inom samma enhet-typ (error om blandning)
**Recept-API:**
- CRUD för recept och ingredienser
- **Parse-markdown endpoint:** Tolkar Markdown-format, matchar ingredienser mot databas
- **Matchningsalgoritm (3 nivåer):**
1. Exakt match (normalizedName eller canonicalName efter normalisering): **100 poäng**
2. Delsträng-match (ingrediens i produktnamn eller vice versa): **70 poäng**
3. Levenshtein-distans-baserad likhet: **40100 poäng** (under 40 filtreras bort)
- Top 5 förslag per ingrediens
- Sortering: Högsta poäng först
- **Inventory-preview:** Jämför recept mot inventarie
- Returnerar status för varje ingrediens: räcker | saknas | enhetskonflikt
- Automatisk enhetskonvertering vid jämförelse
- **Normalisering:** `normalize-name()` utility för consistent namn-matching
**Produkt-API:**
- CRUD för produkter (create, read, update, delete)
- **Duplicate detection:** `findDuplicates()` - Hitta produkter med samma normalizedName
- **Merge-preview:** `previewMerge()` - Förhandsgranska merge operation (visa inventory-counts, outcome)
- **Merge operation:** `merge()` - Slå ihop två produkter
- Flytta alla inventarieföremål från källa till mål
- Soft-delete källan (isActive = false, deletedAt = nu)
- Uppdatera recept-ingredienser från källa till mål
- **Canonical name management:**
- `updateCanonicalName()` - Uppdatera canonical name för ett produktnamn
- `backfillCanonical()` - Fylla på canonical names för alla produkter (admin-funktion)
- **Soft delete & restore:**
- `remove()` - Soft-delete produkt (isActive = false)
- `restore()` - Återställ borttagen produkt
---
## API-endpoints (fullständig lista)
### 🏥 Health endpoints
```
GET /health Övergripande hälsakontroll (200/503)
GET /health/db Databasspecifik hälsa + responseTime
```
### 📦 Inventarie-endpoints
```
GET /api/inventory Lista inventarieföremål
Params: ?location=... &sort=...
GET /api/inventory/expiring Utgångna/snart utgångna varor
POST /api/inventory Skapa nytt inventarieföremål
PATCH /api/inventory/:id Uppdatera inventarieföremål
POST /api/inventory/:id/consume Konsumera (registrera brukat amount)
GET /api/inventory/:id/consumption-history Konsumtionshistorik
```
### 🍽️ Recept-endpoints
```
POST /api/quick-import SNITT: Snabbimport (ICA-skrapning)
Body: { input: string (URL eller filsökväg) }
POST /api/recipes/parse-markdown Tolka Markdown-recept (matchningslogik)
GET /api/recipes Lista alla recept
POST /api/recipes Skapa nytt recept
GET /api/recipes/:id Hämta specifikt recept
PATCH /api/recipes/:id Uppdatera recept
DELETE /api/recipes/:id Ta bort recept (204 No Content)
GET /api/recipes/:id/inventory-preview Jämför recept mot inventarie
```
### 🏷️ Produkt-endpoints
```
GET /api/products Lista alla aktiva produkter
POST /api/products Skapa ny produkt
GET /api/products/:id Hämta specifik produkt
PATCH /api/products/:id Uppdatera produktens namn
DELETE /api/products/:id Soft-delete produkt
POST /api/products/:id/restore Återställ raderad produkt
GET /api/products/duplicates Lista duplicerade namn (grupperade)
GET /api/products/merge-preview Förhandsgranska merge
?sourceProductId=X &targetProductId=Y
POST /api/products/merge Slå ihop två produkter
PATCH /api/products/:id/canonical-name Uppdatera canonical name
POST /api/products/backfill-canonical Backfill canonical names (admin)
```
---
## Datamodell (Prisma ORM)
### Product
```prisma
model Product {
id Int @id @default(autoincrement())
name String # Visningsnamn
normalizedName String @unique # Normaliserat namn (lowercase, utan skiljetecken)
canonicalName String? # Canonical namn för receptmatchning
category String? # Produktkategori
isActive Boolean @default(true) # Soft-delete flag
deletedAt DateTime? # Tidpunkt för soft-delete
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inventoryItems InventoryItem[]
recipeIngredients RecipeIngredient[]
}
```
### InventoryItem
```prisma
model InventoryItem {
id Int @id @default(autoincrement())
productId Int # Foreign key till Product
quantity Decimal @db.Decimal(10, 2) # Kvantitet (decimal för precision)
unit String # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
brand String? # Varumärke
location String? # Lagerplats (Kyl, Frys, Skafferi, etc)
priority Int? # Prioritetsordning
purchaseDate DateTime? # Köpdatum
opened Boolean? # Markering för öppnad produkt
shelfNote String? # Lagringsnot
suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian")
isOnSale Boolean? # Är på rea
priceLevel Int? # Priskategori (15)
bestBeforeDate DateTime? # Bäst före-datum
proteinType String? # Proteintyp (t.ex. "beef", "chicken")
isLeftover Boolean? # Är från tidigare lagnning
comment String? # Fri kommentar
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
consumptions InventoryConsumption[]
@@index([productId])
}
```
### InventoryConsumption
```prisma
model InventoryConsumption {
id Int @id @default(autoincrement())
inventoryItemId Int # Foreign key till InventoryItem
amountUsed Decimal @db.Decimal(10, 2) # Konsumerad kvantitet
comment String? # Kommentar
createdAt DateTime @default(now())
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
}
```
### Recipe
```prisma
model Recipe {
id Int @id @default(autoincrement())
name String # Receptnamn
description String? # Receptbeskrivning
instructions String? @db.Text # Tillagningsinstruktioner (kan vara långt, stöder Markdown)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ingredients RecipeIngredient[]
}
```
### RecipeIngredient
```prisma
model RecipeIngredient {
id Int @id @default(autoincrement())
recipeId Int # Foreign key till Recipe
productId Int # Foreign key till Product
quantity Decimal @db.Decimal(10, 2) # Receptkvantitet
unit String # Enhet enligt recept
note String? # Ingrediensnot (t.ex. variation)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
recipe Recipe @relation(fields: [recipeId], references: [id])
product Product @relation(fields: [productId], references: [id])
}
```
---
## Receptimport och receptskaping — Detaljerad arkitektur
### Syfte och struktur
Recipe App erbjuder tre vägar för att lägga till recept:
1. **Snabbimport** — Klistra in ICA-länk för automatisk skrapning (ny feature)
2. **Skriv in recept** (`/recipes/write`) — Markdown-baserad inmatning där användaren skriver receptet i enkelt format
3. **Importera från fil** (`/recipes/import`) — Ladda upp PDF, länk eller andra receptkällor (under utveckling)
Alla vägar möjliggör automatisk matchning av ingredienser mot databasen.
### Strukturöversikt
#### Snabbimport-fältet
**Frontend: `/recipes/create/page.tsx`**
- Ovanför de två huvudvalen visas ett gult inmatningsfält för snabbimport
- Användaren klistrar in en ICA-receptlänk eller filsökväg
- Vid submit:
1. Frontend skickar till `/api/quick-import-proxy`
2. Proxy proxiar till backend `POST /api/quick-import`
3. Backend returnerar Markdown-text
4. Frontend sparar i `sessionStorage('recipeMarkdown')`
5. Omdirigera till `/recipes/write` med förifylld Markdown
**Backend: `QuickImportService` (ny modul)**
- Ansvarig för ICA-skrapning, PDF-tolkning, URL-validering
- **Huvudmetod:** `importFromInput(input: string)` — Detekterar input-typ och delegerar
- **ICA-specifik:**
- Validerar URL (måste vara ICA.se)
- Fetchar HTML via `fetch()`
- Parsar HTML med regex för: receptnamn, ingredienser, instruktioner
- Konverterar till Markdown-format
- **Felhantering:** Specifika felmeddelanden per scenario
- **PDF-support:** Stubben för framtida integration (throwError: "PDF-import är under utveckling")
- **Error-strategi:**
- `400 Bad Request` — Tomt input, inte URL/fil
- `400 Bad Request` — Länken är inte från ICA.se
- `503 Service Unavailable` — Network-fel vid hämtning (HTTP-fel från ICA)
- `400 Bad Request` — HTML-parsing misslyckades (receptnamn/ingredienser inte hittade)
**API-endpoint:**
```
POST /api/quick-import
Input: { input: string }
Output: { markdown: string, source: 'ica' | 'pdf' | 'other' }
```
**Proxy-route (Next.js):**
- `/api/quick-import-proxy` — Proxies till backend
- Hanterar error-konvertering (BE HTTP → FE error message)
- Returnerar Markdown eller JSON-error
### Markdown-format och parsningsregler
### Markdown-format och parsningsregler
**Format:**
```markdown
# Receptnamn
Valfri beskrivning av receptet.
## Ingredienser
- 500 g köttfärs
- 1 st lök
- 2.5 msk tomatpuré
- 1 dl grädde (vispgrädde)
- salt
## Tillvägagångssätt
Stek löken i lite smör. Tillsätt köttfärsen…
```
**Parsningsregler i detalj:**
| Element | Tolkning |
|---------|----------|
| `# Rubrik` | Receptnamn (första H1) |
| Text mellan H1 och `## Ingredienser` | Receptbeskrivning (flera rader OK, valfritt) |
| `## Ingredienser` | Ingred markerare (case-insensitive) |
| `- ANTAL ENHET NAMN` | Ingrediens: quantity=ANTAL, unit=ENHET, name=NAMN |
| `- ANTAL NAMN` | Ingrediens utan enhet: unit sätts till "st" |
| `- NAMN` | Ingrediens utan kvantitet: quantity=0, unit="" |
| `(text i parentes)` | Ingrediensnot (sparas, t.ex. "vispgrädde") |
| `## Tillvägagångssätt` / `## Instruktioner` | Instruktionssektion markerare |
| Text under instruktioner | Tillagningsinstruktioner (flera rader OK) |
**Exempel ingrediensparsning:**
```
"- 500 g köttfärs" → {quantity: 500, unit: "g", rawName: "köttfärs"}
"- 1,5 dl grädde (vispgrädde)" → {quantity: 1.5, unit: "dl", rawName: "grädde", note: "vispgrädde"}
"- 3 ägg" → {quantity: 3, unit: "st", rawName: "ägg"}
"- salt" → {quantity: 0, unit: "", rawName: "salt"}
```
### Komponenter och dataflöde
#### 1. `recipe-document-converter/` bibliotek
**Modulstruktur:**
- `src/parser.ts` — Markdown-parser med ingrediensparsning
- `src/index.ts` — Biblioteksexport
- `package.json` — npm-paket (noll externa beroenden)
- `tsconfig.json` — TypeScript-konfiguration
**Huvudexport:**
```typescript
export function parseRecipeMarkdown(markdown: string): ParsedRecipe
interface ParsedRecipe {
name: string;
description: string; # Kan vara tom
instructions: string; # Kan vara tom
ingredients: ParsedIngredient[];
}
interface ParsedIngredient {
rawName: string; # Extraherat ingrediensnamn
quantity: number; # Numerisk kvantitet (eller 0 om ingen)
unit: string; # Enhet (eller "" om ingen)
note: string | null; # Text i parentes (eller null)
}
```
**Ingrediensparsning i detalj:**
- Försöker matcha regex `(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)` för "ANTAL ENHET NAMN"
- Fallback: `(\d+(?:[.,]\d+)?)\s+(.+)` för "ANTAL NAMN" (unit → "st")
- Fallback: bara namn, quantity=0, unit=""
- Kommatecken i tal: `,``.` (t.ex. "1,5" blir 1.5)
- Parentes-extraktion: Matchar sista `(text)` i raden
**Byggning:**
- TypeScript → JavaScript via `tsc`
- Separerad npm-modul
- Kompileras i Docker-build steg 1 (converter-build)
#### 2. Backend: `POST /api/recipes/parse-markdown` endpoint
**Klassbank:**
- `recipe-document-converter` — Markdown-parser
- `@prisma/client` — Databasaccess
- Common utils: `normalize-name.ts`
**Processflöde:**
```
1. Motta: ParseMarkdownDto { markdown: string }
2. Anropa parseRecipeMarkdown() → ParsedRecipe
3. Hämta alla aktiva produkter från DB
4. För varje ingrediens:
a. Normalisera: lowercase + trim + remove accents/punctuation
b. Matchningsalgoritm (se nedan)
c. Top 5 förslag sortera efter score
5. Returnera ParsedRecipe + suggestions
```
**Matchningsalgoritm:**
```
Normalisering: lowercase + trim + åäö-handling + skilljetecken-borttagning
1. EXAKT MATCH (100 poäng)
IF (ingrediens == product.canonicalName_normalized) OR
(ingrediens == product.normalizedName_normalized)
THEN score = 100
2. DELSTRÄNG-MATCH (70 poäng)
IF (ingrediens IN product.name_normalized) OR
(product.name_normalized IN ingrediens)
THEN score = 70
3. LEVENSHTEIN-LIKHET (40100 poäng)
Calculate Levenshtein distance between ingrediens and product.name_normalized
similarity% = (1 - (distance / max_length)) * 100
IF similarity% >= 40 THEN score = similarity%
ELSE filter out
Sortering: Högsta poäng först
Top 5: Max 5 förslag per ingrediens
```
**Svarsobjekt:**
```json
{
"name": "Köttfärssås",
"description": "En klassisk…",
"instructions": "Stek löken…",
"ingredients": [
{
"rawName": "köttfärs",
"quantity": 500,
"unit": "g",
"note": null,
"suggestions": [
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
{ "productId": 34, "productName": "Blandfärs", "score": 65 },
{ "productId": 56, "productName": "Nötfärs", "score": 55 }
]
}
]
}
```
#### 3. Frontend: Receptskapsidor
**Huvudmeny: `/recipes/create/page.tsx`**
- Presenterar två val-kort (card-baserad UI)
- "Skriv in recept" → `/recipes/write`
- "Importera från fil/länk" → `/recipes/import`
**Skriv in recept: `/recipes/write/WriteRecipePage.tsx`**
- Main client component (3-steps state machine)
- Samma logik som tidigare `ImportRecipePage`
- **Steg 1:** Markdown-inmatning
- **Steg 2:** Granska ingredienser, välj produkter
- **Steg 3:** Spara recept
- Använder `/api/parse-markdown-proxy` för backend-anrop
**Importera från fil: `/recipes/import/ImportFilePage.tsx`**
- Tabs/toggle mellan två metoder:
1. **Fil-upload** — Dra-och-släpp eller välja PDF/TXT/DOCX
2. **URL-import** — Ange länk till receptsida
- Placeholder för framtida integration
- Visar tips för att använda "Skriv in recept" tills dessa funktioner är klara
#### 4. API-proxy-route (Next.js)
**`/api/parse-markdown-proxy/route.ts`**
- POST-endpoint
- Proxies anrop till backend `POST /api/recipes/parse-markdown`
- Hanterar CORS, headers, error-svarsöversättning
---
## Enhetskonvertering (backendsida)
### Stödda enhetstyper
| Typ | Enheter | Bassystem |
|-----|---------|-----------|
| **Vikt** | g, kg | gram (g) |
| **Volym** | ml, dl | milliliter (ml) |
| **Portioner** | tsk, msk | tesked (tsk), där 1 msk = 3 tsk |
| **Stycken** | st | kan inte konverteras |
### Normalisering (inom `RecipesService.normalizeUnit()`)
| Input | Output | Typ |
|-------|--------|-----|
| "tesked", "test" | "tsk" | Portion |
| "matsled", "matsked" | "msk" | Portion |
| "gram" | "g" | Vikt |
| "kilogram", "kilo", "kg" | "kg" | Vikt |
| "milliliter" | "ml" | Volym |
| "deciliter" | "dl" | Volym |
| "stycke" | "st" | Styck |
| (other) | (as-is) | (as-is) |
### Konverteringslogik (`convertUnit()` metod)
```typescript
convertUnit(quantity: number, fromUnit: string, toUnit: string, productName: string): number {
// 1. Validering (quantity > 0, units inte tomma)
// 2. Normalisera båda enheter
// 3. Om identiska efter normalisering → return quantity
// 4. Bestäm enhetstyp för båda
// 5. Om olika typer → throw Error
// 6. Konvertera via basenhet:
// quantity * factor[fromUnit] / factor[toUnit]
}
```
### Användning i `getInventoryPreview()`
Då receptjämförelse jämförs mot inventarie:
```
För varje ingrediens:
1. Hämta alla inventarieföremål för denna produkt
2. Gruppera efter enhet:
- Samma enhet som ingrediens → summera direkt
- Annan enhet → försök konvertera
3. Om konvertering misslyckas (t.ex. st ↔ g) → hoppa över denna post
4. Summera totalt available (samma + konverterad)
5. Returnera status: räcker | saknas | enhetskonflikt
```
---
## Infrastruktur & DevOps
### Docker Compose setup
**Services:**
- `recipe-frontend` — Next.js container (port 3000)
- `recipe-api` — NestJS backend (port 8080)
- `db` — MariaDB database (port 3306)
- `proxy` — (valfritt) Caddy reverse proxy
**Volumer:**
- Database data persistence
- Build layers caching
**Networks:**
- External `proxy` network (för Caddy integrering, valfritt)
- Intern `recipe-network` mellan services
### Backend-Dockerfile (3-stage build)
**Stage 1: converter-build**
```dockerfile
FROM node:22-alpine AS converter-build
# Bygg recipe-document-converter biblioteket
```
**Stage 2: builder**
```dockerfile
FROM node:22-alpine AS builder
# Installera backend-deps
# Kopiera converter från stage 1
# Generera Prisma-klient
# Bygg NestJS-appen (nest build)
```
**Stage 3: runner**
```dockerfile
FROM node:22-alpine AS runner
# Minimal production image
# Enbart dist/, node_modules/, prisma/
```
**Viktigt:** `backend/package.json` har `"recipe-document-converter": "file:../recipe-document-converter"` för lokal dev. I Docker ignoreras denna; convertern kopieras in från stage 1.
Build-kommando:
```bash
docker compose build recipe-api
docker compose up -d recipe-api
```
### Frontend-Dockerfile
Standard Next.js build → standalone output
### Miljövariabler
Konfigureras via `.env` eller `docker compose up`:
- `DATABASE_URL` — MariaDB-anslutning (backend)
- `PORT` — Backend port (default 8080)
- Ev. Caddy-konfiguration
---
## UTF-8 och lokalisering
- Database: utf8mb4
- Backend: Normaliseringsfunktion hanterar åäö
- Frontend: Svenska felmeddelanden och UI-text
- Endpoints: Svenska benämningar och kategori-namn
---
## Säkerhet & Utbyggbarhet
- **Ingen auth i grundutförande** (kan enkelt byggas på)
- **Validering:** Alla DTO:er valideras med class-validator
- **Felhantering:** GlobalExceptionFilter med svenska meddelanden
- **CORS:** Proxies hanteras via Next.js API routes
### Möjliga utbyggnader
- Authentication (JWT, OAuth)
- Multi-user support
- Shoppinglistor
- Recept-delning
- Nutrition facts
- Allergi-tracking