09-update-architecture.md: the update path finally has a document, and it is a living one
gates / gates (push) Successful in 17s
gates / gates (push) Successful in 17s
R-438's document half. It records how an update works AS MEASURED, quotes the RestartStack comment that proves the restart half was CHOSEN (a design decision is not a defect), carries the three operator rulings of 2026-09-02, strikes the word 'rollback' (once a migration has run the old image will not start), states the target shape, and lists the seven slices with a status each. R-438 and R-440 amended and BOTH STAY OPEN: the mechanism is documented, not changed. Nothing closed, so CLOSED-ITEMS.md is untouched. Eight new register rows, 194 -> 202: R-446 (Naprakesz can be false for the 23 floating pins), R-447..R-451 (one per remaining slice, with a rank and an owner), R-452 (no gate enforces catalog_since - the runner fetches at --depth 1), and R-453 (the vaulted dashboard password is stale on BOTH demo boxes, which is what stopped the badge render from being validated live). Live evidence for slices 1 and 2 in documentation/tests/. The record is PROVEN LIVE through the boot reconciler on demo-hp - one entry per compose service, digests matching ground truth read independently. The badge RENDER is not, and the five attempts are listed rather than summarised.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
# VALIDATION — update arc slices 1 & 2, live on demo-hp (2026-09-02)
|
||||
|
||||
**Controller `0.233.0`, guest 9201 on `demo-hp` (Tier 0 — disposable).** Evidence copied off the box
|
||||
at the end of the phase that produced it, not at the end of the session.
|
||||
|
||||
**Method: endpoint/production-path level.** `claude-in-chrome` is not available on DooPlex.
|
||||
**Which path was used, stated exactly: the BOOT RECONCILER**, `bootrecon.Run → StackProvider.StartStack
|
||||
→ compose up -d → recordInstalledImages` — a REAL production caller, the same one
|
||||
`SPIKE-app-update-2026-09-01` §2 variant 1c-ii used, and no hand-set state anywhere.
|
||||
**Why not the customer's Restart button: see §4 — the vaulted dashboard password no longer opens
|
||||
either demo controller.**
|
||||
|
||||
---
|
||||
|
||||
## 1. Deployed version
|
||||
|
||||
```
|
||||
$ pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'
|
||||
gitea.dooplex.hu/admin/felhom-controller:0.233.0 Up 19 seconds (healthy)
|
||||
```
|
||||
|
||||
Previous: `0.232.0`. Image digest `sha256:df5940ccf5a548ceee9065a2cc55a467f8941c636138441e0d77e320dc2023ab`.
|
||||
|
||||
## 2. The record — PROVEN LIVE, on a single-service AND a multi-service app
|
||||
|
||||
### 2.0 Before — nine deployed apps, ZERO with a record
|
||||
|
||||
```
|
||||
bentopdf deployed=1 installed_images=0 paperless-ngx deployed=1 installed_images=0
|
||||
bookstack deployed=1 installed_images=0 privatebin deployed=1 installed_images=0
|
||||
calibre-web deployed=1 installed_images=0 romm deployed=1 installed_images=0
|
||||
docmost deployed=1 installed_images=0
|
||||
kimai deployed=1 installed_images=0
|
||||
opengist deployed=1 installed_images=0
|
||||
```
|
||||
|
||||
**That is the legacy case, and it is the state the badge must render NOTHING for.**
|
||||
|
||||
### 2.0b Ground truth, read from the containers BEFORE anything was touched
|
||||
|
||||
```
|
||||
bentopdf ref=ghcr.io/alam00000/bentopdf:v2.8.6 rd=…@sha256:eaeea1e447205a79cb61d7efdc6966f37311dc1bc9c36a3a5c897bf79107c2c3
|
||||
bookstack ref=lscr.io/linuxserver/bookstack:26.05.2 rd=…@sha256:3db259db582808ab498d49ae96b0a63f935d9cf3635c9d5bd8b8815c6ff1f8a1
|
||||
bookstack-db ref=mariadb:12.3 rd=…@sha256:a02fe89cb597d4375812b2eac90cf9d0775d4686daa7f7cc750ebbcad7525bbc
|
||||
```
|
||||
|
||||
**This is the discriminator.** Every digest below is compared against these, taken independently.
|
||||
|
||||
### 2.1 Single service — `bentopdf`
|
||||
|
||||
Staged exactly as the spike stages a boot orphan: `docker rm -f bentopdf` (chosen because it has **no
|
||||
database, no volume and no data of any kind**), then the controller restarted so the reconciler runs.
|
||||
`desired_state: running` was left untouched. **Nothing else was staged; the reconciler selected the app
|
||||
on its own.**
|
||||
|
||||
```
|
||||
18:27:32 main.go:2162: [INFO] [bootrecon] boot window: fleet settled after 30s — sweeping
|
||||
18:27:32 bootrecon.go:259: [INFO] [bootrecon] Boot reconciliation: 1 boot-orphaned app(s) found: [bentopdf]
|
||||
18:27:32 manager.go:1066: [INFO] [stacks] Starting stack: bentopdf
|
||||
18:27:32 manager.go:1081: [INFO] [stacks] Stack bentopdf started successfully (took 0.3s)
|
||||
18:27:32 installed.go:408: [INFO] [stacks] installed-images bentopdf: recorded 1 service(s) (bentopdf=ghcr.io/alam00000/bentopdf:v2.8.6 (sha256:eaeea1e44720…))
|
||||
```
|
||||
|
||||
`/opt/docker/stacks/bentopdf/app.yaml` after:
|
||||
|
||||
```yaml
|
||||
installed_images:
|
||||
bentopdf:
|
||||
ref: ghcr.io/alam00000/bentopdf:v2.8.6
|
||||
digest: sha256:eaeea1e447205a79cb61d7efdc6966f37311dc1bc9c36a3a5c897bf79107c2c3
|
||||
at: "2026-09-02T18:27:32Z"
|
||||
```
|
||||
|
||||
**Digest MATCHES §2.0b exactly.**
|
||||
|
||||
### 2.2 Multi-service — `bookstack`. **The case that matters.**
|
||||
|
||||
**One entry for the whole stack is the wrong outcome, and only a multi-container app can tell.**
|
||||
|
||||
Same staging (`docker rm -f bookstack bookstack-db`; both volumes are NAMED —
|
||||
`bookstack_bookstack_config`, `bookstack_bookstack_db_data` — and were verified present, so no data
|
||||
was at risk), then the controller restarted.
|
||||
|
||||
```
|
||||
18:28:48 bootrecon.go:259: [INFO] [bootrecon] Boot reconciliation: 1 boot-orphaned app(s) found: [bookstack]
|
||||
18:28:48 manager.go:1066: [INFO] [stacks] Starting stack: bookstack
|
||||
18:28:54 installed.go:408: [INFO] [stacks] installed-images bookstack: recorded 2 service(s) (bookstack=lscr.io/linuxserver/bookstack:26.05.2 (sha256:3db259db5828…), bookstack-db=mariadb:12.3 (sha256:a02fe89cb597…))
|
||||
18:28:54 bootrecon.go:303: [INFO] [bootrecon] Boot reconciliation complete: 1 app(s) recovered in 1 attempt(s)
|
||||
```
|
||||
|
||||
```yaml
|
||||
installed_images:
|
||||
bookstack:
|
||||
ref: lscr.io/linuxserver/bookstack:26.05.2
|
||||
digest: sha256:3db259db582808ab498d49ae96b0a63f935d9cf3635c9d5bd8b8815c6ff1f8a1
|
||||
at: "2026-09-02T18:28:54Z"
|
||||
bookstack-db:
|
||||
ref: mariadb:12.3
|
||||
digest: sha256:a02fe89cb597d4375812b2eac90cf9d0775d4686daa7f7cc750ebbcad7525bbc
|
||||
at: "2026-09-02T18:28:54Z"
|
||||
```
|
||||
|
||||
**TWO entries, keyed by COMPOSE SERVICE NAME, both digests matching §2.0b exactly.**
|
||||
|
||||
**A NOTE ON WHY IT TOOK TWO PASSES, because it is a real fact about the reconciler and not a mishap.**
|
||||
The first pass removed only the `bookstack` app container and left `bookstack-db` running; the
|
||||
reconciler did **not** select bookstack — it found `[bentopdf]` alone. A stack with one live member is
|
||||
not a boot orphan to it. Removing the DB container as well made the whole stack orphaned and it was
|
||||
selected on the next pass. Both apps are running and healthy at the end (§5).
|
||||
|
||||
### 2.3 The encrypted secrets survived the write — checked, not assumed
|
||||
|
||||
`bookstack`'s `app.yaml` holds two encrypted values. Before and after the record was written, both
|
||||
`ENC:` strings are **byte-identical**, and `deployed`, `deployed_at`, `locked_fields` and
|
||||
`desired_state` are unchanged. This is the copy-and-overlay property (R-100) holding under a new field.
|
||||
|
||||
## 3. `catalog_since` reached the box by the NORMAL 15-minute sync
|
||||
|
||||
No force, no hand-edit:
|
||||
|
||||
```
|
||||
$ grep -n catalog_since /opt/docker/stacks/bookstack/.felhom.yml
|
||||
13:catalog_since: "2026-07-18"
|
||||
```
|
||||
|
||||
## 4. The badge render — **NOT LIVE-VALIDATED. What was tried, in full.**
|
||||
|
||||
**A "no access" claim must list its attempts.** These are the attempts:
|
||||
|
||||
| # | attempt | result |
|
||||
|---|---|---|
|
||||
| 1 | `POST /login` to demo-hp guest `https://192.168.0.138:443`, `Host: felhom.enkisfelhom.hu`, `-k`, password from DooPlex `~/.config/credentials` `PASSWORD` (extracted with `sed`, never `cut` — the values are quoted) | **HTTP 200 with the login page and the body string `Hibás jelszó`** |
|
||||
| 2 | the controller's own log, as the discriminator between "wrong host header" and "wrong password" | `auth.go:176: [WARN] [web] Failed login from 172.18.0.3` — **wrong password, not a routing problem** |
|
||||
| 3 | the same password against the OTHER demo box, demo-felhom guest `https://192.168.0.149:443`, `Host: felhom.demo-felhom.eu` | **HTTP 200 + `Hibás jelszó` as well** |
|
||||
| 4 | every other key in `~/.config/credentials` — `HUB_PW`, `R_DEMO-HP`, `R_DEMO-FELHOM`, `R_C11_REWALK`, `R_PART4`, `TS_KEY`, `HETZNER_API`, `ISO_S3_*` | none is a dashboard password; the `R_*` keys are escrow recovery codes |
|
||||
| 5 | reading the source for an unauthenticated route to an app page | only `/claim`, `/claim/request-new-code`, `/api/health` and `/static/` are exempt (`internal/web/auth.go`) |
|
||||
|
||||
**So the vaulted `PASSWORD` is stale on BOTH demo controllers.** It was already recorded as having
|
||||
drifted once (memory `demo-hp-guest-controller-access`, 2026-08-09, put back on operator instruction);
|
||||
demo-hp was reinstalled and re-claimed on 2026-08-21, and demo-felhom has now drifted too.
|
||||
|
||||
**There is no operator-side route to a customer's dashboard password** — the claim code is
|
||||
bcrypt-hashed hub-side and only emailed (R-119). Re-setting it means writing a new bcrypt hash into the
|
||||
guest's `data/settings.json`, which is a decision about a customer account and was done under operator
|
||||
instruction last time. **It is therefore a HUMAN step, by this project's own rule**, and it is not
|
||||
taken here.
|
||||
|
||||
**What IS established about the badge, and it stops short of the render:** every INPUT the badge reads
|
||||
is verified live and consistent on this box — `installed_images` present with refs matching the
|
||||
template's pins exactly (§2.2 vs the compose file's `lscr.io/linuxserver/bookstack:26.05.2` and
|
||||
`mariadb:12.3`), and `catalog_since: "2026-07-18"` present (§3). So bookstack's inputs are the
|
||||
`Naprakész` case and the seven undisturbed apps are the no-record case. **That is an inference from
|
||||
verified inputs, not an observation of the rendered page, and it is not counted as evidence.**
|
||||
|
||||
The render itself is covered by unit tests that render the PRODUCTION templates
|
||||
(`TestGroupD_BadgeRendersOnBothSurfaces`, both surfaces, all states, with a companion red-proof), which
|
||||
is the strongest statement available without the password.
|
||||
|
||||
## 5. End state — nothing left broken, nothing provisioned
|
||||
|
||||
```
|
||||
$ pct exec 9201 -- docker ps --format '{{.Names}}|{{.Image}}|{{.Status}}' | grep -E 'bookstack|bentopdf'
|
||||
bookstack|lscr.io/linuxserver/bookstack:26.05.2|Up 10 seconds (health: starting)
|
||||
bookstack-db|mariadb:12.3|Up 16 seconds (healthy)
|
||||
bentopdf|ghcr.io/alam00000/bentopdf:v2.8.6|Up About a minute (healthy)
|
||||
```
|
||||
|
||||
All three containers are back on the SAME images they ran before, both named volumes untouched, and
|
||||
both apps carry a complete record. **This run provisioned nothing** — no guest, no storage, no hub
|
||||
record — so there is no teardown to report on any of the three layers.
|
||||
Reference in New Issue
Block a user