Stale intel reflected commit 01f7456 (pre-Phase-4). Refresh adds the shared-lists
+ SSE subsystem: 21 API endpoints (was 10), lists/sse routes, listEmitter/listAccess/rank
libs, PWA lists surface, 7 new arch decisions, and Phase 4 deps (react-router, @dnd-kit,
fractional-indexing). Canonical filenames preserved; API-SURFACE.md + .last-refresh.json regenerated.
510 lines
16 KiB
JSON
510 lines
16 KiB
JSON
{
|
|
"_meta": {
|
|
"updated_at": "2026-06-09T18:56:37.326Z",
|
|
"commit": "99f59c3999f4ef992f01ef8f8a38c1d86d2f2a0f",
|
|
"version": 3
|
|
},
|
|
"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/lists.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/lists.ts": {
|
|
"exports": [
|
|
"listsRouter",
|
|
"listItemsRouter"
|
|
],
|
|
"imports": [
|
|
"hono",
|
|
"@hono/zod-validator",
|
|
"zod",
|
|
"drizzle-orm",
|
|
"../db/client.js",
|
|
"../db/schema.js",
|
|
"../auth/middleware.js",
|
|
"../auth/user.js",
|
|
"../auth/devBypass.js",
|
|
"../lib/rank.js",
|
|
"../lib/listEmitter.js"
|
|
],
|
|
"type": "module",
|
|
"notes": "listsRouter: GET/POST /api/lists, PATCH/DELETE /api/lists/:id, POST/GET /api/lists/:id/items. listItemsRouter: PATCH/DELETE /api/list-items/:itemId. Owner-guard on isShared mutations (T-04-07/T-04-08). Auto-populates list_shares on isShared=true creation (D-01/D-02). publishListEvent fan-out after every mutation."
|
|
},
|
|
"apps/api/src/routes/sse.ts": {
|
|
"exports": [
|
|
"sseRouter"
|
|
],
|
|
"imports": [
|
|
"hono",
|
|
"hono/streaming",
|
|
"../auth/middleware.js",
|
|
"../auth/user.js",
|
|
"../auth/devBypass.js",
|
|
"../lib/listEmitter.js",
|
|
"../lib/listAccess.js"
|
|
],
|
|
"type": "module",
|
|
"notes": "GET /api/sse/heartbeat — 10s interval smoke-test. GET /api/sse/lists — scoped live-list fan-out (LIST-04, D-04); subscribes per-accessible-list via subscribeListEvents; 30s keepalive heartbeat."
|
|
},
|
|
"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/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",
|
|
"lists",
|
|
"listShares",
|
|
"listItems"
|
|
],
|
|
"imports": [
|
|
"drizzle-orm/mysql-core"
|
|
],
|
|
"type": "config",
|
|
"notes": "Drizzle schema for all 8 MariaDB tables. Phase 4 adds lists, list_shares, list_items. list_items.rank uses varcharBin (COLLATE utf8mb4_bin) for fractional-indexing sort correctness. calendarOutbox status enum: pending|done|failed|dead."
|
|
},
|
|
"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/lib/listEmitter.ts": {
|
|
"exports": [
|
|
"publishListEvent",
|
|
"subscribeListEvents",
|
|
"ListEvent"
|
|
],
|
|
"imports": [
|
|
"node:events"
|
|
],
|
|
"type": "module",
|
|
"notes": "In-process singleton EventEmitter for list change fan-out (D-18). Per-list channels keyed as list:${listId}. publishListEvent broadcasts; subscribeListEvents returns an unsubscribe fn. Max 200 listeners (T-04-04). Redis swap seam: abstraction boundary is inside this module."
|
|
},
|
|
"apps/api/src/lib/listAccess.ts": {
|
|
"exports": [
|
|
"getAccessibleListIds"
|
|
],
|
|
"imports": [
|
|
"drizzle-orm",
|
|
"../db/client.js",
|
|
"../db/schema.js"
|
|
],
|
|
"type": "module",
|
|
"notes": "getAccessibleListIds(userId): returns deduped list IDs the user owns OR has a list_shares row for. Gate used by SSE endpoint to scope subscriptions (D-04, T-04-02, T-04-03)."
|
|
},
|
|
"apps/api/src/lib/rank.ts": {
|
|
"exports": [
|
|
"rankForAppend",
|
|
"rankBetween"
|
|
],
|
|
"imports": [
|
|
"fractional-indexing"
|
|
],
|
|
"type": "module",
|
|
"notes": "Pure helpers wrapping fractional-indexing generateKeyBetween. rankForAppend(lastRank) → rank after last active item. rankBetween(prev, next) → rank between two items. No DB access."
|
|
},
|
|
"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-router",
|
|
"./components/CalendarShell.js",
|
|
"./routes/ListsIndex.js",
|
|
"./routes/ListDetail.js",
|
|
"./components/BottomTabBar.js"
|
|
],
|
|
"type": "entry-point",
|
|
"notes": "BrowserRouter shell. Routes: / → /calendar redirect, /calendar → CalendarShell, /lists → ListsIndex, /lists/:listId → ListDetail. BottomTabBar rendered as persistent sibling of Routes."
|
|
},
|
|
"apps/pwa/src/routes/ListsIndex.tsx": {
|
|
"exports": [
|
|
"ListsIndex"
|
|
],
|
|
"imports": [
|
|
"react",
|
|
"@tanstack/react-query",
|
|
"../api/listsClient.js",
|
|
"../components/"
|
|
],
|
|
"type": "module",
|
|
"notes": "Lists overview route (/lists). TanStack Query ['lists'] → fetchLists. Renders ListCard per list, ListsEmptyState when empty, CreateListSheet for new list, ListDeleteDialog for delete confirmation. Optimistic delete with rollback."
|
|
},
|
|
"apps/pwa/src/routes/ListDetail.tsx": {
|
|
"exports": [
|
|
"ListDetail"
|
|
],
|
|
"imports": [
|
|
"react",
|
|
"@tanstack/react-query",
|
|
"fractional-indexing",
|
|
"@dnd-kit/core",
|
|
"@dnd-kit/sortable",
|
|
"../api/listsClient.js",
|
|
"../hooks/useListSSE.js",
|
|
"../components/"
|
|
],
|
|
"type": "module",
|
|
"notes": "Single list view (/lists/:listId). Splits items into active (!checked, rank ASC) and completed sections. dnd-kit drag-to-reorder with PATCH { position }. useListSSE for live sync (D-10/D-11). 30s polling fallback (D-12). Optimistic check/uncheck + add + delete."
|
|
},
|
|
"apps/pwa/src/api/listsClient.ts": {
|
|
"exports": [
|
|
"fetchLists",
|
|
"createList",
|
|
"patchList",
|
|
"deleteList",
|
|
"fetchListItems",
|
|
"addItem",
|
|
"patchListItem",
|
|
"deleteItem",
|
|
"List",
|
|
"ListItem",
|
|
"ListsResponse",
|
|
"ListItemsResponse"
|
|
],
|
|
"imports": [],
|
|
"type": "module",
|
|
"notes": "Typed fetch wrappers for all lists API endpoints. credentials: 'include' for OIDC session cookie. Same opaqueredirect pattern as client.ts."
|
|
},
|
|
"apps/pwa/src/api/client.ts": {
|
|
"exports": [
|
|
"fetchMe",
|
|
"fetchEvents",
|
|
"createEvent",
|
|
"updateEvent",
|
|
"deleteEvent",
|
|
"fetchSyncStatus",
|
|
"fetchWritableCalendars"
|
|
],
|
|
"imports": [],
|
|
"type": "module",
|
|
"notes": "Typed fetch wrappers for all calendar API endpoints. Uses credentials: 'include' + redirect: 'manual' for OIDC opaqueredirect detection."
|
|
},
|
|
"apps/pwa/src/hooks/useListSSE.ts": {
|
|
"exports": [
|
|
"useListSSE"
|
|
],
|
|
"imports": [
|
|
"react",
|
|
"@tanstack/react-query"
|
|
],
|
|
"type": "module",
|
|
"notes": "Bounded-backoff EventSource hook for /api/sse/lists (D-11). Backoff: 250ms→500ms→1s→2s→4s→cap 8s; stops after MAX_ATTEMPTS. withCredentials: true (T-04-01). On open: invalidates ['list', listId] for full refetch (D-10). On event: invalidates relevant query. Polling fallback (D-12) lives in ListDetail."
|
|
},
|
|
"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/BottomTabBar.tsx": {
|
|
"exports": [
|
|
"BottomTabBar"
|
|
],
|
|
"imports": [
|
|
"react",
|
|
"react-router",
|
|
"../store/listsStore.js"
|
|
],
|
|
"type": "module",
|
|
"notes": "Phone-only bottom navigation tab bar. Tabs: Calendar (/calendar) and Lists (/lists). Persistent across route changes (rendered outside <Routes>). Visibility controlled by CSS at ≥768px."
|
|
},
|
|
"apps/pwa/src/store/listsStore.ts": {
|
|
"exports": [
|
|
"useListsStore"
|
|
],
|
|
"imports": [
|
|
"zustand"
|
|
],
|
|
"type": "module",
|
|
"notes": "Zustand UI-only state for lists surface: activeTab, createListSheetOpen. No server data. Follows calendarStore.ts pattern — no persist, no immer."
|
|
},
|
|
"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."
|
|
}
|
|
}
|
|
}
|