diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md index 6adad06..54d009c 100644 --- a/.planning/research/ARCHITECTURE.md +++ b/.planning/research/ARCHITECTURE.md @@ -1,424 +1,583 @@ # Architecture Research -**Domain:** Self-hosted family calendar + list hub (Fastmail broker + MariaDB + React PWA) -**Researched:** 2026-06-03 -**Confidence:** MEDIUM (Fastmail personal-calendar ACL mechanics unconfirmed — see flags below) +**Domain:** FamilySync v1.1 — integration analysis for Operability & Polish milestone +**Researched:** 2026-06-10 +**Confidence:** HIGH (grounded in actual codebase) ## Standard Architecture ### System Overview -```mermaid -graph TD - subgraph Public["Public Internet"] - iOS["iOS PWA (Safari)"] - Android["Android PWA (Chrome)"] - end - - subgraph Tunnel["Pangolin/Newt Tunnel (no open ports)"] - direction TB - Authelia["Authelia OIDC\n(already deployed)"] - end - - subgraph Docker["Docker Compose — Unraid"] - direction TB - PWA["React PWA\n(static, served by API or nginx)"] - API["App API\n(Node/Express or Fastify)"] - - subgraph Broker["Fastmail Broker Layer"] - CalDAVClient["CalDAV Client\n(node-caldav / tsdav)"] - Cache["Calendar Cache\n(MariaDB: events + ctag)"] - Poller["Background Poller\n(cron, 5-min interval)"] - end - - subgraph Lists["List Domain"] - ListAPI["List CRUD\n(REST endpoints)"] - ListDB["lists / items tables\n(MariaDB)"] - end - - subgraph Realtime["Real-time Layer"] - WSHub["WebSocket Hub\n(ws or Socket.IO)"] - RedisPubSub["Redis Pub/Sub\n(optional, single instance OK)"] - end - - subgraph Push["Web Push"] - PushSender["VAPID Push Sender\n(web-push npm)"] - SubStore["push_subscriptions table\n(MariaDB)"] - Scheduler["Reminder Scheduler\n(node-cron)"] - end - - MariaDB[("MariaDB\n(users, calendars_cache,\nlists, push_subscriptions)")] - Redis[("Redis\n(pub/sub channels)")] - end - - subgraph Fastmail["Fastmail (external, source of truth)"] - FM_Shared["Shared Family Calendar"] - FM_Personal["Personal Calendar Collections\n(one per user)"] - end - - iOS -->|HTTPS via tunnel| Authelia - Android -->|HTTPS via tunnel| Authelia - Authelia -->|forwards authed request| API - PWA <-->|REST + WebSocket| API - - API --> CalDAVClient - API --> ListAPI - API --> WSHub - - CalDAVClient <-->|CalDAV over HTTPS\napp password auth| FM_Shared - CalDAVClient <-->|CalDAV over HTTPS\nper-user app password OR shared ACL| FM_Personal - CalDAVClient --> Cache - - Poller -->|every 5 min: PROPFIND ctag| CalDAVClient - Poller -->|on ctag change: full sync| Cache - - ListAPI --> ListDB - ListDB --> MariaDB - Cache --> MariaDB - - WSHub <-->|subscribe/publish| RedisPubSub - RedisPubSub --> Redis - - ListAPI -->|on write| RedisPubSub - PushSender --> SubStore - SubStore --> MariaDB - Scheduler -->|check upcoming events| MariaDB - Scheduler --> PushSender - ListAPI -->|on important change| PushSender +``` +┌──────────────────────────────────────────────────────────────────┐ +│ React PWA (apps/pwa/src/) │ +│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │ +│ │ EventForm.tsx│ │SettingsSheet │ │ [NEW] SetupWizard / │ │ +│ │ + reminder │ │ + Admin tab │ │ AdminSettings │ │ +│ │ selector │ │ │ │ │ │ +│ └──────┬───────┘ └──────┬───────┘ └────────────┬──────────────┘ │ +│ │ api/client.ts (typed fetch wrappers) │ │ +└─────────┼──────────────────────────────────────┬─┴───────────────┘ + │ │ + ▼ HTTP / SSE ▼ HTTP +┌──────────────────────────────────────────────────────────────────┐ +│ Hono API (apps/api/src/index.ts) │ +│ ┌──────────────┐ ┌────────────┐ ┌──────────────────────────────┐│ +│ │ routes/ │ │ routes/ │ │ [NEW] routes/admin.ts + ││ +│ │ events.ts │ │ push.ts │ │ routes/setup.ts ││ +│ │ (enqueue to │ │ │ │ (role-gated credential mgmt, ││ +│ │ outbox) │ │ │ │ first-run wizard endpoints) ││ +│ └──────┬───────┘ └────────────┘ └──────────────────────────────┘│ +│ │ │ +│ ┌──────▼──────────────────────────────────────────────────────┐ │ +│ │ broker/ │ │ +│ │ outboxWorker.ts (15s setInterval + NEW event-driven drain) │ │ +│ │ reminderScheduler.ts (1-min setInterval, MODIFIED: per- │ │ +│ │ event VALARM lead, variable window) │ │ +│ │ poller.ts (5-min setInterval, UNCHANGED) │ │ +│ │ vevent.ts [MODIFIED: buildVeventString adds VALARM] │ │ +│ │ sync.ts [MODIFIED: extract VALARM -> reminder_lead_minutes] │ │ +│ │ crypto.ts (AES-256-GCM, REUSED by admin credential writes) │ │ +│ └──────┬──────────────────────────────────────────────────────┘ │ +└─────────┼────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ Data layer (apps/api/src/db/) │ +│ schema.ts: users (+is_admin), calendars, calendarEvents │ +│ (+reminder_lead_minutes), calendarOutbox, │ +│ memberCredentials, pushSubscriptions, lists, ... │ +│ [NEW] app_config table (setup_complete flag, etc.) │ +│ │ +│ MariaDB (mariadb:11) + Redis (7-alpine, ioredis for pub/sub) │ +└──────────────────────────────────────────────────────────────────┘ ``` ### Component Responsibilities -| Component | Responsibility | Notes | -|-----------|----------------|-------| -| React PWA | All UI rendering; calendar view, list co-edit, push subscription registration | Static build; served from API container or separate nginx | -| App API | Auth middleware (OIDC token validation), REST endpoints, WebSocket upgrade, orchestrates broker + list + push | Single Node process; Fastify recommended for performance | -| CalDAV Client | Issues PROPFIND/REPORT against caldav.fastmail.com using app password(s); parses iCalendar | Use `tsdav` (TypeScript, actively maintained) or `node-ical` for parsing | -| Calendar Cache | Stores raw VEVENT blobs + ctag per calendar in MariaDB; serves as the read path for the API | Never exposed directly; always through API | -| Background Poller | Cron job running inside API process; checks ctag every 5 min, triggers full REPORT sync on change | 5-min polling is sufficient for family use; no Fastmail push webhook available | -| List CRUD | REST handlers for list/item create-read-update-delete; emits to Redis on every write | Simple; MariaDB is source of truth | -| WebSocket Hub | Maintains open connections per authenticated user; pushes Redis messages to correct connections | Keyed by user ID extracted from OIDC sub claim | -| Redis Pub/Sub | Message bus for list change events; decouples list writes from WebSocket delivery | Single-instance Redis is fine for 2-person household; no clustering needed | -| VAPID Push Sender | Calls browser push services (FCM, APNs Web Push) with encrypted payloads | `web-push` npm package; keys stored in environment, not DB | -| Reminder Scheduler | Cron job: queries events starting in next 15 min (configurable), fires push notifications | Runs inside API process; reads from calendar cache | -| MariaDB | Persistent storage: users, calendar event cache, lists, push subscriptions | Single source for everything the app owns | +| Component | File | Responsibility | v1.1 Status | +|-----------|------|----------------|-------------| +| Event form | `apps/pwa/src/components/EventForm.tsx` | Create/edit event UI | MODIFY: add reminder selector | +| Settings sheet | `apps/pwa/src/components/SettingsSheet.tsx` | Notifications toggle | MODIFY: add Admin section | +| API client | `apps/pwa/src/api/client.ts` | Typed fetch wrappers | MODIFY: admin + setup endpoints | +| Events route | `apps/api/src/routes/events.ts` | Calendar CRUD, outbox enqueue | MODIFY: pass reminder in payload, signal drain | +| Push route | `apps/api/src/routes/push.ts` | VAPID subscription management | UNCHANGED | +| VEVENT builder | `apps/api/src/broker/vevent.ts` | iCalendar string construction | MODIFY: add VALARM | +| CalDAV sync | `apps/api/src/broker/sync.ts` | Fastmail REPORT -> DB upsert | MODIFY: extract VALARM trigger | +| Outbox worker | `apps/api/src/broker/outboxWorker.ts` | CalDAV write-back drain | MODIFY: event-driven trigger subscription | +| Reminder scheduler | `apps/api/src/broker/reminderScheduler.ts` | Push reminders for events | MODIFY: variable VALARM-based lead | +| Poller | `apps/api/src/broker/poller.ts` | 5-min CalDAV sync | UNCHANGED | +| Crypto | `apps/api/src/broker/crypto.ts` | AES-256-GCM encrypt/decrypt | UNCHANGED (reused by admin) | +| DB schema | `apps/api/src/db/schema.ts` | Drizzle table definitions | MODIFY: is_admin, reminder_lead_minutes, app_config | +| Index / wiring | `apps/api/src/index.ts` | App bootstrap + worker startup | MODIFY: mount admin + setup routes | +| [NEW] Admin route | `apps/api/src/routes/admin.ts` | Role-gated credential + calendar mgmt | NEW | +| [NEW] Setup route | `apps/api/src/routes/setup.ts` | First-run wizard endpoints + validation | NEW | +| [NEW] Admin UI | `apps/pwa/src/components/AdminSettings.tsx` | Member credential UI, shared-cal picker | NEW | +| [NEW] Setup wizard | `apps/pwa/src/components/SetupWizard.tsx` | First-run guided bootstrap | NEW | +| [NEW] Outbox trigger | `apps/api/src/lib/outboxTrigger.ts` | In-process EventEmitter for drain signal | NEW | +| [NEW] CI workflow | `.gitea/workflows/ci.yml` | Lint/typecheck/test on PR | NEW | -## Recommended Project Structure +--- -``` -familysync/ -├── apps/ -│ ├── api/ # Node backend -│ │ ├── src/ -│ │ │ ├── auth/ # OIDC token validation, user upsert -│ │ │ ├── broker/ # Fastmail CalDAV client + cache sync -│ │ │ │ ├── client.ts # tsdav wrapper -│ │ │ │ ├── poller.ts # ctag polling cron -│ │ │ │ ├── sync.ts # REPORT sync → DB write -│ │ │ │ └── expand.ts # RRULE expansion + timezone normalise -│ │ │ ├── calendars/ # REST routes: GET /calendars, GET /events -│ │ │ ├── lists/ # REST routes + Redis emit -│ │ │ ├── push/ # VAPID sender, subscription routes, scheduler -│ │ │ ├── realtime/ # WebSocket server, Redis subscriber -│ │ │ ├── db/ # Knex migrations + query helpers (MariaDB) -│ │ │ └── server.ts # Fastify app bootstrap -│ │ └── Dockerfile -│ └── pwa/ # React PWA -│ ├── src/ -│ │ ├── features/ -│ │ │ ├── calendar/ # Calendar view, event form -│ │ │ └── lists/ # List view, item row, optimistic updates -│ │ ├── auth/ # OIDC redirect handling, token storage -│ │ ├── push/ # Service worker registration, push consent -│ │ └── api/ # Typed fetch client -│ ├── public/ -│ │ └── sw.js # Service worker (push + offline cache) -│ └── Dockerfile -├── db/ -│ └── migrations/ # Knex migration files (versioned) -├── docker-compose.yml -└── .env.example +## Feature Integration Analysis + +### (a) Per-Event Reminders: VALARM Authoring + Variable-Lead Scheduling + +#### Write path — what changes + +**`apps/pwa/src/components/EventForm.tsx`** — MODIFY + +Add a "Reminder" `