Skip to content

UI (SPA)

Repo: secrets-bridge/ui · Stack: Vite 6 + React 18 + TypeScript 5 + Tailwind 3 + TanStack Query 5 + React Hook Form 7 + Zod 3 + react-router-dom v6 · Container: multi-stage Node build → nginx:1.27-alpine, unprivileged user 101

The operator-facing surface. A single-page application served as static assets behind the same hostname as the api (path-based routing: / → ui, /api/v1/* → api).

Pages shipped today

Route Page What
/ Dashboard KPI cards, pending approvals (preview), recent audit activity (preview), providers health (preview), agents card (real), requests-this-week chart (preview)
/requests Requests My-requests filter pills (All / Pending / Approved / Rejected / Executed × patch / read) + Approver queue with inline Approve / Reject (with-reason)
/requests/:id Request detail Timeline, Approvals, Wraps (single-shot Reveal modal — read flow), GitOps observations (disabled banner when §26 is off), Details, Approver actions, Cancel-own
/agents Agents Mint drawer with reveal-once panel for agent secret + bash env snippet, Revoke confirm
/secrets Discovered secrets Filter strip (cluster / provider / ref prefix / status / label chips), two-pane (table + sticky details with provider_config JSON)
/audit Audit log Filter strip + correlation drill-in chip on every row, sticky details with metadata JSON
/admin/projects Projects Master-detail (340px project rail + envs panel); soft-delete via archive/restore
/admin/roles Roles Permission chip picker hydrated from GET /permissions (canonical catalog)
/admin/assignments Assignments User × role × scope table; scope chips with = separator
/admin/workflows Workflows TTL knobs, min approvers, allow self-approval, justification required
/admin/policies Policies Selector → workflow priority-ordered list
/admin/integrations Integrations ArgoCD endpoints + GitOps app mappings; feature-gated banner when §26 is off
/login Login Real email + password against POST /auth/login (token in memory only)

Hard rules baked into the SPA

Rule Where
Token in memory only — never localStorage / sessionStorage / IndexedDB / cookies src/auth/AuthContext.tsx keeps it in React state; reload signs out
HTTPS required on non-localhost src/api/client.ts throws at module-load if VITE_API_BASE_URL is http:// for non-loopback
No SSR Vite SPA build; nginx serves static assets only
Strict CSP default-src 'self'; frame-ancestors 'none'
Initial bundle ≤ 500 KB gzipped (FR-13) CI bundle-size budget; currently ~125 KB
Typed shapes carry no secret values src/api/types.ts has no value / plaintext / token fields by design
Container unprivileged + RO-FS friendly nginx USER 101, tmp paths under /tmp
Reveal-once modals clear plaintext on close useEffect(() => () => setDecoded(null), []) on both the wrap-reveal and agent-secret modals

Design system

The SPA matches the Figma file Secrets Bridge — Brand byte-for-byte on the brand pages — fonts (Inter + JetBrains Mono), colors (Navy 900 / Dark Navy / Cyan accent), brand-gradient primary CTAs, StatusPill variants, and the reveal-once UX pattern.

See Design system for the brand voice, tokens, and Figma file reference.

Configuration

The SPA is configured at build time, not runtime:

Env var Default Notes
VITE_API_BASE_URL empty (same-origin) Set to https://api.example.com only when serving the UI on a different origin

Production deployments serve the UI and the api behind the same ingress with path-based routing so cookies + CORS aren't in play.