A dashboard that installs anywhere: retiring Angular for server-rendered HTML
The desktop dashboard shipped ~640 npm packages — bleeding-edge Angular, a 73 MB code editor, native build tooling — and choked behind locked-down enterprise registries. We replaced it with a server-rendered UI that ships inside the server itself: well under 250 mainstream packages, no native builds, the same look.
SpecShip's dashboard is the loopback UI you get from specship serve --ui — stat tiles, the spec funnel, a drift queue, a tool-call heatmap, cost analytics. It was an Angular single-page app, and for most people it Just Worked. Then an enterprise user tried to build SpecShip from source inside their network and it fell over: their internal npm registry didn't carry a few of the dashboard's dependencies, and the build died.
The root cause wasn't a bug. It was the shape of the dependency tree.
640 packages, and the ones you can't mirror
The Angular dashboard resolved to roughly 640 npm packages: bleeding-edge Angular 21, a 73 MB Monaco code editor, a full bundler toolchain, and native modules that fetch prebuilt binaries from the internet at install time. A locked-down corporate registry is a mirror — it proxies the public registry but only carries what it's been asked to. Bleeding-edge and niche packages often aren't there yet, and the post-install binary fetches reach hosts the network blocks outright. One missing package anywhere in that tree, and npm ci fails.
You can't fix that by pinning versions. The only durable fix is a smaller, more mainstream tree — few enough packages, common enough that any mirror already has them, and zero native compilation.
"Fewer direct dependencies" and "a smaller install tree" aren't the same thing. The bundler is most of the weight, and every framework needs one.
Server-rendered, folded into the server
So the dashboard moved off Angular entirely. Every page is now rendered to HTML on the server — in the very same process that already serves the /api the old SPA called. There's no client framework, no router, no hydration; the only JavaScript shipped is a tiny vanilla island for the one genuinely interactive surface, the node graph (pan, zoom, hover). Pages that don't need interactivity ship no script at all.
The renderer lives in packages/server and pulls its data through an in-process request to the existing API handlers — no second server, no HTTP round-trip. The dashboard's dependency tree collapsed from ~640 packages to well under 250, all mainstream, all mirrorable, with no native builds. It installs behind a restricted registry because there's nothing left in it that a mirror wouldn't already have.
Read-only, on purpose
The enterprise need was a viewer, not an editor, so the dashboard is now read-only. That let the biggest single dependency — the Monaco editor — go entirely: specs render as formatted Markdown instead of in a code editor, and the write paths that came with it are gone. You still launch and approve workflows, edit specs, and run everything else the way you always did — from the CLI and the /specship:* slash commands. The dashboard shows you the state; Claude Code and the CLI change it.
Same look, and a smaller core
None of this changed the design. The dashboard reuses the exact same design tokens it always had, so it reads as the same product — just rendered a different way. And because the dashboard is now optional and self-contained, there's a new npm run build:core that builds just the CLI and MCP server: if all you want is the Claude Code integration, you can skip the dashboard's tree completely and every command except serve --ui still works.
The whole thing was written as a spec first — DASH-LEAN-DOC — with the dependency ceiling, the read-only contract, and the page-parity set as testable requirements, then implemented against it. If you're building SpecShip behind a firewall, it now installs like anything else.