diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 9de93b0..4e87ccf 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -892,7 +892,7 @@ validate input with Zod via `parseInput`, authorize via `requireMember` / ### Forms & waivers: `forms.ts` (migrations 026-029) Management actions are gated by `forms.manage` (checked via the `user_has_permission` RPC; the `forms` RLS independently enforces it): -- `getPublicForm({ space, slug })` — service-client read for the public `/f/[space]/[slug]` page; returns only a published, non-members form. `createForm(input)` — friendly error on slug collision (slug is unique per space, migration 028). `updateForm(input)` — slug is immutable; for a published waiver, a legal-text or schema change bumps `version` (existing submissions stay valid against their own snapshot, non-blocking re-sign). `setFormStatus(input)` — draft/published/closed. `deleteForm(input)` — refused if the form has any submissions (close instead; submissions are immutable records). `listForms()`, `getFormResults(input)`, `exportFormResultsCsv(input)` (audited via `activity_log`). +- `getPublicForm({ space, slug })` — service-client read for the public `/f/[space]/[slug]` page; returns only a published, non-members form. `createForm(input)` — friendly error on slug collision (slug is unique per space, migration 028). `updateForm(input)` — slug is immutable; for a published waiver, a legal-text or schema change bumps `version` (existing submissions stay valid against their own snapshot, non-blocking re-sign). `setFormStatus(input)` — draft/published/closed. `deleteForm({formId, confirm:true})` — **permanently** deletes the form and FK-cascades every submission (including signed waivers); requires explicit `confirm:true` (a stale/accidental call cannot destroy records) and audits the destroyed count; the UI gates it behind a destructive confirm. `deleteSubmission({submissionId})` — `forms.manage`; permanently removes one submission via the service client (form_submissions has no client write policy), space-scoped, audited. `relinkAllSubmissions()` — `forms.manage`; re-runs email→member association across the whole space (members earliest-joined first for deterministic shared-email claim; reuses `linkSubmissionsByEmail`; NULL-only). `listForms()`, `getFormResults(input)`, `exportFormResultsCsv(input)` (audited via `activity_log`). - `submitForm(input)` — public entry point (anonymous, signed-in non-member, or member by form `visibility`). Reads the form with the service client (anon has no `forms` grant), enforces visibility + waiver consent, validates `answers` against the stored field schema (`lib/forms-schema.ts`, unknown keys discarded), snapshots schema/legal-text/version, captures IP + user-agent, and writes the row with the service client (`form_submissions` has no write policy, so this is the only path; rows are immutable). - `linkSubmissionsForMember(input)` — `forms.manage` admin manual re-link of prior submissions by email (kept for explicit re-linking). - `listMemberSubmissions({ memberId })` — `forms.manage`; the forms a member has submitted (title, kind, version, date), for the per-member "Forms" panel on `/members`. Honors the `form_submissions`/`forms` RLS (no service-client bypass); metadata only — answers stay in the audited results surface. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6653c7d..b48ef2f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -459,10 +459,15 @@ all submissions (anonymous, public-authenticated, or members) are written by a single validated server action using the service client AFTER server-side schema validation and snapshotting (same pattern as `finishOnboarding`); the `form_submissions` table has no INSERT/UPDATE/DELETE policy, so RLS hard-denies -every non-service client and submissions are immutable. The public fill page is -served by a service-client server action, so the `anon` Postgres role gets no -grant on `forms`. Form management is gated by the new additive `forms.manage` -permission via `user_has_permission`. +every non-service client; submissions are immutable to clients but a +`forms.manage` holder can permanently delete an individual submission +(`deleteSubmission`) or a whole form (`deleteForm`, which FK-cascades all its +submissions including signed waivers) through validated service-client actions +behind destructive UI confirms (`deleteForm` also requires an explicit +`confirm:true`). The public fill page is served by a service-client server +action, so the `anon` Postgres role gets no grant on `forms`. Form management +is gated by the new additive `forms.manage` permission via +`user_has_permission`. Email-match association (migration 039 + app, owner-chosen 2026-05): a submission is linked to a member whenever `submitter_email` matches a member diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 68c2430..ce79e54 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,7 +4,23 @@ Append-only. Newest entries on top. Keep each entry to one screen. --- -## 2026-05-17 (pass 32) — Forms↔member email association + landing mobile; LOCAL, awaiting deploy +## 2026-05-17 (pass 33) — Form/submission deletion + space-wide re-link; LOCAL, awaiting deploy + +Branch `main`. 4 commits `c4ae5d2..` (this entry). No migration, no browser verification here. + +- **DF1 `c4ae5d2`** actions: `deleteForm` now PERMANENTLY deletes the form + FK-cascades all submissions (the old "refuse if submissions" guard removed); requires explicit `confirm:true` (`deleteFormSchema`) and audits the destroyed count. `deleteSubmission({submissionId})` — forms.manage, service client (form_submissions has no client write policy), space-scoped, audited. `relinkAllSubmissions()` — forms.manage, re-runs email→member linking space-wide (members earliest-joined first; reuses `linkSubmissionsByEmail`; NULL-only). +- **DF2 `1e6e5d5`** UI: forms list per-form Delete (destructive confirm spelling out permanent loss incl. signed waivers) + header "Re-link submissions"; results page per-response Delete (confirm) + "Delete form" header action. Optimistic updates. +- **DF3 (this)**: API_REFERENCE + ARCHITECTURE updated (deleteForm now cascades; immutability caveat); HANDOFF. Suite 464, build clean each phase. + +### Decision note +User explicitly wanted forms + results + individual entries deletable with a confirmation; this overrides the prior "submissions are immutable, close instead" stance. Hard delete (no archive); CSV export remains available and the confirm copy points to it. Documented so an audit doesn't revert it. + +### Open +- LOCAL & undeployed: pass-33 (4 commits). **Awaiting deploy approval** (no migration; app + docs only). After push: smoke /forms (delete a draft form, delete a response, Re-link button), confirm cascade removed submissions. Default remains ASK-before-deploy. + +--- + +## 2026-05-17 (pass 32) — Forms↔member email association + landing mobile (DEPLOYED, run 25984078567) Branch `main`. 6 commits `ea521de..` (this entry). No browser verification here.