docs: localisation slice 2 CLOSED (controller v0.254.0) — R-577, R-578, and a probe rule
gates / gates (push) Successful in 23s
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
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
---
|
||||
unconditional: true
|
||||
# Deliberately always-loading. This rule exists because TWO sessions made the same destructive
|
||||
# mistake on a live box, the second one WITH a prompt line telling it not to. A path-scoped rule
|
||||
# would load when you edit the handler; the mistake is made when you probe it, from anywhere.
|
||||
---
|
||||
|
||||
# Live probes — what a probe may touch on a real box
|
||||
|
||||
> One rule, earned twice in two days by two different sessions, both of which had a prompt line
|
||||
> telling them not to. A prompt is read once; a rule file is loaded every session, which is the whole
|
||||
> reason this file exists.
|
||||
|
||||
## Never send a deploy request for an app that is not installed — not even expecting a refusal
|
||||
|
||||
**`POST /api/stacks/<name>/deploy` ACCEPTS FIRST AND VALIDATES LATER.** It answers `202 Telepítés
|
||||
elindítva` and runs the validation inside a goroutine, so a probe that expects a refusal gets a 202 —
|
||||
and if the app happens to need no required field, it is now installed on the box.
|
||||
|
||||
- 2026-09-17: a session probing the required-field refusal picked an app that needed no field. It
|
||||
installed. Recorded in `STATUS.md`.
|
||||
- 2026-09-18: a session that had read that record, and had a prompt line forbidding it, did the same
|
||||
thing with `vaultwarden`. Recorded in
|
||||
`documentation/audits/i18n-slice2-2026-09-18/B/live/README.md`.
|
||||
|
||||
The lesson that sticks is narrower than "pick a different app": **the deploy endpoint cannot be used
|
||||
to probe a refusal at all.**
|
||||
|
||||
**Instead, use a request that is refused BEFORE anything is created:**
|
||||
|
||||
| you want to see | use |
|
||||
|---|---|
|
||||
| a deploy-path refusal | an app that is ALREADY installed → `409 already deployed` |
|
||||
| a not-found path | a name that exists nowhere → `404` |
|
||||
| a validator's sentence | `POST /sharing/shares` with a bad name, `POST /api/disks/assign` with a bad mount point — both refuse before they write |
|
||||
| a protected-resource refusal | `POST /api/stacks/felhom-controller/remove` → `403` |
|
||||
|
||||
## If a probe does create something, remove it through the product
|
||||
|
||||
Not by hand, and not by `docker rm`: stop it, then `POST /api/stacks/<name>/remove` with
|
||||
`remove_hdd_data` and `remove_backups`, and then **verify** — no container, no `/opt/felhom/stacks/<name>`,
|
||||
no volume. Say in the report that it happened. A tidy-up nobody is told about is how the next session
|
||||
learns nothing.
|
||||
|
||||
## The general shape
|
||||
|
||||
**Before sending anything to a live box, ask which side of the write the refusal happens on.** A
|
||||
refusal that comes after the write is not a refusal you can probe — it is a change you are making.
|
||||
@@ -1,55 +1,46 @@
|
||||
# STATUS — what works, what's broken, what's next
|
||||
|
||||
**Updated 2026-09-18 (evening) — the sentences the program writes now follow the language, error messages included.**
|
||||
**Updated 2026-09-18 (night) — the language work is finished, and the switch is now a globe.**
|
||||
|
||||
> **Ready for a volunteer: yes, unchanged.** A Hungarian household sees exactly what it saw
|
||||
> yesterday. I did not read the pages to decide that — I compared them, letter by letter.
|
||||
> **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.
|
||||
|
||||
**Decisions I took.** None you have to reverse. One choice you were asked for stayed on its own
|
||||
stated default: notes saved to disk overnight will be written in the box's language at the time they
|
||||
are written, so a household that switches sees last night's note in the old language until the next
|
||||
run. That is what the plan says happens if nobody decides.
|
||||
**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 I did.** Two releases. The first moved the sentences the program writes into the pages — the
|
||||
little green line after you press a button, the yellow banners, the country names, the answers the
|
||||
page fetches in the background. The second did the **error messages**: about 180 sentences that are
|
||||
written deep inside the program and printed by whatever catches them. All of them now travel with a
|
||||
short code, so the page that shows them writes them in your language. **None are left in Hungarian
|
||||
only.**
|
||||
**What I did today.** Three releases, and the language job is done.
|
||||
|
||||
**My mistake, and what I did about it.** My first test of an error message asked the box to install an
|
||||
app with nothing filled in, expecting a refusal — and it installed the app instead, because that
|
||||
particular one needs nothing filled in. **This is the same mistake the session before mine made and
|
||||
wrote down, and I repeated it.** I stopped the app and removed it the way a customer would, with its
|
||||
data, and checked: no container, no folder, no leftover storage. The 23 apps that should be running
|
||||
are all running. The real lesson is sharper than "pick a different app": that button accepts first and
|
||||
checks afterwards, so it cannot be used to test a refusal at all. The tests were rewritten to use
|
||||
requests that are refused before anything is created.
|
||||
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**.
|
||||
|
||||
**How I know Hungarian did not change.** A check freezes every Hungarian sentence as it stood before I
|
||||
started — 7 467 of them — and refuses any that is not exactly that, down to a comma. Then on the box:
|
||||
ten Hungarian pages before and after, six identical, the other four differing only in a clock ticking
|
||||
over, an app unhealthy for a minute, and the "update available" line.
|
||||
**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.
|
||||
|
||||
**Two things went wrong in my own tools, and I am naming them rather than tidying them away.** My
|
||||
bulk converter quietly dropped the second half of sentences that were written across two lines — seven
|
||||
of them — and **the freeze-check did not notice**, because each surviving half really was a real old
|
||||
sentence. What caught it was two tests that read the sentence a customer would see. And my counting
|
||||
script ignored capital letters, so it told me "none left" while five were. Both are written down with
|
||||
a number.
|
||||
**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.
|
||||
|
||||
**What is left.** The notes saved to disk overnight — one more release. Plus a handful of small gaps,
|
||||
each with a number.
|
||||
**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, five opened across the day. The register went from **264** to **269** open rows.
|
||||
**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.250.0 to **0.253.0**. The N100
|
||||
demo box took it by itself in about twenty seconds and is healthy, with its other four apps still
|
||||
running; 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, and Tester 1 for a day. They will take it on their own
|
||||
whenever they come back, which is how a floor always works, and 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.254.0?**
|
||||
- **If you do:** every box gets the globe on its next check-in, and the saved notes start following
|
||||
the language. The two sleeping boxes take it whenever they wake.
|
||||
- **If you do nothing:** households keep the two text links. Nothing breaks, and nothing is at risk.
|
||||
- I would raise it — this release changes what every household sees, and seeing it is the point.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ session). Each slice ends with the parity gate green for every page it touched.
|
||||
|---|---|---|---|---|
|
||||
| 0 (done) | — | mechanism, 3 pages + layout, setting, report field, gates | Hungarian unchanged by measurement; English reachable | spent |
|
||||
| 1 (done, v0.248.0–v0.250.0) | R-556 | the other 31 dashboard templates (~1 500 strings, 600 of them JS), parity fixtures per page; switch shown to everyone; English retrieval stems; the extractor's ASCII word list reviewed per page | the whole dashboard in English with Hungarian byte-identical | 12–16 h, three releases |
|
||||
| 2 | R-557 (after R-553) | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages | a page's server messages follow the language | 16–20 h, three releases · **A and B shipped 2026-09-18; C remains** |
|
||||
| 2 (done, v0.252.0–v0.254.0) | R-557 | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages; the saved notes; the globe | a page's server messages follow the language | **CLOSED 2026-09-18** |
|
||||
| 3 | R-558 | hub: per-customer language at creation; `configgen` renders it; `customerMessages` (39), the 5 lifecycle mails and the self-bind page in English; dispatcher reads the reported language | a household's e-mails arrive in its language | 6–8 h, one hub + one controller release |
|
||||
| 4 | R-559 | console banner (34 lines, console-font limits) and the download page | an English household meets English from the first boot screen (in scope — ruling 1b) | 4–6 h + an ISO train |
|
||||
| 5 | R-560 | catalog: §7 format, controller reads it, 835 strings / ~5 000 words, catalog copy gates per language | an app card, its settings and its first steps in English | 12–16 h |
|
||||
@@ -394,6 +394,68 @@ with both controls.
|
||||
string with no error to carry a key and no language where it is built, so it renders Hungarian on an
|
||||
English page. The copy is in the bundle; only the render is fixed. Named in the code, filed as a row.
|
||||
|
||||
### 10.3 Slice 2 release C — the saved notes, and a globe (v0.254.0). SLICE 2 CLOSED.
|
||||
|
||||
**[DESIGN] A note a background run SAVES is written in the BOX's language at write time** (operator
|
||||
ruling, §16 option 1). ~70 producers. **The consequence, recorded because it is the cost of the
|
||||
choice:** a household that switches language sees the previous run's note in the old language until
|
||||
the next run rewrites it — usually the next night. The alternative (store a code, render live) needs a
|
||||
dozen new persisted fields and a legacy path for each: the R-570 shape a dozen times over.
|
||||
`EndRestoreOp` now receives no Hungarian literal from anywhere.
|
||||
|
||||
**[FACT] A deadlock, introduced and caught by the suite hanging (R-578).** `UpdateOffboxStatus` holds
|
||||
the settings WRITE lock while running its callback; `boxLang()` reads the language through the READ
|
||||
lock; `sync.RWMutex` is not reentrant. A note rendered inside that callback deadlocks **holding the
|
||||
settings lock**, which wedges everything else on the box that touches `settings.json`. The only
|
||||
symptom was `go test` going from 8 minutes to a 25-minute timeout. **The general rule this establishes:
|
||||
a helper that takes a lock must never be called from inside a callback that holds one** — and a hang
|
||||
is the worst symptom to diagnose, which is why R-578 asks for a gate rather than one test in one package.
|
||||
|
||||
**[DESIGN] Decision 6, superseded a second time: the switch is a GLOBE, and a visitor's language is
|
||||
their own.** One answerable sentence: *how does someone who cannot read Hungarian find the way out of
|
||||
Hungarian?* Options: (1) keep two text links („Magyar"/„English") in the sidebar footer — cost: they
|
||||
wrap at the sidebar's width, and finding them means recognising two words as links; (2) one globe, the
|
||||
symbol every web user already reads as "language". **Chosen (2)**, `<details>`/`<summary>` so the menu
|
||||
needs no script and a screen reader announces it. Language names inside are shown in their own
|
||||
language and are never translated. Drawn inline rather than in the icon sprite, because the sprite
|
||||
lives only in `layout.html` and the pages outside the dashboard chrome have their own shell.
|
||||
|
||||
**[DESIGN] Who the page is FOR decides where its globe posts, and getting that wrong makes the button
|
||||
do nothing.**
|
||||
|
||||
| page | reader | globe posts to | their choice lives in |
|
||||
|---|---|---|---|
|
||||
| every dashboard page | the household, signed in | `/settings/language` (session CSRF) | `settings.json` |
|
||||
| `/recovery` — an AUTHENTICATED route | the household, signed in | `/settings/language` | `settings.json` |
|
||||
| `/login`, `/claim` | a visitor, no session | `/lang` (no CSRF) | the `felhom_lang` cookie, their browser |
|
||||
| the two guest share pages, the catch-all | a stranger / nobody | **no globe** | — (R-577, the operator's) |
|
||||
|
||||
`langFor`'s order is fixed: `?lang=` → **the household's setting when a session exists** → the cookie
|
||||
when there is none → the setting → `hu`. **A signed-in household never reads the cookie**, so they
|
||||
cannot inherit a language a previous visitor picked in the same browser. The recovery row above was
|
||||
measured live before it was right: an anonymous form there sets a cookie that `langFor` then ignores,
|
||||
and the button appears to do nothing.
|
||||
|
||||
**[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
|
||||
protocol-relative `//host` as well as an absolute URL — *"starts with `/`"* alone is not the test,
|
||||
because a browser reads `//evil.example` as another origin. **If that handler ever gains a second
|
||||
effect it needs CSRF that day.** That is the anonymous surface `04-control-plane-authorization.md`
|
||||
governs: changing what a visitor reads is within it, changing anything the household owns is not.
|
||||
|
||||
**[DESIGN] §16, the operator's default, taken: a successful CLAIM carries the visitor's language into
|
||||
the household's setting.** Someone who switched the claim page to English and then claimed the box
|
||||
chose English. Only on success, and only there — the one moment an anonymous visitor becomes the
|
||||
household.
|
||||
|
||||
**[FACT] The parity exceptions, measured rather than asserted.** 106 fixtures, a real (LCS) diff
|
||||
against the fixtures as they stood at v0.253.0: **3 change shapes** (the footer; the recovery globe;
|
||||
the login/claim globe) and **5 byte-identical**, which are exactly the pages that must not change.
|
||||
`audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt`. **A second measurement error worth
|
||||
keeping: the first attempt compared LINE BY INDEX, and an insertion shifts every line below it — it
|
||||
reported 60 520 changed lines and measured nothing. A line-index compare is not a diff.**
|
||||
|
||||
---
|
||||
|
||||
## 11. Operator decisions
|
||||
@@ -414,6 +476,11 @@ These are rulings, not proposals. Anything specced against a different assumptio
|
||||
5. **Mechanism (b2)** — §2.1.
|
||||
6. ~~**Switch hidden while only three pages are English** — §3.~~ **Superseded 2026-09-17** by slice 1
|
||||
release C (v0.250.0): every template converted, switch shown to every household (§3).
|
||||
**Superseded again 2026-09-18** by slice 2 release C (v0.254.0): the switch is a GLOBE, it is on the
|
||||
sign-in and claim pages too, and a visitor's choice lives in their own browser (§10.3).
|
||||
|
||||
8. **A successful claim carries the visitor's language into the household's setting** (§16 default,
|
||||
taken 2026-09-18). Only on success; every other anonymous request leaves `settings.json` alone.
|
||||
|
||||
### 2026-09-17 (evening) — the two open questions, ruled
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Live validation — controller v0.254.0 on demo-hp guest 9201 (2026-09-18, release C)
|
||||
|
||||
**Method: endpoint-level, stated as such.** No browser on DooPlex. Nothing was installed, created,
|
||||
formatted or deleted, and **the deploy endpoint was not touched at all** — the new rule
|
||||
(`.claude/rules/live-probes.md`) written after yesterday's mistake. The password was passed as a file
|
||||
and deleted from host and guest afterwards.
|
||||
|
||||
## 1. A visitor's language is their own (no session) — before and after
|
||||
|
||||
| probe | 0.253.0 | 0.254.0 |
|
||||
|---|---|---|
|
||||
| globe on `/login` | **0** | **1** |
|
||||
| `POST /lang lang=en back=/login` | 302 (no such route → the login redirect) | **303, `felhom_lang=en` set** |
|
||||
| `/login` with that cookie | `<html lang="hu">` | **`<html lang="en">`**, and "Forgot password" present |
|
||||
| **`settings.json` `language` after it** | `"hu"` | **`"hu"` — unchanged.** An anonymous request cannot write what the household owns |
|
||||
| `POST /lang lang=xx` | 302 | **400**, no cookie |
|
||||
| `POST /lang back=//evil.example/x` | the login redirect | **`/`** — a protocol-relative URL is another origin |
|
||||
| globe on `/claim` | 0 | **1** |
|
||||
|
||||
## 2. A signed-in household reads their own setting, never the cookie
|
||||
|
||||
`/launcher` with a session **and** the `felhom_lang=en` cookie → **`<html lang="hu">`**. The cookie is
|
||||
not read once there is a session; that is the row that keeps a household from inheriting a language a
|
||||
previous visitor picked in the same browser.
|
||||
|
||||
## 3. The dashboard globe, end to end
|
||||
|
||||
`POST /settings/language lang=en` (session CSRF) → 302 → `settings.json` `"language": "en"` →
|
||||
`/launcher`, `/backups`, `/backups/remote` all `<html lang="en">` → switched back → `"hu"`.
|
||||
|
||||
The footer renders in order — version, globe, sign-out — and the old two-text-link switch is gone
|
||||
(`lang-switch-btn` count 1 → **0**):
|
||||
|
||||
```
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.254.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" …globe…/></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language">…<button … class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language">…<button … class="lang-globe-item">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
|
||||
```
|
||||
|
||||
## 4. The saved note, and the §16 consequence, seen live
|
||||
|
||||
With the box switched to English, the stored tier-2 note stayed as it was written:
|
||||
`"last_warning": "A belső SSD-n csak a konfiguráció, adatbázis és a köte…"`. **That is the operator's
|
||||
choice working, not a bug:** a saved note is written in the box's language at write time and shown
|
||||
verbatim afterwards, so a household that switches sees the previous run's note in the old language
|
||||
until the next run rewrites it.
|
||||
|
||||
## 5. What could NOT be proven live, and why
|
||||
|
||||
**The `/recovery` globe.** `recoveryPageHandler` redirects to `/backups/remote` unless
|
||||
`recoveryOffer()` is true — the page exists only while a box is in a lost-machine situation, and
|
||||
demo-hp is not. It 302s with or without a session. So its globe is proven by the render tests
|
||||
(`TestGlobeOnAnonymousShells`, `TestI18nDirectRenderPagesFollowLanguage`) and by its 7 parity
|
||||
fixtures, **not live**. Said plainly rather than left to be assumed from the other rows.
|
||||
|
||||
That page is also where release C's own defect was: it is an AUTHENTICATED route, so its globe must
|
||||
write the household's SETTING; the first draft gave it the anonymous form, which sets a cookie
|
||||
`langFor` ignores once there is a session — the button would have done nothing. Found by this probe
|
||||
reporting no globe on `/recovery` and then reading the route table.
|
||||
|
||||
**The `<details>` menu opening in a browser.** `claude-in-chrome` is not available on DooPlex. The
|
||||
markup is asserted; whether it looks and behaves right on screen is a manual click-through.
|
||||
|
||||
## 6. State left behind
|
||||
|
||||
Controller **0.254.0**; saved language **`hu`**; the visitor cookie was set only inside the probe's
|
||||
own curl jar and is gone with it. 23 standing containers up, unchanged. Nothing installed, nothing
|
||||
removed, no drive touched, no floor raised, no golden baked. **Provisioned nothing.**
|
||||
@@ -0,0 +1,17 @@
|
||||
##### A — NO SESSION: the visitor's own language
|
||||
settings.json language before: "language": "hu"
|
||||
/login with no cookie -> html lang=<html lang="hu"
|
||||
globe present on /login -> 1
|
||||
POST /lang lang=en back=/login -> 303 felhom_lang en
|
||||
/login WITH the cookie -> html lang=<html lang="en"
|
||||
and an English phrase -> 1
|
||||
settings.json language after: "language": "hu"
|
||||
POST /lang lang=xx -> 400
|
||||
POST /lang back=//evil -> /
|
||||
/claim and /recovery globes: /claim=1 /recovery=0
|
||||
##### B — WITH A SESSION: the household's own setting, cookie NOT read
|
||||
/launcher with session + the en cookie -> html lang=<html lang="hu"
|
||||
the footer: version, globe, sign-out in order ->
|
||||
the OLD text links are gone -> lang-switch-btn count = 0
|
||||
##### C — the saved notes, as they stand on this box
|
||||
"last_warning": "A belső SSD-n csak a konfiguráció, adatbázis és a kötelező adatok férnek
|
||||
@@ -0,0 +1,15 @@
|
||||
##### A — NO SESSION: the visitor's own language
|
||||
settings.json language before: (unreadable)
|
||||
/login with no cookie -> html lang=<html lang="hu"
|
||||
globe present on /login -> 0
|
||||
POST /lang lang=en back=/login -> 302 /login WITH the cookie -> html lang=<html lang="hu"
|
||||
and an English phrase -> 0
|
||||
settings.json language after: (unreadable)
|
||||
POST /lang lang=xx -> 302
|
||||
POST /lang back=//evil -> /login?next=%2Flang
|
||||
/claim and /recovery globes: /claim=0 /recovery=0
|
||||
##### B — WITH A SESSION: the household's own setting, cookie NOT read
|
||||
/launcher with session + the en cookie -> html lang=<html lang="hu"
|
||||
the footer: version, globe, sign-out in order ->
|
||||
the OLD text links are gone -> lang-switch-btn count = 1
|
||||
##### C — the saved notes, as they stand on this box
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Release C probe. Every request below either reads a page or sets a display cookie. Nothing is
|
||||
# installed, created, formatted or deleted; no deploy endpoint is touched at all (.claude/rules/live-probes.md).
|
||||
IP=172.17.0.2:8080
|
||||
H="Host: felhom.enkisfelhom.hu"
|
||||
PW=$(cat /tmp/.felhompw); J=/tmp/pj.txt; rm -f $J /tmp/vj.txt
|
||||
echo "##### A — NO SESSION: the visitor's own language"
|
||||
echo -n " settings.json language before: "; grep -o '"language": *"[a-z]*"' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null || echo "(no language key yet = hu)"
|
||||
echo -n " /login with no cookie -> html lang="; curl -s -H "$H" "http://$IP/login" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " globe present on /login -> "; curl -s -H "$H" "http://$IP/login" | grep -c 'class="shell-lang"'
|
||||
echo -n " POST /lang lang=en back=/login -> "; curl -s -c /tmp/vj.txt -H "$H" -o /dev/null -w "%{http_code} " -X POST "http://$IP/lang" --data-urlencode "lang=en" --data-urlencode "back=/login"; grep -o 'felhom_lang[[:space:]]*[a-z]*' /tmp/vj.txt | head -1
|
||||
echo -n " /login WITH the cookie -> html lang="; curl -s -b /tmp/vj.txt -H "$H" "http://$IP/login" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " and an English phrase -> "; curl -s -b /tmp/vj.txt -H "$H" "http://$IP/login" | grep -c 'Forgot password'
|
||||
echo -n " settings.json language after: "; grep -o '"language": *"[a-z]*"' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null || echo "(no language key yet = hu)"
|
||||
echo -n " POST /lang lang=xx -> "; curl -s -H "$H" -o /dev/null -w "%{http_code}\n" -X POST "http://$IP/lang" --data-urlencode "lang=xx" --data-urlencode "back=/login"
|
||||
echo -n " POST /lang back=//evil -> "; curl -s -H "$H" -o /dev/null -w "%{redirect_url}\n" -X POST "http://$IP/lang" --data-urlencode "lang=en" --data-urlencode "back=//evil.example/x" 2>/dev/null | sed 's|http://[^/]*||'
|
||||
echo -n " /claim and /recovery globes: "; for P in /claim /recovery; do echo -n "$P=$(curl -s -H "$H" "http://$IP$P" | grep -c 'class=\"shell-lang\"') "; done; echo
|
||||
echo "##### B — WITH A SESSION: the household's own setting, cookie NOT read"
|
||||
curl -s -c $J -H "$H" "http://$IP/login" -o /tmp/lg.html
|
||||
CSRF=$(grep -o 'name="_csrf" value="[^"]*"' /tmp/lg.html | head -1 | sed 's/.*value="//;s/"//')
|
||||
SESS=$(curl -s -b $J -H "$H" -D - -o /dev/null -X POST "http://$IP/login" --data-urlencode "password=$PW" --data-urlencode "_csrf=$CSRF" | grep -i '^set-cookie: felhom_session' | head -1 | sed 's/[Ss]et-[Cc]ookie: //;s/;.*//')
|
||||
[ -z "$SESS" ] && { echo "LOGIN FAILED"; exit 1; }
|
||||
LANGC=$(grep -o 'felhom_lang[[:space:]]*[a-z]*' /tmp/vj.txt | head -1 | awk '{print $2}')
|
||||
echo -n " /launcher with session + the en cookie -> html lang="
|
||||
curl -s -H "$H" -H "Cookie: $SESS; felhom_lang=$LANGC" "http://$IP/launcher" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " the footer: version, globe, sign-out in order -> "
|
||||
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/launcher" | grep -o 'class="sidebar-footer".*logout-link' | grep -o 'class="version"\|class="lang-globe"\|class="logout-link"' | tr '\n' ' '; echo
|
||||
echo -n " the OLD text links are gone -> lang-switch-btn count = "
|
||||
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/launcher" | grep -c 'lang-switch-btn'
|
||||
echo "##### C — the saved notes, as they stand on this box"
|
||||
grep -o '"last_warning": *"[^"]\{0,80\}' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null | head -2; grep -o '"last_error": *"[^"]\{0,80\}' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null | head -2
|
||||
@@ -0,0 +1,46 @@
|
||||
PARITY EXCEPTIONS — controller v0.254.0 (R-557 slice 2 release C)
|
||||
|
||||
Every Hungarian fixture rendered NOW, against the fixture as it stood at eb6aa58 (v0.253.0) —
|
||||
itself captured from the UNCONVERTED templates and never regenerated to make a conversion pass.
|
||||
A real diff (LCS), not a line-index compare: the globe ADDS lines, and a line-index compare calls
|
||||
every line below an insertion changed, which measures nothing (redproofs.txt, catch D).
|
||||
The per-session CSRF token is blanked on both sides — it is random per session and can never be
|
||||
a fixture value; what is still pinned is that the field is THERE and WHICH form it sits in.
|
||||
|
||||
[89 fixtures] e.g. app_export.html, app_import_bundles.html, app_import_empty.html
|
||||
- <span class="version">0.247.0</span><form method="POST" action="/settings/language" class="lang-switch"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" class="lang-sw
|
||||
+ <span class="version">0.247.0</span><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" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <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="submit" name="lang" value="hu" lang="hu" class="lang-globe
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe
|
||||
+ </ul>
|
||||
+</details>
|
||||
|
||||
[7 fixtures] e.g. recovery_locked_can.html, recovery_locked_cannot.html, recovery_locked_confirm.html
|
||||
+ <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" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <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="submit" name="lang" value="hu" lang="hu" class="lang-globe
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe
|
||||
+ </ul>
|
||||
+</details></div>
|
||||
|
||||
[5 fixtures] e.g. claim_reset_code.html, claim_reset_nocode.html, claim_setup_code.html
|
||||
+ <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" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <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 current" aria-current="true">Magyar</button></form></l
|
||||
+ <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">English</button></form></li>
|
||||
+ </ul>
|
||||
+</details></div>
|
||||
|
||||
[5 fixtures] IDENTICAL — not one byte moved: catchall_app.html, catchall_unknown.html, launcher_share_password.html, launcher_shared_apps.html, launcher_shared_empty.html
|
||||
|
||||
DISTINCT CHANGE SHAPES: 3.
|
||||
The release declares TWO blocks — the dashboard footer, and the globe on the pages outside the
|
||||
dashboard chrome. The second has two forms, because WHO reads the page decides where its globe
|
||||
posts: /recovery is an AUTHENTICATED route so its reader is the household and writes their
|
||||
setting; /login and /claim are met with no session and write the visitor's own cookie. The five
|
||||
untouched fixtures are exactly the pages that must not change: both guest share pages and the
|
||||
catch-all (R-577).
|
||||
@@ -52,3 +52,43 @@ A. The bulk converter SILENTLY DROPPED the continuation of a multi-line concaten
|
||||
B. My own counting script was CASE-SENSITIVE, so it reported "0 error literals left" while five
|
||||
remained ("occ parancs sikertelen", "hub hiba", "OnlyOffice aldomain nem ismert" x2). The
|
||||
R-565 shape, in the instrument. Re-measured with re.I; the five are converted.
|
||||
|
||||
RELEASE C (v0.254.0), 2026-09-18 — saved notes, and a globe for the language switch
|
||||
|
||||
15. langFor drops the `!s.hasSession(r)` guard (a signed-in household inherits a visitor's cookie)
|
||||
CONVICTS: TestLangForPrecedence/SESSION_→_the_household's_setting,_cookie_NOT_read
|
||||
— langFor = "en", want "hu"
|
||||
16. safeBackPath allows a protocol-relative URL (an open redirect off the box)
|
||||
CONVICTS: TestLangCookieHandler/back_may_only_be_a_same-origin_path
|
||||
— back="//evil.example/x" → Location "//evil.example/x", want "/"
|
||||
17. the R-570 producer is translated (the one saved sentence release C may not touch)
|
||||
CONVICTS: TestR570SentenceStaysHungarian, BOTH arms — the literal is gone AND a bundle key
|
||||
appeared, each named separately
|
||||
18. one byte changed inside the FOOTER parity-exception block (lang-globe-menu → -menu2)
|
||||
CONVICTS: TestI18nParity on all 101 re-captured fixtures
|
||||
19. one byte changed inside the SHELL parity-exception block (shell-lang → shell-lang2)
|
||||
CONVICTS: TestI18nParity naming login at line 11
|
||||
|
||||
TWO LIVE CATCHES IN RELEASE C — neither planted:
|
||||
C. The parity HARNESS rendered the three visitor shells through addLanguageData, the DASHBOARD path.
|
||||
The fixture would have baked a globe posting to /settings/language with a CSRF field — a form the
|
||||
real page never serves. Caught by reading the diff before re-capturing, and independently by
|
||||
TestI18nDirectRenderPagesFollowLanguage, which renders through the REAL executeTemplateLang.
|
||||
The harness now branches on i18nDirectTemplates.
|
||||
D. The first "is the change only the two declared blocks?" measurement compared LINE BY INDEX, and an
|
||||
insertion shifts every line below it — so it reported 60 520 changed lines and measured nothing.
|
||||
Redone as a real (LCS) diff: exactly TWO change shapes across 106 fixtures, and 5 fixtures
|
||||
byte-identical (the two guest share pages and the catch-all — the three that must not change).
|
||||
|
||||
RELEASE C, second round — after the recovery-page finding
|
||||
|
||||
E. LIVE CATCH, not planted: /recovery is in the AUTHENTICATED route table, so its reader is the
|
||||
HOUSEHOLD — but release C's first draft gave it the anonymous globe, which sets a cookie langFor
|
||||
deliberately ignores once there is a session. The button would have done NOTHING. Found by the
|
||||
live probe reporting `/recovery globes: 0` (302 to /login) and then reading the route table.
|
||||
Fixed: executeTemplateLang branches on hasSession — household form with CSRF, or visitor form
|
||||
without. Two tests and the parity harness now carry the same branch.
|
||||
F. The per-session CSRF token cannot be a fixture value. Blanked on both sides of every parity
|
||||
comparison, exactly as relative ages already were; what stays pinned is that the field is THERE
|
||||
and WHICH form it sits in — which is the half that says whether the globe writes the household's
|
||||
setting or the visitor's cookie.
|
||||
|
||||
@@ -733,7 +733,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
|
||||
| **R-552** | **[P3-LOW] An interrupted-restore notice for an app that is then REMOVED stays on the restore page for ever.** FOUND 2026-09-17 by CC reviewing its own controller v0.246.0 (R-550) during live validation. The per-app notice (`Manager.opInterrupted`, persisted in `restore-status.json`) is cleared in exactly one place — `BeginRestoreOp` for that app (`internal/backup/opstatus.go`) — and `removeStack` (`internal/api/router.go`) never touches the restore record. So a household that answers „A visszaállítás megszakadt … indítsd el újra" by REMOVING the app instead of restoring it keeps a „Megszakadt visszaállítás" card about an app that no longer exists. Measured shape, not hypothetical: on 9201 the notice cleared only when homebox was restored again (08:54:50Z, card count 0) — the teardown deliberately took that path before removing it. **Fix shape:** `removeStack` clears the app's notice (a `ClearInterruptedRestore(stack)` beside the existing update-hold clear, R-491's precedent), with a wiring test. Not fixed in v0.246.0: found after the release was built; one release per repo per session. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-554** | **[P3-LOW] Delete the first-boot setup wizard — obsolete by design, still reachable.** OPERATOR DECISION 2026-09-17 (localisation starter, decision 4: „out of scope, obsolete"). `02-controller-module-map.md` L56 calls `internal/setup/` obsolete; `cmd/controller/main.go` L322 still enters it when `setup.NeedsSetup(cfg)` — `customer.id` empty after bootstrap ingestion, or a `.needs-setup` marker (`internal/setup/setup.go` L17-25). Ingestion leaves `customer.id` empty on a missing/invalid `bootstrap.json`, a failed hub pull, or a failed merge/write/reload (`internal/bootstrap/bootstrap.go` L109-163) — so a box whose first boot cannot reach the hub shows a household an 8-page wizard (95 Hungarian strings, its own template set and CSRF). **Fix shape:** decide what such a box shows instead (a single „cannot reach Felhom yet, retrying" page — needs no decision beyond copy), then delete `internal/setup/` and `runSetupMode`; red-proof that a failed ingestion renders the waiting page, not a 404. **Check first** whether any drill/golden path still relies on `.needs-setup`. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-555** | **[P3-LOW] The wire-contract gate counts a field as received when its name appears in a Go COMMENT on the receiving side.** FOUND 2026-09-17 by CC adding the report's `language` field (controller v0.247.0): `scripts/wire_contract_gate.py` passed WITHOUT an allowlist entry, because `receiver_tokens()` tokenises whole files and the word „language" occurs hub-side only in a comment (`hub/internal/web/configs.go:558`, „this page's existing language"). The shape is the gate's own named failure class (name-for-fact, R-421): any English tag name that also appears in hub prose passes unread. The field was allowlisted by hand with this row named. **Fix shape:** strip `//` and `/* */` comments (and template `{{/* */}}`) before tokenising; add the decoy „a tag whose name appears only in a receiver comment must convict"; expect a handful of currently-passing tags to surface — each is a finding, not noise. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-557** | **[P3-LOW] Localisation slice 2 — Go-side customer strings follow the language.** PLAN 2026-09-17 (10 §10). Inventory §2.2: 947 shown + 184 error literals in 84 files; 237 format strings, 111 concatenations, 42 numeric `%d` (English plurals). Flash messages travel inside the redirect URL (`?flash=<Hungarian>`) and must become keys; `cloudflare/countries.go` (113 country names); alert texts; handler errors printed with `err.Error()`. **After R-553** (the four compare-not-show sites). Cost 16–20 CC-hours. **DEPENDENCY added 2026-09-17 (R-553 shipped, v0.251.0):** the behaviour-by-wording sites are fixed, so this slice is unblocked — EXCEPT one producer: `"Sikeres — nincs mentésre jelölt alkalmazás"` (controller/internal/backup/offbox.go) must stay Hungarian until **R-570** closes, because the page's legacy fallback still reads it on boxes that have not run off-site since 0.251.0. Everything else this slice touches is now decided by a kind, not by its words. **RELEASE A SHIPPED 2026-09-18 (controller v0.252.0):** 226 Go literals converted -- flash-as-key (8 writers, 8 readers, legacy prose still shown verbatim), page data and view-model text, the `internal/api` JSON answers, the alert banners (`Alert.MessageKey`), 237 country names at DISPLAY (the cloudflare table is untouched -- only CODES are on the wire, the task claimed otherwise), and the four app-named page titles (R-566 CLOSED). New gate `i18n_go_parity.py` + `i18n_go_base.json` (7 467 base-commit literals, frozen) refuses any key whose Hungarian is not byte-identical; three decoys. Wire goldens freeze `internal/monitor` and `internal/notify` -- the hub MAILS the event message when it has no `customerMessages` entry, so those stay Hungarian until R-558. `HU_FORMAL_CEILING` 16 -> 18 (measured, no word changed). **RELEASE B SHIPPED 2026-09-18 (controller v0.253.0): all 179 error literals carry a key** via `util.MsgError` -- `Error()` is still the Hungarian byte for byte, `errors.Is` answers for the kind AND a wrapped cause, an error ARGUMENT renders recursively, and a foreign error (restic/docker/ssh/stdlib) prints verbatim. 76 display sites go through `errText`, pinned by `TestNoErrErrorInPageOutput`. `memoryVerdict` returns an error; `UpdateRefusal` gained a `Cause`. Plurals are a BUNDLE rule (a key with `.one`/`.other` takes its count first), not a call-site flag. **ZERO Hungarian error literals remain** (ASCII search with both controls, case-insensitive). Two tooling defects found and filed: R-576 (the converter dropped concatenations and the gate could not see it) and the case-sensitive counter. New gap: R-575. **RELEASE C: persisted text**, written in the box language at write time (the task's s16 option 1, its stated default). Gaps found and filed: R-572, R-573, R-574. | **IN PROGRESS - releases A and B shipped 2026-09-18; release C (persisted text) remains; rank P3-LOW; owner: CC** |
|
||||
| **R-557** | **[P3-LOW] Localisation slice 2 — Go-side customer strings follow the language.** PLAN 2026-09-17 (10 §10). Inventory §2.2: 947 shown + 184 error literals in 84 files; 237 format strings, 111 concatenations, 42 numeric `%d` (English plurals). Flash messages travel inside the redirect URL (`?flash=<Hungarian>`) and must become keys; `cloudflare/countries.go` (113 country names); alert texts; handler errors printed with `err.Error()`. **After R-553** (the four compare-not-show sites). Cost 16–20 CC-hours. **DEPENDENCY added 2026-09-17 (R-553 shipped, v0.251.0):** the behaviour-by-wording sites are fixed, so this slice is unblocked — EXCEPT one producer: `"Sikeres — nincs mentésre jelölt alkalmazás"` (controller/internal/backup/offbox.go) must stay Hungarian until **R-570** closes, because the page's legacy fallback still reads it on boxes that have not run off-site since 0.251.0. Everything else this slice touches is now decided by a kind, not by its words. **RELEASE A SHIPPED 2026-09-18 (controller v0.252.0):** 226 Go literals converted -- flash-as-key (8 writers, 8 readers, legacy prose still shown verbatim), page data and view-model text, the `internal/api` JSON answers, the alert banners (`Alert.MessageKey`), 237 country names at DISPLAY (the cloudflare table is untouched -- only CODES are on the wire, the task claimed otherwise), and the four app-named page titles (R-566 CLOSED). New gate `i18n_go_parity.py` + `i18n_go_base.json` (7 467 base-commit literals, frozen) refuses any key whose Hungarian is not byte-identical; three decoys. Wire goldens freeze `internal/monitor` and `internal/notify` -- the hub MAILS the event message when it has no `customerMessages` entry, so those stay Hungarian until R-558. `HU_FORMAL_CEILING` 16 -> 18 (measured, no word changed). **RELEASE B SHIPPED 2026-09-18 (controller v0.253.0): all 179 error literals carry a key** via `util.MsgError` -- `Error()` is still the Hungarian byte for byte, `errors.Is` answers for the kind AND a wrapped cause, an error ARGUMENT renders recursively, and a foreign error (restic/docker/ssh/stdlib) prints verbatim. 76 display sites go through `errText`, pinned by `TestNoErrErrorInPageOutput`. `memoryVerdict` returns an error; `UpdateRefusal` gained a `Cause`. Plurals are a BUNDLE rule (a key with `.one`/`.other` takes its count first), not a call-site flag. **ZERO Hungarian error literals remain** (ASCII search with both controls, case-insensitive). Two tooling defects found and filed: R-576 (the converter dropped concatenations and the gate could not see it) and the case-sensitive counter. New gap: R-575. **RELEASE C SHIPPED 2026-09-18 (controller v0.254.0) -- SLICE 2 CLOSED.** ~70 saved-note producers write in the BOX language at write time (s16 option 1; a household that switches sees the previous run's note in the old language until the next run). `EndRestoreOp` takes no Hungarian literal from anywhere. The switch became a GLOBE on the dashboard and on the pages outside the dashboard chrome; a visitor with no session gets a display-only `felhom_lang` cookie that `langFor` reads ONLY when there is no session, and a successful CLAIM carries it into the setting (s16). Two parity blocks, measured with a real diff: 3 change shapes across 106 fixtures (footer; the recovery globe posting to the household switch; the login/claim globe posting to /lang), 5 byte-identical. **A DEADLOCK was introduced and caught by the suite hanging (R-578).** Gaps filed: R-572..R-578. | **CLOSED 2026-09-18 - controller v0.252.0 + v0.253.0 + v0.254.0** |
|
||||
| **R-558** | **[P3-LOW] Localisation slice 3 — the hub's customer e-mails follow the household's language; the operator sets it at customer creation.** PLAN 2026-09-17 (10 §3, §10), operator decision 2. The box already reports `"language"` (controller v0.247.0); nothing hub-side reads it. Build: a per-customer language on the hub (default hu) set at creation and rendered into `controller.yaml` beside `customer.*` (`hub/internal/configgen/configgen.go`), used by the box only while `settings.json` has no choice; the dispatcher picks the language the box REPORTS; English for the 39 `customerMessages`, the 4 severity labels, the body wrapper, the claim/re-enroll/reset/claimed/self-bind mails and the public bind page (inventory §2.3). Delete the `language` allowlist entry in `wire_contract_gate.py` when the field is read. Cost 6-8 CC-hours, one hub + one controller release. **THE WIRE SURVEY (R-557 release A, 2026-09-18) HANDS THIS ROW ITS INPUT LIST:** (1) `internal/notify/notifier.go` -- 31 event messages. The task assumed the hub composes its own mail from the event kind; it does NOT. `FormatCustomerEmail` uses `customerMessages[eventType]` when it has one and **falls back to the controller's message when it has none**, and appends it as „- Üzenet: %s” whenever the two differ -- several types carry no entry precisely so the controller's dynamic sentence IS the mail (hub `api/handler.go` L2034/L2045/L2066). So an English household's mail is Hungarian until this row ships. (2) `internal/monitor/healthcheck.go` -- 5 storage sentences in report `health.warnings`/`health.issues`, operator-facing. Both are frozen by wire goldens in those packages, so no later slice can translate them by accident; this row is what unfreezes them. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-559** | **[P3-LOW] Localisation slice 4 — the console banner and the download page in English.** IN SCOPE — operator ruling 2026-09-17 evening („yes", 10 §11 1b). PLAN 2026-09-17 (10 §10, open decision 1b): the starter listed them; the operator's scope ruling names „the controller, emails, guide, app catalog" and not them. Inventory §2.4: 34 banner lines (`scripts/iso/felhom-bootstrap.sh`, printf to the console; `/etc/issue` byte-coupled to `iso/pkg/debian/postinst`, console font avoids ő/ű) and 32 strings on `website/letoltes.html`. Cost 4–6 CC-hours plus an ISO release train. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-560** | **[P3-LOW] Localisation slice 5 — catalog cards, settings and first steps in English.** PLAN 2026-09-17 (10 §7, §10). Inventory §2.5: 835 strings, ~4 984 words across all 53 `.felhom.yml` (use_cases 262, first_steps 233, deploy_fields descriptions 79 / labels 68, prerequisites 60, description 56, tagline 53). Proposed format: an `i18n: {en: {...}}` block inside each `.felhom.yml` (controller's `yaml.Unmarshal` ignores unknown keys, so older controllers are unaffected), field-by-field fallback. Needs a controller change to read it and catalog copy gates with an English rule. Cost 12–16 CC-hours. | **READY - rank P3-LOW; owner: CC (catalog + controller)** |
|
||||
@@ -752,6 +752,8 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
|
||||
| **R-574** | **[P3-LOW] `web/handler_debug.go` mixes page copy with JSON payload, so neither half could be converted safely.** FOUND 2026-09-18 by localisation slice 2 release A (R-557): the file holds 39 Hungarian literals and the inventory classifies them by STATEMENT, not by data flow (`I18N-INVENTORY-2026-09-17.md` §4), so which are section headings the debug page renders and which are values inside a diagnostic dump the operator copies out is not established. Converting a dump value would change what an operator pastes into a report; leaving a heading Hungarian leaves a half-English page. **Fix shape:** walk the file once and label every literal page-copy or payload in the same table slice 2 release A used, then convert only the page-copy half. Belongs to slice 2 release B or C. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **R-575** | **[P3-LOW] The soft memory-overcommit warning is returned as a Hungarian STRING, so it renders Hungarian on an English page.** FOUND 2026-09-18 by localisation slice 2 release B (R-557, controller v0.253.0): `memoryVerdict` now returns its REFUSAL as an error carrying a key (`util.MsgErrorf(ErrNotEnoughMemory, …)`), but its WARNING is a plain string with no error to carry one, and no language is known where it is built — so it goes through `msgHU` and is always Hungarian. The copy is in the bundle (a translator sees it; `i18n_go_parity.py` pins it), only the render is fixed to `hu`. The named helper and this consequence are stated in `internal/stacks/deploy_errors.go`. **Fix shape:** the caller carries the key the way `Alert` and `UpdateRefusal` now do — `memoryVerdict` returns `(refusal error, warningKey string, warningArgs []any)` and the deploy answer renders it — not a helper guessing a language it cannot know. | **READY - rank P3-LOW; owner: CC** |
|
||||
| **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-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)** |
|
||||
|
||||
Reference in New Issue
Block a user