# ⚙️ Luna Recipes — Tech-Stack

## 1. Übersicht

```
┌─────────────────────────────────────────┐
│              Frontend (PWA)             │
│  React 19 + Vite + TailwindCSS v4      │
│  React Router v7 · Tanstack Query v5   │
├─────────────────────────────────────────┤
│              Backend (API)              │
│  Fastify v5 + better-sqlite3           │
│  Bild-Upload: @fastify/multipart       │
├─────────────────────────────────────────┤
│             Datenbank                   │
│  SQLite (WAL-Mode) + FTS5              │
└─────────────────────────────────────────┘
```

## 2. Frontend

### Core
| Paket | Version | Zweck |
|---|---|---|
| `react` | ^19.0 | UI-Framework |
| `react-dom` | ^19.0 | DOM-Rendering |
| `vite` | ^6.x | Build-Tool, Dev-Server, HMR |
| `tailwindcss` | ^4.x | Utility-first CSS |
| `react-router` | ^7.x | Client-Side Routing |
| `@tanstack/react-query` | ^5.x | Server-State, Caching, Mutations |

### UI & Interaktion
| Paket | Zweck |
|---|---|
| `react-masonry-css` | Pinterest-artiges Grid-Layout |
| `framer-motion` | Animationen, Page-Transitions, Swipe-Gesten |
| `react-hot-toast` | Toast-Benachrichtigungen |
| `lucide-react` | Icon-Set (sauber, konsistent) |
| `react-hook-form` + `zod` | Formulare + Validierung |

### PWA
| Paket | Zweck |
|---|---|
| `vite-plugin-pwa` | Service Worker Generation, Manifest |
| `workbox` (via Plugin) | Caching-Strategien, Offline-Support |

### Projektstruktur

```
frontend/
├── public/
│   ├── icons/              ← PWA Icons (192, 512px)
│   └── manifest.webmanifest
├── src/
│   ├── main.tsx
│   ├── App.tsx
│   ├── api/                ← API-Client (fetch-Wrapper)
│   │   ├── client.ts
│   │   ├── recipes.ts
│   │   ├── shopping.ts
│   │   └── types.ts
│   ├── components/
│   │   ├── layout/         ← AppShell, BottomNav, TopBar
│   │   ├── recipe/         ← RecipeCard, IngredientList, StepList
│   │   ├── cooking/        ← CookingStep, CookingTimer
│   │   ├── shopping/       ← ShoppingItem, ShoppingSection
│   │   ├── forms/          ← RecipeForm, ImageUpload
│   │   └── ui/             ← Button, Badge, Input, Skeleton
│   ├── pages/
│   │   ├── HomePage.tsx
│   │   ├── SearchPage.tsx
│   │   ├── RecipePage.tsx
│   │   ├── CookingModePage.tsx
│   │   ├── ShoppingPage.tsx
│   │   ├── CreateRecipePage.tsx
│   │   └── SettingsPage.tsx
│   ├── hooks/              ← useRecipes, useTimer, useWakeLock
│   ├── lib/                ← Hilfsfunktionen, Konstanten
│   └── styles/
│       └── globals.css     ← Tailwind-Imports, Custom Properties
├── index.html
├── tailwind.config.ts
├── vite.config.ts
├── tsconfig.json
└── package.json
```

### PWA-Konfiguration (vite.config.ts)

```typescript
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['icons/*.png'],
      manifest: {
        name: 'Luna Recipes',
        short_name: 'Luna',
        description: 'Deine persönliche Rezeptsammlung',
        theme_color: '#C4737E',
        background_color: '#FBF8F5',
        display: 'standalone',
        orientation: 'portrait',
        start_url: '/',
        icons: [
          { src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
          { src: 'icons/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
        ],
      },
      workbox: {
        runtimeCaching: [
          {
            urlPattern: /\/api\/recipes/,
            handler: 'StaleWhileRevalidate',
            options: { cacheName: 'recipes-cache', expiration: { maxEntries: 200 } },
          },
          {
            urlPattern: /\/images\//,
            handler: 'CacheFirst',
            options: { cacheName: 'image-cache', expiration: { maxEntries: 500, maxAgeSeconds: 30 * 24 * 60 * 60 } },
          },
        ],
      },
    }),
  ],
});
```

### Tailwind Theme (tailwind.config.ts)

```typescript
export default {
  theme: {
    extend: {
      colors: {
        primary: { DEFAULT: '#C4737E', light: '#F2D7DB' },
        secondary: '#D4A574',
        cream: '#FBF8F5',
        espresso: '#2D2016',
        'warm-grey': '#7A6E65',
        sage: '#7BAE7F',
        berry: '#C94C4C',
        sand: '#E8E0D8',
      },
      fontFamily: {
        display: ['Playfair Display', 'serif'],
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
    },
  },
};
```

