From 18a41e9ced30ee2b2888aff0fe91908a143319e0 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sun, 30 Aug 2026 09:03:42 +0200 Subject: [PATCH] Plan showing an unreachable origin in the web UI (step 19) A wrong git token made the watcher fail every fetch for 57 minutes while the branches view kept showing its last known list. WatcherState already records lastFetchError and /api/watcher already serves it; only the UI never renders it. The step also folds in the logging volume: one outage wrote 297 identical warnings. Co-Authored-By: Claude Opus 5 --- docs/plan/19-watcher-health-in-ui.md | 52 ++++++++++++++++++++++++++++ docs/plan/README.md | 5 +++ 2 files changed, 57 insertions(+) create mode 100644 docs/plan/19-watcher-health-in-ui.md diff --git a/docs/plan/19-watcher-health-in-ui.md b/docs/plan/19-watcher-health-in-ui.md new file mode 100644 index 0000000..cbf37ab --- /dev/null +++ b/docs/plan/19-watcher-health-in-ui.md @@ -0,0 +1,52 @@ +# Step 19: Show a stalled watcher in the web UI + +Prerequisites: none — the data already exists, only nothing renders it. +Read `README.md` first. + +## Why + +On 2026-08-30 the Gitea token in the machine config on vm4006 was replaced by a placeholder string. +For 57 minutes GitTally failed `git fetch --prune origin` every ten seconds and wrote 297 warnings to the journal. +The branches view showed a calm, ordinary list the whole time: every branch with its last build, nothing amiss. +The failure was noticed only because an expected build did not start, and it took reading the journal to see why. + +A watcher that cannot reach origin means every branch row on the page is stale — the one thing the page exists to tell. +Silence is the wrong answer, and the journal is not the user interface. + +## What Already Works + +`Watcher.poll` records the failure: `state = state.copy(lastPollAt = …, lastFetchError = e.message ?: …)`, cleared again after a clean cycle. +`WatcherApiController` serves it at `/api/watcher` as `WatcherState`, which distinguishes three failure modes: + +- `lastFetchError` — origin unreachable (credentials, network, gone remote); +- `lastPollError` — the cycle crashed after a successful fetch; +- `running: false` — the poll loop is not scheduled at all. + +All three mean the same thing to a reader: what you see is not current. +The endpoint needs no change. + +## Code + +- `static/gittally.js`: fetch `/api/watcher` from the same polling cycle that refreshes the view, and show a banner while any of the three conditions holds. + Keep the existing discipline — a timeout on the fetch, and a failure of *this* request must never break the view's own refresh. +- `templates/fragments.html`: add a `watcher-banner` fragment to the `nav(view)` row so every view inherits it; hidden unless the script fills it. +- Wording says what is stale and since when, not just that something failed: the branch list is not updating, since `lastPollAt`, because ``. + Timestamps go through the shared formatting — `UiFormats` and `gittally.js` must produce identical formats (invariant in `AGENTS.md`). +- Do not overload `live-indicator`: it reports whether the *browser* reaches the server. + This banner reports whether the *server* reaches origin. Two independent failures, two independent signals. + +While in there, fix the logging volume: one outage produced 297 identical warnings. +Log the fetch failure when its message changes, not on every cycle, and log once more when the fetch succeeds again. +This is the same class as the `atTimes` warning that repeats every poll for an invalid slot — fix both or neither, but do not leave the new one behind. + +## Tests + +- `server/WatcherApiControllerTest.kt` — assert that a state carrying `lastFetchError` reaches the JSON, so the field the UI depends on is covered. +- `watcher/WatcherTest.kt` — a failing `fetchOrigin` sets `lastFetchError` and a following clean cycle clears it; a repeated identical failure logs once. +- The JavaScript has no test harness; the banner is verified manually below. + +## Verification + +- Break it deliberately in a scratch repo install: set `git.token` to a wrong value, and watch the banner appear within one poll interval and disappear again after fixing it. +- Stop the watcher (`running: false`) and confirm the banner says so in its own words rather than reporting a fetch error. +- On a narrow viewport the banner must not push the table off screen. diff --git a/docs/plan/README.md b/docs/plan/README.md index 61508d4..6a5ea90 100644 --- a/docs/plan/README.md +++ b/docs/plan/README.md @@ -82,6 +82,10 @@ Added after v0.9.19 replaced the per-branch settings with build definitions (202 - [ ] `18-remove-branches-section.md` — delete the legacy `branches` section and its `autoBuild` schedule; run around 2026-09-05, after the precondition check in the step file +Added after a silent 57-minute fetch outage on vm4006 (2026-08-30): + +- [ ] `19-watcher-health-in-ui.md` — show an unreachable origin in the web UI instead of only in the journal + Added for running GitTally on Hostsharing Managed Webspaces (2026-08-10): - [ ] `17-bwrap-build-runtime.md` — GitTally on a Managed Webspace: bubblewrap user-namespace build sandbox with a prepared rootfs (precondition check first — see the step file), plus web access under a domain via the platform's Apache proxy and Let's Encrypt @@ -94,3 +98,4 @@ Step 13 depends on 07, 11, and 12. Step 15 depends on 12 and 13 and revises the containerized-runtime sketch in `docs/bootstrapping.md` (ADR 0006 is written as part of the step; GraalVM native image was evaluated and rejected there). Step 17 depends on 11, 15, and 16, and starts with a hard precondition check on the target webspace (ADR 0007 is written as part of the step). Step 18 depends on nothing in code but on the watched repository having migrated — its precondition check is a hard gate, not a formality. +Step 19 depends on nothing; `WatcherState` and `/api/watcher` already carry everything it needs to render.