2026-08-30 12:26:56 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Maakt van een statuspagina een versie die je in een browser kunt openen.
|
|
|
|
|
//
|
|
|
|
|
// Draaien:
|
|
|
|
|
//
|
|
|
|
|
// node tools/voorbeeldpagina.mjs
|
|
|
|
|
// node tools/voorbeeldpagina.mjs whatsnext-electrum-gate
|
|
|
|
|
//
|
|
|
|
|
// Het resultaat komt in `voorbeeld/<app>.html` en die map is gitignored.
|
|
|
|
|
//
|
|
|
|
|
// ── Waarom dit bestaat ───────────────────────────────────────────────────────
|
|
|
|
|
//
|
2026-09-07 20:20:42 +02:00
|
|
|
// Een statuspagina is niet zomaar te openen. Twee dingen staan in de weg, en ze
|
2026-08-30 12:26:56 +02:00
|
|
|
// zijn allebei fundamenteel en niet op te lossen door er anders naar te kijken:
|
|
|
|
|
//
|
2026-09-07 20:20:42 +02:00
|
|
|
// 1. in een `index.html.template` staan accolade-variabelen die umbreld invult.
|
|
|
|
|
// Onopgelost staat er letterlijk "v${APP_VERSION}" in de kop. Een pagina die
|
|
|
|
|
// in een eigen image zit heeft dit niet meer, maar haalt de versie dan uit
|
|
|
|
|
// status.json, dus zonder agent staat er niets;
|
2026-08-30 12:26:56 +02:00
|
|
|
// 2. de pagina haalt al zijn gegevens bij een agent die alleen in de app bestaat.
|
|
|
|
|
// Zonder die agent zie je één foutmelding en verder een pagina vol "unknown":
|
|
|
|
|
// geen lijsten, geen teller, geen knoppen.
|
|
|
|
|
//
|
|
|
|
|
// Dit script vult het eerste in en maakt het tweede na. Wat je dan ziet is de
|
|
|
|
|
// opmaak met plausibele gegevens erin, en dat is genoeg om te beoordelen of de
|
|
|
|
|
// twee apps naast elkaar hetzelfde ontwerp zijn.
|
|
|
|
|
//
|
|
|
|
|
// ── Wat het NIET is ──────────────────────────────────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// Geen test en geen bewijs. De gegevens zijn verzonnen, dus dit zegt niets over
|
|
|
|
|
// of de pagina de echte status juist weergeeft; dat blijft handwerk op het
|
|
|
|
|
// apparaat. Wat er wél mee te vinden is: alles wat met opmaak te maken heeft, en
|
|
|
|
|
// dat is precies waar deze pagina's steeds op aangepast worden.
|
|
|
|
|
//
|
|
|
|
|
// Voor de fouten die je niet ziet staan er twee toetsen: `test_paginas_parsen.mjs`
|
|
|
|
|
// (loopt het script) en de dollarteken-toets in `test_appstore_vorm.py` (blijft
|
|
|
|
|
// het script heel na de invulling).
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
|
import { dirname, join } from 'node:path';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
const REPO = join(HIER, '..');
|
|
|
|
|
const UIT = join(REPO, 'voorbeeld');
|
|
|
|
|
|
|
|
|
|
const app = process.argv[2] || 'whatsnext-evolu-relay';
|
2026-09-07 20:20:42 +02:00
|
|
|
|
|
|
|
|
// De pagina staat op een van twee plekken: als *.template in de app-map, of als
|
|
|
|
|
// gewoon bestand in het bouwrecept onder tools/ als hij in een eigen image zit.
|
|
|
|
|
// Electrum Gate is sinds 0.1.0 het tweede geval. Alleen de eerste gaat door
|
|
|
|
|
// envsubst; voor de tweede is de invulling hieronder een lege stap.
|
|
|
|
|
const template = join(REPO, app, 'index.html.template');
|
|
|
|
|
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
|
|
|
|
|
const inImage = join(REPO, 'tools', kort, 'index.html');
|
|
|
|
|
const bron = existsSync(template) ? template : inImage;
|
2026-08-30 12:26:56 +02:00
|
|
|
|
|
|
|
|
if (!existsSync(bron)) {
|
2026-09-07 20:20:42 +02:00
|
|
|
console.error(`FOUT: geen pagina gevonden op ${template} of ${inImage}`);
|
2026-08-30 12:26:56 +02:00
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// De variabelen die umbreld invult. Alleen wat de pagina's gebruiken; de rest zou
|
|
|
|
|
// op het apparaat leeg worden en dat gebeurt hieronder ook.
|
|
|
|
|
const WAARDEN = {
|
|
|
|
|
APP_VERSION: leesVersie(app),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function leesVersie(appId) {
|
|
|
|
|
const manifest = readFileSync(join(REPO, appId, 'umbrel-app.yml'), 'utf8');
|
|
|
|
|
for (const regel of manifest.split('\n')) {
|
|
|
|
|
if (regel.startsWith('version:')) {
|
|
|
|
|
return regel.split(':')[1].trim().replace(/['"]/g, '');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return '0.0.0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── De nagemaakte status van Evolu Relay ──────────────────────────────────────
|
|
|
|
|
// Met opzet niet de gelukkige gevallen alleen: er staat een eigenaar met en zonder
|
|
|
|
|
// label, een geblokkeerde, twee geweigerde pogingen waarvan één met veel pogingen,
|
|
|
|
|
// en de teller loopt. Dat is de drukste toestand die de pagina kan hebben, en
|
|
|
|
|
// daarin vind je de opmaakfouten.
|
|
|
|
|
function relayStatus(nu) {
|
|
|
|
|
const iso = (msVanaf) => new Date(nu + msVanaf).toISOString();
|
|
|
|
|
return {
|
2026-09-08 16:36:30 +02:00
|
|
|
version: leesVersie('whatsnext-evolu-relay'),
|
2026-08-30 12:26:56 +02:00
|
|
|
relay: { reachable: true, checked: iso(-5000), publicPort: 3852 },
|
|
|
|
|
owners: {
|
|
|
|
|
problem: null,
|
|
|
|
|
learning: true,
|
|
|
|
|
learningUntil: iso(97000),
|
|
|
|
|
learningSecondsLeft: 97,
|
|
|
|
|
allowed: [
|
|
|
|
|
{
|
|
|
|
|
id: 'k7Qw2mZp9RtY4bVn6XsL1cHgJdFaEuOi',
|
|
|
|
|
allowed: true,
|
|
|
|
|
label: 'Trezor Suite, hoofdwallet',
|
|
|
|
|
firstSeen: iso(-86400000 * 2),
|
|
|
|
|
lastSeen: iso(-120000),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'p3Ne8UyTr5WqAzXc2VbNm9KlJhGfDsAq',
|
|
|
|
|
allowed: true,
|
|
|
|
|
label: null,
|
|
|
|
|
firstSeen: iso(-3600000),
|
|
|
|
|
lastSeen: iso(-45000),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
blocked: [
|
|
|
|
|
{
|
|
|
|
|
id: 'zZ9YyXxWwVvUuTtSsRrQqPpOoNnMmLlK',
|
|
|
|
|
allowed: false,
|
|
|
|
|
label: 'Oude telefoon',
|
|
|
|
|
firstSeen: iso(-86400000 * 9),
|
|
|
|
|
lastSeen: iso(-86400000 * 3),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
rejected: [
|
|
|
|
|
{
|
|
|
|
|
id: 'aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV',
|
|
|
|
|
label: null,
|
|
|
|
|
firstSeen: iso(-1800000),
|
|
|
|
|
lastSeen: iso(-30000),
|
|
|
|
|
attempts: 7,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'QqWwEeRrTtYyUuIiOoPpAaSsDdFfGgHh',
|
|
|
|
|
label: 'Laptop van de buren?',
|
|
|
|
|
firstSeen: iso(-600000),
|
|
|
|
|
lastSeen: iso(-600000),
|
|
|
|
|
attempts: 1,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
database: { bytes: 2374144, modified: iso(-120000) },
|
|
|
|
|
pendingCommand: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let html = readFileSync(bron, 'utf8');
|
|
|
|
|
|
|
|
|
|
// Doet na wat envsubst doet: bekende variabelen krijgen hun waarde, onbekende
|
|
|
|
|
// worden leeg. Dat laatste is geen slordigheid maar het gedrag op het apparaat.
|
|
|
|
|
html = html.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (heel, naam) =>
|
|
|
|
|
Object.prototype.hasOwnProperty.call(WAARDEN, naam) ? WAARDEN[naam] : '',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Het icoon staat naast de template en niet naast het resultaat.
|
|
|
|
|
html = html.replace(
|
|
|
|
|
/src="icon\.png"/g,
|
|
|
|
|
`src="${join(REPO, app, 'icon.png').replace(/\\/g, '/')}"`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// De agent nabootsen. Vóór het eigen script van de pagina, want dat begint meteen
|
|
|
|
|
// met een ronde. Alleen voor Evolu Relay; Electrum Gate leest een ander bestand en
|
|
|
|
|
// daarvoor is dit script (nog) niet uitgebreid, dus die pagina toont zijn eigen
|
|
|
|
|
// melding dat er geen gegevens zijn. Dat is genoeg voor de kop en de dialoog.
|
|
|
|
|
if (app === 'whatsnext-evolu-relay') {
|
|
|
|
|
const stub = [
|
|
|
|
|
'<script>',
|
|
|
|
|
'(function () {',
|
|
|
|
|
` var status = ${JSON.stringify(relayStatus(Date.now()))};`,
|
|
|
|
|
' window.fetch = function (url, opties) {',
|
|
|
|
|
" if (String(url).indexOf('status') !== -1) {",
|
|
|
|
|
' return Promise.resolve({ ok: true, json: function () { return Promise.resolve(status); } });',
|
|
|
|
|
' }',
|
|
|
|
|
' return Promise.resolve({ ok: true, json: function () { return Promise.resolve({}); } });',
|
|
|
|
|
' };',
|
|
|
|
|
'})();',
|
|
|
|
|
'</script>',
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
|
|
const anker = '<script>\n(function () {';
|
|
|
|
|
if (html.indexOf(anker) === -1) {
|
|
|
|
|
// Hard falen en niet stil doorgaan: zonder de nabootsing is het resultaat een
|
|
|
|
|
// pagina vol "unknown", en dan denk je dat je naar de opmaak kijkt terwijl je
|
|
|
|
|
// naar een foutmelding kijkt.
|
|
|
|
|
console.error('FOUT: het ankerpunt voor het eigen script is niet gevonden');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
html = html.replace(anker, `${stub}\n${anker}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mkdirSync(UIT, { recursive: true });
|
|
|
|
|
const doel = join(UIT, `${app}.html`);
|
|
|
|
|
writeFileSync(doel, html, 'utf8');
|
|
|
|
|
|
|
|
|
|
console.log(`Geschreven: ${doel}`);
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('Openen kan met de voorbeeldserver uit .claude/launch.json, of door het');
|
|
|
|
|
console.log('bestand rechtstreeks in een browser te slepen.');
|