Commit Graph

1430 Commits

Author SHA1 Message Date
admin d3b284863e hub v0.120.0: app_update_undone/held reach the household, per app, in its language
gates / gates (push) Successful in 26s
Allowlisted, not operator-only, seeded for new households and added
once (add-only) to every existing enabled_events row. mail.event entries
in hu and en name the app from details.stack_name. Per-app cooldown on
both the operator and the household leg.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 13:47:13 +02:00
admin 05ea21e918 The undo, built and proven live: controller v0.263.2 (09 decision 15)
gates / gates (push) Successful in 25s
- 09 §6.1 phase table (copying, undoing, undone), §6.1a SHIPPED with the two
  live-only defects, §6.4 part 1 SHIPPED.
- Capability map: a failed update is undone by the box - PROVEN-LIVE.
- Live evidence on 9202: three apps undone by the product with seeds before
  the backup, after it and seconds before the press read back; cut-off copy
  held honestly; power cut during the undo resumed; manual press after undo.
- Register: R-637, R-639, R-641, R-642 closed; R-638, R-640 narrowed; R-643
  ruled; R-646 opened. STATUS asks the floor question.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 12:25:13 +02:00
admin 5a349d9884 Undo bake-off: copy the folder wins (09 §3 decisions 19-20, §6.1a)
gates / gates (push) Successful in 26s
- 09 §3: decision 19 (the copy method is chosen by a bake-off) and 20 (the
  full-system backup waits for the update leg, inside its window; built later).
- Bake-off on 9202, docmost / romm / vikunja: both methods pass every case;
  the folder copy wins because an app with no database server gets no dump,
  so dump-and-load would need the folder copy anyway. 1-5 s extra downtime,
  ~420 MB/s, disk = the volumes.
- R-645 filed: lifting an update hold by hand lets the recovery unit be
  re-captured with the failed definition within seconds.

Documents and evidence only; product code follows in the controller.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 10:28:56 +02:00
admin 4c92beab8f Update arc: the undo and ladder spiked, the build plan for the 2026-09-23 rulings
gates / gates (push) Successful in 26s
- Part 1 (09 §6.1a, audit): the undo performed by hand on 9202 for docmost
  (PostgreSQL), romm (MariaDB) and vikunja (SQLite volume) - all three came
  back with data written before AND after the backup. The product's loader
  cannot do it: over a migrated PG database it fails on the new tables'
  foreign keys; over MariaDB it leaves them behind. A truncated PG copy loads
  rc 0 into an empty database. No-DB apps have no last-second copy.
- Part 2: one press jumps A -> C; the box's catalog clone is depth 1.
  Ladder format recommended: update_ladder in .felhom.yml, not git history.
- Part 3: memory watch red-proof results (harness change in the catalog repo).
- Part 4 (09 §6.4): ten parts, ~22 evenings; one open point (R-643).
- Rows R-637..R-644 opened; R-446/450/451/462/463 updated. STATUS, CONTEXT.

No product code. Live catalog untouched; 9202 back on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 08:54:41 +02:00
admin 805ad1e962 docs(update arc): record the operator's rulings of 2026-09-23 (09 §3 decisions 11-18)
gates / gates (push) Successful in 29s
- 09 §3: decisions 11 (one window = a leg of the backup chain), 12 (automatic,
  per-box switch on by default), 13 (the test decides, not the tag - replaces
  decision 3's "never across a major"), 14 (the ladder), 15 (the box undoes a
  failed update - replaces §6.1's no-auto-undo), 16 (Postgres majors converted
  by the box), 17 (digests), 18 (fleet view, later).
- 09 §3b marked ANSWERED with a pointer per question; kept as the reasoning.
- 09 §6.2 rewritten to the ruled shape; §6.1 abort paragraph and §4 point at 15.
- Register: R-450, R-451, R-446, R-463 cite the decisions.
- STATUS: the seven questions no longer wait on the operator.

