GitHub
05/13/2026, 4:08 PMfrontend/ directory at the repo root. The SPA fully covers what the legacy osctrl-admin templates do — every page surface is replicated. Both UIs can run side-by-side during a migration window (dev compose serves the SPA on :8088 while the legacy admin stays on :8443); the legacy admin is not touched by this PR.
⚠️ Stacked on #813 + #814. When those merge in order, this branch will be re-targeted at the new main HEAD with no conflicts.
End-to-end tested against a Kali docker deployment.
What's in frontend/
frontend/
├── package.json + package-lock.json
├── vite.config.ts ← Vite 7, /api → :8081 dev proxy
├── tsconfig.json ← TS 5 strict
├── public/favicon.svg ← osctrl Geometric tower mark
├── scripts/copy-monaco.mjs ← Self-host Monaco runtime under CSP
├── monaco-runtime.sha256 ← Supply-chain pin for Monaco
└── src/
├── main.tsx ← Router + Query bootstrap
├── routes/ ← TanStack Router (file-based)
├── components/ ← primitives / atoms / chrome / data / forms
├── features/ ← One folder per page surface
├── api/ ← Typed clients per resource
├── lib/ ← cn, time, design-tokens
└── styles/ ← tokens.css + Tailwind base
Tech stack
• React 19 + TypeScript 5 (strict)
• Vite 7 + @tailwindcss/vite + Tailwind CSS v4
• TanStack Router (typed file-based routing), TanStack Query 5, TanStack Table 8
• react-hook-form 7 + zod 3
• Radix UI primitives (à la carte), lucide-react (icons)
• Monaco editor (lazy-loaded for osquery / config editors)
• Vitest + @testing-library/react + jsdom
Bundle: ~780 KB JS / ~52 KB CSS pre-compression → ~214 KB JS + ~9 KB CSS after gzip. Monaco is code-split into its own chunk so pages that don't use it don't pay the editor cost.
Pages
Every page surface the legacy admin offers is covered:
• Login (env picker via pre-auth /login/environments)
• Dashboard (cross-env KPIs, agent versions, active queries, recently seen, failed enrolls)
• Nodes table (paginated, sortable, searchable, quick-filters) — 4×24h activity heatmap per row
• Node detail (system info, status logs, result logs, distributed queries, carves, activity tab)
• Queries (list, run form with target selector + Monaco editor, results with virtual-scroll + CSV export, saved queries CRUD)
• Carves (list, run form, detail with archive download)
• Tags (env-scoped + global)
• Users (list, permissions modal, token modal)
• Profile (password change, token refresh)
• Environments (list, create, edit, Monaco-based config editor with DiffView)
• Enroll page (per-OS one-liners + downloads)
• Audit log (paginated, filtered)
• Settings (per-service, typed inputs)
Design system
Locked tokens, captured in frontend/src/styles/tokens.css and frontend/src/lib/design-tokens.ts (kept in sync):
• Dark default, full light parity — data-theme="dark|light" on <html>
• Signal-teal accent (#2bc4be dark / #0a8a85 light), one accent active per screen
• Semantic status colors that always carry an icon or label (a11y)
• Inter (body) + Space Grotesk (display / KPIs) + IBM Plex Mono (UUIDs / timestamps / cells)
• Tabular nums throughout, no row-jitter on refresh
• Density modes (comfortable / compact / dense) via CSS custom properties
Auth flow
• HttpOnly osctrl_token cookie set by the API on login (no token in localStorage)
• Double-submit CSRF (osctrl_csrf cookie + X-CSRF-Token header) for mutating requests
• 401 on any endpoint → redirect to /login/$env?next=...
• CLI / Bearer clients unaffected (no cookie present → no CSRF needed)
Deployment
Three patterns, all reference each other for consistency:
1. nginx (recommended) — deploy/nginx/frontend.conf.example shows the production pattern: root + try_files for the SPA, /api/* to osctrl-api, baseline security headers (HSTS / CSP / X-CTO / XFO / Referrer-Policy / Permissions-Policy), immutable cache for hashed assets, no-cache for index.html.
2. Docker — `deploy/docker/dockerfiles/Dockerfile-osctrl-frontend`: multi-stage (node:20 builds dist/, nginx:alpine serves it + reverse-proxies /api/*). Single image, single binary's worth of operational surface.
3. Static hosting + CDN — upload frontend/dist/ to S3/Cloudfront/etc., configure CORS on osctrl-api.
The dev compose stack adds an osctrl-frontend service that builds the same multi-stage image on :8088 alongside the legacy admin on :8443 so operators can compare the two on the same data.
Make targets
| Target | Effect |
| --------------------- | ---------------------------------------------- |
| make frontend-install | npm ci |
| make frontend-dev | Vite dev server on :5173, proxies /api → :8081 |
| make frontend-test | vitest + tsc |
| make frontend-build | Produces frontend/dist/ |
| make frontend | install + build (CI / Docker shorthand) |
CI
`.github/workflows/frontend-build.yml`:
• Pinned action SHAs (matches osctrl convention)
• Typecheck (npm run check → tsc --noEmit)
• Tests (npm test → vitest)
• Build (npm run build → vite)
• dangerouslySetInnerHTML gate: build fails if it appears anywhere under src/. Every node-originating field must be JSX-escaped — this gate prevents a future contributor from silently regressing the XSS surface.
• Uploads frontend/dist/ as a 7-day artifact
Test plan
• npx tsc --noEmit — clean
• npx vitest run — 19 test files, 92 tests pass
• npm run build — produces frontend/dist/ cleanly
• Backend untouched: go build ./..., go vet ./..., all 14 Go packages' tests pass
• End-to-end smoke against a Kali docker deployment (login → nodes table → run a query → see results → carve a file → log out)
Why a separate frontend/ directory
• The SPA's build / test / lint loop is fundamentally npm-based; it doesn't want to sit inside cmd/* next to the Go binaries.
• A separate top-level folder makes it obvious to drive-by contributors what the directory is and what tooling expectations apply.
• Per-folder CI: the workflow defaults to working-directory: frontend and can evolve independently of the Go workflows.
jmpsec/osctrlGitHub
05/16/2026, 12:25 PM