7585097f26
gates / gates (push) Successful in 4m29s
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
94 lines
6.1 KiB
JavaScript
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); });
|