New import in version 0.1

This commit is contained in:
Nils-Johan Gynther
2026-04-11 15:38:24 +02:00
parent 8552c6f757
commit 5448da1b98
12 changed files with 868 additions and 7 deletions
+171
View File
@@ -1,5 +1,7 @@
# Teknisk beskrivning av Recipe App
> Se [README.md](README.md) för användarinformation och kom-igång-guide.
## Översikt
Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och inköpsplanering. 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.
@@ -114,6 +116,7 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
- Skapa, redigera, ta bort recept
- Jämför mot hemmavaror (räcker/saknas/enhetskonflikt)
- Visar instruktioner och saknade ingredienser
- **Importera recept från Markdown** (se nedan)
### Produkter (Admin)
- Sök, sortera, redigera canonical name
@@ -135,12 +138,180 @@ Recipe App är en fullstack-applikation för hantering av hemmavaror, recept och
- `POST /api/recipes` Skapa recept
- `PATCH /api/recipes/:id` Uppdatera recept
- `DELETE /api/recipes/:id` Ta bort recept
- `POST /api/recipes/parse-markdown` Tolka Markdown-recept (se nedan)
- `GET /api/products` Lista produkter
- `PATCH /api/products/:id` Uppdatera produkt
- `GET /health` Hälsokontroll
---
---
## Receptimport via Markdown
### Syfte
Användaren kan importera ett recept skrivet i ett enkelt Markdown-format istället för att fylla i formularet manuellt. Systemet tolkar texten, föreslår matchande produkter från databasen och låter användaren granska och bekräfta innan receptet sparas.
### Markdown-format
```markdown
# Receptnamn
Valfri beskrivning av receptet.
## Ingredienser
- 500 g köttfärs
- 1 st lök
- 2 msk tomatpuré
- 1 dl grädde (vispgrädde)
## Tillvägagångssätt
Stek löken i lite smör. Tillsats köttfärsen...
```
Regler:
- Rad med `#` tolkas som receptnamn
- Text mellan `#`-rubriken och `## Ingredienser` tolkas som beskrivning
- Rader under `## Ingredienser` med mönstret `- ANTAL ENHET NAMN` tolkas som ingredienser
- Text i parentes efter ingrediensnamnet (`(vispgrädde)`) sparas som anteckning
- Text under `## Tillvägagångssätt` (eller `## Instruktioner`) tolkas som instruktioner
### Arkitektur
```
Användaren Frontend Backend Bibliotek
(klistrar in MD) → /recipes/import → POST /api/ → recipe-document-
ImportRecipePage recipes/ converter/
parse-markdown parseRecipeMarkdown()
Granskar förslag
Väljer produkter
POST /api/recipes (befintlig endpoint)
```
### Komponenterna
#### `recipe-document-converter/` (fristående TypeScript-bibliotek)
Ett eget npm-paket som inte har externa beroenden. Det enda som exporteras är:
```typescript
parseRecipeMarkdown(markdown: string): ParsedRecipe
```
Returnerar:
```typescript
type ParsedRecipe = {
name: string;
description?: string;
instructions?: string;
ingredients: Array<{
rawName: string; // fråntext, t.ex. "köttfärs"
quantity: number; // t.ex. 500
unit: string; // t.ex. "g"
note?: string; // text i parentes, t.ex. "nötfärs"
}>;
};
```
Biblioteket kompileras i ett separat Docker-byggsteg och länkas till backend via `"recipe-document-converter": "file:../recipe-document-converter"` i `backend/package.json`.
#### Backend — `POST /api/recipes/parse-markdown`
Endpoint som tar emot `{ markdown: string }` och returnerar det tolkade receptet åtsamman med produktmatchförslag för varje ingrediens.
Matchningslogik:
1. Anropar `parseRecipeMarkdown()` från biblioteket
2. Hämtar alla aktiva produkter ur databasen
3. Jämför varje ingrediensnamn mot `product.canonicalName` / `product.normalizedName` med tre metoder i ordning:
- **Exakt match** (efter normalisering) → 100 poäng
- **Delsträngsmatch** → 70 poäng
- **Levenshtein-likhet** → 0100 poäng (filtreras under 40)
4. Returnerar upp till 5 förslag per ingrediens, sorterade efter poäng
Svar:
```json
{
"name": "Köttfärssås",
"description": "En klassisk...",
"instructions": "Stek löken...",
"ingredients": [
{
"rawName": "köttfärs",
"quantity": 500,
"unit": "g",
"suggestions": [
{ "productId": 12, "productName": "Köttfärs", "score": 100 },
{ "productId": 34, "productName": "Blandfärs", "score": 55 }
]
}
]
}
```
#### Frontend — `/recipes/import`
En 3-stegsvy (client component):
| Steg | Innehåll |
|------|----------|
| 1. Klistra in | Textarea för Markdown + "Tolka recept"-knapp |
| 2. Granska | Redigerbara fält för namn/beskrivning/instruktioner; varje ingrediens har en dropdown med föreslagna produkter överst, sedan alla produkter |
| 3. Spara | Knapp som POSTar till befintlig `POST /api/recipes` |
Ingrediensräder med ingen matchning markeras visuellt (gul ram) så att användaren ser att de behöver väljas manuellt. Receptet sparas inte förrän minst en ingrediens har en vald produkt.
Flöde i Next.js:
```
/recipes/import
└─ ImportRecipePage.tsx (client component, 3-stegsflödet)
/api/parse-markdown-proxy
└─ route.ts (POST-proxy till backend, omgår CORS)
```
### Docker-bygget
Backend-Dockerfilen använder nu projektets rot (`.`) som byggkontext. Converter-biblioteket kompileras i en separat stage:
```dockerfile
# Stage 1: Bygg converter-biblioteket
FROM node:22-alpine AS converter-build
WORKDIR /converter
COPY recipe-document-converter/package.json ./
RUN npm install
COPY recipe-document-converter/src ./src
COPY recipe-document-converter/tsconfig.json ./
RUN npm run build
# Stage 2: Installera backend-beroenden (converter kopieras in)
FROM node:22-alpine AS deps
WORKDIR /app
COPY --from=converter-build /converter /recipe-document-converter
...
```
Bygga om backend efter ändringar:
```bash
docker compose build recipe-api
```
### Relevanta filer
| Fil | Syfte |
|-----|-------|
| `recipe-document-converter/src/parser.ts` | Markdown-parser |
| `recipe-document-converter/src/index.ts` | Biblioteksexport |
| `backend/src/recipes/dto/parse-markdown.dto.ts` | Inkommande DTO |
| `backend/src/recipes/recipes.controller.ts` | Nytt endpoint |
| `backend/src/recipes/recipes.service.ts` | Matchningslogik |
| `frontend/app/recipes/import/ImportRecipePage.tsx` | 3-stegsvy |
| `frontend/app/api/parse-markdown-proxy/route.ts` | Proxy-route |
---
## Säkerhet
- Ingen auth i grundutförande (kan enkelt byggas på)