style: replace em dashes with hyphens throughout codebase
Replace all — with - in all source files (JS, CSS, HTML, JSON, Markdown) for consistency and readability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Oikos — Umgebungsvariablen
|
||||
# Oikos - Umgebungsvariablen
|
||||
# Kopiere diese Datei nach .env und passe die Werte an.
|
||||
|
||||
# Server
|
||||
|
||||
@@ -12,7 +12,7 @@ What problem does this solve? Describe the use case from a family member's persp
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
How do you envision this working? Be as specific as you like — UI sketch, API shape, interaction flow.
|
||||
How do you envision this working? Be as specific as you like - UI sketch, API shape, interaction flow.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [0.5.8] - 2026-04-03
|
||||
|
||||
### Added
|
||||
- Add Italian (Italiano) localization — full translation of all 497 i18n keys (thanks @albanobattistella, PR #7)
|
||||
- Add Italian (Italiano) localization - full translation of all 497 i18n keys (thanks @albanobattistella, PR #7)
|
||||
- Add Italian as selectable language in Settings locale picker
|
||||
|
||||
## [0.5.7] - 2026-04-03
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ PRs are reviewed by the maintainer. Expect feedback within a few days. Once appr
|
||||
### Frontend
|
||||
|
||||
- Web Component prefix: `oikos-` (one component per file)
|
||||
- All UI text via i18n keys (`t('key')`) — never hardcode text in components. German (`de`) is the reference locale.
|
||||
- All UI text via i18n keys (`t('key')`) - never hardcode text in components. German (`de`) is the reference locale.
|
||||
- Date format: `DD.MM.YYYY` - Time format: `HH:MM` (24h)
|
||||
- CSS uses design tokens from `public/styles/tokens.css` - never hardcode values
|
||||
- Pages export a `render()` function, no side effects on import
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ Oikos uses a flat family authorization model:
|
||||
- **Admin** can create, edit, and delete all user accounts and all shared data.
|
||||
- **Member** can read and write all shared data (tasks, shopping lists, meals, calendar events, notes, contacts, budget entries) but cannot manage user accounts.
|
||||
|
||||
There is no per-user data isolation — all family members see and can edit all data. This is intentional: Oikos is a shared family planner, not a multi-tenant application.
|
||||
There is no per-user data isolation - all family members see and can edit all data. This is intentional: Oikos is a shared family planner, not a multi-tenant application.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
# Oikos — Product Specification
|
||||
# Oikos - Product Specification
|
||||
|
||||
Self-hosted family planner web app for a single household (2–6 people). No app store, no public access. Deployment via Docker on a private Linux server behind an Nginx reverse proxy with SSL.
|
||||
|
||||
@@ -217,7 +217,7 @@ Masonry grid with colored sticky notes.
|
||||
|
||||
### Login (`/login`)
|
||||
|
||||
Unauthenticated users are redirected here. No public registration form — admin creates users via setup wizard (`setup.js`) or Settings.
|
||||
Unauthenticated users are redirected here. No public registration form - admin creates users via setup wizard (`setup.js`) or Settings.
|
||||
|
||||
- Username + password form
|
||||
- Error display for wrong credentials
|
||||
@@ -232,7 +232,7 @@ User management and app configuration. Logged-in users only.
|
||||
- **User management (admin):** create new users, edit/delete existing users, assign roles (admin/member)
|
||||
- **Calendar integration:** connect/disconnect Google Calendar OAuth, store Apple Calendar (CalDAV) credentials, configure sync interval
|
||||
- **Weather:** configure OpenWeatherMap location
|
||||
- **Language:** System (follows `navigator.language`), German, English — via `oikos-locale-picker` web component; switch without page reload
|
||||
- **Language:** System (follows `navigator.language`), German, English - via `oikos-locale-picker` web component; switch without page reload
|
||||
- **App info:** version, license
|
||||
|
||||
### Budget (`/budget`)
|
||||
@@ -299,14 +299,14 @@ User management and app configuration. Logged-in users only.
|
||||
- **Cards:** `var(--color-surface)`, `var(--radius-md)`, `var(--shadow-sm)`. Consistent padding `var(--space-4)` (16px) across all modules.
|
||||
- **Buttons:** Primary = accent + white. Secondary = outline. Min-height 44px. Submit buttons show success (checkmark, 700ms green via `.btn--success`) and error (shake via `.btn--shaking`).
|
||||
- **Inputs:** `var(--radius-sm)`, 1.5px border, padding 12px 16px. `[required]` fields receive validation status on blur (`.form-field--error` / `.form-field--valid`). Enter moves focus to the next field; Enter on the last field triggers submit.
|
||||
- **FAB (Floating Action Button):** Color follows the module accent token (`--module-accent`) — each module defines its own accent color. Hidden when the virtual keyboard is open (`visualViewport.resize`, threshold 75% of window height).
|
||||
- **Module accent colors:** `--module-accent` is applied on three visual layers — (1) active nav tab (bottom bar + sidebar stripe), (2) toolbar `border-top: 3px`, (3) cards/rows `border-left: 3px`. The active accent is written to `--active-module-accent` on `:root` on every navigation change. Falls back to `--color-accent` for pages without a module context.
|
||||
- **FAB (Floating Action Button):** Color follows the module accent token (`--module-accent`) - each module defines its own accent color. Hidden when the virtual keyboard is open (`visualViewport.resize`, threshold 75% of window height).
|
||||
- **Module accent colors:** `--module-accent` is applied on three visual layers - (1) active nav tab (bottom bar + sidebar stripe), (2) toolbar `border-top: 3px`, (3) cards/rows `border-left: 3px`. The active accent is written to `--active-module-accent` on `:root` on every navigation change. Falls back to `--color-accent` for pages without a module context.
|
||||
- **Navigation:** Bottom tab bar on mobile (Dashboard, Tasks, Calendar, Meals, More). Sidebar on desktop.
|
||||
- **Transitions:** Directional slide-X animation on page change (forward = from right, back = from left, 200ms). Respects `prefers-reduced-motion`.
|
||||
- **Empty states:** Consistent `.empty-state` class across all modules (icon + title + description, centered). Compact variant `.empty-state--compact` for meal slots.
|
||||
- **Modals:** Centered panel on desktop. On mobile (< 768px) bottom sheet — slides in from below, sheet handle visible, swipe-to-close (> 80px downward). `focusin` scrolls inputs into view when the virtual keyboard is open.
|
||||
- **List animation:** Staggered fade-in on load (`stagger()` from `public/utils/ux.js`) — max 5 elements staggered (30ms gap), rest appear immediately.
|
||||
- **Vibration:** `vibrate()` from `public/utils/ux.js` — short pulses for light actions (10–40ms), pattern `[30, 50, 30]` for destructive actions (delete). Respects `prefers-reduced-motion`.
|
||||
- **Modals:** Centered panel on desktop. On mobile (< 768px) bottom sheet - slides in from below, sheet handle visible, swipe-to-close (> 80px downward). `focusin` scrolls inputs into view when the virtual keyboard is open.
|
||||
- **List animation:** Staggered fade-in on load (`stagger()` from `public/utils/ux.js`) - max 5 elements staggered (30ms gap), rest appear immediately.
|
||||
- **Vibration:** `vibrate()` from `public/utils/ux.js` - short pulses for light actions (10–40ms), pattern `[30, 50, 30]` for destructive actions (delete). Respects `prefers-reduced-motion`.
|
||||
- **PWA install prompt:** Appears only after 2 user interactions. Dismiss window 7 days; interaction counter resets after dismiss.
|
||||
- **PWA offline fallback:** Service worker serves `/offline.html` when the network is unreachable and `index.html` is not cached. Includes a reload button.
|
||||
|
||||
@@ -323,11 +323,11 @@ All UI strings are managed via `public/i18n.js`. No hardcoded text in JS files o
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Module:** `public/i18n.js` — exports: `initI18n()`, `setLocale()`, `t(key, params?)`, `getLocale()`, `getSupportedLocales()`, `formatDate(date)`, `formatTime(date)`
|
||||
- **Locale files:** `public/locales/de.json` (reference), `public/locales/en.json` — structure: `{ "module.camelCaseKey": "Value" }`
|
||||
- **Module:** `public/i18n.js` - exports: `initI18n()`, `setLocale()`, `t(key, params?)`, `getLocale()`, `getSupportedLocales()`, `formatDate(date)`, `formatTime(date)`
|
||||
- **Locale files:** `public/locales/de.json` (reference), `public/locales/en.json` - structure: `{ "module.camelCaseKey": "Value" }`
|
||||
- **Variables:** `{{variable}}` syntax in translation strings, e.g. `t('tasks.assignedTo', { name: 'Anna' })`
|
||||
- **Fallback chain:** active locale → German (`de`) → key itself
|
||||
- **Date format:** `Intl.DateTimeFormat` with current locale — use `formatDate()` and `formatTime()` from `i18n.js`
|
||||
- **Date format:** `Intl.DateTimeFormat` with current locale - use `formatDate()` and `formatTime()` from `i18n.js`
|
||||
|
||||
### Language Detection
|
||||
|
||||
@@ -343,4 +343,4 @@ All UI strings are managed via `public/i18n.js`. No hardcoded text in JS files o
|
||||
|
||||
### Locale Switching
|
||||
|
||||
`setLocale(locale)` saves the selection, loads the new locale file, and fires the `locale-changed` custom event. All page modules and web components listen to this event and re-render — no page reload required.
|
||||
`setLocale(locale)` saves the selection, loads the new locale file, and fires the `locale-changed` custom event. All page modules and web components listen to this event and re-render - no page reload required.
|
||||
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
# CLAUDE.md Audit — Oikos
|
||||
# CLAUDE.md Audit - Oikos
|
||||
|
||||
**Datum:** 2026-04-02
|
||||
**Aktuelle Länge:** 109 Zeilen (exkl. Leerzeilen)
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
**Fehlend:**
|
||||
- Kein expliziter Hard-Constraints-Block. "No SPA framework" ist in einen Fließtext-Absatz eingebettet statt als nicht-verhandelbare Regel markiert.
|
||||
- `public/utils/ux.js` wird von 7 Seiten importiert — kein Wort davon.
|
||||
- `public/utils/ux.js` wird von 7 Seiten importiert - kein Wort davon.
|
||||
- `public/i18n.js` wird als System erwähnt, aber nicht als kanonische Datei referenziert.
|
||||
- `offline.html` fehlt im Architecture-Tree.
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
**Redundanzen mit CONTRIBUTING.md:**
|
||||
- Project Structure: fast identischer Tree in beiden Dateien
|
||||
- Code Conventions: ES modules, Semicolons, Header comments, try/catch — alles doppelt
|
||||
- Code Conventions: ES modules, Semicolons, Header comments, try/catch - alles doppelt
|
||||
- Testing: identische Beschreibung in beiden
|
||||
- Changelog: gleiche Anweisung in beiden
|
||||
- Migration append-only: in beiden Dateien
|
||||
@@ -37,9 +37,9 @@
|
||||
### 1.3 Signalqualität
|
||||
|
||||
**Probleme:**
|
||||
- "No SPA framework" ist deskriptiv formuliert statt imperativ — ein LLM könnte es als Kontextinfo statt als harte Regel lesen.
|
||||
- "No SPA framework" ist deskriptiv formuliert statt imperativ - ein LLM könnte es als Kontextinfo statt als harte Regel lesen.
|
||||
- Security Model ist eine Faktenbeschreibung. Kein Satz sagt "tu X" oder "tu niemals Y".
|
||||
- Backend-Dependency-Liste ist reines Inventar — Claude kann `package.json` lesen.
|
||||
- Backend-Dependency-Liste ist reines Inventar - Claude kann `package.json` lesen.
|
||||
- Deployment-Dockerfile-Snippet: rein informativ, kein Handlungsbezug.
|
||||
- `node:20-slim` im CLAUDE.md-Deployment-Block, aber Dockerfile verwendet tatsächlich `node:22-slim` → **falsche Information**.
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
| Erwähnt in CLAUDE.md | Tatsächlicher Zustand | Problem |
|
||||
|---|---|---|
|
||||
| `public/assets/` (apple-touch-icon, favicons) | Verzeichnis existiert nicht — Icons in `public/icons/` | **Falscher Pfad** |
|
||||
| `public/assets/` (apple-touch-icon, favicons) | Verzeichnis existiert nicht - Icons in `public/icons/` | **Falscher Pfad** |
|
||||
| `scripts/generate-icons.js` | Existiert ✅, aber `scripts/seed-demo.js` fehlt im Tree | Unvollständig |
|
||||
| Dockerfile `node:20-slim` | Dockerfile nutzt `node:22-slim` | **Falsche Version** |
|
||||
| Components: `modal.js`, `oikos-install-prompt.js` | Auch `oikos-locale-picker.js` existiert | Unvollständig |
|
||||
@@ -64,7 +64,7 @@
|
||||
|-----------|--------|------------|
|
||||
| Nur verhaltenssteuernd, keine Spec-Duplikation | ⚠️ | Security Model, Deployment, Dependency-Liste sind rein informativ |
|
||||
| Harte Constraints klar abgegrenzt | ❌ | Kein separater Block; "no framework" in Fließtext versteckt |
|
||||
| Referenztabelle vollständig & aktuell | ❌ | Keine Referenztabelle vorhanden — nur ein Einzeiler zu SPEC.md |
|
||||
| Referenztabelle vollständig & aktuell | ❌ | Keine Referenztabelle vorhanden - nur ein Einzeiler zu SPEC.md |
|
||||
| Pfade/Module korrekt | ❌ | `public/assets/` existiert nicht, Dockerfile-Version falsch |
|
||||
| Keine Redundanz mit CONTRIBUTING.md | ❌ | ~40% Überlappung (Structure, Conventions, Testing, Changelog) |
|
||||
| Keine Redundanz mit docs/ | ✅ | SPEC.md wird referenziert, nicht dupliziert |
|
||||
@@ -96,12 +96,12 @@ Siehe `CLAUDE.md.proposed` im Repo-Root.
|
||||
|
||||
| Was | Warum verhaltensrelevant |
|
||||
|-----|--------------------------|
|
||||
| Expliziter Hard Constraints Block | Claude muss sofort wissen, was nicht verhandelbar ist — nicht erst aus Fließtext erschließen. |
|
||||
| Expliziter Hard Constraints Block | Claude muss sofort wissen, was nicht verhandelbar ist - nicht erst aus Fließtext erschließen. |
|
||||
| `public/utils/ux.js` als kanonische Utility | Wird von 7 Pages importiert. Ohne dieses Wissen würde Claude Utility-Funktionen duplizieren. |
|
||||
| `public/i18n.js` + `public/locales/` in Structure | Kanonische Orte für i18n — Claude muss wissen, wo Locale-Keys definiert werden. |
|
||||
| `public/i18n.js` + `public/locales/` in Structure | Kanonische Orte für i18n - Claude muss wissen, wo Locale-Keys definiert werden. |
|
||||
| `offline.html` im Tree | Existiert, wird vom Service Worker referenziert, fehlte komplett. |
|
||||
| Reference Documents Tabelle | Claude muss wissen, wo welche kanonische Wahrheit liegt, statt zu raten. |
|
||||
| `oikos-locale-picker.js` in Components | Existierende Komponente fehlte — Claude könnte sie unwissentlich neu bauen. |
|
||||
| `oikos-locale-picker.js` in Components | Existierende Komponente fehlte - Claude könnte sie unwissentlich neu bauen. |
|
||||
|
||||
### Umformuliert
|
||||
|
||||
@@ -115,6 +115,6 @@ Siehe `CLAUDE.md.proposed` im Repo-Root.
|
||||
|
||||
| Was | Von | Nach | Grund |
|
||||
|-----|-----|------|-------|
|
||||
| Security Model Details | CLAUDE.md | Bereits in Code (middleware, auth.js) | Kein eigenes Dokument nötig — Code ist die Wahrheit |
|
||||
| Security Model Details | CLAUDE.md | Bereits in Code (middleware, auth.js) | Kein eigenes Dokument nötig - Code ist die Wahrheit |
|
||||
| Deployment env vars, Dockerfile | CLAUDE.md | README.md (bereits dort vorhanden) | Ops-Information, nicht Entwicklungsverhalten |
|
||||
| Detaillierter Project Structure Tree | CLAUDE.md | CONTRIBUTING.md (bereits dort vorhanden) | Duplikation eliminieren — CONTRIBUTING.md hat den vollständigen Tree |
|
||||
| Detaillierter Project Structure Tree | CLAUDE.md | CONTRIBUTING.md (bereits dort vorhanden) | Duplikation eliminieren - CONTRIBUTING.md hat den vollständigen Tree |
|
||||
|
||||
+6
-6
@@ -3,21 +3,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oikos — The Self-Hosted Family Planner</title>
|
||||
<title>Oikos - The Self-Hosted Family Planner</title>
|
||||
<meta name="description" content="A privacy-first, self-hosted family planner with tasks, shopping lists, meal planning, calendar sync, budget tracking, and more. No cloud, no tracking, your data.">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Oikos — The Self-Hosted Family Planner">
|
||||
<meta property="og:description" content="A privacy-first, self-hosted family planner. Tasks, shopping, meals, calendar sync, budget — all self-hosted, no cloud dependency.">
|
||||
<meta property="og:title" content="Oikos - The Self-Hosted Family Planner">
|
||||
<meta property="og:description" content="A privacy-first, self-hosted family planner. Tasks, shopping, meals, calendar sync, budget - all self-hosted, no cloud dependency.">
|
||||
<meta property="og:image" content="https://ulsklyc.github.io/oikos/og-image.png">
|
||||
<meta property="og:url" content="https://ulsklyc.github.io/oikos/">
|
||||
<meta property="og:site_name" content="Oikos">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Oikos — The Self-Hosted Family Planner">
|
||||
<meta name="twitter:description" content="A privacy-first, self-hosted family planner. Tasks, shopping, meals, calendar sync, budget — all self-hosted.">
|
||||
<meta name="twitter:title" content="Oikos - The Self-Hosted Family Planner">
|
||||
<meta name="twitter:description" content="A privacy-first, self-hosted family planner. Tasks, shopping, meals, calendar sync, budget - all self-hosted.">
|
||||
<meta name="twitter:image" content="https://ulsklyc.github.io/oikos/twitter-image.png">
|
||||
|
||||
<link rel="canonical" href="https://ulsklyc.github.io/oikos/">
|
||||
@@ -568,7 +568,7 @@
|
||||
<img src="logo.svg" alt="Oikos Logo" class="hero-logo" width="80" height="80">
|
||||
<h1 data-i18n="hero_title">Oikos</h1>
|
||||
<p class="hero-tagline" data-i18n="hero_tagline">The self-hosted family planner</p>
|
||||
<p class="hero-desc" data-i18n="hero_desc">Manage your household together. Tasks, shopping, meals, calendar, budget — all in one place. Self-hosted, private, yours.</p>
|
||||
<p class="hero-desc" data-i18n="hero_desc">Manage your household together. Tasks, shopping, meals, calendar, budget - all in one place. Self-hosted, private, yours.</p>
|
||||
<a href="https://github.com/ulsklyc/oikos" class="hero-cta" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
<span data-i18n="hero_cta">View on GitHub</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Repository Hygiene Audit — Oikos
|
||||
# Repository Hygiene Audit - Oikos
|
||||
|
||||
**Datum:** 2026-04-02
|
||||
**Version:** 0.5.2 (Tag `v0.5.2`)
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
| # | Datei | Status | Begründung | Empfehlung |
|
||||
|---|-------|--------|------------|------------|
|
||||
| 1 | `.worktrees/feature/` | Verwaist | Leeres Verzeichnis — Überrest eines abgeschlossenen Git-Worktree. Kein Inhalt. | **Löschen** (`rm -rf .worktrees/feature/`) |
|
||||
| 1 | `.worktrees/feature/` | Verwaist | Leeres Verzeichnis - Überrest eines abgeschlossenen Git-Worktree. Kein Inhalt. | **Löschen** (`rm -rf .worktrees/feature/`) |
|
||||
|
||||
**Keine verwaisten JS/CSS/HTML-Dateien gefunden.** Alle Module in `public/pages/`, `public/components/`, `public/styles/` und `server/routes/` sind korrekt referenziert:
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
| # | Datei | Status | Begründung | Empfehlung |
|
||||
|---|-------|--------|------------|------------|
|
||||
| — | — | — | Keine gefunden. | — |
|
||||
| - | - | - | Keine gefunden. | - |
|
||||
|
||||
Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
|
||||
@@ -40,7 +40,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
| 1 | `.env` | OK | Existiert lokal, **nicht** von Git getrackt, in `.gitignore` gelistet. | Kein Handlungsbedarf |
|
||||
| 2 | `.claude/` | OK | In `.gitignore`, nicht getrackt. | Kein Handlungsbedarf |
|
||||
| 3 | `.worktrees/` | OK | In `.gitignore`, nicht getrackt. Leeres `feature/`-Verzeichnis (s. 1.1). | Leeres Subdir löschen |
|
||||
| 4 | `docs/superpowers/` | OK | In `.gitignore`, nicht getrackt. Enthält 7 Plandokumente (3076 Zeilen) lokal. | Kein Handlungsbedarf — `.gitignore` greift |
|
||||
| 4 | `docs/superpowers/` | OK | In `.gitignore`, nicht getrackt. Enthält 7 Plandokumente (3076 Zeilen) lokal. | Kein Handlungsbedarf - `.gitignore` greift |
|
||||
|
||||
### 1.4 Prompt-Dateien
|
||||
|
||||
@@ -50,15 +50,15 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
|
||||
| # | Datei | Status | Begründung | Empfehlung |
|
||||
|---|-------|--------|------------|------------|
|
||||
| — | — | — | Keine leeren oder Stub-Dateien gefunden. | — |
|
||||
| - | - | - | Keine leeren oder Stub-Dateien gefunden. | - |
|
||||
|
||||
### 1.6 Sonderfall: Hilfs-/Script-Dateien
|
||||
|
||||
| # | Datei | Zeilen | Genutzt von | Empfehlung |
|
||||
|---|-------|--------|-------------|------------|
|
||||
| 1 | `server/db-schema-test.js` | 185 | Allen 7 Test-Dateien (`test-*.js`) | **Behalten** — aktiv genutzte Test-Infrastruktur |
|
||||
| 2 | `scripts/seed-demo.js` | 343 | Manuell (CLI-Script für Screenshots) | **Behalten** — nützliches Dev-Tool |
|
||||
| 3 | `test-browser-loader.mjs` | — | `package.json` test:modal-utils | **Behalten** — aktiv genutzt für Browser-API-Mocks |
|
||||
| 1 | `server/db-schema-test.js` | 185 | Allen 7 Test-Dateien (`test-*.js`) | **Behalten** - aktiv genutzte Test-Infrastruktur |
|
||||
| 2 | `scripts/seed-demo.js` | 343 | Manuell (CLI-Script für Screenshots) | **Behalten** - nützliches Dev-Tool |
|
||||
| 3 | `test-browser-loader.mjs` | - | `package.json` test:modal-utils | **Behalten** - aktiv genutzt für Browser-API-Mocks |
|
||||
|
||||
---
|
||||
|
||||
@@ -75,9 +75,9 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
| `SECURITY.md` | ✅ Vorhanden & vollständig | Vulnerability Reporting via GitHub Private Advisories, Scope-Definition, Security-Features-Übersicht, Supported-Versions-Policy. |
|
||||
| `CHANGELOG.md` | ⚠️ Vorhanden, kleiner Fehler | Keep a Changelog Format, retroaktive Versionierung ab 0.1.0 ✅. **Problem:** Compare-Links am Dateiende: `[Unreleased]` verweist auf `v0.5.1...HEAD` statt `v0.5.2...HEAD`. Außerdem fehlt der `[0.5.2]`-Compare-Link. |
|
||||
| `CLAUDE.md` | ✅ Vorhanden & aktuell | Behavioral guide (109 Zeilen), nicht von Git getrackt (in .gitignore). Beschreibt Architektur, Konventionen, Security, DB, Deployment, Implementation Status. Keine Duplikation von SPEC.md. |
|
||||
| `BACKLOG.md` | ✅ Vorhanden & aktuell | Enthält erledigte Features als Referenztabelle (BL-01 bis BL-10 mit Versionszuordnung). Aktiv keine offenen Einträge — korrekt für aktuellen Projektstand. |
|
||||
| `BACKLOG.md` | ✅ Vorhanden & aktuell | Enthält erledigte Features als Referenztabelle (BL-01 bis BL-10 mit Versionszuordnung). Aktiv keine offenen Einträge - korrekt für aktuellen Projektstand. |
|
||||
| `docs/SPEC.md` | ✅ Vorhanden | Nicht im Detail geprüft (Umfang), aber von CLAUDE.md und README.md referenziert und zentral für alle Module. |
|
||||
| `.dockerignore` | ⚠️ Vorhanden, kleiner Fehler | Sinnvolle Ausschlüsse. **Problem:** Referenziert `docs/social-preview.html` — diese Datei existiert nicht (nur `docs/social-preview.png` existiert). Harmlos (Ausschluss einer nicht-existenten Datei), aber irreführend. |
|
||||
| `.dockerignore` | ⚠️ Vorhanden, kleiner Fehler | Sinnvolle Ausschlüsse. **Problem:** Referenziert `docs/social-preview.html` - diese Datei existiert nicht (nur `docs/social-preview.png` existiert). Harmlos (Ausschluss einer nicht-existenten Datei), aber irreführend. |
|
||||
| `docker-compose.yml` | ✅ Vorhanden | Nicht im Detail geprüft, aber vorhanden und funktional laut README/CHANGELOG. |
|
||||
| `Dockerfile` | ✅ Vorhanden | Nicht im Detail geprüft, vorhanden. |
|
||||
| `.github/ISSUE_TEMPLATE/bug_report.md` | ✅ Vorhanden & vollständig | Gutes Template mit Description, Steps to Reproduce, Expected/Actual Behavior, Environment, Logs (collapsible), Additional Context. |
|
||||
@@ -89,7 +89,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
| Datei | Status | Bewertung |
|
||||
|-------|--------|-----------|
|
||||
| `docs/` Verzeichnis | ✅ Vorhanden | Enthält SPEC.md, logo.svg, social-preview.png, Screenshots (4 Varianten × 9 Module = 36 Screenshots). |
|
||||
| `manifest.json` | ✅ Vorhanden | `public/manifest.json` — PWA-Manifest. |
|
||||
| `manifest.json` | ✅ Vorhanden | `public/manifest.json` - PWA-Manifest. |
|
||||
| Service Worker | ✅ Vorhanden | `public/sw.js` + `public/sw-register.js`. |
|
||||
| `.nvmrc` | ✅ Vorhanden | Inhalt: `22`. Konsistent mit `engines.node: ">=22.0.0"` in package.json. |
|
||||
| `.env.example` | ✅ Vorhanden & vollständig | 39 Zeilen. Alle dokumentierten Env-Vars abgedeckt: PORT, NODE_ENV, SESSION_SECRET, SESSION_SECURE, DB_PATH, DB_ENCRYPTION_KEY, OpenWeather, Google Calendar, Apple CalDAV, Sync-Intervall, Rate-Limiting. |
|
||||
@@ -100,7 +100,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
|
||||
| Element | Status | Bewertung |
|
||||
|---------|--------|-----------|
|
||||
| Git Tags | ✅ Konsistent | v0.1.0 bis v0.5.2 — alle CHANGELOG-Versionen haben entsprechende Tags. |
|
||||
| Git Tags | ✅ Konsistent | v0.1.0 bis v0.5.2 - alle CHANGELOG-Versionen haben entsprechende Tags. |
|
||||
| `scripts/` | ✅ Sinnvoll | `generate-icons.js` (Dev-Tool) und `seed-demo.js` (Screenshot-Generierung). Beide in `.dockerignore` exkludiert. |
|
||||
| `entrypoint.sh` | ✅ Vorhanden | Docker-Entrypoint. |
|
||||
| `setup.js` | ✅ Vorhanden | Admin-Setup-Wizard, in README dokumentiert. |
|
||||
@@ -111,7 +111,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
|
||||
| Kriterium | Status | Bewertung |
|
||||
|-----------|--------|-----------|
|
||||
| Projektname und Kurzbeschreibung | ✅ Vorhanden | Logo + "Oikos" + "The self-hosted family planner that respects your privacy." — klar und prägnant. |
|
||||
| Projektname und Kurzbeschreibung | ✅ Vorhanden | Logo + "Oikos" + "The self-hosted family planner that respects your privacy." - klar und prägnant. |
|
||||
| Badges | ✅ Vorhanden | 7 Badges: Release, License, Node.js, Docker, SQLCipher, PWA, i18n. flat-square Style. |
|
||||
| Screenshots/Demo | ✅ Vorhanden & hervorragend | 9 Mobile-Screens (dark/light auto-switch via `<picture>`), 8 Tablet-Screens in collapsible Section. Alle 4 Varianten (mobile-dark, mobile-light, tablet-dark, tablet-light). |
|
||||
| Features-Übersicht | ✅ Vorhanden | 8 Module in 2-Spalten-Tabelle + "And also"-Absatz mit Querschnittsfunktionen. |
|
||||
@@ -145,7 +145,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
- **Aufwand**: 5 Minuten
|
||||
- **Dateien**: `CHANGELOG.md` (Zeilen 139–147)
|
||||
|
||||
### [P2] .dockerignore — Phantomreferenz entfernen
|
||||
### [P2] .dockerignore - Phantomreferenz entfernen
|
||||
|
||||
- **Was**: `.dockerignore` exkludiert `docs/social-preview.html`, aber diese Datei existiert nicht (nur `docs/social-preview.png` existiert).
|
||||
- **Warum**: Irreführend bei Code-Review oder Wartung. Kein funktionaler Impact.
|
||||
@@ -165,12 +165,12 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
|
||||
|
||||
| Kategorie | Ergebnis |
|
||||
|-----------|----------|
|
||||
| **Verwaiste Dateien** | 1 Fund (leeres Worktree-Dir) — P3 |
|
||||
| **Verwaiste Dateien** | 1 Fund (leeres Worktree-Dir) - P3 |
|
||||
| **Superseded Files** | 0 Funde |
|
||||
| **Dev Artifacts** | Korrekt durch .gitignore abgedeckt |
|
||||
| **Prompt-Dateien** | Keine im Repo |
|
||||
| **Leere Dateien** | 0 Funde |
|
||||
| **Projektdateien** | 2 Minor-Issues (CHANGELOG-Links, .dockerignore Phantom) — P2 |
|
||||
| **Projektdateien** | 2 Minor-Issues (CHANGELOG-Links, .dockerignore Phantom) - P2 |
|
||||
| **README** | Vollständig, keine Lücken |
|
||||
| **P0-Findings** | 0 |
|
||||
| **P1-Findings** | 0 |
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.5.8",
|
||||
"description": "Selbstgehosteter Familienplaner — Kalender, Aufgaben, Einkauf, Essensplan, Budget und mehr. Privat, offen, ohne Abo.",
|
||||
"description": "Selbstgehosteter Familienplaner - Kalender, Aufgaben, Einkauf, Essensplan, Budget und mehr. Privat, offen, ohne Abo.",
|
||||
"main": "server/index.js",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
|
||||
@@ -138,7 +138,7 @@ function _wireSheetSwipe(panel) {
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// _doClose — gemeinsame Cleanup-Logik
|
||||
// _doClose - gemeinsame Cleanup-Logik
|
||||
// --------------------------------------------------------
|
||||
|
||||
function _doClose() {
|
||||
@@ -169,12 +169,12 @@ function _doClose() {
|
||||
* Öffnet ein Modal mit dem Shared-System.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.title — Titel im Modal-Header
|
||||
* @param {string} opts.content — HTML-String für den Modal-Body
|
||||
* @param {Function} [opts.onSave] — Callback, wird nach Einfügen in DOM aufgerufen
|
||||
* @param {string} opts.title - Titel im Modal-Header
|
||||
* @param {string} opts.content - HTML-String für den Modal-Body
|
||||
* @param {Function} [opts.onSave] - Callback, wird nach Einfügen in DOM aufgerufen
|
||||
* (zum Binden von Form-Events)
|
||||
* @param {Function} [opts.onDelete] — Falls vorhanden, wird ein Löschen-Button eingebaut
|
||||
* @param {string} [opts.size='md'] — 'sm' | 'md' | 'lg'
|
||||
* @param {Function} [opts.onDelete] - Falls vorhanden, wird ein Löschen-Button eingebaut
|
||||
* @param {string} [opts.size='md'] - 'sm' | 'md' | 'lg'
|
||||
*/
|
||||
export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) {
|
||||
// Vorheriges Modal schließen (kein Stacking)
|
||||
|
||||
@@ -27,7 +27,7 @@ class OikosInstallPrompt extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Bereits im Standalone-Modus — nichts anzeigen
|
||||
// Bereits im Standalone-Modus - nichts anzeigen
|
||||
if (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
navigator.standalone === true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* oikos-locale-picker — Sprachauswahl-Web-Component
|
||||
* oikos-locale-picker - Sprachauswahl-Web-Component
|
||||
* Zeigt ein <select>-Dropdown für System/Deutsch/English.
|
||||
* Bei Auswahl: setLocale() oder localStorage-Eintrag löschen (System).
|
||||
* Dependencies: i18n.js
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* i18n — Internationalisierung / Übersetzungsmodul
|
||||
* i18n - Internationalisierung / Übersetzungsmodul
|
||||
* Bietet t(), initI18n(), setLocale(), getLocale(), getSupportedLocales(),
|
||||
* formatDate(), formatTime() für die gesamte App.
|
||||
* Dependencies: none (vanilla JS, Fetch API, Intl API)
|
||||
@@ -33,7 +33,7 @@ async function loadLocale(locale) {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
/** Initialisierung — einmal beim App-Start aufrufen */
|
||||
/** Initialisierung - einmal beim App-Start aufrufen */
|
||||
export async function initI18n() {
|
||||
currentLocale = resolveLocale();
|
||||
fallbackTranslations = await loadLocale(DEFAULT_LOCALE);
|
||||
@@ -50,7 +50,7 @@ export async function initI18n() {
|
||||
document.documentElement.lang = currentLocale;
|
||||
}
|
||||
|
||||
/** Sprache wechseln — löst 'locale-changed' Event aus */
|
||||
/** Sprache wechseln - löst 'locale-changed' Event aus */
|
||||
export async function setLocale(locale) {
|
||||
if (!SUPPORTED_LOCALES.includes(locale)) return;
|
||||
localStorage.setItem(STORAGE_KEY, locale);
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Oikos" />
|
||||
<meta name="description" content="Oikos — Familienplaner" />
|
||||
<meta name="description" content="Oikos - Familienplaner" />
|
||||
<title>Oikos</title>
|
||||
|
||||
<!-- PWA -->
|
||||
@@ -59,7 +59,7 @@
|
||||
<script src="/lucide.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- App-Shell — wird durch JavaScript gefüllt -->
|
||||
<!-- App-Shell - wird durch JavaScript gefüllt -->
|
||||
<div id="app" class="app-shell">
|
||||
<!-- Skeleton-Loading während Initialisierung -->
|
||||
<div id="app-loading" class="app-loading" aria-live="polite" aria-label="Lade Oikos…">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"errorOccurred": "Etwas ist schiefgelaufen.",
|
||||
"unexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
"errorGeneric": "Ein Fehler ist aufgetreten.",
|
||||
"updateAvailable": "Update verfügbar — Seite neu laden für die neueste Version.",
|
||||
"updateAvailable": "Update verfügbar - Seite neu laden für die neueste Version.",
|
||||
"titleRequired": "Titel ist erforderlich",
|
||||
"nameRequired": "Name ist erforderlich",
|
||||
"contentRequired": "Inhalt ist erforderlich",
|
||||
@@ -82,7 +82,7 @@
|
||||
"title": "Aufgaben",
|
||||
"newTask": "Neue Aufgabe",
|
||||
"editTask": "Aufgabe bearbeiten",
|
||||
"emptyTitle": "Keine Aufgaben — alles erledigt?",
|
||||
"emptyTitle": "Keine Aufgaben - alles erledigt?",
|
||||
"emptyDescription": "Neue Aufgaben über den + Button erstellen.",
|
||||
"titleLabel": "Titel *",
|
||||
"titlePlaceholder": "Was muss erledigt werden?",
|
||||
@@ -93,7 +93,7 @@
|
||||
"dueDateLabel": "Fälligkeit",
|
||||
"dueTimeLabel": "Uhrzeit",
|
||||
"assignedLabel": "Zugewiesen an",
|
||||
"assignedNobody": "— Niemand —",
|
||||
"assignedNobody": "- Niemand -",
|
||||
"statusLabel": "Status",
|
||||
"priorityUrgent": "Dringend",
|
||||
"priorityHigh": "Hoch",
|
||||
@@ -259,7 +259,7 @@
|
||||
"locationLabel": "Ort",
|
||||
"locationPlaceholder": "Optional",
|
||||
"assignedLabel": "Zugewiesen an",
|
||||
"assignedNobody": "— Niemand —",
|
||||
"assignedNobody": "- Niemand -",
|
||||
"colorLabel": "Farbe",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"descriptionPlaceholder": "Optional…",
|
||||
@@ -428,7 +428,7 @@
|
||||
"savedToast": "Eintrag gespeichert",
|
||||
"deletedToast": "Eintrag gelöscht",
|
||||
"loadError": "Budget konnte nicht geladen werden.",
|
||||
"trendNeutral": "— wie {{month}}",
|
||||
"trendNeutral": "- wie {{month}}",
|
||||
"validAmountRequired": "Gültigen Betrag eingeben",
|
||||
"dateRequired": "Datum ist erforderlich",
|
||||
"catFood": "Lebensmittel",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"errorOccurred": "Something went wrong.",
|
||||
"unexpectedError": "An unexpected error occurred.",
|
||||
"errorGeneric": "An error occurred.",
|
||||
"updateAvailable": "Update available — reload the page to get the latest version.",
|
||||
"updateAvailable": "Update available - reload the page to get the latest version.",
|
||||
"titleRequired": "Title is required",
|
||||
"nameRequired": "Name is required",
|
||||
"contentRequired": "Content is required",
|
||||
@@ -82,7 +82,7 @@
|
||||
"title": "Tasks",
|
||||
"newTask": "New Task",
|
||||
"editTask": "Edit Task",
|
||||
"emptyTitle": "No tasks — all done?",
|
||||
"emptyTitle": "No tasks - all done?",
|
||||
"emptyDescription": "Create new tasks with the + button.",
|
||||
"titleLabel": "Title *",
|
||||
"titlePlaceholder": "What needs to be done?",
|
||||
@@ -93,7 +93,7 @@
|
||||
"dueDateLabel": "Due date",
|
||||
"dueTimeLabel": "Time",
|
||||
"assignedLabel": "Assigned to",
|
||||
"assignedNobody": "— Nobody —",
|
||||
"assignedNobody": "- Nobody -",
|
||||
"statusLabel": "Status",
|
||||
"priorityUrgent": "Urgent",
|
||||
"priorityHigh": "High",
|
||||
@@ -259,7 +259,7 @@
|
||||
"locationLabel": "Location",
|
||||
"locationPlaceholder": "Optional",
|
||||
"assignedLabel": "Assigned to",
|
||||
"assignedNobody": "— Nobody —",
|
||||
"assignedNobody": "- Nobody -",
|
||||
"colorLabel": "Color",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Optional…",
|
||||
@@ -428,7 +428,7 @@
|
||||
"savedToast": "Entry saved",
|
||||
"deletedToast": "Entry deleted",
|
||||
"loadError": "Budget could not be loaded.",
|
||||
"trendNeutral": "— same as {{month}}",
|
||||
"trendNeutral": "- same as {{month}}",
|
||||
"validAmountRequired": "Please enter a valid amount",
|
||||
"dateRequired": "Date is required",
|
||||
"catFood": "Groceries",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"errorOccurred": "Si è verificato un errore.",
|
||||
"unexpectedError": "Si è verificato un errore imprevisto.",
|
||||
"errorGeneric": "Si è verificato un errore.",
|
||||
"updateAvailable": "Aggiornamento disponibile — ricarica la pagina per ottenere l'ultima versione.",
|
||||
"updateAvailable": "Aggiornamento disponibile - ricarica la pagina per ottenere l'ultima versione.",
|
||||
"titleRequired": "Il titolo è obbligatorio",
|
||||
"nameRequired": "Il nome è obbligatorio",
|
||||
"contentRequired": "Il contenuto è obbligatorio",
|
||||
@@ -82,7 +82,7 @@
|
||||
"title": "Compiti",
|
||||
"newTask": "Nuovo compito",
|
||||
"editTask": "Modifica compito",
|
||||
"emptyTitle": "Nessun compito — tutto fatto?",
|
||||
"emptyTitle": "Nessun compito - tutto fatto?",
|
||||
"emptyDescription": "Crea nuovi compiti con il pulsante +.",
|
||||
"titleLabel": "Titolo *",
|
||||
"titlePlaceholder": "Cosa bisogna fare?",
|
||||
@@ -93,7 +93,7 @@
|
||||
"dueDateLabel": "Data di scadenza",
|
||||
"dueTimeLabel": "Ora",
|
||||
"assignedLabel": "Assegnato a",
|
||||
"assignedNobody": "— Nessuno —",
|
||||
"assignedNobody": "- Nessuno -",
|
||||
"statusLabel": "Stato",
|
||||
"priorityUrgent": "Urgente",
|
||||
"priorityHigh": "Alta",
|
||||
@@ -259,7 +259,7 @@
|
||||
"locationLabel": "Luogo",
|
||||
"locationPlaceholder": "Opzionale",
|
||||
"assignedLabel": "Assegnato a",
|
||||
"assignedNobody": "— Nessuno —",
|
||||
"assignedNobody": "- Nessuno -",
|
||||
"colorLabel": "Colore",
|
||||
"descriptionLabel": "Descrizione",
|
||||
"descriptionPlaceholder": "Opzionale…",
|
||||
@@ -428,7 +428,7 @@
|
||||
"savedToast": "Voce salvata",
|
||||
"deletedToast": "Voce eliminata",
|
||||
"loadError": "Impossibile caricare il bilancio.",
|
||||
"trendNeutral": "— come {{month}}",
|
||||
"trendNeutral": "- come {{month}}",
|
||||
"validAmountRequired": "Inserisci un importo valido",
|
||||
"dateRequired": "La data è obbligatoria",
|
||||
"catFood": "Spesa alimentare",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { api } from '/api.js';
|
||||
import { t, formatDate, formatTime, getLocale } from '/i18n.js';
|
||||
|
||||
// Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert.
|
||||
// Hält den AbortController des aktuellen FAB-Listeners - wird bei jedem render() erneuert.
|
||||
let _fabController = null;
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -225,7 +225,7 @@ function renderTodayMeals(meals) {
|
||||
<div class="meal-slot ${meal ? 'meal-slot--filled' : ''}" data-route="/meals" role="button" tabindex="0">
|
||||
<i data-lucide="${MEAL_ICONS[type]}" class="meal-slot__icon" aria-hidden="true"></i>
|
||||
<div class="meal-slot__type">${mealLabels[type]}</div>
|
||||
<div class="meal-slot__title">${meal ? meal.title : '—'}</div>
|
||||
<div class="meal-slot__title">${meal ? meal.title : '-'}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -487,7 +487,7 @@ export async function render(container, { user }) {
|
||||
|
||||
refreshBtn.addEventListener('click', doWeatherRefresh, { signal: _fabController.signal });
|
||||
|
||||
// 30-Minuten Auto-Refresh — abortiert wenn Seite verlassen wird
|
||||
// 30-Minuten Auto-Refresh - abortiert wenn Seite verlassen wird
|
||||
const timerId = setInterval(doWeatherRefresh, 30 * 60 * 1000);
|
||||
_fabController.signal.addEventListener('abort', () => clearInterval(timerId));
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ function wireDragDrop(grid) {
|
||||
async function onUp(ev) {
|
||||
if (!dragging) return;
|
||||
const { mealId, sourceDate, sourceType, slot: sourceSlot } = dragging;
|
||||
cleanup(); // setzt dragging = null — Werte daher vorher destrukturieren
|
||||
cleanup(); // setzt dragging = null - Werte daher vorher destrukturieren
|
||||
|
||||
if (ghost) ghost.style.display = 'none';
|
||||
const el = document.elementFromPoint(ev.clientX, ev.clientY);
|
||||
|
||||
@@ -13,9 +13,9 @@ import { t } from '/i18n.js';
|
||||
// --------------------------------------------------------
|
||||
|
||||
// Swipe-Gesten Konstanten (identisch zu tasks.js)
|
||||
const SWIPE_THRESHOLD = 80; // px — Mindestweg für Aktion
|
||||
const SWIPE_MAX_VERT = 12; // px — vertikaler Toleranzbereich
|
||||
const SWIPE_LOCK_VERT = 30; // px — ab diesem Weg gilt es als Scroll
|
||||
const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
|
||||
const SWIPE_MAX_VERT = 12; // px - vertikaler Toleranzbereich
|
||||
const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll
|
||||
|
||||
const ITEM_CATEGORIES = [
|
||||
'Obst & Gemüse', 'Backwaren', 'Milchprodukte', 'Fleisch & Fisch',
|
||||
|
||||
@@ -717,9 +717,9 @@ function updateOverdueBadge() {
|
||||
// Swipe-Gesten (Mobil: links = erledigt, rechts = bearbeiten)
|
||||
// --------------------------------------------------------
|
||||
|
||||
const SWIPE_THRESHOLD = 80; // px — Mindestweg für Aktion
|
||||
const SWIPE_MAX_VERT = 12; // px — vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch)
|
||||
const SWIPE_LOCK_VERT = 30; // px — ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
|
||||
const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
|
||||
const SWIPE_MAX_VERT = 12; // px - vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch)
|
||||
const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
|
||||
|
||||
function wireSwipeGestures(container) {
|
||||
const listEl = container.querySelector('#task-list');
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Oikos Familienplaner — self-hosted, private
|
||||
# Oikos Familienplaner - self-hosted, private
|
||||
# Keine öffentlichen Inhalte zum Indexieren
|
||||
|
||||
User-agent: *
|
||||
|
||||
+4
-4
@@ -34,7 +34,7 @@ const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|
||||
/**
|
||||
* Setzt die theme-color Meta-Tags (Light + Dark Variante).
|
||||
* @param {string} lightColor
|
||||
* @param {string} [darkColor] — Falls nicht angegeben, wird lightColor für beide gesetzt
|
||||
* @param {string} [darkColor] - Falls nicht angegeben, wird lightColor für beide gesetzt
|
||||
*/
|
||||
function setThemeColor(lightColor, darkColor) {
|
||||
if (!isStandalone) return;
|
||||
@@ -116,7 +116,7 @@ async function navigate(path, userOrPushState = true, pushState = true) {
|
||||
pushState = userOrPushState;
|
||||
}
|
||||
|
||||
// Alten Pfad merken, bevor currentPath aktualisiert wird — für Richtungsberechnung
|
||||
// Alten Pfad merken, bevor currentPath aktualisiert wird - für Richtungsberechnung
|
||||
const previousPath = currentPath;
|
||||
currentPath = path;
|
||||
|
||||
@@ -176,7 +176,7 @@ async function renderPage(route, previousPath = null) {
|
||||
throw new Error(`Seite ${route.page} exportiert keine render()-Funktion.`);
|
||||
}
|
||||
|
||||
// App-Shell einmalig aufbauen BEVOR render() aufgerufen wird —
|
||||
// App-Shell einmalig aufbauen BEVOR render() aufgerufen wird -
|
||||
// page-content muss im DOM existieren damit document.getElementById()
|
||||
// in Seiten-Modulen funktioniert.
|
||||
if (!document.querySelector('.nav-bottom') && currentUser) {
|
||||
@@ -446,7 +446,7 @@ window.addEventListener('locale-changed', () => {
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen
|
||||
// Erkennung via visualViewport — Höhe < 75% des Fensters = Keyboard aktiv.
|
||||
// Erkennung via visualViewport - Höhe < 75% des Fensters = Keyboard aktiv.
|
||||
// Nur auf Mobilgeräten relevant (< 1024px), Desktop hat keine virtuelle Tastatur.
|
||||
// --------------------------------------------------------
|
||||
if (window.visualViewport) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Modul: Dashboard
|
||||
* Zweck: Styles für das Dashboard — Begrüßung, Widget-Grid, alle Widget-Typen, FAB-Speed-Dial
|
||||
* Zweck: Styles für das Dashboard - Begrüßung, Widget-Grid, alle Widget-Typen, FAB-Speed-Dial
|
||||
* Abhängigkeiten: tokens.css, layout.css
|
||||
*/
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
/* Widget hover lift (desktop) — dezent, max 1px */
|
||||
/* Widget hover lift (desktop) - dezent, max 1px */
|
||||
@media (min-width: 1024px) {
|
||||
.widget {
|
||||
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
|
||||
+15
-15
@@ -208,7 +208,7 @@
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Page FAB — Schwebender Erstellen-Button (alle Breakpoints)
|
||||
* Page FAB - Schwebender Erstellen-Button (alle Breakpoints)
|
||||
*
|
||||
* Einheitlicher runder Plus-Button auf allen Unterseiten.
|
||||
* Mobile: über der Bottom-Nav. Desktop: unten rechts im Content.
|
||||
@@ -252,7 +252,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Toolbar-"Neu"-Buttons überall verstecken — FAB übernimmt */
|
||||
/* Toolbar-"Neu"-Buttons überall verstecken - FAB übernimmt */
|
||||
#btn-new-task,
|
||||
#notes-add-btn,
|
||||
#contacts-add-btn,
|
||||
@@ -262,7 +262,7 @@
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Sidebar Navigation — Desktop (≥ 1024px)
|
||||
* Sidebar Navigation - Desktop (≥ 1024px)
|
||||
*
|
||||
* Design: Flach, kein Neumorphismus. Dezenter Seitenrand.
|
||||
* Aktiver State: Hintergrund-Highlight + Akzentstreifen links.
|
||||
@@ -413,7 +413,7 @@
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Sidebar Expanded (≥ 1280px) — Labels sichtbar
|
||||
* Sidebar Expanded (≥ 1280px) - Labels sichtbar
|
||||
* ================================================================ */
|
||||
@media (min-width: 1280px) {
|
||||
:root {
|
||||
@@ -515,11 +515,11 @@
|
||||
* - Hover auf Desktop: leichter Lift (1px)
|
||||
*
|
||||
* Varianten:
|
||||
* .card — Basis (kein Padding)
|
||||
* .card--padded — Mit Padding
|
||||
* .card--compact — Enges Padding (12px)
|
||||
* .card--flat — Kein Shadow, nur Border
|
||||
* .card--interactive — Hover-Lift + Cursor
|
||||
* .card - Basis (kein Padding)
|
||||
* .card--padded - Mit Padding
|
||||
* .card--compact - Enges Padding (12px)
|
||||
* .card--flat - Kein Shadow, nur Border
|
||||
* .card--interactive - Hover-Lift + Cursor
|
||||
* -------------------------------------------------------- */
|
||||
.card {
|
||||
background-color: var(--color-surface);
|
||||
@@ -1048,15 +1048,15 @@
|
||||
* Wiederverwendbare Content-Area-Patterns für Desktop.
|
||||
* Mobile: immer single-column (Stacking).
|
||||
*
|
||||
* .layout-master-detail — Liste links, Detail rechts (Aufgaben, Einkauf)
|
||||
* .layout-content-aside — Hauptinhalt + schmale Seitenleiste (Kalender)
|
||||
* .layout-center — Zentrierter schmaler Content (Settings, Login)
|
||||
* .layout-wide — Volle Breite mit max-width (Dashboard)
|
||||
* .layout-master-detail - Liste links, Detail rechts (Aufgaben, Einkauf)
|
||||
* .layout-content-aside - Hauptinhalt + schmale Seitenleiste (Kalender)
|
||||
* .layout-center - Zentrierter schmaler Content (Settings, Login)
|
||||
* .layout-wide - Volle Breite mit max-width (Dashboard)
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
/* ── Master-Detail ──
|
||||
* Mobile: gestapelt (Detail wird programmatisch ein-/ausgeblendet).
|
||||
* Desktop: 2 Spalten — Liste ~40%, Detail ~60%.
|
||||
* Desktop: 2 Spalten - Liste ~40%, Detail ~60%.
|
||||
*/
|
||||
.layout-master-detail {
|
||||
display: flex;
|
||||
@@ -1344,7 +1344,7 @@
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Swipe-Wrapper — Gemeinsame Basis (Tasks + Shopping)
|
||||
* Swipe-Wrapper - Gemeinsame Basis (Tasks + Shopping)
|
||||
* Modul-spezifische Styles (.swipe-reveal--edit, .swipe-reveal--delete,
|
||||
* .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS.
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Slot-Typ-Farben — zentrale Tokens aus tokens.css */
|
||||
/* Slot-Typ-Farben - zentrale Tokens aus tokens.css */
|
||||
.meal-slot[data-type="breakfast"] .meal-slot__type-label { color: var(--meal-breakfast); }
|
||||
.meal-slot[data-type="lunch"] .meal-slot__type-label { color: var(--meal-lunch); }
|
||||
.meal-slot[data-type="dinner"] .meal-slot__type-label { color: var(--meal-dinner); }
|
||||
|
||||
@@ -31,7 +31,7 @@ body {
|
||||
}
|
||||
|
||||
/* Touch-Targets werden über tokens.css (--target-sm/md/lg) und
|
||||
* komponentenspezifische Styles gehandhabt — siehe Redesign Phase E.
|
||||
* komponentenspezifische Styles gehandhabt - siehe Redesign Phase E.
|
||||
* Keine globale min-size-Regel hier, da sie mit dem bestehenden
|
||||
* Touch-Target-System kollidiert (::before-Expansion auf kleinen Elementen). */
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
.settings-page { --module-accent: var(--module-settings); }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Seiten-Layout — nutzt layout-center (max 720px)
|
||||
Seiten-Layout - nutzt layout-center (max 720px)
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-page {
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
.item-delete:hover { color: var(--color-danger); }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Swipe-Wrapper — Shopping-spezifische Styles
|
||||
* Swipe-Wrapper - Shopping-spezifische Styles
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
/* Kein Margin mehr am shopping-item selbst (übernimmt swipe-row) */
|
||||
@@ -444,7 +444,7 @@
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* × Löschen-Button auf Mobile ausblenden — Swipe übernimmt */
|
||||
/* × Löschen-Button auf Mobile ausblenden - Swipe übernimmt */
|
||||
@media (max-width: 1023px) {
|
||||
.item-delete {
|
||||
display: none;
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Swipe-Wrapper — Task-spezifische Styles
|
||||
* Swipe-Wrapper - Task-spezifische Styles
|
||||
* Basis-Styles (.swipe-row, .swipe-reveal, .swipe-reveal--done)
|
||||
* liegen in layout.css
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
+34
-34
@@ -23,7 +23,7 @@
|
||||
|
||||
:root {
|
||||
/* --------------------------------------------------------
|
||||
* 1. Farben — Neutral-Skala
|
||||
* 1. Farben - Neutral-Skala
|
||||
* Leicht warmgetönt (kein reines Grau) für einladende Atmosphäre.
|
||||
* Benannt als --neutral-{stufe} für direkte Nutzung,
|
||||
* plus semantische Aliase (--color-bg, --color-surface etc.)
|
||||
@@ -56,7 +56,7 @@
|
||||
--color-text-disabled: var(--neutral-300);
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 2. Farben — Akzent (konfigurierbar)
|
||||
* 2. Farben - Akzent (konfigurierbar)
|
||||
* Wärmerer Blauton statt reinem Corporate-Blau.
|
||||
* -------------------------------------------------------- */
|
||||
--color-accent: #2563EB;
|
||||
@@ -68,7 +68,7 @@
|
||||
--color-btn-primary-hover: #1E429A;
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 3. Farben — Semantisch
|
||||
* 3. Farben - Semantisch
|
||||
* -------------------------------------------------------- */
|
||||
--color-success: #15803D;
|
||||
--color-success-hover: #166534;
|
||||
@@ -84,22 +84,22 @@
|
||||
--color-info-light: #DDF4FF;
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 4. Farben — Modul-Akzente
|
||||
* 4. Farben - Modul-Akzente
|
||||
* Jedes Modul hat eine eigene dezente Akzentfarbe.
|
||||
* Einsatz in Modul-Headern, Icons, aktiven States.
|
||||
* -------------------------------------------------------- */
|
||||
--module-dashboard: #2563EB; /* Blau — Übersicht, neutral */
|
||||
--module-tasks: #15803D; /* Grün — Erledigung, Fortschritt */
|
||||
--module-calendar: #8250DF; /* Violett — Termine, Zeit */
|
||||
--module-meals: #B45309; /* Orange — Essen, Wärme */
|
||||
--module-shopping: #D4511E; /* Rot-Orange — Einkaufen, Aktion */
|
||||
--module-notes: #BF8700; /* Gold — Notizen, Pinnwand */
|
||||
--module-contacts: #0969DA; /* Kräftiges Blau — Kontakte */
|
||||
--module-budget: #1A7F5A; /* Teal — Finanzen, Stabilität */
|
||||
--module-settings: #6E7781; /* Grau — Konfiguration */
|
||||
--module-dashboard: #2563EB; /* Blau - Übersicht, neutral */
|
||||
--module-tasks: #15803D; /* Grün - Erledigung, Fortschritt */
|
||||
--module-calendar: #8250DF; /* Violett - Termine, Zeit */
|
||||
--module-meals: #B45309; /* Orange - Essen, Wärme */
|
||||
--module-shopping: #D4511E; /* Rot-Orange - Einkaufen, Aktion */
|
||||
--module-notes: #BF8700; /* Gold - Notizen, Pinnwand */
|
||||
--module-contacts: #0969DA; /* Kräftiges Blau - Kontakte */
|
||||
--module-budget: #1A7F5A; /* Teal - Finanzen, Stabilität */
|
||||
--module-settings: #6E7781; /* Grau - Konfiguration */
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 5. Farben — Mahlzeit-Typen
|
||||
* 5. Farben - Mahlzeit-Typen
|
||||
* Zentrale Tokens statt Hardcoding in meals.css
|
||||
* -------------------------------------------------------- */
|
||||
--meal-breakfast: #B45309;
|
||||
@@ -112,7 +112,7 @@
|
||||
--meal-snack-light: #FFECE3;
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 6. Farben — Prioritäten
|
||||
* 6. Farben - Prioritäten
|
||||
* -------------------------------------------------------- */
|
||||
--color-priority-low: var(--neutral-500);
|
||||
--color-priority-medium: #B45309;
|
||||
@@ -159,15 +159,15 @@
|
||||
--font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
|
||||
|
||||
/* Size-Skala */
|
||||
--text-xs: 0.75rem; /* 12px — Minimum, Captions, Badges, Nav-Labels */
|
||||
--text-sm: 0.8125rem; /* 13px — Small/Secondary */
|
||||
--text-base: 0.875rem; /* 14px — Body (Desktop), kompakter */
|
||||
--text-md: 1rem; /* 16px — Body (Mobile), Inputs */
|
||||
--text-lg: 1.125rem; /* 18px — Section-Title */
|
||||
--text-xl: 1.25rem; /* 20px — Subtitle */
|
||||
--text-2xl: 1.5rem; /* 24px — Page-Title */
|
||||
--text-3xl: 1.875rem; /* 30px — Page-Title Desktop */
|
||||
--text-4xl: 2.25rem; /* 36px — Hero/Greeting */
|
||||
--text-xs: 0.75rem; /* 12px - Minimum, Captions, Badges, Nav-Labels */
|
||||
--text-sm: 0.8125rem; /* 13px - Small/Secondary */
|
||||
--text-base: 0.875rem; /* 14px - Body (Desktop), kompakter */
|
||||
--text-md: 1rem; /* 16px - Body (Mobile), Inputs */
|
||||
--text-lg: 1.125rem; /* 18px - Section-Title */
|
||||
--text-xl: 1.25rem; /* 20px - Subtitle */
|
||||
--text-2xl: 1.5rem; /* 24px - Page-Title */
|
||||
--text-3xl: 1.875rem; /* 30px - Page-Title Desktop */
|
||||
--text-4xl: 2.25rem; /* 36px - Hero/Greeting */
|
||||
|
||||
/* Line-Heights */
|
||||
--line-height-tight: 1.25;
|
||||
@@ -182,7 +182,7 @@
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 11. Abstände — 4px-Raster
|
||||
* 11. Abstände - 4px-Raster
|
||||
* -------------------------------------------------------- */
|
||||
--space-0: 0px;
|
||||
--space-px: 1px;
|
||||
@@ -216,7 +216,7 @@
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 13. Sidebar
|
||||
* Reduzierter Neumorphismus — subtilere Schatten.
|
||||
* Reduzierter Neumorphismus - subtilere Schatten.
|
||||
* -------------------------------------------------------- */
|
||||
--sidebar-bg: var(--neutral-100);
|
||||
--sidebar-shadow-light: rgba(255, 255, 255, 0.6);
|
||||
@@ -280,7 +280,7 @@
|
||||
--sidebar-shadow-light: rgba(255, 255, 255, 0.04);
|
||||
--sidebar-shadow-dark: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Akzent — Dark Mode */
|
||||
/* Akzent - Dark Mode */
|
||||
--color-accent: #60A5FA;
|
||||
--color-accent-hover: #3B82F6;
|
||||
--color-accent-active: #2563EB;
|
||||
@@ -289,7 +289,7 @@
|
||||
--color-btn-primary: #3B82F6;
|
||||
--color-btn-primary-hover: #2563EB;
|
||||
|
||||
/* Semantische Farben — Dark Mode */
|
||||
/* Semantische Farben - Dark Mode */
|
||||
--color-success: #4ADE80;
|
||||
--color-warning: #F59E0B;
|
||||
--color-danger: #FCA5A5;
|
||||
@@ -300,7 +300,7 @@
|
||||
--color-danger-light: #3D1C1A;
|
||||
--color-info-light: #1A2D40;
|
||||
|
||||
/* Modul-Akzente — Dark Mode */
|
||||
/* Modul-Akzente - Dark Mode */
|
||||
--module-dashboard: #60A5FA;
|
||||
--module-tasks: #4ADE80;
|
||||
--module-calendar: #A78BFA;
|
||||
@@ -311,7 +311,7 @@
|
||||
--module-budget: #34D399;
|
||||
--module-settings: #94A3B8;
|
||||
|
||||
/* Mahlzeit-Typ — Dark Mode */
|
||||
/* Mahlzeit-Typ - Dark Mode */
|
||||
--meal-breakfast: #F59E0B;
|
||||
--meal-dinner: #60A5FA;
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
--sidebar-shadow-light: rgba(255, 255, 255, 0.04);
|
||||
--sidebar-shadow-dark: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Akzent — Dark Mode */
|
||||
/* Akzent - Dark Mode */
|
||||
--color-accent: #60A5FA;
|
||||
--color-accent-hover: #3B82F6;
|
||||
--color-accent-active: #2563EB;
|
||||
@@ -371,7 +371,7 @@
|
||||
--color-btn-primary: #3B82F6;
|
||||
--color-btn-primary-hover: #2563EB;
|
||||
|
||||
/* Semantische Farben — Dark Mode */
|
||||
/* Semantische Farben - Dark Mode */
|
||||
--color-success: #4ADE80;
|
||||
--color-warning: #F59E0B;
|
||||
--color-danger: #FCA5A5;
|
||||
@@ -382,7 +382,7 @@
|
||||
--color-danger-light: #3D1C1A;
|
||||
--color-info-light: #1A2D40;
|
||||
|
||||
/* Modul-Akzente — Dark Mode */
|
||||
/* Modul-Akzente - Dark Mode */
|
||||
--module-dashboard: #60A5FA;
|
||||
--module-tasks: #4ADE80;
|
||||
--module-calendar: #A78BFA;
|
||||
@@ -393,7 +393,7 @@
|
||||
--module-budget: #34D399;
|
||||
--module-settings: #94A3B8;
|
||||
|
||||
/* Mahlzeit-Typ — Dark Mode */
|
||||
/* Mahlzeit-Typ - Dark Mode */
|
||||
--meal-breakfast: #F59E0B;
|
||||
--meal-dinner: #60A5FA;
|
||||
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@ self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// API: immer Netzwerk — niemals Nutzerdaten cachen
|
||||
// API: immer Netzwerk - niemals Nutzerdaten cachen
|
||||
if (url.pathname.startsWith('/api/')) return;
|
||||
|
||||
// Nur GET cachen
|
||||
@@ -123,7 +123,7 @@ self.addEventListener('fetch', (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bilder + Fonts: Cache-First, langer TTL — nur Same-Origin
|
||||
// Bilder + Fonts: Cache-First, langer TTL - nur Same-Origin
|
||||
// Cross-Origin-Assets (z.B. Wetter-Icons von openweathermap.org) nicht
|
||||
// abfangen: opaque Responses führen im PWA-Modus zu Darstellungsfehlern.
|
||||
if (isAsset(url.pathname) && url.origin === self.location.origin) {
|
||||
|
||||
+4
-4
@@ -10,9 +10,9 @@
|
||||
*
|
||||
* @param {NodeList|Element[]} elements
|
||||
* @param {Object} [opts]
|
||||
* @param {number} [opts.delay=30] — ms zwischen jedem Element
|
||||
* @param {number} [opts.duration=180] — ms pro Element
|
||||
* @param {number} [opts.max=5] — Maximale Anzahl gestaffelter Elemente
|
||||
* @param {number} [opts.delay=30] - ms zwischen jedem Element
|
||||
* @param {number} [opts.duration=180] - ms pro Element
|
||||
* @param {number} [opts.max=5] - Maximale Anzahl gestaffelter Elemente
|
||||
*/
|
||||
export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {}) {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
@@ -33,7 +33,7 @@ export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {})
|
||||
* Vibrationsmuster abspielen, wenn die API verfügbar ist und
|
||||
* keine reduzierte Bewegung gewünscht wird.
|
||||
*
|
||||
* @param {number|number[]} pattern — ms oder [an, aus, an, ...]-Array
|
||||
* @param {number|number[]} pattern - ms oder [an, aus, an, ...]-Array
|
||||
*/
|
||||
export function vibrate(pattern) {
|
||||
if (!navigator.vibrate) return;
|
||||
|
||||
@@ -53,7 +53,7 @@ function createAppleTouchSvg() {
|
||||
return createLogoSvg(180);
|
||||
}
|
||||
|
||||
/** Favicon (32x32): simplified — just gradient background with house */
|
||||
/** Favicon (32x32): simplified - just gradient background with house */
|
||||
function createFaviconSvg() {
|
||||
return createLogoSvg(32);
|
||||
}
|
||||
|
||||
+45
-45
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Demo Seed Script — Oikos
|
||||
* Demo Seed Script - Oikos
|
||||
* Fills the database with realistic English demo content for screenshots/mockups.
|
||||
* Usage: node scripts/seed-demo.js [--db /path/to/oikos.db]
|
||||
*
|
||||
@@ -96,18 +96,18 @@ const insertTask = db.prepare(`
|
||||
|
||||
[
|
||||
['Book dentist appointment', 'Annual check-up for the whole family', 'health', 'high', 'open', daysFromNow(3), alexId, alexId],
|
||||
['Pay electricity bill', 'Due end of month — online banking', 'finance', 'urgent', 'open', daysFromNow(2), alexId, alexId],
|
||||
['Pay electricity bill', 'Due end of month - online banking', 'finance', 'urgent', 'open', daysFromNow(2), alexId, alexId],
|
||||
['Renew car insurance', 'Compare quotes on check24.de first', 'finance', 'high', 'open', daysFromNow(10), alexId, alexId],
|
||||
['Fix leaking bathroom faucet', 'Replace washer, tools in basement', 'home', 'medium', 'open', daysFromNow(7), samId, alexId],
|
||||
['Order birthday cake', "Emma's 8th birthday — chocolate cake", 'family', 'high', 'open', daysFromNow(5), samId, samId ],
|
||||
['Order birthday cake', "Emma's 8th birthday - chocolate cake", 'family', 'high', 'open', daysFromNow(5), samId, samId ],
|
||||
['Clean out garage', 'Donate old stuff to charity', 'home', 'low', 'open', daysFromNow(14), alexId, alexId],
|
||||
['Sign school permission slip', 'Field trip to the science museum', 'school', 'urgent', 'open', daysFromNow(1), samId, samId ],
|
||||
['Renew library cards', 'All three cards expired last month', 'admin', 'low', 'open', daysFromNow(20), alexId, alexId],
|
||||
['Plan summer holiday', 'Italy or Croatia — check flights', 'family', 'medium', 'open', daysFromNow(30), alexId, alexId],
|
||||
['Plan summer holiday', 'Italy or Croatia - check flights', 'family', 'medium', 'open', daysFromNow(30), alexId, alexId],
|
||||
['Tax return 2025', 'Documents ready in the folder', 'finance', 'high', 'open', daysFromNow(18), alexId, alexId],
|
||||
['Grocery run', 'See shopping list for details', 'home', 'medium', 'done', daysFromNow(-1), samId, samId ],
|
||||
['Call insurance about claim', 'Reference: CLM-2025-0492', 'finance', 'high', 'done', daysFromNow(-3), alexId, alexId],
|
||||
['Oil change — VW Golf', 'Every 15 000 km / 12 months', 'home', 'medium', 'open', daysFromNow(6), alexId, alexId],
|
||||
['Oil change - VW Golf', 'Every 15 000 km / 12 months', 'home', 'medium', 'open', daysFromNow(6), alexId, alexId],
|
||||
['Buy birthday gift for Mum', 'Amazon wishlist or book voucher', 'family', 'medium', 'open', daysFromNow(8), samId, samId ],
|
||||
['Update home inventory', 'For insurance purposes', 'admin', 'low', 'open', daysFromNow(25), alexId, alexId],
|
||||
].forEach(row => insertTask.run(...row));
|
||||
@@ -122,20 +122,20 @@ const insertEvent = db.prepare(`
|
||||
|
||||
[
|
||||
["Emma's Birthday Party", 'Bouncy castle & cake at home', daysFromNow(5) + 'T14:00', daysFromNow(5) + 'T17:00', 0, 'Home', '#F59E0B', samId, samId ],
|
||||
['Dentist — Family', 'Dr. Müller, bring insurance cards', daysFromNow(3) + 'T10:00', daysFromNow(3) + 'T11:30', 0, 'Dental Practice Müller', '#EF4444', alexId, alexId],
|
||||
['Dentist - Family', 'Dr. Müller, bring insurance cards', daysFromNow(3) + 'T10:00', daysFromNow(3) + 'T11:30', 0, 'Dental Practice Müller', '#EF4444', alexId, alexId],
|
||||
['Parent-Teacher Evening', 'Room 12, bring report card', daysFromNow(9) + 'T18:30', daysFromNow(9) + 'T20:00', 0, 'Westpark Primary School', '#8B5CF6', samId, samId ],
|
||||
['Science Museum Field Trip', 'Emma — permission slip signed', daysFromNow(1) + 'T08:30', daysFromNow(1) + 'T15:00', 0, 'Natural History Museum', '#06B6D4', samId, samId ],
|
||||
['Family BBQ — Mum & Dad', 'Bring potato salad', daysFromNow(12) + 'T13:00', daysFromNow(12) + 'T19:00', 0, "Grandma's Garden", '#F59E0B', alexId, alexId],
|
||||
['Science Museum Field Trip', 'Emma - permission slip signed', daysFromNow(1) + 'T08:30', daysFromNow(1) + 'T15:00', 0, 'Natural History Museum', '#06B6D4', samId, samId ],
|
||||
['Family BBQ - Mum & Dad', 'Bring potato salad', daysFromNow(12) + 'T13:00', daysFromNow(12) + 'T19:00', 0, "Grandma's Garden", '#F59E0B', alexId, alexId],
|
||||
['Car Service Appointment', 'VW Golf, oil change + tyre check', daysFromNow(6) + 'T09:00', daysFromNow(6) + 'T10:30', 0, 'AutoHaus König', '#6B7280', alexId, alexId],
|
||||
['Yoga Class', 'Weekly — bring mat', daysFromNow(2) + 'T19:00', daysFromNow(2) + 'T20:00', 0, 'FitLife Studio', '#10B981', samId, samId ],
|
||||
['Yoga Class', 'Weekly — bring mat', daysFromNow(9) + 'T19:00', daysFromNow(9) + 'T20:00', 0, 'FitLife Studio', '#10B981', samId, samId ],
|
||||
['Yoga Class', 'Weekly - bring mat', daysFromNow(2) + 'T19:00', daysFromNow(2) + 'T20:00', 0, 'FitLife Studio', '#10B981', samId, samId ],
|
||||
['Yoga Class', 'Weekly - bring mat', daysFromNow(9) + 'T19:00', daysFromNow(9) + 'T20:00', 0, 'FitLife Studio', '#10B981', samId, samId ],
|
||||
['Mum\'s Birthday', '', daysFromNow(8) + 'T00:00', daysFromNow(8) + 'T00:00', 1, '', '#EC4899', alexId, alexId],
|
||||
['Company All-Hands', 'Q2 results + roadmap presentation', daysFromNow(4) + 'T10:00', daysFromNow(4) + 'T12:00', 0, 'Office — Conference Room B','#2563EB', alexId, alexId],
|
||||
['Football Training — Leo', 'Boots & water bottle', daysFromNow(2) + 'T17:00', daysFromNow(2) + 'T18:30', 0, 'Sports Ground West', '#F97316', samId, samId ],
|
||||
['Football Training — Leo', 'Boots & water bottle', daysFromNow(7) + 'T17:00', daysFromNow(7) + 'T18:30', 0, 'Sports Ground West', '#F97316', samId, samId ],
|
||||
['Holiday Planning Evening', 'Italy vs Croatia — laptops out', daysFromNow(3) + 'T21:00', daysFromNow(3) + 'T22:00', 0, 'Home', '#14B8A6', alexId, samId ],
|
||||
['GP Appointment — Alex', 'Annual health check', daysFromNow(15) + 'T11:00', daysFromNow(15) + 'T11:30', 0, 'Dr. Weber — City Practice', '#EF4444', alexId, alexId],
|
||||
['Weekend City Break', 'Hotel booked — just pack bags!', daysFromNow(20) + 'T00:00', daysFromNow(22) + 'T00:00', 1, 'Amsterdam', '#0EA5E9', alexId, alexId],
|
||||
['Company All-Hands', 'Q2 results + roadmap presentation', daysFromNow(4) + 'T10:00', daysFromNow(4) + 'T12:00', 0, 'Office - Conference Room B','#2563EB', alexId, alexId],
|
||||
['Football Training - Leo', 'Boots & water bottle', daysFromNow(2) + 'T17:00', daysFromNow(2) + 'T18:30', 0, 'Sports Ground West', '#F97316', samId, samId ],
|
||||
['Football Training - Leo', 'Boots & water bottle', daysFromNow(7) + 'T17:00', daysFromNow(7) + 'T18:30', 0, 'Sports Ground West', '#F97316', samId, samId ],
|
||||
['Holiday Planning Evening', 'Italy vs Croatia - laptops out', daysFromNow(3) + 'T21:00', daysFromNow(3) + 'T22:00', 0, 'Home', '#14B8A6', alexId, samId ],
|
||||
['GP Appointment - Alex', 'Annual health check', daysFromNow(15) + 'T11:00', daysFromNow(15) + 'T11:30', 0, 'Dr. Weber - City Practice', '#EF4444', alexId, alexId],
|
||||
['Weekend City Break', 'Hotel booked - just pack bags!', daysFromNow(20) + 'T00:00', daysFromNow(22) + 'T00:00', 1, 'Amsterdam', '#0EA5E9', alexId, alexId],
|
||||
].forEach(row => insertEvent.run(...row));
|
||||
|
||||
// ── Meals ────────────────────────────────────────────────────────────────────
|
||||
@@ -191,18 +191,18 @@ const insertContact = db.prepare(`
|
||||
`);
|
||||
|
||||
[
|
||||
['Dr. Anna Weber', 'medical', '+49 231 445 2210', 'praxis@dr-weber.de', 'Bürgerstraße 12, Dortmund', 'GP — appointments Mon–Thu'],
|
||||
['Dr. Anna Weber', 'medical', '+49 231 445 2210', 'praxis@dr-weber.de', 'Bürgerstraße 12, Dortmund', 'GP - appointments Mon–Thu'],
|
||||
['Dr. Thomas Müller', 'medical', '+49 231 887 0034', 'info@zahnarzt-mueller.de', 'Hansastraße 55, Dortmund', 'Family dentist'],
|
||||
['Grandma & Grandpa Johnson', 'family','+49 2304 78 221', 'oma.johnson@gmail.com', 'Ahornweg 4, Castrop-Rauxel', "Emma & Leo's grandparents"],
|
||||
['Westpark Primary School','school', '+49 231 556 8810', 'office@westpark-grundschule.de', 'Westparkstraße 20, Dortmund', "Emma's school — Mrs Bauer is class teacher"],
|
||||
['AutoHaus König', 'services', '+49 231 997 1100', 'service@autohaus-koenig.de','Industriestraße 88, Dortmund', 'VW service partner — Ref: Golf TDI 2021'],
|
||||
['FitLife Studio', 'services', '+49 231 340 5060', 'hello@fitlife-dortmund.de', 'Rheinlanddamm 14, Dortmund', "Sam's yoga — Tuesdays 19:00"],
|
||||
['Uncle Mike Johnson', 'family', '+49 172 3340 551', 'mike.j@outlook.com', '', 'Alex\'s brother — lives in Hamburg'],
|
||||
['Westpark Primary School','school', '+49 231 556 8810', 'office@westpark-grundschule.de', 'Westparkstraße 20, Dortmund', "Emma's school - Mrs Bauer is class teacher"],
|
||||
['AutoHaus König', 'services', '+49 231 997 1100', 'service@autohaus-koenig.de','Industriestraße 88, Dortmund', 'VW service partner - Ref: Golf TDI 2021'],
|
||||
['FitLife Studio', 'services', '+49 231 340 5060', 'hello@fitlife-dortmund.de', 'Rheinlanddamm 14, Dortmund', "Sam's yoga - Tuesdays 19:00"],
|
||||
['Uncle Mike Johnson', 'family', '+49 172 3340 551', 'mike.j@outlook.com', '', 'Alex\'s brother - lives in Hamburg'],
|
||||
['Aunt Claire Becker', 'family', '+49 151 2234 8876','claire.becker@web.de', 'Fichtenweg 7, Bochum', 'Sam\'s sister'],
|
||||
['Leo\'s Football Coach', 'school', '+49 176 5512 4490','trainer@svwest-dortmund.de','Sportplatz West, Dortmund', 'Training Tues & Sat 17:00'],
|
||||
['City Library', 'services', '+49 231 502 6600', 'stadtbibliothek@dortmund.de','Königswall 18, Dortmund', 'Family cards — renew every 2 years'],
|
||||
['Landlord — Mr Groß', 'services', '+49 231 112 7743', 'vermieter.gross@gmail.com', '', 'Emergency maintenance: same number'],
|
||||
['Emma\'s Best Friend Lena','family', '+49 231 774 3309', '', '', "Lena Braun — mum is Katrin +49 231 774 3308"],
|
||||
['City Library', 'services', '+49 231 502 6600', 'stadtbibliothek@dortmund.de','Königswall 18, Dortmund', 'Family cards - renew every 2 years'],
|
||||
['Landlord - Mr Groß', 'services', '+49 231 112 7743', 'vermieter.gross@gmail.com', '', 'Emergency maintenance: same number'],
|
||||
['Emma\'s Best Friend Lena','family', '+49 231 774 3309', '', '', "Lena Braun - mum is Katrin +49 231 774 3308"],
|
||||
].forEach(row => insertContact.run(...row));
|
||||
|
||||
// ── Budget ───────────────────────────────────────────────────────────────────
|
||||
@@ -215,41 +215,41 @@ const insertBudget = db.prepare(`
|
||||
|
||||
[
|
||||
// Income
|
||||
['Alex — Monthly Salary', 3850.00, 'income', thisMonthDate(1), 1, alexId],
|
||||
['Sam — Part-time Work', 1200.00, 'income', thisMonthDate(1), 1, alexId],
|
||||
['Alex - Monthly Salary', 3850.00, 'income', thisMonthDate(1), 1, alexId],
|
||||
['Sam - Part-time Work', 1200.00, 'income', thisMonthDate(1), 1, alexId],
|
||||
['Child Benefit (Kindergeld)', 250.00, 'income', thisMonthDate(5), 1, alexId],
|
||||
|
||||
// Fixed expenses
|
||||
['Rent', -1450.00, 'housing', thisMonthDate(1), 1, alexId],
|
||||
['Car Insurance — VW Golf', -89.50, 'transport', thisMonthDate(1), 1, alexId],
|
||||
['Car Insurance - VW Golf', -89.50, 'transport', thisMonthDate(1), 1, alexId],
|
||||
['Health Insurance', -310.00, 'insurance', thisMonthDate(1), 1, alexId],
|
||||
['Internet & Phone Bundle', -49.99, 'utilities', thisMonthDate(5), 1, alexId],
|
||||
['Electricity Bill', -78.00, 'utilities', thisMonthDate(15), 1, alexId],
|
||||
['Netflix', -17.99, 'leisure', thisMonthDate(10), 1, alexId],
|
||||
['Spotify Family', -16.99, 'leisure', thisMonthDate(10), 1, alexId],
|
||||
['Gym — FitLife Monthly', -39.00, 'health', thisMonthDate(1), 1, alexId],
|
||||
['Gym - FitLife Monthly', -39.00, 'health', thisMonthDate(1), 1, alexId],
|
||||
|
||||
// Variable this month
|
||||
['Weekly Groceries — Wk 1', -142.30, 'food', thisMonthDate(4), 0, samId ],
|
||||
['Weekly Groceries — Wk 2', -118.75, 'food', thisMonthDate(11), 0, samId ],
|
||||
['Weekly Groceries — Wk 3', -134.20, 'food', thisMonthDate(18), 0, samId ],
|
||||
['Weekly Groceries - Wk 1', -142.30, 'food', thisMonthDate(4), 0, samId ],
|
||||
['Weekly Groceries - Wk 2', -118.75, 'food', thisMonthDate(11), 0, samId ],
|
||||
['Weekly Groceries - Wk 3', -134.20, 'food', thisMonthDate(18), 0, samId ],
|
||||
['School Trip Payment', -25.00, 'school', thisMonthDate(3), 0, samId ],
|
||||
['Birthday Gift — Mum', -60.00, 'family', thisMonthDate(7), 0, alexId],
|
||||
['Restaurant — Date Night', -87.50, 'leisure', thisMonthDate(9), 0, alexId],
|
||||
['Fuel — VW Golf', -68.00, 'transport', thisMonthDate(6), 0, alexId],
|
||||
['Birthday Gift - Mum', -60.00, 'family', thisMonthDate(7), 0, alexId],
|
||||
['Restaurant - Date Night', -87.50, 'leisure', thisMonthDate(9), 0, alexId],
|
||||
['Fuel - VW Golf', -68.00, 'transport', thisMonthDate(6), 0, alexId],
|
||||
['Pharmacy', -22.40, 'health', thisMonthDate(8), 0, samId ],
|
||||
['Leo\'s Football Boots', -54.99, 'school', thisMonthDate(12), 0, samId ],
|
||||
['Home Improvement — Tools', -43.00, 'home', thisMonthDate(14), 0, alexId],
|
||||
['Clothing — Emma', -38.50, 'clothing', thisMonthDate(16), 0, samId ],
|
||||
['Home Improvement - Tools', -43.00, 'home', thisMonthDate(14), 0, alexId],
|
||||
['Clothing - Emma', -38.50, 'clothing', thisMonthDate(16), 0, samId ],
|
||||
['Weekend Trip Deposit', -200.00, 'leisure', thisMonthDate(19), 0, alexId],
|
||||
|
||||
// Last month (for trend comparison)
|
||||
['Alex — Monthly Salary', 3850.00, 'income', lastMonthDate(1), 0, alexId],
|
||||
['Sam — Part-time Work', 1200.00, 'income', lastMonthDate(1), 0, alexId],
|
||||
['Alex - Monthly Salary', 3850.00, 'income', lastMonthDate(1), 0, alexId],
|
||||
['Sam - Part-time Work', 1200.00, 'income', lastMonthDate(1), 0, alexId],
|
||||
['Rent', -1450.00, 'housing', lastMonthDate(1), 0, alexId],
|
||||
['Weekly Groceries', -489.00, 'food', lastMonthDate(10), 0, samId ],
|
||||
['Electricity Bill', -82.00, 'utilities', lastMonthDate(15), 0, alexId],
|
||||
['Fuel — VW Golf', -71.00, 'transport', lastMonthDate(8), 0, alexId],
|
||||
['Fuel - VW Golf', -71.00, 'transport', lastMonthDate(8), 0, alexId],
|
||||
].forEach(row => insertBudget.run(...row));
|
||||
|
||||
// ── Notes ────────────────────────────────────────────────────────────────────
|
||||
@@ -262,7 +262,7 @@ const insertNote = db.prepare(`
|
||||
|
||||
[
|
||||
['Holiday Checklist 🌍',
|
||||
'Passports (exp. 2028)\nTravel insurance — check!\nEuro cash — €300\nBook airport parking\nAsk Mike to water plants\nPack sunscreen SPF 50',
|
||||
'Passports (exp. 2028)\nTravel insurance - check!\nEuro cash - €300\nBook airport parking\nAsk Mike to water plants\nPack sunscreen SPF 50',
|
||||
'#0EA5E9', 1, alexId],
|
||||
|
||||
['WiFi & Smart Home',
|
||||
@@ -270,23 +270,23 @@ const insertNote = db.prepare(`
|
||||
'#F59E0B', 1, alexId],
|
||||
|
||||
["Emma's School Info",
|
||||
"Class: 3b — Mrs Bauer\nSchool starts: 08:10\nCollection: 13:30 (Tue/Thu 15:00)\nAllergy: mild lactose intolerance\nBest friends: Lena, Sophie, Tim",
|
||||
"Class: 3b - Mrs Bauer\nSchool starts: 08:10\nCollection: 13:30 (Tue/Thu 15:00)\nAllergy: mild lactose intolerance\nBest friends: Lena, Sophie, Tim",
|
||||
'#EC4899', 1, samId],
|
||||
|
||||
['Leo\'s Activities',
|
||||
'Football: Tues & Sat 17:00 — SV West\nSwimming: Fri 16:00 — Westbad\nNeeds: boots size 35, goggles\nCoach: Herr Krüger +49 176 5512 4490',
|
||||
'Football: Tues & Sat 17:00 - SV West\nSwimming: Fri 16:00 - Westbad\nNeeds: boots size 35, goggles\nCoach: Herr Krüger +49 176 5512 4490',
|
||||
'#F97316', 1, samId],
|
||||
|
||||
['Emergency Numbers',
|
||||
'Police: 110\nFire / Ambulance: 112\nPoison Control: 0800 192 11 10\nLocal GP out-of-hours: 116 117\nNearest A&E: Klinikum Dortmund',
|
||||
'#EF4444', 1, alexId],
|
||||
|
||||
['Car — Important Dates',
|
||||
['Car - Important Dates',
|
||||
'Next service: June 2025 (60,000 km)\nTÜV due: September 2025\nWinter tyres: stored at AutoHaus König\nInsurance renewal: October 2025',
|
||||
'#6B7280', 0, alexId],
|
||||
|
||||
['Book Recommendations',
|
||||
'Currently reading: "Atomic Habits" — James Clear\nWishlist:\n• The Thursday Murder Club\n• Lessons in Chemistry\n• Tomorrow, and Tomorrow, and Tomorrow',
|
||||
'Currently reading: "Atomic Habits" - James Clear\nWishlist:\n• The Thursday Murder Club\n• Lessons in Chemistry\n• Tomorrow, and Tomorrow, and Tomorrow',
|
||||
'#8B5CF6', 0, samId],
|
||||
|
||||
['Garden To-Do',
|
||||
@@ -325,7 +325,7 @@ const insertItem = db.prepare(`
|
||||
['Bananas', '6', 'fruit', 0],
|
||||
['Blueberries', '125 g', 'fruit', 0],
|
||||
['Lemons', '4', 'fruit', 0],
|
||||
['Pasta — spaghetti', '500 g', 'pantry', 0],
|
||||
['Pasta - spaghetti', '500 g', 'pantry', 0],
|
||||
['Basmati rice', '1 kg', 'pantry', 0],
|
||||
['Olive oil', '500 ml', 'pantry', 0],
|
||||
['Tomato passata', '2 × 500 g','pantry', 0],
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ const router = express.Router();
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Session-Store (better-sqlite3, gleiche DB-Instanz wie App)
|
||||
// Eigene Implementierung — kein connect-sqlite3 (nutzt sqlite3-Bindings,
|
||||
// Eigene Implementierung - kein connect-sqlite3 (nutzt sqlite3-Bindings,
|
||||
// die separat kompiliert werden müssten und die Fehlerquelle waren).
|
||||
// --------------------------------------------------------
|
||||
class BetterSQLiteStore extends session.Store {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Modul: DB-Schema-Export für Tests
|
||||
* Zweck: SQL-Strings aus MIGRATIONS für node:sqlite-Tests exportieren.
|
||||
* Nur für Testzwecke — db.js nutzt die MIGRATIONS direkt intern.
|
||||
* Nur für Testzwecke - db.js nutzt die MIGRATIONS direkt intern.
|
||||
* Abhängigkeiten: keine
|
||||
*/
|
||||
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ function init() {
|
||||
|
||||
/**
|
||||
* Alle Migrationen in aufsteigender Reihenfolge.
|
||||
* Neue Migrations am Ende anhängen — niemals bestehende ändern.
|
||||
* Neue Migrations am Ende anhängen - niemals bestehende ändern.
|
||||
*/
|
||||
const MIGRATIONS = [
|
||||
{
|
||||
@@ -352,7 +352,7 @@ function currentVersion() {
|
||||
* @returns {import('better-sqlite3').Database}
|
||||
*/
|
||||
function get() {
|
||||
if (!db) throw new Error('[DB] Nicht initialisiert — init() zuerst aufrufen.');
|
||||
if (!db) throw new Error('[DB] Nicht initialisiert - init() zuerst aufrufen.');
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -94,7 +94,7 @@ app.use('/api/', (req, res, next) => {
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Statische Dateien (Frontend) — differenzierte Caching-Strategie
|
||||
// Statische Dateien (Frontend) - differenzierte Caching-Strategie
|
||||
//
|
||||
// HTML + JS + CSS: no-cache (Browser revalidiert via ETag/304, kein stale Content
|
||||
// nach Deployment). Bei unverändertem File → 304 Not Modified ohne Übertragung.
|
||||
@@ -113,7 +113,7 @@ app.use(express.static(path.join(__dirname, '..', 'public'), {
|
||||
} else if (['.png', '.jpg', '.jpeg', '.ico', '.svg', '.webp', '.woff2', '.woff'].includes(ext)) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable'); // 30 Tage
|
||||
} else {
|
||||
// HTML, JS, CSS, JSON, manifest, sw — immer revalidieren
|
||||
// HTML, JS, CSS, JSON, manifest, sw - immer revalidieren
|
||||
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
||||
}
|
||||
// manifest.json: korrekter MIME-Type für PWA-Erkennung durch Chrome/Android
|
||||
|
||||
@@ -17,7 +17,7 @@ const { str, oneOf, date, num, rrule, collectErrors, MAX_TITLE, MONTH_RE } = req
|
||||
|
||||
/**
|
||||
* Erstellt fehlende Instanzen wiederkehrender Budget-Einträge für den angefragten Monat.
|
||||
* Läuft idempotent — bereits vorhandene oder explizit übersprungene Instanzen werden ignoriert.
|
||||
* Läuft idempotent - bereits vorhandene oder explizit übersprungene Instanzen werden ignoriert.
|
||||
* @param {import('better-sqlite3').Database} database
|
||||
* @param {string} month YYYY-MM
|
||||
*/
|
||||
|
||||
@@ -228,7 +228,7 @@ router.get('/google/callback', async (req, res) => {
|
||||
|
||||
await googleCalendar.handleCallback(code);
|
||||
|
||||
// Initialen Sync im Hintergrund starten (kein await — Redirect soll sofort erfolgen)
|
||||
// Initialen Sync im Hintergrund starten (kein await - Redirect soll sofort erfolgen)
|
||||
googleCalendar.sync().catch((e) => console.error('[Google] Initialer Sync fehlgeschlagen:', e.message));
|
||||
|
||||
res.redirect('/settings?sync_ok=google');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Modul: Dashboard
|
||||
* Zweck: Aggregierter Endpoint — liefert Daten aller Dashboard-Widgets in einem Request
|
||||
* Zweck: Aggregierter Endpoint - liefert Daten aller Dashboard-Widgets in einem Request
|
||||
* Abhängigkeiten: express, server/db.js
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ const db = require('../db');
|
||||
/**
|
||||
* GET /api/v1/dashboard
|
||||
* Liefert aggregierte Daten für alle Dashboard-Widgets.
|
||||
* Jedes Widget-Objekt hat ein eigenes `error`-Feld falls die Abfrage fehlschlägt —
|
||||
* Jedes Widget-Objekt hat ein eigenes `error`-Feld falls die Abfrage fehlschlägt -
|
||||
* so bricht ein fehlerhaftes Widget nicht das gesamte Dashboard.
|
||||
*
|
||||
* Response: {
|
||||
|
||||
@@ -40,7 +40,7 @@ function weekEnd(dateStr) {
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Routen — Mahlzeiten-Vorschläge (vor dynamischen Routen!)
|
||||
// Routen - Mahlzeiten-Vorschläge (vor dynamischen Routen!)
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ router.get('/suggestions', (req, res) => {
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Routen — Wochenübersicht
|
||||
// Routen - Wochenübersicht
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -137,7 +137,7 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// CRUD — Mahlzeiten
|
||||
// CRUD - Mahlzeiten
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -266,7 +266,7 @@ router.delete('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// CRUD — Zutaten
|
||||
// CRUD - Zutaten
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,7 +94,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// --------------------------------------------------------
|
||||
// GET /api/v1/weather/icon/:code
|
||||
// Proxy für OpenWeatherMap-Icons — vermeidet externe Bild-Requests
|
||||
// Proxy für OpenWeatherMap-Icons - vermeidet externe Bild-Requests
|
||||
// im PWA-Standalone-Modus (CORS/CSP-Probleme auf Android Chrome).
|
||||
// Erlaubte Codes: 2–4 alphanumerische Zeichen (z.B. "01d", "10n").
|
||||
// Response: PNG-Bild mit 24h-Cache
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* Modul: Apple Calendar Sync (CalDAV)
|
||||
* Zweck: Bidirektionaler Sync mit iCloud Calendar via CalDAV-Protokoll
|
||||
* Abhängigkeiten: tsdav (ESM — dynamisch importiert), server/db.js
|
||||
* Abhängigkeiten: tsdav (ESM - dynamisch importiert), server/db.js
|
||||
*
|
||||
* Konfiguration (.env):
|
||||
* APPLE_CALDAV_URL — z.B. https://caldav.icloud.com
|
||||
* APPLE_USERNAME — Apple-ID E-Mail
|
||||
* APPLE_APP_SPECIFIC_PASSWORD — App-spezifisches Passwort aus appleid.apple.com
|
||||
* APPLE_CALDAV_URL - z.B. https://caldav.icloud.com
|
||||
* APPLE_USERNAME - Apple-ID E-Mail
|
||||
* APPLE_APP_SPECIFIC_PASSWORD - App-spezifisches Passwort aus appleid.apple.com
|
||||
*
|
||||
* sync_config-Schlüssel:
|
||||
* apple_last_sync — ISO-8601-Timestamp des letzten Syncs
|
||||
* apple_last_sync - ISO-8601-Timestamp des letzten Syncs
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
@@ -134,7 +134,7 @@ function parseICS(ics) {
|
||||
const location = get('LOCATION') || null;
|
||||
const rrule = get('RRULE') ? `RRULE:${get('RRULE')}` : null;
|
||||
|
||||
// DTSTART — mit optionalem TZID oder VALUE=DATE
|
||||
// DTSTART - mit optionalem TZID oder VALUE=DATE
|
||||
const dtStartRaw = (() => {
|
||||
const m = /^DTSTART(?:;[^:]*)?:(.*)$/im.exec(block);
|
||||
return m ? m[1].trim() : null;
|
||||
@@ -148,7 +148,7 @@ function parseICS(ics) {
|
||||
const dtstart = dtStartRaw ? formatICSDate(dtStartRaw, allDay) : null;
|
||||
let dtend = dtEndRaw ? formatICSDate(dtEndRaw, allDay) : null;
|
||||
|
||||
// RFC 5545: DTEND for VALUE=DATE is exclusive — subtract one day
|
||||
// RFC 5545: DTEND for VALUE=DATE is exclusive - subtract one day
|
||||
if (allDay && dtend) {
|
||||
const d = new Date(dtend + 'T00:00:00');
|
||||
d.setDate(d.getDate() - 1);
|
||||
@@ -215,7 +215,7 @@ function applyDuration(dtstart, dur, allDay) {
|
||||
base.setHours(base.getHours() + hours, base.getMinutes() + mins, base.getSeconds() + secs);
|
||||
|
||||
if (allDay) {
|
||||
// Duration end is exclusive for DATE values — subtract one day for inclusive storage
|
||||
// Duration end is exclusive for DATE values - subtract one day for inclusive storage
|
||||
base.setDate(base.getDate() - 1);
|
||||
return `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, '0')}-${String(base.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
@@ -247,7 +247,7 @@ function buildICS(event) {
|
||||
|
||||
if (event.all_day) {
|
||||
const startDate = event.start_datetime.slice(0, 10).replace(/-/g, '');
|
||||
// RFC 5545: DTEND for VALUE=DATE is exclusive — add one day
|
||||
// RFC 5545: DTEND for VALUE=DATE is exclusive - add one day
|
||||
const endSrc = (event.end_datetime || event.start_datetime).slice(0, 10);
|
||||
const endD = new Date(endSrc + 'T00:00:00');
|
||||
endD.setDate(endD.getDate() + 1);
|
||||
@@ -288,7 +288,7 @@ async function sync() {
|
||||
throw new Error('[Apple] Keine Credentials konfiguriert (weder in DB noch in .env).');
|
||||
}
|
||||
|
||||
// tsdav ist ESM-only — dynamischer Import aus CommonJS
|
||||
// tsdav ist ESM-only - dynamischer Import aus CommonJS
|
||||
const { createDAVClient } = await import('tsdav');
|
||||
|
||||
const client = await createDAVClient({
|
||||
@@ -307,7 +307,7 @@ async function sync() {
|
||||
// created_by: ersten existierenden User verwenden (nicht hardcoded ID 1)
|
||||
const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
||||
if (!owner) {
|
||||
console.warn('[Apple] Kein User in der Datenbank — Sync übersprungen.');
|
||||
console.warn('[Apple] Kein User in der Datenbank - Sync übersprungen.');
|
||||
return;
|
||||
}
|
||||
const createdBy = owner.id;
|
||||
@@ -397,7 +397,7 @@ async function sync() {
|
||||
}
|
||||
|
||||
cfgSet('apple_last_sync', new Date().toISOString());
|
||||
console.log(`[Apple] Sync abgeschlossen — ${totalObjects} Objekte aus ${syncCalendars.length} Kalendern inbound, ${localEvents.length} lokal → iCloud.`);
|
||||
console.log(`[Apple] Sync abgeschlossen - ${totalObjects} Objekte aus ${syncCalendars.length} Kalendern inbound, ${localEvents.length} lokal → iCloud.`);
|
||||
}
|
||||
|
||||
module.exports = { sync, getStatus, saveCredentials, clearCredentials, testConnection };
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* Abhängigkeiten: googleapis, server/db.js
|
||||
*
|
||||
* sync_config-Schlüssel:
|
||||
* google_access_token — OAuth Access Token
|
||||
* google_refresh_token — OAuth Refresh Token (langlebig)
|
||||
* google_token_expiry — ISO-8601-Timestamp bis wann Access Token gültig ist
|
||||
* google_sync_token — Inkrementeller Sync-Token von Google (events.list)
|
||||
* google_last_sync — ISO-8601-Timestamp des letzten erfolgreichen Syncs
|
||||
* google_access_token - OAuth Access Token
|
||||
* google_refresh_token - OAuth Refresh Token (langlebig)
|
||||
* google_token_expiry - ISO-8601-Timestamp bis wann Access Token gültig ist
|
||||
* google_sync_token - Inkrementeller Sync-Token von Google (events.list)
|
||||
* google_last_sync - ISO-8601-Timestamp des letzten erfolgreichen Syncs
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
@@ -65,7 +65,7 @@ function loadAuthorizedClient() {
|
||||
const refreshToken = cfgGet('google_refresh_token');
|
||||
|
||||
if (!accessToken || !refreshToken) {
|
||||
throw new Error('[Google] Nicht konfiguriert — zuerst OAuth durchführen.');
|
||||
throw new Error('[Google] Nicht konfiguriert - zuerst OAuth durchführen.');
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
@@ -103,7 +103,7 @@ function getAuthUrl() {
|
||||
|
||||
/**
|
||||
* OAuth-Callback: tauscht Code gegen Tokens, speichert in sync_config.
|
||||
* @param {string} code — Code aus dem OAuth-Callback-Query-Parameter
|
||||
* @param {string} code - Code aus dem OAuth-Callback-Query-Parameter
|
||||
*/
|
||||
async function handleCallback(code) {
|
||||
const client = createClient();
|
||||
@@ -117,7 +117,7 @@ async function handleCallback(code) {
|
||||
cfgSet('google_refresh_token', tokens.refresh_token);
|
||||
if (tokens.expiry_date) cfgSet('google_token_expiry', String(tokens.expiry_date));
|
||||
|
||||
console.log('[Google] OAuth erfolgreich — Tokens gespeichert.');
|
||||
console.log('[Google] OAuth erfolgreich - Tokens gespeichert.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +179,7 @@ async function sync() {
|
||||
} catch (err) {
|
||||
if (err.code === 410) {
|
||||
// syncToken abgelaufen → vollständiger Resync
|
||||
console.warn('[Google] syncToken ungültig — vollständiger Resync.');
|
||||
console.warn('[Google] syncToken ungültig - vollständiger Resync.');
|
||||
cfgDel('google_sync_token');
|
||||
syncToken = null;
|
||||
continue;
|
||||
@@ -220,7 +220,7 @@ async function sync() {
|
||||
}
|
||||
|
||||
cfgSet('google_last_sync', new Date().toISOString());
|
||||
console.log(`[Google] Sync abgeschlossen — ${localEvents.length} lokal → Google, Inbound via syncToken.`);
|
||||
console.log(`[Google] Sync abgeschlossen - ${localEvents.length} lokal → Google, Inbound via syncToken.`);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Modul: Setup-Script
|
||||
* Zweck: Erstmalige Einrichtung — ersten Admin-User anlegen.
|
||||
* Zweck: Erstmalige Einrichtung - ersten Admin-User anlegen.
|
||||
* Wird einmalig nach dem ersten Start ausgeführt: `node setup.js`
|
||||
* Abhängigkeiten: server/db.js, bcrypt, dotenv
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* test-browser-loader.mjs — Node.js Custom Loader für Tests
|
||||
* test-browser-loader.mjs - Node.js Custom Loader für Tests
|
||||
* Zweck: Browser-absolute Pfade (/foo.js) auf Stubs umleiten, damit
|
||||
* Frontend-Module im Node-Test-Kontext importierbar sind.
|
||||
* Verwendung: node --loader ./test-browser-loader.mjs test-xxx.js
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ test('Kommende Termine (upcoming)', () => {
|
||||
// Sortierung
|
||||
// --------------------------------------------------------
|
||||
test('Sortierung: ganztägig nach uhrzeit-basierten Terminen', () => {
|
||||
// Gleicher Tag: Ganztägig sollte nach hinten oder flexibel — hier: all_day DESC in der Abfrage
|
||||
// Gleicher Tag: Ganztägig sollte nach hinten oder flexibel - hier: all_day DESC in der Abfrage
|
||||
const events = db.prepare(`
|
||||
SELECT * FROM calendar_events
|
||||
WHERE DATE(start_datetime) = '2026-03-24'
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Modul: Datenbank-Test
|
||||
* Zweck: Schema-Migration mit node:sqlite (built-in) validieren.
|
||||
* Kein Kompilieren nötig — läuft direkt mit Node 22+.
|
||||
* Kein Kompilieren nötig - läuft direkt mit Node 22+.
|
||||
* Testet SQL-Korrektheit, FK-Reihenfolge, Triggers, Indizes.
|
||||
*
|
||||
* Ausführen: node test-db.js
|
||||
@@ -13,7 +13,7 @@ const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Migrations-SQL direkt aus db.js extrahieren
|
||||
// (Nur für Tests — in Produktion läuft db.js mit better-sqlite3)
|
||||
// (Nur für Tests - in Produktion läuft db.js mit better-sqlite3)
|
||||
// --------------------------------------------------------
|
||||
const { MIGRATIONS_SQL } = require('./server/db-schema-test');
|
||||
|
||||
|
||||
+1
-1
@@ -251,7 +251,7 @@ test('Zutaten → Einkaufsliste übertragen (INSERT + Flag setzen)', () => {
|
||||
});
|
||||
|
||||
test('Zweiter Transfer überträgt nichts (alle bereits markiert)', () => {
|
||||
// Mahlzeit aus vorherigem Test — alle on_shopping_list = 1
|
||||
// Mahlzeit aus vorherigem Test - alle on_shopping_list = 1
|
||||
const suppe = db.prepare(`SELECT id FROM meals WHERE title = 'Suppe'`).get();
|
||||
const open = db.prepare(`
|
||||
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests: Modal Utilities (wireBlurValidation, btnSuccess, btnError)
|
||||
* Modul: /public/components/modal.js
|
||||
* Läuft im Node-Kontext — die Utility-Funktionen greifen ausschließlich
|
||||
* Läuft im Node-Kontext - die Utility-Funktionen greifen ausschließlich
|
||||
* über ihre Parameter auf DOM-Objekte zu, daher kein DOM-Polyfill nötig.
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Modul: Notes / Contacts / Budget — Tests
|
||||
* Modul: Notes / Contacts / Budget - Tests
|
||||
* Zweck: Validiert CRUD, Constraints, Filterabfragen, Aggregation für alle drei Module
|
||||
* Ausführen: node --experimental-sqlite test-notes-contacts-budget.js
|
||||
*/
|
||||
|
||||
+4
-4
@@ -72,21 +72,21 @@ test('Liste umbenennen', () => {
|
||||
// --------------------------------------------------------
|
||||
// Artikel-CRUD
|
||||
// --------------------------------------------------------
|
||||
test('Artikel hinzufügen — Obst & Gemüse', () => {
|
||||
test('Artikel hinzufügen - Obst & Gemüse', () => {
|
||||
const r = db.prepare(`INSERT INTO shopping_items (list_id, name, quantity, category)
|
||||
VALUES (?, 'Äpfel', '1 kg', 'Obst & Gemüse')`).run(listId);
|
||||
itemId1 = r.lastInsertRowid;
|
||||
assert(itemId1 > 0);
|
||||
});
|
||||
|
||||
test('Artikel hinzufügen — Milchprodukte', () => {
|
||||
test('Artikel hinzufügen - Milchprodukte', () => {
|
||||
const r = db.prepare(`INSERT INTO shopping_items (list_id, name, quantity, category)
|
||||
VALUES (?, 'Milch', '1 Liter', 'Milchprodukte')`).run(listId);
|
||||
itemId2 = r.lastInsertRowid;
|
||||
assert(itemId2 > 0);
|
||||
});
|
||||
|
||||
test('Artikel hinzufügen — Backwaren', () => {
|
||||
test('Artikel hinzufügen - Backwaren', () => {
|
||||
const r = db.prepare(`INSERT INTO shopping_items (list_id, name, category)
|
||||
VALUES (?, 'Brot', 'Backwaren')`).run(listId);
|
||||
itemId3 = r.lastInsertRowid;
|
||||
@@ -184,7 +184,7 @@ test('Autocomplete-Suggestions nach Prefix', () => {
|
||||
assert(results[0].name === 'Joghurt', `Erwartet Joghurt, erhalten: ${results[0].name}`);
|
||||
});
|
||||
|
||||
test('Autocomplete — kein Match gibt leeres Array', () => {
|
||||
test('Autocomplete - kein Match gibt leeres Array', () => {
|
||||
const results = db.prepare(`
|
||||
SELECT DISTINCT name FROM shopping_items WHERE name LIKE ? COLLATE NOCASE
|
||||
`).all('XXXXXXXX%');
|
||||
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* Tests: UX Utilities (stagger, vibrate)
|
||||
* Läuft im Node-Kontext — kein DOM verfügbar, daher nur Pure-Logic-Tests.
|
||||
* Läuft im Node-Kontext - kein DOM verfügbar, daher nur Pure-Logic-Tests.
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// Minimales Window/Navigator-Mock für Node
|
||||
const { stagger, vibrate } = await (async () => {
|
||||
// stagger braucht window.matchMedia — wir mocken es
|
||||
// stagger braucht window.matchMedia - wir mocken es
|
||||
global.window = {
|
||||
matchMedia: () => ({ matches: false }),
|
||||
};
|
||||
// navigator ist in Node ein getter-only property — über defineProperty überschreiben
|
||||
// navigator ist in Node ein getter-only property - über defineProperty überschreiben
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { vibrate: null },
|
||||
writable: true,
|
||||
|
||||
Reference in New Issue
Block a user