Per-file type-only fixes surfaced after the Supabase client was correctly
typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error.
- lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database>
(same ssr/js drift as server.ts; fixes it centrally for all callers).
- Supabase embedded-select joins whose FK metadata is missing from the
generated types are bridged with `as unknown as <target>` + a TODO to remove
after regenerating types/database.ts.
- setState optimistic-update variance and raw <select> enum values cast to the
precise element/union type; .insert payload fields (Json, enums) given the
precise column type via satisfies/field casts.
- Added narrowing guards where a value was legitimately nullable (comms channel,
public form page) without changing the intended path.
The 14 remaining errors are confined to the four files fixed in Phase B (real
bugs + the tautological actions.test rewrite). 639 tests still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.
- 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.
- 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.
- S6: unlayered :focus-visible ring for form controls and links so
keyboard focus is perceivable (was focus:outline-none + 1px border)
- S5: >=44px touch targets at the named hotspots (dashboard actions,
list-row buttons, ops pills/icons, customize number inputs); dense
rows use a 44px hit area without growing row height
- S8: 28 form fields associated via id/htmlFor; search inputs get
aria-label
- S7: claim/complete/delete/link handlers now surface result.error via
toast and stop instead of failing silently
- new components/ui/confirm.tsx: ConfirmProvider + useConfirm() built
on the existing Radix alert-dialog (focus trap, ESC, aria for free)
- wired once into the app layout
- all 13 native window.confirm() delete/remove guards now use the
styled, accessible confirm with a destructive action button
- one Empty/EmptyHeader/EmptyMedia/EmptyTitle/EmptyDescription
component replaces ~18 ad-hoc empty states
- projects gains a real empty state (was four bare columns) with a
create CTA wired to the existing handler
- governance/ops/customize/comms/comments empty states unified;
existing CTAs and conditions preserved (presentational only)
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.
IA refactor and the custom-roles editor pass 11 left as data-only.
Customize hub:
- New /customize (admin/board) with a left section rail: Roles,
Membership tiers, Areas, Invite codes, Onboarding.
- One focused module per panel under app/(app)/customize/panels/
plus a shared card and types file. The client shell is ~75 lines;
each panel is self-contained (smart separation of concerns).
- Settings slimmed to Space, Integrations, Webhooks. Removed the
Roles/Tiers/Invites/Onboarding tabs and the Areas block from
settings-client.tsx (~700 lines) and the matching fetches from
settings/page.tsx. Dead props/state/types removed.
- Sidebar: added a Customize NavLink (admin/board).
Roles editor:
- Rename/recolor the five built-in roles (upsertRoleLabel) and full
CRUD on custom org roles. Adds the read path the audit flagged as
missing: lib/role-labels.ts (getRoleLabelMap/roleDisplayName) is
consumed in (app)/layout.tsx so the sidebar shows the renamed role.
Logo:
- Reverted the brand mark to the terminal >_ glyph
(components/brand-mark.tsx) on the landing nav/footer, onboarding
header, and favicon (public/logo.svg). Removed the dead local
IcoTerminal. AtlasLogo stays only on the resources project page.
Invite links:
- InvitesPanel has Copy code and Copy link (/signup?invite=CODE).
signup reads ?invite= on mount, prefills the code, preselects join.
Deferred and tracked in HANDOFF: types/database.ts sync, renamed-role
display on the members table, splitting the still-large
settings-client.tsx, custom-role assignment UI.
266/266 tests pass. Production build clean; /customize, /onboarding,
slimmed /settings all in the route table. No migration this pass.