hub v0.176.1: Restart controller for every guest of a customer host (0.176.0 gated it on a controller version the agent does not report — found live); R-1345 filed (token scopes cannot be edited) — test red first
gates / gates (push) Successful in 1m46s

This commit is contained in:
admin committed 2026-10-29 12:57:14 +01:00
1 parent 682015db86
commit d0dbc6f1cd
5 files changed
+29 -5

No files matched your search

+2 -1
View File
@@ -323,7 +323,7 @@ stopping line that lies.
| **R-528** | Monitoring & notifications | P2 | **[P2-MEDIUM] Docker does not report an OOM kill inside a Felhom LXC guest: `OOMKilled` stays false and no `oom` event fires, so the v0.243.0 OOM line is not proven live.** MEASURED 2026-09-15 on scratch 9202 (Docker 29.8.0): Paperless capped at 128M restarted 11 times with `OOMKilled=false` and zero `docker events --filter event=oom`; a memory hog inside the running container was killed (rc 137) with the same silence (`E2-oom-signal-measure-9202.txt`). BIGNIGHT VM 333 did read `oomkilled=true`, so the shape differs by case. **Fix shape:** the agent reads the guest container cgroups' `memory.events oom_kill` counters (host-side, reliable), or the controller alarms on a restart-count trend **RE-MEASURED 2026-09-16 on the DRILL box (fresh install, nested VM 334, Docker in an LXC guest, controller 0.243.0), so the finding is not a property of one machine:** the Paperless webserver was capped at 128 M with `docker update --memory`; it restarted 9-10 times, and all three signals stayed silent - `OOMKilled=false` on every inspect, `docker events --filter event=oom` EMPTY for the whole window, the container's cgroup not visible from inside the guest, and `dmesg` unreadable there. Identical to scratch 9202. So the v0.243.0 OOM line cannot fire on ANY Felhom box as shipped, on either host. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-m1-oom.txt`. | WATCHING — **released 2026-10-29:** agent 0.168.0 (vouched 10:21:56 CET) + controller 0.347.0 (the global floor). Proven on the release on Tester 1: a hog in a 128 MB Gokapi — host counter 1, one `app_oom` „seen by docker,cgroup", one operator mail (`audits/d63-2026-10-29/run-p4-oom.log`; the repeated WARN is R-1341). Closes at the dated check 2026-11-12 if it holds. | — | 2026-11-12: read the hub for any failed `oom_check` or an `app_oom` with no restart reason since 2026-10-12; none → close | CC |
| **R-1344** | Monitoring & notifications | P3 | **A controller that RUNS but does not answer is not restarted by anything — only a stopped one is (R-523).** Operator brief 2026-10-29, Part 2: the agent may restart it only after a measurement shows a busy night (backups, updates) never looks „not answering". Built 2026-10-29 as ALARM ONLY (agent `637c92c`: 6 consecutive sweeps of Docker's health check „unhealthy" — on top of Docker's own 3 failed probes, ~4.5 min in all — set `unresponsive_since`; the hub mails `controller_unresponsive`, operator-only). The measurement: Docker's own probe record (`curl -f localhost:8080/api/health`, every 30 s, timeout 5 s) logged every 30 s on Tester 1 and demo-hp from 2026-10-29 11:14 UTC to 2026-10-30 07:00 UTC — through tonight's real night chain (a day run cannot hold the whole-guest vzdump). | OPEN — measuring | — | read the measurement (worst probe times, any 5-s timeout, any „unhealthy"); if none fired: switch the alarm to a restart (same guards and brake, `controller_unresponsive_restart`) | CC |
## Hub & operator — 12 rows (P3 3, P4 9)
## Hub & operator — 13 rows (P3 3, P4 10)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -339,6 +339,7 @@ stopping line that lies.
| **R-1056** | Hub & operator | P4 | **Five operator routes fit no token group, so they are session only until the operator places them:** apps reset-telemetry and dismiss-issues, the Configuration asset re-seed, the off-site WireGuard endpoint save, the off-site abandon-cancel (`05` §17.3). Nothing needs them by token today. | WAITING-ON-OPERATOR — filed 2026-10-15 by the token build | operator decision | Place each in a group, or leave session only | operator |
| **R-1164** | Hub & operator | P4 | **Move the website to Hetzner (maybe the JARRs.eu prod server) — an operator idea, no decision asked yet.** If it is done, the website's git-syncs must read the public `https://gitea.dooplex.hu/admin/felhom.eu.git` again: since 2026-10-19 (R-1163) all four read Gitea in-cluster, which works only inside the DooPlex cluster (`01-topology-and-trust.md` §7, `manifests/webpage.yaml`). | OPEN | the operator's idea becoming a decision | When decided: switch the four `--repo` values together (gate 6 keeps them identical); keep `--max-failures=-1` | operator |
| **R-1293** | Hub & operator | P4 | **Once a year the hub's 02:00 database copy never reaches ep0, and the 20:00 copy is pushed twice instead.** Measured 2026-10-26 during R-1183's read (`audits/night-2026-10-25/r1183-measurement-2026-10-26.txt`): on 2026-10-25 (the clocks go back) 02:00–03:00 happens twice. The hub's `nextDailyRun` uses `time.Date`, which picks the SECOND 02:00 (CET, 01:00Z — measured with Go). DooPlex's `felhom-hub-db-backup.timer` fired at the FIRST 02:30 (CEST, 00:31Z) and pushed `hub-20261024T180000Z.db`, 391 min old — under the 7 h limit, so no refusal and no alarm; the 08:30 push took the 08:00 copy. Effect: for one morning a year ep0's newest hub copy is 12 h old, not 6 h; nothing is lost on the hub. The spring night (2027-03-28, 02:00–03:00 does not exist) is not measured. | OPEN | — | pick one: the push refuses (and says so) a newest snapshot older than the slot it serves, or the hub and the timer agree on one 02:00; then read the 2027-03-28 night (DUE-CHECKS) | CC |
| **R-1345** | Hub & operator | P4 | **An access token's scopes cannot be changed after it is made — widening or narrowing one means a new token (and a new secret).** The operator, 2026-10-29: to give CC `hosts:read` + `hosts:write` for the live proofs he had to make `CC_HOSTS` (now in `sec`) beside `cc-releases-2`. Wanted: edit a token's scopes in the session (an audit line per change), so a scope can also be taken away after a session without retiring the secret. | OPEN | — | design + build the scope edit (session only), red-first test that an unticked scope is refused at once | CC |
## Business & legal — 14 rows (P2 4, P3 2, P4 8)
+6
View File
@@ -1,3 +1,9 @@
## v0.176.1 (2026-10-29) — „Restart controller" for every guest of a customer host (0.176.0 showed it only with a reported controller version, which the agent does not report)
- Found live on 0.176.0 (Tester 1's host page): no button — the agent's host report carries no controller version, so the
version-gated button never rendered. Every guest of a customer host has it now; the agent refuses a guest it did not
provision. Test `TestHostPage_RestartRendersForAGuestWithNoReportedVersion` (red on 0.176.0's template).
## v0.176.0 (2026-10-29) — „Restart controller" and „Clear hold" on the host page (D64 = B); controller_unresponsive (R-1344) — DEPLOYED on the operator's yes (image sha256:53372198…, from `3d0c907b`, CI 2503)
- **The AGENT's operator actions (operator brief 2026-10-29, Part 1; `04` §6.1).** `operator_actions` gains a `host_id`
+17
View File
@@ -92,3 +92,20 @@ func TestHostPage_RestartAndClearHoldPosts(t *testing.T) {
t.Fatalf("rows = %d, want the two accepted presses only", len(rows))
}
}
// Live 2026-10-29 (hub 0.176.0, Tester 1): the agent reports guests WITHOUT a controller version (the column reads „—"),
// so a button gated on the version never rendered. Every guest of a customer host gets it; the agent refuses a guest it
// did not provision. RED-PROOF: seen failing on 0.176.0's template (gated on .ControllerVersion).
func TestHostPage_RestartRendersForAGuestWithNoReportedVersion(t *testing.T) {
s, st := newTestServer(t)
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil {
t.Fatal(err)
}
now := time.Now()
if err := st.UpsertGuestFromReport(&store.Guest{GuestID: "h1-9201", CustomerID: "c1", HostID: "h1", VMID: 9201, Status: "running", LastSeenAt: &now}); err != nil {
t.Fatal(err)
}
if body := renderHost(t, s, "h1"); !strings.Contains(body, `data-restart-controller="9201"`) {
t.Fatal("no Restart controller for a guest whose controller version the agent does not report")
}
}
+3 -3
View File
@@ -174,8 +174,8 @@ func TestHandleHostDetail(t *testing.T) {
// Decision 185 (2026-10-08): plus the four operator-action buttons — a closed list, none of which
// deletes or mutates the host (TestOperatorActions_* pin what they may do). Every OTHER button
// remains absent: 2 log requests + 4 operator actions, and the 4 all post to /operator-action.
// 2026-10-29 (operator brief, Part 1): plus ONE „Restart controller" per guest that runs a controller (the AGENT's
// closed list, behind a confirm step; this fixture has one such guest). „Clear hold" appears only per held app (none
// 2026-10-29 (operator brief, Part 1): plus ONE „Restart controller" per guest of a customer host (the AGENT's closed
// list, behind a confirm step; this fixture has one guest). „Clear hold" appears only per held app (none
// here). No other button.
if got := strings.Count(strings.ToLower(body), "<button"); got != 7 {
t.Errorf("host detail has %d buttons, want exactly the 2 log-request + 4 operator-action + 1 restart-controller buttons", got)
@@ -184,7 +184,7 @@ func TestHandleHostDetail(t *testing.T) {
t.Errorf("operator-action forms = %d, want 5", got)
}
if got := strings.Count(body, `value="restart_controller"`); got != 1 {
t.Errorf("restart_controller forms = %d, want 1 (one guest with a controller)", got)
t.Errorf("restart_controller forms = %d, want 1 (one guest)", got)
}
if strings.Count(body, `action="/hosts/demo-felhom-01/request-logs"`) != 2 {
t.Error("the request-logs forms are missing — every button must be a log-bundle request")
@@ -134,7 +134,7 @@
<td>{{if .DisplayName}}{{.DisplayName}}{{else}}—{{end}}</td>
<td>{{if eq .Status "running"}}<span style="color: var(--green)">{{.Status}}</span>{{else if eq .Status "stopped"}}<span style="color: var(--red)">{{.Status}}</span>{{else}}{{.Status}}{{end}}</td>
<td>{{if .ControllerVersion}}<code>{{.ControllerVersion}}</code>{{else}}—{{end}}
{{if and .ControllerVersion $.CustomerID}}{{/* 2026-10-29: the AGENT restarts it (the supervisor's act and guards; 3 in 15 min, then refused for 30) — works with the controller stopped */}}
{{if $.CustomerID}}{{/* 2026-10-29: the AGENT restarts it (every guest: the agent reports no controller version, and it refuses a guest it did not provision) (the supervisor's act and guards; 3 in 15 min, then refused for 30) — works with the controller stopped */}}
<details class="op-confirm" data-restart-controller="{{.VMID}}">
<summary class="btn btn-sm btn-outline">Restart controller</summary>
<form method="POST" action="/hosts/{{$.HostID}}/operator-action">