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
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:
@@ -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
Reference in New Issue
Block a user