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:
Ulas
2026-04-03 17:04:39 +02:00
parent 6046cac7a8
commit 1122bd269b
56 changed files with 256 additions and 256 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Oikos Umgebungsvariablen # Oikos - Umgebungsvariablen
# Kopiere diese Datei nach .env und passe die Werte an. # Kopiere diese Datei nach .env und passe die Werte an.
# Server # Server
+1 -1
View File
@@ -12,7 +12,7 @@ What problem does this solve? Describe the use case from a family member's persp
## Proposed Solution ## 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 ## Alternatives Considered
+1 -1
View File
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.5.8] - 2026-04-03 ## [0.5.8] - 2026-04-03
### Added ### 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 - Add Italian as selectable language in Settings locale picker
## [0.5.7] - 2026-04-03 ## [0.5.7] - 2026-04-03
+1 -1
View File
@@ -184,7 +184,7 @@ PRs are reviewed by the maintainer. Expect feedback within a few days. Once appr
### Frontend ### Frontend
- Web Component prefix: `oikos-` (one component per file) - 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) - Date format: `DD.MM.YYYY` - Time format: `HH:MM` (24h)
- CSS uses design tokens from `public/styles/tokens.css` - never hardcode values - CSS uses design tokens from `public/styles/tokens.css` - never hardcode values
- Pages export a `render()` function, no side effects on import - Pages export a `render()` function, no side effects on import
+1 -1
View File
@@ -43,7 +43,7 @@ Oikos uses a flat family authorization model:
- **Admin** can create, edit, and delete all user accounts and all shared data. - **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. - **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 ## Supported Versions
+12 -12
View File
@@ -1,4 +1,4 @@
# Oikos Product Specification # Oikos - Product Specification
Self-hosted family planner web app for a single household (26 people). No app store, no public access. Deployment via Docker on a private Linux server behind an Nginx reverse proxy with SSL. Self-hosted family planner web app for a single household (26 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`) ### 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 - Username + password form
- Error display for wrong credentials - 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) - **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 - **Calendar integration:** connect/disconnect Google Calendar OAuth, store Apple Calendar (CalDAV) credentials, configure sync interval
- **Weather:** configure OpenWeatherMap location - **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 - **App info:** version, license
### Budget (`/budget`) ### 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. - **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`). - **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. - **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). - **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. - **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. - **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`. - **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. - **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. - **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. - **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 (1040ms), pattern `[30, 50, 30]` for destructive actions (delete). Respects `prefers-reduced-motion`. - **Vibration:** `vibrate()` from `public/utils/ux.js` - short pulses for light actions (1040ms), 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 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. - **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 ### Architecture
- **Module:** `public/i18n.js` exports: `initI18n()`, `setLocale()`, `t(key, params?)`, `getLocale()`, `getSupportedLocales()`, `formatDate(date)`, `formatTime(date)` - **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" }` - **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' })` - **Variables:** `{{variable}}` syntax in translation strings, e.g. `t('tasks.assignedTo', { name: 'Anna' })`
- **Fallback chain:** active locale → German (`de`) → key itself - **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 ### Language Detection
@@ -343,4 +343,4 @@ All UI strings are managed via `public/i18n.js`. No hardcoded text in JS files o
### Locale Switching ### 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
View File
@@ -1,4 +1,4 @@
# CLAUDE.md Audit Oikos # CLAUDE.md Audit - Oikos
**Datum:** 2026-04-02 **Datum:** 2026-04-02
**Aktuelle Länge:** 109 Zeilen (exkl. Leerzeilen) **Aktuelle Länge:** 109 Zeilen (exkl. Leerzeilen)
@@ -17,7 +17,7 @@
**Fehlend:** **Fehlend:**
- Kein expliziter Hard-Constraints-Block. "No SPA framework" ist in einen Fließtext-Absatz eingebettet statt als nicht-verhandelbare Regel markiert. - 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. - `public/i18n.js` wird als System erwähnt, aber nicht als kanonische Datei referenziert.
- `offline.html` fehlt im Architecture-Tree. - `offline.html` fehlt im Architecture-Tree.
@@ -27,7 +27,7 @@
**Redundanzen mit CONTRIBUTING.md:** **Redundanzen mit CONTRIBUTING.md:**
- Project Structure: fast identischer Tree in beiden Dateien - 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 - Testing: identische Beschreibung in beiden
- Changelog: gleiche Anweisung in beiden - Changelog: gleiche Anweisung in beiden
- Migration append-only: in beiden Dateien - Migration append-only: in beiden Dateien
@@ -37,9 +37,9 @@
### 1.3 Signalqualität ### 1.3 Signalqualität
**Probleme:** **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". - 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. - 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**. - `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 | | 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 | | `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** | | 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 | | 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 | | 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 | | 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 | | Pfade/Module korrekt | ❌ | `public/assets/` existiert nicht, Dockerfile-Version falsch |
| Keine Redundanz mit CONTRIBUTING.md | ❌ | ~40% Überlappung (Structure, Conventions, Testing, Changelog) | | Keine Redundanz mit CONTRIBUTING.md | ❌ | ~40% Überlappung (Structure, Conventions, Testing, Changelog) |
| Keine Redundanz mit docs/ | ✅ | SPEC.md wird referenziert, nicht dupliziert | | Keine Redundanz mit docs/ | ✅ | SPEC.md wird referenziert, nicht dupliziert |
@@ -96,12 +96,12 @@ Siehe `CLAUDE.md.proposed` im Repo-Root.
| Was | Warum verhaltensrelevant | | 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/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. | | `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. | | 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 ### Umformuliert
@@ -115,6 +115,6 @@ Siehe `CLAUDE.md.proposed` im Repo-Root.
| Was | Von | Nach | Grund | | 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 | | 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
View File
@@ -3,21 +3,21 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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."> <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 --> <!-- Open Graph -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:title" content="Oikos The Self-Hosted Family Planner"> <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: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:image" content="https://ulsklyc.github.io/oikos/og-image.png">
<meta property="og:url" content="https://ulsklyc.github.io/oikos/"> <meta property="og:url" content="https://ulsklyc.github.io/oikos/">
<meta property="og:site_name" content="Oikos"> <meta property="og:site_name" content="Oikos">
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Oikos The Self-Hosted Family Planner"> <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: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"> <meta name="twitter:image" content="https://ulsklyc.github.io/oikos/twitter-image.png">
<link rel="canonical" href="https://ulsklyc.github.io/oikos/"> <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"> <img src="logo.svg" alt="Oikos Logo" class="hero-logo" width="80" height="80">
<h1 data-i18n="hero_title">Oikos</h1> <h1 data-i18n="hero_title">Oikos</h1>
<p class="hero-tagline" data-i18n="hero_tagline">The self-hosted family planner</p> <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"> <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> <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> <span data-i18n="hero_cta">View on GitHub</span>
+16 -16
View File
@@ -1,4 +1,4 @@
# Repository Hygiene Audit Oikos # Repository Hygiene Audit - Oikos
**Datum:** 2026-04-02 **Datum:** 2026-04-02
**Version:** 0.5.2 (Tag `v0.5.2`) **Version:** 0.5.2 (Tag `v0.5.2`)
@@ -12,7 +12,7 @@
| # | Datei | Status | Begründung | Empfehlung | | # | 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: **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 | | # | Datei | Status | Begründung | Empfehlung |
|---|-------|--------|------------|------------| |---|-------|--------|------------|------------|
| | | | Keine gefunden. | | | - | - | - | Keine gefunden. | - |
Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber. 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 | | 1 | `.env` | OK | Existiert lokal, **nicht** von Git getrackt, in `.gitignore` gelistet. | Kein Handlungsbedarf |
| 2 | `.claude/` | OK | In `.gitignore`, nicht getrackt. | 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 | | 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 ### 1.4 Prompt-Dateien
@@ -50,15 +50,15 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
| # | Datei | Status | Begründung | Empfehlung | | # | Datei | Status | Begründung | Empfehlung |
|---|-------|--------|------------|------------| |---|-------|--------|------------|------------|
| | | | Keine leeren oder Stub-Dateien gefunden. | | | - | - | - | Keine leeren oder Stub-Dateien gefunden. | - |
### 1.6 Sonderfall: Hilfs-/Script-Dateien ### 1.6 Sonderfall: Hilfs-/Script-Dateien
| # | Datei | Zeilen | Genutzt von | Empfehlung | | # | Datei | Zeilen | Genutzt von | Empfehlung |
|---|-------|--------|-------------|------------| |---|-------|--------|-------------|------------|
| 1 | `server/db-schema-test.js` | 185 | Allen 7 Test-Dateien (`test-*.js`) | **Behalten** aktiv genutzte Test-Infrastruktur | | 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 | | 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 | | 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. | | `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. | | `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. | | `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. | | `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. | | `docker-compose.yml` | ✅ Vorhanden | Nicht im Detail geprüft, aber vorhanden und funktional laut README/CHANGELOG. |
| `Dockerfile` | ✅ Vorhanden | Nicht im Detail geprüft, vorhanden. | | `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. | | `.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 | | Datei | Status | Bewertung |
|-------|--------|-----------| |-------|--------|-----------|
| `docs/` Verzeichnis | ✅ Vorhanden | Enthält SPEC.md, logo.svg, social-preview.png, Screenshots (4 Varianten × 9 Module = 36 Screenshots). | | `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`. | | Service Worker | ✅ Vorhanden | `public/sw.js` + `public/sw-register.js`. |
| `.nvmrc` | ✅ Vorhanden | Inhalt: `22`. Konsistent mit `engines.node: ">=22.0.0"` in package.json. | | `.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. | | `.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 | | 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. | | `scripts/` | ✅ Sinnvoll | `generate-icons.js` (Dev-Tool) und `seed-demo.js` (Screenshot-Generierung). Beide in `.dockerignore` exkludiert. |
| `entrypoint.sh` | ✅ Vorhanden | Docker-Entrypoint. | | `entrypoint.sh` | ✅ Vorhanden | Docker-Entrypoint. |
| `setup.js` | ✅ Vorhanden | Admin-Setup-Wizard, in README dokumentiert. | | `setup.js` | ✅ Vorhanden | Admin-Setup-Wizard, in README dokumentiert. |
@@ -111,7 +111,7 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
| Kriterium | Status | Bewertung | | 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. | | 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). | | 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. | | 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 - **Aufwand**: 5 Minuten
- **Dateien**: `CHANGELOG.md` (Zeilen 139147) - **Dateien**: `CHANGELOG.md` (Zeilen 139147)
### [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). - **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. - **Warum**: Irreführend bei Code-Review oder Wartung. Kein funktionaler Impact.
@@ -165,12 +165,12 @@ Keine `.bak`, `.old`, `.tmp`-Dateien vorhanden. Sauber.
| Kategorie | Ergebnis | | Kategorie | Ergebnis |
|-----------|----------| |-----------|----------|
| **Verwaiste Dateien** | 1 Fund (leeres Worktree-Dir) P3 | | **Verwaiste Dateien** | 1 Fund (leeres Worktree-Dir) - P3 |
| **Superseded Files** | 0 Funde | | **Superseded Files** | 0 Funde |
| **Dev Artifacts** | Korrekt durch .gitignore abgedeckt | | **Dev Artifacts** | Korrekt durch .gitignore abgedeckt |
| **Prompt-Dateien** | Keine im Repo | | **Prompt-Dateien** | Keine im Repo |
| **Leere Dateien** | 0 Funde | | **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 | | **README** | Vollständig, keine Lücken |
| **P0-Findings** | 0 | | **P0-Findings** | 0 |
| **P1-Findings** | 0 | | **P1-Findings** | 0 |
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "oikos", "name": "oikos",
"version": "0.5.8", "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", "main": "server/index.js",
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=22.0.0"
+6 -6
View File
@@ -138,7 +138,7 @@ function _wireSheetSwipe(panel) {
} }
// -------------------------------------------------------- // --------------------------------------------------------
// _doClose gemeinsame Cleanup-Logik // _doClose - gemeinsame Cleanup-Logik
// -------------------------------------------------------- // --------------------------------------------------------
function _doClose() { function _doClose() {
@@ -169,12 +169,12 @@ function _doClose() {
* Öffnet ein Modal mit dem Shared-System. * Öffnet ein Modal mit dem Shared-System.
* *
* @param {Object} opts * @param {Object} opts
* @param {string} opts.title Titel im Modal-Header * @param {string} opts.title - Titel im Modal-Header
* @param {string} opts.content HTML-String für den Modal-Body * @param {string} opts.content - HTML-String für den Modal-Body
* @param {Function} [opts.onSave] Callback, wird nach Einfügen in DOM aufgerufen * @param {Function} [opts.onSave] - Callback, wird nach Einfügen in DOM aufgerufen
* (zum Binden von Form-Events) * (zum Binden von Form-Events)
* @param {Function} [opts.onDelete] Falls vorhanden, wird ein Löschen-Button eingebaut * @param {Function} [opts.onDelete] - Falls vorhanden, wird ein Löschen-Button eingebaut
* @param {string} [opts.size='md'] 'sm' | 'md' | 'lg' * @param {string} [opts.size='md'] - 'sm' | 'md' | 'lg'
*/ */
export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) { export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) {
// Vorheriges Modal schließen (kein Stacking) // Vorheriges Modal schließen (kein Stacking)
+1 -1
View File
@@ -27,7 +27,7 @@ class OikosInstallPrompt extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
// Bereits im Standalone-Modus nichts anzeigen // Bereits im Standalone-Modus - nichts anzeigen
if ( if (
window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone === true navigator.standalone === true
+1 -1
View File
@@ -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. * Zeigt ein <select>-Dropdown für System/Deutsch/English.
* Bei Auswahl: setLocale() oder localStorage-Eintrag löschen (System). * Bei Auswahl: setLocale() oder localStorage-Eintrag löschen (System).
* Dependencies: i18n.js * Dependencies: i18n.js
+3 -3
View File
@@ -1,5 +1,5 @@
/** /**
* i18n Internationalisierung / Übersetzungsmodul * i18n - Internationalisierung / Übersetzungsmodul
* Bietet t(), initI18n(), setLocale(), getLocale(), getSupportedLocales(), * Bietet t(), initI18n(), setLocale(), getLocale(), getSupportedLocales(),
* formatDate(), formatTime() für die gesamte App. * formatDate(), formatTime() für die gesamte App.
* Dependencies: none (vanilla JS, Fetch API, Intl API) * Dependencies: none (vanilla JS, Fetch API, Intl API)
@@ -33,7 +33,7 @@ async function loadLocale(locale) {
return resp.json(); return resp.json();
} }
/** Initialisierung einmal beim App-Start aufrufen */ /** Initialisierung - einmal beim App-Start aufrufen */
export async function initI18n() { export async function initI18n() {
currentLocale = resolveLocale(); currentLocale = resolveLocale();
fallbackTranslations = await loadLocale(DEFAULT_LOCALE); fallbackTranslations = await loadLocale(DEFAULT_LOCALE);
@@ -50,7 +50,7 @@ export async function initI18n() {
document.documentElement.lang = currentLocale; 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) { export async function setLocale(locale) {
if (!SUPPORTED_LOCALES.includes(locale)) return; if (!SUPPORTED_LOCALES.includes(locale)) return;
localStorage.setItem(STORAGE_KEY, locale); localStorage.setItem(STORAGE_KEY, locale);
+2 -2
View File
@@ -14,7 +14,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" /> <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-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Oikos" /> <meta name="apple-mobile-web-app-title" content="Oikos" />
<meta name="description" content="Oikos Familienplaner" /> <meta name="description" content="Oikos - Familienplaner" />
<title>Oikos</title> <title>Oikos</title>
<!-- PWA --> <!-- PWA -->
@@ -59,7 +59,7 @@
<script src="/lucide.min.js"></script> <script src="/lucide.min.js"></script>
</head> </head>
<body> <body>
<!-- App-Shell wird durch JavaScript gefüllt --> <!-- App-Shell - wird durch JavaScript gefüllt -->
<div id="app" class="app-shell"> <div id="app" class="app-shell">
<!-- Skeleton-Loading während Initialisierung --> <!-- Skeleton-Loading während Initialisierung -->
<div id="app-loading" class="app-loading" aria-live="polite" aria-label="Lade Oikos…"> <div id="app-loading" class="app-loading" aria-live="polite" aria-label="Lade Oikos…">
+5 -5
View File
@@ -21,7 +21,7 @@
"errorOccurred": "Etwas ist schiefgelaufen.", "errorOccurred": "Etwas ist schiefgelaufen.",
"unexpectedError": "Ein unerwarteter Fehler ist aufgetreten.", "unexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
"errorGeneric": "Ein 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", "titleRequired": "Titel ist erforderlich",
"nameRequired": "Name ist erforderlich", "nameRequired": "Name ist erforderlich",
"contentRequired": "Inhalt ist erforderlich", "contentRequired": "Inhalt ist erforderlich",
@@ -82,7 +82,7 @@
"title": "Aufgaben", "title": "Aufgaben",
"newTask": "Neue Aufgabe", "newTask": "Neue Aufgabe",
"editTask": "Aufgabe bearbeiten", "editTask": "Aufgabe bearbeiten",
"emptyTitle": "Keine Aufgaben alles erledigt?", "emptyTitle": "Keine Aufgaben - alles erledigt?",
"emptyDescription": "Neue Aufgaben über den + Button erstellen.", "emptyDescription": "Neue Aufgaben über den + Button erstellen.",
"titleLabel": "Titel *", "titleLabel": "Titel *",
"titlePlaceholder": "Was muss erledigt werden?", "titlePlaceholder": "Was muss erledigt werden?",
@@ -93,7 +93,7 @@
"dueDateLabel": "Fälligkeit", "dueDateLabel": "Fälligkeit",
"dueTimeLabel": "Uhrzeit", "dueTimeLabel": "Uhrzeit",
"assignedLabel": "Zugewiesen an", "assignedLabel": "Zugewiesen an",
"assignedNobody": " Niemand ", "assignedNobody": "- Niemand -",
"statusLabel": "Status", "statusLabel": "Status",
"priorityUrgent": "Dringend", "priorityUrgent": "Dringend",
"priorityHigh": "Hoch", "priorityHigh": "Hoch",
@@ -259,7 +259,7 @@
"locationLabel": "Ort", "locationLabel": "Ort",
"locationPlaceholder": "Optional", "locationPlaceholder": "Optional",
"assignedLabel": "Zugewiesen an", "assignedLabel": "Zugewiesen an",
"assignedNobody": " Niemand ", "assignedNobody": "- Niemand -",
"colorLabel": "Farbe", "colorLabel": "Farbe",
"descriptionLabel": "Beschreibung", "descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Optional…", "descriptionPlaceholder": "Optional…",
@@ -428,7 +428,7 @@
"savedToast": "Eintrag gespeichert", "savedToast": "Eintrag gespeichert",
"deletedToast": "Eintrag gelöscht", "deletedToast": "Eintrag gelöscht",
"loadError": "Budget konnte nicht geladen werden.", "loadError": "Budget konnte nicht geladen werden.",
"trendNeutral": " wie {{month}}", "trendNeutral": "- wie {{month}}",
"validAmountRequired": "Gültigen Betrag eingeben", "validAmountRequired": "Gültigen Betrag eingeben",
"dateRequired": "Datum ist erforderlich", "dateRequired": "Datum ist erforderlich",
"catFood": "Lebensmittel", "catFood": "Lebensmittel",
+5 -5
View File
@@ -21,7 +21,7 @@
"errorOccurred": "Something went wrong.", "errorOccurred": "Something went wrong.",
"unexpectedError": "An unexpected error occurred.", "unexpectedError": "An unexpected error occurred.",
"errorGeneric": "An 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", "titleRequired": "Title is required",
"nameRequired": "Name is required", "nameRequired": "Name is required",
"contentRequired": "Content is required", "contentRequired": "Content is required",
@@ -82,7 +82,7 @@
"title": "Tasks", "title": "Tasks",
"newTask": "New Task", "newTask": "New Task",
"editTask": "Edit Task", "editTask": "Edit Task",
"emptyTitle": "No tasks all done?", "emptyTitle": "No tasks - all done?",
"emptyDescription": "Create new tasks with the + button.", "emptyDescription": "Create new tasks with the + button.",
"titleLabel": "Title *", "titleLabel": "Title *",
"titlePlaceholder": "What needs to be done?", "titlePlaceholder": "What needs to be done?",
@@ -93,7 +93,7 @@
"dueDateLabel": "Due date", "dueDateLabel": "Due date",
"dueTimeLabel": "Time", "dueTimeLabel": "Time",
"assignedLabel": "Assigned to", "assignedLabel": "Assigned to",
"assignedNobody": " Nobody ", "assignedNobody": "- Nobody -",
"statusLabel": "Status", "statusLabel": "Status",
"priorityUrgent": "Urgent", "priorityUrgent": "Urgent",
"priorityHigh": "High", "priorityHigh": "High",
@@ -259,7 +259,7 @@
"locationLabel": "Location", "locationLabel": "Location",
"locationPlaceholder": "Optional", "locationPlaceholder": "Optional",
"assignedLabel": "Assigned to", "assignedLabel": "Assigned to",
"assignedNobody": " Nobody ", "assignedNobody": "- Nobody -",
"colorLabel": "Color", "colorLabel": "Color",
"descriptionLabel": "Description", "descriptionLabel": "Description",
"descriptionPlaceholder": "Optional…", "descriptionPlaceholder": "Optional…",
@@ -428,7 +428,7 @@
"savedToast": "Entry saved", "savedToast": "Entry saved",
"deletedToast": "Entry deleted", "deletedToast": "Entry deleted",
"loadError": "Budget could not be loaded.", "loadError": "Budget could not be loaded.",
"trendNeutral": " same as {{month}}", "trendNeutral": "- same as {{month}}",
"validAmountRequired": "Please enter a valid amount", "validAmountRequired": "Please enter a valid amount",
"dateRequired": "Date is required", "dateRequired": "Date is required",
"catFood": "Groceries", "catFood": "Groceries",
+5 -5
View File
@@ -21,7 +21,7 @@
"errorOccurred": "Si è verificato un errore.", "errorOccurred": "Si è verificato un errore.",
"unexpectedError": "Si è verificato un errore imprevisto.", "unexpectedError": "Si è verificato un errore imprevisto.",
"errorGeneric": "Si è verificato un errore.", "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", "titleRequired": "Il titolo è obbligatorio",
"nameRequired": "Il nome è obbligatorio", "nameRequired": "Il nome è obbligatorio",
"contentRequired": "Il contenuto è obbligatorio", "contentRequired": "Il contenuto è obbligatorio",
@@ -82,7 +82,7 @@
"title": "Compiti", "title": "Compiti",
"newTask": "Nuovo compito", "newTask": "Nuovo compito",
"editTask": "Modifica compito", "editTask": "Modifica compito",
"emptyTitle": "Nessun compito tutto fatto?", "emptyTitle": "Nessun compito - tutto fatto?",
"emptyDescription": "Crea nuovi compiti con il pulsante +.", "emptyDescription": "Crea nuovi compiti con il pulsante +.",
"titleLabel": "Titolo *", "titleLabel": "Titolo *",
"titlePlaceholder": "Cosa bisogna fare?", "titlePlaceholder": "Cosa bisogna fare?",
@@ -93,7 +93,7 @@
"dueDateLabel": "Data di scadenza", "dueDateLabel": "Data di scadenza",
"dueTimeLabel": "Ora", "dueTimeLabel": "Ora",
"assignedLabel": "Assegnato a", "assignedLabel": "Assegnato a",
"assignedNobody": " Nessuno ", "assignedNobody": "- Nessuno -",
"statusLabel": "Stato", "statusLabel": "Stato",
"priorityUrgent": "Urgente", "priorityUrgent": "Urgente",
"priorityHigh": "Alta", "priorityHigh": "Alta",
@@ -259,7 +259,7 @@
"locationLabel": "Luogo", "locationLabel": "Luogo",
"locationPlaceholder": "Opzionale", "locationPlaceholder": "Opzionale",
"assignedLabel": "Assegnato a", "assignedLabel": "Assegnato a",
"assignedNobody": " Nessuno ", "assignedNobody": "- Nessuno -",
"colorLabel": "Colore", "colorLabel": "Colore",
"descriptionLabel": "Descrizione", "descriptionLabel": "Descrizione",
"descriptionPlaceholder": "Opzionale…", "descriptionPlaceholder": "Opzionale…",
@@ -428,7 +428,7 @@
"savedToast": "Voce salvata", "savedToast": "Voce salvata",
"deletedToast": "Voce eliminata", "deletedToast": "Voce eliminata",
"loadError": "Impossibile caricare il bilancio.", "loadError": "Impossibile caricare il bilancio.",
"trendNeutral": " come {{month}}", "trendNeutral": "- come {{month}}",
"validAmountRequired": "Inserisci un importo valido", "validAmountRequired": "Inserisci un importo valido",
"dateRequired": "La data è obbligatoria", "dateRequired": "La data è obbligatoria",
"catFood": "Spesa alimentare", "catFood": "Spesa alimentare",
+3 -3
View File
@@ -7,7 +7,7 @@
import { api } from '/api.js'; import { api } from '/api.js';
import { t, formatDate, formatTime, getLocale } from '/i18n.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; 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"> <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> <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__type">${mealLabels[type]}</div>
<div class="meal-slot__title">${meal ? meal.title : ''}</div> <div class="meal-slot__title">${meal ? meal.title : '-'}</div>
</div> </div>
`; `;
}).join(''); }).join('');
@@ -487,7 +487,7 @@ export async function render(container, { user }) {
refreshBtn.addEventListener('click', doWeatherRefresh, { signal: _fabController.signal }); 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); const timerId = setInterval(doWeatherRefresh, 30 * 60 * 1000);
_fabController.signal.addEventListener('abort', () => clearInterval(timerId)); _fabController.signal.addEventListener('abort', () => clearInterval(timerId));
} }
+1 -1
View File
@@ -350,7 +350,7 @@ function wireDragDrop(grid) {
async function onUp(ev) { async function onUp(ev) {
if (!dragging) return; if (!dragging) return;
const { mealId, sourceDate, sourceType, slot: sourceSlot } = dragging; 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'; if (ghost) ghost.style.display = 'none';
const el = document.elementFromPoint(ev.clientX, ev.clientY); const el = document.elementFromPoint(ev.clientX, ev.clientY);
+3 -3
View File
@@ -13,9 +13,9 @@ import { t } from '/i18n.js';
// -------------------------------------------------------- // --------------------------------------------------------
// Swipe-Gesten Konstanten (identisch zu tasks.js) // Swipe-Gesten Konstanten (identisch zu tasks.js)
const SWIPE_THRESHOLD = 80; // px Mindestweg für Aktion const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
const SWIPE_MAX_VERT = 12; // px vertikaler Toleranzbereich const SWIPE_MAX_VERT = 12; // px - vertikaler Toleranzbereich
const SWIPE_LOCK_VERT = 30; // px ab diesem Weg gilt es als Scroll const SWIPE_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll
const ITEM_CATEGORIES = [ const ITEM_CATEGORIES = [
'Obst & Gemüse', 'Backwaren', 'Milchprodukte', 'Fleisch & Fisch', 'Obst & Gemüse', 'Backwaren', 'Milchprodukte', 'Fleisch & Fisch',
+3 -3
View File
@@ -717,9 +717,9 @@ function updateOverdueBadge() {
// Swipe-Gesten (Mobil: links = erledigt, rechts = bearbeiten) // Swipe-Gesten (Mobil: links = erledigt, rechts = bearbeiten)
// -------------------------------------------------------- // --------------------------------------------------------
const SWIPE_THRESHOLD = 80; // px Mindestweg für Aktion const SWIPE_THRESHOLD = 80; // px - Mindestweg für Aktion
const SWIPE_MAX_VERT = 12; // px vertikaler Bewegungs-Toleranzbereich (darunter: kein Scroll-Abbruch) 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_LOCK_VERT = 30; // px - ab diesem Weg gilt es als Scroll (Swipe abgebrochen)
function wireSwipeGestures(container) { function wireSwipeGestures(container) {
const listEl = container.querySelector('#task-list'); const listEl = container.querySelector('#task-list');
+1 -1
View File
@@ -1,4 +1,4 @@
# Oikos Familienplaner self-hosted, private # Oikos Familienplaner - self-hosted, private
# Keine öffentlichen Inhalte zum Indexieren # Keine öffentlichen Inhalte zum Indexieren
User-agent: * User-agent: *
+4 -4
View File
@@ -34,7 +34,7 @@ const isStandalone = window.matchMedia('(display-mode: standalone)').matches
/** /**
* Setzt die theme-color Meta-Tags (Light + Dark Variante). * Setzt die theme-color Meta-Tags (Light + Dark Variante).
* @param {string} lightColor * @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) { function setThemeColor(lightColor, darkColor) {
if (!isStandalone) return; if (!isStandalone) return;
@@ -116,7 +116,7 @@ async function navigate(path, userOrPushState = true, pushState = true) {
pushState = userOrPushState; 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; const previousPath = currentPath;
currentPath = path; currentPath = path;
@@ -176,7 +176,7 @@ async function renderPage(route, previousPath = null) {
throw new Error(`Seite ${route.page} exportiert keine render()-Funktion.`); 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() // page-content muss im DOM existieren damit document.getElementById()
// in Seiten-Modulen funktioniert. // in Seiten-Modulen funktioniert.
if (!document.querySelector('.nav-bottom') && currentUser) { if (!document.querySelector('.nav-bottom') && currentUser) {
@@ -446,7 +446,7 @@ window.addEventListener('locale-changed', () => {
// -------------------------------------------------------- // --------------------------------------------------------
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen // 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. // Nur auf Mobilgeräten relevant (< 1024px), Desktop hat keine virtuelle Tastatur.
// -------------------------------------------------------- // --------------------------------------------------------
if (window.visualViewport) { if (window.visualViewport) {
+2 -2
View File
@@ -1,6 +1,6 @@
/** /**
* Modul: Dashboard * 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 * Abhängigkeiten: tokens.css, layout.css
*/ */
@@ -230,7 +230,7 @@
margin-bottom: var(--space-1); margin-bottom: var(--space-1);
} }
/* Widget hover lift (desktop) dezent, max 1px */ /* Widget hover lift (desktop) - dezent, max 1px */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.widget { .widget {
transition: transform var(--transition-fast), box-shadow var(--transition-fast); transition: transform var(--transition-fast), box-shadow var(--transition-fast);
+15 -15
View File
@@ -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. * Einheitlicher runder Plus-Button auf allen Unterseiten.
* Mobile: über der Bottom-Nav. Desktop: unten rechts im Content. * 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, #btn-new-task,
#notes-add-btn, #notes-add-btn,
#contacts-add-btn, #contacts-add-btn,
@@ -262,7 +262,7 @@
} }
/* ================================================================ /* ================================================================
* Sidebar Navigation Desktop (≥ 1024px) * Sidebar Navigation - Desktop (≥ 1024px)
* *
* Design: Flach, kein Neumorphismus. Dezenter Seitenrand. * Design: Flach, kein Neumorphismus. Dezenter Seitenrand.
* Aktiver State: Hintergrund-Highlight + Akzentstreifen links. * Aktiver State: Hintergrund-Highlight + Akzentstreifen links.
@@ -413,7 +413,7 @@
} }
/* ================================================================ /* ================================================================
* Sidebar Expanded (≥ 1280px) Labels sichtbar * Sidebar Expanded (≥ 1280px) - Labels sichtbar
* ================================================================ */ * ================================================================ */
@media (min-width: 1280px) { @media (min-width: 1280px) {
:root { :root {
@@ -515,11 +515,11 @@
* - Hover auf Desktop: leichter Lift (1px) * - Hover auf Desktop: leichter Lift (1px)
* *
* Varianten: * Varianten:
* .card Basis (kein Padding) * .card - Basis (kein Padding)
* .card--padded Mit Padding * .card--padded - Mit Padding
* .card--compact Enges Padding (12px) * .card--compact - Enges Padding (12px)
* .card--flat Kein Shadow, nur Border * .card--flat - Kein Shadow, nur Border
* .card--interactive Hover-Lift + Cursor * .card--interactive - Hover-Lift + Cursor
* -------------------------------------------------------- */ * -------------------------------------------------------- */
.card { .card {
background-color: var(--color-surface); background-color: var(--color-surface);
@@ -1048,15 +1048,15 @@
* Wiederverwendbare Content-Area-Patterns für Desktop. * Wiederverwendbare Content-Area-Patterns für Desktop.
* Mobile: immer single-column (Stacking). * Mobile: immer single-column (Stacking).
* *
* .layout-master-detail Liste links, Detail rechts (Aufgaben, Einkauf) * .layout-master-detail - Liste links, Detail rechts (Aufgaben, Einkauf)
* .layout-content-aside Hauptinhalt + schmale Seitenleiste (Kalender) * .layout-content-aside - Hauptinhalt + schmale Seitenleiste (Kalender)
* .layout-center Zentrierter schmaler Content (Settings, Login) * .layout-center - Zentrierter schmaler Content (Settings, Login)
* .layout-wide Volle Breite mit max-width (Dashboard) * .layout-wide - Volle Breite mit max-width (Dashboard)
* -------------------------------------------------------- */ * -------------------------------------------------------- */
/* ── Master-Detail ── /* ── Master-Detail ──
* Mobile: gestapelt (Detail wird programmatisch ein-/ausgeblendet). * Mobile: gestapelt (Detail wird programmatisch ein-/ausgeblendet).
* Desktop: 2 Spalten Liste ~40%, Detail ~60%. * Desktop: 2 Spalten - Liste ~40%, Detail ~60%.
*/ */
.layout-master-detail { .layout-master-detail {
display: flex; 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, * Modul-spezifische Styles (.swipe-reveal--edit, .swipe-reveal--delete,
* .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS. * .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS.
* -------------------------------------------------------- */ * -------------------------------------------------------- */
+1 -1
View File
@@ -151,7 +151,7 @@
text-align: left; 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="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="lunch"] .meal-slot__type-label { color: var(--meal-lunch); }
.meal-slot[data-type="dinner"] .meal-slot__type-label { color: var(--meal-dinner); } .meal-slot[data-type="dinner"] .meal-slot__type-label { color: var(--meal-dinner); }
+1 -1
View File
@@ -31,7 +31,7 @@ body {
} }
/* Touch-Targets werden über tokens.css (--target-sm/md/lg) und /* 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 * Keine globale min-size-Regel hier, da sie mit dem bestehenden
* Touch-Target-System kollidiert (::before-Expansion auf kleinen Elementen). */ * Touch-Target-System kollidiert (::before-Expansion auf kleinen Elementen). */
+1 -1
View File
@@ -10,7 +10,7 @@
.settings-page { --module-accent: var(--module-settings); } .settings-page { --module-accent: var(--module-settings); }
/* -------------------------------------------------------- /* --------------------------------------------------------
Seiten-Layout nutzt layout-center (max 720px) Seiten-Layout - nutzt layout-center (max 720px)
-------------------------------------------------------- */ -------------------------------------------------------- */
.settings-page { .settings-page {
+2 -2
View File
@@ -419,7 +419,7 @@
.item-delete:hover { color: var(--color-danger); } .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) */ /* Kein Margin mehr am shopping-item selbst (übernimmt swipe-row) */
@@ -444,7 +444,7 @@
box-shadow: var(--shadow-lg); 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) { @media (max-width: 1023px) {
.item-delete { .item-delete {
display: none; display: none;
+1 -1
View File
@@ -169,7 +169,7 @@
} }
/* -------------------------------------------------------- /* --------------------------------------------------------
* Swipe-Wrapper Task-spezifische Styles * Swipe-Wrapper - Task-spezifische Styles
* Basis-Styles (.swipe-row, .swipe-reveal, .swipe-reveal--done) * Basis-Styles (.swipe-row, .swipe-reveal, .swipe-reveal--done)
* liegen in layout.css * liegen in layout.css
* -------------------------------------------------------- */ * -------------------------------------------------------- */
+34 -34
View File
@@ -23,7 +23,7 @@
:root { :root {
/* -------------------------------------------------------- /* --------------------------------------------------------
* 1. Farben Neutral-Skala * 1. Farben - Neutral-Skala
* Leicht warmgetönt (kein reines Grau) für einladende Atmosphäre. * Leicht warmgetönt (kein reines Grau) für einladende Atmosphäre.
* Benannt als --neutral-{stufe} für direkte Nutzung, * Benannt als --neutral-{stufe} für direkte Nutzung,
* plus semantische Aliase (--color-bg, --color-surface etc.) * plus semantische Aliase (--color-bg, --color-surface etc.)
@@ -56,7 +56,7 @@
--color-text-disabled: var(--neutral-300); --color-text-disabled: var(--neutral-300);
/* -------------------------------------------------------- /* --------------------------------------------------------
* 2. Farben Akzent (konfigurierbar) * 2. Farben - Akzent (konfigurierbar)
* Wärmerer Blauton statt reinem Corporate-Blau. * Wärmerer Blauton statt reinem Corporate-Blau.
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--color-accent: #2563EB; --color-accent: #2563EB;
@@ -68,7 +68,7 @@
--color-btn-primary-hover: #1E429A; --color-btn-primary-hover: #1E429A;
/* -------------------------------------------------------- /* --------------------------------------------------------
* 3. Farben Semantisch * 3. Farben - Semantisch
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--color-success: #15803D; --color-success: #15803D;
--color-success-hover: #166534; --color-success-hover: #166534;
@@ -84,22 +84,22 @@
--color-info-light: #DDF4FF; --color-info-light: #DDF4FF;
/* -------------------------------------------------------- /* --------------------------------------------------------
* 4. Farben Modul-Akzente * 4. Farben - Modul-Akzente
* Jedes Modul hat eine eigene dezente Akzentfarbe. * Jedes Modul hat eine eigene dezente Akzentfarbe.
* Einsatz in Modul-Headern, Icons, aktiven States. * Einsatz in Modul-Headern, Icons, aktiven States.
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--module-dashboard: #2563EB; /* Blau Übersicht, neutral */ --module-dashboard: #2563EB; /* Blau - Übersicht, neutral */
--module-tasks: #15803D; /* Grün Erledigung, Fortschritt */ --module-tasks: #15803D; /* Grün - Erledigung, Fortschritt */
--module-calendar: #8250DF; /* Violett Termine, Zeit */ --module-calendar: #8250DF; /* Violett - Termine, Zeit */
--module-meals: #B45309; /* Orange Essen, Wärme */ --module-meals: #B45309; /* Orange - Essen, Wärme */
--module-shopping: #D4511E; /* Rot-Orange Einkaufen, Aktion */ --module-shopping: #D4511E; /* Rot-Orange - Einkaufen, Aktion */
--module-notes: #BF8700; /* Gold Notizen, Pinnwand */ --module-notes: #BF8700; /* Gold - Notizen, Pinnwand */
--module-contacts: #0969DA; /* Kräftiges Blau Kontakte */ --module-contacts: #0969DA; /* Kräftiges Blau - Kontakte */
--module-budget: #1A7F5A; /* Teal Finanzen, Stabilität */ --module-budget: #1A7F5A; /* Teal - Finanzen, Stabilität */
--module-settings: #6E7781; /* Grau Konfiguration */ --module-settings: #6E7781; /* Grau - Konfiguration */
/* -------------------------------------------------------- /* --------------------------------------------------------
* 5. Farben Mahlzeit-Typen * 5. Farben - Mahlzeit-Typen
* Zentrale Tokens statt Hardcoding in meals.css * Zentrale Tokens statt Hardcoding in meals.css
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--meal-breakfast: #B45309; --meal-breakfast: #B45309;
@@ -112,7 +112,7 @@
--meal-snack-light: #FFECE3; --meal-snack-light: #FFECE3;
/* -------------------------------------------------------- /* --------------------------------------------------------
* 6. Farben Prioritäten * 6. Farben - Prioritäten
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--color-priority-low: var(--neutral-500); --color-priority-low: var(--neutral-500);
--color-priority-medium: #B45309; --color-priority-medium: #B45309;
@@ -159,15 +159,15 @@
--font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace; --font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
/* Size-Skala */ /* Size-Skala */
--text-xs: 0.75rem; /* 12px Minimum, Captions, Badges, Nav-Labels */ --text-xs: 0.75rem; /* 12px - Minimum, Captions, Badges, Nav-Labels */
--text-sm: 0.8125rem; /* 13px Small/Secondary */ --text-sm: 0.8125rem; /* 13px - Small/Secondary */
--text-base: 0.875rem; /* 14px Body (Desktop), kompakter */ --text-base: 0.875rem; /* 14px - Body (Desktop), kompakter */
--text-md: 1rem; /* 16px Body (Mobile), Inputs */ --text-md: 1rem; /* 16px - Body (Mobile), Inputs */
--text-lg: 1.125rem; /* 18px Section-Title */ --text-lg: 1.125rem; /* 18px - Section-Title */
--text-xl: 1.25rem; /* 20px Subtitle */ --text-xl: 1.25rem; /* 20px - Subtitle */
--text-2xl: 1.5rem; /* 24px Page-Title */ --text-2xl: 1.5rem; /* 24px - Page-Title */
--text-3xl: 1.875rem; /* 30px Page-Title Desktop */ --text-3xl: 1.875rem; /* 30px - Page-Title Desktop */
--text-4xl: 2.25rem; /* 36px Hero/Greeting */ --text-4xl: 2.25rem; /* 36px - Hero/Greeting */
/* Line-Heights */ /* Line-Heights */
--line-height-tight: 1.25; --line-height-tight: 1.25;
@@ -182,7 +182,7 @@
--font-weight-bold: 700; --font-weight-bold: 700;
/* -------------------------------------------------------- /* --------------------------------------------------------
* 11. Abstände 4px-Raster * 11. Abstände - 4px-Raster
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--space-0: 0px; --space-0: 0px;
--space-px: 1px; --space-px: 1px;
@@ -216,7 +216,7 @@
/* -------------------------------------------------------- /* --------------------------------------------------------
* 13. Sidebar * 13. Sidebar
* Reduzierter Neumorphismus subtilere Schatten. * Reduzierter Neumorphismus - subtilere Schatten.
* -------------------------------------------------------- */ * -------------------------------------------------------- */
--sidebar-bg: var(--neutral-100); --sidebar-bg: var(--neutral-100);
--sidebar-shadow-light: rgba(255, 255, 255, 0.6); --sidebar-shadow-light: rgba(255, 255, 255, 0.6);
@@ -280,7 +280,7 @@
--sidebar-shadow-light: rgba(255, 255, 255, 0.04); --sidebar-shadow-light: rgba(255, 255, 255, 0.04);
--sidebar-shadow-dark: rgba(0, 0, 0, 0.4); --sidebar-shadow-dark: rgba(0, 0, 0, 0.4);
/* Akzent Dark Mode */ /* Akzent - Dark Mode */
--color-accent: #60A5FA; --color-accent: #60A5FA;
--color-accent-hover: #3B82F6; --color-accent-hover: #3B82F6;
--color-accent-active: #2563EB; --color-accent-active: #2563EB;
@@ -289,7 +289,7 @@
--color-btn-primary: #3B82F6; --color-btn-primary: #3B82F6;
--color-btn-primary-hover: #2563EB; --color-btn-primary-hover: #2563EB;
/* Semantische Farben Dark Mode */ /* Semantische Farben - Dark Mode */
--color-success: #4ADE80; --color-success: #4ADE80;
--color-warning: #F59E0B; --color-warning: #F59E0B;
--color-danger: #FCA5A5; --color-danger: #FCA5A5;
@@ -300,7 +300,7 @@
--color-danger-light: #3D1C1A; --color-danger-light: #3D1C1A;
--color-info-light: #1A2D40; --color-info-light: #1A2D40;
/* Modul-Akzente Dark Mode */ /* Modul-Akzente - Dark Mode */
--module-dashboard: #60A5FA; --module-dashboard: #60A5FA;
--module-tasks: #4ADE80; --module-tasks: #4ADE80;
--module-calendar: #A78BFA; --module-calendar: #A78BFA;
@@ -311,7 +311,7 @@
--module-budget: #34D399; --module-budget: #34D399;
--module-settings: #94A3B8; --module-settings: #94A3B8;
/* Mahlzeit-Typ Dark Mode */ /* Mahlzeit-Typ - Dark Mode */
--meal-breakfast: #F59E0B; --meal-breakfast: #F59E0B;
--meal-dinner: #60A5FA; --meal-dinner: #60A5FA;
@@ -362,7 +362,7 @@
--sidebar-shadow-light: rgba(255, 255, 255, 0.04); --sidebar-shadow-light: rgba(255, 255, 255, 0.04);
--sidebar-shadow-dark: rgba(0, 0, 0, 0.4); --sidebar-shadow-dark: rgba(0, 0, 0, 0.4);
/* Akzent Dark Mode */ /* Akzent - Dark Mode */
--color-accent: #60A5FA; --color-accent: #60A5FA;
--color-accent-hover: #3B82F6; --color-accent-hover: #3B82F6;
--color-accent-active: #2563EB; --color-accent-active: #2563EB;
@@ -371,7 +371,7 @@
--color-btn-primary: #3B82F6; --color-btn-primary: #3B82F6;
--color-btn-primary-hover: #2563EB; --color-btn-primary-hover: #2563EB;
/* Semantische Farben Dark Mode */ /* Semantische Farben - Dark Mode */
--color-success: #4ADE80; --color-success: #4ADE80;
--color-warning: #F59E0B; --color-warning: #F59E0B;
--color-danger: #FCA5A5; --color-danger: #FCA5A5;
@@ -382,7 +382,7 @@
--color-danger-light: #3D1C1A; --color-danger-light: #3D1C1A;
--color-info-light: #1A2D40; --color-info-light: #1A2D40;
/* Modul-Akzente Dark Mode */ /* Modul-Akzente - Dark Mode */
--module-dashboard: #60A5FA; --module-dashboard: #60A5FA;
--module-tasks: #4ADE80; --module-tasks: #4ADE80;
--module-calendar: #A78BFA; --module-calendar: #A78BFA;
@@ -393,7 +393,7 @@
--module-budget: #34D399; --module-budget: #34D399;
--module-settings: #94A3B8; --module-settings: #94A3B8;
/* Mahlzeit-Typ Dark Mode */ /* Mahlzeit-Typ - Dark Mode */
--meal-breakfast: #F59E0B; --meal-breakfast: #F59E0B;
--meal-dinner: #60A5FA; --meal-dinner: #60A5FA;
+2 -2
View File
@@ -111,7 +111,7 @@ self.addEventListener('fetch', (event) => {
const { request } = event; const { request } = event;
const url = new URL(request.url); const url = new URL(request.url);
// API: immer Netzwerk niemals Nutzerdaten cachen // API: immer Netzwerk - niemals Nutzerdaten cachen
if (url.pathname.startsWith('/api/')) return; if (url.pathname.startsWith('/api/')) return;
// Nur GET cachen // Nur GET cachen
@@ -123,7 +123,7 @@ self.addEventListener('fetch', (event) => {
return; 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 // Cross-Origin-Assets (z.B. Wetter-Icons von openweathermap.org) nicht
// abfangen: opaque Responses führen im PWA-Modus zu Darstellungsfehlern. // abfangen: opaque Responses führen im PWA-Modus zu Darstellungsfehlern.
if (isAsset(url.pathname) && url.origin === self.location.origin) { if (isAsset(url.pathname) && url.origin === self.location.origin) {
+4 -4
View File
@@ -10,9 +10,9 @@
* *
* @param {NodeList|Element[]} elements * @param {NodeList|Element[]} elements
* @param {Object} [opts] * @param {Object} [opts]
* @param {number} [opts.delay=30] ms zwischen jedem Element * @param {number} [opts.delay=30] - ms zwischen jedem Element
* @param {number} [opts.duration=180] ms pro Element * @param {number} [opts.duration=180] - ms pro Element
* @param {number} [opts.max=5] Maximale Anzahl gestaffelter Elemente * @param {number} [opts.max=5] - Maximale Anzahl gestaffelter Elemente
*/ */
export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {}) { export function stagger(elements, { delay = 30, duration = 180, max = 5 } = {}) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 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 * Vibrationsmuster abspielen, wenn die API verfügbar ist und
* keine reduzierte Bewegung gewünscht wird. * 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) { export function vibrate(pattern) {
if (!navigator.vibrate) return; if (!navigator.vibrate) return;
+1 -1
View File
@@ -53,7 +53,7 @@ function createAppleTouchSvg() {
return createLogoSvg(180); return createLogoSvg(180);
} }
/** Favicon (32x32): simplified just gradient background with house */ /** Favicon (32x32): simplified - just gradient background with house */
function createFaviconSvg() { function createFaviconSvg() {
return createLogoSvg(32); return createLogoSvg(32);
} }
+45 -45
View File
@@ -1,5 +1,5 @@
/** /**
* Demo Seed Script Oikos * Demo Seed Script - Oikos
* Fills the database with realistic English demo content for screenshots/mockups. * Fills the database with realistic English demo content for screenshots/mockups.
* Usage: node scripts/seed-demo.js [--db /path/to/oikos.db] * 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], ['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], ['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], ['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], ['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 ], ['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], ['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], ['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 ], ['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], ['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 ], ['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], ['Update home inventory', 'For insurance purposes', 'admin', 'low', 'open', daysFromNow(25), alexId, alexId],
].forEach(row => insertTask.run(...row)); ].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 ], ["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 ], ['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 ], ['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], ['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], ['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(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(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], ['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], ['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(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 ], ['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 ], ['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], ['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], ['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)); ].forEach(row => insertEvent.run(...row));
// ── Meals ──────────────────────────────────────────────────────────────────── // ── 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 MonThu'], ['Dr. Anna Weber', 'medical', '+49 231 445 2210', 'praxis@dr-weber.de', 'Bürgerstraße 12, Dortmund', 'GP - appointments MonThu'],
['Dr. Thomas Müller', 'medical', '+49 231 887 0034', 'info@zahnarzt-mueller.de', 'Hansastraße 55, Dortmund', 'Family dentist'], ['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"], ['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"], ['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'], ['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"], ['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'], ['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'], ['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'], ['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'], ['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'], ['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"], ['Emma\'s Best Friend Lena','family', '+49 231 774 3309', '', '', "Lena Braun - mum is Katrin +49 231 774 3308"],
].forEach(row => insertContact.run(...row)); ].forEach(row => insertContact.run(...row));
// ── Budget ─────────────────────────────────────────────────────────────────── // ── Budget ───────────────────────────────────────────────────────────────────
@@ -215,41 +215,41 @@ const insertBudget = db.prepare(`
[ [
// Income // Income
['Alex Monthly Salary', 3850.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], ['Sam - Part-time Work', 1200.00, 'income', thisMonthDate(1), 1, alexId],
['Child Benefit (Kindergeld)', 250.00, 'income', thisMonthDate(5), 1, alexId], ['Child Benefit (Kindergeld)', 250.00, 'income', thisMonthDate(5), 1, alexId],
// Fixed expenses // Fixed expenses
['Rent', -1450.00, 'housing', thisMonthDate(1), 1, alexId], ['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], ['Health Insurance', -310.00, 'insurance', thisMonthDate(1), 1, alexId],
['Internet & Phone Bundle', -49.99, 'utilities', thisMonthDate(5), 1, alexId], ['Internet & Phone Bundle', -49.99, 'utilities', thisMonthDate(5), 1, alexId],
['Electricity Bill', -78.00, 'utilities', thisMonthDate(15), 1, alexId], ['Electricity Bill', -78.00, 'utilities', thisMonthDate(15), 1, alexId],
['Netflix', -17.99, 'leisure', thisMonthDate(10), 1, alexId], ['Netflix', -17.99, 'leisure', thisMonthDate(10), 1, alexId],
['Spotify Family', -16.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 // Variable this month
['Weekly Groceries Wk 1', -142.30, 'food', thisMonthDate(4), 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 2', -118.75, 'food', thisMonthDate(11), 0, samId ],
['Weekly Groceries Wk 3', -134.20, 'food', thisMonthDate(18), 0, samId ], ['Weekly Groceries - Wk 3', -134.20, 'food', thisMonthDate(18), 0, samId ],
['School Trip Payment', -25.00, 'school', thisMonthDate(3), 0, samId ], ['School Trip Payment', -25.00, 'school', thisMonthDate(3), 0, samId ],
['Birthday Gift Mum', -60.00, 'family', thisMonthDate(7), 0, alexId], ['Birthday Gift - Mum', -60.00, 'family', thisMonthDate(7), 0, alexId],
['Restaurant Date Night', -87.50, 'leisure', thisMonthDate(9), 0, alexId], ['Restaurant - Date Night', -87.50, 'leisure', thisMonthDate(9), 0, alexId],
['Fuel VW Golf', -68.00, 'transport', thisMonthDate(6), 0, alexId], ['Fuel - VW Golf', -68.00, 'transport', thisMonthDate(6), 0, alexId],
['Pharmacy', -22.40, 'health', thisMonthDate(8), 0, samId ], ['Pharmacy', -22.40, 'health', thisMonthDate(8), 0, samId ],
['Leo\'s Football Boots', -54.99, 'school', thisMonthDate(12), 0, samId ], ['Leo\'s Football Boots', -54.99, 'school', thisMonthDate(12), 0, samId ],
['Home Improvement Tools', -43.00, 'home', thisMonthDate(14), 0, alexId], ['Home Improvement - Tools', -43.00, 'home', thisMonthDate(14), 0, alexId],
['Clothing Emma', -38.50, 'clothing', thisMonthDate(16), 0, samId ], ['Clothing - Emma', -38.50, 'clothing', thisMonthDate(16), 0, samId ],
['Weekend Trip Deposit', -200.00, 'leisure', thisMonthDate(19), 0, alexId], ['Weekend Trip Deposit', -200.00, 'leisure', thisMonthDate(19), 0, alexId],
// Last month (for trend comparison) // Last month (for trend comparison)
['Alex Monthly Salary', 3850.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], ['Sam - Part-time Work', 1200.00, 'income', lastMonthDate(1), 0, alexId],
['Rent', -1450.00, 'housing', lastMonthDate(1), 0, alexId], ['Rent', -1450.00, 'housing', lastMonthDate(1), 0, alexId],
['Weekly Groceries', -489.00, 'food', lastMonthDate(10), 0, samId ], ['Weekly Groceries', -489.00, 'food', lastMonthDate(10), 0, samId ],
['Electricity Bill', -82.00, 'utilities', lastMonthDate(15), 0, alexId], ['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)); ].forEach(row => insertBudget.run(...row));
// ── Notes ──────────────────────────────────────────────────────────────────── // ── Notes ────────────────────────────────────────────────────────────────────
@@ -262,7 +262,7 @@ const insertNote = db.prepare(`
[ [
['Holiday Checklist 🌍', ['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], '#0EA5E9', 1, alexId],
['WiFi & Smart Home', ['WiFi & Smart Home',
@@ -270,23 +270,23 @@ const insertNote = db.prepare(`
'#F59E0B', 1, alexId], '#F59E0B', 1, alexId],
["Emma's School Info", ["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], '#EC4899', 1, samId],
['Leo\'s Activities', ['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], '#F97316', 1, samId],
['Emergency Numbers', ['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', '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], '#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', '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], '#6B7280', 0, alexId],
['Book Recommendations', ['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], '#8B5CF6', 0, samId],
['Garden To-Do', ['Garden To-Do',
@@ -325,7 +325,7 @@ const insertItem = db.prepare(`
['Bananas', '6', 'fruit', 0], ['Bananas', '6', 'fruit', 0],
['Blueberries', '125 g', 'fruit', 0], ['Blueberries', '125 g', 'fruit', 0],
['Lemons', '4', 'fruit', 0], ['Lemons', '4', 'fruit', 0],
['Pasta spaghetti', '500 g', 'pantry', 0], ['Pasta - spaghetti', '500 g', 'pantry', 0],
['Basmati rice', '1 kg', 'pantry', 0], ['Basmati rice', '1 kg', 'pantry', 0],
['Olive oil', '500 ml', 'pantry', 0], ['Olive oil', '500 ml', 'pantry', 0],
['Tomato passata', '2 × 500 g','pantry', 0], ['Tomato passata', '2 × 500 g','pantry', 0],
+1 -1
View File
@@ -17,7 +17,7 @@ const router = express.Router();
// -------------------------------------------------------- // --------------------------------------------------------
// Session-Store (better-sqlite3, gleiche DB-Instanz wie App) // 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). // die separat kompiliert werden müssten und die Fehlerquelle waren).
// -------------------------------------------------------- // --------------------------------------------------------
class BetterSQLiteStore extends session.Store { class BetterSQLiteStore extends session.Store {
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Modul: DB-Schema-Export für Tests * Modul: DB-Schema-Export für Tests
* Zweck: SQL-Strings aus MIGRATIONS für node:sqlite-Tests exportieren. * 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 * Abhängigkeiten: keine
*/ */
+2 -2
View File
@@ -60,7 +60,7 @@ function init() {
/** /**
* Alle Migrationen in aufsteigender Reihenfolge. * 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 = [ const MIGRATIONS = [
{ {
@@ -352,7 +352,7 @@ function currentVersion() {
* @returns {import('better-sqlite3').Database} * @returns {import('better-sqlite3').Database}
*/ */
function get() { 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; return db;
} }
+2 -2
View File
@@ -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 // HTML + JS + CSS: no-cache (Browser revalidiert via ETag/304, kein stale Content
// nach Deployment). Bei unverändertem File → 304 Not Modified ohne Übertragung. // 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)) { } else if (['.png', '.jpg', '.jpeg', '.ico', '.svg', '.webp', '.woff2', '.woff'].includes(ext)) {
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable'); // 30 Tage res.setHeader('Cache-Control', 'public, max-age=2592000, immutable'); // 30 Tage
} else { } 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'); res.setHeader('Cache-Control', 'no-cache, must-revalidate');
} }
// manifest.json: korrekter MIME-Type für PWA-Erkennung durch Chrome/Android // manifest.json: korrekter MIME-Type für PWA-Erkennung durch Chrome/Android
+1 -1
View File
@@ -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. * 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 {import('better-sqlite3').Database} database
* @param {string} month YYYY-MM * @param {string} month YYYY-MM
*/ */
+1 -1
View File
@@ -228,7 +228,7 @@ router.get('/google/callback', async (req, res) => {
await googleCalendar.handleCallback(code); 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)); googleCalendar.sync().catch((e) => console.error('[Google] Initialer Sync fehlgeschlagen:', e.message));
res.redirect('/settings?sync_ok=google'); res.redirect('/settings?sync_ok=google');
+2 -2
View File
@@ -1,6 +1,6 @@
/** /**
* Modul: Dashboard * 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 * Abhängigkeiten: express, server/db.js
*/ */
@@ -13,7 +13,7 @@ const db = require('../db');
/** /**
* GET /api/v1/dashboard * GET /api/v1/dashboard
* Liefert aggregierte Daten für alle Dashboard-Widgets. * 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. * so bricht ein fehlerhaftes Widget nicht das gesamte Dashboard.
* *
* Response: { * Response: {
+4 -4
View File
@@ -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
// -------------------------------------------------------- // --------------------------------------------------------
/** /**
+1 -1
View File
@@ -94,7 +94,7 @@ router.get('/', async (req, res) => {
// -------------------------------------------------------- // --------------------------------------------------------
// GET /api/v1/weather/icon/:code // 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). // im PWA-Standalone-Modus (CORS/CSP-Probleme auf Android Chrome).
// Erlaubte Codes: 24 alphanumerische Zeichen (z.B. "01d", "10n"). // Erlaubte Codes: 24 alphanumerische Zeichen (z.B. "01d", "10n").
// Response: PNG-Bild mit 24h-Cache // Response: PNG-Bild mit 24h-Cache
+12 -12
View File
@@ -1,15 +1,15 @@
/** /**
* Modul: Apple Calendar Sync (CalDAV) * Modul: Apple Calendar Sync (CalDAV)
* Zweck: Bidirektionaler Sync mit iCloud Calendar via CalDAV-Protokoll * 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): * Konfiguration (.env):
* APPLE_CALDAV_URL z.B. https://caldav.icloud.com * APPLE_CALDAV_URL - z.B. https://caldav.icloud.com
* APPLE_USERNAME Apple-ID E-Mail * APPLE_USERNAME - Apple-ID E-Mail
* APPLE_APP_SPECIFIC_PASSWORD App-spezifisches Passwort aus appleid.apple.com * APPLE_APP_SPECIFIC_PASSWORD - App-spezifisches Passwort aus appleid.apple.com
* *
* sync_config-Schlüssel: * sync_config-Schlüssel:
* apple_last_sync ISO-8601-Timestamp des letzten Syncs * apple_last_sync - ISO-8601-Timestamp des letzten Syncs
*/ */
'use strict'; 'use strict';
@@ -134,7 +134,7 @@ function parseICS(ics) {
const location = get('LOCATION') || null; const location = get('LOCATION') || null;
const rrule = get('RRULE') ? `RRULE:${get('RRULE')}` : 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 dtStartRaw = (() => {
const m = /^DTSTART(?:;[^:]*)?:(.*)$/im.exec(block); const m = /^DTSTART(?:;[^:]*)?:(.*)$/im.exec(block);
return m ? m[1].trim() : null; return m ? m[1].trim() : null;
@@ -148,7 +148,7 @@ function parseICS(ics) {
const dtstart = dtStartRaw ? formatICSDate(dtStartRaw, allDay) : null; const dtstart = dtStartRaw ? formatICSDate(dtStartRaw, allDay) : null;
let dtend = dtEndRaw ? formatICSDate(dtEndRaw, 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) { if (allDay && dtend) {
const d = new Date(dtend + 'T00:00:00'); const d = new Date(dtend + 'T00:00:00');
d.setDate(d.getDate() - 1); 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); base.setHours(base.getHours() + hours, base.getMinutes() + mins, base.getSeconds() + secs);
if (allDay) { 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); base.setDate(base.getDate() - 1);
return `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, '0')}-${String(base.getDate()).padStart(2, '0')}`; 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) { if (event.all_day) {
const startDate = event.start_datetime.slice(0, 10).replace(/-/g, ''); 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 endSrc = (event.end_datetime || event.start_datetime).slice(0, 10);
const endD = new Date(endSrc + 'T00:00:00'); const endD = new Date(endSrc + 'T00:00:00');
endD.setDate(endD.getDate() + 1); 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).'); 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 { createDAVClient } = await import('tsdav');
const client = await createDAVClient({ const client = await createDAVClient({
@@ -307,7 +307,7 @@ async function sync() {
// created_by: ersten existierenden User verwenden (nicht hardcoded ID 1) // 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(); const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
if (!owner) { if (!owner) {
console.warn('[Apple] Kein User in der Datenbank Sync übersprungen.'); console.warn('[Apple] Kein User in der Datenbank - Sync übersprungen.');
return; return;
} }
const createdBy = owner.id; const createdBy = owner.id;
@@ -397,7 +397,7 @@ async function sync() {
} }
cfgSet('apple_last_sync', new Date().toISOString()); 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 }; module.exports = { sync, getStatus, saveCredentials, clearCredentials, testConnection };
+10 -10
View File
@@ -4,11 +4,11 @@
* Abhängigkeiten: googleapis, server/db.js * Abhängigkeiten: googleapis, server/db.js
* *
* sync_config-Schlüssel: * sync_config-Schlüssel:
* google_access_token OAuth Access Token * google_access_token - OAuth Access Token
* google_refresh_token OAuth Refresh Token (langlebig) * google_refresh_token - OAuth Refresh Token (langlebig)
* google_token_expiry ISO-8601-Timestamp bis wann Access Token gültig ist * google_token_expiry - ISO-8601-Timestamp bis wann Access Token gültig ist
* google_sync_token Inkrementeller Sync-Token von Google (events.list) * google_sync_token - Inkrementeller Sync-Token von Google (events.list)
* google_last_sync ISO-8601-Timestamp des letzten erfolgreichen Syncs * google_last_sync - ISO-8601-Timestamp des letzten erfolgreichen Syncs
*/ */
'use strict'; 'use strict';
@@ -65,7 +65,7 @@ function loadAuthorizedClient() {
const refreshToken = cfgGet('google_refresh_token'); const refreshToken = cfgGet('google_refresh_token');
if (!accessToken || !refreshToken) { 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(); const client = createClient();
@@ -103,7 +103,7 @@ function getAuthUrl() {
/** /**
* OAuth-Callback: tauscht Code gegen Tokens, speichert in sync_config. * 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) { async function handleCallback(code) {
const client = createClient(); const client = createClient();
@@ -117,7 +117,7 @@ async function handleCallback(code) {
cfgSet('google_refresh_token', tokens.refresh_token); cfgSet('google_refresh_token', tokens.refresh_token);
if (tokens.expiry_date) cfgSet('google_token_expiry', String(tokens.expiry_date)); 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) { } catch (err) {
if (err.code === 410) { if (err.code === 410) {
// syncToken abgelaufen → vollständiger Resync // 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'); cfgDel('google_sync_token');
syncToken = null; syncToken = null;
continue; continue;
@@ -220,7 +220,7 @@ async function sync() {
} }
cfgSet('google_last_sync', new Date().toISOString()); 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 -1
View File
@@ -1,6 +1,6 @@
/** /**
* Modul: Setup-Script * 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` * Wird einmalig nach dem ersten Start ausgeführt: `node setup.js`
* Abhängigkeiten: server/db.js, bcrypt, dotenv * Abhängigkeiten: server/db.js, bcrypt, dotenv
*/ */
+1 -1
View File
@@ -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 * Zweck: Browser-absolute Pfade (/foo.js) auf Stubs umleiten, damit
* Frontend-Module im Node-Test-Kontext importierbar sind. * Frontend-Module im Node-Test-Kontext importierbar sind.
* Verwendung: node --loader ./test-browser-loader.mjs test-xxx.js * Verwendung: node --loader ./test-browser-loader.mjs test-xxx.js
+1 -1
View File
@@ -169,7 +169,7 @@ test('Kommende Termine (upcoming)', () => {
// Sortierung // Sortierung
// -------------------------------------------------------- // --------------------------------------------------------
test('Sortierung: ganztägig nach uhrzeit-basierten Terminen', () => { 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(` const events = db.prepare(`
SELECT * FROM calendar_events SELECT * FROM calendar_events
WHERE DATE(start_datetime) = '2026-03-24' WHERE DATE(start_datetime) = '2026-03-24'
+2 -2
View File
@@ -1,7 +1,7 @@
/** /**
* Modul: Datenbank-Test * Modul: Datenbank-Test
* Zweck: Schema-Migration mit node:sqlite (built-in) validieren. * 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. * Testet SQL-Korrektheit, FK-Reihenfolge, Triggers, Indizes.
* *
* Ausführen: node test-db.js * Ausführen: node test-db.js
@@ -13,7 +13,7 @@ const { DatabaseSync } = require('node:sqlite');
// -------------------------------------------------------- // --------------------------------------------------------
// Migrations-SQL direkt aus db.js extrahieren // 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'); const { MIGRATIONS_SQL } = require('./server/db-schema-test');
+1 -1
View File
@@ -251,7 +251,7 @@ test('Zutaten → Einkaufsliste übertragen (INSERT + Flag setzen)', () => {
}); });
test('Zweiter Transfer überträgt nichts (alle bereits markiert)', () => { 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 suppe = db.prepare(`SELECT id FROM meals WHERE title = 'Suppe'`).get();
const open = db.prepare(` const open = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0 SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Tests: Modal Utilities (wireBlurValidation, btnSuccess, btnError) * Tests: Modal Utilities (wireBlurValidation, btnSuccess, btnError)
* Modul: /public/components/modal.js * 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. * über ihre Parameter auf DOM-Objekte zu, daher kein DOM-Polyfill nötig.
*/ */
import { test } from 'node:test'; import { test } from 'node:test';
+1 -1
View File
@@ -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 * Zweck: Validiert CRUD, Constraints, Filterabfragen, Aggregation für alle drei Module
* Ausführen: node --experimental-sqlite test-notes-contacts-budget.js * Ausführen: node --experimental-sqlite test-notes-contacts-budget.js
*/ */
+4 -4
View File
@@ -72,21 +72,21 @@ test('Liste umbenennen', () => {
// -------------------------------------------------------- // --------------------------------------------------------
// Artikel-CRUD // 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) const r = db.prepare(`INSERT INTO shopping_items (list_id, name, quantity, category)
VALUES (?, 'Äpfel', '1 kg', 'Obst & Gemüse')`).run(listId); VALUES (?, 'Äpfel', '1 kg', 'Obst & Gemüse')`).run(listId);
itemId1 = r.lastInsertRowid; itemId1 = r.lastInsertRowid;
assert(itemId1 > 0); 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) const r = db.prepare(`INSERT INTO shopping_items (list_id, name, quantity, category)
VALUES (?, 'Milch', '1 Liter', 'Milchprodukte')`).run(listId); VALUES (?, 'Milch', '1 Liter', 'Milchprodukte')`).run(listId);
itemId2 = r.lastInsertRowid; itemId2 = r.lastInsertRowid;
assert(itemId2 > 0); 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) const r = db.prepare(`INSERT INTO shopping_items (list_id, name, category)
VALUES (?, 'Brot', 'Backwaren')`).run(listId); VALUES (?, 'Brot', 'Backwaren')`).run(listId);
itemId3 = r.lastInsertRowid; itemId3 = r.lastInsertRowid;
@@ -184,7 +184,7 @@ test('Autocomplete-Suggestions nach Prefix', () => {
assert(results[0].name === 'Joghurt', `Erwartet Joghurt, erhalten: ${results[0].name}`); 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(` const results = db.prepare(`
SELECT DISTINCT name FROM shopping_items WHERE name LIKE ? COLLATE NOCASE SELECT DISTINCT name FROM shopping_items WHERE name LIKE ? COLLATE NOCASE
`).all('XXXXXXXX%'); `).all('XXXXXXXX%');
+3 -3
View File
@@ -1,17 +1,17 @@
/** /**
* Tests: UX Utilities (stagger, vibrate) * 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 { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
// Minimales Window/Navigator-Mock für Node // Minimales Window/Navigator-Mock für Node
const { stagger, vibrate } = await (async () => { const { stagger, vibrate } = await (async () => {
// stagger braucht window.matchMedia wir mocken es // stagger braucht window.matchMedia - wir mocken es
global.window = { global.window = {
matchMedia: () => ({ matches: false }), 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', { Object.defineProperty(global, 'navigator', {
value: { vibrate: null }, value: { vibrate: null },
writable: true, writable: true,