mirror of
https://github.com/mvschwarz/openrig-studio-apps.git
synced 2026-11-07 05:56:21 -10:00
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:
1 parent
e04cdd9319
commit
5d75813a96
3 files changed
+50
-9
No files matched your search
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
Reference in new issue
Block a user