From b31af6181cc3fa64847ba49f08e4d317d974e1c8 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Wed, 22 Apr 2026 19:37:12 +0200 Subject: [PATCH] Refactor next_steps_flutter and teknisk_beskrivning_flutter for user-scope implementation - Updated next_steps_flutter.md to reflect completed tasks for user-scoped PantryItem and MealPlanEntry, including API contract publication and migration application. - Enhanced the prioritization plan with clear completion dates and added localization tasks. - Expanded teknisk_beskrivning_flutter.md with details on inventory filtering, sorting, and user-scoped backend changes, including migration notes and localization setup. - Improved error handling documentation and localization usage guidelines. --- TEKNISK_BESKRIVNING.md | 1267 +++++++++++++++++--------------- next_steps_flutter.md | 64 +- teknisk_beskrivning_flutter.md | 45 +- 3 files changed, 760 insertions(+), 616 deletions(-) diff --git a/TEKNISK_BESKRIVNING.md b/TEKNISK_BESKRIVNING.md index 978f43ed..76edf69e 100644 --- a/TEKNISK_BESKRIVNING.md +++ b/TEKNISK_BESKRIVNING.md @@ -1,12 +1,12 @@ -# Teknisk beskrivning av Recipe App +# Teknisk beskrivning av Recipe App -> Se [README.md](README.md) för användarinformation och kom-igång-guide. -> Se [NEXT_STEPS.md](NEXT_STEPS.md) för förslag på nästa steg i projektet. -> Se [AI-FUNKTIONER.md](AI-FUNKTIONER.md) för planerade AI-funktioner och modellval. +> Se [README.md](README.md) för användarinformation och kom-igÃ¥ng-guide. +> Se [NEXT_STEPS.md](NEXT_STEPS.md) för förslag pÃ¥ nästa steg i projektet. +> Se [AI-FUNKTIONER.md](AI-FUNKTIONER.md) för planerade AI-funktioner och modellval. -## Översikt +## Ö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. +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. --- @@ -29,56 +29,56 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och --- -## Utvecklingsmiljö och deployment +## Utvecklingsmiljö och deployment ### Infrastruktur -**Utvecklingsmiljön är en remote server** med följande struktur: +**Utvecklingsmiljön är en remote server** med följande struktur: - **Server:** Dedikerad maskin med Docker installerat -- **SSH-baserad utveckling:** All utveckling, bygge och körning av applikationen sker via SSH-kommandon på servern -- **Lokal förberedelse:** Mindre arbetsuppgifter kan förberedas lokalt och sedan pushas till servern för slutförande +- **SSH-baserad utveckling:** All utveckling, bygge och körning av applikationen sker via SSH-kommandon pÃ¥ servern +- **Lokal förberedelse:** Mindre arbetsuppgifter kan förberedas lokalt och sedan pushas till servern för slutförande -### Arbetsflöde +### Arbetsflöde ``` Lokal maskin - │ - ├─── Git-ändringar (commit) ──┐ - │ │ - └─── Push till Gitea-server ──→ Privat Gitea-instans + │ + ├─── Git-ändringar (commit) ──┐ + │ │ + └─── Push till Gitea-server ──→ Privat Gitea-instans (i Docker-container) - │ - ↓ + │ + ↓ Remote server - │ - ├─── git pull ────────────────┤ - │ │ - └─── docker compose build ─────┤ - │ │ - └─── docker compose up ───────┘ + │ + ├─── git pull ────────────────┤ + │ │ + └─── docker compose build ─────┤ + │ │ + └─── docker compose up ───────┘ ``` ### Git-server (Gitea) -- **Gitea** körs i en Docker-container på servern -- **Privat git-server** för denna applikation -- Ändringar pushas lokalt till denna server: +- **Gitea** körs i en Docker-container pÃ¥ servern +- **Privat git-server** för denna applikation +- Ändringar pushas lokalt till denna server: ```bash git push origin main ``` -### Bygge och körning på servern +### Bygge och körning pÃ¥ servern Efter push till Gitea: -1. **SSH in på servern** +1. **SSH in pÃ¥ servern** ```bash ssh user@server cd /opt/containers/recipe-app ``` -2. **Hämta senaste ändringar** +2. **Hämta senaste ändringar** ```bash git pull origin main ``` @@ -89,7 +89,7 @@ Efter push till Gitea: docker compose up -d ``` -Alla tjänster (frontend, backend, databas) startas via Docker Compose enligt `compose.yml`. +Alla tjänster (frontend, backend, databas) startas via Docker Compose enligt `compose.yml`. ### Rekommenderat kommandomonster @@ -120,24 +120,24 @@ Tumregel: --- -## Container- och deployupplägg +## 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: +- `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 | +| 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.: +Använd dessa namn vid `docker exec`, t.ex.: ```bash 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;" +docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;" ``` ### Orphan-containers vid blandade compose-filer @@ -165,7 +165,7 @@ Obs: detta kan stoppa `recipe-flutter`, som da maste startas igen med override-f ## Caddy-konfiguration (reverse proxy) -Caddy sitter framför applikationen och distribuerar trafik. Ordningen på `handle`-blocken är kritisk — Caddy väljer det första matchande blocket. +Caddy sitter framför applikationen och distribuerar trafik. Ordningen pÃ¥ `handle`-blocken är kritisk — Caddy väljer det första matchande blocket. ```caddy (auth) { @@ -204,7 +204,7 @@ recept.gynther.se { import common # === IMPORT SERVICE (Document Converter) === - # Dessa endpoints måste komma FÖRST innan backend reglerna! + # Dessa endpoints mÃ¥ste komma FÖRST innan backend reglerna! handle /api/recipes/import* { reverse_proxy recipe-import-service:3000 } @@ -224,7 +224,7 @@ recept.gynther.se { } # === RECIPE BACKEND API ENDPOINTS === - # Backend körs på port 8080 (från docker-compose) + # Backend körs pÃ¥ port 8080 (frÃ¥n docker-compose) handle /api/products* { reverse_proxy recipe-api:8080 } @@ -243,7 +243,7 @@ recept.gynther.se { } # === CATCH ALL === - # Övriga /api/* går till frontend + # Övriga /api/* gÃ¥r till frontend handle /api/* { reverse_proxy recipe-frontend:3000 } @@ -254,37 +254,37 @@ recept.gynther.se { ``` **Kommentarer:** -- Blocket `(common)` innehåller alla grundläggande säkerhetshuvuden och komprimering. -- Blocket `(auth)` används för basic auth på utvalda endpoints. -- Importera `(common)` i varje site-block för att återanvända headers och inställningar. -- Proxyreglerna styr trafik till rätt backend/frontend-tjänst beroende på path. -- Kommentarer i filen förklarar ordningen på reglerna och varför vissa hanteras först. +- Blocket `(common)` innehÃ¥ller alla grundläggande säkerhetshuvuden och komprimering. +- Blocket `(auth)` används för basic auth pÃ¥ utvalda endpoints. +- Importera `(common)` i varje site-block för att Ã¥teranvända headers och inställningar. +- Proxyreglerna styr trafik till rätt backend/frontend-tjänst beroende pÃ¥ path. +- Kommentarer i filen förklarar ordningen pÃ¥ reglerna och varför vissa hanteras först. --- -## Säkerhetshuvuden +## Säkerhetshuvuden -Säkerhetshuvuden är implementerade i tre lager för djupförsvar: +Säkerhetshuvuden är implementerade i tre lager för djupförsvar: -### Lager 1: Caddy (globalt för alla tjänster) +### Lager 1: Caddy (globalt för alla tjänster) -Sätts i `(common)`-blocket och gäller alla domäner som importerar det. +Sätts i `(common)`-blocket och gäller alla domäner som importerar det. -| Header | Värde | Syfte | +| Header | Värde | Syfte | |--------|-------|-------| | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | Tvingar HTTPS, skyddar mot protocol downgrade | -| `X-Content-Type-Options` | `nosniff` | Förhindrar MIME-sniffing | +| `X-Content-Type-Options` | `nosniff` | Förhindrar MIME-sniffing | | `X-Frame-Options` | `DENY` | Skyddar mot clickjacking | -| `X-XSS-Protection` | `1; mode=block` | Legacy XSS-skydd (äldre webbläsare) | -| `Referrer-Policy` | `strict-origin-when-cross-origin` | Begränsar referrer-läckage | -| `Permissions-Policy` | `geolocation=(), microphone=(), camera=(), payment=()` | Inaktiverar känsliga webbläsar-API:er | +| `X-XSS-Protection` | `1; mode=block` | Legacy XSS-skydd (äldre webbläsare) | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Begränsar referrer-läckage | +| `Permissions-Policy` | `geolocation=(), microphone=(), camera=(), payment=()` | Inaktiverar känsliga webbläsar-API:er | | `Cross-Origin-Opener-Policy` | `same-origin` | Isolerar browsing context | -| `Cross-Origin-Resource-Policy` | `same-origin` | Förhindrar cross-origin läsning av resurser | -| `Cross-Origin-Embedder-Policy` | `require-corp` | Kräver explicit cross-origin permission | +| `Cross-Origin-Resource-Policy` | `same-origin` | Förhindrar cross-origin läsning av resurser | +| `Cross-Origin-Embedder-Policy` | `require-corp` | Kräver explicit cross-origin permission | ### Lager 2: NestJS Helmet (backup) -Helmet konfigurerat i `backend/src/main.ts` som säkerhetsbackup ifall Caddy kringgås eller misslyckas. CSP är inaktiverat i Helmet (`contentSecurityPolicy: false`) eftersom det hanteras av Next.js. +Helmet konfigurerat i `backend/src/main.ts` som säkerhetsbackup ifall Caddy kringgÃ¥s eller misslyckas. CSP är inaktiverat i Helmet (`contentSecurityPolicy: false`) eftersom det hanteras av Next.js. ``` Aktiveras vid: docker compose up --build backend @@ -292,57 +292,57 @@ Aktiveras vid: docker compose up --build backend ### Lager 3: Next.js Content Security Policy -CSP sätts i `frontend/next.config.js` via `headers()`-funktionen och gäller alla routes (`/:path*`). +CSP sätts i `frontend/next.config.js` via `headers()`-funktionen och gäller alla routes (`/:path*`). -| Direktiv | Tillåtna källor | Motivering | +| Direktiv | TillÃ¥tna källor | Motivering | |----------|----------------|------------| | `default-src` | `'self'` | Restriktiv default | -| `script-src` | `'self' 'unsafe-eval' 'unsafe-inline'` | Krävs av Next.js runtime | +| `script-src` | `'self' 'unsafe-eval' 'unsafe-inline'` | Krävs av Next.js runtime | | `style-src` | `'self' 'unsafe-inline' fonts.googleapis.com` | Inline-stilar + Google Fonts | -| `img-src` | `'self' data: https:` | Tillåter externa bilder via HTTPS | +| `img-src` | `'self' data: https:` | TillÃ¥ter externa bilder via HTTPS | | `font-src` | `'self' fonts.gstatic.com` | Google Fonts-filer | | `connect-src` | `'self' api.mistral.ai` | API-anrop inkl. Mistral AI | -| `frame-src` | `'none'` | Inga inbäddade frames tillåtna | +| `frame-src` | `'none'` | Inga inbäddade frames tillÃ¥tna | | `object-src` | `'none'` | Inga plugins (Flash, etc.) | | `base-uri` | `'self'` | Skyddar mot base-tag-injektion | -| `form-action` | `'self'` | Formulär får bara posta till samma origin | +| `form-action` | `'self'` | Formulär fÃ¥r bara posta till samma origin | -> **Notering:** `'unsafe-eval'` och `'unsafe-inline'` i `script-src` är nödvändiga för Next.js 16 med App Router. Undvik att ta bort dessa utan noggrann testning. +> **Notering:** `'unsafe-eval'` och `'unsafe-inline'` i `script-src` är nödvändiga för Next.js 16 med App Router. Undvik att ta bort dessa utan noggrann testning. -### Felsökning av CSP-brott +### Felsökning av CSP-brott Om en funktion slutar fungera efter CSP-aktivering: -1. Öppna webbläsarens devtools → Console för att se CSP-felmeddelanden -2. Kontrollera vilken domän/resurs som blockeras -3. Lägg till domänen i rätt direktiv i `frontend/next.config.js` -4. Vanliga undantag: WebSockets kräver `wss:` i `connect-src`, Service Workers kräver `worker-src 'self'` +1. Öppna webbläsarens devtools → Console för att se CSP-felmeddelanden +2. Kontrollera vilken domän/resurs som blockeras +3. Lägg till domänen i rätt direktiv i `frontend/next.config.js` +4. Vanliga undantag: WebSockets kräver `wss:` i `connect-src`, Service Workers kräver `worker-src 'self'` --- -## Arkitekturprincip: API routes framför Server Actions +## Arkitekturprincip: API routes framför Server Actions -> **Regel: Använd Next.js API routes (`/app/api/...`) för all mutation från klientkomponenter. Använd INTE Server Actions för detta.** +> **Regel: Använd Next.js API routes (`/app/api/...`) för all mutation frÃ¥n klientkomponenter. Använd INTE Server Actions för detta.** ### Bakgrund -Next.js Server Actions returnerar alltid ett **RSC-payload** (React Server Component flight-format) som svar — även om funktionen bara returnerar ett vanligt JSON-objekt. När en klientkomponent anropar en Server Action via `startTransition` försöker React tolka svaret som ett siduppdateringspaket. Detta orsakar kraschen **"can't reload page"** / `TypeError: r is not iterable` i React 19 om sidans RSC-träd inte kan återskapas korrekt (t.ex. p.g.a. Caddy-routing, auth-state eller timing). +Next.js Server Actions returnerar alltid ett **RSC-payload** (React Server Component flight-format) som svar — även om funktionen bara returnerar ett vanligt JSON-objekt. När en klientkomponent anropar en Server Action via `startTransition` försöker React tolka svaret som ett siduppdateringspaket. Detta orsakar kraschen **"can't reload page"** / `TypeError: r is not iterable` i React 19 om sidans RSC-träd inte kan Ã¥terskapas korrekt (t.ex. p.g.a. Caddy-routing, auth-state eller timing). -### Rätt mönster: Next.js API route +### Rätt mönster: Next.js API route ``` -Klientkomponent → fetch('/api/admin/...') → Next.js API route → Backend API +Klientkomponent → fetch('/api/admin/...') → Next.js API route → Backend API ``` -- API routen körs server-side och har tillgång till sessionen via `auth()` → kan lägga till auth-headers -- Returnerar ren JSON — inga RSC-payload-problem -- Caddy-safe: använd `/api/admin/` som prefix (faller igenom till `recipe-frontend:3000`) +- API routen körs server-side och har tillgÃ¥ng till sessionen via `auth()` → kan lägga till auth-headers +- Returnerar ren JSON — inga RSC-payload-problem +- Caddy-safe: använd `/api/admin/` som prefix (faller igenom till `recipe-frontend:3000`) - Klientkomponenten hanterar UI-state lokalt efter svar (uppdatera/ta bort ur lokal state) **Exempel** (se [app/api/admin/product/[id]/route.ts](frontend/app/api/admin/product/%5Bid%5D/route.ts)): ```ts // API route (server-side, har session) export async function PATCH(req, { params }) { - const authHeaders = await getAuthHeaders(); // använder auth() + const authHeaders = await getAuthHeaders(); // använder auth() const res = await fetch(`${API_BASE}/api/products/${id}`, { method: 'PATCH', headers: authHeaders, ... }); return Response.json(await res.json()); } @@ -353,117 +353,117 @@ const updated = await res.json(); setProducts(prev => prev.map(p => p.id === updated.id ? updated : p)); // lokal state-uppdatering ``` -### När är Server Actions OK? +### När är Server Actions OK? -Server Actions kan fortfarande användas för operationer som **inte anropas från klientkomponenter med `startTransition`**, t.ex.: +Server Actions kan fortfarande användas för operationer som **inte anropas frÃ¥n klientkomponenter med `startTransition`**, t.ex.: - Form submissions i rena Server Components (inget `useTransition`) -- Admin-operationer som ändå triggar en helsidsladdning efteråt +- Admin-operationer som ändÃ¥ triggar en helsidsladdning efterÃ¥t -### Befintliga undantag att känna till +### Befintliga undantag att känna till -Dessa Server Actions finns kvar men bör migreras om de orsakar problem: -- `bulkSetCategory` — anropas från `AdminProductList` (klientkomponent) -- `suggestProductCategory` / `suggestBulkCategories` — AI-kategorisering, anropas från klient +Dessa Server Actions finns kvar men bör migreras om de orsakar problem: +- `bulkSetCategory` — anropas frÃ¥n `AdminProductList` (klientkomponent) +- `suggestProductCategory` / `suggestBulkCategories` — AI-kategorisering, anropas frÃ¥n klient - **Framework:** Next.js 16.2 (App Router, server + client components) -- **Språk:** TypeScript 5.4.5 +- **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 +- **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 anropar `auth()`. Den får aldrig importeras av client components — rendera den alltid i `page.tsx` (server component). +> **Viktigt:** `Navigation.tsx` är en async server component som anropar `auth()`. Den fÃ¥r aldrig importeras av client components — rendera den alltid i `page.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; länkarna "⚙️ Admin", "⏳ Förslag" och "👥 Användare" visas enbart om sessionens roll är `admin` | +| **Navigering** | `app/Navigation.tsx` | Huvudmeny, inloggad användare, länk till profil; länkarna "⚙️ Admin", "⏳ Förslag" och "👥 Användare" visas enbart om sessionens roll är `admin` | | **Inloggning** | `app/login/page.tsx` | Inloggningssida med Auth.js Credentials | -| **Profil** | `app/profil/page.tsx` | Flikbaserad profil-/adminsida (server component): läser `?tab=`-param, kontrollerar admin-roll via `auth()`, laddar rätt flik dynamiskt | +| **Profil** | `app/profil/page.tsx` | Flikbaserad profil-/adminsida (server component): läser `?tab=`-param, kontrollerar admin-roll via `auth()`, laddar rätt flik dynamiskt | | | `app/profil/ProfileTabs.tsx` | Klientkomponent: fliknavigering med Link-basad URL-routing (`?tab=profil\|anvandare\|databas`) | -| | `app/profil/tabs/MinProfilTab.tsx` | Profilformulär (förnamn, efternamn, e-post) | -| | `app/profil/tabs/AnvandareTab.tsx` | Server component: hämtar användarlista, renderar AnvandareClient | -| | `app/profil/tabs/AnvandareClient.tsx` | Klientkomponent: skapa/ta bort användare, rollbyte, plan-byte (Free/Paid ✨ → isPremium), e-postbyte, lösenordsåterställning med kopierings-modal | +| | `app/profil/tabs/MinProfilTab.tsx` | Profilformulär (förnamn, efternamn, e-post) | +| | `app/profil/tabs/AnvandareTab.tsx` | Server component: hämtar användarlista, renderar AnvandareClient | +| | `app/profil/tabs/AnvandareClient.tsx` | Klientkomponent: skapa/ta bort användare, rollbyte, plan-byte (Free/Paid ✨ → isPremium), e-postbyte, lösenordsÃ¥terställning med kopierings-modal | | | `app/profil/tabs/DatabsTab.tsx` | Server component: produktdatabas (importerar admin/products-komponenter) | | **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 | +| | `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 | +| | `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 | +| | `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): Markdown-inmatning → ingrediensgranskning (produktval + portionsantal) → spara | -| **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) | +| | `app/recipes/write/WriteRecipePage.tsx` | Markdown-baserat receptskapande (3-steg): Markdown-inmatning → ingrediensgranskning (produktval + portionsantal) → spara | +| **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) | | **Matplan** | `app/matplan/page.tsx` | Matplanering (server component) | -| | `app/matplan/MealPlanClient.tsx` | Veckovy, receptval per dag, portionsjustering, inköpslista, inventariejämförelse | +| | `app/matplan/MealPlanClient.tsx` | Veckovy, receptval per dag, portionsjustering, inköpslista, inventariejämförelse | | **Kvittoimport** | `app/import/page.tsx` | Server component med Navigation + flikvy | | | `app/import/ImportTabsClient.tsx` | Klientkomponent: kvitto/recept-flikar | -| **Admin: Användare** | `app/admin/users/page.tsx` | Redirect till `/profil?tab=anvandare` | +| **Admin: Användare** | `app/admin/users/page.tsx` | Redirect till `/profil?tab=anvandare` | | **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel | -| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering; AI-bulk-knapp ("✨ AI-kategorisera") med bekräftelsemodal | -| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar; "✨ Fråga AI"-knapp med suggestion-chip (grön = hög, gul = fallback) | -| | `ResetProductsButton.tsx` | Knapp för att rensa all produktdata | -| | `MergePreviewForm.tsx` | Förhandsgranska merge | +| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering; AI-bulk-knapp ("✨ AI-kategorisera") med bekräftelsemodal | +| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar; "✨ FrÃ¥ga AI"-knapp med suggestion-chip (grön = hög, gul = fallback) | +| | `ResetProductsButton.tsx` | Knapp för att rensa all produktdata | +| | `MergePreviewForm.tsx` | Förhandsgranska merge | | | `actions.ts` | Server actions: updateProduct, deleteProduct, resetAllProducts, bulkSetCategory, suggestProductCategory, suggestBulkCategories, setProductStatus | -| **Admin: Väntande produkter** | `app/admin/products/pending/page.tsx` | Server component, auth-skyddad, hämtar pending-produkter | -| | `PendingProductsClient.tsx` | Tabell: Produkt / Kategori (AI) / Föreslagen av / Datum / Åtgärd; "✓ Godkänn" / "✕ Avvisa"-knappar | +| **Admin: Väntande produkter** | `app/admin/products/pending/page.tsx` | Server component, auth-skyddad, hämtar pending-produkter | +| | `PendingProductsClient.tsx` | Tabell: Produkt / Kategori (AI) / Föreslagen av / Datum / Ã…tgärd; "✓ Godkänn" / "✕ Avvisa"-knappar | | **Baslager** | `app/baslager/page.tsx` | Visa och hantera baslager (server component) | -| | `AddToPantryForm.tsx` | Lägg till produkt i baslager (dropdown) | +| | `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 använder `withAuth(handler)`-wrappern (från `lib/with-auth.ts`) som läser auth-token via `request.auth` och vidarebefordrar `Authorization: Bearer ` till backend. `withAuth` löser kompatibilitetsproblem med `auth()` standalone i Next.js 16 + NextAuth beta.28. +Alla proxy-routes använder `withAuth(handler)`-wrappern (frÃ¥n `lib/with-auth.ts`) som läser auth-token via `request.auth` och vidarebefordrar `Authorization: Bearer ` till backend. `withAuth` löser kompatibilitetsproblem med `auth()` standalone i Next.js 16 + NextAuth beta.28. | Route | Metod | Syfte | |-------|-------|-------| -| `/api/admin-users` | GET, POST | Hämtar alla användare / skapar ny användare (kräver admin-roll i session) | -| `/api/admin-users/[id]` | PATCH, DELETE, PUT | Ändrar roll / tar bort användare / byter e-post; om body innehåller `isPremium` → anropar `PATCH /api/users/:id/premium` (kräver admin-roll i session) | -| `/api/admin-users/[id]/reset-password` | POST | Återställer lösenord och returnerar tillfälligt lösenord + meddelandetext (kräver admin-roll) | +| `/api/admin-users` | GET, POST | Hämtar alla användare / skapar ny användare (kräver admin-roll i session) | +| `/api/admin-users/[id]` | PATCH, DELETE, PUT | Ändrar roll / tar bort användare / byter e-post; om body innehÃ¥ller `isPremium` → anropar `PATCH /api/users/:id/premium` (kräver admin-roll i session) | +| `/api/admin-users/[id]/reset-password` | POST | Ã…terställer lösenord och returnerar tillfälligt lösenord + meddelandetext (kräver admin-roll) | | `/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/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/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/recipe-preview-proxy` | GET | Receptförhandsvisning | | `/api/admin/merge-preview-proxy` | GET | Produktmerge-preview | | `/api/receipt-import-proxy` | POST | Kvittoimport via Mistral AI | | `/api/meal-plan-proxy` | GET, POST, DELETE | Matplanering (veckovy, upsert, ta bort) | -| `/api/meal-plan-shopping-proxy` | GET | Inköpslista för datumintervall | -| `/api/meal-plan-compare-proxy` | GET | Inventariejämförelse för datumintervall | -| `/api/user-products` | GET, POST, DELETE | Användarspecifika produkter | +| `/api/meal-plan-shopping-proxy` | GET | Inköpslista för datumintervall | +| `/api/meal-plan-compare-proxy` | GET | Inventariejämförelse för datumintervall | +| `/api/user-products` | GET, POST, DELETE | Användarspecifika produkter | ### Autentisering (Auth.js v5) -- `auth.ts` — NextAuth-konfiguration med Credentials provider; sparar `accessToken`, `userId`, `username` och `role` i JWT-token och session -- `proxy.ts` — Next.js 16 kräver att auth-middleware heter `proxy.ts` (inte `middleware.ts`). Skyddar alla routes utom `/login`, `/register` och `/api/*`: ej inloggade redirectas till `/login`, och inloggade icke-admins blockeras från `/admin/*` (redirect till `/`). Täcker alltså både autentisering och rollbaserad accesskontroll i ett enda fil. -- `lib/auth-headers.ts` — `getAuthHeaders()` hämtar Bearer-token från session (server-side, används i Next.js API route-proxies) -- `lib/api.ts` — `fetchJson()` lägger automatiskt till auth-headers server-side, redirectar till `/login` vid 401 -- `lib/with-auth.ts` — `withAuth(handler)`-wrapper för Next.js Route Handlers som behöver autentisering via `request.auth`. Löser kompatibilitetsproblemet med `auth()` standalone i Next.js 16 + NextAuth beta. -- `lib/use-auth-fetch.ts` — `useAuthFetch()`-hook för klientkomponenter. Returnerar en `fetch`-funktion som automatiskt lägger till `Authorization: Bearer `. Används i komponenter som gör anrop till endpoints som Caddy routar direkt till NestJS. -- `app/Providers.tsx` — Client component som wrappa appen med `SessionProvider` (krävs för att `useSession()` ska fungera) +- `auth.ts` — NextAuth-konfiguration med Credentials provider; sparar `accessToken`, `userId`, `username` och `role` i JWT-token och session +- `proxy.ts` — Next.js 16 kräver att auth-middleware heter `proxy.ts` (inte `middleware.ts`). Skyddar alla routes utom `/login`, `/register` och `/api/*`: ej inloggade redirectas till `/login`, och inloggade icke-admins blockeras frÃ¥n `/admin/*` (redirect till `/`). Täcker alltsÃ¥ bÃ¥de autentisering och rollbaserad accesskontroll i ett enda fil. +- `lib/auth-headers.ts` — `getAuthHeaders()` hämtar Bearer-token frÃ¥n session (server-side, används i Next.js API route-proxies) +- `lib/api.ts` — `fetchJson()` lägger automatiskt till auth-headers server-side, redirectar till `/login` vid 401 +- `lib/with-auth.ts` — `withAuth(handler)`-wrapper för Next.js Route Handlers som behöver autentisering via `request.auth`. Löser kompatibilitetsproblemet med `auth()` standalone i Next.js 16 + NextAuth beta. +- `lib/use-auth-fetch.ts` — `useAuthFetch()`-hook för klientkomponenter. Returnerar en `fetch`-funktion som automatiskt lägger till `Authorization: Bearer `. Används i komponenter som gör anrop till endpoints som Caddy routar direkt till NestJS. +- `app/Providers.tsx` — Client component som wrappa appen med `SessionProvider` (krävs för att `useSession()` ska fungera) ### Kritisk regel: Caddy-routing och klientkomponenter -Caddy routar `/api/recipes*`, `/api/products*`, `/api/inventory*` och `/api/pantry*` **direkt till NestJS** — Next.js route handlers körs aldrig för dessa sökvägar. Det innebär att klientkomponenter (`'use client'`) som gör `fetch()` till dessa endpoints **inte kan lita på att withAuth eller getAuthHeaders lägger till token automatiskt**. +Caddy routar `/api/recipes*`, `/api/products*`, `/api/inventory*` och `/api/pantry*` **direkt till NestJS** — Next.js route handlers körs aldrig för dessa sökvägar. Det innebär att klientkomponenter (`'use client'`) som gör `fetch()` till dessa endpoints **inte kan lita pÃ¥ att withAuth eller getAuthHeaders lägger till token automatiskt**. -**Lösning: använd alltid `useAuthFetch()` i klientkomponenter:** +**Lösning: använd alltid `useAuthFetch()` i klientkomponenter:** ```ts import { useAuthFetch } from '../../../lib/use-auth-fetch'; @@ -471,31 +471,31 @@ import { useAuthFetch } from '../../../lib/use-auth-fetch'; // I komponentfunktionen: const authFetch = useAuthFetch(); -// Anrop — Content-Type och Authorization sätts automatiskt: +// Anrop — Content-Type och Authorization sätts automatiskt: const res = await authFetch('/api/recipes/1', { method: 'PATCH', body: JSON.stringify(data) }); const res = await authFetch('/api/recipes', { method: 'POST', body: JSON.stringify(body) }); const res = await authFetch(`/api/recipes/${id}`, { method: 'DELETE' }); ``` -Hooken hämtar token via `useSession()` (kräver att `SessionProvider` finns i layout) och är memoizerad med `useCallback`. +Hooken hämtar token via `useSession()` (kräver att `SessionProvider` finns i layout) och är memoizerad med `useCallback`. -**Endpoints som kräver `useAuthFetch()` i klientkod (Caddy-direktrouting):** +**Endpoints som kräver `useAuthFetch()` i klientkod (Caddy-direktrouting):** -| Prefix | Destination | Kräver useAuthFetch i klient? | +| Prefix | Destination | Kräver useAuthFetch i klient? | |--------|-------------|-------------------------------| -| `/api/recipes*` | `recipe-api:8080` direkt | ✅ Ja | -| `/api/products*` | `recipe-api:8080` direkt | ✅ Ja | -| `/api/inventory*` | `recipe-api:8080` direkt | ✅ Ja | -| `/api/pantry*` | `recipe-api:8080` direkt | ✅ Ja | +| `/api/recipes*` | `recipe-api:8080` direkt | ✅ Ja | +| `/api/products*` | `recipe-api:8080` direkt | ✅ Ja | +| `/api/inventory*` | `recipe-api:8080` direkt | ✅ Ja | +| `/api/pantry*` | `recipe-api:8080` direkt | ✅ Ja | | `/api/admin/*` | `recipe-frontend:3000` via catch-all | Nej (withAuth hanterar) | | `/api/auth/*` | `recipe-frontend:3000` via catch-all | Nej | | `/api/categories` | `recipe-frontend:3000` via catch-all | Nej | -> ⚠️ **Återkommande misstag (2026-04-21):** Vid implementationen av `InventoryView` och `PantryView` (klientkomponenter i Databas-tabben) användes plain `fetch('/api/inventory')` och `fetch('/api/pantry')` utan token. NestJS returnerade 401 och komponenterna visade "Kunde inte hämta inventarie". Felet uppstod trots att en Next.js API-route för `/api/inventory` redan existerade — Caddy routar dessa paths förbi Next.js helt. **Regel: varje ny klientkomponent som hämtar data från `/api/inventory*`, `/api/pantry*`, `/api/recipes*` eller `/api/products*` måste använda `useAuthFetch()`.** +> ⚠️ **Ã…terkommande misstag (2026-04-21):** Vid implementationen av `InventoryView` och `PantryView` (klientkomponenter i Databas-tabben) användes plain `fetch('/api/inventory')` och `fetch('/api/pantry')` utan token. NestJS returnerade 401 och komponenterna visade "Kunde inte hämta inventarie". Felet uppstod trots att en Next.js API-route för `/api/inventory` redan existerade — Caddy routar dessa paths förbi Next.js helt. **Regel: varje ny klientkomponent som hämtar data frÃ¥n `/api/inventory*`, `/api/pantry*`, `/api/recipes*` eller `/api/products*` mÃ¥ste använda `useAuthFetch()`.** - Svenska felmeddelanden via `lib/error-handler.ts` (`parseErrorResponse`) -- Centraliserad API-access via `lib/api.ts` (`fetchJson`) — för server-side anrop -- Klientside auth-fetch via `lib/use-auth-fetch.ts` (`useAuthFetch`) — för klientkomponenter som pratar direkt med NestJS +- Centraliserad API-access via `lib/api.ts` (`fetchJson`) — för server-side anrop +- Klientside auth-fetch via `lib/use-auth-fetch.ts` (`useAuthFetch`) — för klientkomponenter som pratar direkt med NestJS - Typade inventory/recipe data i `features/inventory/types.ts` --- @@ -503,320 +503,324 @@ Hooken hämtar token via `useSession()` (kräver att `SessionProvider` finns i l ## Backend (NestJS) - **Framework:** NestJS 10.3 -- **Språk:** TypeScript 5.4.5 +- **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 globalt, `RolesGuard` kontrollerar rollkrav, `@Public()` dekorator för öppna endpoints -- **Rollbaserad behörighet:** `@Roles('admin')` dekoratorn via `SetMetadata`; `RolesGuard` kastar 403 om rätt roll saknas +- **Autentisering:** JWT (7 dagars token), `JwtAuthGuard` skyddar alla routes globalt, `RolesGuard` kontrollerar rollkrav, `@Public()` dekorator för öppna endpoints +- **Rollbaserad behörighet:** `@Roles('admin')` dekoratorn via `SetMetadata`; `RolesGuard` kastar 403 om rätt roll saknas - **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden) -- **Hälsokontroll:** /health endpoints -- **Bygg:** `nest build`, körs i Docker-container +- **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, POST /api/auth/register -│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role + isPremium) -│ ├── auth.module.ts -│ ├── jwt.strategy.ts # Passport JWT-strategi (returnerar userId, username, role, isPremium) -│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public) -│ ├── roles.guard.ts # Guard som kontrollerar @Roles() metadata; kastar 403 -│ └── decorators/ -│ ├── public.decorator.ts # @Public() – markerar öppen endpoint -│ ├── current-user.decorator.ts # @CurrentUser() – extraherar {userId, username, role, isPremium} -│ └── roles.decorator.ts # @Roles('admin') – sätter rollkrav via SetMetadata -├── ai/ -│ ├── ai.service.ts # AiService: suggestCategory() — anropar Mistral API med kategorikontext -│ └── ai.module.ts # Exporterar AiService (importeras av ProductsModule, ReceiptImportModule) -├── users/ -│ ├── users.controller.ts # GET /api/users/me, PATCH /api/users/me -│ │ # GET /api/users (admin), PATCH /api/users/:id/role (admin) -│ │ # POST /api/users (admin), DELETE /api/users/:id (admin) -│ │ # POST /api/users/:id/reset-password (admin), PATCH /api/users/:id/email (admin) -│ │ # PATCH /api/users/:id/premium (admin) -│ ├── users.service.ts # findByUsername, findById, create, updateProfile -│ │ # findAll, setRole, adminCreate, deleteUser, resetPassword, updateEmail, setPremium -│ ├── admin-bootstrap.service.ts # OnApplicationBootstrap: skapar/uppdaterar 4 seed-användare -│ └── users.module.ts -├── categories/ -│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public) -│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk), findFlattened (med full path) -│ └── 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 -│ │ # GET /products/pending (admin) -│ │ # GET /products/:id/suggest-category (premium/admin) -│ │ # POST /products/ai-categorize-bulk (admin) -│ │ # PATCH /products/:id/status (admin) -│ ├── products.service.ts # Produktlogik inkl. resetAll(), findPending(), setStatus() -│ ├── products.module.ts # Importerar AiModule + CategoriesModule -│ └── 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) -├── meal-plan/ -│ ├── meal-plan.controller.ts # GET/POST/DELETE + shopping-list + inventory-compare -│ ├── meal-plan.service.ts # Upsert, shoppingList (portionsskalad), inventoryCompare -│ ├── meal-plan.module.ts -│ └── dto/ -│ └── create-meal-plan-entry.dto.ts # { date, recipeId, servings? } -├── receipt-import/ -│ ├── receipt-import.controller.ts # POST /api/receipt-import (multipart, kräver JWT) -│ │ # Skickar isPremium till service (premium/admin → AI-enrichment) -│ ├── receipt-import.service.ts # Mistral AI-anrop, bildtolkning, enrichWithAiCategories() -│ ├── receipt-import.module.ts # Importerar AiModule + CategoriesModule -│ └── dto/ -│ └── parsed-receipt-item.dto.ts # Inkl. categorySuggestion?: CategorySuggestion -├── receipt-alias/ -│ ├── receipt-alias.controller.ts # CRUD /api/receipt-alias -│ ├── receipt-alias.service.ts -│ └── dto/ -└── 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``` +├── app.module.ts # Root module +├── main.ts # Startpunkt (port 8080, global prefix "api") +├── auth/ +│ ├── auth.controller.ts # POST /api/auth/login, POST /api/auth/register +│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role + isPremium) +│ ├── auth.module.ts +│ ├── jwt.strategy.ts # Passport JWT-strategi (returnerar userId, username, role, isPremium) +│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public) +│ ├── roles.guard.ts # Guard som kontrollerar @Roles() metadata; kastar 403 +│ └── decorators/ +│ ├── public.decorator.ts # @Public() – markerar öppen endpoint +│ ├── current-user.decorator.ts # @CurrentUser() – extraherar {userId, username, role, isPremium} +│ └── roles.decorator.ts # @Roles('admin') – sätter rollkrav via SetMetadata +├── ai/ +│ ├── ai.service.ts # AiService: suggestCategory() — anropar Mistral API med kategorikontext +│ └── ai.module.ts # Exporterar AiService (importeras av ProductsModule, ReceiptImportModule) +├── users/ +│ ├── users.controller.ts # GET /api/users/me, PATCH /api/users/me +│ │ # GET /api/users (admin), PATCH /api/users/:id/role (admin) +│ │ # POST /api/users (admin), DELETE /api/users/:id (admin) +│ │ # POST /api/users/:id/reset-password (admin), PATCH /api/users/:id/email (admin) +│ │ # PATCH /api/users/:id/premium (admin) +│ ├── users.service.ts # findByUsername, findById, create, updateProfile +│ │ # findAll, setRole, adminCreate, deleteUser, resetPassword, updateEmail, setPremium +│ ├── admin-bootstrap.service.ts # OnApplicationBootstrap: skapar/uppdaterar 4 seed-användare +│ └── users.module.ts +├── categories/ +│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public) +│ ├── categories.service.ts # findAll (flat), findTree (hierarkisk), findFlattened (med full path) +│ └── 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 +│ │ # GET /products/pending (admin) +│ │ # GET /products/:id/suggest-category (premium/admin) +│ │ # POST /products/ai-categorize-bulk (admin) +│ │ # PATCH /products/:id/status (admin) +│ ├── products.service.ts # Produktlogik inkl. resetAll(), findPending(), setStatus() +│ ├── products.module.ts # Importerar AiModule + CategoriesModule +│ └── 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) +├── meal-plan/ +│ ├── meal-plan.controller.ts # GET/POST/DELETE + shopping-list + inventory-compare +│ ├── meal-plan.service.ts # Upsert, shoppingList (portionsskalad), inventoryCompare +│ ├── meal-plan.module.ts +│ └── dto/ +│ └── create-meal-plan-entry.dto.ts # { date, recipeId, servings? } +├── receipt-import/ +│ ├── receipt-import.controller.ts # POST /api/receipt-import (multipart, kräver JWT) +│ │ # Skickar isPremium till service (premium/admin → AI-enrichment) +│ ├── receipt-import.service.ts # Mistral AI-anrop, bildtolkning, enrichWithAiCategories() +│ ├── receipt-import.module.ts # Importerar AiModule + CategoriesModule +│ └── dto/ +│ └── parsed-receipt-item.dto.ts # Inkl. categorySuggestion?: CategorySuggestion +├── receipt-alias/ +│ ├── receipt-alias.controller.ts # CRUD /api/receipt-alias +│ ├── receipt-alias.service.ts +│ └── dto/ +└── 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) +- Ö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/)) +**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 + - 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 + 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. + - 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) + - **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) +- 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") +- 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 +- 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 +- **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) +- 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 +- **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) + - `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 -- **Bulk-uppdatering:** `bulkUpdate(ids, data)` — Uppdatera ett godtyckligt antal produkter i ett enda DB-anrop (`updateMany`). Används primärt för bulk-kategorisering i admin-UI. Body: `{ ids: number[], categoryId?: number | null }` + - `restore()` - Ã…terställ borttagen produkt +- **Bulk-uppdatering:** `bulkUpdate(ids, data)` — Uppdatera ett godtyckligt antal produkter i ett enda DB-anrop (`updateMany`). Används primärt för bulk-kategorisering i admin-UI. Body: `{ ids: number[], categoryId?: number | null }` **Matplan-API:** -- **`upsert(dto)`** — Skapar eller uppdaterar en `MealPlanEntry` för ett givet datum (unik per dag). Sparar `recipeId` och valfritt `servings`. -- **`findByRange(from, to)`** — Hämtar alla planerade dagar i ett datumintervall, inkl. receptinfo. -- **`shoppingList(from, to)`** — Aggregerar ingrediensmängder för alla planerade recept i intervallet. - - Om `entry.servings` och `recipe.servings` är satta beräknas en skala: `scale = entry.servings / recipe.servings` - - Ingrediensmängder multipliceras med skalan innan aggregering +- **`upsert(userId, dto)`** — Skapar eller uppdaterar en `MealPlanEntry` för inloggad användare och ett givet datum (unik per `userId + date`). Sparar `recipeId` och valfritt `servings`. +- **`findByRange(userId, from, to)`** — Hämtar alla planerade dagar för inloggad användare i ett datumintervall, inkl. receptinfo. +- **`shoppingList(userId, from, to)`** — Aggregerar ingrediensmängder för inloggad användares planerade recept i intervallet. + - Om `entry.servings` och `recipe.servings` är satta beräknas en skala: `scale = entry.servings / recipe.servings` + - Ingrediensmängder multipliceras med skalan innan aggregering - Returnerar lista av `{ productName, quantity, unit }` -- **`inventoryCompare(from, to)`** — Kör samma aggregering som `shoppingList` men jämför sedan varje ingrediens mot: - 1. **Pantry (baslager):** Om produkten finns i `PantryItem`-tabellen returneras `status: 'pantry'`, `missing: 0` — varan räknas alltid som tillgänglig oavsett inventariet. - 2. **Inventariet:** Övriga ingredienser jämförs mot `InventoryItem`. Returnerar `status: 'enough' | 'missing'`. - - Sorteringsordning: `missing` → `enough` → `pantry` - - Frontend visar 📦-ikon och ”(baslager)” för pantry-varor; de visas aldrig som ”saknas” i inköpslistan. +- **`inventoryCompare(from, to)`** — Kör samma aggregering som `shoppingList` men jämför sedan varje ingrediens mot: +- **`inventoryCompare(userId, from, to)`** — Kör samma aggregering som `shoppingList` men jämför sedan varje ingrediens mot: + 1. **Pantry (baslager):** Om produkten finns i `PantryItem` för `userId` returneras `status: 'pantry'`, `missing: 0` — varan räknas alltid som tillgänglig oavsett inventariet. + - Sorteringsordning: `missing` → `enough` → `pantry` + - Frontend visar 📦-ikon och ”(baslager)” för pantry-varor; de visas aldrig som ”saknas” i inköpslistan. **Kvittoimport-API:** -- **`parseReceipt(file, isPremium)`** — Tar emot en bild eller PDF (max 15 MB), skickar den till Mistral AI för tolkning och returnerar en lista av kandidatprodukter med namn, kvantitet och enhet. -- Alias-matchning: före returneringen slås varje rånamn upp mot `ReceiptAlias`-tabellen och mot `Product.normalizedName`. Träffar kopplas automatiskt till rätt produkt-ID. -- **AI-kategorisuggestion (premium):** Om `isPremium = true` (eller admin) och en vara varken alias-matchas eller ordbaserat matchas anropas `AiService.suggestCategory()`. Svaret inkluderas som `categorySuggestion` i retur-DTO:n och visas som ett lila "✨"-chip i frontend. -- Stödda MIME-typer: `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` +- **`parseReceipt(file, isPremium)`** — Tar emot en bild eller PDF (max 15 MB), skickar den till Mistral AI för tolkning och returnerar en lista av kandidatprodukter med namn, kvantitet och enhet. +- Alias-matchning: före returneringen slÃ¥s varje rÃ¥namn upp mot `ReceiptAlias`-tabellen och mot `Product.normalizedName`. Träffar kopplas automatiskt till rätt produkt-ID. +- **AI-kategorisuggestion (premium):** Om `isPremium = true` (eller admin) och en vara varken alias-matchas eller ordbaserat matchas anropas `AiService.suggestCategory()`. Svaret inkluderas som `categorySuggestion` i retur-DTO:n och visas som ett lila "✨"-chip i frontend. +- Stödda MIME-typer: `image/jpeg`, `image/png`, `image/webp`, `image/heic`, `image/heif`, `application/pdf` **AI-API (`AiService`):** -- **`suggestCategory(productName, categories: FlatCategory[])`** — Skickar produktnamnet och en flat lista av alla kategorier (med full sökväg, t.ex. "Mejeri och ägg > Mjölk och grädde") till Mistral API (`mistral-small-2603`). Returnerar `CategorySuggestion`. +- **`suggestCategory(productName, categories: FlatCategory[])`** — Skickar produktnamnet och en flat lista av alla kategorier (med full sökväg, t.ex. "Mejeri och ägg > Mjölk och grädde") till Mistral API (`mistral-small-2603`). Returnerar `CategorySuggestion`. - **Svar-typ `CategorySuggestion`:** `{ categoryId, categoryName, path, confidence: 'high'|'medium'|'low', usedFallback: boolean }` -- **Fallback-strategi:** Om AI returnerar ett ogiltigt kategori-ID eller om anropet misslyckas: försök hitta "Övrigt"-underkategori → fallback till rot-"Övrigt" (id 221) med `confidence: 'low'` och `usedFallback: true` +- **Fallback-strategi:** Om AI returnerar ett ogiltigt kategori-ID eller om anropet misslyckas: försök hitta "Övrigt"-underkategori → fallback till rot-"Övrigt" (id 221) med `confidence: 'low'` och `usedFallback: true` - **Integration:** `AiModule` exporterar `AiService` och importeras av `ProductsModule` och `ReceiptImportModule` -- **CategoriesService tillägg:** `findFlattened()` bygger en flat lista med `{ id, name, path }` där `path` är den fullständiga kategorivägen (används som kontext till Mistral) +- **CategoriesService tillägg:** `findFlattened()` bygger en flat lista med `{ id, name, path }` där `path` är den fullständiga kategorivägen (används som kontext till Mistral) --- -## API-endpoints (fullständig lista) +## API-endpoints (fullständig lista) -### 🏥 Health endpoints +### 🏥 Health endpoints ``` -GET /api/health Övergripande hälsakontroll (200/503) -GET /api/health/db Databasspecifik hälsa + responseTime +GET /api/health Övergripande hälsakontroll (200/503) +GET /api/health/db Databasspecifik hälsa + responseTime ``` -### 📦 Inventarie-endpoints +### 📦 Inventarie-endpoints ``` -GET /api/inventory Lista inventarieföremål +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 +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 +### 🍽️ Recept-endpoints ``` -POST /api/quick-import Snabbimport från URL, PDF eller bild +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 +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 +GET /api/recipes/:id/inventory-preview Jämför recept mot inventarie ``` -### 🏷️ Produkt-endpoints +### 🏷️ Produkt-endpoints ``` GET /api/products Lista alla aktiva produkter POST /api/products Skapa ny produkt -GET /api/products/:id Hämta specifik 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 +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 +GET /api/products/merge-preview Förhandsgranska merge ?sourceProductId=X &targetProductId=Y -POST /api/products/merge Slå ihop två produkter +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) -POST /api/products/bulk-update Uppdatera flera produkter (t.ex. sätt kategori) +POST /api/products/bulk-update Uppdatera flera produkter (t.ex. sätt kategori) Body: { ids: number[], categoryId?: number | null } GET /api/products/pending Lista pending-produkter (admin) -GET /api/products/:id/suggest-category AI-förslag på kategori för produkt (premium/admin) -POST /api/products/ai-categorize-bulk Kör AI-kategorisering på alla okategoriserade (admin) +GET /api/products/:id/suggest-category AI-förslag pÃ¥ kategori för produkt (premium/admin) +POST /api/products/ai-categorize-bulk Kör AI-kategorisering pÃ¥ alla okategoriserade (admin) Returnerar lista av { productId, productName, suggestion: CategorySuggestion } -PATCH /api/products/:id/status Sätt produktstatus active/rejected (admin) +PATCH /api/products/:id/status Sätt produktstatus active/rejected (admin) Body: { status: 'active' | 'rejected' } ``` ### Kategori-endpoints ``` GET /api/categories Flat lista av alla kategorier (@Public) -GET /api/categories/tree Hierarkiskt träd (@Public) +GET /api/categories/tree Hierarkiskt träd (@Public) ``` -### Användar-endpoints +### Användar-endpoints ``` POST /api/auth/login Logga in, returnerar JWT inkl. role + isPremium (@Public) -GET /api/users/me Hämta inloggad användares profil (inkl. role, isPremium) +GET /api/users/me Hämta inloggad användares profil (inkl. role, isPremium) PATCH /api/users/me Uppdatera firstName, lastName, email -GET /api/users Lista alla användare (kräver admin-roll) -PATCH /api/users/:id/role Ändra roll för användare (kräver admin-roll) -PATCH /api/users/:id/premium Sätt isPremium true/false (kräver admin-roll) +GET /api/users Lista alla användare (kräver admin-roll) +PATCH /api/users/:id/role Ändra roll för användare (kräver admin-roll) +PATCH /api/users/:id/premium Sätt isPremium true/false (kräver admin-roll) Body: { isPremium: boolean } ``` ### 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 +GET /api/pantry Lista inloggad användares baslager (inkl. produktinfo) +POST /api/pantry Lägg till produkt i baslagret (kräver JWT) +DELETE /api/pantry/:id Ta bort produkt frÃ¥n baslagret (kräver JWT, ägarskap) ``` -### 🗓️ Matplan/matsedel-endpoints +> Alla anrop kräver JWT. Varje användare ser bara sitt eget baslager. + +### 🗓️ Matplan/matsedel-endpoints ``` -GET /api/meal-plan?from=YYYY-MM-DD&to=YYYY-MM-DD Lista planerade recept för datumintervall -POST /api/meal-plan Skapa eller uppdatera post (upsert per datum) +GET /api/meal-plan?from=YYYY-MM-DD&to=YYYY-MM-DD Lista planerade recept för datumintervall (per inloggad användare) +POST /api/meal-plan Skapa eller uppdatera post (upsert per datum, per användare) Body: { date, recipeId, servings? } -DELETE /api/meal-plan/:date Ta bort recept för ett specifikt datum +DELETE /api/meal-plan/:date Ta bort recept för ett specifikt datum (per inloggad användare) -GET /api/meal-plan/shopping-list?from=...&to=... Generera inköpslista för veckan +GET /api/meal-plan/shopping-list?from=...&to=... Generera inköpslista för veckan (per inloggad användare) Skalad proportionellt efter portionsjustering -GET /api/meal-plan/inventory-compare?from=...&to=... Jämför inköpslista mot inventarie - Returnerar status per ingrediens: räcker | saknas | enhetskonflikt +GET /api/meal-plan/inventory-compare?from=...&to=... Jämför inköpslista mot inventarie (användarens baslager) + Returnerar status per ingrediens: räcker | saknas | enhetskonflikt ``` -### 🧾 Kvitto-endpoints +> Alla anrop kräver JWT. Varje användare ser och hanterar bara sin egen matplan. + +### 🧾 Kvitto-endpoints ``` POST /api/receipt-import Tolka kvittobild (JPEG, PNG, WebP, HEIC, PDF) Multipart-form med "file"; max 15 MB Returnerar lista av { name, quantity, unit, productId?, confidence } GET /api/receipt-alias Lista alla kvitto-alias -POST /api/receipt-alias Skapa nytt alias (receiptName → productId) +POST /api/receipt-alias Skapa nytt alias (receiptName → productId) DELETE /api/receipt-alias/:id Ta bort alias ``` @@ -834,22 +838,22 @@ model User { lastName String? passwordHash String role String @default("user") # "user" eller "admin" - isPremium Boolean @default(false) # Styr tillgång till AI-premium-funktioner + isPremium Boolean @default(false) # Styr tillgÃ¥ng till AI-premium-funktioner createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ``` -**Bootstrap-användare:** När backend startar kör `AdminBootstrapService.onApplicationBootstrap()` och skapar eller uppdaterar fyra användare baserade på miljövariabler: +**Bootstrap-användare:** När backend startar kör `AdminBootstrapService.onApplicationBootstrap()` och skapar eller uppdaterar fyra användare baserade pÃ¥ miljövariabler: -| Användarnamn | Roll | isPremium | E-post | Miljövariabel | +| Användarnamn | Roll | isPremium | E-post | Miljövariabel | |---|---|---|---|---| | Nadmin | admin | false | nadmin@localhost | `ADMIN_NADMIN_PASSWORD` | | Padmin | admin | false | padmin@localhost | `ADMIN_PADMIN_PASSWORD` | | user1 | user | false | user1@localhost | `SEED_USER1_PASSWORD` | | user2 | user | false | user2@localhost | `SEED_USER2_PASSWORD` | -Om en användare redan finns men har fel roll rättas rollen automatiskt. Om miljövariabeln saknas hoppas den användaren över med en varning i loggen. +Om en användare redan finns men har fel roll rättas rollen automatiskt. Om miljövariabeln saknas hoppas den användaren över med en varning i loggen. ### Category ```prisma @@ -857,35 +861,35 @@ model Category { id Int @id @default(autoincrement()) name String parentId Int? - parent Category? @relation("CategoryTree", ...) # Förälder (null = toppnivå) + 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 → Standard mjölk` +Hierarkin har 3 nivÃ¥er: **Huvudkategori → Underkategori → Typ** +Exempelträd: `Mejeri, ost & ägg → Mjölk → Standard mjölk` #### Kategori- och produktseed -All seed-data för kategorier och produkter hanteras av **`db/seeds/seed_all.sql`** — den enda sanningskällan. +All seed-data för kategorier och produkter hanteras av **`db/seeds/seed_all.sql`** — den enda sanningskällan. -**Vad filen gör:** -1. **TRUNCATE `Category`** + nollställer `Product.categoryId` — rensar alla ev. gamla/duplicerade kategorier -2. **Bygger upp hela kategoriträdet** (nivå 1–3) med rena `INSERT` — exakt en rad per kategori, inga dubbletter möjliga -3. **`INSERT IGNORE` ~190 produkter** — hoppar över produkter som redan finns -4. **`UPDATE Product SET categoryId`** — kopplar produkterna till rätt kategori via JOIN-subqueries +**Vad filen gör:** +1. **TRUNCATE `Category`** + nollställer `Product.categoryId` — rensar alla ev. gamla/duplicerade kategorier +2. **Bygger upp hela kategoriträdet** (nivÃ¥ 1–3) med rena `INSERT` — exakt en rad per kategori, inga dubbletter möjliga +3. **`INSERT IGNORE` ~190 produkter** — hoppar över produkter som redan finns +4. **`UPDATE Product SET categoryId`** — kopplar produkterna till rätt kategori via JOIN-subqueries -**Körs manuellt på servern:** +**Körs manuellt pÃ¥ servern:** ```bash DB_PASS=$(grep MARIADB_ROOT_PASSWORD .env | cut -d= -f2) docker exec -i recipe-db mariadb -uroot -p"$DB_PASS" recipe_app < db/seeds/seed_all.sql ``` -> **OBS:** Migrationen `20260417310000_add_category_tree/migration.sql` innehåller fortfarande gamla kategori-INSERTs från ursprungsimplementationen. Dessa kör en gång vid `prisma migrate deploy` men seed_all.sql nollställer och skriver över dem vid nästa körning. Se NEXT_STEPS.md för planerat refactor-arbete. +> **OBS:** Migrationen `20260417310000_add_category_tree/migration.sql` innehÃ¥ller fortfarande gamla kategori-INSERTs frÃ¥n ursprungsimplementationen. Dessa kör en gÃ¥ng vid `prisma migrate deploy` men seed_all.sql nollställer och skriver över dem vid nästa körning. Se NEXT_STEPS.md för planerat refactor-arbete. -> **OBS:** Om `InventoryItem`-rader pekar på produkter som inte längre finns (t.ex. efter TRUNCATE av Product) uppstår Prisma-felet "Field product is required". Rensa orphan-rader med: +> **OBS:** Om `InventoryItem`-rader pekar pÃ¥ produkter som inte längre finns (t.ex. efter TRUNCATE av Product) uppstÃ¥r Prisma-felet "Field product is required". Rensa orphan-rader med: > ```sql > DELETE FROM InventoryItem WHERE productId NOT IN (SELECT id FROM Product); > ``` @@ -896,10 +900,10 @@ 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 + 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 status String @default("active") # "active" | "pending" | "rejected" @@ -916,23 +920,23 @@ model Product { ``` **Produktstatus:** -- `active` — normal produkt synlig i alla listor (default) -- `pending` — föreslagen produkt som väntar på admin-godkännande; visas på `/admin/products/pending` -- `rejected` — avvisad produkt; visas inte i vanliga listor +- `active` — normal produkt synlig i alla listor (default) +- `pending` — föreslagen produkt som väntar pÃ¥ admin-godkännande; visas pÃ¥ `/admin/products/pending` +- `rejected` — avvisad produkt; visas inte i vanliga listor ### 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) + 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 + brand String? # Varumärke location String? # Lagerplats (Kyl, Frys, Skafferi, etc) - purchaseDate DateTime? # Köpdatum - opened Boolean? # Markering för öppnad produkt - suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian") - bestBeforeDate DateTime? # Bäst före-datum + purchaseDate DateTime? # Köpdatum + opened Boolean? # Markering för öppnad produkt + suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian") + bestBeforeDate DateTime? # Bäst före-datum comment String? # Fri kommentar createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -963,9 +967,9 @@ model Recipe { id Int @id @default(autoincrement()) name String # Receptnamn description String? # Receptbeskrivning - servings Int? # Antal portioner receptet är dimensionerat för + servings Int? # Antal portioner receptet är dimensionerat för imageUrl String? # URL till receptbild (valfritt) - instructions String? @db.Text # Tillagningsinstruktioner (kan vara långt, stöder Markdown) + instructions String? @db.Text # Tillagningsinstruktioner (kan vara lÃ¥ngt, stöder Markdown) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -974,7 +978,7 @@ model Recipe { } ``` -`servings` är grundportionsantalet — matplanen använder det för att skala ingrediensmängder om användaren anger ett avvikande portionsantal per dag. +`servings` är grundportionsantalet — matplanen använder det för att skala ingrediensmängder om användaren anger ett avvikande portionsantal per dag. ### RecipeIngredient ```prisma @@ -997,37 +1001,49 @@ model RecipeIngredient { ```prisma model PantryItem { id Int @id @default(autoincrement()) - productId Int @unique # En produkt kan bara finnas en gång i baslagret + userId Int # FK till User — baslager är per användare + productId Int # FK till Product createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) # En produkt kan bara finnas en gÃ¥ng per användare + @@index([userId]) } ``` +> **Beslut 2026-04-22:** Baslager är user-scopat. Varje inloggad användare har ett eget baslager. Den gamla globala modellen (`@unique([productId])`) ersattes med den sammansatta unika nyckeln `(userId, productId)`. Se [migrationssektionen nedan](#migration-user-scope-pantry-och-meal-plan). + ### MealPlanEntry ```prisma model MealPlanEntry { id Int @id @default(autoincrement()) - date DateTime # Datum för planerad måltid (en per dag) + userId Int # FK till User — matplan är per användare + date DateTime # Datum för planerad mÃ¥ltid recipeId Int # Foreign key till Recipe - servings Int? # Justerat portionsantal för den dagen (null = använd receptets grundvärde) + servings Int? # Justerat portionsantal för den dagen (null = använd receptets grundvärde) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) recipe Recipe @relation(fields: [recipeId], references: [id]) - @@unique([date]) # Bara ett recept per dag + @@unique([userId, date]) # Bara ett recept per dag och användare + @@index([userId]) } ``` -**Portionsskalning:** Om `servings` är satt och skiljer sig från `recipe.servings` beräknar `shoppingList()` och `inventoryCompare()` en skala: `scale = entry.servings / recipe.servings`. Alla ingrediensmängder multipliceras med denna faktor. +> **Beslut 2026-04-22:** Matplan är user-scopat. Den gamla globala modellen (`@@unique([date])`) ersattes med `@@unique([userId, date])`. Se [migrationssektionen nedan](#migration-user-scope-pantry-och-meal-plan). + +**Portionsskalning:** Om `servings` är satt och skiljer sig frÃ¥n `recipe.servings` beräknar `shoppingList()` och `inventoryCompare()` en skala: `scale = entry.servings / recipe.servings`. Alla ingrediensmängder multipliceras med denna faktor. ### ReceiptAlias ```prisma model ReceiptAlias { id Int @id @default(autoincrement()) - receiptName String @unique # Namn som kvittosystemet returnerar (råtext) + receiptName String @unique # Namn som kvittosystemet returnerar (rÃ¥text) productId Int # FK till matchad Product createdAt DateTime @default(now()) @@ -1035,57 +1051,147 @@ model ReceiptAlias { } ``` -Kvitto-alias lagrar mappningar från kvittots råtext till produkt-ID. När Mistral AI returnerar t.ex. "ICA Kvarg Jordg" slås det upp mot alias-tabellen. Om träff hoppas manuell matchning över. +Kvitto-alias lagrar mappningar frÃ¥n kvittots rÃ¥text till produkt-ID. När Mistral AI returnerar t.ex. "ICA Kvarg Jordg" slÃ¥s det upp mot alias-tabellen. Om träff hoppas manuell matchning över. --- -## Receptimport och receptskaping — Detaljerad arkitektur + +--- + +## Arkitektur: User-scope för baslager och matplan (2026-04-22) + +### Bakgrund och beslut + +Baslager (`PantryItem`) och matplan (`MealPlanEntry`) var ursprungligen globala — delade av alla användare. Det skapade problem när flera användare loggade in eftersom de såg och påverkade varandras data. + +**Beslut 2026-04-22:** Baslager och matplan är user-scopade. Varje inloggad användare har ett eget, isolerat baslager och en egen matplan. + +### Databasschema-ändringar + +Båda modellerna fick ett `userId`-fält med FK till `User` och nya sammansatta unika nycklar: + +| Modell | Gammal unik nyckel | Ny unik nyckel | +|---|---|---| +| `PantryItem` | `@@unique([productId])` | `@@unique([userId, productId])` | +| `MealPlanEntry` | `@@unique([date])` | `@@unique([userId, date])` | + +`User`-modellen fick relationslistor: `pantryItems PantryItem[]` och `mealPlanEntries MealPlanEntry[]`. + +### Migration + +Migration: `backend/prisma/migrations/20260422130000_user_scope_pantry_meal_plan/migration.sql` + +Viktiga steg i migrationens SQL: +1. Lägg till `userId`-kolonn (nullable) i `PantryItem` och `MealPlanEntry`. +2. Backfill `userId` till `(SELECT id FROM User LIMIT 1)` för alla befintliga rader. +3. Sätt `userId NOT NULL`. +4. Lägg till icke-unik index `PantryItem_productId_idx` **innan** det gamla unika indexet tas bort (MySQL/MariaDB kräver att ett FK-index finns kvar när ett annat tas bort). +5. Droppa gammalt unikt index `PantryItem_productId_key`. +6. Lägg till nytt sammansatt unikt index `@@unique([userId, productId])` och FK till `User`. +7. Droppa `MealPlanEntry_date_key`, lägg till `@@unique([userId, date])` och FK till `User`. + +> **Känd fallgrop:** MariaDB ger fel 1553 om ett unikt index som används av en FK-constraint droppas utan att ett ersättande index finns. Migrationens steg 4 (lägga till `PantryItem_productId_idx` innan drop) är lösningen på detta. + +> **Prisma-kommentarsyntax:** Inline `#`-kommentarer på `@@unique`-rader är ogiltig Prisma-syntax. Använd `//`-kommentarer på en separat rad ovanför attributet. + +### Controller/Service-mönster + +Alla pantry- och matplan-endpoints extraherar `userId` via `@CurrentUser()`-dekoratorn: + +```typescript +// pantry.controller.ts +@Get() +findAll(@CurrentUser() user: { userId: number }) { + return this.pantryService.findAll(user.userId); +} + +@Post() +create(@CurrentUser() user: { userId: number }, @Body() dto: CreatePantryItemDto) { + return this.pantryService.create(user.userId, dto); +} + +@Delete(':id') +remove(@CurrentUser() user: { userId: number }, @Param('id') id: string) { + return this.pantryService.remove(user.userId, +id); +} +``` + +Service-metoderna skickar alltid `userId` i Prisma `where`-klausuler: + +```typescript +// pantry.service.ts +findAll(userId: number) { + return this.prisma.pantryItem.findMany({ where: { userId }, include: { product: true } }); +} + +create(userId: number, dto: CreatePantryItemDto) { + // Kontrollera duplikat per användare + const existing = await this.prisma.pantryItem.findUnique({ + where: { userId_productId: { userId, productId: dto.productId } } + }); + if (existing) return existing; + return this.prisma.pantryItem.create({ data: { userId, productId: dto.productId } }); +} + +remove(userId: number, id: number) { + const item = await this.prisma.pantryItem.findFirst({ where: { id, userId } }); + if (!item) throw new NotFoundException(); + return this.prisma.pantryItem.delete({ where: { id } }); +} +``` + +### Påverkan på Next.js-frontenden + +Inga funktionella ändringar krävdes i Next.js-frontenden. Alla anrop till `/api/pantry` och `/api/meal-plan` går via auth-proxies (`withAuth`) som automatiskt vidarebefordrar användarens Bearer-token. Backend extraherar `userId` från token via JWT-strategin. + +Dubblerad död kod i `PantryView.tsx` och `PantryList.tsx` städades bort i samband med arbetet. +## Receptimport och receptskaping — Detaljerad arkitektur ### Syfte och struktur -Recipe App erbjuder tre vägar för att lägga till recept: +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 +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. +Alla vägar möjliggör automatisk matchning av ingredienser mot databasen. -### Strukturöversikt +### Strukturöversikt -#### Snabbimport-fältet +#### 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 +- 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 + 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 +- 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 + - `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 + - 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 + - Stoppar om ingen läsbar text hittas - **Bildlogik:** - OCR via `tesseract.js` - - Svensk och engelsk språkmodell (`swe+eng`) + - 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 + - `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:** ``` @@ -1095,8 +1201,8 @@ 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) +- `/api/quick-import-proxy` — Proxies till backend +- Hanterar error-konvertering (BE HTTP → FE error message) - Returnerar Markdown eller JSON-error ### Markdown-format och parsningsregler @@ -1110,98 +1216,98 @@ Output: { markdown: string, source: 'ica' | 'pdf' | 'other' } Valfri beskrivning av receptet. ## Ingredienser -- 500 g köttfärs -- 1 st lök -- 2.5 msk tomatpuré -- 1 dl grädde (vispgrädde) +- 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… +## 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) | +| `# Rubrik` | Receptnamn (första H1) | | Text mellan H1 och `## Ingredienser` | Receptbeskrivning (flera rader OK, valfritt) | | `## Ingredienser` | Ingrediens markerare (case-insensitive) | | `- ANTAL ENHET NAMN` | Ingrediens: quantity=ANTAL, unit=ENHET, name=NAMN | -| `- ANTAL NAMN` | Ingrediens utan enhet: unit sätts till "st" | +| `- 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 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"} +"- 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 +### Komponenter och dataflöde #### 2. Backend: `POST /api/recipes/parse-markdown` endpoint -- `@prisma/client` — Databasaccess +- `@prisma/client` — Databasaccess - Common utils: `normalize-name.ts` -**Processflöde:** +**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: +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 + c. Top 5 förslag sortera efter score 5. Returnera ParsedRecipe + suggestions ``` **Matchningsalgoritm:** ``` -Normalisering: lowercase + trim + åäö-handling + skilljetecken-borttagning +Normalisering: lowercase + trim + åäö-handling + skilljetecken-borttagning -1. EXAKT MATCH (100 poäng) +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) +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) +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 +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…", + "name": "KöttfärssÃ¥s", + "description": "En klassisk…", + "instructions": "Stek löken…", "ingredients": [ { - "rawName": "köttfärs", + "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 } + { "productId": 12, "productName": "Köttfärs", "score": 100 }, + { "productId": 34, "productName": "Blandfärs", "score": 65 }, + { "productId": 56, "productName": "Nötfärs", "score": 55 } ] } ] @@ -1211,65 +1317,65 @@ Top 5: Max 5 förslag per ingrediens #### 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` +- 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 2:** Granska ingredienser, välj produkter - **Steg 3:** Spara recept -- Använder `/api/parse-markdown-proxy` för backend-anrop +- 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 +**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 +- Hanterar CORS, headers, error-svarsöversättning --- -## Matplanering och portionsjustering — Detaljerad arkitektur +## Matplanering och portionsjustering — Detaljerad arkitektur ### Syfte -Matplaneringsfunktionen låter användaren planera veckans måltider dag för dag och generera en inköpslista automatiskt. Portionsjusteringen gör det möjligt att anpassa mängden per dag utan att ändra receptet — t.ex. laga en dubbel sats en dag. +Matplaneringsfunktionen lÃ¥ter användaren planera veckans mÃ¥ltider dag för dag och generera en inköpslista automatiskt. Portionsjusteringen gör det möjligt att anpassa mängden per dag utan att ändra receptet — t.ex. laga en dubbel sats en dag. -### Dataflöde +### Dataflöde ``` -Användaren väljer recept + portionsantal för ett datum - → POST /api/meal-plan { date, recipeId, servings } - → MealPlanEntry upserteras (unik per datum) +Användaren väljer recept + portionsantal för ett datum + → POST /api/meal-plan { date, recipeId, servings } + → MealPlanEntry upserteras (unik per datum) -Veckovy hämtar alla poster i intervallet - → GET /api/meal-plan?from=...&to=... +Veckovy hämtar alla poster i intervallet + → GET /api/meal-plan?from=...&to=... -Inköpslista genereras - → GET /api/meal-plan/shopping-list?from=...&to=... - → Varje ingredient × scale (entry.servings / recipe.servings, eller 1 om ej satt) - → Aggregerat per produkt + enhet +Inköpslista genereras + → GET /api/meal-plan/shopping-list?from=...&to=... + → Varje ingredient × scale (entry.servings / recipe.servings, eller 1 om ej satt) + → Aggregerat per produkt + enhet -Inventariejämförelse - → GET /api/meal-plan/inventory-compare?from=...&to=... - → Samma aggregering, sedan jämförs mot aktuell inventarie - → Status: räcker | saknas | enhetskonflikt +Inventariejämförelse + → GET /api/meal-plan/inventory-compare?from=...&to=... + → Samma aggregering, sedan jämförs mot aktuell inventarie + → Status: räcker | saknas | enhetskonflikt ``` ### Frontend: MealPlanClient - Veckovy renderar en kolumn per dag med aktuellt recept -- Om receptet har `servings` satt visas ett portionsinmatningsfält direkt i dagsvyn -- Avviker inmatat portionsantal från receptets grundvärde visas en återställningsknapp (↩ N portioner) +- Om receptet har `servings` satt visas ett portionsinmatningsfält direkt i dagsvyn +- Avviker inmatat portionsantal frÃ¥n receptets grundvärde visas en Ã¥terställningsknapp (↩ N portioner) - `handleServingsChange()` POSTar direkt till backend och uppdaterar lokal state utan sidomladdning ### Portionsskalning i backend @@ -1279,21 +1385,21 @@ const scale = recipeServings && entryServings ? entryServings / recipeServings : 1; -// Exempel: recept för 4, vill laga 6 → scale = 1.5 -// 200 g pasta → 300 g pasta i inköpslistan +// Exempel: recept för 4, vill laga 6 → scale = 1.5 +// 200 g pasta → 300 g pasta i inköpslistan ``` --- ## Enhetskonvertering (backendsida) -### Stödda enhetstyper +### 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 | +| **Portioner** | tsk, msk | tesked (tsk), där 1 msk = 3 tsk | | **Stycken** | st | kan inte konverteras | ### Normalisering (inom `RecipesService.normalizeUnit()`) @@ -1314,27 +1420,27 @@ const scale = recipeServings && entryServings ```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 + // 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()` +### Användning i `getInventoryPreview()` -Då receptjämförelse jämförs mot inventarie: +DÃ¥ receptjämförelse jämförs mot inventarie: ``` -För varje ingrediens: - 1. Hämta alla inventarieföremål för denna produkt +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 + - 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 + 5. Returnera status: räcker | saknas | enhetskonflikt ``` --- @@ -1344,17 +1450,17 @@ För varje ingrediens: ### 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 +- `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) +- External `proxy` network (för Caddy integrering, valfritt) - Intern `recipe-network` mellan services ### Backend-Dockerfile (3-stage build) @@ -1364,7 +1470,7 @@ För varje ingrediens: ```dockerfile FROM node:22-alpine AS builder # Installera backend-deps -# Kopiera converter från stage 1 +# Kopiera converter frÃ¥n stage 1 # Generera Prisma-klient # Bygg NestJS-appen (nest build) ``` @@ -1385,13 +1491,13 @@ docker compose up -d recipe-api ### Frontend-Dockerfile -Standard Next.js build → standalone output +Standard Next.js build → standalone output -### Miljövariabler +### Miljövariabler Konfigureras via `.env` eller `docker compose up`: -- `DATABASE_URL` — MariaDB-anslutning (backend) -- `PORT` — Backend port (default 8080) +- `DATABASE_URL` — MariaDB-anslutning (backend) +- `PORT` — Backend port (default 8080) - Ev. Caddy-konfiguration --- @@ -1399,27 +1505,27 @@ Konfigureras via `.env` eller `docker compose up`: ## UTF-8 och lokalisering - Database: utf8mb4 -- Backend: Normaliseringsfunktion hanterar åäö +- Backend: Normaliseringsfunktion hanterar åäö - Frontend: Svenska felmeddelanden och UI-text -- Endpoints: Svenska benämningar och kategori-namn +- Endpoints: Svenska benämningar och kategori-namn --- -## Säkerhet & Utbyggbarhet +## Säkerhet & Utbyggbarhet -- **Autentisering:** JWT-baserad, 7 dagars token. Auth.js v5 (Credentials provider) i frontend. Alla backend-routes skyddas av `JwtAuthGuard` — öppna endpoints markeras med `@Public()`. -- **Middleware:** `middleware.ts` skyddar alla Next.js-routes utom `/login`, `/register` och `/api/auth`. Oinloggade användare omdirigeras automatiskt. -- **Validering:** Alla DTO:er valideras med `class-validator`. Inkommande fält i server actions bör kompletteringsvalideras (se teknisk skuld E). -- **Felhantering:** `GlobalExceptionFilter` fångar alla oupphanterade fel och returnerar svenska felmeddelanden. -- **CORS:** API-anrop proxias via Next.js API routes — klientkod når aldrig backenden direkt. -- **Filuppladdning:** Multer med `memoryStorage` och MIME-typvalidering; max 15 MB för kvittoimport. +- **Autentisering:** JWT-baserad, 7 dagars token. Auth.js v5 (Credentials provider) i frontend. Alla backend-routes skyddas av `JwtAuthGuard` — öppna endpoints markeras med `@Public()`. +- **Middleware:** `middleware.ts` skyddar alla Next.js-routes utom `/login`, `/register` och `/api/auth`. Oinloggade användare omdirigeras automatiskt. +- **Validering:** Alla DTO:er valideras med `class-validator`. Inkommande fält i server actions bör kompletteringsvalideras (se teknisk skuld E). +- **Felhantering:** `GlobalExceptionFilter` fÃ¥ngar alla oupphanterade fel och returnerar svenska felmeddelanden. +- **CORS:** API-anrop proxias via Next.js API routes — klientkod nÃ¥r aldrig backenden direkt. +- **Filuppladdning:** Multer med `memoryStorage` och MIME-typvalidering; max 15 MB för kvittoimport. -### Möjliga utbyggnader -- Användarroller (user / admin) — rollbaserad guard, skyddade admin-routes +### Möjliga utbyggnader +- Användarroller (user / admin) — rollbaserad guard, skyddade admin-routes - Delade recept / recept-export -- Push-notifieringar för utgångna varor -- Nutrition-baserat receptförslag -- Allergi-tracking per användare +- Push-notifieringar för utgÃ¥ngna varor +- Nutrition-baserat receptförslag +- Allergi-tracking per användare --- @@ -1427,89 +1533,89 @@ Konfigureras via `.env` eller `docker compose up`: ### Databaskonfiguration -**MariaDB 11** hostas i Docker-container (`recipe-db`) enligt `compose.yml`. Databasen använder: -- **Teckenuppsättning:** UTF-8 (utf8mb4) för korrekt stöd för svenska tecken -- **Sortering:** utf8mb4_swedish_ci för svensk alfabetisk sortering -- **Anslutning:** Via Prisma ORM från backend-tjänsten +**MariaDB 11** hostas i Docker-container (`recipe-db`) enligt `compose.yml`. Databasen använder: +- **Teckenuppsättning:** UTF-8 (utf8mb4) för korrekt stöd för svenska tecken +- **Sortering:** utf8mb4_swedish_ci för svensk alfabetisk sortering +- **Anslutning:** Via Prisma ORM frÃ¥n backend-tjänsten **Initialisering:** -1. `db/init/001-init.sql` körs vid första container-start (grundläggande tablestatus) -2. Prisma migrations körs automatiskt vid backend-start -3. `db/seeds/seed_all.sql` körs manuellt för att fylla på initiala kategorier och produkter +1. `db/init/001-init.sql` körs vid första container-start (grundläggande tablestatus) +2. Prisma migrations körs automatiskt vid backend-start +3. `db/seeds/seed_all.sql` körs manuellt för att fylla pÃ¥ initiala kategorier och produkter -### seed_all.sql — Kategori- och produktdatabas +### seed_all.sql — Kategori- och produktdatabas **Filplats:** `db/seeds/seed_all.sql` **Syfte:** -`seed_all.sql` är ensam sanningskälla för kategoriträdet. Den innehåller: -1. **Kategoristruktur** — Hierarkiskt träd från nivå 1 (toppkategorier) till nivå 3 (underkategorier) -2. **Initiala produkter** — 200+ standardprodukter (köttfärs, ost, frukt, etc.) -3. **Kategoritilldelning** — Kopplar produkterna till rätt kategorier +`seed_all.sql` är ensam sanningskälla för kategoriträdet. Den innehÃ¥ller: +1. **Kategoristruktur** — Hierarkiskt träd frÃ¥n nivÃ¥ 1 (toppkategorier) till nivÃ¥ 3 (underkategorier) +2. **Initiala produkter** — 200+ standardprodukter (köttfärs, ost, frukt, etc.) +3. **Kategoritilldelning** — Kopplar produkterna till rätt kategorier **Struktur:** | Steg | Vad | Detaljer | |------|-----|----------| -| **RESET** | Förberedelse | `SET foreign_key_checks = 0` → `TRUNCATE TABLE Category` → `UPDATE Product SET categoryId = NULL` | -| **STEG 1** | Kategorier NIVÅ 1-3 | INSERT INTO Category för alla toppkategorier, nivå 2 och nivå 3-kategorier. Använder SELECT med JOINs för hierarkisk struktur. | -| **STEG 2** | Produkter | `INSERT IGNORE INTO Product` — Befintliga produkter hoppas över (inga dubletter). | -| **STEG 3** | Kategoritilldelning | UPDATE Product ... WHERE name IN (...) för att tilldela rätt categoryId baserat på produktnamn. | +| **RESET** | Förberedelse | `SET foreign_key_checks = 0` → `TRUNCATE TABLE Category` → `UPDATE Product SET categoryId = NULL` | +| **STEG 1** | Kategorier NIVÃ… 1-3 | INSERT INTO Category för alla toppkategorier, nivÃ¥ 2 och nivÃ¥ 3-kategorier. Använder SELECT med JOINs för hierarkisk struktur. | +| **STEG 2** | Produkter | `INSERT IGNORE INTO Product` — Befintliga produkter hoppas över (inga dubletter). | +| **STEG 3** | Kategoritilldelning | UPDATE Product ... WHERE name IN (...) för att tilldela rätt categoryId baserat pÃ¥ produktnamn. | -**Köra seed_all.sql:** +**Köra seed_all.sql:** ```bash -# Hämta lösenordet från .env och kör kommandot: +# Hämta lösenordet frÃ¥n .env och kör kommandot: DB_PASS=$(grep MARIADB_ROOT_PASSWORD .env | cut -d= -f2) docker exec recipe-db mariadb -uroot -p"$DB_PASS" recipe_app -e "SHOW TABLES;" ``` -**Viktigt:** Denna kommando kan köras **hur många gånger som helst**: -- `TRUNCATE` raderar alla befintliga kategorier (men inte produkterna själva) -- `INSERT IGNORE` förhindrar dubbletter av produkter -- Befintliga produkt-data (namn, märke, etc.) påverkas inte — endast `categoryId` uppdateras +**Viktigt:** Denna kommando kan köras **hur mÃ¥nga gÃ¥nger som helst**: +- `TRUNCATE` raderar alla befintliga kategorier (men inte produkterna själva) +- `INSERT IGNORE` förhindrar dubbletter av produkter +- Befintliga produkt-data (namn, märke, etc.) pÃ¥verkas inte — endast `categoryId` uppdateras -**Fördelar med denna design:** -- ✅ Reproducerbar — idempotent, kan köras flera gånger -- ✅ Enkel migrering — alla ändringar i kategoriträdet är i en fil -- ✅ Ingen datarisk — produkter försvinner aldrig, enbart omkategorisering -- ✅ Admin-gränssnittet kan senare modifiera kategorier interaktivt (seed_all är enbart för initial setup) +**Fördelar med denna design:** +- ✅ Reproducerbar — idempotent, kan köras flera gÃ¥nger +- ✅ Enkel migrering — alla ändringar i kategoriträdet är i en fil +- ✅ Ingen datarisk — produkter försvinner aldrig, enbart omkategorisering +- ✅ Admin-gränssnittet kan senare modifiera kategorier interaktivt (seed_all är enbart för initial setup) ### Kategoristruktur -**Nivå 1 (Toppkategorier, exempel):** -- Bröd & Kakor +**NivÃ¥ 1 (Toppkategorier, exempel):** +- Bröd & Kakor - Dryck -- Färdigmat +- Färdigmat - Fryst -- Frukt & Grönt +- Frukt & Grönt - Glass, godis & snacks -- Kött, chark & fågel -- Mejeri, ost & ägg +- Kött, chark & fÃ¥gel +- Mejeri, ost & ägg - Skafferi - Fisk & Skaldjur - Vegetariskt -**Nivå 2 (Underkategorier, exempel):** -- Bröd & Kakor > Bröd, Matbröd, Rostbröd,Fastfoodbröd, Kex & Kakor, Knäckebröd & Skorpor -- Mejeri, ost & ägg > Ost, Allergi mejeri, Mjölk, Filmjölk & Yoghurt, Matlagning, Smör/margarin & jäst, Havre-/Soja-/Risdryck, Kvarg & Cottage cheese -- Frukt & Grönt > Grönsaker +**NivÃ¥ 2 (Underkategorier, exempel):** +- Bröd & Kakor > Bröd, Matbröd, Rostbröd,Fastfoodbröd, Kex & Kakor, Knäckebröd & Skorpor +- Mejeri, ost & ägg > Ost, Allergi mejeri, Mjölk, Filmjölk & Yoghurt, Matlagning, Smör/margarin & jäst, Havre-/Soja-/Risdryck, Kvarg & Cottage cheese +- Frukt & Grönt > Grönsaker -**Nivå 3 (Specifika kategorier, exempel):** -- Bröd & Kakor > Bröd > Matbröd, Rostbröd -- Mejeri, ost & ägg > Mjölk > Standard mjölk -- Frukt & Grönt > Grönsaker > Sallad & Kål +**NivÃ¥ 3 (Specifika kategorier, exempel):** +- Bröd & Kakor > Bröd > Matbröd, Rostbröd +- Mejeri, ost & ägg > Mjölk > Standard mjölk +- Frukt & Grönt > Grönsaker > Sallad & KÃ¥l ### Uppdatering av kategorier -Nya kategorier läggs till direkt i `db/seeds/seed_all.sql`. Efter ändringar: +Nya kategorier läggs till direkt i `db/seeds/seed_all.sql`. Efter ändringar: ```bash -# Kör seed_all.sql igen för att uppdatera kategoriträdet +# Kör seed_all.sql igen för att uppdatera kategoriträdet docker exec -i recipe-db mariadb -uroot -p"$DB_PASS" recipe_app < db/seeds/seed_all.sql ``` -Produkterna behålls och omkategoriseras enligt de nya UPDATE-satserna. +Produkterna behÃ¥lls och omkategoriseras enligt de nya UPDATE-satserna. --- @@ -1517,52 +1623,53 @@ Produkterna behålls och omkategoriseras enligt de nya UPDATE-satserna. ## Microservice Importer -Recipe App har ett **companion-projekt** för receptimport: [`microservice-importer`](../microservice-importer/) +Recipe App har ett **companion-projekt** för receptimport: [`microservice-importer`](../microservice-importer/) -### Nuläge -Quick-import-funktionen är för närvarande **integrerad i Recipe App** med full funktionalitet. +### 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: +### 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) +- Lägre komplexitet (ingen databaskonfiguration) - Docker kontainer -Se [microservice-importer README](../microservice-importer/README.md) för komplett dokumentation och deployment-instruktioner när separation blir aktuell. +Se [microservice-importer README](../microservice-importer/README.md) för komplett dokumentation och deployment-instruktioner när separation blir aktuell. -### Arkitektur för Microservice Importer -Se ovan samt nedan: Gemensam arkitektur för companion-projekt +### Arkitektur för Microservice Importer +Se ovan samt nedan: Gemensam arkitektur för companion-projekt ## Microservice Shopping/Todo-lista -En möjlig framtida utveckling är ett separat companion-projekt till Recipe App i form av en **standalone microservice för shopping/todo-lista**. Denna tjänst ska: +En möjlig framtida utveckling är ett separat companion-projekt till Recipe App i form av en **standalone microservice för shopping/todo-lista**. Denna tjänst ska: - Vara helt oberoende av Recipe App, men kunna interagera med den -- Erbjuda sömlös integrering mellan sig själv och Recipe App -- Inte kräva någon avancerad databaskonfiguration, utan endast använda en enkel filbaserad databaslösning inom appen +- Erbjuda sömlös integrering mellan sig själv och Recipe App +- Inte kräva nÃ¥gon avancerad databaskonfiguration, utan endast använda en enkel filbaserad databaslösning inom appen - Docker kontainer -Syftet är att möjliggöra delad eller fristående användning av shopping/todo-listor, med enkel integration för användare som vill koppla ihop funktionaliteten med Recipe App. +Syftet är att möjliggöra delad eller fristÃ¥ende användning av shopping/todo-listor, med enkel integration för användare som vill koppla ihop funktionaliteten med Recipe App. -### Arkitektur för Microservice Shopping/Todo-lista -Se ovan samt nedan: Gemensam arkitektur för companion-projekt +### Arkitektur för Microservice Shopping/Todo-lista +Se ovan samt nedan: Gemensam arkitektur för companion-projekt -## Gemensam arkitektur för companion-projekten +## Gemensam arkitektur för companion-projekten **Databasval:** -Companion-projekten ska använda SQLite som databas, eftersom det möjliggör återanvändning av datamodell och queries mellan backend och native app (iOS/Android). SQLite stöds direkt i Node.js, iOS och Android och gör det enkelt att dela databasstruktur och logik mellan olika plattformar. -**Gemensam teknisk grund och säkerhet:** -Både microservice-importer och companion-projektet för shopping/todo-lista ska dela teknisk grund med recipe-app, särskilt vad gäller säkerhetslösningar och API-hantering. Detta innebär: -- JWT-baserad autentisering och rollhantering, med tokens som används för att skydda API-endpoints +Companion-projekten ska använda SQLite som databas, eftersom det möjliggör Ã¥teranvändning av datamodell och queries mellan backend och native app (iOS/Android). SQLite stöds direkt i Node.js, iOS och Android och gör det enkelt att dela databasstruktur och logik mellan olika plattformar. +**Gemensam teknisk grund och säkerhet:** +BÃ¥de microservice-importer och companion-projektet för shopping/todo-lista ska dela teknisk grund med recipe-app, särskilt vad gäller säkerhetslösningar och API-hantering. Detta innebär: +- JWT-baserad autentisering och rollhantering, med tokens som används för att skydda API-endpoints - API-design enligt REST-principer, med tydlig separation mellan autentiserade och publika endpoints -- Proxy-lösning (Caddy) för att styra trafik och säkerställa att rätt endpoints skyddas -- Återanvändning av auth-middleware och utility-funktioner för tokenhantering +- Proxy-lösning (Caddy) för att styra trafik och säkerställa att rätt endpoints skyddas +- Ã…teranvändning av auth-middleware och utility-funktioner för tokenhantering - Felhantering och svar i JSON-format -I recipe-app används Auth.js v5 (NextAuth) för autentisering, JWT-sessioner, och globala guards i backend (NestJS) för att skydda alla API-routes. Frontend och backend kommunicerar via tydliga API-routes, och klientkomponenter använder utility-funktioner för att alltid skicka med rätt auth-token. Samma principer och kodmönster ska tillämpas i båda microservices för enhetlig säkerhet och API-hantering. +I recipe-app används Auth.js v5 (NextAuth) för autentisering, JWT-sessioner, och globala guards i backend (NestJS) för att skydda alla API-routes. Frontend och backend kommunicerar via tydliga API-routes, och klientkomponenter använder utility-funktioner för att alltid skicka med rätt auth-token. Samma principer och kodmönster ska tillämpas i bÃ¥da microservices för enhetlig säkerhet och API-hantering. **Databasval:** -Microservice-importer ska använda SQLite som databas, av samma skäl som companion-projektet för shopping/todo-lista: enkel filbaserad setup, portabilitet och möjlighet att återanvända datamodell och queries mellan backend och native app (iOS/Android). +Microservice-importer ska använda SQLite som databas, av samma skäl som companion-projektet för shopping/todo-lista: enkel filbaserad setup, portabilitet och möjlighet att Ã¥teranvända datamodell och queries mellan backend och native app (iOS/Android). + +--- ---- \ No newline at end of file diff --git a/next_steps_flutter.md b/next_steps_flutter.md index 0c8c0b9f..99dcd368 100644 --- a/next_steps_flutter.md +++ b/next_steps_flutter.md @@ -32,39 +32,40 @@ Adminfloden migreras efter att ovanstaende ar verifierat. ## Prioriterad plan (ordning) -## Fas 0 - Backend-forarbete for user-scope (ny) -- Gor `PantryItem` user-scopad (userId + productId unik per anvandare). -- Gor matplan user-scopad och filtrera list/upsert/delete per inloggad anvandare. -- Uppdatera matplanens inventory-jamforelse till anvandarspecifikt pantry. -- Publicera uppdaterade API-kontrakt innan vidare Flutter-parity for matplan/baslager. +## Fas 0 - Backend-forarbete for user-scope (KLAR 2026-04-22) +- [x] Gor `PantryItem` user-scopad (userId + productId unik per anvandare). +- [x] Gor matplan user-scopad och filtrera list/upsert/delete per inloggad anvandare. +- [x] Uppdatera matplanens inventory-jamforelse till anvandarspecifikt pantry. +- [x] Publicera uppdaterade API-kontrakt innan vidare Flutter-parity for matplan/baslager. +- [x] Migration 20260422130000_user_scope_pantry_meal_plan applicerad. -## Fas 1 - Stabil app-shell (forst) -- Bygg tydlig auth-gate i router. -- Centralisera API-fel (401/403/500) i ett gemensamt lager. -- Skapa gemensamma UI-komponenter for loading, empty, error. -- Satt en enhetlig navigationsstruktur (web forst, mobil-redo). +## Fas 1 - Stabil app-shell (KLAR 2026-04-22) +- [x] Bygg tydlig auth-gate i router. +- [x] Centralisera API-fel (401/403/500) i ett gemensamt lager (`mapErrorToUserMessage`). +- [x] Skapa gemensamma UI-komponenter for loading, empty, error. +- [x] Satt en enhetlig navigationsstruktur (web forst, mobil-redo). +- [x] Lokalisering: ARB-infrastruktur pa plats (`flutter_localizations`, `l10n.yaml`, `app_sv.arb`, `synthetic-package: false`, `flutter gen-l10n` i Dockerfile). +- [x] Regressionstest for svenska strangkvalitet tillagd. -Motivering: minskar regressionsrisk och gor resten av migreringen snabbare. +## Fas 2 - Auth parity (KLAR 2026-04-22) +- [x] Hardna loginflodet (tydliga felmeddelanden, retries dar relevant). +- [x] Verifiera token-livscykel (reload/hard refresh/logout). +- [x] Implementera automatisk hantering av utgangen token (401 -> logout -> login). -## Fas 2 - Auth parity -- Hardna loginflodet (tydliga felmeddelanden, retries dar relevant). -- Verifiera token-livscykel (reload/hard refresh/logout). -- Implementera automatisk hantering av utgangen token (401 -> logout -> login). +## Fas 3 - Recept parity (KLAR 2026-04-22) +- [x] Lista -> detalj -> skapa -> redigera -> ta bort. +- [x] Knyt ihop med parse-markdown-proxy. +- [x] Behall backend som enda plats for matchning, validering och affarslogik. -## Fas 3 - Recept parity -- Lista -> detalj -> skapa -> redigera -> ta bort. -- Knyt ihop med parse-markdown-proxy. -- Behall backend som enda plats for matchning, validering och affarslogik. +## Fas 4 - Inventarie parity (KLAR 2026-04-22) +- [x] Lista med filter/sortering (plats + sort via Riverpod-querystate). +- [x] Skapa och uppdatera inventariepost. +- [x] Konsumtion och konsumtionshistorik. -## Fas 4 - Inventarie parity -- Lista med filter/sortering. -- Skapa och uppdatera inventariepost. -- Konsumtion och konsumtionshistorik. - -## Fas 5 - Matplan parity -- Veckovy med receptval per dag. +## Fas 5 - Matplan parity (NASTA) +- Veckovy med receptval per dag mot nu user-scopat `GET /api/meal-plan?from=&to=`. - Portionsjustering per dag. -- Inkoplista och inventariejamforelse. +- Inkoplista och inventariejamforelse mot anvandarens pantry. ## Fas 6 - Import parity - URL/PDF/bild via befintliga endpoints. @@ -103,10 +104,11 @@ En feature ar klar nar allt nedan ar uppfyllt: ## Nasta konkreta sprint (rekommenderad) -1. Fas 0: backend-andringar for user-scope i pantry/matplan. -2. Fas 1: app-shell hardening. -3. Fas 2: auth parity helt klar. -4. Smoke-test pa testdomanen och avstamning. +1. Fas 5: Matplan parity mot user-scopat API (veckovy, inkoplista, inventariejamforelse). +2. Fa 6: Import parity. +3. Fas 7: Profil/admin parity. +4. Fortatt flytt av UI-strangar till ARB (inventarie, pantry, recept). +5. Smoke-test pa testdomanen och avstamning. ## Tumregel diff --git a/teknisk_beskrivning_flutter.md b/teknisk_beskrivning_flutter.md index b0116d57..d0692757 100644 --- a/teknisk_beskrivning_flutter.md +++ b/teknisk_beskrivning_flutter.md @@ -23,6 +23,23 @@ Relaterade dokument: - Exponering via extern Caddy med site `test.gynther.se` -> `recipe-flutter:5000`. - API anrop gar same-origin via `/api` och proxas internt till `recipe-api:8080`. +### Inventarie (2026-04-22) +- Filtrering per plats (alla/kyl/frys/skafferi) via `inventoryLocationFilterProvider`. +- Sortering (namn A-O, bast fore stigande/fallande) via `inventorySortFilterProvider`. +- Riverpod-query (`InventoryQuery`) skickar `location` och `sort` som queryparametrar till backenden. +- Alla felmeddelanden gar via `mapErrorToUserMessage(error, context)`. + +### Baslager/Pantry (2026-04-22) +- Pantry-produkter grupperas nu pa kategori utifrån backend-relationen `categoryRef` (rekursiv `parent`-kedja -> `categoryPath`), med fallback till legacy `product.category` och sist `Ovrigt`. +- `PantryProduct` har `categoryId` och `categoryPath` som parsas fran API-svaret. + +### Backend: user-scope for pantry och matplan (2026-04-22) +- `PantryItem` och `MealPlanEntry` ar nu user-scopade i Prisma-schemat. +- `pantry`-controller/service filtrerar alltid per `userId` fran JWT. +- `meal-plan`-controller/service filtrerar alltid per `userId`; `inventoryCompare` anvander inloggad anvandares pantry. +- Migration: `20260422130000_user_scope_pantry_meal_plan` applicerad pa server. +- Next.js-frontenden kravde inga funktionella andringar (forfrågningar gar redan via auth-proxy). + ## Arkitektur ### Lager - Presentation: skarmar och widgets i `flutter/lib/features/*/presentation`. @@ -55,7 +72,25 @@ Relaterade dokument: - `ApiClient` i `flutter/lib/core/api/api_client.dart` exponerar: `getJson`, `postJson`, `patchJson`, `putJson`, `deleteJson`. - Centralicerad HTTP-felklassning: 401 -> `ApiErrorType.unauthorized`, 403 -> `forbidden`, 5xx -> `server`, natverksfel -> `network`. - `ApiException` i `flutter/lib/core/api/api_exception.dart` ar den enda feltypen som propageras fran repositories. -- `mapErrorToUserMessage()` i `flutter/lib/core/api/api_error_mapper.dart` oversatter fel till svenska anvandarmedddelanden. +- `mapErrorToUserMessage(error, context)` i `flutter/lib/core/api/api_error_mapper.dart` oversatter fel till lokaliserade anvandarmedddelanden. Tar numera `BuildContext` som andra argument for att hämta korrekt sprak fran `AppLocalizations`. + +### Lokalisering (2026-04-22) +- Flutter `flutter_localizations` + `intl` tillagda i `pubspec.yaml` med `generate: true`. +- Konfigurationsfil: [flutter/l10n.yaml](flutter/l10n.yaml) med `synthetic-package: false`. +- Kallstrangar i [flutter/lib/l10n/app_sv.arb](flutter/lib/l10n/app_sv.arb) och [flutter/lib/l10n/app_en.arb](flutter/lib/l10n/app_en.arb). +- Generade filer hamnar i `flutter/lib/l10n/generated/` och checkas inte in. +- Hjalpare `context.l10n` i [flutter/lib/core/l10n/l10n.dart](flutter/lib/core/l10n/l10n.dart). +- `MaterialApp.router` konfigurerad med `localizationsDelegates`, `supportedLocales` och `locale: const Locale('sv')`. +- Dockerfilen kor `flutter gen-l10n` innan `flutter build web` for att generera Dart-koden i containerbygget. +- Regressionstest i [flutter/test/core/swedish_strings_regression_test.dart](flutter/test/core/swedish_strings_regression_test.dart) failar om vanliga ASCII-varianter (Valj, Lagg, Forsok, etc.) dyker upp igen i `lib/`. + +#### Anvandning av lokalisering +For att lagga till en ny lokaliserad strang: +1. Lagg till nyckeln i `app_sv.arb` (och `app_en.arb`). +2. Kör `flutter gen-l10n` lokalt (eller lat Docker-bygget gora det). +3. Anvand `context.l10n.dinNyckel` i widgetkoden. + +For felstrangar fran API: anvand alltid `mapErrorToUserMessage(error, context)` — lagg inte in hardkodade strängar. ### Recipes - `GET /api/recipes` — receptlista. @@ -145,7 +180,7 @@ Om `test.gynther.se` slutar svara efter städning med `--remove-orphans`, starta ## Nasta tekniska steg Fortsatt migrering enligt prioritering i [next_steps_flutter.md](next_steps_flutter.md): -1. Auth parity (session behavior refinements). -2. Recipes parity (list -> detail -> create/edit). -3. Inventory and meal plan pages. -4. Profile/admin parity. +1. Fortatt lokalisering av inventarie/pantry/recept-strangar till ARB. +2. Matplan parity mot nu user-scopat API. +3. Import parity. +4. Profil/admin parity.