Files
recipe-app/TEKNISK_BESKRIVNING.md
T

31 KiB
Raw Blame History

Teknisk beskrivning av Recipe App

Se README.md för användarinformation och kom-igång-guide.
Se NEXT_STEPS.md för förslag på nästa steg i projektet.

Ö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

Container- och deployupplägg

  • compose.yml bygger lokala images för frontend och backend
  • pull_policy: never används för appens lokala images för att undvika felaktiga registry-pulls i Portainer
  • Health checks finns för databas, API och frontend
  • depends_on med hälsovillkor används för stabilare startordning i Docker och Portainer
  • Fasta containernamn — alla tjänster har container_name satt i compose.yml, vilket ger förutsebara namn oavsett projektkatalog:
Tjänst Container-namn
Frontend (Next.js) recipe-frontend
Backend (NestJS) recipe-api
Databas (MariaDB) recipe-db

Använd dessa namn vid docker exec, t.ex.:

docker exec recipe-api npx prisma migrate dev --name migration_name
docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;"

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
EditProductForm.tsx Inline redigering av name, canonicalName, category + soft-delete
MergePreviewForm.tsx Förhandsgranska merge
actions.ts Server actions: updateProduct, deleteProduct
Baslager app/baslager/page.tsx Visa och hantera baslager (server component)
AddToPantryForm.tsx Lägg till produkt i baslager (dropdown)
PantryList.tsx Visa baslager grupperat per kategori
actions.ts Server actions: addPantryItem, removePantryItem

API-proxy routes (Next.js)

Route Metod Syfte
/api/quick-import-proxy POST Proxies POST /api/quick-import för URL-, PDF- och bildimport
/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
/api/recipes GET, POST Lista recept + spara nytt recept (proxy till backend)

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/                    # 🆕 Snabbimport-modul
│   ├── quick-import.controller.ts   # POST /api/quick-import
│   ├── quick-import.service.ts      # ICA-skrapning, URL-parsing
│   ├── quick-import.module.ts       # Module definition
│   └── parsers/
│       ├── base.parser.ts           # Abstract RecipeParser class
│       ├── ica.parser.ts            # ICA.se-specifik parser (JSON-LD)
│       └── generic.parser.ts        # Fallback-parser (HTML + JSON-LD)
└── 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└── pantry/
    ├── pantry.controller.ts         # GET/POST/DELETE /api/pantry
    ├── pantry.service.ts            # Baslagerlogik
    ├── pantry.module.ts
    └── dto/
        └── create-pantry-item.dto.ts```

### Backend-funktioner

**Health API:**
- Övergripande systemstatus (uptime, service info)
- Databasspecifik hälsokontroll (responseTime, connection test)
- Returnerar statusCode 200 eller 503

**Quick-Import API:** 📌 (Även tillgänglig via [Microservice Importer](../microservice-importer/))
- **Endpoint:** `POST /api/quick-import`
- **Input:**
  - JSON-body med `input` för URL eller servermonterad filsökväg
  - `multipart/form-data` med `file` för uppladdad PDF eller bild
- **Stödda format:** PDF, PNG, JPG, JPEG, WEBP, BMP samt receptlänkar
- **Process:**
  1. Typdetektering av URL, PDF eller bild
  2. URL-import via site-specifik eller generisk parser
  3. PDF-import via `pdf-parse`
  4. Bildimport via `tesseract.js` OCR (`swe+eng`)
  5. Normalisering till Markdown-format för vidare receptgranskning
- **Parser-arkitektur:**
  - **Base Parser** (`RecipeParser`): Abstract class med gemensam parseIngredientLine()-logik
    - Hanterar bråkmängder (1 1/2 dl), parentetiska noter, unit-validering
    - Kända enheter: g, kg, hg, mg, ml, dl, l, tl, st, tsk, msk, krm, port, efter smak, förp, klyfta, m.fl.
  - **ICA Parser** (`IcaRecipeParser`): Prioriterar JSON-LD structured data, fallback HTML
  - **Generic Parser** (`GenericRecipeParser`): Försöker alla webbplatser (JSON-LD → HTML)
- **Output:** Markdown-format recepttext med `source: 'ica' | 'pdf' | 'image' | 'other'`

**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 /api/health Övergripande hälsakontroll (200/503) GET /api/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 Snabbimport från URL, PDF eller bild Body: { input: string } eller multipart-form med file 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, canonicalName eller kategori 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)


### 🛀 Baslager-endpoints

GET /api/pantry Lista alla baslagerartiklar (inkl. produktinfo) POST /api/pantry Lägg till produkt i baslagret DELETE /api/pantry/:id Ta bort produkt från baslagret


---

## 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

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

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

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

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])
}

PantryItem

model PantryItem {
  id        Int      @id @default(autoincrement())
  productId Int      @unique              # En produkt kan bara finnas en gång i baslagret
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)
}

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, bild eller länk och få en första Markdown-version automatiskt

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 URL-import, PDF-tolkning, bild-OCR och Markdown-normalisering
  • Huvudmetoder:
    • importFromInput(input: string) — Detekterar URL eller serverfilsökväg
    • importFromUpload(file) — Hanterar uppladdad PDF eller bildfil
  • URL-specifik logik:
    • Validerar URL
    • Fetchar HTML via fetch() med User-Agent
    • Väljer site-specifik parser eller generisk fallback
    • Konverterar resultatet till Markdown-format
  • PDF-logik:
    • Extraherar text med pdf-parse
    • Stoppar om ingen läsbar text hittas
  • Bildlogik:
    • OCR via tesseract.js
    • Svensk och engelsk språkmodell (swe+eng)
  • Error-strategi:
    • 400 Bad Request — Tomt input eller saknad fil
    • 400 Bad Request — Ostödd filtyp eller ingen läsbar text
    • 503 Service Unavailable — Misslyckad PDF- eller OCR-behandling
    • 400 Bad Request — HTML-parsing eller hämtning misslyckades

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:

# 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:

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:

{
  "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)

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

FROM node:22-alpine AS converter-build
# Bygg recipe-document-converter biblioteket

Stage 2: builder

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

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:

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

Framtida arkitektur: Microservice Importer

Recipe App har ett companion-projekt för receptimport: microservice-importer

Nuläge

Quick-import-funktionen är för närvarande integrerad i Recipe App med full funktionalitet.

Framtida möjlighet

I framtiden kan snabbimport-logiken extraheras till en standalone microservice för:

  • Oberoende scaling
  • Enklare API-integration med andra system
  • Lägre komplexitet (ingen databaskonfiguration)

Se microservice-importer README för komplett dokumentation och deployment-instruktioner när separation blir aktuell.