# Nyheter och forbattringar (2026-05-20)

- **Deploy-flode forenklat:** `deploy.sh` har kvar `--clean-database` men separat `--migrate` ar borttagen for att undvika redundant migreringslogik.
- **Migrering i runtime-miljo:** Vid `--clean-database` kor `deploy.sh` alltid `npx prisma migrate deploy --schema prisma/schema.prisma` i `recipe-api` efter att backend ar redo.
- **Databasrensning utan kategoriforlust:** Efter explicit migrering kor `--clean-database` `backend/prisma/maintenance/clean-database.sql` mot `recipe-db` och bevarar `Category`/anvandare.
- **Prisma Client-output i deploy-logg:** `deploy.sh` visar output fran `npx prisma generate` i terminalen sa att schema/client-status och eventuella versionsnotiser blir synliga direkt.
- **Specialfil for underhall:** Ny fil `backend/prisma/maintenance/clean-database.sql` ar avsedd att vara permanent och uppdateras nar schema/tabeller forandras.
- **Hotfix efter produktionstest:** SQL-filen korrigerad till faktiska Prisma-tabeller (`ShoppingListItem`, `InventoryConsumption`, `MealPlanEntry`, `RecipeShare`, `UserProduct`, m.fl.) och felaktiga tabeller (`ShoppingList`, `InventoryTransaction`, `MealPlanItem`) borttagna.
- **Operativ erfarenhet:** Forsta korningen misslyckade pa server med `ERROR 1146 ... ShoppingList doesn't exist`; fixen ar incheckad och pushad for robust korning i varierande databaslagen.

# Nyheter och förbättringar (2026-05-19)

- **Flyer-session API i recipe-api:** `flyer-import.controller.ts` exponerar `GET /api/flyer-import/sessions/latest` och `GET /api/flyer-import/sessions/:sessionId`.
- **Sessionmappning i service-lager:** `toFlyerImportResponseFromSession(...)` samlar mappning av session + items till standardiserat API-svar.
- **HTTP-semantik:** Saknad session returnerar `NotFoundException` (404), vilket förenklar klientlogik och observability.
- **Query-optimering:** `getLatestSession(...)` använder en enda Prisma-fråga med `include: items` i stället för dubbelhämtning.
- **Flutter-cache-strategi:** Klienten persisterar endast metadata (`sessionId`, filnamn, valda rader) och hämtar full data från backend vid hydrering.
- **Teststatus:** `flyer-import.service.spec.ts` verifierar not-found, ägd session och tom latest-session (3/3 gröna).

# Migrering: Import-funktion → microservice-importer (GENOMFÖRD 2026-04-30)

Recipe-apps importflöde (quick-import, parse-markdown, receipt-import) är nu migrerat till en separat microservice-importer:
- All URL-skrapning, OCR, PDF-parsning och AI-kvittoparsning sker i microservice-importer (NestJS, Docker, port 3001 internt).
- Backend-till-backend-integration: recipe-app anropar microservice-importer via HTTP, frontend är oförändrad.
- OCR via tesseract.js och Alpine-paket.
- Compose-länkning och healthchecks på plats.
- Bildoptimering sker fortfarande i recipe-app vid sparande.
- Lessons learned: Separat import-microservice ger enklare scaling, robustare deploy och tydligare ansvarsfördelning.

Verifiering:
- Alla endpoints testade (ICA-URL, PDF, bild, markdown, kvitto) och fungerar.
- Auth och proxy-routes oförändrade.

Se även: README.md för användarflöde, och AI-FUNKTIONER.md för AI-detaljer.

# Prisma-migreringar: P3009 recovery och lessons learned

# Nyheter och förbättringar (2026-05-18)

- **Flyerimport intern i recipe-api:** `/api/flyer-import/parse` använder nu en intern pipeline i backend (`TextExtractorService` + `AiFlyerParserService` + `FlyerNormalizerService`) och är inte längre beroende av `importer-api`.
- **Textutvinning för flyer:** PDF tolkas primärt med `pdf-parse`; vid bildfiler eller skannad PDF-fallback används OCR via `tesseract.js`.
- **AI-parse för flyer:** Mistral Tiny används för strukturerad extraktion av flyer-rader till JSON, följt av normalisering av pris/enhet/kategori.
- **Timeout/retry-härdning för flyer-AI:** `AiFlyerParserService` har konfigurerbar timeout (`FLYER_AI_TIMEOUT_MS`, default 30000 ms) och retry med successivt kortare textfönster (`FLYER_AI_RETRIES`, default 2) för att minska 503 vid långsamma modellanrop.

- **Backend linting i CI:** ESLint är infört för backend (`backend/eslint.config.mjs`, `npm run lint`) och körs i `.github/workflows/test.yml`.
- **Flutter lint-konfiguration:** `flutter/analysis_options.yaml` är tillagd och inkluderar `package:flutter_lints/flutter.yaml`.
- **Prisma query logging (miljöstyrd):** `PrismaService` konfigurerar loggnivåer via env-variabeln `PRISMA_LOG_QUERIES`.
- **Runtime-konfiguration:** `compose.yml` exponerar `PRISMA_LOG_QUERIES` till `recipe-api` med default `0`.
- **Aktivering i testmiljö:** Sätt `PRISMA_LOG_QUERIES=1` och starta om `recipe-api` för att få SQL query-loggar.
- **Verifierad testjustering:** `receipt-import.security.spec.ts` validerar nu `UnauthorizedException` vid saknat användar-id i `upsertUnitMapping`.

# Drift och deploy (2026-05-11)

- **Flutter build-artifacts:** Byggda filer i `flutter/build/` och `.flutter-plugins-dependencies` ska inte versionshanteras. Vid deploy på server: kör `git restore flutter/build flutter/.flutter-plugins-dependencies` och `git clean -fd flutter/build` innan `git pull`.
- **Vanliga fel:** Om du får felmeddelandet "Your local changes to the following files would be overwritten by merge", beror det på att genererade filer är modifierade lokalt. Se till att alltid rensa dessa innan uppdatering.

**Problem:**
Prisma migrationer kan fastna i failed state (P3009) om en migration körts med fel SQL-citering (t.ex. "User" istället för `User` i MySQL) eller om deploy avbryts mitt i en migrering.

**Symptom:**
```
migrate found failed migrations in the target database, new migrations will not be applied. The `20260506144000_add_ai_engine_enabled` migration ... failed
```

