Negen punten uit echt gebruik met 0.4.0, alle negen gedaan. Twee ervan waren onderzoeksvragen en die staan onderaan. Evolu Relay 0.5.0 - Een tijdvenster van twee minuten voor nieuwe eigenaars, met een stopknop. De teller zit in het relay-proces en niet in de pagina: een teller in een tabblad dat je sluit, sluit de deur niet. policy.js kreeg learningUntil, isLearningOpen en expireLearning. - decideOwner kijkt naar isLearningOpen en niet naar het veld learning. De lus die een verlopen venster opruimt loopt elke twee seconden, en in dat gat zou een onbekende alsnog binnenkomen. - Zonder STATE_VERSION te verhogen, met een toets die dat verdedigt: een verhoging zou de allowlist van de draaiende installatie laten afwijzen en de deur sluiten voor eigenaars die er al in stonden. - Labels op een eigenaar-id, in een eigen labels.json met de agent als enige schrijver. Een label zegt niets over toegang, dus de relay hoeft het niet te weten; het is daardoor meteen opgeslagen en werkt ook als de relay omligt. - Geblokkeerde en geweigerde eigenaars in een kader, met een badge die zegt welke van de twee het is. De badge staat buiten het hover-blok, anders is dat onderscheid onzichtbaar tenzij je over de regel gaat. - Maatvoering gelijk aan Electrum Gate: 1760px, hetzelfde raster, icoon van 64 pixels, dezelfde kop, versienummer erachter. Uitleg uit de kaders, knoppen pas bij hover, geen voetregel. Electrum Gate 0.0.24 - Menu-item "About this app", in beide apps. - De statuswidget zei "Answering" met "answered in 7 ms, from inside the app" en zegt nu "Running" met de meting eronder. De nuance dat de controle van container naar container loopt is verplaatst naar een eigen kopje in die dialoog, waar er ruimte voor is; vier woorden waren te weinig. Toetsen en gereedschap - tests/test_relay_agent.py (nieuw, 65 toetsen) en tests/test_paginas_parsen.mjs (nieuw). Muteertests gedraaid op de beslissende regels. - Een dollarteken-toets in test_appstore_vorm.py. Het commentaar in drie bestanden beweerde al dat die test bestond; nu is dat waar. - Een toets dat er geen werkbestanden in een app-map staan. umbreld kopieert de hele map naar het apparaat en in de back-up. - tools/voorbeeldpagina.mjs maakt van een *.template een pagina die je in een browser kunt openen. Dat vond meteen twee echte opmaakfouten. De twee onderzoeksvragen - Een geweigerde eigenaar komt niet in de database: isOwnerAllowed zit in de WebSocket-upgrade, dus het is een 401 en een gesloten socket. Het gewenste gevolg treedt wel op, via de client: die is local-first en levert bij toelating de hele geschiedenis. Blokkeren werkt daarentegen pas bij de volgende verbinding, en dat staat als open punt. - De blobs zijn niet met een xpub te ontcijferen; een OwnerId komt daar niet uit. Met de SLIP-21-node van het apparaat kan het wel, maar die geeft volledige zeggenschap, dus dat hoort niet in een relay. Als plan-punt opgenomen bij de tool in HomeGit/Trezor. Nog niet uitgerold: de image 0.5.0 moet gebouwd en geduwd worden. De digest staat daarom niet in de compose, want een oude digest onder een nieuwe tag levert stil de oude relay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 lines
7.3 KiB
JavaScript
183 lines
7.3 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// 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 ───────────────────────────────────────────────────────
|
|
//
|
|
// 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 = [
|
|
'<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.');
|