All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m12s
Het audit-logboek las request.remote_addr uit in de achtergrondthread van de job. De RuntimeError die daarop volgde belandde in de foutafhandeling, die de zojuist gemaakte backup terugzette — inclusief het oude compose-bestand. - Adres vóór de threadstart vastleggen bij herconfigureren, koppelen, backup maken en terugrollen. - UniFi: init-script probeert de beheerders 'unifi' en 'root' en herkent een server zonder authenticatie, zodat ook een oud Mongo-volume gerepareerd wordt; bestaande rollen blijven staan en 'backup'/'restore' worden aangevuld. - UniFi: healthcheck van Mongo zonder inloggegevens, anders bleef de container ongezond en startte de reparatiecontainer nooit. - Backups: mongodump/mongorestore gebruiken de beheerder uit compose in plaats van de verzonnen gebruiker 'app'. - Appkaart: knop die de webinterface opent, met het adres uit /api/stacks. - Joblog: geen dubbele regels meer doordat polls elkaar konden overlappen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
66 lines
2.6 KiB
JavaScript
66 lines
2.6 KiB
JavaScript
// Draait de echte Alpine-component uit index.html en toetst dat pollJob niet
|
|
// twee keer tegelijk vraagt. Wordt aangeroepen door tests/test_ui_logica.py;
|
|
// los draaien kan met: node tests/js/joblog.test.js
|
|
const fs = require('fs');
|
|
const html = fs.readFileSync(__dirname + '/../../server-up/templates/index.html', 'utf8');
|
|
const start = html.indexOf('function app() {');
|
|
const eind = html.indexOf('</script>', start);
|
|
const bron = html.slice(start, eind);
|
|
global.localStorage = { getItem: () => null, setItem: () => {} };
|
|
global.location = { hash: '', search: '' };
|
|
global.window = { matchMedia: () => ({matches:false, addEventListener(){}}),
|
|
addEventListener(){}, innerWidth: 1200 };
|
|
global.document = { documentElement: {classList:{add(){},remove(){}}},
|
|
addEventListener(){}, querySelector: () => null };
|
|
global.navigator = { clipboard: { writeText: async () => {} }, language: 'nl' };
|
|
global.crypto = require('crypto').webcrypto;
|
|
eval(bron);
|
|
|
|
let fouten = 0;
|
|
const check = (naam, waar) => { if (!waar) { fouten++; console.log(' ✖ ' + naam); }
|
|
else console.log(' ✓ ' + naam); };
|
|
const rustig = () => new Promise(r => setTimeout(r, 0));
|
|
|
|
(async () => {
|
|
const c = app();
|
|
c.t = (k) => k;
|
|
c.$nextTick = (fn) => fn();
|
|
c.$refs = {};
|
|
|
|
// De tikken van setInterval zelf in de hand houden: zo kunnen we een tweede
|
|
// tik afvuren terwijl het antwoord op de eerste nog onderweg is — precies
|
|
// wat er gebeurt bij een trage stap zoals een backup terugzetten.
|
|
let tik = null;
|
|
global.setInterval = (fn) => { tik = fn; return 1; };
|
|
global.clearInterval = () => {};
|
|
|
|
const openstaand = [];
|
|
c.rpc = () => new Promise(res => openstaand.push(res));
|
|
|
|
c.pollJob('abc123');
|
|
tik(); await rustig();
|
|
tik(); await rustig();
|
|
tik(); await rustig();
|
|
|
|
check('maar één verzoek tegelijk onderweg', openstaand.length === 1);
|
|
|
|
openstaand.forEach(res => res({lines: [{level: 'dim', text: 'Backup wordt teruggezet…'}],
|
|
status: 'running'}));
|
|
await rustig();
|
|
|
|
check('de regel staat er één keer',
|
|
c.job.lines.filter(l => l.text === 'Backup wordt teruggezet…').length === 1);
|
|
|
|
// Daarna schuift de offset op en mag er weer gevraagd worden.
|
|
tik(); await rustig();
|
|
check('volgende tik vraagt verder vanaf de nieuwe offset', openstaand.length === 2);
|
|
|
|
let klaar = '';
|
|
c.job.timer = 1;
|
|
openstaand[1]({lines: [], status: 'done'});
|
|
await rustig();
|
|
check('afgeronde job stopt de poll', c.job.status === 'done');
|
|
|
|
console.log(fouten === 0 ? '\nAlle joblogcontroles geslaagd' : `\n${fouten} FOUTEN`);
|
|
process.exit(fouten ? 1 : 0);
|
|
})();
|