Commit Graph
100 Commits
Author SHA1 Message Date
Moheeb Zara 4feecdaad0 feat(ux): sortable members table (name/tier/joined/last payment/status)
Clickable column headers cycle asc -> desc -> none with a caret
indicator; pure client state over the already-filtered list. Lets a
100+-member space sort by last payment or join date (the core dues/
onboarding workflows).
2026-05-17 01:22:03 -07:00
Moheeb Zara 6c487802a5 feat(ux): ⌘K/Ctrl-K command palette for fast navigation
New CommandPalette over the existing unused cmdk primitive, mounted in
the (app) layout. Grouped nav destinations mirroring the sidebar; admin/
manage entries gated by the same isAdmin + navPerms the layout already
resolves (so it never offers a route the guard would bounce). Opens on
Cmd/Ctrl-K, type-to-filter, Enter to jump.
2026-05-17 01:20:06 -07:00
Moheeb Zara 521fb1f8a8 fix(payments): prepend synced rows instead of full page reload
PayPal sync route now returns the inserted rows; the client prepends
them (mirrors handleLogCash) instead of window.location.reload(),
preserving scroll/filter state. Was the only full reload in the app.
2026-05-17 01:18:05 -07:00
Moheeb Zara 7054b35ddb fix(import): route member CSV import through the server action (security/data)
The members branch was inserting space_members directly from the
browser with user_id: crypto.randomUUID() -- a fabricated id that
violates the auth.users FK (offline members must be user_id NULL) and
bypassed all server-side validation. Now maps columns and calls the
existing admin-gated, Zod-validated, batched importMembers action;
surfaces count + skipped. Dropped the now-unused spaceId prop.
2026-05-17 01:18:05 -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 9f0ee189a6 fix(forms): link by an email collected as a form field, not only submitter_email
Root cause: association only ever looked at the dedicated submitter_email
(public 'your email' box / auth email). A form that asks for email as an
ordinary field stored it in answers, so submitter_email stayed null and
nothing matched -- 'Re-link' reported 0 ('already linked') and the
submission never appeared under the member. New pure deriveSubmitterEmail
(explicit > email-type field answer > any email-looking answer; trimmed
lowercase) + 5 tests. submitForm now derives + persists submitter_email
from answers; relinkAllSubmissions re-derives per submission from its
form_snapshot and backfills member_id + submitter_email for existing
rows. Suite 469.
2026-05-17 00:31:33 -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 1e6e5d523b feat(forms): delete UI (destructive confirms) + Re-link submissions button
Forms list: per-form Delete with a destructive confirm that spells out
permanent loss of the form + all responses incl. signed waivers, plus a
header 'Re-link submissions' button (relinkAllSubmissions). Results page:
per-response Delete (confirm) and a 'Delete form' header action that
returns to /forms. Optimistic list updates; all behind useConfirm.
2026-05-17 00:10:47 -07:00
Moheeb Zara c4ae5d27ac feat(forms): delete form (confirm+cascade), delete submission, relink-all
deleteForm now permanently deletes the form AND all submissions via FK
cascade (the submissions-refusal guard is removed); requires an explicit
confirm:true (deleteFormSchema) so an accidental call cannot destroy
records, and audits the destroyed count. New deleteSubmission
(forms.manage, service client since form_submissions has no client write
policy, space-scoped, audited). New relinkAllSubmissions (forms.manage):
re-runs email->member linking space-wide, members earliest-joined first
for deterministic shared-email claim (reuses linkSubmissionsByEmail).
2026-05-17 00:09:28 -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 ed8d629472 feat(forms): per-member 'Forms submitted' view on the members page
listMemberSubmissions (forms.manage, RLS-honoring, metadata only) + a
FORMS column/button -> MemberFormsDialog listing the member's
submissions (title, kind, version, date) with a link to the audited
forms results surface. Gated by forms.manage (matches existing
submission-read RLS; no new exposure).
2026-05-16 23:50:52 -07:00
Moheeb Zara 832a0aeba2 feat(forms): migration 039 backfill existing submissions to members by email
Data-only, idempotent (NULL member_id only): links each prior unlinked
form_submission to the earliest-joined matching space_member by
(space_id, lower(email)=lower(submitter_email)). Not mirrored in
schema.sql (fresh DB has no submissions). Documented in
DATABASE_SCHEMA + DB_SCHEMA_MAP (migration range bumped to 039).
2026-05-16 23:46:56 -07:00
Moheeb Zara dbed9ac1bd feat(forms): link prior submissions on member add + email change
addMember and updateMember (when email set/changed) now call
linkSubmissionsByEmail so an admin-established member email retroactively
picks up earlier unlinked submissions. Service client (form_submissions
has no client write policy), space-scoped.
2026-05-16 23:45:23 -07:00
Moheeb Zara ea521de342 feat(forms): associate submissions with members by email match
submitForm now links member_id whenever submitter_email matches a space
member (including raw anonymous public submissions) -- owner-chosen
product decision, with the attribution tradeoff documented inline so a
future audit does not revert it. New pure escapeLike (ILIKE-safe email
match; '_' in addresses is literal) + pickMemberForEmail (deterministic
earliest-joined on duplicates) + 5 tests (suite 464). Shared
linkSubmissionsByEmail helper for reuse; updated the now-superseded
verified-only comment.
2026-05-16 23:44:03 -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 d5a71c9448 feat(attendance): richer /attendance (here-now, search, day filter, grouping)
Per request. /attendance is now a client view: a 'Here now' section
with host badges + inline self check-in / check-in-as-host / check-out
with a note; and a history with name search, a day picker, grouped-by-
day headers, per-visit duration, status, notes, and a live
visits/members summary for the current filter. New pure tested helpers
dayKey (tz-stable UTC bucket) + visitDurationMinutes (+5 tests, suite
459).
2026-05-16 22:42:07 -07:00
Moheeb Zara 1e1b93e8c4 harden(door): correct IPv6 pin handling via pure normalizeHost
The SSRF pin parser did pinnedHost.split(':')[0], which mangled any
IPv6 pin (e.g. '::1' -> '') so IPv6 controllers could never be pinned
(failed closed). New pure, exhaustively-tested normalizeHost strips
scheme/userinfo/port/path and unwraps [ipv6]/[ipv6]:port/bare-ipv6,
applied to both the request host and the pin so they compare
apples-to-apples. All prior behavior preserved (IPv4, host:port,
userinfo-spoof reject, trailing-dot reject, metadata always blocked);
IPv4-mapped-IPv6 vs IPv4 pin still fails closed. 29 door-logic tests
(was 21); suite 454.
2026-05-16 22:39:25 -07:00
Moheeb Zara 2dc0af5178 fix(paypal): tenant-scoped, idempotent sync (no fragile ON CONFLICT)
payments has no unique index on external_id, so the
onConflict:'external_id' upsert was unreliable and fell back to an
insert that stripped external_id -- creating duplicate, non-idempotent
rows on every re-sync, and not tenant-scoped. Now: look up which PayPal
transaction ids already exist in THIS space and insert only the new
rows (external_id preserved). No schema change.
2026-05-16 22:37:15 -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 dc96d6f46a test: audit-driven coverage for security-relevant pure logic
+16 tests (432 -> 448): permissions-catalog least-privilege guard
(non-board roles carry no high-risk capability; member/associate are
read-only); new invite-logic suite (exact-match creator role, non-admin
grants nothing, invalid/mis-cased targets); door-logic SSRF host-spoof
edges (userinfo-spoofed host rejected, case-insensitive pin match,
trailing-dot FQDN fails closed) + redactDoorSecrets regex-special
password and end-of-string e= value; classes pickPromotion unlimited-
capacity branch + equal-timestamp tie-break.
2026-05-16 22:29:58 -07:00
Moheeb Zara c4cba104d1 fix(nav): gate Admin manage links by actual permission, not just role
Audit follow-up (UI P1). The page guards authorize feature management via
user_has_permission, but the Admin nav links were shown by role only:
a member with a delegated forms/certifications/classes/equipment/door
*.manage permission had no link (orphaned), and an admin lacking the
permission saw a link that redirected. The layout now resolves those five
permissions and the sidebar shows each link on (isAdmin || permission);
Customize/Import/Settings stay admin-only. Also: aria-label on the
forum delete button; comment the intentional Classes/Equipment
nav-active exact-match.
2026-05-16 22:27:26 -07:00
Moheeb Zara 3efa3cdb46 docs: fix audit-found drift (status enum, env, limitations, action layout)
Audit follow-up. ARCHITECTURE: status now 'In production', authoritative
note that server actions are per-domain under lib/actions/* (no monolithic
lib/actions.ts), rewrote stale Known Limitations (test suite exists;
PayPal sync route; social auth wired; webhooks signing-only; single-space
assumption), fixed env table (removed phantom SUPABASE_JWT_SECRET, added
SECRETS_ENCRYPTION_KEY/OAuth). DATABASE_SCHEMA/DB_SCHEMA_MAP: ~43 tables /
migrations through 038; space_members.status is member_status enum default
'unverified' (was wrongly 'active'); fixed the active/pending flow text.
README: webhook wording matches WEBHOOKS.md. API_REFERENCE: real
lib/actions/* path + 4 previously-undocumented actions. createSecret docs
were already correct (ADMIN_ROLES = admin+board) -- left as-is.
2026-05-16 22:25:11 -07:00
Moheeb Zara dd9e3aced6 harden: defense-in-depth on vote/signup/area-lead boundaries
Audit follow-up (RLS already enforced these; adding boundary checks so a
future RLS regression cannot silently allow cross-space writes):
- castVote: verify the proposal is in the caller's space and still open
  before the upsert.
- cancelMySignup: scope every admin-client query/update in the waitlist
  promotion path by member.space_id (not session_id alone).
- setAreaLead: reject a lead_id that is not a member of the space.
- signUpForClass: comment the space-safety invariant on the count.
No behavior change for valid callers.
2026-05-16 22:21:01 -07:00
Moheeb Zara e125f52ca6 docs(presence): API_REFERENCE + ARCHITECTURE sections for migration 038 2026-05-16 22:18:42 -07:00
Moheeb Zara c6b3306a71 feat(presence): dashboard Who's-here panel + /attendance + /me history + nav
Dashboard right column gets a 'Who's here' panel: present members with
Host badges + your own Check in / Check in as host / Check out with an
optional note (router.refresh after). New /attendance page (all members)
lists recent visits with status + duration + notes. /me gains a 'My
recent visits' section. Sidebar 'Attendance' link under People.
2026-05-16 22:17:17 -07:00
Moheeb Zara 8a77b1d301 feat(presence): check-in/out + present/attendance actions
Self-only (member resolved server-side, never from input), validated
service-client writes (no client RLS write policy). checkIn enforces
host eligibility via spaces.host_requires_card + active-card count,
blocks a fresh double check-in, and auto-closes a stale open visit
before a new one (handles forgotten check-outs without a cron).
checkOut closes the caller's open visit. listPresentNow (any member;
staleness applied in pure logic), getMyVisits (/me), listAttendance
(all members, last 250). Zod checkIn/checkOut schemas; barrel export.
2026-05-16 22:14:02 -07:00
Moheeb Zara 654255f837 feat(presence): pure presence-logic (status/host-eligibility/summary) + 11 tests
presenceStatus(in,out,now,maxOpenHours) -> present|checked_out|stale
(>=window boundary = stale; unparseable in-time = stale, never present);
hostEligibility honoring the per-space host_requires_card toggle;
summarizePresence counts present + hosts. PRESENCE_MAX_OPEN_HOURS=18.
Suite 421 -> 432.
2026-05-16 22:12:28 -07:00
Moheeb Zara 1ca70eb1b5 feat(presence): migration 038 space_visits + spaces.host_requires_card
space_visits: one row per visit (check-in/out, is_host, in/out notes);
partial UNIQUE (space_id,member_id) WHERE checked_out_at IS NULL = one
open visit per member. RLS additive/default-deny: SELECT = any space
member (presence is social), NO client write policy (validated
self-resolved service-client actions only; immutable history). No new
permission code; org-wide attendance is all-members by product decision.
spaces.host_requires_card (bool, default true) gates host check-in.
Mirrored in schema.sql, types, DATABASE_SCHEMA, DB_SCHEMA_MAP.
2026-05-16 22:11:17 -07:00
Moheeb Zara 51087748d6 fix(landing): say '23b door system', and derive the modules count
Door controller wording changed from 'native HeatSync' to 'native 23b'
(the platform is generic; 23b is the controller lineage). The 'What it
does' label was hardcoded '06 modules' but MODULES has 12 -- now derived
from MODULES.length.
2026-05-16 22:03:04 -07:00
Moheeb Zara 4b292804c7 refactor(nav): rename sidebar category Learn -> Access & Resources
The group holds Classes, Equipment, Doors and Forms -- only Classes is
about learning; the rest are facility access and resources. Section id
kept as 'learn' so members' saved collapse state is preserved.
2026-05-16 21:57:03 -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 2fdc27597e feat(door): member /doors page (self-entry + my cards + my activity)
New listMyDoorActivity (service client after requireMember; rows where
the caller is actor or target; detail already redacted at write). /doors
composes the existing self-entry control, the masked own cards
(getMyCards, last4 only), and the member's own recent door activity;
empty-state when none. Sidebar 'Doors' link added under Learn.
2026-05-16 21:45:17 -07:00
Moheeb Zara c93ee9bdb7 feat(equipment): per-item reservations roster on /equipment/manage
New EquipmentReservations component (listEquipmentReservations,
equipment.manage-gated) behind a per-item 'Reservations' toggle: shows
who reserved + time range + status, with a manager Cancel. Mirrors the
classes SessionAttendance pattern.
2026-05-16 21:43:25 -07:00
Moheeb Zara 69fa64c82d feat(classes): per-session signups roster on /classes/manage (staff)
Reuses the existing SessionAttendance component (listSessionSignups,
classes.manage/instruct-gated) behind a per-session 'Signups' toggle on
the manage page, so managers can see who signed up + status + mark
attendance + complete without leaving the manage screen. Members still
see only spots-left (privacy posture unchanged).
2026-05-16 21:41:59 -07:00
Moheeb Zara f3b05afe14 feat(classes): optional required-form gate at signup (waiver-on-file)
Pure signupFormEligibility (manager override bypasses) + 4 tests.
signUpForClass now: classes.manage override + sign-up-on-behalf
(targetMemberId), and a hard form gate -- if the class has a
required_form_id the member must have a form_submissions row on file
(checked via service client, since a class manager need not hold
forms.manage). create/updateClass validate the form is in-space and
published. listUpcomingSessions exposes required_form {title,url,
satisfied}. Manage UI: published-form picker on create/edit + a 'requires
form' badge. Member UI: shows the requirement, links to the form, and
replaces Sign up with 'Complete required form' until it is on file.
Mirrors the equipment cert-gate pattern.
2026-05-16 21:41:01 -07:00
Moheeb Zara 92c5737a25 feat(classes): migration 037 classes.required_form_id (optional form gate)
Nullable FK classes.required_form_id -> forms(id) ON DELETE SET NULL.
Mirrored in schema.sql, types, DATABASE_SCHEMA, DB_SCHEMA_MAP. Additive
nullable column on the already-policied classes table (no RLS change);
the signup gate is enforced in the app, mirroring the equipment cert-gate.
2026-05-16 21:33:53 -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 c787766ae3 feat(door): P3-D operate UI on /door/manage (open/lock/unlock + grant/revoke)
listDoorCards (door.operate, service client, masked last4 only) lists
active cards with the slot each holds on a connection. /door/manage now
passes canOperate (door.operate rpc) and, when set, shows per-connection
Open/Unlock/Lock and an expandable card list with Grant/Revoke. Every
live action is behind a confirm; grant shows the allocated slot.
2026-05-16 21:11:56 -07:00
Moheeb Zara e5cedb62e5 feat(door): P3-C live grant/revoke/open-lock-unlock actions
door.operate, rate-limited, every attempt writes one redacted
door_access_log row. Operators have no RLS read on door_connections/
member_cards so reads use the service client after the permission check,
space-scoped. grantCard reserves the slot in the DB first (unique
constraints arbitrate concurrency), calls the controller, rolls the
reservation back on failure; idempotent re-grant; slot_exhausted when
full. revokeCard idempotent; frees the slot only on confirmed controller
success so the app map never diverges from the device. doorControl
open/unlock/lock via the single hardened executor. API_REFERENCE.
2026-05-16 21:09:59 -07:00
Moheeb Zara a5e1ed985c feat(door): P3-B pure slot allocator (lowest-free, range-generic) + tests
pickLowestFreeSlot fills gaps before extending (deterministic, compact,
predictable for admins reading the controller). Range is a parameter
(HeatSync passes 0-200) so it stays adapter-generic. Ignores duplicate/
out-of-range/non-integer used slots; reports slot_exhausted when full.
slotCapacity for 'controller full N/N' messaging. 8 unit tests.
2026-05-16 21:07:36 -07:00
Moheeb Zara 3985419157 feat(door): P3-A door_card_slots per-connection slot allocation map
Migration 036 + schema.sql Section 23: per-connection integer-slot
allocation table for controllers that key cards by slot (HeatSync 0-200).
UNIQUE(connection_id,slot) lets the DB arbitrate concurrent grants;
UNIQUE(connection_id,card_id) makes re-grant idempotent. RLS additive,
default-deny: SELECT = door.manage/operate, no client write policy
(service-client executor only). Types + DATABASE_SCHEMA + DB_SCHEMA_MAP.
2026-05-16 21:06:21 -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 1db93167db feat(door): flexible transport + clearer controller/generic API docs
The app is cloud-hosted, so a connection's target is not assumed to be
LAN-only: any pinned host (public controller/proxy or VPN-reachable LAN
device) is allowed except cloud-metadata/link-local. KNOWN_DOOR_
CONTROLLERS surfaces the controller type with source links (HeatSync/23b
native -> the heatsynclabs + zyphlar repos; otherwise generic HTTP).
The generic adapter form now explains the request model and every
{slot}/{tag}/{perm}/{door}/{pw} placeholder. Copy and the migration
comment corrected away from "LAN-only".
2026-05-16 20:38:52 -07:00
Moheeb Zara ccd0c07ee7 docs(landing,readme): cover the full current feature set
Landing page modules and README features now reflect forms/waivers,
certifications, classes, equipment, access control, onboarding/invites,
the permissions matrix, markdown ops docs, and anonymous incident
tracking, alongside the original members/tasks/projects/payments/
governance/comms.
2026-05-16 20:35:59 -07:00
Moheeb Zara 5e40b91550 docs: HANDOFF pass-25 (Door P1 deployed, P2 local) 2026-05-16 20:32:21 -07:00
Moheeb Zara 59e9fcbd4f feat(door): P2 connections + SSRF-safe executor + native adapter + audit
Migration 035 adds door_connections (per-space controller integration:
secret_ref -> AES-256-GCM secrets vault, never a plaintext password;
pinned_host SSRF pin; native_heatsync|generic_http adapter;
allow_member_self_entry opt-in for phase 3) and door_access_log
(append-only, secrets redacted, service-client-only writes; immutable).
RLS additive/default-deny: connections CRUD = door.manage; log SELECT =
door.manage/operate.

Safety core is pure and heavily unit-tested (door-logic, suite 409):
validateDoorTarget (request host must equal the pinned host;
metadata/link-local always blocked; http(s) only), the
firmware-verified HeatSync encoders with fixed-width zero-padding
(slot 3 / perm 3 / tag 8 / 4-char password), generic template
substitution, and redactDoorSecrets. lib/door/executor.ts is the single
hardened egress: re-validates the pin, refuses redirects, caps time and
body, redacts before any audit write. The door password is read from the
secrets vault and decrypted server-side only, never returned or logged.
/door/manage (door.manage) configures connections, picks a vault secret,
and runs a safe status-only test. Sidebar Admin link added; "Learn"
moved before "People" per request. Docs updated.
2026-05-16 20:31:43 -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 d941c7a6cd feat(nav): collapsible sidebar categories incl. Finance and Learn
Sidebar is now grouped into collapsible sections (Workspace,
Governance, People, Finance, Learn, Account, Admin). Finance splits
payments/financials into their own category; Learn groups
classes/equipment/forms. Each section header toggles open/closed and the
state persists per category in localStorage (defaults open so nothing is
hidden on first visit). Admin section stays admin/board-gated.
2026-05-16 20:14:56 -07:00
Moheeb Zara a8892a2dc8 feat(door): P1 member access cards + door permissions
Migration 034 adds member_cards (RFID/NFC UID -> member). The card UID
is treated as a credential: door.manage-only RLS with no member SELECT
policy; the member's own "My access cards" view goes through a service
-client action that returns only count + last4 (never the raw UID).
New door.manage / door.operate permissions (group Access), seeded to
board and backfilled, introduced now for the whole Door epic. Pure
door-logic masking is unit-tested. A per-member "Cards" panel on
/members is gated to door.manage (independent of admin). No controller
calls and no anonymous path in this phase. Mirrored in schema.sql; types
and schema docs updated.
2026-05-16 20:13:22 -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 746c5969a2 feat(equipment): member browse + reserve + sidebar link
/equipment lists active equipment for any member with status and the
required-certification gate shown up front (Reserve is disabled when the
item is unavailable or the member is not certified). Reserving opens an
inline start/end/notes form; the action enforces eligibility and
no-overlap server-side. Sidebar gets a member-visible "Equipment" link.
2026-05-16 19:54:16 -07:00
Moheeb Zara f6a1530574 feat(equipment): admin manage pages + sidebar
/equipment/manage (guarded by equipment.manage) lists equipment with
inline create/edit, status (available/maintenance/retired), an optional
required-certification picker, asset tag, archive/restore and delete
(refused once reservations exist). Sidebar gets a "Manage equipment"
link in the Admin block. Member browse/reserve is the next phase.
2026-05-16 19:53:14 -07:00
Moheeb Zara 39c00260c7 feat(equipment): server actions + page guard
equipment.ts: registry CRUD (equipment.manage; delete refused once
reservations exist, archive instead), listEquipmentForMembers (active
items with a member_certified flag computed via the service client),
reserveEquipment / cancelReservation as validated service-client writes
that enforce status, no-overlap and the required-certification gate with
an equipment.manage override (and manager booking on a member's behalf),
listEquipmentReservations and getMyReservations. equipment-guard mirrors
the other module guards. Barrel + API_REFERENCE updated.
2026-05-16 19:51:51 -07:00
Moheeb Zara d06c8d9e46 feat(equipment): pure overlap/eligibility logic + Zod schemas
equipment-logic.ts: half-open intervalsOverlap, hasConflict (only
reserved rows block), reservationEligibility (status/active gate, future
window, no-overlap, required-cert with manager override). Dependency-free,
9 unit tests (suite 382 -> 391). validations.ts: equipment/reservation
schemas; reserve carries an optional manager-only memberId.
2026-05-16 19:50:00 -07:00
Moheeb Zara 38fdfb0cd1 feat(equipment): schema + RLS + permission (data layer)
Migration 033 adds equipment (registry; status, optional
required_certification_id, is_active archive) and equipment_reservations
(member time-window reservation; denormalized space_id). RLS is additive
and default-deny: equipment readable by managers (all) or members
(active only), reservations by managers (all) or the member (own) with
manager-only UPDATE and no INSERT/DELETE policy so reserve/cancel
funnels through a validated service-client action (status, no-overlap
and required-certification enforced there, with manager override). New
equipment.manage permission (group Equipment) seeded to board and
backfilled. Mirrored idempotently in schema.sql; types and schema docs
updated.
2026-05-16 19:48:43 -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 15176b652a feat(classes): member calendar/signup + instructor attendance
/classes lists upcoming sessions for any member with spots-left,
sign-up / join-waitlist and cancel (waitlist auto-promotes on cancel via
the action). Holders of classes.instruct/manage get an inline attendees
panel to mark attendance and complete the session; completion issues the
class's certification through the normal grant path and the UI reports
when certificates were skipped (no certifications.grant). Sidebar gets a
member-visible "Classes" link.
2026-05-16 19:36:21 -07:00
Moheeb Zara e2eff0d1af feat(classes): admin manage pages + sidebar
/classes/manage (guarded by classes.manage) lists classes with inline
create/edit, archive/restore, delete (refused once sessions exist), an
optional grants-certification picker and a generic payment link, plus
per-class session scheduling with cancel/delete. Sidebar gets a "Manage
classes" link in the Admin block. The member-facing catalog/signup is
the next phase.
2026-05-16 19:34:28 -07:00
Moheeb Zara 2732269d5f feat(classes): server actions + page guard
classes.ts: class/session CRUD (classes.manage; delete refused once
sessions/signups exist, archive instead), member signUpForClass /
cancelMySignup as validated service-client writes enforcing eligibility,
capacity vs waitlist, de-dupe and earliest-waitlist promotion on cancel,
listUpcomingSessions (service-client counts so members see spots-left
without seeing who signed up), instructor markAttendance and
completeSession. completeSession awards a class's certification via the
normal grantCertification path, so it only issues when the acting
instructor also holds certifications.grant; otherwise completion still
succeeds and reports certificatesSkipped. classes-guard mirrors the
forms/certifications guards. Barrel + API_REFERENCE updated.
2026-05-16 19:32:17 -07:00
Moheeb Zara dc23e9261f feat(classes): pure capacity/waitlist logic + Zod schemas
classes-logic.ts: effectiveCapacity (session override > class > unlimited),
computeSignupStatus (registered vs waitlisted), sessionTiming, canSignUp
eligibility, pickPromotion (earliest waitlisted when a seat frees).
Dependency-free, 10 unit tests (suite 372 -> 382).

validations.ts: class/session/signup/attendance schemas. payment_link is
a generic http(s) link only (empty -> null); capacity is a positive int;
no live payment integration.
2026-05-16 19:29:08 -07:00
Moheeb Zara 0c028f52cc feat(classes): schema + RLS + permissions (data layer)
Migration 032 adds classes (offering; optional payment_link and
grants_certification_id, is_active archive), class_sessions (scheduled
occurrence; denormalized space_id) and class_signups (member signup;
partial unique = one non-cancelled signup per member+session). RLS is
additive and default-deny: classes readable by managers (all) or members
(active only), class_sessions by any space member, class_signups by
managers/instructors (all) or the member (own) with instructor-only
UPDATE and no INSERT/DELETE policy so signup/cancel funnels through a
validated service-client action (capacity/waitlist/dedupe). New
classes.manage + classes.instruct permissions (group Classes) seeded to
board and backfilled. Mirrored idempotently in schema.sql; types and
schema docs updated.
2026-05-16 19:27:32 -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 af503c3869 feat(certifications): per-member award/revoke panel on Members
A "Certs" column appears for anyone holding certifications.grant (the
Instructor capability), independent of admin/board, so a non-admin
instructor can reach it. The button opens a self-contained dialog that
loads the member's grants and the active cert catalog, and supports
award (with optional note + expiry override), soft revoke, and renew.
Status badges come from the shared certifications-logic. The members
server page resolves certifications.grant via the RPC and passes it in.
2026-05-16 18:59:44 -07:00
Moheeb Zara 446177bf86 feat(certifications): admin cert-type pages + sidebar nav
/certifications (guarded by certifications.manage) lists cert types with
inline create/edit, archive/restore (is_active), and delete (refused
server-side once granted). Sidebar gets a Certifications link in the
Admin block next to Forms & waivers. Awarding to members is a separate
permission handled from the Members page (next phase).
2026-05-16 18:57:02 -07:00
Moheeb Zara f23abb0050 feat(certifications): server actions + page guard
certifications.ts: create/update/delete/list cert types (gated by
certifications.manage; delete refused once granted, archive instead),
and grant/revoke/renew/listMemberCertifications/getMyCertifications
(gated by certifications.grant, the Instructor capability). Grant
verifies cert and target member are in-space and the cert is active,
computes expiry from validity_months unless overridden, and relies on
the partial unique index for one active grant per member+cert. Revoke
is soft; nothing is ever deleted. getMyCertifications takes no params
(member resolved from session; RLS restricts to own rows).

certifications-guard.ts mirrors the forms guard for the admin pages.
Barrel re-export and API_REFERENCE updated.
2026-05-16 18:55:17 -07:00
Moheeb Zara ddf6717d1b feat(certifications): pure expiry/status logic + Zod schemas
certifications-logic.ts: computeExpiry (month math with end-of-month
clamping and year rollover), isCertificationActive, certificationStatus
(revoked > expired > expiring_soon > active). Dependency-free and
unit-tested (16 cases incl. Jan-31 and leap-year boundaries).

validations.ts: create/update/grant/revoke/renew + id/list schemas.
grant's expires_at is an optional override (empty -> null); validity is
a positive integer count of months.
2026-05-16 18:52:53 -07:00
Moheeb Zara e439b1130f feat(certifications): schema + RLS + permissions (data layer)
Migration 030 adds certifications (per-space cert types, optional
validity_months, is_active archive) and member_certifications (per-member
grants; expires_at snapshotted at grant time; soft revoke; partial unique
on one active grant per member+cert). RLS is additive and default-deny:
cert types readable by any space member, write-gated by
certifications.manage; grants readable by managers/granters (all) or the
member (own), written by certifications.grant, with no DELETE policy so
the history is immutable. No anonymous path.

Two new permission codes in the catalog (group Certifications):
certifications.manage and certifications.grant (the Instructor
capability, assignable to any role via the existing additive model).
Seeded to board and backfilled. Mirrored idempotently in schema.sql;
types and schema docs updated.
2026-05-16 18:51:33 -07:00
Moheeb Zara 04f1a31999 fix(members): surface invite-link UI on the members page
The first-run "Invite or add members" step links to /members, but
invite creation only lived in Customize, so the page dead-ended. Reuse
the existing InvitesPanel: the members server page loads invites + space
slug for admin/board and composes the panel into MembersClient via an
inviteSlot prop (MembersClient stays unaware of invites). First-run copy
updated to match.
2026-05-16 18:26:42 -07:00
Moheeb Zara a762fae1f1 feat(auth): gate OAuth buttons on login behind config flags
GitHub/Google buttons on the login page pointed at providers that may
not be configured in Supabase, dead-ending in an error. Add
NEXT_PUBLIC_OAUTH_GITHUB / NEXT_PUBLIC_OAUTH_GOOGLE (opt-in, "true" to
show); a provider button renders only when enabled and the whole social
block is hidden when neither is. Pure resolveOAuthProviders helper is
unit-tested; flags documented in .env.example and DEPLOYMENT.md.
2026-05-16 18:26:36 -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 2559166092 feat(forms): visible public link + one-click Copy on member and admin lists
- shared components/forms/copy-link-button.tsx (one control, no drift):
  copies the absolute /f/<space>/<slug> URL, check-mark + toast feedback
- admin /forms list: Copy link button on every published public form
- member /my-forms list: shows the /f/<space>/<slug> path and a Copy
  button for public forms; row restructured so the button is not nested
  in the row link
- members-only forms show no public link (correct: they have none)

337 tests; build verified. Local commit (deploy on your word).
2026-05-16 18:05:23 -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 df6951c323 feat(invites): role-granting + usage-capped invites; audit hardening (P7)
Invites
- migration 029: space_invites.role (member_role enum, default
  'member' — legacy invites and the spaces.invite_code path keep
  granting member). schema.sql + types/database.ts mirrored
- lib/invite-logic.ts (pure, tested): canAssignInviteRole — admin may
  grant any role; board may grant anything except admin; nobody else.
  Privilege-escalation guard on top of the existing admin/board gate
- createInvite/updateInvite take + enforce role via that guard;
  createInviteSchema/updateInviteSchema validate the enum
- joinSpace applies the invite's granted role (validated), default
  member; uses_count/max_uses/expiry already enforced
- invites panel: role picker (filtered to what the creator may grant),
  one-click "single use", per-invite role badge
- space-scoped invite link /join/<space>?code=… via a presentational
  app/join/[space] landing that hands off to the proven
  /signup?invite= flow; /join added to middleware PUBLIC_ROUTES

Audit hardening (from this pass's review)
- /login now honors a validated same-origin ?next= (was hardcoded to
  /dashboard) so the public_auth form sign-in round-trips; OAuth
  threads next through the callback (which already honored it)
- submitForm requires formId; submitFormSchema drops the now-ambiguous
  slug path (form slugs are per-space after 028). Both callers already
  pass formId. Tests updated

337 tests; build verified (Compiled successfully, exit 0). No deploy.
2026-05-16 17:34:45 -07:00
Moheeb Zara 8bbe99d34f feat(forms): space-scoped public URL /f/[space]/[slug] (P6)
- migration 028: drop global UNIQUE(forms.slug), add
  UNIQUE(space_id, slug); schema.sql mirrored. Form slugs are now
  unique per space (fixes cross-tenant slug squatting)
- getPublicForm now takes { space, slug }: resolves the space by its
  slug then the form by space_id + slug (maybeSingle)
- route moved app/f/[slug] -> app/f/[space]/[slug]; the public client
  submits by formId (slug is no longer globally unambiguous) and the
  sign-in link round-trips the full /f/space/slug path
- forms-guard returns the space slug; the builder + forms list show
  the full /f/<space>/<slug> share URL; new/edit pass it through
- middleware /f allowlist already covers /f/<space>/<slug>
- getPublicFormSchema + tests updated (333 pass)

build verified (Compiled successfully, exit 0; new route present). No deploy.
2026-05-16 17:21:39 -07:00
Moheeb Zara 5a49ecb2cf fix(forms): allow anonymous access to the public /f/[slug] page
The middleware PUBLIC_ROUTES allowlist did not include the public
form/waiver fill route, so unauthenticated visitors hitting
/f/<slug> were redirected to /login — making the entire public/
anonymous forms feature unreachable in production. Add '/f' to the
allowlist; only /f/[slug] lives under it (the /forms* management
routes do not match the '/f/' prefix and stay auth-gated). The page
already serves only published public forms and submitForm enforces
visibility, so this exposes nothing new.
2026-05-16 17:06:00 -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 5c39317877 test(forms): extract pure decision logic and add exhaustive coverage
The forms security/correctness logic was buried in DB-coupled server
actions and the builder component, so it had no real tests (the suite
is pure-logic only; nothing exercises Supabase). Extract the pure
parts behaviour-unchanged into lib/forms-logic.ts and test them for
real:

- csvCell (results-export escaping), parseClientIp (e-sign IP capture
  filtering), shouldBumpFormVersion (waiver non-blocking re-sign),
  evaluateRequiredFormStep (onboarding required/auto-satisfy/fail-open),
  slugify + deriveFieldKeys (builder key derivation + dedupe)
- forms.ts / onboarding.ts / form-builder.tsx now consume these; the
  local copies are removed (no behaviour change — verified by the full
  suite staying green)
- __tests__/forms.test.ts expanded 16 -> 62: every validateAnswers
  type/branch (type coercion, length caps, required, unknown-key
  stripping, first-error ordering), parseFormSchema, every new Zod
  schema incl. formSlug boundaries, and all extracted helpers

333/333 tests; build verified (Compiled successfully, exit 0). No deploy.
2026-05-16 16:40:09 -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 da25950988 harden(forms): prevent duplicate onboarding form submission on Back/Forward
Phase 4 self-audit: alreadySubmitted was a page-load snapshot, so a
member who submitted a form/waiver step then navigated Back and
Forward would insert a second submission. Track submitted steps in
session state and treat them as already-submitted.
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 01b924b2f5 harden(forms): use const for derived field-key base (Phase 3 audit) 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 b7461f07fd feat(ux): Tier 3 security-adjacent UX (presentational only)
- secrets: revealed value gets a copy-to-clipboard with feedback,
  auto re-hides after 30s, re-hides on window blur, and the timer is
  cleared on unmount (no change to revealSecret or RLS)
- permissions matrix: a view-only notice when the viewer is not admin,
  and a per-checkbox aria-label so the grid is usable non-visually
- OpsAclEditor: pills are now aria-pressed toggles with a visible
  check when selected and a success toast on save

No server action, RLS, or permission logic changed.
2026-05-16 14:13:11 -07:00