**Lösning/playbook:**
1. Rätta migrationsfilen så att den använder backticks (`) för tabell- och kolumnnamn (MySQL-stil).
2. Kör:
  ```bash
  docker exec recipe-api sh -lc "cd /app && npx prisma migrate resolve --rolled-back 20260506144000_add_ai_engine_enabled --schema prisma/schema.prisma"
  docker exec recipe-api sh -lc "cd /app && npx prisma migrate deploy --schema prisma/schema.prisma"
  ```
3. Om deploy klagar på duplicate column (dvs kolumnen finns redan):
  ```bash
  docker exec recipe-api sh -lc "cd /app && npx prisma migrate resolve --applied 20260506144000_add_ai_engine_enabled --schema prisma/schema.prisma"
  docker exec recipe-api sh -lc "cd /app && npx prisma migrate deploy --schema prisma/schema.prisma"
  ```
4. Verifiera status:
  ```bash
  docker exec recipe-api sh -lc "cd /app && npx prisma migrate status --schema prisma/schema.prisma"
  ```

**Lessons learned:**
- Kontrollera alltid SQL-citering i migrationsfiler (MySQL kräver backticks, inte dubbla citattecken).
- Vid P3009: använd `migrate resolve` för att markera migrationen som rolled-back eller applied beroende på DB-läge.
- Kör alltid migrationskommandon i rätt container/miljö för att undvika env- och version-mismatch.

# Session 2026-05-06: User-scoped AI-fallback, admin-toggles och refaktor

Denna session har genomfört en omfattande refaktor och utbyggnad av AI-funktionalitet i Recipe App, med fokus på:

- **User-scoped AI-fallback:** AI-förslag för ingrediensmatchning (receptimport) och kategorimatchning (kvittoimport) är nu individuellt aktiverbara per användare. Endast användare med premium-tillgång (adminstyrd toggle) får AI-hints vid import.
- **Admin-toggles:** Ny endpoint och backendlogik för att admin ska kunna slå på/av AI-funktioner per användare. Prisma-schema utökat med `aiEngineEnabled` på User, migration och endpoint (`PATCH /users/:id/ai-engine`).
- **Premium-scope:** Flutter och backend respekterar nu premium-flagga och AI-tillgång i alla relevanta flöden. Endast premiumanvändare får AI-förslag i UI.
- **Rematch och manuell produkt:** Flutter har fått stöd för att trigga ommatchning (rematch) av ingredienser och för att skapa produkter manuellt direkt från importgranskningen.
- **Arkitektur:** All AI-fallback är fallback-first och aldrig blockerande för import eller sparande. Backend och frontend är strikt separerade vad gäller analys, import och matchning.
- **Lessons learned:**
  - Nullable propagation i Prisma och DTO:er kräver noggrannhet för att undvika runtime-fel.
  - Fallback-first AI och tydlig separation av analyskontrakt ger robustare flöden och bättre UX.
  - User-scoped features kräver att all produkt- och matchningslogik filtrerar på ownerId/userId.
  - Admin-toggles och premium-scope måste vara synkade i backend, UI och auth.
  - Manuella migrationer kan krävas vid DB-connectivity-problem (se migrering-MSI.md).

Se även:
- [AI-FUNKTIONER.md](_archive/microservice-ai/AI-FUNKTIONER.md) för detaljerad AI-översikt och modellval.
- [RECIPE_IMPORT_REFACTOR_PLAN.md](_archive/docs/RECIPE_IMPORT_REFACTOR_PLAN.md) för fullständig refaktorplan.
- [NEXT_STEPS.md](NEXT_STEPS.md) för roadmap och prioriteringar.
# Nyheter och förbättringar (2026-05-10)

- **Admin-inventarie:** Backend och Flutter har nu fullständigt stöd för admin att hantera, filtrera, sortera, skapa, redigera, slå ihop och ta bort poster i den globala inventarietabellen. Endast admin har tillgång till dessa endpoints och UI-flöden.
- **User-scope och IDOR-skydd:** Inventory och produkter är nu strikt user-scopade. Alla operationer kräver och filtrerar på userId. Tester verifierar att åtkomst nekas vid försök till IDOR.
- **Säkerhetshärdning:** DTO-validering, guard-ordning, logging, throttling, merge abuse-skydd, och rollbaserad access är implementerat och testat.
- **Optimeringar:** DRY i service-lager, striktare query parsing, preview-cache, API-cleanup, och kodduplication eliminerad.
- **Testtäckning:** Utökade enhets-, integrations- och säkerhetstester för alla kritiska flöden.

# 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 [_archive/microservice-ai/AI-FUNKTIONER.md](_archive/microservice-ai/AI-FUNKTIONER.md) fÃ¶r planerade AI-funktioner och modellval.

### Ytterligare förbättringar (2026-05-11)

- **Produktomkategorisering (user-scope):**
  - Ny endpoint `PATCH /products/mine/:id/category` för att användare ska kunna omkategorisera sina egna produkter (ej globala).
  - Backend-policy: endast produkter märkta med `isPrivate: true` och där `ownerId === userId` kan omkategoriseras. Globala produkter är låsta för ändring.
  - DTO-validering och ParseIntPipe skyddar mot ogiltiga id:n och payloads.
  - Service och controller har utökats med tydliga undantag och felhantering.

- **Säkerhetshärdning och testtäckning:**
  - Forbidden-meddelanden från backend mappas nu via allowlist i Flutter för att undvika informationsläckage.
  - Utökad testtäckning: service-tester, controller-tester och HTTP endpoint-tester för alla grenar (inklusive ogiltig path-param, forbidden, valid/invalid payload).
  - Testerna verifierar att globala produkter inte kan ändras, att endast ägaren kan omkategorisera, och att felaktiga anrop returnerar rätt statuskod.
  - Testerna körs automatiskt i CI/CD och valideras även efter `npm ci` (lockfile-baserad installation).

- **Flutter/Frontend:**
  - Produktpicker och inventory-flöden har förbättrats: picker kan öppnas även om listan är tom, och det går att skapa produkt direkt från picker.
  - Omkategorisering av produkt sker nu automatiskt vid kategoriändring i inventory-edit/create.
  - Deduplicerad logik för produktmutationer i helper-fil.
  - Error-mapping i Flutter tillåter nu backend-forbidden-meddelanden på allowlist, annars visas generiskt fel.

- **Seed-data och deploy:**
  - Nya kategorier tillagda i seed (t.ex. `Marmelad`, `Sylt`, `Mos`, `Korvbröd`, `Grädde`).
  - Deploy-scriptet kör seed automatiskt och verifierar att kategoriträdet är uppdaterat.
  - Dokumentation för hur man verifierar seed och hanterar merge-konflikter vid deploy.

- **Kodkvalitet och serverkompatibilitet:**
  - Inga absoluta Windows-sökvägar används i kodbasen (validerat via sökning).
  - Alla beroenden är låsta i `package-lock.json` och testade med Node >=14.18.
  - `supertest` används med CommonJS-import i tester för maximal kompatibilitet.
  - Testsviten (21 tester för produkter) är grön efter både lokal och serverliknande (npm ci) installation.

- **CI/CD och testinfrastruktur:**
  - HTTP endpoint-tester täcker nu även felaktiga path-parametrar och validerar att service inte anropas vid 400.
  - Testerna körs automatiskt i pipeline och måste passera för att deploy ska ske.

Se även:
- [products.update-category.http.spec.ts](backend/src/products/products.update-category.http.spec.ts) för HTTP-tester
- [products.service.spec.ts](backend/src/products/products.service.spec.ts) för servicetester
- [products.security.spec.ts](backend/src/products/products.security.spec.ts) för controller/metadata-tester


## Dokumentstatus (2026-05-03)

### Målgrupp
Detta dokument är skrivet för systemadministratörer och programmerare. Fokus är arkitektur, datamodell, integrationer och driftkonsekvenser.

### Tillägg från senaste sessionerna

## Nyheter och förbättringar (2026-05-11)

- **Inventarie och baslager:** Möjlighet att se, sätta och ändra kategori på produkter direkt i inventarie- och baslagervyn. Identisk, sökbar/autocomplete category-picker i alla relevanta vyer (inventarie, baslager, admin, import). UX-standardisering: samma dropdown och interaktionsmönster överallt.
- **Seed-data:** Nya kategorier under `Skafferi > Sylt, mos & marmelad`: `Marmelad`, `Sylt`, `Mos` tillagda i seed.
- **Navigation/UI:** Admin-knappen flyttad från sidomenyn till profilmenyn ovanför logout.
- **Deploy och seed:** Förtydligande om att seed-data körs automatiskt vid `./deploy.sh --backend` och att seed-kontroll sker i deploy-scriptet. Hur man verifierar att seed körts och att nya kategorier finns.
- **Kodförenklingar/optimeringar:** Samtliga tre förenklingar/optimeringar från senaste commit är nu implementerade (se SESSION_2026-05-09_RECEIPT_IMPORT.md för detaljer).

- **Kvittoimport, regelmotor:** förbättrad tolkning av multipack och enheter i importerade kvittorader.
- **Kategori-guardrails:** utökade regler för bröd/rostbröd samt contradiction guards för att minska felaktiga AI-träffar.
- **Kategori-seed:** ny kategori `Korvbröd` under `Fastfoodbröd` och `Grädde` under `Matlagning` i seed-data.
- **Klientpersistens i Flutter:** kvittoimportens arbetssession sparas i klienten och återläses vid behov.
- **Dataperspektiv:** kvittosessionen lagras inte server-side; backend-kontraktet är oförändrat för den delen.
- **Kvittokategorisering:** förbättrad deterministisk kategorisering med nya regler för pasta, grädde, ägg, juice, godis, och potatis. AI-guardrail justerad för att tillåta medium confidence-kategorisering.
- **Testinfrastruktur:** parametriserade enhetstester för kvittoimport (18 testfall) och CI/CD-pipeline med automatiserad testkörning på push.
- **CI/CD:** GitHub Actions-integration för automatiserad testkörning vid push och pull request.
- **Node.js versionsparitet:** `package-lock.json` spåras i git för båda repos; Dockerfiles kör `npm ci` för reproducerbara byggen.
- **PDF-kvittoimport:** `pdf-parse` importeras med `require()` (CJS); `pdfjs-dist/legacy/build/pdf.js` som fallback undviker `DOMMatrix`-fel.
- **Felkods-forwarding:** `receipt-import.service.ts` kastar `ServiceUnavailableException` vid 503/429 från importer-api (tidigare alltid 400).
- **AI-skippning (PDF):** `looksLikeReceiptProductLine()` i importer-api filtrerar rader utan siffra — minskar Mistral-anrop drastiskt för vanliga PDF-kvitton.
- **Simplified Matching Logic (2026-05-09):** Unified matcher konsoliderar receipt-import matching och categorization. Tidigare var logiken splittrad mellan `matchProducts()` och `enrichWithAiCategories()`; nu är allt i `matchAndEnrichReceiptItem()` med explicit steg: Alias lookup → Word-match → Categorization (Rules → AI → Guards → Hard overrides). Bättre trace-logging och debuggability. Context-loading görs en gång per receipt (parallell loading) istället för repeated queries.

### Driftnotering
Verifiera efter deploy att seed-körning inkluderar uppdaterat kategoriträd och att kvittoflödet använder den senaste regelbaserade parserlogiken.

## Sakerhetsstatus (2026-05-07)

Denna sektion beskriver säkerhetsfunktioner som är implementerade i kodbasen och hur de är implementerade tekniskt.

### Nytt 2026-05-07: Inventory user-scope, IDOR-skydd och deploy-hygien

- **Inventory är nu user-scopad:** Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester).
- **IDOR-skydd för inventory:** Det är nu omöjligt för användare att läsa eller ändra andras inventarieposter. Tester verifierar att åtkomst nekas vid försök till IDOR.
- **.gitignore och deploy-hygien:** backend/dist och backend/tsconfig.tsbuildinfo ignoreras och är ej längre spårade i git. .env och .env.* ignoreras, men .env.example finns och är uppdaterad.
- **CI/CD-härdning:** npm audit och prisma validate körs i pipeline. Alla tester och byggen måste passera.


### Implementerat i backend

- JWT-skydd globalt: `JwtAuthGuard` ar registrerad som global APP_GUARD i `backend/src/app.module.ts`. Alla endpoints kravs pa giltig bearer-token om de inte ar markerade med `@Public()`.
- Rollbaserad access: `RolesGuard` ar global guard och laser metadata fran `@Roles(...)`. Endpoints med adminkrav blockerar icke-admin med `ForbiddenException`.
- Rate limiting: `ThrottlerGuard` ar globalt aktiv med standard 120 anrop/minut. Login/register har hardare throttle (`10/min`) i `auth.controller.ts`.
- Input-hardening: global `ValidationPipe` i `backend/src/main.ts` med `whitelist: true`, `forbidNonWhitelisted: true`, `transform: true`.
  Detta tar bort odefinierade falt och nekar payloads med extra fields.
- Security headers: `helmet(...)` ar aktiverat i `backend/src/main.ts` med HSTS, `x-frame-options`, `x-content-type-options`, referrer policy och cross-origin policies.
- CORS-begransning: origin satts via `ALLOWED_ORIGIN` (fallback host i kod) och credentials ar explicit styrda i `main.ts`.
- Losenordshashning: `bcryptjs` med kostnad 12 i `auth.service.ts` (`bcrypt.hash(password, 12)`).
- User-scope i flera domaner:
  - `recipes`: controllers/services skickar vidare `@CurrentUser().userId` och filtrerar pa agarskap.
  - `pantry` och `meal-plan`: alla las/skriv-operationer ar user-scopade via `userId` i service-lagret.
  - `receipt-alias`: las/skriv/radera styrs av owner/global-regler och aktuell anvandare.

### Tidigare risk som nu ar stangd

- Inventory user-scope ar nu genomford (schema, migration, controller, service och tester).
- Tidigare IDOR-risk for inventory bedoms som stangd i nuvarande implementation.
- Kvarvarande fokus ar regressionsskydd via tester och verifiering efter deploy.

### Driftrelaterad hardening som finns

- Secrets injiceras via miljo i `compose.yml` (t.ex. `JWT_SECRET`, `DATABASE_URL`, API-nycklar) och ar inte hardkodade i appkod.
- Prisma migrationer kor vid containerstart och recovery-playbook for P3009 finns dokumenterad i detta dokument.

## Ã–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.

---
##
Viktigt att komma ihåg vid implementering av nya funktioner och kodning är att inte använda windows sökvägar. Att inte använda c:/dev/recpie-app....  Detta eftersom bygg- och testmiljön är på en remote ubuntu-server. Utveckling sker lokalt och test samt drift
sker på remote server.   Säkerställ att inga absoluta Windows-sökvägar används i koden, för att stödja bygg och drift på Linux/Ubuntu

---

## Nyheter och förbättringar (2026-05-02)

## Nyheter och förbättringar (2026-05-07)

- **Alias-strategi och matchedVia:**
  - Backend: `ParsedReceiptItem` har fått fältet `matchedVia: 'alias' | 'wordmatch' | 'ai' | 'none'` som sätts i matchningslogiken och returneras till klienten.
  - Flutter: matchedVia visas som badge i UI (Alias/Ordmatch/AI-kategori). Alias-lärande är öppet för alla användare (user-alias), admins kan spara globala alias.
  - Användare kan se och radera sina alias på profilsidan. Admin-panelen visar produkt-ID och tydligare radstruktur.
  - Prioritering: Egna alias (ownerId=userId) används före globala alias (isGlobal=true), därefter ordmatchning och sist AI.
  - Tester: 3 nya tester för matchedVia i backend, totalt 66 tester gröna.
- **Inventory user-scope och IDOR-skydd:** InventoryItem-modellen har fått userId, migration har backfillat data, och alla endpoints kräver nu CurrentUser. Service och controller är uppdaterade, och tester för IDOR-skydd är på plats.
- **.gitignore och deploy-hygien:** backend/dist och backend/tsconfig.tsbuildinfo är nu ignorerade och ej längre spårade. .env och .env.* ignoreras, .env.example är komplett.
- **CI/CD-härdning:** npm audit och prisma validate körs i pipeline. Testsviten (66 tester) och build måste passera.


### Alias-strategi och matchedVia (detaljer)

- **Datamodell:**
  - `ReceiptAlias` har fälten `ownerId` (user-alias), `isGlobal` (global alias) och prioriteras i matchningslogik.
  - `matchedVia` sätts i backend och skickas till Flutter.
- **UI-flöde:**
  - Vid kvittoimport visas badge för matchkälla.
  - "Spara som alias" är tillgängligt för alla användare (sparar user-alias), admins kan spara globalt.
  - Profilsidan har en alias-lista där användaren kan radera egna alias.
  - Admin-panelen visar alla globala alias, produktnamn och produkt-ID.
- **Prioriteringsordning:**
  1. Egna alias (ownerId=userId)
  2. Globala alias (isGlobal=true)
  3. Ordmatchning
  4. AI-kategori


Produkttabellen är omgjord till ett fullständigt user-scope-modell. Beslutet grundar sig på att en global produktkatalog skapade falska matchningar i kvittoimport för nya användare (produkter "hittades" fast användaren aldrig lagt till dem).

**Vad som ändrades:**

| Komponent | Förändring |
|---|---|
| `Product.ownerId` | `Int?` → `Int` (obligatorisk, non-nullable) |
| `Product.owner` | `onDelete: SetNull` → `onDelete: Cascade` |
| `db/seeds/seed_all.sql` | Innehåller nu enbart kategorier — inga `INSERT INTO Product` |
| Migration `20260502160000` | Raderar alla globala produkter (`ownerId IS NULL`), gör FK non-null |
| `receipt-import.service.ts` | `matchProducts(items, userId)` filtrerar på `ownerId = userId` |
| `receipt-import.controller.ts` | Extraherar `userId` från JWT och skickar till service |

**Flöde för nya användare:**
1. Kvittoimport → AI/OCR parsar kvitto → inga produktmatcher (user har inga produkter ännu)
2. Regelbaserad kategoridetektion + AI-kategorisering körs för alla rader
3. Användaren bekräftar i Flutter → produkten skapas via `POST /products` med `ownerId = userId`
4. Nästa kvittoimport med samma vara → alias/ordmatch hittar den user-ägda produkten

**Framtida mallar (planerat):** En mallhanterare i UI kan låta användare seeda sin produktkatalog från fördefinierade livsmedelsmallar utan att det kräver global data i databasen.

### Kategorisystem utökat

Nya noder sedan 2026-05-01:

| Nivå | Kategori |
|---|---|
| L2 under Bröd & Kakor | Kondis & fika |
| L3 under Kondis & fika | Kaffebröd (wienerbröd, donuts, munkar, kanelbullar m.m.) |
| L2 under Dryck | Te & choklad |
| L3 under Te & choklad | Te (chai, vanilla chai, ceylon te m.m.) |
| L3 under Allergi mejeri | Laktosfri mjölk |
| L3 under Allergi mejeri | Filmjölk & Yoghurt |
| L3 under Allergi mejeri | Kvarg & Cottage cheese |
| L3 under Allergi mejeri | Matfett |
| L3 under Allergi mejeri | Allergi matlagning |


### Regelbaserad kategoridetektion (`ruleBasedCategorySuggestion`)

Funktionen i `receipt-import.service.ts` matchar kvittonamn mot nyckelord och returnerar rätt kategori direkt — utan AI-anrop. Täcker:
- **Te** — `te`, `tea`, `chai`, `tepas`, `tepak`
- **Kaffebröd** — `wienerbrod`, `donut`, `munk`, `croissant`, `kanelbulle`, `bakelse`, `semla`, `dammsugare`, `kladdkaka`, `muffin`, `cupcake`, `chokladboll`
- **Allergi mejeri** — kombinationer av mejeri-markörer + allergen/växtbaserade markörer

### AI-guardrail

`AiService.suggestCategory()` remappar `low`/`medium`-konfidenspoäng till L1-föräldern istället för att returnera ett potentiellt fel L2/L3. Loggning sker via NestJS Logger.


### Förbättrad produktmatchning och alias

`matchProducts()` i `receipt-import.service.ts`:
- Returnerar nu även `matchedVia` för varje rad: 'alias', 'wordmatch', 'ai' eller 'none'.
- Alias prioriteras enligt ovan.
- Flutter visar badge och styr alias-lärande utifrån matchedVia.

`findWordMatch()` i `receipt-import.service.ts`:
- **Diakritiksnormalisering** — `normalizeToken()` konverterar å→a, ä→a, ö→o före jämförelse. Löser t.ex. `gradde` == `grädde`.
- **Enstaka lång partiell matchning** — ett produktord på ≥5 tecken som är en delmatchning räcker nu som stark signal. Löser t.ex. "Vispgrädde 5dl" → produkt "grädde".

---

## Nyheter och forbattringar (2026-05-04)

- **Receptimport parser centraliserad:** parsinglogik for markdownrecept ar flyttad till en gemensam utility i backend (`src/common/utils/recipe-parser.ts`) och duplicerad parserkod i recipes-service ar borttagen.
- **Parser prestanda/robusthet:** regex och enhetslista ar lyfta till modulniva, braktal och intervall (ex. `ca 600-700 g`) hanteras konsekvent, och ingrediensnamn med `eller` delas upp i alternativ.
- **Alternativa ingredienser (Option A i drift):** `RecipeIngredient` har nu `alternativeProductIds` (JSON) for att lagra alternativa produktval per ingrediensrad.
- **Validering vid create/update av recept:** backend validerar att valda produkter ar aktiva innan recept sparas.
- **Atomar uppdatering av ingredienser:** `update()` for recept kor nu inom Prisma-transaction (delete + create/update i samma transaktion).
- **Sakrare felhantering vid bildimport:** om receptskapande fallerar efter nedladdning av bild stadas orphan-filer bort.
- **Ownerless recept hardening:** legacy-recept utan owner claimas av forsta redigerande anvandare; recept med annan owner fortsatter att vara blockerade for obehoriga.
- **Inventory preview med alternativ:** lagerkoll kan rakna in alternativa produkter nar primar produkt saknas.
- **Testning:** nya enhetstester for recipe-parser lagda; total testsvit verifierad gron (57 tester).

### Prisma migration driftlarning (2026-05-04)

Vid deploy av migrationen `20260504220420_add_alternative_product_ids` uppstod ett produktionsstopp med Prisma P3009/P3018. Rotorsaker var en tidigare failed migration samt ogiltig SQL-korning i miljo.

Praktisk recovery-sekvens som verifierades:
1. Kor `prisma migrate resolve --rolled-back <migration_name>` i samma runtime-miljo som appen (containern), inte i ett shell utan korrekt DB-konfiguration.
2. Verifiera `DATABASE_URL` i runtime (container) innan ny deploy.
3. Kor `prisma migrate deploy` igen.
4. Om schema redan ar uppdaterat men migrationshistorik halkat efter: markera migrationen som `--applied` och deploya pa nytt.

Driftrekommendation:
- Kor migrationskommandon via app-containern i compose-stacken for att undvika skillnader i Prisma-version, env-laddning och DNS/hostnamn.

## Nyheter och förbättringar (2026-04-30)

- **Microservice-importer integrerad** — `importer-api` körs nu som intern Docker-tjänst i `recipe-app/compose.yml`. All URL-skrapning, OCR, PDF-parsning och AI-kvittoparsning delegeras dit. `recipe-api` behåller Levenshtein-matchning, produktdatabas och AI-kategorisering. Se [migrering-MSI.md](_archive/docs/migrering-MSI.md) för fullständig lista över ändrade filer.
- **Bygg-instruktion** — Katalogen `../microservice-importer` måste finnas sida vid sida med `recipe-app` på servern. `./deploy.sh` bygger båda automatiskt.

## Nyheter och förbättringar (2026-04-22)

- **User-scope för pantry och matplan** — Prisma-schema, backend och API är migrerade så att alla baslager- och matplansdata är per användare. JWT används för filtrering i alla endpoints.
- **Robust bildimport** — Bild-URL normaliseras, laddas ner och optimeras i backend. Fallback till extern URL om nedladdning misslyckas. Bilden kopplas till receptet och raderas vid delete. Diagnostikloggning på alla steg.
- **Importflöde** — Quick-import och receipt-import har förbättrats med robust multipart-hantering, timeout, och felhantering. Markdown och bild-url skickas hela vägen till UI.
- **Flutter-parity** — Matplan, inventarie, baslager och receptflöden är nu fullt migrerade till Flutter med user-scope och robust felhantering.
- **Felsökningslogg** — Se `IMPORT_IMAGE_DEBUG_2026-04-22.md` för detaljerad felsökningshistorik kring bildimport och importflöde.

### Kända begränsningar
- Bildimport kraver att containrar ar uppdaterade med senaste kod och konfiguration.
- Avancerad AI-integration (utokad audit/loggning och framtida modellstrategi) ar planerad post-MVP.

---
- **Privat git-server** fÃ¶r denna applikation
- Ã„ndringar pushas lokalt till denna server:
  ```bash
  git push origin main
  ```

### Bygge och kÃ¶rning pÃ¥ servern

Efter push till Gitea:

1. **SSH in pÃ¥ servern**
   ```bash
   ssh user@server
   cd /opt/containers/recipe-app
   ```

2. **HÃ¤mta senaste Ã¤ndringar**
   ```bash
   git pull origin main
   ```

3. **Bygg och starta applikationen**
   ```bash
   docker compose build
   docker compose up -d
   ```

Alla tjÃ¤nster (frontend, backend, databas) startas via Docker Compose enligt `compose.yml`.

### Rekommenderat kommandomonster

For att undvika forvirring mellan huvudappen och Flutter-sparat bor dessa kommandon anvandas konsekvent:

**Huvudappen (Next.js + API + DB):**
```bash
docker compose build recipe-frontend recipe-api
docker compose up -d recipe-db recipe-api recipe-frontend
```

**Enbart backend:**
```bash
docker compose build recipe-api
docker compose up -d recipe-db recipe-api
```

**Flutter-sparat (separat klient):**
```bash
docker compose -f compose.yml -f compose.flutter.yml build recipe-flutter
docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flutter
```

Tumregel:
- `compose.yml` styr huvudappen pa `recept.gynther.se`.
- `compose.yml` + `compose.flutter.yml` styr Flutter-klienten pa `test.gynther.se`.
- Att bygga en image startar inte containern; `docker compose up -d ...` kravs alltid efter build.

---

## Container- och deployupplÃ¤gg

- `compose.yml` bygger lokala images fÃ¶r frontend och backend
- `pull_policy: never` anvÃ¤nds fÃ¶r appens lokala images fÃ¶r att undvika felaktiga registry-pulls i Portainer
- Health checks finns fÃ¶r databas, API och frontend
- `depends_on` med hÃ¤lsovillkor anvÃ¤nds fÃ¶r stabilare startordning i Docker och Portainer
- **Fasta containernamn** â€” alla tjÃ¤nster har `container_name` satt i `compose.yml`, vilket ger fÃ¶rutsebara namn oavsett projektkatalog:

| TjÃ¤nst | Container-namn |
|---|---|
| Frontend (Next.js) | `recipe-frontend` |
| Backend (NestJS) | `recipe-api` |
| Databas (MariaDB) | `recipe-db` |

AnvÃ¤nd dessa namn vid `docker exec`, t.ex.:
```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;"
```

### Orphan-containers vid blandade compose-filer

Vid arbete med bade huvudappen och Flutter-sparet kan Docker Compose visa varningen om `orphan containers`, ofta for `recipe-flutter`.

Detta betyder normalt bara att:
- en container startades med en annan compose-filskombination tidigare,
- och att den inte finns med i kommandot du kor just nu.

Exempel:
- `docker compose up -d recipe-frontend` kanner inte till `recipe-flutter` eftersom den bara finns i `compose.flutter.yml`.
- `docker compose -f compose.yml -f compose.flutter.yml up -d recipe-flutter` kanner till Flutter-sparet.

Varningen ar i sig inte ett fel och paverkar inte Prisma-migrationer eller databasens schema.

Stada endast bort orphan-containers om du verkligen vill stoppa dem:
```bash
docker compose down --remove-orphans
```

Obs: detta kan stoppa `recipe-flutter`, som da maste startas igen med override-filen.

---

## 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
(auth) {
    basicauth {
        admin $2a$14$DahHUWD2cKyXJ96sH5VQwuQv1bqmIn0gsdoSaw4mofzfdNY2Y0VsO
    }
}

(common) {
    encode gzip zstd
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
        Cross-Origin-Opener-Policy "same-origin"
        Cross-Origin-Resource-Policy "same-origin"
        Cross-Origin-Embedder-Policy "require-corp"
    }
}

# ============================================
# Import Service (Document Converter) - Standalone UI
# ============================================
import.gynther.se {
    import common
    reverse_proxy recipe-import-service:3000
}

# ============================================
# RECIPE APP + IMPORT SERVICE
# ============================================
recept.gynther.se {
    import common

    # === IMPORT SERVICE (Document Converter) ===
    # Dessa endpoints mÃ¥ste komma FÃ–RST innan backend reglerna!
    handle /api/recipes/import* {
        reverse_proxy recipe-import-service:3000
    }

    # === RECIPE FRONTEND PROXY ENDPOINTS ===
    # Next.js API routes
    handle /api/inventory-history-proxy {
        reverse_proxy recipe-frontend:3000
    }

    handle /api/admin/merge-preview-proxy {
        reverse_proxy recipe-frontend:3000
    }

    handle /api/recipe-preview-proxy {
        reverse_proxy recipe-frontend:3000
    }

    # === RECIPE BACKEND API ENDPOINTS ===
    # Backend kÃ¶rs pÃ¥ port 8080 (frÃ¥n docker-compose)
    handle /api/products* {
        reverse_proxy recipe-api:8080
    }

    handle /api/inventory* {
        reverse_proxy recipe-api:8080
    }

    handle /api/recipes* {
        reverse_proxy recipe-api:8080
    }

    # === HEALTH CHECKS ===
    handle /health {
        reverse_proxy recipe-api:8080
    }

    # === CATCH ALL ===
    # Ã–vriga /api/* gÃ¥r till frontend
    handle /api/* {
        reverse_proxy recipe-frontend:3000
    }

    # Frontend - catch all remaining routes (port 3000)
    reverse_proxy /* recipe-frontend:3000
}
```

**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.

---

## SÃ¤kerhetshuvuden

SÃ¤kerhetshuvuden Ã¤r implementerade i tre lager fÃ¶r djupfÃ¶rsvar:

### Lager 1: Caddy (globalt fÃ¶r alla tjÃ¤nster)

SÃ¤tts i `(common)`-blocket och gÃ¤ller alla domÃ¤ner som importerar det.

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

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

```
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*`).

