style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
+50
-49
@@ -1,4 +1,5 @@
|
||||
<!-- generated-by: gsd-doc-writer -->
|
||||
|
||||
# FamilySync Architecture
|
||||
|
||||
FamilySync is a self-hosted family organization hub — a unified, color-coded calendar and shared collaborative lists — delivered as a React PWA. The system is a two-container Docker Compose stack (API + MariaDB) running on Unraid, exposed through a Pangolin/Newt tunnel with Authelia providing OIDC authentication.
|
||||
@@ -87,34 +88,34 @@ familysync/
|
||||
|
||||
### Directory Rationale
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `apps/api/src/routes/` | One file per resource — `events.ts`, `lists.ts`, `me.ts`, `push.ts`, `sse.ts`, `health.ts` |
|
||||
| Directory | Purpose |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `apps/api/src/routes/` | One file per resource — `events.ts`, `lists.ts`, `me.ts`, `push.ts`, `sse.ts`, `health.ts` |
|
||||
| `apps/api/src/broker/` | All CalDAV I/O: `client.ts` (tsdav factory), `sync.ts` (REPORT→DB), `poller.ts` (5-min ctag check), `outboxWorker.ts` (async write-back), `expand.ts` (RRULE expansion), `write.ts` (PUT/DELETE), `vevent.ts` (ICS builder), `crypto.ts` (AES-256-GCM for app passwords) |
|
||||
| `apps/api/src/auth/` | `middleware.ts` (re-exports `@hono/oidc-auth`), `devBypass.ts` (DEV_AUTH_BYPASS inject), `persistSessionCookie.ts` (session lifetime extension), `user.ts` (upsert on first OIDC login) |
|
||||
| `apps/api/src/db/` | `schema.ts` (Drizzle `mysqlTable` definitions), `client.ts` (mysql2 pool), `migrations/` (drizzle-kit output) |
|
||||
| `apps/api/src/lib/` | Stateless helpers: `listEmitter.ts` (EventEmitter fan-out), `listChangeDispatcher.ts`, `eventChangeDispatcher.ts`, `pushDispatcher.ts` (VAPID send), `pushCoalescer.ts`, `listAccess.ts`, `rank.ts` (fractional indexing) |
|
||||
| `apps/pwa/src/api/` | Thin typed fetch wrappers — `client.ts` (events, me, sync-status), `listsClient.ts` (lists and items) |
|
||||
| `apps/pwa/src/store/` | `calendarStore.ts` and `listsStore.ts` — Zustand UI-only state (no server data) |
|
||||
| `apps/pwa/src/hooks/` | `useListSSE.ts` (bounded-backoff EventSource), `usePushSubscription.ts` (VAPID subscribe) |
|
||||
| `apps/api/src/auth/` | `middleware.ts` (re-exports `@hono/oidc-auth`), `devBypass.ts` (DEV_AUTH_BYPASS inject), `persistSessionCookie.ts` (session lifetime extension), `user.ts` (upsert on first OIDC login) |
|
||||
| `apps/api/src/db/` | `schema.ts` (Drizzle `mysqlTable` definitions), `client.ts` (mysql2 pool), `migrations/` (drizzle-kit output) |
|
||||
| `apps/api/src/lib/` | Stateless helpers: `listEmitter.ts` (EventEmitter fan-out), `listChangeDispatcher.ts`, `eventChangeDispatcher.ts`, `pushDispatcher.ts` (VAPID send), `pushCoalescer.ts`, `listAccess.ts`, `rank.ts` (fractional indexing) |
|
||||
| `apps/pwa/src/api/` | Thin typed fetch wrappers — `client.ts` (events, me, sync-status), `listsClient.ts` (lists and items) |
|
||||
| `apps/pwa/src/store/` | `calendarStore.ts` and `listsStore.ts` — Zustand UI-only state (no server data) |
|
||||
| `apps/pwa/src/hooks/` | `useListSSE.ts` (bounded-backoff EventSource), `usePushSubscription.ts` (VAPID subscribe) |
|
||||
|
||||
---
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
| Abstraction | File | Description |
|
||||
|-------------|------|-------------|
|
||||
| `app` (Hono) | `apps/api/src/index.ts` | Root Hono app; mounts all routes and serves the PWA static build |
|
||||
| Drizzle schema | `apps/api/src/db/schema.ts` | Single source of truth for all table definitions (`users`, `memberCredentials`, `calendars`, `calendarEvents`, `calendarOutbox`, `lists`, `listShares`, `listItems`, `pushSubscriptions`) |
|
||||
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
||||
| `expandOccurrences` | `apps/api/src/broker/expand.ts` | Server-side RRULE expansion using `ical.js` + `rrule`; never runs in the browser |
|
||||
| `CalendarOccurrence` | `apps/api/src/broker/expand.ts` | Wire type for a single concrete event occurrence; mirrored in the PWA's `api/client.ts` |
|
||||
| `calendarOutbox` table | `apps/api/src/db/schema.ts` | Transactional outbox pattern — CalDAV writes are enqueued here and drained asynchronously |
|
||||
| `runOutboxDrain` | `apps/api/src/broker/outboxWorker.ts` | Drains pending outbox rows every 15s; handles retry backoff, 412 conflict, dead-lettering, and edit-as-move ordering |
|
||||
| `publishListEvent` / `subscribeListEvents` | `apps/api/src/lib/listEmitter.ts` | In-process EventEmitter fan-out keyed per list; SSE route subscribes on open and unsubscribes on disconnect |
|
||||
| `dispatchPush` | `apps/api/src/lib/pushDispatcher.ts` | Centralised VAPID-signed push sender; handles 410/404 subscription pruning |
|
||||
| `SessionExpiredError` | `apps/pwa/src/api/client.ts` | Typed error thrown by all fetch wrappers on 401/opaqueredirect; global `QueryCache` handler arms the session-expiry interstitial |
|
||||
| Zustand stores | `apps/pwa/src/store/` | UI-only ephemeral state (open panels, selected date, active tab); server state always in TanStack Query |
|
||||
| Abstraction | File | Description |
|
||||
| ------------------------------------------ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `app` (Hono) | `apps/api/src/index.ts` | Root Hono app; mounts all routes and serves the PWA static build |
|
||||
| Drizzle schema | `apps/api/src/db/schema.ts` | Single source of truth for all table definitions (`users`, `memberCredentials`, `calendars`, `calendarEvents`, `calendarOutbox`, `lists`, `listShares`, `listItems`, `pushSubscriptions`) |
|
||||
| `syncCalendar` | `apps/api/src/broker/sync.ts` | REPORT → ical.js parse → `onDuplicateKeyUpdate` upsert into MariaDB |
|
||||
| `expandOccurrences` | `apps/api/src/broker/expand.ts` | Server-side RRULE expansion using `ical.js` + `rrule`; never runs in the browser |
|
||||
| `CalendarOccurrence` | `apps/api/src/broker/expand.ts` | Wire type for a single concrete event occurrence; mirrored in the PWA's `api/client.ts` |
|
||||
| `calendarOutbox` table | `apps/api/src/db/schema.ts` | Transactional outbox pattern — CalDAV writes are enqueued here and drained asynchronously |
|
||||
| `runOutboxDrain` | `apps/api/src/broker/outboxWorker.ts` | Drains pending outbox rows every 15s; handles retry backoff, 412 conflict, dead-lettering, and edit-as-move ordering |
|
||||
| `publishListEvent` / `subscribeListEvents` | `apps/api/src/lib/listEmitter.ts` | In-process EventEmitter fan-out keyed per list; SSE route subscribes on open and unsubscribes on disconnect |
|
||||
| `dispatchPush` | `apps/api/src/lib/pushDispatcher.ts` | Centralised VAPID-signed push sender; handles 410/404 subscription pruning |
|
||||
| `SessionExpiredError` | `apps/pwa/src/api/client.ts` | Typed error thrown by all fetch wrappers on 401/opaqueredirect; global `QueryCache` handler arms the session-expiry interstitial |
|
||||
| Zustand stores | `apps/pwa/src/store/` | UI-only ephemeral state (open panels, selected date, active tab); server state always in TanStack Query |
|
||||
|
||||
---
|
||||
|
||||
@@ -188,35 +189,35 @@ routes/sse.ts ──→ lib/listEmitter.ts (subscribeListEvents)
|
||||
|
||||
### Frontend data ownership
|
||||
|
||||
| Concern | Owner |
|
||||
|---------|-------|
|
||||
| Calendar events | TanStack Query `['events', start, end]` |
|
||||
| List data + items | TanStack Query `['lists']`, `['list', listId]` |
|
||||
| Current user | TanStack Query `['me']` |
|
||||
| Writable calendars | TanStack Query `['writableCalendars']` |
|
||||
| Outbox sync status | TanStack Query `['syncStatus', uid]` |
|
||||
| Selected calendar view + date | Zustand `calendarStore` |
|
||||
| Event form open/mode | Zustand `calendarStore` |
|
||||
| Active tab, create-list sheet | Zustand `listsStore` |
|
||||
| Concern | Owner |
|
||||
| ----------------------------- | ---------------------------------------------- |
|
||||
| Calendar events | TanStack Query `['events', start, end]` |
|
||||
| List data + items | TanStack Query `['lists']`, `['list', listId]` |
|
||||
| Current user | TanStack Query `['me']` |
|
||||
| Writable calendars | TanStack Query `['writableCalendars']` |
|
||||
| Outbox sync status | TanStack Query `['syncStatus', uid]` |
|
||||
| Selected calendar view + date | Zustand `calendarStore` |
|
||||
| Event form open/mode | Zustand `calendarStore` |
|
||||
| Active tab, create-list sheet | Zustand `listsStore` |
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Runtime | Node.js 22 LTS |
|
||||
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
||||
| Database | MariaDB 11 (Docker volume) |
|
||||
| ORM | Drizzle ORM 0.45.x (`mysql2` dialect) |
|
||||
| Auth IdP | Authelia (pre-deployed, external) — OIDC authorization code + PKCE |
|
||||
| Session middleware | `@hono/oidc-auth` — storage-less signed JWT cookies |
|
||||
| Calendar source | Fastmail CalDAV (`caldav.fastmail.com`) — read via `tsdav`, write via transactional outbox |
|
||||
| Calendar parsing | `ical.js` (VCALENDAR/VEVENT parse) + `rrule` (RRULE expansion) |
|
||||
| App password storage | AES-256-GCM encrypted in `member_credentials.encrypted_password` |
|
||||
| Push notifications | `web-push` (VAPID) → APNs (iOS) / FCM (Android) |
|
||||
| Live list sync | In-process Node.js `EventEmitter` → SSE (`text/event-stream`) |
|
||||
| Redis | Present in stack (image: `redis:7-alpine`); not used in current runtime (reserved for future multi-process pub/sub) |
|
||||
| PWA | React 19 + Vite 8 + `vite-plugin-pwa` (Workbox `injectManifest` mode) |
|
||||
| Networking | Pangolin/Newt tunnel — no open ports; split-DNS internal domain |
|
||||
| Deployment | Docker Compose on Unraid; single `api` container serves both the API and the PWA static build |
|
||||
| Component | Technology |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Runtime | Node.js 22 LTS |
|
||||
| HTTP framework | Hono 4.x (`@hono/node-server`) |
|
||||
| Database | MariaDB 11 (Docker volume) |
|
||||
| ORM | Drizzle ORM 0.45.x (`mysql2` dialect) |
|
||||
| Auth IdP | Authelia (pre-deployed, external) — OIDC authorization code + PKCE |
|
||||
| Session middleware | `@hono/oidc-auth` — storage-less signed JWT cookies |
|
||||
| Calendar source | Fastmail CalDAV (`caldav.fastmail.com`) — read via `tsdav`, write via transactional outbox |
|
||||
| Calendar parsing | `ical.js` (VCALENDAR/VEVENT parse) + `rrule` (RRULE expansion) |
|
||||
| App password storage | AES-256-GCM encrypted in `member_credentials.encrypted_password` |
|
||||
| Push notifications | `web-push` (VAPID) → APNs (iOS) / FCM (Android) |
|
||||
| Live list sync | In-process Node.js `EventEmitter` → SSE (`text/event-stream`) |
|
||||
| Redis | Present in stack (image: `redis:7-alpine`); not used in current runtime (reserved for future multi-process pub/sub) |
|
||||
| PWA | React 19 + Vite 8 + `vite-plugin-pwa` (Workbox `injectManifest` mode) |
|
||||
| Networking | Pangolin/Newt tunnel — no open ports; split-DNS internal domain |
|
||||
| Deployment | Docker Compose on Unraid; single `api` container serves both the API and the PWA static build |
|
||||
|
||||
Reference in New Issue
Block a user