Files
felhom.eu/documentation/audits/dashboard-layout-2026-10-08/tools/shots.js
T
admin 7585097f26
gates / gates (push) Successful in 4m29s
Dashboard layout: R-906/R-907 fixed on main (controller d5f2e47, 423b6d3), R-909..R-911 filed, evidence from 9202, report
Register 131 -> 134 (R-909 Apps card header, fixed on main; R-910 retake the website's dashboard pictures
after tomorrow's release; R-911 storage-unavailable banner stays Hungarian on English pages).
Evidence: before (0.303.0) / after (0.304.0-layout.rc2) pictures and measurements on scratch 9202, red proofs,
fixture diff. 9202 is back on 0.303.0.

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

94 lines
6.1 KiB
JavaScript

// Dashboard layout evidence (2026-10-08): scratch guest 9202, headless Chrome inside 9202, through traefik with the real
// host name. Session cookie from a 0600 file (never printed). Per page and width: a picture, plus MEASUREMENTS:
// sideways scroll, and on /stacks for each card whether the name box overlaps any tag box and the logo/name top offset.
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();
const TAG = process.env.TAG; // before | after
const out = [];
let LANG = 'hu';
function log(...a) { const l = a.join(' '); console.log(l); out.push(l); }
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(() => {});
log('language now', await p.$eval('html', e => e.lang));
}
async function measure(p, name) {
const m = await p.evaluate(() => {
const r = {scrollW: document.documentElement.scrollWidth, innerW: window.innerWidth};
r.banner = Array.from(document.querySelectorAll('.alerts-container .alert-banner')).map(b => b.className.replace('alert-banner ', '') + ': ' + b.innerText.trim().slice(0, 70));
const btn = document.querySelector('.share-open-btn');
if (btn) { const b = btn.getBoundingClientRect(); r.share = {left: Math.round(b.left), right: Math.round(b.right), top: Math.round(b.top), text: btn.innerText.trim()}; }
r.cards = [];
for (const c of document.querySelectorAll('.stack-detail-card')) {
const h = c.querySelector('h3'), logo = c.querySelector('.stack-logo-lg');
if (!h || !logo) continue;
const rg = document.createRange(); rg.selectNodeContents(h);
const tr = rg.getBoundingClientRect(), lr = logo.getBoundingClientRect(), cr = c.getBoundingClientRect();
const tags = Array.from(c.querySelectorAll('.tag')).map(t => t.getBoundingClientRect());
const overlap = tags.some(t => !(t.right <= tr.left || t.left >= tr.right || t.bottom <= tr.top || t.top >= tr.bottom));
const outside = tags.some(t => t.right > cr.right + 0.5);
const addr = c.querySelector('.subdomain-link');
let addrLines = 0;
if (addr) { const ar = document.createRange(); ar.selectNodeContents(addr); { const rs = Array.from(ar.getClientRects()).filter(x => x.width > 0); const t0 = Math.min(...rs.map(x => x.top)); const h0 = Math.min(...rs.map(x => x.height)); addrLines = 1 + rs.filter(x => x.top > t0 + h0 * 0.6).length; } }
r.cards.push({name: h.innerText.trim(), tags: tags.length, nameUnderTag: overlap, tagOutsideCard: outside,
logoTopMinusNameTop: Math.round(lr.top - tr.top), addr: addr ? addr.innerText.trim().replace(/\s+/g, ' ') : '', addrLines,
addrTitle: addr ? addr.title : ''});
}
return r;
});
log(`${name}: layout width ${m.innerW} (screen ${process.env.W}) scrollWidth ${m.scrollW} — wider than the screen: ${m.innerW > +process.env.W || m.scrollW > +process.env.W}`);
log(`${name}: banners ${JSON.stringify(m.banner)}`);
if (m.share) log(`${name}: share button ${JSON.stringify(m.share)} past the screen edge: ${m.share.right > +process.env.W}`);
if (m.cards.length) {
const ov = m.cards.filter(c => c.nameUnderTag).map(c => c.name);
const offs = [...new Set(m.cards.map(c => c.logoTopMinusNameTop))];
log(`${name}: ${m.cards.length} cards; name text under a tag on ${ov.length} ${JSON.stringify(ov)}; tag outside card on ${m.cards.filter(c => c.tagOutsideCard).length}; logo top minus name top ${JSON.stringify(offs)}; address on more than one line on ${m.cards.filter(c => c.addrLines > 1).length}`);
for (const c of m.cards.filter(c => /paperless|filebrowser|immich/i.test(c.name))) log(`${name}: ${JSON.stringify(c)}`);
}
}
async function shot(p, name, path) {
await p.goto(BASE + path + (LANG === 'en' ? '?lang=en' : ''), {waitUntil: 'networkidle2'});
await sleep(1200);
log(`${TAG}-${name}: html lang ${await p.$eval('html', e => e.lang)}, version ${await p.evaluate(() => (document.body.innerText.match(/\b0\.\d+\.\d+\S*/) || ['?'])[0])}`);
await p.screenshot({path: `/out/${TAG}-${name}.png`});
await measure(p, `${TAG}-${name}`);
}
async function cards(p, name) { // the three cards the brief names, as one element picture each
for (const [key, re] of [['installed', /paperless/i], ['notinstalled', /immich/i], ['builtin', /filebrowser|f.jlkezel/i]]) {
const hs = await p.$$('.stack-detail-card');
for (const h of hs) {
const t = await h.$eval('h3', e => e.innerText).catch(() => '');
if (re.test(t)) { await h.screenshot({path: `/out/${TAG}-${name}-card-${key}.png`}); break; }
}
}
}
(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 ['hu', 'en']) {
process.env.W = '1440';
await p.setViewport({width: 1440, height: 900, deviceScaleFactor: 1});
LANG = lang;
await shot(p, `launcher-${lang}-1440`, '/launcher');
await shot(p, `apps-${lang}-1440`, '/stacks');
await cards(p, `apps-${lang}-1440`);
await shot(p, `backups-${lang}-1440`, '/backups');
process.env.W = '390';
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2, isMobile: true, hasTouch: true});
await shot(p, `launcher-${lang}-390`, '/launcher');
await shot(p, `apps-${lang}-390`, '/stacks');
await cards(p, `apps-${lang}-390`);
await shot(p, `backups-${lang}-390`, '/backups');
}
fs.writeFileSync(`/out/${TAG}-measure.txt`, out.join('\n') + '\n');
await b.close();
})().catch(e => { console.error(e); process.exit(1); });