Providers: the reset you type is now saved AND read back — two bugs, not one

"It doesn't save the calendar, it keeps it for a second then reverts." Two
independent defects stacked, and the second was invisible behind the first.

ONE: THE WRITE ROUTE WAS NOT ROUTED. The surface calls five fleet verbs; the
provider declared TWO. /api/fleet/account, /assign and /switch resolved to
"no such contract route" and 404'd. So every write this app makes -- set a reset,
set capacity, rename an account -- was failing, and the surface said nothing,
because a failed write looks exactly like a write that has not been polled back
yet.

THE ASYMMETRY THAT HID IT, worth naming: a verb declared REQUIRED but unserved
refuses to boot, loudly and at startup. A verb SERVED but undeclared fails at call
time, silently. The manifest only catches the direction that was never the risk
here. Both manifests now list all five.

TWO: THE VIEW NEVER READ THE OVERRIDE BACK. The merge block resolves label and
capacityLeft from overrides and stops, so a typed reset was written to
overrides.json and then ignored -- the row kept rendering the value derived from
samples. On screen that is indistinguishable from a failed save: it holds until
the next poll, then appears to revert. It never reverted; it was never displayed.

A typed reset now wins outright. Not decided on recency like capacity is: capacity
has two honest sources and a fresh measurement can supersede an old typed figure,
but a reset is typed precisely BECAUSE the derived one is wrong.

AND MY OWN CHECK WAS VACUOUS, which is why the first fix looked complete. I
"verified the Pacific round-trip" by re-saving a picker at its CURRENT value and
observing no change -- and no change is exactly what a 404 produces. A no-op test
cannot distinguish a correct conversion from a save that never happened. Setting a
DIFFERENT value is what exposed both bugs, thirty seconds later.

Verified end to end at the shape the founder reported: set 2026-08-27T18:15 in the
picker, correct ISO in overrides.json, still showing after the 30-second poll that
used to revert it.

131/131.
This commit is contained in:
mvschwarz committed 2026-08-04 19:53:47 -07:00
1 parent e04cdd9319
commit 5d75813a96
3 files changed
+50 -9

No files matched your search

