Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2781b29f5a | |||
| 9028c79215 |
@@ -1 +0,0 @@
|
||||
Flytta manuellt mappen recipe-document-converter till _archive/recipe-document-converter. Verktyget kan inte flytta kataloger automatiskt i denna miljö.
|
||||
@@ -0,0 +1,79 @@
|
||||
Read memory [](file:///c%3A/Users/Nils-JohanGynther/AppData/Roaming/Code/User/workspaceStorage/e6ea1b0bd55239bec87a0a6ab7819f74/GitHub.copilot-chat/memory-tool/memories/NTkxM2ZhMmYtYjViYi00YTE0LTg2NGEtNmYyYzZjMTcxNWEw/microservice-todo.md)
|
||||
|
||||
Jag kan inte direkt flytta filer mellan minnesområdet och projektmappen, men här är hela planen. Kopiera och spara innehållet nedan som recipe-app/microservice-todo.md i din projektmapp:
|
||||
|
||||
---
|
||||
|
||||
## Plan: Microservice-Todo (Shopping/Todo-list)
|
||||
|
||||
En skalbar, Dockeriserad microservice för shopping/todo-listor, med tight integration mot recipe-app men även möjlighet att köras fristående (SQLite fallback). Backend i NestJS/Prisma, REST API med JWT och användarscope, enkel Flutter-webbklient, och DevOps/CI/CD enligt befintliga mönster.
|
||||
|
||||
**Steg**
|
||||
|
||||
### FAS 1: Backend och Databas
|
||||
1. Skapa nytt NestJS-projekt (t.ex. `todo-api`) med Prisma ORM.
|
||||
2. Definiera Prisma-schema för `TodoList`, `TodoItem`, och användarrelation (userId, foreign key mot User i recipe-db om integrerat, annars lokal User-tabell för standalone/SQLite).
|
||||
3. Implementera MariaDB som primär databas, med fallback till SQLite (env-styrd konfiguration).
|
||||
4. Skapa migreringar och seed-data för test/demo.
|
||||
5. Implementera JWT-baserad autentisering och användarscope på alla endpoints.
|
||||
6. Bygg REST API:
|
||||
- CRUD för listor och items (alla operationer användarspecifika)
|
||||
- Endpoint för att ta emot poster från recipe-app (t.ex. POST /api/todo/from-recipe)
|
||||
- Health endpoint (GET /health)
|
||||
7. Lägg till admin-endpoints (valfritt): lista alla användares listor/items, ta bort, återställ.
|
||||
|
||||
### FAS 2: Dockerisering och DevOps
|
||||
8. Skriv Dockerfile (multi-stage, production-ready, .dockerignore).
|
||||
9. Lägg till service i compose.yml och ev. `compose.override.yml` för recipe-app.
|
||||
10. Automatisera migreringar vid start (entrypoint-script eller Docker CMD).
|
||||
11. Lägg till healthcheck i Docker och Compose.
|
||||
12. Dokumentera miljövariabler och konfigurationsmöjligheter (MariaDB/SQLite, JWT-secret, etc).
|
||||
|
||||
### FAS 3: Flutter Webbklient
|
||||
13. Skapa nytt Flutter-projekt (t.ex. `todo-flutter`) enligt recipe-app-mönster.
|
||||
14. Implementera login (JWT), lista/skapa/ta bort todo-listor och items, markera som klar, enkel UI.
|
||||
15. Använd Riverpod, GoRouter, och ARB-lokalisering (svenska/engelska).
|
||||
16. Lägg till Dockerfile och Compose-service för Flutter-klienten.
|
||||
17. Testa integration mot backend (same-origin /api, JWT i header).
|
||||
|
||||
### FAS 4: Integration och Extensibilitet
|
||||
18. Definiera API-kontrakt för integration med recipe-app (t.ex. POST /api/todo/from-recipe, schema för item-data).
|
||||
19. Lägg till möjlighet att skicka todo-poster från recipe-app (t.ex. via webhook eller direkt API-anrop).
|
||||
20. Förbered för framtida AI-funktioner (t.ex. smarta förslag, auto-kategorisering) med extensibel tjänstestruktur.
|
||||
|
||||
### FAS 5: Testning och Verifiering
|
||||
21. Skriv integrationstester och E2E-tester för backend och frontend.
|
||||
22. Säkerställ DTO-validering och roll/accesskontroller.
|
||||
23. Testa fallback till SQLite (standalone-läge).
|
||||
24. Dokumentera testflöden och verifieringssteg.
|
||||
|
||||
**Relevanta filer**
|
||||
- `todo-api/prisma/schema.prisma` — datamodell och migreringar
|
||||
- `todo-api/src/main.ts`, `app.module.ts` — bootstrap och modulstruktur
|
||||
- `todo-api/src/todo/todo.controller.ts` — REST endpoints
|
||||
- `todo-api/src/todo/todo.service.ts` — affärslogik
|
||||
- `todo-api/Dockerfile`, `.dockerignore` — Dockerisering
|
||||
- `todo-flutter/lib/` — Flutter-klientens kod
|
||||
- compose.yml, `compose.override.yml` — Compose-integration
|
||||
|
||||
**Verifiering**
|
||||
1. Alla endpoints kräver JWT och filtrerar på userId.
|
||||
2. CRUD på listor/items fungerar i både MariaDB och SQLite-läge.
|
||||
3. Flutter-klienten kan logga in, visa, skapa och markera todo-poster.
|
||||
4. Integration med recipe-app: poster kan skickas och dyker upp i todo-listan.
|
||||
5. Health endpoints och Docker healthchecks fungerar.
|
||||
6. Alla tester (integration/E2E) passerar.
|
||||
|
||||
**Beslut och avgränsningar**
|
||||
- Scope: Enkel todo/shopping-lista, användarscope, tight integration, men ingen avancerad AI/UX i v1.
|
||||
- Exkluderar: Mobilappar, avancerad AI, komplexa adminflöden, realtidsuppdateringar.
|
||||
- Följer recipe-apps patterns för auth, API, Docker, Compose, testning och lokalisering.
|
||||
|
||||
**Vidare överväganden**
|
||||
1. Hur ska användar-identity hanteras i standalone-läge? Rekommendation: lokal User-tabell med enkel auth.
|
||||
2. Hur ska recipe-app skicka poster? Rekommendation: direkt API-anrop med JWT, schema-dokumentation.
|
||||
3. Hur mycket adminfunktionalitet behövs? Rekommendation: börja enkelt, utöka vid behov.
|
||||
|
||||
---
|
||||
|
||||
Vill du ha planen i ett annat format eller med ytterligare detaljer?
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "recipe-document-converter",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { parseRecipeMarkdown } from './parser';
|
||||
export type { ParsedIngredient, ParsedRecipe } from './parser';
|
||||
@@ -1,146 +0,0 @@
|
||||
export interface ParsedIngredient {
|
||||
rawName: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
export interface ParsedRecipe {
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
ingredients: ParsedIngredient[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsar ett recept i Markdown-format och extraherar namn, beskrivning,
|
||||
* instruktioner och ingredienser.
|
||||
*
|
||||
* Förväntat format:
|
||||
* # Receptnamn
|
||||
* Beskrivning (valfritt stycke efter titeln)
|
||||
*
|
||||
* ## Ingredienser
|
||||
* - 400 g kycklingfilé
|
||||
* - 2 dl grädde (eller crème fraiche)
|
||||
*
|
||||
* ## Instruktioner
|
||||
* 1. Stek kycklingen …
|
||||
*/
|
||||
export function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
let name = '';
|
||||
let description = '';
|
||||
let instructions = '';
|
||||
const ingredients: ParsedIngredient[] = [];
|
||||
|
||||
let currentSection: 'none' | 'description' | 'ingredients' | 'instructions' = 'none';
|
||||
const descriptionLines: string[] = [];
|
||||
const instructionLines: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// H1 — receptnamn
|
||||
if (/^#\s+/.test(trimmed) && !trimmed.startsWith('##')) {
|
||||
name = trimmed.replace(/^#\s+/, '').trim();
|
||||
currentSection = 'description';
|
||||
continue;
|
||||
}
|
||||
|
||||
// H2 — sektionsrubriker
|
||||
if (/^##\s+/.test(trimmed)) {
|
||||
const heading = trimmed.replace(/^##\s+/, '').trim().toLowerCase();
|
||||
if (/ingrediens/.test(heading)) {
|
||||
currentSection = 'ingredients';
|
||||
} else if (/instruktion|tillagning|gör så här|steg/.test(heading)) {
|
||||
currentSection = 'instructions';
|
||||
} else {
|
||||
currentSection = 'none';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Samla rader beroende på sektion
|
||||
switch (currentSection) {
|
||||
case 'description':
|
||||
if (trimmed.length > 0) {
|
||||
descriptionLines.push(trimmed);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ingredients':
|
||||
if (/^[-*]\s+/.test(trimmed)) {
|
||||
const ingredientText = trimmed.replace(/^[-*]\s+/, '');
|
||||
ingredients.push(parseIngredientLine(ingredientText));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'instructions':
|
||||
if (trimmed.length > 0) {
|
||||
instructionLines.push(trimmed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
description = descriptionLines.join('\n');
|
||||
instructions = instructionLines.join('\n');
|
||||
|
||||
return { name, description, instructions, ingredients };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsar en ingrediensrad, t.ex.:
|
||||
* "400 g kycklingfilé"
|
||||
* "2 dl grädde (eller crème fraiche)"
|
||||
* "1 kruka basilika"
|
||||
* "salt"
|
||||
*/
|
||||
function parseIngredientLine(text: string): ParsedIngredient {
|
||||
const trimmed = text.trim();
|
||||
|
||||
// Extrahera eventuell parentes-not i slutet
|
||||
let note: string | null = null;
|
||||
let main = trimmed;
|
||||
const parenMatch = trimmed.match(/\(([^)]+)\)\s*$/);
|
||||
if (parenMatch) {
|
||||
note = parenMatch[1].trim();
|
||||
main = trimmed.slice(0, parenMatch.index).trim();
|
||||
}
|
||||
|
||||
// Försök matcha "kvantitet enhet namn" — t.ex. "400 g kycklingfilé" eller "2.5 dl grädde"
|
||||
const fullMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/);
|
||||
if (fullMatch) {
|
||||
return {
|
||||
quantity: parseNumber(fullMatch[1]),
|
||||
unit: fullMatch[2],
|
||||
rawName: fullMatch[3].trim(),
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
// Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
|
||||
const noUnitMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(.+)$/);
|
||||
if (noUnitMatch) {
|
||||
return {
|
||||
quantity: parseNumber(noUnitMatch[1]),
|
||||
unit: 'st',
|
||||
rawName: noUnitMatch[2].trim(),
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
// Bara ett namn, ingen kvantitet — t.ex. "salt"
|
||||
return {
|
||||
quantity: 0,
|
||||
unit: '',
|
||||
rawName: main,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
function parseNumber(s: string): number {
|
||||
return parseFloat(s.replace(',', '.'));
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2021"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user