9 Commits
Author SHA1 Message Date
Moheeb ZaraandClaude Opus 4.8 b1edb5c460 types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)
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]>
2026-08-01 18:56:11 -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 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 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
Moheeb Zara 075b274204 fix(ux): focus visibility, tap targets, form labels, surfaced errors
- 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
2026-05-16 00:00:23 -07:00
Moheeb Zara 1db9434930 feat(ux): accessible confirm dialog replaces window.confirm
- 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
2026-05-15 22:18:15 -07:00
Moheeb Zara f885ac73d3 feat(ux): adopt shared Empty primitive across all empty states
- 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)
2026-05-15 22:12:52 -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
Moheeb Zara 71d5e243cf feat: Customize hub + roles editor, terminal logo, invite links
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.
2026-05-15 16:03:52 -07:00