Author’s note: this post was drafted by Claude (Anthropic) from my project notes and source code, then reviewed and edited by me before publishing. The voice and judgments are mine; the typing isn’t.
By the middle of this year I was running enough local services — this website’s dev server, a streaming aggregator, a political research tool, a finance dashboard, an LLM runtime, and more — that the overhead of operating them became its own problem. Each had its own start script, its own port, its own logs scrolling in its own terminal window. Worst of all was the orphan problem: stop a launcher script and its child process keeps the port, so the restart fails and you’re in Task Manager hunting PIDs.
Control Plane is the fix: a local desktop app — Rust backend, system WebView, built with Tauri — that starts, stops, and restarts each service, watches health, streams logs, embeds each service’s web UI in a tab, and runs the website’s deploy and publish jobs from one window. It also doubles as a Rust/Tauri learning project: there’s a Code tab inside the app that explains its own source, file by file.
(Screenshots are pending — the dashboard shows real machine paths and service names, so they need a scrub-and-crop pass before they go on the public internet. The fleet view on /projects/ is a faithful cousin in the meantime.)
The core insight
Every service I run already has a web UI on localhost. So the app only needs to be two things: a process supervisor — the part a browser can’t do — and a web-view shell that embeds the UIs that already exist. Everything else follows from refusing to build more than that.
The registry is the source of truth
Services are declared in a services.toml registry, one block each:
[[service]]
id = "myapp"
name = "my app (local)"
cwd = 'C:\path\to\project'
command = ['.venv\Scripts\python.exe', 'app.py']
port = 5000 # used by the health check
embed_url = "http://127.0.0.1:5000/"
managed = true # false = watch-only (no start/stop)
The registry is re-read on every start and on the health poll, so edits take effect in seconds with no rebuild. Adding a service is a config change, not a code change. Some entries are deliberately watch-only — infrastructure the app reports on but must never start or stop, because it doesn’t own them.
The real registry is gitignored (it holds actual machine paths); a template ships in its place, and the in-app Code tab embeds the template, never the real file — so my filesystem layout can’t end up compiled into a binary.
The health-cache decision
The one architectural decision worth a section. Tauri runs synchronous commands on the UI thread, and an early version did live TCP health-checks inline in the list_services call — so every 3-second poll briefly froze the window, and a port that hit its connect timeout froze it properly.
The fix has two layers: a background thread probes every port on a ~2-second timer and writes results into a mutex-guarded cache, so the hot path reads memory and does no I/O; and every command that touches files, network, or subprocesses is declared async, which moves it to a worker thread. The rule of thumb that fell out, and that I’d reuse on any Tauri project: any command that does I/O is async; only pure instant reads stay sync.
Windows process management, the rabbit hole
Three gotchas, all handled in the supervisor, all earned the hard way:
- Relative program paths.
Command::current_dirdoes not affect how Windows resolves the program path — it looks next to the parent process. So a relative exe like.venv\Scripts\python.exeis joined with the service’scwdinto an absolute path before spawning. .cmdshims.npmis reallynpm.cmd, whichCreateProcesscan’t launch directly. Anything shim-shaped gets wrapped ascmd /c npm ….- Tree kill. Killing a launcher orphans its children — the classic “stop script leaves the listener alive.” The supervisor walks the process tree with a Win32 Toolhelp snapshot and terminates each descendant, and finds the PID holding a port via
GetExtendedTcpTable. It deliberately does not shell out totaskkillornetstat: those exact command lines trip antivirus “malicious command line” heuristics, which I learned by tripping them.
The remaining hardening item I’d do next is Windows Job Objects, which reap the whole tree automatically even if the supervisor itself dies.
Deploy is not Publish
The Ops tab runs the website’s two outward-facing jobs as two separate buttons, because they are two separate acts: Deploy freezes the site and ships it to the host; Publish runs the curated source through a content scanner and pushes the mirror to GitHub. Different targets, different content sets — the site carries media the repo doesn’t, the repo carries source the site doesn’t. The publish path always runs non-interactively so it can’t hang on a prompt, and it is not allowed to bypass the scanner with a direct git push. The same tab watches the scheduled agents (blog digests, backups, the file organiser) with their live last-run/next-run state, and only the explicitly allowlisted ones get a “Run now” button.
The security posture, in one paragraph
Local only — the backend binds nothing to the network; it spawns local processes and embeds 127.0.0.1 URLs. No secrets in the registry — environment references, never inline keys. And the frontend can only call an explicit allow-list of commands across the IPC bridge. A supervisor with start/stop/kill powers is exactly the kind of tool that should be boring about security.
Where the design language came from
If you’ve read the projects page or noticed this site’s status dots and flat cards: that idiom started here. A card per service, an honest status glyph driven by a real health check, logs one click away, no decoration that doesn’t carry information. Reworking the website, the Control Plane’s dashboard was the reference — the site now speaks the same language, and the projects page is in effect the public, read-only control plane of the ecosystem. The app came first; the aesthetic followed the operations.
— Luke Simmons, Auckland