# Swirl Master — Architectuurdocument v1.0 ## 1. Systeemoverzicht **Swirl Master** is een full-stack webapplicatie voor het beheren van recepten voor de Ninja Swirl by CREAMi ijsmachine. De applicatie bestaat uit een frontend (HTML/CSS/JS) met embedded data en een optionele Python/Flask backend met SQLite database. ### 1.1 Componentenkaart | Component | Technologie | Poort | Doel | |-----------|-------------|-------|------| | Frontend | Vanilla HTML5/CSS3/ES6 | — | User interface (5 tabs) | | API Server | Python 3 / Flask 3.0 | 5060 | REST API + statische bestanden | | Database | SQLite 3 | — | Recepten, favorieten, ratings | | Reverse Proxy | Nginx | 80/443 | TLS, caching, rate limiting | | Process Manager | PM2 | — | Auto-restart, logging | ### 1.2 Dataflow ``` Gebruiker Browser │ ├── HTML/CSS/JS (statisch via Nginx) │ └── app.js bevat RECIPES[] array (30+ recepten) │ └── Frontend werkt INDEPENDENT van backend │ ├── localStorage (favorieten, theme) │ └── REST API (optioneel, via Nginx proxy) ├── GET /api/recipes → SQLite ├── POST /api/mixmatch/suggest → Algoritme └── GET/POST/DELETE /api/favorites → SQLite ``` ## 2. Database Schema ### Tabel: recipes | Kolom | Type | Default | Beschrijving | |-------|------|---------|-------------| | id | TEXT PK | — | Unieke ID (bijv. "ic-vanilla") | | type | TEXT NOT NULL | — | Programma type (ice_cream, gelato, etc.) | | name | TEXT NOT NULL | — | Receptnaam | | emoji | TEXT | '🍦' | Visual emoji | | description | TEXT | — | Korte beschrijving | | difficulty | TEXT | — | "Beginner" / "Gemiddeld" | | difficulty_score | INT | 1 | 1=easy, 2=medium, 3=hard | | prep_time | TEXT | — | Voorbereidingstijd | | freeze_time | TEXT | '24u' | Vries tijd | | total_time | TEXT | — | Totale tijd | | servings | INT | 4 | Aantal porties | | rating | REAL | 4.5 | Gemiddelde beoordeling | | popularity | INT | 50 | Populariteit score | | tags | TEXT (JSON) | [] | Tags array | | diet | TEXT (JSON) | [] | Diet tags (vegan, keto, etc.) | | program | TEXT | — | Aanbevolen programma naam | | ingredients | TEXT (JSON) | [] | Ingrediënten array | | steps | TEXT (JSON) | [] | Bereidingsstappen array | | mixins | TEXT (JSON) | [] | Mix-in suggesties | | nutrition | TEXT (JSON) | {} | {calories, fat, carbs, protein} | | tips | TEXT | — | Pro tip tekst | | is_custom | INT | 0 | 0=built-in, 1=user-created | | created_at | TEXT | now() | Aanmaak timestamp | | updated_at | TEXT | now() | Update timestamp | ### Tabel: favorites | Kolom | Type | Beschrijving | |-------|------|-------------| | id | INT PK AUTO | — | | session_id | TEXT NOT NULL | Browser session ID | | recipe_id | TEXT NOT NULL | FK→recipes.id | | created_at | TEXT | Timestamp | ### Tabel: ratings | Kolom | Type | Beschrijving | |-------|------|-------------| | id | INT PK AUTO | — | | session_id | TEXT NOT NULL | Browser session ID | | recipe_id | TEXT NOT NULL | FK→recipes.id | | rating | INT CHECK(1-5) | Sterren | | created_at | TEXT | Timestamp | ## 3. API Schema ### 3.1 Endpoints | Endpoint | Method | Auth | Query Params | Response | |----------|--------|------|-------------|----------| | `/api/health` | GET | — | — | `{status, database, version}` | | `/api/recipes` | GET | — | type, diet[], q, sort, limit | `{count, recipes[]}` | | `/api/recipes/` | GET | — | — | `{recipe object}` | | `/api/recipes` | POST | API Key | — | `{status, id}` | | `/api/programs` | GET | — | — | `{count, programs{}}` | | `/api/programs/` | GET | — | — | `{program object}` | | `/api/favorites` | GET | Session | — | `{count, favorites[]}` | | `/api/favorites/` | POST | Session | — | `{status: "added"}` | | `/api/favorites/` | DELETE | Session | — | `{status: "removed"}` | | `/api/ratings/` | POST | Session | `{rating: 1-5}` | `{status, average}` | | `/api/mixmatch/suggest` | POST | — | `{base, flavor, mixin, topping}` | `{compatibility, ingredients, steps}` | | `/api/stats` | GET | — | — | `{totalRecipes, recipesByType}` | ### 3.2 Error Responses | Code | Betekenis | Voorbeeld | |------|-----------|----------| | 200 | Success | Normal response | | 400 | Bad Request | Missing required fields | | 401 | Unauthorized | Missing/invalid API key | | 404 | Not Found | Recipe/program not found | | 500 | Server Error | Database error | ## 4. Frontend Logica ### 4.1 State Management ```javascript state = { currentTab: 'dashboard', recipeFilter: { type, diet, search, sort }, favorites: [], favFilter: 'all', mmSelections: { base, flavor, mixin, topping }, guideSection: 'programs', quizAnswers: [], darkMode: boolean } ``` ### 4.2 Belangrijke Functies | Functie | Doel | |---------|------| | `renderDashboard()` | Dashboard vullen met stats, featured, tips, quiz | | `getFilteredRecipes()` | Filtering + sorting van recepten | | `renderRecipes()` | Recipe grid renderen | | `openRecipe(id)` | Recipe detail modal openen | | `toggleFav(id)` | Favoriet toevoegen/verwijderen | | `calculateCompatibility()` | Mix & Match score algoritme | | `recommendProgram()` | Programma aanbeveling op basis van selectie | | `generateMixMatch()` | Mix & Match recept genereren | | `renderQuiz()` | Quiz interface renderen | ### 4.3 Data Structuren **RECIPES[]**: Array van recept-objecten (30+ entries) **PROGRAMS{}**: Object met 13 programma definities **MIXMATCH_DATA{}**: Object met bases, flavors, mixins, toppings arrays **PAIRING_RULES{}**: Smaakcompatibiliteit matrix **GUIDE_CONTENT{}**: HTML content voor de machine gids tabs **DAILY_TIPS[]**: Roterende tips array **QUIZ_QUESTIONS[]**: 3-vragen quiz data ## 5. Mix & Match Algoritme ### 5.1 Compatibility Scoring ``` Base score: 60 IF (flavor + mixin) in PAIRING_RULES: score *= PAIRING_RULES[flavor][mixin] (0.5 - 1.5 multiplier) ELSE IF flavor: score = (score + flavor.pairing) / 1.5 IF topping: score += (topping.pairing - 50) * 0.3 Clamp: [45, 98] ``` ### 5.2 Programma Aanbeveling Logica ``` IF base == "greek-yogurt" → "Frozen Yogurt" IF base == "protein-shake" → "High Protein Soft" IF base ∈ (coconut/almond/oat milk) AND flavor ∈ (mango/lemon/strawberry) → "Sorbet" ELSE → "Ice Cream" ``` ## 6. Beveiliging ### 6.1 Authentication - **Frontend**: Geen authenticatie (publieke webapp) - **API GET endpoints**: Geen authenticatie vereist - **API POST (write)**: Optioneel API key via `X-API-Key` header - **Favorites**: Session-based (gebruikt `X-Session-ID` header of IP-hash) ### 6.2 Input Validatie - SQL injectie: SQLite parameterized queries - XSS: Geen innerHTML met user input (alle data is hardcoded in app.js) - Path traversal: Nginx configuratie blokkeert `../` in paden - Rate limiting: Nginx `limit_req` zone (10 req/s per IP) ### 6.3 Security Headers (via Nginx) - HSTS: `max-age=63072000; includeSubDomains` - X-Frame-Options: `SAMEORIGIN` - X-Content-Type-Options: `nosniff` - X-XSS-Protection: `1; mode=block` ## 7. Performance ### 7.1 Frontend - **Laadtijd**: < 200KB totaal (HTML + CSS + JS) - **Geen externe dependencies** behalve Google Fonts - **Geen framework overhead** (vanilla JS) - **Lazy rendering**: Tabs renderen pas bij activatie ### 7.2 Backend - **SQLite**: Snel voor read-heavy workloads - **WAL mode**: Voor concurrent reads - **Gzip**: Nginx comprimeert alle text responses - **Static caching**: 30 dagen voor assets ## 8. Deployment Topologie ``` Internet │ ▼ ┌─────────────┐ │ Nginx │ ← TLS, caching, rate limit │ Port 443 │ └──────┬──────┘ │ ├── / → Static files (index.html, style.css, app.js) │ └── /api/ → Proxy │ ▼ ┌─────────────┐ │ Flask │ ← PM2 managed │ Port 5060 │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ SQLite │ │ swirl.db │ └─────────────┘ ```