67b3fd23d1
gates / gates (push) Failing after 13m1s
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
109 lines
7.1 KiB
JavaScript
109 lines
7.1 KiB
JavaScript
// SparkyFitness screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own
|
|
// traefik, passing the setup gate as the household. An INVENTED demo account signs up through SparkyFitness's own
|
|
// sign-up (open while the setup gate is up), then its own UI is shot. Better-auth rate-limits sign-in per IP: a 429
|
|
// is waited out, never read as a verdict.
|
|
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(URL, ''));
|
|
const auth = await p.evaluate(async (S) => {
|
|
const J = async (u, body) => { const x = await fetch(u, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)}); return [x.status, (await x.text()).slice(0, 160)]; };
|
|
let r = S.skipseed ? [0, ''] : await J('/api/auth/sign-up/email', {email: S.user, password: S.pw, name: S.name});
|
|
const out = [['sign-up', r[0], r[0] >= 300 ? r[1] : '']];
|
|
r = await J('/api/auth/sign-in/email', {email: S.user, password: S.pw});
|
|
out.push(['sign-in', r[0], r[0] >= 300 ? r[1] : '']);
|
|
return out;
|
|
}, S);
|
|
console.log(JSON.stringify(auth));
|
|
// SparkyFitness keeps the language in the account's own preferences (Settings): Hungarian, and a Hungarian date format
|
|
console.log('lang ->', await p.evaluate(async () => {
|
|
const res = [];
|
|
for (const m of ['PUT', 'POST']) {
|
|
const x = await fetch('/api/user-preferences', {method: m, headers: {'Content-Type': 'application/json'}, body: JSON.stringify({language: 'hu', date_format: 'yyyy-MM-dd'})});
|
|
res.push(m + ' ' + x.status); if (x.ok) break;
|
|
}
|
|
return res.join(', ');
|
|
}));
|
|
await p.evaluate(() => { localStorage.setItem('i18nextLng', 'hu'); localStorage.setItem('language', 'hu');
|
|
localStorage.setItem('dismissedReleaseVersion', 'v1.8.0'); });
|
|
if (S.seedfood) { // INVENTED foods, today's diary, and two weeks of weight check-ins, through the app's own API
|
|
console.log('seed', JSON.stringify(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 today = new Date().toISOString().slice(0, 10);
|
|
const foods = [['Zabpehely', 100, 'g', 372, 13, 59, 7, 'breakfast', 60], ['Banán', 100, 'g', 89, 1.1, 23, 0.3, 'breakfast', 120],
|
|
['Csirkemell, sült', 100, 'g', 165, 31, 0, 3.6, 'lunch', 180], ['Jázmin rizs, főtt', 100, 'g', 130, 2.7, 28, 0.3, 'lunch', 150],
|
|
['Görög joghurt', 100, 'g', 97, 9, 4, 5, 'snacks', 150], ['Lencsefőzelék', 100, 'g', 116, 9, 20, 0.4, 'dinner', 300]];
|
|
for (const [name, ss, su, cal, pr, cb, fat, meal, qty] of foods) {
|
|
const [fc, ft] = await J('POST', '/api/foods', {name, serving_size: ss, serving_unit: su, calories: cal, protein: pr, carbs: cb, fat, is_custom: true});
|
|
let f = {}; try { f = JSON.parse(ft); } catch (e) {}
|
|
const vid = f.default_variant_id || (f.default_variant && f.default_variant.id) || f.variant_id || (f.variants && f.variants[0] && f.variants[0].id);
|
|
out.push([name, fc, fc >= 300 ? ft.slice(0, 120) : Object.keys(f).slice(0, 12).join(',')]);
|
|
let v = vid;
|
|
if (!v && f.id) { const [vc, vt] = await J('GET', '/api/foods/food-variants?food_id=' + f.id); try { const a = JSON.parse(vt); v = (Array.isArray(a) ? a : a.variants || [])[0].id; } catch (e) { out.push(['variants', vc, vt.slice(0, 120)]); } }
|
|
const [ec, et] = await J('POST', '/api/food-entries', {food_id: f.id, variant_id: v, meal_type: meal, quantity: qty, unit: su, entry_date: today});
|
|
out.push(['entry ' + name, ec, ec >= 300 ? et.slice(0, 160) : '']);
|
|
}
|
|
for (let i = 13; i >= 0; i--) {
|
|
const d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
|
|
const w = Math.round((82.4 - (13 - i) * 0.18 + (i % 3 === 0 ? 0.3 : 0)) * 10) / 10;
|
|
const [cc, ct] = await J('POST', '/api/measurements/check-in', {entry_date: d, weight: w, waist: 92 - (13 - i) * 0.1, steps: 6000 + (i * 733) % 5000});
|
|
if (i === 13 || cc >= 300) out.push(['check-in ' + d, cc, cc >= 300 ? ct.slice(0, 160) : '']);
|
|
}
|
|
return out;
|
|
})));
|
|
}
|
|
const clickText = async (texts) => p.evaluate((texts) => { // a DOM click (the dialog's X is a visually-hidden label)
|
|
const b = [...document.querySelectorAll('button, a')].find(e => texts.includes(e.innerText.trim()));
|
|
if (b) { b.click(); return b.innerText.trim(); }
|
|
return null;
|
|
}, texts);
|
|
const closeModal = async () => { // the "new version available" notice: read to the end, then "don't show again"
|
|
await p.evaluate(() => document.querySelectorAll('[role=dialog] *').forEach(e => { if (e.scrollHeight > e.clientHeight + 5) e.scrollTop = e.scrollHeight; }));
|
|
await sleep(800);
|
|
console.log('dismiss ->', await clickText(["Don't show again for this version"]));
|
|
await sleep(1200);
|
|
};
|
|
await p.goto(URL + '/', {waitUntil: 'networkidle2'});
|
|
await sleep(3000);
|
|
await closeModal();
|
|
console.log('skip ->', await clickText(['Kihagyás', 'Skip'])); await sleep(3000);
|
|
await closeModal();
|
|
await sleep(2000);
|
|
console.log('home', p.url().replace(URL, ''));
|
|
if (S.water) { // a few glasses of water, with the diary's own "+" button
|
|
for (let k = 0; k < 5; k++) {
|
|
await p.evaluate(() => { const b = [...document.querySelectorAll('button')].filter(e => e.querySelector('svg.lucide-plus') && e.closest('div') && e.innerText.trim() === ''); if (b.length) b[b.length - 1].click(); });
|
|
await sleep(900);
|
|
}
|
|
}
|
|
await p.evaluate(() => window.scrollTo(0, 0));
|
|
await sleep(1500);
|
|
await p.screenshot({path: '/out/1.png'});
|
|
console.log('reports ->', await clickText(['Jelentések', 'Reports']));
|
|
await sleep(4000);
|
|
console.log('measurements ->', await clickText(['Measurements', 'Mérések', 'Testméretek']));
|
|
await sleep(4000);
|
|
await p.screenshot({path: '/out/2.png'});
|
|
await p.evaluate(() => window.scrollBy(0, 330));
|
|
await sleep(1500);
|
|
await p.screenshot({path: '/out/2b.png'});
|
|
console.log('diary ->', await clickText(['Napló', 'Diary']));
|
|
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2});
|
|
await sleep(3000);
|
|
await p.evaluate(() => window.scrollTo(0, 0));
|
|
await sleep(1000);
|
|
await p.screenshot({path: '/out/3.png'});
|
|
await b.close();
|
|
})().catch(e => { console.error(e); process.exit(1); });
|