# Oikos - Product Specification Self-hosted family planner web app for a single household (2–6 people). No app store, no public access. Deployment via Docker on a private Linux server behind an Nginx reverse proxy with SSL. --- ## Data Model Every table: `id INTEGER PRIMARY KEY`, `created_at TEXT`, `updated_at TEXT` (ISO 8601). ### Users | Column | Type | Constraint | |--------|------|-----------| | username | TEXT | UNIQUE NOT NULL | | 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 | |--------|------|-----------| | title | TEXT | NOT NULL | | description | TEXT | | | category | TEXT | Household, School, Shopping, Repairs, Other | | priority | TEXT | none (default), low, medium, high, urgent | | status | TEXT | open, in_progress, done, archived | | due_date | TEXT | DATE, nullable | | due_time | TEXT | TIME, nullable | | assigned_to | INTEGER | FK → Users | | created_by | INTEGER | FK → Users, NOT NULL | | is_recurring | INTEGER | 0/1 | | recurrence_rule | TEXT | iCal RRULE | | parent_task_id | INTEGER | FK → Tasks (max 2 levels) | ### Shopping Lists | Column | Type | Constraint | |--------|------|-----------| | name | TEXT | NOT NULL (e.g. "Supermarket", "Hardware store") | ### Shopping Items | Column | Type | Constraint | |--------|------|-----------| | list_id | INTEGER | FK → Shopping Lists, NOT NULL | | name | TEXT | NOT NULL | | quantity | TEXT | e.g. "500g", "2 pieces" | | category | TEXT | FK → Shopping Categories (by name) | | is_checked | INTEGER | 0/1 | | added_from_meal | INTEGER | FK → Meals, nullable | ### Shopping Categories Custom, household-wide category list for shopping items. Replaces the old hardcoded category set. | Column | Type | Constraint | |--------|------|-----------| | id | INTEGER | PRIMARY KEY | | name | TEXT | NOT NULL | | sort_order | INTEGER | NOT NULL | | created_at | TEXT | | | updated_at | TEXT | | ### Meals | Column | Type | Constraint | |--------|------|-----------| | date | TEXT | DATE, NOT NULL | | meal_type | TEXT | breakfast, lunch, dinner, snack | | title | TEXT | NOT NULL | | notes | TEXT | | | recipe_url | TEXT | nullable, URL to recipe | | recipe_id | INTEGER | FK → Recipes (ON DELETE SET NULL), nullable | | created_by | INTEGER | FK → Users, NOT NULL | ### Recipes Reusable recipe cards that can be pre-filled into meal slots. | Column | Type | Constraint | |--------|------|-----------| | title | TEXT | NOT NULL | | notes | TEXT | | | recipe_url | TEXT | nullable | | created_by | INTEGER | FK → Users (CASCADE delete) | ### Recipe Ingredients | Column | Type | Constraint | |--------|------|-----------| | recipe_id | INTEGER | FK → Recipes (CASCADE delete), NOT NULL | | name | TEXT | NOT NULL | | quantity | TEXT | | | category | TEXT | NOT NULL (default 'Sonstiges') | ### Meal Ingredients | Column | Type | Constraint | |--------|------|-----------| | meal_id | INTEGER | FK → Meals, NOT NULL | | name | TEXT | NOT NULL | | quantity | TEXT | | | on_shopping_list | INTEGER | 0/1 | ### Calendar Events | Column | Type | Constraint | |--------|------|-----------| | title | TEXT | NOT NULL | | description | TEXT | | | start_datetime | TEXT | DATETIME, NOT NULL | | end_datetime | TEXT | DATETIME | | all_day | INTEGER | 0/1 | | location | TEXT | | | color | TEXT | HEX | | icon | TEXT | Lucide icon name, default 'calendar' | | assigned_to | INTEGER | FK → Users | | created_by | INTEGER | FK → Users, NOT NULL | | external_calendar_id | TEXT | ID from external calendar | | external_source | TEXT | local, google, apple, ics | | recurrence_rule | TEXT | iCal RRULE | | subscription_id | INTEGER | FK → ICS Subscriptions (CASCADE delete) | | user_modified | INTEGER | 0/1 — prevents sync overwrite when 1 | | calendar_ref_id | INTEGER | FK → External Calendars (ON DELETE SET NULL) | | attachment_name | TEXT | Original filename of attached file, nullable | | attachment_mime | TEXT | MIME type (e.g. image/jpeg, application/pdf), nullable | | attachment_size | INTEGER | File size in bytes, nullable | | attachment_data | TEXT | Base64 data URL of attachment (≤ 5 MB), nullable | ### External Calendars Display metadata (name, color) for synced Google/Apple calendars. Populated automatically during sync. | Column | Type | Constraint | |--------|------|-----------| | source | TEXT | 'google' or 'apple', NOT NULL | | external_id | TEXT | Calendar ID from the provider, NOT NULL | | name | TEXT | Display name from the provider, NOT NULL | | color | TEXT | Background color from the provider (HEX) | | UNIQUE | | (source, external_id) | ### Notes | Column | Type | Constraint | |--------|------|-----------| | title | TEXT | nullable | | content | TEXT | NOT NULL | | color | TEXT | HEX | | pinned | INTEGER | 0/1 | | created_by | INTEGER | FK → Users, NOT NULL | ### Contacts | Column | Type | Constraint | |--------|------|-----------| | name | TEXT | NOT NULL | | category | TEXT | Doctor, School/Nursery, Authority, Insurance, Tradesperson, Emergency, Other | | phone | TEXT | | | email | TEXT | | | address | TEXT | | | notes | TEXT | | | family_user_id | INTEGER | FK → Users (CASCADE delete), UNIQUE (one linked user per contact), nullable | ### Budget Entries | Column | Type | Constraint | |--------|------|-----------| | title | TEXT | NOT NULL | | amount | REAL | NOT NULL (positive = income, negative = expense) | | category | TEXT | FK → Budget Categories (by key), NOT NULL | | subcategory | TEXT | FK → Budget Subcategories (by key), default '' | | date | TEXT | DATE, NOT NULL | | is_recurring | INTEGER | 0/1 | | recurrence_rule | TEXT | iCal RRULE | | recurrence_parent_id | INTEGER | FK → Budget Entries (generated instance points to original) | | created_by | INTEGER | FK → Users, NOT NULL | ### Budget Categories Expense and income category list, DB-backed with stable English slug keys. Predefined set (8 expense, 5 income); users can add custom categories inline from the entry modal. | Column | Type | Constraint | |--------|------|-----------| | key | TEXT | PRIMARY KEY (stable English slug, e.g. `housing`) | | name | TEXT | NOT NULL | | type | TEXT | `'expense'` or `'income'` | | sort_order | INTEGER | NOT NULL DEFAULT 0 | | created_at | TEXT | ISO 8601 | ### Budget Subcategories Optional subcategories scoped to an expense category. Predefined set (35 entries); users can add custom subcategories inline. Income categories have no subcategories. | Column | Type | Constraint | |--------|------|-----------| | key | TEXT | PRIMARY KEY | | category_key | TEXT | FK → Budget Categories (CASCADE delete), NOT NULL | | name | TEXT | NOT NULL | | sort_order | INTEGER | NOT NULL DEFAULT 0 | | created_at | TEXT | ISO 8601 | | UNIQUE | | (category_key, name) | ### Budget Recurrence Skipped Stores instances of a recurring entry deleted by the user so they are not re-generated. | Column | Type | Constraint | |--------|------|-----------| | parent_id | INTEGER | FK → Budget Entries, NOT NULL | | 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 | | family_user_id | INTEGER | FK → Users (CASCADE delete), UNIQUE (one linked user per birthday), 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. | Column | Type | Constraint | |--------|------|-----------| | id | INTEGER | PRIMARY KEY AUTOINCREMENT | | name | TEXT | NOT NULL | | token_hash | TEXT | NOT NULL UNIQUE (SHA-256) | | token_prefix | TEXT | NOT NULL (first 8 chars, for display) | | created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL | | expires_at | TEXT | ISO 8601, nullable | | revoked_at | TEXT | ISO 8601, nullable | | last_used_at | TEXT | ISO 8601, nullable | | created_at | TEXT | ISO 8601 NOT NULL | ### ICS Subscriptions External calendar feeds subscribed by users (read-only, auto-synced). | Column | Type | Constraint | |--------|------|-----------| | name | TEXT | NOT NULL | | url | TEXT | NOT NULL (https:// or webcal://) | | color | TEXT | HEX, default #6366f1 | | shared | INTEGER | 0/1 — visible to all family members when 1 | | created_by | INTEGER | FK → Users (SET NULL on delete) | | etag | TEXT | HTTP ETag for conditional fetch | | last_modified | TEXT | HTTP Last-Modified for conditional fetch | | last_sync | TEXT | ISO timestamp of last successful sync | | created_at | TEXT | ISO timestamp | ### Family Documents Upload and manage family files with per-document access control. | Column | Type | Constraint | |--------|------|-----------| | name | TEXT | NOT NULL (display name) | | description | TEXT | nullable | | category | TEXT | medical, school, identity, insurance, finance, home, vehicle, legal, travel, pets, warranty, taxes, work, other (default) | | status | TEXT | active (default), archived | | visibility | TEXT | family (default), restricted, private | | original_name | TEXT | NOT NULL (original filename) | | mime_type | TEXT | NOT NULL | | file_size | INTEGER | NOT NULL (bytes) | | content_data | TEXT | NOT NULL (Base64 data URL) | | storage_provider | TEXT | local (default), external | | storage_key | TEXT | nullable (external storage path) | | created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL | ### Family Document Access Allowlist for `visibility = 'restricted'` documents — only listed users can see the document. | Column | Type | Constraint | |--------|------|-----------| | document_id | INTEGER | FK → Family Documents (CASCADE delete), NOT NULL | | user_id | INTEGER | FK → Users (CASCADE delete), NOT NULL | | PRIMARY KEY | | (document_id, user_id) | ### Budget Loans Instalment-based loans with per-payment tracking. Active loans show remaining balance and due months; paid-off loans are automatically closed. | Column | Type | Constraint | |--------|------|-----------| | title | TEXT | NOT NULL | | borrower | TEXT | NOT NULL | | total_amount | REAL | NOT NULL CHECK(> 0) | | installment_count | INTEGER | NOT NULL CHECK(> 0) | | start_month | TEXT | YYYY-MM, NOT NULL | | notes | TEXT | nullable | | status | TEXT | 'active' (default) or 'paid' | | created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL | ### Budget Loan Payments Individual payment records for a budget loan. Each installment number is unique per loan. | Column | Type | Constraint | |--------|------|-----------| | loan_id | INTEGER | FK → Budget Loans (CASCADE delete), NOT NULL | | installment_number | INTEGER | NOT NULL CHECK(> 0), UNIQUE per loan | | amount | REAL | NOT NULL CHECK(> 0) | | paid_date | TEXT | DATE, NOT NULL | | budget_entry_id | INTEGER | FK → Budget Entries (SET NULL on delete), nullable | | created_by | INTEGER | FK → Users (CASCADE delete), NOT NULL | ### Sync Config Key-value table for OAuth tokens and CalDAV credentials. | Column | Type | Constraint | |--------|------|-----------| | key | TEXT | PRIMARY KEY | | value | TEXT | NOT NULL | --- ## Modules ### Dashboard (`/`) Responsive grid: 1 column on mobile, 2 on tablet, 3 on desktop. **Widgets:** - Greeting: "Good [morning/afternoon/evening], [Name]" + date - Weather: OpenWeatherMap proxy, 3-day preview, refresh every 30 min, hide widget on API error - Upcoming events: next 3–5, color-coded by person - Urgent tasks: priority urgent/high + due_date ≤48h - Today's meals: meals for the current day - Pinboard preview: 2–3 pinned notes - FAB (quick actions): + Task, + Event, + Shopping list item, + Note **Widget sizes:** each widget has a configurable size using named presets (Tiny, Narrow, Standard, Large, Full) that map to `columns × rows` in the CSS grid. Sizes are persisted in user preferences and survive page reloads. Skeleton loading instead of spinners. Clicking any widget navigates to that module. ### Tasks (`/tasks`) **Views:** - List view (default): grouped by category or due date (toggleable), filter: person, priority, status - Kanban: columns Open → In Progress → Done, drag & drop - View mode persisted in localStorage; URL parameter `?view=kanban` overrides (useful for tablet kiosk setups) **Features:** - CRUD + subtasks (max 2 levels, checkbox list, progress bar) - Assignment to users (avatar color as indicator) - Priorities shown visually via color/icon - Recurring: automatically create next instance on completion - Archive: completed tasks can be archived (status = 'archived'); visible in a separate Archived filter - Inline reminder presets: offset from due date/time — 15 min, 1 h, 1 d, 2 d, 1 w, 2 w, or fully custom offset - Mobile swipe: left = done, right = edit - Badge for overdue tasks ### Shopping Lists (`/shopping`) - Multiple lists in parallel - Items: name, category, quantity, checkbox - Grouping by category (aisle logic) - Integration with meal plan: "Add ingredients to shopping list" transfers with source reference - Checked items shown with strikethrough + moved to bottom - "Clear list" = remove checked items only - Autocomplete from previous entries (local) - Mobile swipe: left = check/uncheck, right = delete; × delete button hidden on mobile (swipe takes over) ### Meal Plan (`/meals`) Weekly view (Mon–Sun), slots: breakfast / lunch / dinner / snack. - Meal: title + notes + ingredient list - "→ Shopping list" button: transfer unchecked ingredients of the week to a selected list - Week navigation forward/back - Drag & drop between days/slots - Autocomplete from meal history - **Recipe integration:** Select a saved recipe from the meal modal to auto-fill title, notes, URL, and ingredients. Scale ingredient quantities by a numeric factor. Save the current meal as a new recipe with one click. - **Customizable meal visibility:** In Settings, users can toggle which meal types (breakfast, lunch, dinner, snack) are shown in the planner. Stored as household-wide preference in `sync_config` (key: `visible_meal_types`). At least one type must remain active. ### Recipes (`/recipes`) Reusable recipe cards linked to meal slots. - CRUD: title, notes, recipe link, per-ingredient category - Duplicate existing recipes - "Add to meal plan" navigates to `/meals` with the selected recipe pre-filled in the modal - REST API: `GET/POST /api/v1/recipes`, `PUT/DELETE /api/v1/recipes/:id` with ingredient sync ### Calendar (`/calendar`) **Views:** Month (default, dot indicators), Week (hour grid), Day (timeline), Agenda (list). - CRUD: title, description, start/end, all-day, location, color, assignment - Color-coding per person - Recurring via iCal RRULE - **Google Calendar:** OAuth 2.0, Calendar API v3, two-way sync - **Apple Calendar:** CalDAV (tsdav), two-way sync - **ICS Subscriptions:** Subscribe to any public ICS/webcal URL (e.g. public holidays, sports schedules). Per-subscription color, private/shared visibility, manual "Sync now" and automatic sync on the shared interval. Edit name, color, and visibility of any subscription inline. RRULE events expanded into a rolling ±6/+12 month window. SSRF-protected (DNS pre-resolution), ETag/Last-Modified conditional fetch, 10 MB limit, 15 s timeout. User-edited events are protected from being overwritten (`user_modified`); a "Reset to original" link restores them. - **External calendar names & colors:** Google and Apple sync stores each calendar's display name and background color in the `external_calendars` table (migration v14). A colored `event-cal-label` badge appears in event popups, agenda, month, week, and day views when `cal_name` is present. - **Event location:** Event popup and dashboard display the location field with RFC 5545 backslash-escape normalization (`\n`, `\,`, `\;`, `\\`) via `fmtLocation()` in `public/utils/html.js`. - **Custom event icons:** Each event can have an icon chosen from 102 validated Lucide icons via a visual picker. Birthday events are automatically assigned the `cake` icon. Icon stored in `calendar_events.icon`. - **File attachments:** Events support a single file attachment (images, PDFs, Office documents, ≤ 5 MB). Images are displayed inline in the event popup; other files show a download link. Drag-and-drop upload supported in the event modal. Stored as Base64 in `attachment_data`. - **Overlapping events:** In week and day views, timed events that overlap in time are rendered side-by-side using a column-layout algorithm instead of stacking. - Configurable sync interval (default 15 min) - External events visually distinguishable - Conflicts: external event wins, local additions are preserved ### Notes (`/notes`) Masonry grid with colored sticky notes. - CRUD: title (optional), content, color - Pin → appears at top + on dashboard - Creator shown (avatar color) - Markdown-light: bold, italic, lists (regex-based) - Full-text search: client-side filter bar, filters instantly by title + content ### Contacts (`/contacts`) - CRUD with category filter - Phone: `tel:` link, email: `mailto:` link - Address: Maps link (Google/Apple via user agent) - Real-time search filter - vCard export: each contact downloadable as `.vcf` (`GET /api/v1/contacts/:id/vcard`) - vCard import: upload file → client-side parser (FN, TEL, EMAIL, ADR, NOTE, CATEGORIES) → create contact ### Documents (`/documents`) Upload and manage family files with per-document access control. - CRUD: name, description, category, file upload (PDF, images, text, Office documents; ≤ 5 MB) - Drag-and-drop upload in the new-document modal - **Grid / list view** toggle; view mode persisted in localStorage - **Category tags:** 14 predefined categories (medical, school, identity, insurance, finance, home, vehicle, legal, travel, pets, warranty, taxes, work, other) - **Visibility:** family (all members see it), restricted (only selected members), private (only the uploader) - **Archive / restore** — archived documents hidden from the main view, accessible via the Archive filter - **Download** — original file downloaded with its original filename - API: `GET /api/v1/documents`, `POST /api/v1/documents`, `GET /api/v1/documents/:id`, `PUT /api/v1/documents/:id`, `DELETE /api/v1/documents/:id`, `GET /api/v1/documents/:id/download` ### Login (`/login`) Unauthenticated users are redirected here. No public registration form - admin creates users via setup wizard (`setup.js`) or Settings. - Username + password form - Error display for wrong credentials - Rate limiting: 5 attempts/min/IP, 15-min lockout - After successful login: redirect to dashboard ### Settings (`/settings`) User management and app configuration. Logged-in users only. - **Profile:** change display name, avatar color, password - **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; manage ICS URL subscriptions (add, delete, sync now, set color and visibility) - **Weather:** configure OpenWeatherMap location - **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 - **Backup Management (admin):** download the current database as a file (`GET /api/v1/backup/database`) or restore from a backup file (`POST /api/v1/backup/restore`, drag-and-drop supported). Validates that the uploaded file is a valid Oikos database. A rollback copy is created automatically before restore. - **Tab navigation:** Settings is organized in nine tabs (General, Meals, Budget, Shopping, Calendar, Family, API Tokens, Backup, Account). Admin-only tabs: Family, API Tokens, Backup. 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, and set per-member phone, email, and birthday — automatically synced to Contacts and Birthdays. 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`) **Views:** - Monthly overview: income vs. expenses, balance, bar chart by category (Canvas, no library) - Transaction list: chronological, filterable - CRUD: title, amount, category, subcategory, date - Categories: DB-backed with stable English slug keys; 8 predefined expense categories, 5 income categories; users can add custom categories inline from the entry modal - Subcategories: 35 predefined subcategories across expense categories; users can add custom subcategories inline; displayed alongside category in each entry's metadata line - Recurring entries - Monthly comparison (current vs. previous month) - CSV export includes a subcategory column and English column headers - **Loans tab:** create instalment-based loans (borrower, total amount, number of instalments, start month); record individual payments; remaining balance and due months shown automatically; paid-off loans marked as closed; filter budget transactions by loan - 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` - Loans API: `GET /api/v1/budget/loans`, `POST /api/v1/budget/loans`, `GET /api/v1/budget/loans/:id`, `PUT /api/v1/budget/loans/:id`, `DELETE /api/v1/budget/loans/:id`, `GET /api/v1/budget/loans/:id/payments`, `POST /api/v1/budget/loans/:id/payments`, `DELETE /api/v1/budget/loans/:id/payments/:paymentId` ### 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 An OpenAPI 3.0 specification is served at `/api/v1/openapi.json` and `/openapi.json`. Append `?download=1` to download as a file. The spec covers all authenticated endpoints and can be imported into any OpenAPI-compatible client (Insomnia, Postman, etc.). Authentication options for external integrations: - **Session cookie:** standard browser session after login - **Bearer token:** `Authorization: Bearer ` — tokens created via Settings → API Tokens (admin only) - **X-API-Key header:** `X-API-Key: ` — alternative header accepted alongside Bearer --- ## Design System ### Colors (CSS Custom Properties) Source of truth: `public/styles/tokens.css`. Key values (as of v0.20.39): **Palette rationale:** Warm-tinted neutral scale (`#FAFAF8 → #121211`) anchored by an **Indigo-600 primary** (`#4F46E5`) that harmonises with the Calendar module violet and the secondary accent. Module colors are semantically separated from severity colors — no hue shared without explicit documentation in `tokens.css`. ```css :root { /* Neutral canvas — warm linen/unbleached-paper atmosphere */ --color-bg: #F5F4F1; /* neutral-100 */ --color-surface: #FFFFFF; --color-border: #E8E7E2; /* neutral-200 */ --color-text-primary: #1C1C1A; /* neutral-900, 14.7:1 on bg */ --color-text-secondary: #6C6B67; /* neutral-600, 5.0:1 on white */ --color-text-tertiary: #6A6964; /* 4.61:1 on bg */ /* Primary accent — Indigo (warm bias, harmonises with Calendar/violet) */ --color-accent: #4F46E5; /* Indigo-600, 4.93:1 on white (AA) */ --color-accent-hover: #4338CA; /* Indigo-700, 7.04:1 (AAA) */ --color-accent-active: #3730A3; /* Indigo-800 */ --color-accent-deep: #2E2D82; /* deep Indigo for gradients/weather */ --color-accent-secondary: #7C5CFC; /* violet — logo gradient, same Indigo family */ --color-accent-light: #EEF2FF; /* Indigo-50 */ --color-accent-subtle: #E0E7FF; /* Indigo-100 */ --color-btn-primary: #4338CA; /* Indigo-700, 7.04:1 on white (AAA) */ --color-btn-primary-hover:#3730A3; /* Severity — hue-separated from module colors */ --color-success: #15803D; /* 4.54:1 */ --color-warning: #A15C0A; /* 5.23:1 — Amber, distinct from --module-meals */ --color-danger: #B91C1C; /* Red-700, 6.90:1 (AAA) */ --color-info: #0969DA; /* 4.64:1 */ /* Module accents — domain-specific, not interchangeable with severity */ --module-dashboard: #4F46E5; /* Indigo — follows primary accent */ --module-tasks: #15803D; /* Green — intentional share with --color-success */ --module-calendar: #8250DF; /* Violet */ --module-meals: #C2410C; /* Orange-700 */ --module-shopping: #DB2777; /* Pink-600 — distinct from Meals/Warning */ --module-notes: #CA8A04; /* Gold, 4.08:1 — icons/large-text only */ --module-contacts: #0969DA; /* Blue — distinct from Indigo primary */ --module-budget: #0F766E; /* Teal-700, 5.11:1 */ --module-reminders: #0E7490; /* Cyan-700, WCAG AA */ --module-settings: #6E7781; /* Neutral grey */ /* Priority */ --color-priority-medium: #A16207; /* Amber-700, 6.3:1 — distinct from Warning+Meals */ --color-priority-high: #C2410C; /* = --module-meals (documented share: "hot") */ --color-priority-urgent: #B91C1C; /* = --color-danger (documented share: "destructive") */ /* Glass layer tokens */ --glass-bg: rgba(255,255,255,0.72); --glass-border: rgba(255,255,255,0.55); --blur-2xs: blur(2px); --blur-md: 16px; --radius-glass-button: 9999px; /* capsule */ --ease-glass: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring */ /* Glass Vibrancy tokens (Phase 4) */ --glass-bg-card: rgba(255,255,255,0.52); --glass-bg-card-hover: rgba(255,255,255,0.65); --glass-bg-input: rgba(255,255,255,0.48); --glass-bg-toolbar: rgba(255,255,255,0.58); --glass-tint-strength: 6%; /* Glass inset specular highlights */ --glass-inset-soft: inset 0 1px 0 rgba(255,255,255,0.18); --glass-inset-base: inset 0 1px 0 rgba(255,255,255,0.20); --glass-inset-medium: inset 0 1px 0 rgba(255,255,255,0.22); --glass-inset-elevated: inset 0 1px 0 rgba(255,255,255,0.28); --glass-inset-strong: inset 0 1px 0 rgba(255,255,255,0.32); } /* Dark mode — Hue preserved (Indigo-400), only Lightness/Saturation adjusted. Private --_name tokens prevent duplication between @media and [data-theme]. */ @media (prefers-color-scheme: dark) { :root { --color-bg: #1A1A18; /* deep warm */ --color-surface: #222220; --color-border: #2C2C2A; --color-text-primary: #F5F4F1; --color-text-secondary: #AEADB0; --color-accent: #818CF8; /* Indigo-400, 6.8:1 on dark surface */ --color-accent-hover: #6366F1; /* Indigo-500 */ --color-accent-active: #4F46E5; /* Indigo-600 — mirrors light primary */ --color-accent-light: #2E2D5B; --color-accent-subtle: #252255; --color-btn-primary: #6366F1; /* 5.5:1 */ --color-btn-primary-hover: #4F46E5; --module-dashboard: #818CF8; --module-meals: #FB923C; /* Orange-400 */ --module-shopping: #F472B6; /* Pink-400 — mirrors light entanglement */ --module-budget: #2DD4BF; /* Teal-400 */ --module-reminders: #22D3EE; /* Cyan-400 */ --meal-dinner: #818CF8; --glass-bg: rgba(28,28,26,0.75); --glass-border: rgba(255,255,255,0.12); --glass-bg-card: rgba(38,38,36,0.50); --glass-tint-strength: 8%; } } ``` ### Typography - System font stack, headings 600–700 - Body: 16px mobile, 15px desktop, line-height 1.5 - Caption: 13px, `var(--color-text-secondary)` ### Glass Layer (`public/styles/glass.css`) Additive CSS file loaded globally after `layout.css`. Implements a Liquid Glass design language inspired by Apple's iOS 26 Liquid Glass, adapted for CSS/web: **Phase 1-3 (Shell + Components + Polish):** - **Translucent surfaces:** `backdrop-filter: blur()` on bottom nav, sidebar, modal overlay, cards on hover. All blur effects are inside `@supports (backdrop-filter: blur(1px))` for progressive enhancement. - **Glass tokens:** Section 16 of `tokens.css` defines `--glass-bg*`, `--glass-border*`, `--blur-2xs` through `--blur-xl`, `--opacity-glass-*`, `--glass-highlight*`, `--glass-shadow-sm/md/lg`, `--radius-glass-card/inner/chip/button`, `--ease-glass`, `--transition-glass`. Full dark mode overrides. - **Capsule shapes:** Buttons, FAB, and search inputs use `--radius-glass-button` (pill shape). - **Spring animations:** Modal entrance (`glass-modal-scale-in` / `glass-sheet-in`), page transitions, and list stagger all use `cubic-bezier(0.34, 1.56, 0.64, 1)` spring easing. - **FAB attention pulse:** `fab-ring-pulse` keyframe expands a ring around the FAB to signal readiness. - **Nav auto-hide:** Bottom bar hides on scroll-down, reappears on scroll-up (mobile only, < 1024px, 4 px hysteresis). CSS: `.nav-bottom--hidden { transform: translateY(calc(100% + var(--safe-area-inset-bottom))); }`. JS: `initNavHideOnScroll()` in `router.js`. **Phase 4 (Vibrancy + Tint):** - **Deeper glass penetration:** Dashboard widgets, task cards, note items, meal slots, form inputs, toolbars, group toggles, and FAB speed-dial actions all use semi-transparent glass backgrounds (`--glass-bg-card`, 52% opacity) with `backdrop-filter: blur() saturate()` so underlying content shines through. - **Module tint:** Each glass surface receives a subtle accent color gradient overlay via `::after` pseudo-element using `color-mix(in srgb, var(--module-accent) var(--glass-tint-strength), transparent)`. Strength is 6% in light mode, 8% in dark mode. - **App vibrancy background:** `app-content` uses a radial gradient with the active module accent at 3% opacity to provide an ambient color base that glass elements refract. - **Load-order safety:** All Phase 4 glass selectors use parent-scoped specificity (`.dashboard .widget`, `.tasks-page .task-card`, `.meals-page .meal-slot`) to prevent override by on-demand page CSS that loads after `glass.css`. **Accessibility:** `prefers-reduced-transparency`, `prefers-reduced-motion`, and `prefers-contrast: more` blocks deactivate blur/animation and restore solid fallbacks across all phases. ### Components - **Cards:** `var(--color-surface)` base, glass vibrancy via `var(--glass-bg-card)` + `backdrop-filter: blur(8px) saturate(180%)` when supported. `var(--radius-md)`, `var(--shadow-sm)`. Module tint overlay via `::after`. Consistent padding `var(--space-4)` (16px) across all modules. - **Buttons:** Primary = accent + white. Secondary = outline. Min-height 44px. Capsule shape via `--radius-glass-button`. 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. Search inputs use `--radius-glass-button` and `--glass-border-subtle`. `[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. Specular inner highlight + attention ring pulse. Hidden when the virtual keyboard is open (`visualViewport.resize`, threshold 75% of window height). - **Module accent colors:** `--module-accent` is applied on three visual layers - (1) active nav tab (bottom bar + sidebar stripe), (2) toolbar `border-top: 3px`, (3) cards/rows `border-left: 3px`. The active accent is written to `--active-module-accent` on `:root` on every navigation change. Falls back to `--color-accent` for pages without a module context. - **Navigation:** Bottom tab bar on mobile (Dashboard, Tasks, Calendar, Meals, More), auto-hides on scroll-down. Sidebar on desktop. Both use glass blur surface. - **Transitions:** Directional slide-X animation on page change (forward = from right, back = from left, 200ms) with spring easing. Respects `prefers-reduced-motion`. - **Empty states:** Consistent `.empty-state` class across all modules (icon + title + description, centered). Compact variant `.empty-state--compact` for meal slots. - **Modals:** Centered panel on desktop with glass overlay. On mobile (< 768px) bottom sheet - spring slide-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 spring fade-in on load (`stagger()` from `public/utils/ux.js`) - max 5 elements staggered (30ms gap), rest appear immediately. - **Vibration:** `vibrate()` from `public/utils/ux.js` - short pulses for light actions (10-40ms), pattern `[30, 50, 30]` for destructive actions (delete). Respects `prefers-reduced-motion`. - **PWA install prompt:** Appears only after 2 user interactions. Dismiss window 7 days; interaction counter resets after dismiss. - **PWA offline fallback:** Service worker serves `/offline.html` when the network is unreachable and `index.html` is not cached. Includes a reload button. ### Breakpoints - Mobile: < 768px (1 column, bottom nav) - Tablet: 768–1024px (2 columns, bottom nav) - Desktop: > 1024px (sidebar + content) --- ## Internationalization (i18n) All UI strings are managed via `public/i18n.js`. No hardcoded text in JS files outside of locale files. ### Architecture - **Module:** `public/i18n.js` - exports: `initI18n()`, `setLocale()`, `t(key, params?)`, `getLocale()`, `getSupportedLocales()`, `formatDate(date)`, `formatTime(date)` - **Locale files:** `public/locales/de.json` (reference), `public/locales/en.json`, `public/locales/es.json`, `public/locales/fr.json`, `public/locales/it.json`, `public/locales/sv.json`, `public/locales/el.json`, `public/locales/ru.json`, `public/locales/tr.json`, `public/locales/zh.json`, `public/locales/ja.json`, `public/locales/ar.json`, `public/locales/hi.json`, `public/locales/pt.json` - structure: `{ "module.camelCaseKey": "Value" }` - **Variables:** `{{variable}}` syntax in translation strings, e.g. `t('tasks.assignedTo', { name: 'Anna' })` - **Fallback chain:** active locale → German (`de`) → key itself - **Date format:** `Intl.DateTimeFormat` with current locale - use `formatDate()` and `formatTime()` from `i18n.js` ### Language Detection 1. `localStorage` entry `oikos-locale` (manual selection) 2. `navigator.languages[0]` (browser language) 3. Fallback: `en` ### Supported Languages | Code | Language | Status | |------|----------|--------| | `de` | German | Reference locale (all keys defined here) | | `en` | English | Full translation | | `es` | Spanish | Full translation | | `fr` | French | Full translation (added v0.16.3) | | `it` | Italian | Full translation (added v0.5.8) | | `sv` | Swedish | Full translation (added v0.11.3) | | `el` | Greek | Full translation (added v0.16.3) | | `ru` | Russian | Full translation (added v0.16.3) | | `tr` | Turkish | Full translation (added v0.16.3) | | `zh` | Chinese (Simplified) | Full translation (added v0.16.3) | | `ja` | Japanese | Full translation (added v0.19.0) | | `ar` | Arabic | Full translation (added v0.19.0) | | `hi` | Hindi | Full translation (added v0.19.0) | | `pt` | Portuguese | Full translation (added v0.19.0) | ### Adding a New Language 1. Create `public/locales/xx.json` (copy of `de.json`, translate) 2. Add `'xx'` to `SUPPORTED_LOCALES` in `public/i18n.js` 3. Add label in `oikos-locale-picker` (`LOCALE_LABELS['xx'] = 'Name'`) ### 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.