refactor: split CLAUDE.md into agent instructions + product spec
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# Oikos
|
||||||
|
|
||||||
|
Self-hosted family planner PWA. Node.js/Express, Vanilla JS (ES modules, no build step), SQLite/SQLCipher, Docker.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # Production server (PORT from .env, default 3000)
|
||||||
|
npm run dev # Development with --watch
|
||||||
|
npm test # All test suites (requires Node ≥22 for --experimental-sqlite)
|
||||||
|
docker compose up -d # Production deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
index.js # Express entry, middleware chain, static serving
|
||||||
|
db.js # SQLite/SQLCipher connection, migrations
|
||||||
|
auth.js # Session auth middleware + login/logout/user-mgmt routes
|
||||||
|
routes/ # One file per module: tasks, shopping, meals, calendar, notes, contacts, budget, weather
|
||||||
|
services/ # google-calendar.js, apple-calendar.js, recurrence.js (RRULE)
|
||||||
|
public/
|
||||||
|
index.html # SPA shell — single entry point
|
||||||
|
router.js # History API router (~50 lines, no library)
|
||||||
|
api.js # Fetch wrapper: auth, CSRF, error handling
|
||||||
|
styles/ # tokens.css (design tokens), reset.css, layout.css, [module].css
|
||||||
|
components/ # Web Components: oikos-[module]-[name].js
|
||||||
|
pages/ # Page modules loaded by router
|
||||||
|
sw.js # Service worker (app-shell caching)
|
||||||
|
manifest.json
|
||||||
|
docs/
|
||||||
|
SPEC.md # Full product spec — module definitions, data model, design system
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request flow:** Client → Express static (public/) or `/api/v1/*` → auth middleware (session check) → route handler → better-sqlite3 (sync) → JSON response.
|
||||||
|
|
||||||
|
**No SPA framework.** Client-side routing via History API. Pages are ES modules that export a `render()` function. Web Components for reusable UI. No React, Vue, Svelte, or build tooling.
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
- ES modules everywhere (`type: "module"` in package.json, `import`/`export` in all JS)
|
||||||
|
- Semicolons: yes
|
||||||
|
- Web Component prefix: `oikos-` (not `fb-`), one component per file
|
||||||
|
- All UI text in German. Dates: `DD.MM.YYYY`. Times: `HH:MM` (24h)
|
||||||
|
- API responses: `{ data: ... }` on success, `{ error: string, code: number }` on failure
|
||||||
|
- Every route handler: `try/catch` wrapping, no unhandled promise rejections
|
||||||
|
- No `eval()`, no `innerHTML` with user input — use `textContent` or DOM API
|
||||||
|
- No external frontend dependencies except Lucide Icons (SVG sprite, self-hosted — no CDN at runtime)
|
||||||
|
- Backend deps minimal: express, better-sqlite3, bcrypt, express-session, express-rate-limit, helmet, dotenv
|
||||||
|
- Header comment in every file: purpose, module, dependencies
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests use Node.js built-in test runner with `--experimental-sqlite` for in-memory SQLite (no SQLCipher dep in tests). Each module gets a test file in `tests/`. Run: `npm test`. Add new tests: create `tests/[module].test.js`, it auto-discovers via glob pattern.
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
- **Auth:** Session-based. `express-session` with SQLite store, `httpOnly + secure + sameSite: strict` cookies. 7-day TTL. No public registration — admin creates users.
|
||||||
|
- **CSRF:** Double Submit Cookie pattern. Backend sets `csrfToken` cookie; frontend sends it as `X-CSRF-Token` header. Validate on all state-changing requests.
|
||||||
|
- **Rate limiting:** 5 login attempts/min/IP, 15-min lockout via `express-rate-limit`.
|
||||||
|
- **Passwords:** bcrypt, cost factor 12.
|
||||||
|
- **Headers:** `helmet()` defaults + strict CSP allowing only `'self'`.
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
SQLite via `better-sqlite3`. Optional SQLCipher encryption (AES-256) — enabled when `DB_ENCRYPTION_KEY` is set in `.env`.
|
||||||
|
|
||||||
|
**Migrations:** `server/db.js` runs migrations sequentially on startup. Each migration is an idempotent SQL block in a `migrations` array. Add new tables/columns by appending to that array — never modify existing entries.
|
||||||
|
|
||||||
|
**Schema conventions:** Every table has `id INTEGER PRIMARY KEY`, `created_at TEXT DEFAULT (datetime('now'))`, `updated_at TEXT DEFAULT (datetime('now'))`. Foreign keys enforced via `PRAGMA foreign_keys = ON`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Base: node:20-slim + SQLCipher build deps (libsqlcipher-dev)
|
||||||
|
# Volume: /app/data (SQLite DB file)
|
||||||
|
# Expose: 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Required env vars: `SESSION_SECRET`, `DB_ENCRYPTION_KEY` (optional), `PORT` (default 3000).
|
||||||
|
Optional: `OPENWEATHERMAP_API_KEY`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`.
|
||||||
|
|
||||||
|
Runs behind Nginx reverse proxy with SSL. Example config in `nginx.conf.example`.
|
||||||
|
|
||||||
|
## Bootstrap Sequence
|
||||||
|
|
||||||
|
When starting from scratch, follow this order:
|
||||||
|
|
||||||
|
1. `npm init` + install deps + `.env.example` + `.gitignore` + `Dockerfile` + `docker-compose.yml`
|
||||||
|
2. Express server + SQLite connection + migration runner + auth system
|
||||||
|
3. Frontend app shell: SPA router, nav, layout, CSS design tokens
|
||||||
|
4. Modules one by one (see `docs/SPEC.md` for detailed specs per module)
|
||||||
|
5. Cross-module integrations (meal→shopping, dashboard widgets)
|
||||||
|
6. PWA (service worker, manifest, offline shell)
|
||||||
|
7. Security hardening (CSRF, rate limiting, CSP, input validation)
|
||||||
|
|
||||||
|
Read `docs/SPEC.md` before implementing any module — it contains the data model, UI specs, and design system.
|
||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
# Oikos — Produktspezifikation
|
||||||
|
|
||||||
|
Selbstgehostete Familienplaner-Web-App für eine einzelne Familie (2–6 Personen). Kein App-Store, kein öffentlicher Zugang. Deployment via Docker auf privatem Linux-Server hinter Nginx Reverse Proxy mit SSL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenmodell
|
||||||
|
|
||||||
|
Jede Tabelle: `id INTEGER PRIMARY KEY`, `created_at TEXT`, `updated_at TEXT` (ISO 8601).
|
||||||
|
|
||||||
|
### Users
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| username | TEXT | UNIQUE NOT NULL |
|
||||||
|
| display_name | TEXT | |
|
||||||
|
| password_hash | TEXT | bcrypt |
|
||||||
|
| avatar_color | TEXT | HEX-Farbcode |
|
||||||
|
| role | TEXT | 'admin' oder 'member' |
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| title | TEXT | NOT NULL |
|
||||||
|
| description | TEXT | |
|
||||||
|
| category | TEXT | Haushalt, Schule, Einkauf, Reparatur, Sonstiges |
|
||||||
|
| priority | TEXT | low, medium, high, urgent |
|
||||||
|
| status | TEXT | open, in_progress, done |
|
||||||
|
| due_date | TEXT | DATE, nullable |
|
||||||
|
| due_time | TEXT | TIME, nullable |
|
||||||
|
| assigned_to | INTEGER | FK → Users |
|
||||||
|
| created_by | INTEGER | FK → Users, NOT NULL |
|
||||||
|
| is_recurring | INTEGER | 0/1 |
|
||||||
|
| recurrence_rule | TEXT | iCal RRULE |
|
||||||
|
| parent_task_id | INTEGER | FK → Tasks (max 2 Ebenen) |
|
||||||
|
|
||||||
|
### Shopping Lists
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| name | TEXT | NOT NULL (z.B. "REWE", "Baumarkt") |
|
||||||
|
|
||||||
|
### Shopping Items
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| list_id | INTEGER | FK → Shopping Lists, NOT NULL |
|
||||||
|
| name | TEXT | NOT NULL |
|
||||||
|
| quantity | TEXT | z.B. "500g", "2 Stück" |
|
||||||
|
| category | TEXT | Obst & Gemüse, Milchprodukte, Fleisch & Fisch, Backwaren, Getränke, Tiefkühl, Haushalt, Drogerie, Sonstiges |
|
||||||
|
| is_checked | INTEGER | 0/1 |
|
||||||
|
| added_from_meal | INTEGER | FK → Meals, nullable |
|
||||||
|
|
||||||
|
### Meals
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| date | TEXT | DATE, NOT NULL |
|
||||||
|
| meal_type | TEXT | breakfast, lunch, dinner, snack |
|
||||||
|
| title | TEXT | NOT NULL |
|
||||||
|
| notes | TEXT | |
|
||||||
|
| created_by | INTEGER | FK → Users, NOT NULL |
|
||||||
|
|
||||||
|
### Meal Ingredients
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| meal_id | INTEGER | FK → Meals, NOT NULL |
|
||||||
|
| name | TEXT | NOT NULL |
|
||||||
|
| quantity | TEXT | |
|
||||||
|
| on_shopping_list | INTEGER | 0/1 |
|
||||||
|
|
||||||
|
### Calendar Events
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| title | TEXT | NOT NULL |
|
||||||
|
| description | TEXT | |
|
||||||
|
| start_datetime | TEXT | DATETIME, NOT NULL |
|
||||||
|
| end_datetime | TEXT | DATETIME |
|
||||||
|
| all_day | INTEGER | 0/1 |
|
||||||
|
| location | TEXT | |
|
||||||
|
| color | TEXT | HEX |
|
||||||
|
| assigned_to | INTEGER | FK → Users |
|
||||||
|
| created_by | INTEGER | FK → Users, NOT NULL |
|
||||||
|
| external_calendar_id | TEXT | ID aus externem Kalender |
|
||||||
|
| external_source | TEXT | local, google, apple |
|
||||||
|
| recurrence_rule | TEXT | iCal RRULE |
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| title | TEXT | nullable |
|
||||||
|
| content | TEXT | NOT NULL |
|
||||||
|
| color | TEXT | HEX |
|
||||||
|
| pinned | INTEGER | 0/1 |
|
||||||
|
| created_by | INTEGER | FK → Users, NOT NULL |
|
||||||
|
|
||||||
|
### Contacts
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| name | TEXT | NOT NULL |
|
||||||
|
| category | TEXT | Arzt, Schule/Kita, Behörde, Versicherung, Handwerker, Notfall, Sonstiges |
|
||||||
|
| phone | TEXT | |
|
||||||
|
| email | TEXT | |
|
||||||
|
| address | TEXT | |
|
||||||
|
| notes | TEXT | |
|
||||||
|
|
||||||
|
### Budget Entries
|
||||||
|
| Spalte | Typ | Constraint |
|
||||||
|
|--------|-----|-----------|
|
||||||
|
| title | TEXT | NOT NULL |
|
||||||
|
| amount | REAL | NOT NULL (positiv=Einnahme, negativ=Ausgabe) |
|
||||||
|
| category | TEXT | Lebensmittel, Miete, Versicherung, Mobilität, Freizeit, Kleidung, Gesundheit, Bildung, Sonstiges |
|
||||||
|
| date | TEXT | DATE, NOT NULL |
|
||||||
|
| is_recurring | INTEGER | 0/1 |
|
||||||
|
| recurrence_rule | TEXT | iCal RRULE |
|
||||||
|
| created_by | INTEGER | FK → Users, NOT NULL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module
|
||||||
|
|
||||||
|
### Dashboard (`/`)
|
||||||
|
|
||||||
|
Responsive Grid: 1 Spalte mobil, 2 Tablet, 3 Desktop.
|
||||||
|
|
||||||
|
**Widgets:**
|
||||||
|
- Begrüßung: "Guten [Morgen/Tag/Abend], [Name]" + Datum
|
||||||
|
- Wetter: OpenWeatherMap-Proxy, 3-Tage-Vorschau, Refresh 30min, bei API-Fehler Widget ausblenden
|
||||||
|
- Anstehende Termine: nächste 3–5, farbcodiert nach Person
|
||||||
|
- Dringende Aufgaben: priority urgent/high + due_date ≤48h
|
||||||
|
- Heutiges Essen: Mahlzeiten des Tages
|
||||||
|
- Pinnwand-Vorschau: 2–3 angepinnte Notizen
|
||||||
|
- FAB (Schnellaktionen): + Aufgabe, + Termin, + Einkaufslisteneintrag, + Notiz
|
||||||
|
|
||||||
|
Skeleton-Loading statt Spinner. Klick auf jedes Widget navigiert zum Modul.
|
||||||
|
|
||||||
|
### Aufgaben (`/tasks`)
|
||||||
|
|
||||||
|
**Ansichten:**
|
||||||
|
- Listenansicht (Standard): gruppiert nach Kategorie oder Fälligkeit (umschaltbar), Filter: Person, Priorität, Status
|
||||||
|
- Kanban: Spalten Offen → In Bearbeitung → Erledigt, Drag & Drop
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- CRUD + Teilaufgaben (max 2 Ebenen, Checkbox-Liste, Fortschrittsbalken)
|
||||||
|
- Zuweisung an User (Avatar-Farbe als Indikator)
|
||||||
|
- Prioritäten visuell durch Farbe/Icon
|
||||||
|
- Wiederkehrend: bei Erledigung nächste Instanz automatisch erstellen
|
||||||
|
- Swipe mobil: links = erledigt, rechts = bearbeiten
|
||||||
|
- Badge bei überfälligen Aufgaben
|
||||||
|
|
||||||
|
### Einkaufslisten (`/shopping`)
|
||||||
|
|
||||||
|
- Mehrere Listen parallel
|
||||||
|
- Artikel: Name, Kategorie, Menge, Checkbox
|
||||||
|
- Gruppierung nach Kategorie (Gang-Logik)
|
||||||
|
- Integration mit Essensplan: "Zutaten auf Einkaufsliste" überträgt mit Quell-Referenz
|
||||||
|
- Erledigte Artikel durchgestrichen + nach unten
|
||||||
|
- "Liste leeren" = nur abgehakte entfernen
|
||||||
|
- Autocomplete aus bisherigen Einträgen (lokal)
|
||||||
|
|
||||||
|
### Essensplan (`/meals`)
|
||||||
|
|
||||||
|
Wochenansicht (Mo–So), Slots: Frühstück/Mittag/Abend/Snack.
|
||||||
|
|
||||||
|
- Mahlzeit: Titel + Notizen + Zutatenliste
|
||||||
|
- Button "→ Einkaufsliste": nicht-abgehakte Zutaten der Woche auf wählbare Liste übertragen
|
||||||
|
- Wochennavigation vor/zurück
|
||||||
|
- Drag & Drop zwischen Tagen/Slots
|
||||||
|
- Autocomplete aus Mahlzeiten-Historie
|
||||||
|
|
||||||
|
### Kalender (`/calendar`)
|
||||||
|
|
||||||
|
**Ansichten:** Monat (Standard, Punkt-Indikatoren), Woche (Stundenraster), Tag (Timeline), Agenda (Liste).
|
||||||
|
|
||||||
|
- CRUD: Titel, Beschreibung, Start/Ende, Ganztägig, Ort, Farbe, Zuweisung
|
||||||
|
- Farbcodierung pro Person
|
||||||
|
- Wiederkehrend via iCal RRULE
|
||||||
|
- **Google Calendar:** OAuth 2.0, Calendar API v3, Zwei-Wege-Sync
|
||||||
|
- **Apple Calendar:** CalDAV (tsdav), Zwei-Wege-Sync
|
||||||
|
- Sync-Intervall konfigurierbar (Standard 15min)
|
||||||
|
- Externe Termine visuell unterscheidbar
|
||||||
|
- Konflikte: externes Event gewinnt, lokale Ergänzungen bleiben
|
||||||
|
|
||||||
|
### Pinnwand (`/notes`)
|
||||||
|
|
||||||
|
Masonry-Grid mit farbigen Sticky Notes.
|
||||||
|
|
||||||
|
- CRUD: Titel (optional), Inhalt, Farbe
|
||||||
|
- Anpinnen → erscheint oben + Dashboard
|
||||||
|
- Ersteller angezeigt (Avatar-Farbe)
|
||||||
|
- Markdown-Light: fett, kursiv, Listen (regex-basiert)
|
||||||
|
|
||||||
|
### Kontakte (`/contacts`)
|
||||||
|
|
||||||
|
- CRUD mit Kategorie-Filter
|
||||||
|
- Telefon: `tel:`-Link, E-Mail: `mailto:`-Link
|
||||||
|
- Adresse: Maps-Link (Google/Apple via User-Agent)
|
||||||
|
- Echtzeit-Suchfilter
|
||||||
|
|
||||||
|
### Budget (`/budget`)
|
||||||
|
|
||||||
|
**Ansichten:**
|
||||||
|
- Monatsübersicht: Einnahmen vs. Ausgaben, Saldo, Balkendiagramm nach Kategorie (Canvas, keine Library)
|
||||||
|
- Transaktionsliste: chronologisch, filterbar
|
||||||
|
|
||||||
|
- CRUD: Titel, Betrag, Kategorie, Datum
|
||||||
|
- Wiederkehrende Buchungen
|
||||||
|
- Monatsvergleich (aktuell vs. Vormonat)
|
||||||
|
- CSV-Export
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design-System
|
||||||
|
|
||||||
|
### Farben (CSS Custom Properties)
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--color-bg: #F5F5F7;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-border: #E5E5EA;
|
||||||
|
--color-text-primary: #1C1C1E;
|
||||||
|
--color-text-secondary: #8E8E93;
|
||||||
|
--color-accent: #007AFF;
|
||||||
|
--color-accent-light: #E3F2FF;
|
||||||
|
--color-success: #34C759;
|
||||||
|
--color-warning: #FF9500;
|
||||||
|
--color-danger: #FF3B30;
|
||||||
|
--color-info: #5AC8FA;
|
||||||
|
--color-priority-low: #8E8E93;
|
||||||
|
--color-priority-medium: #FF9500;
|
||||||
|
--color-priority-high: #FF6B35;
|
||||||
|
--color-priority-urgent: #FF3B30;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-bg: #1C1C1E;
|
||||||
|
--color-surface: #2C2C2E;
|
||||||
|
--color-border: #3A3A3C;
|
||||||
|
--color-text-primary: #F5F5F7;
|
||||||
|
--color-text-secondary: #8E8E93;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typografie
|
||||||
|
- System Font Stack, Überschriften 600–700
|
||||||
|
- Body: 16px mobil, 15px Desktop, line-height 1.5
|
||||||
|
- Caption: 13px, `var(--color-text-secondary)`
|
||||||
|
|
||||||
|
### Komponenten
|
||||||
|
- **Cards:** `var(--color-surface)`, `var(--radius-md)`, `var(--shadow-sm)`
|
||||||
|
- **Buttons:** Primär = Accent + weiß. Sekundär = Outline. Min-Höhe 44px
|
||||||
|
- **Inputs:** `var(--radius-sm)`, 1.5px border, padding 12px 16px
|
||||||
|
- **Navigation:** Bottom Tab Bar mobil (Dashboard, Aufgaben, Kalender, Essen, Mehr). Sidebar Desktop
|
||||||
|
- **Transitions:** `all 0.2s ease`. Seiten: Slide-Animation
|
||||||
|
- **Empty States:** Illustration + CTA in jeder Liste
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
- Mobil: < 768px (1 Spalte, Bottom Nav)
|
||||||
|
- Tablet: 768–1024px (2 Spalten, Bottom Nav)
|
||||||
|
- Desktop: > 1024px (Sidebar + Content)
|
||||||
Reference in New Issue
Block a user