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
121 lines
8.0 KiB
JavaScript
121 lines
8.0 KiB
JavaScript
// Mealie screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own traefik,
|
|
// passing the setup gate as the household. The first account (changeme@example.com, the password this install
|
|
// generated) adds a few INVENTED recipes and a week's meal plan through Mealie's own API, then its own UI is shot.
|
|
// No recipe site is contacted.
|
|
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}`;
|
|
const RECIPES = [
|
|
['Rakott krumpli', 'Réteges, tejfölös, kolbászos egytálétel a hétvégére.', '6 adag', '1 óra 30 perc',
|
|
['1,5 kg burgonya', '8 tojás', '300 g parasztkolbász', '500 g tejföl', 'só, bors, vaj'],
|
|
['A burgonyát és a tojásokat héjában megfőzzük, majd felkarikázzuk.', 'Kivajazott tálba rétegezzük a burgonyát, a tojást és a kolbászt.',
|
|
'Minden réteget megkenünk tejföllel, a tetejére is tejföl kerül.', '180 fokon kb. 40 percig sütjük.']],
|
|
['Gulyásleves', 'Bográcsban vagy fazékban, ahogy a nagymama főzte.', '8 adag', '2 óra',
|
|
['800 g marhalábszár', '2 fej vöröshagyma', '2 sárgarépa', '1 fehérrépa', '600 g burgonya', 'pirospaprika, kömény'],
|
|
['A hagymát zsíron üvegesre pároljuk, rászórjuk a pirospaprikát.', 'Hozzáadjuk a felkockázott húst, és fehéredésig pirítjuk.',
|
|
'Felöntjük vízzel, és fedő alatt puhára főzzük.', 'Hozzáadjuk a zöldségeket és a burgonyát, készre főzzük.']],
|
|
['Túrós csusza', 'Gyors vacsora tepertővel.', '4 adag', '30 perc',
|
|
['500 g széles metélt', '500 g tehéntúró', '200 g tejföl', '150 g tepertő'],
|
|
['A tésztát sós vízben kifőzzük.', 'A tepertőt kicsit megpirítjuk.', 'A tésztára túrót, tejfölt és tepertőt szórunk.']],
|
|
['Meggyes pite', 'Linzeres tészta, savanykás meggytöltelék.', '12 szelet', '1 óra 15 perc',
|
|
['500 g liszt', '250 g vaj', '150 g porcukor', '2 tojás', '800 g magozott meggy', '3 ek darált keksz'],
|
|
['A tészta hozzávalóit összegyúrjuk, és fél órára hűtőbe tesszük.', 'A tészta felét tepsibe nyújtjuk, megszórjuk keksszel.',
|
|
'Ráterítjük a meggyet, befedjük a másik tésztalappal.', '180 fokon aranybarnára sütjük.']],
|
|
['Lecsó', 'Nyári klasszikus, tojással vagy kolbásszal.', '4 adag', '40 perc',
|
|
['1 kg TV paprika', '500 g paradicsom', '2 fej vöröshagyma', 'só, pirospaprika'],
|
|
['A hagymát megpároljuk.', 'Hozzáadjuk a felcsíkozott paprikát, majd a paradicsomot.', 'Sűrűre főzzük.']],
|
|
['Palacsinta', 'Lekvárral, kakaóval vagy túróval.', '15 darab', '45 perc',
|
|
['250 g liszt', '3 tojás', '5 dl tej', '2 dl szódavíz', 'csipet só', 'olaj a sütéshez'],
|
|
['A hozzávalókból csomómentes tésztát keverünk.', 'Fél órát pihentetjük.', 'Forró serpenyőben vékonyra kisütjük.']],
|
|
];
|
|
(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 + '/login', {waitUntil: 'networkidle2'});
|
|
await sleep(1500);
|
|
console.log('landed', p.url().replace(/\?.*/, ''));
|
|
if (!S.skipseed) {
|
|
const r = await p.evaluate(async (S, RECIPES) => {
|
|
const out = [];
|
|
const fd = new URLSearchParams({username: 'changeme@example.com', password: S.gen.ADMIN_PASSWORD});
|
|
const t = await fetch('/api/auth/token', {method: 'POST', body: fd});
|
|
out.push(['auth', t.status]);
|
|
if (t.status !== 200) return out;
|
|
const tok = 'Bearer ' + (await t.json()).access_token;
|
|
const J = async (m, u, body) => { const x = await fetch(u, {method: m, headers: {'Content-Type': 'application/json', Authorization: tok}, body: body ? JSON.stringify(body) : undefined}); return [x.status, await x.text()]; };
|
|
const ids = [];
|
|
for (const [name, desc, yld, total, ing, steps] of RECIPES) {
|
|
const [c, slugTxt] = await J('POST', '/api/recipes', {name});
|
|
const slug = JSON.parse(slugTxt);
|
|
const [gc, full] = await J('GET', '/api/recipes/' + slug);
|
|
const o = JSON.parse(full);
|
|
o.description = desc; o.recipeYield = yld; o.totalTime = total;
|
|
o.recipeIngredient = ing.map(n => ({note: n, display: n, quantity: 0, disableAmount: true}));
|
|
o.recipeInstructions = steps.map(s => ({text: s, title: '', ingredientReferences: []}));
|
|
const [pc, pt] = await J('PUT', '/api/recipes/' + slug, o);
|
|
out.push([name, c, gc, pc, pc >= 300 ? pt.slice(0, 160) : '']);
|
|
ids.push(o.id);
|
|
}
|
|
// a week's meal plan from today
|
|
const d0 = new Date();
|
|
for (let i = 0; i < 6; i++) {
|
|
const d = new Date(d0.getTime() + i * 86400000).toISOString().slice(0, 10);
|
|
const [mc, mt] = await J('POST', '/api/households/mealplans', {date: d, entryType: 'dinner', recipeId: ids[i % ids.length]});
|
|
out.push(['plan ' + d, mc, mc >= 300 ? mt.slice(0, 120) : '']);
|
|
}
|
|
// the account's display name and language
|
|
const [uc, ut] = await J('GET', '/api/users/self');
|
|
const u = JSON.parse(ut);
|
|
return out.concat([['self', uc, u.fullName, u.locale || u.language || '']]);
|
|
}, S, RECIPES);
|
|
console.log(JSON.stringify(r));
|
|
}
|
|
// sign in: Mealie's /api/auth/token answer sets its own session cookie in this browser (no form typing: the login
|
|
// page pre-fills the demo defaults over typed text, and five wrong passwords lock the account for hours)
|
|
const st = await p.evaluate(async (pw) => {
|
|
const fd = new URLSearchParams({username: 'changeme@example.com', password: pw, remember_me: 'true'});
|
|
return (await fetch('/api/auth/token', {method: 'POST', body: fd, credentials: 'same-origin'})).status;
|
|
}, S.gen.ADMIN_PASSWORD);
|
|
console.log('sign-in status', st);
|
|
await p.goto(URL + '/', {waitUntil: 'networkidle2'});
|
|
await sleep(2000);
|
|
if (p.url().includes('/admin/setup')) { // Mealie's first-login wizard: its own "already set up" button
|
|
await p.click("::-p-text(I'm already set up)").catch(e => console.log('no skip button', e.message));
|
|
await sleep(3000);
|
|
console.log('after setup skip', p.url().replace(URL, ''));
|
|
}
|
|
if (S.rename) { // the first account's display name: an invented household name instead of "Change Me"
|
|
console.log('rename', await p.evaluate(async () => {
|
|
const u = await (await fetch('/api/users/self')).json();
|
|
u.fullName = 'Demo Család';
|
|
return (await fetch('/api/users/' + u.id, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(u)})).status;
|
|
}));
|
|
}
|
|
// Mealie's own language switch keeps the choice in this cookie: Hungarian
|
|
await p.setCookie({name: 'i18n_redirected', value: 'hu-HU', domain: `${process.env.SUB}.${D}`, path: '/', secure: true});
|
|
await p.goto(URL + '/g/home', {waitUntil: 'networkidle2'});
|
|
await sleep(4000);
|
|
console.log('after login', p.url().replace(URL, ''));
|
|
await p.screenshot({path: '/out/1.png'});
|
|
const home = p.url();
|
|
await p.click('::-p-text(Rakott krumpli)').catch(e => console.log('no recipe card', e.message));
|
|
await sleep(4000);
|
|
console.log('shot 2 at', p.url().replace(URL, ''));
|
|
await p.screenshot({path: '/out/2.png'});
|
|
const recipeUrl = p.url();
|
|
await p.goto(URL + '/household/mealplan/planner/view', {waitUntil: 'networkidle2'}).catch(() => {});
|
|
await sleep(3000);
|
|
console.log('shot 3 at', p.url().replace(URL, ''));
|
|
await p.screenshot({path: '/out/3.png'});
|
|
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2});
|
|
await p.goto(recipeUrl, {waitUntil: 'networkidle2'}).catch(() => {});
|
|
await sleep(3000);
|
|
await p.screenshot({path: '/out/4.png'});
|
|
await b.close();
|
|
})().catch(e => { console.error(e); process.exit(1); });
|