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.
This commit is contained in:
Moheeb Zara
2026-05-16 23:54:46 -07:00
parent ed8d629472
commit 61c97e3b8d
4 changed files with 50 additions and 5 deletions
+3 -2
View File
@@ -894,11 +894,12 @@ validate input with Zod via `parseInput`, authorize via `requireMember` /
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`).
- `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-link of prior anonymous submissions by email. The automatic verified-email retro-link is Phase 5.
- `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.
Onboarding integration (Phase 4, migration 027): a `space_onboarding_steps` step can have `step_type = 'form'` with `config.form_id`. The onboarding flow renders the form/waiver and submits via `submitForm`; `finishOnboarding` treats a required form step as satisfied if a submission for that form by the member already exists (any version — re-sign is non-blocking), and fails open if the configured form is missing/unpublished so a misconfiguration cannot trap members.
Retro-link (Phase 5): `claimMyAnonymousSubmissions()` is a safe, self-only, verified-email-gated action — it resolves the member/email from the caller's session (no trusted params), so a client can only ever link its own anonymous submissions, and only with a confirmed email (the locked decision). It is invoked best-effort from `joinSpace` (new membership, verified email) and `finishOnboarding`. `addMember`/`importMembers` are deliberately NOT auto-linked: those emails are admin-asserted, not verified, so auto-linking would violate the verified-email rule and risk mis-attributing a waiver; the admin uses the forms.manage `linkSubmissionsForMember` for those (the "admin manual-link" path).
Email-match association (PRODUCT DECISION 2026-05, owner-chosen — supersedes the earlier verified-email-only model): a submission is associated with a member whenever `submitter_email` matches a member in the space, **including raw anonymous public submissions**. Accepted tradeoff: someone could get a submission attributed to another member by typing that member's email (attribution only — grants no access). Paths: `submitForm` links at submit time (`pickMemberForEmail`, earliest-joined on duplicates; `escapeLike` makes `_`/`%` in addresses literal); `addMember` and email-changing `updateMember` call the shared `linkSubmissionsByEmail`; `claimMyAnonymousSubmissions()` (self-only, session-resolved) still runs from `joinSpace`/`finishOnboarding`; migration 039 backfilled existing rows. The inline note in `submitForm` flags this as intentional so an audit does not revert it.
### Certifications + Instructor: `certifications.ts` (migration 030)
Cert-type actions are gated by `certifications.manage`; grant actions by `certifications.grant` (the Instructor capability). Both checked via the `user_has_permission` RPC; the `certifications` / `member_certifications` RLS independently enforces the same permission.
+10
View File
@@ -464,6 +464,16 @@ 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
in the space — at submit time, on member add/email-change
(`linkSubmissionsByEmail`), via self-claim at join/onboarding, and a one-time
backfill. This intentionally includes raw anonymous submissions (attribution
only, no access); the tradeoff is documented inline in `submitForm` so it is
not mistaken for a bug. `forms.manage` holders see a member's submissions on
the `/members` per-member panel (`listMemberSubmissions`, RLS-honoring,
metadata only).
### Certifications & Instructor (migration 030)
`certifications` (per-space cert types; optional `validity_months`;
+21 -1
View File
@@ -4,7 +4,27 @@ Append-only. Newest entries on top. Keep each entry to one screen.
---
## 2026-05-17 (pass 31) — Audit follow-ups + attendance polish; LOCAL, awaiting deploy
## 2026-05-17 (pass 32) — Forms↔member email association + landing mobile; LOCAL, awaiting deploy
Branch `main`. 6 commits `ea521de..` (this entry). No browser verification here.
### Forms email-match association (owner-chosen looser model)
Owner explicitly chose to link by email match INCLUDING raw anonymous typed-email submissions (the question spelled out the impersonated-attribution tradeoff; accepted — attribution only, no access). This SUPERSEDES the codebase's prior verified-email-only stance; the decision is documented inline in `submitForm`, in API_REFERENCE, and ARCHITECTURE so a future audit does not "fix" it back.
- **G1 `ea521de`**: pure `escapeLike` (ILIKE-safe; `_`/`%` in emails literal) + `pickMemberForEmail` (earliest-joined deterministic) + 5 tests (suite 464). `submitForm` links member_id on any submitter_email match. Shared `linkSubmissionsByEmail` helper. Superseded verified-only comment updated.
- **G2 `dbed9ac`**: `addMember` + email-changing `updateMember` call `linkSubmissionsByEmail` (service client, space-scoped).
- **G3 `832a0ae`**: migration **039** — data-only, idempotent backfill of existing `form_submissions.member_id` (NULL only) by `(space_id, lower email match)`, earliest-joined. Not in schema.sql (no structural change). Docs bumped to 039.
- **G4 `ed8d629`**: `listMemberSubmissions` (forms.manage, RLS-honoring, metadata only) + FORMS column/button on `/members` → `MemberFormsDialog` (title/kind/version/date + link to `/forms/[id]/results`).
- **G5 (this)**: API_REFERENCE + ARCHITECTURE updated (decision documented), HANDOFF.
### Landing mobile (L1, owner ask)
`app/(landing)/landing.css`: tightened the ≤600px breakpoint (nav padding/gap, section/cta/hero padding, card padding/min-height) and added a ≤400px block (nav gap/font, button padding, wordmark size) so the nav row fits ~320px without overflow. CSS-only inside media queries; desktop untouched. Next auto-injects the viewport meta (no override anywhere) so scaling is correct. Not device-tested here.
### Open
- LOCAL & undeployed: pass-32 (6 commits incl migration 039). Suite 464, build clean each phase. **Awaiting deploy approval** (migration 039 = data backfill; safe/idempotent). After push: confirm 039 ran (submissions linked), exercise public form submit by a member's email → appears under member's FORMS panel; eyeball the landing page at 320/375/414px widths. Default remains ASK-before-deploy.
---
## 2026-05-17 (pass 31) — Audit follow-ups + attendance polish (DEPLOYED, run 25982959375)
Branch `main`. Continuation of the pass-30 audit. User: handle the deferred items where confident + properly test/doc; "stick to single space for now". 5 commits `2dc0af5..` (this entry).