Research, 2026-10-16. Question from the owner: "Deno (the TypeScript
runtime) + Fresh (the framework) — would it support all of the cockpit's features today?"
Plus: what it would take to bring image-studio in as a monolith.
Companion documents: checklist-research.html,
~/projects/cockpit/README.md, ~/manifest.md.
Two questions, one recommendation:
Context: rose-web is retired (2026-10-16); the cockpit is now the sole central face. It runs
on Node v26.11.1 (verified on this system 2026-10-16); Deno is not installed —
its installation is a single-step prerequisite, covered in §7.
Verified against ~/projects/cockpit (git 4817ef1, 2026-10-15). Zero npm
dependencies; one Node server + one static HTML page.
| Surface | What it does | Key mechanics |
|---|---|---|
/ the HUD | Dark dashboard: vitals, service grid, deep-work card, open tasks | Single static index.html (~199 lines, no framework); 5-second auto-refresh via fetch |
/api/status | Battery %, RAM %, load + 5 service probes | execFile for ioreg -r -c AppleSmartBattery, vm_stat + sysctl; fetch probes from services.json (http: ollama 11434, pi-web 8504, comfyui 8188, orbit 8000 · cmd: tailscale ip) |
/api/agent | Deep-work gate + agent lanes + 6h activity strip + recent flips | Spawns ~/bin/pi-activity --json, punchclock timeline, punchclock tail focus; reads ~/.run/focus.json |
/api/tasks | Open checklist items + counts (read-only) | Spawns checklist ls --all --json; POST returns 405 (owner ruling 2026-10-15: the dashboard observes, the terminal mutates) |
/api/who | Which identity came through | x-forwarded-for = tailnet marker; Tailscale-User-Login/-Name headers vs allowed.json allowlist (one owner login); loopback stays open |
| Tailnet front | One face on port 443, two paths | serve-map.json: / → :8090, /studio → :8096; cockpit on/off applies it; cert managed by tailscaled |
| Process contract | Pausible, two-layer | start/stop/status/restart (app) + on/off (tailnet web); no launchd, no KeepAlive; pid + log in .run/ |
Deno 2.5 (released 2025-09) is the current line: V8 14.0, TypeScript 5.9.2 native, LTS
releases, one static binary. Since Deno 2 (2024-09) it is also backwards compatible with
Node.js and npm: it reads package.json, resolves node_modules, runs
CommonJS and ES modules, and implements the node: built-in modules
(node:http, node:child_process, node:fs, node:sqlite, …).
In principle the existing server.js would run under Deno unchanged.
But the reason to choose Deno is its native API, not Node compatibility:
| Need (from cockpit) | Deno API | Status in 2.5 |
|---|---|---|
| HTTP server | Deno.serve | stable |
Spawning ioreg/vm_stat/punchclock/checklist | Deno.Command / Deno.spawn (2.5 adds .text()/.json() conveniences on stdout) | stable, --allow-run |
| Probing services / calling Ollama | global fetch + AbortSignal.timeout | stable |
Reading/writing .run/, allowed.json, services.json | Deno.readTextFile / writeTextFile | stable |
| Single-binary deploy | deno compile → one executable, no runtime to install on the box | stable (improved in 2.3) |
| Type safety on the whole codebase | TypeScript is the first-class citizen — no node transpile step, built-in deno check, linter, formatter, test runner | built in |
| Permissions | Capability model (read/write/run/env paths); 2.5 adds named permission sets in deno.json + a DENO_AUDIT_PERMISSIONS audit log | built in |
Sources: deno.com/blog/v2.0, /v2.2, /v2.3, /v2.5; docs.deno.com/api/deno/subprocess; docs.deno.com/runtime/fundamentals/node; docs.deno.com/runtime/migrate.
Fresh is the Deno web framework: islands architecture — pages are rendered on the
server; only the interactive parts (components in islands/) receive JavaScript. Fresh 2.3
(march 2026) made the "zero JavaScript by default" promise real: a page with no islands ships
zero client JS. Current trajectory and state:
| Milestone | When | What it gave |
|---|---|---|
| 2.0 (63 alphas → beta/RC) | 2024-08 | Vite integration (optional plugin: HMR, full Vite plugin ecosystem), rewritten composable core, Preact-fast rendering via Deno's precompile transform, built-in OpenTelemetry |
| 2.1.x | 2025-10 (2.1.2) | Stability line; org moved to freshframework/fresh; monorepo @fresh/core + @fresh/update migration tool |
| 2.2.x | 2026 early | Vite-integration fixes round-out |
| 2.3.0 | 2026-04-24 (100+ commits, 20 contributors) | True zero-JS by default, View Transitions, CSP nonce for inline script/style, IP-filter middleware, Temporal in islands, first-class WebSocket support |
Credibility: in production at deno.com and Deno Deploy (Deno's own site and their cloud run pre-release Fresh). Fresh "runs anywhere Deno 2 runs" — self-hosted, Docker, or bare process — so nothing about this machine's topology (tailscale front, loopback-only bind) depends on the cloud. The one structural caveat: it is a young framework — 15 months post-2.0. For a single-person single-machine dashboard, that ships risk, not drama.
Sources: deno.com/blog/fresh-and-vite; deno.com/blog/an-update-on-fresh; usefresh.dev/docs/introduction + /concepts/architecture + /migration-guide; github.com/denoland/fresh releases 2.0.0 / 2.1.2 / 2.3.0; deno.com/blog/fresh-2.3.
Row by row against the §2 inventory. "How" names the Fresh/Deno piece that does the job.
| Cockpit feature | Today (Node) | Fresh + Deno | Verdict |
|---|---|---|---|
| HUD page | static HTML + hand-rolled 5s polling | A route; with zero islands it ships no client JS (Fresh 2.3), the poll lives in one island, or in zero-JS + a meta-refresh. Fresh's file-based routing + _middleware.ts composes cleanly for both / (cockpit) and /studio (see §5) | ✓ |
| Battery / RAM / CPU | execFile + regex parsing |
Deno.Command is the direct equivalent; the regex parsing ports unchanged (it's string code) | ✓ |
| Service grid probes | fetch + execFile per services.json |
Identical primitives; services.json stays the config | ✓ |
| Deep-work card (punchclock) | 3 spawned CLIs + focus.json | 3 Deno.Command calls; punchclock/checklist CLIs unchanged — they stay the writers | ✓ |
| Tasks card (checklist, read-only) | spawned checklist ls --all --json; POST → 405 |
Same spawn + a route that 405s on POST keeps the owner ruling intact | ✓ |
| Tailnet identity auth | header allowlist vs x-forwarded-for + loopback check |
One _middleware.ts reading the same headers; Fresh 2.3's IP-filter middleware adds cheap defense-in-depth (only tailscale/loopback reachable) | ✓ |
/api/who | header inspection | One tiny route | ✓ |
| Two-layer on/off + serve-map | bin/cockpit + tailscale |
Unchanged — deno run (or a deno compile single binary) behind the same scripts; serve-map.json untouched | ✓ |
| Zero-dependency simplicity | stdlib only, no node_modules |
Preserved by choice: a no-islands Fresh app is deno.json + routes, no node_modules; Fresh ships Preact from JSR when islands are added (JSR = versioned, no lockfile churn). The "zero dep" property is kept, not lost | ✓ |
First the topology, verified 2026-10-16. ~/.run and the source tell a consistent
story:
https://…:443 (tailscale serve, Let's Encrypt)
├── / → cockpit:8090 Node v26, ~180 lines, zero deps — dashboard
└── /studio → image-studio:8096 Python 3.15, FastAPI/uvicorn, ~1 KB py — control plane
├── → ComfyUI:8188 — own venv, torch 2.14 MPS, ~17.6 GB weights, in-repo (separate process)
└── → Ollama:11434 — 9B expander model, shared brain (external process)
image-studio's FastAPI surface is ~330 lines of glue: two HTML pages, gallery listing
(with a path-escape guard), file delete, job queue with focus-gate-aware pumping, engine
adapters (test, qwen → ComfyUI, ideogram — planned), the expander endpoint
(/api/expand → Ollama), focus get/set. The actual inference does not live in that
process: the two heavy engines are already external (ComfyUI's own venv/process; Ollama's
daemon). So —
Three scopes, cheapest first:
Keep the two apps; serve-map.json already presents one face. This is the baseline to
beat. Nothing is bought.
| Work item | Source | Estimate |
|---|---|---|
| Port cockpit: 5 routes + auth middleware + vitals + probes to Fresh routes | server.js, ~180 lines | half a day |
Port studio: 2 HTML pages as SSR routes; /studio/* paths preserved | public/index.html, dark.html | 2 h |
| Port studio API: engines, expand (→ Ollama HTTP), generate, jobs, gallery, delete, focus | server.py, ~330 lines TS-transcribed | 1 day |
Job queue + persistence + focus-gate-aware pump (file-backed, contract with ~/.run/focus.json) | server.py pump + focusgate.py | half a day |
Engine adapters as Deno.Command children of ComfyUI (qwen) + Ollama calls (expander, ideogram venv via its own python invocations) | engines/*.py | half a day — adapters talk HTTP/subprocess, already process-agnostic |
punchclock stamps, .run/ hygiene, one bin/ on/off pair (standards 3 + 5: full drill from running → dead → running in both directions) | both bin/ scripts | 2 h |
Trim serve-map.json to a single / → :8090 handler; drop 8096; update manifest + allowed.json guard consolidation | — | 1 h |
Total: a weekend-scale task. The monolith's true payload: one process to watch, one auth guard, one port behind the tailnet face, one repo for the central face, and — the main prize — §6.
There is no TypeScript equivalent of the torch/MLX engine stack, and pulling 17.6 GB of weights through a Deno-compiled binary is not a deployment, it's a different machine. S3 does not happen.
Monolith trade to name out loud (it's real): today cockpit and image-studio are independently pausable — two on/off pairs. Merged, one crash takes the HUD and the studio down together. On a single-owner machine where both serve the same human, that blast radius is acceptable and the manifest updates to one row; the off/on standard is met by a single clean pair.
Replacing a working 180-line server with the same thing in TypeScript is a costume change. The case for Fresh is the live checklist, in order:
| Checklist item | Hand-rolled (today's way) | Under Fresh |
|---|---|---|
1.2 task queue view (orbit FS queue / SSE from events.jsonl) | SSE plumbing + DOM patching by hand, every time | One island subscribing to SSE; server push route; typed end-to-end |
| 1.3 gallery of outputs (videos) | Second hand-built page | A route + an island; the studio gallery ports alongside |
| 1.4 chat with Rosey in the dashboard (t045 consolidation: one process, one port) | A chat island is exactly the workload Fresh exists for — and t045's "one process, one port" is the monolith itself | Same |
| 1.6 tech tree visualization | A canvas/SVG hand-built | An island, HMR while building it, View Transitions |
Four of the open roadmap items are interactive client surfaces. The static 5-second poller coaches them; islands and islands-style SSE do not. The migration pays for itself the moment item 1.2 ships.
~/projects/cockpit (renamed or not, owner's call), 8096 retired, one face.curl -fsSL https://deno.land/install.sh | sh,
brew-style placement per house convention; (b) scaffold the app in a branch;
(c) port vitals/probes/agent/tasks/who behind the same auth; (d) port the studio
control plane (S1 table); (e) the off/on standard-5 drill, both directions, before retiring the
old two-process topology; (f) manifest §1 row: one row now, cert + serve-map unchanged.deno.json); single-process blast radius (§5); Deno not yet on this machine
(install is one step, reversible).
Deno: Deno 2 ·
2.2 ·
2.3 ·
2.5 (current) ·
subprocess API ·
Node compatibility ·
migrate guide
Fresh: 2.0 beta + Vite ·
status update ·
introduction ·
islands architecture ·
2.3 release ·
releases (2.0.0 → 2.3.0, published 2026-04-24)
Local, verified 2026-10-16: ~/projects/cockpit/{server.js,README.md,services.json,allowed.json,serve-map.json} (git 4817ef1),
~/projects/image-studio/{src/image_studio/server.py,bin/image-studio} (git 44dd6ca),
node --version = v26.11.1, deno = not installed, python3 = 3.15.0,
~/manifest.md, mem skim, checklist ls, flags ls +cockpit +image-studio (no open flags).