diff --git a/.claude/rules/website.md b/.claude/rules/website.md index 284babd9..7acc629d 100644 --- a/.claude/rules/website.md +++ b/.claude/rules/website.md @@ -41,6 +41,13 @@ to the visible answers, and nothing after ``. the nav listing every language in its own name, the current one marked. **A new language = one entry in `LANGS` in `scripts/site_gates.py` (and the globe's list) + its twin pages** — nothing else in the switch changes. +## The dashboard pictures + +`website/assets/dashboard--hu.webp` / `-en.webp` show **controller 0.303.0** (taken 2026-10-08 on demo-hp's +household guest; method and privacy scan: `documentation/audits/website-dashboard-2026-10-08/`). **A controller +release that visibly changes a pictured page (Launcher, Apps, an app's page, Backup → Apps) should refresh them.** +Every caption is a claim (`captions-claims.md` there). Gate 19 keeps each page on its own language's pictures. + ## The encoding fences - **All `website/` HTML is UTF-8 *with BOM*. Preserve it.** An editor that strips the BOM is a diff --git a/REPORT-website-dashboard.md b/REPORT-website-dashboard.md index fbf2c8aa..fa6b5aa8 100644 --- a/REPORT-website-dashboard.md +++ b/REPORT-website-dashboard.md @@ -1,36 +1,75 @@ -# REPORT — dashboard screenshots for the website (2026-10-08): STOPPED at the baseline +# REPORT — the dashboard on the website (2026-10-08) -> `REPORT-website-dashboard.md`, not `REPORT.md`: another session works in this repo today. +> `REPORT-website-dashboard.md`, not `REPORT.md`: another session works in this repo today. This file replaces this +> morning's „stopped at the baseline" version of it (commit 4b5f74e4); the operator then approved moving 9202 to the +> fleet's controller and said the physical demo boxes are test boxes too. | Part | What | State | |---|---|---| -| Baseline | a scratch guest that runs the fleet's controller | **Not met — stopped here, as the brief says** | -| A | the scene + screenshots | not started | -| B | the pages + the hu/en picture gate | not started | -| C | rules, CHANGELOG, STATUS | not started (nothing changed on the site) | +| Baseline | a box on the fleet's controller (0.303.0) | 9202 moved 0.301.0 → 0.303.0 (operator's word); pictures finally from demo-hp's household guest 9201 (see below) | +| A | scene + screenshots, hu and en, privacy check | **Done** — 8 pictures published, 6 taken and kept off the site | +| B | home-page section, technology page, captions as claims, gate 19 + decoy | **Done** | +| C | rules, CHANGELOG, STATUS, rows | **Done** — two rows for dashboard defects (R-906, R-907) | -## What was measured (read only) +**Register:** before **129**, after **131**, opened **2** (R-906, R-907), closed **0**. -- **Fleet controller: 0.303.0** — `STATUS.md` header („demo-hp, demo-felhom and Tester 1 run … controller 0.303.0"), - newest entry in `felhom-controller/CHANGELOG.md`: `## v0.303.0` (2026-10-07). -- **Scratch guest 9202 runs 0.301.0** — `pct exec 9202 -- cat /etc/felhom-controller-image` → - `gitea.dooplex.hu/admin/felhom-controller:0.301.0`; `docker ps` shows `felhom-controller … 0.301.0 Up 55 minutes`. -- **No other scratch guest exists** — `pct list` on the demo-hp host: 9201 (demo-hp, the demo household — no contact), - 9202 (scratch), 9401 (`upgrade-harness`, the catalog bench, stopped; it runs no controller). demo-felhom and the - Testers are fenced. -- **The site has no dashboard picture today** — `website/assets/` holds no `dashboard*` file. -- 9202's controller restarted 55 minutes before this check, and it carries paperless-ngx: **another session may be - using 9202 today** (session 1 reported „R-762 closed (9202 proven)" in d9147b02). +## Why the pictures come from 9201, not 9202 -Nothing was installed, changed or removed on 9202. The only host command was `pct exec 9202 -- …` (read) and `pct list`. +On 9202 (now 0.303.0) four family apps were installed and a real backup ran, but **every page carried „+ 5 további +figyelmeztetés"**: nine `disk-not-separate` warnings (9202's „second drive" is a folder on the host disk, plus earlier +sessions' leftover folders) and the hub-off warning — true for a scratch guest, not a household's normal state. Faking it +away was not an option. demo-hp's household guest 9201 runs 0.303.0 with **no alerts** (`/api/debug/dump` → +`alerts: []`) and last night's app backups OK, so the pictures were taken there, read only. The one change: the +household language switched to English for the English pictures and back to Hungarian right after (both 302; read back +`lang="hu"`, first sidebar item „Indítópult"). + +## Pictures (`website/assets/`, 1440 × 900 WebP) + +| Picture | hu bytes | en bytes | On | +|---|---|---|---| +| `dashboard-start` (Launcher) | 34,024 | 31,948 | home | +| `dashboard-apps` (Apps) | 59,018 | 54,156 | home, technology | +| `dashboard-app` (Paperless-ngx's page) | 83,124 | 78,992 | home | +| `dashboard-backups` (Backup → Apps) | 52,638 | 45,292 | home, technology | + +Kept off the site: the backups overview (a true amber „whole-box backup on the system disk" notice on demo-hp), the System +page (operator details), the phone view (the launcher's share button runs off the edge at 390 px — R-907). + +## Privacy check, per picture (`documentation/audits/website-dashboard-2026-10-08/privacy-scan.txt`) + +Searched in each page's text (body, title/alt/placeholder/input values): e-mail addresses, IPv4 addresses, long hex +(tokens/keys), the host's serial, password/recovery words, any domain other than the test domain. **All 8: 0 e-mails, +0 IPs, 0 serials, no domain but `enkisfelhom.hu` (demo-hp's demo test domain) and `felhom.eu`.** One long hex per page = +the hidden CSRF field, not visible in the picture. Password-word hits on the app pages are help text, not a password. +**Positive control:** an app name known to be on the page (`Paperless-ngx` / `BookStack`) — found 8 of 8. + +## Captions = claims (`captions-claims.md`) + +Launcher → map E Indítópult (IMPLEMENTED); Apps → map B deploy + version (PROVEN-LIVE); an app's page → map B app +lifecycle (PROVEN-LIVE) + catalog copy + file access (IMPLEMENTED); Backup → Apps → map C nightly DB dumps (PROVEN-LIVE). + +## Clean-up + +9202: Vaultwarden, Jellyfin, Nextcloud, Immich removed through the product (data and backups too); Paperless-ngx +(running) and PrivateBin (stopped) were there before and are as found; screenshot folder removed, session files +shredded; 9202 stays on 0.303.0. 9201: language back to Hungarian. demo-hp host: only `pct exec`/`pct list`. Hub: +nothing. demo-felhom: one read-only `pct list` + `docker ps` (only one app there, so not used). + +## Gate 19 decoy (`decoy.txt`) + +Mutation: `website/en/index.html` `dashboard-start-en.webp` → `dashboard-start-hu.webp`. +Failure line: `FAIL: en/index.html: a hu dashboard picture (/assets/dashboard-start-hu.webp) on a en page — use the page's own language`. +Also in `scripts/test_gate_decoys.py` as `site/dashboard-picture-wrong-language` (with `must=` on that line). + +## Rows filed (dashboard defects seen in the pictures; controller not changed) + +- **R-906** (P4): a page can show „+5 more warnings" with none above it — the overflow is counted before the page filter. +- **R-907** (P4): on a phone the Launcher's share button runs off the right edge. + +## Files and pushes + +Named in the chat after the push (commit hash and CI by commit). ## Decision for you -**May I move scratch guest 9202 to controller 0.303.0 — the fleet's version, not past it — and then take the -screenshots?** (On 9202 the image is set by hand in `/etc/felhom-controller-image` + a restart of its bootstrap -service; `operations/nodes.md` — allowed only there.) -- **Pick: yes**, at a time when the other session is not using 9202. Cost: about two minutes of 9202 restart. The - pictures then show what households really run. -- Or: take the pictures on 0.301.0 now. Cost: the pictures show a dashboard two releases old (0.302.0 changed the - format list and 22 messages). -- **If you do nothing:** no pictures; the site stays as it is. +None. Look at the home page once (`felhom.eu/#vezerlopult`, `felhom.eu/en/#dashboard`). diff --git a/STATUS.md b/STATUS.md index d32a5211..58461684 100644 --- a/STATUS.md +++ b/STATUS.md @@ -58,6 +58,7 @@ Full designs: `documentation/audits/day-2026-10-08/`. in the menu goes to the same page in the other language. - **A missing-page (404) page exists now.** - **The language link is now a globe icon**, like the dashboard's: a click shows „Magyar" and „English". +- **The website now shows the dashboard**: four real pictures on the home page (and two on the technology page), Hungarian on Hungarian pages, English on English ones. Two small dashboard defects were filed. - **The contact form's lost program code was searched for everywhere I can reach: not found.** The running program is now saved in git, so it cannot be lost. A plan for a replacement is written. One question for you: is the February folder on your Windows computer? **Needs you:** two choices in `REPORT-website-refresh.md`. If nothing: SparkyFitness stays listed; the contact diff --git a/documentation/audits/website-dashboard-2026-10-08/README.md b/documentation/audits/website-dashboard-2026-10-08/README.md new file mode 100644 index 00000000..1cdecee7 --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/README.md @@ -0,0 +1,53 @@ +# Dashboard pictures for the website — evidence (2026-10-08) + +## The scene, and why it moved + +1. **9202 updated to the fleet's controller** (operator's word, 2026-10-08): `/etc/felhom-controller-image` + `…:0.301.0` → `…:0.303.0` (old line kept as `/etc/felhom-controller-image.bak-0.301.0` in the guest), bootstrap + restarted; `felhom-controller … 0.303.0 Up 13 seconds (healthy)`. +2. **On 9202:** Vaultwarden, Jellyfin, Nextcloud and Immich installed through the product (Immich's first pull was + refused by ghcr.io „toomanyrequests"; the second try deployed). One whole-box backup press and one Tier-2 run: + `db_dump success, count 3`; „Tier 2 run complete: 6 app(s)". +3. **The 9202 pictures were not used.** Every page carried a banner „+ 5 további figyelmeztetés" — nine + `disk-not-separate` warnings (9202's „second drive" is a folder on the host's NVMe, plus earlier sessions' leftover + app folders) and the hub-off warning. True for a scratch guest, not a household's normal state. The banner itself + showed none of the five warnings it counted on those pages: **R-906**. +4. **Pictures taken on demo-hp's household guest 9201** (operator: the physical boxes are test boxes too): controller + 0.303.0, 10 standing apps, hub connected, **no alerts** (`/api/debug/dump` → `alerts: []`), last night's app backup + `success` at 04:16. Read only; the household language was switched to English for the English pictures and back to + Hungarian right after (both POSTs 302; read back `lang="hu"`). Headless Chrome ran on 9202, reaching 9201's traefik + at `192.168.0.155`; nothing ran on 9201. + +## The pictures (`website/assets/`, 1440 × 900, WebP q82) + +| File | Bytes | +|---|---| +| dashboard-start-hu.webp / -en | 34,024 / 31,948 | +| dashboard-apps-hu.webp / -en | 59,018 / 54,156 | +| dashboard-app-hu.webp / -en (Paperless-ngx) | 83,124 / 78,992 | +| dashboard-backups-hu.webp / -en (Backup → Apps) | 52,638 / 45,292 | + +Taken but not published: the backups overview, the System page, the phone view (`captions-claims.md` says why). +The full-size PNGs stay out of the repository. + +## Privacy + +`privacy-scan.txt` — every published picture's page text: 0 e-mails, 0 IPv4 addresses, 0 serials, no domain except the +test domain `enkisfelhom.hu` and `felhom.eu`; the one long hex value is the hidden CSRF field (not visible). Positive +control: an app name known to be on each page, found 8 of 8. By eye: the pictures show the test domain, app names and +backup sizes/times; no person's name (the household is „Demo HP", shown only on the unpublished System page). + +## Clean-up (three layers) + +- **9202:** Vaultwarden, Jellyfin, Nextcloud, Immich removed through the product (stop + remove with drive data and + backups; volumes gone, stack folders back to their template files). Paperless-ngx (running) and PrivateBin + (stopped) — there before this task — left as found; the whole-box backup restarted Paperless and it came back healthy. + `/root/dash` removed; the session cookie file shredded on both sides. The `userdata/{immich,jellyfin,nextcloud}` + folders on the scratch drive hold earlier sessions' content (`kept/`, `backups/`) and were left. **9202 stays on + 0.303.0** (operator-approved). +- **9201:** nothing changed except the language round trip (ends Hungarian, as found). +- **demo-hp host:** only `pct exec 9202/9201 -- …` and `pct list`. **Hub:** nothing touched. + +## Decoy + +`decoy.txt` — gate 19, mutation and failure line. diff --git a/documentation/audits/website-dashboard-2026-10-08/captions-claims.md b/documentation/audits/website-dashboard-2026-10-08/captions-claims.md new file mode 100644 index 00000000..0b7b0767 --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/captions-claims.md @@ -0,0 +1,23 @@ +# Dashboard pictures — every caption is a claim (2026-10-08) + +Pictures: demo-hp's household guest 9201, controller **0.303.0** (the fleet's version), read only — the only change +was the household language switched to English for the English pictures and straight back to Hungarian (both +switches answered 302; the launcher read back `lang="hu"`, first sidebar item „Indítópult"). Taken from scratch guest +9202 with headless Chrome through 9201's traefik (`192.168.0.155`, host `felhom.enkisfelhom.hu`), 1440 × 900. + +| Picture | Caption (hu / en) | Capability-map row | Status | +|---|---|---|---| +| `dashboard-start-*` | „Indítópult — minden alkalmazásod egy kattintásra." / „Launcher — every app you have, one click away." | E „Indítópult (app launcher) — one-tap grid of the household's openable apps" | IMPLEMENTED | +| `dashboard-apps-*` | „Alkalmazások — telepítés pár kattintással, és látod, mi fut és mi naprakész." / „Apps — install in a few clicks, and see what runs and what is up to date." | B „Deploy an app from the catalog" (PROVEN-LIVE); B „What VERSION a box is running, and whether it is behind the catalog" (PROVEN-LIVE) | PROVEN-LIVE | +| `dashboard-app-*` | „Egy alkalmazás oldala — mire jó, hova tedd a fájlokat, és hogyan kezdj hozzá." / „An app's page — what it is for, where your files go, and how to start." | B „App lifecycle: start/stop/restart/update/logs/remove/redeploy" (PROVEN-LIVE); the page's copy is the catalog's (`10-localisation.md` §7, slice 5); the drop-zone link is map E „File access via browser" (IMPLEMENTED, R-75) | PROVEN-LIVE / IMPLEMENTED | +| `dashboard-backups-*` | „Mentések — minden éjjel minden alkalmazás adatbázisa mentésre kerül, és ellenőrizzük." / „Backups — every night each app's database is backed up and checked." | C „Nightly DB dumps (postgres/mariadb autodiscovery), atomic writes" (PROVEN-LIVE); the picture shows the night's run at 04:16 with a table count per database | PROVEN-LIVE | + +Not published (taken, kept off the site): the backups **overview** (on demo-hp it truthfully shows an amber „the whole-box +backup is on the system disk" notice — not a household's normal state), the **System** page (operator details: the +template source and the monitoring address), and the **phone** view (the launcher's share button runs off the right edge +at 390 px — R-907). + +Privacy: `privacy-scan.txt` (per picture: e-mail, IPv4, long hex, the host's serial, password/recovery words, any domain +other than the test domain `enkisfelhom.hu`; positive control an app name known to be on the page — found on all 8). +The only long hex value is the page's hidden CSRF field, which is not visible in the picture. The two password-word hits +are help text („a jelszó a Beállítások oldalon", „password manager"), not a password. diff --git a/documentation/audits/website-dashboard-2026-10-08/decoy.txt b/documentation/audits/website-dashboard-2026-10-08/decoy.txt new file mode 100644 index 00000000..d396f9cc --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/decoy.txt @@ -0,0 +1,3 @@ +mutation: website/en/index.html src="/assets/dashboard-start-en.webp" -> dashboard-start-hu.webp +FAIL: en/index.html: a hu dashboard picture (/assets/dashboard-start-hu.webp) on a en page — use the page's own language +SITE GATES FAILED: 1 problem(s) diff --git a/documentation/audits/website-dashboard-2026-10-08/privacy-scan.txt b/documentation/audits/website-dashboard-2026-10-08/privacy-scan.txt new file mode 100644 index 00000000..a5525d2e --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/privacy-scan.txt @@ -0,0 +1,59 @@ +Privacy scan of the page text behind each published picture (body text + title/alt/placeholder/input values). +Allowed by design: the test domain enkisfelhom.hu (demo-hp's demo customer) and felhom.eu. + +== dashboard-start-hu (positive control 'Paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 0 [] + domain other than the test domain: 1 ['Felhom.eu'] +== dashboard-start-en (positive control 'Paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 0 [] + domain other than the test domain: 1 ['Felhom.eu'] +== dashboard-apps-hu (positive control 'BookStack': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 0 [] + domain other than the test domain: 2 ['Cal.com', 'Felhom.eu'] +== dashboard-apps-en (positive control 'BookStack': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 1 ['password'] + domain other than the test domain: 2 ['Cal.com', 'Felhom.eu'] +== dashboard-app-hu (positive control 'Paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 1 ['jelszó'] + domain other than the test domain: 1 ['Felhom.eu'] +== dashboard-app-en (positive control 'Paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 1 ['password'] + domain other than the test domain: 1 ['Felhom.eu'] +== dashboard-bapps-hu (positive control 'paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 0 [] + domain other than the test domain: 1 ['Felhom.eu'] +== dashboard-bapps-en (positive control 'paperless-ngx': FOUND) + e-mail: 0 [] + IPv4: 0 [] + long hex (token/key): 1 ['hidden CSRF field value, not visible on the page'] + serial 8CN944035T: 0 [] + recovery/password words: 0 [] + domain other than the test domain: 1 ['Felhom.eu'] diff --git a/documentation/audits/website-dashboard-2026-10-08/tools/deploy.py b/documentation/audits/website-dashboard-2026-10-08/tools/deploy.py new file mode 100644 index 00000000..3e49b0b3 --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/tools/deploy.py @@ -0,0 +1,8 @@ +import sys, os, json +sys.path.insert(0, '/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/website-refresh-2026-10-08/shots/tools'); import sw; b = sw.b +b.login() +for app, sub in [("immich", "photos")]: + ok = b.deploy(app, sub, {"HDD_PATH": "/mnt/felhom-drives/scratch_hdd"}) + print(app, "deploy", ok, flush=True) + if ok: + print(app, "answers", b.wait_app(sub), flush=True) diff --git a/documentation/audits/website-dashboard-2026-10-08/tools/lang.py b/documentation/audits/website-dashboard-2026-10-08/tools/lang.py new file mode 100644 index 00000000..1b341243 --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/tools/lang.py @@ -0,0 +1,16 @@ +import sys, os, re +sys.path.insert(0, '/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts'); import box_walk as b +lang = sys.argv[1] +b.login() +sess = open(f"{b.SC}/sess{os.getpid()}.txt").read().strip() +r = b.sh(["curl", "-sk", "-H", b.HOSTHDR, "-H", f"Cookie: {sess}", f"{b.BASE}/launcher"]) +tok = re.search(r'name="_csrf" value="([^"]+)"', r.stdout).group(1) +r = b.sh(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "-H", b.HOSTHDR, "-H", f"Cookie: {sess}", + "-H", f"Origin: https://felhom.{b.DOMAIN}", "-H", f"Referer: https://felhom.{b.DOMAIN}/launcher", + "--data-urlencode", f"_csrf={tok}", "--data-urlencode", f"lang={lang}", "--data-urlencode", "back=/launcher", + f"{b.BASE}/settings/language"]) +print("POST /settings/language", lang, "->", r.stdout) +r = b.sh(["curl", "-sk", "-H", b.HOSTHDR, "-H", f"Cookie: {sess}", f"{b.BASE}/launcher"]) +print("launcher :", re.search(r'([^<]+)<', r.stdout).group(1) if re.search(r'#i-rocket"/>([^<]+)<', r.stdout) else "?") +fd = os.open(f"{b.SC}/session.txt", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600); os.write(fd, sess.encode()); os.close(fd) diff --git a/documentation/audits/website-dashboard-2026-10-08/tools/pages.py b/documentation/audits/website-dashboard-2026-10-08/tools/pages.py new file mode 100644 index 00000000..67497c1d --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/tools/pages.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Dashboard pictures on the website (2026-10-08): a home-page section after „Mit tud a doboz ma?" and two pictures in +the technology page's dashboard card, each page in its own language. Reads the CURRENT pages; every insert asserts +its anchor once.""" +import io, os, re, sys +W = "/mnt/5_hdd/felhom.eu/git/felhom.eu/website" + + +def rd(p): return io.open(os.path.join(W, p), encoding="utf-8-sig").read() + + +def wr(p, s): + with io.open(os.path.join(W, p), "w", encoding="utf-8-sig", newline="\n") as f: f.write(s) + + +def sub1(s, old, new): + assert s.count(old) == 1, (old[:80], s.count(old)); return s.replace(old, new) + + +SHOTS = { # page: (hu alt, hu caption, en alt, en caption) + "start": ("A vezérlőpult Indítópultja: az otthon telepített alkalmazások csempéi", + "Indítópult — minden alkalmazásod egy kattintásra.", + "The Dashboard's Launcher: tiles of the apps installed at home", + "Launcher — every app you have, one click away."), + "apps": ("Az Alkalmazások oldal: telepített és telepíthető alkalmazások kártyái, állapottal", + "Alkalmazások — telepítés pár kattintással, és látod, mi fut és mi naprakész.", + "The Apps page: cards of installed and installable apps, with their status", + "Apps — install in a few clicks, and see what runs and what is up to date."), + "app": ("Egy alkalmazás saját oldala: mire jó, képek, és az első lépések", + "Egy alkalmazás oldala — mire jó, hova tedd a fájlokat, és hogyan kezdj hozzá.", + "An app's own page: what it is for, pictures, and the first steps", + "An app's page — what it is for, where your files go, and how to start."), + "backups": ("A mentések oldala: minden alkalmazás adatbázisának éjszakai mentése, ellenőrizve", + "Mentések — minden éjjel minden alkalmazás adatbázisa mentésre kerül, és ellenőrizzük.", + "The backups page: each app's database backed up overnight, and checked", + "Backups — every night each app's database is backed up and checked."), +} + + +def fig(page, lang): + hu_alt, hu_cap, en_alt, en_cap = SHOTS[page] + alt, cap = (hu_alt, hu_cap) if lang == "hu" else (en_alt, en_cap) + src = "/assets/dashboard-%s-%s.webp" % (page, lang) + return ('
\n' + ' %s\n' + '
%s
\n' + '
') % (src, src, alt, cap) + + +def home_section(lang): + if lang == "hu": + sid, h2, sub = "vezerlopult", "Így néz ki a vezérlőpult", "Valódi képek egy Felhom dobozról — kattints rájuk a teljes mérethez" + else: + sid, h2, sub = "dashboard", "What the Dashboard looks like", "Real pictures from a Felhom box — click one for the full size" + figs = "\n".join(fig(p, lang) for p in ("start", "apps", "app", "backups")) + return ('
\n
\n' + '
\n

%s

\n

%s

\n
\n' + '
\n%s\n
\n
\n
\n\n') % (sid, h2, sub, figs) + + +ANCHOR_HOME = '
\n' +for p, lang in (("index.html", "hu"), ("en/index.html", "en")): + s = rd(p) + i = s.index('id="mit-tud"' if lang == "hu" else 'id="what-it-does"') + j = s.index(ANCHOR_HOME, i) + s = s[:j] + home_section(lang) + s[j:] + wr(p, s) + print(p, "section added") + +TECH_ANCHOR = {"hu": '
\n

Tökéletes:', + "en": '

\n

Good for:'} +for p, lang in (("technologiak.html", "hu"), ("en/technology.html", "en")): + s = rd(p) + block = ('

\n%s\n
\n\n' + % "\n".join(" " + l for f in (fig("apps", lang), fig("backups", lang)) for l in f.split("\n"))) + s = sub1(s, TECH_ANCHOR[lang], block + TECH_ANCHOR[lang]) + wr(p, s) + print(p, "pictures added") diff --git a/documentation/audits/website-dashboard-2026-10-08/tools/shots.js b/documentation/audits/website-dashboard-2026-10-08/tools/shots.js new file mode 100644 index 00000000..e908d44f --- /dev/null +++ b/documentation/audits/website-dashboard-2026-10-08/tools/shots.js @@ -0,0 +1,44 @@ +// Dashboard screenshots for felhom.eu (2026-10-08): demo-hp's household guest 9201 (read only; run from 9202), controller 0.303.0, through traefik with the +// real host name, headless Chrome. The session cookie comes from a 0600 file (never printed); each page's visible text +// is saved beside its picture for the privacy scan. +const puppeteer = require('puppeteer'); +const fs = require('fs'); +const sleep = ms => new Promise(r => setTimeout(r, ms)); +const HOST = 'felhom.enkisfelhom.hu', BASE = 'https://' + HOST; +const sess = fs.readFileSync('/out/session.txt', 'utf8').trim(); // felhom_session=… +const APP = process.env.APP || 'immich'; +const PAGES = (process.env.PAGES ? process.env.PAGES.split(',').map(x => x.split('=')) : [['start', '/launcher'], ['apps', '/stacks'], ['app', '/apps/' + APP], ['backups', '/backups'], ['system', '/settings']]); +async function setLang(p, lang) { + await p.goto(BASE + '/launcher', {waitUntil: 'networkidle2'}); + const btn = await p.$(`button.lang-globe-item[value="${lang}"]`); + if (!btn) throw new Error('no globe item ' + lang); + await p.evaluate(b => b.closest('form').submit(), btn); + await p.waitForNavigation({waitUntil: 'networkidle2'}).catch(() => {}); + const got = await p.$eval('html', e => e.lang); + console.log('language now', got); +} +async function shot(p, name) { + await sleep(1500); + await p.screenshot({path: `/out/${name}.png`}); + const t = await p.evaluate(() => document.body.innerText + '\n' + Array.from(document.querySelectorAll('[title],[alt],[placeholder],input[value]')).map(e => [e.title, e.alt, e.placeholder, e.value].join(' ')).join('\n')); + fs.writeFileSync(`/out/${name}.txt`, t); + console.log('shot', name); +} +(async () => { + const b = await puppeteer.launch({args: ['--no-sandbox', '--ignore-certificate-errors', `--host-resolver-rules=MAP * ${process.env.TRAEFIK}`]}); + const p = await b.newPage(); + const [k, v] = sess.split('='); + await p.setCookie({name: k, value: v, domain: HOST, path: '/', secure: true, httpOnly: true}); + for (const lang of (process.env.LANGS || 'hu,en').split(',')) { + await p.setViewport({width: 1440, height: 900, deviceScaleFactor: 1}); + for (const [name, path] of PAGES) { + await p.goto(BASE + path, {waitUntil: 'networkidle2'}); + await shot(p, `${name}-${lang}`); + } + if (process.env.NOPHONE) continue; + await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2, isMobile: true, hasTouch: true}); + await p.goto(BASE + '/launcher', {waitUntil: 'networkidle2'}); + await shot(p, `phone-${lang}`); + } + await b.close(); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 1dd3dcf4..e41b8e52 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -122,7 +122,7 @@ stopping line that lies. | **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC | | **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator | -## Apps & catalog — 9 rows (P3 3, P4 6) +## Apps & catalog — 10 rows (P3 3, P4 7) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -135,6 +135,7 @@ stopping line that lies. | **R-770** | Apps & catalog | P4 | **[P3-LOW] Invidious — fit check only; the recommendation is not to build it.** READ 2026-10-01: playback needs `invidious-companion` (rolling `latest`, no version tags); PostgreSQL 14 (EOL 2026-11); `registration_enabled: true` by default; upstream: a bot check means „your IP is blocked from YouTube”, a 429 can last 24 h, triggered by „someone on your network” — on our boxes that IP is the household's. One bad period in 2026 (March, ~2 weeks). No report found of a family's other devices being bot-checked (inference). **Needs:** the operator's go / no-go. `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator | | **R-771** | Apps & catalog | P4 | **[P3-LOW] moonlight-web — fit check only; not buildable through an HTTP-only tunnel at usable latency.** READ 2026-10-01: two unrelated projects (MrCreativ3001/moonlight-web-stream, the original; linckosz/moonlight-web); both need Sunshine/Apollo/Wolf on a gaming PC on the LAN and WebRTC over UDP (40000-40100/udp; linckosz recommends host networking and sends telemetry by default); both have a WebSocket fallback (high latency, all video through the tunnel); a logged-in user controls the PC's desktop. **Needs:** the operator's go / no-go (LAN-only use would need a different publishing model). `audits/new-apps-2026-10-01/FIT.md` | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator** **Re-ranked 2026-10-03: P3→P4: a new-app idea waiting on a decision.** | — | — | operator | | **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC | +| **R-907** | Apps & catalog | P4 | **On a phone (390 px) the Launcher's „Indítópult megosztása" / „Share the launcher" button runs off the right edge of the screen.** SEEN 2026-10-08 (website dashboard pictures, controller 0.303.0, demo-hp guest 9201, headless Chrome 390 × 844 @2): the page title, the domain chip and the button sit on one row; the button's text is cut at the right edge (`audits/website-dashboard-2026-10-08/README.md`). Not fixed here (controller out of scope for the website task). | **OPEN — owner: CC** | — | Let the launcher header wrap below ~480 px; re-shoot the phone picture | CC | ## App updates — 4 rows (P3 4) @@ -222,7 +223,7 @@ stopping line that lies. | **R-242** | Box system & updates | P3 | **A controller release that changes customer-visible behaviour is not delivered until a golden carries it — and nothing enforces that.** R-239 is the symptom; this is the mechanism, recorded **2026-08-07** and deliberately **NOT built** (the task that found it scoped it as a record-only item). **Two releases went out without a golden and the gap was invisible until a walk measured it from the customer's side**: v0.204.0 (R-237) and v0.205.0 (R-234) were written, tested, pushed, and CHANGELOG'd, and every one of those steps passed while a machine installed that night received neither. The register said CLOSED; the fleet said otherwise. **Nothing in the release path knows a golden exists.** The version bump, the image push, the CHANGELOG entry and the register closure are all repo-local; the manifest's `golden_version` is edited by a separate operator act, in a different repo, with no link back. **Proposed shapes, cheapest first — the choice is the operator's and is not taken here.** (a) **A release-path checklist step** — one line in the controller's end-of-session checklist: *a release that changes customer-visible behaviour is not finished until a golden carries it or a register row says why not.* Costs nothing, catches nothing mechanically. (b) **A gate in `repo_gates.py`** comparing the manifest's `golden_version` against the newest released controller and FAILING (or warning) past a tolerance of one minor. Mechanical, runs on every push, and would have fired the morning after v0.204.0. (c) **A hub-side checker** — the hub already knows every box's running controller version from `/hosts` and the vouched golden from the manifest; a periodic comparison against the newest published image would catch drift the repo cannot see, including a vouch that was made and then rolled back. **Earliest catch: (b).** It fires on the push that creates the gap, before any box is installed, and it needs no live fleet. **(c) catches strictly more but only after boxes exist.** (a) is worth doing regardless because it is free. **Not built. No gate was written this session.** **⚠ IT RECURRED WITHIN A DAY, WHICH IS THE ARGUMENT FOR BUILDING IT.** Controller **v0.206.0** shipped the R-241 fixes on 2026-08-07 while the vouched golden still carried **0.205.0** — so a machine installed on the morning of 2026-08-08 would have received neither. Third occurrence of the shape in three days (R-111/R-115/R-120 are the older family). **✅ SHAPE (b) BUILT 2026-08-08 — `scripts/golden_currency_gate.py`, registered in `repo_gates.py` as gate 7.** **It was shown FAILING against that exact state before anything was baked**, which is its red-proof and the reason its own introducing push needed `--no-verify` (stated in the session report rather than worked around): `newest released controller : 0.206.0 / newest golden baked : 0.205.0 → CONVICTED`. **IT IS `--fast`, AND THAT FORCED ITS DESIGN:** both `.githooks/pre-push` AND CI run `repo_gates.py --fast`, so a non-fast gate would run in NEITHER — the R-29 census failure this runner exists to end. **THEREFORE IT CHECKS THE BAKE, NOT THE VOUCH**, because the vouched version lives only in the hub's `hub_settings` with no copy in git, and putting a copy there would create a second source of truth that can drift — a green gate over a false claim being the worst outcome available. **A bake without a vouch still passes: that half is NOT closed and stays on this row.** It also compares versions rather than behaviour, so a release changing nothing customer-visible trips it too — accepted deliberately, because judging that by hand is what failed three times and the cost of a false trip is one bake; a waiver belongs here, never in a habit of bypassing. **VOUCHED 2026-08-08 with the operator's approval** — golden `0.206.0` / sha `c85230b4…108e`; `agent_version` and `min_agent` both stayed `0.127.0`, and `wrapper_sha256` was carried through explicitly because the handler clears it when omitted. **The gate was CONVICTED before the bake and OK after it** — red→green on the same command, which is its proof that it measures something real. **⚠ THE GATE FIRED FOR REAL, 2026-08-08 — and it was right.** Controller **v0.207.0** (R-249/R-252/R-253) is released, tested and pushed, and **no golden carries it** — the newest bake is 0.206.0 — so `golden_currency_gate.py` FAILED, saying exactly the true thing: *a machine installed right now would receive v0.206.0*. **The `felhom.eu` push therefore used `git push --no-verify`, declared here, in the commit message and in the session report.** A bypass and NOT a waiver, deliberately: the gate offers a waiver only for a release that *deliberately needs no golden*, and this one needs one. **Owed: bake golden 0.207.0 and vouch it** (`RUNBOOK-manual-build.md` §4.1; the vouch is a three-field change). **This row's own remaining half is unchanged — nothing gates the VOUCH itself.** **✅ THE OWED BAKE IS DONE, SAME DAY — golden 0.207.0 baked, published, round-trip verified and VOUCHED (2026-08-08).** The gate went from red to **green**, and the `--no-verify` bypass declared above is now historical rather than standing. **Round trip is the evidence, not the build log:** the published bytes were downloaded back — 656 879 192 B, sha256 `20ec9602…22995`, both identical to what the bake reported — and **`./etc/felhom-controller-image` read OUT of the downloaded archive says `felhom-controller:0.207.0`**, which is the delivered artifact naming the controller it will start. **The vouch was a three-field change with all three checked deliberately** (`MinAgent 0.127.0` read from the golden's controller CHANGELOG header, not assumed; `agent_version` already ≥ it; `min_agent` not above `agent_version`, so not the R-216 shape) and verified by **re-reading the manifest rather than trusting the flash**. **This row's remaining half is UNCHANGED and is the whole of what is still open: nothing gates the VOUCH itself** — the currency gate's own docstring says it checks the bake, so a baked-but-unvouched golden still passes it silently. Evidence: `tests/golden-0.207.0-2026-08-08/`. **⚠ RED AGAIN, 2026-08-08 (second time in two days) — controller v0.208.0 (R-254) is released and the vouched golden is 0.207.0.** `golden_currency_gate.py` FAILS, correctly: a machine installed right now receives 0.207.0 and none of today's fixes. **The `felhom.eu` push used `git push --no-verify`, declared in the commit, the CHANGELOG and the session report** — **a bypass, not a waiver**, on the same reasoning as yesterday: the gate offers a waiver only for a release that *deliberately needs no golden*, and this one needs one. **Owed: bake golden 0.208.0 and vouch it** (`RUNBOOK-manual-build.md` §4.1; three-field change, `MinAgent 0.127.0` unchanged). **Note the cadence this is establishing: two releases, two bakes owed within 24 h.** That is the argument for this row's OTHER half — nothing gates the vouch, so the only thing standing between a release and an undelivered fleet is somebody remembering. **⚠ RED AGAIN, 2026-08-30 — controller v0.224.0 (R-330) and v0.225.0 (R-331) are released and the newest golden carries 0.223.0.** `golden_currency_gate.py` FAILS, correctly: a machine installed right now receives 0.223.0 and neither of today's fixes. **The `felhom.eu` push used `git push --no-verify`, declared in the commit message, in `hub/CHANGELOG.md` and in `REPORT.md` — a BYPASS, not a waiver**, on the same reasoning as the two 2026-08-08 entries above: the gate offers a waiver only for a release that *deliberately needs no golden*, and these need one. **The operator was asked and ruled bypass-now-bake-later on 2026-08-30**, on the stated ground that neither fix bites a DAY-0 box — R-330 is a nightly false alarm about apps a new box has not installed yet, and R-331 is a hub-side display over backups a new box has not taken yet — and both arrive by self-update afterwards. **That ground is recorded because it is the thing to re-check, not a general licence: the next release that changes first-boot behaviour cannot reuse it.** **OWED: bake a golden carrying 0.225.0 and vouch it** (`RUNBOOK-manual-build.md` §4.1; three-field change — `golden_version` + `agent_version` + `min_agent`; MinAgent is 0.129.0 per both CHANGELOG headers). **Cadence note, unchanged and now worse: this is the fourth bypass of this gate, and the gap it names is now two releases wide rather than one.** **⚠ WIDENED TO THREE THE SAME DAY — v0.226.0 (R-353/R-357/R-358/R-360) shipped 2026-08-30 and the golden still carries 0.223.0.** The `felhom.eu` push carrying that release's documentation used `git push --no-verify` on the operator's standing ruling from earlier the same day, declared in the commit and in `REPORT.md`. **The day-0 ground still holds for all three and was re-checked rather than assumed:** R-330 alarms about apps a new box has not installed; R-331 is a hub display over backups a new box has not taken; **R-353/357/358/360 are restore-surface fixes, and a day-0 box has nothing to restore.** **The ground expires the moment a release changes first-boot behaviour — that is the thing to re-check, not a licence.** **Owed: ONE bake carrying 0.226.0 covers all three** (`RUNBOOK-manual-build.md` §4.1; three-field vouch, MinAgent 0.129.0), then raise the floor. **✅ PAID THE SAME DAY — golden `0.226.1` baked, published, round-trip verified, VOUCHED, and the floor RAISED (2026-08-30).** Evidence: `documentation/tests/golden-0.226.1-2026-08-30/`. `golden_currency_gate.py` went **red → green** on the same command, which is its proof that it measures something real. **The three declared bypasses above are now HISTORICAL rather than standing.** **The round trip is the evidence, not the build log:** the published bytes were downloaded back — **657 197 592 B, sha256 `70ed8e93…baefe69`**, both identical to what the bake reported — and `./etc/felhom-controller-image` read **out of the downloaded archive** says `felhom-controller:0.226.1`, i.e. the delivered artifact naming the controller it will start. **The three-field vouch was checked deliberately, not assumed:** `MinAgent 0.129.0` read from the golden's controller CHANGELOG header, `agent_version 0.130.0 ≥ min_agent 0.129.0` (so NOT the R-216 shape), and the result verified by **re-reading the manifest** rather than trusting the flash — golden option `0.226.1 SELECTED`, all four shas matching. **The floor is proven ACTING, not merely set:** `demo-felhom` self-updated within 30 s, logging `[selfupdate] Post-update startup: update successful (0.225.0 → 0.226.1)`. **⚠ AND IT HAPPENED AGAIN THE SAME DAY, AND WAS PAID AGAIN.** v0.227.0/v0.227.1 (R-359/R-397) shipped after the 0.226.1 bake, the gate convicted a fifth time, that `felhom.eu` push used `--no-verify` and declared it, and golden **0.227.1** was baked, published, round-trip verified, **VOUCHED** and the floor **RAISED to 0.227.1** within the hour. Evidence: `documentation/tests/golden-0.227.1-2026-08-30/`. **THE CADENCE IS NOW MEASURED RATHER THAN ASSERTED: five convictions and two full bakes in one day.** Every bypass was declared and every debt was paid — but the pattern this row exists to name is exactly that a release and its delivery are separate acts, performed hours apart, by whoever remembers. **The floor was proven ACTING both times:** `demo-felhom` self-updated 0.225.0→0.226.1, then 0.226.1→0.227.1 — and the second time it also registered the new `offsite-integrity` job **by itself, on a box nobody deployed to**, which is the strongest evidence this row has ever carried that a floor delivers rather than merely records. **This row's OTHER half is still open and untouched: nothing gates the VOUCH itself** — the currency gate's own docstring says it checks the bake, so a baked-but-unvouched golden still passes it silently. **2026-08-31, the SEVENTH debt and it was paid the same day — twice in one day.** v0.230.0 shipped in the morning with the newest golden at 0.229.0, **which is the build R-403 says deletes a good copy**, so the gate was red across four commits (`dddcc80`, `6e550ae`, `130f7a6`, `32a4c35`). Golden **0.230.0** baked, published, round-trip verified, vouched, and the fleet floor raised 0.229.0 → 0.230.0; `demo-felhom` moved itself off the defective build unattended (`controller-swap: new controller healthy`, 16:21:40 CEST). Evidence: `documentation/tests/golden-0.230.0-2026-08-31/`. **The gate did its job and its own weakness surfaced doing it — R-410.** | **READY — the vouch half only** — owner Viktor **⚠ SIXTH CONVICTION, 2026-08-31 — controller v0.229.0 (R-102/R-103) is released and the newest golden carries 0.228.0.** `golden_currency_gate.py` FAILS, correctly: a machine installed right now receives 0.228.0 and neither of today's fixes. The `felhom.eu` push carrying this release's documentation used `git push --no-verify`, declared in the commit message and in `felhom-controller/REPORT.md` - **a BYPASS, not a waiver**, on the same reasoning as the five entries above: the gate offers a waiver only for a release that *deliberately needs no golden*, and this one needs one. **The day-0 ground was RE-CHECKED rather than reused:** R-102 and R-103 are restore-surface changes on the Tier-2 card, and a day-0 box has taken no Tier-2 copy and has nothing to restore from one; no first-boot behaviour changed, and `MinAgent` is unchanged at 0.129.0. **The ground still expires the moment a release changes first-boot behaviour.** **OWED: bake a golden carrying 0.229.0 and vouch it** (`RUNBOOK-manual-build.md` §4.1; three-field change - `golden_version` + `agent_version` + `min_agent`, MinAgent 0.129.0), then raise the floor. Fleet floor and golden are 0.228.0 today. **Golden and fleet delivery are the operator's (this row).** **✅ PAID THE SAME DAY — golden `0.229.0` baked, published, round-trip verified, VOUCHED, and the floor RAISED (2026-08-31).** Evidence: `documentation/tests/golden-0.229.0-2026-08-31/`. `golden_currency_gate.py` went **red to green** on the same command, which is its proof that it measures something real. **The `--no-verify` bypass declared above is now HISTORICAL rather than standing.** **The round trip is the evidence, not the build log:** the published bytes were downloaded back - **656 864 331 B, sha256 `39aa886d…d7bdae87`**, both identical to what the bake reported - and `./etc/felhom-controller-image` read **out of the downloaded archive** says `felhom-controller:0.229.0`. **A THIRD independent reader agreed before anything was vouched:** the hub's own Day-0 dropdown read the same sha straight from Gitea, a different code path from the round trip. **The three-field vouch was checked deliberately, not assumed** (`MinAgent 0.129.0` read from the golden's controller CHANGELOG header; `agent_version 0.130.0` >= `min_agent 0.129.0`, so NOT the R-216 shape; `agent_sha256` and `wrapper_sha256` carried through explicitly because the handler clears a field it is not sent), and verified by **re-reading the manifest** rather than trusting the flash. The **R-120 gate passed rather than being bypassed** - fleet newest 0.229.0, golden 0.229.0. **The floor is proven ACTING:** `demo-felhom` self-updated `0.228.0 -> 0.229.0` and logged `settle-gate: GO - at/above floor 0.229.0 (we are 0.229.0)` - **nobody deployed to that box.** **Cadence note: this is the SECOND bake in one day (0.228.0 then 0.229.0) and the sixth conviction, and both debts were paid within the hour.** This row's OTHER half is still open and untouched: **nothing gates the VOUCH itself** - the currency gate checks the bake, so a baked-but-unvouched golden still passes it silently. **⚠ SEVENTH CONVICTION, 2026-08-31 — controller v0.230.0 (R-403) is released and the newest golden carries 0.229.0. AND THIS ONE IS NOT LIKE THE OTHERS: the day-0 ground does NOT apply and must not be reused.** Every previous bypass rested on 'a day-0 box has nothing to restore / nothing to alarm about yet'. R-403 is a defect in the NIGHTLY TIER-2 COPY, which a day-0 box starts running on its first night: a machine installed on 0.229.0 can have a complete recovery package on its second drive replaced by an empty one, and that is measured, not suspected (120 082 104 B -> 7 036 B on demo-hp). **The row's own standing sentence - 'the ground expires the moment a release changes first-boot behaviour' - is what expires it here.** The `felhom.eu` push carrying this release's documentation used `git push --no-verify`, declared in the commit message and in `felhom-controller/REPORT.md` - a BYPASS, not a waiver. **OWED, and more urgent than the previous six: bake a golden carrying 0.230.0, vouch it (three fields, MinAgent 0.129.0 unchanged), and raise the floor.** `demo-hp` was updated by hand; `demo-felhom` is still on 0.229.0 and still carries the defect. **See also R-404, filed today: this is the seventh bypass and the habit is now the thing being reported.** **2026-09-01 (R-404): THE BAKE HALF IS UNCHANGED AND THE VOUCH HALF IS STILL OPEN.** R-404 moved WHO the bake check refuses and added a notice in the controller repo; it did NOT touch what is checked. **Nothing gates the VOUCH.** A baked-but-unvouched golden still passes both the gate and the new notice, and the reason is unchanged and forced: the vouched version lives only in the hub's `hub_settings` table, there is no copy in git, and a hub-reading gate could not be `--fast` so it would run in neither the hook nor CI. **Do not read R-404's closure as closing this.** **2026-09-13 — NARROWED: the WAIVER half is BUILT (R-468).** The docstring's *"honest fix is a recorded waiver in the register, never a habit of bypassing"* is now a mechanism: `golden_currency_gate.py` reads `documentation/tests/golden-waiver.yml` (dated, ≤ 14 days, row-bound), turns a BEHIND conviction into a loud advisory while valid, and is red again when it expires — the difference from this row's original rule, which recurred the next day, is that a dated waiver cannot be forgotten. It never covers an UNRECORDED golden (R-385). Operator ruling the same day: goldens weekly and before any install, not per release. **What stays open on THIS row is exactly one thing: nothing gates the VOUCH.** The waiver does not touch it, and the reason it is unbuilt is unchanged (the vouched version lives only in the hub). | — | — | operator | | **R-468** | Box system & updates | P3 | **[P3-LOW] THE GOLDEN WAIVER — goldens on a cadence, not per release (operator ruling 2026-09-13).** 25 goldens in 26 days in August, almost one per release, because `golden_currency_gate.py` trips on every release by design and the only honest ways past it were a bake or a declared `--no-verify` (thirteen by 2026-09-01, R-404/R-417). **The ruling: bake WEEKLY, and always before any drill or fresh install.** Every release still raises the FLOOR, so both demo boxes keep getting each release in ~20 s; only the golden — which protects a fresh install and nothing else — moves to a cadence. **The mechanism (built 2026-09-13):** `documentation/tests/golden-waiver. **⚠ CORRECTED THE SAME DAY (R-472): between bakes the floor does NOT carry a release — the hub holds any floor above the vouched golden (publish-train rule 1), so releases between bakes reach the demo boxes only by hand-deploy.**yml`, four lines (`issued`, `expires`, `reason`, `register_row: R-468`), read by the gate. While valid, a golden BEHIND the record makes the gate print a loud ADVISORY and exit 0; when it expires the gate is red again until someone bakes or renews. **The 14-day cap is enforced by the gate, not the runbook** — a longer, undated, unparseable, reason-less or row-less waiver is INCONCLUSIVE (exit 2), never 0 and never silently ignored. **It never covers a golden that is UNRECORDED (R-385)** — that is not a cadence choice. **A dated waiver cannot be forgotten; it just expires** — the difference from R-242's original rule, which recurred the day after it was written. Tests: `scripts/test_golden_currency_gate.py` cases 5–15 (E/F/G/H, a 15-day, absent, unparseable, bad-row and empty-reason waiver each 2; the R-421 decoy — a file saying only `expires` — 2). **This is a PRE-CUSTOMER arrangement: the first external install retires it** (delete the file in that commit). Cadence written into `RUNBOOK-manual-build.md` §4.2 and the `felhom.eu` end-of-session checklist. **Does NOT touch R-242's open half (nothing gates the VOUCH).** | **WATCHING — rank P3-LOW; owner: CC (renew ≤ 14 days or bake); retire at the first external install** | — | — | CC | -## Monitoring & notifications — 15 rows (P2 2, P3 9, P4 4) +## Monitoring & notifications — 16 rows (P2 2, P3 9, P4 5) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -240,6 +241,7 @@ stopping line that lies. | **R-285** | Monitoring & notifications | P4 | **A planned, supervised reinstall pages the operator as if the machine had died — there is no notion of expected downtime anywhere.** During the 2026-08-09 rehearsal the hub sent, all `status: sent` to the operator channel: `host_stale` 08:58 UTC, `node_stale` 09:00, **`host_down` 09:28 (error)**, **`node_down` 09:30 (error)**, `host_leaf_changed` 09:31, `host_recovered` 09:31, `node_recovered` 09:34, `offsite_delivery_stuck` 09:34 — eight operator mails for work that was deliberate, attended and announced. **This is the OPPOSITE gap from the one R-281 filed:** the alarms are not missing, they are indiscriminate. `host_stale` at 30 min and `host_down` at 60 min (`monitor/host_staleness.go:22-23`, `downAfter = 2 * threshold`) cannot distinguish a wiped-on-purpose box from a dead one, and `host_leaf_changed` firing on a reinstall is correct-but-expected. **Note the interaction with the mute used on 2026-08-09 evening:** blocking a customer silences everything, so today the only two settings are *page me for planned work* and *tell me nothing at all*. **What is owed is a middle:** a maintenance window, or an operator-set expected-downtime flag, that suppresses staleness and leaf-change while leaving genuine faults audible | **READY (M) — NEW 2026-08-09** | — | The evidence is the operator's mailbox plus `events`/`notification_log` for 2026-08-09 | CC | | **R-886** | Monitoring & notifications | P3 | **DooPlex's Alertmanager cannot write its own state since the Longhorn restart of 2026-10-05 13:20Z** — every 15 min `Running maintenance failed … open /alertmanager/nflog.…: permission denied` (and the same for `silences`), 8 times by 14:21Z; the pod was recreated 13:20:48Z by that restart. Mail still goes out (`alertmanager_notifications_total{integration="email"}` 5 → 6, `failed_total` 0, 14:23Z), but a silence set now and the record of what was already sent do not survive the next pod restart — so a restart can re-send every active alarm or drop a silence. Likely collateral of the restart (volume ownership on re-attach), not measured. **Checked from source 2026-10-05 (burn-down round 2):** homelab-manifests@87dfc29 mon-system/alertmanager.yaml:137-247: the Deployment has NO securityContext / fsGroup / runAsUser at all (grep), runs prom/alertmanager:v0.34.1 (:199, non-root `nobody` image) with --storage.path=/alertmanager on the Longhorn PVC alertmanager-data (:202, :212-213, :245-247). The comment :239-244 asserts silences now survive a restart -- an invariant with no test, which is exactly what this row says broke. Last structural change 58d1cd2 (2026-08-14, 'give alertmanager re | **OPEN** | — | Compare the volume's file owner with the pod's `securityContext` (`fsGroup`/`runAsUser`); fix in homelab-manifests; prove with a silence that survives a pod restart | operator | | **R-884** | Monitoring & notifications | P4 | **ArgoCD app `monitoring` shows `Deployment/prometheus` OutOfSync** (seen 2026-10-05 while syncing the R-173 alarm rules; only the rules ConfigMap was synced, so the Deployment drift is untouched and its cause unknown). A full sync would change the running Prometheus in an unknown way. **Checked from source 2026-10-05 (burn-down round 2):** Strong lead from source: homelab-manifests@87dfc29 commit 53c6e99 (Renovate, 2026-10-03) changed ONLY mon-system/monitoring.yaml `prom/prometheus:v3.14.0` -> `v3.15.0` (monitoring.yaml:419), and the `monitoring` Application has no `automated` syncPolicy in git (argocd-apps/homelab.yaml:602-605). So the drift is most likely an unsynced Renovate bump, i.e. a full sync = Prometheus 3.14 -> 3.15 upgrade (plus pod restart; R-211: no reloader). Not confirmed live. | **OPEN** | — | `argocd app diff monitoring` (or the CR's resource diff) to see what differs, then decide git or live | operator | +| **R-906** | Monitoring & notifications | P4 | **A page can show „+ 5 további figyelmeztetés" (+5 more warnings) with no warning above it.** SEEN 2026-10-08 on scratch 9202 (controller 0.303.0): `GetAlerts` caps the list at 5 and counts the rest into the overflow line BEFORE the layout drops the alerts that belong on other pages (`disk-not-separate` is `Inline` + `PageOnly` dashboard/monitoring, `web/alerts.go` ~L281); on the launcher, apps, backups and system pages all five visible ones were such alerts, so only the overflow line rendered. Not fixed here (controller out of scope). | **OPEN — owner: CC** | — | Count the overflow after the page filter (a render test per page) | CC | ## Hub & operator — 12 rows (P2 1, P3 5, P4 6) diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 6183cadd..3ca9d00b 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,3 +1,9 @@ +## gates — site gate 19: dashboard pictures in the page's own language (2026-10-08) + +- `site_gates.py` gate 19: every `/assets/dashboard-*-(hu|en).webp` on a page must match the page's language set. + Decoy `site/dashboard-picture-wrong-language` (the English home page given `dashboard-start-hu.webp`) convicts with + „en/index.html: a hu dashboard picture … on a en page". + ## gates — the decoy suite runs one at a time (2026-10-08, fixed without a row) - `test_gate_decoys.py` takes an exclusive `fcntl.flock` on `.git/decoy-suite.lock` before planting anything. Two runs at diff --git a/scripts/site_gates.py b/scripts/site_gates.py index c835d62c..4932e69f 100644 --- a/scripts/site_gates.py +++ b/scripts/site_gates.py @@ -28,6 +28,7 @@ Gates (all must pass; non-zero exit on any failure): retrieval promise beyond what the Hungarian twin makes 17. faq-ld — each FAQ page's FAQPage JSON-LD carries exactly its visible questions and answers 18. tail — nothing after (a stray file-dump summary sat visible below technologiak.html) + 19. dash-lang — a page shows dashboard pictures only in its own language (`dashboard-*-hu.webp` / `-en.webp`) """ import html as _html, io, json, os, re, sys, unicodedata @@ -439,8 +440,20 @@ for p, s in pages.items(): if i < 0 or s[i + len(""):].strip(): fail("%s: text after (%r) — it renders on the page" % (p, s[i + 7:i + 87] if i >= 0 else "no ")) +# gate 19: dashboard pictures in the page's own language (2026-10-08). The dashboard speaks both languages and the +# site shows it in each: an English page may not carry a `dashboard-*-hu.webp`, a Hungarian page a `-en.webp`. +_DASH_RE = re.compile(r"/assets/dashboard-[a-z0-9-]+?-(hu|en)\.webp") +_dash_n = 0 +for p, s in pages.items(): + want = lang_of(p) + for m in _DASH_RE.finditer(s): + _dash_n += 1 + if m.group(1) != want: + fail("%s: a %s dashboard picture (%s) on a %s page — use the page's own language" % (p, m.group(1), m.group(0), want)) +print(" dashboard pictures: %d references, each in its page's language" % _dash_n) + if fails: print("\nSITE GATES FAILED: %d problem(s)" % len(fails)) sys.exit(1) print("site gates OK — BOM, emoji=0, nav/footer per language, analytics, no CDN, no legacy tokens, no