Documents only. No product code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 07:55:04 +02:00
admin 267dcad01b floor raised 0.261.0 -> 0.262.1, MinAgent 0.131.0 declared
gates / gates (push) Successful in 26s
Both demo boxes took it in under 20 seconds and read healthy. drill-r50 is DOWN and correctly HELD:
its agent is 0.129.0, below the declared MinAgent, so the hub refuses to hand it a controller it
cannot run (R-472's guard, working).

Boxes below the floor: 5 -> 3. The three that remain are drill-r50 and the two guests the hub does
not hear from, including scratch 9202 which runs hub.enabled: false.

Read back from the hub rather than from the POST's own answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 22:10:38 +02:00
admin 22439b0e43 v0.262.0 + v0.262.1 live on 9202: four scenarios proven, R-630/R-633/R-621/R-614 closed
gates / gates (push) Successful in 30s
A (R-630, the P1): paperless-ngx, same app same button - failed at +313.0s with the app stopped
under v0.261.0, done at +53.4s now, with no "no probe container" warning because the explicit
healthcheck.container resolved the target.

B (R-633): the busy guard fires - RemoveStack REFUSED (busy): a backup or restore is running - and
the live proof caught it answering HTTP 500, because router.go maps remove errors by grepping the
error TEXT. v0.262.1 makes it a typed error and a 409; re-proven live.

C (R-634 half): a half-state with app.yaml on disk and deployed=false answered 200, leftovers NONE.
Under v0.261.0 the same call said "not deployed".

F (R-614): phase done before the remove, no phase at all after redeploying the same name.

Also: the first B run proved NOTHING and nearly went down as a pass - the refusal came from the
pre-existing "still running" check, not the new guard. Recorded.

09 6.1 and 8.8, the capability map, and STATUS updated. R-625 and R-634's mechanism are named as
owed, not half-done. Register 325.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 21:53:53 +02:00
admin 1de6aaf904 corrections: calibre-web restore was REFUSED, calcom was INCONCLUSIVE
gates / gates (push) Successful in 25s
Both from their own records, not re-run.

calibre-web: the flash_error refusal is in its log; that walk predated the refusal-capture code.
The report's own prose already said so while the table disagreed.

calcom: the restore was accepted and the app read running; the classifier then saw "starting", a
settling state it did not list beside running/unhealthy, and fell through to failed. The brief
supposed a read-back artefact - that is wrong, and restore_state_seen says so.

Restores correctly refused: 2 -> 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 20:18:37 +02:00
admin 737694c603 CORRECTION: the app_oom alarm DID fire - R-635 was wrong, R-636 opened
gates / gates (push) Successful in 26s
The operator produced the mails. The controller HAS an OOM detector (main.go:821), it emits
app_oom (notifier.go:726), the hub allow-lists it (dispatcher.go:636) and delivered it to the
OPERATOR channel - two mails, 11:09 and 17:48 CEST, each naming the app and linking the dashboard.
CUSTOMER skipped, correctly.

I asserted an absence without opening the hub's Events or Notifications tab, reasoning instead from
a memory note that said the signal was UNPROVEN - not that it was missing. That is R-628's shape
again, from the same hand, four days later.

R-636: the real defect is the signal's SHAPE. notifier.go:715-724 keys on container|startedAt and
emits once per container lifetime, so 4530 worker kills over six hours produced exactly one
warning-level mail - indistinguishable from one transient kill. The magnitude was already collected
(App Telemetry: RomM 5023 errors, 632 warnings) but nothing turns it into a louder event.

Memory note lxc-docker-oom-signals-unreliable corrected with the positive reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 20:13:41 +02:00
admin 8efd2d00df romm OOM storm on demo-hp: fixed, measured, closed (R-635)
gates / gates (push) Successful in 26s
Found because the operator heard the fans. romm 5.3.0 was promoted that morning; the update read
`done` and the app ran clean for two hours, then OOM-crash-looped for six - 4530 worker SIGKILLs,
~500% CPU, host load 5.2 while otherwise idle, and nothing alarmed.

Raising the limit to 768M was still a guess and fixed nothing (memory.peak hit exactly 768 MiB).
Measured instead: ~216 MiB per warm uvicorn worker, so the image's default of 4 workers needs
~882 MiB. /init reads WEB_SERVER_CONCURRENCY; set to 2.

Proven under load, not just at idle: 26,645 requests over 300 s, memory 416-614 MiB against 768,
trending down, zero SIGKILLs, OOMKilled false. Idle CPU 500% -> 1.64%.

The first soak measured nothing - it was pointed at the scratch-guest subdomain, every request
404'd at traefik in 9 ms, and the counter reported 14,026 successes. Positive and negative controls
are now asserted before any load is driven.

Carried into R-462: `proven` has meant "the update applied and the data survived", not "the new
version runs".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 18:04:49 +02:00
admin 186546d562 THE TWENTY-EIGHT: every app no drill had touched, walked in one night
gates / gates (push) Successful in 27s
All 28 walked on scratch guest 9202 against the private drill catalog. 26 deployed, 6 proven,
5 inconclusive, 14 with no upstream edge, 1 failed honestly (outline 1.9.1->1.10.1, HELD with the
right sentence), 2 undeployable - one (plant-it) by design, refused by the lifecycle gate, proven
live for the first time. Each app also got the half the update night skipped: a restore from its own
copy with the seed read back again - 21 restored, 2 correctly REFUSED per 07 6.2.

R-630 RAISED TO P1 by measurement: a stack with NO probe container does not skip verifying - it
waits out the full health timeout and HOLDS, stopping an app whose three containers read healthy.
The controller's own words: "not healthy within 5m0s (last: no probe container)".

R-633 opened: a remove sent during a restore reports success and leaves a container restarting with
a live public route. The product already refuses that clash for update and for restore, naming the
blocker; remove has no such guard.

R-634 opened: an app can be running, healthy and serving while recorded as deployed=false, and is
then unremovable. Reproducible alone on sparkyfitness; concurrency-linked on two others.

R-631 and R-632 CLOSED. Register 321 -> 323. Seven interventions, six of them my own harness -
named, with what each cost. No product code. The live catalog's image: lines are byte-identical to
the start of the night.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 16:00:56 +02:00
admin a975cfde5b probe fix, the gate, and the promotion train (R-618 closed, R-630..632 opened)
gates / gates (push) Successful in 28s
Part 1: tandoor/zipline/wger probes corrected in the catalog and red-proofed live on 9202 in both
directions - "Nem egeszseges" with the front door serving 200, then "Fut" after the real sync with
no redeploy. tandoor's failed edge re-walked: done at +41.1s where it was failed at +361.9s.

Part 2: fifteen proven versions on the live catalog, one commit per app; the guarded Update pressed
on four apps on demo-hp, all four done.

Opened: R-630 (paperless-ngx's probe has never run on any box - a silent absence, worse than the
wrong probe that was found in one night), R-631 (five templates no static rule can judge),
R-632 (28 of 53 templates never deployed by any drill). Closed: R-618.

Register 318 -> 321. No product code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 11:10:58 +02:00
admin 462ab4a5ff Part 0: repair the register, gate its shape, and record Hetzner's answers (R-627, R-628, R-629)
gates / gates (push) Successful in 29s
THE REPAIR. Last night's append regex ate the state cells of R-446 and R-458, left them as a stray
fourth cell on duplicated copies of R-626 and R-625, and split the table with blank lines. The
register read 317 rows for 315 findings. Both cells restored from the cells that carried them, the
two duplicates deleted, and 15 blank lines that split the register into 12 separate markdown tables
removed. Every row's text is byte-identical afterwards, proven by diff; no row added or removed.

THE RED-PROOF FOUND AN OLDER INSTANCE: R-254 lost its state cell on 2026-08-08 (59527d0) and had
rendered without a State column for 45 days. Its own verdict sentence was the cell; it has it back.

THE GATE. scripts/register_shape_gate.py, gate 14 in repo_gates.py and reached by the pre-push
hook: a row that does not end with `|` (an eaten state cell), a duplicated id, or a blank line
splitting the table. Four decoys in test_gate_decoys.py — three convicting on the exact damage
shapes, one asserting a healthy register still passes. The red-proof corrected the gate twice: a
first draft counted CELLS and convicted 125 innocent rows (register cells carry literal `|` in
prose and shell snippets, so a row cannot be split on `|`), and it skipped malformed rows before
counting ids, reporting 5 duplicates where there were 2. It then immediately caught a blank line
left by this session's own next insert.

R-618's rank now reads P1-HIGH in both its title and its state cell.

HETZNER ANSWERED, AND I FIRST SAID THEY HAD NOT. The operator supplied ticket #2026090103040671.
Q1: with the MAIN account, files and directories can be downloaded from a snapshot (Storage Box
docs govern, not the Storage Share FAQ); a restore reverts the whole box. Q2: --append-only is
enforced by pinning `command="rclone serve restic --stdio --append-only path/to/repo"` to the key
in authorized_keys — so append-only IS expressible on a Storage Box, which is what R-95 was blocked
on. Neither is measured; R-436's due check now asks for the measurement, not the question.

My error is filed as R-628 and is precise: the query was fine — a control returns 201 threads and
reaches SENT and TRASH — and the thread genuinely is not in this mailbox. What was invented was the
step from "absent here" to "Hetzner has not answered". An absent record in one place cannot answer
what someone else did. The rule is now in the Gmail-access memory.

R-629: the drill repo inherited has_actions from the migrate call and mailed the operator 47 CI
failures overnight, into the mailbox that was carrying real off-site alarms. Actions disabled and
verified; 09 §6.5 now makes it a step of creating a drill repo. The 47 mails are the operator's to
clear: subject:"gates FAILED in admin/app-catalog-drill".

Gates: repo_gates.py --fast — all 16 OK, including the new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 10:22:37 +02:00
admin d27663dd91 Update night: record the one later catalog commit, and re-prove no image line moved
gates / gates (push) Successful in 26s
The teardown was taken before the vikunja verb correction (test code only), so the live catalog's
main now reads d4392e2a10f4 rather than 4463243f2e09. The check that matters was re-run after it:
git diff f5f6a152b513 origin/main -- templates/ is EMPTY. Not one image: line moved on the live
catalog at any point in the night.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 23:14:52 +02:00
admin 1ee14ce166 Update night: the final PROGRESS lines — teardown, documents, CI green by id
gates / gates (push) Successful in 24s
Both CI runs confirmed by job id: app-catalog-felhom.eu job 830 and felhom.eu job 870, each
'completed' / 'success', matched on head_sha. unproven.py --summary did not move and that is
stated rather than left for the reader to notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 22:35:42 +02:00
admin 8d786f7940 Update night 2026-09-21: the full record, twelve rows, and the answers to five of the seven questions
gates / gates (push) Successful in 28s
The drill is complete. Teardown done in three layers plus Gitea; the live catalog's every `image:`
line is proven identical to before.

WHAT WAS MEASURED. 21 edges across 19 apps, on scratch guest 9202 through the product's own
guarded Update, against a PRIVATE DRILL CATALOG so the live catalog carried no test reference at
any point: 14 proven, 3 failed, 4 inconclusive. Each app seeded and read back through its OWN
front door, with a negative control on every readback. Ten of the fourteen printed a verbatim
migration line. Up from the three apps this project had ever measured.

THE RESULT THAT MATTERS. R-618, P1: three of the 53 templates name a health probe the app does not
answer, and because the guarded update WAITS on that same probe, a SUCCESSFUL update ends by
STOPPING a working app. tandoor was measured serving HTTP 200 on the new version at four samples
across five minutes, docker's own healthcheck green, and was then stopped and the household sent
to a restore they did not need. zipline and wger are the same defect, both confirmed live. The
gate that catches all three is static and cheap: both health checks already sit in the same file.

WHAT THE NIGHT ANSWERED that was open. The UNATTENDED HOLD (312.9 s, pressed once, never again) —
which needed a purpose-built image store, because the rule that makes automatic updates safe is
the same rule that refuses the obvious way to break one. MariaDB across a major through the real
button, all four observables, first time. PostgreSQL across a major, refusing exactly as predicted,
with the conversion costed at ~9 s of engine work. There is NO single-flight: five updates ran at
once and all ended honest. And the two EARLY power-cut phases nobody had cut in.

TWELVE NEW ROWS (R-615..R-626), register 303 -> 315, and eight existing rows updated with what was
measured — including two CORRECTIONS: R-606 records the pre-flight refusals as reaching an English
household in English and they do not, and R-446/R-458 are both narrower than their rows state.

Two instrument fixes were needed before anything could be trusted: the unattended caller turned
every success into a timeout (R-623), and one of my own reproductions was wrong and is kept
labelled with what it actually measured.

Interventions: zero. No controller, agent or hub code written. The hub was never touched beyond
the floor the operator asked for.

Gates: repo_gates.py --fast, all 15 OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 22:34:24 +02:00
admin 9c69b3ff07 Update night: Phases 2-4 evidence — both engines, the unattended HOLD, and five new findings
gates / gates (push) Successful in 27s
Evidence off the machine at the end of the phases that produced it (R-320). Teardown follows.

PHASE 2 — the two database engines, through the REAL Update button:
- MariaDB 11.6 -> 12.3 on nextcloud: PROVEN, and pressed through the button for the first time.
  All four SPIKE-r459 observables: the datadir's own record moved 11.6.2 -> 12.3.3; the engine
  itself says "already upgraded ... no need to run mariadb-upgrade again"; the entrypoint says
  "Major version upgrade detected ... Check required!" and then STARTED and FINISHED it (not the
  `skipped due to $MARIADB_AUTO_UPGRADE` line R-459 feared); and the engine took its own
  pre-upgrade backup, 631 905 B. The seeded Nextcloud account read back.
- PostgreSQL 16 -> 17 on docmost: FAILED exactly as R-463 predicted and nobody had measured.
  5.1 s to held; the pin named 17 while nothing ran; the restore brought it back in 29.1 s.
  The engine's REFUSAL LINE was destroyed by failAndHold before any probe could read it, so it
  was REPRODUCED INDEPENDENTLY with a control on every step (R-320).

PHASE 3 — the bad days. B1 produced THE UNATTENDED HOLD, which this project has never had: the
caller pressed once with nobody watching, the app held after 312.9 s, and passes 2 and 3 pressed
nothing. B2 put the pin back on a pull failure in 1.0 s. B3 refused `busy` six times. B4 showed
there is NO single-flight — 5 of 5 updates ran at once and all ended honest. B5 cut the power in
`backing-up` and the box recovered itself and said so. B7 refused under the 2 GB floor. B9 found
R-458's risk narrower than the row states.

PHASE 4 — every badge on the box is TRUE, and the held app answers all four of Q4's questions.

FINDINGS, five new and three corrections to existing rows. The one that matters: R-618 is P1 —
two templates name a health probe the app does not answer, and because the guarded update waits
on that same probe, a SUCCESSFUL update ends by STOPPING a working app. Measured: tandoor served
HTTP 200 on the new version at four samples across five minutes and was then stopped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 22:13:57 +02:00
admin da20722e76 Update night 2026-09-21: Phase 0 and Phase 1 evidence, the drill method, and two instrument fixes
gates / gates (push) Successful in 27s
INTERIM CHECKPOINT — evidence off the machine at the end of the phase that produced it (R-320),
not at the end of the session. Phases 2-5 follow in a later commit.

Phase 0, all three mechanisms proven with their controls:
- the fleet floor to 0.261.0 with its declared MinAgent — both demo boxes in 13 s, the hub
  logging `managed floor SERVED ... from declared (golden 0.258.0)`.
- a PRIVATE DRILL CATALOG (admin/app-catalog-drill), so that broken, dummy, cross-repo and
  engine-major edges can be measured without the live catalog ever carrying one. Positive
  control quoted, and two negative controls: the live catalog's main and both real boxes'
  caches unchanged.
- a throwaway image store on the scratch guest, which is what makes an UNATTENDED HOLD
  measurable at all: an edge that PASSES the within-a-major test and still fails.
  CompareImageRefs was proven to order host:port/ references by RUNNING it (4 positive cases
  + 1 negative control), not by reading it.

Phase 1: real within-a-major upstream edges walked on guest 9202 through the product's own
guarded Update, each app seeded and read back through its OWN front door (R-156), with a
per-edge verdict record in 09's shape. `inconclusive` is never collapsed into `failed`.

TWO INSTRUMENT FIXES, both in this repo's own evidence code:
- 00-api-recipe.md said the app page is /app/<n>; it is /apps/<n>, and every call it described
  404s. Corrected, with the session-expiry note that cost the same time.
- unattended-caller.py's follow() read update_phase/updating off the API ENVELOPE, so both were
  always None and EVERY followed update ran to its 900 s timeout and was then recorded
  `timeout` and never-press-again. Fixed before B1 relied on it. R-623.

No controller, agent or hub code was written. The live catalog carries no broken reference.

Gates: repo_gates.py --fast — all 15 OK, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 21:17:46 +02:00
admin c85262111c The update arc's two missing measurements, the lock, and the floor to 0.260.0
gates / gates (push) Successful in 23s
Part 0 — floor raised to 0.260.0, MinAgent 0.131.0 declared. 3 boxes below, all
down or blocked; both demo boxes SERVED.

Part 1 (R-610) — the DANGEROUS power cut, measured three times with three apps and
two cut mechanisms. All ended honest: resumed, completed, and pinned/installed/live
compose/docker inspect all agreed. vikunja's 2.6.0 migration had ALREADY run 0.64 s
after the cut decision and the seeded data read back intact — so the branch that is
one step from old-binary-on-migrated-database is now evidence, not argument.
Instrument limit stated: `starting` lasts under a second; all three landed in
`verifying`, which RecoverUpdates handles in the same branch.

Part 3 (R-611) — the night the previous session skipped without saying so. An app
updated with nobody pressing anything; a terminally-refused app was pressed exactly
once and never again over three passes. The unattended HOLD was NOT produced: the
within-a-major rule correctly refused the broken edge before it was attempted, so
Q4 still rests on the attended hold from slice 4. Said plainly rather than implied.

Rows: closed R-608/609/610/611; opened R-612 (P1 wishlist unusable on a fresh
install, and its error is a lie), R-613 (uptime-kuma healthy on its setup wizard),
R-614 (stale update phase survives a redeploy). R-520's pointer corrected.

Catalog: two drill pairs, both reverted; every image line byte-identical to
ff9717d3. The alpine:3.20 negative control a security review flagged is cleared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 15:00:09 +02:00
admin d19f07ea04 File R-607 (a sync that says 'no change' while the cache moves) and sharpen the audit
gates / gates (push) Successful in 26s
Found by the live run, not by reading: POST /api/sync answered 'nincs valtozas'
while the box's catalog cache HAD moved, and catalog_images stayed stale until a
separate rescan. Since CatalogImages is the one input CatalogOrder compares
against, the badge answers from a stale catalog for that window — and the session
nearly recorded a stale tag-ok badge as proof of the R-524 ahead arm.

Neither half is isolated, so the row records the observation, not a diagnosis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 13:14:10 +02:00
admin 0c263c77f2 Update arc resumed: the state measured, R-524/R-520/R-589/R-469 closed, seven questions put to the operator
gates / gates (push) Successful in 24s
Phase 0 — measured, never estimated:
- both demo boxes: 10 apps, 0 behind, 0 unknown
- 46 of 58 exact catalog pins are behind upstream; 39 within a major, 7 across
- 6 of 7 measurable floating pins have been repushed since the catalog set them
  (R-446 is no longer theoretical)
- the "23 of 66 floating pins" figure repeated in four places was STALE; recounted
  to 10, with the definition written down beside it

Three claims in the brief corrected, named first:
- R-589 was NOT open — it shipped in v0.258.0; only the row was stale
- the chaos-night canary is NOT a defect — both gates refused to certify by design
- the hub half of the report confirmed, with the nuance that the raw payload is
  stored whole, so Slice 7 is cheaper than the row implies

Closed: R-524 (controller v0.260.0, proven live in both languages), R-520 (power cut
during a REAL version change — the pin goes back, the app runs, the page says so),
R-589, R-469 (MariaDB half). Filed: R-605, R-606. R-462's stale scope corrected.

09 gains §3 decision 10 (decided by CC unattended — operator may reverse), §3b with
the seven questions in the decision shape, §6.2/6.3 the two open slices, and §6.4 an
update night costed from R-462's real numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 13:13:23 +02:00
admin bcdd5b2058 floor 0.259.0 raised; R-601 withdrawn as FALSE; R-604 filed
gates / gates (push) Successful in 28s
R-601 said demo-hp was unreachable. The operator looked at the hub and said it
was online. It was, and had been up four and a half weeks, reporting every few
minutes. Both of my SSH routes pointed at stale addresses: `demo-hp` at a
tailnet peer for a box that has no tailscale installed at all, and `demo-hp-lan`
at 192.168.0.87 when the box is statically on .104 since a reprovision. The hub
had carried the right address in every host report, and `ip neigh` on felhom-pve
had .104 four lines above the .87 I quoted — I searched that output for the
address I expected instead of reading it for the address that was there.

Both ssh entries repointed and verified; nodes.md corrected, including that the
tailnet route for this box does not exist.

The hunt then found R-604, which is the real defect: demo-hp carried a
per-customer floor override of 0.243.0 left over from the 2026-09-16 drill, so
it had silently missed the raises to 0.253.0, 0.254.0, 0.257.0 and 0.259.0.
`managed floor SERVED` fires once per change by design, so a box behind a static
override is silent for ever and its silence is indistinguishable from a box that
already logged. Cleared; demo-hp self-updated to 0.259.0 in under four minutes
and its claim page now answers "Wrong or expired code" in English.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 09:13:57 +02:00
admin 11aaeaee8a the drill's own screen, walked in English on the live box
gates / gates (push) Successful in 28s
'Wrong or expired code' — the sentence that stopped the 2026-09-20 walk — read
back off guest 9201 through the felhom_lang cookie, with the byte-identical
Hungarian beside it. The first pass could not see it because its own Hungarian
attempts had tripped the lockout; the window was waited out rather than cleared
by a restart, because restarting to make a probe pass measures a box nobody runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 08:18:48 +02:00
admin fcdc948909 the closing verdict, the capability-map row, and the live evidence
gates / gates (push) Successful in 26s
The three blockers yesterday's English walk found are closed and each proven on
a live system. The verdict is deliberately 'nothing known now stands in their
way' rather than 'the walk passed': fixes are not a journey, and the hour has
not been re-walked by a stranger on a fresh install.

Also filed: the HP demo box answers on no route this session has (R-601), the
cookie-vs-session language instrument trap that would have had me fix R-598
twice (R-602), and the apostrophe that silently never matches a rendered page
(R-603).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 08:04:31 +02:00
admin 83b558b3ea deploy hub 0.119.0 (R-597)
gates / gates (push) Successful in 23s
Rollback: set this line back to felhom-hub:0.118.1, commit, sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 07:57:59 +02:00
admin e02bc03819 hub v0.119.0 — English households get English words for their codes (R-597); R-596/R-598 closed
gates / gates (push) Successful in 24s
The setup code and the owner passphrase now follow the household's language,
one word longer in English so the entropy never drops (setup 3 hu / 4 en,
passphrase 5 hu / 6 en). List and count are chosen together so a caller cannot
pair an English list with a Hungarian count. Hungarian is byte-unchanged.

Three claims in the row were wrong and are recorded as such:
  - the RECOVERY CODE is minted by felhom-agent from the EFF list and has
    always been English; the hub does not own it and no row was added.
  - no claim mail states a word count; the only count wording was the bind
    page's passphrase hint, whose English half is now count-free.
  - the proposed phone-safe filter removes 68% of the list (5270 of 7772
    words) and was measured, then declined, with the reason in source.

Also: guide_quote_gate binds the English volunteer guide's three quoted
messages to the controller's English bundle — nothing did, so the guide would
have gone on quoting Hungarian after the fix. Seven decoys, all convicting,
including the name-for-fact one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 07:56:56 +02:00
admin a499327236 CLAUDE.md: the CI-check recipe was wrong in two ways, both measured today
gates / gates (push) Successful in 24s
1. The response key is `jobs`, not `workflow_runs`. A parser reading the latter gets
   an empty list and prints nothing, which reads exactly like "no CI run for this
   commit". That is R-417's own shape — a field the API never populates — committed
   while following the instructions R-417 wrote.
2. Job ids are not ordered within a page, so page = total/50 + 1 does not hold the
   newest rows. Scan every page and match on head_sha.

Guessing the page produced three consecutive false "no CI job" readings in one
session before the raw response was finally looked at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 19:59:50 +02:00
admin 732e9b9e0c Teardown verified on ep0, and the two periods I got wrong (R-600)
gates / gates (push) Successful in 25s
The customer delete cascade logged "full teardown" while the drill box's WireGuard
peer 10.77.0.5 was still configured on ep0. Checked THERE rather than inferred from
the hub, then watched until it went: gone about 6 minutes later. The mechanism is
asynchronous, not broken; the log line claims a completeness it does not yet have.

Both periods this session inferred from two log lines were wrong — the delete's
staleness window and wgsync's push interval. A period read off two log lines is not
a measurement, and both rows now carry what was actually observed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 19:48:20 +02:00
admin 75bc699f78 drill evidence: drop the raw PPM screendumps, keep the PNGs
gates / gates (push) Successful in 24s
The PPMs are 2.3 MB each and carry nothing the PNG does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 19:26:33 +02:00
admin 91f047dfc0 Localisation slice 6: the guide in English, and a stranger's first hour (R-561)
gates / gates (push) Successful in 29s
Part 0 shipped as controller v0.258.0 (its own commit). Part A is the guide's
English twin. Part B is the walk: a box installed from scratch that day, used by an
English speaker following only the English guide and the screens.

VERDICT: not yet ready for an English-speaking tester, because the claim page — the
one screen between them and their box — is English chrome with Hungarian messages
(R-596, P1). Everything else held: the download page, the bilingual console, all
three customer mails, the bind page and its refusal, the dashboard's first language
from customer.language alone, both app pages, the whole catalog, the language switch
both ways — every one of them with zero Hungarian lines.

One intervention (I1 = R-494, filed 2026-09-14); the stop rule was not reached. The
walk exercised what 2026-09-14 could not: the graphical installer, the auto-reboot,
and the mailed link and self-bind page end to end — that walk's H1 is closed, because
this session had a mailbox.

R-214 CLOSED as a side effect and seen rather than reasoned about: the console's last
paint is now the bilingual "the box is linked" banner.

R-516 does NOT close, and the item-by-item note says why: more than half its twelve
items are about what a HUNGARIAN household reads, and an English walk cannot see
them. It now waits on a Hungarian walk with a second drive.

Rows opened: R-596 (P1, the claim page), R-597 (the setup code is three Hungarian
words), R-598 (the Backup page's protection warnings), R-599 (a drill's teardown is
blocked 30 minutes by report staleness and the 409 does not say so).

Golden 0.258.0 baked, published, vouched, with its record. The waiver was NOT retired
and the record says why in one line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 19:26:16 +02:00
admin cf09c78743 Slice 6 parts 0 and A: the guide in English, golden 0.258.0, and the CI finding
gates / gates (push) Successful in 30s
The volunteer guide has an English twin. It is a TRANSLATION, not a rewrite: 16
sections in the same order, identical step counts, table rows and warning blocks per
section (measured, 0 sections differing in structure). Word counts are NOT a twin —
English runs 19 % longer overall and up to 42 % on the short sections, because
Hungarian is agglutinative; the +-15 % criterion the task asked for does not survive
contact with this language pair, so structure is the measure reported instead.

Golden 0.258.0 baked, published and vouched, with its record. One run, no aborted
attempts: the 0.246.0 bake's two traps were both avoided by following its own record.
Token proven not to leak with a planted control before the zero was believed.

The waiver is NOT retired, and the record says why in one line: it is the mechanism of
operator ruling R-468, not a note about this golden, and deleting it would turn the
next release without a bake red immediately. It is also not load-bearing today.

R-595: the catalog's copy gate could not run in CI at all — six pushes red, six alarm
mails, while the local hook was green. Found by reading the operator's inbox, not by
anything in the session that caused it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 18:29:33 +02:00
admin 8e9401c6bf Localisation slice 5 CLOSED: the catalog speaks English and the floor is at 0.257.0
gates / gates (push) Successful in 27s
Part C shipped the same day the pilot was read: fifty apps in three pushes, 1 031 of
1 032 strings. The English Apps list shows ZERO Hungarian app descriptions across all
53 apps — the only Hungarian left on it is the "Naprakész" badge (R-589) and the
language picker naming itself, which is correct.

The Hungarian Apps list is identical to the pre-slice capture once the per-session
CSRF token AND Docker's own "Up N hours" container string are normalised. Both
normalisations are stated in the evidence rather than applied quietly — the second
one moved because two hours of wall clock passed between captures, not because any
copy changed.

Fleet floor raised to 0.257.0 with the declared MinAgent 0.131.0, above the vouched
golden so the declaration carries it. demo-felhom went 0.255.0 -> 0.257.0 by itself
in under 12 seconds and THEN rendered the English tagline: the floor delivered the
feature, not a version string.

Rows: R-593 (papra describes a session-signing key as "the app's subdomain" — the one
string left untranslated) and R-594 (the catalog gate can convict a retrieval promise
but has no way to REGISTER a true one, which the shared vocabulary's design calls
for). R-560 closed. 281 -> 287 rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 16:40:59 +02:00
admin f538a03bd5 Localisation slice 5: the catalog's copy model is MEASURED, not proposed (R-560)
gates / gates (push) Successful in 25s
10-localisation.md §7 goes from [DESIGN, proposed] to [FACT], with the numbers it was
specced against corrected — and one correction chose an instrument rather than a
footnote. The catalog has 1 032 copy strings, not 835. 832 carry a Hungarian letter,
which was right. But the ASCII-ONLY Hungarian is ~120 strings, not three: „Aldomain"
appears 53 times and „A szerver domain neve" 53 times, and the three the plan named
(„Igen"/„Nem"/„Nincs") do not occur in this catalog at all. An accent-only gate passes
every one of them inside an English block — R-565's blind spot arriving again in a
different repo.

New §10.6 records what was proven live rather than reasoned about: the English pages
show English; the seven Hungarian pages are byte-identical before and after the push
apart from the per-session CSRF token; and a 0.255.0 box with the block synced onto it
renders identically and logs no warning, in a 93-line window that contains the sync's
own lines, so the absence is evidence and not a dead log.

Rows R-589 (the update badge is Hungarian on an English page), R-590 (the data-folder
card's backup promise, likewise, and it is a promise about the customer's files),
R-591 (Stack.Copy() deep-copies five Meta fields and not the new I18n map — safe
today, which is precisely why it is a row), R-592 (three defects inside the new
catalog gate, closed the same session, each found by its own decoy). R-560 updated:
Parts A and B done, Part C waiting on the operator's read of the pilot.

STATUS asks for that read, and for the floor to 0.257.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 14:44:20 +02:00
admin 31eeb36e88 ISO 1.29.0 PUBLISHED — bilingual console, proven on both menu entries (R-559)
gates / gates (push) Successful in 22s
Live at iso.felhom.eu, sha256 dceacae5da247d76cad065bf6c0d3bbefac8d8a5f8e571
db2fd8449a97e94829, and both download pages now name it.

Every gate criterion is recorded with its OBSERVED value in
documentation/tests/iso-release-1.29.0-2026-09-18/ — including two proof
installs from the published bytes, one per boot-menu entry, each with a first
boot AND one reboot: /etc/issue bilingual with zero hits for 8006, pvebanner
masked, package 1.29.0 installed, unit enabled and fired, pairing code present,
and the installed script byte-identical to repo HEAD. G11: the downloaded bytes
hash to the published checksum.

A defect was caught BETWEEN builds by looking at the screen rather than at the
config: the second menu entry read "Felhom telepítés (szöveges mód) / Install
Felhom (text mode)" — 58 characters — and the GRUB menu box cut it at "Instal".
The English half was unreadable on the boot screen. Shortened to "… / text" and
rebuilt; the published image is the rebuilt one. The Hungarian half is the part
that may not change, so the English half is the part that gave.

Teardown: VMs 323/324/325 destroyed, the two unclaimed appliance registrations
discarded (zero left in `registered`), guest 9201 untouched — 23 containers
before and after. The two stale *.rootpw.txt files were shredded from the
publish source directory before the upload ran from it (R-587, files gone; the
guard that would stop it recurring is still open).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 21:16:57 +02:00
admin 8d539f971c STATUS: slice 4 — the bilingual screens, and the installer image that waits for a supervised publish
gates / gates (push) Successful in 24s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 17:48:13 +02:00
admin 5a654ebc9a slice 4: bilingual console + the English download page is live (R-559)
The image is BUILT but NOT PUBLISHED, and that is deliberate: publishing to
iso.felhom.eu is public and irreversible, and the runbook needs a proof install
on BOTH menu entries plus a reboot against the uploaded bytes. That is
supervised, so I stopped there. felhom-installer-1.29.0-pve9.2-1.iso, sha256
c67ceaa3…fb02, with every mechanically checkable criterion passing (G1, G2, G5,
G6, G7, G9, G16 — including both payload files byte-identical to repo HEAD).

The download pages still name 1.28.0, the image that IS published. Pointing
them at a file that is not there would hand every reader a 404. A new site gate
refuses the two pages naming different installer files or checksums, so
whoever publishes 1.29.0 cannot update one and forget the other.

Measured rather than read: the pairing banner is 24 rows on a 25-row console.
One row of margin — so the height is now pinned, because two more lines push
the HUNGARIAN code at row 5 off the top, and a banner whose code has scrolled
away is furniture.

R-587: two root-password files from July sit in the directory the public ISO is
published from. Both 404 on the bucket (against a 200 control), so nothing
leaked — but the only thing keeping them off is an --include pattern they miss
by an accident of naming. A pattern that protects by coincidence is not a
control.

R-588: release records live in two different places, which made me wrongly
conclude 1.28.0's gate had never been run. It had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 17:47:58 +02:00
admin eb1ae37095 ISO 1.29.0 source + an English download page (R-559 slice 4, source only)
gates / gates (push) Successful in 23s
THE IMAGE IS NOT BUILT AND NOT PUBLISHED BY THIS COMMIT. Publishing to
iso.felhom.eu is public and irreversible and its runbook requires a proof
install on BOTH menu entries plus the 16-criterion gate run against the exact
uploaded bytes. That is the operator's step. The download pages therefore still
name 1.28.0 - the image that is actually published - and a new site gate
refuses the two pages naming different files or hashes.

Three texts a person meets before any dashboard become bilingual: Hungarian
block first, byte for byte as before, then English, inside the same frame. The
pairing banner, the bound banner, /etc/issue (and the postinst's byte-coupled
copy), plus an English half on the GRUB entries.

The Hungarian is a GOLDEN, not a grep: test/golden/*.hu.txt were captured from
the script at 183727db9c before one English line existed, and the harness
asserts each banner's first N lines are exactly the golden. Red-proofed by one
changed byte, by an "a" planted in the English block, and by an over-wide line.

R-586, found on the way in: running the harness UNCHANGED at the base commit
failed two R-496 checks. The script paints with `>`, which truncates a FILE but
is a no-op on a console device; ISO 1.28.0's new bound banner (c033b3b) paints
straight after the pairing one and wiped it before the check read it. c033b3b
did not touch the harness, and nobody saw it because the harness is in no gate
and no CI run. Fixed with a FIFO; production code untouched. The harness being
ungated is still open.

The release gate's G16 required every Felhom string to be Hungarian and would
have STOPPED this publication. Operator ruling 1b of 2026-09-17 supersedes that
scope, so G16 is rewritten rather than waived: Hungarian FIRST, pinned by the
golden, each secret named once per language.

letoltes.html changes by four lines only. The English link is not in the nav -
the nav is a shared block site_gates.py pins across every page, and the gate
convicted the first attempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 17:40:29 +02:00
admin 183727db9c docs: localisation slice 3 CLOSED — evidence, R-558/R-555 closed, R-585 filed
gates / gates (push) Successful in 25s
Part B's live proof is a LOG LINE rather than an e-mail, and the evidence page
says why: severityNotifies drops `info`, so every safe converted producer mails
nobody, and every converted producer above `info` describes something bad that
is not true. Triggering one would mean a false record on a real box's timeline
or a state change the fences forbid. The log line was built in v0.256.1 for
exactly this, after finding there was nothing to look at on either side of the
wire.

  15:07:28  controller_started [hu-only]        — Controller elindult (0.256.1)
  15:08:25  controller_started [+household(en)] — Controller elindult (0.256.1)

The Hungarian sentence is identical in both, and the hub stored the Hungarian
in every case including the English-household push.

R-558 CLOSED, R-555 closed with it. R-585 filed: six producers still send
Hungarian only because their sentence arrives already finished from another
package — `offbox_enlarge_blocked` matters most, since it has no hub entry so
its raw sentence IS the household's whole mail.

10-localisation.md gains 10.4; STATUS rewritten for the operator with the two
decisions left (raise the floor to 0.256.1; whether to rotate the demo
password after R-584).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 17:11:15 +02:00
admin 1637fa655d docs: slice 3 Part A live evidence, and two findings (R-583, R-584)
gates / gates (push) Successful in 22s
The live proof: the box's own "send test notification" button pressed twice,
74 seconds apart, on demo-hp. Reporting `en` it produced "[Felhom] Test
notification / Dear Customer, ..."; switched to `hu` it produced "[Felhom]
Teszt értesítés / Kedves Ügyfél! ...", byte-for-byte the v0.117.0 literal. The
operator's copy is identical in both, which is the half worth stating.

R-583 (closed, hub v0.118.1): the test mail was the one customer mail that did
not follow the language, and it is the mail an operator would use to CHECK
that the language works. The surface you would use to check a feature is the
one most worth checking first.

R-584 (open, P2): five probe scripts from slice 2's releases B/C/D were still
in the guest's /tmp carrying the controller password INLINE. The rule to
delete them exists, was loaded, and was not followed three times running - so
the rule is not the mechanism. All shredded; whether to rotate the shared demo
password is the operator's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 16:33:49 +02:00
admin a2c52ebf2a hub v0.118.1: the test mail follows the language too (R-558)
gates / gates (push) Successful in 23s
Found during v0.118.0's own live proof, which is the only reason it was found:
I went to press "send test notification" for the English demo box and read
sendTestEmail first.

It had its own hardcoded Hungarian subject and body and never went through
FormatCustomerEmail, so it was the one customer mail v0.118.0 did not localise
— and it is the only customer mail an operator can trigger on demand, which
makes it the one most likely to be used to check whether the localisation
works. Pressing the button for an English household would have answered that
question wrongly, and convincingly.

The two sentences are extracted byte-for-byte into the bundle, so the Hungarian
test mail is unchanged. Red-proofed against the hardcoded version.

The general form worth keeping: the surface you would use to CHECK a feature is
the one most worth checking first. A broken instrument that reports success is
worse than a broken feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 16:25:20 +02:00
admin 9167cf53af hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
gates / gates (push) Successful in 23s
The hub has written every customer e-mail in Hungarian whatever the box was set
to. The box has published its language since controller v0.247.0; nothing read
it. Now it does.

Nothing an operator reads changes. The Hungarian mails are byte-identical, and
that is a diff rather than a reading: 56 goldens per language captured from
v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after
every sentence was routed through the new bundle.

- internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0.
- customerMessages/severityLabels are DERIVED from the bundle, so a sentence is
  written in one place and all 40+ tests that read those maps still work.
- Language order: last reported -> created-with -> hu. reports.language defaults
  to EMPTY, never hu: "never told us" is not "chose Hungarian".
- message_customer on POST /api/v1/event, additive and optional forever, for the
  sentences the box composes and the hub cannot translate.
- The bind page is per-language, and its `expired` state stays Hungarian: it is
  the state an unknown token lands in, so rendering a real English customer's
  token in English would make the LANGUAGE answer what the TEXT refuses to.

Two defects found inside the release:
- R-581: the newest report was picked by received_at, which has SECOND
  granularity, so same-second reports tied and the winner was arbitrary. Ordered
  by the autoincrement id now. GetCustomers() still has the shape - row open.
- R-582: the English copy-guard stems, ported word for word from Hungarian,
  convicted 141 honest sentences. The English claim is a phrase with a modal.

R-555 closed: the language allowlist entry is out of wire_contract_gate.py.
hub_copy_gate.py follows the sentences into the bundle - without that it would
have scanned four files that no longer hold any customer text and reported
success. Three new decoys incl. an innocent control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 16:20:11 +02:00
admin 20aafc3dec ops: fleet floor raised to 0.255.0 — and it delivered on its own
gates / gates (push) Successful in 23s
POST /configuration/global-floor with min_controller_version=0.255.0 and the
declared min_agent=0.131.0 (R-472); 303 flash=floor_set, read back from the
form, not from the POST.

The proof is the N100: it was never hand-deployed and its own Docker reports
felhom-controller:0.255.0 healthy within five minutes of the save. Three boxes
remain below — all BLOCKED or DOWN, which is a floor being held, not a floor
failing; each takes it on its next check-in.

R-580 filed: curl's %{redirect_url} rebuilds the request URL WITH the --netrc
credentials in it, so the hub password was printed into the session's own
output. Nothing written to a file, nothing committed. The build-deploy skill
now carries the rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 15:37:15 +02:00
admin 4df2cd5174 docs: controller v0.255.0 — the globe fix on the sign-in-flow pages
gates / gates (push) Successful in 22s
R-579 filed and closed the same day: five shell templates loaded style.css
with no cache-buster, so a browser holding the pre-0.254.0 file rendered the
new globe unstyled; and the globe sat outside the card.

- STATUS.md rewritten for the operator: what was seen, why, the third defect
  found while fixing it (version disclosure on the guest share page, caught by
  TestShareGuest_HeadersTilesNoAdminChrome), and the one decision left —
  raise the fleet floor to 0.255.0, with what happens either way.
- 10-localisation.md §3: the shells' asset tag, and why the two guest pages get
  an opaque tag rather than the version.
- Audit D: the parity diff (91 of 106 fixtures identical, every dashboard page
  among them) and the live endpoint evidence from demo-hp guest 9201.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 15:12:20 +02:00
admin 3d0eb19a86 docs: fleet floor raised to 0.254.0 — evidence and the operator note
gates / gates (push) Successful in 23s
Agent versions checked FIRST (both live boxes 0.132.0, above the declared 0.131.0), because a
floor is held for a box whose agent is below the requirement. Positive observables at both ends,
and the box's is the one that counts: demo-felhom's own log reads "settle-gate: GO — at/above
floor 0.254.0", its image file and running container agree, and its four other containers stayed
up. The release's visible change is on that second box too: one globe on the sign-in page, zero
of the old text links.

What the table must not be read as: the two DOWN customers got nothing and will take 0.254.0
unattended when they next report, from 0.115.0 and 0.245.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 14:42:23 +02:00
admin 95c10954ae docs: localisation slice 2 CLOSED (controller v0.254.0) — R-577, R-578, and a probe rule
gates / gates (push) Successful in 23s
10-localisation.md §10.3: the saved notes follow the box language at write time, with the
one-night consequence stated rather than hidden; the globe, and the table of WHO reads which
page and where its globe posts — getting that wrong makes the button do nothing, which it did
on /recovery until the live probe found it. Decision 6 superseded a second time; decision 8
(a claim carries the visitor's language) recorded. Decision 5 of §11's anonymous-surface line:
changing what a VISITOR reads is within what an anonymous request may do; changing anything the
household owns is not, and POST /lang can do only the first.

R-578 — the deadlock, and why it is a row rather than a fixed bug: UpdateOffboxStatus holds the
settings write lock while running its callback, boxLang() wants the read lock, sync.RWMutex is
not reentrant. On a real box an off-site run would have hung FOREVER holding that lock. The
symptom was a test suite going from 8 minutes to a 25-minute timeout. Fixed and guarded, but the
guard covers one package and three helper names; the class needs a gate.

R-577 — a guest share visitor still has no way to pick a language, and the household's setting
is the wrong default for a stranger. Deliberately left, pinned by a test, and the operator's to
decide because it is a promise the share feature makes.

.claude/rules/live-probes.md, unconditional: never send a deploy request for an app that is not
installed, not even expecting a refusal — the endpoint accepts first and validates later. Two
sessions made that mistake in two days, the second WITH a prompt line forbidding it. A prompt is
read once; a rule file is loaded every session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 14:31:49 +02:00
admin ed0b0f5c92 docs: fleet floor raised to 0.253.0 — evidence and the operator note
gates / gates (push) Successful in 23s
The floor went 0.250.0 -> 0.253.0 with min_agent 0.131.0 declared, which is what carries a floor
above the vouched golden (R-472). Agent versions were checked FIRST, because the floor is held for
a box whose agent is below the requirement.

Positive observables at both ends, and the box's is the one that counts: the hub logs "managed
floor SERVED … from declared", and demo-felhom's own log reads "settle-gate: GO — at/above floor
0.253.0 (we are 0.253.0)" with the running image and four untouched containers to match. The
release then answered in both languages on that second box.

What the table must not be read as: the two DOWN customers got nothing and will take 0.253.0
unattended when they next report, from 0.115.0 and 0.245.0 — nobody has carried a box forward
from 0.115.0 in one step. Ordinary for a floor, and stated rather than left implied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 12:23:05 +02:00
admin 08e9ddbd5c docs: localisation slice 2 release B (controller v0.253.0) — R-557 progress, R-575, R-576
gates / gates (push) Successful in 25s
All 179 Hungarian error messages carry a key; zero remain. 10-localisation.md gains §10.2: the
four properties util.MsgError had to have at once and the failure each one prevents, and the
plural rule as a BUNDLE rule rather than a per-call-site flag, with the answerable sentence and
what the other option would have cost.

Two instrument defects recorded rather than tidied away, because both shapes recur:

R-576 — the parity gate has a measured blind spot. The bulk converter dropped the continuation
of multi-line concatenations, damaging 7 producers, and the gate stayed GREEN: every surviving
fragment WAS a byte-equal base-commit literal, so its question ("is this text real?") was
answered yes while the CALL had lost half its sentence. Two behaviour tests caught it. The
general form: a structural gate over the TEXT cannot see a defect in the CALL.

And the script counting what was left was case-sensitive, so it said "0 remain" while five did —
R-565's shape inside the measurement. Every "no Hungarian left" claim in this slice is now made
case-insensitively and with both controls.

R-575 — the soft memory-overcommit warning has no error to carry a key and no language where it
is built, so it renders Hungarian on an English page. Named in the code, not hidden.

Live evidence includes a mistake I made and corrected: a probe of the deploy refusal INSTALLED
vaultwarden on demo-hp (the endpoint accepts before it validates), the same mistake the previous
session recorded. Removed through the product's own path with its data; verified gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 11:51:06 +02:00
admin 92ee0f9881 docs: live evidence for controller v0.252.0 + STATUS (R-557 slice 2 release A)
gates / gates (push) Successful in 23s
Endpoint-level on demo-hp guest 9201, method stated. The flash key renders the sentence in
each language where 0.251.0 showed the raw key; a link an OLD controller minted still shows
its prose, in both languages; country names follow the language and the list re-sorts.

Hungarian parity measured on the page BODIES, not on a hash: ten pages on 0.252.0, the guest
rolled back to 0.251.0, the same ten again, then rolled forward. Six byte-identical; the other
four differ only in live state (a clock crossing a minute, an app unhealthy for a moment, and
the update-available line, which is true on the old version and false on the new). The first
pass had saved only hashes — a hash cannot show WHAT moved — so the before-state was
reproduced rather than asserted.

Box left on 0.252.0 with its saved language `hu`; every English probe used the ?lang= override,
which is not persisted. Provisioned nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 10:24:52 +02:00
admin ea4f6ab340 docs: localisation slice 2 release A (controller v0.252.0) — R-557 progress, R-566 closed, R-572..R-574
gates / gates (push) Successful in 21s
10-localisation.md gains §10.1: the measured numbers (1 120 base literals, not 1 141; 226
converted), the flash-as-key design and why a key must be resolved by the READER, word order
through Go's explicit argument indexes rather than a second placeholder syntax, and the parity
gate that makes "byte-identical" a measurement instead of a reading.

Two claims the plan carried that live source disproved, both about the wire, both recorded:
the country table is NOT on the wire (only codes are), and the hub does NOT always compose its
own customer mail — it falls back to the controller's event message, which is why those 31
sentences stay Hungarian until R-558. That survey is handed to R-558 as its input list.

R-566 CLOSED (four app-named page titles now carry a %s). New rows R-572 (two funcmap helpers
with no English form), R-573 (the two channel-health banners arrive as finished Hungarian),
R-574 (handler_debug.go mixes page copy with payload).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-18 10:16:24 +02:00
admin 2bd6fbfac9 R-553 + R-563 CLOSED (controller v0.251.0): evidence, 10 §9 rewritten, R-569..R-571, slice-2 dependency
gates / gates (push) Successful in 24s
- audits/r553-2026-09-17/: live before/after on demo-hp (CSRF redacted), the 409 refusal, the hub
  health block, red-proofs for all five sites, the site-5 fixture diff, gates.
- 10-localisation.md §9: the five decisions with what each reads now; the rule (a text signature may
  remain only where the text is not ours); the one legacy exception and its end date.
- Register: R-553 and R-563 closed to CLOSED-ITEMS; R-569 (four API handlers match English words),
  R-570 (the legacy stale-note fallback + the slice-2 fence), R-571 (classifier and alert placement
  documented nowhere). R-557 carries the R-570 dependency. 263 -> 264 open.
- STATUS, including the live probe that installed an app and was removed the same minute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-17 21:11:31 +02:00