Files
felhom.eu/documentation/audits/website-refresh-2026-10-08/shots/tools/homebox.js
T
admin 67b3fd23d1
gates / gates (push) Failing after 13m1s
website refresh: true numbers (56 apps), claims checked against the capability map, ten missing apps, an English twin of every public page (public, choice B), 404 page; site gates 13-18 + 11 decoys; R-902 opened
Part A: claim table documentation/audits/website-refresh-2026-10-08/claims.md; "100% open source" corrected
from the licence read; unbacked claims cut (firewall, RAID, snapshots, new-machine restore, self-managed
mode, household VPN, "never lost"). Part B: dawarich, docmost, grimmory, homebox, karakeep, mealie, metube,
radicale, recipe-importer, sparkyfitness cards; plant-it and wger cut (not offered). Part C: /en/ twins,
nav language switch, hreflang both ways, sitemap with xhtml:link, og-image-en.png. Part D: site_gates.py
twins/lang/same-apps/no-Hungarian/FAQ-JSON-LD/tail. Operator ruling 9 + choice B in 10-localisation.md §11,
§10.7. Register: R-902 (contact mailer source in no repo); R-813, R-784, R-559 annotated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-08 08:29:17 +02:00

126 lines
8.1 KiB
JavaScript

// Homebox screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own traefik
// (every name mapped to it), passing the setup gate as the household (the dashboard session vouches). An invented
// demo account and a few invented things are created through Homebox's own API in the page, then its own UI is shot.
const puppeteer = require('puppeteer');
const fs = require('fs');
const sleep = ms => new Promise(r => setTimeout(r, ms));
const S = JSON.parse(fs.readFileSync('/out/secrets.json'));
const D = process.env.DOMAIN, URL = `https://${process.env.SUB}.${D}`;
(async () => {
const b = await puppeteer.launch({args: ['--no-sandbox', '--ignore-certificate-errors', '--lang=hu-HU',
'--host-resolver-rules=MAP * ' + process.env.TRAEFIK]});
const p = await b.newPage();
await p.setCookie({name: 'felhom_session', value: S.session, domain: `felhom.${D}`, path: '/', secure: true});
await p.setViewport({width: 1280, height: 800});
await p.goto(URL + '/', {waitUntil: 'networkidle2'});
await sleep(1500);
console.log('landed', p.url().replace(/\?.*/, ''));
const r = await p.evaluate(async (S) => {
const j = (m, u, body, tok) => fetch(u, {method: m, headers: Object.assign({'Content-Type': 'application/json'},
tok ? {Authorization: tok} : {}), body: body ? JSON.stringify(body) : undefined}).then(async x => [x.status, await x.text()]);
const out = [];
if (S.skipseed) return out;
const jp = t => { try { return JSON.parse(t); } catch (e) { return {}; } };
out.push(['register', ...(await j('POST', '/api/v1/users/register', {name: S.name, email: S.user, password: S.pw})).map(x => String(x).slice(0, 80))]);
const [lc, lb] = await j('POST', '/api/v1/users/login', {username: S.user, password: S.pw, stayLoggedIn: true});
out.push(['login', lc]);
let tok;
try { tok = JSON.parse(lb).token; } catch (e) { return out.concat([['loginbody', lb.slice(0, 80)]]); }
const [tc, tt] = await j('GET', '/api/v1/entity-types', null, tok);
const types = jp(tt); out.push(['types', tc, tt.slice(0, 300)]);
const tl = (Array.isArray(types) ? types : (types.items || [])).find(x => x.isLocation) || {};
const ti = (Array.isArray(types) ? types : (types.items || [])).find(x => !x.isLocation) || {};
const loc = {};
for (const [n, d] of [['Nappali', 'Földszint'], ['Konyha', 'Földszint'], ['Garázs', 'Udvar'], ['Dolgozószoba', 'Emelet']]) {
const [c, t] = await j('POST', '/api/v1/entities', {name: n, description: d, entityTypeId: tl.id}, tok); loc[n] = jp(t).id; out.push(['loc', c, c >= 300 ? t.slice(0, 80) : '']);
}
const lab = {};
for (const [n, col] of [['Elektronika', '#3b82f6'], ['Garanciális', '#16a34a'], ['Szerszám', '#ea580c']]) {
const [c, t] = await j('POST', '/api/v1/tags', {name: n, color: col}, tok); lab[n] = jp(t).id; out.push(['label', c, c >= 300 ? t.slice(0, 80) : '']);
}
const items = [
['Mosogatógép', 'Beépített, 60 cm', 'Konyha', ['Elektronika', 'Garanciális']],
['Televízió 55"', 'Nappali fali konzolon', 'Nappali', ['Elektronika', 'Garanciális']],
['Akkus fúró-csavarozó', '18 V, két akkumulátorral', 'Garázs', ['Szerszám']],
['Létra, 3 fokos', 'Alumínium', 'Garázs', ['Szerszám']],
['Laptop', 'Munkához', 'Dolgozószoba', ['Elektronika']],
['Kávéfőző', 'Kapszulás', 'Konyha', ['Elektronika']],
['Porszívó', 'Vezeték nélküli', 'Nappali', ['Elektronika', 'Garanciális']],
['Fűnyíró', 'Elektromos', 'Garázs', ['Szerszám', 'Garanciális']],
];
for (const [n, d, l, ls] of items) {
const [c, t] = await j('POST', '/api/v1/entities', {name: n, description: d, entityTypeId: ti.id, parentId: loc[l], tagIds: ls.map(x => lab[x])}, tok);
out.push(['item', c, c >= 300 ? t.slice(0, 120) : '']);
}
return out;
}, S);
console.log(JSON.stringify(r));
await p.goto(URL + '/', {waitUntil: 'networkidle2'});
await sleep(1500);
await p.screenshot({path: '/out/login.png'});
if (process.env.STOP) { await b.close(); return; }
if (await p.$('input[type=password]')) { // the seed's API sign-in normally leaves the browser signed in already
const email = await p.$('input[type=email], input[name=email], input[type=text]');
if (email) await email.type(S.user);
await p.type('input[type=password]', S.pw);
await p.keyboard.press('Enter');
await p.waitForNavigation({waitUntil: 'networkidle2', timeout: 30000}).catch(() => {});
}
await sleep(2000);
// Homebox's own language preference (its Profile page writes it to localStorage): Hungarian
console.log('prefs before', await p.evaluate(() => JSON.stringify(Object.keys(localStorage))));
await p.evaluate(() => {
const k = 'homebox/preferences/location';
let v = {}; try { v = JSON.parse(localStorage.getItem(k) || '{}'); } catch (e) {}
v.language = 'hu'; localStorage.setItem(k, JSON.stringify(v));
});
if (S.cleanup) {
// Homebox's own default English locations and tags (empty) removed so the demo reads in one language; the
// collection's currency set to forint; invented prices and quantities on the invented things.
const r = await p.evaluate(async () => {
const out = [];
const J = async (m, u, b) => { const x = await fetch(u, {method: m, headers: {'Content-Type': 'application/json'}, body: b ? JSON.stringify(b) : undefined}); return [x.status, await x.text()]; };
const mine = ['Nappali', 'Konyha', 'Garázs', 'Dolgozószoba', 'Elektronika', 'Garanciális', 'Szerszám'];
const tree = JSON.parse((await J('GET', '/api/v1/entities/tree'))[1]);
for (const n of tree) if (!mine.includes(n.name) && !(n.children || []).length) out.push(['del loc ' + n.name, (await J('DELETE', '/api/v1/entities/' + n.id))[0]]);
const tags = JSON.parse((await J('GET', '/api/v1/tags'))[1]);
for (const t of (Array.isArray(tags) ? tags : tags.items || [])) if (!mine.includes(t.name)) out.push(['del tag ' + t.name, (await J('DELETE', '/api/v1/tags/' + t.id))[0]]);
const g = JSON.parse((await J('GET', '/api/v1/groups'))[1]);
out.push(['currency', (await J('PUT', '/api/v1/groups', {name: g.name, currency: 'HUF'}))[0]]);
const price = {'Mosogatógép': 189990, 'Televízió 55"': 249990, 'Akkus fúró-csavarozó': 42990, 'Létra, 3 fokos': 12990,
'Laptop': 329990, 'Kávéfőző': 34990, 'Porszívó': 89990, 'Fűnyíró': 64990};
const list = JSON.parse((await J('GET', '/api/v1/entities?pageSize=100'))[1]);
for (const it of (list.items || [])) {
if (!(it.name in price)) continue;
const [c, full] = await J('GET', '/api/v1/entities/' + it.id);
const o = JSON.parse(full);
o.purchasePrice = price[it.name]; o.quantity = 1; o.insured = /Televízió|Laptop|Mosogatógép/.test(it.name);
o.tagIds = (o.tags || []).map(t => t.id); o.parentId = o.parent ? o.parent.id : o.parentId;
const [pc, pt] = await J('PUT', '/api/v1/entities/' + it.id, o);
out.push(['price ' + it.name, pc, pc >= 300 ? pt.slice(0, 120) : '']);
}
return out;
});
console.log(JSON.stringify(r));
}
await p.reload({waitUntil: 'networkidle2'});
await sleep(3000);
console.log('after login', p.url());
await p.screenshot({path: '/out/1.png'});
await p.click('::-p-text(Fűnyíró)').catch(e => console.log('no tv row', e.message));
await sleep(3000);
console.log('shot 2 at', p.url().replace(/[0-9a-f-]{36}/, '<id>'));
await p.screenshot({path: '/out/2.png'});
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2});
await p.goto(URL + '/home', {waitUntil: 'networkidle2'}).catch(() => {});
await p.evaluate(() => { // a full navigation put the language back to English: set it again, as on the desktop
const k = 'homebox/preferences/location';
let v = {}; try { v = JSON.parse(localStorage.getItem(k) || '{}'); } catch (e) {}
v.language = 'hu'; localStorage.setItem(k, JSON.stringify(v));
});
await p.reload({waitUntil: 'networkidle2'});
await sleep(3000);
await p.screenshot({path: '/out/3.png'});
await b.close();
})().catch(e => { console.error(e); process.exit(1); });