An honest look at what Janice does today, where it's bloated, and how we want to split it so the permanent work is durable and the temporary work can retire cleanly.
Concrete enumeration — not the 6-month-old mental model, the 2026-04 reality.
You remembered Janice as the supervisory control layer — the YAML-driven rule engine that chose nav mode transitions. That's still in there, but it's now a small piece of what the service owns. Since absorbing ace_onboard_api earlier this year (and per ADR-014, the canonical pairing flow), Janice has accreted twelve distinct responsibilities across four audiences. The ~13% CPU footprint you observed is real, and the breakdown below shows where it comes from.
Not making excuses — showing the accretion path so the bloat is legible.
A supervisory control layer. Poll nav status, evaluate YAML rules, issue transition commands. ~1,500 LOC. Named after Janus — god of beginnings, endings, transitions. The name fit.
ace_onboard_apiWhen we retired the old onboard-API service, its responsibilities moved into Janice rather than standing up a third on-robot service. Session tokens, JWT verification, user profile fetches — all came along.
Capture Jetson posts shot_complete webhooks; phone wants live feed; cloud wants uploaded batches. Janice became the broker between three parties that can't otherwise talk to each other because the phone is on an internet-less AP (ADR-004).
Janice already had an HTTP server, so serving two Svelte apps from it was "free." It's still free in the strict sense — but it's blurred the line between "what Janice orchestrates" and "what Janice hosts."
None of these accretions were wrong in isolation. But the cumulative effect is that the name "Janice" now covers two logically distinct things: the supervisory control layer you remember, and a phone-on-AP API gateway that emerged from the ace_onboard_api absorption. That's the framing we want to fix.
One stays Python + Janus-themed. One is a new, durable TypeScript API gateway.
One Python process. One systemd unit. One codename covering everything.
The original Janice. Keeps the Janus codename.
ace_gateway_api TypeScript · permanentNew repo. Mirrors robot-capture-service naming + conventions.
The split is conceptually useful immediately (we name the pieces differently in conversation and docs) and physically separable at the ACL-87 trigger (the already-planned TypeScript migration for Janice). Doing the split in TS lets us carve out the Gateway cleanly while leaving the Sequencer in Python on a shrinking horizon — two rewrites at once would be a mistake.
This follows ADR-017 (Valeron Language Policy, 2026-04-14).
ADR-017 codifies our language split: realtime-core is C++, service-layer is TypeScript, Python remains only where an ML/vision library forces it or as acknowledged debt. The Janice rewrite to TypeScript is already filed as ACL-87 ("acknowledged debt, not urgent"). The Gateway carve-out is the natural moment to cash that in.
robot-capture-serviceace_gateway_api (new)robot-capture-service. Service-layer default.Specifically on the Python / TS question: we are not porting the Sequencer to TS as part of this change. The rule/sequence engine is small, stable, YAML-driven, and heading toward a much smaller surface as L4 matures. Putting engineering cycles into a 4–6 week TS rewrite of a service on a shrinking trajectory is bad allocation. The Gateway — which is permanent — is where we spend the TS investment.
Getting work off the robot where it doesn't need to be there.
Three concrete moves, ordered by leverage:
The PiP is consumed by exactly one Svelte component (CameraPiP) in one view (MainView.svelte). Not by the golfer app, not by the Capture Jetson, not by enrollment. It's a dev-convenience we forgot to turn off in production. Flipping camera.enabled: false in configs/field-test.yaml is a config-only change (hot-reloadable, no deploy) that reclaims the ~5–7% CPU we identified. Zero risk, zero code change, zero coordination.
The operator dashboard has always been misnamed. It's where we build and monitor demo sequences — that's a sequencer interface. Renaming it aligns the UI with its backing service (Sequencer service ↔ Sequencer UI). After PiP is gone and the Stop button moves to Field Control's two-operator model (see below), the Sequencer UI is view-only and has no safety responsibilities — it can be cloud-hosted on Vercel/etc. without any latency concern. Removes another ~1% CPU of state-polling-broadcast load from the robot.
Today Field Control routes commands through Janice. Once your ws lands on the nav stack with its own armed/disarmed safety state, Field Control can bypass Janice entirely for command execution. Sub-millisecond latency on LAN, no Janice hop. The operator dashboard becomes a two-person demo model: one operator on the Sequencer UI watching, one on Field Control with E-stop ready. Cleaner ops, lower latency, safer.
Toggle what's in / out to see the projected state. The "impact" panel updates live.
Three concrete collaboration points, none urgent.
camera.enabled: false is cheap — a one-line config change, hot-reloadable. If we measure Janice CPU before and after, we get empirical ground truth for the rest of the migration planning. We can run that experiment any time the robot is back on Tailscale.ace_gateway_api. Your nav-stack ws is the enabler that lets most of the cleanup happen.