website: the dashboard shown — 4 real screenshots per language on the home page, 2 on the technology page (controller 0.303.0, demo-hp household guest, privacy-scanned); site gate 19 (pictures in the page's language) + decoy; R-906, R-907 (dashboard defects seen); 129 -> 131
gates / gates (push) Successful in 4m12s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-08 12:47:48 +02:00
parent 4b5f74e46c
commit aaac24b05c
41 changed files with 500 additions and 46 deletions
@@ -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.
@@ -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.
@@ -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)
@@ -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']
@@ -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)
@@ -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 <html lang>:", re.search(r'<html lang="([^"]+)"', r.stdout).group(1) if re.search(r'<html lang="([^"]+)"', r.stdout) else "?",
"| sidebar first item:", re.search(r'#i-rocket"/></svg>([^<]+)<', r.stdout).group(1) if re.search(r'#i-rocket"/></svg>([^<]+)<', 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)
@@ -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",
"<strong>Indítópult</strong> — minden alkalmazásod egy kattintásra.",
"The Dashboard's Launcher: tiles of the apps installed at home",
"<strong>Launcher</strong> — every app you have, one click away."),
"apps": ("Az Alkalmazások oldal: telepített és telepíthető alkalmazások kártyái, állapottal",
"<strong>Alkalmazások</strong> — 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",
"<strong>Apps</strong> — 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",
"<strong>Egy alkalmazás oldala</strong> — 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",
"<strong>An app's page</strong> — 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",
"<strong>Mentések</strong> — 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",
"<strong>Backups</strong> — 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 (' <figure class="dash-shot">\n'
' <a href="%s"><img src="%s" alt="%s" width="1440" height="900" loading="lazy"></a>\n'
' <figcaption>%s</figcaption>\n'
' </figure>') % (src, src, alt, cap)
def home_section(lang):
if lang == "hu":
sid, h2, sub = "vezerlopult", "Így néz ki <span>a vezérlőpult</span>", "Valódi képek egy Felhom dobozról — kattints rájuk a teljes mérethez"
else:
sid, h2, sub = "dashboard", "What the <span>Dashboard looks like</span>", "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 (' <section class="content-section" id="%s">\n <div class="container">\n'
' <div class="section-header">\n <h2>%s</h2>\n <p>%s</p>\n </div>\n'
' <div class="dash-grid">\n%s\n </div>\n </div>\n </section>\n\n') % (sid, h2, sub, figs)
ANCHOR_HOME = ' <section class="content-section alt-bg">\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": ' <div class="highlight-box">\n <p><strong>Tökéletes:</strong>',
"en": ' <div class="highlight-box">\n <p><strong>Good for:</strong>'}
for p, lang in (("technologiak.html", "hu"), ("en/technology.html", "en")):
s = rd(p)
block = (' <div class="dash-grid">\n%s\n </div>\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")
@@ -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); });
File diff suppressed because one or more lines are too long