feat: landing redesign in resources aesthetic; fix KB search crash

Landing:
- Rebuilt app/(landing)/page.tsx from a 539-line client monolith to a
  ~150-line server component in the /resources editorial style.
- New scoped app/(landing)/landing.css mirrors the resources palette
  (#0c0c0c / lime #d4ff00 / #252525), IBM Plex Mono + Libre
  Baskerville loaded in (landing)/layout.tsx, faint grid overlay.
- Removed the animated Mini* previews and inline Ico* set (clashed
  with the editorial look). Split into components/landing/icons.tsx
  and components/landing/resource-showcase.tsx for separation.
- Resource showcase: one row of six square tiles with the real
  project SVG logos linking to atlas/space-after-dark/proposal-duel/
  zine/learner/governance. Responsive 6 -> 3 -> 2.
- BrandMark accepts an optional style prop.

Fix:
- KB search crashed the Ops page. filteredLeads referenced
  l.member_name (the column is lead_handle), so undefined.toLowerCase()
  threw on every keystroke since the filter memos recompute on shared
  search state across tabs. All Ops filter predicates are now
  null-safe via a shared has() helper; filteredSecrets no longer
  assumes a non-null title.

Docs:
- docs/PERMISSIONS_DESIGN.md: concrete plan for customizable
  permissions, per-item Ops ACLs, and area-lead roles. That work is
  security-sensitive (RLS rewrites on secrets/knowledge_base) and is
  scoped as its own next pass, not bundled here.

266/266 tests pass. Production build clean. No migration this pass.
This commit is contained in:
Moheeb Zara
2026-05-15 16:15:56 -07:00
parent 71d5e243cf
commit 43f9c09d70
10 changed files with 534 additions and 506 deletions
+21
View File
@@ -4,6 +4,27 @@ Append-only. Newest entries on top. Keep each entry to one screen.
---
## 2026-05-15 (pass 14) — Landing redesign + KB search fix
Branch: `main`.
### What shipped
- **Landing rebuilt in the resources editorial aesthetic.** New scoped `app/(landing)/landing.css` mirrors the `/resources` palette (`#0c0c0c` / lime `#d4ff00` / `#252525` borders), IBM Plex Mono + Libre Baskerville (loaded in `(landing)/layout.tsx`, which now wraps `.landing-root` + grid overlay). `app/(landing)/page.tsx` went from a 539-line `'use client'` monolith to a ~150-line server component. The animated `Mini*` previews and inline `Ico*` set were removed (they clashed with the editorial look). Modules extracted for separation of concerns: `components/landing/icons.tsx`, `components/landing/resource-showcase.tsx`. `BrandMark` now accepts `style`.
- **Resource showcase**: a single row of six square tiles at the foot of the landing, each with the real project SVG logo (`AtlasLogo`, `CubeIcon`, `TraceIcon`, `SpaceIcon`, `GridIcon`, `KeyIcon`), linking to /atlas, /space-after-dark, /proposal-duel, /zine, the external learner, /governance. Responsive 6 -> 3 -> 2 columns.
- **KB search crash fixed** (`app/(app)/ops/ops-client.tsx`). Root cause: `filteredLeads` referenced `l.member_name`, a column that does not exist (it is `lead_handle`), so `undefined.toLowerCase()` threw. Because `search` is shared state and all four filter memos recompute on every keystroke regardless of the active tab, typing in the KB box crashed the whole page. All four predicates are now null-safe via a shared `has()` helper; `filteredSecrets` also no longer assumes a non-null `title`.
### Verification
- `pnpm build` green (`/` route present), `pnpm test` 266/266.
- No migration this pass.
### Next pass (designed, not yet built): permissions + Ops ACLs + area-lead roles
User asked for: customizable permissions; per-secret and per-Ops-item multi-role access; area-lead roles that show "vacant" until a member is assigned (from a dedicated UI or by clicking a member in the directory) and that grant permissions wherever that area-lead role is selected. This is a security-sensitive subsystem (RLS rewrites on `secrets` and `knowledge_base`) and is deliberately NOT bundled with the landing redesign. Design is captured in `docs/PERMISSIONS_DESIGN.md`.
---
## 2026-05-15 (pass 13) — Customize hub, roles editor, logo revert
Branch: `main`.
+101
View File
@@ -0,0 +1,101 @@
# Permissions, Ops ACLs, and Area-Lead Roles — Design
Status: designed, not yet implemented. This is the plan for the next focused
pass. It is security-sensitive (it rewrites RLS on `secrets` and
`knowledge_base`), so it ships on its own, with its own tests, not bundled
with unrelated UI work.
## Goals (from the user)
1. Customizable permissions, not just role labels.
2. Per-secret access: choose which roles (multiple) can see each secret.
3. Same per-item access control across all Ops and Facilities content
(knowledge base, processes, secrets, area leads).
4. Area-lead roles: an admin can create them. Each shows as "vacant" until a
member is assigned, either from a dedicated picker or by clicking a member
in the directory. Being the lead of an area grants the permissions attached
wherever that area-lead role is referenced.
## Model
Three layers, smallest blast radius first.
### A. Permission catalog + role grants
- `permissions` is a fixed code list in app code (not a table): e.g.
`ops.kb.read`, `ops.kb.write`, `ops.secrets.read`, `ops.secrets.write`,
`members.manage`, `payments.manage`, `governance.manage`, etc.
- New table `space_role_permissions (space_id, role_or_custom, permission)`
where `role_or_custom` is either a built-in `member_role` value or a
`space_custom_roles.slug`. Admin always has all permissions implicitly
(never lockable out — guard in code and RLS).
- A SQL helper `user_has_permission(uid, sid, perm text) returns boolean`
(SECURITY DEFINER, fixed search_path) used by RLS and a TS mirror
`hasPermission()` used in server actions for early rejection + UI gating.
### B. Per-item ACLs for Ops content
- New table `ops_acl (id, space_id, entity_type, entity_id, role text)`
where `entity_type in ('secret','kb','process','area_lead')` and `role`
is a built-in role, a custom-role slug, or the sentinel
`area_lead:<area_code>`.
- An item with zero ACL rows falls back to its existing visibility column
(`knowledge_base.visibility`, the board/admin default for `secrets`), so
this is additive and backward compatible.
- RLS SELECT on `secrets` / `knowledge_base` becomes: existing rule OR an
`EXISTS` against `ops_acl` joined to the caller's effective roles
(built-in role + assigned custom roles + area-lead memberships). Write
stays admin/board (plus `ops.*.write` permission once layer A lands).
### C. Area-lead roles
- Reuse `space_custom_roles` with a flag `is_area_lead boolean` and an
`area_code` link, OR add `space_area_lead_roles (id, space_id, area_code,
name, color)`. Prefer the dedicated table: it is the thing that renders
"vacant".
- `area_lead_assignments (space_id, area_lead_role_id, member_id)`. Absence
of a row => the role renders "Vacant". Assignment is created from a picker
in `/customize` -> Areas or from a "Make area lead" action on a member row
in the directory.
- Effective-roles resolution (used by `ops_acl` SELECT and
`user_has_permission`) treats a member who holds an area-lead assignment as
also holding `area_lead:<area_code>`, so any Ops item whose ACL includes
that sentinel becomes visible to them, and any permission granted to that
area-lead role applies.
## UI surfaces
- `/customize` -> new "Permissions" panel: a role x permission matrix.
- `/customize` -> Areas: per-area "Lead role" with a member picker; shows
"Vacant" when unassigned.
- `/ops` create/edit modals (KB, process, secret): a multi-select of roles +
area-lead roles that may access this item.
- `/members`: row action "Assign as area lead" -> area picker.
## Migration plan
`scripts/023_permissions.sql` + `schema.sql` Section 15:
- `space_role_permissions`, `ops_acl`, `space_area_lead_roles`,
`area_lead_assignments`. All with full RLS (SELECT for space members,
write for admin/board, delete admin).
- Helper functions `user_effective_roles(uid, sid)` and
`user_has_permission(uid, sid, perm)`.
- Rewrite `secrets` and `knowledge_base` SELECT policies to add the ACL
branch. Keep the old branch so nothing that works today breaks.
- Seed sensible default `space_role_permissions` per space (admin: all;
board: ops + members + governance; treasurer: payments; member: kb.read).
- Backfill: none required (empty ACL == current behavior).
- Refresh `types/database.ts`, `DB_SCHEMA_MAP.md`, `DATABASE_SCHEMA.md`,
`API_REFERENCE.md`.
## Risks / why this is its own pass
- RLS on `secrets`/`knowledge_base` is the highest-risk surface in the app.
A wrong policy either leaks a credential cross-role or locks admins out.
Needs dedicated RLS tests (the `__tests__` governance/RLS pattern) before
it goes near production.
- Permission resolution must never let a member escalate themselves (the
migration-015 trigger pattern). The `user_has_permission` helper must be
SECURITY DEFINER with a pinned search_path, same as the existing helpers.
- Ship behind the additive fallback so existing spaces are unaffected until
an admin opts in by setting ACLs/permissions.