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
This commit is contained in:
2026-09-18 15:12:20 +02:00
parent 3d0eb19a86
commit 4df2cd5174
7 changed files with 154 additions and 37 deletions
+24 -37
View File
@@ -1,49 +1,36 @@
# STATUS — what works, what's broken, what's next
**Updated 2026-09-18 (night) — the language work is finished, and the switch is now a globe.**
**Updated 2026-09-18 (late) — the globe you photographed is fixed.**
> **Ready for a volunteer: yes.** A Hungarian household sees what it saw yesterday, apart from one
> deliberate change: the two small "Magyar / English" links at the bottom of the menu are now a globe.
> **Ready for a volunteer: yes.**
**Decisions I took.** One you were asked for, taken on its own stated default: **if someone switches
the claim page to English and then claims the box, the box becomes English.** They chose it, and the
first screen they see should be in it. You can reverse it; nothing else depends on it.
**What you saw, and why.** The globe on the sign-in page came out as a bare triangle and two plain
words, floating outside the card. Two separate faults:
**What I did today.** Three releases, and the language job is done.
1. **Your browser was still using the old stylesheet.** Those pages asked for the style file without a
version number on the end, so a browser that had already downloaded it kept the old copy — and the
old copy knows nothing about a globe. The main dashboard has always asked with a version number;
these pages never did. **Five pages had it, not three.**
2. **Even styled, it sat outside the card**, pinned to the corner of the window, so it read as part of
the browser rather than part of the page. It is now inside the card, centred under the footer, and
the little menu opens upward.
1. The sentences the program writes into the pages.
2. The error messages — about 180 of them, written deep inside the program.
3. Today's last piece: **the notes the box saves overnight**, and **the globe**.
**A third thing I found while fixing it.** My first fix would have printed the exact software version
onto the page a guest opens from a share link — someone you sent a link to, who should not be told
which build you are running. **An existing test caught it.** Those two pages now get a scrambled tag
instead: it does the same job and says nothing.
**The globe.** Two text links at the bottom of the menu asked you to recognise two words as links,
and they wrapped. Now there is one globe — the symbol everyone already reads as "language". Click it
and a small list opens: Magyar, English, with the current one ticked. **The sign-in page and the claim
page have it too**, which matters: someone who cannot read Hungarian could not previously find their
way out of Hungarian before signing in. Their choice is kept in their own browser only — it never
changes what the household has chosen, and a signed-in household never picks up a stranger's choice.
**Checked on the demo HP box:** the page now asks for the versioned file, the file it gets back
carries the new rule, and the old rule is gone. **What I cannot check is how it looks** — there is no
browser on this machine. That part is your click.
**One thing to expect, and it is the choice you were offered.** The notes saved overnight are written
in the box's language at the moment they are written. If you switch language, last night's note stays
in the old language until the next night rewrites it.
**Rows.** One opened and closed the same day.
**What broke, and what I did about it.** **I introduced a freeze.** The code that writes the overnight
note asked the box "what language are you?" at a moment when that question could not be answered —
and it would have hung there **forever, holding a lock the rest of the box needs**. On a real machine
an overnight cloud backup would have stopped and taken everything else with it. The test run caught it
by taking 25 minutes instead of 8. It is fixed, and there is now a check that names the exact line in
a second instead of hanging. I also found and fixed a second one before it shipped: the recovery
screen's globe would have looked like it worked and done nothing.
**Rows.** One closed (the whole language job), two opened. The register went from **269** to **271**.
**The floor is raised, as you asked.** The fleet minimum went from 0.253.0 to **0.254.0**. The N100
demo box took it by itself in about forty seconds and is healthy, with its other four apps still
running; its sign-in page now shows the globe and none of the old text links. The HP demo box already
had it. **The two boxes that are switched off did not get it** — Peti's has been off for 65 days on a
much older version, Tester 1 for a day — and they will take it on their own when they come back,
which is how a floor always works. Neither has been tried on this version.
**Needs you.** Nothing. If you do nothing: the two sleeping boxes update themselves when they wake.
**Needs you — one decision.** **Raise the floor to 0.255.0?**
- **If you do:** every box serves the fixed pages on its next check-in, and nobody sees the broken globe.
- **If you do nothing:** boxes on 0.254.0 keep showing it to anyone whose browser cached the old style
file. Nothing is at risk; it just looks wrong.
- I would raise it — the thing it fixes is the thing you noticed.
---
@@ -436,6 +436,16 @@ cannot inherit a language a previous visitor picked in the same browser. The rec
measured live before it was right: an anonymous form there sets a cookie that `langFor` then ignores,
and the button appears to do nothing.
**[FACT] Where the globe SITS, and a stale-stylesheet trap that only a screenshot could show
(v0.255.0, R-579).** On the pages outside the dashboard chrome the globe is **inside the card, centred
under the footer**, with the menu opening upward — the shared `.lang-globe-menu` rule, so the two
surfaces cannot drift apart. It was first placed at the corner of the VIEWPORT, which read as a stray
browser control rather than part of the page. And five of those shells requested `style.css` with **no
`?v=`**, so a browser holding a copy from before the globe existed kept serving CSS with no
`.lang-globe` rules and it rendered as a bare, unstyled `<details>`. **Every test passed, because they
all read the markup and the fault was in which CSS file the browser fetched.** `Version` is now set in
`executeTemplateLang`, once, for every shell.
**[DESIGN] `POST /lang` is CSRF-exempt, for a reason narrow enough to check.** The only achievable
effect of a forged request is to change the language of the page the victim's own browser shows them.
It writes one display-only cookie, reads nothing, touches no setting, and `safeBackPath` refuses a
@@ -0,0 +1,28 @@
# Live validation — controller v0.255.0 on demo-hp guest 9201 (2026-09-18)
**Method: endpoint-level.** No browser on DooPlex. Three GETs, no session needed, nothing written.
The deploy endpoint was not touched (`.claude/rules/live-probes.md`).
```
stylesheet link : <link rel="stylesheet" href="/static/style.css?v=0.255.0">
globe inside card: card@285 footer@1164 globe@1304 -> INSIDE the card, under the footer
GET style.css?v=0.255.0 : 200, 93 551 bytes
the .shell-lang rule it serves:
.shell-lang { display: flex; justify-content: center; margin-top: 1.5rem; }
old absolute rule gone: 0 occurrences
```
Both halves of the defect are answered on the live box: the page now asks for a **versioned** URL, so
a browser holding the pre-0.254.0 file fetches a new one; and the file it fetches carries the **new**
`.shell-lang` rule, with the old viewport-absolute rule gone. The globe's markup sits inside the card
and below the footer.
**What this does NOT prove:** how it LOOKS. `claude-in-chrome` is not available here, so the byte-level
facts above are the whole of what a machine can check. The screenshot that found the bug is the kind
of evidence that settles it — an operator click-through on `https://felhom.enkisfelhom.hu/login`.
**The guest share page** could not be fetched: there is no live share token on this box, so `/s/<x>`
is a 404. Its change is the cache-buster only, and it is an **opaque tag** rather than the version —
pinned by `TestShareGuest_HeadersTilesNoAdminChrome`, which refused the version when it was tried.
**State:** controller 0.255.0, language `hu`, 23 standing containers up, nothing installed or removed.
@@ -0,0 +1,7 @@
stylesheet link : <link rel="stylesheet" href="/static/style.css?v=0.255.0">
globe inside card: card@285 footer@1164 globe@1304 -> INSIDE the card, under the footer
GET style.css?v=0.255.0 : 200 93551 bytes
the .shell-lang rule it serves:
.shell-lang { display: flex; justify-content: center; margin-top: 1.5rem; }
old absolute rule gone: 0
guest share page link : 404 (404 as expected — no live share token)
@@ -0,0 +1,16 @@
#!/bin/bash
# v0.255.0 proof. Read-only: three GETs, no session needed.
IP=172.17.0.2:8080; H="Host: felhom.enkisfelhom.hu"
curl -s -H "$H" "http://$IP/login" -o /tmp/lg5.html
echo -n " stylesheet link : "; grep -o '<link rel="stylesheet"[^>]*>' /tmp/lg5.html
echo -n " globe inside card: "; python3 - <<'PY'
import io
s=io.open('/tmp/lg5.html',encoding='utf-8').read()
c=s.find('class="login-card"'); g=s.find('class="shell-lang"'); f=s.find('class="login-footer"')
print("card@%d footer@%d globe@%d -> %s"%(c,f,g, "INSIDE the card, under the footer" if c<f<g else "WRONG"))
PY
V=$(grep -o 'style.css?v=[^"]*' /tmp/lg5.html | sed 's/.*v=//')
echo -n " GET style.css?v=$V : "; curl -s -o /tmp/css5 -w "%{http_code} %{size_download} bytes\n" -H "$H" "http://$IP/static/style.css?v=$V"
echo " the .shell-lang rule it serves:"; grep -o '\.shell-lang *{[^}]*}' /tmp/css5 | head -2 | sed 's/^/ /'
echo -n " old absolute rule gone: "; grep -c 'shell-lang { position: absolute' /tmp/css5
echo -n " guest share page link : "; curl -s -H "$H" "http://$IP/s/nosuchtoken" -o /tmp/gs.html -w "%{http_code} " ; grep -o '<link rel="stylesheet"[^>]*>' /tmp/gs.html 2>/dev/null || echo "(404 as expected — no live share token)"
@@ -0,0 +1,68 @@
PARITY DIFF — controller v0.255.0, against the fixtures at 2e9d402 (v0.254.0)
A real (LCS) diff; the per-session CSRF token is blanked on both sides. Two things change and
nothing else: the stylesheet gains its cache-buster, and the globe moves INSIDE the card.
[7 fixtures] recovery_locked_can.html, recovery_locked_cannot.html, recovery_locked_confirm.html, recovery_unlocked_apps.html, recovery_unlocked_empty.html, recovery_unlocked_unavailable.html, recovery_unlocked_untagged.html
- <link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/style.css?v=0.247.0">
- <div class="shell-lang"><details class="lang-globe">
- <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- <ul class="lang-globe-menu">
- <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="sub
- <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="sub
- </ul>
-</details></div>
+ <div class="shell-lang"><details class="lang-globe">
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ <ul class="lang-globe-menu">
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="sub
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="sub
+ </ul>
+</details></div>
[4 fixtures] claim_reset_code.html, claim_reset_nocode.html, claim_setup_code.html, claim_setup_nocode.html
- <link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/style.css?v=test">
- <div class="shell-lang"><details class="lang-globe">
- <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- <ul class="lang-globe-menu">
- <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item
- <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item
- </ul>
-</details></div>
+ <div class="shell-lang"><details class="lang-globe">
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ <ul class="lang-globe-menu">
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item
+ </ul>
+</details></div>
[3 fixtures] launcher_share_password.html, launcher_shared_apps.html, launcher_shared_empty.html
- <link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/style.css?v=7da11d7a">
[1 fixtures] login.html
- <link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/style.css?v=0.250.0">
- <div class="shell-lang"><details class="lang-globe">
- <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- <ul class="lang-globe-menu">
- <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item
- <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item
- </ul>
-</details></div>
+ <div class="shell-lang"><details class="lang-globe">
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+ <ul class="lang-globe-menu">
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item
+ </ul>
+</details></div>
[91 fixtures] IDENTICAL — EVERY dashboard page among them: zero re-captures outside the shells.
DISTINCT CHANGE SHAPES: 4 — the three shells that gained the moved globe AND the buster, and the
two guest share pages, which gained ONLY the buster (they carry no globe) and take an OPAQUE tag
rather than the version: a stranger holding a capability URL is not told which build is running.
+1
View File
@@ -754,6 +754,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-576** | **[P3-LOW] `i18n_go_parity.py` cannot see a call site that LOST text; it only checks that the text a key carries is real.** FOUND 2026-09-18 by localisation slice 2 release B, and found the hard way: the bulk converter silently dropped the continuation of a multi-line concatenation (`fmt.Errorf("a: "+ "b: %s", x)` kept only `"a: "`), damaging **7** producers — and the gate stayed GREEN throughout, because every surviving fragment WAS a byte-equal base-commit literal. Its question ("is this text real?") was answered yes while the CALL had lost half its sentence and its arguments. Two behaviour tests caught it (`TestR356_ScenarioC_UndeployedAppIsStillRefused`, `TestR379_ScenarioA_RollbackSucceeds_AppComesBack`), because they assert the sentence a customer READS. **Fix shape:** the gate learns a second question — for every `util.MsgError("key", …)` call site, the count of its arguments must equal the count of printf verbs in the key's Hungarian value, and no key-naming literal may be adjacent to a `+`. Both are cheap and would have convicted all 7. **The general lesson, worth keeping whatever is built: a structural gate over the TEXT cannot see a defect in the CALL.** | **READY - rank P3-LOW; owner: CC** |
| **R-577** | **[P3-LOW] A guest SHARE visitor has no way to pick a language, and the household's setting is the wrong default for them.** FOUND 2026-09-18 by localisation slice 2 release C (R-557, controller v0.254.0): every other page a person can reach now carries a language globe — the dashboard (the household's setting), and the sign-in and claim pages (the visitor's own cookie). The two guest share pages (`launcher_shared`, `launcher_share_password`) deliberately do NOT, and `TestGuestSharePagesHaveNoGlobe` pins that so it stays a decision rather than an oversight. **Why it is the operator's and not CC's:** a share visitor is a stranger the household sent a link to, and what language they are shown is a promise the SHARE FEATURE makes, not an implementation detail. The `felhom_lang` cookie already built would fit them exactly (display-only, their own browser, never the household's setting). **Fix shape, if the operator says yes:** add `{{template "lang_globe" .}}` to both shells with the anonymous form, and one render case per page per language. | **READY - rank P3-LOW; owner: operator (the decision), CC (the change)** |
| **R-578** | **[P3-LOW] A helper that takes the settings lock must never be called from inside a settings callback — there is no gate, only one test in one package.** FOUND 2026-09-18 the hard way, by localisation slice 2 release C introducing exactly that: `UpdateOffboxStatus` holds the settings WRITE lock while it runs its callback, `boxLang()` reads the language through the READ lock, and `sync.RWMutex` is not reentrant — so the off-site run's final status write DEADLOCKED, **holding the settings lock**, which would wedge everything else on that box that touches `settings.json`. The only symptom was `go test ./internal/backup/` going from 8 minutes to a 25-minute timeout. Fixed by hoisting the language resolution; `TestNoteHelpersAreNotCalledUnderTheSettingsLock` (internal/backup) now names the file and line in a second. **What is still open:** that test covers `internal/backup` only, and it knows only the `note`/`noteErr`/`boxLang` helpers. Any other settings-reading helper, in any other package, can make the same mistake with nothing to catch it but a hang. **Fix shape:** promote it to a gate over every package, keyed on "a call to a method that reads settings, inside a literal passed to a `settings.Update*` function"; or give `Settings` a re-entrant read path and remove the class. | **READY - rank P3-LOW; owner: CC** |
| **R-579** | **[P3-LOW] Five page shells loaded `style.css` with NO cache-buster, so a browser holding an older copy kept being served CSS that did not know about the newest UI.** FOUND 2026-09-18 by the operator's screenshot of the v0.254.0 language globe: it rendered as a bare, unstyled `<details>` — a stray triangle and two plain words outside the card — because `login.html`, `claim.html`, `recovery.html`, `launcher_shared.html` and `launcher_share_password.html` requested `/static/style.css` with no `?v=`, while `layout.html` has used `?v={{.Version}}` since v0.166.0. **`.Version` was also absent from three of those five data maps.** FIXED AND CLOSED in the same session (controller v0.255.0): the parameter on all five, and `Version` set once in `executeTemplateLang` so a new shell cannot miss it; `TestGlobeOnAnonymousShells` now refuses an absent or EMPTY `?v=`. **The general form, which is the part worth keeping: a template that loads a versioned asset WITHOUT its version is invisible to every test that reads markup — the markup is correct and the browser fetches the wrong file.** A gate over "every stylesheet/script link in a template carries `?v=`" would catch the class; not built, because 8 first-boot-wizard templates would fail it and R-554 deletes them. | **CLOSED 2026-09-18 - controller v0.255.0** |
| **R-537** | **[P1-HIGH] The app-backup page labels the tier-1 backup „DB + Konfig + Adatok" and prints the app's data-drive size next to it — but the tier-1 unit contains NO drive-side app data at all.** MEASURED 2026-09-16 on the drill box (fresh install, controller 0.243.0, one drive, tier 2 and tier 3 both „Nincs beállítva"): five photos (3 000 000 B) were uploaded into Nextcloud through its own WebDAV interface, then the customer-visible „Mentés most" was pressed (`POST /api/backup/run` → 200, the unit grew 25 337 B → 978 MB). The resulting unit's `manifest.json` lists `db-dumps` + three **docker volume** dumps and nothing else; listing the 781 MB `nextcloud_nextcloud_html.tar` (29 346 entries, positive control `version.php` = 3 hits) gives **`Fotok` = 0 and `nyaralas` = 0**, and `./data/` is the empty bind-mount point. A `find` over the whole `backups/` tree for `*appdata*` / `*Fotok*` returns nothing. The page nevertheless renders „1. mentés … DB + Konfig + Adatok" and „Nextcloud Adatlemez 65.1 MB" — a size measured on exactly the data it does not copy (`internal/web/handlers.go:1176-1178`, `BackupContents`). **This is a truth defect, not a design defect:** `07-backup-architecture.md` §6.2 places nextcloud's file leg at **Tier 2 and Tier 3 only**, and its „[FACT] What the whole-guest tiers do NOT carry" says `mp8 /mnt/felhom-drives` is out of vzdump scope (confirmed live: „excluding bind mount point mp8 … (not a volume)"). So on a one-drive box with no off-site tier — the state every fresh install starts in — the household's files are in **no backup**, while the page says „Adatok". Same family as R-517/R-518. **Fix shape:** render tier-1 contents from the capture set actually written (`ComputeCaptureSet`), so a unit with no file leg reads „DB + Konfig" and the drive size is not shown beside it; and say on the page that the app's files need tier 2 or tier 3. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-f10.txt`. **CLOSED 2026-09-16 — controller v0.244.0, proven live.** The contents label is computed PER TIER from what that tier captures: Tier 1 says „Adatok" only when the app's data really is in the volumes the unit captured, and a class-A app carries one sentence saying where its files ARE protected. Proven on demo-hp through the page the customer opens: Paperless-ngx reads „1. mentés … DB + Konfig" with „Az alkalmazás fájljait a távoli másolat (és a második meghajtó) védi …", while its „2. mentés" row still reads „DB + Konfig + Adatok". Red-proof: restoring the old app-shaped label fails `TestAppBackupRows_Tier1LabelDoesNotClaimFilesItCannotHold`. **RE-PROVEN 2026-09-16 on a FRESH box** (installed from the built ISO 1.28.0, controller 0.244.0, off-site on by default): the Nextcloud row read „1. mentés … DB + Konfig" with the new sentence, „2. mentés … Nincs 2. (off-drive) másolat", „3. mentés Sikeres restic → …your-storagebox.de"; „DB + Konfig + Adatok" appeared ZERO times while the local unit held no file leg. | **CLOSED 2026-09-16 — controller v0.244.0 (proven live on demo-hp)** |
| **R-538** | **[P1-HIGH] A tier-1 app restore reports plain success and leaves Nextcloud listing files whose bytes were never in the backup — and it destroys the app's own trash, the customer's last copy.** MEASURED 2026-09-16 on the drill box, F10 („a child deletes the photo folder"): the five photos were deleted through Nextcloud (DELETE 204, PROPFIND 404), then restored through the page exactly as a customer would (`POST /backup/restore` `stack_name=nextcloud` `snapshot_id=helyi` → 302, finished in **35 s**, „A(z) nextcloud: 3 adatkötet és az adatbázis visszaállítva — az alkalmazás újraindult."). Afterwards the folder is back and **lists all five photos**, and **none of them opens**: `GET nyaralas-1..5` = 404 / 503×4 with `Sabre\DAV\Exception\NotFound`, while the positive controls at the same moment pass (`status.php` 200, WebDAV PUT 201, GET 200). Cause: the replayed MariaDB dump (11:01:45Z) knows the photos, the bytes live on `mp8` and were never captured (R-537). **Worse:** the bytes were still on the drive in Nextcloud's own trash (`appdata/nextcloud/admin/files_trashbin/files/Fotok.d1789556707/nyaralas-1..5.jpg`, all five present) and the restored database no longer references them — the trash listing comes back **empty**, so „restore from trash", the one route that would have worked, is gone. The customer is left with five unopenable photos, a success message, and no warning. **Fix shape:** before replaying a database whose app has an uncaptured file leg, refuse or warn („ennek az alkalmazásnak a fájljai nincsenek ebben a mentésben — a visszaállítás után a fájlok hiányozni fognak"); and never present a DB-only restore of a class-A app as a complete one. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-f10.txt`. **CLOSED 2026-09-16 — controller v0.244.0, proven live.** A unit restore refuses before anything is touched when the unit cannot return the app's drive-side files, and names the route that can. Fired live on demo-hp: `POST /backup/restore` for paperless-ngx → 302 with „Ez a mentés nem tartalmazza az alkalmazás fájljait, ezért nem állítjuk vissza az adatbázist föléjük — a fájlok így a helyükön maradnak. A fájlok a távoli másolatból állíthatók vissza …", and the app read `running` before AND after, so nothing was stopped and no trash was made unreachable. The database-and-settings-only path exists as a separately worded second step. Red-proof: disabling the guard fails `TestUnitRestore_RefusesWhenTheUnitCannotHoldTheFiles`. **RE-PROVEN 2026-09-16 on a FRESH box, and this time the refusal had somewhere to point:** after five photos were deleted, `POST /backup/restore` was refused with „…a fájlok így a helyükön maradnak. A fájlok a távoli másolatból állíthatók vissza: … „Teljes visszaállítás (fájlok + adatbázis)"", the app read `running` before AND after, and the wastebasket was untouched. The off-site route then returned all five photos — 200 with the exact uploaded sizes and sha256 IDENTICAL to the originals, 5/5, with a negative control. Evidence: `audits/evidence-backup-promise-2026-09-16/phaseE-photos.txt`. | **CLOSED 2026-09-16 — controller v0.244.0 (proven live on demo-hp)** |
| **R-525** | **[P3-LOW] FileBrowser has its own login; putting it behind the dashboard session (traefik forwardAuth or Quantum proxy auth) is a new mechanism nobody has measured.** Filed 2026-09-15 by the P1-fixes task (B.5). R-513 closed the default-password hole with a generated password; a household still has two logins. **What it needs:** a spike on a scratch guest — forwardAuth to the controller session, and what FileBrowser Quantum does with a trusted header. | **READY — rank P3-LOW; owner: CC (spike)** |