From 5d75813a9616dcc67cccc97d3788341f1e7b9af5 Mon Sep 17 00:00:00 2001 From: mvschwarz Date: Tue, 4 Aug 2026 19:53:47 -0700 Subject: [PATCH] =?UTF-8?q?Providers:=20the=20reset=20you=20type=20is=20no?= =?UTF-8?q?w=20saved=20AND=20read=20back=20=E2=80=94=20two=20bugs,=20not?= =?UTF-8?q?=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- apps/provider-manager/app.json | 19 ++++++++++++++----- providers/studio-fleet/fleet-state.mjs | 20 ++++++++++++++++++++ providers/studio-fleet/provider.json | 20 ++++++++++++++++---- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/apps/provider-manager/app.json b/apps/provider-manager/app.json index e916263..79a316b 100644 --- a/apps/provider-manager/app.json +++ b/apps/provider-manager/app.json @@ -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." } } diff --git a/providers/studio-fleet/fleet-state.mjs b/providers/studio-fleet/fleet-state.mjs index 271fd01..64af117 100644 --- a/providers/studio-fleet/fleet-state.mjs +++ b/providers/studio-fleet/fleet-state.mjs @@ -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" diff --git a/providers/studio-fleet/provider.json b/providers/studio-fleet/provider.json index 968925b..5c6db67 100644 --- a/providers/studio-fleet/provider.json +++ b/providers/studio-fleet/provider.json @@ -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." }