<#815 osctrl-frontend: React admin SPA at frontend...
# osctrl
g
#815 osctrl-frontend: React admin SPA at frontend/ (round 3 of 3) Pull request opened by alvarofraguas Summary Round 3 of 3. Lands the React + TypeScript + Vite SPA under a new
frontend/
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/
Copy code
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/osctrl