// ═══════════════════════════════════════════════════════════════════════════════ // 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/.html` en die map is gitignored. // // ── Waarom dit bestaat ─────────────────────────────────────────────────────── // // Een `index.html.template` is niet te openen. Twee dingen staan in de weg, en ze // zijn allebei fundamenteel en niet op te lossen door er anders naar te kijken: // // 1. er staan accolade-variabelen in die umbreld invult. Onopgelost staat er // letterlijk "v${APP_VERSION}" in de kop; // 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'; const bron = join(REPO, app, 'index.html.template'); if (!existsSync(bron)) { console.error(`FOUT: ${bron} bestaat niet`); 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 { 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 = [ '', ].join('\n'); const anker = '