# Handoff Log Append-only. Newest entries on top. Keep each entry to one screen. --- ## 2026-05-17 (pass 20) — Forms epic deployed; research; incident tracking; forward roadmap ### Deployed & verified - Pushed P6/P7/P8 + docs (commits up to `73eef72`); deploy run succeeded; migrations `028` (forms slug unique per space) + `029` (`space_invites.role`) applied. Smoke-verified on prod: `/f//` returns the anonymous 200 page (NOT a login redirect — the long-unverified public-forms route is finally confirmed), `/join/` public, `/forms` + `/my-forms` correctly auth-gated. Forms+invites (migrations 026-029) fully shipped. - Local commits awaiting deploy at pass close: `2559166` (forms public link + Copy button on member & admin lists) and the incident-tracking work in this entry. ### Integration research (web-verified; saved to memory `integration-api-facts`) - Payments are manual/CSV only today (no live API; known limitation). For real integration: **Stripe** is the one with a maintained official SDK + signature-verified webhooks (raw body, integer minor units). PayPal v2 works but official Node SDK has gaps (Subscriptions/webhook-verify need raw REST). Zeffy = read-only pull API + manual link. **Venmo has no usable API** (manual link / via PayPal-Braintree only). Canonical platform-agnostic payment record shape recorded in memory. - HeatSync door = Arduino HTTP query-string API (`?m&p&t&e=` add card, `?r` revoke, `?o1`/`?u` open), no TLS, LAN/VPN only. Generic adapter pattern (configurable URL + auth + verbs grant/revoke/open + inbound log) recorded. ### Anonymous incident tracking (this entry, LOCAL) - Gap found: `fileIncident` mints a `reporter_token` and the filing UI shows it ("the only way to look it up later") but there was NO lookup UI. Built it: pure `lib/incident-logic.ts` `publicIncidentView` (redacts board-only updates, subjects, decision makers; disposition only after decided/closed; double-filtered at DB + logic); `trackIncident` service-client action (token = bearer credential, 192-bit UNIQUE, generic not-found = enumeration-safe); public `/track` page (+ middleware allowlist); filing UI deep-links `/track?token=`; `__tests__/incident-tracking.test.ts`. ### Forward roadmap (decisions LOCKED with user — see memory `heatsync-members-site`, `integration-api-facts`, `architecture-standards`) Build order, each as its own well-separated module (migration + pure `*-logic.ts` tested + `lib/actions/*` + pages + docs; permissions via the additive catalog; the guarded RLS rule applies): 1. **Certifications + Instructor** (NEXT): `certifications` + `member_certifications` tables/RLS; `certifications.manage` + `certifications.grant` permissions (grant = the Instructor capability, assignable to roles/area-leads); admin pages + member-facing "My certifications & permissions" view. 2. **Classes**: `classes`/`class_sessions`/`class_signups`; `classes.manage`/`classes.instruct`; `payment_link` field at minimum; calendar; a class may grant a cert. 3. **Equipment**: registry + availability + reservations; gated perms. 4. **Door epic ("all of it")**: `member_cards` (RFID/NFC) ↔ member; configurable per-space door integration (URL+auth+verbs+inbound log); native HeatSync adapter; universal API-call UI builder with a door template. Largest/riskiest; own design pass; build last; LAN-only transport caveat. - Instructor/Host/Champion = PERMISSIONS, not new built-in roles. Payments: add a `payment_link` field generally; a real Stripe integration is a separate, later, explicitly-scoped effort using the verified facts in memory. ### Open / not done - **Production incident-INSERT RLS bug still OPEN** (separate from the tracking UI just built): never retried/diagnosed since `025`. Needs a prod retry; do not touch the permissions/RLS surface without diagnosis + approval. - Not browser-verified (no live browser this session): the forms-link Copy buttons, the `/track` flow end-to-end, and the broader forms/onboarding/retro-link chain. Smoke-test lists in pass-18/19. - Systemic test gap remains: `__tests__/actions.test.ts` largely asserts its own mock; no Supabase mock harness, so action *orchestration* is not unit-tested (pure logic is well covered). Recommended discrete follow-up. --- ## 2026-05-17 (pass 19) — Forms URL restructure + role invites + member surface + audit, LOCAL User redirected after Phases 1-5 shipped. Decisions (memory `forms-feature` updated): per-space form slug + `/f/[space]/[slug]` (reverses the original global-slug lock), space-scoped invites incl. role-granting + usage caps, member-facing forms surface, starter templates, obvious publish flow. ### What shipped (local commits on top of `origin/main`) - `5a49ecb` (PUSHED, deploy triggered, NEVER VERIFIED — superseded): `/f` added to middleware PUBLIC_ROUTES so the old `/f/[slug]` was anon-reachable. P6 replaces that route; re-verify the new one post-deploy. - `8bbe99d` P6: migration 028 (drop global `UNIQUE(forms.slug)`, add `UNIQUE(space_id,slug)`); `getPublicForm({space,slug})`; route moved `app/f/[slug]` → `app/f/[space]/[slug]`; public client submits by `formId`; builder/list show `/f//`; forms-guard returns `spaceSlug`. - `df6951c` P7 + audit hardening: migration 029 `space_invites.role` (member_role, default member); `lib/invite-logic.ts` `canAssignInviteRole` (admin→any, board→non-admin); createInvite/updateInvite enforce it; `joinSpace` applies invite role; invites panel role picker + single-use; `/join/[space]` landing (+ `/join` public); `/login` honors validated `?next=` (was hardcoded); `submitForm` requires `formId` (slug ambiguous post-028). - P8 (this entry's commit): builder publish-flow UX (draft/published/closed banners, create→edit redirect so Publish is right there, members-only "no public link" note); member-facing `/my-forms` list + `/my-forms/[id]` fill (sidebar "Forms" for all members — fixes the long-standing members-only-forms-have-no-surface gap); starter templates picker on `/forms/new` (`lib/form-templates.ts`); forms-client relabels members-only forms (no misleading public URL). - Docs corrected (two background audit agents ran): DATABASE_SCHEMA / DB_SCHEMA_MAP / ARCHITECTURE / API_REFERENCE — migration list 014-029, per-space slug, `/f/[space]/[slug]`, getPublicForm signature, invite roles. `next.config` `ignoreBuildErrors` noted (pre-existing). ### Audit results (parallel read-only agents + my review) - Forms/invites P0: none. RLS sound, anon cannot read submissions/drafts, snapshot immutable, public route genuinely anon, retro-link verified-email-gated (no hijack), onboarding fail-open verified, permissions surface additive/untouched. - Fixed this pass: `/login` `?next=` (P1), `submitForm` slug ambiguity (P1), members-only misleading public URL (P2). ### Open / residual gaps (honest) - **NOTHING new deployed.** `origin/main` is at `5a49ecb` (unverified). P6/P7/P8 + doc commits are LOCAL. Migrations 026/027 deployed; 028/029 ship with the next push. Awaiting deploy approval. - NOT browser-verified (no live browser): the whole builder→publish→public-submit→onboarding→retro-link→member-fill→role-invite→/join chain. Smoke-test list spans pass-18 + this entry. - Systemic test gap: `__tests__/actions.test.ts` (49 "tests") largely asserts its own Supabase mock, not real actions. New pure logic is well covered (forms-logic, invite-logic, validateAnswers, schemas — 337 tests); action *orchestration* (visibility/RLS/retro-link IO) is NOT unit-tested (no mock harness in repo). Recommended discrete follow-up: a Supabase mock harness, then real action tests. - Member fill has no "already submitted" indicator (members can't read form_submissions under RLS; would need a service-client probe). Duplicate member submissions possible; acceptable (waivers re-sign), noted. - Production incident-filing RLS bug still open (not retried since `025`). --- ## 2026-05-16 (pass 18) — Forms + waivers Phases 1-5 COMPLETE (schema/RLS + actions + builder UI + onboarding step + retro-link), LOCAL, awaiting deploy approval Branch: `main`. Migration: `026` (not yet deployed). `pnpm build` + `pnpm exec vitest run` gate pending in this entry's session. ### State reconciliation (pass-17 prose was stale) - Pass-17 said Tier 2+3 (`a1ea92a`, `b7461f0`) were "LOCAL/unpushed". They are in fact DEPLOYED: `git log origin/main..main` is empty, and Actions run `25973229017` (commit `3ace1af`) succeeded. So at pass-18 start: `origin/main == main`, clean, all UX Tiers 1-3 live. The pass-17 entry is left as-written (append-only); this is the correction of record. - Branches present: `main`, `ux-polish-wip` (deletable backup), `hotfix-incident` (stale, not deleted — flag for user). ### Incident-filing RLS open item — STILL OPEN - User confirms it has NOT been retried on prod since `025`. `025` only fixed cause #2 (policy drift). Item stays open: before any RLS change, retry on prod; if it still fails, run the discriminating SQL in the pass-17 entry to separate cause #1 (auth.uid() null in SSR) vs #3 (membership data drift). Permissions/RLS surface NOT to be modified without that diagnosis + user approval (guardrail). ### Forms + waivers Phase 1 (this session) — schema + RLS only, LOCAL - New `scripts/026_forms.sql` (idempotent) + mirrored as `schema.sql` Section 17 + Section 15 seed-function updated. - Tables: `forms` (space_id, globally-UNIQUE `slug`, `kind` form/waiver, `visibility` public_anon/public_auth/members, `status` draft/published/closed, `schema` jsonb, `legal_text`, `version`, `created_by`) and `form_submissions` (form_id, denormalized space_id, member_id, submitter_email, answers, `form_snapshot`/`legal_text_snapshot`/`form_version` immutable snapshots, ip, user_agent). Indexes on space/slug/form/email/member. - RLS (additive, default-deny): `forms` SELECT = `forms.manage` holders (all forms) or ordinary members (published only — drafts/closed hidden from non-managers); INSERT/UPDATE/DELETE = `user_has_permission(..., 'forms.manage')`. `form_submissions` SELECT = `forms.manage` only; **no write policy at all** → RLS hard-denies every non-service client and makes submissions immutable. Every submission funnels through one validated service-client server action (Phase 2). Public `/f/[slug]` will be served by a service-client server action, so `anon` gets no grant on `forms` (user-chosen option). - New `forms.manage` permission added to `lib/permissions-catalog.ts` (group Community) + board default; `seed_default_role_permissions()` extended; backfill `board -> forms.manage` for existing spaces (ON CONFLICT DO NOTHING). - `types/database.ts`: `forms` + `form_submissions` added. Docs updated: DATABASE_SCHEMA (026 row), DB_SCHEMA_MAP (quick-map + migrations), ARCHITECTURE (forms note). ### Phase 1 self-audit (commit `66dd9b7`, local) Audited the migration before continuing. Three fixes applied: dropped redundant `idx_forms_slug` (UNIQUE already indexes slug); tightened `forms_select` to `forms.manage` (all) OR member+`status='published'` (drafts/closed no longer exposed via raw PostgREST); made the `submitter_email`/`member_id` submission indexes partial. None weaken access; the SELECT change strengthens it. Safe because `forms` is a new table with no existing dependents. ### Forms + waivers Phase 2 (this session) — server actions + Zod + tests, LOCAL - `lib/validations.ts`: form field/array schemas (8 field types, duplicate-key + select-options refinements), `createForm`/`updateForm`/`setFormStatus`/`formId`/`submitForm`/`linkSubmissions` schemas. slug immutable on update. - `lib/forms-schema.ts` (pure, unit-tested): `parseFormSchema` + `validateAnswers` (server-side answer validation against the stored schema; unknown keys discarded so no arbitrary jsonb injection). - `lib/actions/forms.ts`: `createForm`, `updateForm` (waiver version bump on legal/schema change, non-blocking), `setFormStatus`, `deleteForm` (refused when submissions exist), `listForms`, `getFormResults`, `exportFormResultsCsv` (audited), `submitForm` (service-client read + write, visibility + consent enforcement, snapshot, IP/UA capture, no anon DB grant), `linkSubmissionsForMember` (forms.manage manual-link; auto verified-email path is Phase 5). forms.manage gate via `user_has_permission` RPC + RLS defense-in-depth. - `types/database.ts`: typed `user_has_permission` + `user_effective_roles` (were stale since 023). `lib/actions/index.ts` re-exports forms. `__tests__/forms.test.ts` (14 tests). API_REFERENCE updated. - Gate: `pnpm exec vitest run` 285/285; `pnpm build` exit 0, clean. ### Forms + waivers Phase 3 (this session) — builder UI + public page, LOCAL - `components/forms/form-renderer.tsx`: one controlled renderer for all 8 field types, shared by the builder preview and the public page so they cannot drift. - `components/forms/form-builder.tsx`: the easy builder (locked first-class priority). Label-only field creation (keys auto-derived + de-duped at save, never typed), add via one-click type buttons, up/down reorder buttons (no integer order field), inline options editor, live preview pane. Edit mode adds publish/close/draft/delete (via `useConfirm`) and a copy-public-link affordance. - Pages (server-gated by `requireFormsManagerPage` in `lib/forms-guard.ts` — forms.manage via the same RPC, redirect non-managers; RLS still the real boundary): `/forms` directory (status/visibility badges, Empty state), `/forms/new`, `/forms/[id]/edit`, `/forms/[id]/results` (table + client CSV download via `exportFormResultsCsv`). - Public `app/f/[slug]/page.tsx` (top-level, outside `(app)`): `getPublicForm` (service client, published + public-visibility only — members-only forms are NOT exposed here; their schema stays in-app), waiver legal text + consent, optional email for anon, signed-in gate for `public_auth`, thank-you state. - Sidebar: "Forms & waivers" NavLink in the Admin block. Toasts use `sonner` (matched the codebase convention; the custom use-toast has no mounted Toaster). New `getPublicForm` + `formSlug` export; API_REFERENCE updated. - Gate: `pnpm exec vitest run` 285/285; `pnpm build` exit 0; `/forms`, `/forms/[id]`, `/forms/new`, `/f/[slug]` all compiled. - NOT browser-verified (no live browser this session): smoke-test the builder (add/reorder/remove fields, live preview, create + edit + publish + close + delete), the results CSV download, and a public submission for each visibility (anon, public_auth signed-out gate, waiver consent). ### Forms + waivers Phase 4 (this session) — onboarding form step, LOCAL - `scripts/027_onboarding_form_step.sql` (idempotent constraint replace) + `schema.sql` Section 14 inline CHECK now allows `step_type = 'form'`. Referenced form id lives in the step's existing `config.form_id` (no new column). validations `onboardingStepTypeSchema` + onboarding action union + customize types/casts include `'form'`. - `finishOnboarding`: a required `form` step is satisfied if a submission for `config.form_id` by this member exists (any version — non-blocking re-sign / auto-satisfy). Fails open if the form is missing or unpublished so a misconfig cannot trap a member out of their space. Submission probe uses the service client (submissions are forms.manage-only under RLS). - `app/onboarding/page.tsx` enriches `form` steps with the published form def + a prior-submission flag; `onboarding-flow.tsx` renders `FormRenderer` + waiver legal text/consent and submits via `submitForm` then `markOnboardingStepDone`; already-submitted or unavailable forms show a non-blocking notice. - Admin: `customize/page.tsx` fetches the space's forms; `onboarding-panel.tsx` gets a "+ Form step" button and a form `` sends `"2026-05-22"` (date-only) but `z.string().datetime()` requires full RFC 3339. Eight other schemas had the same latent bug (`createProjectSchema.due_date`, `addMemberSchema.joined_at`, `logCashPaymentSchema.transaction_date`, both proposal voting timestamps, `createPolicySchema.effective_at`, `supersedePolicySchema.effective_at`, `meetingMinutesSchema.meeting_date`). 2. **Areas were hardcoded constants** in `tasks-client.tsx` (9 strings) and `projects-client.tsx` (9 different strings). Not configurable per-space. No defaults seeded for new spaces. ### Fix 1: shared `flexibleDateTime` preprocessor `lib/validations.ts` exports `flexibleDateTime()`. A `z.preprocess` that maps `""`/`null`/`undefined` → `null`, passes through full RFC 3339 unchanged, normalises `YYYY-MM-DD` and `YYYY-MM-DDTHH:MM` through `new Date(v).toISOString()`, and leaves unparseable input for Zod to reject. Wired into all nine date fields. The original bug screenshot is gone: `"2026-05-22"` validates and inserts as `"2026-05-22T00:00:00.000Z"`. Added 7 new tests. ### Fix 2: `space_areas` table + Settings UI Migration `scripts/020_areas.sql`: - `space_areas` table: `(id, space_id, code, name, icon, sort_order, is_archived)`. UNIQUE per `(space_id, code)` and `(space_id, name)`. - Sort index `(space_id, sort_order, name)`. - RLS: members read, admin/board write, admin delete. - `seed_default_areas()` trigger fires after spaces INSERT, seeding ten defaults: 3D Printing, Electronics, Woodshop, Laser, Metal Shop, Facilities, Software, Kitchen, Admin, General. Codes are stable slugs; names are display labels. - Backfill populates every existing space that has no areas. Idempotent. - Folded into `scripts/schema.sql` as Section 12. Library surface: - `lib/types.ts`: `SpaceArea` type. - `lib/validations.ts`: `createAreaSchema`, `updateAreaSchema` (slug regex, length caps). - `lib/actions/areas.ts`: `createArea`, `updateArea`, `deleteArea`. Use `requireMemberWithRole(ADMIN_ROLES)`. Re-exported via the barrel. UI wiring: - `app/(app)/tasks/page.tsx` and `app/(app)/projects/page.tsx` fetch `space_areas` in parallel and pass `areas: string[]` to the client. - `tasks-client.tsx` and `projects-client.tsx` accept the new prop; old hardcoded arrays renamed `DEFAULT_AREAS` as a fallback. - `app/(app)/settings/page.tsx` fetches the full area list and passes to `SettingsClient`. - `settings-client.tsx`: new **Areas** card on the Space tab. Lists every area with inline-editable name + sort_order, archive/restore toggle, admin-only delete with confirm prompt. "Add area" form takes code + display name. ### Verification (live local stack) - Migration applied to the running Postgres. 20 area rows across 2 spaces (Demo Hackerspace + the user's HeatSync Labs space created earlier through the UI), 10 each. - Test user fetched `space_areas` via REST with their JWT; got the 10 defaults sorted by `sort_order`. RLS clean, no cross-tenant leak. - **266 tests across 8 files** (was 259; +7 for flexibleDateTime). - `pnpm build` clean, 31 routes. - Dev server still running, hot-reloaded the changes. ### Files changed ``` Added: scripts/020_areas.sql lib/actions/areas.ts Edited: lib/validations.ts (flexibleDateTime + area schemas) lib/types.ts (SpaceArea) lib/actions/index.ts (export areas) scripts/schema.sql (Section 12) scripts/README.md (020 listed) app/(app)/tasks/page.tsx (fetch + pass areas) app/(app)/tasks/tasks-client.tsx (accept areas prop) app/(app)/projects/page.tsx (fetch + pass areas) app/(app)/projects/projects-client.tsx (accept areas prop) app/(app)/settings/page.tsx (fetch areas) app/(app)/settings/settings-client.tsx (Areas management card) __tests__/validations.test.ts (7 new tests) types/database.ts (regenerated; includes space_areas) docs/HANDOFF.md (this entry) ``` In the browser: `/tasks` → New Task → due_date `2026-05-22` saves cleanly. `/settings` → Space tab → scroll to Areas → add a custom area; it appears in the Tasks and Projects dropdowns on next render. --- ## 2026-05-14 (pass 8) — Local stack verified end-to-end, seed + LOCAL_DEV docs Branch: `main`. Author: Claude (no commits made). ### What I did Brought the system from "passes tests + builds" to "runs against a real Supabase stack on this machine, end-to-end." 1. Installed Supabase CLI via `brew install supabase/tap/supabase`. Started a local stack (`supabase start`) — Postgres, GoTrue, PostgREST, Realtime, Storage, Studio, Mailpit all up in Docker. 2. Applied `scripts/schema.sql` from scratch against the empty local DB. **18 tables created**. 3. Wrote `scripts/seed.sql` and applied it: 1 demo space, 5 offline members across all roles, 1 active code-of-conduct policy v1, 3 tasks, 1 project, 1 KB entry, 3 proposals (open / draft / decided) with 7 votes already cast, 1 open incident, 8 activity-log rows. 4. **Migration 019** (`scripts/019_proposal_expiry.sql`): `public.expire_proposals()` flips open proposals past their deadline to `decided` (if quorum met) or `expired`. Schedules itself hourly via `pg_cron` if the extension is enabled. Folded into `schema.sql` as Section 11. 5. Wrote `docs/LOCAL_DEV.md`: 8-step quickstart, wipe + re-seed recipe, troubleshooting section. 6. Regenerated `types/database.ts` from the live local DB. 1696 lines, 18 tables, 1 view, 3 RPC functions. 7. Wrote `.env.local` with the local Supabase URL and keys. 8. Started `pnpm dev`. Health endpoint returns 200, public routes return 200, `/dashboard` correctly 307s to `/login`. ### Verification done with real data - Anon-key RLS gate confirmed (empty array on no-session). - Service-role bypass confirmed. - Created `tester@demo.local` via Auth Admin API, signed in via `/auth/v1/token`, got a real ES256 JWT, queried proposals + channels + policies — all RLS-scoped, all returning seeded data. - **Privilege-escalation guard (migration 015)**: simulated a plain-member self-promotion via JWT-claimed authenticated role. Postgres rejected with the exact `Members cannot change their own role, tier, status, approval, card access, or space.` message. - `expire_proposals()` runs cleanly, returns 0. - HTML smoke: `/login` title correct, `/signup` contains expected copy. - **259 tests across 8 files passing**. ### Files changed ``` Added: supabase/config.toml (from supabase init) scripts/019_proposal_expiry.sql scripts/seed.sql docs/LOCAL_DEV.md .env.local (gitignored) Edited: scripts/schema.sql (Section 11) types/database.ts (regenerated) ``` --- ## 2026-05-14 (pass 7) — Tier 2/3 UI closure + Markdown + proxy rename Branch: `main` Author: Claude (no commits made). ### What I did Finishing the user-facing surface for the schema work passes 5 and 6 added. 1. **Renamed `middleware.ts` → `proxy.ts`** for Next.js 16. Renamed the exported `middleware` function to `proxy`. The deprecation warning that the build had been emitting since pass 4 is now gone. 2. **Markdown rendering**: installed `react-markdown` + `remark-gfm`. Created `components/markdown.tsx` (`MarkdownBody`) with safe defaults — HTML escaped, no `rehypeRaw`, every link gets `rel="noopener noreferrer"` and `target="_blank"`. Wired it into the three governance detail pages so proposal bodies, incident bodies, incident dispositions, incident updates, and policy formal/plain text now render headings, lists, links, code, blockquotes, tables, strikethrough. 3. **`/profile` route** (Tier 3.1 + 3.2 UI closure): - Server page reads the caller's `space_members` row. - `ProfileForm` (client) lets a member edit display_name, handle, phone, skills, interests, willing_to. Uses a tiny custom `ChipInput` component for tag-style multi-value fields with `Enter`/`,` to commit and `Backspace` to delete. - `AffiliationsForm` (client) lets the member disclose COI affiliations. Submit stamps `coi_last_disclosed_at` via `discloseAffiliations`. Surfaces an orange callout for privileged-role members who haven't disclosed yet. - The `willing_to` field ships with suggestion chips: `board_candidate`, `treasurer_candidate`, `host_volunteer`, `area_lead_candidate`, `event_organizer`, `safety_committee`, `docs_steward`. Members can also free-form. 4. **`/recruitment` route** (Tier 3 succession view): - Board / admin only (server-side redirect for others). - Queries `space_members` for the current space; groups by each `willing_to` tag; renders one card per tag with the willing members listed underneath. - For privileged-role members (admin / board / treasurer) the card shows a "COI on file" or "no COI" badge based on `coi_last_disclosed_at`. - A separate "Skills declared (no role opt-in)" section catches members who declared skills without willing_to. 5. **Settings extensions** (Tier 2.1 + Tier 2.2 + mission statement): - Added `mission_statement` field to `updateSpaceSettingsSchema` (max 5000 chars, nullable). - The Space tab on `/settings` now has a Mission Statement textarea inside the existing space-settings form. - A new Visibility card on the Space tab exposes two dropdowns: `financial_visibility` (3 options) and `member_directory_visibility` (4 options). Saves via the existing `updateSpaceVisibility` action from pass 6. 6. **Mission statement display in the app shell**: when `space.mission_statement` is set, the app layout renders it in a small italic footer on every protected page — the "stated-mission visibility on every page" principle from `docs/GOVERNANCE_FEATURES.md` section 1. 7. **Dashboard widget**: above the main grid, two side-by-side cards. - **Open proposals awaiting your vote**: queries open proposals, joins against the caller's `proposal_votes` to filter out ones they've already voted on, shows up to 5 with the voting-window close timestamp and a live quorum percentage. - **Incidents needing response** (admin/board only): shows `received` and `under_review` incidents, sorted newest first, with a red "respond by" date if the SLA has elapsed. - The whole section only renders if at least one of the two has content. 8. **Sidebar**: added "My profile" (`UserCircle` icon) and admin-gated "Recruitment" (`UserSearch` icon) under the People group. 9. **4 new tests** in `__tests__/governance.test.ts` for the mission_statement extension to `updateSpaceSettingsSchema`. Total now **259 across 8 files**. ### Verification - `pnpm vitest run` → **259 tests pass across 8 files**. - `pnpm build` → success. Build emits **31 routes** (added `/profile`, `/recruitment`). - **The middleware-deprecation warning is gone**. Only remaining build warning is the stray lockfile at `/Users/obsidian/Projects/package-lock.json`, which is outside the repo and harmless on DO. ### Open items, priority order 1. **Apply pending migrations** on production (015, 017, 018) if not already done. Fresh deploys get them all via `scripts/schema.sql`. 2. **Regenerate `types/database.ts`** with the Supabase CLI to clear the ~217 TS errors that `typescript.ignoreBuildErrors` masks. Once clean, remove `ignoreBuildErrors` so future drift surfaces in CI. 3. **Configure OAuth providers** in Supabase if you want the GitHub / Google buttons to work. 4. **Auto-expire open proposals**: when `voting_closes_at < now()`, status should auto-flip from `open` to `expired`. Currently manual via the admin's "Mark decided" button. A cron-triggered server action would close this. 5. **Mark KB entry as meeting minutes**: schema (column added in 018) and validation schema in place, but no UI control yet. 6. **Notifications on incident status change**: activity_log row written but no in-app comms message or email triggered. Add when notification infra is ready. 7. **Hosting calendar, sponsor / partner registry, community surveys**: Tier 4, deferred indefinitely until adoption signals demand. 8. **Secrets encryption at rest**: `secrets.value` and `integrations.config` still plain text. Address before storing real production credentials. ### Files changed this pass ``` Added: proxy.ts (replaces middleware.ts) components/markdown.tsx app/(app)/profile/page.tsx app/(app)/profile/profile-form.tsx app/(app)/profile/affiliations-form.tsx app/(app)/profile/chip-input.tsx app/(app)/recruitment/page.tsx Removed: middleware.ts (renamed to proxy.ts) Edited: app/(app)/layout.tsx (mission statement footer) app/(app)/dashboard/page.tsx (governance widget) app/(app)/settings/settings-client.tsx (mission statement + visibility section) app/(app)/proposals/[id]/page.tsx (MarkdownBody) app/(app)/incidents/[id]/page.tsx (MarkdownBody, ×3) app/(app)/policies/[slug]/page.tsx (MarkdownBody, ×2) components/app-sidebar.tsx (Profile, Recruitment links) lib/validations.ts (mission_statement on updateSpaceSettingsSchema) __tests__/governance.test.ts (4 new tests) package.json (react-markdown, remark-gfm) docs/HANDOFF.md (this entry) ``` ### How to verify locally ```bash pnpm install # picks up react-markdown + remark-gfm pnpm vitest run # 259 tests, 8 files pnpm build # 31 routes, no middleware deprecation warning ``` In the browser: - Visit `/profile`, edit skills / willing_to (chip input), save. - Visit `/recruitment` as admin/board — see members grouped by willing_to. - Visit `/settings`, scroll to "Visibility", change financial_visibility to `treasurer_only`, save, then visit `/financials` as a plain member — should be blocked. Switch back to `all_members_visible` — visible again. - Set a mission statement on `/settings`. It appears as a footer on every protected page. - Open a proposal, edit body to include `# heading\n\n- list\n- items\n\n[link](https://example.com)`. Renders rich. --- ## 2026-05-14 (pass 6) — Audit fixes + Tier 2/3 partial-ship Branch: `main` Author: Claude (no commits made). ### Audit findings Read my pass-5 work hard. Four real defects, plus several lower-severity items I documented but didn't change. **Critical (fixed in migration 017):** 1. `proposal_votes.votes_insert` accepted a vote where `member_id` was a `space_members` row from space A but the proposal was in space B, because the policy never joined them. Fixed: insert WITH CHECK now requires `m.space_id = p.space_id` for the joined member. 2. `votes_update` had the same bug. Same fix. 3. `incidents.incidents_insert` let the reporter set `reporter_id` to any `space_members.id` they owned — across spaces. Fixed: WITH CHECK requires the row's `space_id` match the incident's. 4. `incident_updates.incident_updates_insert` did not validate `author_id` at all. Fixed: WITH CHECK requires the author is the calling user and in the incident's space. Plus: added partial unique index `policies_one_active_per_slug ON policies (space_id, slug) WHERE status = 'active'` so two admins concurrently activating different drafts cannot leave two `active` rows. **Lower-severity (documented, deferred):** - `requireMember().single()` makes multi-space membership effectively impossible. The app design has always assumed one membership per user. - `appealIncident` does insert-then-update without a transaction; mid-flight failure leaves an orphaned proposal. Acceptable risk. - `supersedePolicy` race between two admins computing `version + 1` simultaneously is caught by the existing `UNIQUE (space_id, slug, version)`. - Reporter can post `visibility='board_only'` on their own incident_update via raw API and then can't read it. Self-inflicted. - `/proposals` and `/incidents` lists don't paginate. Defer. - Incident detail never shows the reporter's name to admin/board. Future ergonomic improvement. - Dashboard doesn't surface open proposals or pending incidents. ### Built in this pass (audit fixes + Tier 2/3 schema + one Tier 2 UI) 1. **Migration 017** `scripts/017_governance_rls_hardening.sql`: the four RLS fixes + the partial unique index. Mirrored into `scripts/schema.sql`. 2. **Migration 018** `scripts/018_member_state_and_visibility.sql`: - `space_members` gains `skills`, `interests`, `willing_to`, `affiliations` (all `text[]`) and `coi_last_disclosed_at`. GIN indexes on `skills` and `willing_to`. - `spaces` gains `financial_visibility` and `member_directory_visibility` enums. - `knowledge_base` gains `is_meeting_minutes` and `meeting_date` (partial index). - `schedule_card_review` trigger inserts a 180-day task whenever `has_card_access` flips on. - `inactive_members` view: members in good standing with no `activity_log` entry in 180 days. 3. **New server actions**: - `members.ts → updateMyProfile({...})`: member self-edit of display_name, handle, phone, skills, interests, willing_to. The privilege-escalation trigger from migration 015 keeps role/tier/status off-limits. - `members.ts → discloseAffiliations({ affiliations })`: stamps `coi_last_disclosed_at` and logs to activity_log. - `settings.ts → updateSpaceVisibility({...})`: admin-only. 4. **Validation schemas**: `updateMyProfileSchema`, `discloseAffiliationsSchema`, `updateSpaceVisibilitySchema`, `meetingMinutesSchema`, plus exported tuples (`financialVisibilities`, `directoryVisibilities`). 5. **13 new tests** in `__tests__/governance.test.ts`. Total now **255 across 8 files**. 6. **`/financials` route**: server-component aggregations from `payments` (this month, this year, lifetime, linked vs unlinked, by platform with sparkline-style bars, last 12 months). Gated by `space.financial_visibility` against caller's role. 7. **Sidebar**: added "Financials" under People with the `LineChart` icon. ### Verification - `pnpm vitest run` → **255 tests pass across 8 files**. - `pnpm build` → success. **29 routes emit** (added `/financials`). - `pnpm exec tsc --noEmit` → same Supabase generic-drift count as before. No new error categories. ### Deployment steps for this pass Run on every existing database, in order: ```sql \i scripts/017_governance_rls_hardening.sql \i scripts/018_member_state_and_visibility.sql ``` Then restart PostgREST (`docker compose exec rest restart` on self-hosted; managed Supabase auto-refreshes). Fresh deploys get both via `scripts/schema.sql` (sections 9.8 and 10). No additional config needed; new columns ship with sensible defaults. ### Open items, priority order 1. **Apply 017 on production immediately** if any chance of multi-space membership (admin client could have set it up). 2. **Apply 018** for the Tier 2/3 columns and the card-review trigger. 3. **Profile / COI / settings-visibility UI**: server actions are in place, no UI yet. A `/profile` route + extending Settings would close Tier 3.1, 3.2 and the financial-visibility toggle. 4. **Recruitment view**: query `space_members` by `willing_to` for board-only succession planning. 5. **Mark KB entry as meeting minutes from UI**: schema ready, no UI control. 6. Earlier-pass items still open: regenerate `types/database.ts`, run migration 015 if not already, rename `middleware.ts` → `proxy.ts` for Next.js 16. ### Files changed this pass ``` Added (schema): scripts/017_governance_rls_hardening.sql scripts/018_member_state_and_visibility.sql Added (UI): app/(app)/financials/page.tsx Edited: scripts/schema.sql (sections 9.8 + 10) scripts/README.md (017, 018 listed) lib/types.ts (FinancialVisibility + DirectoryVisibility) lib/validations.ts (4 new schemas + tuples) lib/actions/members.ts (updateMyProfile, discloseAffiliations) lib/actions/settings.ts (updateSpaceVisibility) components/app-sidebar.tsx (Financials nav link) __tests__/governance.test.ts (13 new tests) docs/GOVERNANCE_FEATURES.md (Tier 2 + 3 marked partial-shipped) docs/HANDOFF.md (this entry) ``` --- ## 2026-05-14 (pass 5) — Tier 1 governance kernel shipped Branch: `main` Author: Claude (no commits made). ### What I did Built the full Tier 1 governance kernel from `docs/GOVERNANCE_FEATURES.md`. Three new tightly integrated modules: proposals, incidents, policies. Schema + server actions + validations + tests + UI + sidebar nav. 1. **Schema**: `scripts/016_governance_kernel.sql` (and mirrored into `scripts/schema.sql` as Section 9). 5 new tables (`proposals`, `proposal_votes`, `incidents`, `incident_updates`, `policies`), 8 new enums, 6 new columns on `spaces` for governance defaults (quorum percent/floor, voting window, incident SLA, mission statement). Triggers: `compute_proposal_quorum`, `refresh_proposal_tally`, `compute_incident_sla`, `touch_updated_at`. Full RLS: any member proposes / files / reads; admin+board transition status; votes are immutable post-window; incidents readable only to reporter + admin/board. Idempotent re-run safe (`DROP POLICY IF EXISTS`, `CREATE OR REPLACE`, `EXCEPTION WHEN duplicate_object`). 2. **Types** in `lib/types.ts`: hand-typed `Proposal`, `ProposalVote`, `Incident`, `IncidentUpdateRow`, `Policy` and all 8 governance enums. `types/database.ts` should be regenerated via Supabase CLI to fold these in. 3. **Validations** in `lib/validations.ts`: 11 new Zod schemas (`createProposalSchema`, `castVoteSchema`, `decideProposalSchema`, `withdrawProposalSchema`, `openProposalSchema`, `fileIncidentSchema`, `updateIncidentStatusSchema`, `addIncidentUpdateSchema`, `appealIncidentSchema`, `createPolicySchema`, `supersedePolicySchema`, `updatePolicyStatusSchema`). Tested before being wired into actions (test-driven). 4. **Server actions**: `lib/actions/proposals.ts`, `lib/actions/incidents.ts`, `lib/actions/policies.ts`. Use the existing `requireMember` / `requireMemberWithRole` / `parseInput` / `logActivity` helpers. Re-exported via the barrel in `lib/actions/index.ts`. Notable: `fileIncident` generates an opaque `reporter_token` when anonymous; `appealIncident` atomically spawns a draft `membership_vote` proposal and flips the incident to `appealed`; `supersedePolicy` inserts a new version with `prior_version_id` set rather than mutating the prior row; `updatePolicyStatus(... , 'active')` atomically supersedes any prior active version with the same slug. 5. **Tests**: `__tests__/governance.test.ts` adds 40 tests covering every new Zod schema (valid input, invalid enum values, required field enforcement, recusal-requires-reason cross-field validation, slug character set, etc). The cross-cutting "governance contract surface" suite enforces that the enum tuples we export match the DB enums in workflow order. 6. **UI**: server-component-driven, minimal client islands. - `/proposals`: open / draft / archive sections with live tally and quorum hints. - `/proposals/[id]`: title + body + status, tally panel with quorum progress, vote form (yes/no/abstain/recused-with-required-reason), management buttons (open / withdraw / decide) gated by role, vote list with comments and recusal reasons. - `/proposals/new`: form with type, threshold, "open immediately" checkbox. - `/incidents`: open / decided / closed sections. Members see only their own; admin/board sees all (RLS enforced). - `/incidents/[id]`: title + body + disposition + update thread, status transition form (admin/board), post-update form (everyone with access), one-click "request membership appeal" for reporter on dismissed incidents. - `/incidents/new`: title, body, category, severity, optional subjects multi-select, anonymous checkbox with honest disclaimer; on anonymous submit, surfaces the opaque tracking token to the reporter once. - `/policies`: one row per slug showing the active (or latest) version. - `/policies/[slug]`: plain-language + formal text + version history, with "activate" / "supersede" / "deprecate" buttons for admins. - `/policies/new`: slug + title + section ref + plain + formal text. Admin-only route. 7. **Sidebar**: new "Governance" section with three links (Proposals, Incidents, Policies). Lucide icons: `Vote`, `ShieldAlert`, `ScrollText`. 8. **Docs updated**: - `docs/GOVERNANCE_FEATURES.md`: Tier 1 marked SHIPPED with implementation pointers. - `scripts/README.md`: migration 016 listed. - This handoff entry. ### Verification - `pnpm vitest run` → **242 tests pass across 8 files** (was 202 / 7). The 40 new tests are in `__tests__/governance.test.ts`. - `pnpm build` → success. **9 new routes emit cleanly** (`/proposals`, `/proposals/[id]`, `/proposals/new`, `/incidents`, `/incidents/[id]`, `/incidents/new`, `/policies`, `/policies/[slug]`, `/policies/new`). 28 routes total. - `pnpm exec tsc --noEmit` → 217 errors (was 157). All 60 new errors are the same Supabase generic-drift pattern (`Property 'X' does not exist on type 'never'`) that the pre-existing 157 are. Runtime is unaffected. Regenerating `types/database.ts` with the Supabase CLI clears them all. ### Open items, priority order 1. **Apply `scripts/016_governance_kernel.sql`** to any existing production database. Fresh deploys get it via `scripts/schema.sql` automatically. 2. **Regenerate `types/database.ts`** with `supabase gen types typescript --project-id `. Clears the 60 + 157 = 217 TS errors. Then remove `typescript.ignoreBuildErrors` from `next.config.mjs`. 3. **Pass 4 priority 1 still applies**: run `scripts/015_prevent_member_self_role_change.sql` before external users sign in. 4. **Auto-expiry of open proposals**: when `voting_closes_at < now()`, the status should flip from `open` to `expired` (or `decided` if board acts). Currently manual. A cron-triggered server action or DB job can do this. 5. **Markdown rendering**: proposals' body is currently rendered as `
` whitespace-preserved text. Pulling in `react-markdown` would give links, lists, and the `[policy:slug#section]` citation syntax from the roadmap doc. Defer until adoption is real.
6. **No notifications on incident status change**. The activity_log row is inserted but no in-app comms message or email is sent. Add later when notification infra is ready.
7. **Rename `middleware.ts` → `proxy.ts`** for Next.js 16 (still works, deprecation warning only).

### Decisions pending

- Whether to back-port Tier 2 features (skill/interest fields, COI disclosure, mid-tenure card review trigger, financial dashboard visibility settings) in the next pass.
- Whether to add markdown rendering with citation hover-cards now or wait.
- Whether to add a worker / cron to auto-flip expired proposals.

### Files changed this pass

```
Added (schema):
  scripts/016_governance_kernel.sql

Added (lib):
  lib/actions/proposals.ts
  lib/actions/incidents.ts
  lib/actions/policies.ts

Added (tests):
  __tests__/governance.test.ts

Added (UI):
  app/(app)/proposals/page.tsx
  app/(app)/proposals/proposal-badges.tsx
  app/(app)/proposals/new/page.tsx
  app/(app)/proposals/new/new-proposal-form.tsx
  app/(app)/proposals/[id]/page.tsx
  app/(app)/proposals/[id]/proposal-actions.tsx
  app/(app)/incidents/page.tsx
  app/(app)/incidents/new/page.tsx
  app/(app)/incidents/new/incident-form.tsx
  app/(app)/incidents/[id]/page.tsx
  app/(app)/incidents/[id]/incident-actions.tsx
  app/(app)/policies/page.tsx
  app/(app)/policies/[slug]/page.tsx
  app/(app)/policies/[slug]/policy-actions.tsx
  app/(app)/policies/new/page.tsx
  app/(app)/policies/new/new-policy-form.tsx

Edited:
  scripts/schema.sql                  (added Section 9: governance kernel)
  scripts/README.md                   (listed migration 016)
  lib/types.ts                        (governance entity types + enum unions)
  lib/validations.ts                  (11 new Zod schemas + enum exports)
  lib/actions/index.ts                (re-export the three new modules)
  components/app-sidebar.tsx          (Governance section with 3 links)
  docs/GOVERNANCE_FEATURES.md         (Tier 1 marked SHIPPED)
  docs/HANDOFF.md                     (this entry)
```

### Deployment steps for this pass

On any existing database:

```sql
-- In Supabase SQL editor or psql:
\i scripts/016_governance_kernel.sql
```

For a fresh deploy, the canonical `scripts/schema.sql` already includes it (Section 9). No app-level config changes needed; the new `spaces` columns ship with sensible defaults (`default_quorum_percent=10`, `default_quorum_floor=1`, `default_voting_window_hours=216` (9 days), `default_threshold='simple_majority'`, `incident_sla_hours=72`).

After applying, restart PostgREST (`docker compose restart rest` on self-hosted) so the schema cache refreshes.

---

## 2026-05-14 (pass 4) — Deploy readiness + self-hosted DO guide

Branch: `main`
Author: Claude (no commits made).

### What I did

1. **Verified deploy readiness** end to end:
   - `pnpm vitest run`: 202 tests pass across 7 files.
   - `pnpm build`: success. 19 routes emitted (including `/api/health`, `/api/paypal/sync`, `/auth/callback`, `(landing) /`, all `(app)/*` routes).
   - `pnpm exec tsc --noEmit`: 157 errors, all pre-existing supabase-js generic drift, all masked by `typescript.ignoreBuildErrors: true` in `next.config.mjs`. No new errors introduced.
2. **Fixed one self-inflicted test bug**: `__tests__/auth-helpers.test.ts` was written in pass 3 using the old `r.error` access pattern, which does not narrow now that `Result` is a tagged union (`ok: true | false`). Rewrote each test to gate on `if (r.ok)` / `if (!r.ok)` before reading `data` or `error`.
3. **Surfaced two non-blocking build warnings** that the user should know about:
   - Next.js 16 deprecates the `middleware` file convention in favour of `proxy`. Build still works with `middleware.ts`; rename to `proxy.ts` whenever convenient.
   - Next.js detected a stray `/Users/obsidian/Projects/package-lock.json` outside the repo (workspace root inference). Local-only nuisance, does not affect CI or DO builds.
4. **Wrote `docs/DEPLOY_DO_SELFHOSTED.md`**: complete end-to-end guide for running the entire stack on one DigitalOcean Droplet with self-hosted open-source Supabase. 16 sections, ~700 lines, copy-pasteable commands throughout. Covers:
   - Architecture and cost (~$48/mo for 8 GB Droplet).
   - Droplet provisioning, hardening, Docker/Nginx/Certbot install.
   - DNS records for `yourdomain.com`, `supabase.yourdomain.com`, `studio.yourdomain.com`.
   - Self-hosting Supabase: cloning `supabase/supabase`, generating `JWT_SECRET` / `ANON_KEY` / `SERVICE_ROLE_KEY` via a Node one-liner, full `.env` template, port-remap override (Studio defaults to 3000 and collides with the Next.js app; doc rebinds it to 3001 and forces all internal ports to `127.0.0.1` to keep them off the public internet).
   - Applying `scripts/schema.sql`, `014_*.sql`, `015_*.sql` via `docker compose exec db psql`.
   - Building the app image with `NEXT_PUBLIC_*` build args (otherwise the client bundle ships with `undefined` for them — common gotcha).
   - Nginx server blocks for app, Supabase API, and basic-auth-gated Studio.
   - Let's Encrypt SSL covering all four hostnames.
   - Post-deploy verification including a regression test for migration 015's privilege-escalation guard.
   - Daily `pg_dump` backups with 14-day local rotation and an off-site path to DO Spaces.
   - Operations: app upgrades, incremental schema migrations (with `docker compose restart rest` to refresh PostgREST's schema cache), Supabase version upgrades.
   - Hardening checklist (14 items) and a troubleshooting section covering the eight problems people actually hit.
5. **Linked the new doc** from `README.md` (deployment table + body section) and from the top of `docs/DEPLOYMENT.md`. Made it explicit that `DEPLOYMENT.md` covers managed-Supabase paths and `DEPLOY_DO_SELFHOSTED.md` covers full self-hosting.

### Deployment quick reference (so the next session does not have to re-read everything)

| Path | When to use | Guide |
|------|-------------|-------|
| Vercel + managed Supabase | Fastest path, no infra to manage | `docs/DEPLOYMENT.md` section 4 |
| DO App Platform + managed Supabase | Want DO but not infra | `docs/DEPLOYMENT.md` section 5, spec at `.do/app.yaml` |
| DO Droplet + managed Supabase | Container-only on your VPS, Supabase Cloud for DB | `docs/DEPLOYMENT.md` section 6 |
| DO Droplet + self-hosted Supabase | Full data ownership, single Droplet | `docs/DEPLOY_DO_SELFHOSTED.md` |

Migrations to run on every fresh deploy, in order:
1. `scripts/schema.sql` (canonical, idempotent)
2. `scripts/014_member_user_id_nullable.sql`
3. `scripts/015_prevent_member_self_role_change.sql` (security; do not skip)

### Open items, priority order

1. **Run migration 015 on any live database** before external users sign in. Without it, any authenticated member can `UPDATE space_members SET role = 'admin'` on their own row via direct PostgREST.
2. **Rename `middleware.ts` to `proxy.ts`** to clear the Next.js 16 deprecation. Single-file rename; the export and matcher contract is unchanged.
3. **Regenerate `types/database.ts`** via `supabase gen types typescript --project-id ` (managed) or against the self-hosted Postgres. Clears the 157 TS errors. Then drop `typescript.ignoreBuildErrors: true` from `next.config.mjs` so future type drift surfaces in CI.
4. **Wire OAuth providers** in Supabase dashboard (GitHub, Google). The client code already calls `supabase.auth.signInWithOAuth`; only the provider configuration is missing.
5. **Encrypt `secrets.value` and `integrations.config`** before storing real production credentials.
6. **Stale docs**: `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` still claim 4 default channels. Schema creates 3 (`general`, `announcements`, `ops`).
7. **`signUp` trigger metadata mismatch** in `scripts/schema.sql` (`handle_space_signup` reads `space_id` but the app never passes it). Benign; trigger short-circuits. Either remove the trigger or rewire it.

### Decisions pending from the user

- Whether to rename `middleware.ts` → `proxy.ts` now or wait for a future Next.js major version that removes the alias.
- Whether to regenerate `types/database.ts` against managed Supabase (`supabase gen types` requires the project ref) or wait until self-hosting is up and run it against that.
- Whether to commit the pass 1–4 changes as one large commit, four sequenced commits matching the pass boundaries, or branch-and-PR per concern.

### How to verify locally

```bash
pnpm install
pnpm vitest run                                # 202 tests, 7 files
pnpm build                                     # success in ~3s; warnings about middleware/lockfile are non-blocking
pnpm exec tsc --noEmit 2>&1 | grep -v never | wc -l   # remaining non-Supabase errors should be 0
```

### Files changed this pass

```
Added:
  docs/DEPLOY_DO_SELFHOSTED.md

Edited:
  __tests__/auth-helpers.test.ts
  README.md
  docs/DEPLOYMENT.md
  docs/HANDOFF.md
```

No application code, no schema, no UI, no business rules were changed in pass 4. The pass added a major deployment guide and confirmed the work from passes 1–3 produces a clean build and a green test suite.

---

## 2026-05-14 (pass 3) — Architecture refactor + real tests

Branch: `main`
Author: Claude (no commits made).

### What I did

1. **Split `lib/actions.ts` (914 lines, 29 functions) into 10 domain files** under `lib/actions/`. Each file owns one entity:
   - `tasks.ts`, `projects.ts`, `members.ts`, `contacts.ts`, `payments.ts`, `knowledge-base.ts`, `secrets.ts`, `area-leads.ts`, `settings.ts`, `imports.ts`
   - `lib/actions/index.ts` re-exports everything via `export * from './foo'`
   - Every client import `from '@/lib/actions'` still resolves correctly.
2. **Created `lib/permissions.ts`**: role constants (`ROLES`, `ADMIN_ROLES`, `TREASURER_ROLES`, `ACTIVE_STATUSES`), `Role` type alias, `hasRole(role, allowed)` helper. Removes magic-string role chains.
3. **Created `lib/auth-helpers.ts`**: `getAuthMember`, `requireMember`, `requireMemberWithRole`, `parseInput`, `logActivity`. Result types use the canonical `ok: boolean` discriminator so TypeScript narrows on `if (!r.ok)` reliably.
4. **Wired validation into every server action** that has a schema (was previously only `createTask` and `claimTask`). 18 more actions now reject bad input at the boundary instead of hoping the database rejects it.
5. **Expanded `lib/types.ts`** with re-exports for `Role`, `Member`, `MemberSummary`, `Result`, `MemberResult`, `ActionResult`, and the role/permission constants. One-stop import for typed work.
6. **Deleted dead code**: `app/auth/login/page.tsx` (duplicate login linking to nowhere), `components/app-shell.tsx` (unused, hardcoded badge counts).
7. **Wrote real tests**: 76 new tests across four files exercise real project code instead of inline assertions.
   - `__tests__/validations.test.ts`: 37 tests verifying every Zod schema accepts real DB enums and rejects legacy ones.
   - `__tests__/permissions.test.ts`: 10 tests for role constants and `hasRole`.
   - `__tests__/security.test.ts`: 25 tests for `sanitize*`, `escapeHtml`, `checkRateLimit`, `isValidUuid`.
   - `__tests__/auth-helpers.test.ts`: 4 tests for `parseInput`.
   - Full suite: **202 tests, 7 files, all passing**.
8. **Fixed vitest config**:
   - `vitest.setup.ts` had inline JSX in a `.ts` file (esbuild refuses). Replaced with `React.createElement`.
   - `vitest.config.ts` was picking up Playwright e2e specs. Added `include: ['__tests__/**/*.{test,spec}.{ts,tsx}']` and `exclude: ['e2e/**']`.
9. **Added `scripts/README.md`** documenting the migration convention (canonical `schema.sql` + numbered incremental files, idempotency rules, security baseline, verification queries).

### Open items, priority order

1. **TypeScript drift between `@supabase/supabase-js` generics and our `Database` type**: 38 errors in `lib/actions/*` of the form `'X' is not assignable to parameter of type 'never'`. These existed in the original `lib/actions.ts` too (the build masks them via `next.config.mjs: typescript.ignoreBuildErrors: true`). Fix is to regenerate `types/database.ts` with the latest supabase CLI so the generated `Database` shape matches what `@supabase/ssr` expects.
2. **103 app/ TypeScript errors** in client components (`comms-client.tsx`, `members-client.tsx`, etc). Pre-existing v0 codegen output; same root cause as item 1. Untouched in this pass to avoid UI regressions.
3. **`getCurrentMember` was removed** from `lib/actions.ts` when I split it; no caller referenced it. If anything starts to need it, define a server action in `lib/actions/index.ts` or use `getAuthMember` from `lib/auth-helpers.ts`.
4. **Pass 2 priority 1 (run `scripts/015_*.sql` on production) is still outstanding** until the user runs it.
5. **`signUp` trigger metadata mismatch** remains documented but unfixed.
6. **Stale docs**: `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` say 4 default channels. Schema creates 3.

### Decisions pending

- Whether to regenerate `types/database.ts` via the Supabase CLI to clear the remaining 141 TS errors.
- Whether to also run `next.config.mjs` without `typescript.ignoreBuildErrors: true` once types are clean.
- Whether to add CI to run `pnpm vitest` and `pnpm exec tsc --noEmit` on every push.

### Files changed this pass

```
Added:
  lib/permissions.ts
  lib/auth-helpers.ts
  lib/actions/tasks.ts
  lib/actions/projects.ts
  lib/actions/members.ts
  lib/actions/contacts.ts
  lib/actions/payments.ts
  lib/actions/knowledge-base.ts
  lib/actions/secrets.ts
  lib/actions/area-leads.ts
  lib/actions/settings.ts
  lib/actions/imports.ts
  lib/actions/index.ts
  __tests__/validations.test.ts
  __tests__/permissions.test.ts
  __tests__/security.test.ts
  __tests__/auth-helpers.test.ts
  scripts/README.md

Edited:
  lib/types.ts
  vitest.config.ts
  vitest.setup.ts

Removed:
  lib/actions.ts            (replaced by lib/actions/ directory)
  app/auth/login/page.tsx   (dead duplicate)
  components/app-shell.tsx  (dead, unused)
```

### How to verify locally

```bash
pnpm install
pnpm vitest run
# expect: 202 tests passing across 7 files
```

---

## 2026-05-13 (pass 2) — Application repairs

Branch: `main`
Author: Claude (no commits made).

### What I did

1. **Fixed dashboard "Quick Chores" bug** (`app/(app)/dashboard/page.tsx:49`). The filter was `.neq('status','done')` but `completeTask()` writes `'completed'`. Completed tasks were leaking into the open list. Replaced with a positive filter on the open statuses.
2. **Fixed signup copy**: said default channels were "general, announcements, random". Schema creates "general, announcements, ops". Aligned.
3. **Fixed `lib/validations.ts` enum drift**: nine schemas had values that did not match the database. Now aligned: task type and recurrence, project status, member role, contact type, KB visibility, cash payment shape, settings, area lead.
4. **Wired validation into high-risk server actions**: `createProject`, `updateProjectStatus`, `addMember`, `logCashPayment`, `linkPaymentToMember`. Other actions still pass raw `formData`; documented in `docs/AUDIT.md` section 4.4.
5. **Patched privilege escalation in `space_members`**: added a `BEFORE UPDATE` trigger (`prevent_member_self_role_change`) that rejects self-updates of `role`, `tier`, `status`, `approved`, `has_card_access`, `space_id` unless the user is already admin/board/treasurer in that space. Applied to `schema.sql`. Shipped as incremental migration `scripts/015_prevent_member_self_role_change.sql` for existing databases. Run this on any production database immediately.
6. **Removed duplicate auth functions** in `lib/actions.ts` (`signIn`, `signOut`, `getUser`). Canonical versions live in `lib/auth-actions.ts`. Verified no caller imports them from `actions.ts`.
7. **Deleted dead scripts**: `fix-actions.js`, `fix-sidebar.js`, `fix-sidebar.mjs`, `patch-dashboard.mjs`. All had hardcoded `/vercel/share/v0-project` paths.
8. **Synced `types/database.ts`** for `space_members.user_id` to `string | null` matching the schema fix from pass 1.

### Open items, priority order

1. **Run `scripts/015_prevent_member_self_role_change.sql` on production** before any external user can authenticate. Until then, any member can self-promote to admin via direct PostgREST.
2. **`app/auth/login/page.tsx`** is a dead duplicate login page linking to non-existent `/auth/onboarding`. Delete the directory.
3. **`components/app-shell.tsx`** is dead (no importer). Delete.
4. **Wire validation into remaining actions** listed in `docs/AUDIT.md` 4.4 (updateMember, contacts, KB, secrets, area lead, settings, integrations, imports).
5. **Secrets at rest**: still plain text.
6. **`signUp` trigger metadata mismatch** is benign (trigger short-circuits) but should be removed or re-aligned. See `docs/AUDIT.md` 4.5.
7. **Stale docs**: `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` still say 4 default channels. Schema creates 3.

### Decisions pending

- Whether to wire validation into the remaining actions in the next session.
- Whether to delete `app/auth/login/` and `components/app-shell.tsx` now.
- Whether to regenerate `types/database.ts` properly via `supabase gen types` instead of the manual edit applied here.

### How to verify the security fix

```sql
-- Connect to your Supabase project's SQL editor as a regular user (NOT service role):
-- The following should now fail with "Members cannot change their own role..."
UPDATE public.space_members SET role = 'admin' WHERE user_id = auth.uid();
```

### Files changed this pass

```
Edited:
  app/(app)/dashboard/page.tsx
  app/signup/page.tsx
  lib/validations.ts
  lib/actions.ts
  scripts/schema.sql
  types/database.ts
  docs/AUDIT.md
  docs/HANDOFF.md

Added:
  scripts/015_prevent_member_self_role_change.sql

Removed:
  scripts/fix-actions.js
  scripts/fix-sidebar.js
  scripts/fix-sidebar.mjs
  scripts/patch-dashboard.mjs
```

---

## 2026-05-13 (pass 1) — Deep audit + fresh-deploy hardening

Branch: `main`
Author: Claude (per CLAUDE.md, no commits made this session).

### What I did

1. Wrote `CLAUDE.md` at the repo root. It is the standing brief: no Claude attribution in commits, no emojis, no em dashes, no AI tells, test-driven where practical, always document, handoff after long sessions, smart separation of concerns.
2. Audited every source file. Findings in `docs/AUDIT.md`.
3. Fixed three idempotency / fresh-deploy issues in `scripts/schema.sql`:
   - `space_members.user_id` is now nullable (offline members work).
   - Realtime `ALTER PUBLICATION` wrapped in `DO ... EXCEPTION` blocks.
   - All RLS policies preceded by `DROP POLICY IF EXISTS`.
4. Added `scripts/014_member_user_id_nullable.sql` for upgrading existing databases.
5. Added every missing deployment artifact: `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `.do/app.yaml`, `.env.example`, `app/api/health/route.ts`.
6. Updated `middleware.ts` to let `/api/health` through without auth.
7. Updated `docs/DEPLOYMENT.md` to reference bundled artifacts instead of asking the user to write their own.

### What I did not touch

- Application logic (`lib/actions.ts`, `lib/auth-actions.ts`, all `*-client.tsx`).
- UI components.
- Tests.
- Validation schemas (they are wrong in places, see audit section 4.1, but fixing them is a behavior change).

### Open items, in priority order

1. **Privilege escalation on `space_members.role`** (audit 4.11). A self-update via the `members_update` RLS policy lets a member rewrite their own row, including `role`. Fix before production.
2. **Validation schema enum mismatches** (audit 4.1). Six schemas in `lib/validations.ts` reject valid database values. Wire schemas into actions and align the enums.
3. **Dead code**: four `scripts/fix-*.{js,mjs}` files with hardcoded `/vercel/share` paths, and `components/app-shell.tsx` which no page imports. Delete on next cleanup.
4. **Duplicate auth functions**: `lib/actions.ts` lines 30 to 47 redeclare `signIn`, `signOut`, `getUser` that already live in `lib/auth-actions.ts`. Consolidate.
5. **Secrets at rest**: `secrets.value` and `integrations.config` are plain text. Encrypt before production.
6. **UI-only features**: OAuth buttons, CSV import processor, webhook endpoint, payment platform syncs (except PayPal), email notifications. See audit 4.7.
7. **`package.json` name is `"my-project"`**. Cosmetic.
8. **Older docs reference 4 default chat channels**; schema creates 3 (`general`, `announcements`, `ops`). `DB_SCHEMA_MAP.md` and `docs/ARCHITECTURE.md` are stale on this point.

### Decisions pending from the user

- Whether to delete dead scripts now (low risk) or in a separate change.
- Whether to apply `scripts/014_member_user_id_nullable.sql` against the live database (production may already have user_id nullable per `DB_SCHEMA_MAP.md`, this migration is idempotent).
- Whether to wire validation schemas into existing server actions in the next session.

### How to verify the fresh-deploy fixes locally

```bash
# 1. Spin up a fresh Supabase project at supabase.com
# 2. In Supabase SQL Editor, paste scripts/schema.sql and run it. Should report no errors.
# 3. Re-run scripts/schema.sql. Should still report no errors (idempotent).
# 4. Copy .env.example to .env.local, fill in Supabase URL + anon + service role.
pnpm install
pnpm dev
# 5. Visit http://localhost:3000, sign up, create a space, confirm /dashboard renders.
# 6. curl http://localhost:3000/api/health  -> {"status":"ok"}
```

### How to verify the Docker build

```bash
# Requires .env.local populated.
docker compose build
docker compose up -d
curl http://localhost:3000/api/health
docker compose logs -f app
```

### Files changed this session

```
Edited:
  middleware.ts
  scripts/schema.sql
  docs/DEPLOYMENT.md

Added:
  CLAUDE.md
  Dockerfile
  .dockerignore
  docker-compose.yml
  .do/app.yaml
  .env.example
  app/api/health/route.ts
  scripts/014_member_user_id_nullable.sql
  docs/AUDIT.md
  docs/HANDOFF.md
```

No git commits were made. The working tree carries all changes; the user decides what to commit and when.