956-line ops-client.tsx → a 230-line orchestrator + ops/panels/{kb,secrets,
area-leads}-panel.tsx (kb+processes share kb-panel via a variant prop) +
ops/components/{kb-modal,secret-modal,area-lead-modal,secret-row,kb-entry-row}.tsx
+ types.ts, following the customize/ exemplar. Behavior preserved (direct writes
kept as-is for now — the secrets one is fixed in the next commit). tsc 0, build ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
KB/process content was shown via a bare unsanitized ReactMarkdown in the
ops view dialog (the /ops/[id] route is the editor, not a view). Route
it through the shared SafeMarkdown instead. New dependency-free
lib/markdown-anchors.ts (heading-text flatten, GitHub-ish slug,
per-render dedupe, in-doc href test; unit-tested) is wired into both
SafeMarkdown and MarkdownBody so headings get stable ids and a
[link](#heading) resolves. In-document links no longer force a new tab,
which previously broke the anchor jump for policies/proposals too.
rehype-slug was not added (no offline install); the helper is equivalent
and tested.
- 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.
S3 (final Tier 1 item). ops (4 modals), members/tasks/projects/
payments/contacts add-edit modals, settings integration-credentials
modal, and the payments link-member modal now use components/ui/dialog
(focus trap, ESC, aria, scroll lock for free). Forms, fields, handlers,
validation and close/reset behavior preserved verbatim; onOpenChange
mirrors the old Cancel/X reset. Removed now-unused X imports; replaced
a stray em-dash with a middot in the link-member subtitle.
- 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)
- new components/ui/page-title.tsx (PageHeader/PageTitle/SectionTitle)
- every page heading routed through PageTitle (one source of truth
for the page-title style; identical render)
- members restricted-directory screen now uses the standard
PageHeader bar instead of an off-style mono heading
Continuing the audit backlog and finishing the permissions UI.
Import validation (recurring audit finding):
- Added importMembersSchema / importPaymentsCsvSchema. importMembers
and importPaymentsCsv now validate per row (a single bad row no
longer rejects the whole file), lowercase emails, normalize dates
through flexibleDateTime(), enforce tier/platform enums and
positive finite amounts, and return a `skipped` count instead of
silently dropping invalid rows.
Permissions UI completion:
- OpsAclEditor is now wired into the KB/process viewer modal
(admin/board), with entity type derived from the `process` tag.
Per-item Ops access control now covers secrets + KB + processes,
matching "all ops facilities stuff".
Left as-is with rationale in HANDOFF: /ops + /import denial paths
(unreachable behind the layout gate or already a clear non-leaking
denial); members-row make-area-lead shortcut (Customize -> Area
leads fully covers assignment); types/database.ts + docs regen
(pure hygiene, zero runtime/security impact, still tracked).
271/271 tests pass. Build clean. No migration this pass.
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.
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.
Big feature batch driven by the super-audit findings and the user's
follow-up requests.
Schema migration 021 (folded into schema.sql Section 13):
- forum_threads + polymorphic comments table keyed on (entity_type,
entity_id). Entity types: forum_thread, proposal, incident, policy.
Trigger maintains forum_threads.comment_count and last_comment_at.
- space_tiers per-space with monthly_price_cents, billing_cadence,
is_system, sort_order. Trigger seeds plus/basic/associate on every
space INSERT and backfills existing spaces. space_members.tier_id FK
added and backfilled from the legacy enum value.
- space_role_labels (rename/recolor built-in roles per space).
space_custom_roles + space_member_custom_roles for non-privileged
extra labels. RLS gates by admin/board.
- space_invites: multi-code invites with expires_at, max_uses,
uses_count, is_enabled. Legacy spaces.invite_code backfilled as a
permanent enabled invite.
- secrets.encrypted_value bytea + encryption_version. Secrets RLS
tightened. New comms_channels INSERT policy: any member can create;
default channels are protected from deletion.
- knowledge_base.render_markdown boolean (default true).
Secrets encryption-at-rest:
- AES-256-GCM in lib/secrets/crypto.ts. Master key from
SECRETS_ENCRYPTION_KEY env (64 hex chars).
- New revealSecret(id) server action: only path to plaintext, writes
secret.revealed to activity_log.
- /ops/page.tsx no longer selects value or encrypted_value on the list
endpoint - the original screenshot bug where ciphertext was already
in the page bundle before "Reveal" is fixed.
UI:
- /forum index, /forum/new, /forum/[id]. Pin/lock/delete actions for
admin/board.
- CommentThread component embedded on /proposals/[id], /incidents/[id],
/policies/[slug]. Markdown rendering via react-markdown + remark-gfm.
- /comms sidebar: inline "+ New channel" form.
- /ops KB: row click opens markdown-rendering viewer modal.
- /settings: new Tiers tab and Invites tab. Inline price edit and
archive/delete on tiers; copy/enable/disable/delete on invites.
- AtlasLogo replaces the terminal icon on the landing nav and footer.
public/logo.svg wired as the favicon in app/layout.tsx metadata.
- Sidebar has a Forum nav link.
Server actions added:
- lib/actions/forum.ts, comms.ts, tiers.ts, roles.ts, invites.ts.
- lib/actions/secrets.ts rewritten with updateSecret + revealSecret.
Auth flow:
- joinSpace honors codes from space_invites first (enabled, expiry,
use cap), falls back to legacy spaces.invite_code, increments
uses_count on success.
Env:
- SECRETS_ENCRYPTION_KEY documented in .env.example and set on the
Droplet's /opt/hackerspace-ops/.env.production.
README: live site link added (https://hackerspace.sh).
266/266 unit tests pass. Production build clean. Migration 021 will be
applied on the next deploy via /opt/hackerspace-ops/deploy.sh's
idempotent migration runner.