| Direktiv | TillÃ¥tna kÃ¤llor | Motivering |
|----------|----------------|------------|
| `default-src` | `'self'` | Restriktiv default |
| `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 |
| `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 |
| `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 |

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

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'`

---

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

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

### RÃ¤tt mÃ¶nster: Next.js API route

```
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`)
- 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 res = await fetch(`${API_BASE}/api/products/${id}`, { method: 'PATCH', headers: authHeaders, ... });
  return Response.json(await res.json());
}

// Klientkomponent
const res = await fetch(`/api/admin/product/${id}`, { method: 'PATCH', body: JSON.stringify(data) });
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?

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

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



- **Framework:** Next.js 16.2 (App Router, server + client components)
- **SprÃ¥k:** TypeScript 5.4.5
- **UI:** React 19.2, ingen CSS-ramverk (ren CSS-in-JS och inline-stilar)
- **Autentisering:** Auth.js v5 (next-auth beta), JWT-session, `auth()` i server components
- **Bygg:** Standalone output, kÃ¶rs i Docker-container
- **API-anrop:** `fetchJson` (server-side med auth-headers) + Next.js API route-proxies (client-side)
- **Felhantering:** Global parseErrorResponse utility, svenska felmeddelanden

> **Viktigt:** `Navigation.tsx` Ã¤r en async server component som 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` |
| **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 |
| | `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/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 |
| | `InventoryConsumptionHistory.tsx` | Visa konsumtionshistorik |
| | `ProductForm.tsx` | VÃ¤lja produkt fÃ¶r inventariefÃ¶remÃ¥l |
| | `actions.ts` | Server actions fÃ¶r inventarie |
| **Recept** | `app/recipes/page.tsx` | Lista recept |
| | `RecipePreview.tsx` | ReceptfÃ¶rhandsvisning med inventariestatus |
| **LÃ¤gg till recept** | `app/recipes/create/page.tsx` | Server component med Navigation |
| | `app/recipes/create/CreateRecipeClient.tsx` | Klientkomponent: snabbimport + metodval |
| **Skriv in recept** | `app/recipes/write/page.tsx` | Server component med Navigation |
| | `app/recipes/write/WriteRecipePage.tsx` | Markdown-baserat receptskapande (3-steg): 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 |
| **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: 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 |
| | `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 |
| **Baslager** | `app/baslager/page.tsx` | Visa och hantera baslager (server component) |
| | `AddToPantryForm.tsx` | LÃ¤gg till produkt i baslager (dropdown) |
| | `PantryList.tsx` | Visa baslager grupperat per kategori |
| | `actions.ts` | Server actions: addPantryItem, removePantryItem |

### API-proxy routes (Next.js)

Alla proxy-routes anvÃ¤nder `withAuth(handler)`-wrappern (frÃ¥n `lib/with-auth.ts`) som lÃ¤ser auth-token via `request.auth` och vidarebefordrar `Authorization: Bearer <token>` 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/auth/[...nextauth]` | GET, POST | Auth.js handlers (login, logout, session) |
| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) |
| `/api/categories` | GET | Kategorihierarki (publik, proxies `/api/categories/tree`) |
| `/api/profile` | GET, PATCH | HÃ¤mta/uppdatera anvÃ¤ndarprofil |
| `/api/recipes` | GET, POST | Lista recept + spara nytt |
| `/api/quick-import-proxy` | POST | URL-, PDF- och bildimport |
| `/api/parse-markdown-proxy` | POST | Markdown-tolkning fÃ¶r skriv-in-recept |
| `/api/inventory-history-proxy` | GET | Konsumtionshistorik |
| `/api/recipe-preview-proxy` | GET | ReceptfÃ¶rhandsvisning |
| `/api/admin/merge-preview-proxy` | GET | Produktmerge-preview |
| `/api/receipt-import-proxy` | POST | Kvittoimport via Mistral AI |
| `/api/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 |

### 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 <token>`. 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**.

