36 KiB
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.ymlbygger lokala images för frontend och backendpull_policy: neveranvä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_onmed hälsovillkor används för stabilare startordning i Docker och Portainer- Fasta containernamn — alla tjänster har
container_namesatt icompose.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)
- Autentisering: Auth.js v5 (next-auth beta), JWT-session,
auth()i server components - Bygg: Standalone output, körs i Docker-container
- API-anrop:
fetchJson(server-side med auth-headers) + Next.js API route-proxies (client-side) - Felhantering: Global parseErrorResponse utility, svenska felmeddelanden
Viktigt:
Navigation.tsxär en async server component som anroparauth(). Den får aldrig importeras av client components — rendera den alltid ipage.tsx(server component).
Frontend-sidor och komponenter
| Sida | Fil | Funktionalitet |
|---|---|---|
| Hem | app/page.tsx |
Startsida |
| Navigering | app/Navigation.tsx |
Huvudmeny, inloggad användare, länk till profil |
| Inloggning | app/login/page.tsx |
Inloggningssida med Auth.js Credentials |
| Profil | app/profil/page.tsx |
Redigera firstName, lastName, email |
app/profil/ProfileClient.tsx |
Klientkomponent för profilformulär | |
| 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 |
Server component med Navigation |
app/recipes/create/CreateRecipeClient.tsx |
Klientkomponent: snabbimport + metodval | |
| Skriv in recept | app/recipes/write/page.tsx |
Server component med Navigation |
app/recipes/write/WriteRecipePage.tsx |
Markdown-baserat receptskapande (3-steg) | |
| Importera från fil | app/recipes/import/page.tsx |
Startpunkt för fil/länk-import |
app/recipes/import/ImportFilePage.tsx |
Fil-/länk-import (PDF, URL, etc) | |
| Import (flikar) | app/import/page.tsx |
Server component med Navigation + flikvy |
app/import/ImportTabsClient.tsx |
Klientkomponent: kvitto/recept-flikar | |
| 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: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar | |
ResetProductsButton.tsx |
Knapp för att rensa all produktdata | |
MergePreviewForm.tsx |
Förhandsgranska merge | |
actions.ts |
Server actions: updateProduct, deleteProduct, resetAllProducts | |
| 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)
Alla proxy-routes läser auth-token via auth() (Auth.js v5) och vidarebefordrar Authorization: Bearer <token> till backend.
| Route | Metod | Syfte |
|---|---|---|
/api/auth/[...nextauth] |
GET, POST | Auth.js handlers (login, logout, session) |
/api/products |
GET | Produktlista (auth-wrappat med auth(req)) |
/api/categories |
GET | Kategorihierarki (publik, proxies /api/categories/tree) |
/api/profile |
GET, PATCH | Hämta/uppdatera användarprofil |
/api/recipes |
GET, POST | Lista recept + spara nytt |
/api/quick-import-proxy |
POST | URL-, PDF- och bildimport |
/api/parse-markdown-proxy |
POST | Markdown-tolkning för skriv-in-recept |
/api/inventory-history-proxy |
GET | Konsumtionshistorik |
/api/recipe-preview-proxy |
GET | Receptförhandsvisning |
/api/admin/merge-preview-proxy |
GET | Produktmerge-preview |
/api/receipt-import-proxy |
POST | Kvittoimport via Mistral AI |
/api/user-products |
GET, POST, DELETE | Användarspecifika produkter |
Autentisering (Auth.js v5)
auth.ts— NextAuth-konfiguration med Credentials providermiddleware.ts— Skyddar alla routes utom/login,/registeroch/api/authlib/auth-headers.ts—getAuthHeaders()hämtar Bearer-token från session (server-side)lib/api.ts—fetchJson()lägger automatiskt till auth-headers server-side, redirectar till/loginvid 401
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
- Autentisering: JWT (7 dagars token), JwtAuthGuard skyddar alla routes,
@Public()dekorator för öppna endpoints - Felhantering: GlobalExceptionFilter (svenska felmeddelanden)
- Hälsokontroll: /health endpoints
- Bygg:
nest build, körs i Docker-container
Backend-moduler och strukturen
backend/src/
├── app.module.ts # Root module
├── main.ts # Startpunkt (port 8080, global prefix "api")
├── auth/
│ ├── auth.controller.ts # POST /api/auth/login
│ ├── auth.service.ts # validateUser, login (JWT-signering)
│ ├── auth.module.ts
│ ├── jwt.strategy.ts # Passport JWT-strategi
│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public)
│ └── decorators/
│ └── public.decorator.ts # @Public() – markerar öppen endpoint
├── users/
│ ├── users.controller.ts # GET/PATCH /api/users/me
│ ├── users.service.ts # findByUsername, create, updateProfile
│ └── users.module.ts
├── categories/
│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public)
│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk)
│ └── categories.module.ts
├── common/
│ ├── filters/
│ │ └── global-exception.filter.ts # Centraliserad felhantering
│ └── utils/
│ └── normalize-name.ts # Namnormalisering
├── health/
│ ├── health.controller.ts # GET /health, /health/db (@Public)
│ ├── health.service.ts
│ └── 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, reset-all
│ ├── products.service.ts # Produktlogik inkl. resetAll()
│ ├── 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: **40–100 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) POST /api/products/reset-all Rensa all produktdata (admin)
### Kategori-endpoints
GET /api/categories Flat lista av alla kategorier (@Public) GET /api/categories/tree Hierarkiskt träd (@Public)
### Användar-endpoints
POST /api/auth/login Logga in, returnerar JWT (@Public) GET /api/users/me Hämta inloggad användares profil PATCH /api/users/me Uppdatera firstName, lastName, email
### 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)
### User
```prisma
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
firstName String?
lastName String?
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Category
model Category {
id Int @id @default(autoincrement())
name String
parentId Int?
parent Category? @relation("CategoryTree", ...) # Förälder (null = toppnivå)
children Category[] @relation("CategoryTree") # Underkategorier
products Product[]
@@unique([name, parentId])
}
Hierarkin har 3 nivåer: Huvudkategori → Underkategori → Typ
Exempelträd: Mejeri, ost & ägg → Mjölk → Laktosfri mjölk
Kategori-seed
Kategorier seedas på två sätt:
-
Migrationen
20260417310000_add_category_tree/migration.sql— seedar grundläggande kategorier vidprisma migrate deploy(körs bara en gång). -
db/seeds/categories_supplement.sql— idempotent supplementfil med ytterligare kategorier (använderINSERT IGNORE). Körs automatiskt avdeploy.shvid varje deploy:docker exec -i recipe-db mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" \ < db/seeds/categories_supplement.sqlFilen är säker att köra flera gånger — befintliga kategorier hoppas över. Lägg till nya kategorier i slutet av filen och kör
deploy.shför att applicera dem.
Product
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? # Fritext-kategori (äldre fält, ersätts av categoryRef)
subcategory String? # Fritext-underkategori (äldre fält)
brand String? # Varumärke
categoryId Int? # FK till Category (ny hierarki)
categoryRef Category? # Relation till Category
isActive Boolean @default(true)
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inventoryItems InventoryItem[]
recipeIngredients RecipeIngredient[]
tags ProductTag[]
nutrition Nutrition?
}
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 (1–5)
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:
- Snabbimport — Klistra in ICA-länk för automatisk skrapning (ny feature)
- Skriv in recept (
/recipes/write) — Markdown-baserad inmatning där användaren skriver receptet i enkelt format - 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:
- Frontend skickar till
/api/quick-import-proxy - Proxy proxiar till backend
POST /api/quick-import - Backend returnerar Markdown-text
- Frontend sparar i
sessionStorage('recipeMarkdown') - Omdirigera till
/recipes/writemed förifylld Markdown
- Frontend skickar till
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ägimportFromUpload(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
- Extraherar text med
- Bildlogik:
- OCR via
tesseract.js - Svensk och engelsk språkmodell (
swe+eng)
- OCR via
- Error-strategi:
400 Bad Request— Tomt input eller saknad fil400 Bad Request— Ostödd filtyp eller ingen läsbar text503 Service Unavailable— Misslyckad PDF- eller OCR-behandling400 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 ingrediensparsningsrc/index.ts— Biblioteksexportpackage.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 (40–100 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-proxyför backend-anrop
Importera från fil: /recipes/import/ImportFilePage.tsx
- Tabs/toggle mellan två metoder:
- Fil-upload — Dra-och-släpp eller välja PDF/TXT/DOCX
- 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
proxynetwork (för Caddy integrering, valfritt) - Intern
recipe-networkmellan 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.