Commit Graph
105 Commits
Author SHA1 Message Date
Moheeb Zara 41193d3f68 docs: HANDOFF pass-39 (backlog complete; product spine is the next thread) 2026-05-17 02:03:01 -07:00
Moheeb Zara d3481129a8 docs: HANDOFF pass-38 (mobile card tables; bulk/a11y remaining) 2026-05-17 01:54:55 -07:00
Moheeb Zara 51f733414b docs: HANDOFF pass-37 (dashboard attention + cached perms; remaining queued) 2026-05-17 01:37:35 -07:00
Moheeb Zara e7463c0b95 docs: HANDOFF pass-36 (F1/F2 + UX pack part 1) 2026-05-17 01:24:48 -07:00
Moheeb Zara 363e2a667f docs: HANDOFF pass-35 clean checkpoint + next-session kickoff prompt 2026-05-17 00:59:56 -07:00
Moheeb Zara 2427b6b822 docs: HANDOFF pass-34 deploy-state (deployed, run 25985079088) 2026-05-17 00:52:01 -07:00
Moheeb Zara 8befee903c docs: HANDOFF pass-34 (email-link bugfix root cause + fix) 2026-05-17 00:32:03 -07:00
Moheeb Zara c2f8368634 docs: HANDOFF pass-33 deploy-state (deployed, run 25984491717) 2026-05-17 00:22:14 -07:00
Moheeb Zara cec2d69097 docs: deleteForm cascade + deleteSubmission/relinkAll; HANDOFF pass-33
API_REFERENCE + ARCHITECTURE updated (deleteForm now permanently
cascades incl. signed waivers; immutability caveat; new deleteSubmission
+ relinkAllSubmissions). Corrected pass-32 header to DEPLOYED.
2026-05-17 00:12:43 -07:00
Moheeb Zara bff8deac55 docs: HANDOFF pass-32 deploy-state (deployed, run 25984078567) 2026-05-17 00:01:31 -07:00
Moheeb Zara 61c97e3b8d docs+landing: forms email-link decision documented; landing mobile polish
Docs: API_REFERENCE + ARCHITECTURE updated to the owner-chosen
email-match association model (incl. anonymous, tradeoff documented) and
listMemberSubmissions; HANDOFF pass-32; corrected pass-31 header.
Landing: tightened the <=600px breakpoint (nav/section/card padding) and
added a <=400px block so the nav fits ~320px without overflow. CSS-only
inside media queries; desktop unchanged.
2026-05-16 23:54:46 -07:00
Moheeb Zara a364d152ca docs: HANDOFF pass-31 deploy-state (deployed, run 25982959375) 2026-05-16 23:02:19 -07:00
Moheeb Zara d0aef9a6aa docs: correct ARCHITECTURE layout/§7, note intentional settings + single-space
Project tree + section 7 module paths now reflect the per-domain
lib/actions/* reality (no monolithic lib/actions.ts); explicit note that
/settings admin secret loading is by-design same-space management;
getAuthMember single-space invariant documented in code. HANDOFF pass-31.
2026-05-16 22:44:21 -07:00
Moheeb Zara 0bb934d5bd docs: HANDOFF pass-30 deploy-state (deployed, run 25982428014) 2026-05-16 22:32:30 -07:00
Moheeb Zara c12a103e11 docs: HANDOFF pass-30 (presence module + full audit pass + deferred items) 2026-05-16 22:30:43 -07:00
Moheeb Zara e7475d954c docs: HANDOFF pass-29 deploy-state (deployed, run 25981712253) 2026-05-16 21:53:30 -07:00
Moheeb Zara 46b294286a docs: API_REFERENCE + ARCHITECTURE + HANDOFF pass-29 (class form gate, rosters, /doors) 2026-05-16 21:48:46 -07:00
Moheeb Zara 268d78dd6d docs: HANDOFF pass-28 deploy-state (self-entry deployed, run 25981195445) 2026-05-16 21:23:50 -07:00
Moheeb Zara 6d1c8536cd feat(door): member self-entry on the dashboard (locked design)
selfEntry({connectionId}): any active member with >=1 active card on file
may trigger a momentary open on a connection that is enabled and has
allow_member_self_entry on (locked rule: any active card, no slot row
required). Membership/cards resolved server-side so a member can only open
for themselves; never unlock/lock/grant/revoke; never anonymous. Strict
5/min per-member rate limit, one redacted self_entry audit row (denials
audited too), through the existing hardened executor. listSelfEntryDoors
returns the enabled self-entry connections only when the caller has an
active card, so the dashboard 'Door access' panel is hidden for
ineligible members. API_REFERENCE + ARCHITECTURE + HANDOFF pass-28.
2026-05-16 21:21:09 -07:00
Moheeb Zara 14a2554d06 docs: HANDOFF pass-27 deploy-state (Door P3 deployed, run 25981082371) 2026-05-16 21:17:18 -07:00
Moheeb Zara d0478202b6 docs(door): P3 ARCHITECTURE + HANDOFF pass-27 (slot allocator, live actions, self-entry checkpoint) 2026-05-16 21:15:08 -07:00
Moheeb Zara 7ea3dea697 docs: HANDOFF pass-26 — clean checkpoint, Door P3-P5 plan 2026-05-16 20:42:57 -07:00
Moheeb Zara 5e40b91550 docs: HANDOFF pass-25 (Door P1 deployed, P2 local) 2026-05-16 20:32:21 -07:00
Moheeb Zara 1cbea138ba docs: HANDOFF pass-24 (door design approved, P1 + nav local, adapter facts re-verified) 2026-05-16 20:17:30 -07:00
Moheeb Zara f32743d8bd feat(equipment): My reservations on /me + docs
/me gains a "My equipment reservations" section (the member's own
reservations with status and window). ARCHITECTURE documents the
equipment module; HANDOFF pass-23 records Classes deployed and the
code-complete-but-local equipment module.
2026-05-16 19:55:49 -07:00
Moheeb Zara 209622867f feat(classes): My classes on /me + docs
/me gains a "My classes" section (the member's own signups with status
and attendance). ARCHITECTURE documents the classes module; HANDOFF
pass-22 records the code-complete-but-local module and the reaffirmed
roadmap.
2026-05-16 19:38:37 -07:00
Moheeb Zara f783541a87 docs: mark production incident-INSERT RLS bug resolved (user-confirmed) 2026-05-16 19:18:24 -07:00
Moheeb Zara 70da0c3752 feat(markdown): KB/process render + working in-document anchor links
KB/process content was shown via a bare unsanitized ReactMarkdown in the
ops view dialog (the /ops/[id] route is the editor, not a view). Route
it through the shared SafeMarkdown instead. New dependency-free
lib/markdown-anchors.ts (heading-text flatten, GitHub-ish slug,
per-render dedupe, in-doc href test; unit-tested) is wired into both
SafeMarkdown and MarkdownBody so headings get stable ids and a
[link](#heading) resolves. In-document links no longer force a new tab,
which previously broke the anchor jump for policies/proposals too.
rehype-slug was not added (no offline install); the helper is equivalent
and tested.
2026-05-16 19:11:49 -07:00
Moheeb Zara 2687c1578d fix(secrets): honor ops.secrets.read permission for reveal/list
A regular member granted reveal access either via the ops.secrets.read
role permission or a per-secret ops_acl entry could not actually see or
reveal secrets: revealSecret hard-gated on built-in role admin/board
(ignoring both the permission and the ACL), and secrets_select RLS never
consulted the permission, and the Ops page short-circuited the secrets
fetch by built-in role.

Migration 031 additively adds an OR user_has_permission(...,
'ops.secrets.read') branch to secrets_select; the admin/board and
per-secret ops_acl branches are unchanged so deployment is access-neutral
unless an admin has granted the permission. revealSecret now resolves
the member and lets RLS be the boundary (a denied row simply does not
return). The Ops page always queries secrets (RLS filters the set) and
shows the section to admin/board, a permission holder, or anyone with an
ACL-granted secret. Write paths (create/update/delete) are unchanged.
Mirrored in schema.sql; migration docs and HANDOFF updated.
2026-05-16 19:07:06 -07:00
Moheeb Zara b2ac0e0271 feat(certifications): member-facing /me view + docs
/me shows the signed-in member their own certifications (status badges
from the shared logic) and a read-only view of their effective
permissions. Sidebar gets a "My access" link. ARCHITECTURE documents the
module; HANDOFF pass-21 records the UX fixes (deployed) and the
code-complete-but-local certifications module.
2026-05-16 19:01:57 -07:00
Moheeb Zara ad477252c8 feat(incidents): anonymous report tracking by token + public /track
Gap: fileIncident minted a reporter_token and the filing UI showed it
("the only way to look it up later") but there was no lookup UI.

- lib/incident-logic.ts (pure, tested): publicIncidentView redacts —
  board_only updates, subjects, decision-maker ids dropped; disposition
  only surfaced once decided/closed; status label mapping
- trackIncident: public service-client action (token is a 192-bit
  bearer credential on a UNIQUE column; anon has no RLS path so the
  service client is used AFTER token validation; generic not-found is
  enumeration-safe; board_only filtered at the query AND in logic)
- public /track page + client (also accepts ?token= deep link);
  /track added to middleware PUBLIC_ROUTES
- filing UI now deep-links /track?token=<code>
- trackIncidentSchema; __tests__/incident-tracking.test.ts (6)
- docs: API_REFERENCE + HANDOFF pass-20 (session arc, web-verified
  integration facts, locked forward roadmap, open items incl. the
  still-open production incident-INSERT RLS bug)

343 tests; build verified (Compiled successfully, exit 0).
2026-05-16 18:13:09 -07:00
Moheeb Zara 73eef7293a feat(forms): obvious publish flow, member Forms surface, templates (P8)
- builder: draft/published/closed status banners; creating a form now
  lands on its edit page so Publish is immediately visible; a
  members-only form shows a "no public link" note instead of a dead URL
- member-facing forms: /my-forms list + /my-forms/[id] fill
  (MemberFillClient), "Forms" sidebar item for all members. Fixes the
  long-standing gap where members-only forms had no fill surface
- starter templates: lib/form-templates.ts + a picker on /forms/new
  (Liability waiver, Membership application, Event RSVP, Photo release,
  Blank) that seeds builder state (no table)
- forms-client relabels members-only forms (was showing a misleading
  /f/<space>/<slug> that always 404s for members-only)
- docs corrected per the audit pass: DATABASE_SCHEMA / DB_SCHEMA_MAP /
  ARCHITECTURE / API_REFERENCE now reflect migrations 014-029, per-space
  slug, /f/[space]/[slug], getPublicForm({space,slug}), invite roles;
  HANDOFF pass-19 added with residual gaps

337 tests; build verified (Compiled successfully, exit 0). No deploy.
2026-05-16 17:41:16 -07:00
Moheeb Zara 61403ad29f docs: HANDOFF — test-coverage hardening (forms-logic extraction, 333 tests) 2026-05-16 16:40:32 -07:00
Moheeb Zara 40d4cdad98 feat(forms): verified-email retro-link of anonymous submissions (Phase 5)
- claimMyAnonymousSubmissions(): exported but safe — no trusted params,
  resolves member + email from the caller's session, gated on
  email_confirmed_at, idempotent. Links the caller's own prior
  anonymous submissions to their member row
- hooked best-effort from joinSpace (member insert now returns id;
  links prior anon submissions for the verified email) and
  finishOnboarding
- addMember/importMembers deliberately NOT auto-linked: those emails
  are admin-asserted, not verified; the locked decision forbids
  auto-link there, so the forms.manage linkSubmissionsForMember stays
  the admin manual-link path
- also repairs a build break committed in 295d220: finishOnboarding
  declared `const admin` twice (Phase 4 added a second). The probe and
  the completion update now share one admin client
- docs: API_REFERENCE / ARCHITECTURE / HANDOFF

287/287 tests, build compiles clean (verified Compiled successfully). No deploy.
2026-05-16 16:29:32 -07:00
Moheeb Zara 295d220849 feat(forms): onboarding form/waiver step with required enforcement (Phase 4)
- scripts/027 (idempotent CHECK replace) + schema.sql Section 14: allow
  space_onboarding_steps.step_type = 'form'; referenced form id lives in
  the step's existing config.form_id (no new column)
- finishOnboarding: a required form step is satisfied when a submission
  for config.form_id by the member exists (any version — re-sign is
  non-blocking / auto-satisfy); fails open if the form is missing or
  unpublished so a misconfig cannot trap a member. Probe uses the
  service client (submissions are forms.manage-only under RLS)
- onboarding page enriches form steps with the published form def +
  prior-submission flag; onboarding-flow renders FormRenderer + waiver
  legal text/consent and submits via submitForm then marks the step done
- customize: forms fetched and threaded to the onboarding panel; new
  "+ Form step" button + form picker writing config.form_id
- validations/types/casts include 'form'; forms.test.ts +2
- docs: DATABASE_SCHEMA/DB_SCHEMA_MAP/API_REFERENCE/ARCHITECTURE/HANDOFF

287/287 tests, build exit 0. No deploy.
2026-05-16 15:15:17 -07:00
Moheeb Zara 03bb3a7466 feat(forms): builder UI, management pages, and public fill page (Phase 3)
- components/forms/form-renderer.tsx: one controlled renderer for all 8
  field types, shared by the builder preview and the public page
- components/forms/form-builder.tsx: easy builder — label-only field
  creation (keys auto-derived and de-duped at save), one-click type
  buttons, up/down reordering, inline options editor, live preview;
  edit mode adds publish/close/draft/delete and copy-public-link
- /forms directory, /forms/new, /forms/[id]/edit, /forms/[id]/results
  (CSV download). Server-gated by lib/forms-guard.ts (forms.manage via
  the same RPC the RLS uses; RLS remains the real boundary)
- public app/f/[slug] (outside the app group): getPublicForm serves
  only published public forms — members-only forms are never exposed
  there; waiver consent, anon email capture, signed-in gate for
  public_auth, thank-you state
- sidebar "Forms & waivers" link; sonner toasts (codebase convention)
- ARCHITECTURE + HANDOFF updated

285/285 tests, build clean, all four routes compiled. No deploy.
2026-05-16 15:03:48 -07:00
Moheeb Zara 897fa30ec1 feat(forms): server actions, Zod schemas, and dynamic answer validation (Phase 2)
- lib/validations.ts: form field/array schemas (8 field types,
  duplicate-key and select-options refinements) and create/update/
  status/submit/link envelope schemas; slug immutable on update
- lib/forms-schema.ts: pure parseFormSchema + validateAnswers; unknown
  answer keys are discarded so a caller cannot inject arbitrary jsonb
- lib/actions/forms.ts: create/update/setStatus/delete/list/results/
  CSV export (forms.manage via user_has_permission + RLS), submitForm
  (service-client read+write, visibility + waiver consent enforced,
  schema/legal/version snapshot, IP/UA capture), and
  linkSubmissionsForMember (manual admin link; auto path is Phase 5)
- updateForm bumps a published waiver's version on legal/schema change
  (non-blocking; existing submissions stay valid against their snapshot)
- deleteForm refuses when submissions exist (immutable records)
- types/database.ts: type user_has_permission/user_effective_roles
  (stale since migration 023)
- __tests__/forms.test.ts (14); API_REFERENCE + HANDOFF updated

285/285 tests, build clean. No deploy.
2026-05-16 14:51:20 -07:00
Moheeb Zara 66dd9b7ec0 harden(forms): self-audit fixes for Phase 1 RLS and indexes
- drop redundant idx_forms_slug (UNIQUE constraint already indexes slug)
- tighten forms_select: forms.manage sees all; ordinary members see
  only published forms (drafts/closed no longer exposed via PostgREST)
- make submitter_email and member_id submission indexes partial
  (both columns are frequently NULL)
2026-05-16 14:42:51 -07:00
Moheeb Zara 3a2d3ca13f feat(forms): schema + RLS for custom forms and waivers (Phase 1)
Adds the forms and form_submissions tables behind a custom form/waiver
builder. Phase 1 is schema + RLS only; server actions, builder UI, the
public /f/[slug] page, onboarding integration, and retro-linking follow
in later phases.

- scripts/026_forms.sql (idempotent), mirrored as schema.sql Section 17
- forms: globally-unique slug, kind (form/waiver), visibility
  (public_anon/public_auth/members), status, jsonb field schema,
  legal_text, version
- form_submissions: per-row snapshot of schema/legal-text/version for
  immutable waiver records, plus ip/user_agent for e-sign defensibility
- RLS additive + default-deny: forms write gated by forms.manage via
  user_has_permission; form_submissions has no write policy, so it is
  service-client-only and immutable; anon gets no grant on forms
- new forms.manage permission seeded to board + backfilled
- types/database.ts and DATABASE_SCHEMA/DB_SCHEMA_MAP/ARCHITECTURE/HANDOFF
  updated
2026-05-16 14:38:28 -07:00
Moheeb Zara 3ace1afa17 docs: polish Tiers 1-3 complete; Tier 2+3 local pending deploy 2026-05-16 14:14:04 -07:00
Moheeb Zara ca6caac005 docs: HANDOFF — Tier 1 complete (S3 local), forms decisions recorded 2026-05-16 14:03:57 -07:00
Moheeb Zara 97cb4d756a docs: HANDOFF reflects UX Tier 1 (minus S3) being deployed this push 2026-05-16 00:21:19 -07:00
Moheeb Zara b8bb213bf5 docs: HANDOFF/CHANGELOG reflect S5-S8 done, S3 + Tier 2/3 remaining 2026-05-16 00:01:11 -07:00
Moheeb Zara ea49e2129d docs: HANDOFF pass-17 reflects deployed incident hotfix + Tier 1 progress 2026-05-15 22:19:07 -07:00
Moheeb Zara 64d7d626db 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.
2026-05-15 22:13:03 -07:00
Moheeb Zara fa71d9a63e docs: HANDOFF session-close state-of-the-app summary 2026-05-15 22:12:52 -07:00
Moheeb Zara 4c9b55147b chore: clear audit backlog (types sync, email norm, docs, members shortcut)
Finishes all remaining tracked work from the full-app audit.

- types/database.ts synced: added the 8 missing tables (forum_threads,
  comments, space_tiers, space_invites, space_onboarding_steps,
  space_role_labels, space_custom_roles, space_member_custom_roles),
  the comment_entity_type enum (type + Constants), and the missing
  columns (space_members.tier_id/onboarding_completed_at/
  onboarding_progress, secrets.encrypted_value/encryption_version,
  knowledge_base.render_markdown).
- Email canonicalization: new emailField() helper (trim+lowercase)
  applied to signIn/signUp/addMember/updateMember/createContact and
  the import path; prevents case-variant duplicates and sign-in
  mismatches.
- Members directory: per-row "make area lead" picker (admin/board)
  assigns a member to any area-lead role; mirrors Customize.
- Docs refreshed and re-dated: DATABASE_SCHEMA.md (migrations 014-024
  summary), DB_SCHEMA_MAP.md (new-table quick map), API_REFERENCE.md
  (all server actions from migrations 016-024). schema.sql named as
  source of truth.

Backlog is now empty. 271/271 tests pass, build clean. No migration.
2026-05-15 19:01:52 -07:00
Moheeb Zara 86d44f6efc feat: import row validation + KB/process per-item ACLs
Continuing the audit backlog and finishing the permissions UI.

Import validation (recurring audit finding):
- Added importMembersSchema / importPaymentsCsvSchema. importMembers
  and importPaymentsCsv now validate per row (a single bad row no
  longer rejects the whole file), lowercase emails, normalize dates
  through flexibleDateTime(), enforce tier/platform enums and
  positive finite amounts, and return a `skipped` count instead of
  silently dropping invalid rows.

Permissions UI completion:
- OpsAclEditor is now wired into the KB/process viewer modal
  (admin/board), with entity type derived from the `process` tag.
  Per-item Ops access control now covers secrets + KB + processes,
  matching "all ops facilities stuff".

Left as-is with rationale in HANDOFF: /ops + /import denial paths
(unreachable behind the layout gate or already a clear non-leaking
denial); members-row make-area-lead shortcut (Customize -> Area
leads fully covers assignment); types/database.ts + docs regen
(pure hygiene, zero runtime/security impact, still tracked).

271/271 tests pass. Build clean. No migration this pass.
2026-05-15 18:33:53 -07:00
Moheeb Zara f5d670477b fix(security): close member self-escalation + harden after full audit
Full-application audit (5 parallel read-only audits). Two agent
"blockers" were false alarms and verified as already-correct. Real
findings fixed:

- Self-escalation (BLOCKER): migration 024 + schema.sql section 16
  extend prevent_member_self_role_change to also block a
  non-privileged member changing their own tier_id (self tier
  upgrade) and onboarding_completed_at (skipping required
  code-of-conduct via a raw PostgREST call). finishOnboarding and
  skipOnboarding now set onboarding_completed_at through the service
  client AFTER their server-side required-steps check, so the
  legitimate path is unaffected.
- Cross-tenant write scoping: added .eq('space_id', member.space_id)
  to updateForumThread, deleteForumThread, editComment, deleteComment,
  deleteChannel, renameChannel (RLS-only -> defence-in-depth).
- generateInviteCode: crypto.getRandomValues, widened to 8 chars
  (was Math.random, ~21 bits, enumerable under the join rate limit).
- SafeMarkdown: dropped iframe and the global style attribute
  (no unsandboxed embeds / UI-redress); markdown + safe HTML stay.
- /settings: server-side admin redirect before fetching space and
  integration config (was client-hidden only).
- app/(app)/layout.tsx: export const dynamic = 'force-dynamic'.
- Bounded queries: loadComments .limit(500), tasks .limit(2000).

Backlog from the audit is tracked in HANDOFF (import Zod schemas,
types/database.ts + docs regen, email lowercasing, unauthorized-
handling consistency, KB-modal ACL hookup).

271/271 tests pass. Build clean. Migrations 023 + 024 idempotent,
apply via deploy.sh on next deploy.
2026-05-15 17:35:11 -07:00
Moheeb Zara 9447b5ee80 feat: customizable permissions, per-item Ops ACLs, area-lead roles
Migration 023 (folded into schema.sql section 15). ADDITIVE and
backward compatible: with no permission/ACL rows the app behaves
exactly as before.

Schema:
- space_role_permissions (space_id, subject, permission); ops_acl
  (space_id, entity_type secret|kb|process|area_lead, entity_id,
  role). Full RLS, writes admin/board.
- user_effective_roles(uid,sid): built-in role + custom-role slugs +
  area_lead:<id> for areas the member leads. SECURITY DEFINER.
- user_has_permission(uid,sid,perm): admin implicit-all else a grant
  via an effective role. SECURITY DEFINER.
- secrets/knowledge_base SELECT rewritten as
  `exact existing rule OR ops_acl match` (only widens; zero change
  with no ACL rows). Seed + backfill default role permissions.

Area-lead roles reuse the existing area_leads table rather than
duplicating the concept: a row is a role, lead_id NULL = Vacant,
assigning sets lead_id, the member then holds area_lead:<id>.

App:
- lib/permissions-catalog.ts (codes + per-role defaults + guardrails),
  Zod schemas, lib/actions/permissions.ts (setRolePermissions,
  setOpsAcl, createAreaLeadRole, assign/unassign/delete).
- Customize gains Permissions (role x permission matrix, admin shown
  implicit-all/locked) and Area leads (create/Vacant/assign) panels,
  modular under customize/panels/. Reusable
  components/ops/ops-acl-editor.tsx wired into the Secrets list.

Audit-found latent fixes:
- revealSecret errors on encrypted-but-no-ciphertext corruption
  instead of silently returning the stale plaintext column.
- upsertAreaLead rejects null/empty area_code (NULLs are distinct in
  unique constraints; allowed duplicate rows).

types/database.ts: added the two new tables. 271/271 tests pass,
build clean. KB-modal ACL wiring + members-row shortcut deferred and
tracked in HANDOFF.
2026-05-15 17:15:14 -07:00