**LÃ¶sning: anvÃ¤nd alltid `useAuthFetch()` i klientkomponenter:**

```ts
import { useAuthFetch } from '../../../lib/use-auth-fetch';

// I komponentfunktionen:
const authFetch = useAuthFetch();

// 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`.

**Endpoints som krÃ¤ver `useAuthFetch()` i klientkod (Caddy-direktrouting):**

| 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/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()`.**

- 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
- Typade inventory/recipe data i `features/inventory/types.ts`

---

## Backend (NestJS)

- **Framework:** NestJS 10.3
- **SprÃ¥k:** TypeScript 5.4.5
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
- **API:** REST, validering med class-validator
- **Autentisering:** JWT (7 dagars token), `JwtAuthGuard` skyddar alla routes 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

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

### Backend-funktioner

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

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

**Inventarie-API:**
- CRUD fÃ¶r inventariefÃ¶remÃ¥l (produktreferens, kvantitet, enhet, plats, mÃ¤rke, bÃ¤st fÃ¶re, mm)
- Konsumtionshistorik-tracking (registrera brukat amount och kommentar)
- Sortering: efter plats, bÃ¤st fÃ¶re-datum, namn (Aâ€“Ã–)
- Filtrera utgÃ¥ende varor
- **Enhetskonvertering:** StÃ¶d fÃ¶r viktenheter (g/kg), volymenheter (ml/dl), portionsenheter (tsk/msk)
  - Normalisering av enheter (t.ex. "tesked" â†’ "tsk", "gram" â†’ "g")
  - Konverteringsregler per enhet-typ
  - Kan endast konvertera inom samma enhet-typ (error om blandning)

**Recept-API:**
- CRUD fÃ¶r recept och ingredienser
- **Parse-markdown endpoint:** Tolkar Markdown-format, matchar ingredienser mot databas
- **Matchningsalgoritm (3 nivÃ¥er):**
  1. Exakt match (normalizedName eller canonicalName efter normalisering): **100 poÃ¤ng**
  2. DelstrÃ¤ng-match (ingrediens i produktnamn eller vice versa): **70 poÃ¤ng**
  3. Levenshtein-distans-baserad likhet: **40â€“100 poÃ¤ng** (under 40 filtreras bort)
  - Top 5 fÃ¶rslag per ingrediens
  - Sortering: HÃ¶gsta poÃ¤ng fÃ¶rst
- **Inventory-preview:** JÃ¤mfÃ¶r recept mot inventarie
  - Returnerar status fÃ¶r varje ingrediens: rÃ¤cker | saknas | enhetskonflikt
  - Automatisk enhetskonvertering vid jÃ¤mfÃ¶relse
- **Normalisering:** `normalize-name()` utility fÃ¶r consistent namn-matching

**Produkt-API:**
- CRUD fÃ¶r produkter (create, read, update, delete)
- **Duplicate detection:** `findDuplicates()` - Hitta produkter med samma normalizedName
- **Merge-preview:** `previewMerge()` - FÃ¶rhandsgranska merge operation (visa inventory-counts, outcome)
- **Merge operation:** `merge()` - SlÃ¥ ihop tvÃ¥ produkter
  - Flytta alla inventariefÃ¶remÃ¥l frÃ¥n kÃ¤lla till mÃ¥l
  - Soft-delete kÃ¤llan (isActive = false, deletedAt = nu)
  - Uppdatera recept-ingredienser frÃ¥n kÃ¤lla till mÃ¥l
- **Canonical name management:**
  - `updateCanonicalName()` - Uppdatera canonical name fÃ¶r ett produktnamn
  - `backfillCanonical()` - Fylla pÃ¥ canonical names fÃ¶r alla produkter (admin-funktion)
- **Soft delete & restore:**
  - `remove()` - Soft-delete produkt (isActive = false)
  - `restore()` - Ã…terstÃ¤ll borttagen produkt
- **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(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:
- **`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`

**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`.
- **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`
- **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)

---

## API-endpoints (fullstÃ¤ndig lista)

### ðŸ¥ Health endpoints
```
GET  /api/health                Ã–vergripande hÃ¤lsakontroll (200/503)
GET  /api/health/db             Databasspecifik hÃ¤lsa + responseTime
```

### ðŸ“¦ Inventarie-endpoints
```
GET    /api/inventory                     Lista inventariefÃ¶remÃ¥l
                                          Params: ?location=... &sort=...
