Files
oikos/docs/SPEC.md
T

14 KiB
Raw Blame History

Oikos — Produktspezifikation

Selbstgehostete Familienplaner-Web-App für eine einzelne Familie (26 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
recurrence_parent_id INTEGER FK → Budget Entries (generierte Instanz zeigt auf Original)
created_by INTEGER FK → Users, NOT NULL

Budget Recurrence Skipped

Speichert vom Nutzer gelöschte Instanzen eines wiederkehrenden Eintrags, damit sie nicht erneut generiert werden.

Spalte Typ Constraint
parent_id INTEGER FK → Budget Entries, NOT NULL
month TEXT YYYY-MM, NOT NULL
PRIMARY KEY (parent_id, month)

Sync Config

Schlüssel-Wert-Tabelle für OAuth-Tokens und CalDAV-Credentials.

Spalte Typ Constraint
key TEXT PRIMARY KEY
value TEXT 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 35, farbcodiert nach Person
  • Dringende Aufgaben: priority urgent/high + due_date ≤48h
  • Heutiges Essen: Mahlzeiten des Tages
  • Pinnwand-Vorschau: 23 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)
  • Swipe mobil: links = abhaken/zurück, rechts = löschen; × Löschen-Button auf Mobile ausgeblendet (Swipe übernimmt)

Essensplan (/meals)

Wochenansicht (MoSo), 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)
  • Volltextsuche: client-seitige Filterleiste, filtert sofort nach Titel + Inhalt

Kontakte (/contacts)

  • CRUD mit Kategorie-Filter
  • Telefon: tel:-Link, E-Mail: mailto:-Link
  • Adresse: Maps-Link (Google/Apple via User-Agent)
  • Echtzeit-Suchfilter
  • vCard-Export: jeder Kontakt als .vcf herunterladbar (GET /api/v1/contacts/:id/vcard)
  • vCard-Import: Datei hochladen → client-seitiger Parser (FN, TEL, EMAIL, ADR, NOTE, CATEGORIES) → Kontakt anlegen

Login (/login)

Nicht-authentifizierte Nutzer werden hierhin umgeleitet. Kein öffentliches Registrierungsformular — Admin erstellt Benutzer über Setup-Wizard (setup.js) oder Settings.

  • Username + Passwort-Formular
  • Fehleranzeige bei falschen Credentials
  • Rate-Limiting: 5 Versuche/min/IP, 15-min Lockout
  • Nach erfolgreichem Login: Redirect auf Dashboard

Einstellungen (/settings)

Benutzerverwaltung und App-Konfiguration. Nur für eingeloggte Nutzer.

  • Profil: Display-Name, Avatar-Farbe ändern, Passwort ändern
  • Benutzerverwaltung (Admin): Neue Benutzer anlegen, bestehende Benutzer bearbeiten/löschen, Rollen zuweisen (admin/member)
  • Kalender-Integration: Google Calendar OAuth verbinden/trennen, Apple Calendar (CalDAV) Credentials hinterlegen, Sync-Intervall konfigurieren
  • Wetter: OpenWeatherMap Standort konfigurieren
  • Sprache: System (folgt navigator.language), Deutsch, English — via oikos-locale-picker Web Component; Wechsel ohne Reload
  • App-Info: Version, Lizenz

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)