## 3. Backend

### Core
| Paket | Zweck |
|---|---|
| `fastify` v5 | HTTP-Framework (schnell, Schema-basiert) |
| `better-sqlite3` | SQLite-Treiber (synchron, schnell) |
| `@fastify/multipart` | Bild-Upload |
| `@fastify/static` | Statische Dateien (Bilder) |
| `@fastify/cors` | CORS für Frontend |
| `@fastify/rate-limit` | Rate-Limiting (Bot-API Schutz) |
| `sharp` | Bild-Verarbeitung (Resize, WebP) |
| `ulid` | ID-Generierung |
| `zod` | Request-Validierung |

### Projektstruktur

```
backend/
├── src/
│   ├── index.ts            ← Server-Start
│   ├── app.ts              ← Fastify-App Setup
│   ├── db/
│   │   ├── connection.ts   ← SQLite-Verbindung
│   │   ├── migrate.ts      ← Schema-Migrationen
│   │   └── migrations/     ← SQL-Dateien
│   ├── routes/
│   │   ├── recipes.ts      ← CRUD + Suche
│   │   ├── categories.ts   ← Kategorien
│   │   ├── shopping.ts     ← Einkaufsliste
│   │   ├── notes.ts        ← Notizen
│   │   ├── images.ts       ← Bild-Upload
│   │   └── bot.ts          ← Bot-API (Token-Auth)
│   ├── services/
│   │   ├── recipe.service.ts
│   │   ├── shopping.service.ts
│   │   └── image.service.ts
│   ├── schemas/            ← Zod-Schemas
│   └── lib/                ← Hilfsfunktionen
├── data/
│   ├── luna.db             ← SQLite-Datenbank
│   └── images/             ← Hochgeladene Bilder
├── tsconfig.json
└── package.json
```

### Authentifizierung

Einfaches Token-System (Single-User App):

```
# .env
BOT_API_TOKEN=geheimer-bot-token-hier
ADMIN_TOKEN=optionaler-admin-token
```

- **Frontend:** Kein Auth nötig (lokale Nutzung / privat)
- **Bot-API:** Bearer-Token im Header: `Authorization: Bearer {BOT_API_TOKEN}`
- **Optional Phase 2:** Session-basierte Auth falls öffentlich gehostet

## 4. API-Endpunkte

### Rezepte

| Methode | Pfad | Beschreibung |
|---|---|---|
| `GET` | `/api/recipes` | Alle Rezepte (paginiert, filterbar) |
| `GET` | `/api/recipes/:slug` | Einzelnes Rezept (komplett mit Zutaten, Steps, Notes) |
| `POST` | `/api/recipes` | Neues Rezept erstellen |
| `PUT` | `/api/recipes/:id` | Rezept aktualisieren |
| `DELETE` | `/api/recipes/:id` | Rezept löschen |
| `PATCH` | `/api/recipes/:id/favorite` | Favorit togglen |
| `GET` | `/api/recipes/search` | Volltextsuche |

#### Query-Parameter für `GET /api/recipes`

| Parameter | Typ | Beschreibung |
|---|---|---|
| `category` | string | Filter nach Kategorie-Slug |
| `favorite` | boolean | Nur Favoriten |
| `difficulty` | string | easy/medium/hard |
| `maxTime` | number | Maximale Gesamtzeit in Minuten |
| `ingredient` | string | Enthält Zutat (Textsuche) |
| `page` | number | Seite (default: 1) |
| `limit` | number | Einträge pro Seite (default: 20, max: 50) |
| `sort` | string | `newest`, `oldest`, `title`, `time` |

#### Request-Body `POST /api/recipes`

```json
{
  "title": "Schwarzwälder Kirschtorte",
  "description": "Klassiker der deutschen Backkunst",
  "category_id": "cat_torten",
  "servings": 12,
  "prep_time_min": 45,
  "cook_time_min": 35,
  "difficulty": "medium",
  "source_url": "https://pinterest.com/...",
  "tags": ["schokolade", "kirschen", "sahne"],
  "ingredients": [
    { "amount": 200, "unit": "g", "name": "Mehl", "group_name": "Teig" },
    { "amount": 150, "unit": "g", "name": "Zucker", "group_name": "Teig" },
    { "amount": 500, "unit": "ml", "name": "Sahne", "group_name": "Füllung" }
  ],
  "steps": [
    { "instruction": "Eier trennen und Eiweiß steif schlagen.", "timer_minutes": null },
    { "instruction": "Teig in Springform füllen und backen.", "timer_minutes": 35, "timer_label": "Backen" }
  ]
}
```

### Kategorien

