website refresh: true numbers (56 apps), claims checked against the capability map, ten missing apps, an English twin of every public page (public, choice B), 404 page; site gates 13-18 + 11 decoys; R-902 opened
gates / gates (push) Failing after 13m1s
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
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// Docmost screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own traefik,
|
||||
// passing the setup gate as the household. Docmost's own first-run setup creates the invented workspace and its first
|
||||
// (admin) account; a few INVENTED household pages are added through Docmost's own Markdown import; then its own UI
|
||||
// is shot. (Docmost 0.96 ships no Hungarian interface; the pages are Hungarian.)
|
||||
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 PAGES = [
|
||||
['Házirend.md', `# Házirend
|
||||
|
||||
Közös szabályok, hogy mindenki tudja, mi hol van és mi a teendő.
|
||||
|
||||
## Hétköznap
|
||||
|
||||
- A mosogatógépet az indítja el, aki utoljára tesz bele.
|
||||
- A szelektív hulladék kedden és pénteken megy ki.
|
||||
- Este 10 után csendes pihenő.
|
||||
|
||||
## Hétvégén
|
||||
|
||||
| Feladat | Ki? |
|
||||
|---|---|
|
||||
| Porszívózás | Anna |
|
||||
| Fűnyírás | Péter |
|
||||
| Bevásárlás | közösen |
|
||||
`],
|
||||
['Wifi és eszközök.md', `# Wifi és eszközök
|
||||
|
||||
Vendégeknek a **Vendég** hálózat való, a jelszó a hűtőn van.
|
||||
|
||||
## Eszközök
|
||||
|
||||
- Nyomtató: a dolgozószobában, hálózaton elérhető
|
||||
- Okostévé: a nappaliban
|
||||
- Router: az előszobai szekrényben
|
||||
|
||||
> Ha valami nem működik: először újraindítás, utána szólj.
|
||||
`],
|
||||
['Nyaralás 2027.md', `# Nyaralás 2027
|
||||
|
||||
## Ötletek
|
||||
|
||||
1. Balaton, Tihany – kerékpártúra a part mentén
|
||||
2. Mátra – túrázás, kilátók
|
||||
3. Őrség – csend, erdő, tökmagolaj
|
||||
|
||||
## Csomaglista
|
||||
|
||||
- [ ] Személyi igazolványok
|
||||
- [ ] Naptej
|
||||
- [ ] Elsősegélycsomag
|
||||
- [ ] Töltők
|
||||
`],
|
||||
['Receptek, amiket szeretünk.md', `# Receptek, amiket szeretünk
|
||||
|
||||
- Rakott krumpli – vasárnap
|
||||
- Gulyásleves – vendégeknek
|
||||
- Palacsinta – mindig
|
||||
`],
|
||||
['Autó karbantartás.md', `# Autó karbantartás
|
||||
|
||||
| Mikor | Mi | Megjegyzés |
|
||||
|---|---|---|
|
||||
| 2026. március | Olajcsere | 15 000 km |
|
||||
| 2026. október | Téli gumi | szerviz előre bejelentkezve |
|
||||
| 2027. január | Műszaki vizsga | |
|
||||
`],
|
||||
];
|
||||
(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, ''));
|
||||
if (!S.skipseed) {
|
||||
const r = await p.evaluate(async (S, PAGES) => {
|
||||
const out = [];
|
||||
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()]; };
|
||||
const [sc, st] = await J('/api/auth/setup', {name: S.name, email: S.user, password: S.pw, workspaceName: 'Demo Család'});
|
||||
out.push(['setup', sc, sc >= 300 ? st.slice(0, 160) : '']);
|
||||
const [lc, lt] = await J('/api/spaces', {page: 1, limit: 20});
|
||||
const sp = JSON.parse(lt); const items = (sp.data && sp.data.items) || sp.items || [];
|
||||
out.push(['spaces', lc, items.map(x => x.name).join(',')]);
|
||||
const spaceId = items[0] && items[0].id;
|
||||
for (const [fn, md] of PAGES) {
|
||||
const fd = new FormData();
|
||||
fd.append('spaceId', spaceId);
|
||||
fd.append('file', new File([md], fn, {type: 'text/markdown'}));
|
||||
const x = await fetch('/api/pages/import', {method: 'POST', body: fd});
|
||||
out.push([fn, x.status, x.status >= 300 ? (await x.text()).slice(0, 120) : '']);
|
||||
}
|
||||
return out;
|
||||
}, S, PAGES);
|
||||
console.log(JSON.stringify(r));
|
||||
}
|
||||
await p.goto(URL + '/home', {waitUntil: 'networkidle2'});
|
||||
await sleep(3000);
|
||||
console.log('home', p.url().replace(URL, ''));
|
||||
if (await p.$('input[type=password]')) { // not signed in by the setup call: Docmost's own sign-in form
|
||||
await p.type('input[type=email]', S.user);
|
||||
await p.type('input[type=password]', S.pw);
|
||||
await p.keyboard.press('Enter');
|
||||
await p.waitForNavigation({waitUntil: 'networkidle2', timeout: 30000}).catch(() => {});
|
||||
await sleep(3000);
|
||||
console.log('after sign-in', p.url().replace(URL, ''));
|
||||
}
|
||||
await p.screenshot({path: '/out/1.png'});
|
||||
await p.click('::-p-text(Házirend)').catch(e => console.log('no page link', e.message));
|
||||
await sleep(4000);
|
||||
console.log('shot 2 at', p.url().replace(URL, ''));
|
||||
await p.screenshot({path: '/out/2.png'});
|
||||
const pageUrl = p.url();
|
||||
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2});
|
||||
await p.goto(pageUrl, {waitUntil: 'networkidle2'}).catch(() => {});
|
||||
await sleep(3000);
|
||||
await p.screenshot({path: '/out/3.png'});
|
||||
await b.close();
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,3 @@
|
||||
# source me: the two env vars wr.py needs (SC holds only 0600 files; EV is this evidence dir)
|
||||
export SC=/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/a8c3ad67-72de-408f-9706-5ad993306ff7/scratchpad/shots
|
||||
export EV=/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/website-refresh-2026-10-08/shots
|
||||
@@ -0,0 +1,125 @@
|
||||
// Homebox screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own traefik
|
||||
// (every name mapped to it), passing the setup gate as the household (the dashboard session vouches). An invented
|
||||
// demo account and a few invented things are created through Homebox's own API in the page, then its own UI is shot.
|
||||
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(/\?.*/, ''));
|
||||
const r = await p.evaluate(async (S) => {
|
||||
const j = (m, u, body, tok) => fetch(u, {method: m, headers: Object.assign({'Content-Type': 'application/json'},
|
||||
tok ? {Authorization: tok} : {}), body: body ? JSON.stringify(body) : undefined}).then(async x => [x.status, await x.text()]);
|
||||
const out = [];
|
||||
if (S.skipseed) return out;
|
||||
const jp = t => { try { return JSON.parse(t); } catch (e) { return {}; } };
|
||||
out.push(['register', ...(await j('POST', '/api/v1/users/register', {name: S.name, email: S.user, password: S.pw})).map(x => String(x).slice(0, 80))]);
|
||||
const [lc, lb] = await j('POST', '/api/v1/users/login', {username: S.user, password: S.pw, stayLoggedIn: true});
|
||||
out.push(['login', lc]);
|
||||
let tok;
|
||||
try { tok = JSON.parse(lb).token; } catch (e) { return out.concat([['loginbody', lb.slice(0, 80)]]); }
|
||||
const [tc, tt] = await j('GET', '/api/v1/entity-types', null, tok);
|
||||
const types = jp(tt); out.push(['types', tc, tt.slice(0, 300)]);
|
||||
const tl = (Array.isArray(types) ? types : (types.items || [])).find(x => x.isLocation) || {};
|
||||
const ti = (Array.isArray(types) ? types : (types.items || [])).find(x => !x.isLocation) || {};
|
||||
const loc = {};
|
||||
for (const [n, d] of [['Nappali', 'Földszint'], ['Konyha', 'Földszint'], ['Garázs', 'Udvar'], ['Dolgozószoba', 'Emelet']]) {
|
||||
const [c, t] = await j('POST', '/api/v1/entities', {name: n, description: d, entityTypeId: tl.id}, tok); loc[n] = jp(t).id; out.push(['loc', c, c >= 300 ? t.slice(0, 80) : '']);
|
||||
}
|
||||
const lab = {};
|
||||
for (const [n, col] of [['Elektronika', '#3b82f6'], ['Garanciális', '#16a34a'], ['Szerszám', '#ea580c']]) {
|
||||
const [c, t] = await j('POST', '/api/v1/tags', {name: n, color: col}, tok); lab[n] = jp(t).id; out.push(['label', c, c >= 300 ? t.slice(0, 80) : '']);
|
||||
}
|
||||
const items = [
|
||||
['Mosogatógép', 'Beépített, 60 cm', 'Konyha', ['Elektronika', 'Garanciális']],
|
||||
['Televízió 55"', 'Nappali fali konzolon', 'Nappali', ['Elektronika', 'Garanciális']],
|
||||
['Akkus fúró-csavarozó', '18 V, két akkumulátorral', 'Garázs', ['Szerszám']],
|
||||
['Létra, 3 fokos', 'Alumínium', 'Garázs', ['Szerszám']],
|
||||
['Laptop', 'Munkához', 'Dolgozószoba', ['Elektronika']],
|
||||
['Kávéfőző', 'Kapszulás', 'Konyha', ['Elektronika']],
|
||||
['Porszívó', 'Vezeték nélküli', 'Nappali', ['Elektronika', 'Garanciális']],
|
||||
['Fűnyíró', 'Elektromos', 'Garázs', ['Szerszám', 'Garanciális']],
|
||||
];
|
||||
for (const [n, d, l, ls] of items) {
|
||||
const [c, t] = await j('POST', '/api/v1/entities', {name: n, description: d, entityTypeId: ti.id, parentId: loc[l], tagIds: ls.map(x => lab[x])}, tok);
|
||||
out.push(['item', c, c >= 300 ? t.slice(0, 120) : '']);
|
||||
}
|
||||
return out;
|
||||
}, S);
|
||||
console.log(JSON.stringify(r));
|
||||
await p.goto(URL + '/', {waitUntil: 'networkidle2'});
|
||||
await sleep(1500);
|
||||
await p.screenshot({path: '/out/login.png'});
|
||||
if (process.env.STOP) { await b.close(); return; }
|
||||
if (await p.$('input[type=password]')) { // the seed's API sign-in normally leaves the browser signed in already
|
||||
const email = await p.$('input[type=email], input[name=email], input[type=text]');
|
||||
if (email) await email.type(S.user);
|
||||
await p.type('input[type=password]', S.pw);
|
||||
await p.keyboard.press('Enter');
|
||||
await p.waitForNavigation({waitUntil: 'networkidle2', timeout: 30000}).catch(() => {});
|
||||
}
|
||||
await sleep(2000);
|
||||
// Homebox's own language preference (its Profile page writes it to localStorage): Hungarian
|
||||
console.log('prefs before', await p.evaluate(() => JSON.stringify(Object.keys(localStorage))));
|
||||
await p.evaluate(() => {
|
||||
const k = 'homebox/preferences/location';
|
||||
let v = {}; try { v = JSON.parse(localStorage.getItem(k) || '{}'); } catch (e) {}
|
||||
v.language = 'hu'; localStorage.setItem(k, JSON.stringify(v));
|
||||
});
|
||||
if (S.cleanup) {
|
||||
// Homebox's own default English locations and tags (empty) removed so the demo reads in one language; the
|
||||
// collection's currency set to forint; invented prices and quantities on the invented things.
|
||||
const r = 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 mine = ['Nappali', 'Konyha', 'Garázs', 'Dolgozószoba', 'Elektronika', 'Garanciális', 'Szerszám'];
|
||||
const tree = JSON.parse((await J('GET', '/api/v1/entities/tree'))[1]);
|
||||
for (const n of tree) if (!mine.includes(n.name) && !(n.children || []).length) out.push(['del loc ' + n.name, (await J('DELETE', '/api/v1/entities/' + n.id))[0]]);
|
||||
const tags = JSON.parse((await J('GET', '/api/v1/tags'))[1]);
|
||||
for (const t of (Array.isArray(tags) ? tags : tags.items || [])) if (!mine.includes(t.name)) out.push(['del tag ' + t.name, (await J('DELETE', '/api/v1/tags/' + t.id))[0]]);
|
||||
const g = JSON.parse((await J('GET', '/api/v1/groups'))[1]);
|
||||
out.push(['currency', (await J('PUT', '/api/v1/groups', {name: g.name, currency: 'HUF'}))[0]]);
|
||||
const price = {'Mosogatógép': 189990, 'Televízió 55"': 249990, 'Akkus fúró-csavarozó': 42990, 'Létra, 3 fokos': 12990,
|
||||
'Laptop': 329990, 'Kávéfőző': 34990, 'Porszívó': 89990, 'Fűnyíró': 64990};
|
||||
const list = JSON.parse((await J('GET', '/api/v1/entities?pageSize=100'))[1]);
|
||||
for (const it of (list.items || [])) {
|
||||
if (!(it.name in price)) continue;
|
||||
const [c, full] = await J('GET', '/api/v1/entities/' + it.id);
|
||||
const o = JSON.parse(full);
|
||||
o.purchasePrice = price[it.name]; o.quantity = 1; o.insured = /Televízió|Laptop|Mosogatógép/.test(it.name);
|
||||
o.tagIds = (o.tags || []).map(t => t.id); o.parentId = o.parent ? o.parent.id : o.parentId;
|
||||
const [pc, pt] = await J('PUT', '/api/v1/entities/' + it.id, o);
|
||||
out.push(['price ' + it.name, pc, pc >= 300 ? pt.slice(0, 120) : '']);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
console.log(JSON.stringify(r));
|
||||
}
|
||||
await p.reload({waitUntil: 'networkidle2'});
|
||||
await sleep(3000);
|
||||
console.log('after login', p.url());
|
||||
await p.screenshot({path: '/out/1.png'});
|
||||
await p.click('::-p-text(Fűnyíró)').catch(e => console.log('no tv row', e.message));
|
||||
await sleep(3000);
|
||||
console.log('shot 2 at', p.url().replace(/[0-9a-f-]{36}/, '<id>'));
|
||||
await p.screenshot({path: '/out/2.png'});
|
||||
await p.setViewport({width: 390, height: 844, deviceScaleFactor: 2});
|
||||
await p.goto(URL + '/home', {waitUntil: 'networkidle2'}).catch(() => {});
|
||||
await p.evaluate(() => { // a full navigation put the language back to English: set it again, as on the desktop
|
||||
const k = 'homebox/preferences/location';
|
||||
let v = {}; try { v = JSON.parse(localStorage.getItem(k) || '{}'); } catch (e) {}
|
||||
v.language = 'hu'; localStorage.setItem(k, JSON.stringify(v));
|
||||
});
|
||||
await p.reload({waitUntil: 'networkidle2'});
|
||||
await sleep(3000);
|
||||
await p.screenshot({path: '/out/3.png'});
|
||||
await b.close();
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,25 @@
|
||||
// Mealie probe (no writes): after the API sign-in, what the box holds and what the frontend keeps for language.
|
||||
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.goto(URL + '/login', {waitUntil: 'networkidle2'});
|
||||
const r = await p.evaluate(async (pw) => {
|
||||
const fd = new URLSearchParams({username: 'changeme@example.com', password: pw, remember_me: 'true'});
|
||||
const st = (await fetch('/api/auth/token', {method: 'POST', body: fd})).status;
|
||||
const g = async (u, n = 400) => { const x = await fetch(u); return [x.status, (await x.text()).slice(0, n)]; };
|
||||
const rec = await g('/api/recipes?perPage=50', 1e6);
|
||||
return {st, recipes: rec[0] + ' ' + (rec[1].match(/"total":\d+/) || [''])[0], names: (rec[1].match(/"name":"[^"]+"/g) || []).join(','),
|
||||
startup: await g('/api/app/about/startup-info'), self: Object.keys(JSON.parse((await g('/api/users/self', 1e6))[1] || '{}'))};
|
||||
}, S.gen.ADMIN_PASSWORD);
|
||||
console.log(JSON.stringify(r).replace(/"(access_)?tok[a-z_]*":"[^"]*"/gi, '"…":"…"'));
|
||||
await p.goto(URL + '/g/home', {waitUntil: 'networkidle2'});
|
||||
await sleep(2000);
|
||||
console.log('g/home ->', p.url(), JSON.stringify(await p.evaluate(() => Object.keys(localStorage))), JSON.stringify((await p.cookies()).map(c => c.name)));
|
||||
await b.close();
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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); });
|
||||
@@ -0,0 +1,46 @@
|
||||
// Recipe Importer screenshots for the website card: on scratch guest 9202, headless Chrome through the box's own
|
||||
// traefik, passing the setup gate as the household. No recipe site is contacted (a scraped recipe would be a third
|
||||
// party's text and picture). The app's own settings form is saved with an INVENTED Mealie address and a placeholder
|
||||
// key (no Mealie exists behind it), which is what opens its import page; the shots are its import and settings pages.
|
||||
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 DEMO_URL = 'https://www.example.com/receptek/rakott-krumpli';
|
||||
(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 + '/settings', {waitUntil: 'networkidle2'});
|
||||
await sleep(1000);
|
||||
const set = async (sel, v) => { const f = await p.$(sel); if (f) { await f.click({clickCount: 3}); await f.type(v); } else console.log('no field', sel); };
|
||||
await set('input[name=mealie_url]', 'https://recipes.example.hu');
|
||||
await set('input[name=mealie_api_key]', 'demo-placeholder');
|
||||
await Promise.all([p.waitForNavigation({waitUntil: 'networkidle2'}).catch(() => {}),
|
||||
p.$eval('input[name=mealie_url]', el => el.form.submit())]);
|
||||
await sleep(1000);
|
||||
console.log('after save', p.url());
|
||||
await p.screenshot({path: '/out/settings.png'});
|
||||
await p.goto(URL + '/import', {waitUntil: 'networkidle2'});
|
||||
await sleep(1500);
|
||||
console.log('import ->', p.url());
|
||||
const ta = await p.$('textarea, input[type=url], input[name=url]');
|
||||
if (ta) await ta.type(DEMO_URL); else console.log('no url field');
|
||||
await sleep(500);
|
||||
await p.screenshot({path: '/out/import.png'});
|
||||
// the bulk tab: several invented addresses at once
|
||||
await p.click('::-p-text(Tömeges importálás)').catch(e => console.log('no bulk tab', e.message));
|
||||
await sleep(800);
|
||||
const bulk = await p.$$('textarea');
|
||||
const vis = [];
|
||||
for (const t of bulk) if (await t.isVisible()) vis.push(t);
|
||||
if (vis.length) await vis[0].type(['https://www.example.com/receptek/rakott-krumpli', 'https://www.example.com/receptek/gulyasleves',
|
||||
'https://www.example.com/receptek/turos-csusza', 'https://www.example.com/receptek/meggyes-pite'].join('\n'));
|
||||
else console.log('no visible bulk textarea');
|
||||
await sleep(500);
|
||||
await p.screenshot({path: '/out/bulk.png'});
|
||||
await b.close();
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -0,0 +1,108 @@
|
||||
// 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); });
|
||||
@@ -0,0 +1,17 @@
|
||||
"""sw.py — website-refresh 2026-10-08 screenshot helper. A shim over the catalog's box client
|
||||
(app-catalog-felhom.eu/scripts/box_walk.py, read-only), TARGET 9202, with ONE change: guest() pipes the script
|
||||
into `pct exec 9202 -- bash -s` over `ssh hp`, so nothing is written on the demo-hp host itself (no pct push,
|
||||
no temp file there). SC (0600 scratch with `.ctlpw`) and EV come from the environment. No secret is printed."""
|
||||
import os, subprocess, sys
|
||||
os.environ.setdefault("TARGET", "9202")
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as b # noqa: E402
|
||||
|
||||
|
||||
def guest(script, timeout=600):
|
||||
r = b.sh(["ssh", "-o", "ConnectTimeout=20", "-o", "BatchMode=yes", "hp",
|
||||
"LC_ALL=C pct exec 9202 -- env LC_ALL=C bash -s"], timeout=timeout, inp=script)
|
||||
return (r.stdout or "") + (("\n[stderr] " + r.stderr.strip()[-800:]) if r.returncode and r.stderr else "")
|
||||
|
||||
|
||||
b.guest = guest
|
||||
@@ -0,0 +1,13 @@
|
||||
"""towebp.py <app> [quality] — raw/<n>.png in the evidence dir -> <app>-screenshot-<n>.webp in website/assets
|
||||
(Pillow, method 6). Refuses to overwrite an existing asset."""
|
||||
import os, sys
|
||||
from PIL import Image
|
||||
app = sys.argv[1]; q = int(sys.argv[2]) if len(sys.argv) > 2 else 80
|
||||
EV = os.environ["EV"]; A = "/mnt/5_hdd/felhom.eu/git/felhom.eu/website/assets"
|
||||
for n in (1, 2, 3):
|
||||
src = f"{EV}/{app}/raw/{n}.png"; dst = f"{A}/{app}-screenshot-{n}.webp"
|
||||
if os.path.exists(dst) and not os.environ.get("MINE"):
|
||||
sys.exit(f"refused: {dst} exists")
|
||||
im = Image.open(src).convert("RGB")
|
||||
im.save(dst, "WEBP", quality=q, method=6)
|
||||
print(dst, im.size, os.path.getsize(dst), "B")
|
||||
@@ -0,0 +1,128 @@
|
||||
"""wr.py — website-refresh 2026-10-08: install ONE catalog app on scratch guest 9202 through the product, take its
|
||||
screenshots with headless Chrome inside 9202, copy the raw PNGs off the box, remove the app through the product,
|
||||
verify it is gone.
|
||||
|
||||
python3 wr.py baseline
|
||||
python3 wr.py deploy <app> <sub>
|
||||
python3 wr.py shoot <app> <sub> <script.js> [KEY=VAL ...] # secrets.json in the guest, 0600, removed after
|
||||
python3 wr.py fetch <app> <guest-path> <local-path> # one file off the guest
|
||||
python3 wr.py remove <app>
|
||||
python3 wr.py verify <app>
|
||||
|
||||
Env: SC (0600 scratch with .ctlpw), EV (evidence dir). No secret is ever printed: the dashboard session and the
|
||||
invented demo account's password travel only inside files (SC on DooPlex, /root/wr-shots/<app>/secrets.json in the
|
||||
guest, removed by `shoot` when Chrome exits)."""
|
||||
import base64, json, os, secrets, sys, time
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import sw # noqa: E402
|
||||
b = sw.b
|
||||
SC, EV = os.environ["SC"], os.environ["EV"]
|
||||
IMG = "ghcr.io/puppeteer/puppeteer:latest"
|
||||
TRAEFIK = "172.18.0.5"
|
||||
APPS = ["docmost", "homebox", "mealie", "recipe-importer", "sparkyfitness"]
|
||||
|
||||
|
||||
def log(app, *a):
|
||||
b.say(*a)
|
||||
os.makedirs(f"{EV}/{app}", exist_ok=True)
|
||||
with open(f"{EV}/{app}/run.log", "a") as f:
|
||||
f.write(" ".join(map(str, a)) + "\n")
|
||||
|
||||
|
||||
def secfile(app):
|
||||
p = f"{SC}/sec-{app}.json"
|
||||
if os.path.exists(p):
|
||||
return json.load(open(p))
|
||||
d = {"user": "demo@example.com", "pw": "Demo-" + secrets.token_hex(10), "name": "Demo Csalad"}
|
||||
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
os.write(fd, json.dumps(d).encode()); os.close(fd)
|
||||
return d
|
||||
|
||||
|
||||
def baseline():
|
||||
b.login()
|
||||
c, d = b.ctl("GET", "/api/stacks")
|
||||
dep = [(s.get("name"), s.get("state")) for s in (d.get("data") or []) if isinstance(s, dict) and s.get("deployed")]
|
||||
out = b.guest("df -h / | tail -1; docker ps --format '{{.Names}} {{.Image}}'; "
|
||||
"docker volume ls --format '{{.Name}}' | grep -E 'docmost|homebox|mealie|recipe|sparky' || echo 'no volumes of the five'; "
|
||||
+ "; ".join(f"echo {a}: $(ls -A /opt/docker/stacks/{a})" for a in APPS)
|
||||
+ f"; docker inspect -f '{{{{.RepoDigests}}}} {{{{.Created}}}}' {IMG}")
|
||||
txt = f"deployed (dashboard API): {dep}\n{out}"
|
||||
os.makedirs(EV, exist_ok=True)
|
||||
open(f"{EV}/baseline-9202.txt", "w").write(txt + "\n")
|
||||
print(txt)
|
||||
|
||||
|
||||
def deploy(app, sub):
|
||||
b.login()
|
||||
ok = b.deploy(app, sub)
|
||||
if b.GENERATED.get(app):
|
||||
p = f"{SC}/gen-{app}.json"
|
||||
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
os.write(fd, json.dumps(b.GENERATED[app]).encode()); os.close(fd)
|
||||
log(app, f"generated deploy secrets kept in scratch (keys {list(b.GENERATED[app])})")
|
||||
log(app, f"deploy {app} -> {ok}")
|
||||
if ok:
|
||||
log(app, f"app answers: {b.wait_app(sub)}")
|
||||
|
||||
|
||||
def shoot(app, sub, script, extra):
|
||||
b.login()
|
||||
sess = open(f"{SC}/sess{os.getpid()}.txt").read().strip().split("=", 1)[1]
|
||||
sec = dict(secfile(app))
|
||||
gp = f"{SC}/gen-{app}.json"
|
||||
if os.path.exists(gp):
|
||||
sec["gen"] = json.load(open(gp))
|
||||
sec["session"] = sess
|
||||
for kv in extra:
|
||||
k, v = kv.split("=", 1); sec[k] = v
|
||||
js = open(script).read()
|
||||
gdir = f"/root/wr-shots/{app}"
|
||||
payload = base64.b64encode(json.dumps(sec).encode()).decode()
|
||||
jsb = base64.b64encode(js.encode()).decode()
|
||||
cmd = f"""set -e
|
||||
umask 077; mkdir -p {gdir}; chmod 700 {gdir}
|
||||
echo {payload} | base64 -d > {gdir}/secrets.json; chmod 600 {gdir}/secrets.json
|
||||
echo {jsb} | base64 -d > {gdir}/shot.js
|
||||
set +e
|
||||
timeout 600 docker run --rm --network traefik-public --user root -e PUPPETEER_CACHE_DIR=/home/pptruser/.cache/puppeteer \
|
||||
-e NODE_PATH=/home/pptruser/node_modules -e DOMAIN=enkisfelhom.hu -e SUB={sub} -e TRAEFIK={TRAEFIK} -e STOP=${{STOP:-}} \
|
||||
-v {gdir}:/out {IMG} node /out/shot.js 2>&1 | sed -E "s/[A-Za-z0-9_.+/=-]{{28,}}/<long-value-redacted>/g" | tail -40
|
||||
rc=${{PIPESTATUS[0]}}
|
||||
shred -u {gdir}/secrets.json
|
||||
echo "chrome rc=$rc"; ls -la {gdir}
|
||||
"""
|
||||
out = b.guest(cmd, timeout=900)
|
||||
log(app, out)
|
||||
|
||||
|
||||
def fetch(app, gpath, lpath):
|
||||
out = b.guest(f"base64 -w0 {gpath}")
|
||||
data = base64.b64decode(out.strip().split("\n")[0])
|
||||
os.makedirs(os.path.dirname(lpath), exist_ok=True)
|
||||
open(lpath, "wb").write(data)
|
||||
log(app, f"fetched {gpath} -> {lpath} ({len(data)} B)")
|
||||
|
||||
|
||||
def remove(app):
|
||||
b.login()
|
||||
b.remove(app)
|
||||
verify(app)
|
||||
|
||||
|
||||
def verify(app):
|
||||
b.login()
|
||||
st = b.stack(app)
|
||||
out = b.guest(f"echo containers: $(docker ps -a --format '{{{{.Names}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
|
||||
f"echo volumes: $(docker volume ls --format '{{{{.Name}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
|
||||
f"echo networks: $(docker network ls --format '{{{{.Name}}}}' | grep -i '{app.split('-')[0]}' || echo none); "
|
||||
f"echo stackdir: $(ls -A /opt/docker/stacks/{app}); "
|
||||
f"echo drive: $(ls -d /mnt/felhom-drives/scratch_hdd/userdata/{app} 2>/dev/null || echo none)")
|
||||
log(app, f"VERIFY {app}: deployed={st.get('deployed')} state={st.get('state')}\n{out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:]
|
||||
{"baseline": lambda: baseline(), "deploy": lambda: deploy(a[1], a[2]),
|
||||
"shoot": lambda: shoot(a[1], a[2], a[3], a[4:]), "fetch": lambda: fetch(a[1], a[2], a[3]),
|
||||
"remove": lambda: remove(a[1]), "verify": lambda: verify(a[1])}[a[0]]()
|
||||
Reference in New Issue
Block a user