Task 40 — Web Push (VAPID): - Add web-push package + @types/web-push - pnpm vapid:generate script prints VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, NEXT_PUBLIC_VAPID_PUBLIC_KEY - push_subscriptions schema + migration 0013 - _core/push.ts: sendPush() iterates subscriptions, prunes 404/410 stale entries - SW push/notificationclick event handlers added to generated sw.js template - PushOptIn client component on /settings (opt-in, disable, send test) Task 42 — Notification bus + ntfy adapter: - notifications table + notif_push/notif_inapp/notif_ntfy user columns (migration 0013) - _core/notify.ts: notify() fans out to push, in-app DB, and optional ntfy POST - NotificationBell server component in AppNav: unread badge, dropdown inbox, mark-read - NotifyChannelToggles client component in /settings Task 41 — Reminders engine: - fired_at + created_by added to reminders; default channel changed to 'auto' - _core/reminders.ts: scheduleReminder (upsert), cancelReminder, listReminders, tickReminders - tickReminders uses pg_try_advisory_xact_lock for horizontal-scale safety - src/instrumentation.ts starts reminder worker (30s tick) on Node.js boot - Notes actions use scheduleReminder/cancelReminder instead of raw SQL - Calendar createEvent: optional remindMinutesBefore, deleteEvent: cancelReminder - Calendar-shell: "Remind me 30 min before" checkbox on new event form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
Status
Living progress tracker. Update at the end of each task. Codex and Claude Code both work on this project, so write status notes and next-step instructions for either agent to resume. Canonical briefs are AGENTS.md and CLAUDE.md; keep them synchronized. Task briefs live in docs/tasks/.
Done
-
01 — Repo init & tooling (commit
b89690a). pnpm 10 + TS strict + ESLint flat + Prettier. All acceptance criteria green. -
02 — Next.js app skeleton. Next.js 15 + React 19 + Tailwind v4 + shadcn/ui (button, card, input, dialog).
pnpm devserves placeholder,pnpm buildproduces.next/standalone/,pnpm lintclean. Added.npmrcwithnode-linker=hoistedfor Windows symlink compatibility. -
03 — Drizzle + Postgres setup. drizzle-orm + postgres driver + drizzle-kit wired up.
src/modules/_core/schema.tsdeclaresusers,households,household_members.docker-compose.dev.yamlstarts Postgres 16.drizzle/0000_silent_magma.sqlgenerated and applied.tsc --noEmitpasses. -
04 — Module loader & registry.
src/modules/_core/module.tstypes (ModuleManifest,EntityTypeRegistration,DashboardWidget, etc.),registry.tssingleton withregisterModule/getRegistry/getEntityType/getWidget, barrel_core/index.ts. Stub manifests forcalendar,lists,notes.src/modules/index.tsloader. Root layout imports loader;AppNavreads registry for nav links./debug/registrydumps full registry JSON (dev only). Uses zod v4 + built-inz.toJSONSchema().tsc --noEmit,pnpm build,pnpm lintall clean. -
05 — Compose + Caddy.
Dockerfile(3-stage: deps/builder/runner, pnpm fetch + offline install, non-rootnextjsuser,output: standalone),deploy/compose.yaml(famapp + famapp-db + full Authentik stack onfamapp_net),deploy/Caddyfile.snippet,.env.production.example.docker build -t famapp .succeeds (~311 MB);docker compose -f deploy/compose.yaml configvalidates clean. Added.dockerignoreandpublic/.gitkeep. -
06 — Authentik OIDC.
next-auth@beta+@auth/drizzle-adapterwired up.src/lib/auth.tsconfigures OIDC provider (Authentik), database sessions,authorizedcallback guarding all routes except/login,/s/*,/api/auth/*.src/middleware.tsexportsauthas middleware.src/app/api/auth/[...nextauth]/route.tsmounts the handlers.src/app/login/page.tsxhas a single "Sign in with SSO" server action.getCurrentUser()available for server components/actions. Schema extended:users→ addedname/emailVerified/image; newaccounts,sessions,verificationTokenstables; migration0001_auth_tables.sqlgenerated.deploy/authentik/README.mddocuments the manual Authentik bootstrap.tsc --noEmitandpnpm lintpass clean. -
07 — Household seed.
pnpm db:seedinserts household "Home" idempotently (powered bytsx).signIncallback assignsownerto the first member of the household andmemberto all subsequent users.getCurrentSession()insrc/lib/session.tsreturns{ user, household, role }and throws if unauthenticated or unmembered./settings/householdrenders member list for all roles and a rename form for owner only.tsc --noEmitandpnpm lintboth clean. -
08 — Theming infrastructure. CSS-variable multi-theme system (
default+warm) ×{light, dark, system}.users.theme+users.themeModecolumns in migration0002_naive_starbolt.sql. Theme registry insrc/modules/_core/themes.ts(THEMES/THEME_MODES arrays — adding a third theme is one CSS block + one registry entry). Root layout reads session and setsdata-theme/darkon<html>server-side; inline pre-paint<script>covers system mode and signed-out pages (no flash).useTheme()hook optimistically flips attributes, writeslocalStorage, and callssetUserThemeserver action.<ThemePicker />component mounts on/settings(select for theme, segmented buttons for mode).tsc --noEmit,pnpm lint,pnpm buildall clean. -
10 — Calendar module. Added
calendarsandcalendar_eventsschema + migration0003_rainy_ravenous.sql, default Home/Personal calendar seeding, first-login default calendar creation, visibility-safe calendar/event queries, CRUD server actions, FullCalendar-backed/calendarUI with sidebar calendar management and event create/edit/delete/drag updates. Calendar manifest now registers share/reminder/search capabilities, two configurable widgets, and quick-add entries. Added Playwright happy-path spec intests/e2e/calendar.spec.ts.pnpm db:generate,pnpm typecheck,pnpm lint, andpnpm buildpass. -
11 — Lists module. Added
listsandlist_itemsschema + migration0004_opposite_wraith.sql, default Shopping/Tasks seeding on first access/sign-in/seed, household-gated list and item CRUD server actions, reorder support, and PostgresLISTEN/NOTIFYto SSE bridge documented in ADR0002. Added/listsgrouped index,/lists/[id]keyboard-first item entry with checkbox toggles and swipe/delete, manifest entity/search/widget/quick-add registrations, and Playwright happy-path spec intests/e2e/lists.spec.ts.pnpm typecheck,pnpm lint, andpnpm buildpass. -
12 — Notes module. Added generic core
reminderstable plus household-scopednotesschema in migration0006_new_hannibal_king.sql, notes CRUD server actions, reminder synchronization fornotes.note,/notesindex,/notes/new,/notes/[id]editor with safe markdown preview, manifest entity/search/reminder/share registration,notes.filteredwidget registration, quick-add placeholder, and Playwright happy-path spec intests/e2e/notes.spec.ts.pnpm typecheck,pnpm lint,pnpm build, and notes E2E pass. -
20 — Dashboard composition (single-dashboard MVP). Added
default_dashboard_layoutjsonb column tousers+ migration0007_uneven_living_lightning.sql. Createdsrc/modules/_core/manifest.tsx(core.activityplaceholder widget) and registered it. Updated all three module manifests (calendar, lists, notes) with real async server component widget renders (data-fetching, empty states). Createdsrc/lib/dashboard.ts(layout parsing +computeDefaultLayoutgreedy packer). Builtsrc/app/page.tsx— 12-col CSS Grid, staticsmColSpanlookup for Tailwind class safety, per-widget<Suspense>for parallel loading, graceful skip for unknown widget IDs.pnpm typecheck,pnpm lint,pnpm build, and all 4 E2E specs pass. -
21 — Quick-add registry. Added
url: stringtoQuickAddActiontype (action is now optional). AddedgetQuickAdds()/SerializedQuickAddItemto registry (strips non-serializableactionfn before crossing server→client boundary). Updated all three module manifests with navigation URLs. BuiltQuickAddProvider(context + cmd+k global shortcut),QuickAddFab(opens sheet, replaces plain button in dashboard),QuickAddSheet(bottom drawer / desktop popover grouped by module), andCommandPalette(cmdk-powered modal with arrow + enter + esc keyboard nav). Provider in root layout receives actions fromgetQuickAdds()at render time — adding a module'squickAddsautomatically appears in both surfaces. Also added.claude/**to ESLint ignores to prevent stale worktree build artifacts from failing lint.pnpm typecheck,pnpm lint,pnpm build, and all 4 E2E specs pass. -
22 — Activity log. Added
activity_logtable to_core/schema.tswith index on(household_id, created_at desc). Migration0008_activity_log.sqlapplied.logActivity()server function in_core/activity.tsreads current session and inserts a row. AddedActivityLogEntrytype and optionalrenderActivity?(entry): stringtoEntityTypeRegistrationin_core/module.ts. All three module manifests implementrenderActivityfor each entity type (human-readable, no hardcoded branches in the widget). Replacedcore.activitywidget stub with a real async server component that queries the last 20 rows viagetEntityType(entry.entityType)?.renderActivity(entry). WiredlogActivity()into every create/update/delete in calendar, lists, and notes server actions. Also addedtexttogetAuthorizedItemselect so toggle/delete log the item text.pnpm typecheck,pnpm lint,pnpm build, and all 4 E2E specs pass. -
30 — Share-link service. Added
share_linkstable to_core/schema.ts+ migration0009_share_links.sql. Created_core/share.tswithcreateShareLink,resolveShareToken,revokeShareLink, andgetActiveShareLinks. Token is 32 random bytes (URL-safe base64), stored as SHA-256 hash — raw token only returned at creation.createShareLinkguards that the entity type is registered withcanShare === true.resolveShareTokenreturns null for expired or revoked tokens. All three functions exported from_core/index.ts./settingspage gained a Share links card: lists active links (entity label, read/write capabilities, expiry) with a Revoke button per link (server action insettings/actions.ts).pnpm typecheck,pnpm lint,pnpm build, and all 4 E2E specs pass. -
25 — Multiple dashboards per user. Added
dashboardstable (migration0012_dashboards.sql). Migrated each user'sdefault_dashboard_layoutinto a "Home" dashboard row withis_default = true; dropped the interim column. Server actions:listDashboards,createDashboard,renameDashboard,deleteDashboard,setDefaultDashboard,reorderDashboards,saveDashboardLayout,resetDashboardLayout,resolveWidgetConfigOptions./redirects to the user's default/d/<slug>. Dashboard switcher in AppNav renders tabs (active highlighted client-side) with a+button to create new dashboards and a kebab menu on the active tab for rename / set-default / delete.pnpm typecheck,pnpm lint,pnpm buildpass. -
26 — Customizable layout + widget configuration. Installed
react-grid-layoutv2. Dashboard pages check?edit=1to enter edit mode, rendering a clientDashboardEditorinstead of the static grid. Editor usesreact-grid-layoutwithgridConfig/dragConfigv2 API; each widget shell shows a drag handle, configure button (⚙), and remove button (🗑).WidgetPickeris a two-step modal: step 1 lists all registry widgets grouped by category; step 2 is aWidgetConfiguratorauto-generated from the widget's default config — handles"all"|string[]multi-selects, booleans, numbers, and enums.resolveWidgetConfigOptionsserver action fetches dynamic options (calendars, lists). Save validates each config against its registered Zod schema. Reset to defaults callscomputeDefaultLayout().pnpm typecheck,pnpm buildpass. -
31 — Public share viewer. Made
actorIdnullable inactivity_log(migration0010_nullable_actor_id.sql,onDelete: "set null") for anonymous share-page mutations. AddedlogShareActivityto_core/activity.ts(no session, explicithouseholdId). AddedhouseholdIdtoresolveShareTokenreturn. AddedrenderSharedViewtoEntityTypeRegistrationtype. Each module implementsloadForShare(bare DB queries, no session) andrenderSharedView: calendar shows upcoming 90-day events or single-event details, lists shows items with optional toggle, notes shows title + body.toggleShareListItemserver action lives inlists/server/share-actions.ts— validates token write capability, verifies item→list→household chain, logsshare.togglewithactorId = null./app/s/[token]/page.tsxresolves token, dispatches toloadForShare+renderSharedView, returns friendly error for invalid/expired tokens, setsnoindex. Middleware/s/*exemption confirmed present.pnpm typecheck,pnpm lint,pnpm build, and all 4 E2E specs pass. -
50 — PWA shell.
public/manifest.webmanifest(name, short_name, icons, theme_color, display: standalone, start_url/). Placeholder PNG icons at 180, 192, 384, 512 (regular + maskable) generated byscripts/generate-icons.mjs(pnpm gen:icons);public/icon.svgcommitted as source. Hand-rolled service worker atpublic/sw.js: precachesoffline.htmlon install, cache-first for/_next/static/, network-first for navigation with offline fallback, network-only for API routes.src/components/pwa-register.tsxregisters the SW client-side.src/components/install-prompt.tsxshows a dismissible banner:beforeinstallprompton Android/Chrome, a one-time "Add to Home Screen" hint on iOS (detected via UA +navigator.maxTouchPoints, suppressed in standalone mode). Root layout exportsviewport(themeColor), updatedmetadata(manifest, appleWebApp, apple-touch-icon), and mounts both new components.pnpm typecheck,pnpm lint,pnpm buildall clean. -
51 — Offline shell + service worker caching.
next.config.tsgeneratespublic/sw.jsas a side effect on everynext build/next devinvocation, embedding a build timestamp asCACHE_VERSION(stable"dev"string in development to avoid hot-reload cache churn; epoch milliseconds in production). SW strategies: stale-while-revalidate for/_next/static/chunks and navigation HTML (cached page served instantly, network update fires in background); network-first with 2-second abort timeout for API GETs falling back to cache; network-only for mutations (POST/PATCH/DELETE/PUT) — if offline, all controlled clients receive{ type: "OFFLINE_MUTATION" }viapostMessageand a synthetic 503 is returned. Activate handler evicts allfamapp-*caches whose suffix doesn't match the current version, then claims clients.pwa-register.tsxextended with three inline toasts: amber "offline" banner (persistent, driven bynavigator.onLine+online/offlineevents), red "changes can't be saved" toast (auto-dismisses in 4 s, driven by SW postMessage), and indigo "new version available — refresh" bottom toast (driven bycontrollerchangewithhadControllerguard).pnpm typecheck,pnpm buildpass. -
40 — Web Push (VAPID). Installed
web-push+@types/web-push. Addedpnpm vapid:generatescript (scripts/vapid-generate.mjs) that prints all three env vars to stdout. Addedpush_subscriptionstable to_core/schema.ts+ migration0013_push_notify_reminders.sql. Created_core/push.tswithsendPush(userId, payload)— iterates subscriptions, removes 404/410 stale entries. Addedpushandnotificationclickevent handlers to the generatedpublic/sw.jstemplate. Created<PushOptIn />client component on/settings(opt-in button →subscribeToPushserver action, disable button →unsubscribeFromPush, test button →sendTestNotification). DocumentedNEXT_PUBLIC_VAPID_PUBLIC_KEYin.env.example.pnpm typecheck,pnpm lint,pnpm buildpass. -
42 — Notification bus + ntfy adapter. Added
notificationstable andnotif_push/notif_inapp/notif_ntfycolumns onusers(migration0013_push_notify_reminders.sql). Created_core/notify.tswithnotify(userId, { title, body, url, channels? })— fans out to push (if VAPID configured), in-app DB insert, and ntfy POST (ifNTFY_URL+NTFY_TOPICset). Added<NotificationBell />async server component inAppNav: queries last 20 notifications, shows unread badge, dropdown inbox with mark-read and mark-all-read. Added<NotifyChannelToggles />client component with per-channel checkboxes in/settings.pnpm typecheck,pnpm lint,pnpm buildpass. -
41 — Reminders engine. Added
fired_atandcreated_bycolumns toreminderstable (migration0013_push_notify_reminders.sql); default channel changed to'auto'. Created_core/reminders.tswithscheduleReminder(upsert by entity),cancelReminder,listReminders, andtickReminders(30 s tick,pg_try_advisory_xact_lockguard).startReminderWorker()started viasrc/instrumentation.tson the Node.js runtime. Notes actions updated to usescheduleReminder/cancelReminderinstead of raw SQL. CalendarcreateEventaccepts optionalremindMinutesBeforeand schedules a reminder;deleteEventcallscancelReminder. Calendar-shell event dialog shows "Remind me 30 min before" checkbox (new events only, checked by default). Reminder worker confirmed starting on server boot (logged in dev server output).pnpm typecheck,pnpm lint,pnpm buildpass.
Next up
- Next task in
docs/tasks/.
Development login/testing notes
- Local development can use the documented Dev login flow in
docs/dev-login.md. It creates a database-backed Auth.js session forDEV_LOGIN_EMAILwhenENABLE_DEV_LOGIN=trueandNODE_ENV !== "production". - The production cleanup gate is tracked in
docs/tasks/09-production-dev-login-removal.md. Complete it before first production deployment.
Phase 1 remaining
- 03 Drizzle + Postgres → 04 module loader → 05 compose/Caddy → 06 Authentik OIDC → 07 household seed → 08 theming infrastructure (newly added; multi-theme + per-user dark/light, must land before module work so module UIs adopt the token system from day one).
Recent architectural decisions
- Calendars are first-class entities. Like lists, users can create as many as they want with
privateorhouseholdvisibility. Both calendar entities are independently shareable. Updated CLAUDE.md data model + task 10. - Uniform widget contract — no singletons. Every dashboard widget declares a
configSchemaanddefaultConfig; every placement is an independent instance. Same widget can appear N times on a dashboard pointed at different things. Updated task 04; tasks 11/12 widget sections updated to match. - Per-user customizable dashboards. Each user can have any number of named dashboards, switch between them, drag/resize widgets, and configure each placement. Tasks 25 (multiple dashboards) and 26 (customizable layout + widget configuration) added; phase 3 index updated.
How to resume in a fresh session
- Open the repo root in VS Code.
- Tell Codex or Claude Code: "Read AGENTS.md, CLAUDE.md, and STATUS.md, then complete the next task in docs/tasks/. Stop at the acceptance criteria."
- After it lands, append the result to the Done section here, bump Next up, and commit.
Environment notes
- Repo: https://github.com/ginnoir/famapp (HTTPS remote on
origin). - Local dev tooling installed: Node 22+, pnpm 10.33.3.
.envis not committed; copy.env.example→.envwhen needed.- VS Code recommended extensions in
.vscode/extensions.json; copy.vscode/settings.json.example→.vscode/settings.jsonfor the workspace defaults. - Memory files (cross-session, only seen by Claude):
C:\Users\MattC\.claude\projects\C--Users-MattC-Documents-famapp\memory\.