GET    /api/inventory/expiring            UtgÃ¥ngna/snart utgÃ¥ngna varor
POST   /api/inventory                     Skapa nytt inventariefÃ¶remÃ¥l
PATCH  /api/inventory/:id                 Uppdatera inventariefÃ¶remÃ¥l
POST   /api/inventory/:id/consume         Konsumera (registrera brukat amount)
GET    /api/inventory/:id/consumption-history  Konsumtionshistorik
```

### ðŸ½ï¸ Recept-endpoints
```
POST   /api/quick-import                  Snabbimport frÃ¥n URL, PDF eller bild
                                          Body: { input: string } eller multipart-form med file
POST   /api/recipes/parse-markdown        Tolka Markdown-recept (matchningslogik)
GET    /api/recipes                       Lista alla recept
POST   /api/recipes                       Skapa nytt recept
GET    /api/recipes/:id                   HÃ¤mta specifikt recept
PATCH  /api/recipes/:id                   Uppdatera recept
DELETE /api/recipes/:id                   Ta bort recept (204 No Content)
GET    /api/recipes/:id/inventory-preview JÃ¤mfÃ¶r recept mot inventarie
```

### ðŸ·ï¸ Produkt-endpoints
```
GET    /api/products                      Lista alla aktiva produkter
POST   /api/products                      Skapa ny produkt
GET    /api/products/:id                  HÃ¤mta specifik produkt
PATCH  /api/products/:id                  Uppdatera produktens namn, canonicalName eller kategori
DELETE /api/products/:id                  Soft-delete produkt
POST   /api/products/:id/restore          Ã…terstÃ¤ll raderad produkt

