Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
6 changed files with 732 additions and 0 deletions
Showing only changes of commit 2a093468a4 - Show all commits
+105
View File
@@ -0,0 +1,105 @@
# API Surface
> Generated from `.planning/intel/api-map.json`. Do not edit by hand.
## `GET /health`
- **method:** GET
- **path:** /health
- **auth:** none
- **file:** apps/api/src/routes/health.ts
- **description:** DB liveness probe. Returns { ok: true, db: 'up' } or 503.
## `GET /callback`
- **method:** GET
- **path:** /callback
- **auth:** none (OIDC callback — must be before auth guard)
- **file:** apps/api/src/index.ts
- **description:** OIDC authorization-code callback. Processed by @hono/oidc-auth processOAuthCallback.
## `GET /api/login`
- **method:** GET
- **path:** /api/login
- **auth:** oidcAuthMiddleware
- **file:** apps/api/src/index.ts
- **description:** Auth entry point. Redirects to / after successful OIDC login. PWA navigates here for re-auth.
## `GET /api/me`
- **method:** GET
- **path:** /api/me
- **auth:** oidcAuthMiddleware
- **file:** apps/api/src/routes/me.ts
- **response:** { user: { id: number, displayName: string|null, color: string } }
- **description:** Returns authenticated member's identity and assigned color. Upserts user row on first call.
## `GET /api/events`
- **method:** GET
- **path:** /api/events
- **auth:** oidcAuthMiddleware
- **params:** start (YYYY-MM-DD, required), end (YYYY-MM-DD, required)
- **file:** apps/api/src/routes/events.ts
- **response:** { occurrences: CalendarOccurrence[] }
- **description:** Windowed calendar events. Max 90-day window. Reads only from MariaDB cache; never hits Fastmail. Expands RRULEs server-side.
## `POST /api/events/create`
- **method:** POST
- **path:** /api/events/create
- **auth:** oidcAuthMiddleware
- **body:** CreateEventPayload (title, allDay, start, end, recurrence?, location?, description?, calendarUrl?)
- **file:** apps/api/src/routes/events.ts
- **response:** 202 { uid: string }
- **description:** Enqueues create to calendarOutbox. Async CalDAV write-back via outbox worker. Returns uid immediately.
## `PATCH /api/events/:uid/edit`
- **method:** PATCH
- **path:** /api/events/:uid/edit
- **auth:** oidcAuthMiddleware
- **params:** uid (path)
- **body:** CreateEventPayload
- **file:** apps/api/src/routes/events.ts
- **response:** 202 { uid: string }
- **description:** Enqueues update (or delete+create pair for calendar-move) to calendarOutbox. Async write-back.
## `DELETE /api/events/:uid`
- **method:** DELETE
- **path:** /api/events/:uid
- **auth:** oidcAuthMiddleware
- **params:** uid (path)
- **file:** apps/api/src/routes/events.ts
- **response:** 202 { uid: string }
- **description:** Enqueues delete to calendarOutbox with cached etag (If-Match). Async write-back.
## `GET /api/events/sync-status`
- **method:** GET
- **path:** /api/events/sync-status
- **auth:** oidcAuthMiddleware
- **params:** uid (query, required)
- **file:** apps/api/src/routes/events.ts
- **response:** { uid: string, status: 'pending'|'done'|'failed'|'dead', error?: string }
- **description:** Outbox status poll for a given event UID, scoped to current member. Used by SyncStateToast.
## `GET /api/events/writable-calendars`
- **method:** GET
- **path:** /api/events/writable-calendars
- **auth:** oidcAuthMiddleware
- **file:** apps/api/src/routes/events.ts
- **response:** { calendars: [{ url, displayName, color, isShared }] }
- **description:** Authoritative D-03 writable set: member's own calendars + shared Family calendar. Client never derives this itself.
## `GET /api/sse/heartbeat`
- **method:** GET
- **path:** /api/sse/heartbeat
- **auth:** oidcAuthMiddleware
- **file:** apps/api/src/routes/sse.ts
- **response:** text/event-stream — event: heartbeat, data: { ts, id } every 10s
- **description:** SSE smoke-test endpoint for Pangolin tunnel validation. Phase 4 list-sync events not yet wired.
+100
View File
@@ -0,0 +1,100 @@
{
"_meta": {
"updated_at": "2026-06-09T00:00:00Z",
"commit": "01f7456b81dd55d477d0bd7530df818f61a873c2",
"version": 1
},
"entries": {
"GET /health": {
"method": "GET",
"path": "/health",
"auth": "none",
"file": "apps/api/src/routes/health.ts",
"description": "DB liveness probe. Returns { ok: true, db: 'up' } or 503."
},
"GET /callback": {
"method": "GET",
"path": "/callback",
"auth": "none (OIDC callback — must be before auth guard)",
"file": "apps/api/src/index.ts",
"description": "OIDC authorization-code callback. Processed by @hono/oidc-auth processOAuthCallback."
},
"GET /api/login": {
"method": "GET",
"path": "/api/login",
"auth": "oidcAuthMiddleware",
"file": "apps/api/src/index.ts",
"description": "Auth entry point. Redirects to / after successful OIDC login. PWA navigates here for re-auth."
},
"GET /api/me": {
"method": "GET",
"path": "/api/me",
"auth": "oidcAuthMiddleware",
"file": "apps/api/src/routes/me.ts",
"response": "{ user: { id: number, displayName: string|null, color: string } }",
"description": "Returns authenticated member's identity and assigned color. Upserts user row on first call."
},
"GET /api/events": {
"method": "GET",
"path": "/api/events",
"auth": "oidcAuthMiddleware",
"params": ["start (YYYY-MM-DD, required)", "end (YYYY-MM-DD, required)"],
"file": "apps/api/src/routes/events.ts",
"response": "{ occurrences: CalendarOccurrence[] }",
"description": "Windowed calendar events. Max 90-day window. Reads only from MariaDB cache; never hits Fastmail. Expands RRULEs server-side."
},
"POST /api/events/create": {
"method": "POST",
"path": "/api/events/create",
"auth": "oidcAuthMiddleware",
"body": "CreateEventPayload (title, allDay, start, end, recurrence?, location?, description?, calendarUrl?)",
"file": "apps/api/src/routes/events.ts",
"response": "202 { uid: string }",
"description": "Enqueues create to calendarOutbox. Async CalDAV write-back via outbox worker. Returns uid immediately."
},
"PATCH /api/events/:uid/edit": {
"method": "PATCH",
"path": "/api/events/:uid/edit",
"auth": "oidcAuthMiddleware",
"params": ["uid (path)"],
"body": "CreateEventPayload",
"file": "apps/api/src/routes/events.ts",
"response": "202 { uid: string }",
"description": "Enqueues update (or delete+create pair for calendar-move) to calendarOutbox. Async write-back."
},
"DELETE /api/events/:uid": {
"method": "DELETE",
"path": "/api/events/:uid",
"auth": "oidcAuthMiddleware",
"params": ["uid (path)"],
"file": "apps/api/src/routes/events.ts",
"response": "202 { uid: string }",
"description": "Enqueues delete to calendarOutbox with cached etag (If-Match). Async write-back."
},
"GET /api/events/sync-status": {
"method": "GET",
"path": "/api/events/sync-status",
"auth": "oidcAuthMiddleware",
"params": ["uid (query, required)"],
"file": "apps/api/src/routes/events.ts",
"response": "{ uid: string, status: 'pending'|'done'|'failed'|'dead', error?: string }",
"description": "Outbox status poll for a given event UID, scoped to current member. Used by SyncStateToast."
},
"GET /api/events/writable-calendars": {
"method": "GET",
"path": "/api/events/writable-calendars",
"auth": "oidcAuthMiddleware",
"file": "apps/api/src/routes/events.ts",
"response": "{ calendars: [{ url, displayName, color, isShared }] }",
"description": "Authoritative D-03 writable set: member's own calendars + shared Family calendar. Client never derives this itself."
},
"GET /api/sse/heartbeat": {
"method": "GET",
"path": "/api/sse/heartbeat",
"auth": "oidcAuthMiddleware",
"file": "apps/api/src/routes/sse.ts",
"response": "text/event-stream — event: heartbeat, data: { ts, id } every 10s",
"description": "SSE smoke-test endpoint for Pangolin tunnel validation. Phase 4 list-sync events not yet wired."
}
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"_meta": {
"updated_at": "2026-06-09T00:00:00Z",
"commit": "01f7456b81dd55d477d0bd7530df818f61a873c2",
"version": 1
},
"entries": {
"broker-cache-api-pattern": {
"title": "Broker-Cache-API pattern (two planes never cross)",
"decision": "Backend split into a broker plane (apps/api/src/broker/) that owns all Fastmail I/O and an API plane (apps/api/src/routes/) that reads only from MariaDB. Broker crons are not reachable from the HTTP layer.",
"files": ["apps/api/src/broker/poller.ts", "apps/api/src/broker/outboxWorker.ts", "apps/api/src/routes/"]
},
"write-broker-boundary": {
"title": "Write-broker boundary invariant",
"decision": "No route file imports tsdav or createFastmailClient; no broker file handles HTTP requests. Routes enqueue calendar_outbox rows and return 202 (optimistic-accept); the outbox worker performs the Fastmail write asynchronously.",
"files": ["apps/api/src/routes/events.ts", "apps/api/src/broker/write.ts"]
},
"identity-keying": {
"title": "Identity keyed on oidc_iss + oidc_sub",
"decision": "Users are keyed on oidc_iss + oidc_sub (never email). A hex color from the palette is auto-assigned on first login.",
"files": ["apps/api/src/routes/me.ts", "apps/api/src/auth/middleware.ts"]
},
"D-03-writable-set": {
"title": "D-03 calendar ownership / writable-set predicate",
"decision": "Every writable-set query uses WHERE userId = currentUser.id OR isShared = true. Another member's personal calendar is a read-only overlay.",
"files": ["apps/api/src/routes/events.ts"]
},
"D-13-dual-field-dtstart": {
"title": "D-13 all-day vs timed events (dual dtstart fields)",
"decision": "dtstart_utc is NULL for all-day events; dtstart_date is NULL for timed events. Never coerce DATE to DATETIME.",
"files": ["apps/api/src/db/schema.ts"]
},
"D-16-shared-fastmail-account": {
"title": "D-16 shared Fastmail account, per-member credentials",
"decision": "Both members share one Fastmail account. Calendar identity in DB is (userId, url) — the same collection URL appears once per member credential. CalDAV credential per member is stored AES-256-GCM encrypted in member_credentials.",
"files": ["apps/api/src/db/schema.ts", "apps/api/src/broker/poller.ts"]
},
"outbox-status-machine": {
"title": "Outbox status machine",
"decision": "calendar_outbox rows transition pending -> done | failed | dead. failed rows retry up to a limit; dead is terminal. The sync-status endpoint surfaces worst-status-first per uid.",
"files": ["apps/api/src/broker/outboxWorker.ts", "apps/api/src/routes/events.ts"]
},
"oidc-behind-pangolin": {
"title": "OIDC behind Pangolin requires OIDC_AUTH_EXTERNAL_URL",
"decision": "OIDC_AUTH_EXTERNAL_URL must be set to the public HTTPS URL to construct a correct redirect_uri; without it the callback resolves to the internal container address.",
"files": ["apps/api/src/auth/middleware.ts", "apps/api/src/index.ts"]
},
"dev-auth-bypass": {
"title": "Dev auth bypass",
"decision": "DEV_AUTH_BYPASS=true with NODE_ENV!=production injects DEV_USER via Hono context; OIDC middleware is never mounted in this mode.",
"files": ["apps/api/src/auth/devBypass.js", "apps/api/src/index.ts"]
},
"pwa-static-serving": {
"title": "PWA static serving + SPA fallback",
"decision": "Hono serveStatic serves ./public (Vite build output); SPA routes fall through to an index.html catch-all registered after /health, /api/*, and /callback so those win.",
"files": ["apps/api/src/index.ts"]
},
"schedule-x-routing": {
"title": "Schedule-X calendar routing",
"decision": "Events are routed to Schedule-X calendars by isShared ? 'shared' : String(ownerUserId) — never by calendarId. hydrateEvents.ts enforces this.",
"files": ["apps/pwa/src/hydrateEvents.ts", "apps/pwa/src/components/CalendarShell.tsx"]
},
"state-ownership": {
"title": "Client state ownership split",
"decision": "Server state is owned by TanStack Query; UI-only state (selected range, color map, drawer) by Zustand. Schedule-X renders the calendar UI.",
"files": ["apps/pwa/src/store/calendarStore.ts", "apps/pwa/src/components/CalendarShell.tsx"]
}
}
}
+156
View File
@@ -0,0 +1,156 @@
{
"_meta": {
"updated_at": "2026-06-09T00:00:00Z",
"commit": "01f7456b81dd55d477d0bd7530df818f61a873c2",
"version": 1
},
"entries": {
"hono": {
"version": "4.12.23",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/index.ts", "apps/api/src/routes/"]
},
"@hono/node-server": {
"version": "2.0.4",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/index.ts"]
},
"@hono/oidc-auth": {
"version": "1.8.3",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/auth/middleware.ts"]
},
"@hono/zod-validator": {
"version": "0.8.0",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/routes/events.ts"]
},
"drizzle-orm": {
"version": "0.45.2",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/db/client.ts", "apps/api/src/db/schema.ts", "apps/api/src/routes/"]
},
"mysql2": {
"version": "3.22.4",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/db/client.ts"]
},
"tsdav": {
"version": "2.2.2",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/broker/client.ts", "apps/api/src/broker/write.ts"]
},
"ical.js": {
"version": "2.2.1",
"type": "production",
"workspace": "both (@familysync/api + @familysync/pwa)",
"invocation": "require",
"used_by": ["apps/api/src/broker/expand.ts", "apps/api/src/broker/vevent.ts", "apps/api/src/broker/sync.ts"]
},
"zod": {
"version": "^3.25.0",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/routes/events.ts"]
},
"node-cron": {
"version": "^4.2.1",
"type": "production",
"workspace": "@familysync/api",
"invocation": "require",
"used_by": ["apps/api/src/broker/poller.ts", "apps/api/src/broker/outboxWorker.ts"]
},
"drizzle-kit": {
"version": "0.31.10",
"type": "development",
"workspace": "@familysync/api",
"invocation": "npm run db:generate / npm run db:migrate",
"used_by": ["npm run db:generate", "npm run db:migrate", "npm run db:push"]
},
"temporal-polyfill": {
"version": "0.3.2",
"type": "production",
"workspace": "both",
"invocation": "require",
"used_by": ["apps/api/src/broker/expand.ts", "apps/pwa/src/lib/eventDateTime.ts"]
},
"react": {
"version": "^19.0.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/"]
},
"vite": {
"version": "8.0.16",
"type": "development",
"workspace": "@familysync/pwa",
"invocation": "npm run dev / npm run build",
"used_by": ["npm run dev", "npm run build"]
},
"vite-plugin-pwa": {
"version": "^1.3.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "implicit",
"used_by": ["apps/pwa/vite.config.ts"]
},
"@tanstack/react-query": {
"version": "5.101.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/App.tsx", "apps/pwa/src/components/"]
},
"zustand": {
"version": "5.0.14",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/store/calendarStore.ts"]
},
"@schedule-x/calendar": {
"version": "4.6.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/components/CalendarShell.tsx", "apps/pwa/src/lib/calendarConfig.ts"]
},
"@schedule-x/react": {
"version": "4.1.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/components/CalendarShell.tsx"]
},
"lucide-react": {
"version": "1.17.0",
"type": "production",
"workspace": "@familysync/pwa",
"invocation": "require",
"used_by": ["apps/pwa/src/components/"]
},
"vitest": {
"version": "^4.1.8",
"type": "development",
"workspace": "both",
"invocation": "npm test",
"used_by": ["npm test", "npm run test:watch"]
}
}
}
+259
View File
@@ -0,0 +1,259 @@
{
"_meta": {
"updated_at": "2026-06-09T00:00:00Z",
"commit": "01f7456b81dd55d477d0bd7530df818f61a873c2",
"version": 1
},
"entries": {
"apps/api/src/index.ts": {
"exports": ["app"],
"imports": [
"@hono/node-server",
"@hono/node-server/serve-static",
"hono",
"./routes/health.js",
"./routes/me.js",
"./routes/events.js",
"./routes/sse.js",
"./auth/middleware.js",
"./auth/devBypass.js",
"./broker/poller.js",
"./broker/outboxWorker.js"
],
"type": "entry-point",
"notes": "Hono app factory + HTTP server; mounts routes, OIDC guard, static PWA assets. Broker workers started only when isMainModule()."
},
"apps/api/src/routes/events.ts": {
"exports": ["eventsRouter"],
"imports": [
"node:crypto",
"hono",
"@hono/zod-validator",
"zod",
"drizzle-orm",
"../db/client.js",
"../db/schema.js",
"../broker/expand.js",
"../broker/vevent.js",
"../auth/middleware.js",
"../auth/user.js",
"../auth/devBypass.js"
],
"type": "module",
"notes": "GET /api/events (windowed), POST /api/events/create, PATCH /api/events/:uid/edit, DELETE /api/events/:uid, GET /api/events/sync-status, GET /api/events/writable-calendars. Writes enqueue to calendarOutbox only — never calls Fastmail directly."
},
"apps/api/src/routes/me.ts": {
"exports": ["meRouter"],
"imports": [
"hono",
"../auth/middleware.js",
"../auth/user.js",
"../auth/devBypass.js"
],
"type": "module",
"notes": "GET /api/me — returns { user: { id, displayName, color } }. Upserts user on first login."
},
"apps/api/src/routes/sse.ts": {
"exports": ["sseRouter"],
"imports": ["hono", "hono/streaming"],
"type": "module",
"notes": "GET /api/sse/heartbeat — server-sent events smoke-test; 10s interval heartbeat. Phase 4 SSE fan-out to be added."
},
"apps/api/src/routes/health.ts": {
"exports": ["healthRouter"],
"imports": ["hono", "../db/client.js", "drizzle-orm"],
"type": "module",
"notes": "GET /health — unauthenticated. Runs SELECT 1 against DB; returns { ok, db }."
},
"apps/api/src/db/schema.ts": {
"exports": ["users", "memberCredentials", "calendars", "calendarEvents", "calendarOutbox"],
"imports": ["drizzle-orm/mysql-core"],
"type": "config",
"notes": "Drizzle schema for all 5 MariaDB tables. calendarOutbox status enum: pending|done|failed|dead. calendarEvents dual-field dtstart (dtstartUtc / dtstartDate) for timed vs all-day."
},
"apps/api/src/db/client.ts": {
"exports": ["db"],
"imports": ["drizzle-orm/mysql2", "mysql2/promise"],
"type": "module",
"notes": "Drizzle client bound to mysql2 pool. Reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME from env."
},
"apps/api/src/auth/middleware.ts": {
"exports": ["oidcAuthMiddleware", "processOAuthCallback", "getAuth"],
"imports": ["@hono/oidc-auth", "hono"],
"type": "module",
"notes": "OIDC middleware for Hono. Reads OIDC_AUTH_EXTERNAL_URL (mandatory behind Pangolin), OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_ISSUER from env."
},
"apps/api/src/auth/devBypass.ts": {
"exports": ["devAuthBypass", "DEV_USER"],
"imports": ["hono"],
"type": "module",
"notes": "Dev-only auth bypass middleware. Active only when DEV_AUTH_BYPASS=true AND NODE_ENV!=production. Augments Hono ContextVariableMap with 'user' key."
},
"apps/api/src/auth/user.ts": {
"exports": ["upsertUser", "deriveDisplayName"],
"imports": ["../db/client.js", "../db/schema.js", "drizzle-orm"],
"type": "module",
"notes": "User upsert keyed on oidc_iss + oidc_sub. deriveDisplayName: name → preferred_username → email → sub."
},
"apps/api/src/broker/poller.ts": {
"exports": ["startBrokerPoller"],
"imports": ["node-cron", "./sync.js", "../db/client.js", "../db/schema.js"],
"type": "module",
"notes": "5-minute cron that polls Fastmail CalDAV for each member credential. ctag change-detection (D-13)."
},
"apps/api/src/broker/outboxWorker.ts": {
"exports": ["startOutboxWorker"],
"imports": ["node-cron", "./write.js", "../db/client.js", "../db/schema.js"],
"type": "module",
"notes": "15-second cron that drains pending calendarOutbox rows. Dispatches create/update/delete to Fastmail. Status machine: pending → done|failed|dead."
},
"apps/api/src/broker/sync.ts": {
"exports": ["syncCalendarsForCredential"],
"imports": ["./client.js", "./expand.js", "../db/client.js", "../db/schema.js", "ical.js"],
"type": "module",
"notes": "CalDAV PROPFIND + REPORT → upserts calendars and calendarEvents rows."
},
"apps/api/src/broker/write.ts": {
"exports": ["executeOutboxRow"],
"imports": ["./client.js", "./vevent.js", "../db/client.js", "../db/schema.js"],
"type": "module",
"notes": "Executes a single outbox row: builds VEVENT, calls tsdav PUT/DELETE with If-Match etag."
},
"apps/api/src/broker/client.ts": {
"exports": ["createFastmailClient"],
"imports": ["tsdav", "./crypto.js", "../db/client.js", "../db/schema.js"],
"type": "module",
"notes": "Creates a tsdav DAVClient per member credential (decrypted AES-256-GCM)."
},
"apps/api/src/broker/crypto.ts": {
"exports": ["encrypt", "decrypt"],
"imports": ["node:crypto"],
"type": "module",
"notes": "AES-256-GCM encrypt/decrypt for Fastmail app passwords stored in memberCredentials."
},
"apps/api/src/broker/expand.ts": {
"exports": ["expandOccurrences"],
"imports": ["ical.js", "temporal-polyfill"],
"type": "module",
"notes": "Expands raw VCALENDAR string into CalendarOccurrence[] for a [start, end) window. Handles RRULE, EXDATE, DST via ical.js + Temporal."
},
"apps/api/src/broker/vevent.ts": {
"exports": ["buildVevent", "extractRruleString"],
"imports": ["ical.js"],
"type": "module",
"notes": "Builds VCALENDAR/VEVENT strings from CreateEventPayload. extractRruleString preserves RRULE on calendar-move edits."
},
"apps/pwa/src/main.tsx": {
"exports": [],
"imports": ["react-dom/client", "./App.tsx"],
"type": "entry-point",
"notes": "React root mount."
},
"apps/pwa/src/App.tsx": {
"exports": ["default"],
"imports": [
"react",
"@tanstack/react-query",
"./components/CalendarShell.tsx",
"./components/InstallPrompt.tsx"
],
"type": "entry-point",
"notes": "Root component. Sets up QueryClient, renders CalendarShell + InstallPrompt."
},
"apps/pwa/src/api/client.ts": {
"exports": [
"fetchMe",
"fetchEvents",
"createEvent",
"updateEvent",
"deleteEvent",
"fetchSyncStatus",
"fetchWritableCalendars"
],
"imports": [],
"type": "module",
"notes": "Typed fetch wrappers for all API endpoints. Uses credentials: 'include' + redirect: 'manual' for OIDC opaqueredirect detection."
},
"apps/pwa/src/components/CalendarShell.tsx": {
"exports": ["CalendarShell"],
"imports": [
"react",
"@tanstack/react-query",
"@schedule-x/react",
"../api/client.ts",
"../lib/calendarConfig.ts",
"../lib/hydrateEvents.ts",
"../lib/loginRedirect.ts",
"../store/calendarStore.ts",
"./EventDetailPopover.tsx",
"./EventForm.tsx",
"./SyncStateToast.tsx",
"./ColorLegend.tsx",
"./SkeletonCalendar.tsx",
"./ErrorBoundary.tsx"
],
"type": "module",
"notes": "Top-level calendar view. Orchestrates TanStack Query fetches, Schedule-X calendar, event create/edit/delete flows, sync toasts."
},
"apps/pwa/src/components/EventForm.tsx": {
"exports": ["EventForm"],
"imports": ["react", "../api/client.ts"],
"type": "module",
"notes": "Create/edit event form. Posts to createEvent/updateEvent. Supports recurrence presets, allDay toggle, calendar picker."
},
"apps/pwa/src/components/EventDetailPopover.tsx": {
"exports": ["EventDetailPopover"],
"imports": ["react", "../api/client.ts"],
"type": "module",
"notes": "Popover shown on event click. Shows title/time/location/description, edit/delete actions."
},
"apps/pwa/src/components/SyncStateToast.tsx": {
"exports": ["SyncStateToast"],
"imports": ["react", "@tanstack/react-query", "../api/client.ts"],
"type": "module",
"notes": "Polls /api/events/sync-status to show pending → done | failed toast for async CalDAV writes."
},
"apps/pwa/src/components/ColorLegend.tsx": {
"exports": ["ColorLegend"],
"imports": ["react", "../store/calendarStore.ts"],
"type": "module",
"notes": "Displays per-member color swatches from calendarStore."
},
"apps/pwa/src/store/calendarStore.ts": {
"exports": ["useCalendarStore"],
"imports": ["zustand"],
"type": "module",
"notes": "Zustand store for UI-only state: selectedDateRange, calendarId→color map, drawer open/closed. No server state."
},
"apps/pwa/src/lib/calendarConfig.ts": {
"exports": ["buildCalendarConfig"],
"imports": [],
"type": "module",
"notes": "Builds Schedule-X calendar config from member color map and MeUser."
},
"apps/pwa/src/lib/hydrateEvents.ts": {
"exports": ["hydrateEvents"],
"imports": ["../api/client.ts"],
"type": "module",
"notes": "Maps CalendarOccurrence[] → Schedule-X event objects. Routes by isShared/ownerUserId (never calendarId)."
},
"apps/pwa/src/lib/eventDateTime.ts": {
"exports": ["formatEventDateTime", "toScheduleXDateTime"],
"imports": ["temporal-polyfill"],
"type": "module",
"notes": "Date/time formatting helpers for Schedule-X event start/end fields."
},
"apps/pwa/src/lib/loginRedirect.ts": {
"exports": ["maybeRedirectToLogin"],
"imports": [],
"type": "module",
"notes": "Top-level navigation to /api/login when OIDC 302/opaqueredirect detected. CORS-bypass strategy."
},
"apps/pwa/src/lib/colorUtils.ts": {
"exports": ["assignMemberColors"],
"imports": [],
"type": "module",
"notes": "Assigns hex colors from palette to members deterministically."
}
}
}
+43
View File
@@ -0,0 +1,43 @@
{
"_meta": {
"updated_at": "2026-06-09T00:00:00Z",
"commit": "01f7456b81dd55d477d0bd7530df818f61a873c2",
"version": 1
},
"languages": ["TypeScript", "SQL"],
"frameworks": ["Hono 4.12.23", "React 19", "Drizzle ORM 0.45.2"],
"tools": [
"Vite 8.0.16",
"vite-plugin-pwa 1.3.0",
"Vitest",
"drizzle-kit 0.31.10",
"ESLint",
"node-cron"
],
"build_system": "pnpm workspaces + tsc (api) + vite build (pwa)",
"test_framework": "Vitest",
"package_manager": "pnpm 11.5.1",
"runtime": "Node.js 22 LTS",
"database": "MariaDB via mysql2 3.22.4",
"cache": "Redis (ioredis — planned for Phase 4 list sync; not yet wired)",
"auth": "Authelia OIDC — authorization_code + PKCE via @hono/oidc-auth 1.8.3",
"calendar_backend": "Fastmail CalDAV via tsdav 2.2.2 + ical.js 2.2.1",
"calendar_ui": "@schedule-x/calendar 4.6.0",
"server_state": "@tanstack/react-query 5.101.0",
"client_state": "zustand 5.0.14",
"content_formats": [
"TypeScript (source)",
"SQL (Drizzle migrations)",
"iCalendar / VCALENDAR (CalDAV payloads)",
"Markdown (planning docs)"
],
"infra": {
"hosting": "Unraid + Docker Compose",
"networking": "Pangolin/Newt tunnel (no open ports), split-DNS"
},
"workspaces": {
"root": "familysync (pnpm workspace root)",
"api": "@familysync/api — apps/api",
"pwa": "@familysync/pwa — apps/pwa"
}
}