+14 -5
View File
@@ -11,7 +11,7 @@
"surface": {
"entry": "app/provider-manager.html",
"path": "/surfaces/provider-manager.html",
"glyph": "◈",
"glyph": "\u25c8",
"hint": "accounts across your boxes: usage, and when a parked one comes back"
},
"roots": {
@@ -25,21 +25,30 @@
},
"/api/fleet/sample": {
"required": true
},
"/api/fleet/account": {
"required": true
},
"/api/fleet/assign": {
"required": true
},
"/api/fleet/switch": {
"required": true
}
},
"_calls_note": "What this app CALLS. Both are required: a dashboard that cannot read fleet state is a blank page, and one that cannot sample can never refresh a reading.",
"_roots_note": "A NEW KIND, and it had to be. The fleet list is box-level — one box has many projects and one fleet — so project/media/canvas/footage are all wrong, and the runtime state dir is disposable while this config is AUTHORED. Declaring it surfaced that the root-kind vocabulary was a closed hardcoded set of four in the SDK; it is open now (studio.json roots{}), pinned by test/root-kinds.test.mjs.",
"_calls_note": "All five, and all required: this surface cannot do its job without any of them. Three were missing and the writes they carry -- set a reset, set capacity, rename an account, assign or switch a machine -- were failing with nothing on screen to say so.",
"_roots_note": "A NEW KIND, and it had to be. The fleet list is box-level \u2014 one box has many projects and one fleet \u2014 so project/media/canvas/footage are all wrong, and the runtime state dir is disposable while this config is AUTHORED. Declaring it surfaced that the root-kind vocabulary was a closed hardcoded set of four in the SDK; it is open now (studio.json roots{}), pinned by test/root-kinds.test.mjs.",
"_verbs_note": "Two verbs with deliberately different costs. /api/fleet/state is free and is what the surface polls, so opening the dashboard spends nothing. /api/fleet/sample spends one real request per box and is only ever reached by an explicit click.",
"vendor": [],
"requires": {
"binaries": [
"ssh"
],
"_binaries_note": "The controller reaches IN. Boxes have one-way ACLs and cannot initiate back, so a pull is the only shape that works — and it is also what keeps the credential on the box instead of here. ssh targets are the operator's existing ones; this app deliberately manages no hosts file, no keys and no tunnels. If ssh to a declared name does not already work, that is a provisioning fact to report, not something a dashboard should paper over."
"_binaries_note": "The controller reaches IN. Boxes have one-way ACLs and cannot initiate back, so a pull is the only shape that works \u2014 and it is also what keeps the credential on the box instead of here. ssh targets are the operator's existing ones; this app deliberately manages no hosts file, no keys and no tunnels. If ssh to a declared name does not already work, that is a provisioning fact to report, not something a dashboard should paper over."
},
"provider": {
"package": "@openrig/studio-fleet",
"required": true,
"_note": "A REFERENCE, not a run spec. How to start this backend is declared by the provider itself in providers/studio-fleet/provider.json — including the deliberate ABSENCE of a companion, since sampling spends real API calls against the budget it reports and must never run on a timer."
"_note": "A REFERENCE, not a run spec. How to start this backend is declared by the provider itself in providers/studio-fleet/provider.json \u2014 including the deliberate ABSENCE of a companion, since sampling spends real API calls against the budget it reports and must never run on a timer."
}
}
+20
View File
@@ -323,6 +323,26 @@ export function buildView({ samples, boxes, labels = {}, boxLabels = {}, now = D
capacityLeft: typedWins ? ov.capacityLeft : a.capacityLeft,
capacitySource: typedWins ? "typed" : (a.capacityLeft === null ? "none" : "measured"),
capacitySetAt: typedWins ? ov.capacitySetAt : null,
// A TYPED RESET WINS OUTRIGHT, and it was not being read back at all.
//
// This block already resolved label and capacity from the override and
// then stopped, so a reset time you typed was WRITTEN to overrides.json
// and never looked at again: the row kept rendering the value derived
// from samples. On screen that is indistinguishable from the save having
// failed — you set it, it holds until the next poll, and then it appears
// to revert. It never reverted; it was never displayed.
//
// Unlike capacity, this is not decided on recency. Capacity has two
// honest sources — a fresh measurement really can supersede an old typed
// figure — but a reset time is typed precisely BECAUSE the derived one is
// wrong, and there is no setAt to compare against. So an override here
// means override.
...(ov.resetsWeeklyAt ? {
resetObservedAt: ov.resetsWeeklyAt,
resetsWeeklyAt: weeklySlot(ov.resetsWeeklyAt),
nextResetAt: projectWeeklyReset(ov.resetsWeeklyAt, now),
} : {}),
};
})(),
state: a.readingSupersededByReset ? "reset — full capacity, presumed"
+16 -4
View File
@@ -2,10 +2,22 @@
"package": "@openrig/studio-fleet",
"run": {
"entry": "fleet-server.mjs",
"args": ["--port", "{{port}}", "--fleet", "{{root:fleet}}"],
"args": [
"--port",
"{{port}}",
"--fleet",
"{{root:fleet}}"
],
"_args_note": "ONE bound root, not a config flag plus a store flag. fleet.json is the DECLARED fleet and samples.jsonl is what we have actually read from it; splitting them across two flags lets the two disagree about which fleet you are looking at."
},
"verbs": ["/api/fleet/state", "/api/fleet/sample"],
"_no_companion_note": "THE ABSENCE IS THE DECLARATION. This provider deliberately ships no companion, because sampling spends a real API call against the very budget it reports — a background poller would be an instrument that quietly consumes what it measures, and would do it on every box forever. The caller asks; nothing polls. If someone later adds a timer, it belongs here as a declared companion and it needs an argument for why the cost is worth it, not a default.",
"_why_this_file_exists": "Same reason as the other three lifts: the run spec lived on apps/provider-manager, which made a fact about this PACKAGE the property of one CONSUMER of it. With this file the app is a client that names its backend, and the backend says how to start itself."
"verbs": [
"/api/fleet/state",
"/api/fleet/sample",
"/api/fleet/account",
"/api/fleet/assign",
"/api/fleet/switch"
],
"_no_companion_note": "THE ABSENCE IS THE DECLARATION. This provider deliberately ships no companion, because sampling spends a real API call against the very budget it reports \u2014 a background poller would be an instrument that quietly consumes what it measures, and would do it on every box forever. The caller asks; nothing polls. If someone later adds a timer, it belongs here as a declared companion and it needs an argument for why the cost is worth it, not a default.",
"_why_this_file_exists": "Same reason as the other three lifts: the run spec lived on apps/provider-manager, which made a fact about this PACKAGE the property of one CONSUMER of it. With this file the app is a client that names its backend, and the backend says how to start itself.",
"_verbs_note": "EVERY route this server answers, not just the read ones. It declared only state and sample while serving five, so account, assign and switch were routed nowhere and 404'd -- and the surface reported nothing, because a failed write looks exactly like a write that has not been polled back yet. Every WRITE in PROVIDER MANAGER was silently dead. The asymmetry that hid it: a verb declared REQUIRED but unserved refuses to boot, loudly; a verb SERVED but undeclared fails at call time, silently."
}