GET    /api/products/duplicates           Lista duplicerade namn (grupperade)
GET    /api/products/merge-preview        FÃ¶rhandsgranska merge
               ?sourceProductId=X &targetProductId=Y
POST   /api/products/merge                SlÃ¥ ihop tvÃ¥ produkter
PATCH  /api/products/:id/canonical-name   Uppdatera canonical name
POST   /api/products/backfill-canonical   Backfill canonical names (admin)
POST   /api/products/reset-all            Rensa all produktdata (admin)
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)
               Returnerar lista av { productId, productName, suggestion: CategorySuggestion }
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)
```

### 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)
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)
               Body: { isPremium: boolean }
```

### Baslager-endpoints
```
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)
```

> 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 (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 (per inloggad anvÃ¤ndare)

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 (anvÃ¤ndarens baslager)
               Returnerar status per ingrediens: rÃ¤cker | saknas | enhetskonflikt
```

> 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)
DELETE /api/receipt-alias/:id             Ta bort alias
```

---

## Datamodell (Prisma ORM)

### User
```prisma
model User {
  id           Int      @id @default(autoincrement())
  username     String   @unique
  email        String   @unique
  firstName    String?
  lastName     String?
  passwordHash String
  role         String   @default("user")       # "user" eller "admin"
  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:

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

### Category
```prisma
model Category {
  id       Int        @id @default(autoincrement())
  name     String
  parentId Int?
  parent   Category?  @relation("CategoryTree", ...)  # FÃ¶rÃ¤lder (null = toppnivÃ¥)
  children Category[] @relation("CategoryTree")       # Underkategorier
  products Product[]

  @@unique([name, parentId])
}
```
Hierarkin har 3 nivÃ¥er: **Huvudkategori â†’ Underkategori â†’ Typ**  
ExempeltrÃ¤d: `Mejeri, ost & Ã¤gg â†’ MjÃ¶lk â†’ 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.

**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:**
```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:** 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);
> ```

### Product
```prisma
model Product {
  id             Int             @id @default(autoincrement())
  name           String          # Visningsnamn
  normalizedName String @unique  # Normaliserat namn (lowercase, utan skiljetecken)
  canonicalName  String?         # Canonical namn fÃ¶r receptmatchning
  category       String?         # 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"
  isActive       Boolean @default(true)
  deletedAt      DateTime?
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  inventoryItems    InventoryItem[]
  recipeIngredients RecipeIngredient[]
  tags              ProductTag[]
  nutrition         Nutrition?
}
```

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

### InventoryItem
```prisma
model InventoryItem {
  id             Int      @id @default(autoincrement())
  productId      Int      # Foreign key till Product
  quantity       Decimal @db.Decimal(10, 2)  # Kvantitet (decimal fÃ¶r precision)
  unit           String   # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
  brand          String?  # VarumÃ¤rke
  location       String?  # Lagerplats (Kyl, Frys, Skafferi, etc)
  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

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

  @@index([productId])
}
```

### InventoryConsumption
```prisma
model InventoryConsumption {
  id              Int           @id @default(autoincrement())
  inventoryItemId Int           # Foreign key till InventoryItem
  amountUsed      Decimal       @db.Decimal(10, 2)  # Konsumerad kvantitet
  comment         String?       # Kommentar
  createdAt       DateTime      @default(now())

  inventoryItem   InventoryItem @relation(fields: [inventoryItemId], references: [id])
}
```

### Recipe
```prisma
model Recipe {
  id          Int      @id @default(autoincrement())
  name        String   # Receptnamn
  description String?  # Receptbeskrivning
  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)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  ingredients  RecipeIngredient[]
  mealPlanEntries MealPlanEntry[]
}
```

`servings` Ã¤r grundportionsantalet â€” matplanen anvÃ¤nder det fÃ¶r att skala ingrediensmÃ¤ngder om anvÃ¤ndaren anger ett avvikande portionsantal per dag.

### RecipeIngredient
```prisma
model RecipeIngredient {
  id        Int      @id @default(autoincrement())
  recipeId  Int      # Foreign key till Recipe
  productId Int      # Foreign key till Product
  quantity  Decimal  @db.Decimal(10, 2)  # Receptkvantitet
  unit      String   # Enhet enligt recept
  note      String?  # Ingrediensnot (t.ex. variation)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  recipe Recipe @relation(fields: [recipeId], references: [id])
  product Product @relation(fields: [productId], references: [id])
}
```

### PantryItem
```prisma
model PantryItem {
  id        Int      @id @default(autoincrement())
  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())
  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)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
  recipe Recipe @relation(fields: [recipeId], references: [id])

  @@unique([userId, date])  # Bara ett recept per dag och anvÃ¤ndare
  @@index([userId])
}
```

> **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)
  productId   Int               # FK till matchad Product
  createdAt   DateTime @default(now())

  product Product @relation(...)
}
```

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.

---


---

## Arkitektur: Privata produkter och kvittoimport produktval (2026-05-01)

### Bakgrund

Kvittoimportflödet (Fas 6b) behövde ett sätt för användare att koppla kvittorader till produkter i systemet. Tre problem identifierades:

1. **Produktsökning gav nollträffar** när AI-kategorins namn (t.ex. "Frukt & Grönt") matades in som söksträng i produktfältet — produkter heter inte samma sak som kategorier.
2. **AI-föreslagna kategorier var ofta L1/L2** (t.ex. "Mat") men produkter är kopplade till L3-noder. Att filtrera på bara det exakta kategori-ID:t gav 0 produkter.
3. **Saknad produkt** — om varan på kvittot inte finns i systemet alls (t.ex. "Röda äpplen") hade användaren ingen väg framåt.

### Lösning 1: Tvåstegs-picker (Kategori → Produkt)

En ny widget `CategoryThenProductPicker` (i `lib/core/ui/`) orkestrerar ett tvåstegsflöde:

**Steg 1 — Kategoriträdet:** Användaren bläddrar i L1 → L2 → L3 eller söker fritext. Sökresultaten visar hela sökvägen som brödsmula. Alla icke-lövnoder (L1, L2) har en "Välj"-knapp som fallback om man vill stoppa på mellannivå.

**Steg 2 — Produktpicker:** `ProductPickerField.showSheet()` öppnas filtrerad på den valda kategorins ID **plus alla ättlingars ID:n** (insamlade rekursivt via `_collectIds()`). Det innebär att valet av "Mat > Frukt & Grönt" ger alla produkter i samtliga L3-underkategorier.

```
L1: Mat
  L2: Frukt & Grönt          ← välj här → ger produkter från alla L3 nedan
    L3: Äpplen                ← välj här → ger bara äpple-produkter
    L3: Bananer
    L3: Citrusfrukter
