diff --git a/recipe-document-converter b/recipe-document-converter deleted file mode 160000 index e13c39a7..00000000 --- a/recipe-document-converter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e13c39a7574adbc094b2fb60ae30a007ae2d89ad diff --git a/recipe-document-converter/CONTROLLER_EXAMPLE.ts b/recipe-document-converter/CONTROLLER_EXAMPLE.ts new file mode 100644 index 00000000..40fe02e4 --- /dev/null +++ b/recipe-document-converter/CONTROLLER_EXAMPLE.ts @@ -0,0 +1,60 @@ +/** + * RECIPE CONTROLLER INTEGRATION EXAMPLE + * + * This shows how to add import endpoints to your existing recipe controller. + * Add these methods to backend/src/modules/recipes/recipes.controller.ts + */ + +import { Controller, Post, UseInterceptors, UploadedFile, Get } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ImportService } from '../import/import.service'; +import { RecipesService } from './recipes.service'; + +@Controller('recipes') +export class RecipesController { + constructor( + private recipesService: RecipesService, + private importService: ImportService, // Add this + ) {} + + // ... existing endpoints ... + + /** + * Import a recipe from PDF without saving + * Useful for preview before saving + * + * POST /recipes/import/pdf + * Content-Type: multipart/form-data + * Body: { file: } + */ + @Post('import/pdf') + @UseInterceptors(FileInterceptor('file')) + async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { + return this.importService.importRecipeFromPDF(file); + } + + /** + * Import a recipe from PDF and save it to database + * + * POST /recipes/import/pdf/save + * Content-Type: multipart/form-data + * Body: { file: } + */ + @Post('import/pdf/save') + @UseInterceptors(FileInterceptor('file')) + async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { + const imported = await this.importService.importRecipeFromPDF(file); + return this.importService.saveImportedRecipe(imported.data); + } + + /** + * Check if import service is available + * Useful for health monitoring + * + * GET /recipes/import/health + */ + @Get('import/health') + async importServiceHealth() { + return this.importService.checkImportServiceHealth(); + } +} diff --git a/recipe-document-converter/Caddyfile.fixed b/recipe-document-converter/Caddyfile.fixed new file mode 100644 index 00000000..e84a0d99 --- /dev/null +++ b/recipe-document-converter/Caddyfile.fixed @@ -0,0 +1,113 @@ +(common) { + encode gzip zstd + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + } +} + +test.gynther.se { + respond "det fungerar" +} + +bazarr.gynther.se { + import common + reverse_proxy http://bazarr:6767 +} + +prowlarr.gynther.se { + import common + reverse_proxy http://prowlarr:9696 +} + +radarr.gynther.se { + import common + reverse_proxy http://radarr:7878 +} + +sonarr.gynther.se { + import common + reverse_proxy http://sonarr:8989 +} + +jellyfin.gynther.se { + reverse_proxy http://jellyfin:8096 +} + +qbittorrent.gynther.se { + import common + reverse_proxy 192.168.50.4:8080 +} + +wetty.gynther.se { + import common + basic_auth { + admin $2a$14$DahHUWD2cKyXJ96sH5VQwuQv1bqmIn0gsdoSaw4mofzfdNY2Y0VsO + } + redir / /wetty + reverse_proxy wetty:3000 +} + +portainer.gynther.se { + reverse_proxy portainer:9000 +} + +gitea.gynther.se { + import common + reverse_proxy 192.168.50.2:3002 +} + +# ============================================ +# 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 +} diff --git a/recipe-document-converter/Caddyfile.production b/recipe-document-converter/Caddyfile.production new file mode 100644 index 00000000..d2350fda --- /dev/null +++ b/recipe-document-converter/Caddyfile.production @@ -0,0 +1,126 @@ +# Production Caddyfile för recipe-app + import-service +# Uppdaterad för import-service integration +# Placera denna fil på: /opt/containers/caddy/conf/Caddyfile + +(common) { + encode gzip zstd + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + } +} + +test.gynther.se { + respond "det fungerar" +} + +bazarr.gynther.se { + import common + reverse_proxy http://bazarr:6767 +} + +prowlarr.gynther.se { + import common + reverse_proxy http://prowlarr:9696 +} + +radarr.gynther.se { + import common + reverse_proxy http://radarr:7878 +} + +sonarr.gynther.se { + import common + reverse_proxy http://sonarr:8989 +} + +jellyfin.gynther.se { + reverse_proxy http://jellyfin:8096 +} + +qbittorrent.gynther.se { + import common + reverse_proxy 192.168.50.4:8080 +} + +wetty.gynther.se { + import common + basic_auth { + admin $2a$14$DahHUWD2cKyXJ96sH5VQwuQv1bqmIn0gsdoSaw4mofzfdNY2Y0VsO + } + redir / /wetty + reverse_proxy wetty:3000 +} + +portainer.gynther.se { + reverse_proxy portainer:9000 +} + +gitea.gynther.se { + import common + reverse_proxy 192.168.50.2:3002 +} + +# ============================================ +# Import Service (Document Converter) - Standalone UI +# ============================================ +import.gynther.se { + import common + reverse_proxy recipe-import-service:3000 +} + +# ============================================ +# Recipe App + Import Service Integration +# ============================================ +recept.gynther.se { + import common + + # === IMPORT SERVICE (Document Converter) === + # Dessa endpoints måste komma FÖRST innan backend/frontend reglerna! + # POST /api/recipes/import/pdf - Importera PDF (preview) + # POST /api/recipes/import/pdf/save - Importera och spara + # GET /api/recipes/import/health - Health check + + handle /api/recipes/import* { + reverse_proxy recipe-import-service:3000 + } + + # === RECIPE FRONTEND PROXY ENDPOINTS === + # Proxy-endpoints för Next.js API routes (måste komma FÖRE backend-reglerna!) + 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 === + # Proxy specifika backend-endpoints + 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 === + # Health endpoint för backend + handle /health { + reverse_proxy recipe-api:8080 + } + + # === CATCH ALL === + # Övriga /api/* går till Next.js frontend + handle /api/* { + reverse_proxy recipe-frontend:3000 + } + + # Alla andra requests går till frontend (Next.js) + reverse_proxy recipe-frontend:3000 +} diff --git a/recipe-document-converter/DEPLOYMENT_GUIDE.md b/recipe-document-converter/DEPLOYMENT_GUIDE.md new file mode 100644 index 00000000..bd5f9dd3 --- /dev/null +++ b/recipe-document-converter/DEPLOYMENT_GUIDE.md @@ -0,0 +1,368 @@ +# 🚀 Deployment Guide: Import Service Integration + +Din nuvarande setup: +``` +recept.gynther.se → Caddy Proxy + ├─ recipe-frontend:3000 (Next.js) + ├─ recipe-api:8080 (NestJS Backend) + └─ MariaDB +``` + +Ny setup (med import-service): +``` +recept.gynther.se → Caddy Proxy + ├─ recipe-frontend:3000 (Next.js) + ├─ recipe-api:8080 (NestJS Backend) *← kan anropa import-service* + ├─ recipe-import-service:3000 (NestJS Import) *← NYT* + └─ MariaDB +``` + +--- + +## 📋 Steg-för-steg deployment + +### 1. Uppdatera Caddy configuration + +**Option A: Manuell uppdatering** +```bash +# SSH till server +ssh user@server.se + +# Backup nuvarande Caddyfile +cp /opt/containers/caddy/conf/Caddyfile /opt/containers/caddy/conf/Caddyfile.backup + +# Uppdatera med nya import-service reglerna +# Se Caddyfile.production för den kompletta konfigurationen +``` + +**Option B: Använda versionerad fil** +```bash +# Kopiera Caddyfile.production till servern +scp Caddyfile.production user@server.se:/opt/containers/caddy/conf/Caddyfile + +# Reload Caddy +docker exec caddy-proxy caddy reload +``` + +--- + +### 2. Starta import-service container + +**Om du använder din nuvarande docker-compose:** + +```bash +# SSH till server +ssh user@server.se + +# Navigera till import-service mapp +cd /path/to/recipe-document-converter/recipe-document-converter + +# Build och starta +docker-compose up -d recipe-import-service + +# Eller använd production docker-compose: +docker-compose -f docker-compose.production.yml up -d recipe-import-service +``` + +**Kontrollera att den körs:** +```bash +docker ps | grep import-service +docker logs recipe-import-service + +# Test health endpoint +curl http://localhost:3000/health +``` + +--- + +### 3. Integrera import-service i recipe-app backend + +Lägg till i ditt `recipe-app/backend` projekt: + +**Skapa `src/modules/import/import.service.ts`:** +```typescript +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import FormData from 'form-data'; +import * as fs from 'fs'; +import { PrismaService } from '../../prisma/prisma.service'; + +interface RecipeImportDTO { + name: string; + ingredients: Array<{ name: string; quantity?: number; unit?: string }>; + instructions: string[]; + metadata?: { prepTime?: string; servings?: number }; +} + +@Injectable() +export class ImportService { + private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://recipe-import-service:3000'; + + constructor(private prisma: PrismaService) {} + + async importRecipeFromPDF(file: Express.Multer.File) { + try { + if (!file || file.mimetype !== 'application/pdf') { + throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); + } + + const form = new FormData(); + form.append('file', fs.createReadStream(file.path)); + + const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { + headers: form.getHeaders(), + timeout: 30000, + }); + + if (!response.data.success) { + throw new HttpException(response.data.error, HttpStatus.BAD_REQUEST); + } + + return { success: true, data: response.data.structuredData, metadata: response.data.pdfMetadata }; + } catch (error) { + if (file?.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); + if (error instanceof axios.AxiosError) { + throw new HttpException(`Import misslyckades: ${error.message}`, HttpStatus.SERVICE_UNAVAILABLE); + } + throw error; + } + } + + async saveImportedRecipe(recipeData: RecipeImportDTO) { + try { + const recipe = await this.prisma.recipe.create({ + data: { + name: recipeData.name, + instructions: recipeData.instructions.join('\n'), + prepTime: recipeData.metadata?.prepTime, + servings: recipeData.metadata?.servings, + ingredients: { + create: recipeData.ingredients.map((ing) => ({ + name: ing.name, + quantity: ing.quantity, + unit: ing.unit, + })), + }, + }, + include: { ingredients: true }, + }); + return { success: true, recipe }; + } catch (error) { + throw new HttpException('Kunde inte spara recept', HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} +``` + +**Skapa `src/modules/import/import.module.ts`:** +```typescript +import { Module } from '@nestjs/common'; +import { ImportService } from './import.service'; + +@Module({ + providers: [ImportService], + exports: [ImportService], +}) +export class ImportModule {} +``` + +**Uppdatera `src/modules/recipes/recipes.controller.ts`:** +```typescript +import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ImportService } from '../import/import.service'; + +@Controller('recipes') +export class RecipesController { + constructor(private importService: ImportService) {} + + @Post('import/pdf') + @UseInterceptors(FileInterceptor('file')) + async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { + return this.importService.importRecipeFromPDF(file); + } + + @Post('import/pdf/save') + @UseInterceptors(FileInterceptor('file')) + async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { + const imported = await this.importService.importRecipeFromPDF(file); + return this.importService.saveImportedRecipe(imported.data); + } +} +``` + +**Uppdatera `src/app.module.ts`:** +```typescript +import { ImportModule } from './modules/import/import.module'; + +@Module({ + imports: [ + // ... existing modules + ImportModule, + ], +}) +export class AppModule {} +``` + +--- + +### 4. Uppdatera miljövariabler + +**`recipe-app/backend/.env` eller docker environment:** +```env +DATABASE_URL=mysql://recipe_user:secure_password@recipe-db:3306/recipe_db +IMPORT_SERVICE_URL=http://recipe-import-service:3000 +``` + +--- + +### 5. Deploy + +**Lokalt (development):** +```bash +# Terminal 1: import-service +cd recipe-document-converter/recipe-document-converter +npm install +npm run start:dev + +# Terminal 2: recipe-app backend +cd recipe-app/backend +npm install +IMPORT_SERVICE_URL=http://localhost:3000 npm run start:dev + +# Terminal 3: recipe-app frontend +cd recipe-app/frontend +npm run dev +``` + +**Production (Docker):** +```bash +# Option A: Starta import-service separat +ssh user@server.se +cd /path/to/recipe-document-converter +docker-compose up -d recipe-import-service + +# Option B: Använd production docker-compose (all-in-one) +docker-compose -f docker-compose.production.yml up -d + +# Reload Caddy +docker exec caddy-proxy caddy reload +``` + +--- + +## 🧪 Testing + +### Test 1: Import-service health +```bash +curl https://recept.gynther.se/api/recipes/import/health +``` + +**Expected Response:** +```json +{ + "status": "ok", + "timestamp": "...", + "service": "recipe-import-service", + "version": "1.0.0" +} +``` + +### Test 2: Import PDF (preview) +```bash +curl -X POST \ + -F "file=@recipe.pdf" \ + https://recept.gynther.se/api/recipes/import/pdf +``` + +### Test 3: Import & Save +```bash +curl -X POST \ + -F "file=@recipe.pdf" \ + https://recept.gynther.se/api/recipes/import/pdf/save +``` + +--- + +## 📊 Routing Overview + +``` +https://recept.gynther.se + ↓ +Caddy Proxy (:443) + ├─ / → recipe-frontend:3000 + ├─ /api/recipes/import/* → recipe-import-service:3000 + ├─ /api/products* → recipe-api:8080 + ├─ /api/inventory* → recipe-api:8080 + ├─ /api/recipes* → recipe-api:8080 (BUT /api/recipes/import/* intercepts here) + └─ /api/* → recipe-frontend:3000 +``` + +**Wichtigt:** Import-endpoints måste komma FÖRE andra /api/recipes* regler för att inte fastna! + +--- + +## 🔍 Troubleshooting + +### Import-service inte tillgänglig +```bash +# Kontrollera container +docker ps | grep import-service + +# Se loggar +docker logs recipe-import-service + +# Kontrollera Caddy routing +docker exec caddy-proxy curl http://recipe-import-service:3000/health + +# Reload Caddy +docker exec caddy-proxy caddy reload +``` + +### 502 Bad Gateway från Caddy +```bash +# Kontrollera att import-service körs +docker logs recipe-import-service + +# Kontrollera nätverksanslutning +docker network inspect recipe-network + +# Verify environment variable +docker inspect recipe-api | grep IMPORT_SERVICE_URL +``` + +### Filuppladdning misslyckas +```bash +# Kontrollera volym permissions +docker exec recipe-import-service ls -la /app/uploads/ + +# Kontrollera filstorlek +ls -lh recipe.pdf + +# Öka MAX_FILE_SIZE om behövs (standard: 50MB) +docker exec recipe-import-service env | grep MAX_FILE_SIZE +``` + +--- + +## ✅ Deployment Checklist + +- [ ] Caddy.production är uppdaterad med import-service reglerna +- [ ] Import-service container är byggd och klar +- [ ] ImportModule är tillagd i recipe-app backend +- [ ] IMPORT_SERVICE_URL är satt i miljövariabler +- [ ] axios är installerat i backend +- [ ] Caddy reloadad +- [ ] Health endpoints testad +- [ ] PDF import testad +- [ ] Loggar monitorade + +--- + +## 🎯 Nästa Steg + +1. **LLM Integration (Mistral)** - För avancerad recepttolkning +2. **Excel/Word support** - Expandera filformat +3. **Caching** - Redis för snabbare import +4. **Error Monitoring** - Lägg till Sentry eller liknande +5. **Rate Limiting** - Skydda import-endpoint diff --git a/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts b/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts new file mode 100644 index 00000000..30bd47b0 --- /dev/null +++ b/recipe-document-converter/IMPORT_MODULE_EXAMPLE.ts @@ -0,0 +1,15 @@ +/** + * RECIPE APP MODULE SETUP + * + * Add import service to your recipe app backend. + * File: backend/src/modules/import/import.module.ts + */ + +import { Module } from '@nestjs/common'; +import { ImportService } from './import.service'; + +@Module({ + providers: [ImportService], + exports: [ImportService], +}) +export class ImportModule {} diff --git a/recipe-document-converter/INTEGRATION_EXAMPLE.ts b/recipe-document-converter/INTEGRATION_EXAMPLE.ts new file mode 100644 index 00000000..d30aacce --- /dev/null +++ b/recipe-document-converter/INTEGRATION_EXAMPLE.ts @@ -0,0 +1,168 @@ +/** + * INTEGRATION GUIDE: RECIPE IMPORT SERVICE + * + * This file shows how to integrate recipe-document-converter with recipe-app backend. + * Copy and adapt this code to your recipe-app/backend directory. + * + * Path: backend/src/modules/import/import.service.ts + */ + +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import FormData from 'form-data'; +import * as fs from 'fs'; +import { PrismaService } from '../../prisma/prisma.service'; // Adjust path based on your project + +interface RecipeImportDTO { + name: string; + ingredients: Array<{ + name: string; + quantity?: number; + unit?: string; + }>; + instructions: string[]; + metadata?: { + prepTime?: string; + servings?: number; + author?: string; + }; +} + +@Injectable() +export class ImportService { + private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://import-service:3000'; + + constructor(private prisma: PrismaService) {} + + /** + * Upload a PDF file to the import service and extract recipe data + * + * Usage in your recipe controller: + * @Post('import/pdf') + * @UseInterceptors(FileInterceptor('file')) + * async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { + * return this.importService.importRecipeFromPDF(file); + * } + */ + async importRecipeFromPDF(file: Express.Multer.File) { + try { + // Validate file + if (!file) { + throw new HttpException('Ingen fil angiven', HttpStatus.BAD_REQUEST); + } + + if (file.mimetype !== 'application/pdf') { + throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); + } + + // Call import-service + const form = new FormData(); + form.append('file', fs.createReadStream(file.path)); + + const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { + headers: form.getHeaders(), + timeout: 30000, + }); + + if (!response.data.success) { + throw new HttpException(response.data.error || 'Receptextrahering misslyckades', HttpStatus.BAD_REQUEST); + } + + // Extract and validate recipe data + const recipeData = this.mapImportDataToRecipe(response.data.structuredData); + + return { + success: true, + data: recipeData, + metadata: response.data.pdfMetadata, + }; + } catch (error) { + // Clean up uploaded file + if (file && file.path && fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + + if (error instanceof axios.AxiosError) { + throw new HttpException( + `Receptextrahering misslyckades: ${error.message}`, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + throw error; + } + } + + /** + * Save imported recipe to database + * + * Usage in your recipe controller: + * @Post('import/pdf/save') + * @UseInterceptors(FileInterceptor('file')) + * async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { + * const imported = await this.importService.importRecipeFromPDF(file); + * return this.importService.saveImportedRecipe(imported.data); + * } + */ + async saveImportedRecipe(recipeData: RecipeImportDTO) { + try { + // Create recipe with ingredients + const recipe = await this.prisma.recipe.create({ + data: { + name: recipeData.name, + instructions: recipeData.instructions.join('\n'), + prepTime: recipeData.metadata?.prepTime, + servings: recipeData.metadata?.servings, + author: recipeData.metadata?.author, + ingredients: { + create: recipeData.ingredients.map((ing) => ({ + name: ing.name, + quantity: ing.quantity, + unit: ing.unit, + })), + }, + }, + include: { + ingredients: true, + }, + }); + + return { + success: true, + message: 'Recept sparat', + recipe, + }; + } catch (error) { + throw new HttpException( + `Kunde inte spara recept: ${error instanceof Error ? error.message : 'Okänt fel'}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Map import service data to recipe format + */ + private mapImportDataToRecipe(importedData: any): RecipeImportDTO { + return { + name: importedData.name || 'Importerat recept', + ingredients: importedData.ingredients || [], + instructions: importedData.instructions || [], + metadata: importedData.metadata || {}, + }; + } + + /** + * Health check for import service (useful for monitoring) + */ + async checkImportServiceHealth() { + try { + const response = await axios.get(`${this.IMPORT_SERVICE_URL}/health`, { + timeout: 5000, + }); + return response.data; + } catch { + throw new HttpException('Import-service är ej tillgänglig', HttpStatus.SERVICE_UNAVAILABLE); + } + } +} diff --git a/recipe-document-converter/INTEGRATION_GUIDE.md b/recipe-document-converter/INTEGRATION_GUIDE.md new file mode 100644 index 00000000..d0216728 --- /dev/null +++ b/recipe-document-converter/INTEGRATION_GUIDE.md @@ -0,0 +1,558 @@ +# 🔌 Integration Guide: Recipe Import Service + +Denna guide visar hur du integrerar **recipe-document-converter** (import-service) med din befintliga **recipe-app**. + +## 📋 Innehållsförteckning + +1. [Installation & Setup](#installation--setup) +2. [Arkitektur](#arkitektur) +3. [Backend Integration](#backend-integration) +4. [Docker Deployment](#docker-deployment) +5. [API Endpoints](#api-endpoints) +6. [Testing](#testing) +7. [Troubleshooting](#troubleshooting) + +--- + +## Installation & Setup + +### 1. Krav + +- **recipe-app** — Din befintliga receptapp (Next.js frontend + NestJS backend) +- **recipe-document-converter** — Import-service (denna repo) +- **Docker** 24+ och **Docker Compose** +- **Node.js** 22.x + +### 2. Klona och organisera projekten + +```bash +dev/ +├── recipe-app/ # Din befintliga app +│ ├── frontend/ +│ ├── backend/ +│ ├── Dockerfile +│ └── compose.yml +│ +└── recipe-document-converter/ # Import-service + ├── recipe-document-converter/ + ├── Dockerfile + ├── docker-compose.yml + ├── Caddyfile + └── README.md +``` + +### 3. Uppdatera recipe-document-converter paket + +```bash +cd recipe-document-converter/recipe-document-converter + +# TypeScript är redan uppdaterad till 5.4.5 ✅ +# Installera dependencies +npm install +``` + +--- + +## Arkitektur + +### System Diagram + +``` +┌─────────────────────────────────────────────────────────┐ +│ Caddy Reverse Proxy │ +│ (port 80/443) │ +└────────────┬────────────────┬─────────────────┬─────────┘ + │ │ │ + ┌───────▼───┐ ┌──────▼────────┐ ┌──▼─────────┐ + │ Frontend │ │ Backend API │ │ Import │ + │ :4000 │ │ :3001 │ │ :3000 │ + │ │ │ (NestJS) │ │ (NestJS) │ + │ Next.js │ │ │ │ │ + │ 16.2 │ │ Recipes ┐ │ │ PDF │ + │ │ │ Inventory ├─────┼──extract │ + └───────────┘ │ Products │ │ │ structure │ + │ │ │ │ │ + └──────┬────┘ │ └──────┬──────┘ + │ │ │ + ┌──────▼──────────────┐ │ + │ MariaDB 11 │ │ + │ (recipe_db) │ │ + └─────────────────────┘ │ + │ + ┌─────────────▼─┐ + │ Uploads/ │ + │ (Volym) │ + └───────────────┘ +``` + +### Data Flow + +``` +1. Användare laddar upp PDF + ↓ +2. recipe-app frontend → backend POST /recipes/import/pdf + ↓ +3. Backend → FormData → import-service POST /import/pdf + ↓ +4. import-service extraherar → JSON (ingredients, instructions, metadata) + ↓ +5. Backend validerar + sparar i Prisma/MariaDB + ↓ +6. Frontend visar importerat recept +``` + +--- + +## Backend Integration + +### Steg 1: Uppdatera recipe-app backend + +```bash +cd recipe-app/backend + +# Installera axios om det saknas +npm install axios + +# Om du använder FormData +npm install form-data +``` + +### Steg 2: Skapa import-modul + +Skapa `backend/src/modules/import/import.service.ts`: + +```typescript +import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import FormData from 'form-data'; +import * as fs from 'fs'; +import { PrismaService } from '../../prisma/prisma.service'; + +interface RecipeImportDTO { + name: string; + ingredients: Array<{ + name: string; + quantity?: number; + unit?: string; + }>; + instructions: string[]; + metadata?: { + prepTime?: string; + servings?: number; + }; +} + +@Injectable() +export class ImportService { + private readonly IMPORT_SERVICE_URL = process.env.IMPORT_SERVICE_URL || 'http://import-service:3000'; + + constructor(private prisma: PrismaService) {} + + async importRecipeFromPDF(file: Express.Multer.File) { + try { + if (!file) { + throw new HttpException('Ingen fil angiven', HttpStatus.BAD_REQUEST); + } + + if (file.mimetype !== 'application/pdf') { + throw new HttpException('Endast PDF-filer tillåtna', HttpStatus.BAD_REQUEST); + } + + // Call import-service + const form = new FormData(); + form.append('file', fs.createReadStream(file.path)); + + const response = await axios.post(`${this.IMPORT_SERVICE_URL}/import/pdf`, form, { + headers: form.getHeaders(), + timeout: 30000, + }); + + if (!response.data.success) { + throw new HttpException(response.data.error, HttpStatus.BAD_REQUEST); + } + + const recipeData = this.mapImportDataToRecipe(response.data.structuredData); + + return { + success: true, + data: recipeData, + metadata: response.data.pdfMetadata, + }; + } catch (error) { + if (file?.path && fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + + if (error instanceof axios.AxiosError) { + throw new HttpException( + `Import misslyckades: ${error.message}`, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + throw error; + } + } + + async saveImportedRecipe(recipeData: RecipeImportDTO) { + try { + const recipe = await this.prisma.recipe.create({ + data: { + name: recipeData.name, + instructions: recipeData.instructions.join('\n'), + prepTime: recipeData.metadata?.prepTime, + servings: recipeData.metadata?.servings, + ingredients: { + create: recipeData.ingredients.map((ing) => ({ + name: ing.name, + quantity: ing.quantity, + unit: ing.unit, + })), + }, + }, + include: { ingredients: true }, + }); + + return { success: true, message: 'Recept sparat', recipe }; + } catch (error) { + throw new HttpException( + `Kunde inte spara recept: ${error instanceof Error ? error.message : 'Okänt fel'}`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private mapImportDataToRecipe(importedData: any): RecipeImportDTO { + return { + name: importedData.name || 'Importerat recept', + ingredients: importedData.ingredients || [], + instructions: importedData.instructions || [], + metadata: importedData.metadata || {}, + }; + } +} +``` + +### Steg 3: Skapa import-modul + +Skapa `backend/src/modules/import/import.module.ts`: + +```typescript +import { Module } from '@nestjs/common'; +import { ImportService } from './import.service'; + +@Module({ + providers: [ImportService], + exports: [ImportService], +}) +export class ImportModule {} +``` + +### Steg 4: Lägg till endpoints i recipes controller + +Uppdatera `backend/src/modules/recipes/recipes.controller.ts`: + +```typescript +import { Controller, Post, Get, UseInterceptors, UploadedFile } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ImportService } from '../import/import.service'; + +@Controller('recipes') +export class RecipesController { + constructor(private importService: ImportService) {} + + /** + * Import recipe from PDF without saving + * POST /recipes/import/pdf + */ + @Post('import/pdf') + @UseInterceptors(FileInterceptor('file')) + async importRecipeFromPDF(@UploadedFile() file: Express.Multer.File) { + return this.importService.importRecipeFromPDF(file); + } + + /** + * Import and save recipe + * POST /recipes/import/pdf/save + */ + @Post('import/pdf/save') + @UseInterceptors(FileInterceptor('file')) + async importAndSaveRecipe(@UploadedFile() file: Express.Multer.File) { + const imported = await this.importService.importRecipeFromPDF(file); + return this.importService.saveImportedRecipe(imported.data); + } + + /** + * Check import service health + * GET /recipes/import/health + */ + @Get('import/health') + async importServiceHealth() { + return this.importService.checkImportServiceHealth(); + } +} +``` + +### Steg 5: Registrera import-modul + +Uppdatera `backend/src/app.module.ts`: + +```typescript +import { ImportModule } from './modules/import/import.module'; + +@Module({ + imports: [ + // ... existing modules + ImportModule, + ], +}) +export class AppModule {} +``` + +--- + +## Docker Deployment + +### Option 1: Lokal utveckling (utan Docker) + +```bash +# Terminal 1: Start import-service +cd recipe-document-converter/recipe-document-converter +npm install +npm run start:dev + +# Terminal 2: Start recipe-app backend +cd recipe-app/backend +npm install +IMPORT_SERVICE_URL=http://localhost:3000 npm run start:dev + +# Terminal 3: Start recipe-app frontend +cd recipe-app/frontend +npm install +npm run dev +``` + +**Access:** +- Frontend: http://localhost:3000 (Next.js default) +- Backend: http://localhost:3001/api +- Import: http://localhost:3000/import + +--- + +### Option 2: Docker Compose (Full Stack) + +**Förutsättningar:** +- `recipe-app/` ligger bredvid `recipe-document-converter/` +- recipe-app har `Dockerfile` i root + +**Starta all-in-one:** + +```bash +cd recipe-document-converter/recipe-document-converter + +# Bygg och starta alla tjänster +docker-compose up -d + +# Eller med bygge +docker-compose up -d --build +``` + +**Tjänster:** +``` +- Frontend: http://localhost (via Caddy) +- Backend API: http://localhost/api +- Import Service: http://localhost/import +- Health: http://localhost/health +``` + +**Se loggar:** +```bash +docker-compose logs -f recipe-app-backend +docker-compose logs -f import-service +docker-compose logs -f recipe-db +docker-compose logs -f caddy +``` + +**Stoppa allt:** +```bash +docker-compose down +``` + +--- + +## API Endpoints + +### Import Service Endpoints + +| Method | Endpoint | Beskrivning | Auth | +|--------|----------|-------------|------| +| `GET` | `/health` | Hälsokontroll | Nej | +| `POST` | `/import/pdf` | Importera PDF | Nej | + +### Recipe App Backend Integration Endpoints + +| Method | Endpoint | Beskrivning | +|--------|----------|-------------| +| `POST` | `/api/recipes/import/pdf` | Importera PDF (preview) | +| `POST` | `/api/recipes/import/pdf/save` | Importera och spara | +| `GET` | `/api/recipes/import/health` | Health check | + +--- + +## Testing + +### 1. Test import-service isolerat + +```bash +curl -X POST \ + -F "file=@recipe.pdf" \ + http://localhost:3000/import/pdf +``` + +**Response:** +```json +{ + "success": true, + "rawText": "...", + "pdfMetadata": { + "fileName": "recipe.pdf", + "pages": 1, + "author": "Unknown" + }, + "structuredData": { + "name": "Kycklingcurry", + "ingredients": [...], + "instructions": [...] + } +} +``` + +### 2. Test recipe-app integration + +```bash +curl -X POST \ + -F "file=@recipe.pdf" \ + http://localhost:3001/api/recipes/import/pdf +``` + +### 3. Test health checks + +```bash +# Import service +curl http://localhost:3000/health + +# Recipe app backend +curl http://localhost:3001/health + +# Import from backend +curl http://localhost:3001/api/recipes/import/health +``` + +### 4. Test full end-to-end (Docker) + +```bash +# Start services +docker-compose up -d + +# Wait for services to be healthy +sleep 10 + +# Check all services +curl http://localhost/health +curl http://localhost/api/health +curl http://localhost/import/health + +# Import recipe +curl -X POST \ + -F "file=@recipe.pdf" \ + http://localhost/api/recipes/import/pdf/save +``` + +--- + +## Troubleshooting + +### Import Service är inte tillgänglig + +```bash +# Kontrollera om den körs +docker ps | grep import-service + +# Se loggar +docker logs recipe-import-service + +# Kontrollera hälsa +curl http://localhost:3000/health +``` + +### Docker nätverk-error + +```bash +# Reinitialize docker-compose +docker-compose down +docker system prune -a +docker-compose up --build +``` + +### Filuppladdning misslyckas + +```bash +# Kontrollera filstorlek (default 50MB) +ls -lh recipe.pdf + +# Kontrollera uploads-mapp permissions +docker exec recipe-import-service ls -la /app/uploads/ +``` + +### TypeScript fel + +```bash +# Uppdatera dependencies +npm install + +# Clear cache +npm cache clean --force + +# Rebuild +npm run build +``` + +### Databas-anslutning misslyckad + +```bash +# Kontrollera MariaDB +docker logs recipe-db + +# Kontrollera URL +echo $DATABASE_URL + +# Test direkten +docker exec recipe-db mysql -urecipe_user -psecure_password -e "USE recipe_db; SHOW TABLES;" +``` + +--- + +## Production Checklist + +- [ ] TypeScript compilerar utan fel +- [ ] Alla miljövariabler är satta +- [ ] Database-backup är konfigurerat +- [ ] Import-service är tillgänglig från backend +- [ ] Caddy certifikater är satta upp +- [ ] Log rotation är konfigurerat +- [ ] Health endpoints fungerar +- [ ] File uploads permissions är rätt +- [ ] Rate limiting är konfigurerat (if needed) +- [ ] Monitoring setup är på plats + +--- + +## Nästa steg + +1. **LLM Integration** — Lägg till Mistral för avancerad strukturering +2. **Excel/Word support** — Expandera till fler filformat +3. **OCR** — Lägg till stöd för skannade dokument +4. **Caching** — Implementera Redis för snabbare import +5. **Authentication** — Lägg till auth om behövs + +--- diff --git a/recipe-document-converter/README.md b/recipe-document-converter/README.md new file mode 100644 index 00000000..01104002 --- /dev/null +++ b/recipe-document-converter/README.md @@ -0,0 +1,89 @@ +# recipe-document-converter + +🍳 **A Docker-based import service for converting recipe documents (PDF, Word, Excel, Images) into structured JSON data.** + +## 🎯 Project Overview + +This repository contains a **microservice architecture** designed to: + +1. **Extract** recipe content from multiple document formats +2. **Structure** unorganized data using LLM (Mistral) for intelligent parsing +3. **Integrate** seamlessly with recipe applications via REST API +4. **Scale** independently using Docker containerization + +## 📦 Structure + +``` +recipe-document-converter/ +├── recipe-document-converter/ # Main import service (this is the actual service) +│ ├── src/ # TypeScript source code +│ ├── Dockerfile # Container definition +│ ├── docker-compose.yml # Multi-service orchestration +│ ├── package.json # Dependencies +│ └── README.md # Service documentation +└── README.md # This file +``` + +## 🚀 Quick Start + +### Using Docker Compose (Recommended) + +```bash +cd recipe-document-converter + +# Start the service +docker-compose up -d + +# Test the service +curl http://localhost:3000/health +``` + +### Local Development + +```bash +cd recipe-document-converter + +# Install dependencies +npm install + +# Start development server +npm run start:dev +``` + +## 📖 API Endpoints + +| Method | Endpoint | Description | +|--------|---------------|--------------------------------| +| `GET` | `/health` | Health check | +| `POST` | `/import/pdf` | Import and extract PDF recipe | + +## 📚 Full Documentation + +See [recipe-document-converter/README.md](recipe-document-converter/README.md) for complete documentation. + +## 🔮 Planned Features + +- [x] PDF extraction +- [x] Basic recipe structuring +- [ ] Mistral LLM integration +- [ ] Excel support +- [ ] Word support +- [ ] Image OCR support +- [ ] Web scraping + +## 🛠️ Tech Stack + +- **NestJS** — Node.js framework +- **TypeScript** — Type safety +- **Docker** — Containerization +- **pdf-parse** — PDF extraction +- **Zod** — Schema validation (coming soon) +- **Mistral AI** — LLM integration (coming soon) + +## 🤝 Contributing + +Contributions welcome! Open an issue or submit a PR. + +## 📄 License + +MIT diff --git a/recipe-document-converter/docker-compose.import-only.yml b/recipe-document-converter/docker-compose.import-only.yml new file mode 100644 index 00000000..f52cf691 --- /dev/null +++ b/recipe-document-converter/docker-compose.import-only.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + recipe-import-service: + build: + context: ./recipe-document-converter + dockerfile: Dockerfile + image: recipe-import-service:local + container_name: recipe-import-service + restart: unless-stopped + environment: + NODE_ENV: "production" + PORT: "3000" + LOG_LEVEL: "info" + MAX_FILE_SIZE: "50000000" + volumes: + - recipe_imports:/app/uploads + networks: + - recipe-internal + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + recipe_imports: + +networks: + recipe-internal: + driver: bridge diff --git a/recipe-document-converter/docker-compose.production.yml b/recipe-document-converter/docker-compose.production.yml new file mode 100644 index 00000000..4cf7ae9e --- /dev/null +++ b/recipe-document-converter/docker-compose.production.yml @@ -0,0 +1,162 @@ +# Production Docker Compose för recipe-app + import-service +# +# Denna konfiguration: +# - Körs med diagram som visat i Caddyfile.production +# - Integrerar recipe-app (frontend + backend) + import-service +# - Använder MariaDB databas +# - Caddy som reverse proxy +# +# Usage: +# docker-compose -f docker-compose.production.yml up -d +# +# Anpassningar: +# - Uppdatera MYSQL_ROOT_PASSWORD och MYSQL_PASSWORD +# - Uppdatera DATABASE_URL för backend +# - Säkerställ att alla services kan nå varandra via docker-network + +version: '3.8' + +services: + # ============================================ + # Frontend: Next.js 16.2 + # ============================================ + recipe-frontend: + build: + context: ./recipe-app/frontend + dockerfile: Dockerfile + container_name: recipe-frontend + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=https://recept.gynther.se/api + networks: + - recipe-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ============================================ + # Backend: NestJS 10.3 + Prisma + # ============================================ + recipe-api: + build: + context: ./recipe-app/backend + dockerfile: Dockerfile + container_name: recipe-api + depends_on: + recipe-db: + condition: service_healthy + recipe-import-service: + condition: service_healthy + environment: + - NODE_ENV=production + - PORT=8080 + - DATABASE_URL=mysql://recipe_user:${DB_PASSWORD:-secure_password}@recipe-db:3306/recipe_db?schema=public + - IMPORT_SERVICE_URL=http://recipe-import-service:3000 + - LOG_LEVEL=info + networks: + - recipe-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ============================================ + # Database: MariaDB 11 + # ============================================ + recipe-db: + image: mariadb:11 + container_name: recipe-db + environment: + - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-root_password} + - MYSQL_DATABASE=recipe_db + - MYSQL_USER=recipe_user + - MYSQL_PASSWORD=${DB_PASSWORD:-secure_password} + - MYSQL_INITDB_SKIP_TZINFO=1 + volumes: + - recipe-db-data:/var/lib/mysql + networks: + - recipe-network + restart: unless-stopped + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + # ============================================ + # Import Service: Document Converter (NestJS) + # ============================================ + recipe-import-service: + build: + context: ./recipe-document-converter/recipe-document-converter + dockerfile: Dockerfile + container_name: recipe-import-service + environment: + - NODE_ENV=production + - PORT=3000 + - LOG_LEVEL=info + - MAX_FILE_SIZE=50000000 + volumes: + - recipe-imports-data:/app/uploads + networks: + - recipe-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ============================================ + # Reverse Proxy: Caddy 2.x + # ============================================ + caddy: + image: caddy:2.7 + container_name: caddy-proxy + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile.production:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + networks: + - recipe-network + depends_on: + - recipe-frontend + - recipe-api + - recipe-import-service + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +# ============================================ +# Networks +# ============================================ +networks: + recipe-network: + driver: bridge + +# ============================================ +# Volumes +# ============================================ +volumes: + recipe-db-data: + driver: local + recipe-imports-data: + driver: local + caddy-data: + driver: local + caddy-config: + driver: local diff --git a/recipe-document-converter/docker-compose.your-setup.yml b/recipe-document-converter/docker-compose.your-setup.yml new file mode 100644 index 00000000..5ea82598 --- /dev/null +++ b/recipe-document-converter/docker-compose.your-setup.yml @@ -0,0 +1,89 @@ +services: + recipe-frontend: + image: recipe-frontend:local + container_name: recipe-frontend + restart: unless-stopped + environment: + NODE_ENV: "production" + HOSTNAME: "0.0.0.0" + PORT: "3000" + NEXT_PUBLIC_APP_URL: "https://recept.gynther.se" + NEXT_PUBLIC_API_URL: "https://api.recept.gynther.se" + NEXT_PUBLIC_API_URL_INTERNAL: "http://recipe-api:8080" + networks: + - proxy + - recipe-internal + + recipe-api: + image: recipe-api:local + container_name: recipe-api + restart: unless-stopped + depends_on: + recipe-db: + condition: service_healthy + recipe-import-service: + condition: service_healthy + environment: + NODE_ENV: "production" + DATABASE_URL: "mysql://recipe_user:Imminent-Umpire-Undertook8-Crunchy@recipe-db:3306/recipe_app" + # New: Import service URL for backend to call + IMPORT_SERVICE_URL: "http://recipe-import-service:3000" + networks: + - proxy + - recipe-internal + + recipe-db: + image: mariadb:11 + container_name: recipe-db + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: "Encrust6-Deserve-Stricken-Spectacle" + MARIADB_DATABASE: "recipe_app" + MARIADB_USER: "recipe_user" + MARIADB_PASSWORD: "Imminent-Umpire-Undertook8-Crunchy" + volumes: + - recipe_db_data:/var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + networks: + - recipe-internal + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + # === NEW: Import Service (Document Converter) === + recipe-import-service: + build: + context: ./recipe-document-converter/recipe-document-converter + dockerfile: Dockerfile + image: recipe-import-service:local + container_name: recipe-import-service + restart: unless-stopped + environment: + NODE_ENV: "production" + PORT: "3000" + LOG_LEVEL: "info" + MAX_FILE_SIZE: "50000000" + volumes: + - recipe_imports:/app/uploads + networks: + - recipe-internal # Only internal communication with recipe-api + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + recipe_db_data: + recipe_imports: + +networks: + proxy: + external: true + recipe-internal: + driver: bridge diff --git a/recipe-document-converter/recipe-document-converter b/recipe-document-converter/recipe-document-converter new file mode 160000 index 00000000..34f2279e --- /dev/null +++ b/recipe-document-converter/recipe-document-converter @@ -0,0 +1 @@ +Subproject commit 34f2279eb2bfa89356bb20193f2c909a7d1d4bad