Relay 0.7.0: een weg naar binnen, en de melding bij elke klik weg
Vier punten van de gebruiker, opgekomen tijdens het beproeven met de testclient. De getimede leerstand is eruit; de wachtlijst is de enige weg naar binnen. Zijn redenering: allebei de wegen vragen iemand die bij de app kan, dus het is dubbelop, en het venster is de zwakste omdat het iedereen toelaat die er toevallig in verbindt. Dat weegt zwaarder nu de relay op een publiek wss-adres kan staan. De oorspronkelijke reden voor de leerstand, dat je je eigen OwnerId nergens kon aflezen, verviel toen de weigerlijst dat id ging tonen. Daarmee verdwijnt ook de bug die hij dezelfde dag meldde: een geleerde eigenaar bleef in de weigerlijst staan terwijl hij al kon schrijven en lezen, want decideOwner haalde hem niet van die lijst af en de knop allow wel. "Refused owners" heet "Waiting list", met de badge Waiting en een teller waar de widget van het tijdvenster stond. Het veld op schijf blijft rejected: hernoemen zou een migratie zijn voor een woord dat niemand ziet. Het adres onderaan zei http:// en dat kan nergens werken, want de relay spreekt WebSocket en nooit HTTP. Nu ws://<host>:3852, met een regel over wss://<domein> zonder poort achter een reverse proxy. Dat is precies de fout die diezelfde dag een ronde kostte bij het koppelen van de testclient. De melding bij elke klik is weg. Die stond in de gewone stroom van de pagina, dus alles eronder schoof omlaag en weer omhoog. Nu gaan de knoppen in de lijsten even op slot tot de ronde de nieuwe stand heeft; foutmeldingen blijven wel staan, want die zeggen iets wat je nergens anders ziet. STATE_VERSION blijft 1 en een owners.json van 0.6.0 leest door: learning en learningUntil worden gelezen, genegeerd en niet teruggeschreven. Een verhoging zou store.js de allowlist van een werkende installatie opzij laten schuiven. Twee toetsen bewaken dat de leerstand niet terugsluipt: een onbekende eigenaar wordt geweigerd ook met learning: true in het bestand, en set-learning is een onbekende actie. Beide mutatie-getest. De compose staat op 0.7.0 zonder digest, zodat het hard faalt tot de image bestaat. Bouwen, duwen en pinnen ligt bij de gebruiker. Suite: 424 goed, 0 fout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+104
-139
@@ -431,6 +431,20 @@ body {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* De regel onder het adres: hoe het van buiten heet. Geen kader eromheen, want
|
||||
het is uitleg bij de waarde erboven en geen tweede waarde. */
|
||||
.note {
|
||||
margin: 0.7rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-sec);
|
||||
}
|
||||
.note code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.95em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Lijstregels ────────────────────────────────────────────────────────── */
|
||||
.list-item {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
@@ -462,6 +476,13 @@ body {
|
||||
.btn:hover { background: var(--neutral-bg); }
|
||||
.btn:active { transform: scale(0.97); }
|
||||
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||
|
||||
/* Bevroren terwijl er een opdracht onderweg is. Alleen de lijsten, want alleen
|
||||
daar staan knoppen die iets veranderen, en de kaders erboven horen gewoon te
|
||||
blijven bijwerken. Geen verschuiving en geen overlay: dit verandert niets aan
|
||||
de afmetingen, en dat is precies waarom het de melding bovenaan vervangt. */
|
||||
body.is-busy .list-item { opacity: 0.55; }
|
||||
body.is-busy .list-item button { cursor: progress; }
|
||||
.btn-solid { background: var(--accent); color: #fff; border-color: transparent; }
|
||||
.btn-solid:hover { background: var(--accent); opacity: 0.88; }
|
||||
.btn-danger { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 45%, transparent); }
|
||||
@@ -710,28 +731,17 @@ body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Het tijdvenster voor nieuwe eigenaars. De twee knoppen staan in de kop
|
||||
en verschijnen bij hover over dit kader.
|
||||
|
||||
De teller loopt in het relay-proces en niet hier: een teller in de
|
||||
browser verdwijnt als je het tabblad sluit, en dan blijft de deur open
|
||||
staan zonder dat iemand dat ziet. Wat de pagina doet is de resterende
|
||||
tijd van de agent aftellen tussen twee rondes. -->
|
||||
<!-- Hoeveel eigenaars er op een beslissing wachten.
|
||||
Tot 0.6.0 stond hier het tijdvenster voor nieuwe eigenaars, met een
|
||||
knop om het twee minuten open te zetten. Dat is eruit in 0.7.0: er is
|
||||
nog één weg naar binnen, en dat is de wachtlijst hieronder. Twee wegen
|
||||
was dubbelop, en het venster was de zwakste van de twee omdat het
|
||||
iederéén toeliet die er toevallig in verbond. -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h2 class="t-h3">New owners</h2>
|
||||
<!-- "Open" en niet "Open 2 min": de twee minuten staan op de onderregel,
|
||||
waar er ruimte voor is. Met de lange tekst erin brak de kop van dit
|
||||
kader over twee regels en stond de waarde lager dan in de drie
|
||||
kaders ernaast. -->
|
||||
<div class="hover-only">
|
||||
<button class="btn btn-solid hidden" id="learn-open" type="button">Open</button>
|
||||
<button class="btn btn-danger hidden" id="learn-stop" type="button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-head"><h2 class="t-h3">Waiting</h2></div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-val text unknown" id="learning-state">unknown</div>
|
||||
<div class="stat-sub" id="learning-sub"></div>
|
||||
<div class="stat-val text unknown" id="waiting-state">unknown</div>
|
||||
<div class="stat-sub" id="waiting-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- De statuswidget staat RECHTS en niet links (keuze gebruiker,
|
||||
@@ -759,14 +769,20 @@ body {
|
||||
<div id="allowed"></div>
|
||||
</div>
|
||||
|
||||
<!-- Geweigerd en geblokkeerd in één kader, op verzoek van de gebruiker
|
||||
<!-- Wachtend en geblokkeerd in één kader, op verzoek van de gebruiker
|
||||
(30-08-2026). Het waren twee kaders, en het onderscheid ertussen is voor
|
||||
de gebruiker geen verschil in wat hij ermee doet: allebei zijn ze
|
||||
buitengesloten en allebei kun je ze toelaten. Dat het bij de een een
|
||||
eerdere keuze van jou was en bij de ander een poging van buiten, staat als
|
||||
badge op de regel. -->
|
||||
badge op de regel.
|
||||
|
||||
"Waiting list" en niet "Refused owners" sinds 0.7.0, op voorstel van de
|
||||
gebruiker. Dat is wat het is: kandidaten die op een beslissing wachten, en
|
||||
sinds het tijdvenster eruit is, is dit de enige weg naar binnen. Het veld
|
||||
heet in owners.json nog `rejected`; hernoemen zou een migratie zijn voor
|
||||
een woord dat niemand ziet. -->
|
||||
<div class="card col-6">
|
||||
<div class="card-head"><h2 class="t-h3">Refused owners</h2></div>
|
||||
<div class="card-head"><h2 class="t-h3">Waiting list</h2></div>
|
||||
<div id="refused"></div>
|
||||
</div>
|
||||
|
||||
@@ -776,6 +792,15 @@ body {
|
||||
<div class="card col-full">
|
||||
<div class="card-head"><h2 class="t-h3">Relay address</h2></div>
|
||||
<div class="url-box" id="relay-url">unknown</div>
|
||||
<!-- De tweede regel is er sinds 0.7.0. Het adres hierboven werkt binnen je
|
||||
eigen netwerk; van buiten loopt het via een reverse proxy, en dan is de
|
||||
vorm anders én is dit de meest gemaakte fout: het schema wordt wss://
|
||||
en de poort valt weg, want de proxy luistert op 443. -->
|
||||
<p class="note">
|
||||
That address works inside your own network. Through a reverse proxy with a certificate it
|
||||
becomes <code>wss://your.domain</code>, <strong>without</strong> a port number: the proxy
|
||||
listens on 443 and forwards to this one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -833,14 +858,15 @@ body {
|
||||
<h3>Who may use it</h3>
|
||||
<p>
|
||||
What this app adds is the part a self-hosted relay needs and a public one does not: control
|
||||
over who may use it. Opening the window for new owners accepts and remembers whoever connects
|
||||
while it is open, so open it only while you pair a device of your own. One app can use more
|
||||
than one owner id, so wait until nothing new appears before you close it again.
|
||||
over who may use it. Nobody gets in by default. An owner that connects for the first time is
|
||||
turned away and lands on the <strong>waiting list</strong> with its owner id, and you decide
|
||||
there whether it may stay. One app can use more than one owner id, so expect more than one
|
||||
row per device.
|
||||
</p>
|
||||
<p>
|
||||
An owner that is refused syncs nothing at all, and nothing of it is stored: the relay turns
|
||||
the connection away before it starts. Its device keeps everything locally and retries, so
|
||||
allowing it later still brings the whole history across.
|
||||
An owner on the waiting list syncs nothing at all, and nothing of it is stored: the relay
|
||||
turns the connection away before it starts. Its device keeps everything locally and retries,
|
||||
so allowing it later still brings the whole history across, and you do not have to be quick.
|
||||
</p>
|
||||
<p>
|
||||
Owner ids are long and meaningless on purpose. Hover a row and use <strong>Label</strong> to
|
||||
@@ -854,12 +880,6 @@ body {
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Hoelang de deur open gaat als je op de knop drukt. Twee minuten, op verzoek
|
||||
// van de gebruiker (30-08-2026). Deze waarde gaat mee in de opdracht; de agent
|
||||
// en het relay-proces begrenzen hem allebei nog een keer, want dit is de kant
|
||||
// die van buiten komt.
|
||||
var LEARNING_SECONDS = 120;
|
||||
|
||||
var root = document.documentElement;
|
||||
|
||||
// Eén opdracht tegelijk: de agent weigert een tweede zolang de eerste nog in de
|
||||
@@ -885,12 +905,6 @@ body {
|
||||
// een class-attribuut die er ook om opmaakredenen kan staan.
|
||||
var refusal = false;
|
||||
|
||||
// De resterende seconden van het tijdvenster, zoals de agent ze doorgaf, en het
|
||||
// moment waarop dat was. Daartussen telt de pagina zelf af, zodat de teller
|
||||
// loopt in plaats van elke vijf seconden te verspringen.
|
||||
var learningLeft = null;
|
||||
var learningLeftAt = 0;
|
||||
|
||||
// ── Hulpjes ───────────────────────────────────────────────────────────────
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
@@ -951,12 +965,6 @@ body {
|
||||
return Math.round(hours / 24) + ' days ago';
|
||||
}
|
||||
|
||||
function formatClock(seconds) {
|
||||
var whole = Math.max(0, Math.round(seconds));
|
||||
var minutes = Math.floor(whole / 60);
|
||||
var rest = whole % 60;
|
||||
return minutes + ':' + (rest < 10 ? '0' : '') + rest;
|
||||
}
|
||||
|
||||
function shorten(ownerId) {
|
||||
if (typeof ownerId !== 'string') { return 'unknown'; }
|
||||
@@ -1045,19 +1053,39 @@ body {
|
||||
hide('alert-notice');
|
||||
}
|
||||
|
||||
// Bevriezen in plaats van een melding bovenaan.
|
||||
//
|
||||
// Tot 0.6.0 verscheen bij élke klik "Sent. The relay applies changes within a
|
||||
// few seconds." in de balk bovenaan. Die balk staat in de gewone stroom van de
|
||||
// pagina, dus alles eronder schoof omlaag en bij het verdwijnen weer omhoog.
|
||||
// Gemeld door de gebruiker op 09-09-2026, en terecht: dat is een melding over
|
||||
// het verzendmechanisme en niet over wat jij deed. Wat je deed, zie je aan de
|
||||
// lijsten zodra de relay de opdracht heeft opgepakt.
|
||||
//
|
||||
// In plaats daarvan gaan de knoppen even op slot. Dat verschuift niets, het
|
||||
// toont dat er iets loopt, en het voorkomt een tweede klik terwijl de eerste
|
||||
// nog onderweg is. De foutmeldingen blijven wél in de balk staan: die zeggen
|
||||
// iets wat je nergens anders ziet, en die hóórt de pagina in de weg te leggen.
|
||||
function setFrozen(frozen) {
|
||||
document.body.classList.toggle('is-busy', frozen);
|
||||
var knoppen = document.querySelectorAll('.list-item button');
|
||||
for (var i = 0; i < knoppen.length; i += 1) { knoppen[i].disabled = frozen; }
|
||||
}
|
||||
|
||||
function send(command) {
|
||||
if (busy) { return; }
|
||||
busy = true;
|
||||
setFrozen(true);
|
||||
clearNotice();
|
||||
// De melding is met opzet kort van leven: de ronde hieronder haalt hem weg
|
||||
// zodra het relay-proces de opdracht heeft opgepakt, en dan is de nieuwe stand
|
||||
// in de kaders zelf te zien. Blijft hij staan, dan heeft de relay hem nog
|
||||
// niet gezien, en dán is er iets te melden.
|
||||
post('api/command', command, function () {
|
||||
show('alert-notice', 'Sent. The relay applies changes within a few seconds.', 'alert-neutral');
|
||||
}).then(function () {
|
||||
busy = false;
|
||||
window.setTimeout(refresh, 2500);
|
||||
post('api/command', command).then(function () {
|
||||
// Verversen en pas daarná ontdooien: de ronde bouwt de lijsten opnieuw op,
|
||||
// en tot dat gebeurd is staat de oude stand er nog. Ontdooien vóór de
|
||||
// ronde zou een klikbare knop opleveren die nog bij de vorige stand hoort.
|
||||
window.setTimeout(function () {
|
||||
refresh();
|
||||
busy = false;
|
||||
setFrozen(false);
|
||||
}, 2500);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1069,13 +1097,6 @@ body {
|
||||
post('api/label', { ownerId: ownerId, label: label }).then(refresh);
|
||||
}
|
||||
|
||||
el('learn-open').addEventListener('click', function () {
|
||||
send({ action: 'set-learning', value: true, seconds: LEARNING_SECONDS });
|
||||
});
|
||||
el('learn-stop').addEventListener('click', function () {
|
||||
send({ action: 'set-learning', value: false });
|
||||
});
|
||||
|
||||
// ── Een regel in een lijst ────────────────────────────────────────────────
|
||||
// `kind` is 'allowed', 'blocked' of 'rejected'. Dat bepaalt de badge en welke
|
||||
// knoppen er staan; de rest van de regel is voor alle drie hetzelfde.
|
||||
@@ -1128,7 +1149,7 @@ body {
|
||||
var tags = document.createElement('div');
|
||||
tags.className = 'owner-tags';
|
||||
if (kind === 'blocked') { tags.appendChild(badge('Blocked', 'badge-neg')); }
|
||||
if (kind === 'rejected') { tags.appendChild(badge('Turned away', 'badge-neutral')); }
|
||||
if (kind === 'rejected') { tags.appendChild(badge('Waiting', 'badge-neutral')); }
|
||||
row.appendChild(tags);
|
||||
|
||||
var actions = document.createElement('div');
|
||||
@@ -1284,66 +1305,6 @@ body {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Het tijdvenster ───────────────────────────────────────────────────────
|
||||
function drawLearning(owners) {
|
||||
var open = el('learn-open');
|
||||
var stop = el('learn-stop');
|
||||
var node = el('learning-state');
|
||||
|
||||
if (owners.learning === true) {
|
||||
// Met een tijdslot toont de widget de teller; zonder tijdslot alleen dat hij
|
||||
// openstaat. Dat verschil is echt en hoort zichtbaar te zijn: "open" zonder
|
||||
// teller sluit zichzelf niet.
|
||||
if (typeof learningLeft === 'number') {
|
||||
put('learning-state', formatClock(remainingSeconds()));
|
||||
el('learning-sub').textContent = 'closes on its own';
|
||||
} else {
|
||||
put('learning-state', 'Open');
|
||||
el('learning-sub').textContent = 'stays open until you close it';
|
||||
}
|
||||
node.classList.remove('bad');
|
||||
node.classList.add('ok');
|
||||
open.classList.remove('hidden');
|
||||
open.textContent = 'Restart';
|
||||
stop.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
node.classList.remove('ok', 'bad');
|
||||
if (owners.learning === false) {
|
||||
put('learning-state', 'Closed');
|
||||
// Hier staat wat de knop niet zegt: hoe lang hij open gaat. Dat is de reden
|
||||
// dat de knop "Open" mag heten en niet "Open 2 min" hoeft te heten.
|
||||
el('learning-sub').textContent = 'opens for two minutes at a time';
|
||||
open.classList.remove('hidden');
|
||||
open.textContent = 'Open';
|
||||
stop.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Onbekend is geen synonym voor uit: uit is een keuze, onbekend is een reden
|
||||
// om te kijken. Dat verschil hoort de pagina te tonen.
|
||||
put('learning-state', null);
|
||||
el('learning-sub').textContent = 'the relay has not reported this yet';
|
||||
open.classList.add('hidden');
|
||||
stop.classList.add('hidden');
|
||||
}
|
||||
|
||||
function remainingSeconds() {
|
||||
if (typeof learningLeft !== 'number') { return 0; }
|
||||
return Math.max(0, learningLeft - (Date.now() - learningLeftAt) / 1000);
|
||||
}
|
||||
|
||||
// Eén keer per seconde de teller bijwerken. Alleen de teller: de rest van de
|
||||
// pagina komt uit de ronde hieronder, die elke vijf seconden loopt.
|
||||
window.setInterval(function () {
|
||||
if (typeof learningLeft !== 'number') { return; }
|
||||
var node = el('learning-state');
|
||||
if (node.classList.contains('ok')) {
|
||||
node.textContent = formatClock(remainingSeconds());
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// ── De ronde ──────────────────────────────────────────────────────────────
|
||||
function refresh() {
|
||||
fetch('api/status', { cache: 'no-store' }).then(function (response) {
|
||||
@@ -1380,26 +1341,19 @@ body {
|
||||
put('db-modified', formatAgo(status.database.modified));
|
||||
el('db-modified-sub').textContent = formatTime(status.database.modified) || 'never written to';
|
||||
|
||||
// ws:// en niet http://, en dat is geen schoonheidsfoutje: de relay is een
|
||||
// WebSocket-server en spreekt nooit HTTP tegen een cliënt. Er stond http://
|
||||
// tot 0.7.0, en dat kostte op 09-09-2026 een ronde zoeken bij het koppelen
|
||||
// van de testcliënt.
|
||||
el('relay-url').textContent =
|
||||
'http://' + window.location.hostname + ':' + status.relay.publicPort;
|
||||
'ws://' + window.location.hostname + ':' + status.relay.publicPort;
|
||||
el('relay-url').classList.remove('unknown');
|
||||
|
||||
// Het tijdvenster. De resterende tijd komt van de agent, want die rekent met
|
||||
// de klok van de Umbrel en niet met die van de bezoeker.
|
||||
if (typeof status.owners.learningSecondsLeft === 'number'
|
||||
&& status.owners.learningSecondsLeft > 0) {
|
||||
learningLeft = status.owners.learningSecondsLeft;
|
||||
learningLeftAt = Date.now();
|
||||
} else {
|
||||
learningLeft = null;
|
||||
}
|
||||
drawLearning(status.owners);
|
||||
|
||||
// De lijsten, tenzij er iemand aan een label bezig is.
|
||||
if (editing === null) {
|
||||
fillList('allowed', [
|
||||
{ kind: 'allowed', entries: status.owners.allowed }
|
||||
], 'None yet. Open the window above and connect your app.');
|
||||
], 'None yet. Connect your app; it will show up in the waiting list.');
|
||||
|
||||
fillList('refused', [
|
||||
{ kind: 'blocked', entries: status.owners.blocked },
|
||||
@@ -1407,9 +1361,20 @@ body {
|
||||
], 'None.');
|
||||
}
|
||||
|
||||
// De teller met wachtenden. Dit is de plek waar tot 0.6.0 het tijdvenster
|
||||
// stond; nu telt hij wie er op een beslissing wacht, want dat is sinds
|
||||
// 0.7.0 de enige weg naar binnen.
|
||||
var wachtend = (status.owners.rejected || []).length;
|
||||
put('waiting-state', String(wachtend));
|
||||
el('waiting-state').classList.remove('unknown');
|
||||
el('waiting-sub').textContent = wachtend === 0
|
||||
? 'nobody is asking to get in'
|
||||
: (wachtend === 1 ? 'one owner wants in; allow it below if it is yours'
|
||||
: 'owners want in; allow the ones you recognise below');
|
||||
|
||||
if (status.owners.problem === 'onleesbaar') {
|
||||
show('alert-owners', 'The list of owners could not be read and was moved aside. New owners '
|
||||
+ 'are refused until you open the window above again.');
|
||||
show('alert-owners', 'The list of owners could not be read and was moved aside. Everyone is '
|
||||
+ 'on the waiting list again; allow the owners you recognise below.');
|
||||
} else {
|
||||
hide('alert-owners');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user