```

### Lösning 2: AI-direkthopp

Om `preselectedCategoryId` finns (AI-förslag) hoppar `CategoryThenProductPicker.show()` **direkt till steg 2** utan att visa trädet. Användaren klickar på AI-chip → produktpickern öppnas direkt med rätt kategorifilter. Om kategorin inte hittas i trädet visas trädet som fallback.

### Lösning 3: Privata produkter

**Problemet:** `POST /products` (admin) skapar globala produkter. Vanliga användare ska kunna skapa egna produkter utan att smutsa ned den globala produktkatalogen.

**Modell:** `Product` fick ett nytt fält `isPrivate: Boolean @default(false)`. Privata produkter är kopplade till en specifik användare via `ownerId` och syns bara för den användaren.

**`normalizedName`-prefix:** För att undvika kollision i det unika indexet prefixas privata produkters normaliserade namn med `private:{userId}:`. Det innebär att "röda äpplen" för användare 5 lagras som `private:5:roda applen` och kolliderar varken med en global produkt med samma namn eller en annan användares privata produkt.

**Nya endpoints:**

| Endpoint | Åtkomst | Beskrivning |
|---|---|---|
| `GET /products` | Publik | Returnerar bara globala produkter (`isPrivate: false`) |
| `GET /products/mine` | Inloggad | Returnerar inloggad användares privata produkter |
| `POST /products/private` | Inloggad | Skapar en privat produkt kopplad till inloggad användare |

**Migration:** `backend/prisma/migrations/20260501000000_add_product_is_private/migration.sql`

### Flutter-integration

- `_loadProducts()` i `receipt_import_tab.dart` laddar `GET /products` och `GET /products/mine` parallellt via `Future.wait` och slår ihop till en lista.
- `_EditDialogState` håller en lokal `_localProducts`-kopia som utökas om en ny produkt skapas under dialogen — utan ytterligare nätverksanrop.
- "Skapa ny"-dialogen i produktpickern är förifylld med den aktuella söksträngen för snabbt namngivande.

## Arkitektur: User-scope för baslager och matplan (2026-04-22)

> **Designregel: User-scope vid ny funktionalitet**
>
> All data som tillhör en specifik användare **måste** ha ett `userId`-fält med FK till `User` och user-filtrering i service-lagret. Följ detta mönster vid all ny funktionsutveckling:
>
> 1. **Databasschema:** `userId Int` med `@relation` till `User`, icke-null. Sammansatt `@@unique([userId, ...])` om raden ska vara unik per användare.
> 2. **Migration:** Backfill till första användaren för befintliga rader innan kolumnen sätts NOT NULL.
> 3. **Backend service:** Alla `findAll`/`findOne`/`create`/`delete` filtrerar eller sätter `where: { userId }`. Returnera aldrig andra användares data.
> 4. **Controller:** Extrahera `userId` via `@CurrentUser()` eller `@Request() req` — aldrig från body (säkerhetsrisk).
> 5. **Globala vs. privata resurser:** Produkter och kategorier är globala. Inventarie, baslager, matplan och användarspecifika produkter (`isPrivate: true`) är user-scopade.
> 6. **Privata produkter** (`isPrivate: true`): skapas via `POST /products/private`, visas via `GET /products/mine`. Normaliserat namn prefixas med `private:{userId}:` för att undvika kollision med globala produkter.
>
> **Kontrollera alltid:** Kan två användare se varandras data? Om ja — lägg till `userId`-scope.

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

1. **Snabbimport** â€” Klistra in ICA-lÃ¤nk fÃ¶r automatisk skrapning (ny feature)
2. **Skriv in recept** (`/recipes/write`) â€” Markdown-baserad inmatning dÃ¤r anvÃ¤ndaren skriver receptet i enkelt format
3. **Importera frÃ¥n fil** (`/recipes/import`) â€” Ladda upp PDF, bild eller lÃ¤nk och fÃ¥ en fÃ¶rsta Markdown-version automatiskt

Alla vÃ¤gar mÃ¶jliggÃ¶r automatisk matchning av ingredienser mot databasen.

### StrukturÃ¶versikt

#### Snabbimport-fÃ¤ltet

**Frontend: `/recipes/create/page.tsx`**
- OvanfÃ¶r de tvÃ¥ huvudvalen visas ett gult inmatningsfÃ¤lt fÃ¶r snabbimport
- AnvÃ¤ndaren klistrar in en ICA-receptlÃ¤nk eller filsÃ¶kvÃ¤g
- Vid submit:
  1. Frontend skickar till `/api/quick-import-proxy`
  2. Proxy proxiar till backend `POST /api/quick-import`
  3. Backend returnerar Markdown-text
  4. Frontend sparar i `sessionStorage('recipeMarkdown')`
  5. Omdirigera till `/recipes/write` med fÃ¶rifylld Markdown

**Backend: `QuickImportService` (ny modul)**
- Ansvarig fÃ¶r URL-import, PDF-tolkning, bild-OCR och Markdown-normalisering
- **Huvudmetoder:**
  - `importFromInput(input: string)` â€” Detekterar URL eller serverfilsÃ¶kvÃ¤g
  - `importFromUpload(file)` â€” Hanterar uppladdad PDF eller bildfil
- **URL-specifik logik:**
  - Validerar URL
  - Fetchar HTML via `fetch()` med User-Agent
  - VÃ¤ljer site-specifik parser eller generisk fallback
  - Konverterar resultatet till Markdown-format
- **PDF-logik:**
  - Extraherar text med `pdf-parse`
  - Stoppar om ingen lÃ¤sbar text hittas
- **Bildlogik:**
  - OCR via `tesseract.js`
  - Svensk och engelsk sprÃ¥kmodell (`swe+eng`)
- **Error-strategi:**
  - `400 Bad Request` â€” Tomt input eller saknad fil
  - `400 Bad Request` â€” OstÃ¶dd filtyp eller ingen lÃ¤sbar text
  - `503 Service Unavailable` â€” Misslyckad PDF- eller OCR-behandling
  - `400 Bad Request` â€” HTML-parsing eller hÃ¤mtning misslyckades

**API-endpoint:**
```
POST /api/quick-import
Input:  { input: string }
Output: { markdown: string, source: 'ica' | 'pdf' | 'other' }
```

**Proxy-route (Next.js):**
- `/api/quick-import-proxy` â€” Proxies till backend
- Hanterar error-konvertering (BE HTTP â†’ FE error message)
- Returnerar Markdown eller JSON-error

### Markdown-format och parsningsregler

### Markdown-format och parsningsregler

**Format:**
```markdown
# Receptnamn

Valfri beskrivning av receptet.

## Ingredienser
- 500 g kÃ¶ttfÃ¤rs
- 1 st lÃ¶k
- 2.5 msk tomatpurÃ©
- 1 dl grÃ¤dde (vispgrÃ¤dde)
- salt

## TillvÃ¤gagÃ¥ngssÃ¤tt
Stek lÃ¶ken i lite smÃ¶r. TillsÃ¤tt kÃ¶ttfÃ¤rsenâ€¦
```

**Parsningsregler i detalj:**
| Element | Tolkning |
|---------|----------|
| `# Rubrik` | Receptnamn (fÃ¶rsta H1) |
| Text mellan H1 och `## Ingredienser` | Receptbeskrivning (flera rader OK, valfritt) |
| `## Ingredienser` | Ingrediens markerare (case-insensitive) |
| `- ANTAL ENHET NAMN` | Ingrediens: quantity=ANTAL, unit=ENHET, name=NAMN |
| `- ANTAL NAMN` | Ingrediens utan enhet: unit sÃ¤tts till "st" |
| `- NAMN` | Ingrediens utan kvantitet: quantity=0, unit="" |
| `(text i parentes)` | Ingrediensnot (sparas, t.ex. "vispgrÃ¤dde") |
| `## TillvÃ¤gagÃ¥ngssÃ¤tt` / `## Instruktioner` | Instruktionssektion markerare |
| Text under instruktioner | Tillagningsinstruktioner (flera rader OK) |

**Exempel ingrediensparsning:**
```
"- 500 g kÃ¶ttfÃ¤rs"              â†’ {quantity: 500, unit: "g", rawName: "kÃ¶ttfÃ¤rs"}
"- 1,5 dl grÃ¤dde (vispgrÃ¤dde)"  â†’ {quantity: 1.5, unit: "dl", rawName: "grÃ¤dde", note: "vispgrÃ¤dde"}
"- 3 Ã¤gg"                       â†’ {quantity: 3, unit: "st", rawName: "Ã¤gg"}
"- salt"                        â†’ {quantity: 0, unit: "", rawName: "salt"}
```

### Komponenter och dataflÃ¶de


#### 2. Backend: `POST /api/recipes/parse-markdown` endpoint

- `@prisma/client` â€” Databasaccess
- Common utils: `normalize-name.ts`

**ProcessflÃ¶de:**

```
1. Motta: ParseMarkdownDto { markdown: string }
2. Anropa parseRecipeMarkdown() â†’ ParsedRecipe
3. HÃ¤mta alla aktiva produkter frÃ¥n DB
4. FÃ¶r varje ingrediens:
   a. Normalisera: lowercase + trim + remove accents/punctuation
   b. Matchningsalgoritm (se nedan)
   c. Top 5 fÃ¶rslag sortera efter score
5. Returnera ParsedRecipe + suggestions
```

**Matchningsalgoritm:**
```
Normalisering: lowercase + trim + Ã¥Ã¤Ã¶-handling + skilljetecken-borttagning

1. EXAKT MATCH (100 poÃ¤ng)
   IF (ingrediens == product.canonicalName_normalized) OR 
      (ingrediens == product.normalizedName_normalized)
   THEN score = 100

2. DELSTRÃ„NG-MATCH (70 poÃ¤ng)
   IF (ingrediens IN product.name_normalized) OR
      (product.name_normalized IN ingrediens)
   THEN score = 70

3. LEVENSHTEIN-LIKHET (40â€“100 poÃ¤ng)
   Calculate Levenshtein distance between ingrediens and product.name_normalized
   similarity% = (1 - (distance / max_length)) * 100
   IF similarity% >= 40 THEN score = similarity%
   ELSE filter out

Sortering: HÃ¶gsta poÃ¤ng fÃ¶rst
Top 5: Max 5 fÃ¶rslag per ingrediens
```

**Svarsobjekt:**
```json
{
  "name": "KÃ¶ttfÃ¤rssÃ¥s",
  "description": "En klassiskâ€¦",
  "instructions": "Stek lÃ¶kenâ€¦",
  "ingredients": [
    {
      "rawName": "kÃ¶ttfÃ¤rs",
      "quantity": 500,
      "unit": "g",
      "note": null,
      "suggestions": [
        { "productId": 12, "productName": "KÃ¶ttfÃ¤rs", "score": 100 },
        { "productId": 34, "productName": "BlandfÃ¤rs", "score": 65 },
        { "productId": 56, "productName": "NÃ¶tfÃ¤rs", "score": 55 }
      ]
    }
  ]
}
```

#### 3. Frontend: Receptskapsidor

**Huvudmeny: `/recipes/create/page.tsx`**
- Presenterar tvÃ¥ val-kort (card-baserad UI)
- "Skriv in recept" â†’ `/recipes/write`
- "Importera frÃ¥n fil/lÃ¤nk" â†’ `/recipes/import`

**Skriv in recept: `/recipes/write/WriteRecipePage.tsx`**
- Main client component (3-steps state machine)
- Samma logik som tidigare `ImportRecipePage`
- **Steg 1:** Markdown-inmatning
- **Steg 2:** Granska ingredienser, vÃ¤lj produkter
- **Steg 3:** Spara recept
- AnvÃ¤nder `/api/parse-markdown-proxy` fÃ¶r backend-anrop

