@control-tower/viewer-core
v0.2.0
Published
Control Tower run/trajectory viewer as a reusable Next.js library.
Readme
@control-tower/viewer-core
The Control Tower run/trajectory viewer as a library. Pages, API routes, server actions, data layer, auth middleware — no deployment-specific content. Consumers provide the shell (env vars, Tailwind setup, Dockerfile) and re-export the routes.
See apps/viewer in the control-tower repo for the reference consumer. An external consumer (paper repo, Vercel template) has the same shape.
Quickstart
From an empty directory:
# 1. Install the library and its peer deps.
bun add @control-tower/viewer-core \
next react react-dom mongodb archiver js-yaml \
react-markdown remark-gfm react-syntax-highlighter \
react-plotly.js plotly.js-dist-min @vercel/analytics
bun add -d tailwindcss @tailwindcss/postcss @tailwindcss/typography \
typescript @types/react @types/react-dom @types/node \
@types/archiver @types/js-yaml @types/react-plotly.js @types/react-syntax-highlighter
# 2. Scaffold app/, globals.css, next.config.js, proxy.ts.
bunx viewer-init
# 3. Configure mode (pick one) + Mongo.
cat > .env.local <<EOF
MONGOURI=mongodb://localhost:27017/control_tower
CONTROL_TOWER_VIEWER_PUBLIC=true
# or: CONTROL_TOWER_API_AUTH_USERNAME=admin
# CONTROL_TOWER_API_AUTH_PASSWORD=...
EOF
# 4. Build and run.
bun run build && bun run startEvery generated route is a 2-line re-export; the real code stays in the package. Re-run bunx viewer-init --force after a package upgrade to refresh shims. Use --no-config to preserve a customised globals.css / next.config.js / proxy.ts.
Runtime modes
Exactly one must be configured:
| Env vars | Mode | Behaviour |
|---|---|---|
| CONTROL_TOWER_VIEWER_PUBLIC=true | public | Private routes 404. No editors. |
| CONTROL_TOWER_API_AUTH_USERNAME + CONTROL_TOWER_API_AUTH_PASSWORD | private | Login required. Full editor UI. |
| Neither (or both) | misconfigured | Every request 500s. Fail-closed. |
Also required: MONGOURI.
Optional branding/customization (all picked up at build time — recompile to change):
| Env var | Default | Effect |
|---|---|---|
| NEXT_PUBLIC_VIEWER_BRAND_NAME | Control Tower | Appears in the nav ("<brand> Private" / "<brand> Data") and <title>. |
| NEXT_PUBLIC_VIEWER_PINNED_DATASETS | unset | Comma-separated dataset names rendered in a "Pinned" row on /datasets. |
| NEXT_PUBLIC_VIEWER_EXTERNAL_RUN_URL | unset | Base href for cross-domain run links on the sabotage-runs detail page. When unset, links use relative paths. |
URL-migration redirects (e.g. from a pre-restructure bookmark) are the consumer's job — configure them in next.config.js via Next.js's built-in redirects(). See apps/viewer/next.config.js for a reference.
Package layout
@control-tower/viewer-core/
├── routes/ ← page.tsx, route.ts, layout.tsx (consumer shims re-export these)
├── actions/ ← "use server" action files
├── components/ ← React components
├── lib/ ← Mongo repositories, auth helpers
├── proxy.ts ← Next middleware
└── bin/init.ts ← the viewer-init CLISubpath imports use no file extension — @control-tower/viewer-core/routes/datasets/page, @control-tower/viewer-core/components/TrajectoryDetail. Patterns are enumerated in package.json under "exports".
Known constraints
- Route segment configs (
dynamic,revalidate,runtime, …) and the proxyconfig.matchermust be inline in the shim — Turbopack rejects re-exports.viewer-initcopies them automatically. - Tailwind 4
@sourcesilently skips files alphabetically after Next route-group dirs like(private).viewer-initworks around it by@sourceing leaf subtrees (routes/datasets,routes/api, …) rather than the package root. bin/init.tsis a.tsfile — runnable viabun/bunx, not plain Node. For npm/pnpm consumers we'd need to compile to JS before publishing.- Bun doesn't register workspace-package bins in
.bin/, so inside this monorepo the CLI is invoked via aviewer-initscript inapps/viewer/package.json. External consumers getbunx viewer-initas normal.
