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 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-30 09:03:42 +02:00
co-authored by Claude Opus 5
parent ef8b46b6e1
commit 18a41e9ced
2 changed files with 57 additions and 0 deletions
+52
View File
@@ -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 `<error>`.
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.
+5
View File
@@ -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 - [ ] `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): 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 - [ ] `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 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 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 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.