**Importera frÃ¥n fil: `/recipes/import/ImportFilePage.tsx`**
- Tabs/toggle mellan tvÃ¥ metoder:
  1. **Fil-upload** â€” Dra-och-slÃ¤pp eller vÃ¤lja PDF/TXT/DOCX
  2. **URL-import** â€” Ange lÃ¤nk till receptsida
- Placeholder fÃ¶r framtida integration
- Visar tips fÃ¶r att anvÃ¤nda "Skriv in recept" tills dessa funktioner Ã¤r klara

#### 4. API-proxy-route (Next.js)

**`/api/parse-markdown-proxy/route.ts`**
- POST-endpoint
- Proxies anrop till backend `POST /api/recipes/parse-markdown`
- Hanterar CORS, headers, error-svarsÃ¶versÃ¤ttning

---

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

### DataflÃ¶de

```
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=...

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

### 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)
- `handleServingsChange()` POSTar direkt till backend och uppdaterar lokal state utan sidomladdning

### Portionsskalning i backend

```typescript
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
```

---

## Enhetskonvertering (backendsida)

### StÃ¶dda enhetstyper

| Typ | Enheter | Bassystem |
|-----|---------|-----------|
| **Vikt** | g, kg | gram (g) |
| **Volym** | ml, dl | milliliter (ml) |
| **Portioner** | tsk, msk | tesked (tsk), dÃ¤r 1 msk = 3 tsk |
| **Stycken** | st | kan inte konverteras |

### Normalisering (inom `RecipesService.normalizeUnit()`)

| Input | Output | Typ |
|-------|--------|-----|
| "tesked", "test" | "tsk" | Portion |
| "matsled", "matsked" | "msk" | Portion |
| "gram" | "g" | Vikt |
| "kilogram", "kilo", "kg" | "kg" | Vikt |
| "milliliter" | "ml" | Volym |
| "deciliter" | "dl" | Volym |
| "stycke" | "st" | Styck |
| (other) | (as-is) | (as-is) |

### Konverteringslogik (`convertUnit()` metod)

```typescript
convertUnit(quantity: number, fromUnit: string, toUnit: string, productName: string): number {
  // 1. Validering (quantity > 0, units inte tomma)
  // 2. Normalisera bÃ¥da enheter
  // 3. Om identiska efter normalisering â†’ return quantity
  // 4. BestÃ¤m enhetstyp fÃ¶r bÃ¥da
  // 5. Om olika typer â†’ throw Error
  // 6. Konvertera via basenhet:
  //    quantity * factor[fromUnit] / factor[toUnit]
}
```

### AnvÃ¤ndning i `getInventoryPreview()`

DÃ¥ receptjÃ¤mfÃ¶relse jÃ¤mfÃ¶rs mot inventarie:
```
FÃ¶r varje ingrediens:
  1. HÃ¤mta alla inventariefÃ¶remÃ¥l fÃ¶r denna produkt
  2. Gruppera efter enhet:
     - Samma enhet som ingrediens â†’ summera direkt
     - Annan enhet â†’ fÃ¶rsÃ¶k konvertera
  3. Om konvertering misslyckas (t.ex. st â†” g) â†’ hoppa Ã¶ver denna post
  4. Summera totalt available (samma + konverterad)
  5. Returnera status: rÃ¤cker | saknas | enhetskonflikt
```

---

## Infrastruktur & DevOps

### Docker Compose setup

**Services:**
- `recipe-frontend` â€” Next.js container (port 3000)
- `recipe-api` â€” NestJS backend (port 8080)
- `db` â€” MariaDB database (port 3306)
- `proxy` â€” (valfritt) Caddy reverse proxy

**Volumer:**
- Database data persistence
- Build layers caching

**Networks:**
- External `proxy` network (fÃ¶r Caddy integrering, valfritt)
- Intern `recipe-network` mellan services

### Backend-Dockerfile (3-stage build)


**Stage 2: builder**
```dockerfile
FROM node:22-alpine AS builder
# Installera backend-deps
# Kopiera converter frÃ¥n stage 1
# Generera Prisma-klient
# Bygg NestJS-appen (nest build)
```

**Stage 3: runner**
```dockerfile
FROM node:22-alpine AS runner
# Minimal production image
# Enbart dist/, node_modules/, prisma/
```


Build-kommando:
```bash
docker compose build recipe-api
docker compose up -d recipe-api
```

### Frontend-Dockerfile

Standard Next.js build â†’ standalone output

### MiljÃ¶variabler

Konfigureras via `.env` eller `docker compose up`:
- `DATABASE_URL` â€” MariaDB-anslutning (backend)
- `PORT` â€” Backend port (default 8080)
- Ev. Caddy-konfiguration

---

## UTF-8 och lokalisering

- Database: utf8mb4
- Backend: Normaliseringsfunktion hanterar Ã¥Ã¤Ã¶
- Frontend: Svenska felmeddelanden och UI-text
- Endpoints: Svenska benÃ¤mningar och kategori-namn

---

## SÃ¤kerhet & Utbyggbarhet

- **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
- Delade recept / recept-export
- Push-notifieringar fÃ¶r utgÃ¥ngna varor
- Nutrition-baserat receptfÃ¶rslag
- Allergi-tracking per anvÃ¤ndare

---

## Databaskonfiguration och seeding

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

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

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

**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. |

**KÃ¶ra seed_all.sql:**

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

**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
- Dryck
- FÃ¤rdigmat
- Fryst
- Frukt & GrÃ¶nt
- Glass, godis & snacks
- 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Ã¥ 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:

```bash
# 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.

---

# Framtida projekt - Companion-projekt

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

### 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)
- Docker kontainer

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

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

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

### Arkitektur fÃ¶r Microservice Shopping/Todo-lista
Se ovan samt nedan: Gemensam arkitektur fÃ¶r companion-projekt

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

**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).

---


## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.

## 2026-05-12: Alias-scopehantering (admin), kategori-chip, beroendehärdning och quality-gates

### Alias-scopehantering (admin → global)

- **Ny backend-regel:** Admin kan ändra ett privat alias till globalt via `PATCH /receipt-alias/:id`. Vanliga användare blockeras med `ForbiddenException` om de försöker ändra `isGlobal`.
- **Global → privat skyddad:** Om ett globalt alias saknar `ownerId` returneras `BadRequestException` vid försök att göra det privat — systemet hindrar föräldralösa privata alias.
- **DTO utökat:** `UpdateReceiptAliasDto` har fått `isGlobal?: boolean` med `@IsBoolean()`.
- **Admin-UI:** Admin alias-panelen visar nu en switch för att höja ett privat alias till globalt. Switchen är låst (disabled) om aliaset redan är globalt — riktning är enbart privat → global. Bekräftelse-chip visas temporärt (6 s) i listan efter scope-byte.
- **Tester:** Nya tester täcker:
  - Admin kan höja privat → global.
  - Vanlig användare blockeras från scope-ändring.
  - Global → privat utan owner ger `BadRequestException`.
  - Controller delegerar korrekt DTO med `isGlobal` till service.

### Kategori-chip

- Fallback-chip för tom kategoriväg returnerar nu `SizedBox.shrink()` globalt i den gemensamma chip-buildern — "okänd"-chip visas inte längre i admin- eller importpaneler.

### Beroendehärdning (Nest 11 + multer 2.1.1)

- Backend uppgraderat till **NestJS 11** (`@nestjs/common/core/platform-express/testing/jwt/passport` 11.x).
- `multer` uppgraderat till **2.1.1** — eliminerar tidigare high-severity CVE i transitivt beroende.
- `@types/express` uppgraderat till **5.0.5**, `@nestjs/cli` **11.0.21**, `@nestjs/schematics` **11.1.0**.
- `JWT_SECRET` fail-fast check tillagd i `jwt.strategy.ts`; `JWT_SECRET` satt i CI-env.
- `npm audit --audit-level=high` rapporterar **0 sårbarheter**.

### Quality-gates (npm scripts + CI)

Nya scripts i `backend/package.json`:

| Script | Kommando |
|---|---|
| `prisma:validate` | `prisma validate --schema prisma/schema.prisma` |
| `typecheck` | `tsc --noEmit` |
| `audit:high` | `npm audit --audit-level=high` |
| `quality:ci` | Kedja: validate → generate → typecheck → test → build → audit |

CI-workflow (`.gitea/workflows/test.yml`) uppdaterad:
- Alla Prisma-steg använder nu `npm run prisma:validate`.
- Audit-steg använder `npm run audit:high`.
- Nytt **typecheck-steg** tillagt i både PR-snabbjobb och push-fulljobb.
- `npm ci` används genomgående (var `npm install` i delar av flödet).

# Prisma Query Logging

För att aktivera Prisma query logging i testmiljön:

1. **Sätt miljövariabeln** `PRISMA_LOG_QUERIES=1` i din miljö.
   - För Docker Compose: lägg till i `.env`-filen eller direkt i `compose.yml` under `environment`.
   - Exempel för `compose.yml`:
     ```yaml
     environment:
       PRISMA_LOG_QUERIES: "1"
     ```

2. **Starta om containern** så att den nya miljövariabeln laddas:
   ```bash
   docker compose up -d recipe-api
   ```

3. **Verifiera** att loggar visas i backendens konsol eller loggfil. Loggarna inkluderar alla SQL-frågor, parametrar och exekveringstider.

> **Notera:**
> - Aktivera endast i test/staging, inte i produktion.
> - Loggarna kan vara omfattande och påverka prestanda.
> - Variabeln är avsiktligt inte dokumenterad i huvudkonfigurationen för att undvika oavsiktlig aktivering.
