De pagina van Evolu Relay op de lijst van de gebruiker
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>
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "voorbeeldpagina",
|
||||||
|
"runtimeExecutable": "python",
|
||||||
|
"runtimeArgs": ["-m", "http.server", "8765", "--directory", "voorbeeld"],
|
||||||
|
"port": 8765
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -64,3 +64,9 @@ build/
|
|||||||
# .pyc glipte een keer mee in een commit.
|
# .pyc glipte een keer mee in een commit.
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
|
# De uitvoer van tools/voorbeeldpagina.mjs: een statuspagina met de variabelen
|
||||||
|
# ingevuld en nagemaakte gegevens erin, om in een browser te bekijken. Genereerbaar
|
||||||
|
# uit de template, dus er is niets te bewaren, en het zou een tweede versie van de
|
||||||
|
# pagina in de repo zetten die stilzwijgend achterloopt.
|
||||||
|
voorbeeld/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ gelden, dan per app wat alleen daar geldt.
|
|||||||
| App | Map | Toestand |
|
| App | Map | Toestand |
|
||||||
|-|-|-|
|
|-|-|-|
|
||||||
| **Electrum Gate** | `whatsnext-electrum-gate/` | draait op de Umbrel, wordt gebruikt |
|
| **Electrum Gate** | `whatsnext-electrum-gate/` | draait op de Umbrel, wordt gebruikt |
|
||||||
| **Evolu Relay** | `whatsnext-evolu-relay/` | gepakketteerd, nog nooit geïnstalleerd; zie het plan **Umbrelapp** |
|
| **Evolu Relay** | `whatsnext-evolu-relay/` | draait op de Umbrel sinds 28-08-2026 en wordt gebruikt. Let op de bouwstap: zie **Evolu Relay** hieronder |
|
||||||
|
|
||||||
**Zet geen CLAUDE.md of andere werkbestanden in een app-map.** umbreld kopieert bij installatie de héle
|
**Zet geen CLAUDE.md of andere werkbestanden in een app-map.** umbreld kopieert bij installatie de héle
|
||||||
app-map naar `~/umbrel/app-data/<app-id>/` met `rsync --archive`, dus alles wat daar staat belandt op het
|
app-map naar `~/umbrel/app-data/<app-id>/` met `rsync --archive`, dus alles wat daar staat belandt op het
|
||||||
@@ -65,9 +65,24 @@ python tests/test_appstore_vorm.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
Geen testrunner en geen afhankelijkheden: het zijn losse scripts die 0 teruggeven als alles goed is. Ze
|
Geen testrunner en geen afhankelijkheden: het zijn losse scripts die 0 teruggeven als alles goed is. Ze
|
||||||
horen samen met de test van Evolu Relay hieronder bij "de suite" en draaien voor een commit; de uitvoer
|
horen samen met de tests van Evolu Relay hieronder bij "de suite" en draaien voor een commit; de uitvoer
|
||||||
eindigt bij alle vier met een regel "N goed, M fout". Er is geen watch-modus; de suite kost minder dan een
|
eindigt bij elk met een regel "N goed, M fout". Er is geen watch-modus; de suite kost minder dan een
|
||||||
seconde.
|
seconde. Noem hier geen aantal: dat klopte tot 30-08-2026 al niet meer.
|
||||||
|
|
||||||
|
Er is ook een test over de **pagina's** van beide apps, en die is geen Python:
|
||||||
|
|
||||||
|
```
|
||||||
|
node tests/test_paginas_parsen.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Hij toetst dat de JavaScript in elke `index.html.template` parseert, en dat hij dat ook nog doet ná de
|
||||||
|
invulling door umbreld. Dat is de enige klasse paginafouten die niet op het apparaat gevonden hoeft te
|
||||||
|
worden; alles wat de pagina *toont* blijft handwerk in een browser.
|
||||||
|
|
||||||
|
**Wil je een pagina bekijken, gebruik `tools/voorbeeldpagina.mjs`.** Een `*.template` is niet te openen: er
|
||||||
|
staan accolade-variabelen in en de gegevens komen van een agent die alleen in de app bestaat. Dat script
|
||||||
|
vult beide in en zet het resultaat in `voorbeeld/` (gitignored). Geen bewijs, wel het gereedschap dat op
|
||||||
|
30-08-2026 twee echte opmaakfouten vond voordat ze uitgerold waren.
|
||||||
|
|
||||||
`test_appstore_vorm.py` toetst **expres niet** dat images op een digest gepind zijn. Dat is wél de regel,
|
`test_appstore_vorm.py` toetst **expres niet** dat images op een digest gepind zijn. Dat is wél de regel,
|
||||||
maar geen van de twee apps haalt hem vandaag, en een suite die altijd rood staat wordt niet gelezen. Hij
|
maar geen van de twee apps haalt hem vandaag, en een suite die altijd rood staat wordt niet gelezen. Hij
|
||||||
@@ -114,7 +129,11 @@ dan een app die weigert en zegt waarom.
|
|||||||
node tests/test_limiter.mjs
|
node tests/test_limiter.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
Dit is de vierde test van de suite en de enige die geen Python is. Hij toetst het toegangsbeleid uit
|
```
|
||||||
|
python tests/test_relay_agent.py
|
||||||
|
```
|
||||||
|
|
||||||
|
De eerste toetst het toegangsbeleid uit
|
||||||
`tools/evolu-relay/src/policy.js`: wie er op de relay mag schrijven, wat er met een onbekende eigenaar
|
`tools/evolu-relay/src/policy.js`: wie er op de relay mag schrijven, wat er met een onbekende eigenaar
|
||||||
gebeurt, en of een geblokkeerde eigenaar er niet alsnog in komt doordat de leerstand aanstaat. Node 24 of
|
gebeurt, en of een geblokkeerde eigenaar er niet alsnog in komt doordat de leerstand aanstaat. Node 24 of
|
||||||
hoger, geen afhankelijkheden, en het bestand is met opzet `.mjs`: in de repo-root is er geen `package.json`,
|
hoger, geen afhankelijkheden, en het bestand is met opzet `.mjs`: in de repo-root is er geen `package.json`,
|
||||||
@@ -123,7 +142,11 @@ dus een `.js` zou als CommonJS gelezen worden en de import falen.
|
|||||||
**Het beleid staat los van de relay en dat is de reden dat dit te testen is.** `policy.js` bevat alleen
|
**Het beleid staat los van de relay en dat is de reden dat dit te testen is.** `policy.js` bevat alleen
|
||||||
pure functies: geen bestanden, geen netwerk, geen klok. Wat wél schijf raakt staat in `store.js`, en de
|
pure functies: geen bestanden, geen netwerk, geen klok. Wat wél schijf raakt staat in `store.js`, en de
|
||||||
relay zelf wordt in `index.js` alleen aangeroepen. Houd die scheiding aan; anders is er van deze test niets
|
relay zelf wordt in `index.js` alleen aangeroepen. Houd die scheiding aan; anders is er van deze test niets
|
||||||
meer over.
|
meer over. **Ook het tijdvenster heeft geen klok**: `isLearningOpen(state, now)` krijgt het tijdstip mee.
|
||||||
|
|
||||||
|
De tweede toetst de agent: de labels en de resterende tijd van het tijdvenster. Dat zijn de twee dingen die
|
||||||
|
de agent zélf uitrekent in plaats van doorgeeft, en daarmee de enige plek in dit bestand waar hij eigen
|
||||||
|
logica heeft. De HTTP-laag zit er niet in: die handlers zijn zonder socket niet aan te roepen.
|
||||||
|
|
||||||
### Architectuurregels
|
### Architectuurregels
|
||||||
|
|
||||||
@@ -138,23 +161,43 @@ herinstallatie, en het zou de installatie minuten laten hangen.
|
|||||||
**Verhoog je `VERSION` in `build.sh`, dan verhoog je ook `version` in het manifest.** Anders is er een
|
**Verhoog je `VERSION` in `build.sh`, dan verhoog je ook `version` in het manifest.** Anders is er een
|
||||||
nieuwe image en een oude installatie, zonder dat iets dat meldt. Houd die twee gelijk.
|
nieuwe image en een oude installatie, zonder dat iets dat meldt. Houd die twee gelijk.
|
||||||
|
|
||||||
> **De app-map loopt achter op `tools/`.** Het programma hierboven is nieuw sinds 28-08-2026; de compose en
|
**De poorten staan omgekeerd ten opzichte van wat je verwacht, en dat is de kern van de compose.** De
|
||||||
> het manifest in `whatsnext-evolu-relay/` beschrijven nog het oude pakket met de relay van Trezor, een
|
**pagina** hangt achter `app_proxy` mét de inlog van umbrelOS; de **relay** publiceert zijn eigen
|
||||||
> Postgres en een quota-manager. Dat wordt vervangen in fase 5 van het plan **Umbrelapp**. De drie alinea's
|
host-poort 3852. Tot 0.0.2 stond het andersom en moest `PROXY_AUTH_ADD` op `"false"`, want een sync-cliënt
|
||||||
> hieronder over de app-proxy en de quota-manager gelden dus voor wat er staat, niet voor wat er komt.
|
is geen browser met een sessiecookie en zou een inlogpagina krijgen in plaats van de relay. Daardoor was
|
||||||
|
een statuspagina op diezelfde poort net zo onbeschermd als de relay zelf. Zet het niet terug zonder het plan
|
||||||
|
**Umbrelapp**, `PLAN.md` §4h te lezen.
|
||||||
|
|
||||||
**De app-proxy staat op `PROXY_AUTH_ADD: "false"` en dat is met opzet.** Trezor Suite is geen browser met
|
**Eén schrijver per bestand onder `data/relay/`, en dat is geen stijlkwestie.** Twee processen die in
|
||||||
een sessiecookie. Zet het niet "voor de veiligheid" terug: dan krijgt Suite een inlogpagina in plaats van
|
dezelfde allowlist schrijven is een wedloop die je een keer per jaar treft en dan niet kunt reproduceren.
|
||||||
de relay en werkt de app niet meer. De keerzijde hoort erbij en staat in de compose: wie de poort bereikt,
|
De verdeling:
|
||||||
bereikt de relay. Voeg dus geen pagina toe achter diezelfde poort zonder daar apart over na te denken; zie
|
|
||||||
het plan **Umbrelapp**, `OPEN.md` punt 2.
|
|
||||||
|
|
||||||
**De quota-manager hoort erbij en is geen restje.** De relay weigert elke eigenaar zonder rij in de
|
| Bestand | Schrijver | Waarom daar |
|
||||||
limietentabel, en dit is wat die rijen maakt. Haal hem er niet uit omdat hij bij Trezor bij betaalde
|
|-|-|-|
|
||||||
hosting hoort.
|
| `owners.json` | het relay-proces | daar staat wie er binnen mag, en dat beslist het proces dat de verbindingen aanneemt |
|
||||||
|
| `command.json` | de agent | de postbus. De agent legt erin, het relay-proces past toe en ruimt op |
|
||||||
|
| `labels.json` | de agent | een label zegt niets over toegang, dus de relay hoeft het niet te weten. Dat het hier staat en niet in `owners.json` is wat labelen meteen laat werken, ook als de relay omgevallen is |
|
||||||
|
|
||||||
**"Gebouwd" is hier nog verder van "werkend" dan bij Electrum Gate:** er is nog nooit iets van deze app op
|
**Het tijdvenster voor nieuwe eigenaars loopt in het relay-proces, nooit in de pagina.** Een teller in een
|
||||||
een Umbrel gedraaid. Meld dat expliciet in plaats van het te laten meelezen als werkend.
|
tabblad dat je sluit, sluit de deur niet. En `decideOwner` kijkt naar `isLearningOpen(state, now)` 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.
|
||||||
|
|
||||||
|
**Het beleid is de enige plek waar staat wie er binnen mag, en `isOwnerAllowed` wordt alleen bij de
|
||||||
|
WebSocket-upgrade gesteld.** Gevolg om te kennen voordat je iets belooft op de pagina: **blokkeren werkt pas
|
||||||
|
bij de volgende verbinding.** Zie `Docs/Referenties/Upstream-evolu-relay.md` §10 en het plan **Umbrelapp**,
|
||||||
|
`OPEN.md` punt 10.
|
||||||
|
|
||||||
|
**Zet geen ontcijfering in deze app.** Het kan technisch, maar niet met een xpub: er is de SLIP-21-node van
|
||||||
|
het apparaat voor nodig, en die geeft volledige zeggenschap over de gegevens. De hele grond waarop je een
|
||||||
|
relay ergens kunt neerzetten is dat hij de sleutel niet heeft, en het manifest belooft dat letterlijk.
|
||||||
|
Onderhoudsfuncties die de inhoud kennen horen in de tool in `HomeGit/Trezor`. Zie §11 van hetzelfde
|
||||||
|
naslagdocument en `OPEN.md` punt 11.
|
||||||
|
|
||||||
|
**Deze app draait en wordt gebruikt sinds 28-08-2026**, maar dat geldt niet voor elke wijziging: er zit een
|
||||||
|
bouwstap tussen de repo en het apparaat. Een wijziging in `tools/evolu-relay/src/` is pas uitgerold als de
|
||||||
|
image gebouwd, geduwd en met zijn digest in de compose gezet is, en dat kan alleen de gebruiker. Meld dus
|
||||||
|
per wijziging wat er wél geverifieerd is; "de app werkt" is geen uitspraak over de code van vandaag.
|
||||||
|
|
||||||
## Repo-feiten
|
## Repo-feiten
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,46 @@ Wat er nog moet gebeuren staat **niet** hier maar in de plannen; zie [CONTINUE_H
|
|||||||
Een lijst met geplande features op twee plekken loopt uit elkaar, en dan is geen van beide meer te
|
Een lijst met geplande features op twee plekken loopt uit elkaar, en dan is geen van beide meer te
|
||||||
vertrouwen.
|
vertrouwen.
|
||||||
|
|
||||||
|
## [0.0.24] - 2026-08-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Een menu-item "About this app…", met een dialoog erachter.** De marketingtekst stond alleen in
|
||||||
|
`umbrel-app.yml` en dus alleen in de winkel, terwijl je juist ná het installeren nog eens wil kunnen
|
||||||
|
nalezen wat de app voor je doet. Vijf kopjes: waar het voor is, het certificaat, wat er niet in het midden
|
||||||
|
zit, wat de statuswidget bewijst, en welke wallets.
|
||||||
|
|
||||||
|
**Dezelfde toevoeging zit in Evolu Relay**, op verzoek van de gebruiker: het is één store en de twee
|
||||||
|
pagina's horen zich hetzelfde te gedragen.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **De statuswidget zei "Answering" en zegt nu "Running".** Gemeld door de gebruiker: die tegel is
|
||||||
|
onduidelijk, en die van Evolu Relay leest meteen. Dat klopt om twee redenen die los van elkaar staan.
|
||||||
|
|
||||||
|
"Answering" is een tegenwoordig deelwoord en leest dus als een handeling die bezig is, niet als een
|
||||||
|
toestand; "Checking" ernaast is er wél een, en dat maakt het erger. En de onderregel "answered in 7 ms,
|
||||||
|
from inside the app" probeerde in vier woorden een nuance te dragen waar vier woorden te weinig voor zijn:
|
||||||
|
wie het niet al weet, leest er niets uit.
|
||||||
|
|
||||||
|
De onderregel is nu "the TLS port answered in 7 ms". **De nuance is niet weggelaten maar verplaatst** naar
|
||||||
|
de nieuwe dialoog, onder een eigen kopje: de controle loopt van container naar container, dus een router
|
||||||
|
die de poort niet meer doorstuurt leest hier alsnog als Running. Haal dat kopje niet weg zonder in de
|
||||||
|
widget iets terug te zetten, want dan staat die bewering nergens meer.
|
||||||
|
|
||||||
|
## [0.0.23] - 2026-08-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **De pagina kon leeg blijven of oude waarden tonen zodra er een tweede app uit deze store bijkwam.** Hij
|
||||||
|
bereikte zijn agent via de korte containernaam `agent`, en Evolu Relay heeft er ook een op poort 8000.
|
||||||
|
Alle apps van umbrelOS delen één Docker-netwerk, dus Docker verdeelde die naam over beide containers en
|
||||||
|
ongeveer de helft van de verzoeken kwam bij de verkeerde app uit.
|
||||||
|
|
||||||
|
Beide apps wijzen nu naar de volledige naam `<app-id>_agent_1`, en die vorm is per app uniek.
|
||||||
|
`tests/test_appstore_vorm.py` toetst dit voortaan voor élke app in de store, want dit is precies het soort
|
||||||
|
fout dat stil terugkomt bij de volgende app die iemand toevoegt.
|
||||||
|
|
||||||
## [0.0.22] - 2026-08-27
|
## [0.0.22] - 2026-08-27
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -4,6 +4,83 @@ Nieuwste bovenaan. Elke regel hier hoort bij een `version` in
|
|||||||
`whatsnext-evolu-relay/umbrel-app.yml`; zonder verhoging van dat nummer rolt umbrelOS een wijziging niet
|
`whatsnext-evolu-relay/umbrel-app.yml`; zonder verhoging van dat nummer rolt umbrelOS een wijziging niet
|
||||||
uit.
|
uit.
|
||||||
|
|
||||||
|
**Er hoort een tweede nummer bij: `VERSION` in `tools/evolu-relay/build.sh`, het etiket op de image.** Die
|
||||||
|
twee horen gelijk te zijn. Ze mogen uiteenlopen als er alleen iets in de app-map wijzigt, want dan is er
|
||||||
|
geen nieuwe image, maar sinds 0.5.0 worden ze gelijkgehouden: uiteenlopende nummers waren bij 0.4.0 al
|
||||||
|
verwarrend (manifest 0.4.0, image 0.3.0).
|
||||||
|
|
||||||
|
> **Deze geschiedenis is op 30-08-2026 bijgewerkt en liep tot dat moment achter:** er stond alleen 0.0.1,
|
||||||
|
> terwijl de app op 0.4.0 zat. De entries voor 0.2.0 tot en met 0.4.0 zijn met terugwerkende kracht
|
||||||
|
> geschreven uit de `releaseNotes` in het manifest en uit `PROGRESS.md` van het plan **Umbrelapp**. Ze zijn
|
||||||
|
> daarom korter dan de rest.
|
||||||
|
|
||||||
|
## 0.5.0 - 30-08-2026
|
||||||
|
|
||||||
|
**De statuspagina op de lijst van de gebruiker, na een week met 0.4.0 gewerkt te hebben.** Negen punten,
|
||||||
|
alle negen gedaan.
|
||||||
|
|
||||||
|
**Een tijdvenster van twee minuten in plaats van een schakelaar.** Openzetten, apparaat koppelen, en de deur
|
||||||
|
sluit zichzelf; vroegtijdig sluiten of de twee minuten opnieuw starten kan ook. **De teller loopt in het
|
||||||
|
relay-proces en niet in de pagina**, want een teller in een tabblad dat je sluit, sluit de deur niet.
|
||||||
|
`policy.js` heeft er `learningUntil`, `isLearningOpen` en `expireLearning` voor gekregen, zonder
|
||||||
|
`STATE_VERSION` te verhogen: een verhoging zou de allowlist van de draaiende installatie laten afwijzen.
|
||||||
|
|
||||||
|
**Labels op een eigenaar-id.** Een `OwnerId` is een reeks tekens zonder betekenis; nu kun je er een naam aan
|
||||||
|
hangen. De labels staan in een eigen `labels.json` met de agent als enige schrijver, dus ze zijn meteen
|
||||||
|
opgeslagen en labelen werkt ook als de relay omgevallen is.
|
||||||
|
|
||||||
|
**Geblokkeerde en geweigerde eigenaars in één kader**, met een badge die zegt welke van de twee het is. Wat
|
||||||
|
je ermee doet was toch hetzelfde: toelaten of vergeten.
|
||||||
|
|
||||||
|
**De pagina is gelijkgetrokken met Electrum Gate**: dezelfde breedte van 1760 pixels, hetzelfde raster,
|
||||||
|
hetzelfde icoon van 64 pixels, dezelfde kop en het versienummer erachter. Knoppen verschijnen bij hover over
|
||||||
|
een regel, de uitleggende alinea's zijn verdwenen, en de voetregel is vervangen door een menu-item "About
|
||||||
|
this app" waar die uitleg nu staat.
|
||||||
|
|
||||||
|
**Wat er aan toetsen bij kwam**, want de pagina's waren met bijna drieduizend regels ongetoetst:
|
||||||
|
`tests/test_relay_agent.py` (nieuw), `tests/test_paginas_parsen.mjs` (nieuw), een dollarteken-toets waarvan
|
||||||
|
het commentaar al beweerde dat hij bestond, en een toets dat er geen werkbestanden in een app-map staan.
|
||||||
|
Verder `tools/voorbeeldpagina.mjs`, waarmee een `*.template` in een browser te bekijken is; dat vond meteen
|
||||||
|
twee echte fouten in de nieuwe pagina.
|
||||||
|
|
||||||
|
## 0.4.0 - 28-08-2026
|
||||||
|
|
||||||
|
**Reparatie.** De statuspagina wisselde tussen werken en een foutmelding: hij bereikte zijn agent via de
|
||||||
|
korte containernaam `agent`, en Electrum Gate in deze store heeft er ook een. Ongeveer de helft van de
|
||||||
|
verzoeken kwam bij de verkeerde app uit, en het maakte de pagina van Gate mee kapot. Beide apps wijzen nu
|
||||||
|
naar `<app-id>_agent_1`.
|
||||||
|
|
||||||
|
Verder de tekst die de app aan één cliënt verbond eruit: het is een algemene Evolu-relay. En de uitleg onder
|
||||||
|
de schakelaar voor nieuwe eigenaars klopte niet, die beschreef de open stand ook als hij dicht stond.
|
||||||
|
|
||||||
|
Let op: het manifest stond hier op 0.4.0 terwijl de image 0.3.0 bleef, want er wijzigde niets in het
|
||||||
|
relay-programma.
|
||||||
|
|
||||||
|
## 0.3.0 - 28-08-2026
|
||||||
|
|
||||||
|
**De eerste versie die op de Umbrel draait en gebruikt wordt.** De eigen eigenaars-allowlist erin, met de
|
||||||
|
statuspagina achter de umbrelOS-inlog, en TLS via Zoraxy op een eigen subdomein. Trezor Suite
|
||||||
|
synchroniseert eroverheen, heen én terug; die tweede richting is gemeten met een tweede gebruikersaccount
|
||||||
|
dat met een lege database alle labels binnenkreeg.
|
||||||
|
|
||||||
|
De poorten zijn omgedraaid ten opzichte van 0.0.2: de **pagina** hangt achter de app-proxy mét de inlog, en
|
||||||
|
de **relay** publiceert zijn eigen host-poort 3852. Een sync-cliënt is geen browser met een sessiecookie en
|
||||||
|
zou achter de inlog een inlogpagina krijgen.
|
||||||
|
|
||||||
|
## 0.2.0 - 28-08-2026
|
||||||
|
|
||||||
|
**De relay is vervangen.** Tot 0.0.2 pakketteerde deze app de eigen uitrol van een leverancier, met een
|
||||||
|
quota-manager en een PostgreSQL erbij, en die kon buiten hun eigen dienst in de kern niet werken: cliënten
|
||||||
|
slaan de quota-manager over zodra je ze naar een eigen relay wijst, terwijl die relay elke eigenaar weigert
|
||||||
|
die de quota-manager nooit geregistreerd heeft.
|
||||||
|
|
||||||
|
Hiervoor in de plaats komt de relay van het Evolu-project zelf, aangeroepen uit ons eigen programma in
|
||||||
|
`tools/evolu-relay/`. Gemeten en niet aangenomen: een cliënt synchroniseerde ernaartoe en de data kwam aan.
|
||||||
|
Drie containers werden een relay plus een statuspagina, en de database met zijn wachtwoord is verdwenen.
|
||||||
|
|
||||||
|
Wat wij toevoegen zijn de twee terugroepfuncties die `createRelay` daarvoor heeft, `isOwnerAllowed` en
|
||||||
|
`isOwnerWithinQuota`. De relay zelf is niet nagebouwd en niet aangepast.
|
||||||
|
|
||||||
## 0.0.1 - 25-08-2026
|
## 0.0.1 - 25-08-2026
|
||||||
|
|
||||||
Eerste versie, nog niet geinstalleerd.
|
Eerste versie, nog niet geinstalleerd.
|
||||||
|
|||||||
@@ -29,9 +29,9 @@
|
|||||||
|
|
||||||
| Plan | App | Volgende stap | Status |
|
| Plan | App | Volgende stap | Status |
|
||||||
|-|-|-|-|
|
|-|-|-|-|
|
||||||
| [Umbrelapp](Plannen/Actief/008-Umbrelapp/TAKEN.md) | Relay | **Van A naar B op 28-08-2026: het doel is gehaald.** De app draait als 0.3.0 met een eigen eigenaars-allowlist, een statuspagina achter de umbrelOS-inlog en TLS via Zoraxy op een eigen subdomein, en Trezor Suite synchroniseert eroverheen **heen én terug**; die tweede richting is bewezen met een tweede gebruikersaccount op de Mac dat met een lege database alle labels binnenkreeg. iOS doet niet mee en dat ligt niet aan dit pakket: de `OwnerId` wordt op de Trezor afgeleid, en een model dat niet aan een iPhone kan geeft geen sleutel (open punt 9, met bronregels). Wat nog openstaat is klein en wacht nergens op: een herstart van de app overleven, `python:3-alpine` en `nginx:alpine` pinnen, en data per eigenaar kunnen wissen | 🔶 |
|
| [Umbrelapp](Plannen/Actief/008-Umbrelapp/TAKEN.md) | Relay | **De pagina is op 30-08-2026 verbouwd op de lijst van de gebruiker; 0.5.0 ligt klaar maar is nog niet uitgerold.** Er zit een tijdvenster van twee minuten voor nieuwe eigenaars in (teller in het relay-proces, niet in de browser), labels op een eigenaar-id, de twee lijsten met buitengesloten eigenaars samengevoegd, en de maatvoering gelijk aan Electrum Gate. **De volgende stap kan niet op een laptop en is van de gebruiker:** `sh tools/evolu-relay/build.sh` plus een push, en dan de digest in de compose. Die staat er bewust níet in, want een oude digest onder een nieuwe tag levert stil de oude relay. Verder open en klein: herstart overleven, `python:3-alpine` en `nginx:alpine` pinnen, data per eigenaar wissen | 🔶 |
|
||||||
| [Proefopstelling](Plannen/Actief/007-Proefopstelling/TAKEN.md) | Relay | **Volledig ingehaald op 28-08-2026 en klaar voor het archief.** Beide vragen die het nog bezat zijn beantwoord door **Umbrelapp**: Trezor Suite accepteert een eigen relay, en een eigenaar registreren is niet meer nodig sinds de quota-manager eruit is. Wat er in fase 2 en 3 stond gaat over een pakket dat niet meer bestaat. **Eén beslissing van de gebruiker: opheffen of laten staan** | 🔶 |
|
| [Proefopstelling](Plannen/Actief/007-Proefopstelling/TAKEN.md) | Relay | **Volledig ingehaald op 28-08-2026 en klaar voor het archief.** Beide vragen die het nog bezat zijn beantwoord door **Umbrelapp**: Trezor Suite accepteert een eigen relay, en een eigenaar registreren is niet meer nodig sinds de quota-manager eruit is. Wat er in fase 2 en 3 stond gaat over een pakket dat niet meer bestaat. **Eén beslissing van de gebruiker: opheffen of laten staan** | 🔶 |
|
||||||
| [Webinterface](Plannen/Actief/005-Webinterface/TAKEN.md) | Gate | **De pagina is af en goedgekeurd, op een breed scherm én op een telefoon (27-08-2026).** Op 28-08-2026 kapotgegaan door de tweede app en gerepareerd in 0.0.23: de korte containernaam `agent` is op het gedeelde Docker-netwerk niet uniek. Van A naar B: wat er nog staat wacht op tijd (het activiteitenlog een etmaal laten lopen) of is een beslissing van de gebruiker (waar Nginx Proxy Manager zijn certificaten neerzet, en of Zoraxy een harde afhankelijkheid wordt) | 🔶 |
|
| [Webinterface](Plannen/Actief/005-Webinterface/TAKEN.md) | Gate | **De pagina is af en goedgekeurd, op een breed scherm én op een telefoon (27-08-2026).** Op 28-08-2026 kapotgegaan door de tweede app en gerepareerd in 0.0.23. In 0.0.24 (30-08-2026) is de statuswidget van "Answering" naar "Running" gegaan en is er een menu-item "About this app" bij gekomen; dat kwam uit het werk aan Evolu Relay, waar de gebruiker de twee pagina's naast elkaar zag. **Nog nakijken op het apparaat, en dat is een blik en geen sessie.** Wat er verder staat wacht op tijd (het activiteitenlog een etmaal laten lopen) of is een beslissing van de gebruiker (waar Nginx Proxy Manager zijn certificaten neerzet, en of Zoraxy een harde afhankelijkheid wordt) | 🔶 |
|
||||||
| [Appstore](Plannen/Actief/010-Appstore/TAKEN.md) | Gate | De repo `ElectrumTLS` weghalen op de Git-server; daar staat het domein nog in de historie. **De omschakeling naar Fulcrum is op 27-08-2026 gelukt zonder aanpassing**, dus daarvan is alleen de herstartcontrole nog over, en die komt vanzelf bij de eerstvolgende herstart | 🔶 |
|
| [Appstore](Plannen/Actief/010-Appstore/TAKEN.md) | Gate | De repo `ElectrumTLS` weghalen op de Git-server; daar staat het domein nog in de historie. **De omschakeling naar Fulcrum is op 27-08-2026 gelukt zonder aanpassing**, dus daarvan is alleen de herstartcontrole nog over, en die komt vanzelf bij de eerstvolgende herstart | 🔶 |
|
||||||
|
|
||||||
## C - Wacht op afhankelijkheid
|
## C - Wacht op afhankelijkheid
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# Voortgang - Webinterface
|
# Voortgang - Webinterface
|
||||||
|
|
||||||
|
## 30-08-2026 - wat de buur duidelijker deed, is hier overgenomen
|
||||||
|
|
||||||
|
Geen gepland werk aan dit plan. Bij het verbouwen van de pagina van Evolu Relay zag de gebruiker de twee
|
||||||
|
naast elkaar, en toen kwam eruit wat op deze pagina al maanden stond zonder dat iemand erover viel: de
|
||||||
|
statuswidget zei "Answering" met "answered in 7 ms, from inside the app" eronder, en dat is in beide helften
|
||||||
|
onduidelijk. Nu "Running", met de meting eronder.
|
||||||
|
|
||||||
|
**De nuance die daarin zat is niet weg maar verplaatst.** Dat "from inside the app" was er niet voor niets:
|
||||||
|
de agent verbindt van container naar container, dus een router die de poort niet meer doorstuurt leest hier
|
||||||
|
alsnog als in orde. Die bewering staat nu onder een eigen kopje in de nieuwe dialoog "About this app", waar
|
||||||
|
er ruimte is om hem uit te schrijven. Vier woorden waren er te weinig voor.
|
||||||
|
|
||||||
|
Die dialoog is de tweede wijziging, en hij zit in beide apps: de marketingtekst stond alleen in het manifest
|
||||||
|
en dus alleen in de winkel, terwijl je hem ná het installeren nog eens wil kunnen nalezen. Versie 0.0.24.
|
||||||
|
|
||||||
## 28-08-2026 - deze pagina ging kapot door een andere app, en dat was te voorzien
|
## 28-08-2026 - deze pagina ging kapot door een andere app, en dat was te voorzien
|
||||||
|
|
||||||
Geen werk aan dit plan; wel een reparatie die er thuishoort. `nginx.conf.template` proxyde naar
|
Geen werk aan dit plan; wel een reparatie die er thuishoort. `nginx.conf.template` proxyde naar
|
||||||
|
|||||||
@@ -28,6 +28,22 @@
|
|||||||
|
|
||||||
## Volgende stap
|
## Volgende stap
|
||||||
|
|
||||||
|
- [x] **De statuswidget verduidelijkt en een dialoog "About this app" toegevoegd (30-08-2026, 0.0.24).**
|
||||||
|
Dit kwam niet uit dit plan maar uit het werk aan Evolu Relay: de gebruiker zag de twee pagina's naast
|
||||||
|
elkaar en meldde dat de tegel "Answering / answered in 7 ms, from inside the app" onduidelijk is
|
||||||
|
terwijl "Running" bij de relay meteen leest.
|
||||||
|
|
||||||
|
Dat was op twee losse punten terecht. "Answering" is een tegenwoordig deelwoord en leest als een
|
||||||
|
handeling die bezig is, niet als een toestand, en "Checking" ernaast is er wél een. En "from inside
|
||||||
|
the app" probeerde in vier woorden de nuance te dragen dat deze controle van container naar container
|
||||||
|
loopt en dus niets zegt over de router; vier woorden zijn daar te weinig voor.
|
||||||
|
|
||||||
|
Nu: "Running", met "the TLS port answered in 7 ms" eronder. **De nuance is verplaatst en niet
|
||||||
|
weggelaten**, naar een eigen kopje in de nieuwe dialoog. Dat kopje is dus geen sier: haal je het weg,
|
||||||
|
dan staat die bewering nergens meer.
|
||||||
|
- [ ] **0.0.24 op het apparaat nakijken. Eigenaar: gebruiker.** Alleen de dialoog en de widgettekst zijn
|
||||||
|
gewijzigd, dus het is een blik en geen sessie: opent het menu-item, staat er "Running", en klopt het
|
||||||
|
versienummer in de kop
|
||||||
- [x] **0.0.10 op een telefoon nagekeken. In orde (20-08-2026).** De gebruiker meldt dat het er goed uitziet
|
- [x] **0.0.10 op een telefoon nagekeken. In orde (20-08-2026).** De gebruiker meldt dat het er goed uitziet
|
||||||
op mobiel. Eén ding kwam eruit: de kopieerknoppen stonden niet op één lijn, omdat de knop door het
|
op mobiel. Eén ding kwam eruit: de kopieerknoppen stonden niet op één lijn, omdat de knop door het
|
||||||
omvouwen de lengte van het adres volgde. Verholpen in 0.0.11 met `margin-left: auto` op de knop.
|
omvouwen de lengte van het adres volgde. Verholpen in 0.0.11 met `margin-left: auto` op de knop.
|
||||||
|
|||||||
@@ -9,6 +9,50 @@
|
|||||||
|
|
||||||
## Nog te beslissen
|
## Nog te beslissen
|
||||||
|
|
||||||
|
10. **Wat doen we ermee dat blokkeren pas bij de volgende verbinding werkt?**
|
||||||
|
|
||||||
|
Gevonden op 30-08-2026 bij het uitzoeken van open vraag over geweigerde eigenaars; de bronregels staan
|
||||||
|
in [Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md) §10.
|
||||||
|
|
||||||
|
`isOwnerAllowed` wordt door de relay van Evolu aangeroepen in de **WebSocket-upgrade** en daarna nooit
|
||||||
|
meer voor die verbinding. Gevolg: de knop **Block** op de statuspagina houdt een eigenaar buiten bij zijn
|
||||||
|
volgende verbinding, maar niet nu. Zolang zijn socket openstaat, blijft hij schrijven. Bij een
|
||||||
|
sync-cliënt die zijn verbinding lang openhoudt kan dat lang duren.
|
||||||
|
|
||||||
|
Wat er dus scheelt, is niet de knop maar wat hij belooft. Drie wegen, van goedkoop naar goed:
|
||||||
|
|
||||||
|
- **niets doen en het zeggen.** Een regel in de dialoog "About this app" dat blokkeren bij de volgende
|
||||||
|
verbinding ingaat. Kost niets en het is eerlijk, maar het lost het niet op;
|
||||||
|
- **de relay opnieuw laten starten na een block.** Dan vallen alle verbindingen weg en wordt de vraag
|
||||||
|
voor iedereen opnieuw gesteld. Grof, want het raakt ook de eigenaars die niets gedaan hebben, en het
|
||||||
|
is precies het soort ingreep waarvoor deze store geen Docker-socket wil hebben;
|
||||||
|
- **kijken of Evolu een weg biedt om een verbinding te sluiten.** Niet uitgezocht. `createRelay` geeft
|
||||||
|
een `Relay` terug en wat daarop zit is nog niet nagelezen. Dit is de enige weg die het echt oplost.
|
||||||
|
|
||||||
|
Er is nog geen aanleiding om dit nu te doen: er is één gebruiker en die blokkeert zijn eigen apparaten.
|
||||||
|
Het staat hier zodat het niet als verrassing terugkomt.
|
||||||
|
**Moment:** zodra iemand een eigenaar blokkeert die niet van hemzelf is · **Eigenaar:** gebruiker beslist
|
||||||
|
welke van de drie
|
||||||
|
|
||||||
|
11. **Nemen we onderhoudsfuncties op die de inhoud kennen?** - **beslist op 30-08-2026: nee, niet in deze
|
||||||
|
app.**
|
||||||
|
|
||||||
|
De vraag van de gebruiker was of de app de blobs kan ontcijferen, want dan zouden er onderhoudsfuncties
|
||||||
|
bij kunnen. Technisch kan het, maar niet met een xpub: er is een 32-byte SLIP-21-node van het apparaat
|
||||||
|
voor nodig, en die geeft **volledige** zeggenschap over de gegevens van die wallet, niet alleen
|
||||||
|
leesrecht. Zie [Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md) §11.
|
||||||
|
|
||||||
|
De reden om het niet te doen is de reden dat deze app bestaat: de hele grond waarop je een relay bij wie
|
||||||
|
dan ook kunt neerzetten, is dat hij de sleutel niet heeft. Zet je hem erin, dan is dat weg, en wel voor
|
||||||
|
de kopie die op een apparaat staat dat aan het internet hangt. De beschrijving in het manifest zegt
|
||||||
|
letterlijk dat de relay de sleutel nooit heeft; dat zou dan niet meer waar zijn.
|
||||||
|
|
||||||
|
**Waar het wél hoort is de tool in `HomeGit/Trezor`**, die al met `trezorlib` tegen het apparaat praat en
|
||||||
|
de sleutel dus legitiem in handen heeft. Daar staat het als plan-punt bij **AdvancedUI**.
|
||||||
|
|
||||||
|
Wat deze app zónder sleutel wél kan blijven doen: rijen per `OwnerId` opruimen. Dat staat al als taak in
|
||||||
|
[TAKEN.md](TAKEN.md) fase 5.
|
||||||
|
|
||||||
6. **Pakketteren we wel de juiste relay?** - **beslist op 28-08-2026 door de proef: nee, en het wordt de
|
6. **Pakketteren we wel de juiste relay?** - **beslist op 28-08-2026 door de proef: nee, en het wordt de
|
||||||
kale Evolu-relay.** Dit punt blijft hier staan tot de verbouwing gedaan is, want het beschrijft de reden
|
kale Evolu-relay.** Dit punt blijft hier staan tot de verbouwing gedaan is, want het beschrijft de reden
|
||||||
waarom het huidige pakket eruit gaat. De volledige meting staat in [PLAN.md](PLAN.md) §6a.
|
waarom het huidige pakket eruit gaat. De volledige meting staat in [PLAN.md](PLAN.md) §6a.
|
||||||
|
|||||||
@@ -3,6 +3,32 @@
|
|||||||
> Chronologisch sessielog, nieuwste bovenaan. Kort: 3 tot 6 regels per entry. Wat er is gebeurd en waarom,
|
> Chronologisch sessielog, nieuwste bovenaan. Kort: 3 tot 6 regels per entry. Wat er is gebeurd en waarom,
|
||||||
> niet wat er nog moet: dat staat in [TAKEN.md](TAKEN.md).
|
> niet wat er nog moet: dat staat in [TAKEN.md](TAKEN.md).
|
||||||
|
|
||||||
|
## 30-08-2026 - de pagina op de lijst van de gebruiker, plus twee onderzoeksvragen
|
||||||
|
|
||||||
|
De lijst waar taak "statuspagina verbeteren" op wachtte, kwam er: negen punten uit echt gebruik. Alle negen
|
||||||
|
in één ronde gedaan, als 0.5.0. Het grootste stuk is het **tijdvenster van twee minuten** voor nieuwe
|
||||||
|
eigenaars, en de bepalende keuze daarin is dat de teller in het relay-proces zit en niet in de pagina: een
|
||||||
|
teller in een tabblad dat je sluit, sluit de deur niet. Verder labels op een eigenaar-id, de twee lijsten met
|
||||||
|
buitengesloten eigenaars samengevoegd, en de pagina qua maatvoering gelijkgetrokken met Electrum Gate. Beide
|
||||||
|
apps hebben nu een menu-item "About this app".
|
||||||
|
|
||||||
|
**De twee onderzoeksvragen zijn beantwoord en één premisse bleek fout.** Een geweigerde eigenaar komt níet in
|
||||||
|
de database: `isOwnerAllowed` zit in de WebSocket-upgrade, dus het is een 401 en een gesloten socket. Wat de
|
||||||
|
gebruiker wilde gebeurt alsnog, maar via de cliënt: die is local-first en levert bij toelating de hele
|
||||||
|
geschiedenis. En de blobs zijn niet met een xpub te ontcijferen, want een `OwnerId` wordt niet uit een xpub
|
||||||
|
gemaakt; daar is de SLIP-21-node van het apparaat voor nodig, en die geeft volledige zeggenschap. Dat hoort
|
||||||
|
dus in de Trezor-tool en niet hier.
|
||||||
|
|
||||||
|
**Wat deze ronde onverwacht opleverde was gereedschap.** Een `*.template` is niet in een browser te openen,
|
||||||
|
dus is er nu `tools/voorbeeldpagina.mjs` die er een bekijkbare versie van maakt met nagemaakte gegevens. Dat
|
||||||
|
vond meteen twee echte fouten die ik anders op het apparaat had gevonden: de badges stonden in het hover-blok
|
||||||
|
en waren dus onzichtbaar, en de onzichtbare knoppen namen wél ruimte in de kop waardoor er een kop over twee
|
||||||
|
regels brak. Daarnaast drie nieuwe toetsen, waaronder de dollarteken-toets waarvan het commentaar in drie
|
||||||
|
bestanden al beweerde dat hij bestond.
|
||||||
|
|
||||||
|
**Wat er nog moet en niet op een laptop kan:** de image 0.5.0 bouwen en duwen. De digest staat daarom niet in
|
||||||
|
de compose, want een oude digest onder een nieuwe tag levert stil de oude relay.
|
||||||
|
|
||||||
## 28-08-2026 (slot) - de app draait en wordt gebruikt; iOS doet niet mee
|
## 28-08-2026 (slot) - de app draait en wordt gebruikt; iOS doet niet mee
|
||||||
|
|
||||||
Het pakket is af en geïnstalleerd als 0.3.0. De Mac synchroniseert eroverheen, over TLS via Zoraxy op een
|
Het pakket is af en geïnstalleerd als 0.3.0. De Mac synchroniseert eroverheen, over TLS via Zoraxy op een
|
||||||
|
|||||||
@@ -16,6 +16,25 @@
|
|||||||
|
|
||||||
## Volgende stap
|
## Volgende stap
|
||||||
|
|
||||||
|
- [ ] **De image 0.5.0 bouwen en duwen, en dan de digest in de compose zetten. Eigenaar: gebruiker.** Dit
|
||||||
|
is de enige stap die 0.5.0 tegenhoudt, en hij kan niet op een laptop: er staat een wijziging in het
|
||||||
|
relay-programma (het tijdvenster) en umbreld kan niet bij een image die alleen lokaal bestaat.
|
||||||
|
|
||||||
|
```
|
||||||
|
sh tools/evolu-relay/build.sh
|
||||||
|
docker login sc.kamenier-hamer.nl
|
||||||
|
docker push sc.kamenier-hamer.nl/sysop/evolu-relay:0.5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**De digest staat op dit moment níet in de compose**, en dat is met opzet: een oude digest onder een
|
||||||
|
nieuwe tag levert stilzwijgend de oude relay, en dan werkt de timer niet zonder dat iets dat meldt.
|
||||||
|
Ongepind faalt hard en zichtbaar. Zet de digest uit de push-uitvoer erachter zodra hij er is;
|
||||||
|
`test_appstore_vorm.py` drukt de pinstatus af, dus de suite blijft het zeggen tot het gedaan is
|
||||||
|
- [ ] **Daarna op het apparaat nakijken wat een browser moet bewijzen (30-08-2026).** De pagina is met
|
||||||
|
nagemaakte gegevens bekeken en dat vond twee echte fouten, maar drie dingen kan alleen de app zelf
|
||||||
|
zeggen: loopt de teller écht af en gaat de deur dan dicht, blijft een label na een herstart staan, en
|
||||||
|
klopt het adres in het kader "Relay address" (dat gebruikt `window.location.hostname`, en achter de
|
||||||
|
app-proxy is dat een ander adres dan op een laptop)
|
||||||
- [x] **De kále Evolu-relay geprobeerd, en hij werkt (28-08-2026).** Trezor Suite op de desktop stuurde
|
- [x] **De kále Evolu-relay geprobeerd, en hij werkt (28-08-2026).** Trezor Suite op de desktop stuurde
|
||||||
elf labels naar `docker.io/evoluhq/relay:latest` en die kwamen aan: de database in het volume groeide
|
elf labels naar `docker.io/evoluhq/relay:latest` en die kwamen aan: de database in het volume groeide
|
||||||
van 40960 naar 49152 bytes. **De protocolversie klopt**, en daarmee is de richting uit
|
van 40960 naar 49152 bytes. **De protocolversie klopt**, en daarmee is de richting uit
|
||||||
@@ -100,6 +119,91 @@ Wat er niet staat: iOS doet niets mee, en daardoor is de leeskant nooit gemeten.
|
|||||||
met de WebSocket-upgrade aan. De handshake-curl uit [PLAN.md](PLAN.md) §6a stap 3 geeft daar
|
met de WebSocket-upgrade aan. De handshake-curl uit [PLAN.md](PLAN.md) §6a stap 3 geeft daar
|
||||||
`101 Switching Protocols`, en de desktop synchroniseert eroverheen
|
`101 Switching Protocols`, en de desktop synchroniseert eroverheen
|
||||||
|
|
||||||
|
## Fase 6 - De statuspagina op de lijst van de gebruiker (0.5.0)
|
||||||
|
|
||||||
|
**De lijst kwam op 30-08-2026, nadat de gebruiker met 0.4.0 gewerkt had.** Negen punten, en alle negen
|
||||||
|
gedaan. Twee ervan waren onderzoeksvragen; die staan onderaan.
|
||||||
|
|
||||||
|
Waarom dit in één ronde kon: de pagina is een `*.template` en zit dus in de update-whitelist. Zodra hij in
|
||||||
|
een eigen image zit (het plan **Eigenimage**) kost elke tweak een bouw plus een digest, en dán is dit
|
||||||
|
duurder. Die volgorde stond als reden bij de vorige taak en is daarmee ingelost.
|
||||||
|
|
||||||
|
- [x] **Gelijkgetrokken met Electrum Gate.** Dezelfde `max-width: 1760px`, hetzelfde raster van twaalf
|
||||||
|
kolommen, hetzelfde icoon van 64 pixels met terugval, dezelfde `t-h1` van 2rem, hetzelfde menu met
|
||||||
|
drie punten, en het versienummer achter de tagline. De pagina leende dat ontwerp al maar op eigen maten
|
||||||
|
- [x] **Een tijdvenster van twee minuten voor nieuwe eigenaars, met een stopknop.** **De teller zit in het
|
||||||
|
relay-proces en niet in de pagina**, en dat is de kern van deze taak: een teller in een tabblad dat je
|
||||||
|
sluit, sluit de deur niet. `policy.js` heeft er een veld `learningUntil` voor gekregen, plus
|
||||||
|
`isLearningOpen` en `expireLearning`
|
||||||
|
- [x] **`decideOwner` kijkt naar `isLearningOpen` en niet naar het veld `learning`.** Dat is geen
|
||||||
|
netheid: de lus die het bestand opruimt loopt elke twee seconden, en in dat gat zou een onbekende
|
||||||
|
alsnog binnenkomen. Muteertest gedaan: de juiste drie toetsen vielen om
|
||||||
|
- [x] **Zonder `STATE_VERSION` te verhogen**, en met een toets die dat verdedigt. Een verhoging zou
|
||||||
|
`normalizeState` het bestand van de draaiende installatie laten afwijzen, en dan schuift `store.js` de
|
||||||
|
allowlist opzij en gaat de deur dicht voor eigenaars die er al in stonden
|
||||||
|
- [x] **Geweigerde en geblokkeerde eigenaars in één kader.** Wat je ermee doet is hetzelfde: toelaten of
|
||||||
|
vergeten. Welke van de twee een regel is, staat als badge op de regel, en die badge staat **buiten**
|
||||||
|
het hover-blok. Dat was de eerste van de twee fouten die de voorbeeldweergave vond: erbinnen was het
|
||||||
|
onderscheid onzichtbaar tenzij je over de regel ging, en dat is juist de informatie waarvoor de twee
|
||||||
|
kaders zijn samengevoegd
|
||||||
|
- [x] **De uitleggende regels eruit.** De koppen zeggen genoeg; wat er te weten valt staat in de nieuwe
|
||||||
|
dialoog. Wat níet weggegooid is maar verplaatst: dat het adres met `http` moet beginnen en niet met
|
||||||
|
`ws`, en wat de relay wel en niet kan zien
|
||||||
|
- [x] **Knoppen alleen bij hover, per regel.** Met opacity en niet met `display: none`, zodat ze met de
|
||||||
|
tab-toets bereikbaar blijven; `focus-within` maakt ze dan zichtbaar, en op een aanraakscherm staan ze
|
||||||
|
altijd aan. Dit had één gevolg dat je niet ziet aankomen: onzichtbare knoppen houden hun ruimte, dus
|
||||||
|
de kop "New owners" brak over twee regels en de waarde stond lager dan in de drie kaders ernaast.
|
||||||
|
Opgelost met kortere knoptekst plus hetzelfde afbreekpunt van 1400px dat Gate gebruikt
|
||||||
|
- [x] **Geen voetregel, en een menu-item "About this app" in plaats daarvan.** In **beide** apps, op verzoek
|
||||||
|
van de gebruiker. Bij Gate staat daar bovendien wat de statuswidget wél en niet bewijst
|
||||||
|
- [x] **Labels op een eigenaar-id, te onderhouden.** Hover een regel en klik **Label**: het veld komt in de
|
||||||
|
plaats van de titel, Enter bewaart, Escape breekt af. **De labels staan in een eigen bestand
|
||||||
|
`labels.json` met de agent als enige schrijver**, en niet in `owners.json`. Drie redenen: één schrijver
|
||||||
|
per bestand blijft de afspraak, een label is meteen opgeslagen in plaats van na de volgende ronde van
|
||||||
|
de relay, en labelen blijft werken als de relay omgevallen is. De relay hoeft dit niet te weten, want
|
||||||
|
een label zegt niets over wie er binnen mag
|
||||||
|
- [x] **De statuswidget van Electrum Gate verduidelijkt**, op de opmerking van de gebruiker dat die van
|
||||||
|
Evolu Relay veel duidelijker is. Er stond "Answering" met "answered in 7 ms, from inside the app"; nu
|
||||||
|
"Running" met de meting eronder, en de nuance staat onder een eigen kopje in de nieuwe dialoog
|
||||||
|
|
||||||
|
### Wat er aan toetsen bij kwam
|
||||||
|
|
||||||
|
De pagina's zijn samen bijna drieduizend regels en er stond geen enkele toets op. Dat blijft zo voor wat ze
|
||||||
|
tonen, maar drie klassen fouten hoeven niet op het apparaat gevonden te worden:
|
||||||
|
|
||||||
|
- [x] **`tests/test_relay_agent.py`** (nieuw, 65 toetsen): de labels en het tijdvenster in de agent. Twee
|
||||||
|
muteertests gedaan
|
||||||
|
- [x] **`tests/test_paginas_parsen.mjs`** (nieuw): loopt de JavaScript van elke pagina, en loopt hij ook nog
|
||||||
|
ná de invulling door umbreld. Muteertest gedaan met een echte syntaxfout
|
||||||
|
- [x] **Een dollarteken-toets in `test_appstore_vorm.py`.** Dit is de valstrik van dit hele project, en het
|
||||||
|
commentaar in drie bestanden beweerde al dat déze test hem dichthield terwijl dat niet zo was. Nu
|
||||||
|
wel. Hij vond meteen twee valse positieven in de eigen opzet (een kaal dollarteken in een
|
||||||
|
commentaarregel, en de exports van een afhankelijkheid), en die zijn in de toets opgelost en niet in
|
||||||
|
de bestanden
|
||||||
|
- [x] **Een toets dat er geen werkbestanden in een app-map staan.** umbreld kopieert de héle app-map naar
|
||||||
|
het apparaat, dus een bewerkbestand van een icoon van 175 kB gaat mee in elke back-up. Die stond er
|
||||||
|
op 30-08-2026
|
||||||
|
- [x] **`tools/voorbeeldpagina.mjs`** (nieuw): maakt van een `*.template` een pagina die je in een browser
|
||||||
|
kunt openen, met de variabelen ingevuld en nagemaakte gegevens erin. Geen toets en geen bewijs, maar
|
||||||
|
het vond in één keer de twee fouten hierboven. Uitvoer komt in `voorbeeld/` en die map is gitignored
|
||||||
|
|
||||||
|
### De twee onderzoeksvragen
|
||||||
|
|
||||||
|
- [x] **"Komen geweigerde eigenaars toch met de hele blow aan instellingen in de database?" Nee.**
|
||||||
|
`isOwnerAllowed` wordt in de WebSocket-**upgrade** aangeroepen; een weigering is een HTTP 401 en een
|
||||||
|
gesloten socket, dus er wordt niets opgeslagen. **Maar het gewenste gevolg treedt wel op**: Evolu is
|
||||||
|
local-first, de cliënt houdt alles zelf en probeert opnieuw, dus alsnog toelaten brengt de hele
|
||||||
|
geschiedenis binnen. De redenering met bronregels staat in
|
||||||
|
[Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md) §10, en daar staat ook de
|
||||||
|
scherpe kant: **blokkeren werkt pas bij de volgende verbinding**, want voor een lopende verbinding
|
||||||
|
wordt de vraag nooit opnieuw gesteld. Dat staat als open punt 10 in [OPEN.md](OPEN.md)
|
||||||
|
- [x] **"Kunnen we de blobs ontcijferen met de xpub?" Nee, en de premisse klopt niet.** Een `OwnerId` wordt
|
||||||
|
niet uit een xpub gemaakt: `OwnerId`, `OwnerEncryptionKey` en `OwnerWriteKey` komen alle drie met
|
||||||
|
SLIP-21 uit één 32-byte node die het apparaat aflevert. Dat spoor loopt langs de seed en niet langs
|
||||||
|
BIP32, dus uit een xpub is er niets te halen. **Met die node zelf kan het wél**, en `trezorlib` heeft
|
||||||
|
er een functie voor. Waarom dat níet in deze app hoort, en waar het wél hoort, staat in §11 van
|
||||||
|
hetzelfde document; het plan-punt staat in de repo `HomeGit/Trezor` bij **AdvancedUI**
|
||||||
|
|
||||||
## Fase 1 - De app-map
|
## Fase 1 - De app-map
|
||||||
|
|
||||||
- [x] **`whatsnext-evolu-relay/` aangemaakt met `umbrel-app.yml` en `docker-compose.yml` (25-08-2026).**
|
- [x] **`whatsnext-evolu-relay/` aangemaakt met `umbrel-app.yml` en `docker-compose.yml` (25-08-2026).**
|
||||||
@@ -130,11 +234,8 @@ Wat er niet staat: iOS doet niets mee, en daardoor is de leeskant nooit gemeten.
|
|||||||
zie fase 4. **Let op dat dit één architectuur is** (amd64), want er is alleen amd64 geduwd. Voor de
|
zie fase 4. **Let op dat dit één architectuur is** (amd64), want er is alleen amd64 geduwd. Voor de
|
||||||
officiele store hoort er een multi-arch index-digest met arm64 in; dat staat bij **Publicatie-Relay**
|
officiele store hoort er een multi-arch index-digest met arm64 in; dat staat bij **Publicatie-Relay**
|
||||||
- [x] **De Postgres-pin is vervallen (28-08-2026):** die database zit niet meer in de app
|
- [x] **De Postgres-pin is vervallen (28-08-2026):** die database zit niet meer in de app
|
||||||
- [ ] **De statuspagina verbeteren, op een lijst van de gebruiker.** Hij gaat er eerst mee werken en komt
|
- [x] **De statuspagina verbeteren, op een lijst van de gebruiker.** De lijst kwam op 30-08-2026 en is in
|
||||||
dan met wat er beter moet (28-08-2026). Tot die lijst er is, is dit géén werk: de pagina doet wat hij
|
één ronde uitgevoerd; zie fase 6 hieronder
|
||||||
moet doen. **Doe dit vóór het plan Eigenimage voor deze app**, want zolang de pagina een `*.template`
|
|
||||||
is bereikt elke wijziging een installatie met een push plus versieverhoging; zit hij eenmaal in een
|
|
||||||
image, dan kost elke tweak een bouw en een digest. **Eigenaar: gebruiker levert de lijst**
|
|
||||||
- [ ] **`python:3-alpine` en `nginx:alpine` pinnen op hun multi-arch index-digest.** Dat zijn de twee laatste
|
- [ ] **`python:3-alpine` en `nginx:alpine` pinnen op hun multi-arch index-digest.** Dat zijn de twee laatste
|
||||||
ongepinde images van deze app, en ze staan als TODO in de compose. Met
|
ongepinde images van deze app, en ze staan als TODO in de compose. Met
|
||||||
`docker buildx imagetools inspect` op de Umbrel. Electrum Gate heeft precies dezelfde twee openstaan,
|
`docker buildx imagetools inspect` op de Umbrel. Electrum Gate heeft precies dezelfde twee openstaan,
|
||||||
|
|||||||
@@ -404,3 +404,90 @@ mensen hier tegenaan lopen.
|
|||||||
waarna de rest lokaal blijft. Dat is een totaal, en het is de grens die hun quota-manager bewaakt. Een
|
waarna de rest lokaal blijft. Dat is een totaal, en het is de grens die hun quota-manager bewaakt. Een
|
||||||
zelf-gehoste relay kent die grens niet: wij begrenzen alleen één schrijfactie (zie §8), en dat is dus een
|
zelf-gehoste relay kent die grens niet: wij begrenzen alleen één schrijfactie (zie §8), en dat is dus een
|
||||||
echt verschil in wat de app oplevert en geen detail.
|
echt verschil in wat de app oplevert en geen detail.
|
||||||
|
|
||||||
|
## 10. Wat er met een geweigerde eigenaar gebeurt: niets
|
||||||
|
|
||||||
|
Uitgezocht op 30-08-2026 op de vraag van de gebruiker of een geweigerde eigenaar "toch met de hele blow
|
||||||
|
aan instellingen in de database komt", want dan zou hem daarna toelaten meteen werken. **Het antwoord is
|
||||||
|
nee, en de reden maakt de vraag onbelangrijk.**
|
||||||
|
|
||||||
|
`isOwnerAllowed` wordt aangeroepen in de **WebSocket-upgrade** en niet bij het verwerken van een bericht.
|
||||||
|
De relevante regels uit `packages/nodejs/src/local-first/Relay.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const ownerId = requestUrl ? parseOwnerIdFromOwnerWebSocketTransportUrl(requestUrl) : undefined;
|
||||||
|
// ...
|
||||||
|
if (!result.value) { respondAndDestroy(401); return; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Wat daaruit volgt, en elk punt heeft gevolgen voor het pakket:
|
||||||
|
|
||||||
|
1. **de `OwnerId` staat in de URL van de verbinding.** Hij is dus bekend vóórdat er één bericht over de
|
||||||
|
lijn is, en dat is precies waarom deze goedkope toegangscontrole kan bestaan;
|
||||||
|
2. **een weigering is een HTTP 401 en een gesloten socket.** Er komt geen verbinding tot stand, er wordt
|
||||||
|
geen protocolbericht verwerkt, en er wordt **niets** in de SQLite van de relay geschreven. In
|
||||||
|
`Protocol.ts` bestaat geen eigenaarscontrole; daar zit alleen de write-key-validatie, en die code wordt
|
||||||
|
in dit geval nooit bereikt;
|
||||||
|
3. **de wens van de gebruiker komt alsnog uit, alleen via een andere weg.** Evolu is local-first: de
|
||||||
|
cliënt houdt alles zelf en probeert opnieuw. Laat je de eigenaar later toe, dan komt bij de volgende
|
||||||
|
verbinding de héle geschiedenis binnen. Het werkt dus meteen, niet omdat de relay iets bewaard had maar
|
||||||
|
omdat de cliënt niets kwijt was;
|
||||||
|
4. **en dit is de scherpe kant: `isOwnerAllowed` wordt voor een lopende verbinding nooit opnieuw
|
||||||
|
gesteld.** Iemand blokkeren werkt dus pas bij zijn volgende verbinding. Zolang zijn socket openstaat,
|
||||||
|
blijft hij schrijven. Voor een app die "Block" als knop aanbiedt is dat een grens om te kennen; hij
|
||||||
|
staat als open punt in het plan **Umbrelapp**.
|
||||||
|
|
||||||
|
Bron, geraadpleegd 30-08-2026:
|
||||||
|
[`packages/nodejs/src/local-first/Relay.ts`](https://raw.githubusercontent.com/evoluhq/evolu/main/packages/nodejs/src/local-first/Relay.ts)
|
||||||
|
en [`packages/common/src/local-first/Protocol.ts`](https://raw.githubusercontent.com/evoluhq/evolu/main/packages/common/src/local-first/Protocol.ts).
|
||||||
|
|
||||||
|
## 11. De blobs ontcijferen: niet met een xpub, wél met de OwnerSecret
|
||||||
|
|
||||||
|
Uitgezocht op 30-08-2026 op de gedachte van de gebruiker: we hebben de broncode van Trezor Suite en de
|
||||||
|
xpub waarmee deze `OwnerId`'s gemaakt worden, dus zou de app de blobs kunnen ontcijferen en er
|
||||||
|
onderhoudsfuncties bij kunnen krijgen?
|
||||||
|
|
||||||
|
**De premisse klopt niet, en dat is het hele antwoord: een `OwnerId` wordt niet uit een xpub gemaakt.**
|
||||||
|
Uit `suite-common/suite-sync-evolu/src/createEvoluAppOwnerFromTrezorData.ts`, in de lokale kloon:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const ownerIdBytes = OwnerIdBytes.from(createSlip21(secret, ['OwnerIdBytes']).slice(0, 16));
|
||||||
|
const ownerEncryptionKey = OwnerEncryptionKey.from(createSlip21(secret, ['OwnerEncryptionKey']));
|
||||||
|
const ownerWriteKey = OwnerWriteKey.from(createSlip21(secret, ['OwnerWriteKey']).slice(0, 16));
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle drie komen met **SLIP-21** uit één `secret`, en dat secret is een SLIP-21-node die het apparaat
|
||||||
|
aflevert op het pad `['TREZOR', 'Evolu']` (`core/src/apps/evolu/get_node.py` in de firmware). SLIP-21 is
|
||||||
|
symmetrische afleiding uit de **seed**, langs een heel ander spoor dan de BIP32-afleiding waar een xpub in
|
||||||
|
zit. Een xpub bevat een publieke sleutel en een chaincode van één BIP32-tak; daar is de SLIP-21-wortel niet
|
||||||
|
uit te halen, in geen enkele richting. Dat is geen implementatiedetail maar de bedoeling: §1b legt uit
|
||||||
|
waarom de `OwnerId` juist géén geheim hoeft te zijn.
|
||||||
|
|
||||||
|
**Wat wél werkt is de `OwnerSecret` zelf**, en die is bereikbaar. Suite bewaart hem:
|
||||||
|
`suite-common/suite-sync-storage/src/owner/suiteSyncOwner.ts` heeft naast `ownerId` een veld
|
||||||
|
`ownerSecret` als hex, met in het commentaar "This is an SLIP21 node, it is provided by Trezor Device". En
|
||||||
|
`trezorlib` kan hem opvragen: `python/src/trezorlib/evolu.py` heeft `get_node(session, proof, ...)`, met
|
||||||
|
`get_delegated_identity_key` voor het bewijs dat het apparaat eist.
|
||||||
|
|
||||||
|
**Met die 32 bytes heb je alles**: `OwnerId` om te weten welke rijen bij welke wallet horen,
|
||||||
|
`OwnerEncryptionKey` om te ontsleutelen, `OwnerWriteKey` om te schrijven. Dat is dus geen leesrecht maar
|
||||||
|
volledige zeggenschap over de gegevens van die wallet.
|
||||||
|
|
||||||
|
**En daarom hoort dit niet in deze app.** De hele reden dat je een relay bij wie dan ook kunt neerzetten,
|
||||||
|
is dat hij de sleutel niet heeft. Zet je hem erin, dan is dat weg, en wel voor de kopie die op een
|
||||||
|
apparaat staat dat aan het internet hangt. Onderhoudsfuncties die de inhoud moeten kennen horen aan de
|
||||||
|
kant die de sleutel al heeft. **Dat is de tool in `HomeGit/Trezor`**, die al met `trezorlib` tegen het
|
||||||
|
apparaat praat; het staat daar als plan-punt bij **AdvancedUI**.
|
||||||
|
|
||||||
|
Wat een relay-app zónder sleutel wel kan, en dat is niet niks: rijen per `OwnerId` opruimen. `ownerId` is
|
||||||
|
een systeemkolom in `evolu_message` en `evolu_history` (§8). Dat is schrijven in andermans schema, met de
|
||||||
|
bezwaren die daar staan, maar het vraagt geen sleutel.
|
||||||
|
|
||||||
|
Bronnen, geraadpleegd 30-08-2026 in de lokale klonen (zie §7):
|
||||||
|
|
||||||
|
- `suite-common/suite-sync-evolu/src/createEvoluAppOwnerFromTrezorData.ts`
|
||||||
|
- `suite-common/suite-sync-evolu/src/evoluCreateSuiteSyncOwner.ts`
|
||||||
|
- `suite-common/suite-sync/src/owner/createRetrieveSuiteSyncOwner.ts`
|
||||||
|
- `suite-common/suite-sync-storage/src/owner/suiteSyncOwner.ts`
|
||||||
|
- `trezor-firmware/core/src/apps/evolu/get_node.py`
|
||||||
|
- `trezor-firmware/python/src/trezorlib/evolu.py`
|
||||||
|
|||||||
@@ -316,6 +316,126 @@ def test_containernamen_zijn_volledig(u, app):
|
|||||||
not fout, "korte namen: %r" % fout)
|
not fout, "korte namen: %r" % fout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_geen_werkbestanden_in_de_app_map(u, app):
|
||||||
|
"""In een app-map staat alleen wat umbreld nodig heeft.
|
||||||
|
|
||||||
|
umbreld kopieert bij een installatie de héle app-map naar
|
||||||
|
`~/umbrel/app-data/<app-id>/` met `rsync --archive`. Alles wat daar staat
|
||||||
|
belandt dus op het apparaat en in de back-up: een CLAUDE.md, een gelaagd
|
||||||
|
bewerkbestand van een icoon, een testscript, een aantekening.
|
||||||
|
|
||||||
|
Dat is geen theorie. Op 30-08-2026 stond er een `icon.pdn` van 175 kB in de map
|
||||||
|
van Evolu Relay, een Paint.NET-bestand van een icoon dat nog niet af was. Het
|
||||||
|
apparaat kan er niets mee en het maakt elke back-up groter. Verplaatst naar
|
||||||
|
`tools/icons/`; deze toets is wat voorkomt dat de volgende terugkomt.
|
||||||
|
|
||||||
|
De lijst hieronder is een whitelist en geen blacklist, en dat is met opzet: bij
|
||||||
|
een nieuw soort bestand hoort iemand na te denken of het daar hoort, en een
|
||||||
|
blacklist stelt die vraag nooit.
|
||||||
|
"""
|
||||||
|
TOEGESTAAN_EXACT = {
|
||||||
|
"umbrel-app.yml", "docker-compose.yml", "exports.sh", "torrc",
|
||||||
|
"icon.png", "icon.svg", ".gitkeep",
|
||||||
|
}
|
||||||
|
TOEGESTAANE_MAPPEN = {"data", "hooks"}
|
||||||
|
|
||||||
|
fout = []
|
||||||
|
for naam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||||
|
pad = os.path.join(REPO, app, naam)
|
||||||
|
|
||||||
|
if os.path.isdir(pad):
|
||||||
|
# __pycache__ is niet gecommit (het staat in .gitignore) en komt dus
|
||||||
|
# nooit in de kloon die umbreld ophaalt. Het staat er in een werkboom
|
||||||
|
# zodra een test de agent-template importeert, en daarover klagen zou
|
||||||
|
# de toets rood zetten op iets dat het apparaat niet bereikt.
|
||||||
|
if naam == "__pycache__":
|
||||||
|
continue
|
||||||
|
if naam not in TOEGESTAANE_MAPPEN:
|
||||||
|
fout.append(naam + "/")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if naam in TOEGESTAAN_EXACT:
|
||||||
|
continue
|
||||||
|
# Alles wat umbreld bij een update ververst is een template, en die horen
|
||||||
|
# er dus per definitie.
|
||||||
|
if naam.endswith(".template"):
|
||||||
|
continue
|
||||||
|
fout.append(naam)
|
||||||
|
|
||||||
|
u.check("%s: er staan geen werkbestanden in de app-map" % app,
|
||||||
|
not fout,
|
||||||
|
"deze horen buiten de app-map: %r" % fout)
|
||||||
|
|
||||||
|
|
||||||
|
# De variabelen die umbrelOS werkelijk invult. Alleen deze mogen in een template
|
||||||
|
# staan; zie test_templates_hebben_geen_losse_dollars. Overgenomen uit de tabel in
|
||||||
|
# Docs/Referenties/Umbrel-appstore-spec.md §3.
|
||||||
|
UMBREL_VARIABELEN = {
|
||||||
|
"APP_ID", "APP_VERSION", "APP_DATA_DIR", "APP_MANIFEST_FILE", "UMBREL_ROOT",
|
||||||
|
"DEVICE_HOSTNAME", "DEVICE_DOMAIN_NAME", "APP_DOMAIN",
|
||||||
|
"APP_PROXY_HOSTNAME", "APP_PROXY_PORT", "NETWORK_IP",
|
||||||
|
"TOR_PROXY_IP", "TOR_PROXY_PORT", "TOR_DATA_DIR",
|
||||||
|
"APP_HIDDEN_SERVICE", "APP_SEED", "APP_PASSWORD",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Een app krijgt daarnaast de exports van zijn afhankelijkheden, en die heten
|
||||||
|
# APP_<AFHANKELIJKHEID>_<IETS>. Electrum Gate gebruikt APP_ELECTRS_NODE_IP en
|
||||||
|
# APP_ELECTRS_NODE_PORT. Die kunnen niet in een vaste lijst staan, want welke er
|
||||||
|
# zijn hangt af van de afhankelijkheid; vandaar een vorm in plaats van een naam.
|
||||||
|
EXPORT_VORM = r"^APP_[A-Z0-9]+_[A-Z0-9_]+$"
|
||||||
|
|
||||||
|
|
||||||
|
def test_templates_hebben_geen_losse_dollars(u, app):
|
||||||
|
"""In een *.template staat geen dollarteken dat umbrelOS niet invult.
|
||||||
|
|
||||||
|
Dit is de valstrik van dit hele project, en tot 30-08-2026 beweerde het
|
||||||
|
commentaar in drie bestanden dat déze test hem dichthield terwijl dat niet zo
|
||||||
|
was.
|
||||||
|
|
||||||
|
Wat er gebeurt: umbreld haalt elke `*.template` bij het starten door envsubst.
|
||||||
|
Dat vervangt élke accolade-vorm, ook een variabele die niet bestaat, en die
|
||||||
|
wordt dan leeg. Gevolgen per bestandstype:
|
||||||
|
|
||||||
|
- in een pagina sloopt het een JavaScript-template-literal, want die gebruikt
|
||||||
|
accolades achter een dollarteken. Vandaar dat de pagina's overal strings met
|
||||||
|
een plus aan elkaar plakken;
|
||||||
|
- in een nginx-config verdwijnen `host`, `log_format` en elke variabele in een
|
||||||
|
access_log;
|
||||||
|
- in Python-code verdwijnt stilzwijgend een stuk code.
|
||||||
|
|
||||||
|
Er komt geen foutmelding. Het bestand wordt gewoon anders dan je schreef.
|
||||||
|
|
||||||
|
Beide vormen die envsubst kent worden gevlagd: `${NAAM}` en het kale `$NAAM`.
|
||||||
|
Dat tweede is de vorm waarin een nginx-variabele als `$host` geschreven wordt,
|
||||||
|
en die is hier dus net zo fout als de eerste.
|
||||||
|
|
||||||
|
Wat NIET gevlagd wordt is een los dollarteken zonder naam erachter. Dat laat
|
||||||
|
envsubst staan, en het staat in dit project in de commentaarregels die deze
|
||||||
|
regel juist uitleggen: een toets die zijn eigen uitleg rood zet, wordt
|
||||||
|
uitgezet.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||||
|
if not bestandsnaam.endswith(".template"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
tekst = lees(os.path.join(REPO, app, bestandsnaam))
|
||||||
|
onbekend = []
|
||||||
|
for regelnummer, regel in enumerate(tekst.splitlines(), start=1):
|
||||||
|
for treffer in re.finditer(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?", regel):
|
||||||
|
naam = treffer.group(1)
|
||||||
|
if naam in UMBREL_VARIABELEN:
|
||||||
|
continue
|
||||||
|
if re.match(EXPORT_VORM, naam):
|
||||||
|
continue
|
||||||
|
onbekend.append("regel %d: %r" % (regelnummer, treffer.group(0)))
|
||||||
|
|
||||||
|
u.check("%s/%s: geen dollartekens die umbrelOS leegmaakt" % (app, bestandsnaam),
|
||||||
|
not onbekend,
|
||||||
|
"gevonden: %r" % onbekend[:4])
|
||||||
|
|
||||||
|
|
||||||
def rapporteer_pinstatus(apps):
|
def rapporteer_pinstatus(apps):
|
||||||
"""Afdrukken, niet toetsen. Zie de uitleg bovenaan dit bestand."""
|
"""Afdrukken, niet toetsen. Zie de uitleg bovenaan dit bestand."""
|
||||||
print()
|
print()
|
||||||
@@ -346,6 +466,8 @@ def main():
|
|||||||
test_compose_bestaat_en_hangt_samen(u, app)
|
test_compose_bestaat_en_hangt_samen(u, app)
|
||||||
test_data_onder_data(u, app)
|
test_data_onder_data(u, app)
|
||||||
test_containernamen_zijn_volledig(u, app)
|
test_containernamen_zijn_volledig(u, app)
|
||||||
|
test_geen_werkbestanden_in_de_app_map(u, app)
|
||||||
|
test_templates_hebben_geen_losse_dollars(u, app)
|
||||||
|
|
||||||
rapporteer_pinstatus(apps)
|
rapporteer_pinstatus(apps)
|
||||||
return u.rapport()
|
return u.rapport()
|
||||||
|
|||||||
@@ -13,11 +13,14 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
MAX_LEARNING_SECONDS,
|
||||||
MAX_OWNER_ID_LENGTH,
|
MAX_OWNER_ID_LENGTH,
|
||||||
MAX_REJECTED,
|
MAX_REJECTED,
|
||||||
applyCommand,
|
applyCommand,
|
||||||
createEmptyState,
|
createEmptyState,
|
||||||
decideOwner,
|
decideOwner,
|
||||||
|
expireLearning,
|
||||||
|
isLearningOpen,
|
||||||
normalizeState,
|
normalizeState,
|
||||||
} from '../tools/evolu-relay/src/policy.js';
|
} from '../tools/evolu-relay/src/policy.js';
|
||||||
|
|
||||||
@@ -35,8 +38,12 @@ const toets = (omschrijving, voorwaarde) => {
|
|||||||
|
|
||||||
const NU = '2026-08-28T12:00:00.000Z';
|
const NU = '2026-08-28T12:00:00.000Z';
|
||||||
const LATER = '2026-08-28T13:00:00.000Z';
|
const LATER = '2026-08-28T13:00:00.000Z';
|
||||||
|
// Binnen en buiten een venster van twee minuten dat op NU begint.
|
||||||
|
const BINNEN = '2026-08-28T12:01:00.000Z';
|
||||||
|
const NA = '2026-08-28T12:02:30.000Z';
|
||||||
const EIGEN = 'owner-van-de-gebruiker';
|
const EIGEN = 'owner-van-de-gebruiker';
|
||||||
const VREEMD = 'owner-van-een-ander';
|
const VREEMD = 'owner-van-een-ander';
|
||||||
|
const TWEE_MINUTEN = 120;
|
||||||
|
|
||||||
// ── De leerstand ──────────────────────────────────────────────────────────────
|
// ── De leerstand ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -164,6 +171,160 @@ const VREEMD = 'owner-van-een-ander';
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Het tijdvenster voor nieuwe eigenaars ─────────────────────────────────────
|
||||||
|
//
|
||||||
|
// De pagina zet de deur twee minuten open in plaats van een schakelaar om te
|
||||||
|
// zetten die je moet onthouden. De teller hoort in dít bestand en niet in de
|
||||||
|
// browser: een teller in een tabblad dat je sluit, sluit de deur niet.
|
||||||
|
|
||||||
|
{
|
||||||
|
const vers = createEmptyState();
|
||||||
|
toets('een verse staat heeft geen tijdslot', vers.learningUntil === null);
|
||||||
|
toets('en staat dus open zonder dat er een klok loopt', isLearningOpen(vers, LATER) === true);
|
||||||
|
|
||||||
|
const open = applyCommand(
|
||||||
|
vers,
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
NU,
|
||||||
|
);
|
||||||
|
toets('openzetten met seconden is een wijziging', open.changed === true);
|
||||||
|
toets('en zet een tijdslot', open.state.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||||
|
toets('binnen het venster staat de deur open', isLearningOpen(open.state, BINNEN) === true);
|
||||||
|
toets('erna niet meer', isLearningOpen(open.state, NA) === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// De guard die er het meest toe doet, en dit is de reden dat `decideOwner` naar
|
||||||
|
// `isLearningOpen` kijkt en niet naar het veld `learning`: tussen het aflopen van
|
||||||
|
// het venster en de ronde die het bestand opruimt zitten een paar seconden. In dat
|
||||||
|
// gat mag er niemand binnenkomen.
|
||||||
|
{
|
||||||
|
const open = applyCommand(
|
||||||
|
createEmptyState(),
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
NU,
|
||||||
|
).state;
|
||||||
|
|
||||||
|
toets('in het bestand staat de leerstand nog aan', open.learning === true);
|
||||||
|
|
||||||
|
const binnen = decideOwner(open, EIGEN, BINNEN);
|
||||||
|
toets('binnen het venster wordt een onbekende geleerd', binnen.allowed === true);
|
||||||
|
|
||||||
|
const erna = decideOwner(open, VREEMD, NA);
|
||||||
|
toets(
|
||||||
|
'na het venster wordt hij geweigerd, ook al staat learning nog op true',
|
||||||
|
erna.allowed === false,
|
||||||
|
);
|
||||||
|
toets('met de reden dat er niet geleerd wordt', erna.reason === 'not-learning');
|
||||||
|
toets('en de poging wordt onthouden voor de pagina', erna.state.rejected.length === 1);
|
||||||
|
|
||||||
|
// En wie al binnen was, blijft binnen. Het venster gaat over onbekenden.
|
||||||
|
const bekend = decideOwner(binnen.state, EIGEN, LATER);
|
||||||
|
toets('een geleerde eigenaar mag er na het venster nog steeds in', bekend.allowed === true);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const open = applyCommand(
|
||||||
|
createEmptyState(),
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
NU,
|
||||||
|
).state;
|
||||||
|
|
||||||
|
// Opnieuw indrukken verlengt. Zonder deze uitzondering zou `applyCommand`
|
||||||
|
// 'changed: false' teruggeven omdat learning al true was, en dan liep het
|
||||||
|
// venster af terwijl de gebruiker net verlengde.
|
||||||
|
const opnieuw = applyCommand(
|
||||||
|
open,
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
BINNEN,
|
||||||
|
);
|
||||||
|
toets('opnieuw openzetten is een wijziging', opnieuw.changed === true);
|
||||||
|
toets('en zet de klok terug', opnieuw.state.learningUntil === '2026-08-28T12:03:00.000Z');
|
||||||
|
toets('dus is de deur na het oude venster nog open', isLearningOpen(opnieuw.state, NA) === true);
|
||||||
|
|
||||||
|
// Vroegtijdig sluiten haalt het tijdslot mee weg. Zou het blijven staan, dan
|
||||||
|
// erfde een volgende 'open zonder seconden' een venster dat niet gevraagd is.
|
||||||
|
const dicht = applyCommand(open, { action: 'set-learning', value: false }, BINNEN);
|
||||||
|
toets('vroegtijdig sluiten kan', dicht.state.learning === false);
|
||||||
|
toets('en haalt het tijdslot weg', dicht.state.learningUntil === null);
|
||||||
|
toets('de deur is dan dicht', isLearningOpen(dicht.state, BINNEN) === false);
|
||||||
|
|
||||||
|
const zonderSlot = applyCommand(dicht.state, { action: 'set-learning', value: true }, BINNEN);
|
||||||
|
toets('openzetten zonder seconden blijft bestaan', zonderSlot.state.learning === true);
|
||||||
|
toets('en heeft dan geen tijdslot', zonderSlot.state.learningUntil === null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wat de agent hoort tegen te houden, houdt dit bestand ook tegen. Twee zeven, en
|
||||||
|
// dat is met opzet: dit is de kant die van buiten komt.
|
||||||
|
{
|
||||||
|
const vers = createEmptyState();
|
||||||
|
const rommel = [
|
||||||
|
{ action: 'set-learning', value: true, seconds: 0 },
|
||||||
|
{ action: 'set-learning', value: true, seconds: -60 },
|
||||||
|
{ action: 'set-learning', value: true, seconds: 1.5 },
|
||||||
|
{ action: 'set-learning', value: true, seconds: '120' },
|
||||||
|
{ action: 'set-learning', value: true, seconds: true },
|
||||||
|
{ action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS + 1 },
|
||||||
|
];
|
||||||
|
for (const opdracht of rommel) {
|
||||||
|
const uitkomst = applyCommand(vers, opdracht, NU);
|
||||||
|
toets(
|
||||||
|
`een onbruikbaar aantal seconden wordt geweigerd: ${JSON.stringify(opdracht.seconds)}`,
|
||||||
|
uitkomst.error === 'malformed-command',
|
||||||
|
);
|
||||||
|
toets('en verandert niets', uitkomst.changed === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
toets(
|
||||||
|
'de bovengrens zelf mag wel',
|
||||||
|
applyCommand(vers, { action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS }, NU)
|
||||||
|
.error === null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Een onleesbaar `now` mag geen venster zonder einde opleveren. Dat is de reden
|
||||||
|
// dat `addSeconds` null teruggeeft in plaats van iets te verzinnen.
|
||||||
|
const kapotteKlok = applyCommand(
|
||||||
|
vers,
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
'geen tijdstip',
|
||||||
|
);
|
||||||
|
toets('met een onleesbare klok wordt de opdracht geweigerd', kapotteKlok.error === 'malformed-command');
|
||||||
|
toets('en blijft de staat zoals hij was', kapotteKlok.changed === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Het opruimen. Correctheid hangt hier niet aan, want `isLearningOpen` weigert al;
|
||||||
|
// wat dit oplevert is dat het bestand en de pagina hetzelfde zeggen als de klok.
|
||||||
|
{
|
||||||
|
const open = applyCommand(
|
||||||
|
createEmptyState(),
|
||||||
|
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||||
|
NU,
|
||||||
|
).state;
|
||||||
|
|
||||||
|
const nogNiet = expireLearning(open, BINNEN);
|
||||||
|
toets('binnen het venster wordt er niets opgeruimd', nogNiet.changed === false);
|
||||||
|
toets('en de staat blijft dezelfde', nogNiet.state === open);
|
||||||
|
|
||||||
|
const opgeruimd = expireLearning(open, NA);
|
||||||
|
toets('na het venster gaat de leerstand uit', opgeruimd.state.learning === false);
|
||||||
|
toets('en het tijdslot weg', opgeruimd.state.learningUntil === null);
|
||||||
|
toets('en dat is een wijziging', opgeruimd.changed === true);
|
||||||
|
|
||||||
|
toets(
|
||||||
|
'nog een keer opruimen doet niets',
|
||||||
|
expireLearning(opgeruimd.state, LATER).changed === false,
|
||||||
|
);
|
||||||
|
toets(
|
||||||
|
'een leerstand zonder tijdslot wordt niet opgeruimd',
|
||||||
|
expireLearning(createEmptyState(), LATER).changed === false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Een onleesbaar tijdstip in het bestand is dicht, niet open. Dezelfde regel als
|
||||||
|
// bij een onleesbare owners.json in store.js: bij twijfel weigert de app.
|
||||||
|
const kapot = { ...createEmptyState(), learningUntil: 'ergens volgende week' };
|
||||||
|
toets('een onleesbaar tijdslot leest als dicht', isLearningOpen(kapot, NU) === false);
|
||||||
|
toets('en een onleesbare klok ook', isLearningOpen(open, 'geen tijdstip') === false);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Lezen van schijf ──────────────────────────────────────────────────────────
|
// ── Lezen van schijf ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -185,6 +346,28 @@ const VREEMD = 'owner-van-een-ander';
|
|||||||
for (const invoer of rommel) {
|
for (const invoer of rommel) {
|
||||||
toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null);
|
toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Het tijdslot is toegevoegd zonder STATE_VERSION te verhogen, en dat is de
|
||||||
|
// toets die dat verdedigt: een bestand van een draaiende installatie mag niet
|
||||||
|
// ineens onleesbaar worden. Zou dat gebeuren, dan schuift store.js de allowlist
|
||||||
|
// opzij en gaat de deur dicht voor eigenaars die er al in stonden.
|
||||||
|
const oud = { version: 1, learning: true, owners: [{ id: EIGEN, allowed: true }], rejected: [] };
|
||||||
|
const gelezen = normalizeState(oud);
|
||||||
|
toets('een bestand van vóór het tijdslot leest nog', gelezen !== null);
|
||||||
|
toets('met de leerstand die erin stond', gelezen.learning === true);
|
||||||
|
toets('en zonder tijdslot', gelezen.learningUntil === null);
|
||||||
|
toets('en de eigenaar er nog in', gelezen.owners[0].id === EIGEN);
|
||||||
|
|
||||||
|
const metSlot = normalizeState({ ...oud, learningUntil: '2026-08-28T12:02:00.000Z' });
|
||||||
|
toets('een geldig tijdslot blijft staan', metSlot.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||||
|
|
||||||
|
for (const onzin of ['morgen', '', 42, {}, null]) {
|
||||||
|
toets(
|
||||||
|
`een onleesbaar tijdslot wordt null en geen weigering: ${JSON.stringify(onzin)}`,
|
||||||
|
normalizeState({ ...oud, learningUntil: onzin }) !== null
|
||||||
|
&& normalizeState({ ...oud, learningUntil: onzin }).learningUntil === null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// Toetst dat de JavaScript in de statuspagina's parseert, en dat hij dat ook nog
|
||||||
|
// doet nadat umbreld de template heeft ingevuld.
|
||||||
|
//
|
||||||
|
// Draaien: node tests/test_paginas_parsen.mjs
|
||||||
|
//
|
||||||
|
// Waarom dit bestand er is. De pagina's zijn met bijna drieduizend regels het
|
||||||
|
// grootste deel van deze repo en er staat geen enkele toets op: alles eraan is
|
||||||
|
// handwerk in een browser. Dat blijft zo voor wat het doet, maar één klasse fouten
|
||||||
|
// hoeft niet op het apparaat gevonden te worden: een tikfout waardoor het script
|
||||||
|
// helemaal niet loopt. Het gevolg daarvan is een pagina die stil op "unknown"
|
||||||
|
// blijft staan, en dat lijkt op een agent die niet antwoordt.
|
||||||
|
//
|
||||||
|
// Aanleiding: op 30-08-2026 is de pagina van Evolu Relay in zijn geheel opnieuw
|
||||||
|
// geschreven, negenhonderd regels in één keer. Zo'n wijziging hoort niet ongetoetst
|
||||||
|
// naar een apparaat te gaan.
|
||||||
|
//
|
||||||
|
// De invulling wordt hier nagedaan en niet alleen de bron gelezen, want umbreld
|
||||||
|
// haalt elke *.template door envsubst en wat er draait is dus niet wat er staat.
|
||||||
|
// Dat vindt de gevallen waar het weghalen van een accolade-vorm de syntaxis breekt.
|
||||||
|
//
|
||||||
|
// LET OP wat deze helft NIET vindt, want dat is precies de fout die dit project
|
||||||
|
// vreest: een template-literal als `iets ${x}` wordt na de invulling `iets ` en dat
|
||||||
|
// parseert prima. Hij is dan stil kapot en niet luidruchtig. Wat dát dichthoudt is
|
||||||
|
// de dollarteken-toets in tests/test_appstore_vorm.py, die élke accolade-vorm
|
||||||
|
// vlagt die umbrelOS niet zelf invult. Deze toets is de tweede zeef en niet de
|
||||||
|
// eerste.
|
||||||
|
//
|
||||||
|
// Wat dit verder NIET toetst: of het script het juiste doet. Alleen dat het loopt.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO = join(HIER, '..');
|
||||||
|
|
||||||
|
let goed = 0;
|
||||||
|
let fout = 0;
|
||||||
|
|
||||||
|
const toets = (omschrijving, voorwaarde, uitleg) => {
|
||||||
|
if (voorwaarde) {
|
||||||
|
goed += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fout += 1;
|
||||||
|
console.log(`FOUT: ${omschrijving}${uitleg ? ` - ${uitleg}` : ''}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Elke map in de repo-root met een umbrel-app.yml erin. Zelf gevonden, zoals de
|
||||||
|
* Python-toets over de store dat ook doet: een derde app valt er dan vanzelf
|
||||||
|
* onder. */
|
||||||
|
const appMappen = () =>
|
||||||
|
readdirSync(REPO, { withFileTypes: true })
|
||||||
|
.filter((regel) => regel.isDirectory())
|
||||||
|
.map((regel) => regel.name)
|
||||||
|
.filter((naam) => existsSync(join(REPO, naam, 'umbrel-app.yml')))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
/** De inhoud van elk <script>-blok zonder src-attribuut. */
|
||||||
|
const scriptBlokken = (html) => {
|
||||||
|
const blokken = [];
|
||||||
|
const patroon = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
|
||||||
|
let treffer = patroon.exec(html);
|
||||||
|
while (treffer !== null) {
|
||||||
|
blokken.push(treffer[1]);
|
||||||
|
treffer = patroon.exec(html);
|
||||||
|
}
|
||||||
|
return blokken;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doet na wat umbreld met een template doet.
|
||||||
|
*
|
||||||
|
* envsubst vervangt `${NAAM}` en `$NAAM` door de waarde uit de omgeving, en door
|
||||||
|
* een lege string als die variabele niet bestaat. Wat wij hier invullen is een
|
||||||
|
* plausibele waarde voor de variabelen die de pagina's gebruiken; voor de rest de
|
||||||
|
* lege string, want dat is precies wat er op het apparaat zou gebeuren.
|
||||||
|
*/
|
||||||
|
const vulIn = (tekst) => {
|
||||||
|
const WAARDEN = { APP_VERSION: '0.5.0' };
|
||||||
|
return tekst.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (heel, naam) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(WAARDEN, naam) ? WAARDEN[naam] : '',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parseert het script zonder het te draaien. Geeft de foutmelding of null. */
|
||||||
|
const parseert = (bron) => {
|
||||||
|
try {
|
||||||
|
// new vm.Script parseert bij het aanmaken en draait niets. Dat is precies wat
|
||||||
|
// hier nodig is: draaien zou een document en een window vragen die er niet
|
||||||
|
// zijn, en dan toets je je eigen namaakbrowser.
|
||||||
|
new vm.Script(bron);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
return String(error && error.message ? error.message : error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const app of appMappen()) {
|
||||||
|
const pad = join(REPO, app, 'index.html.template');
|
||||||
|
if (!existsSync(pad)) {
|
||||||
|
// Geen pagina is geen fout: een app hoeft er geen te hebben.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bron = readFileSync(pad, 'utf8');
|
||||||
|
const blokken = scriptBlokken(bron);
|
||||||
|
|
||||||
|
toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0);
|
||||||
|
|
||||||
|
blokken.forEach((blok, index) => {
|
||||||
|
const naam = `${app}: scriptblok ${index + 1}`;
|
||||||
|
|
||||||
|
const rauw = parseert(blok);
|
||||||
|
toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw);
|
||||||
|
|
||||||
|
const ingevuld = parseert(vulIn(blok));
|
||||||
|
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
|
||||||
|
});
|
||||||
|
|
||||||
|
// De HTML zelf: geen parser, maar wel de fout die je in een bestand van
|
||||||
|
// tweeduizend regels echt maakt. Een dialog of een header die niet gesloten is,
|
||||||
|
// laat de rest van de pagina in dat element hangen.
|
||||||
|
for (const tag of ['html', 'head', 'body', 'header', 'script', 'style', 'dialog', 'details']) {
|
||||||
|
const open = (bron.match(new RegExp(`<${tag}[\\s>]`, 'gi')) || []).length;
|
||||||
|
const dicht = (bron.match(new RegExp(`</${tag}>`, 'gi')) || []).length;
|
||||||
|
if (open === 0 && dicht === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toets(
|
||||||
|
`${app}: elke <${tag}> is gesloten`,
|
||||||
|
open === dicht,
|
||||||
|
`${open} keer open, ${dicht} keer dicht`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`${goed} goed, ${fout} fout`);
|
||||||
|
process.exit(fout === 0 ? 0 : 1);
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
"""Toetst de agent van Evolu Relay: de labels en het tijdvenster.
|
||||||
|
|
||||||
|
Waarom deze test bestaat. De agent van deze app beslist niets over toegang, en dat
|
||||||
|
is precies waarom hij tot 30-08-2026 ongetoetst kon blijven: hij las een bestand en
|
||||||
|
legde opdrachten in een postbus, en het beleid eromheen staat in `policy.js` met
|
||||||
|
`tests/test_limiter.mjs` erop. Met deze release doet hij twee dingen erbij die
|
||||||
|
eigen logica hebben:
|
||||||
|
|
||||||
|
- **hij schrijft zelf een bestand**, `labels.json`, en is daar de enige schrijver
|
||||||
|
van. Dat is de eerste plek waar de agent staat bijhoudt in plaats van doorgeeft,
|
||||||
|
en labelen is lezen-wijzigen-schrijven, dus er zit een slot om;
|
||||||
|
- **hij rekent het tijdvenster uit** dat de pagina toont. Een verlopen venster moet
|
||||||
|
hij als dicht rapporteren, ook al staat er in owners.json nog dat de leerstand
|
||||||
|
aan is: het relay-proces ruimt dat op zijn eigen moment op.
|
||||||
|
|
||||||
|
Wat deze test NIET dekt: de HTTP-laag. De handlers zitten in een
|
||||||
|
BaseHTTPRequestHandler en die is zonder socket niet aan te roepen; wat eronder
|
||||||
|
hangt, de validatie en het schrijven, is hier wél getoetst. De pagina zelf blijft
|
||||||
|
handwerk in een browser.
|
||||||
|
|
||||||
|
Draaien:
|
||||||
|
|
||||||
|
python tests/test_relay_agent.py
|
||||||
|
|
||||||
|
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen
|
||||||
|
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat;
|
||||||
|
de eerste toets hieronder controleert precies dat.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Vóór de imports, want anders is het te laat: Python legt bytecode naast
|
||||||
|
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de
|
||||||
|
# app-map. Een keer is zo'n .pyc meegegaan in een commit.
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
|
import importlib.machinery # noqa: E402
|
||||||
|
import importlib.util # noqa: E402
|
||||||
|
import json # noqa: E402
|
||||||
|
import os # noqa: E402
|
||||||
|
import tempfile # noqa: E402
|
||||||
|
from datetime import datetime, timedelta, timezone # noqa: E402
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
APP = os.path.join(HERE, os.pardir, "whatsnext-evolu-relay")
|
||||||
|
TEMPLATE = os.path.join(APP, "agent.py.template")
|
||||||
|
|
||||||
|
|
||||||
|
def load_agent(state_dir):
|
||||||
|
"""Laadt agent.py.template als module, met zijn staat in een tijdelijke map.
|
||||||
|
|
||||||
|
De agent leest `RELAY_STATE_DIR` op moduleniveau, dus die moet vóór het laden
|
||||||
|
in de omgeving staan. Met een expliciete loader, want importlib kijkt normaal
|
||||||
|
naar de extensie en .template staat daar niet tussen.
|
||||||
|
"""
|
||||||
|
os.environ["RELAY_STATE_DIR"] = state_dir
|
||||||
|
loader = importlib.machinery.SourceFileLoader("relay_agent", TEMPLATE)
|
||||||
|
spec = importlib.util.spec_from_file_location("relay_agent", TEMPLATE, loader=loader)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class Uitslag:
|
||||||
|
def __init__(self):
|
||||||
|
self.goed = 0
|
||||||
|
self.fout = []
|
||||||
|
|
||||||
|
def check(self, naam, gelukt, uitleg=""):
|
||||||
|
if gelukt:
|
||||||
|
self.goed += 1
|
||||||
|
else:
|
||||||
|
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||||
|
|
||||||
|
def rapport(self):
|
||||||
|
print()
|
||||||
|
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||||
|
for f in self.fout:
|
||||||
|
print(" FOUT: " + f)
|
||||||
|
return 0 if not self.fout else 1
|
||||||
|
|
||||||
|
|
||||||
|
def iso(seconden_vanaf_nu):
|
||||||
|
"""Een tijdstip in de vorm die het relay-proces schrijft: ISO, met een Z."""
|
||||||
|
when = datetime.now(timezone.utc) + timedelta(seconds=seconden_vanaf_nu)
|
||||||
|
return when.isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def schrijf_owners(state_dir, staat):
|
||||||
|
with open(os.path.join(state_dir, "owners.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(staat, f)
|
||||||
|
|
||||||
|
|
||||||
|
# ── De aanname waar deze hele test op rust ───────────────────────────────────
|
||||||
|
|
||||||
|
def test_template_is_invulbaar_zonder_schade(u):
|
||||||
|
with open(TEMPLATE, "r", encoding="utf-8") as f:
|
||||||
|
inhoud = f.read()
|
||||||
|
u.check("template bevat geen dollartekens",
|
||||||
|
"$" not in inhoud,
|
||||||
|
"umbreld zou die invullen en de Python-code slopen")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Het tijdvenster ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_venster_dat_nog_loopt(agent, u):
|
||||||
|
feiten = agent.learning_facts({"learning": True, "learningUntil": iso(90)})
|
||||||
|
u.check("een lopend venster leest als open", feiten["learning"] is True)
|
||||||
|
u.check("met een resterende tijd", isinstance(feiten["learningSecondsLeft"], int))
|
||||||
|
u.check("die ergens rond de negentig seconden ligt",
|
||||||
|
85 <= feiten["learningSecondsLeft"] <= 90,
|
||||||
|
"kreeg %r" % feiten["learningSecondsLeft"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_venster_dat_verlopen_is(agent, u):
|
||||||
|
"""De toets die er het meest toe doet aan deze kant.
|
||||||
|
|
||||||
|
In het bestand staat `learning: true` en een tijdstip dat voorbij is. Het
|
||||||
|
relay-proces ruimt dat op in zijn lus van twee seconden, dus er is een moment
|
||||||
|
waarop het bestand nog "aan" zegt terwijl de deur dicht is. De pagina hoort dan
|
||||||
|
"closed" te tonen en geen open deur.
|
||||||
|
"""
|
||||||
|
feiten = agent.learning_facts({"learning": True, "learningUntil": iso(-30)})
|
||||||
|
u.check("een verlopen venster leest als dicht", feiten["learning"] is False)
|
||||||
|
u.check("met nul seconden over", feiten["learningSecondsLeft"] == 0)
|
||||||
|
u.check("en zonder tijdstip", feiten["learningUntil"] is None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_venster_zonder_tijdslot(agent, u):
|
||||||
|
feiten = agent.learning_facts({"learning": True, "learningUntil": None})
|
||||||
|
u.check("open zonder tijdslot blijft open", feiten["learning"] is True)
|
||||||
|
u.check("en heeft geen teller", feiten["learningSecondsLeft"] is None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_onleesbaar_tijdslot(agent, u):
|
||||||
|
"""Onleesbaar hoort niet als "voor altijd open" te lezen, maar wel als open.
|
||||||
|
|
||||||
|
Anders dan bij `policy.js`, en dat is met opzet: dit is de weergave en niet de
|
||||||
|
beslissing. Het relay-proces beslist, en dáár is een onleesbaar tijdstip dicht.
|
||||||
|
De pagina die iets anders toont dan het bestand zegt zou verwarrender zijn dan
|
||||||
|
een pagina die de leerstand toont die er staat.
|
||||||
|
"""
|
||||||
|
for onzin in ["morgen", "", 42, {}, []]:
|
||||||
|
feiten = agent.learning_facts({"learning": True, "learningUntil": onzin})
|
||||||
|
u.check("een onleesbaar tijdslot valt terug op de leerstand zelf: %r" % (onzin,),
|
||||||
|
feiten["learning"] is True and feiten["learningSecondsLeft"] is None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_leerstand_onbekend(agent, u):
|
||||||
|
"""Onbekend is geen synonym voor uit.
|
||||||
|
|
||||||
|
Ontbreekt owners.json, dan is er nog nooit iets geschreven en weet niemand wat
|
||||||
|
de leerstand is. Uit is een keuze, onbekend is een reden om te kijken, en de
|
||||||
|
pagina hoort dat verschil te tonen.
|
||||||
|
"""
|
||||||
|
feiten = agent.learning_facts({})
|
||||||
|
u.check("een ontbrekende leerstand blijft onbekend", feiten["learning"] is None)
|
||||||
|
u.check("en heeft geen teller", feiten["learningSecondsLeft"] is None)
|
||||||
|
|
||||||
|
uit = agent.learning_facts({"learning": False, "learningUntil": None})
|
||||||
|
u.check("en uit blijft uit", uit["learning"] is False)
|
||||||
|
|
||||||
|
|
||||||
|
# ── De labels ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_label_zetten_en_weghalen(agent, u):
|
||||||
|
u.check("een label zetten lukt", agent.apply_label("owner-a", "Laptop") is None)
|
||||||
|
u.check("en staat er daarna", agent.read_labels().get("owner-a") == "Laptop")
|
||||||
|
|
||||||
|
u.check("overschrijven lukt", agent.apply_label("owner-a", "Werklaptop") is None)
|
||||||
|
u.check("en vervangt de vorige", agent.read_labels().get("owner-a") == "Werklaptop")
|
||||||
|
|
||||||
|
u.check("een leeg label haalt hem weg", agent.apply_label("owner-a", "") is None)
|
||||||
|
u.check("en dan is hij er niet meer", "owner-a" not in agent.read_labels())
|
||||||
|
|
||||||
|
u.check("nog een keer weghalen is geen fout",
|
||||||
|
agent.apply_label("owner-a", "") is None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_blijft_op_schijf(agent, u, state_dir):
|
||||||
|
"""Het bestand is JSON en te lezen zonder de agent.
|
||||||
|
|
||||||
|
Dat is geen formaliteit: dit bestand staat onder de app-datamap en gaat mee in
|
||||||
|
de back-up van umbrelOS. Een gebruiker die zijn labels kwijt is, moet ze daar
|
||||||
|
kunnen terugvinden.
|
||||||
|
"""
|
||||||
|
agent.apply_label("owner-b", "Telefoon")
|
||||||
|
with open(os.path.join(state_dir, "labels.json"), "r", encoding="utf-8") as f:
|
||||||
|
op_schijf = json.load(f)
|
||||||
|
u.check("het label staat in labels.json", op_schijf.get("owner-b") == "Telefoon")
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_validatie(agent, u):
|
||||||
|
goed = agent.valid_label({"ownerId": "owner-c", "label": " Mijn laptop "})
|
||||||
|
u.check("witruimte wordt samengetrokken", goed[1] == "Mijn laptop")
|
||||||
|
u.check("en er is geen fout", goed[2] is None)
|
||||||
|
|
||||||
|
# Regeleindes eruit: dit is één regel naast een id, en een label met een nieuwe
|
||||||
|
# regel erin zou de lijst uit elkaar trekken.
|
||||||
|
plat = agent.valid_label({"ownerId": "owner-c", "label": "een\ntwee"})
|
||||||
|
u.check("een regeleinde wordt een spatie", plat[1] == "een twee")
|
||||||
|
|
||||||
|
leeg = agent.valid_label({"ownerId": "owner-c"})
|
||||||
|
u.check("een ontbrekend label is leeg en geen fout", leeg[1] == "" and leeg[2] is None)
|
||||||
|
|
||||||
|
for rommel, waarom in [
|
||||||
|
({}, "geen ownerId"),
|
||||||
|
({"ownerId": ""}, "leeg ownerId"),
|
||||||
|
({"ownerId": 42}, "ownerId is geen tekst"),
|
||||||
|
({"ownerId": "x" * (agent.MAX_OWNER_ID_LENGTH + 1)}, "ownerId te lang"),
|
||||||
|
({"ownerId": "owner-c", "label": 42}, "label is geen tekst"),
|
||||||
|
({"ownerId": "owner-c", "label": "x" * (agent.MAX_LABEL_LENGTH + 1)}, "label te lang"),
|
||||||
|
("geen object", "geen object"),
|
||||||
|
]:
|
||||||
|
u.check("een onbruikbaar label wordt geweigerd: %s" % waarom,
|
||||||
|
agent.valid_label(rommel)[2] is not None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_labels_lopen_niet_vol(agent, u):
|
||||||
|
"""Er zit een plafond op. Niet tegen een aanvaller, want deze pagina zit achter
|
||||||
|
de inlog van umbrelOS, maar tegen een lus die per ongeluk blijft schrijven.
|
||||||
|
"""
|
||||||
|
for i in range(agent.MAX_LABELS):
|
||||||
|
agent.apply_label("bulk-%d" % i, "label %d" % i)
|
||||||
|
|
||||||
|
u.check("het plafond is bereikt", len(agent.read_labels()) == agent.MAX_LABELS)
|
||||||
|
u.check("en een label erboven wordt geweigerd",
|
||||||
|
agent.apply_label("een-te-veel", "nog een") is not None)
|
||||||
|
u.check("maar een bestaand label mag nog wél gewijzigd worden",
|
||||||
|
agent.apply_label("bulk-0", "gewijzigd") is None,
|
||||||
|
"anders kun je bij een vol bestand niets meer verbeteren")
|
||||||
|
|
||||||
|
|
||||||
|
def test_onleesbare_labels_zijn_geen_ramp(agent, u, state_dir):
|
||||||
|
"""Anders dan owners.json: daar hangt aan een half begrepen bestand de vraag wie
|
||||||
|
er binnen mag, en dan is weigeren het antwoord. Hier gaat het om een naam naast
|
||||||
|
een id, en het ergste gevolg is dat je de rauwe ids ziet.
|
||||||
|
"""
|
||||||
|
with open(os.path.join(state_dir, "labels.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write("dit is geen json {{{")
|
||||||
|
u.check("een onleesbaar labelbestand leest als leeg", agent.read_labels() == {})
|
||||||
|
|
||||||
|
with open(os.path.join(state_dir, "labels.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"owner-d": "Goed", "owner-e": 42, "": "geen id", "owner-f": " "}, f)
|
||||||
|
labels = agent.read_labels()
|
||||||
|
u.check("de goede regel blijft", labels.get("owner-d") == "Goed")
|
||||||
|
u.check("een label dat geen tekst is valt weg", "owner-e" not in labels)
|
||||||
|
u.check("een leeg id valt weg", "" not in labels)
|
||||||
|
u.check("een label van alleen witruimte valt weg", "owner-f" not in labels)
|
||||||
|
|
||||||
|
|
||||||
|
# ── De opdracht met seconden ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_set_learning_met_seconden(agent, u):
|
||||||
|
goed, problem = agent.valid_command({"action": "set-learning", "value": True, "seconds": 120})
|
||||||
|
u.check("openzetten met seconden mag", problem is None)
|
||||||
|
u.check("en de seconden gaan mee naar de relay", goed.get("seconds") == 120)
|
||||||
|
|
||||||
|
zonder, problem = agent.valid_command({"action": "set-learning", "value": True})
|
||||||
|
u.check("openzetten zonder seconden mag ook", problem is None)
|
||||||
|
u.check("en dan staat er geen seconds in de opdracht", "seconds" not in zonder)
|
||||||
|
|
||||||
|
for seconden, waarom in [
|
||||||
|
(0, "nul"),
|
||||||
|
(-60, "negatief"),
|
||||||
|
(1.5, "geen heel getal"),
|
||||||
|
("120", "tekst"),
|
||||||
|
# isinstance(True, int) is in Python waar, dus zonder de uitsluiting in de
|
||||||
|
# agent zou een boolean hier als aantal seconden doorglippen.
|
||||||
|
(True, "een boolean"),
|
||||||
|
(agent.MAX_LEARNING_SECONDS + 1, "boven de bovengrens"),
|
||||||
|
]:
|
||||||
|
_, problem = agent.valid_command(
|
||||||
|
{"action": "set-learning", "value": True, "seconds": seconden})
|
||||||
|
u.check("een onbruikbaar aantal seconden wordt geweigerd: %s" % waarom,
|
||||||
|
problem is not None)
|
||||||
|
|
||||||
|
_, problem = agent.valid_command(
|
||||||
|
{"action": "set-learning", "value": True, "seconds": agent.MAX_LEARNING_SECONDS})
|
||||||
|
u.check("de bovengrens zelf mag wel", problem is None)
|
||||||
|
|
||||||
|
# Seconden bij dichtzetten is onzin en hoort dus geweigerd te worden in plaats
|
||||||
|
# van stil genegeerd: stil negeren zou een sluitopdracht met een tikfout laten
|
||||||
|
# slagen terwijl er iets anders gebeurt dan er staat.
|
||||||
|
_, problem = agent.valid_command(
|
||||||
|
{"action": "set-learning", "value": False, "seconds": 120})
|
||||||
|
u.check("seconden bij dichtzetten wordt geweigerd", problem is not None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_hangt_labels_aan_de_regels(agent, u, state_dir):
|
||||||
|
"""De pagina krijgt het label bij de eigenaar, en het bestand van de relay
|
||||||
|
blijft ongemoeid. Dat tweede is de reden dat `met_label` een kopie maakt.
|
||||||
|
"""
|
||||||
|
schrijf_owners(state_dir, {
|
||||||
|
"version": 1,
|
||||||
|
"learning": False,
|
||||||
|
"learningUntil": None,
|
||||||
|
"owners": [
|
||||||
|
{"id": "owner-g", "allowed": True, "firstSeen": iso(-600), "lastSeen": iso(-60)},
|
||||||
|
{"id": "owner-h", "allowed": False, "firstSeen": iso(-900), "lastSeen": None},
|
||||||
|
],
|
||||||
|
"rejected": [{"id": "owner-i", "firstSeen": iso(-300), "lastSeen": iso(-10),
|
||||||
|
"attempts": 3}],
|
||||||
|
})
|
||||||
|
agent.apply_label("owner-g", "Laptop")
|
||||||
|
agent.apply_label("owner-i", "Onbekend apparaat")
|
||||||
|
|
||||||
|
status = agent.build_status()
|
||||||
|
owners = status["owners"]
|
||||||
|
|
||||||
|
u.check("de toegelaten eigenaar krijgt zijn label",
|
||||||
|
owners["allowed"][0]["label"] == "Laptop")
|
||||||
|
u.check("de geblokkeerde staat in blocked", owners["blocked"][0]["id"] == "owner-h")
|
||||||
|
u.check("en heeft geen label", owners["blocked"][0]["label"] is None)
|
||||||
|
u.check("de geweigerde poging staat in rejected", owners["rejected"][0]["id"] == "owner-i")
|
||||||
|
u.check("en krijgt zijn label", owners["rejected"][0]["label"] == "Onbekend apparaat")
|
||||||
|
u.check("het aantal pogingen gaat mee", owners["rejected"][0]["attempts"] == 3)
|
||||||
|
|
||||||
|
u.check("de leerstand komt uit learning_facts", owners["learning"] is False)
|
||||||
|
u.check("en de statusvelden voor de teller staan erin",
|
||||||
|
"learningSecondsLeft" in owners and "learningUntil" in owners)
|
||||||
|
|
||||||
|
with open(os.path.join(state_dir, "owners.json"), "r", encoding="utf-8") as f:
|
||||||
|
op_schijf = json.load(f)
|
||||||
|
u.check("owners.json is niet aangeraakt",
|
||||||
|
all("label" not in regel for regel in op_schijf["owners"]),
|
||||||
|
"de agent hoort niet in het bestand van de relay te schrijven")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
u = Uitslag()
|
||||||
|
test_template_is_invulbaar_zonder_schade(u)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as state_dir:
|
||||||
|
agent = load_agent(state_dir)
|
||||||
|
|
||||||
|
test_venster_dat_nog_loopt(agent, u)
|
||||||
|
test_venster_dat_verlopen_is(agent, u)
|
||||||
|
test_venster_zonder_tijdslot(agent, u)
|
||||||
|
test_onleesbaar_tijdslot(agent, u)
|
||||||
|
test_leerstand_onbekend(agent, u)
|
||||||
|
|
||||||
|
test_label_zetten_en_weghalen(agent, u)
|
||||||
|
test_label_blijft_op_schijf(agent, u, state_dir)
|
||||||
|
test_label_validatie(agent, u)
|
||||||
|
test_onleesbare_labels_zijn_geen_ramp(agent, u, state_dir)
|
||||||
|
|
||||||
|
test_set_learning_met_seconden(agent, u)
|
||||||
|
test_status_hangt_labels_aan_de_regels(agent, u, state_dir)
|
||||||
|
|
||||||
|
# Als laatste: deze vult het labelbestand tot het plafond en laat dus geen
|
||||||
|
# bruikbare staat achter voor een toets erna.
|
||||||
|
test_labels_lopen_niet_vol(agent, u)
|
||||||
|
|
||||||
|
return u.rapport()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -36,7 +36,7 @@ set -eu
|
|||||||
# verandert, want anders rolt umbrelOS hem niet uit. Dat heeft hier een keer een
|
# 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
|
# dag gekost. Ze zijn dus gelijk zolang alleen de image wijzigt, en lopen uiteen
|
||||||
# zodra er een reparatie in de app-map zit.
|
# zodra er een reparatie in de app-map zit.
|
||||||
VERSION="0.3.0"
|
VERSION="0.5.0"
|
||||||
|
|
||||||
# Het register staat er expres in en dit is geen smaakkwestie: umbreld haalt élke
|
# 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
|
# image op via de Docker Engine API, dus een tag die alleen lokaal bestaat is voor
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { installPolyfills } from '@evolu/common/polyfills';
|
|||||||
import { createRelay, createRelayDeps, runMain } from '@evolu/nodejs';
|
import { createRelay, createRelayDeps, runMain } from '@evolu/nodejs';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
|
|
||||||
import { applyCommand, decideOwner } from './policy.js';
|
import { applyCommand, decideOwner, expireLearning } from './policy.js';
|
||||||
import { readState, takeCommand, writeState } from './store.js';
|
import { readState, takeCommand, writeState } from './store.js';
|
||||||
|
|
||||||
installPolyfills();
|
installPolyfills();
|
||||||
@@ -100,8 +100,27 @@ const pollCommands = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Het tijdvenster voor nieuwe eigenaars ─────────────────────────────────────
|
||||||
|
// De pagina kan de leerstand voor een aantal seconden openzetten. Dat aflopen
|
||||||
|
// gebeurt niet híer maar in `isLearningOpen`, dat `decideOwner` gebruikt: een
|
||||||
|
// verlopen venster weigert al vóór deze lus langskomt. Wat deze lus doet is het
|
||||||
|
// bestand bijwerken, zodat de pagina "closed" toont in plaats van een venster dat
|
||||||
|
// afgelopen is.
|
||||||
|
//
|
||||||
|
// Waarom de teller in de relay zit en niet in de pagina: een teller in de browser
|
||||||
|
// verdwijnt als je het tabblad sluit, en dan blijft de deur openstaan zonder dat
|
||||||
|
// iemand dat ziet. Dit is de enige plek waar de staat gezaghebbend is.
|
||||||
|
const closeExpiredLearning = () => {
|
||||||
|
const result = expireLearning(state, new Date().toISOString());
|
||||||
|
if (!result.changed) return;
|
||||||
|
state = result.state;
|
||||||
|
markDirty();
|
||||||
|
console.log('[info] the window for new owners has closed on its own');
|
||||||
|
};
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
pollCommands();
|
pollCommands();
|
||||||
|
closeExpiredLearning();
|
||||||
flush();
|
flush();
|
||||||
}, 2000).unref();
|
}, 2000).unref();
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,29 @@ export const MAX_REJECTED = 20;
|
|||||||
// verder geen aannames over de vorm.
|
// verder geen aannames over de vorm.
|
||||||
export const MAX_OWNER_ID_LENGTH = 256;
|
export const MAX_OWNER_ID_LENGTH = 256;
|
||||||
|
|
||||||
|
// De langste tijdvenster dat de pagina mag vragen. De pagina vraagt er twee
|
||||||
|
// minuten; deze grens staat er voor het geval iets anders de postbus vult. Een
|
||||||
|
// venster van een dag is geen venster meer, en dit is een toegangscontrole: bij
|
||||||
|
// twijfel de kortere kant.
|
||||||
|
export const MAX_LEARNING_SECONDS = 3600;
|
||||||
|
|
||||||
export const createEmptyState = () => ({
|
export const createEmptyState = () => ({
|
||||||
version: STATE_VERSION,
|
version: STATE_VERSION,
|
||||||
// Leerstand. Aan betekent: de eerstvolgende onbekende eigenaar wordt
|
// Leerstand. Aan betekent: de eerstvolgende onbekende eigenaar wordt
|
||||||
// toegelaten. Dit staat aan bij een verse installatie, want anders kan de
|
// toegelaten. Dit staat aan bij een verse installatie, want anders kan de
|
||||||
// eigenaar zichzelf nooit aanmelden: Trezor Suite toont je `OwnerId` nergens.
|
// eigenaar zichzelf nooit aanmelden: Trezor Suite toont je `OwnerId` nergens.
|
||||||
learning: true,
|
learning: true,
|
||||||
|
// Tot wanneer de leerstand open is, als ISO-tijdstip, of `null` voor "tot je
|
||||||
|
// hem zelf sluit". Dat tweede is de begintoestand: een verse installatie moet
|
||||||
|
// te koppelen zijn zonder dat er iemand op tijd op een knop drukt.
|
||||||
|
//
|
||||||
|
// Toegevoegd zonder STATE_VERSION te verhogen, en dat is een keuze. Een
|
||||||
|
// verhoging zou `normalizeState` het bestaande bestand laten afwijzen, en dan
|
||||||
|
// schuift `store.js` de allowlist van een werkende installatie opzij en gaat de
|
||||||
|
// deur dicht. Een veld bijzetten dat ontbrekend `null` betekent is niet
|
||||||
|
// brekend: een oud bestand leest goed, en een oude relay leest een nieuw
|
||||||
|
// bestand ook goed omdat hij het veld gewoon niet kent.
|
||||||
|
learningUntil: null,
|
||||||
owners: [],
|
owners: [],
|
||||||
rejected: [],
|
rejected: [],
|
||||||
});
|
});
|
||||||
@@ -39,6 +56,63 @@ const isUsableOwnerId = (value) =>
|
|||||||
|
|
||||||
const findOwner = (state, ownerId) => state.owners.find((owner) => owner.id === ownerId);
|
const findOwner = (state, ownerId) => state.owners.find((owner) => owner.id === ownerId);
|
||||||
|
|
||||||
|
/** Het tijdstip als getal, of `NaN` als er iets onleesbaars staat. */
|
||||||
|
const asMoment = (value) => (typeof value === 'string' ? Date.parse(value) : NaN);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `now` plus een aantal seconden, als ISO-tijdstip. `null` als dat niet kan.
|
||||||
|
*
|
||||||
|
* Geen klok hierin: `now` komt van de aanroeper, net als bij alles in dit
|
||||||
|
* bestand. Vandaar dat een onleesbare `now` een uitkomst heeft en geen fout: de
|
||||||
|
* aanroeper beslist wat hij met `null` doet, en in `applyCommand` is dat de
|
||||||
|
* opdracht weigeren. Stil "open zonder tijdslot" zou de verkeerde kant zijn.
|
||||||
|
*/
|
||||||
|
const addSeconds = (now, seconds) => {
|
||||||
|
const start = asMoment(now);
|
||||||
|
if (Number.isNaN(start)) return null;
|
||||||
|
return new Date(start + seconds * 1000).toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Staat de deur op dit moment open voor een onbekende eigenaar?
|
||||||
|
*
|
||||||
|
* Dit is de gezaghebbende vraag en niet het veld `learning` op zichzelf. Een
|
||||||
|
* venster dat verlopen is, is dicht, ook al staat er in het bestand nog dat de
|
||||||
|
* leerstand aan is: dat bestand wordt door de lus in `index.js` bijgewerkt en die
|
||||||
|
* loopt op zijn eigen moment. De correctheid mag niet aan die lus hangen, want
|
||||||
|
* dan zit er een gat van een seconde of twee in waarin een onbekende alsnog
|
||||||
|
* binnenkomt. Vandaar dat `decideOwner` deze functie gebruikt en niet het veld.
|
||||||
|
*/
|
||||||
|
export const isLearningOpen = (state, now) => {
|
||||||
|
if (state.learning !== true) return false;
|
||||||
|
if (state.learningUntil === null || state.learningUntil === undefined) return true;
|
||||||
|
|
||||||
|
const deadline = asMoment(state.learningUntil);
|
||||||
|
const moment = asMoment(now);
|
||||||
|
// Onleesbaar aan één van de twee kanten: dicht. Dat is de veilige kant, en het
|
||||||
|
// is dezelfde regel die `store.js` volgt bij een onleesbaar bestand.
|
||||||
|
if (Number.isNaN(deadline) || Number.isNaN(moment)) return false;
|
||||||
|
return moment < deadline;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruimt een verlopen venster op.
|
||||||
|
*
|
||||||
|
* Puur opruimwerk: `isLearningOpen` weigert al vóórdat dit gebeurd is. Wat dit
|
||||||
|
* oplevert is dat het bestand en de pagina hetzelfde zeggen als de klok, en dat
|
||||||
|
* je op de pagina "closed" ziet in plaats van een venster dat afgelopen is.
|
||||||
|
*
|
||||||
|
* @returns {{state: object, changed: boolean}}
|
||||||
|
*/
|
||||||
|
export const expireLearning = (state, now) => {
|
||||||
|
if (state.learning !== true) return { state, changed: false };
|
||||||
|
if (state.learningUntil === null || state.learningUntil === undefined) {
|
||||||
|
return { state, changed: false };
|
||||||
|
}
|
||||||
|
if (isLearningOpen(state, now)) return { state, changed: false };
|
||||||
|
return { state: { ...state, learning: false, learningUntil: null }, changed: true };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leest een staat die van schijf komt. Geeft `null` terug als het niet klopt.
|
* Leest een staat die van schijf komt. Geeft `null` terug als het niet klopt.
|
||||||
*
|
*
|
||||||
@@ -77,7 +151,17 @@ export const normalizeState = (raw) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { version: STATE_VERSION, learning: raw.learning, owners, rejected };
|
// Ontbrekend of onleesbaar wordt `null`, en dat betekent "geen tijdslot". Dat
|
||||||
|
// klinkt als de onveilige kant maar is het niet: `learning` moet daarnaast ook
|
||||||
|
// nog `true` zijn, en dat staat in hetzelfde bestand. Een oud bestand zonder
|
||||||
|
// dit veld hoort te lezen als de leerstand die het beschreef, en niet als een
|
||||||
|
// venster dat meteen verlopen is.
|
||||||
|
const learningUntil =
|
||||||
|
typeof raw.learningUntil === 'string' && !Number.isNaN(asMoment(raw.learningUntil))
|
||||||
|
? raw.learningUntil
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { version: STATE_VERSION, learning: raw.learning, learningUntil, owners, rejected };
|
||||||
};
|
};
|
||||||
|
|
||||||
const rememberRejected = (rejected, ownerId, now) => {
|
const rememberRejected = (rejected, ownerId, now) => {
|
||||||
@@ -121,7 +205,7 @@ export const decideOwner = (state, ownerId, now) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.learning) {
|
if (isLearningOpen(state, now)) {
|
||||||
const owners = [...state.owners, { id: ownerId, allowed: true, firstSeen: now, lastSeen: now }];
|
const owners = [...state.owners, { id: ownerId, allowed: true, firstSeen: now, lastSeen: now }];
|
||||||
return { allowed: true, state: { ...state, owners }, reason: 'learned', changed: true };
|
return { allowed: true, state: { ...state, owners }, reason: 'learned', changed: true };
|
||||||
}
|
}
|
||||||
@@ -151,8 +235,47 @@ export const applyCommand = (state, command, now) => {
|
|||||||
switch (command.action) {
|
switch (command.action) {
|
||||||
case 'set-learning': {
|
case 'set-learning': {
|
||||||
if (typeof command.value !== 'boolean') return unchanged('malformed-command');
|
if (typeof command.value !== 'boolean') return unchanged('malformed-command');
|
||||||
if (state.learning === command.value) return { state, changed: false, error: null };
|
|
||||||
return { state: { ...state, learning: command.value }, changed: true, error: null };
|
// Dicht is dicht: een tijdslot dat nog liep gaat mee weg. Zou het blijven
|
||||||
|
// staan, dan zou een volgende `set-learning true` zonder seconden een
|
||||||
|
// venster erven dat de gebruiker niet gevraagd heeft.
|
||||||
|
if (command.value === false) {
|
||||||
|
if (state.learning === false && !state.learningUntil) {
|
||||||
|
return { state, changed: false, error: null };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state: { ...state, learning: false, learningUntil: null },
|
||||||
|
changed: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open, en `seconds` bepaalt of dat met een tijdslot is. Ontbreekt het veld,
|
||||||
|
// dan is dat het oude gedrag: open tot je hem zelf sluit. Dat pad blijft
|
||||||
|
// bestaan omdat een verse installatie er niet mee gered is als het venster
|
||||||
|
// afloopt terwijl je nog aan het installeren bent.
|
||||||
|
if (command.seconds === undefined || command.seconds === null) {
|
||||||
|
if (state.learning === true && !state.learningUntil) {
|
||||||
|
return { state, changed: false, error: null };
|
||||||
|
}
|
||||||
|
return { state: { ...state, learning: true, learningUntil: null }, changed: true, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(command.seconds) ||
|
||||||
|
command.seconds <= 0 ||
|
||||||
|
command.seconds > MAX_LEARNING_SECONDS
|
||||||
|
) {
|
||||||
|
return unchanged('malformed-command');
|
||||||
|
}
|
||||||
|
|
||||||
|
const until = addSeconds(now, command.seconds);
|
||||||
|
if (until === null) return unchanged('malformed-command');
|
||||||
|
|
||||||
|
// Altijd `changed`, ook als de leerstand al open stond: opnieuw op de knop
|
||||||
|
// drukken hoort de klok terug te zetten. Vergelijken met de oude waarde zou
|
||||||
|
// hier een venster laten aflopen terwijl de gebruiker net verlengde.
|
||||||
|
return { state: { ...state, learning: true, learningUntil: until }, changed: true, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'block': {
|
case 'block': {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# De bronbestanden van de app-iconen
|
||||||
|
|
||||||
|
Hier hoort het werkbestand waaruit `icon.png` van een app komt: de gelaagde versie waarin je nog
|
||||||
|
kunt bewerken. Eén bestand per app, met de app-id als naam.
|
||||||
|
|
||||||
|
**Op dit moment staat er niets.** Beide iconen zijn buiten deze repo gemaakt. Op 28-08-2026 begon de
|
||||||
|
gebruiker aan een 3D-icoon voor Evolu Relay in Paint.NET; dat was op 30-08-2026 nog niet mooi genoeg
|
||||||
|
en is door hem weggehaald. Komt er een volgende poging, dan is dít de map.
|
||||||
|
|
||||||
|
## Waarom hier en niet in de app-map
|
||||||
|
|
||||||
|
**umbreld kopieert bij een installatie de héle app-map naar het apparaat**, met `rsync --archive`
|
||||||
|
naar `~/umbrel/app-data/<app-id>/`. Alles wat daar staat belandt dus op de Umbrel én in de back-up.
|
||||||
|
Voor een gelaagd bewerkbestand is dat allebei zinloos: het apparaat kan er niets mee en de back-up
|
||||||
|
wordt er alleen groter van.
|
||||||
|
|
||||||
|
Dat de app-map niets mag bevatten dat umbreld niet nodig heeft, controleert
|
||||||
|
`tests/test_appstore_vorm.py` sinds 30-08-2026. Kwam je hier omdat die toets over een bestand van
|
||||||
|
jou klaagde: dit is de plek waar het hoort.
|
||||||
|
|
||||||
|
## Wat er wél in de app-map staat
|
||||||
|
|
||||||
|
Alleen het resultaat: `icon.png`. Dat bestand staat **niet** in de whitelist die umbreld bij een
|
||||||
|
update ververst, dus een nieuw icoon bereikt een bestaande installatie niet. Twee dingen volgen
|
||||||
|
daaruit, en de tweede wordt makkelijk vergeten:
|
||||||
|
|
||||||
|
- het manifest verwijst met een `icon:`-regel naar de rauwe URL in deze repo, en dát is wat het
|
||||||
|
dashboard van umbrelOS toont. Een nieuw icoon is daar dus meteen zichtbaar;
|
||||||
|
- de kop van de statuspagina laadt `icon.png` uit de gemounte app-map, en die blijft na een update
|
||||||
|
het oude plaatje tonen tot de app opnieuw geïnstalleerd wordt. Reken er niet op dat een nieuw
|
||||||
|
icoon overal in één keer doorkomt.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// 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.');
|
||||||
@@ -560,6 +560,19 @@ body {
|
|||||||
want in de melding staat een zin die je eerst leest. */
|
want in de melding staat een zin die je eerst leest. */
|
||||||
.alert-action { margin-top: 0.7rem; }
|
.alert-action { margin-top: 0.7rem; }
|
||||||
|
|
||||||
|
/* De dialoog "About this app" is lopende tekst en geen lijst met waarden, dus hij
|
||||||
|
heeft opmaak nodig die de certificaatdialoog niet gebruikt. Kopjes in de maat
|
||||||
|
van een .t-label, want ze verdelen de tekst en zijn er niet om gelezen te
|
||||||
|
worden. */
|
||||||
|
.sheet-prose h3 {
|
||||||
|
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase; color: var(--text-sec);
|
||||||
|
margin: 1.6rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
.sheet-prose h3:first-child { margin-top: 0; }
|
||||||
|
.sheet-prose p { font-size: 0.85rem; line-height: 1.65; color: var(--text-sec); margin-bottom: 0.7rem; }
|
||||||
|
.sheet-prose p strong { color: var(--text-primary); font-weight: 600; }
|
||||||
|
|
||||||
/* ── Het instelkader ────────────────────────────────────────────────────── */
|
/* ── Het instelkader ────────────────────────────────────────────────────── */
|
||||||
.setup > summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: 0.8rem; flex-wrap: wrap; }
|
.setup > summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: 0.8rem; flex-wrap: wrap; }
|
||||||
.setup > summary::-webkit-details-marker { display: none; }
|
.setup > summary::-webkit-details-marker { display: none; }
|
||||||
@@ -692,6 +705,19 @@ body {
|
|||||||
<button class="menu-item" id="menu-certs" type="button">
|
<button class="menu-item" id="menu-certs" type="button">
|
||||||
<span>Settings…</span>
|
<span>Settings…</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- "About this app" op verzoek van de gebruiker (30-08-2026), en aan
|
||||||
|
beide apps in deze store toegevoegd. Hier stond de marketingtekst
|
||||||
|
nergens op de pagina: die staat in umbrel-app.yml en dus alleen in de
|
||||||
|
winkel, terwijl je juist ná het installeren nog eens wil kunnen
|
||||||
|
nalezen wat dit ding voor je doet en wat het niet doet.
|
||||||
|
|
||||||
|
Boven "Appearance" en onder "Settings…": van wat je het meest doet
|
||||||
|
naar wat je het minst doet is hier de verkeerde ordening, want dan zou
|
||||||
|
het thema bovenaan staan. De ordening is wat over de app gaat eerst,
|
||||||
|
wat over deze pagina gaat daarna. -->
|
||||||
|
<button class="menu-item" id="menu-about" type="button">
|
||||||
|
<span>About this app…</span>
|
||||||
|
</button>
|
||||||
<!-- De regel toont de huidige stand en niet de handeling. De losse knop
|
<!-- De regel toont de huidige stand en niet de handeling. De losse knop
|
||||||
hiervoor deed het andersom, met "Light" terwijl de pagina donker was.
|
hiervoor deed het andersom, met "Light" terwijl de pagina donker was.
|
||||||
Als knop klopte dat, want dat was waar je heen ging; als menuregel
|
Als knop klopte dat, want dat was waar je heen ging; als menuregel
|
||||||
@@ -943,6 +969,82 @@ body {
|
|||||||
</div><!-- /sheet-body -->
|
</div><!-- /sheet-body -->
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- De dialoog "About this app", toegevoegd op verzoek van de gebruiker
|
||||||
|
(30-08-2026) en in dezelfde vorm aan Evolu Relay.
|
||||||
|
|
||||||
|
De tekst is die uit umbrel-app.yml, ingekort en met kopjes erin. Wijzig je
|
||||||
|
daar iets aan de strekking, dan hoort het hier ook te veranderen; ze staan
|
||||||
|
niet met een test aan elkaar vast omdat de vorm verschilt.
|
||||||
|
|
||||||
|
Wat hier bovendien staat en nergens anders: de grens van de statuswidget.
|
||||||
|
Die zei tot 0.0.24 "answered in 7 ms, from inside the app", en dat was
|
||||||
|
precies genoeg tekst om onduidelijk te zijn en te weinig om de nuance te
|
||||||
|
dragen. De nuance is nu hier, waar er ruimte voor is. -->
|
||||||
|
<dialog id="about-dialog" class="sheet">
|
||||||
|
<form method="dialog" class="sheet-head">
|
||||||
|
<h2 class="t-h3">About this app</h2>
|
||||||
|
<button class="sheet-close" value="close" aria-label="Close" title="Close">×</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="sheet-body sheet-prose">
|
||||||
|
<h3>What it is for</h3>
|
||||||
|
<p>
|
||||||
|
The privacy win is already yours: you run the Electrum server. A public one gets asked for the
|
||||||
|
history of every address in your wallet, and that tells it which addresses and which balance
|
||||||
|
belong to one person. Your own server never reports back.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What is left is a trade. <strong>Tor</strong> is the more private way in, and every wallet
|
||||||
|
speaks it. But it adds hundreds of milliseconds to every request, it drops when a phone sleeps
|
||||||
|
or changes network, and plenty of networks block it outright. Electrum Gate is the other side
|
||||||
|
of that trade: a TLS front door on your node. Fast, no fingerprint to type over, and it works
|
||||||
|
on any network you happen to be on.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>The certificate</h3>
|
||||||
|
<p>
|
||||||
|
It reuses the Let's Encrypt certificate your reverse proxy already manages, so there is
|
||||||
|
nothing to request and nothing to renew. A renewal is picked up on its own, without dropping
|
||||||
|
connections that are already open. You can also upload a certificate you manage yourself, if
|
||||||
|
you do not run a reverse proxy on this machine.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When more than one certificate could fit and you have not chosen, the app <strong>refuses</strong>
|
||||||
|
rather than picking one. A wrong certificate gives a connection that looks fine and only breaks
|
||||||
|
later, inside your wallet, on name verification.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Nothing in the middle</h3>
|
||||||
|
<p>
|
||||||
|
The connection runs from your wallet straight to your own node, encrypted with a certificate
|
||||||
|
you already own, for a domain you already control. There is no account to create, no tunnel
|
||||||
|
service that terminates your traffic along the way, and no client to install on every device
|
||||||
|
you use: the wallets already speak TLS, they only need an address. What it does ask of you is
|
||||||
|
one forwarded port on your router.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>What the Status card does and does not prove</h3>
|
||||||
|
<p>
|
||||||
|
The app opens a TLS connection to its own front door every minute and checks that the
|
||||||
|
certificate it gets back is the one you selected. <strong>Running</strong> means that
|
||||||
|
succeeded.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It is measured from inside the app, so it says nothing about the way in from outside: a router
|
||||||
|
that no longer forwards the port, or a firewall in between, still reads as Running here. If a
|
||||||
|
wallet cannot connect while this card says Running, the fault is on the route and not in this
|
||||||
|
app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Which wallets</h3>
|
||||||
|
<p>
|
||||||
|
Useful for wallets connecting from outside your home: Trezor Suite, Electrum, Sparrow,
|
||||||
|
BlueWallet, Nunchuk, Blockstream and BitBoxApp. Apps on the Umbrel itself do not need it, they
|
||||||
|
already reach the Electrum server directly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<!-- Voorstel, nog niet besloten. De agent stelt de regels samen uit de
|
<!-- Voorstel, nog niet besloten. De agent stelt de regels samen uit de
|
||||||
stream-access-log van nginx. Bewust zonder client-IP en zonder bronpoort:
|
stream-access-log van nginx. Bewust zonder client-IP en zonder bronpoort:
|
||||||
dat is precies het soort gegeven dat deze app dichtzet, en het is niet
|
dat is precies het soort gegeven dat deze app dichtzet, en het is niet
|
||||||
@@ -1121,6 +1223,20 @@ body {
|
|||||||
el('menu-certs').addEventListener('click', openSheet);
|
el('menu-certs').addEventListener('click', openSheet);
|
||||||
el('alert-nocert-open').addEventListener('click', openSheet);
|
el('alert-nocert-open').addEventListener('click', openSheet);
|
||||||
|
|
||||||
|
// ── De dialoog "About this app" ─────────────────────────────────────────
|
||||||
|
// Dezelfde route als hierboven, met dezelfde terugval voor een browser zonder
|
||||||
|
// showModal.
|
||||||
|
var about = el('about-dialog');
|
||||||
|
|
||||||
|
el('menu-about').addEventListener('click', function () {
|
||||||
|
closeMenu();
|
||||||
|
if (typeof about.showModal === 'function') {
|
||||||
|
about.showModal();
|
||||||
|
} else {
|
||||||
|
about.setAttribute('open', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Hulpjes ─────────────────────────────────────────────────────────────
|
// ── Hulpjes ─────────────────────────────────────────────────────────────
|
||||||
function el(id) { return document.getElementById(id); }
|
function el(id) { return document.getElementById(id); }
|
||||||
|
|
||||||
@@ -1290,12 +1406,26 @@ body {
|
|||||||
// Gebouwd in de agent in 0.0.16, hier getoond sinds de tweede indeling.
|
// Gebouwd in de agent in 0.0.16, hier getoond sinds de tweede indeling.
|
||||||
//
|
//
|
||||||
// Let op het woord dat hier niet staat: "reachable". De agent verbindt van
|
// Let op het woord dat hier niet staat: "reachable". De agent verbindt van
|
||||||
// container naar container, dus dit zegt niets over de poortmapping naar de
|
// container naar container, dus dit zegt niets over de poortmapping naar de host
|
||||||
// host of over de doorstuurregel in de router. "Answering" met daaronder "from
|
// of over de doorstuurregel in de router. Alles wat verder gaat, zou de tegel
|
||||||
// inside the app" is wat er gemeten is; alles wat verder gaat, zou de tegel
|
|
||||||
// laten liegen op precies het punt waar iemand hem gelooft.
|
// laten liegen op precies het punt waar iemand hem gelooft.
|
||||||
|
//
|
||||||
|
// Hier stond "Answering", met daaronder "answered in 7 ms, from inside the app".
|
||||||
|
// De gebruiker meldde op 30-08-2026 dat die tegel onduidelijk is en dat die van
|
||||||
|
// Evolu Relay ("Running") meteen leest, en dat is terecht om twee redenen:
|
||||||
|
//
|
||||||
|
// - "Answering" is een tegenwoordig deelwoord en leest dus als een handeling die
|
||||||
|
// bezig is, niet als een toestand. "Checking" ernaast maakt dat erger, want
|
||||||
|
// dát is er wél een;
|
||||||
|
// - "from inside the app" is de nuance hierboven in vier woorden, en vier
|
||||||
|
// woorden zijn er te weinig voor. Wie het niet al weet, leest er niets uit.
|
||||||
|
//
|
||||||
|
// Wat er nu staat is "Running", met de meting eronder. De nuance is niet
|
||||||
|
// weggelaten maar verplaatst naar de dialoog "About this app", onder een eigen
|
||||||
|
// kopje: daar is ruimte om te zeggen wat het wél en niet bewijst. Haal dat kopje
|
||||||
|
// niet weg zonder hier iets terug te zetten, want dan staat de bewering nergens.
|
||||||
var SELF = {
|
var SELF = {
|
||||||
ok: { label: 'Answering', bad: false },
|
ok: { label: 'Running', bad: false },
|
||||||
failed: { label: 'Not answering', bad: true },
|
failed: { label: 'Not answering', bad: true },
|
||||||
'wrong-certificate': { label: 'Wrong certificate', bad: true },
|
'wrong-certificate': { label: 'Wrong certificate', bad: true },
|
||||||
// Geen certificaat is de normale begintoestand van een verse installatie en
|
// Geen certificaat is de normale begintoestand van een verse installatie en
|
||||||
@@ -1322,7 +1452,10 @@ body {
|
|||||||
node.classList.toggle('bad', kind.bad);
|
node.classList.toggle('bad', kind.bad);
|
||||||
|
|
||||||
if (self.state === 'ok') {
|
if (self.state === 'ok') {
|
||||||
sub.textContent = 'answered in ' + self.ms + ' ms, from inside the app';
|
// Wat er gemeten is, in gewone woorden. Het getal blijft, want dat is het
|
||||||
|
// enige harde gegeven op deze tegel; wat het niet bewijst staat in de
|
||||||
|
// dialoog "About this app".
|
||||||
|
sub.textContent = 'the TLS port answered in ' + self.ms + ' ms';
|
||||||
} else if (self.state === 'off') {
|
} else if (self.state === 'off') {
|
||||||
sub.textContent = 'no certificate selected, so nothing is listening';
|
sub.textContent = 'no certificate selected, so nothing is listening';
|
||||||
} else if (self.state === 'wrong-certificate') {
|
} else if (self.state === 'wrong-certificate') {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ manifestVersion: 1
|
|||||||
id: whatsnext-electrum-gate
|
id: whatsnext-electrum-gate
|
||||||
category: bitcoin
|
category: bitcoin
|
||||||
name: Electrum Gate
|
name: Electrum Gate
|
||||||
version: "0.0.23"
|
version: "0.0.24"
|
||||||
tagline: Your own node from anywhere, without waiting for Tor
|
tagline: Your own node from anywhere, without waiting for Tor
|
||||||
description: >-
|
description: >-
|
||||||
The privacy win is already yours: you run the Electrum server. A public one gets asked for
|
The privacy win is already yours: you run the Electrum server. A public one gets asked for
|
||||||
@@ -56,15 +56,22 @@ description: >-
|
|||||||
# staan. In 0.0.9 stond er daardoor drie keer "Earlier releases" en twee keer
|
# staan. In 0.0.9 stond er daardoor drie keer "Earlier releases" en twee keer
|
||||||
# dezelfde 0.0.4-regel. De toets let er nu op.
|
# dezelfde 0.0.4-regel. De toets let er nu op.
|
||||||
releaseNotes: >-
|
releaseNotes: >-
|
||||||
Fixes a page that could go blank or show stale values once another app from the same store was
|
The menu has a new "About this app" item: what the app is for, what it asks of you, and what the
|
||||||
installed. The page reached its helper by a short name that a second app happened to use as well, so
|
Status card does and does not prove. That last one was the reason for this release. The card used to
|
||||||
roughly half of its requests ended up at the wrong one. It now uses a name that is unique to this app.
|
read "Answering" with "answered in 7 ms, from inside the app" underneath, which was unclear in both
|
||||||
If your page has been behaving oddly, this is why.
|
halves. It now says "Running", with the measurement below it, and the caveat has moved to where there
|
||||||
|
is room to state it properly: the check runs inside the app, so a router that no longer forwards the
|
||||||
|
port still reads as Running here.
|
||||||
|
|
||||||
|
|
||||||
Earlier releases:
|
Earlier releases:
|
||||||
|
|
||||||
|
|
||||||
|
0.0.23 fixed a page that could go blank or show stale values once another app from the same store was
|
||||||
|
installed. The page reached its helper by a short name that a second app happened to use as well, so
|
||||||
|
roughly half of its requests ended up at the wrong one.
|
||||||
|
|
||||||
|
|
||||||
0.0.22 gave panels in light mode an outline as well as a shadow. A soft shadow on white gives depth but
|
0.0.22 gave panels in light mode an outline as well as a shadow. A soft shadow on white gives depth but
|
||||||
no edge, so where a panel ended was a matter of looking closely. Dark mode was unchanged.
|
no edge, so where a panel ended was a matter of looking closely. Dark mode was unchanged.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import os
|
|||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -29,6 +30,18 @@ OWNERS_FILE = STATE_DIR / "owners.json"
|
|||||||
COMMAND_FILE = STATE_DIR / "command.json"
|
COMMAND_FILE = STATE_DIR / "command.json"
|
||||||
DATABASE_FILE = STATE_DIR / "evolu-relay.db"
|
DATABASE_FILE = STATE_DIR / "evolu-relay.db"
|
||||||
|
|
||||||
|
# De labels die de gebruiker aan een eigenaar-id hangt. Een eigen bestand, en dat
|
||||||
|
# is de kern van deze keuze: owners.json is van het relay-proces en labels.json is
|
||||||
|
# van de agent, dus er is per bestand precies één schrijver. Dat is dezelfde
|
||||||
|
# afspraak die de postbus hierboven oplevert, en de reden staat bovenaan dit
|
||||||
|
# bestand.
|
||||||
|
#
|
||||||
|
# Wat het bovendien oplevert: een label is meteen opgeslagen en niet pas als de
|
||||||
|
# relay de postbus leegmaakt, en labelen blijft werken als de relay omgevallen is.
|
||||||
|
# Dat mag, want de relay hoeft dit niet te weten: een label zegt niets over wie er
|
||||||
|
# binnen mag.
|
||||||
|
LABELS_FILE = STATE_DIR / "labels.json"
|
||||||
|
|
||||||
RELAY_HOST = os.environ.get("RELAY_HOST", "")
|
RELAY_HOST = os.environ.get("RELAY_HOST", "")
|
||||||
RELAY_PORT = int(os.environ.get("RELAY_PORT", "4000"))
|
RELAY_PORT = int(os.environ.get("RELAY_PORT", "4000"))
|
||||||
PUBLIC_PORT = int(os.environ.get("RELAY_PUBLIC_PORT", "3852"))
|
PUBLIC_PORT = int(os.environ.get("RELAY_PUBLIC_PORT", "3852"))
|
||||||
@@ -43,6 +56,26 @@ ALLOWED_ACTIONS = ("set-learning", "block", "allow", "forget")
|
|||||||
MAX_BODY_BYTES = 4096
|
MAX_BODY_BYTES = 4096
|
||||||
MAX_OWNER_ID_LENGTH = 256
|
MAX_OWNER_ID_LENGTH = 256
|
||||||
|
|
||||||
|
# Een label is een herkenpunt en geen aantekenveld: het staat op de pagina naast
|
||||||
|
# een id en moet daar op één regel passen.
|
||||||
|
MAX_LABEL_LENGTH = 48
|
||||||
|
|
||||||
|
# Een bovengrens op het aantal labels. De pagina zit achter de inlog van umbrelOS,
|
||||||
|
# dus dit is geen verdediging tegen een aanvaller maar tegen een lus die per
|
||||||
|
# ongeluk blijft schrijven. Ruim boven het aantal eigenaars dat iemand ooit heeft.
|
||||||
|
MAX_LABELS = 200
|
||||||
|
|
||||||
|
# Hoelang de pagina de deur voor nieuwe eigenaars openzet. Dezelfde waarde staat
|
||||||
|
# in de pagina; die stuurt hem mee en het relay-proces begrenst hem nog een keer.
|
||||||
|
# Hier staat hij omdat de agent hem moet toestaan, niet omdat hij hem kiest.
|
||||||
|
MAX_LEARNING_SECONDS = 3600
|
||||||
|
|
||||||
|
# Eén schrijver per bestand is de afspraak, maar de agent zelf is meerdradig:
|
||||||
|
# ThreadingHTTPServer geeft elk verzoek zijn eigen draad. Twee labels die op
|
||||||
|
# hetzelfde moment binnenkomen zouden elkaar dus kunnen overschrijven, want
|
||||||
|
# labelen is lezen-wijzigen-schrijven. Dit slot maakt daar één handeling van.
|
||||||
|
labels_lock = threading.Lock()
|
||||||
|
|
||||||
# Door de achtergrondlus bijgewerkt, door de webserver gelezen. Een dict wordt in
|
# Door de achtergrondlus bijgewerkt, door de webserver gelezen. Een dict wordt in
|
||||||
# zijn geheel vervangen en nooit ter plekke aangepast, zodat een lezer altijd een
|
# zijn geheel vervangen en nooit ter plekke aangepast, zodat een lezer altijd een
|
||||||
# samenhangend beeld heeft zonder slot.
|
# samenhangend beeld heeft zonder slot.
|
||||||
@@ -95,6 +128,102 @@ def read_owners():
|
|||||||
return {"state": data, "problem": None}
|
return {"state": data, "problem": None}
|
||||||
|
|
||||||
|
|
||||||
|
def read_labels():
|
||||||
|
"""De labels, of een lege verzameling.
|
||||||
|
|
||||||
|
Bewust vergevingsgezind, en dat is het omgekeerde van hoe owners.json gelezen
|
||||||
|
wordt. Daar hangt aan een half begrepen bestand de vraag wie er binnen mag, en
|
||||||
|
dan is weigeren het antwoord. Hier gaat het om een naam naast een id: is het
|
||||||
|
onleesbaar, dan is het ergste gevolg dat je de rauwe ids ziet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with LABELS_FILE.open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
schoon = {}
|
||||||
|
for owner_id, label in data.items():
|
||||||
|
if not isinstance(owner_id, str) or not isinstance(label, str):
|
||||||
|
continue
|
||||||
|
if not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH:
|
||||||
|
continue
|
||||||
|
label = label.strip()
|
||||||
|
if label:
|
||||||
|
schoon[owner_id] = label[:MAX_LABEL_LENGTH]
|
||||||
|
return schoon
|
||||||
|
|
||||||
|
|
||||||
|
def write_labels(labels):
|
||||||
|
"""Schrijft de labels. Eerst een tijdelijk bestand en dan hernoemen.
|
||||||
|
|
||||||
|
Hernoemen binnen dezelfde map is atomair, dus een onderbroken schrijfactie
|
||||||
|
laat geen half bestand achter. Dezelfde constructie als de postbus.
|
||||||
|
"""
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = LABELS_FILE.with_suffix(".json.tmp")
|
||||||
|
with temporary.open("w", encoding="utf-8") as handle:
|
||||||
|
json.dump(labels, handle, indent=2, sort_keys=True)
|
||||||
|
handle.write("\n")
|
||||||
|
temporary.replace(LABELS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_moment(value):
|
||||||
|
"""Een ISO-tijdstip uit owners.json als datetime, of None.
|
||||||
|
|
||||||
|
Het relay-proces schrijft `new Date().toISOString()`, dus met milliseconden en
|
||||||
|
met een Z erachter. `fromisoformat` neemt die Z sinds Python 3.11; de image is
|
||||||
|
python:3-alpine en dus nieuwer. Faalt het alsnog, dan is None het antwoord en
|
||||||
|
beslist de aanroeper.
|
||||||
|
"""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
when = datetime.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if when.tzinfo is None:
|
||||||
|
return when.replace(tzinfo=timezone.utc)
|
||||||
|
return when
|
||||||
|
|
||||||
|
|
||||||
|
def learning_facts(state):
|
||||||
|
"""De leerstand zoals de pagina hem hoort te zien.
|
||||||
|
|
||||||
|
Het veld in het bestand is niet het hele antwoord: staat er een tijdstip in dat
|
||||||
|
verstreken is, dan is de deur dicht, ook al staat `learning` nog op true. Het
|
||||||
|
relay-proces ruimt dat op in zijn eigen lus, en tussen het aflopen en die ronde
|
||||||
|
zit een seconde of twee. De pagina hoort daar niet "open" te tonen.
|
||||||
|
|
||||||
|
De resterende tijd wordt hier uitgerekend en niet in de browser. Dat is met
|
||||||
|
opzet: dan telt de klok van de Umbrel en niet die van de bezoeker, en die twee
|
||||||
|
lopen niet per definitie gelijk.
|
||||||
|
"""
|
||||||
|
learning = state.get("learning")
|
||||||
|
until = state.get("learningUntil")
|
||||||
|
|
||||||
|
if learning is not True:
|
||||||
|
return {"learning": learning, "learningUntil": None, "learningSecondsLeft": None}
|
||||||
|
|
||||||
|
when = parse_moment(until)
|
||||||
|
if when is None:
|
||||||
|
# Geen tijdslot: open tot de gebruiker hem zelf sluit. Dat is de
|
||||||
|
# begintoestand van een verse installatie.
|
||||||
|
return {"learning": True, "learningUntil": None, "learningSecondsLeft": None}
|
||||||
|
|
||||||
|
resterend = (when - datetime.now(timezone.utc)).total_seconds()
|
||||||
|
if resterend <= 0:
|
||||||
|
return {"learning": False, "learningUntil": None, "learningSecondsLeft": 0}
|
||||||
|
return {
|
||||||
|
"learning": True,
|
||||||
|
"learningUntil": until,
|
||||||
|
"learningSecondsLeft": int(resterend),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def database_facts():
|
def database_facts():
|
||||||
try:
|
try:
|
||||||
stat = DATABASE_FILE.stat()
|
stat = DATABASE_FILE.stat()
|
||||||
@@ -106,9 +235,30 @@ def database_facts():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def met_label(entries, labels):
|
||||||
|
"""Hangt het label van de gebruiker aan elke regel.
|
||||||
|
|
||||||
|
Een kopie en niet ter plekke: wat hier binnenkomt komt uit het bestand van het
|
||||||
|
relay-proces, en daar horen wij niets aan toe te voegen.
|
||||||
|
"""
|
||||||
|
resultaat = []
|
||||||
|
for entry in entries:
|
||||||
|
regel = dict(entry)
|
||||||
|
regel["label"] = labels.get(entry.get("id"))
|
||||||
|
resultaat.append(regel)
|
||||||
|
return resultaat
|
||||||
|
|
||||||
|
|
||||||
def build_status():
|
def build_status():
|
||||||
owners = read_owners()
|
owners = read_owners()
|
||||||
state = owners["state"] or {}
|
state = owners["state"] or {}
|
||||||
|
labels = read_labels()
|
||||||
|
|
||||||
|
# Ontbreekt de staat, dan is 'learning' onbekend en niet 'false'. De pagina
|
||||||
|
# hoort dat verschil te tonen: onbekend is een reden om te kijken, uit is een
|
||||||
|
# keuze. learning_facts() geeft None door zoals het binnenkwam.
|
||||||
|
learning = learning_facts(state)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"relay": {
|
"relay": {
|
||||||
"reachable": probe["reachable"],
|
"reachable": probe["reachable"],
|
||||||
@@ -117,23 +267,29 @@ def build_status():
|
|||||||
},
|
},
|
||||||
"owners": {
|
"owners": {
|
||||||
"problem": owners["problem"],
|
"problem": owners["problem"],
|
||||||
# Ontbreekt de staat, dan is 'learning' onbekend en niet 'false'. De
|
"learning": learning["learning"],
|
||||||
# pagina hoort dat verschil te tonen: onbekend is een reden om te
|
"learningUntil": learning["learningUntil"],
|
||||||
# kijken, uit is een keuze.
|
"learningSecondsLeft": learning["learningSecondsLeft"],
|
||||||
"learning": state.get("learning"),
|
"allowed": met_label(
|
||||||
"allowed": [
|
[
|
||||||
entry
|
entry
|
||||||
for entry in state.get("owners", [])
|
for entry in state.get("owners", [])
|
||||||
if isinstance(entry, dict) and entry.get("allowed") is True
|
if isinstance(entry, dict) and entry.get("allowed") is True
|
||||||
],
|
],
|
||||||
"blocked": [
|
labels,
|
||||||
|
),
|
||||||
|
"blocked": met_label(
|
||||||
|
[
|
||||||
entry
|
entry
|
||||||
for entry in state.get("owners", [])
|
for entry in state.get("owners", [])
|
||||||
if isinstance(entry, dict) and entry.get("allowed") is False
|
if isinstance(entry, dict) and entry.get("allowed") is False
|
||||||
],
|
],
|
||||||
"rejected": [
|
labels,
|
||||||
entry for entry in state.get("rejected", []) if isinstance(entry, dict)
|
),
|
||||||
],
|
"rejected": met_label(
|
||||||
|
[entry for entry in state.get("rejected", []) if isinstance(entry, dict)],
|
||||||
|
labels,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"database": database_facts(),
|
"database": database_facts(),
|
||||||
# Ligt er nog een opdracht, dan heeft het relay-proces hem nog niet
|
# Ligt er nog een opdracht, dan heeft het relay-proces hem nog niet
|
||||||
@@ -160,14 +316,85 @@ def valid_command(payload):
|
|||||||
value = payload.get("value")
|
value = payload.get("value")
|
||||||
if not isinstance(value, bool):
|
if not isinstance(value, bool):
|
||||||
return None, "waarde moet true of false zijn"
|
return None, "waarde moet true of false zijn"
|
||||||
|
|
||||||
|
seconds = payload.get("seconds")
|
||||||
|
if seconds is None:
|
||||||
|
# Zonder tijdslot: open tot de gebruiker hem zelf sluit. Dat pad blijft
|
||||||
|
# bestaan voor een verse installatie, waar een venster van twee minuten
|
||||||
|
# zou aflopen terwijl je nog aan het koppelen bent.
|
||||||
return {"action": action, "value": value}, None
|
return {"action": action, "value": value}, None
|
||||||
|
|
||||||
|
# isinstance(True, int) is in Python waar, dus een boolean zou hier als
|
||||||
|
# aantal seconden doorglippen. Vandaar de uitsluiting.
|
||||||
|
if isinstance(seconds, bool) or not isinstance(seconds, int):
|
||||||
|
return None, "seconds moet een heel getal zijn"
|
||||||
|
if seconds <= 0 or seconds > MAX_LEARNING_SECONDS:
|
||||||
|
return None, "seconds valt buiten het toegestane bereik"
|
||||||
|
if value is not True:
|
||||||
|
return None, "seconds hoort alleen bij openzetten"
|
||||||
|
return {"action": action, "value": value, "seconds": seconds}, None
|
||||||
|
|
||||||
owner_id = payload.get("ownerId")
|
owner_id = payload.get("ownerId")
|
||||||
if not isinstance(owner_id, str) or not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH:
|
if not isinstance(owner_id, str) or not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH:
|
||||||
return None, "ontbrekende of te lange ownerId"
|
return None, "ontbrekende of te lange ownerId"
|
||||||
return {"action": action, "ownerId": owner_id}, None
|
return {"action": action, "ownerId": owner_id}, None
|
||||||
|
|
||||||
|
|
||||||
|
def valid_label(payload):
|
||||||
|
"""Geeft (ownerId, label) terug, of een foutmelding.
|
||||||
|
|
||||||
|
Een leeg label is geen fout maar de manier om er een weg te halen: dan hoeft er
|
||||||
|
geen tweede opdracht te bestaan voor iets dat de gebruiker als hetzelfde veld
|
||||||
|
ziet.
|
||||||
|
"""
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None, None, "geen object"
|
||||||
|
|
||||||
|
owner_id = payload.get("ownerId")
|
||||||
|
if not isinstance(owner_id, str) or not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH:
|
||||||
|
return None, None, "ontbrekende of te lange ownerId"
|
||||||
|
|
||||||
|
label = payload.get("label")
|
||||||
|
if label is None:
|
||||||
|
label = ""
|
||||||
|
if not isinstance(label, str):
|
||||||
|
return None, None, "label moet tekst zijn"
|
||||||
|
|
||||||
|
# Regeleindes eruit: dit is één regel naast een id, en een label met een
|
||||||
|
# nieuwe regel erin zou de lijst uit elkaar trekken.
|
||||||
|
label = " ".join(label.split()).strip()
|
||||||
|
if len(label) > MAX_LABEL_LENGTH:
|
||||||
|
return None, None, "label is te lang"
|
||||||
|
|
||||||
|
return owner_id, label, None
|
||||||
|
|
||||||
|
|
||||||
|
def apply_label(owner_id, label):
|
||||||
|
"""Zet of haalt een label weg. Geeft een foutmelding terug, of None.
|
||||||
|
|
||||||
|
Onder het slot, want dit is lezen-wijzigen-schrijven en de agent bedient
|
||||||
|
meerdere verzoeken tegelijk.
|
||||||
|
"""
|
||||||
|
with labels_lock:
|
||||||
|
labels = read_labels()
|
||||||
|
if label:
|
||||||
|
if owner_id not in labels and len(labels) >= MAX_LABELS:
|
||||||
|
return "er zijn al te veel labels"
|
||||||
|
labels[owner_id] = label
|
||||||
|
else:
|
||||||
|
if owner_id not in labels:
|
||||||
|
# Niets te doen, en dat is geen fout: de pagina stuurt een leeg
|
||||||
|
# label als je het veld leegmaakt, ook als er nog niets stond.
|
||||||
|
return None
|
||||||
|
del labels[owner_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
write_labels(labels)
|
||||||
|
except OSError as error:
|
||||||
|
return "label kon niet worden weggeschreven: " + str(error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def write_command(command):
|
def write_command(command):
|
||||||
"""Legt de opdracht in de postbus.
|
"""Legt de opdracht in de postbus.
|
||||||
|
|
||||||
@@ -209,25 +436,57 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
self._send(404, {"error": "onbekend pad"})
|
self._send(404, {"error": "onbekend pad"})
|
||||||
|
|
||||||
def do_POST(self):
|
def _read_payload(self):
|
||||||
if self.path.rstrip("/") not in ("/api/command", "/command"):
|
"""Het verzoek als object, of None als er al een fout verstuurd is."""
|
||||||
self._send(404, {"error": "onbekend pad"})
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
length = int(self.headers.get("Content-Length", "0"))
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._send(400, {"error": "lengte ontbreekt"})
|
self._send(400, {"error": "lengte ontbreekt"})
|
||||||
return
|
return None
|
||||||
|
|
||||||
if length <= 0 or length > MAX_BODY_BYTES:
|
if length <= 0 or length > MAX_BODY_BYTES:
|
||||||
self._send(400, {"error": "lege of te grote opdracht"})
|
self._send(400, {"error": "lege of te grote opdracht"})
|
||||||
return
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
return json.loads(self.rfile.read(length).decode("utf-8"))
|
||||||
except (UnicodeDecodeError, ValueError):
|
except (UnicodeDecodeError, ValueError):
|
||||||
self._send(400, {"error": "onleesbare opdracht"})
|
self._send(400, {"error": "onleesbare opdracht"})
|
||||||
|
return None
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
pad = self.path.rstrip("/")
|
||||||
|
|
||||||
|
# Een label gaat NIET via de postbus, en dat is de enige uitzondering op
|
||||||
|
# die regel. De reden dat opdrachten er wel door gaan, is dat het
|
||||||
|
# relay-proces beslist wie er binnen mag en dat twee schrijvers in die
|
||||||
|
# allowlist een wedloop zou zijn. Een label zegt niets over toegang, staat
|
||||||
|
# in een eigen bestand met de agent als enige schrijver, en is meteen
|
||||||
|
# opgeslagen in plaats van na de volgende ronde van de relay.
|
||||||
|
if pad in ("/api/label", "/label"):
|
||||||
|
payload = self._read_payload()
|
||||||
|
if payload is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
owner_id, label, problem = valid_label(payload)
|
||||||
|
if problem is not None:
|
||||||
|
self._send(400, {"error": problem})
|
||||||
|
return
|
||||||
|
|
||||||
|
problem = apply_label(owner_id, label)
|
||||||
|
if problem is not None:
|
||||||
|
self._send(500, {"error": problem})
|
||||||
|
return
|
||||||
|
|
||||||
|
self._send(200, {"ownerId": owner_id, "label": label})
|
||||||
|
return
|
||||||
|
|
||||||
|
if pad not in ("/api/command", "/command"):
|
||||||
|
self._send(404, {"error": "onbekend pad"})
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = self._read_payload()
|
||||||
|
if payload is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
command, problem = valid_command(payload)
|
command, problem = valid_command(payload)
|
||||||
|
|||||||
@@ -49,11 +49,22 @@ services:
|
|||||||
# Voor de officiele store hoort er een multi-arch index-digest met arm64 in;
|
# Voor de officiele store hoort er een multi-arch index-digest met arm64 in;
|
||||||
# zie het masterplan Publicatie-Relay.
|
# zie het masterplan Publicatie-Relay.
|
||||||
#
|
#
|
||||||
# De digest hoort erbij en niet alleen de tag: een tag kan opnieuw geduwd
|
# ── LET OP: hier staat GEEN digest, en dat is tijdelijk ───────────────────
|
||||||
# worden en dan draait er iets anders dan hier staat. Staan ze allebei, dan
|
# 0.5.0 bestaat nog niet in het register: er zit een wijziging in de relay
|
||||||
# bepaalt de digest wat er gehaald wordt en is de tag alleen leesbaarheid.
|
# (het tijdvenster voor nieuwe eigenaars) en die moet eerst gebouwd en geduwd
|
||||||
# Geduwd op 28-08-2026.
|
# worden met `sh tools/evolu-relay/build.sh`.
|
||||||
image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.3.0@sha256:d2d8fcbe0e7cf41ca90acecde4a79f0c91181f61bd4692dd2c04027ee1c8450a
|
#
|
||||||
|
# De digest van 0.3.0 stond hier en is weggehaald in plaats van blijven staan.
|
||||||
|
# Dat is met opzet de minst erge van twee kwaden: staat er een tag én een
|
||||||
|
# digest, dan bepaalt de DIGEST wat er gehaald wordt. Een oude digest onder
|
||||||
|
# een nieuwe tag levert dus stilzwijgend de oude relay, en dan werkt de timer
|
||||||
|
# niet zonder dat iets dat meldt. Ongepind faalt hard en zichtbaar zolang de
|
||||||
|
# image er niet is, en dat is hier het gedrag dat je wil.
|
||||||
|
#
|
||||||
|
# Zet de digest uit de push-uitvoer er weer achter zodra 0.5.0 geduwd is;
|
||||||
|
# tests/test_appstore_vorm.py drukt de pinstatus af, dus de suite blijft het
|
||||||
|
# zeggen tot het gedaan is.
|
||||||
|
image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.5.0
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
ports:
|
||||||
# De relay zelf. Dit is de poort waar Zoraxy met TLS naartoe wijst, en de
|
# De relay zelf. Dit is de poort waar Zoraxy met TLS naartoe wijst, en de
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ manifestVersion: 1
|
|||||||
id: whatsnext-evolu-relay
|
id: whatsnext-evolu-relay
|
||||||
category: files
|
category: files
|
||||||
name: Evolu Relay
|
name: Evolu Relay
|
||||||
version: "0.4.0"
|
version: "0.5.0"
|
||||||
tagline: Encrypted sync and backup for your local-first apps
|
tagline: Encrypted sync and backup for your local-first apps
|
||||||
description: >-
|
description: >-
|
||||||
Local-first apps keep your data on your own device and work whether or not you have a
|
Local-first apps keep your data on your own device and work whether or not you have a
|
||||||
@@ -43,19 +43,39 @@ description: >-
|
|||||||
your app at the address shown there. Away from home you will need a way in, such as Tailscale
|
your app at the address shown there. Away from home you will need a way in, such as Tailscale
|
||||||
or a reverse proxy with your own domain.
|
or a reverse proxy with your own domain.
|
||||||
releaseNotes: >-
|
releaseNotes: >-
|
||||||
Fixes a status page that alternated between working and showing an error. It reached its helper by a
|
Accepting a new owner is now a two minute window instead of a switch you have to remember to turn
|
||||||
short name that another app in this store uses as well, so about half of its requests ended up at the
|
back off. Open it, pair your device, and it closes on its own; you can close it early or restart the
|
||||||
wrong app. It now uses a name that is unique to this one.
|
two minutes if you need longer. The countdown runs on the Umbrel, so closing the page does not leave
|
||||||
|
the door open.
|
||||||
|
|
||||||
|
|
||||||
This release also drops the wording that tied the app to one particular client. It is a general
|
Owner ids can be given a name. They are long strings of random characters that say nothing about
|
||||||
purpose Evolu relay: any app built on Evolu can use it. And the explanation under the new owners
|
which of your devices or wallets they are, so hover a row and label it. Names are kept on your Umbrel
|
||||||
switch now matches the switch: it used to describe the open state even when it was closed.
|
and are never sent anywhere.
|
||||||
|
|
||||||
|
|
||||||
|
Blocked owners and refused attempts were two lists and are now one, because what you do with them is
|
||||||
|
the same: allow, or forget. Each row carries a badge saying which it was.
|
||||||
|
|
||||||
|
|
||||||
|
The page itself has been rebuilt to match Electrum Gate in this store: the same width, the same
|
||||||
|
layout, the same header, and the version number where you can see it. Buttons appear when you hover a
|
||||||
|
row, so a page you are only reading stays quiet. Explanatory paragraphs have moved into a new
|
||||||
|
"About this app" item in the menu, which also spells out what the relay can and cannot see.
|
||||||
|
|
||||||
|
|
||||||
Earlier releases:
|
Earlier releases:
|
||||||
|
|
||||||
|
|
||||||
|
0.4.0 fixed a status page that alternated between working and showing an error. It reached its helper
|
||||||
|
by a short name that another app in this store uses as well, so about half of its requests ended up
|
||||||
|
at the wrong app.
|
||||||
|
|
||||||
|
|
||||||
|
0.4.0 also dropped the wording that tied the app to one particular client. It is a general purpose
|
||||||
|
Evolu relay: any app built on Evolu can use it.
|
||||||
|
|
||||||
|
|
||||||
0.2.0 replaced the relay this app shipped with. Until 0.0.2 it packaged a vendor's own deployment,
|
0.2.0 replaced the relay this app shipped with. Until 0.0.2 it packaged a vendor's own deployment,
|
||||||
which came with a quota manager and a PostgreSQL database and could not work outside that vendor's
|
which came with a quota manager and a PostgreSQL database and could not work outside that vendor's
|
||||||
service: clients skip the quota manager as soon as you point them at a relay of your own, while
|
service: clients skip the quota manager as soon as you point them at a relay of your own, while
|
||||||
|
|||||||
Reference in New Issue
Block a user