diff --git a/.claude/settings.json b/.claude/settings.json
index 1a86153..1f5b896 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,4 +1,18 @@
{
+ "permissions": {
+ "allow": [
+ "Bash(git *)",
+ "Bash(gh *)",
+ "Bash(npm *)",
+ "Bash(node *)",
+ "Bash(ls *)",
+ "Bash(find *)",
+ "Bash(grep *)",
+ "Bash(cat *)",
+ "Bash(jq *)",
+ "Bash(which *)"
+ ]
+ },
"hooks": {
"PostToolUse": [
{
diff --git a/BACKLOG.md b/BACKLOG.md
index 159cf9c..43f910c 100644
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -63,3 +63,26 @@ New suggestion? → [Open an issue](https://github.com/ulsklyc/oikos/issues/new?
| - | Calendar event location display with RFC 5545 backslash-escape normalization (`fmtLocation()`) | v0.23.0 |
| - | Tasks: filter defaults to `status: open`; effective due date sort; due chip shows time component | v0.23.0 |
| - | Dashboard: FAB shortcuts open new-item modal directly after navigation | v0.23.0 |
+| - | Budget: DB-backed expense categories (stable slug keys), subcategories (35 predefined + custom), CSV export with subcategory column | v0.24.0 |
+| - | i18n: all 14 non-German locales extended with budget category & subcategory keys | v0.24.0 |
+| - | Server-side log messages and API error strings translated to English — contributed by @rafaelfoster | v0.24.0 |
+| - | UX/Accessibility: skip-to-content link, modal focus fix, swipe-to-close, 44 px touch targets, unique SVG gradient IDs | v0.24.1 |
+| - | API tokens: named Bearer / X-API-Key tokens for external integrations; SHA-256-hashed, expiry, revocation, last-used tracking | v0.25.0 |
+| - | OpenAPI 3.0 specification at `/api/v1/openapi.json` | v0.25.0 |
+| - | SPA router: dynamic imports, module cache, directional transitions, per-module accent theming | v0.25.x |
+| - | innerHTML → replaceChildren / insertAdjacentHTML migration across all modules (PR #88) | v0.25.x |
+| - | Birthdays module: name, birth date, photo, notes; auto-synced to calendar (yearly recurring event) and reminders (1 day before) | v0.26.0 |
+| - | Dashboard: birthdays widget, family participants widget, budget overview widget; customisable widget order | v0.26.0 |
+| - | Settings › General: custom application name | v0.26.0 |
+| - | Birthday image upload limit: 5 MB | v0.26.5 |
+| - | Family management: family roles (Dad, Mom, Parent, Child, etc.) separate from system access role | v0.27.0 |
+| - | Profile pictures: upload own avatar (PNG/JPEG/WebP, ≤ 5 MB, auto-resized to 512 px) | v0.27.0 |
+| - | Admin: edit any family member's profile (name, role, picture) | v0.27.0 |
+| - | API: `GET /api/v1/family/members`, `PATCH /api/v1/auth/users/:id`, `PATCH /api/v1/auth/me/profile` | v0.27.0 |
+| - | Google Calendar null guard: initial OAuth sync no longer silently fails on empty item arrays | v0.27.1 |
+| - | Navigation: sidebar tooltips in icon-only breakpoint; global keyboard shortcuts (`/`, `n`, `?`, `g d/t/c/s/n`) | v0.28.0 |
+| - | PWA: offline connectivity banner | v0.28.0 |
+| - | UX: `deleteWithUndo` for birthdays; contextual onboarding hints in all empty states | v0.28.0 |
+| - | Calendar: custom event icons (102 Lucide options via visual picker); birthday events auto-assigned `cake` icon | v0.29.0 |
+| - | Calendar: extended reminder presets (2 days, 1 week, 2 weeks) + fully custom reminder (number + unit) | v0.29.0 |
+| - | Calendar: locale-aware date text inputs (MDY / DMY / YMD) in all date fields across Calendar, Tasks, Meals, Birthdays, Budget | v0.29.0 |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 408d44b..110b0eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.29.2] - 2026-04-28
+
+### Changed
+- Docs: SPEC updated with Reminders, Birthdays, and Family Management tables and module sections; Users table reflects `family_role` and `avatar_data` columns
+- Docs: README lists Reminders and Birthdays in the feature tagline and Highlights section
+- Docs: BACKLOG completed-features table brought up to date through v0.29.1
+
## [0.29.1] - 2026-04-28
### Changed
diff --git a/README.md b/README.md
index 077eb10..21117dd 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Oikos
Self-hosted family planner for small households
- Tasks · Shopping Lists · Meal Planning · Recipes · Calendar Sync · Budget · Notes · Contacts
+ Tasks · Shopping Lists · Meal Planning · Recipes · Calendar Sync · Budget · Notes · Contacts · Birthdays · Reminders
@@ -57,6 +57,12 @@
**Notes & Contacts:** Colored sticky notes with Markdown, contact directory with vCard import/export
+**Birthdays:** Personal birthday tracker with automatic annual calendar events, age calculation, profile photos (≤ 5 MB), and 1-day-before reminders — auto-synced, no manual setup needed.
+
+**Reminders:** Time-based reminders on any task or calendar event, with an in-app notification badge. Birthday reminders are created automatically.
+
+**Family Management:** Assign family roles (Dad, Mom, Child, etc.) and upload profile pictures per family member.
+
**API Tokens:** Admins can create named Bearer / X-API-Key tokens for external integrations; tokens are SHA-256-hashed at rest, support optional expiry and revocation. OpenAPI 3.0 specification available at `/api/v1/openapi.json`.
**Zero Build Step:** Pure ES modules, no bundler, no transpiler, no framework. Ships what you write.
diff --git a/docs/SPEC.md b/docs/SPEC.md
index 71fd5f1..4a1bbc4 100644
--- a/docs/SPEC.md
+++ b/docs/SPEC.md
@@ -15,7 +15,9 @@ Every table: `id INTEGER PRIMARY KEY`, `created_at TEXT`, `updated_at TEXT` (ISO
| display_name | TEXT | |
| password_hash | TEXT | bcrypt |
| avatar_color | TEXT | HEX color code |
+| avatar_data | TEXT | Base64 data URL of profile picture (nullable) |
| role | TEXT | 'admin' or 'member' |
+| family_role | TEXT | 'dad', 'mom', 'parent', 'child', 'grandparent', 'relative', 'other' (default 'other') |
### Tasks
| Column | Type | Constraint |
@@ -190,6 +192,31 @@ Stores instances of a recurring entry deleted by the user so they are not re-gen
| month | TEXT | YYYY-MM, NOT NULL |
| PRIMARY KEY | | (parent_id, month) |
+### Reminders
+
+Per-user reminders attached to tasks or calendar events.
+
+| Column | Type | Constraint |
+|--------|------|-----------|
+| entity_type | TEXT | 'task' or 'event', NOT NULL |
+| entity_id | INTEGER | FK → tasks or calendar_events, NOT NULL |
+| remind_at | TEXT | ISO 8601 datetime, NOT NULL |
+| dismissed | INTEGER | 0/1, default 0 |
+| created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL |
+
+### Birthdays
+
+Birthday records with optional profile photo and automatic calendar event + reminder.
+
+| Column | Type | Constraint |
+|--------|------|-----------|
+| name | TEXT | NOT NULL |
+| birth_date | TEXT | DATE (YYYY-MM-DD), NOT NULL |
+| notes | TEXT | nullable |
+| photo_data | TEXT | Base64 data URL (≤ 5 MB), nullable |
+| calendar_event_id | INTEGER | FK → calendar_events (SET NULL on delete), nullable |
+| created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL |
+
### API Tokens
Named Bearer / X-API-Key tokens for non-interactive external integrations. Admin-only creation and revocation. Token values are SHA-256-hashed at rest; the plaintext is shown only once after creation.
@@ -349,6 +376,8 @@ User management and app configuration. Logged-in users only.
- **Language:** System (follows `navigator.language`), German, English, Spanish, French, Italian, Swedish, Greek, Russian, Turkish, Chinese, Japanese, Arabic, Hindi, Portuguese - via `oikos-locale-picker` web component; switch without page reload
- **API Tokens (admin):** create named Bearer / X-API-Key tokens for external integrations; the full token value is shown only once immediately after creation; tokens can be revoked at any time; support optional expiry and track last-used timestamp
- **Tab navigation:** Settings is organized in seven tabs (General, Meals, Budget, Shopping, Calendar, API Tokens, Account). Sticky tab bar, active tab persists in sessionStorage, Calendar tab auto-activates after OAuth callbacks.
+- **Family management (admin):** assign a `family_role` (Dad, Mom, Parent, Child, Grandparent, Relative, Other) to each user. Displayed in the family member list and profile views.
+- **Profile picture:** users can upload a personal avatar (PNG/JPEG/WebP/GIF, ≤ 5 MB), stored as a Base64 data URL in `avatar_data`. Displayed alongside display name across the app.
- **App info:** version, license
### Budget (`/budget`)
@@ -365,6 +394,29 @@ User management and app configuration. Logged-in users only.
- CSV export includes a subcategory column and English column headers
- API: `GET /api/v1/budget/categories`, `GET /api/v1/budget/categories/:key/subcategories` (optional `?lang=` localisation), `POST /api/v1/budget/categories`, `POST /api/v1/budget/categories/:key/subcategories`
+### Birthdays (`/birthdays`)
+
+Personal birthday tracker with automatic calendar integration.
+
+- CRUD: name, birth_date (day/month/year or day/month only for age-unknown entries), notes, photo
+- Profile photo upload (PNG/JPEG/WebP/GIF, ≤ 5 MB, stored as Base64 data URL)
+- **Upcoming view:** birthdays sorted by days until next occurrence; shows age when year is known
+- **Calendar integration:** creating or updating a birthday automatically creates/updates a recurring annual all-day calendar event (title: "🎂 {Name}"); deleting a birthday removes the linked event
+- **Automatic reminder:** a birthday reminder is synced 1 day before each occurrence (auto-dismissed when the birthday passes)
+- Search filter by name
+- API: `GET /api/v1/birthdays`, `GET /api/v1/birthdays/upcoming`, `GET /api/v1/birthdays/:id`, `POST /api/v1/birthdays`, `PUT /api/v1/birthdays/:id`, `DELETE /api/v1/birthdays/:id`
+
+### Reminders (`/reminders`)
+
+Time-based reminders attached to tasks or calendar events.
+
+- One reminder per entity (upsert — creating a new reminder replaces the previous one)
+- Reminder time set via datetime picker in the task or event modal
+- **Pending reminders:** polled on page load and at a fixed interval; displayed as an in-app notification badge/toast
+- **Birthday reminders** auto-synced from the Birthdays module (1 day before each occurrence)
+- Dismissing a reminder marks it `dismissed = 1`; dismissed reminders are not shown again
+- API: `GET /api/v1/reminders/pending`, `GET /api/v1/reminders?entity_type=&entity_id=`, `POST /api/v1/reminders`, `DELETE /api/v1/reminders/:id`, `POST /api/v1/reminders/:id/dismiss`
+
---
## API Documentation
diff --git a/package-lock.json b/package-lock.json
index e4c30e7..5f08386 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "oikos",
- "version": "0.28.1",
+ "version": "0.29.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "oikos",
- "version": "0.28.1",
+ "version": "0.29.2",
"license": "MIT",
"dependencies": {
"bcrypt": "^6.0.0",
diff --git a/package.json b/package.json
index 1fff0e3..f5e15b1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "oikos",
- "version": "0.29.1",
+ "version": "0.29.2",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js",
"type": "module",