From 64d7d626db31a0bb20d7aa5b132f4465ec4ad63f Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Fri, 15 May 2026 22:13:03 -0700 Subject: [PATCH] docs: pass-17 CHANGELOG / HANDOFF / schema migration table Records the UX polish tranche and the incident-RLS diagnosis (incl. the discriminating production SQL) and adds migration 025 to the schema migration table. --- CHANGELOG.md | 6 ++++++ docs/DATABASE_SCHEMA.md | 5 +++-- docs/HANDOFF.md | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5ceef..87e95c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ All notable changes to this project are documented here. Format follows [Keep a - `types/database.ts` brought in sync with the schema (8 tables, the `comment_entity_type` enum, and the new columns); docs (`DATABASE_SCHEMA.md`, `DB_SCHEMA_MAP.md`, `API_REFERENCE.md`) refreshed for migrations 016-024. - The member directory has a per-row "make area lead" assignment for admin/board (mirrors Customize -> Area leads). +### Fixed +- Production report: a basic member filing an incident hit `new row violates row-level security policy for table "incidents"`, including the anonymous path. Not reproducible from source (the code and the 016/017 policy both permit it). Migration 025 idempotently re-asserts the hardened `incidents_insert` policy verbatim so the deployed policy converges to the known-good definition (access-neutral, no schema change). The file-incident action now maps an RLS rejection to an actionable message and logs the technical detail server-side. If the report persists after deploy, the root cause is not policy drift; see `docs/HANDOFF.md` for the discriminating queries. + +### UX +- Accessibility and consistency polish pass (Tier 1 systemic, in progress): distinct Ops nav icon (was a duplicate of Settings), `aria-current` on active nav, accessible mobile drawer (role/aria-modal, Escape, scroll lock, focus management), skip-to-content link, shared `BrandMark` in the sidebar, a shared `PageTitle`/`SectionTitle` primitive adopted app-wide, and a single shared `Empty` primitive replacing ~18 ad-hoc empty states (projects gains a real empty state with a CTA). + ### Security - Closed two member self-escalation paths: a non-privileged member can no longer change their own `tier_id` or `onboarding_completed_at` on the `space_members` row (migration 024 extends the self-change trigger). Onboarding completion now goes through the service client after server-side validation. - Added in-code space scoping to forum/comment/channel mutations (defence-in-depth on top of RLS). diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 37c5a23..b055e75 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -6,7 +6,7 @@ > **Source of Truth**: `scripts/schema.sql` (canonical, idempotent); numbered > migrations `scripts/0NN_*.sql` upgrade existing deployments. The 13-table > reference below is the original baseline; tables added by migrations -> 014-024 are summarized in the "Migrations 014-024" section at the end. +> 014-025 are summarized in the "Migrations 014-025" section at the end. --- @@ -686,7 +686,7 @@ CREATE INDEX idx_activity_space ON activity_log(space_id, created_at); --- -## Migrations 014-024 (additions since the 13-table baseline) +## Migrations 014-025 (additions since the 13-table baseline) `scripts/schema.sql` is the canonical idempotent schema; each numbered migration is mirrored as a section in it. Tables/columns added: @@ -704,6 +704,7 @@ migration is mirrored as a section in it. Tables/columns added: | 022 | `space_onboarding_steps`; `space_members.onboarding_completed_at/onboarding_progress`; seed default steps | | 023 | `space_role_permissions`, `ops_acl`; `user_effective_roles()`, `user_has_permission()` (SECURITY DEFINER); `secrets`/`knowledge_base` SELECT rewritten as `existing-rule OR ops_acl-match` (additive) | | 024 | `prevent_member_self_role_change()` extended to also block self-change of `tier_id` and `onboarding_completed_at` | +| 025 | Re-asserts the hardened `incidents_insert` policy verbatim (production convergence; access-neutral, no schema change) | New enum: `comment_entity_type` = `forum_thread | proposal | incident | policy`. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index d931882..055b734 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,6 +4,39 @@ Append-only. Newest entries on top. Keep each entry to one screen. --- +## 2026-05-15 (pass 17) — UX polish (Tiers 1-3) + production incident-RLS report + +Branch: `main`. Migration: 025 (applies on next deploy). Not pushed. + +### UX polish (in progress, Tiers 1-3 of the audit) + +- Full UI/UX audit done (5 partitioned agents, code-level; no live browser, auth routes need seed). Findings report delivered to the user, grouped Tier 1 systemic / Tier 2 first-run / Tier 3 security-adjacent / Tier 4 brand / Tier 5 per-surface. User chose: implement Tiers 1-3 this pass, pause before 4-5; sidebar nav stays as-is (no role-filtering); comments get one-level nesting + edit (Tier 5, deferred to post-pause). +- Committed (local, build+test green 271/271 each): (1) accessible sidebar/drawer + distinct Ops icon + skip link + BrandMark; (2) shared `PageTitle`/`SectionTitle` primitive adopted across 21 files; (3) shared `Empty` primitive replacing ~18 ad-hoc empty states (projects gains a real empty state). +- Still open in Tier 1: Dialog/AlertDialog adoption (hand-rolled modals, `confirm()` x13), focus-visible, tap targets, label associations, silent-failure toasts. Tier 2/3 not started. + +### Production incident-filing RLS report (priority interrupt) + +User screenshot: a basic active member filing an incident at `/incidents/new` (anonymous checked) gets `new row violates row-level security policy for table "incidents"`. Production is on the OLD deployed commit (sidebar still `{hs}`), so this is pre-existing, not from this session. + +- Not reproducible from source: `fileIncident` sets `reporter_id=NULL` when anonymous and `space_id=member.space_id`; `getAuthMember` resolves by `user_id=auth.uid()`; the 016/017 `incidents_insert` policy allows exactly this. So it is a production deploy/data divergence. +- Three candidate root causes: (1) `auth.uid()` NULL inside the DB request (SSR session not reaching PostgREST); (2) deployed `incidents_insert` policy differs from 016/017; (3) acting `space_members.user_id` != JWT sub on production (data drift). +- Defensive fixes prepared (user-approved, blind, NOT pushed): `scripts/025` idempotently re-asserts the hardened `incidents_insert` policy verbatim (access-neutral; fixes cause #2 only); `fileIncident` maps RLS rejection to an actionable message and logs detail server-side. `scripts/schema.sql` already matches (no change). + +### Pending decision / next step + +Run these on the production DB (service role) to disambiguate before relying on 025: + +```sql +SELECT polname, pg_get_expr(polwithcheck, polrelid) AS with_check +FROM pg_policy WHERE polrelid = 'public.incidents'::regclass AND polcmd = 'a'; +SELECT id, space_id, user_id, status, role FROM public.space_members WHERE user_id = ''; +SELECT public.get_user_space_ids(''::uuid); +``` + +If query 1 already shows the 016/017 expression and query 2 shows a matching `user_id` with the space present, 025 will NOT fix it (cause #1 or #3); investigate the SSR Supabase client / membership data instead. + +--- + ## 2026-05-15 — SESSION CLOSE: state of the app One-screen status for the next session. Detailed history in the pass entries below.