# Teknisk beskrivning av Recipe App

> Se [README.md](README.md) fÃ¶r anvÃ¤ndarinformation och kom-igÃ¥ng-guide.  
> Se [NEXT_STEPS.md](NEXT_STEPS.md) fÃ¶r fÃ¶rslag pÃ¥ nÃ¤sta steg i projektet.  
> Se [AI-FUNKTIONER.md](AI-FUNKTIONER.md) fÃ¶r planerade AI-funktioner och modellval.

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

### Ny databasarkitektur: user-scoped produkter

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

`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 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](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
- Kvittoimport (Fas 6b) är påbörjad men granskningssteg och bulk-spara återstår.
- Bildimport kräver att containrar är uppdaterade med senaste kod — kontrollera att diagnostikloggar syns vid felsökning.
- Vissa adminfunktioner och avancerad AI-integration är planerade men ej migrerade.

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

---

