feat(frontend): the app says what it is running, and says so honestly when it cannot find out (#3329)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 31s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 38s

#3127 checklist 12, plus rule 27 — a capability with no surface the operator
can touch is not shipped.

The step was planned on the premise that nothing read `/api/version`. Two
things did, and the state was worse than nothing:

- `App.vue` fetched it, wrote `version` into a ref initialised to the literal
  `"dev"`, and swallowed the error. An instance that could not answer rendered
  EXACTLY what a healthy local build renders. That is checklist 12's named
  failure — a blank standing in for `unknown` — in the one readout whose whole
  job is to say what is running, and it would have made #3298's debugging
  session no cheaper.
- `SettingsView.vue` fetched the same endpoint again on every mount and wrote
  the result into a local ref no template ever read. A duplicate request whose
  answer was discarded.

So this is not "add a readout"; it is "make the existing one honest, and give
it the three fields nobody could see."

The readout — Settings → Config, first section, beside the other "what is this
instance doing" facts. Three states kept apart, because collapsing any two of
them is the defect:

  not asked yet (tab unopened)   nothing
  answered                       the values, each ABSENT field as "unknown"
  the fetch itself failed        its own message, with a retry

`version` and `channel` prominent, `commit` in full with a copy button so it
can be pasted into a `:sha` lookup (rule 145 — the registry's identity and the
artifact's own must be checkable against each other), `build` kept because its
ABSENCE is the diagnostic part: no ordering key means this build is not in any
update order, which is what a local or hand-built image looks like.

Absence, not falsiness. The payload omits what it does not know rather than
sending `""` or `0` (see `build_version_payload`), so the renderer uses `??`
throughout — `build` is a number and `0` is a legitimate ordering key, which
`||` would report as unknown. `tests/test_version_readout.py` pins that
operator specifically, along with the "no plausible default" property, because
`||` is the form a person reaches for by habit.

Rule 156 — the fetch carries a deadline. This readout is consulted when an
instance is misbehaving, which is exactly when it may never answer; without one
the surface sits on "still loading" forever, which is the same blank arrived at
from the other direction. `apiGet` gains an OPT-IN `timeoutMs` rather than a
default, so no existing call site's behaviour moves. Every other call in the
client still has no deadline — reported separately, not fixed here.

No frontend test runner exists, so verification is the typecheck lane plus four
source-inspection guards in the unit lane, each pinning one property.

Also folded in: `plugin/README.md` now leads with the mint script and offers
`make` second, since `make` is not installed on every workstation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcCs1CcQ1ormdnzSshKqvN
This commit is contained in:
2026-09-02 11:23:43 -04:00
co-authored by Claude Opus 5
parent f5a3643da8
commit 9bb59b73ba
6 changed files with 283 additions and 16 deletions
+3 -3
View File
@@ -79,9 +79,9 @@ On install you'll be asked for:
## Notes
- **Do not hand-edit `version` in `.claude-plugin/plugin.json`.** It is minted
from the clock — run `make mint-plugin` (or `python3
scripts/mint_plugin_version.py`) after changing anything under `plugin/`, and
commit the result. The installer decides whether to refresh the cache it
from the clock — run `python3 scripts/mint_plugin_version.py` (or `make
mint-plugin`, where `make` is installed) after changing anything under
`plugin/`, and commit the result. The installer decides whether to refresh the cache it
executes from by comparing that string, so content that ships without a new
version reaches the repo and stops there (#2209). CI fails the lane if you
forget.