mirror of
https://github.com/mvschwarz/openrig-studio-apps.git
synced 2026-11-07 05:56:21 -10:00
Providers: one reset column with a Pacific picker, and the fleet data moved off /tmp
TACTICAL, as asked. Two reset columns on the account tables collapse to one --
"Next reset (Pacific)" -- carrying a real datetime-local picker instead of a
read-only weekday and an ISO string you had to type by hand. Both providers, same
control. The MACHINES table is untouched: its reset cells inherit from the account
tables below, so a picker there would be a second place to edit one value.
PACIFIC EXPLICITLY, NOT "the reader's timezone", which is what the old helpers
used. A weekly reset is a fact about the ACCOUNT, not about where you are reading
from -- render it in the browser's zone and the same reset shows a different hour
on a laptop that travelled, whereupon it gets "corrected" and is now wrong by the
offset. Converted at both ends, and the offset is MEASURED at the instant rather
than assumed, because these resets straddle DST and a hardcoded -08:00 is wrong
half the year.
Verified by the only test that distinguishes a correct conversion from a broken
one: re-saving a picker at its currently displayed value must be a NO-OP. 14:00Z
renders as 07:00 Pacific, saves back as 14:00Z, unchanged. A wrong conversion
shifts by the offset and nothing else would have shown it.
AND THE PART THAT MATTERED MORE THAN THE UI: this app's state -- your machine
names, capacity figures and reset times -- was living in a scratchpad TEMP
DIRECTORY. Copied to ~/Desktop/studio-workspace/fleet/ and verified by checksum
per file, not by count, before anything was killed; then confirmed byte-identical
at the moment of the cutover so no edit made on the old instance was lost.
CONSOLIDATED: provider-manager now runs in the single studio on 9340 alongside
effects, scanner, gallery, workspace and files. The second studio on 9330 is down.
It was never a different repo -- same app, same openrig-studio-apps -- only a
second studio.mjs process serving floor + provider-manager, running since Aug 3.
WHERE FILES LIVE, written into docs/skills/scanner/the-workspace.md so it stops
being decided case by case. The test:
If I delete and reinstall this app, should this file survive?
Yes -> the workspace. No -> the app folder.
Which rules out the tempting option: never colocate user data with the app. Not
hypothetical -- colocated, reinstalling or forking PROVIDER MANAGER would take
your account labels and reset times with it, and a fork would carry them into a
stranger's copy. <workspace>/<app-id>/ is the sanctioned home for app-scoped data
that is still yours, and fleet state is the worked example.
131/131.
This commit is contained in:
1 parent
fd70adcbca
commit
e04cdd9319
2 files changed
+90
-8
No files matched your search
@@ -79,6 +79,10 @@
|
||||
tr.ready td { background: color-mix(in srgb, var(--fresh) 7%, transparent); }
|
||||
/* An account that has just refilled is the answer to "what do I switch to",
|
||||
so it is the loudest thing on the page for as long as that is true. */
|
||||
/* The picker has to look like the editable control it is — a bare
|
||||
datetime-local inherits nothing and reads as static text. */
|
||||
input.edit.reset { font-family: var(--mono, ui-monospace, monospace); font-size: 12px;
|
||||
padding: 3px 5px; width: 100%; min-width: 175px; }
|
||||
tr.just-reset td { background: color-mix(in srgb, var(--fresh) 15%, transparent); box-shadow: inset 2px 0 0 var(--fresh); }
|
||||
.pick { white-space: nowrap; }
|
||||
.switch { padding: 3px 8px; margin-left: 6px; font-size: 13px; line-height: 1; }
|
||||
@@ -164,6 +168,50 @@
|
||||
})}</span>`;
|
||||
};
|
||||
|
||||
// PACIFIC, EXPLICITLY, NOT "the reader's timezone".
|
||||
//
|
||||
// A weekly reset is a fact about the ACCOUNT, not about where you happen to
|
||||
// be reading — and this fleet's resets are quoted in Pacific. Rendering them
|
||||
// in the browser's zone means the same reset reads as a different hour on a
|
||||
// laptop that travelled, and the person retypes it "correctly" and is now
|
||||
// wrong by the offset. So the picker is Pacific at both ends and says so.
|
||||
const TZ = 'America/Los_Angeles';
|
||||
|
||||
// UTC instant -> the naive "YYYY-MM-DDTHH:mm" a datetime-local input wants,
|
||||
// expressed as PACIFIC WALL TIME.
|
||||
const toPacificInput = (iso) => {
|
||||
if (!iso) return '';
|
||||
const t = new Date(iso);
|
||||
if (Number.isNaN(t.getTime())) return '';
|
||||
const f = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, hour12: false,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
const g = Object.fromEntries(f.formatToParts(t).map((x) => [x.type, x.value]));
|
||||
// en-CA gives 24h, but midnight can format as "24" — normalise it rather
|
||||
// than emit a value the input silently rejects.
|
||||
const hh = g.hour === '24' ? '00' : g.hour;
|
||||
return `${g.year}-${g.month}-${g.day}T${hh}:${g.minute}`;
|
||||
};
|
||||
|
||||
// The reverse. A datetime-local value is NAIVE — no zone — so it has to be
|
||||
// read AS Pacific and converted back to an instant. Done by measuring the
|
||||
// zone's offset at that moment rather than assuming a fixed one, because
|
||||
// these resets straddle DST and a hardcoded -08:00 is wrong half the year.
|
||||
const fromPacificInput = (v) => {
|
||||
if (!v) return '';
|
||||
const guess = new Date(`${v}:00Z`); // provisional: treat as UTC
|
||||
if (Number.isNaN(guess.getTime())) return '';
|
||||
const offset = (d) => {
|
||||
const asTz = new Date(d.toLocaleString('en-US', { timeZone: TZ }));
|
||||
const asUtc = new Date(d.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
return asUtc.getTime() - asTz.getTime();
|
||||
};
|
||||
// One correction pass, then re-measure: the offset at the provisional
|
||||
// instant can differ from the offset at the real one across a DST edge.
|
||||
let t = new Date(guess.getTime() + offset(guess));
|
||||
t = new Date(guess.getTime() + offset(t));
|
||||
return t.toISOString();
|
||||
};
|
||||
|
||||
const when = (iso) => {
|
||||
if (!iso) return '<span class="muted">—</span>';
|
||||
const t = new Date(iso);
|
||||
@@ -256,7 +304,7 @@
|
||||
<div class="table-wrap"><table>
|
||||
<thead><tr>
|
||||
<th>Account</th><th class="numeric">Capacity left</th><th>Source</th>
|
||||
<th>Resets</th><th>Next reset</th><th>On</th>
|
||||
<th>Next reset <span class="muted">(Pacific)</span></th><th>On</th>
|
||||
</tr></thead><tbody>
|
||||
${rows.length ? rows.map(a => `<tr class="${a.justReset ? 'just-reset' : a.readyToSwitchTo ? 'ready' : ''}">
|
||||
<td class="name"><input class="edit" data-account="${esc(a.account)}" data-field="label"
|
||||
@@ -269,15 +317,14 @@
|
||||
value="${a.capacityLeft === null || a.capacityLeft === undefined ? '' : esc(a.capacityLeft)}"
|
||||
placeholder="—"> %</td>
|
||||
<td class="muted">${esc(a.capacitySource === 'none' ? '—' : a.capacitySource || '')}</td>
|
||||
<td>${a.provider === 'codex'
|
||||
? `<input class="edit" data-account="${esc(a.account)}" data-field="resetsWeeklyAt"
|
||||
value="${esc(a.resetObservedAt || '')}" placeholder="2026-08-09T04:09:00Z">`
|
||||
: slot(a.nextResetAt)}</td>
|
||||
<td>${a.nextResetAt ? when(a.nextResetAt) : '<span class="muted">—</span>'}</td>
|
||||
<td><input class="edit reset" type="datetime-local"
|
||||
data-account="${esc(a.account)}" data-field="resetsWeeklyAt"
|
||||
value="${esc(toPacificInput(a.nextResetAt || a.resetsWeeklyAt || a.resetObservedAt))}"
|
||||
title="the next weekly reset, in Pacific"></td>
|
||||
<td>${a.onBox ? esc(boxName(a.onBox))
|
||||
: a.lastSeenOnHost ? `<span class="muted">last on ${esc(boxName(a.lastSeenOnHost))}</span>`
|
||||
: '<span class="muted">parked</span>'}</td>
|
||||
</tr>`).join('') : `<tr><td colspan="6" class="muted">None seen yet.</td></tr>`}
|
||||
</tr>`).join('') : `<tr><td colspan="5" class="muted">None seen yet.</td></tr>`}
|
||||
</tbody></table></div></section>`;
|
||||
|
||||
parts.push(accountTable('Claude accounts', accounts.filter(a => a.provider !== 'codex')));
|
||||
@@ -402,7 +449,12 @@
|
||||
try {
|
||||
render(await post('/api/fleet/account', {
|
||||
account: inp.dataset.account,
|
||||
[field]: field === 'capacityLeft' ? (inp.value === '' ? null : Number(inp.value)) : inp.value,
|
||||
// The reset picker speaks PACIFIC WALL TIME; the store speaks ISO
|
||||
// instants. Converting here rather than in the store keeps the
|
||||
// timezone a presentation concern, which is what it is.
|
||||
[field]: field === 'capacityLeft' ? (inp.value === '' ? null : Number(inp.value))
|
||||
: field === 'resetsWeeklyAt' ? fromPacificInput(inp.value)
|
||||
: inp.value,
|
||||
}));
|
||||
} catch (e) { note(`could not save that: ${e.message}`); }
|
||||
}
|
||||
|
||||
@@ -53,6 +53,36 @@ project, "like an app creating a folder in a user's home dir." What it forbids i
|
||||
an app keeping data inside `app/`, `rigs/` or its own bundle. The distinction is
|
||||
ownership, not nesting.
|
||||
|
||||
### The test that settles every case
|
||||
|
||||
> **If I delete and reinstall this app, should this file survive?**
|
||||
> **Yes → the workspace. No → the app folder.**
|
||||
|
||||
That is the one law restated as something you can apply in five seconds, and it
|
||||
rules out the tempting option: **never colocate user data with the app.** The
|
||||
concrete failure is not hypothetical — PROVIDER MANAGER keeps your machine
|
||||
names, capacity figures and weekly reset times in `overrides.json`. Colocated,
|
||||
reinstalling or forking that app would take all of it with you, and a fork would
|
||||
carry someone else's account labels into a stranger's copy.
|
||||
|
||||
So the layout has four kinds and each is decided by that question alone:
|
||||
|
||||
```
|
||||
<workspace>/ EVERYTHING here is yours and survives any app
|
||||
media/ shared pool — things many apps read and write
|
||||
specs/ shared pool
|
||||
<app-id>/ app-scoped data that is still YOURS (fleet/, …)
|
||||
<project>/ project-scoped data
|
||||
apps/<app-id>/ code, UI, and fixtures that SHIP with the app — nothing of yours
|
||||
```
|
||||
|
||||
`<app-id>/` is the case people get wrong in both directions. It is not a
|
||||
violation — the canon explicitly permits an app to add its own subfolder, "like
|
||||
an app creating a folder in a user's home dir." What makes it legitimate is
|
||||
*where* it sits: inside the workspace, so an uninstall cannot reach it. Fleet
|
||||
state is the worked example — it is not media, not a project, and not the app's
|
||||
to keep, so it is `<workspace>/fleet/`.
|
||||
|
||||
**Bind the workspace, not the media pool.** The pool is derived (`<workspace>/media`).
|
||||
Binding the pool directly makes the root mean "one app's files" and leaves
|
||||
nowhere for projects or a second app's data — which is the mistake this layout
|
||||
|
||||
Reference in new issue
Block a user