Gate 0.1.0: eigen image, het command-blok is een script, de app-map is leeg

Plan Eigenimage, fase 1 tot en met 3. De vier templates verhuizen naar
tools/electrum-gate/ zonder extensie; daarnaast Dockerfile (nginx:1.30-alpine
plus python3), entrypoint.sh (het command-blok van de compose, zonder $$) en
build.sh naar het voorbeeld van Evolu Relay. Een image voor beide containers,
gebouwd op de Umbrel; open punt 2 en 3 daarmee beslist.

Inhoudelijk anders dan alleen verplaatst: het log_format staat in stream.conf
zelf, het backend-adres komt via twee plaatshouders zonder dollarteken uit de
omgeving (ook in de server-service), en de pagina haalt versie en adres uit
status.json via GATE_APP_VERSION.

Tests mee verhuisd en uitgebreid: entrypoint.sh en Dockerfile in plaats van het
command-blok, en de tag in de compose gelijk aan VERSION in build.sh voor elke
eigen image. Mutatie-getest met drie ingrepen.

Nog niet gebouwd: er is hier geen Docker. De tag staat ongepind tot de eerste
push; dat is fase 4 en die is van de gebruiker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Harmen
2026-09-07 20:20:42 +02:00
co-authored by Claude Fable 5.1
parent 7b3804df89
commit 973b24a23f
25 changed files with 981 additions and 410 deletions
+7
View File
@@ -0,0 +1,7 @@
# De bouwcontext van tools/electrum-gate/build.sh is de repo-root, omdat de
# Dockerfile icon.png uit de app-map kopieert. Alles wat de Dockerfile niet
# noemt blijft hier buiten, zodat de context klein is en een wijziging in de
# documentatie de bouwcache niet ongeldig maakt.
*
!tools/electrum-gate
!whatsnext-electrum-gate/icon.png
+2 -2
View File
@@ -59,8 +59,8 @@ build/
*.zip *.zip
# ── Python ──────────────────────────────────────────────────────────────────── # ── Python ────────────────────────────────────────────────────────────────────
# De test importeert agent.py.template als module, en Python legt daar dan # De test importeert tools/electrum-gate/agent.py als module, en Python legt daar
# bytecode naast. De test zet dat zelf uit, maar dit is de vangnetregel: zo'n # dan bytecode naast. De test zet dat zelf uit, maar dit is de vangnetregel: zo'n
# .pyc glipte een keer mee in een commit. # .pyc glipte een keer mee in een commit.
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+42 -21
View File
@@ -9,7 +9,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. Sinds 0.1.0 met een eigen image en dus een bouwstap: zie **Electrum Gate** hieronder |
| **Evolu Relay** | `whatsnext-evolu-relay/` | draait op de Umbrel sinds 28-08-2026 en wordt gebruikt. Let op de bouwstap: zie **Evolu Relay** hieronder | | **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
@@ -25,12 +25,14 @@ Deze gelden voor élke app hier, want het zijn eigenschappen van umbrelOS. Met b
`${...}` in zo'n bestand door `envsubst`, ook een variabele die niet bestaat, en die wordt dan leeg. Dat `${...}` in zo'n bestand door `envsubst`, ook een variabele die niet bestaat, en die wordt dan leeg. Dat
sloopt Python-code, en in een nginx-config betekent het geen `$host`, geen `log_format` en geen sloopt Python-code, en in een nginx-config betekent het geen `$host`, geen `log_format` en geen
`access_log` met variabelen. Moet er tóch een dollarteken in een nginx-directive, dan hoort die directive `access_log` met variabelen. Moet er tóch een dollarteken in een nginx-directive, dan hoort die directive
in een bestand dat het `command`-blok van de compose wegschrijft, want daar is `$$` te ontsnappen. in een bestand dat het `command`-blok van de compose wegschrijft (daar is `$$` te ontsnappen), of in een
eigen image, waar geen envsubst aan te pas komt. Electrum Gate doet sinds 0.1.0 het tweede; Evolu Relay
heeft zijn agent en pagina nog als `*.template`.
**Wat bij een update meekomt is een whitelist**: `docker-compose.yml`, `*.template`, `exports.sh`, `torrc`, **Wat bij een update meekomt is een whitelist**: `docker-compose.yml`, `*.template`, `exports.sh`, `torrc`,
`hooks` en `umbrel-app.yml`. Al het andere bereikt een bestaande installatie nooit, zonder foutmelding. `hooks` en `umbrel-app.yml`. Al het andere bereikt een bestaande installatie nooit, zonder foutmelding.
Zet logica die later nog moet kunnen wijzigen dus in de compose of in een `*.template`, en nooit in een Zet logica die later nog moet kunnen wijzigen dus in de compose, in een `*.template` of in een eigen
los bestand of in een submap. image, en nooit in een los bestand of in een submap van de app-map.
**Een wijziging zonder verhoging van `version` in `umbrel-app.yml` wordt niet uitgerold.** Geen melding, **Een wijziging zonder verhoging van `version` in `umbrel-app.yml` wordt niet uitgerold.** Geen melding,
geen fout; umbrelOS ziet hetzelfde nummer en doet niets. Dat heeft hier een keer een dag gekost. De geen fout; umbrelOS ziet hetzelfde nummer en doet niets. Dat heeft hier een keer een dag gekost. De
@@ -57,7 +59,8 @@ python tests/test_agent_certificates.py
python tests/test_server_start_zonder_certificaat.py python tests/test_server_start_zonder_certificaat.py
``` ```
Deze twee gaan over Electrum Gate en noemen die app-map bij naam. Er is een derde die over de **store** Deze twee gaan over Electrum Gate en noemen `tools/electrum-gate/` en die app-map bij naam. Er is een
derde die over de **store**
gaat en zijn apps zelf vindt, dus over beide: gaat en zijn apps zelf vindt, dus over beide:
``` ```
@@ -75,18 +78,22 @@ Er is ook een test over de **pagina's** van beide apps, en die is geen Python:
node tests/test_paginas_parsen.mjs 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 Hij toetst dat de JavaScript in elke statuspagina parseert. Een pagina staat op een van twee plekken: als
invulling door umbreld. Dat is de enige klasse paginafouten die niet op het apparaat gevonden hoeft te `index.html.template` in de app-map (Evolu Relay; dan toetst hij ook dat het script heel blijft ná de
worden; alles wat de pagina *toont* blijft handwerk in een browser. invulling door umbreld) of als `index.html` in `tools/<app>/` (Electrum Gate sinds 0.1.0; dan toetst hij
dat er geen accolade-variabele meer in staat). 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 **Wil je een pagina bekijken, gebruik `tools/voorbeeldpagina.mjs`.** Een statuspagina is niet zomaar te
staan accolade-variabelen in en de gegevens komen van een agent die alleen in de app bestaat. Dat script openen: de gegevens komen van een agent die alleen in de app bestaat, en in een `*.template` staan
vult beide in en zet het resultaat in `voorbeeld/` (gitignored). Geen bewijs, wel het gereedschap dat op bovendien accolade-variabelen. Dat script vindt de pagina op een van de twee plekken, vult in wat er in te
vullen is 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. 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 volledig, en een suite die altijd rood staat wordt niet gelezen. Hij
drukt de pinstatus wel af. drukt de pinstatus wel af. Wat hij wél toetst: dat de tag van een eigen image gelijk is aan `VERSION` in
het bijbehorende `tools/<app>/build.sh`.
Twee dingen om te weten voordat je een groene uitslag vertrouwt: Twee dingen om te weten voordat je een groene uitslag vertrouwt:
@@ -94,20 +101,34 @@ Twee dingen om te weten voordat je een groene uitslag vertrouwt:
certificaatwinkel van het besturingssysteem nodig, en de subjectAltName-toets heeft netwerk nodig. Staat certificaatwinkel van het besturingssysteem nodig, en de subjectAltName-toets heeft netwerk nodig. Staat
er `OVERGESLAGEN` in de uitvoer, dan is dat deel níet bewezen. Lees de uitvoer dus, tel niet alleen de er `OVERGESLAGEN` in de uitvoer, dan is dat deel níet bewezen. Lees de uitvoer dus, tel niet alleen de
exitcode; exitcode;
- **de suite dekt de agent, niet de pagina.** Alles in `index.html.template` is handwerk in een browser. - **de suite dekt de agent, niet de pagina.** Alles in `index.html` is handwerk in een browser.
### Architectuurregels ### Architectuurregels
**De app-code zit in een eigen image, gebouwd uit `tools/electrum-gate/`** (sinds 0.1.0, 07-09-2026). De
agent, de nginx-configuratie, het stream-blok, de pagina en het startscript staan daar; in de app-map
staan alleen nog de compose, het manifest, het icoon en `data/`. Eén image voor beide containers: de
compose start hem als `server` met het standaardcommando en als `agent` met `python3 /app/agent.py`.
Gevolg dat je moet kennen voordat je iets belooft: **een wijziging in `tools/electrum-gate/` is pas
uitgerold als de image gebouwd, geduwd en met zijn digest in de compose gezet is**, en dat kan alleen de
gebruiker, op de Umbrel. Verhoog je `VERSION` in `build.sh`, dan verhoog je ook `version` in het manifest
en de tag in de compose (twee keer); `test_appstore_vorm.py` houdt tag en `VERSION` gelijk.
**Geen Docker-socket.** De agent kan nginx daarom niet zelf herladen; hij zet een vlagbestand neer en de **Geen Docker-socket.** De agent kan nginx daarom niet zelf herladen; hij zet een vlagbestand neer en de
nginx-container herlaadt zichzelf. Als je denkt de socket nodig te hebben, is er bijna zeker een nginx-container herlaadt zichzelf. Als je denkt de socket nodig te hebben, is er bijna zeker een
vlagbestand-oplossing. vlagbestand-oplossing.
**De pagina start altijd, ook zonder certificaat.** Het TLS-blok staat daarom niet in **De pagina start altijd, ook zonder certificaat.** Het TLS-blok staat daarom niet in `nginx.conf` maar
`nginx.conf.template` maar in `stream.conf.template`, en het `command`-blok van de compose zet dat pas in in `stream.conf`, en `entrypoint.sh` zet dat pas in `/var/lib/gate/tls/` als de agent een `cert.conf`
`/var/lib/gate/tls/` als de agent een `cert.conf` geschreven heeft; `nginx.conf` haalt die map met een geschreven heeft; `nginx.conf` haalt die map met een jokerteken op. Zet het blok niet terug in
jokerteken op. Zet het blok niet terug in `nginx.conf`, hoe netjes dat ook staat: nginx weigert te starten `nginx.conf`, hoe netjes dat ook staat: nginx weigert te starten als een `listen ssl` geen certificaat
als een `listen ssl` geen certificaat heeft, en dan komt de pagina waarop je dat certificaat kiest ook niet heeft, en dan komt de pagina waarop je dat certificaat kiest ook niet omhoog. Dat was de fout in 0.0.3.
omhoog. Dat was de fout in 0.0.3. `tests/test_server_start_zonder_certificaat.py` houdt het dicht. `tests/test_server_start_zonder_certificaat.py` houdt het dicht.
**Het backend-adres komt uit de omgeving, in beide containers.** nginx kan in een `proxy_pass` van het
stream-blok geen omgevingsvariabele lezen, dus `stream.conf` heeft twee plaatshouders zonder dollarteken
die `entrypoint.sh` bij het starten invult. De pagina haalt de versie en het adres uit `status.json`; tot
0.0.29 vulde umbreld die rechtstreeks in de pagina in.
**Bij twijfel over een certificaat weigert de app.** Meerdere kandidaten zonder keuze van de gebruiker **Bij twijfel over een certificaat weigert de app.** Meerdere kandidaten zonder keuze van de gebruiker
levert géén automatische keuze op, ook niet "de nieuwste". Een verkeerd certificaat geeft een verbinding levert géén automatische keuze op, ook niet "de nieuwste". Een verkeerd certificaat geeft een verbinding
@@ -116,7 +137,7 @@ dan een app die weigert en zegt waarom.
### Feiten ### Feiten
- App-id en mapnaam `whatsnext-electrum-gate`. - App-id en mapnaam `whatsnext-electrum-gate`; image `sc.kamenier-hamer.nl/sysop/electrum-gate`.
- De TLS-poort is **50022**, niet de conventionele 50002: die bezet Fulcrum op de host. - De TLS-poort is **50022**, niet de conventionele 50002: die bezet Fulcrum op de host.
- De app leest de certificaatmap van Zoraxy alleen-lezen. Dat is het meest ongebruikelijke aan dit - De app leest de certificaatmap van Zoraxy alleen-lezen. Dat is het meest ongebruikelijke aan dit
pakket; zie het masterplan **Publicatie-Gate** §4. pakket; zie het masterplan **Publicatie-Gate** §4.
+35
View File
@@ -8,6 +8,41 @@ 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.1.0] - 2026-09-07
**Nog niet gebouwd.** De compose verwijst naar `sc.kamenier-hamer.nl/sysop/electrum-gate:0.1.0` zonder
digest; die komt erbij zodra de gebruiker de image op de Umbrel gebouwd en geduwd heeft. Tot die tijd is
dit een versie in de repo en niet op het apparaat.
### Changed
- **De app heeft een eigen image**, gebouwd uit `tools/electrum-gate/` met `build.sh`, op basis van
`nginx:1.30-alpine` plus `python3`. Eén image voor beide containers; de compose start hem als `server`
met het standaardcommando en als `agent` met `python3 /app/agent.py`. Tot nu draaide de app op
`python:3-alpine` en `nginx:alpine` met zijn eigen bestanden als `*.template` uit de app-map gemount.
Waarom, staat in het plan **Eigenimage**, `PLAN.md` §3; het korte antwoord is dat de programmacode nu
niet meer in de app-data staat, en dat het startscript weer leesbaar is.
- **Het `command`-blok van de compose is `entrypoint.sh` in de image.** Ruim honderd regels shell in een
YAML-string, met elk dollarteken verdubbeld omdat umbreld anders elke `${...}` leegmaakte, is nu een
gewoon script. Zelfde gedrag: TLS aan of uit naar de toestand van `cert.conf`, de verbindingsteller uit
`/proc/net/tcp`, de herlaadlus met vangnettak, en `exec nginx` als laatste regel.
- **Het `log_format` staat in `stream.conf` zelf.** Het stond in een los bestand dat het `command`-blok
wegschreef, omdat een dollarteken in een template niet mocht. Die omweg is weg.
- **Het backend-adres komt uit de omgeving, in beide containers.** `stream.conf` heeft twee plaatshouders
zonder dollarteken die `entrypoint.sh` bij het starten invult; de server-service krijgt daarvoor dezelfde
`GATE_ELECTRUM_HOST` en `GATE_ELECTRUM_PORT` als de agent.
- **De pagina haalt de versie en het backend-adres uit `status.json`.** umbreld vulde die tot nu
rechtstreeks in de pagina in; nu de pagina in de image zit, geeft de compose `APP_VERSION` aan de agent
door als `GATE_APP_VERSION`, en zet de agent `version` in de status. Ontbreekt hij, dan toont de kop
geen versie in plaats van een verzonnen.
- **`icon.png` is updatebaar**, want de image kopieert hem uit de app-map. Het bestand blijft ook daar
staan, want het manifest wijst ernaar voor de tegel in de winkel.
### Removed
- De vier `*.template`-bestanden en hun mounts uit de app-map. Wat er overblijft: `docker-compose.yml`,
`umbrel-app.yml`, `icon.png` en `data/`.
## [0.0.29] - 2026-09-04 ## [0.0.29] - 2026-09-04
### Changed ### Changed
+1 -1
View File
@@ -23,7 +23,7 @@
| Plan | App | Volgende stap | Status | | Plan | App | Volgende stap | Status |
|-|-|-|-| |-|-|-|-|
| [Eigenimage](Plannen/Actief/004-Eigenimage/TAKEN.md) | **beide** | **Gepromoveerd op 28-08-2026, want waar het op wachtte is klaar:** de gebruiker wilde eerst Evolu Relay afmaken en dat doel is gehaald. Het register is beslist (Gitea, bij inlevering opnieuw kijken), dus fase 1 kan zo starten: het bouwrecept in `tools/electrum-gate/`, multi-arch, anoniem te halen. Er ligt nu ook een voorbeeld naast: `tools/evolu-relay/` doet precies dit, inclusief de valkuil dat een lokaal gebouwde tag voor umbreld onbereikbaar is. **Sinds 30-08-2026 wegen er twee dingen zwaarder voor dit plan.** De kolom App staat op "beide": §8 is herschreven, want ook Evolu Relay heeft zijn agent en pagina nog op vreemde images staan; alleen de relay zelf is klaar. En dit plan blijkt de poort te zijn voor het pinnen dat **Publicatie-Gate** en **Publicatie-Relay** eisen: die twee images verdwijnen hier, dus eerder pinnen is weggegooid werk. Let op §1: het lost níet het updateprobleem op, want dat bestaat niet meer | | | [Eigenimage](Plannen/Actief/004-Eigenimage/TAKEN.md) | **beide** | **Fase 1 tot en met 3 gedaan op 07-09-2026, als Electrum Gate 0.1.0, ongebouwd.** Het recept staat in `tools/electrum-gate/` (Dockerfile, build.sh, entrypoint.sh en de vier voormalige templates), de app-map bevat alleen nog compose, manifest, icoon en `data/`, en de compose is voor het eerst zonder shellblok. Eén image voor beide containers, gebouwd op de Umbrel; open punt 2 en 3 zijn daarmee beslist. **Volgende stap is van de gebruiker en kan alleen op de Umbrel:** `sh tools/electrum-gate/build.sh`, duwen, de digest twee keer in de compose, committen, updaten, en dan de controles van fase 4. Reken op minstens één ronde, want de image is nog nooit gebouwd; de tests toetsen de tekst van het recept en niet het resultaat. **Daarna:** hetzelfde voor de agent en de pagina van Evolu Relay (§8), pas als de weg bij Gate bewezen is. Multi-arch en de anonieme pull horen bij **Publicatie-Gate** | 🔶 |
## B - Los oppakbaar (geen blokkade, geen vaste volgorde) ## B - Los oppakbaar (geen blokkade, geen vaste volgorde)
+4
View File
@@ -23,6 +23,10 @@ De keten die eronder ligt, van achter naar voren:
**De regel: pin alleen een image die er na Eigenimage nog staat.** In de praktijk is dat de image die je **De regel: pin alleen een image die er na Eigenimage nog staat.** In de praktijk is dat de image die je
zelf uitgeeft, en die is er precies één per app. zelf uitgeeft, en die is er precies één per app.
Stand op 07-09-2026: Electrum Gate heeft die eigen image sinds 0.1.0 en zijn twee vreemde images zijn weg;
de pin komt erbij zodra de gebruiker hem gebouwd en geduwd heeft. Bij Evolu Relay staan `python:3-alpine`
en `nginx:alpine` er nog voor de agent en de pagina, en daarvoor geldt de regel hierboven onverkort.
Twee dingen die dit níet zegt, want anders slaat het de andere kant op door: Twee dingen die dit níet zegt, want anders slaat het de andere kant op door:
- **de eigen image pinnen doe je wél meteen**, bij elke verhoging. Dat is geen toekomstig werk maar de - **de eigen image pinnen doe je wél meteen**, bij elke verhoging. Dat is geen toekomstig werk maar de
+14 -8
View File
@@ -12,17 +12,23 @@
## Nog te beslissen ## Nog te beslissen
2. **Eén image of twee?** [PLAN.md](PLAN.md) §6 stelt één voor: de agent en nginx blijven twee containers, Niets.
maar ze delen één image. Twee images is netter gescheiden en verdubbelt het bouwwerk en het pinwerk.
**Moment:** bij de start van het werk · **Eigenaar:** sessie, met de gebruiker mee
3. **Waar wordt gebouwd?** Op de Umbrel staat Docker, maar dat is een productiemachine en hij is amd64,
terwijl arm64 ook een eis is. Bouwen op de werkplek van de gebruiker vraagt daar Docker. Dit hangt samen
met de multi-arch-stap uit §4 van het plan, die nog nooit geprobeerd is.
**Moment:** bij de start van het werk · **Eigenaar:** gebruiker
## Beslist ## Beslist
2. **Eén image of twee?** - **Eén** (07-09-2026, sessie, volgens het voorstel in [PLAN.md](PLAN.md) §6).
De agent en nginx blijven twee containers met dezelfde image; de compose geeft de agent een eigen
`command`. Basis is `nginx:1.30-alpine` met `python3` uit apk, en niet andersom: de officiële
nginx-image heeft de stream-module ingebouwd, bij alpine's eigen nginx is dat een los pakket.
**Moment:** afgerond · **Eigenaar:**
3. **Waar wordt gebouwd?** - **Op de Umbrel** (07-09-2026), om een eenvoudige reden: het is de enige
machine met Docker; de werkplek heeft het niet. Dat het een productiemachine is weegt minder dan dat
het de plek is waar Evolu Relay ook al gebouwd wordt. Gevolg: één architectuur, amd64, en dat is voor
eigen gebruik genoeg. Multi-arch zit in `build.sh` als schakelaar (`PLATFORMS=...`) en is werk voor
**Publicatie-Gate**; het is nog nooit geprobeerd.
**Moment:** afgerond · **Eigenaar:**
1. **Blijft het Gitea-register ook bij publicatie de bron?** - **Voor nu Gitea, bij inlevering opnieuw 1. **Blijft het Gitea-register ook bij publicatie de bron?** - **Voor nu Gitea, bij inlevering opnieuw
kijken** (27-08-2026, gebruiker). kijken** (27-08-2026, gebruiker).
@@ -3,6 +3,25 @@
> Chronologisch sessielog, nieuwste bovenaan. Kort: 3 tot 6 regels per entry. Wat er is gebeurd en > 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). > waarom, niet wat er nog moet: dat staat in [TAKEN.md](TAKEN.md).
## 07-09-2026 - fase 1 tot en met 3 in één sessie: het recept staat, de app-map is leeg
Op verzoek van de gebruiker opgepakt, direct na Relay 0.5.6. De vier templates zijn met `git mv` naar
`tools/electrum-gate/` verhuisd en heten daar gewoon `agent.py`, `nginx.conf`, `stream.conf` en
`index.html`. Daarnaast nieuw: `Dockerfile` (nginx:1.30-alpine plus python3), `entrypoint.sh` (het
`command`-blok van de compose, zonder `$$`) en `build.sh` naar het voorbeeld van Evolu Relay. Open punt 2
en 3 zijn beslist: één image, gebouwd op de Umbrel.
Wat er inhoudelijk veranderde en niet alleen verhuisde: het `log_format` staat nu in `stream.conf` zelf,
het backend-adres komt via twee plaatshouders zonder dollarteken uit de omgeving (ook de server-service
krijgt ze nu), en de pagina haalt versie en adres uit `status.json`, waarvoor de agent `GATE_APP_VERSION`
doorgeeft. De compose is voor het eerst sinds 0.0.3 zonder shellblok.
Tests: alle zes groen, niets overgeslagen. Drie mutaties (het `exec` voor nginx weg, `ENTRYPOINT []` weg,
één tag in de compose verhoogd) lieten precies de bedoelde toetsen omvallen, ook de nieuwe in
`test_appstore_vorm.py`. **Niet geverifieerd: de image zelf.** Er is hier geen Docker, dus of `apk add
python3` het gedraagt zoals gedacht en of de container start, blijkt pas bij fase 4 op de Umbrel.
Electrum Gate staat op 0.1.0 in het manifest, met de tag ongepind tot de eerste push.
## 27-08-2026 - het register is beslist, en daarna ging de aandacht naar Evolu Relay ## 27-08-2026 - het register is beslist, en daarna ging de aandacht naar Evolu Relay
Open punt 1 beantwoord: **voor nu Gitea, bij inlevering opnieuw kijken.** De twee bezwaren die dat punt Open punt 1 beantwoord: **voor nu Gitea, bij inlevering opnieuw kijken.** De twee bezwaren die dat punt
+67 -41
View File
@@ -18,65 +18,91 @@
## Volgende stap ## Volgende stap
> **Dit plan ligt stil op verzoek van de gebruiker (27-08-2026): hij gaat eerst verder met Evolu Relay, > **Opgepakt op 07-09-2026 op verzoek van de gebruiker.** Fase 1 tot en met 3 zijn in één sessie gedaan
> zie het plan Umbrelapp.** Dat is een volgordekeuze en geen blokkade; er is aan dit plan nog niets > en gecommit als Electrum Gate 0.1.0, ongebouwd. **Wat nu volgt is van de gebruiker en kan alleen op de
> gebouwd, dus er ligt ook niets half af. Beginnen kan met fase 1, en het antwoord dat daarvoor nodig was > Umbrel**, want daar staat Docker:
> staat er. >
> 1. `sh tools/electrum-gate/build.sh`, dan `docker login` en `docker push` zoals het script afdrukt;
> 2. de digest uit de push-uitvoer achter de tag in de compose, **twee keer** (agent en server);
> 3. committen en pushen, dan de app in umbrelOS updaten;
> 4. de vijf controles van fase 4 hieronder.
>
> Dit is de eerste keer dat deze image gebouwd wordt, dus reken op minstens één ronde: een `apk add` die
> anders uitpakt dan gedacht, of een pad dat de Dockerfile wél kent en de container niet. De tests
> hierboven toetsen de tekst van het recept, niet het resultaat.
- [x] **Fase 1 tot en met 3 gedaan (07-09-2026), als 0.1.0.** De vier `*.template`-bestanden staan nu in
`tools/electrum-gate/` zonder de extensie, plus `Dockerfile`, `build.sh` en `entrypoint.sh`. De
compose gebruikt één image voor beide containers, ongepind tot de eerste push. Tests aangepast en
mutatie-getest; details in [PROGRESS.md](PROGRESS.md)
- [ ] **Fase 4: bouwen, duwen, pinnen, installeren, nakijken.** Van de gebruiker; zie hierboven
- [x] **Open punt 1 beantwoord (27-08-2026): voor nu Gitea, bij inlevering opnieuw kijken.** Daarmee is de - [x] **Open punt 1 beantwoord (27-08-2026): voor nu Gitea, bij inlevering opnieuw kijken.** Daarmee is de
enige vraag weg die vóór het bouwen beantwoord moest zijn. Zie [OPEN.md](OPEN.md) enige vraag weg die vóór het bouwen beantwoord moest zijn. Zie [OPEN.md](OPEN.md)
- [ ] **Fase 1 beginnen: het bouwrecept in `tools/electrum-gate/`.** Open punt 2 en 3 horen bij die start
en niet ervoor: één image of twee, en op welke machine er gebouwd wordt. Dat laatste is de enige die
de gebruiker moet beantwoorden, en pas op het moment dat het recept er is en gedraaid moet worden
## Fase 1 - Het bouwrecept ## Fase 1 - Het bouwrecept
- [ ] Beslissen: één image of twee (open punt 2). [PLAN.md](PLAN.md) §6 stelt één voor - [x] Beslissen: één image of twee (open punt 2). **Eén**, zoals [PLAN.md](PLAN.md) §6 voorstelt; beslist
- [ ] Beslissen: waar wordt gebouwd (open punt 3). De Umbrel is amd64 en een productiemachine 07-09-2026, zie [OPEN.md](OPEN.md)
- [ ] `tools/electrum-gate/Dockerfile` schrijven: basisimage, `agent.py`, de nginx-configuratie, de pagina, - [x] Beslissen: waar wordt gebouwd (open punt 3). **Op de Umbrel**, want dat is de enige machine met
en een `entrypoint.sh`. **Niet in de app-map**: een `Dockerfile` staat niet in de update-whitelist, Docker; de werkplek heeft het niet. Zie [OPEN.md](OPEN.md)
dus bouwen-in-de-app kost bij elke versie een deïnstallatie - [x] `tools/electrum-gate/Dockerfile`: `nginx:1.30-alpine` plus `python3` uit apk, de vijf bestanden
- [ ] `tools/electrum-gate/build.sh` naar het voorbeeld van `tools/evolu-relay/build.sh`: pin op de erin, `ENTRYPOINT []` zodat het envsubst-script van de nginx-image niet meedraait. De bouwcontext is
basisimage, bouwen, naar het register duwen, en de digest afdrukken die in de compose moet de repo-root, want `icon.png` blijft in de app-map (het manifest wijst ernaar) en wordt van daar
- [ ] **Multi-arch bouwen, en dat is nieuw werk.** `linux/amd64` én `linux/arm64` in de manifest-lijst. gekopieerd; `.dockerignore` in de root houdt de context klein
Vraagt `buildx` met QEMU en is hier nog nooit geprobeerd; zie [PLAN.md](PLAN.md) §4 - [x] `tools/electrum-gate/build.sh` naar het voorbeeld van Evolu Relay: `VERSION`, controles vooraf
(ontbrekende bestanden, een `${` die niemand meer invult), bouwen, en de vervolgstappen afdrukken
- [ ] **Multi-arch bouwen, en dat is nieuw werk.** `build.sh` heeft er een schakelaar voor:
`PLATFORMS="linux/amd64,linux/arm64"` bouwt met `buildx` en duwt meteen. Nog nooit geprobeerd, en
niet nodig voor eigen gebruik op een amd64-Umbrel. Hoort bij **Publicatie-Gate**; zie
[PLAN.md](PLAN.md) §4
- [ ] **Controleren dat de image anoniem te halen is.** umbreld krijgt geen inloggegevens mee. Uitloggen in - [ ] **Controleren dat de image anoniem te halen is.** umbreld krijgt geen inloggegevens mee. Uitloggen in
dezelfde context waarin je inlogde, anders meet je je eigen sessie. Het commando staat onderaan dezelfde context waarin je inlogde, anders meet je je eigen sessie. Het commando drukt `build.sh` af.
`tools/evolu-relay/build.sh` **Eigenaar: gebruiker**, bij fase 4
## Fase 2 - Het `command`-blok wordt een script ## Fase 2 - Het `command`-blok wordt een script
- [ ] Het inline `command`-blok uit `docker-compose.yml` omzetten naar `entrypoint.sh` in de image. Dat is - [x] `tools/electrum-gate/entrypoint.sh`, uit het `command`-blok van de compose. Zelfde gedrag, geen
ruim honderd regels shell in een YAML-string met een eigen ontsnappingsregel voor het dollarteken; `$$` meer, en het `log_format` staat nu gewoon in `stream.conf` in plaats van in een los bestand dat
zie [PLAN.md](PLAN.md) §2. **Dit is de grootste inhoudelijke winst van het hele plan** het script wegschreef. Nieuw erin: het invullen van het backend-adres in het stream-blok (zie fase 3)
- [ ] Let op wat daarbij niet mag sneuvelen: het `log_format` met `$$`, de sessieteller uit - [x] Wat niet mocht sneuvelen en er staat: de sessieteller uit `/proc/net/tcp`, de herlaadlus met zijn
`/proc/net/tcp`, de herlaadlus met zijn vangnettak, en het feit dat nginx met `exec` het hoofdproces vangnettak, en `exec nginx` als laatste regel. Alle drie met een toets in
wordt. Die laatste is geen detail: andersom blijft de shell PID 1, geeft signalen niet door, en ziet `test_server_start_zonder_certificaat.py`, en die zijn gezien te falen
Docker een geslaagde afsluiting als nginx omvalt
## Fase 3 - De app-map opschonen ## Fase 3 - De app-map opschonen
- [ ] `agent.py.template`, `nginx.conf.template`, `stream.conf.template` en `index.html.template` uit de - [x] De vier templates uit de app-map, met `git mv` naar `tools/electrum-gate/` zonder `.template`
app-map halen zodra ze in de image zitten - [x] **`index.html` heeft geen template-invulling meer.** `${APP_VERSION}` en het backend-adres komen uit
- [ ] **`index.html` heeft dan geen template-invulling meer.** Vandaag staat er `status.json`: de agent krijgt `GATE_APP_VERSION: ${APP_VERSION}` uit de compose en zet `version` in
`${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}` in, ingevuld door umbreld bij het starten. Zit de de status; het adres stond er al als `backend.address`
pagina in de image, dan moet dat adres langs de omgeving naar binnen, bijvoorbeeld doordat de agent - [x] **`stream.conf` heeft geen template-invulling meer.** Twee plaatshouders zonder dollarteken,
het in `status.json` zet en de pagina het daaruit leest. Dat is werk dat nu nog gratis is `__GATE_ELECTRUM_HOST__` en `__GATE_ELECTRUM_PORT__`, die `entrypoint.sh` uit de omgeving invult;
- [ ] De compose houdt zijn `environment`-blok; dat is hoe `${APP_ELECTRS_NODE_IP}` binnenkomt, en die weg de server-service krijgt daarvoor dezelfde twee variabelen als de agent
is op 27-08-2026 bewezen bij de omschakeling naar Fulcrum - [x] De compose houdt zijn `environment`-blok; de mounts van de vijf losse bestanden zijn eruit
- [ ] Wat er in de app-map overblijft: `docker-compose.yml`, `umbrel-app.yml`, `icon.png` en `data/` met - [x] Wat er in de app-map overblijft: `docker-compose.yml`, `umbrel-app.yml`, `icon.png` en `data/` met
zijn twee `.gitkeep`-bestanden zijn twee `.gitkeep`-bestanden. Precies wat [PLAN.md](PLAN.md) §6 zei
- [x] De tests mee verhuisd: `test_agent_certificates.py` laadt `tools/electrum-gate/agent.py`;
`test_server_start_zonder_certificaat.py` toetst `entrypoint.sh` en de Dockerfile in plaats van het
`command`-blok, plus dat de tag in de compose `VERSION` uit `build.sh` is; `test_appstore_vorm.py`
leest ook de nginx-config onder `tools/` en toetst tag gelijk aan `VERSION` voor élke eigen image;
`test_paginas_parsen.mjs` en `tools/voorbeeldpagina.mjs` vinden een pagina op beide plekken
## Fase 4 - Uitrollen en verifiëren ## Fase 4 - Uitrollen en verifiëren
- [ ] Image gepind als `repo:tag@sha256:<digest>` in de compose, en `version` verhoogd in het manifest. - [ ] Image gepind als `repo:tag@sha256:<digest>` in de compose (twee keer), en `version` verhoogd in het
Zonder die verhoging rolt umbrelOS niets uit manifest. Dat laatste staat al op 0.1.0
- [ ] Op de Umbrel installeren en controleren dat alles het nog doet: de pagina, de certificaatkeuze, de - [ ] Op de Umbrel installeren en controleren dat alles het nog doet: de pagina, de certificaatkeuze, de
TLS-poort, de zelfcontrole en het activiteitenlog TLS-poort, de zelfcontrole en het activiteitenlog
- [ ] **`icon.png` wordt hiermee updatebaar**, want die zit dan in de image. Dat was de enige echte - [ ] **De versie in de kop van de pagina**, want die loopt nu via `status.json` en niet meer via umbreld
uitzondering op "alles komt al aan bij een update"; zie [PLAN.md](PLAN.md) §1 - [ ] **`icon.png` is hiermee updatebaar**, want die zit in de image. Dat was de enige echte uitzondering
op "alles komt al aan bij een update"; zie [PLAN.md](PLAN.md) §1
## Daarna - Evolu Relay
- [ ] **Hetzelfde voor de agent en de pagina van Evolu Relay**, die nog als `*.template` op
`python:3-alpine` en `nginx:alpine` draaien; zie [PLAN.md](PLAN.md) §8. Pas na fase 4 hierboven:
eerst zien dat de weg werkt voordat de tweede app hem gaat
## Geblokkeerd / wacht op ## Geblokkeerd / wacht op
Niets, behalve open punt 1 hierboven, en dat is een vraag van één regel aan de gebruiker. Fase 4 wacht op de gebruiker: bouwen kan alleen op de Umbrel.
+1 -1
View File
@@ -353,7 +353,7 @@ Wat daar staat en wat deze app ervan doet:
| Eis | Deze app | | Eis | Deze app |
|-|-| |-|-|
| Elke image gepind als `repo:versie@sha256:<digest>`, met `linux/amd64` én `linux/arm64` in de manifest-lijst. Verboden: `latest`, meebewegende branch-tags, een digest zonder tag | **Nog niet.** `python:3-alpine` en `nginx:alpine` staan er kaal in. Dit is de grootste openstaande eis en hij kan alleen op de Umbrel zelf, met `docker buildx imagetools inspect` | | Elke image gepind als `repo:versie@sha256:<digest>`, met `linux/amd64` én `linux/arm64` in de manifest-lijst. Verboden: `latest`, meebewegende branch-tags, een digest zonder tag | **Half, sinds 0.1.0 (07-09-2026).** De app heeft een eigen image; de digest komt erbij bij de eerste push en dat kan alleen op de Umbrel. Wat nog ontbreekt is `linux/arm64`: `build.sh` bouwt standaard één architectuur, multi-arch zit erin als schakelaar en is nooit geprobeerd |
| Mapnaam gelijk aan het app-id, lowercase kebab-case | Klopt, maar het id heeft nu het store-voorvoegsel `whatsnext-`. Dat is een eis van een **community** store; officiële apps hebben een kaal id, dus dit wordt `electrum-gate` | | Mapnaam gelijk aan het app-id, lowercase kebab-case | Klopt, maar het id heeft nu het store-voorvoegsel `whatsnext-`. Dat is een eis van een **community** store; officiële apps hebben een kaal id, dus dit wordt `electrum-gate` |
| Manifestvelden in een vaste volgorde: `manifestVersion`, `id`, `category`, `name`, `version`, `tagline`, `description`, `releaseNotes`, `developer`, `website`, `dependencies`, `repo`, `support`, `port`, `gallery`, `path`, en daarna de optionele | Klopt sinds 20-08-2026, met een toets erop. Wat de spec niet noemt (`icon`, `backupIgnore`) staat áchter die reeks, dus de kop is letterlijk goed | | Manifestvelden in een vaste volgorde: `manifestVersion`, `id`, `category`, `name`, `version`, `tagline`, `description`, `releaseNotes`, `developer`, `website`, `dependencies`, `repo`, `support`, `port`, `gallery`, `path`, en daarna de optionele | Klopt sinds 20-08-2026, met een toets erop. Wat de spec niet noemt (`icon`, `backupIgnore`) staat áchter die reeks, dus de kop is letterlijk goed |
| `gallery: []` bij een nieuw pakket; het store-team doet de plaatjes | Klopt al. Zie de paragraaf hieronder: de inhoud van dit veld moet bij inlevering leeg zijn, het veld zelf blijft staan | | `gallery: []` bij een nieuw pakket; het store-team doet de plaatjes | Klopt al. Zie de paragraaf hieronder: de inhoud van dit veld moet bij inlevering leeg zijn, het veld zelf blijft staan |
+5 -3
View File
@@ -11,12 +11,14 @@ Reach your own Electrum server from outside your network, over TLS. An Electrum
a wallet on the road wants TLS. This app puts a proxy in between, using the certificate a reverse proxy on a wallet on the road wants TLS. This app puts a proxy in between, using the certificate a reverse proxy on
the same Umbrel already manages. the same Umbrel already manages.
Two containers, both on an off-the-shelf image, both configured from a `*.template`. Two containers, one image, built from [tools/electrum-gate/](tools/electrum-gate/) by its `build.sh`
(nginx on alpine, plus python3). The app folder holds only the compose file, the manifest, the icon and
your data.
| Container | What it does | | Container | What it does |
|-|-| |-|-|
| `server` (`nginx:alpine`) | terminates TLS on 50022 and forwards plain to the Electrum server; serves the dashboard on port 80 behind the umbrelOS app proxy | | `server` | terminates TLS on 50022 and forwards plain to the Electrum server; serves the dashboard on port 80 behind the umbrelOS app proxy |
| `agent` (`python:3-alpine`) | writes `status.json` every minute, reads the certificates from the mounted folders, queries the Electrum server, and accepts the certificate choice | | `agent` | same image, different command; writes `status.json` every minute, reads the certificates from the mounted folders, queries the Electrum server, and accepts the certificate choice |
The agent cannot reload nginx itself, as that would need the Docker socket and it is deliberately absent. The agent cannot reload nginx itself, as that would need the Docker socket and it is deliberately absent.
It writes `cert.conf` with the chosen paths and drops a flag file; the nginx container reloads itself. A It writes `cert.conf` with the chosen paths and drops a flag file; the nginx container reloads itself. A
+22 -18
View File
@@ -11,16 +11,18 @@ Draaien:
python tests/test_agent_certificates.py python tests/test_agent_certificates.py
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen De test laadt `tools/electrum-gate/agent.py` rechtstreeks: het bestand dat de
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat; Dockerfile in de image zet. Tot 0.1.0 heette dat `agent.py.template` en stond het
de eerste toets hieronder controleert precies dat. in de app-map; de eerste toets hieronder is uit die tijd en blijft staan, want
het script haalt zijn instellingen nog altijd uit de omgeving en niet uit een
ingevulde constante.
""" """
import sys import sys
# Vóór de imports, want anders is het te laat: Python legt bytecode naast # Vóór de imports, want anders is het te laat: Python legt bytecode naast agent.py
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de # zodra die geïmporteerd wordt, en die rommel hoort niet in de repo. Een keer is
# app-map. Een keer is zo'n .pyc meegegaan in een commit. # zo'n .pyc meegegaan in een commit.
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
import importlib.machinery # noqa: E402 import importlib.machinery # noqa: E402
@@ -33,17 +35,13 @@ import subprocess # noqa: E402
import tempfile # noqa: E402 import tempfile # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATE = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate", "agent.py.template") AGENT = os.path.join(HERE, os.pardir, "tools", "electrum-gate", "agent.py")
def load_agent(): def load_agent():
"""Laadt agent.py.template als module. """Laadt agent.py als module, buiten sys.path om."""
loader = importlib.machinery.SourceFileLoader("gate_agent", AGENT)
Met een expliciete loader, want importlib kijkt normaal naar de extensie en spec = importlib.util.spec_from_file_location("gate_agent", AGENT, loader=loader)
.template staat daar niet tussen.
"""
loader = importlib.machinery.SourceFileLoader("gate_agent", TEMPLATE)
spec = importlib.util.spec_from_file_location("gate_agent", TEMPLATE, loader=loader)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
return module return module
@@ -83,12 +81,18 @@ def nep_certificaat(tijdtag, tijdtekst):
def test_template_is_invulbaar_zonder_schade(u): def test_template_is_invulbaar_zonder_schade(u):
"""De aanname waar deze hele test op rust.""" """Een restant uit de template-tijd dat blijft staan.
with open(TEMPLATE, "r", encoding="utf-8") as f:
Het bestand zit sinds 0.1.0 in de image en gaat door geen envsubst meer. Maar
een accolade-variabele in Python-code is ook zonder umbreld een fout, en als
iemand dit bestand ooit weer als template in een app-map zet, is dit de toets
die het meteen ziet.
"""
with open(AGENT, "r", encoding="utf-8") as f:
inhoud = f.read() inhoud = f.read()
u.check("template bevat geen accolade-variabelen", u.check("agent.py bevat geen accolade-variabelen",
"${" not in inhoud, "${" not in inhoud,
"umbreld zou die invullen en de Python-code slopen") "een envsubst-stap zou die invullen en de Python-code slopen")
def test_einddatum_gelijk_aan_ssl(agent, u): def test_einddatum_gelijk_aan_ssl(agent, u):
+74 -5
View File
@@ -20,7 +20,9 @@ Wat deze toetsen expres NIET doen: eisen dat elke image op een digest gepind is.
Dat is wél de regel, maar op 25-08-2026 haalt geen van de twee apps hem, en een Dat is wél de regel, maar op 25-08-2026 haalt geen van de twee apps hem, en een
suite die altijd rood staat wordt niet gelezen. Het staat als taak in de plannen suite die altijd rood staat wordt niet gelezen. Het staat als taak in de plannen
Umbrelapp en Publicatie-Gate. Wat hier wel gebeurt is de pinstatus afdrukken, zodat Umbrelapp en Publicatie-Gate. Wat hier wel gebeurt is de pinstatus afdrukken, zodat
je hem ziet zonder erover te struikelen. je hem ziet zonder erover te struikelen. Wat wél getoetst wordt, sinds 07-09-2026:
dat de tag van een eigen image gelijk is aan VERSION in het bijbehorende
tools/<app>/build.sh, want een tag die nooit gebouwd is faalt pas op het apparaat.
Deze toetsen vinden hun apps zelf. Komt er een derde app bij, dan valt die Deze toetsen vinden hun apps zelf. Komt er een derde app bij, dan valt die
automatisch onder alles hierboven en hoeft hier niets bij. automatisch onder alles hierboven en hoeft hier niets bij.
@@ -172,6 +174,61 @@ def test_manifest_volgorde(u, app):
not extra_ertussen, "ertussen: %r" % extra_ertussen) not extra_ertussen, "ertussen: %r" % extra_ertussen)
def recept_map(app):
"""Waar het bouwrecept van een app staat: tools/<app-id zonder store-id>/.
Dat is de afspraak uit CLAUDE.md: een Dockerfile hoort niet in de app-map,
want die staat niet in de update-whitelist. De mapnaam is het app-id zonder
het store-voorvoegsel, dus whatsnext-evolu-relay bouwt in tools/evolu-relay/.
"""
kort = app.split("-", 1)[1] if "-" in app else app
return os.path.join(REPO, "tools", kort)
def test_image_tag_is_de_gebouwde(u, app):
"""De tag in de compose is VERSION uit build.sh, voor elke eigen image.
Een tag die in de compose staat maar nooit gebouwd is, faalt pas op het
apparaat met "pull access denied". En andersom, een VERSION die verhoogd is
zonder de compose mee te nemen, levert een nieuwe image en een oude
installatie zonder dat iets dat meldt. Beide staan als regel in CLAUDE.md;
dit is de toets erop.
Alleen images uit het eigen recept: een vreemde image (postgres, nginx) heeft
geen build.sh en valt hier buiten.
"""
build = os.path.join(recept_map(app), "build.sh")
if not os.path.isfile(build):
return
versie = None
for regel in lees(build).splitlines():
if regel.startswith("VERSION="):
versie = regel.split("=", 1)[1].strip().strip('"')
u.check("%s: build.sh heeft een VERSION" % app, bool(versie))
if not versie:
return
kort = os.path.basename(recept_map(app))
compose = lees(os.path.join(REPO, app, "docker-compose.yml"))
eigen = []
for regel in compose.splitlines():
kaal = regel.strip()
if not kaal.startswith("image:"):
continue
image = kaal.split(":", 1)[1].strip()
repo_en_tag = image.split("@", 1)[0]
repo_naam = repo_en_tag.rsplit(":", 1)[0]
if repo_naam.endswith("/" + kort):
eigen.append(repo_en_tag)
u.check("%s: de compose gebruikt de eigen image" % app, bool(eigen),
"geen image op .../%s gevonden" % kort)
verkeerd = [i for i in eigen if not i.endswith(":" + versie)]
u.check("%s: elke eigen image heeft tag %s uit build.sh" % (app, versie),
not verkeerd, "gevonden: %r" % verkeerd)
def test_compose_bestaat_en_hangt_samen(u, app): def test_compose_bestaat_en_hangt_samen(u, app):
"""De app_proxy moet naar een service wijzen die bestaat. """De app_proxy moet naar een service wijzen die bestaat.
@@ -285,14 +342,25 @@ def test_containernamen_zijn_volledig(u, app):
De vorm <app-id>_<service>_1 is wél uniek. Deze toets kijkt naar de plekken De vorm <app-id>_<service>_1 is wél uniek. Deze toets kijkt naar de plekken
waar een naam als hostnaam wordt gebruikt: proxy_pass in een nginx-config, en waar een naam als hostnaam wordt gebruikt: proxy_pass in een nginx-config, en
de omgevingsvariabelen die een hostnaam dragen. de omgevingsvariabelen die een hostnaam dragen. Een nginx-config kan in de
app-map staan (als *.template) of in het bouwrecept onder tools/ (als hij in
een eigen image zit); beide worden gelezen.
""" """
fout = [] fout = []
bestanden = []
for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))): for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))):
if not bestandsnaam.endswith(".template") and bestandsnaam != "docker-compose.yml": if bestandsnaam.endswith(".template") or bestandsnaam == "docker-compose.yml":
continue bestanden.append(os.path.join(REPO, app, bestandsnaam))
tekst = lees(os.path.join(REPO, app, bestandsnaam)) recept = recept_map(app)
if os.path.isdir(recept):
for bestandsnaam in sorted(os.listdir(recept)):
if bestandsnaam.endswith(".conf"):
bestanden.append(os.path.join(recept, bestandsnaam))
for pad in bestanden:
bestandsnaam = os.path.relpath(pad, REPO).replace(os.sep, "/")
tekst = lees(pad)
for regel in tekst.splitlines(): for regel in tekst.splitlines():
kaal = regel.strip() kaal = regel.strip()
if kaal.startswith("#"): if kaal.startswith("#"):
@@ -464,6 +532,7 @@ def main():
test_verplichte_velden(u, app) test_verplichte_velden(u, app)
test_manifest_volgorde(u, app) test_manifest_volgorde(u, app)
test_compose_bestaat_en_hangt_samen(u, app) test_compose_bestaat_en_hangt_samen(u, app)
test_image_tag_is_de_gebouwde(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_geen_werkbestanden_in_de_app_map(u, app)
+35 -5
View File
@@ -99,14 +99,35 @@ const parseert = (bron) => {
} }
}; };
/**
* Waar de pagina van een app staat, en of umbreld hem invult.
*
* Twee plekken: als *.template in de app-map (dan gaat hij door envsubst), of als
* gewoon bestand in het bouwrecept onder tools/<app-id zonder store-id>/ (dan zit
* hij in een eigen image en wordt hij door niemand ingevuld). Electrum Gate is
* sinds 0.1.0 het tweede geval; zie het plan Eigenimage.
*/
const vindPagina = (app) => {
const template = join(REPO, app, 'index.html.template');
if (existsSync(template)) {
return { pad: template, template: true };
}
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
const inImage = join(REPO, 'tools', kort, 'index.html');
if (existsSync(inImage)) {
return { pad: inImage, template: false };
}
return null;
};
for (const app of appMappen()) { for (const app of appMappen()) {
const pad = join(REPO, app, 'index.html.template'); const pagina = vindPagina(app);
if (!existsSync(pad)) { if (pagina === null) {
// Geen pagina is geen fout: een app hoeft er geen te hebben. // Geen pagina is geen fout: een app hoeft er geen te hebben.
continue; continue;
} }
const bron = readFileSync(pad, 'utf8'); const bron = readFileSync(pagina.pad, 'utf8');
const blokken = scriptBlokken(bron); const blokken = scriptBlokken(bron);
toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0); toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0);
@@ -117,10 +138,19 @@ for (const app of appMappen()) {
const rauw = parseert(blok); const rauw = parseert(blok);
toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw); toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw);
const ingevuld = parseert(vulIn(blok)); if (pagina.template) {
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld); const ingevuld = parseert(vulIn(blok));
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
}
}); });
if (!pagina.template) {
// Een pagina in een image gaat door geen envsubst meer, dus wat er staat is
// wat de browser krijgt. Een accolade-variabele is dan een restant uit de
// template-tijd die letterlijk op de pagina zou belanden.
toets(`${app}: de pagina in de image bevat geen accolade-variabele`, !bron.includes('${'));
}
// De HTML zelf: geen parser, maar wel de fout die je in een bestand van // 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, // tweeduizend regels echt maakt. Een dialog of een header die niet gesloten is,
// laat de rest van de pagina in dat element hangen. // laat de rest van de pagina in dat element hangen.
+211 -62
View File
@@ -9,18 +9,25 @@ niets. De symptomen wezen ergens anders heen: de app_proxy meldde alleen dat de
server niet te bereiken was. server niet te bereiken was.
De reparatie is een verplaatsing, en die is niet aan de code af te lezen: het De reparatie is een verplaatsing, en die is niet aan de code af te lezen: het
stream-blok staat nu in stream.conf.template en wordt door het command-blok van stream-blok staat in stream.conf en wordt door entrypoint.sh pas in
de compose pas in /var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf /var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf haalt die map op met
haalt die map op met een jokerteken, en een jokerteken dat niets matcht is voor een jokerteken, en een jokerteken dat niets matcht is voor nginx geen fout. Eén
nginx geen fout. Eén iemand die dat blok "netjes" terugzet in nginx.conf en de iemand die dat blok "netjes" terugzet in nginx.conf en de klem is terug, zonder
klem is terug, zonder dat een van de andere toetsen iets merkt. dat een van de andere toetsen iets merkt.
Sinds 0.1.0 zitten deze bestanden in een eigen image en staan ze in
tools/electrum-gate/; tot dan waren het *.template-bestanden in de app-map en
was het startscript een command-blok in de compose. De toetsen zijn mee
verhuisd en toetsen hetzelfde gedrag, plus wat er bij die verhuizing nieuw
kapot kan: de Dockerfile die een bestand niet meeneemt, en de plaatshouders in
stream.conf die entrypoint.sh moet invullen.
Draaien: Draaien:
python tests/test_server_start_zonder_certificaat.py python tests/test_server_start_zonder_certificaat.py
Er staan inmiddels een paar toetsen bij die niet over het starten gaan maar wel Er staan een paar toetsen bij die niet over het starten gaan maar wel over
over dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
ziet. De verbindingsteller die stil nul telt, een mount die weer alleen-lezen wordt, ziet. De verbindingsteller die stil nul telt, een mount die weer alleen-lezen wordt,
en twee taglines die uit elkaar lopen. Ze staan hier en niet in een derde bestand, en twee taglines die uit elkaar lopen. Ze staan hier en niet in een derde bestand,
want de suite heeft geen runner en een bestand dat niemand aanroept toetst niets. want de suite heeft geen runner en een bestand dat niemand aanroept toetst niets.
@@ -31,15 +38,20 @@ import sys
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
import os # noqa: E402 import os # noqa: E402
import re # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate") APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate")
RECEPT = os.path.join(HERE, os.pardir, "tools", "electrum-gate")
NGINX_CONF = os.path.join(APP, "nginx.conf.template") NGINX_CONF = os.path.join(RECEPT, "nginx.conf")
STREAM_CONF = os.path.join(APP, "stream.conf.template") STREAM_CONF = os.path.join(RECEPT, "stream.conf")
ENTRYPOINT = os.path.join(RECEPT, "entrypoint.sh")
DOCKERFILE = os.path.join(RECEPT, "Dockerfile")
BUILD = os.path.join(RECEPT, "build.sh")
PAGINA = os.path.join(RECEPT, "index.html")
COMPOSE = os.path.join(APP, "docker-compose.yml") COMPOSE = os.path.join(APP, "docker-compose.yml")
MANIFEST = os.path.join(APP, "umbrel-app.yml") MANIFEST = os.path.join(APP, "umbrel-app.yml")
PAGINA = os.path.join(APP, "index.html.template")
class Uitslag: class Uitslag:
@@ -67,7 +79,7 @@ def lees(pad):
def zonder_commentaar(tekst): def zonder_commentaar(tekst):
"""De regels die nginx daadwerkelijk uitvoert. """De regels die nginx of de shell daadwerkelijk uitvoert.
Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die
noemt juist de dingen die er niet meer mogen staan. noemt juist de dingen die er niet meer mogen staan.
@@ -104,64 +116,71 @@ def test_pagina_hangt_niet_aan_een_certificaat(u):
def test_stream_blok_staat_apart(u): def test_stream_blok_staat_apart(u):
"""Het TLS-deel is compleet, en het is een template.""" """Het TLS-deel is compleet, en het backend-adres komt uit de omgeving."""
u.check("stream.conf.template bestaat", os.path.isfile(STREAM_CONF), u.check("stream.conf bestaat", os.path.isfile(STREAM_CONF))
"anders komt het bij een update niet mee: alleen *.template, "
"docker-compose.yml, exports.sh, torrc, hooks en umbrel-app.yml "
"worden ververst")
if not os.path.isfile(STREAM_CONF): if not os.path.isfile(STREAM_CONF):
return return
tekst = lees(STREAM_CONF) regels = zonder_commentaar(lees(STREAM_CONF))
regels = zonder_commentaar(tekst)
u.check("stream.conf luistert op 50022 met ssl", u.check("stream.conf luistert op 50022 met ssl",
any("listen 50022 ssl" in r for r in regels)) any("listen 50022 ssl" in r for r in regels))
u.check("stream.conf includet de cert.conf van de agent", u.check("stream.conf includet de cert.conf van de agent",
any("include /var/lib/gate/cert.conf" in r for r in regels)) any("include /var/lib/gate/cert.conf" in r for r in regels))
u.check("stream.conf includet het log_format", u.check("stream.conf heeft zijn log_format zelf",
any("include /var/lib/gate/stream-log.conf" in r for r in regels)) any(r.startswith("log_format gate") for r in regels),
u.check("stream.conf verwijst naar de backend uit de omgeving", "tot 0.0.29 schreef het command-blok dat in een los bestand, omdat "
any("proxy_pass ${APP_ELECTRS_NODE_IP}" in r for r in regels)) "een dollarteken in een template leeggemaakt werd; dat hoeft niet meer")
u.check("en schrijft de sessielog waar de agent hem leest",
any(r.startswith("access_log /var/lib/gate/stream.log") for r in regels))
# Dezelfde architectuurregel als voor de andere templates: umbreld vult elke # Het adres van de backend staat niet in dit bestand maar komt uit de
# accolade-variabele in, dus een nginx-variabele met een dollarteken wordt # omgeving, en de weg daarheen is een plaatshouder die entrypoint.sh invult.
# hier stil leeggemaakt. Vandaar dat het log_format uit een ander bestand # Geen dollarteken in die plaatshouder: dan kan geen envsubst-stap, van
# komt. # umbreld of van wie dan ook, hem stil leegmaken.
losse_dollars = [] proxy = [r for r in regels if r.startswith("proxy_pass")]
for regel in regels: u.check("stream.conf heeft precies één proxy_pass", len(proxy) == 1,
pos = regel.find("$") "gevonden: %r" % proxy)
while pos >= 0: u.check("en die verwijst naar de plaatshouders voor host en poort",
if regel[pos + 1:pos + 2] != "{": proxy and "__GATE_ELECTRUM_HOST__:__GATE_ELECTRUM_PORT__" in proxy[0],
losse_dollars.append(regel) "gevonden: %r" % proxy)
break
pos = regel.find("$", pos + 1) u.check("stream.conf bevat geen accolade-variabele meer",
u.check("stream.conf heeft geen nginx-variabelen", "${" not in lees(STREAM_CONF),
not losse_dollars, "die wordt sinds de eigen image door niemand meer ingevuld en zou "
"de template-invulling maakt die leeg: %r" % losse_dollars[:2]) "letterlijk in de configuratie belanden")
def test_compose_wacht_niet_op_de_agent(u): def test_entrypoint_wacht_niet_op_de_agent(u):
"""Het command-blok start nginx altijd, en zet TLS erbij als het kan.""" """entrypoint.sh start nginx altijd, en zet TLS erbij als het kan."""
tekst = lees(COMPOSE) u.check("entrypoint.sh bestaat", os.path.isfile(ENTRYPOINT))
if not os.path.isfile(ENTRYPOINT):
return
tekst = lees(ENTRYPOINT)
regels = zonder_commentaar(tekst) regels = zonder_commentaar(tekst)
u.check("geen wachtlus op cert.conf", u.check("geen wachtlus op cert.conf",
not any("! -f /var/lib/gate/cert.conf" in r and "while" in r not any("! -f" in r and "cert.conf" in r and "while" in r
for r in regels), for r in regels),
"dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, " "dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, "
"geen pagina, dus geen manier om een certificaat te kiezen") "geen pagina, dus geen manier om een certificaat te kiezen")
u.check("stream.conf is in de server gemount", u.check("het stream-blok wordt ingevuld uit de omgeving",
any("stream.conf:/etc/nginx/stream.conf" in r for r in regels), any("__GATE_ELECTRUM_HOST__" in r and "GATE_ELECTRUM_HOST" in r
"anders is er niets om in /var/lib/gate/tls/ te zetten") for r in regels)
and any("__GATE_ELECTRUM_PORT__" in r for r in regels),
"anders staat de plaatshouder letterlijk in de proxy_pass")
u.check("en wordt uit stream.conf.in gelezen, zoals de Dockerfile hem neerzet",
any("/etc/nginx/stream.conf.in" in r for r in regels))
u.check("het stream-blok wordt weggezet als cert.conf bestaat", u.check("het stream-blok wordt weggezet als cert.conf bestaat",
any("cp /etc/nginx/stream.conf /var/lib/gate/tls/" in r any(r.startswith("if cp /etc/nginx/stream.conf") and "/tls/" in r
for r in regels)) for r in regels))
u.check("en weggehaald als cert.conf er niet is", u.check("en weggehaald als cert.conf er niet is",
any("rm -f /var/lib/gate/tls/stream.conf" in r for r in regels), any(r.startswith("rm -f") and "/tls/stream.conf" in r for r in regels),
"een ingetrokken keuze moet de poort ook echt sluiten") "een ingetrokken keuze moet de poort ook echt sluiten")
u.check("een mislukte herlading breekt de wachtlus niet af", u.check("een mislukte herlading breekt de wachtlus niet af",
@@ -169,6 +188,119 @@ def test_compose_wacht_niet_op_de_agent(u):
"onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer " "onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer "
"een wijziging op") "een wijziging op")
u.check("nginx wordt met exec het hoofdproces",
regels and regels[-1].startswith("exec nginx"),
"anders blijft de shell PID 1, geeft signalen niet door, en ziet "
"Docker een geslaagde afsluiting als nginx omvalt; laatste regel: %r"
% (regels[-1] if regels else None))
u.check("het script begint met een shebang",
tekst.startswith("#!/bin/sh"))
u.check("geen verdubbelde dollartekens meer",
"$$" not in "\n".join(regels),
"dat was de ontsnapping voor het command-blok in de compose; in een "
"gewoon script is $$ het proces-id")
def test_dockerfile_neemt_alles_mee(u):
"""Wat entrypoint.sh en nginx.conf verwachten, zet de Dockerfile ook neer.
Dit is de nieuwe klasse fouten van de eigen image: een bestand dat in de
repo staat maar niet in de image, en dan faalt de container pas op het
apparaat. Geen Docker nodig om dat te zien; de paden staan in de tekst.
"""
u.check("Dockerfile bestaat", os.path.isfile(DOCKERFILE))
if not os.path.isfile(DOCKERFILE):
return
regels = zonder_commentaar(lees(DOCKERFILE))
copies = [r for r in regels if r.startswith("COPY ")]
def kopieert(bron, doel):
return any(bron in r and r.endswith(" " + doel) for r in copies)
u.check("nginx.conf gaat naar /etc/nginx/nginx.conf",
kopieert("tools/electrum-gate/nginx.conf", "/etc/nginx/nginx.conf"))
u.check("stream.conf gaat naar /etc/nginx/stream.conf.in",
kopieert("tools/electrum-gate/stream.conf", "/etc/nginx/stream.conf.in"),
"entrypoint.sh leest dat pad en vult het in naar stream.conf")
u.check("de pagina gaat naar de webroot",
kopieert("tools/electrum-gate/index.html", "/usr/share/nginx/html/index.html"))
u.check("het icoon komt uit de app-map",
kopieert("whatsnext-electrum-gate/icon.png", "/usr/share/nginx/html/icon.png"),
"de app-map houdt het origineel omdat het manifest ernaar wijst; "
"de image kopieert het, zodat er niet twee exemplaren zijn")
u.check("de agent gaat naar /app/agent.py",
kopieert("tools/electrum-gate/agent.py", "/app/agent.py"),
"de compose start hem op dat pad")
u.check("entrypoint.sh gaat mee en wordt uitvoerbaar",
kopieert("tools/electrum-gate/entrypoint.sh", "/entrypoint.sh")
and any(r.startswith("RUN chmod") and "/entrypoint.sh" in r for r in regels),
"git op Windows kent geen uitvoerbit, dus die moet de Dockerfile zetten")
u.check("python3 wordt geïnstalleerd",
any(r.startswith("RUN apk add") and "python3" in r for r in regels))
u.check("de basisimage is nginx op een vaste tag",
any(re.match(r"FROM nginx:\d+\.\d+-alpine$", r) for r in regels),
"nginx:alpine beweegt mee en is ook mét digest niet toegestaan; "
"zie Docs/Referenties/Images-pinnen.md")
u.check("het entrypoint van de nginx-image is uitgeschakeld",
"ENTRYPOINT []" in regels,
"anders draait het envsubst-script van die image mee, en dat is "
"precies het soort stille invulling dat dit project vreest")
# De compose noemt paden in de image; die moeten er zijn.
compose = zonder_commentaar(lees(COMPOSE))
u.check("de compose start de agent op het pad waar de Dockerfile hem neerzet",
any(r == "- /app/agent.py" for r in compose))
def test_compose_en_recept_lopen_gelijk(u):
"""Dezelfde image in beide services, met de tag uit build.sh.
Twee services die elk hun eigen tag hebben lopen uit elkaar bij de eerste
verhoging die iemand op één plek doet. En een tag die niet gelijk is aan
VERSION in build.sh betekent een image die nooit gebouwd is.
"""
compose = zonder_commentaar(lees(COMPOSE))
images = [r.split(":", 1)[1].strip() for r in compose if r.startswith("image:")]
u.check("de compose heeft twee images", len(images) == 2, "gevonden: %r" % images)
tags = set()
for image in images:
zonder_digest = image.split("@", 1)[0]
tags.add(zonder_digest)
u.check("en het is twee keer dezelfde", len(tags) == 1, "gevonden: %r" % sorted(tags))
versie = None
for regel in zonder_commentaar(lees(BUILD)):
if regel.startswith("VERSION="):
versie = regel.split("=", 1)[1].strip().strip('"')
u.check("build.sh heeft een VERSION", bool(versie))
if versie and tags:
tag = sorted(tags)[0]
u.check("de tag in de compose is VERSION uit build.sh",
tag.endswith(":" + versie),
"compose %r, build.sh %r" % (tag, versie))
# Wat er niet meer gemount mag worden: de programmabestanden. Zit er weer
# een mount van een los bestand uit de app-map in, dan is dat een terugval
# naar de template-tijd en bereikt een wijziging het apparaat via twee wegen.
losse = [r for r in compose if r.startswith("- ${APP_DATA_DIR}/")
and "/" not in r[len("- ${APP_DATA_DIR}/"):].split(":", 1)[0]]
u.check("geen losse bestanden uit de app-map meer gemount",
not losse, "gevonden: %r" % losse)
u.check("de server krijgt het backend-adres uit de omgeving",
compose.count("GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}") == 2
and compose.count("GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}") == 2,
"agent én server hebben het nodig; entrypoint.sh vult er het "
"stream-blok mee in")
u.check("de agent krijgt de versie van het manifest door",
"GATE_APP_VERSION: ${APP_VERSION}" in compose,
"anders toont de kop van de pagina geen versie")
def test_uploadpad(u): def test_uploadpad(u):
"""De drie dingen buiten de agent die uploaden mogelijk maken. """De drie dingen buiten de agent die uploaden mogelijk maken.
@@ -208,13 +340,12 @@ def test_uploadpad(u):
def test_verbindingsteller(u): def test_verbindingsteller(u):
"""De teller in de nginx-container, en de poort in hex. """De teller in de nginx-container, en de poort in hex.
Hoort hier omdat het om hetzelfde `command`-blok gaat. Het gevaarlijke deel Het gevaarlijke deel is de hex: de teller zoekt in /proc/net/tcp op de poort
is de hex: de teller zoekt in /proc/net/tcp op de poort in hexadecimale vorm, in hexadecimale vorm, en verandert de TLS-poort ooit, dan telt hij stil nul.
en verandert de TLS-poort ooit, dan telt hij stil nul. Een teller die altijd Een teller die altijd nul zegt, ziet eruit als "geen wallet verbonden" en
nul zegt, ziet eruit als "geen wallet verbonden" en niet als een fout. niet als een fout.
""" """
tekst = lees(COMPOSE) regels = zonder_commentaar(lees(ENTRYPOINT))
regels = zonder_commentaar(tekst)
teller = [r for r in regels if "/proc/net/tcp" in r] teller = [r for r in regels if "/proc/net/tcp" in r]
u.check("de teller leest /proc/net/tcp", bool(teller), u.check("de teller leest /proc/net/tcp", bool(teller),
@@ -222,11 +353,12 @@ def test_verbindingsteller(u):
"staan; de agent zit in een andere netwerk-namespace") "staan; de agent zit in een andere netwerk-namespace")
u.check("en schrijft het aantal weg voor de agent", u.check("en schrijft het aantal weg voor de agent",
any("/var/lib/gate/sessions" in r for r in regels)) any("/sessions" in r for r in regels))
# De poort uit de omgeving van de agent is de waarheid; de teller moet # De poort uit de omgeving van de agent is de waarheid; de teller moet
# dezelfde poort in hex zoeken. # dezelfde poort in hex zoeken.
poorten = [r for r in regels if r.startswith("GATE_TLS_PORT:")] compose = zonder_commentaar(lees(COMPOSE))
poorten = [r for r in compose if r.startswith("GATE_TLS_PORT:")]
u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1, u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1,
"gevonden: %r" % poorten) "gevonden: %r" % poorten)
if not poorten or not teller: if not poorten or not teller:
@@ -238,9 +370,13 @@ def test_verbindingsteller(u):
# backslash-vervolg en dan valt de hex op een eigen regel. # backslash-vervolg en dan valt de hex op een eigen regel.
u.check("de teller zoekt de TLS-poort in hex (%d is %s)" % (poort, hexpoort), u.check("de teller zoekt de TLS-poort in hex (%d is %s)" % (poort, hexpoort),
(":" + hexpoort) in " ".join(regels), (":" + hexpoort) in " ".join(regels),
"nergens in het command-blok staat :%s, dus telt de teller nul" "nergens in entrypoint.sh staat :%s, dus telt de teller nul"
% hexpoort) % hexpoort)
stream = zonder_commentaar(lees(STREAM_CONF))
u.check("en stream.conf luistert op diezelfde poort",
any(r == "listen %d ssl;" % poort for r in stream))
def test_data_onder_data(u): def test_data_onder_data(u):
"""Alles wat de app zelf schrijft staat onder `data/`. """Alles wat de app zelf schrijft staat onder `data/`.
@@ -262,14 +398,9 @@ def test_data_onder_data(u):
fout = [] fout = []
for mount in mounts: for mount in mounts:
host = mount.strip('"').split(":")[0].replace("${APP_DATA_DIR}/", "") host = mount.strip('"').split(":")[0].replace("${APP_DATA_DIR}/", "")
# Een bestand naast de compose mag: dat zijn de ingevulde templates en het
# icoon, en die hóren daar omdat de whitelist ze daar verft. Een map die de
# app zelf vult, hoort onder data/.
if "." in os.path.basename(host):
continue
if not host.startswith("data/"): if not host.startswith("data/"):
fout.append(host) fout.append(host)
u.check("elke map die de app zelf vult staat onder data/", u.check("elke mount uit de app-map staat onder data/",
not fout, "deze niet: %r" % fout) not fout, "deze niet: %r" % fout)
u.check("de gedeelde toestand staat onder data/", u.check("de gedeelde toestand staat onder data/",
@@ -382,17 +513,35 @@ def test_tagline_is_overal_dezelfde(u):
"de pagina noemt hem niet: %r" % tagline) "de pagina noemt hem niet: %r" % tagline)
def test_pagina_zonder_invulling(u):
"""De pagina zit in de image en wordt door niemand meer ingevuld.
Een ${...} die blijft staan komt letterlijk in de browser terecht. En de twee
waarden die umbreld vroeger invulde, de versie en het backend-adres, moeten
nu uit status.json komen; anders is de kop leeg en het adres ook.
"""
tekst = lees(PAGINA)
u.check("de pagina bevat geen accolade-variabele", "${" not in tekst)
u.check("de kop haalt de versie uit status.json",
"s.version" in tekst and 'id="head-version"' in tekst)
u.check("het backend-adres komt uit status.json",
"backend.address" in tekst)
def main(): def main():
u = Uitslag() u = Uitslag()
test_pagina_hangt_niet_aan_een_certificaat(u) test_pagina_hangt_niet_aan_een_certificaat(u)
test_stream_blok_staat_apart(u) test_stream_blok_staat_apart(u)
test_compose_wacht_niet_op_de_agent(u) test_entrypoint_wacht_niet_op_de_agent(u)
test_dockerfile_neemt_alles_mee(u)
test_compose_en_recept_lopen_gelijk(u)
test_verbindingsteller(u) test_verbindingsteller(u)
test_uploadpad(u) test_uploadpad(u)
test_data_onder_data(u) test_data_onder_data(u)
test_manifest_volgorde(u) test_manifest_volgorde(u)
test_releasenotes_hebben_een_geschiedenis(u) test_releasenotes_hebben_een_geschiedenis(u)
test_tagline_is_overal_dezelfde(u) test_tagline_is_overal_dezelfde(u)
test_pagina_zonder_invulling(u)
return u.rapport() return u.rapport()
+54
View File
@@ -0,0 +1,54 @@
# ═══════════════════════════════════════════════════════════════════════════════
# De image voor de app whatsnext-electrum-gate.
#
# Eén image voor twee containers. De compose start hem twee keer: als `server`
# met het standaardcommando (entrypoint.sh, dat nginx wordt) en als `agent` met
# `python3 /app/agent.py`. Twee images was netter gescheiden geweest, maar het
# verdubbelt het bouw- en pinwerk, en nginx plus python3 in één alpine is klein.
# Beslist bij de start van het plan Eigenimage, OPEN.md punt 2.
#
# Tot 0.0.29 stonden deze bestanden als *.template in de app-map en draaiden ze
# op python:3-alpine en nginx:alpine. Wat dat kostte staat in het plan
# Eigenimage, PLAN.md §3; het kortste antwoord: het command-blok van de compose
# was ruim honderd regels shell in een YAML-string met verdubbelde dollartekens,
# en dat is nu een gewoon script.
#
# De bouwcontext is de REPO-ROOT, niet deze map. Dat is omdat icon.png in de
# app-map blijft staan (het manifest wijst ernaar voor de tegel in de winkel) en
# de pagina hem óók nodig heeft; kopiëren uit de app-map voorkomt een tweede
# exemplaar dat stil achterloopt. build.sh geeft die context mee.
# ═══════════════════════════════════════════════════════════════════════════════
# nginx en niet python als basis: de officiële nginx-image heeft de stream-module
# ingebouwd, en op alpine's eigen nginx-pakket is dat een los pakket. De app draaide
# al op nginx:alpine, dus dit houdt nginx precies zoals hij was. De tag is de
# stable-reeks (even tweede cijfer); zie Docs/Referenties/Images-pinnen.md.
FROM nginx:1.30-alpine
# De agent gebruikt alleen de standaardbibliotheek, inclusief ssl; dat zit in het
# python3-pakket van alpine.
RUN apk add --no-cache python3
COPY tools/electrum-gate/nginx.conf /etc/nginx/nginx.conf
# Met .in erachter, want dit is nog niet de configuratie die nginx leest: het
# backend-adres komt uit de omgeving en entrypoint.sh vult het bij het starten in.
COPY tools/electrum-gate/stream.conf /etc/nginx/stream.conf.in
COPY tools/electrum-gate/index.html /usr/share/nginx/html/index.html
COPY whatsnext-electrum-gate/icon.png /usr/share/nginx/html/icon.png
COPY tools/electrum-gate/agent.py /app/agent.py
COPY tools/electrum-gate/entrypoint.sh /entrypoint.sh
# De uitvoerbit hier zetten en niet vertrouwen op wat git meegeeft: de repo wordt
# ook op Windows bewerkt en daar bestaat die bit niet.
RUN chmod 0755 /entrypoint.sh
ENV PYTHONUNBUFFERED=1
EXPOSE 80 50022
# De nginx-image heeft een eigen entrypoint dat scripts uit /docker-entrypoint.d
# draait. Die doen hier niets nuttigs (envsubst op een templates-map die leeg is)
# en één ervan zou, als hij ooit wél iets deed, precies de fout maken waar dit
# project al een dag aan kwijt was. Weg ermee: het commando is het commando.
ENTRYPOINT []
CMD ["/entrypoint.sh"]
@@ -1,17 +1,14 @@
"""Electrum Gate agent: schrijft status.json en neemt de certificaatkeuze aan. """Electrum Gate agent: schrijft status.json en neemt de certificaatkeuze aan.
Waarom dit bestand een .template is en er toch geen accolade-variabelen in staan. Dit bestand zit in de image (tools/electrum-gate/Dockerfile) als /app/agent.py.
umbreld ververst bij een update alleen een whitelist van bestanden en *.template Tot 0.0.29 was het een *.template in de app-map, en omdat umbreld elke
staat daarin, dus een gewoon script naast de compose zou na een update ongewijzigd accolade-variabele in zo'n bestand invult, haalt dit script zijn instellingen uit
blijven staan zonder dat er iets gemeld wordt. Maar umbreld vult bij elke start de omgeving en niet uit een ingevulde constante. Dat is zo gebleven: de compose
ook elke accolade-variabele in, en die zou hier Python-code slopen. Daarom haalt zet ze in de omgeving neer, en dat is ook de nette weg.
dit script zijn instellingen uit de omgeving, en zet de compose ze daar neer. Het
ingevulde agent.py is daardoor byte voor byte gelijk aan dit bestand, en dat is
precies wat de test in tests/ erover aanneemt.
Alleen de standaardbibliotheek. De app draait op een kant-en-klare python-image Alleen de standaardbibliotheek. De image installeert python3 uit alpine en verder
en installeert bij het starten niets; dat was een van de dingen die de vorige niets, en dat hoort zo te blijven: de vorige opzet installeerde bij het starten en
opzet fout deed. dat was een van de dingen die fout gingen.
""" """
import calendar import calendar
@@ -47,6 +44,12 @@ ELECTRUM_HOST = os.environ.get("GATE_ELECTRUM_HOST", "")
ELECTRUM_PORT = int(os.environ.get("GATE_ELECTRUM_PORT", "50001")) ELECTRUM_PORT = int(os.environ.get("GATE_ELECTRUM_PORT", "50001"))
TLS_PORT = int(os.environ.get("GATE_TLS_PORT", "50022")) TLS_PORT = int(os.environ.get("GATE_TLS_PORT", "50022"))
# De versie uit het manifest, voor de kop van de pagina. Tot 0.0.29 vulde umbreld
# die rechtstreeks in de pagina in; nu de pagina in de image zit, loopt het via
# status.json. Leeg betekent dat de compose hem niet doorgeeft, en dan toont de
# pagina geen versie in plaats van een verzonnen.
APP_VERSION = os.environ.get("GATE_APP_VERSION", "")
# De zelfcontrole: de agent verbindt met de eigen TLS-poort. Zie het plan # De zelfcontrole: de agent verbindt met de eigen TLS-poort. Zie het plan
# Webinterface, OPEN.md punt 5. # Webinterface, OPEN.md punt 5.
# #
@@ -1069,6 +1072,7 @@ def build_status(events):
status = { status = {
"generated_at": now, "generated_at": now,
"interval_seconds": INTERVAL, "interval_seconds": INTERVAL,
"version": APP_VERSION or None,
# open_connections blijft weg als het niet bekend is. Zie # open_connections blijft weg als het niet bekend is. Zie
# read_open_sessions: nul beweren is hier erger dan zwijgen. # read_open_sessions: nul beweren is hier erger dan zwijgen.
"tls": {"port": TLS_PORT, "self_check": self_check}, "tls": {"port": TLS_PORT, "self_check": self_check},
+111
View File
@@ -0,0 +1,111 @@
#!/bin/sh
# ═══════════════════════════════════════════════════════════════════════════════
# Bouwt de image voor de app whatsnext-electrum-gate.
#
# Eén image voor de twee containers van de app: nginx met de pagina en het
# stream-blok, en python3 met de agent. Wat erin gaat staat in de Dockerfile
# hiernaast; de bestanden zelf staan in deze map, behalve icon.png, dat in de
# app-map blijft omdat het manifest ernaar wijst.
#
# Waarom dit NIET in de app-map staat: umbreld kopieert de hele app-map naar het
# apparaat, en bij een update wordt alleen een whitelist ververst waar een
# Dockerfile niet in zit. Bouwen tijdens het starten van de app zou de installatie
# bovendien minuten laten hangen. Het recept hoort dus in de repo en het resultaat
# in een register; de app verwijst alleen naar de tag.
#
# Draaien: sh tools/electrum-gate/build.sh
# Vereist: docker op de machine waar je bouwt. Op de Umbrel is dat met sudo.
#
# Multi-arch: PLATFORMS="linux/amd64,linux/arm64" sh tools/electrum-gate/build.sh
# Dat vraagt buildx met QEMU en duwt meteen naar het register, want een
# multi-arch-manifest bestaat niet lokaal. Nog nooit geprobeerd; zie het plan
# Eigenimage, PLAN.md §4. Zonder PLATFORMS bouwt dit één architectuur, die van
# de machine waarop je staat, en dat is voor eigen gebruik op een amd64-Umbrel
# genoeg.
# ═══════════════════════════════════════════════════════════════════════════════
set -eu
# ── De pin ────────────────────────────────────────────────────────────────────
# Dit is het etiket op de IMAGE. Verhoog het als de image verandert, dus als er
# iets in deze map of in icon.png wijzigt, en zet dan dezelfde waarde achter
# `image:` in de compose (twee keer: agent en server delen hem).
#
# `version` in umbrel-app.yml is een ander nummer en mag hierop vooruitlopen: dat
# moet bij élke wijziging aan de app omhoog, ook als alleen de compose verandert,
# want anders rolt umbrelOS hem niet uit. Dat heeft hier een keer een dag gekost.
# Sinds de eigen image raakt vrijwel elke wijziging de image, dus ze lopen
# meestal gelijk; de regel blijft: verhoog je VERSION, dan ook het manifest.
VERSION="0.1.0"
# Het register staat er expres in en dit is geen smaakkwestie: umbreld haalt élke
# image op via de Docker Engine API, dus een tag die alleen lokaal bestaat is voor
# hem onbereikbaar en de installatie faalt met "pull access denied". Dat is op
# 25-08-2026 bij Evolu Relay op het apparaat vastgesteld.
IMAGE="sc.kamenier-hamer.nl/sysop/electrum-gate:${VERSION}"
RECEPT="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
# De bouwcontext is de repo-root, want icon.png komt uit de app-map. Zie de kop
# van de Dockerfile.
REPO="$(CDPATH= cd -- "${RECEPT}/../.." && pwd)"
# ── Controles vooraf ──────────────────────────────────────────────────────────
# Beter hier hard falen dan een image bouwen die iets anders bevat dan je denkt.
for bestand in Dockerfile entrypoint.sh agent.py nginx.conf stream.conf index.html; do
if [ ! -f "${RECEPT}/${bestand}" ]; then
echo "FOUT: ${bestand} ontbreekt in ${RECEPT}" >&2
exit 1
fi
done
if [ ! -f "${REPO}/whatsnext-electrum-gate/icon.png" ]; then
echo "FOUT: whatsnext-electrum-gate/icon.png ontbreekt in ${REPO}" >&2
exit 1
fi
# Een accolade-variabele hoort hier nergens meer in: deze bestanden gaan niet
# meer door envsubst, dus wat er staat is wat er draait. Staat er tóch een, dan
# is dat een restant van de template-tijd en zou hij letterlijk in de pagina of
# de configuratie terechtkomen.
if grep -l '\${' "${RECEPT}/nginx.conf" "${RECEPT}/stream.conf" "${RECEPT}/index.html" 2>/dev/null; then
echo "FOUT: de bestanden hierboven bevatten nog een \${...}; die wordt niet meer ingevuld." >&2
exit 1
fi
# ── Bouwen ────────────────────────────────────────────────────────────────────
PLATFORMS="${PLATFORMS:-}"
if [ -n "$PLATFORMS" ]; then
echo "Bouwen als ${IMAGE} voor ${PLATFORMS}, en meteen duwen"
docker buildx build --platform "$PLATFORMS" --tag "$IMAGE" --push \
--file "${RECEPT}/Dockerfile" "$REPO"
echo
echo "Geduwd. De index-digest:"
docker buildx imagetools inspect "$IMAGE" | head -3
else
echo "Bouwen als ${IMAGE}"
docker build --tag "$IMAGE" --file "${RECEPT}/Dockerfile" "$REPO"
echo
echo "Klaar:"
docker image inspect --format '{{.RepoTags}} {{.Id}}' "$IMAGE"
echo
echo "Bouwen is niet genoeg: de app verwijst naar het register, want umbreld kan"
echo "niet bij een image die alleen lokaal staat. Nog te doen:"
echo
echo " docker login sc.kamenier-hamer.nl"
echo " docker push ${IMAGE}"
fi
echo
echo "Zet daarna de digest uit de push-uitvoer achter de tag in"
echo "whatsnext-electrum-gate/docker-compose.yml (twee keer, agent en server), en"
echo "zet \`version\` in het manifest op ${VERSION}. Zonder die verhoging rolt"
echo "umbrelOS het niet uit."
echo
echo "Controleer dat het anoniem te halen is, want umbreld krijgt geen"
echo "inloggegevens mee. Log uit in dezelfde context waarin je inlogde, anders"
echo "meet je je eigen sessie:"
echo
echo " docker logout sc.kamenier-hamer.nl && docker pull ${IMAGE}"
+141
View File
@@ -0,0 +1,141 @@
#!/bin/sh
# ═══════════════════════════════════════════════════════════════════════════════
# Het startscript van de server-container van Electrum Gate.
#
# Tot 0.0.29 stond dit als inline command-blok in docker-compose.yml, omdat de
# compose in de update-whitelist van umbreld staat en een los script niet. Dat
# betekende shell in een YAML-string, met elk dollarteken verdubbeld omdat
# umbreld anders elke ${...} leegmaakt. Sinds de eigen image is het een gewoon
# bestand; wat het doet is niet veranderd.
#
# Drie dingen die hier gebeuren en waarom ze hier gebeuren en niet in de agent:
#
# 1. de TLS-poort aan- of uitzetten naar de toestand van cert.conf. nginx weigert
# te starten met een 'listen ssl' zonder certificaat, en de pagina waarop je
# dat certificaat kiest moet juist altijd omhoog komen;
# 2. de open verbindingen tellen. Dat kan alleen in deze netwerk-namespace;
# 3. nginx herladen als de agent een vlag neerzet. De agent kan dat niet zelf,
# want dat zou de Docker-socket vragen en die is er bewust uit.
# ═══════════════════════════════════════════════════════════════════════════════
set -eu
STATE=/var/lib/gate
mkdir -p "$STATE" "$STATE/tls"
# ── Het stream-blok invullen ──────────────────────────────────────────────────
# Het backend-adres komt uit de omgeving, dezelfde twee variabelen die de agent
# krijgt. Tot 0.0.29 vulde umbreld ze in via stream.conf.template; nu de
# configuratie in de image zit, gebeurt dat hier. nginx kan in een proxy_pass van
# het stream-blok zelf geen omgevingsvariabele lezen.
#
# Twee plaatshouders zonder dollarteken, zodat het bestand zelf nooit door een
# envsubst-stap beschadigd kan worden, ook niet als iemand het ooit weer als
# template in de app-map zet.
if [ -n "${GATE_ELECTRUM_HOST:-}" ] && [ -n "${GATE_ELECTRUM_PORT:-}" ]; then
sed -e "s|__GATE_ELECTRUM_HOST__|${GATE_ELECTRUM_HOST}|g" \
-e "s|__GATE_ELECTRUM_PORT__|${GATE_ELECTRUM_PORT}|g" \
/etc/nginx/stream.conf.in > /etc/nginx/stream.conf
else
# Zonder backend is er niets om naar door te sturen. De pagina komt wel omhoog
# en de agent meldt daar dat het adres ontbreekt; TLS blijft uit.
rm -f /etc/nginx/stream.conf
echo "GATE_ELECTRUM_HOST of GATE_ELECTRUM_PORT ontbreekt; TLS blijft uit, de pagina werkt."
fi
# ── De TLS-poort aan of uit ───────────────────────────────────────────────────
# Hier zat tot 0.0.3 een lus die wachtte tot de agent een certificaat gekozen had,
# en dat was fout: nginx startte dan niet, dus de pagina kwam niet omhoog, en de
# pagina is juist waar je dat certificaat kiest.
#
# Nu start nginx altijd. Het stream-blok komt erbij zodra cert.conf er is, en
# verdwijnt weer als de agent zijn keuze intrekt. nginx.conf haalt de map tls/ op
# met een jokerteken; die matcht dan niets en dat is geen fout.
#
# Deze functie eindigt altijd geslaagd, en dat is met opzet: het script draait
# onder 'set -e', dus een mislukte cp zou de aanroeper afbreken. In de lus
# hieronder is die aanroeper de wachtlus, en die mag om geen enkele reden stoppen.
sync_tls() {
if [ -f "$STATE/cert.conf" ] && [ -f /etc/nginx/stream.conf ]; then
# Kopiëren en niet linken: nginx opent dit pad als de gebruiker nginx, en een
# symlink is hier nodeloos fragiel.
if cp /etc/nginx/stream.conf "$STATE/tls/stream.conf"; then
echo "Certificaat aanwezig, TLS luistert op 50022."
else
echo "stream.conf kon niet worden weggezet; TLS blijft uit."
fi
else
rm -f "$STATE/tls/stream.conf"
echo "Nog geen certificaat gekozen; TLS staat uit, de pagina werkt."
fi
}
sync_tls
# ── De verbindingsteller ──────────────────────────────────────────────────────
# nginx stream schrijft zijn logregel pas bij het sluiten van een sessie, en een
# wallet houdt zijn verbinding uren open; zonder deze teller lijkt een actieve
# wallet dus afwezig. Dat was de vraag van de gebruiker op 20-08-2026.
#
# /proc/net/tcp geldt per netwerk-namespace, dus dit ziet alleen de sockets van
# deze container, en daarom staat de teller hier en niet in de agent: die zit in
# een andere namespace en kan er niet bij. Kolom 2 is het lokale adres met de
# poort in hex, kolom 4 de toestand. C366 is 50022 en 01 is ESTABLISHED, dus de
# luisterende socket (0A) en de verbindingen naar de backend en naar de pagina
# vallen er buiten.
#
# Geteld wordt alleen het aantal. Geen adres en geen bronpoort, net als in het
# log_format in stream.conf.
count_sessions() {
cat /proc/net/tcp /proc/net/tcp6 2>/dev/null \
| awk '$4 == "01" && $2 ~ /:C366$/ { n++ } END { print n+0 }'
}
# Meteen een 0 neerzetten, zodat de pagina "geen verbindingen" kan tonen in
# plaats van "onbekend" in het minuutje voor de eerste ronde.
count_sessions > "$STATE/sessions"
# ── De herlaadlus ─────────────────────────────────────────────────────────────
# Draait op de achtergrond; nginx wordt hieronder met exec het hoofdproces.
# Andersom gaat twee keer mis: de shell blijft dan PID 1 en geeft signalen niet
# door, en als nginx omvalt eindigt het script met exitcode 0, waardoor Docker een
# geslaagde afsluiting ziet en 'restart: on-failure' niet ingrijpt.
reload_nginx() {
# Een certificaat dat nginx niet aanneemt laat de reload falen. Dat mag niet
# met 'set -e' meegaan, want dan verdwijnt de lus en wordt geen enkele latere
# wijziging meer opgepikt, zonder dat er iets te zien is.
if ! nginx -s reload; then
echo "Herladen mislukt; nginx houdt de vorige configuratie."
fi
}
(
while true; do
sleep 10
# Eerst schrijven, dan mv: de agent leest dit bestand op zijn eigen moment en
# mag geen half bestand zien.
count_sessions > "$STATE/sessions.tmp"
mv "$STATE/sessions.tmp" "$STATE/sessions"
if [ -f "$STATE/reload" ]; then
echo "Certificaat gewijzigd, nginx herladen ..."
# Eerst wegzetten, dan herladen. Andersom zou een herlading die mislukt de
# vlag toch opruimen, en dan probeert hij het nooit meer.
mv "$STATE/reload" "$STATE/reload.done"
# Voor de herlading, niet erna: een reload leest de configuratie opnieuw,
# dus het stream-blok moet er dan al liggen.
sync_tls
reload_nginx
elif [ -f "$STATE/cert.conf" ] && [ ! -f "$STATE/tls/stream.conf" ]; then
# De vangnetregel. De agent zet de vlag bij elke wijziging, dus normaal komt
# hier niets langs; wel als de vlag verloren gaat of als cert.conf van een
# vorige installatie al klaarlag. Zonder deze tak zou TLS dan uit blijven
# staan tot de volgende wijziging.
echo "Certificaat gevonden zonder herlaadvlag, TLS aanzetten ..."
sync_tls
reload_nginx
fi
done
) &
exec nginx -g 'daemon off;'
@@ -2,20 +2,17 @@
<!-- <!--
Statuspagina van Electrum Gate. Statuspagina van Electrum Gate.
Dit bestand is een template en dat is bewust. umbreld ververst bij een update Dit bestand zit in de image (tools/electrum-gate/Dockerfile) als
alleen een whitelist, en *.template staat daarin; een pagina onder web/ zou na /usr/share/nginx/html/index.html. Tot 0.0.29 was het een *.template in de
een update ongewijzigd blijven staan zonder dat er iets gemeld wordt. app-map die umbreld bij elke start door envsubst haalde, en daarom staat er in
het script hieronder nergens een template-literal maar overal string-optelling.
Die beperking is weg, maar de stijl is gebleven: één pagina in twee stijlen is
erger dan één stijl die niet meer nodig is.
Twee regels die daaruit volgen en die bij het wijzigen tellen: Wat de pagina weet, komt uit status.json, dat de agent wegschrijft. Ook de
versie en het backend-adres, die umbreld vroeger rechtstreeks invulde. Weet de
1. gebruik geen accolade-variabelen behalve de umbrel-variabelen die hier pagina iets niet, dan zegt hij dat. Hij verzint niets, want hij wordt juist
werkelijk ingevuld moeten worden. De invulling vervangt elke accolade-vorm geraadpleegd op het moment dat er iets mis lijkt te zijn.
en zou de rest stilzwijgend leegmaken. Daarom staat er in het script
hieronder nergens een template-literal maar overal string-optelling;
2. wat de pagina niet uit een umbrel-variabele kan halen, komt uit status.json
dat de achtergrondlus in docker-compose.yml wegschrijft. Weet de pagina iets
niet, dan zegt hij dat. Hij verzint niets, want hij wordt juist geraadpleegd
op het moment dat er iets mis lijkt te zijn.
--> -->
<!-- data-glow schakelt de accentgloed achter de pagina. "off" is de stand sinds <!-- data-glow schakelt de accentgloed achter de pagina. "off" is de stand sinds
0.0.29 (verzoek van de gebruiker, 04-09-2026); "on" zet hem terug, en dat is 0.0.29 (verzoek van de gebruiker, 04-09-2026); "on" zet hem terug, en dat is
@@ -848,7 +845,7 @@ body {
Wijzig je de een, wijzig dan de ander: Wijzig je de een, wijzig dan de ander:
tests/test_server_start_zonder_certificaat.py houdt ze gelijk. --> tests/test_server_start_zonder_certificaat.py houdt ze gelijk. -->
<p class="t-body-sm">Your own node from anywhere, without waiting for Tor <span class="head-version">v${APP_VERSION}</span></p> <p class="t-body-sm">Your own node from anywhere, without waiting for Tor <span class="head-version" id="head-version"></span></p>
</div> </div>
<!-- Het menu rechtsboven. Hier stond een losse knop voor licht en donker; die <!-- Het menu rechtsboven. Hier stond een losse knop voor licht en donker; die
is er op verzoek van de gebruiker (27-08-2026) in gegaan, samen met het is er op verzoek van de gebruiker (27-08-2026) in gegaan, samen met het
@@ -985,7 +982,7 @@ body {
regels die hier stonden zijn weg: "Chosen in the umbrelOS settings" regels die hier stonden zijn weg: "Chosen in the umbrelOS settings"
was uitleg bij een instelling die je hier toch niet wijzigt, en "Last was uitleg bij een instelling die je hier toch niet wijzigt, en "Last
checked" staat als "as of ... ago" onder de reactietijd. --> checked" staat als "as of ... ago" onder de reactietijd. -->
<div class="stat-sub mono" id="backend-address">${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}</div> <div class="stat-sub mono" id="backend-address"></div>
</div> </div>
</div> </div>
@@ -1528,6 +1525,10 @@ body {
put('backend-name', backend.name); put('backend-name', backend.name);
if (backend.address) { put('backend-address', backend.address); } if (backend.address) { put('backend-address', backend.address); }
// De versie in de kop. Geen put(): die zet "unknown" neer als het ontbreekt,
// en een kop met "vunknown" is erger dan een kop zonder versie.
if (s.version) { el('head-version').textContent = 'v' + s.version; }
drawSelfCheck(tls.self_check); drawSelfCheck(tls.self_check);
// Hier stond een badge met "answered 7 seconds ago". Die is eruit op verzoek // Hier stond een badge met "answered 7 seconds ago". Die is eruit op verzoek
@@ -1,16 +1,11 @@
# nginx-configuratie voor Electrum Gate. # nginx-configuratie voor Electrum Gate.
# #
# Dit bestand is een template en dat is bewust. umbrelOS ververst bij een update # Dit bestand zit in de image (tools/electrum-gate/Dockerfile) als
# alleen een whitelist van bestanden, en *.template staat daarin; een gewoon # /etc/nginx/nginx.conf. Tot 0.0.29 was het een *.template in de app-map die
# bestand naast de compose zou na een update ongewijzigd blijven staan. Daarnaast # umbreld bij elke start door envsubst haalde, en daarom mocht er toen geen
# vult umbrelOS bij elke start de omgevingsvariabelen hieronder in en schrijft het # nginx-variabele met een dollarteken in staan. Die beperking is weg. Dat de
# resultaat naast dit bestand weg als nginx.conf. # access_log voor http nog steeds uit staat is een keuze en geen restant: de
# # pagina hangt achter de app_proxy en wat die opvraagt is niet interessant.
# Let op bij het wijzigen: gebruik hier geen nginx-variabelen met een dollarteken
# (zoals de gebruikelijke in een log_format). De invulling vervangt elke ${...}
# en zou die stilzwijgend leegmaken. Daarom staat access_log voor http uit, en
# wordt het log_format van de stream-sessies door het command-blok in
# docker-compose.yml weggeschreven; dáár is een dollarteken te ontsnappen.
user nginx; user nginx;
worker_processes auto; worker_processes auto;
@@ -53,9 +48,7 @@ http {
# heeft. Het ergste wat een geslaagde aanroep doet is een ander, ook # heeft. Het ergste wat een geslaagde aanroep doet is een ander, ook
# bestaand, certificaat kiezen. # bestaand, certificaat kiezen.
# #
# Geen proxy_set_header hier, en dat is geen vergetelheid: die zouden een # Geen proxy_set_header hier: de agent heeft ze niet nodig.
# nginx-variabele vragen, en die haalt de template-invulling weg. De
# agent heeft ze niet nodig.
# #
# De volledige containernaam en niet de servicenaam 'agent'. Dat stond er # De volledige containernaam en niet de servicenaam 'agent'. Dat stond er
# tot 0.0.23 wel, en het werkte zolang deze app de enige was met een # tot 0.0.23 wel, en het werkte zolang deze app de enige was met een
@@ -83,15 +76,14 @@ http {
} }
# ── De TLS-terminatie ──────────────────────────────────────────────────────── # ── De TLS-terminatie ────────────────────────────────────────────────────────
# Die staat niet hier maar in stream.conf.template, en dat is geen ordening maar # Die staat niet hier maar in stream.conf, en dat is geen ordening maar de
# de reparatie van een klem. nginx weigert te starten als een 'listen ... ssl' # reparatie van een klem. nginx weigert te starten als een 'listen ... ssl' geen
# geen certificaat heeft; stond het stream-blok hier, dan kwam met dat blok ook # certificaat heeft; stond het stream-blok hier, dan kwam met dat blok ook de
# de pagina hierboven niet omhoog zolang de agent geen certificaat gekozen had. # pagina hierboven niet omhoog zolang de agent geen certificaat gekozen had. En
# En de pagina is juist de plek waar die keuze gemaakt wordt. # de pagina is juist de plek waar die keuze gemaakt wordt.
# #
# Het command-blok van docker-compose.yml legt stream.conf in de map hieronder # entrypoint.sh legt stream.conf in de map hieronder zodra cert.conf bestaat, en
# zodra cert.conf bestaat, en haalt het weg als dat niet zo is. Een include met # haalt het weg als dat niet zo is. Een include met een jokerteken die nergens op
# een jokerteken die nergens op uitkomt is voor nginx geen fout, dus zonder # uitkomt is voor nginx geen fout, dus zonder certificaat start deze server
# certificaat start deze server zonder poort 50022 en met een pagina die # zonder poort 50022 en met een pagina die vertelt waarom.
# vertelt waarom.
include /var/lib/gate/tls/*.conf; include /var/lib/gate/tls/*.conf;
@@ -7,34 +7,38 @@
# zonder Zoraxy, of met twee kandidaten, zag daardoor helemaal niets en had geen # zonder Zoraxy, of met twee kandidaten, zag daardoor helemaal niets en had geen
# enkele weg vooruit. Zo aangetroffen in 0.0.3. # enkele weg vooruit. Zo aangetroffen in 0.0.3.
# #
# Nu staat het blok hier, en het command-blok van docker-compose.yml zet dit # Nu staat het blok hier, en entrypoint.sh zet dit bestand pas in
# bestand pas in /var/lib/gate/tls/ zodra de agent cert.conf geschreven heeft. # /var/lib/gate/tls/ zodra de agent cert.conf geschreven heeft. nginx.conf haalt
# nginx.conf haalt die map op met een jokerteken, en een jokerteken dat niets # die map op met een jokerteken, en een jokerteken dat niets matcht is voor nginx
# matcht is voor nginx geen fout. Geen certificaat betekent dus: geen # geen fout. Geen certificaat betekent dus: geen stream-blok, geen poort 50022,
# stream-blok, geen poort 50022, maar wel een werkende pagina die vertelt wat # maar wel een werkende pagina die vertelt wat eraan mankeert.
# eraan mankeert.
# #
# Dit is een template om dezelfde reden als de andere: bij een update ververst # Dit bestand zit in de image als /etc/nginx/stream.conf.in. De twee plaatshouders
# umbrelOS alleen een whitelist, en *.template staat daarin. Let daarom ook hier # __GATE_ELECTRUM_HOST__ en __GATE_ELECTRUM_PORT__ vult entrypoint.sh bij het
# op het dollarteken: elke ${...} wordt bij het starten ingevuld, dus geen # starten in uit de omgeving; nginx kan in een proxy_pass van het stream-blok zelf
# nginx-variabelen in dit bestand. Het log_format, dat er niet zonder kan, staat # geen omgevingsvariabele lezen. Tot 0.0.29 was dit een *.template in de app-map
# in stream-log.conf en wordt door het command-blok weggeschreven. # en deed umbreld die invulling, en daarom mocht er toen geen nginx-variabele met
# een dollarteken in staan. Die beperking is weg: het log_format staat nu gewoon
# hier en niet meer in een los bestand dat het command-blok wegschreef.
stream { stream {
# Het log_format en de access_log, geschreven door het command-blok in # Let op wat er NIET in staat: geen client-adres en geen bronpoort. Dat is
# docker-compose.yml. # precies het soort gegeven dat deze app van het netwerk af houdt, en om te
include /var/lib/gate/stream-log.conf; # zien dat het werkt is het niet nodig.
log_format gate '$time_iso8601 $status $bytes_received $bytes_sent $session_time';
access_log /var/lib/gate/stream.log gate;
server { server {
# 50022 en niet 50002; de reden staat bij de ports-regel in # 50022 en niet 50002; de reden staat bij de ports-regel in
# docker-compose.yml. # docker-compose.yml. Wijzig je dit, wijzig dan ook de hex-poort in de
# verbindingsteller van entrypoint.sh.
listen 50022 ssl; listen 50022 ssl;
# ssl_certificate en ssl_certificate_key, geschreven door de agent op # ssl_certificate en ssl_certificate_key, geschreven door de agent op
# grond van wat er in de certificaatmappen staat en wat de gebruiker op # grond van wat er in de certificaatmappen staat en wat de gebruiker op
# het dashboard gekozen heeft. Daarom staat er hier geen domeinnaam meer # het dashboard gekozen heeft. Daarom staat er hier geen domeinnaam meer
# in dit bestand. Dat dit bestand bestaat betekent dat cert.conf er is: # in dit bestand. Dat dit bestand bestaat betekent dat cert.conf er is:
# het command-blok zet het er pas dan neer. # entrypoint.sh zet het er pas dan neer.
# #
# nginx wil de VOLLEDIGE keten in ssl_certificate, dus het bestand van # nginx wil de VOLLEDIGE keten in ssl_certificate, dus het bestand van
# Zoraxy kan er ongewijzigd in. Splitsen in een servercertificaat en een # Zoraxy kan er ongewijzigd in. Splitsen in een servercertificaat en een
@@ -56,6 +60,6 @@ stream {
# verbinding sneller opvallen dan pas bij het volgende verzoek. # verbinding sneller opvallen dan pas bij het volgende verzoek.
proxy_socket_keepalive on; proxy_socket_keepalive on;
proxy_pass ${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}; proxy_pass __GATE_ELECTRUM_HOST__:__GATE_ELECTRUM_PORT__;
} }
} }
+15 -5
View File
@@ -10,11 +10,13 @@
// //
// ── Waarom dit bestaat ─────────────────────────────────────────────────────── // ── Waarom dit bestaat ───────────────────────────────────────────────────────
// //
// Een `index.html.template` is niet te openen. Twee dingen staan in de weg, en ze // Een statuspagina is niet zomaar te openen. Twee dingen staan in de weg, en ze
// zijn allebei fundamenteel en niet op te lossen door er anders naar te kijken: // 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 // 1. in een `index.html.template` staan accolade-variabelen die umbreld invult.
// letterlijk "v${APP_VERSION}" in de kop; // Onopgelost staat er letterlijk "v${APP_VERSION}" in de kop. Een pagina die
// in een eigen image zit heeft dit niet meer, maar haalt de versie dan uit
// status.json, dus zonder agent staat er niets;
// 2. de pagina haalt al zijn gegevens bij een agent die alleen in de app bestaat. // 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": // Zonder die agent zie je één foutmelding en verder een pagina vol "unknown":
// geen lijsten, geen teller, geen knoppen. // geen lijsten, geen teller, geen knoppen.
@@ -44,10 +46,18 @@ const REPO = join(HIER, '..');
const UIT = join(REPO, 'voorbeeld'); const UIT = join(REPO, 'voorbeeld');
const app = process.argv[2] || 'whatsnext-evolu-relay'; const app = process.argv[2] || 'whatsnext-evolu-relay';
const bron = join(REPO, app, 'index.html.template');
// De pagina staat op een van twee plekken: als *.template in de app-map, of als
// gewoon bestand in het bouwrecept onder tools/ als hij in een eigen image zit.
// Electrum Gate is sinds 0.1.0 het tweede geval. Alleen de eerste gaat door
// envsubst; voor de tweede is de invulling hieronder een lege stap.
const template = join(REPO, app, 'index.html.template');
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
const inImage = join(REPO, 'tools', kort, 'index.html');
const bron = existsSync(template) ? template : inImage;
if (!existsSync(bron)) { if (!existsSync(bron)) {
console.error(`FOUT: ${bron} bestaat niet`); console.error(`FOUT: geen pagina gevonden op ${template} of ${inImage}`);
process.exit(1); process.exit(1);
} }
+42 -170
View File
@@ -6,21 +6,31 @@ services:
APP_HOST: whatsnext-electrum-gate_server_1 APP_HOST: whatsnext-electrum-gate_server_1
APP_PORT: 80 APP_PORT: 80
# Beide containers draaien dezelfde image, gebouwd door tools/electrum-gate/
# build.sh. Wat erin zit staat in de Dockerfile daar: nginx met de pagina en het
# stream-blok, en python3 met de agent. Tot 0.0.29 stonden die bestanden hier
# als *.template en draaiden ze op python:3-alpine en nginx:alpine; waarom dat
# veranderd is staat in het plan Eigenimage, PLAN.md §3.
#
# De tag hoort gelijk te zijn aan VERSION in build.sh, en er hoort een digest
# achter zodra de image geduwd is. Let op de valstrik uit KNOWLEDGE.md: een
# oude digest onder een nieuwe tag levert stilzwijgend de oude image, dus laat
# de digest er even van af zolang de nieuwe niet geduwd is.
# De agent. Hij schrijft status.json, leest de certificaten, bevraagt de # De agent. Hij schrijft status.json, leest de certificaten, bevraagt de
# Electrum-server en neemt de certificaatkeuze aan. # Electrum-server en neemt de certificaatkeuze aan.
# #
# Waarom een tweede container en niet een shell-lus in de server hieronder: # Waarom een tweede container en niet een lus in de server hieronder: dit werk
# dit werk is inmiddels een programma. Een certificaatdatum uitlezen, een # is een programma. Een certificaatdatum uitlezen, een JSON-RPC-verzoek doen,
# JSON-RPC-verzoek doen, een geschiedenis bijhouden en een keuze valideren zijn # een geschiedenis bijhouden en een keuze valideren zijn geen dingen die je met
# geen dingen die je met openssl en nc aan elkaar knoopt zonder dat het stil # openssl en nc aan elkaar knoopt zonder dat het stil verkeerde antwoorden gaat
# verkeerde antwoorden gaat geven. Bijkomend voordeel: de app hangt niet meer # geven.
# af van de vraag of die twee gereedschappen in de nginx-image zitten.
agent: agent:
# TODO (fase 4 van het plan Appstore): pinnen op de multi-arch index-digest, image: sc.kamenier-hamer.nl/sysop/electrum-gate:0.1.0
# te bepalen met `docker buildx imagetools inspect python:3-alpine` op de
# Umbrel. Geldt voor beide images in dit bestand.
image: python:3-alpine
restart: on-failure restart: on-failure
command:
- python3
- /app/agent.py
environment: environment:
# Het adres van de Electrum-server die de gebruiker in umbrelOS gekozen # Het adres van de Electrum-server die de gebruiker in umbrelOS gekozen
# heeft. umbrelOS vult dit in op grond van de afhankelijkheid hieronder, # heeft. umbrelOS vult dit in op grond van de afhankelijkheid hieronder,
@@ -28,6 +38,10 @@ services:
GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP} GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}
GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT} GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}
GATE_TLS_PORT: "50022" GATE_TLS_PORT: "50022"
# De versie uit het manifest, voor de kop van de pagina. Die stond tot
# 0.0.29 rechtstreeks in de pagina, ingevuld door umbreld; nu de pagina in
# de image zit, geeft de agent hem door via status.json.
GATE_APP_VERSION: ${APP_VERSION}
# De zelfcontrole: de agent verbindt met de TLS-poort van de server # De zelfcontrole: de agent verbindt met de TLS-poort van de server
# hieronder en maakt de handdruk af. Niet localhost: dat is een andere # hieronder en maakt de handdruk af. Niet localhost: dat is een andere
# container en dus een andere netwerk-namespace. # container en dus een andere netwerk-namespace.
@@ -57,19 +71,13 @@ services:
# bron beschrijfbaar is, hoort te staan naast de mount die dat toestaat. # bron beschrijfbaar is, hoort te staan naast de mount die dat toestaat.
# Leeg zetten schakelt uploaden uit. # Leeg zetten schakelt uploaden uit.
GATE_UPLOAD_DIR: /certs/own GATE_UPLOAD_DIR: /certs/own
PYTHONUNBUFFERED: "1"
volumes: volumes:
# Door umbrelOS ingevuld uit agent.py.template bij het starten. Er staat
# geen accolade-variabele in dat bestand, dus de invulling laat de
# Python-code ongemoeid; de agent leest zijn instellingen uit de omgeving
# hierboven. De test in tests/ controleert die aanname.
- ${APP_DATA_DIR}/agent.py:/app/agent.py:ro
# De gedeelde toestand: status.json, cert.conf, de herlaadvlag en de # De gedeelde toestand: status.json, cert.conf, de herlaadvlag en de
# gekozen certificaat-id. Beide containers zitten hierin. # gekozen certificaat-id. Beide containers zitten hierin.
# #
# Onder data/ en niet naast de templates, want dat is wat andere apps doen: # Onder data/, want dat is wat andere apps doen: electrs mount
# electrs mount ${APP_DATA_DIR}/data/electrs, mempool ${APP_DATA_DIR}/data. # ${APP_DATA_DIR}/data/electrs, mempool ${APP_DATA_DIR}/data. Verplaatst op
# Verplaatst op 20-08-2026 met het oog op publicatie in de officiele store. # 20-08-2026 met het oog op publicatie in de officiele store.
- ${APP_DATA_DIR}/data/runtime:/var/lib/gate - ${APP_DATA_DIR}/data/runtime:/var/lib/gate
# De certificaatbronnen. Zoraxy alleen lezen: dat zijn de certificaten van # De certificaatbronnen. Zoraxy alleen lezen: dat zijn de certificaten van
# een andere app en die raakt deze app niet aan. # een andere app en die raakt deze app niet aan.
@@ -88,17 +96,18 @@ services:
# deze regel aanzetten en de naam toevoegen aan GATE_CERT_SOURCES. # deze regel aanzetten en de naam toevoegen aan GATE_CERT_SOURCES.
# #
# - ${UMBREL_ROOT}/app-data/nginx-proxy-manager/data/letsencrypt/live:/certs/npm:ro # - ${UMBREL_ROOT}/app-data/nginx-proxy-manager/data/letsencrypt/live:/certs/npm:ro
command:
- python
- /app/agent.py
# nginx: de pagina op poort 80 achter de app_proxy, en de TLS-poort 50022 zodra
# de agent een certificaat gekozen heeft. Het startscript is entrypoint.sh in de
# image; tot 0.0.29 stond dat hier als command-blok van ruim honderd regels, met
# elk dollarteken verdubbeld. Wat het doet staat daar uitgelegd.
server: server:
image: nginx:alpine image: sc.kamenier-hamer.nl/sysop/electrum-gate:0.1.0
restart: on-failure restart: on-failure
# De agent moet er zijn voordat nginx start, want de proxy_pass naar # De agent moet er zijn voordat nginx start, want de proxy_pass naar de agent
# http://agent:8000 wordt bij het starten opgelost en een onbekende naam # wordt bij het starten opgelost en een onbekende naam laat nginx afbreken.
# laat nginx afbreken. Op de cert.conf van de agent wordt niet gewacht: de # Op de cert.conf van de agent wordt niet gewacht: de pagina komt hoe dan ook
# pagina komt hoe dan ook omhoog, zie het command-blok hieronder. # omhoog, zie entrypoint.sh.
depends_on: depends_on:
- agent - agent
ports: ports:
@@ -111,22 +120,13 @@ services:
# buiten is toch al een andere, want die staat in de router doorgestuurd. # buiten is toch al een andere, want die staat in de router doorgestuurd.
# Beslist 19-08-2026, open punt 1 van het plan Configuratie. # Beslist 19-08-2026, open punt 1 van het plan Configuratie.
- "50022:50022" - "50022:50022"
environment:
# Dezelfde twee als bij de agent. entrypoint.sh vult ze in het stream-blok
# in, want nginx kan in een proxy_pass geen omgevingsvariabele lezen. Tot
# 0.0.29 deed umbreld dat via stream.conf.template.
GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}
GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}
volumes: volumes:
# Beide door umbrelOS ingevuld uit een .template bij het starten. De
# pagina wordt als los bestand gemount en niet als map: de bron staat in
# de app-root, want alleen daar wordt hij bij een update ververst.
- ${APP_DATA_DIR}/nginx.conf:/etc/nginx/nginx.conf:ro
- ${APP_DATA_DIR}/index.html:/usr/share/nginx/html/index.html:ro
# Het app-icoon, voor de kop van de pagina en het tabblad. Geen template,
# dus dit bestand komt alleen bij een installatie mee en niet bij een
# update; voor een plaatje dat vrijwel nooit wijzigt is dat goed genoeg.
# De pagina valt terug op een ingebouwd merkje als de mount er niet is.
- ${APP_DATA_DIR}/icon.png:/usr/share/nginx/html/icon.png:ro
# Het stream-blok, dus de TLS-poort zelf. Het staat los van nginx.conf
# omdat nginx niet start met een 'listen ssl' zonder certificaat, en het
# command-blok hieronder zet het pas in /var/lib/gate/tls/ zodra de agent
# een certificaat gekozen heeft. Tot die tijd draait alleen de pagina.
- ${APP_DATA_DIR}/stream.conf:/etc/nginx/stream.conf:ro
# Dezelfde gedeelde toestand als de agent. nginx leest hier cert.conf en # Dezelfde gedeelde toestand als de agent. nginx leest hier cert.conf en
# status.json, en schrijft de sessielog. # status.json, en schrijft de sessielog.
- ${APP_DATA_DIR}/data/runtime:/var/lib/gate - ${APP_DATA_DIR}/data/runtime:/var/lib/gate
@@ -134,131 +134,3 @@ services:
# welk pad in cert.conf komt, maar nginx moet het bestand kunnen openen. # welk pad in cert.conf komt, maar nginx moet het bestand kunnen openen.
- ${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs:/certs/zoraxy:ro - ${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs:/certs/zoraxy:ro
- ${APP_DATA_DIR}/data/certs:/certs/own:ro - ${APP_DATA_DIR}/data/certs:/certs/own:ro
command:
- /bin/sh
- -c
- |
set -eu
mkdir -p /var/lib/gate
# Het log_format van de stream-sessies wordt hier geschreven en niet in
# nginx.conf.template. Een log_format bestaat uit nginx-variabelen met
# een dollarteken, en de template-invulling van umbreld zou die
# stilzwijgend leegmaken. In dit blok is een dollarteken als $$ te
# ontsnappen, dus hier kan het wel.
#
# Let op wat er NIET in staat: geen client-adres en geen bronpoort. Dat
# is precies het soort gegeven dat deze app van het netwerk af houdt, en
# om te zien dat het werkt is het niet nodig.
cat > /var/lib/gate/stream-log.conf <<'CONF'
log_format gate '$$time_iso8601 $$status $$bytes_received $$bytes_sent $$session_time';
access_log /var/lib/gate/stream.log gate;
CONF
# De TLS-poort aan of uit zetten, naar de toestand van cert.conf.
#
# Hier zat tot 0.0.3 een lus die wachtte tot de agent een certificaat
# gekozen had, en dat was fout: nginx startte dan niet, dus de pagina
# kwam niet omhoog, en de pagina is juist waar je dat certificaat kiest.
# Zonder Zoraxy, of met twee kandidaten, hing de app daarmee vast op een
# keuze die nergens te maken was.
#
# Nu start nginx altijd. Het stream-blok komt erbij zodra cert.conf er
# is, en verdwijnt weer als de agent zijn keuze intrekt. nginx.conf haalt
# deze map op met een jokerteken; die matcht dan niets en dat is geen
# fout.
mkdir -p /var/lib/gate/tls
#
# Deze functie eindigt altijd geslaagd, en dat is met opzet: het script
# draait onder 'set -e', dus een mislukte cp zou de aanroeper afbreken.
# In de lus hieronder is die aanroeper de wachtlus, en die mag om geen
# enkele reden stoppen.
sync_tls() {
if [ -f /var/lib/gate/cert.conf ]; then
# Kopieren en niet linken: nginx opent dit pad als de gebruiker
# nginx, en een symlink naar een read-only mount is nodeloos fragiel.
if cp /etc/nginx/stream.conf /var/lib/gate/tls/stream.conf; then
echo "Certificaat aanwezig, TLS luistert op 50022."
else
echo "stream.conf kon niet worden weggezet; TLS blijft uit."
fi
else
rm -f /var/lib/gate/tls/stream.conf
echo "Nog geen certificaat gekozen; TLS staat uit, de pagina werkt."
fi
}
sync_tls
# De verbindingsteller. nginx stream schrijft zijn logregel pas bij het
# sluiten van een sessie, en een wallet houdt zijn verbinding uren open;
# zonder deze teller lijkt een actieve wallet dus afwezig. Dat was de
# vraag van de gebruiker op 20-08-2026, en het open punt uit §4e van het
# plan Webinterface: hoe die toestand binnen de container af te lezen is.
#
# Antwoord: /proc/net/tcp. Dat geldt per netwerk-namespace, dus dit ziet
# alleen de sockets van deze container, en daarom staat deze teller hier
# en niet in de agent: die zit in een andere namespace en kan er niet bij.
# Kolom 2 is het lokale adres met de poort in hex, kolom 4 de toestand.
# C366 is 50022 en 01 is ESTABLISHED, dus de luisterende socket (0A) en de
# verbindingen naar de backend en naar de pagina vallen er buiten.
#
# Geteld wordt alleen het aantal. Geen adres en geen bronpoort, net als in
# het log_format hierboven.
count_sessions() {
cat /proc/net/tcp /proc/net/tcp6 2>/dev/null \
| awk '$$4 == "01" && $$2 ~ /:C366$$/ { n++ } END { print n+0 }'
}
# Meteen een 0 neerzetten, zodat de pagina "geen verbindingen" kan tonen
# in plaats van "onbekend" in het minuutje voor de eerste ronde.
count_sessions > /var/lib/gate/sessions
# De herlaadlus. De agent kan nginx niet zelf herladen: dat zou de
# Docker-socket vragen en die is er bewust uit. In plaats daarvan zet hij
# een vlagbestand neer en herlaadt nginx zichzelf. Een reload leest het
# nieuwe certificaat in zonder bestaande verbindingen te verbreken.
#
# Deze lus draait op de achtergrond en nginx wordt hieronder met exec het
# hoofdproces. Andersom gaat twee keer mis: de shell blijft dan PID 1 en
# geeft signalen niet door, en als nginx omvalt eindigt het script met
# exitcode 0, waardoor Docker een geslaagde afsluiting ziet en
# 'restart: on-failure' niet ingrijpt.
(
while true; do
sleep 10
# Eerst schrijven, dan mv: de agent leest dit bestand op zijn eigen
# moment en mag geen half bestand zien.
count_sessions > /var/lib/gate/sessions.tmp
mv /var/lib/gate/sessions.tmp /var/lib/gate/sessions
if [ -f /var/lib/gate/reload ]; then
echo "Certificaat gewijzigd, nginx herladen ..."
# Eerst wegzetten, dan herladen. Andersom zou een herlading die
# mislukt de vlag toch opruimen, en dan probeert hij het nooit meer.
mv /var/lib/gate/reload /var/lib/gate/reload.done
# Voor de herlading, niet erna: een reload leest de configuratie
# opnieuw, dus het stream-blok moet er dan al liggen.
sync_tls
# De herlading mag niet met 'set -e' meegaan. Een certificaat dat
# nginx niet aanneemt laat de reload falen, en dan zou deze lus
# verdwijnen: geen enkele latere wijziging wordt dan nog opgepikt,
# zonder dat er iets te zien is.
if ! nginx -s reload; then
echo "Herladen mislukt; nginx houdt de vorige configuratie."
fi
elif [ -f /var/lib/gate/cert.conf ] && [ ! -f /var/lib/gate/tls/stream.conf ]; then
# De vangnetregel. De agent zet de vlag bij elke wijziging, dus
# normaal komt hier niets langs; wel als de vlag verloren gaat of
# als cert.conf van een vorige installatie al klaarlag. Zonder deze
# tak zou TLS dan uit blijven staan tot de volgende wijziging.
echo "Certificaat gevonden zonder herlaadvlag, TLS aanzetten ..."
sync_tls
if ! nginx -s reload; then
echo "Herladen mislukt; nginx houdt de vorige configuratie."
fi
fi
done
) &
exec nginx -g 'daemon off;'
+11 -1
View File
@@ -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.29" version: "0.1.0"
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,6 +56,16 @@ 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: >-
The app now ships as its own image instead of mounting its program files into two stock images. Nothing
changes in what it does; what changes is where the code lives. The app data folder now holds only your
data: the certificate you chose, the one you uploaded, and the status the page reads. The startup logic
that used to be a hundred lines of shell inside the compose file is a plain script inside the image, and
the app icon updates along with everything else, which it could not before.
Everything in 0.0.29:
The glow behind the page is gone. It arrived in 0.0.25 and it did what it was meant to do, but a status The glow behind the page is gone. It arrived in 0.0.25 and it did what it was meant to do, but a status
page is something you glance at, and a coloured wash over the whole window is a lot of decoration for a page is something you glance at, and a coloured wash over the whole window is a lot of decoration for a
glance. The panels keep their edge, their shadow and their top light, so they still read as panels; only glance. The panels keep their edge, their shadow and their top light, so they still read as panels; only