Convert submodule to regular directory
This commit is contained in:
Submodule recipe-document-converter deleted from e13c39a757
@@ -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: <PDF-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: <PDF-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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 {}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+1
Submodule recipe-document-converter/recipe-document-converter added at 34f2279eb2
Reference in New Issue
Block a user