diff --git a/tests/test_limiter.mjs b/tests/test_limiter.mjs index 57a2757..596997e 100644 --- a/tests/test_limiter.mjs +++ b/tests/test_limiter.mjs @@ -141,6 +141,10 @@ const VREEMD = 'owner-van-een-ander'; const alsnog = applyCommand(geweigerd, { action: 'allow', ownerId: VREEMD }, LATER); toets('een geweigerde poging kan alsnog toegelaten worden', alsnog.state.owners.length === 1); toets('en verdwijnt dan uit de weigeringen', alsnog.state.rejected.length === 0); + toets( + 'en houdt het moment waarop hij zich meldde, niet dat van de klik', + alsnog.state.owners[0].firstSeen === NU, + ); toets( 'en mag er daarna in, ook met de leerstand uit', decideOwner(alsnog.state, VREEMD, LATER).allowed === true, diff --git a/tools/evolu-relay/build.sh b/tools/evolu-relay/build.sh index 8e0ae7b..f0e9d2c 100644 --- a/tools/evolu-relay/build.sh +++ b/tools/evolu-relay/build.sh @@ -36,7 +36,7 @@ set -eu # verandert, want anders rolt umbrelOS hem niet uit. Dat heeft hier een keer een # dag gekost. Ze zijn dus gelijk zolang alleen de image wijzigt, en lopen uiteen # zodra er een reparatie in de app-map zit. -VERSION="0.2.0" +VERSION="0.3.0" # Het register staat er expres in en dit is geen smaakkwestie: umbreld haalt élke # image op via de Docker Engine API, dus een tag die alleen lokaal bestaat is voor diff --git a/tools/evolu-relay/src/index.js b/tools/evolu-relay/src/index.js index 16c4170..55423cb 100644 --- a/tools/evolu-relay/src/index.js +++ b/tools/evolu-relay/src/index.js @@ -115,6 +115,20 @@ process.on('SIGINT', stopCleanly); // ── De relay ────────────────────────────────────────────────────────────────── +// Eén regel per eigenaar per keer dat dit proces draait, en niet per verbinding. +// Zonder deze rem staat het log vol zodra een cliënt opnieuw verbindt; zónder +// logging kostte het op 28-08-2026 juist een half uur om te zien dat een tweede +// eigenaar aan de deur stond te kloppen. De bovengrens erop is er omdat een +// weigering een id bevat dat de ander zelf verzint. +const MAX_ANNOUNCED = 100; +const announced = new Set(); + +const announceOnce = (key, message) => { + if (announced.has(key)) return; + if (announced.size < MAX_ANNOUNCED) announced.add(key); + console.log(message); +}; + await runMain({ ...createRelayDeps(), console })( createRelay({ port, @@ -131,12 +145,32 @@ await runMain({ ...createRelayDeps(), console })( state = decision.state; if (decision.changed) markDirty(); + + const short = typeof ownerId === 'string' ? ownerId.slice(0, 12) : '(unusable)'; if (decision.reason === 'learned') { - console.log('[info] learned a new owner; turn learning off on the status page when done'); + console.log(`[info] learned a new owner ${short}; one app can use more than one, so keep ` + + 'learning open until nothing new shows up'); + } else if (decision.allowed) { + announceOnce(`ok:${short}`, `[info] owner ${short} connected`); + } else { + announceOnce( + `no:${short}`, + `[info] refused owner ${short} (${decision.reason}); allow it on the status page if it is ` + + 'yours', + ); } return decision.allowed; }, - isOwnerWithinQuota: (_ownerId, requiredBytes) => requiredBytes <= maxWriteBytes, + isOwnerWithinQuota: (ownerId, requiredBytes) => { + if (requiredBytes <= maxWriteBytes) return true; + const short = typeof ownerId === 'string' ? ownerId.slice(0, 12) : '(unusable)'; + announceOnce( + `quota:${short}`, + `[warn] refused a write of ${requiredBytes} bytes from ${short}; the limit is ` + + `${maxWriteBytes}. Raise RELAY_MAX_WRITE_BYTES if this is normal for your app.`, + ); + return false; + }, }), ); diff --git a/tools/evolu-relay/src/policy.js b/tools/evolu-relay/src/policy.js index 3320b37..c0c3253 100644 --- a/tools/evolu-relay/src/policy.js +++ b/tools/evolu-relay/src/policy.js @@ -169,11 +169,26 @@ export const applyCommand = (state, command, now) => { const known = findOwner(state, command.ownerId); // Ook een eigenaar die alleen als geweigerde poging bekend is, mag hiermee // alsnog toegelaten worden. Dat is de knop naast zo'n regel op de pagina. + // + // De geschiedenis van die pogingen gaat mee. Dat leek een detail tot het + // op 28-08-2026 misging: de pagina meldde daarna "first seen" op het moment + // van de klik, terwijl die eigenaar al twintig minuten aan de deur stond. + // Juist bij een eigenaar die je alsnog toelaat wil je kunnen terugzien + // sinds wanneer hij er is. + const attempted = state.rejected.find((entry) => entry.id === command.ownerId); const owners = known ? state.owners.map((owner) => owner.id === command.ownerId ? { ...owner, allowed: true } : owner, ) - : [...state.owners, { id: command.ownerId, allowed: true, firstSeen: now, lastSeen: null }]; + : [ + ...state.owners, + { + id: command.ownerId, + allowed: true, + firstSeen: attempted && attempted.firstSeen ? attempted.firstSeen : now, + lastSeen: attempted ? attempted.lastSeen : null, + }, + ]; return { state: { ...state, diff --git a/whatsnext-evolu-relay/docker-compose.yml b/whatsnext-evolu-relay/docker-compose.yml index 65a0c9f..d13e16a 100644 --- a/whatsnext-evolu-relay/docker-compose.yml +++ b/whatsnext-evolu-relay/docker-compose.yml @@ -45,10 +45,15 @@ services: # bepaalt de digest wat er gehaald wordt en is de tag alleen leesbaarheid. # Houd de tag gelijk aan `version` in het manifest en aan VERSION in build.sh. # - # Geduwd op 28-08-2026. Let op: dit is één architectuur (amd64), want er is - # alleen amd64 gebouwd. Voor de officiele store hoort er een multi-arch - # index-digest met arm64 in; zie het masterplan Publicatie-Relay. - image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.2.0@sha256:5b2b4228c24a7e8a12a99869ca39729f47a0a01d6a346c623b22d6e62f2918c2 + # Let op: dit is één architectuur (amd64), want er is alleen amd64 gebouwd. + # Voor de officiele store hoort er een multi-arch index-digest met arm64 in; + # zie het masterplan Publicatie-Relay. + # + # TODO de digest van 0.3.0 erachter zetten zodra hij geduwd is: + # sudo docker inspect --format '{{index .RepoDigests 0}}' + # De digest van 0.2.0 is er expres uit gehaald en niet blijven staan: die + # wijst naar de vorige image, en dan draait er iets anders dan de tag belooft. + image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.3.0 restart: on-failure ports: # De relay zelf. Dit is de poort waar Zoraxy met TLS naartoe wijst, en de diff --git a/whatsnext-evolu-relay/index.html.template b/whatsnext-evolu-relay/index.html.template index 38217d0..df53a9a 100644 --- a/whatsnext-evolu-relay/index.html.template +++ b/whatsnext-evolu-relay/index.html.template @@ -184,15 +184,21 @@ button.action.danger { background: var(--neg-bg); border-color: var(--neg); colo flex-wrap: wrap; } .owner:first-of-type { border-top: none; } + +/* De naam en de gegevens staan onder elkaar in één kolom, en de knoppen worden + met margin-left naar rechts geduwd. Zo staan ze bij elke regel op dezelfde + plek. Eerder stonden id, meta en knoppen náást elkaar, en dan bepaalde de + lengte van de meta-regel of de rij afbrak: een eigenaar met een 'last seen' + duwde zijn knoppen naar de volgende regel en dus naar links, een eigenaar + zonder hield ze rechts. */ +.owner .who { flex: 1 1 240px; min-width: 0; } .owner .id { font-family: var(--font-mono); font-size: 0.8rem; overflow-wrap: anywhere; - flex: 1; - min-width: 220px; } -.owner .meta { color: var(--text-ter); font-size: 0.75rem; } -.owner .buttons { display: flex; gap: 8px; } +.owner .meta { color: var(--text-ter); font-size: 0.75rem; margin-top: 2px; } +.owner .buttons { display: flex; gap: 8px; margin-left: auto; flex: 0 0 auto; } .empty { color: var(--text-ter); font-size: 0.88rem; font-style: italic; } @@ -257,8 +263,7 @@ footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin-
unknown

- While this is open, the next device that connects is accepted and remembered. Close it once your - own devices are paired. + While this is open, owners that connect are accepted and remembered.

@@ -369,11 +374,14 @@ footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin- var row = document.createElement('div'); row.className = 'owner'; + var who = document.createElement('div'); + who.className = 'who'; + var id = document.createElement('div'); id.className = 'id'; id.textContent = shorten(entry.id); id.title = entry.id; - row.appendChild(id); + who.appendChild(id); var meta = document.createElement('div'); meta.className = 'meta'; @@ -382,7 +390,9 @@ footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin- if (entry.lastSeen) parts.push('last seen ' + formatTime(entry.lastSeen)); if (entry.attempts) parts.push(entry.attempts + ' attempts'); meta.textContent = parts.join(' · '); - row.appendChild(meta); + who.appendChild(meta); + + row.appendChild(who); var holder = document.createElement('div'); holder.className = 'buttons'; @@ -433,8 +443,9 @@ footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin- setPill(learningPill, 'open', 'ok'); toggle.textContent = 'Close'; toggle.disabled = false; - learningHint.textContent = 'The next device that connects is accepted and remembered. Close ' - + 'this once your own devices are paired.'; + learningHint.textContent = 'Any owner that connects is accepted and remembered. One app can ' + + 'use more than one owner, so wait until nothing new appears for a minute before you close ' + + 'this.'; } else if (status.owners.learning === false) { setPill(learningPill, 'closed', 'unknown'); toggle.textContent = 'Open'; diff --git a/whatsnext-evolu-relay/umbrel-app.yml b/whatsnext-evolu-relay/umbrel-app.yml index 91850cc..e87178f 100644 --- a/whatsnext-evolu-relay/umbrel-app.yml +++ b/whatsnext-evolu-relay/umbrel-app.yml @@ -11,7 +11,7 @@ manifestVersion: 1 id: whatsnext-evolu-relay category: files name: Evolu Relay -version: "0.2.2" +version: "0.3.0" tagline: Encrypted sync and backup for your local-first apps description: >- Local-first apps keep your data on your own device and work whether or not you have a