| Methode | Pfad | Beschreibung |
|---|---|---|
| `GET` | `/api/categories` | Alle Kategorien |
| `POST` | `/api/categories` | Neue Kategorie (custom) |

### Notizen

| Methode | Pfad | Beschreibung |
|---|---|---|
| `GET` | `/api/recipes/:id/notes` | Notizen eines Rezepts |
| `POST` | `/api/recipes/:id/notes` | Notiz hinzufügen |
| `PUT` | `/api/notes/:id` | Notiz bearbeiten |
| `DELETE` | `/api/notes/:id` | Notiz löschen |

### Einkaufsliste

| Methode | Pfad | Beschreibung |
|---|---|---|
| `GET` | `/api/shopping` | Aktuelle Einkaufsliste |
| `POST` | `/api/shopping/from-recipe/:id` | Zutaten eines Rezepts hinzufügen |
| `POST` | `/api/shopping` | Eigenen Eintrag hinzufügen |
| `PATCH` | `/api/shopping/:id/check` | Abhaken/Enthaken |
| `DELETE` | `/api/shopping/:id` | Eintrag entfernen |
| `DELETE` | `/api/shopping/checked` | Alle abgehakten entfernen |

### Bilder

| Methode | Pfad | Beschreibung |
|---|---|---|
| `POST` | `/api/recipes/:id/image` | Hauptbild hochladen (multipart) |
| `POST` | `/api/recipes/:id/steps/:n/image` | Schrittbild hochladen |

### Bot-API

Alle Bot-Endpunkte erfordern `Authorization: Bearer {token}`.

| Methode | Pfad | Beschreibung |
|---|---|---|
| `POST` | `/api/bot/recipes` | Rezept per Bot erstellen (wie POST /api/recipes) |
| `POST` | `/api/bot/recipes/:id/image` | Bild per URL importieren |
| `GET` | `/api/bot/recipes` | Rezepte auflisten (für Bot-Abfragen) |

#### Bot-spezifisches Feld

```json
{
  "image_url": "https://example.com/foto.jpg",
  "...normaler recipe body..."
}
```

Das Backend lädt das Bild herunter, konvertiert zu WebP, erstellt Thumbnail.

### Tags

| Methode | Pfad | Beschreibung |
|---|---|---|
| `GET` | `/api/tags` | Alle Tags (mit Anzahl Rezepte) |
| `GET` | `/api/tags/:name/recipes` | Rezepte eines Tags |

## 5. Deployment

### Entwicklung

```bash
# Backend
cd backend && npm install && npm run dev   # Port 3000

# Frontend
cd frontend && npm install && npm run dev  # Port 5173 (Proxy → 3000)
```

### Produktion

```bash
# Frontend bauen
cd frontend && npm run build  # → dist/

# Backend serviert Frontend-Build als Static Files
# Alles als ein Prozess:
cd backend && NODE_ENV=production npm start
```

### Docker (empfohlen)

```dockerfile
FROM node:22-alpine

WORKDIR /app

# Backend Dependencies
COPY backend/package*.json backend/
RUN cd backend && npm ci --production

# Frontend Build
COPY frontend/ frontend/
RUN cd frontend && npm ci && npm run build

# Backend Source
COPY backend/ backend/

# Daten-Verzeichnis
VOLUME /app/data

ENV NODE_ENV=production
ENV DATABASE_PATH=/app/data/luna.db
ENV IMAGES_PATH=/app/data/images

EXPOSE 3000
CMD ["node", "backend/dist/index.js"]
```

```yaml
# docker-compose.yml
services:
  luna:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./data:/app/data
    environment:
      - BOT_API_TOKEN=${BOT_API_TOKEN}
    restart: unless-stopped
```

## 6. Performance-Überlegungen

- **SQLite WAL-Mode** → Paralleles Lesen während Schreiben
- **FTS5** → Schnelle Volltextsuche ohne externen Service
- **Bild-Thumbnails** → Kleine Bilder für Card-Grid, große nur bei Detail
- **Stale-While-Revalidate** → Instant UI, Background-Refresh
- **CacheFirst für Bilder** → Bilder ändern sich selten
- **Lazy Loading** → Bilder + Seiten on-demand laden
- **Bundle-Splitting** → Kochmodus als separater Chunk

## 7. Offline-Strategie (PWA)

| Ressource | Strategie | Cache |
|---|---|---|
| App-Shell (HTML/JS/CSS) | Precache | Beim Build |
| API-Responses | StaleWhileRevalidate | Runtime |
| Bilder | CacheFirst | Runtime, max 500 |
| Einkaufsliste | NetworkFirst | Runtime |

Die Einkaufsliste bekommt NetworkFirst, damit im Laden immer der aktuellste Stand gezeigt wird, aber offline trotzdem funktioniert.
