diff --git a/Docs/Plannen/Actief/003-Testclient/OPEN.md b/Docs/Plannen/Actief/003-Testclient/OPEN.md index e30bb7e..6d3fef4 100644 --- a/Docs/Plannen/Actief/003-Testclient/OPEN.md +++ b/Docs/Plannen/Actief/003-Testclient/OPEN.md @@ -43,8 +43,10 @@ 8. **`sync` en het bedieningsvlak weten niet of een verbinding er nog is.** Er staat "verbonden" zodra de instantie gemaakt is, en Evolu meldt niet dat de relay weggevallen is (hij probeert het gewoon opnieuw). - Voor de proeven is `klop` het antwoord, maar op de pagina staat een groen bolletje dat meer belooft dan - het weet. Evolu heeft een `SyncState`; uitzoeken of daar iets bruikbaars in zit. + **Deels opgelost op 09-09-2026:** het bedieningsvlak telt mislukte sockets en test na drie mislukkingen + één keer, zodat een geweigerde eigenaar niet eindeloos blijft kloppen. Wat er nog niet is, is een groen + bolletje dat werkelijk "er staat nu een verbinding" betekent in plaats van "we hebben er een geopend". + Evolu heeft een `SyncState`; uitzoeken of daar iets bruikbaars in zit. 9. **Twee toegelaten eigenaars zijn uit de allowlist van de relay verdwenen (09-09-2026), en we weten niet waarom.** Ze gaven allebei 101 en even later allebei 401, met "turned away" op de statuspagina. diff --git a/Docs/Plannen/Actief/003-Testclient/PROGRESS.md b/Docs/Plannen/Actief/003-Testclient/PROGRESS.md index 5bab52c..8253c61 100644 --- a/Docs/Plannen/Actief/003-Testclient/PROGRESS.md +++ b/Docs/Plannen/Actief/003-Testclient/PROGRESS.md @@ -88,5 +88,26 @@ kort daarvoor nog twee keer 101 gaf. **Daarmee ligt de vraag bij de relay en niet meer bij de cliënt: twee eigenaars die toegelaten waren zijn uit de allowlist verdwenen.** Wat er op de statuspagina van staat is "turned away". Wat dit kan zijn staat -in [OPEN.md](OPEN.md) punt 10; het antwoord zit in de logregels van de relay-container en die kan alleen de +in [OPEN.md](OPEN.md) punt 9; het antwoord zit in de logregels van de relay-container en die kan alleen de gebruiker lezen. + +## 09-09-2026 - "Aankloppen" heet nu "Testen", en een weigering stopt de lus + +Voorstel van de gebruiker, en het tweede deel loste meer op dan een knop. Een verbinding die geweigerd +wordt bleef namelijk eindeloos opnieuw proberen, en élke poging is een regel in de weigerlijst van de +relay; dat is precies hoe die lijst tijdens deze sessie volliep. Drie wijzigingen: + +- **de naam.** `klop` heet `test` op de opdrachtregel en "Testen" op de pagina. Het verschil met verbinden + staat er nu ook uitgelegd: testen is één vraag met een statuscode als antwoord, verbinden is een gesprek + dat Evolu eindeloos blijft herstellen; +- **"Verbinden" gaat uit zodra een test zegt dat de eigenaar geweigerd wordt**, met de uitkomst in het rood + naast de naam. Onbekend telt als toegestaan, en dat is met opzet: eerst moeten testen om iets te mogen zou + betekenen dat je altijd een poging in de weigerlijst zet voordat je iets kunt doen. "Blob schrijven" gaat + door dezelfde poort en schrijft dan lokaal verder; +- **een lopende verbinding stopt zichzelf.** Na drie mislukte sockets wordt er precies één keer getest; is + het een weigering, dan gaat de verbinding eruit. Is het geen weigering, dan blijft hij staan, want dan is + het een relay die even weg is en daar is opnieuw proberen juist goed voor. + +De melding in een dialoogvenster is eruit: de uitkomst staat naast de naam en in het log, en die blijven +staan. In de browser nagekeken: testen geeft 401, de regel verschijnt in het rood, en "Verbinden" is +uitgegrijsd voor de geteste eigenaar terwijl de ongeteste hem gewoon houdt. diff --git a/Docs/Plannen/Actief/003-Testclient/TAKEN.md b/Docs/Plannen/Actief/003-Testclient/TAKEN.md index e8c776c..d2a31f8 100644 --- a/Docs/Plannen/Actief/003-Testclient/TAKEN.md +++ b/Docs/Plannen/Actief/003-Testclient/TAKEN.md @@ -42,9 +42,11 @@ - [x] **Een blob schrijven met een instelbare omvang, en teruglezen.** `schrijf` en `lees`, met `--bytes` en `--lokaal` - [x] **De losse WebSocket-poging die de HTTP-statuscode wél te zien krijgt:** `src/probe.js` en de - opdracht `klop`. Met `node:http` en niet met de WebSocket van Node, want die geeft je bij een - weigering een `error` en geen 401. Daarmee is [OPEN.md](OPEN.md) punt 1 opgelost vóórdat het een - probleem werd + opdracht `test` (tot 09-09-2026 `klop` geheten, hernoemd op voorstel van de gebruiker). Met + `node:http` en niet met de WebSocket van Node, want die geeft je bij een weigering een `error` en + geen 401. Daarmee is [OPEN.md](OPEN.md) punt 1 opgelost vóórdat het een probleem werd +- [x] **`spiegel `**, de enige proef die van de cliëntkant kan bewijzen dat er werkelijk iets op de + relay staat: dezelfde eigenaar in een lege database ernaast ## Fase 3 - het bedieningsvlak (af, 09-09-2026) @@ -57,6 +59,10 @@ nodig is, waarschuwen als `.env` ontbreekt, openen de browser en houden het venster open bij een fout - [x] **In de browser nagekeken** (09-09-2026): een blob schrijven en teruglezen werkt vanuit de pagina, de gegevens zijn dezelfde als die van de opdrachtregel, en licht en donker kloppen allebei +- [x] **"Verbinden" gaat uit na een weigering** (09-09-2026, voorstel van de gebruiker), en een lopende + verbinding die blijft falen test zichzelf één keer en stopt bij een weigering. Dat laatste is de + eigenlijke opbrengst: zonder die rem vult één geweigerde verbinding de weigerlijst van de relay met + pogingen, en dat is tijdens deze sessie ook gebeurd ## Fase 4 - de proeven diff --git a/tools/relay-client/src/argumenten.js b/tools/relay-client/src/argumenten.js index 2158e54..a038feb 100644 --- a/tools/relay-client/src/argumenten.js +++ b/tools/relay-client/src/argumenten.js @@ -12,7 +12,7 @@ export const OPDRACHTEN = { lijst: { argumenten: [], uitleg: 'Toont de eigenaars in het register' }, schrijf: { argumenten: ['naam', 'label'], uitleg: 'Schrijft een blob weg voor deze eigenaar' }, lees: { argumenten: ['naam'], uitleg: 'Toont de blobs van deze eigenaar' }, - klop: { argumenten: ['naam'], uitleg: 'Klopt aan bij de relay en toont de HTTP-status' }, + test: { argumenten: ['naam'], uitleg: 'Test of de relay deze eigenaar toelaat, zonder te verbinden' }, spiegel: { argumenten: ['naam'], uitleg: 'Haalt deze eigenaar op in een lege database, via de relay' }, sync: { argumenten: ['naam'], uitleg: 'Verbindt en blijft draaien tot Ctrl-C' }, vergeet: { argumenten: ['naam'], uitleg: 'Haalt een eigenaar uit het register' }, diff --git a/tools/relay-client/src/cli.js b/tools/relay-client/src/cli.js index 332e808..3d21291 100644 --- a/tools/relay-client/src/cli.js +++ b/tools/relay-client/src/cli.js @@ -146,10 +146,10 @@ const opdrachten = { } }, - klop: async ({ naam }) => { + test: async ({ naam }) => { eisRelay(); const { regel } = haalEigenaarOp(naam); - console.log(`Aankloppen bij ${config.relayUrl} als ${regel.ownerId}`); + console.log(`Testen bij ${config.relayUrl} als ${regel.ownerId}`); const uitkomst = await klopAan(config.relayUrl, regel.ownerId); console.log(` status ${uitkomst.status ?? '-'}`); console.log(` uitkomst ${uitkomst.uitleg}`); diff --git a/tools/relay-client/src/index.html b/tools/relay-client/src/index.html index 1ff8de7..b8adf4f 100644 --- a/tools/relay-client/src/index.html +++ b/tools/relay-client/src/index.html @@ -175,6 +175,13 @@ : 'niet verbonden'; rij.append(toestand); + if (eigenaar.test) { + const test = document.createElement('span'); + test.className = eigenaar.test.geaccepteerd ? 'melding' : 'melding fout'; + test.textContent = `getest ${tijd(eigenaar.test.tijd)}: ${eigenaar.test.status ?? '-'} ${eigenaar.test.uitleg}`; + rij.append(test); + } + const duw = document.createElement('div'); duw.className = 'duw'; @@ -184,14 +191,26 @@ omvang.style.width = '6em'; omvang.addEventListener('input', () => omvangPer.set(eigenaar.naam, omvang.value)); + // Verbinden gaat uit zodra een test zegt dat deze eigenaar geweigerd + // wordt. Niet uit netheid: Evolu blijft bij een weigering eindeloos + // opnieuw proberen, en élke poging is een regel in de weigerlijst van de + // relay. Onbekend telt als toegestaan, want anders moet je eerst testen + // om iets te mogen, en dat is zelf ook een poging. + const verbindKnop = eigenaar.verbonden + ? knop('Verbreken', () => roep('verbreek', { naam: eigenaar.naam })) + : knop('Verbinden', () => roep('verbind', { naam: eigenaar.naam })); + + if (!eigenaar.verbonden && !eigenaar.magVerbinden) { + verbindKnop.disabled = true; + verbindKnop.title = 'De relay weigert deze eigenaar. Laat hem toe op de statuspagina van de app en test opnieuw.'; + } + duw.append( - knop('Aankloppen', async () => { - const uit = await roep('klop', { naam: eigenaar.naam }); - alert(`status ${uit.status ?? '-'}: ${uit.uitleg}`); - }), - eigenaar.verbonden - ? knop('Verbreken', () => roep('verbreek', { naam: eigenaar.naam })) - : knop('Verbinden', () => roep('verbind', { naam: eigenaar.naam })), + // Geen melding in een dialoogvenster: de uitkomst komt naast de naam te + // staan en in het log, en die blijven staan. Een dialoog moet je + // wegklikken en is daarna weg. + knop('Testen', () => roep('test', { naam: eigenaar.naam })), + verbindKnop, omvang, knop('Blob schrijven', () => roep('schrijf', { naam: eigenaar.naam, label: `blob ${tijd(new Date().toISOString())}`, bytes: omvang.value }), diff --git a/tools/relay-client/src/ui.js b/tools/relay-client/src/ui.js index 24ca055..d858d41 100644 --- a/tools/relay-client/src/ui.js +++ b/tools/relay-client/src/ui.js @@ -28,6 +28,27 @@ export const startUi = async ({ config, register, databasesIn }) => { const open = new Map(); const log = []; + /** + * De laatste uitkomst van "Testen", per eigenaar. + * + * Hierop wordt de knop "Verbinden" uitgezet. Onbekend telt als toegestaan, en + * dat is met opzet: anders moet je eerst testen voordat je iets mag, en een + * test is zelf een poging die in de weigerlijst van de relay belandt. + */ + const laatsteTest = new Map(); + + /** + * Hoeveel sockets er achter elkaar mislukt zijn, per eigenaar. + * + * Evolu is local-first en blijft eindeloos opnieuw proberen. Dat is bij een + * relay die even weg is precies goed, maar bij een eigenaar die geweigerd + * wordt is elke poging een regel in de weigerlijst van de relay. Vandaar de + * teller: na een paar mislukkingen wordt er één keer getest, en bij een + * weigering gaat de verbinding eruit in plaats van door te blijven kloppen. + */ + const socketFouten = new Map(); + const FOUTEN_VOOR_TEST = 3; + const meld = (tekst) => { log.unshift({ tijd: new Date().toISOString(), tekst }); if (log.length > MAX_LOG) log.length = MAX_LOG; @@ -48,6 +69,33 @@ export const startUi = async ({ config, register, databasesIn }) => { meld(`${naam}: defect uit een worker, zie de terminal`); console.error(defect); }, + onWebSocket: (bericht) => { + if (bericht.wat === 'open') { + socketFouten.set(naam, 0); + meld(`${naam}: socket open`); + return; + } + if (bericht.wat !== 'dicht') return; + + const aantal = (socketFouten.get(naam) ?? 0) + 1; + socketFouten.set(naam, aantal); + if (aantal !== FOUTEN_VOOR_TEST) return; + + // Precies één keer testen, bij de derde mislukking. Daarna weten we of + // dit een weigering is of een relay die even weg is, en dat scheelt de + // relay een weigerlijst vol pogingen. + meld(`${naam}: ${aantal} sockets mislukt (${bericht.detail}), wordt getest`); + void (async () => { + const uitkomst = await klopAan(config.relayUrl, regel.ownerId); + laatsteTest.set(naam, { ...uitkomst, tijd: new Date().toISOString() }); + if (uitkomst.geaccepteerd) { + meld(`${naam}: de relay laat hem toe, dus dit is iets anders dan een weigering`); + return; + } + meld(`${naam}: geweigerd (${uitkomst.uitleg}), verbinding wordt verbroken`); + await verbreek(naam); + })(); + }, }); const ingang = { store, sinds: new Date().toISOString(), metRelay }; @@ -60,6 +108,7 @@ export const startUi = async ({ config, register, databasesIn }) => { const ingang = open.get(naam); if (!ingang) return; open.delete(naam); + socketFouten.delete(naam); await ingang.store.sluit(); meld(`${naam} losgekoppeld`); }; @@ -76,6 +125,7 @@ export const startUi = async ({ config, register, databasesIn }) => { blobs = null; } } + const test = laatsteTest.get(regel.naam) ?? null; eigenaars.push({ naam: regel.naam, ownerId: regel.ownerId, @@ -85,6 +135,11 @@ export const startUi = async ({ config, register, databasesIn }) => { sinds: ingang?.sinds ?? null, transportUrl: ingang?.store.transportUrl ?? null, blobs, + test, + // Onbekend telt als toegestaan: eerst moeten testen om te mogen + // verbinden zou betekenen dat je altijd een poging in de weigerlijst + // van de relay zet voordat je iets kunt doen. + magVerbinden: test === null || test.geaccepteerd === true, }); } return { relayUrl: config.relayUrl, eigenaars, log }; @@ -115,7 +170,12 @@ export const startUi = async ({ config, register, databasesIn }) => { if (!Number.isInteger(omvang) || omvang < 1 || omvang > 8 * 1024 * 1024) { throw new Error('bytes moet tussen 1 en 8388608 liggen.'); } - const ingang = open.get(naam) ?? (await verbind(naam, Boolean(config.relayUrl))); + // Schrijven opent zo nodig zelf een verbinding, dus dezelfde poort als bij + // de knop "Verbinden": een geweigerde eigenaar gaat lokaal verder in + // plaats van de weigerlijst van de relay te vullen. + const test = laatsteTest.get(naam); + const geweigerd = test != null && test.geaccepteerd === false; + const ingang = open.get(naam) ?? (await verbind(naam, Boolean(config.relayUrl) && !geweigerd)); const body = new Uint8Array(omvang); for (let i = 0; i < omvang; i += 1) body[i] = i % 256; const geschreven = await ingang.store.schrijf(label || 'blob', body); @@ -135,17 +195,19 @@ export const startUi = async ({ config, register, databasesIn }) => { }; }, - klop: async ({ naam }) => { + test: async ({ naam }) => { const regel = register.zoek(naam); if (!regel) throw new Error(`Geen eigenaar "${naam}".`); if (!config.relayUrl) throw new Error('Geen relay-adres. Zet RELAY_URL in .env.'); const uitkomst = await klopAan(config.relayUrl, regel.ownerId); - meld(`${naam}: aankloppen gaf ${uitkomst.status ?? '-'}, ${uitkomst.uitleg}`); + laatsteTest.set(naam, { ...uitkomst, tijd: new Date().toISOString() }); + meld(`${naam}: testen gaf ${uitkomst.status ?? '-'}, ${uitkomst.uitleg}`); return uitkomst; }, vergeet: async ({ naam }) => { await verbreek(naam); + laatsteTest.delete(naam); const weg = register.vergeet(naam); meld(`${weg.naam} uit het register gehaald`); return { naam: weg.naam };