Files
luna-recipes/features/TECH-STACK.md
2026-02-18 09:56:01 +00:00

13 KiB

⚙️ 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)

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)

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

{
  "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

{
  "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

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

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

Produktion

# 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)

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"]
# 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.