fix: real machine specs, a volume meter, and one settings style system - #112
Merged
Conversation
…ilable" On canary a member's My-machine panel showed CPU, Memory, Disk and Price as "Unavailable" and the machine-type select showed the raw id `cx33@hel1`. The fetch was not failing and the catalog was not empty: `GET /machine-types` is what an organization may create NOW, not what its machines run on. `HetznerProvider.listMachineTypes` drops deprecated types, drops locations that report no availability, and keeps only the ids in HETZNER_MACHINE_TYPES; `core/app.ts` drops whole providers whose access is `credential-required`. A live machine on a type that has since left the catalog is normal and documented (hetzner-config.ts), and `machines.find` then finds nothing. The dialog had no answer for that and no reason to give, because it kept only `response.machineTypes` and discarded `failures` and `providerStatuses` — the two fields of the same response that say why the catalog cannot describe the machine. `CreateWorkspaceDialog` already reads all three. The panel now keeps the whole response. When the catalog holds the machine's type it prints the size exactly as before. When it does not, the four spec rows give way to one line naming the real reason: a provider failure and its message, a provider that needs an organization compute credential, or a type the catalog no longer offers. The type id stays on the "Machine type" row, because it is the one fact that is known. Second defect on the same lines: the catch typed its argument `Error` without checking, so a rejection that is not an Error set the banner to `undefined` and rendered an empty `role="alert"` paragraph. It goes through the shared `caughtErrorMessage` helper now. Tests mount the dialog with the exact envelope the control plane serves (machineTypes decorated with providerId and supportsVolumes, beside failures and providerStatuses) and pin the size rows, each reason line, and the non-Error rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6fUBY1B27EzvDwbhfBf52
The volume row said "Attached", which told a member what they could already
see. What they could not see is whether the disk they keep everything on is
about to run out. Nothing in the control plane knew either: a provider reports
a volume's size and never its usage. Only the guest can measure it.
Guest → control plane (new cross-runtime contract `machine-stats`):
- `box/rootfs/usr/local/bin/blitz-machine-stats report` reads `origin` and
`box-credential.json` out of the state directory exactly as `blitz-rules`
does, measures `df --output=pcent` on that directory — the volume's mount
point when there is one — and posts `{ diskUsedPercent }`. Every failure is
expected offline behaviour: it says why on stderr and exits 0, so a disk
figure can never take a box down. A df line that does not match `NN%` is not
sent at all, because `Number('')` is 0 and 0% is the one wrong answer that
looks like a right one.
- The s6 longrun `machine-stats` (dependency: `register`, like `watch`) loops
it every 600 s. The loop lives in the service so the script stays one shot
and stays testable. It reaches the field with the NEXT box image.
- `POST /workspaces/self/machine-stats` (`core/machine-stats.ts`) is
box-authenticated like the other `/workspaces/self/*` routes and writes the
caller's own machine row. It accepts an integer 0-100 and 400s everything
else — a float, a numeric string, a null, a missing field — because a wrong
figure overwrites the last true one and the column cannot say "this is a
guess". Extra keys are tolerated so a newer guest still lands its percentage.
No revision bump: a disk figure moving one point must not wake every poller.
- Fixtures `packages/schema/fixtures/machine-stats/` are the accept rule, with
conformance tests on both sides — the control plane over the real route with
a real box credential, the guest by running the real script against a local
origin and checking what it posts.
Storage and wire:
- Migration 0045 adds `machines.disk_used_percent` (nullable, CHECK 0-100) and
`machines.disk_reported_at`. Neither takes a default: null is the honest
pending state for every guest that predates the reporter, and 0 would read
as an empty disk.
- `MachineView.volumeUsedPercent: number | null` in `wire-machines.ts` and
`packages/schema` together, pinned by `wire-drift.test.ts` with both a
reported and an unreported machine. The projection answers null for a
machine with no volume: such a machine measures its VM's root disk, which is
not the durable thing the field names.
UI, in both places volume state shows:
- `src/VolumeMeter.tsx` renders the three states as three different facts:
attached and measured → the bar with "62% full"; attached and unmeasured →
the empty TRACK with "usage not reported yet"; no volume → "Not attached".
The word "Attached" is gone. The fill turns red at 90%, because a meter that
never warns is decoration.
- The My-machine panel and the details Members rows both use it. A member row
that holds a machine now reports the disk it has instead of a disabled
checkbox; a draft row with no machine keeps the checkbox, which is still a
real choice there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6fUBY1B27EzvDwbhfBf52
Six heading treatments across four stylesheets is why the settings surfaces drifted apart. `src/settings-surface.css` is now the single place the settings look is defined, under one `cfg-` prefix, with the rules stated at the top of the file so the next change extends it instead of inventing a seventh. The two anchors are tokenized rather than retyped: `--cfg-title-*` holds the colour, size and tracking the "Agent rules" heading carried in the Settings tab, and `--cfg-desc-*` holds its paragraph's. Every other value resolves to a token already in tokens.css. Nothing wears the classes yet; the following commits move each surface onto them and delete the one-off it replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the system Anchor 1: the frame is `height`, not `max-height`. It is sized to the tallest tab — Settings, whose form, agent rules, facts and repositories run past the 760px ceiling the dialog already had — so Members and Credentials now leave space under their content instead of shrinking the dialog around it. The body is the only thing that scrolls. Anchor 4: `.workspace-details-list > div:first-child` loses its border-top. The fact lists are divider-free `.cfg-meta` now, and the only lines on the surface are the section boundaries the system draws. All three tabs, plus the agent-rules picker they share with the create dialog, move onto `cfg-`: section heads with an ink sentence-case title and a muted description, sentence-case micro-labels in place of the tracked-out NAME / DEFAULT MACHINE TYPE caps, one actions row per form, and the footer's Delete as `.cfg-danger-action`. Deleted with their last caller: the micro-caps `h2` rules, the `.workspace-details-list` list, `.workspace-details-note`, `.workspace-settings-form`, `.workspace-settings-toggle`, `.workspace-repos h2`, `.workspace-credential-add`, `.workspace-details-delete`, `.blueprint-agent-rules` and `.blueprint-agent-rules-note`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three sections — Machine, Machine type, Lifecycle — with ink
sentence-case titles, the divider-free fact list, `.cfg-help` for the
notes that used to be `.workspace-details-note`, and `.cfg-actions` for
the lifecycle verbs. Destroy wears `.cfg-danger-action`.
The dialog has no tab row, so nothing about it resizes: it keeps its
content height and the 760px ceiling, and only the tabbed dialog is
pinned.
`.cfg-meta-term` joins the vocabulary for the one case a fact list must
re-case its value: a wire enum ("running", "admin") read by a person.
The list itself never re-cases, because an e-mail address is a value
too — which is what the old capitalize-everything rule got wrong the
moment the account profile list joined the system.
The my-machine test now finds the lifecycle buttons through
`.cfg-actions`, the class that styles them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The create-workspace dialog's section headings are the anchor the system was tokenized from, so they are the last thing that should have its own copy of the rule. `.blueprint-selection__heading` and `.blueprint-field` are deleted; every caller — the create dialog, and the template and recipe screens that share the markup — wears `.cfg-section-head`, `.cfg-title`, `.cfg-desc` and `.cfg-field` instead. The visible change is the micro-labels: NAME and DEFAULT MACHINE TYPE were 10px/700 uppercase tracked out .14em, and are now a quiet sentence-case line above their control. The cards keep their outlines — a card is structure, not a divider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profile facts become a divider-free `.cfg-meta` in a section, so
Appearance is separated from them by the system's line rather than by a
`border-top` of its own. Members and Invites get `.cfg-field` labels,
Members gets the shared `.cfg-danger` zone with a red `.cfg-danger-action`
Leave, Usage's notes become `.cfg-help`, and the two connections sections
get `.cfg-section-head` titles.
The all-caps micro-labels go with them: `.connect-field__label` on the
compute and provider forms is `.cfg-label` now, and the panel-header
eyebrow ("ACCOUNT SURFACE", "ORGANIZATION") is sentence case at the
description's colour and size.
Deleted: `.settings-definition-list`, `.settings-field`,
`.settings-danger`, `.settings-appearance` and its label and note,
`.settings-section-heading`'s typography, the credential-section
spacing, and `.connect-field__label`.
Two tests followed their markup: the provider-admin field labels and the
shell smoke test's Leave button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also two leftovers from the dialog conversion: the untabbed My machine body keeps the 240px floor the frame used to give it, and the dead `.workspace-repo-add` rule goes with the wrapper the repo section no longer needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The blitzdev emitter suite is vendor-gated (BLITZDEV_MANAGED=1), so the local gates skipped it and CI caught it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # packages/box/Dockerfile
pythonlearner1025
added a commit
that referenced
this pull request
Aug 30, 2026
main gained the memory boundary (#113), machine-stats (#112), env-file credential import (#114), the managed-count fix (#117), canary's R2 box image (#115) and the connections/mobile fixes. This branch is the Lody port, which deleted the box actor entirely. How each side was chosen: - `packages/box/actor/**` stays deleted. That was a direct user order and nothing here softens it. main's `actor/run` change is dropped with the service it wrapped. - machine-stats is actor-independent guest work, so it stays: the s6 service, `blitz-machine-stats` and the CP consumer land unchanged, and its conformance test moves to `packages/box/guest-tests/test/` the way the deletion commit re-homed every other guest test. The relative paths the test reads (`../../rootfs`, `../../../schema/fixtures`) resolve at the same depth, so nothing in it needed editing. - The memory boundary's one actor dependency was the placement of the process that hosts agents. The Lody daemon is that process now, so it inherits the scope: `lody-daemon/run` enters `user/lody.scope`, and `docs/MEMORY-BOUNDARY.md`, `blitz-cgroup` and `smoke.sh` say so. The daemon is dark by default, so smoke.sh reads the run script rather than looking for a node that is not there. - `.github/workflows/**` takes main's side whole — this branch never meant to touch canary.yml, and after the merge the directory is byte-identical to origin/main. - `CreateRecipeScreen.tsx` takes main's `cfg-` settings-surface styling and drops the chat-harness copy and `chatNeedsModel`, which have no referent on this branch. - `WorkspaceDetailsDialog.test.tsx` takes main's `IMPORT_PREVIEW_DEBOUNCE_MS` import beside this branch's `SessionRail` rename. - `shell-smoke.test.tsx`: main's new mobile-drawer test passed an `acp` port that `StandalonePorts` no longer has. Adding a file under `core/` touches three hand-maintained lists, and the merge is where two branches' additions meet. All three take the union: `worker-source.mjs` gains main's `workspace-credential-import.ts` beside this branch's `workspace-drain.ts`; `core-imports.test.ts` counts 109 (104 at the fork, +2 from main, +3 here); and the managed upload set in `blitzdev-emitter.test.ts` gains `core/wire-sharing.ts`, `core/session-shares.ts` and `core/workspace-drain.ts` to reach 112. Those three are 2.2 KB, 14 KB and 2.9 KB of source, so the emitter's 1 MiB-per-file platform limit still holds with four orders of magnitude to spare — and the test asserts it on every file, not just the new ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vk3ghWvYvp74ae9EUQPPkJ
pythonlearner1025
added a commit
that referenced
this pull request
Aug 30, 2026
§13.1 said "bake the box image" and then gave the command that bakes the GOLDEN HETZNER SNAPSHOT. Those are two different images, and the merge is where that stopped being a wording problem: #115 gave canary its own box image, served as an R2 archive from canary's account, pinned by three `BLITZ_DEPLOY_VAR_BOX_IMAGE_*` lines in canary.yml, and made `docs/BOX-IMAGE.md` the procedure. Cutting a `v*` tag — the old reflex for refreshing an image — now ships the platform to a paying client. So §13.1 names both images, says which one every Lody change is in (the box image, image 1), and says the golden snapshot is optional and reads image 1's pin rather than replacing it. A stale snapshot is only slow: the bootstrap fetches whatever `BOX_IMAGE_*` names, so it cannot ship an old box. §13.2 gains the machine-stats reading, because #112 landed a new guest → control-plane route in the same image and the obvious question is whether it needs a fourth capability cutoff. It does not, and the reason is the rule: a cutoff is needed where the OLD side rejects the NEW payload. The `share` claim has that problem — an older gateway refuses the ticket. A box that never POSTs machine-stats just leaves `volumeUsedPercent` absent. §13.4 gains the two checks the same recycle is the only chance to make: the volume meter appearing within ten minutes, and the daemon sitting in `blitz-user.slice/lody.scope` — the memory leaf it inherited from the deleted actor. Step 1 also notes that `boxImageRef` now reads as an R2 manifest URL, so the tag and digest beside it are what to check. Two line references in §13.2 moved with the merge and are corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vk3ghWvYvp74ae9EUQPPkJ
pythonlearner1025
added a commit
that referenced
this pull request
Sep 1, 2026
The redesign branched at #110, three hours before #112 landed `settings-surface.css` — the `cfg-` canon CLAUDE.md now names as the one system for every settings-shaped screen. Both sides restyled the same surfaces, so the resolution keeps the redesign's STRUCTURE and main's VOCABULARY: primitives, the org switcher, the surface ladder and the danger-zone/About headings survive, expressed in `cfg-` classes rather than in a second set of headings, fields, dividers and fact lists. Per file: - settings.css — the redesign's ladder and its `.settings-switch-*` shape stay; every rule whose markup moved to the canon is gone (definition list, appearance wrapper, `.settings-field*`, `.settings-danger*`, `.settings-note`, `.settings-credential-section`, `.connect-field__label`). The account page keeps its red frame around the canon's `.cfg-danger`. - workspace-details-dialog.css — the ladder's raised cards stay; the heading, field and fact-list rules the canon owns are gone, and #114's credential import and #112's volume meter come across whole. The settings form's machine-type select re-targets `.cfg-field` and takes the one control height. - SettingsPage.tsx — main's `.cfg-meta` profile facts and `.cfg-section` appearance block, plus the redesign's Organizations section, re-headed with `.cfg-section-head` / `.cfg-title`. - MembersPanel.tsx — the redesign's named "Danger zone" over main's `.cfg-danger` / `.cfg-danger-action` zone. - UsagePanel.tsx — the redesign's `SettingsSwitch` (it writes on change, which is what the primitive is for) with main's `.cfg-help` notes. - WorkspaceSettingsTab.tsx — main's `.cfg-field--inline` checkbox, NOT `SettingsSwitch`: that row is part of a draft the Save below sends, and the primitive's own contract forbids a Save beside it. `cfg-actions` and `cfg-meta` as main has them, with the redesign's "About" title. - WorkspaceDetailsDialog.tsx — main's `.cfg-section` credentials tab and its comment field, with the redesign's red Revoke. - CloudApp.tsx — the redesign cuts the desktop statusline; #155's BUG-CV-02 sentence keeps one desktop surface, because a box the browser cannot reach has no other affordance. The bar now shows on desktop when the terminal needs its sign-in pair OR when the gateway is unreachable. Also carried across the version gap: `settings-surface.css` gains the disabled state the redesign gave its danger button, the preview fixtures gain the fields main added to `MachineView`, `WorkspaceCredentialView` and `CatalogEntryView`, the preview gallery passes `refreshWorkspaces`, and two tests drop props the redesign removed. Gates: typecheck and lint:gate pass at baseline (74 anti-slop, 0 house). `npm test` passes except `webapp/test/lody-worktree-composer.test.tsx`, which fails the same way on origin/main alone — a pre-existing failure, not this merge's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two annotation rounds in one branch, follow-up to #110.
My-machine specs (the "Unavailable" bug)
Root cause:
GET /machine-typesis the catalog an org may create NOW — deprecated types, unavailable locations, and credential-gated providers are filtered out — so a machine running a filtered type had no size source and every spec row fell back to "Unavailable" with no error. The panel now keeps the whole response (failures,providerStatuses) and either renders the specs or one line naming the real reason. Rejections route throughcaughtErrorMessage; five tests pin the exact serve-shape.Persistent-volume meter
machines.disk_used_percent(0-100, nullable) +disk_reported_at.POST /workspaces/self/machine-stats, box-authenticated, with a newmachine-statsfixture corpus and conformance tests on both sides.blitz-machine-stats+ s6 service (10-min loop, fail-open) — reaches the field with the next box image; until then the UI says "usage not reported yet".MachineView.volumeUsedPercent; meter UI in My-machine and the Members rows: % bar (red at 90%), pending state, or "Not attached". The bare word "Attached" is gone.Settings surface style system
One canon stylesheet (
settings-surface.css,cfg-prefix, documented rules in its header): fixed dialog height (760px ceiling, body scrolls, tabs never resize the frame); sentence-case ink section titles at the agent-rules anchor size; muted descriptions; exactly one thin divider between adjacent sections and nowhere else; sentence-case micro-labels. Applied across the details dialog's three tabs, My machine, the account settings page and panels, and the create dialog; six scattered heading treatments deleted. CLAUDE.md names the sheet as canon.Gates: typecheck 0; lint at baseline (102/0, 7 max-lines); CP 639, actor 124, webapp 364, all pass; webapp build clean.
🤖 Generated with Claude Code