: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 600700
  • 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). Einheitliches Padding var(--space-4) (16px) in allen Modulen.
  • Buttons: Primär = Accent + weiß. Sekundär = Outline. Min-Höhe 44px. Submit-Buttons zeigen Erfolg (Checkmark, 700ms grün via .btn--success) und Fehler (Shake via .btn--shaking).
  • Inputs: var(--radius-sm), 1.5px border, padding 12px 16px. [required]-Felder erhalten bei Blur Validierungsstatus (.form-field--error / .form-field--valid). Enter navigiert zum nächsten Feld; Enter im letzten Feld löst Submit aus.
  • FAB (Floating Action Button): Farbe folgt dem Modul-Akzent-Token (--module-accent) — jedes Modul definiert seine eigene Akzentfarbe. Wird ausgeblendet, wenn die virtuelle Tastatur geöffnet ist (visualViewport.resize, Schwellwert 75% der Fensterhöhe).
  • Modul-Akzentfarben: --module-accent wird auf drei visuellen Ebenen angewendet — (1) aktiver Nav-Tab (Bottom Bar + Sidebar-Streifen), (2) Toolbar border-top: 3px, (3) Karten/Zeilen border-left: 3px. Der aktive Akzent wird bei jedem Navigationswechsel als --active-module-accent auf :root geschrieben. Fallback auf --color-accent für Seiten ohne Modul-Kontext.
  • Navigation: Bottom Tab Bar mobil (Dashboard, Aufgaben, Kalender, Essen, Mehr). Sidebar Desktop.
  • Transitions: Direktionale Slide-X-Animation bei Seitenwechsel (vorwärts = von rechts, rückwärts = von links, 200ms). Respektiert prefers-reduced-motion.
  • Empty States: Einheitliche .empty-state-Klasse in allen Modulen (Icon + Titel + Beschreibung, zentriert). Kompakte Variante .empty-state--compact für Mahlzeiten-Slots.
  • Modals: Auf Desktop zentriertes Panel. Auf Mobile (< 768px) Bottom Sheet — fährt von unten ein, Sheet-Handle sichtbar, Swipe-to-Close (> 80px nach unten). focusin scrollt Inputs bei virtueller Tastatur in den sichtbaren Bereich.
  • Listen-Animation: Staggered Fade-In beim Laden (stagger() aus public/utils/ux.js) — max. 5 Elemente gestaffelt (30ms Abstand), Rest sofort.
  • Vibration: vibrate() aus public/utils/ux.js — kurze Impulse bei leichten Aktionen (1040ms), Muster [30, 50, 30] bei destructiven Aktionen (Löschen). Respektiert prefers-reduced-motion.
  • PWA Install Prompt: Erscheint erst nach 2 Nutzer-Interaktionen. Dismiss-Fenster 7 Tage; nach Dismiss wird der Interaktionszähler zurückgesetzt.
  • PWA Offline-Fallback: Service Worker liefert /offline.html wenn das Netz nicht erreichbar und index.html nicht gecacht ist. Enthält Reload-Button.

Breakpoints

  • Mobil: < 768px (1 Spalte, Bottom Nav)
  • Tablet: 7681024px (2 Spalten, Bottom Nav)
  • Desktop: > 1024px (Sidebar + Content)

Internationalisierung (i18n)

Alle UI-Strings werden über public/i18n.js verwaltet. Kein hardcodierter Text in JS-Dateien außer in Locale-Dateien.

Architektur

  • Modul: public/i18n.js — exports: initI18n(), setLocale(), t(key, params?), getLocale(), getSupportedLocales(), formatDate(date), formatTime(date)
  • Locale-Dateien: public/locales/de.json (Referenz), public/locales/en.json — Struktur: { "modul.camelCaseKey": "Wert" }
  • Variablen: {{variable}}-Syntax in Übersetzungsstrings, z.B. t('tasks.assignedTo', { name: 'Anna' })
  • Fallback-Kette: aktive Locale → Deutsch (de) → Key selbst
  • Datumsformat: Intl.DateTimeFormat mit aktuellem Locale — formatDate() und formatTime() aus i18n.js

Sprach-Erkennung

  1. localStorage Eintrag oikos-locale (manuelle Auswahl)
  2. navigator.languages[0] (Browser-Sprache)
  3. Fallback: de

Neue Sprache hinzufügen

  1. public/locales/xx.json erstellen (Kopie von de.json, übersetzen)
  2. SUPPORTED_LOCALES in public/i18n.js um 'xx' erweitern
  3. Label in oikos-locale-picker ergänzen (LOCALE_LABELS['xx'] = 'Name')

Locale-Wechsel

setLocale(locale) speichert die Auswahl, lädt die neue Locale-Datei und feuert das locale-changed Custom Event. Alle Seiten-Module und Web Components hören dieses Event und rendern sich neu — kein Seiten-Reload nötig.