Deno + Fresh: replacing the cockpit

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.

The short answer: yes — 100% of the cockpit's feature set is supported by Deno 2.5 + Fresh 2.3 today, feature by feature (table in §4). The honest caveat: the current cockpit is a zero-dependency ~180-line Node server that works. Fresh is a low-risk upgrade, not a rescue. Where it stops being marginal is the roadmap — the queue view (1.2), gallery (1.3), chat UI (1.4), and tech tree (1.6) on the checklist, which are interactive surfaces that Fresh's islands were built for.

1. Question and context

Two questions, one recommendation:

  1. Can Deno (the TypeScript runtime) + Fresh (its web framework) replace the cockpit as the central dashboard today?
  2. What would it take to bring image-studio into that same application as a monolith?

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.

2. What the cockpit is today (feature inventory)

Verified against ~/projects/cockpit (git 4817ef1, 2026-10-15). Zero npm dependencies; one Node server + one static HTML page.

SurfaceWhat it doesKey mechanics
/ the HUDDark dashboard: vitals, service grid, deep-work card, open tasks Single static index.html (~199 lines, no framework); 5-second auto-refresh via fetch
/api/statusBattery %, 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/agentDeep-work gate + agent lanes + 6h activity strip + recent flips Spawns ~/bin/pi-activity --json, punchclock timeline, punchclock tail focus; reads ~/.run/focus.json
/api/tasksOpen 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/whoWhich identity came through x-forwarded-for = tailnet marker; Tailscale-User-Login/-Name headers vs allowed.json allowlist (one owner login); loopback stays open
Tailnet frontOne face on port 443, two paths serve-map.json: / → :8090, /studio → :8096; cockpit on/off applies it; cert managed by tailscaled
Process contractPausible, two-layer start/stop/status/restart (app) + on/off (tailnet web); no launchd, no KeepAlive; pid + log in .run/

3. The two technologies, current state

3.1 Deno — the runtime

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 APIStatus in 2.5
HTTP serverDeno.servestable
Spawning ioreg/vm_stat/punchclock/checklistDeno.Command / Deno.spawn (2.5 adds .text()/.json() conveniences on stdout)stable, --allow-run
Probing services / calling Ollamaglobal fetch + AbortSignal.timeoutstable
Reading/writing .run/, allowed.json, services.jsonDeno.readTextFile / writeTextFilestable
Single-binary deploydeno compile → one executable, no runtime to install on the boxstable (improved in 2.3)
Type safety on the whole codebaseTypeScript is the first-class citizen — no node transpile step, built-in deno check, linter, formatter, test runnerbuilt in
PermissionsCapability model (read/write/run/env paths); 2.5 adds named permission sets in deno.json + a DENO_AUDIT_PERMISSIONS audit logbuilt 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.

3.2 Fresh — the framework

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:

MilestoneWhenWhat it gave
2.0 (63 alphas → beta/RC)2024-08Vite integration (optional plugin: HMR, full Vite plugin ecosystem), rewritten composable core, Preact-fast rendering via Deno's precompile transform, built-in OpenTelemetry
2.1.x2025-10 (2.1.2)Stability line; org moved to freshframework/fresh; monorepo @fresh/core + @fresh/update migration tool
2.2.x2026 earlyVite-integration fixes round-out
2.3.02026-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.

4. Feature parity: does Fresh cover everything the cockpit does today?

Row by row against the §2 inventory. "How" names the Fresh/Deno piece that does the job.

Cockpit featureToday (Node)Fresh + DenoVerdict
HUD pagestatic 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 / CPUexecFile + regex parsing Deno.Command is the direct equivalent; the regex parsing ports unchanged (it's string code)✓
Service grid probesfetch + 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 authheader 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/whoheader inspectionOne tiny route✓
Two-layer on/off + serve-mapbin/cockpit + tailscale Unchanged — deno run (or a deno compile single binary) behind the same scripts; serve-map.json untouched✓
Zero-dependency simplicitystdlib 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✓
Verdict: full parity, 100% of today's features. The migration has no capability gap, and every cockpit primitive maps to a stable Deno/Fresh API. The costs are the soft ones: a young framework, a first-time install of Deno, and re-verifying the off/on contract from scratch (standard 5). The gains land later (§6), not here.

5. The monolith: bringing image-studio in

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 —

The physics of the monolith: no rewrite that stays sensible puts torch/MLX inference inside the web process. 17.6 GB of weights, MPS through torch, a second venv. What a "monolith" can honestly be is a control-plane merge: one app serving both faces and both APIs, with ComfyUI and Ollama remaining the external heavy workers they already are.

Three scopes, cheapest first:

Scope S2 — no merge (status quo, formalized). ~0 effort

Keep the two apps; serve-map.json already presents one face. This is the baseline to beat. Nothing is bought.

Scope S1 — control-plane merge into one Fresh app. The real monolith. Small, bounded work

Work itemSourceEstimate
Port cockpit: 5 routes + auth middleware + vitals + probes to Fresh routesserver.js, ~180 lineshalf a day
Port studio: 2 HTML pages as SSR routes; /studio/* paths preservedpublic/index.html, dark.html2 h
Port studio API: engines, expand (→ Ollama HTTP), generate, jobs, gallery, delete, focusserver.py, ~330 lines TS-transcribed1 day
Job queue + persistence + focus-gate-aware pump (file-backed, contract with ~/.run/focus.json)server.py pump + focusgate.pyhalf a day
Engine adapters as Deno.Command children of ComfyUI (qwen) + Ollama calls (expander, ideogram venv via its own python invocations)engines/*.pyhalf 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/ scripts2 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.

Scope S3 — inference inside the web process. Rejected

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.

6. Why do this at all — the roadmap argument

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 itemHand-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 timeOne island subscribing to SSE; server push route; typed end-to-end
1.3 gallery of outputs (videos)Second hand-built pageA 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 itselfSame
1.6 tech tree visualizationA canvas/SVG hand-builtAn 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.

7. Recommendation

  1. Yes, Fresh can replace the cockpit today — full parity, table in §4. Do not rewrite the cockpit for its own sake. Do it as the monolith (S1): one Fresh app, repo ~/projects/cockpit (renamed or not, owner's call), 8096 retired, one face.
  2. Sequence. (a) install Deno — 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.
  3. Stays external, explicitly: ComfyUI (own venv, 8188), Ollama (11434), punchclock, checklist — all remain CLIs/daemons the app spawns and probes. The monolith is the front desk, not the kitchen.
  4. Risks, honest: Fresh is ~15 months past 2.0 (production-proven at deno.com, so "young" ≠ "unshippable" — but expect an occasional 2.x surprise; pin the version in deno.json); single-process blast radius (§5); Deno not yet on this machine (install is one step, reversible).
  5. Fallback is free: the old repos stay where they are until the drill passes; no deletion before the standard-5 verification.

8. Sources

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).