WIP: build(dev-rig): a disposable server to test the compose path against #6
Loading…
Reference in a new issue
No description provided.
Delete branch "build/dev-rig"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
First of four. Stands up a throwaway server with its own dockerd and OpsDeck nested inside it using the production mounts, so the compose path and the
--ansi neverprogress parsing incompose_progress.tsare actually exercised. The nesting is the point:hostexeconly considers thechrootstrategy when the host root is set and is not/, so running OpsDeck as the rig's own PID 1 drops the probe to["direct"]and silently tests the engine-API fallback instead.No app code.
docker/dev-rig/and one paragraph in CLAUDE.md.Three seeded stacks in a git work tree (
web,jobs,broken), plus a stub IdP underRIG_OIDC=1so role-dependent paths are testable.Also carries the fixes from the review on the first round: the exported image no longer lands in the production build context, a failed IdP start is fatal instead of booting against a dead issuer, the stub implements the refresh grant, SIGTERM shuts the nested engine down instead of letting docker SIGKILL a mid-write DuckDB,
login.shfails loudly at whichever leg broke, anddown.shremoves the rig and every volume it owns.Verified on a cold rig: overlay2,
hostExec chrootwithhostRoot=/host/root, all three stacks up, graceful stop in ~3s, and underRIG_OIDCa viewer 403 / admin 200 split with the refresh grant surviving repeated revalidation.Answering a depends_on prompt started the update without waiting for it and presented the next queued prompt in the same microtask. But the server reserves the whole compose project for the length of an update, and that check runs before the dependents check — so the second acknowledgement came back 409 "update already running", with no needsConfirmation field to route it. The human read a destructive warning, accepted it, and got a raw HTTP 409 in the row's error line: the exact outcome the queue was added to prevent. Queue entries now carry their project, and only a prompt whose project is free is shown. The ones waiting say so in their row instead of going quiet, and the dialog holds a busy state while the update it just acknowledged is being started. The same reservation explains two more. stackLocked only tracked stack updates although /container/:id/update takes the identical lock, so sibling buttons stayed enabled and 409'd; it becomes projectBusy(), which also counts per-container updates. And subscribeLive("docker:update") registered no onResync, so a dropped channel — routine, the socket closes after a minute hidden — lost the terminal event that is the only thing clearing the in-flight flags, wedging every button in the project until a reload.`deno lint` reported 23 problems and nothing enforced it, so they had accumulated. Seven of them were one function: safeUpgradeWebSocket, which took `any` for the handler, the context, the options, the events object and every callback argument, and was then cast to `any` again at the call site behind a deno-lint-ignore. It is typed now: hono's own WSEvents for the shape it must produce, the SDK's ModuleContext["upgradeWebSocket"] for the contract it fills, and a single documented assertion where those two meet — which is the only place they genuinely differ, because the SDK describes the callbacks in its own terms so modules never import hono. The rest: three require-await middlewares now `return await next()`, which is what makes them honestly async (the MiddlewareHandler contract needs the promise, so dropping `async` was not an option); a regex with two literal spaces says ` {2}`; DuckDBConnection is a type-only import; and an unused type import is gone.Every hand-written page fetched its data as `apiFetch(...).then(...)` with no rejection path. A 500, a dropped connection, a restarting server — `data` stays null, the `{#if !data}` branch keeps rendering its skeleton for as long as the page is open, and the only trace is an unhandled rejection in the console. Nothing on screen says anything is wrong, and nothing ever will. Three pages were worse than having no catch: ContainerPage, StackDetailPage and PoolDetailPage caught, handled 404 by navigating away, then `throw e` for everything else — a rethrow inside .catch is a rejected promise nobody awaits, so it produced the same dead skeleton plus a warning. The schema renderer already did this correctly (SchemaPage catches and renders EmptyState with the error as its hint), so the mobile path degraded properly while the desktop pages did not. Pages now match it. The three overview widgets get one line instead: they render into a fixed ~260px slot on the home page where an EmptyState does not fit. StoragePage reports its two fetches separately, because smartctl can be unavailable while the mount table is perfectly fine. Each of these is nine or so lines of the same shape, which wants a shared helper — but the .then bodies do page-specific work (StacksPage prunes its confirmation queue, ContainerPage drops a stale prompt), so a generic loader needs callbacks, and there is no browser test framework here to prove that refactor did not change behaviour. Left as the local pattern SchemaPage already established.The focus effect read `open`, `variant` and the two element bindings, but not `busy` — and both buttons are `disabled={busy}`, so .focus() on them does nothing. It never re-ran when `busy` cleared either. That is reachable in the queued-confirmation flow this branch added: StacksPage renders DependentsConfirm inside `{#key confirmUpdate?.id}` with `busy={confirming}`, so when the answer to one prompt is still in flight and the next queued prompt becomes current, the new dialog is created disabled, focuses nothing, and stays that way. Escape still worked, Enter did nothing, and the Tab trap's swap sent the first Tab to "Update anyway" — the exact invariant ("focus lands on the safe choice for destructive actions") that justified removing the Enter handler in the first place. The focus effect is now separate from the keydown effect and tracks `busy`, so it fires as soon as the buttons can take focus; splitting them also keeps the focus-restore cleanup from running on every re-run. The Tab trap no longer assumes focus is on one of the two buttons — when it is on neither it goes to the safe one instead of falling through to the dangerous one.Superseded by #10, which merges this branch together with the rest of the stack and resolves the collisions between them. Closing rather than merging in place: replaying these merges one PR at a time would reproduce the same conflicts and lose the resolutions. The branch is untouched.
Pull request closed