// 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); });