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:
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user