- New optional recipe_url field in the meal modal (below Notes)
- Link icon appears on meal cards when a URL is set, opens in new tab
- DB migration v6: ALTER TABLE meals ADD COLUMN recipe_url TEXT
- API: recipe_url supported in POST /meals and PUT /meals/:id
- i18n: new keys recipeUrlLabel, recipeUrlPlaceholder, openRecipe (de, en, sv, it)
- New DB table shopping_categories (migration v5) seeds 9 default
categories with Lucide icons and sort_order
- Backend CRUD routes: GET/POST/PUT/DELETE /shopping/categories
plus PATCH /shopping/categories/reorder
- Category validation now uses DB instead of hardcoded constant;
items of deleted category are moved to the next available one
- Frontend shopping page loads categories from API, dropdown and
grouping reflect custom order dynamically
- Settings -> Shopping section: list categories with up/down buttons,
click-to-rename, delete with confirmation; add new categories inline
- i18n keys added in de/en/sv/it
Add household-wide currency preference for the budget section.
Users can select from 13 currencies (EUR, USD, GBP, SEK, NOK, DKK,
CHF, PLN, CZK, HUF, JPY, AUD, CAD) in Settings → Budget.
- preferences API (GET/PUT) now includes currency field
- budget page loads currency from preferences on render
- formatAmount() uses locale-aware Intl.NumberFormat with chosen currency
- settings page gains a Budget section with a currency select
- all three locales (de, en, it) updated with new i18n keys
The dashboard meal widget was showing all meal types regardless of the
household meal visibility settings configured in Settings > Meal Plan.
Root cause: the todayMeals SQL query in dashboard.js did not read
visible_meal_types from sync_config. The Meals page applied this filter
client-side, but the dashboard API returned unfiltered data.
Fix: read visible_meal_types from sync_config before the query and inject
the active types as IN (?) placeholders. Falls back to all four types when
no preference is stored.
Users can now toggle which meal types (breakfast, lunch, dinner, snack)
are displayed in the meal planner via a new Settings section. Preference
is stored household-wide in sync_config and applied as a filter on the
meals page. Includes preferences API, i18n (DE/EN/IT), and Settings UI.
New tasks default to "none" priority instead of "medium". Tasks with no
priority hide the badge in list and dashboard views, reducing visual noise
for routine items. Includes DB migration v4 and i18n keys (de, en, it).
Closes#15
Show shopping lists with open items directly on the dashboard.
Each list displays a progress bar, the first few unchecked items,
and a "+N more" overflow indicator. Widget only appears when there
are lists with open items.
Backend: new shoppingLists query in /api/v1/dashboard (up to 3 lists,
6 open items each). Frontend: renderShoppingLists() widget following
existing widget pattern. CSS: compact list/progress/item styles.
i18n: shoppingMore key added to de/en/it.
Requested in discussion #9
App refuses to start without SESSION_SECRET regardless of NODE_ENV.
Removes risk of accidental insecure deployment when NODE_ENV is not
explicitly set to production.
- Translate German error/warn messages in auth.js to English
- Add CODE_OF_CONDUCT.md (Contributor Covenant v2.1)
- Remove docs/claude-md-migration.md (internal migration artifact)
- Clarify README first-login instruction with credential hint
Convert all server/, test, and setup files from require()/module.exports
to import/export syntax. Activate ESM globally via "type": "module" in
package.json and load dotenv via --import dotenv/config in npm scripts.
Add server/logger.js - zero-dependency, level-based logger that outputs
JSON in production and human-readable format in development. Controlled
via LOG_LEVEL env var (debug/info/warn/error, default: info).
Replaces all 100 console.log/warn/error calls in 14 server files.
Instead of a static 'dev-only-secret-not-for-production' fallback,
generate a random one-time secret in development. Warns that sessions
won't survive restarts. Production guard unchanged.
The `date` import from validate.js shadowed the `date` field from
req.body, so SQLite received a function reference instead of a string
when updating a budget entry - causing a TypeError.
Fix by aliasing the import to `validateDate` and adding `date` to
the req.body destructuring.
Closes#8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix stored XSS in tasks (titles/subtasks) and settings (member list)
by applying escHtml(). Harden trust proxy to loopback default, add
OAuth state parameter for Google Calendar CSRF protection, sanitize
CSV export against formula injection, invalidate sessions on user
deletion, restrict usernames to alphanumeric chars, and require admin
role for calendar sync triggers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Root causes:
1. parseRRule did not strip the "RRULE:" prefix stored by the ICS parser,
causing all recurrence rules from CalDAV sync to silently fail parsing
2. YEARLY frequency (used by birthday events) was not supported
3. expandRecurringEvents filtered instances only by start date, missing
multi-day events that start before the view window but span into it
4. All-day recurring instances got datetime end values instead of date-only
Fixes#5 (follow-up from @tschig)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All-day events showed on the correct day plus the next day because ICS
DTEND for VALUE=DATE is exclusive (RFC 5545) but was treated as inclusive.
Multi-day events using DURATION instead of DTEND were missing entirely.
Birthday calendars were explicitly filtered out during Apple Calendar sync.
Closes#5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Apple Calendar sync hardcoded created_by=1 which fails when no user
with ID 1 exists, causing every single event import to fail silently.
Now dynamically resolves the first available user. Also syncs all
calendars instead of only the first one, adds the missing cfgDel helper,
and gracefully skips unreachable calendars.
Fixes#4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hex-encoded encryption key (x'...') is not valid as a bare PRAGMA
value in better-sqlite3. Wrapping it in double quotes produces valid
SQLCipher PRAGMA syntax.
Fixes#3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SQLCipher PRAGMA key interpolation (hex-encode key to prevent crash on single quotes)
- Enforce min password length (8 chars) on admin user creation
- Add length bounds on username/display_name and login inputs
- Invalidate other sessions on password change
- Multi-stage Docker build (exclude build tools from runtime)
- Exclude docs/ from Docker image
- Consolidate dotenv.config() to single entry point
- Document flat family authorization model in SECURITY.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cookies were sent without Secure flag outside of production (NODE_ENV check).
New logic: secure=true by default; set SESSION_SECURE=false in .env to
allow HTTP explicitly (local dev without reverse proxy). Affects session
cookie, CSRF cookie in login handler, and CSRF middleware.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rate-limit SPA fallback route (missing rate limiting on fs access)
- Add csrfMiddleware to all state-changing auth routes (logout, create
user, change password, delete user) — previously bypassed global CSRF
middleware due to router registration order
- Fix incomplete vCard escaping: escape backslashes before other special
characters to prevent injection via contact fields
- Restrict CI GITHUB_TOKEN to contents: read (least privilege)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
External image requests to openweathermap.org fail silently in Chrome
Android PWA standalone mode. Icons are now proxied via
GET /api/v1/weather/icon/:code, making them same-origin — cacheable by
the service worker and free of CORS/CSP issues.
Tightened CSP: removed openweathermap.org from imgSrc (no longer needed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icons were cached with immutable/30-day headers, so Chrome Android kept
serving the old placeholder even after new icons were deployed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add id field and display_override to manifest.json for reliable
Chrome Android PWA recognition
- Serve manifest.json with application/manifest+json MIME type
- Add /i18n.js and locale files to SW app shell cache (were missing)
- Bump SW cache version to v21
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin can now enter CalDAV URL, Apple-ID and app-specific password
directly in Settings; credentials are tested live before saving and
stored in sync_config (take precedence over .env); disconnect clears
DB-stored credentials without server restart. Auto-sync interval
(15 min, configurable via SYNC_INTERVAL_MINUTES) was already in place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expandRecurringEvents() iterates from the event's original start date,
generating all occurrences within the requested window using the existing
nextOccurrence() service (max 1000 iterations). The SQL query is extended
to also fetch recurring events that started before the window. Event
duration is preserved across instances. Virtual instances carry
is_recurring_instance=1 and are shown with a repeat icon in the agenda
view. /upcoming expands across a 90-day forward window.
Closes BL-01.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds schema migration v3 (recurrence_parent_id column + budget_recurrence_skipped
table). On every GET /api/v1/budget, the server checks all recurring originals
(is_recurring=1, no parent) and creates missing instances for the requested month
using the same day-of-month (clamped to the last day). Deleted instances are
recorded in budget_recurrence_skipped so they are not recreated on the next visit.
Generated instances are shown with a ↩ indicator in the transaction list.
Closes BL-05.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Aufgaben-Widget zeigte nur high/urgent Tasks mit Fälligkeit ≤48h,
Pinnwand-Widget nur explizit gepinnte Notizen. Neue Einträge waren
dadurch im Dashboard unsichtbar.
- Aufgaben: alle offenen Tasks (sortiert nach Priorität), Limit 5
- Notizen: neueste 3 (gepinnte zuerst, dann nach Aktualisierung)
- Greeting-Chip zählt weiterhin nur high/urgent Tasks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Dark Mode: .form-input hatte kein Styling, Browser-Default führte zu
weißem Text auf weißem Hintergrund. Jetzt mit .input zusammengefasst.
2. Essensplan: DATE_RE fehlte im Import (ReferenceError), db.transaction()
wurde doppelt aufgerufen (3 Stellen).
3. Dashboard: Router-Guard verhinderte Re-Render bei Rücknavigation,
Widgets zeigten veraltete Daten.
4. Einkaufsliste: Mengenfeld hatte abweichende Hintergrundfarbe und
überdimensionierte min-height.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auf Desktop wird das Wetter-Widget über allen anderen Widgets platziert
mit horizontalem Layout (aktuelles Wetter links, Vorhersage rechts).
Vorhersagezeitraum skaliert mit Bildschirmbreite: 3 Tage (Mobil),
4 Tage (Tablet), 5 Tage (Desktop/Wide).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CSP: SHA-256-Hash für Theme-Detection Inline-Script hinzugefügt
- docker-compose: SESSION_SECURE=false, damit HSTS und
upgrade-insecure-requests bei direktem HTTP-Zugriff deaktiviert sind
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dark Mode: Manueller Theme-Switch (System/Hell/Dunkel) in Einstellungen
mit localStorage-Persistenz und Flash-Prevention via data-theme Attribut.
RRULE UI: Wiederholungs-Formular in Aufgaben- und Kalender-Modals mit
Frequenz (Täglich/Wöchentlich/Monatlich), Intervall, Wochentag-Auswahl
und optionalem Enddatum. Backend-Routen für is_recurring/recurrence_rule
in POST/PUT erweitert. Repeat-Icon auf wiederkehrenden Einträgen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- helmet: upgrade-insecure-requests und HSTS nur bei SESSION_SECURE=true
- Service Worker Cache-Version auf v10 hochgezählt
- Debug-Code aus router.js entfernt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- connect-sqlite3 durch eigenen BetterSQLiteStore ersetzt (sessions-Tabelle
in der bestehenden DB, keine native Kompilierung nötig)
- db.init() vor require('./auth') gezogen damit BetterSQLiteStore-Konstruktor
db.get() erfolgreich aufrufen kann
- router.js: App-Shell und pageWrapper vor module.render() in DOM einfügen
damit document.getElementById() in Seiten-Modulen funktioniert
- Seiten-Module (meals, notes, contacts, calendar, budget): _container-Referenz
eingeführt, alle document.getElementById() auf _container.querySelector() bzw.
document.querySelector() für body-Elemente umgestellt
- login.js: User-Objekt nach erfolgreichem Login an navigate() übergeben
damit auth.me()-Roundtrip entfällt
- calendar.js: /users → /auth/users korrigiert (404-Fix)
- SW-Cache v8 (erzwingt Reload aller gecachten Seiten-Module)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lucide.min.js v0.469.0 lokal gecacht (kein unpkg-Request mehr)
- Source-Map-Referenz aus Bundle entfernt (behebt NetworkError in DevTools)
- unpkg.com aus CSP entfernt (nicht mehr benötigt)
- preconnect zu unpkg.com aus index.html entfernt
- lucide.min.js zum SW-App-Shell-Cache hinzugefügt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- router.js: navigateTo() existierte nicht → ReferenceError nach fehlgeschlagenem
auth.me(). Ersetzt durch navigate() + currentPath-Reset damit die Weiterleitung
nicht durch den currentPath-Guard geblockt wird.
- csrf.js: SESSION_SECURE=false wurde nicht berücksichtigt → CSRF-Cookie war über
HTTP nicht für JS lesbar → alle schreibenden API-Calls nach Login scheiterten mit 403.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SESSION_SECURE=false in .env deaktiviert das Secure-Flag für Session-
und CSRF-Cookie. Notwendig wenn die App direkt per HTTP erreichbar ist
(kein Nginx/HTTPS davor). Standard bleibt secure=true in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LICENSE: MIT-Lizenz (bereits vorhanden, korrekt)
- package.json: "license": "MIT" ergänzt
- README.md: Lizenz-Badge auf MIT aktualisiert, Lizenz-Sektion angepasst
- .gitignore: .claude/ und *.txt hinzugefügt (verhindert versehentliches
Committen von Claude Code Einstellungen und Token-Textdateien)
- server/auth.js: Fail-Fast in Produktion wenn SESSION_SECRET fehlt;
Fallback-String auf 'dev-only-secret-not-for-production' umbenannt
(klarere Intention, kein bekannter Produktions-Wert im Public Repo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>