Eén app store, twee apps
umbrelOS leest per store één repo, dus twee apps in twee repo's kan niet. Deze repo is de store en bevat vanaf nu Electrum Gate en het werk aan Evolu Relay. Opgezet als verse repo op verzoek van de gebruiker: de historie van ElectrumTLS en van EvoluRelay komt niet mee. Dat heeft één gevolg dat verder gaat dan opruimen. In de historie van ElectrumTLS staat het domein van de gebruiker en het certificaatpad, van vóór de opschoning van 19-08. Die komt hier niet in. Zolang die repo op de Git-server blijft staan verandert dat niets, dus het weghalen ervan is het laatste stuk van open punt 3 van het plan Appstore, en geen bijzaak. De store zelf hoefde niet te veranderen: store-id whatsnext, en dus blijft het app-id whatsnext-electrum-gate. Dat hangt aan het store-id en niet aan de URL, dus voor umbrelOS is dit dezelfde app in een andere store. Dat de store op 19-08 naar de maker genoemd werd in plaats van naar deze ene app, betaalt zich hier uit. Wat de documentatie betreft is dit één wortel voor beide apps, en dat was de reden om samen te voegen en niet de prijs ervan: de appstore-spec, het pinnen van images en de werkwijze golden al voor allebei en stonden in twee repo's naast elkaar. De kruisverwijzing die daarvoor nodig was (Referenties/Umbrel-appstore.md in de oude EvoluRelay-repo) is verdwenen; wat daarin stond over de plekken waar de relay een ander geval is, staat nu als ontwerp in het masterplan Umbrelapp §4. Botsende namen kregen een achtervoegsel met de app, en alleen die: Publicatie werd Publicatie-Gate en Publicatie-Relay, CHANGELOG.md werd CHANGELOG-electrum-gate.md. Proefopstelling kreeg 007, tussen de twee bestaande nummers, zodat de bovenkant van de reeks op tier-orde blijft staan. CONTINUE_HERE.md heeft een kolom App, maar de tiers lopen over beide apps heen: er is één volgorde van werken. Electrum Gate gaat naar 0.0.15, want website, repo, support, submission en icon wijzen nu naar UmbrelApps en zonder versieverhoging rolt dat niet uit. De release notes leggen aan de gebruiker uit dat hij de store opnieuw moet toevoegen. Of een geïnstalleerde app een wisseling van store-URL overleeft is nog steeds niet uitgezocht; dat blijkt bij het omzetten. Twee dingen in de plannen van Electrum Gate waren door deze verhuizing niet meer waar en zijn bijgewerkt: de taak "de repo hernoemen" in fase 7 is afgevinkt, en de repo-vorm in PLAN.md §4a toonde nog de store-id electrumtls, die al sinds fase 7 achterhaald was. Tests: 39 goed 0 fout en 54 goed 0 fout, niets overgeslagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# .gitattributes - UmbrelApps
|
||||
#
|
||||
# Waarom dit bestand er is, en meteen bij het opzetten en niet later: op deze
|
||||
# Windows-machine staat core.autocrlf vaak op true. Zonder deze regels waarschuwt
|
||||
# git bij elke `git add` dat "LF will be replaced by CRLF", en komt er CRLF/LF-ruis
|
||||
# in diffs zodra er afwisselend met een editor en met CLI-tooling gewerkt wordt.
|
||||
#
|
||||
# Voor dit project is er een hardere reden dan diff-ruis. De shellscripts en de
|
||||
# YAML worden op een Umbrel in een Linux-container uitgevoerd. Een CRLF achter
|
||||
# `#!/bin/sh` laat het script stuklopen op een foutmelding die nergens naar het
|
||||
# echte probleem wijst, en een CRLF in docker-compose.yml geeft waarden met een
|
||||
# onzichtbare wagenterugloop erin. Dit bestand voorkomt allebei.
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Standaard: git detecteert tekst vs. binair, slaat tekst op als LF en checkt ook
|
||||
# LF uit. Dat houdt de werkmap identiek aan wat er in de repo staat.
|
||||
* text=auto eol=lf
|
||||
|
||||
# ── Alles wat op de Umbrel draait: LF, zonder uitzondering ────────────────────
|
||||
*.sh text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.conf text eol=lf
|
||||
*.sql text eol=lf
|
||||
*.template text eol=lf
|
||||
|
||||
# ── Broncode en documentatie expliciet als tekst ──────────────────────────────
|
||||
# Strikt genomen dekt `text=auto` dit al. Toch opschrijven: het maakt zichtbaar
|
||||
# waar dit project uit bestaat, en het haalt de gok uit de detectie voor een
|
||||
# bestand dat toevallig op een binair patroon lijkt.
|
||||
*.md text
|
||||
*.txt text
|
||||
*.html text
|
||||
*.css text
|
||||
*.js text
|
||||
*.json text
|
||||
Dockerfile text eol=lf
|
||||
|
||||
# ── Binair: nooit converteren, geen tekst-diff ────────────────────────────────
|
||||
# Het icoon en de gallery-afbeeldingen komen hier bij het plan Appstore.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.webp binary
|
||||
*.svg text
|
||||
*.pem binary
|
||||
*.key binary
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# ── Geheimen: nooit in de repo ────────────────────────────────────────────────
|
||||
# Deze repo wordt publiek, want umbreld kloont een community app store anoniem.
|
||||
# Certificaten en sleutels horen bij Zoraxy op de Umbrel en komen hier nooit in,
|
||||
# ook niet "even om te testen".
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
*.pfx
|
||||
secrets/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*
|
||||
!.env.sample
|
||||
|
||||
# ── Data: nooit in de repo, en niet inlezen ───────────────────────────────────
|
||||
# Evolu Relay krijgt een Postgres, en de proefopstelling zet lokaal een datamap
|
||||
# neer. Die hoort nergens in een repo thuis, ook niet in een private, en hij
|
||||
# wordt ook niet ingelezen: HomeGit/CLAUDE.md, "Data van de gebruiker".
|
||||
pgdata/
|
||||
postgres-data/
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
*.dump
|
||||
*.sql.gz
|
||||
|
||||
# ── Runtime-toestand van de app ───────────────────────────────────────────────
|
||||
.cert-timestamp
|
||||
status.json
|
||||
*.pid
|
||||
*.log
|
||||
|
||||
# ── Werkbestanden ─────────────────────────────────────────────────────────────
|
||||
*.tmp
|
||||
tmp/
|
||||
temp/
|
||||
*.bak
|
||||
*.backup
|
||||
*~
|
||||
|
||||
# ── Editor en OS ──────────────────────────────────────────────────────────────
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-*
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ── Claude Code: lokale instellingen zijn per machine ─────────────────────────
|
||||
.claude/settings.local.json
|
||||
|
||||
# ── Bouwsel ───────────────────────────────────────────────────────────────────
|
||||
dist/
|
||||
build/
|
||||
*.tar.gz
|
||||
*.zip
|
||||
|
||||
# ── Python ────────────────────────────────────────────────────────────────────
|
||||
# De test importeert agent.py.template als module, en Python legt daar dan
|
||||
# bytecode naast. De test zet dat zelf uit, maar dit is de vangnetregel: zo'n
|
||||
# .pyc glipte een keer mee in een commit.
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -0,0 +1,129 @@
|
||||
# UmbrelApps - projectspecifieke afspraken
|
||||
|
||||
De algemene werkafspraken staan in `HomeGit/CLAUDE.md` en worden hier niet herhaald. Hieronder alleen wat
|
||||
eigen is aan dit project.
|
||||
|
||||
**Deze repo is één community app store met twee apps erin.** umbrelOS leest per store één repo, en dat is
|
||||
de reden dat ze samen in één repo zitten in plaats van elk in een eigen. Eerst de regels die voor allebei
|
||||
gelden, dan per app wat alleen daar geldt.
|
||||
|
||||
| App | Map | Toestand |
|
||||
|-|-|-|
|
||||
| **Electrum Gate** | `whatsnext-electrum-gate/` | draait op de Umbrel, wordt gebruikt |
|
||||
| **Evolu Relay** | nog niet | bestaat uit plannen; zie het plan **Proefopstelling** |
|
||||
|
||||
**Zet geen CLAUDE.md of andere werkbestanden in een app-map.** umbreld kopieert bij installatie de héle
|
||||
app-map naar `~/umbrel/app-data/<app-id>/` met `rsync --archive`, dus alles wat daar staat belandt op het
|
||||
apparaat en in de back-up. Documentatie hoort in `Docs/`, tests in `tests/`, allebei in de repo-root.
|
||||
|
||||
## Umbrel-regels die je niet mag omzeilen
|
||||
|
||||
Deze gelden voor élke app hier, want het zijn eigenschappen van umbrelOS. Met bron in
|
||||
[Docs/Referenties/Umbrel-appstore-spec.md](Docs/Referenties/Umbrel-appstore-spec.md).
|
||||
|
||||
**Geen accolade-variabelen in een `*.template` die er niet horen.** umbreld haalt bij elke start elk
|
||||
`${...}` 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
|
||||
`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.
|
||||
|
||||
**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.
|
||||
Zet logica die later nog moet kunnen wijzigen dus in de compose of in een `*.template`, en nooit in een
|
||||
los bestand of in een submap.
|
||||
|
||||
**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
|
||||
versies staan per app los van elkaar.
|
||||
|
||||
**Alle gebruikersstaat onder `${APP_DATA_DIR}/data/...`, en nooit erbuiten schrijven.** Elke map die bij
|
||||
de eerste start moet bestaan, heeft een `.gitkeep` in de repo.
|
||||
|
||||
**Geen wachtwoord of ander geheim in de repo.** umbrelOS levert `${APP_PASSWORD}` en `${APP_SEED}` aan,
|
||||
per installatie afgeleid. Deze repo is publiek, dus een literal is een gepubliceerd wachtwoord. Dat wordt
|
||||
pas echt scherp bij Evolu Relay, want daar hoort een Postgres bij.
|
||||
|
||||
**Mapnaam gelijk aan het `id` in het manifest, en allebei met het store-id `whatsnext` ervoor.**
|
||||
|
||||
## Electrum Gate
|
||||
|
||||
### De tests draaien
|
||||
|
||||
```
|
||||
python tests/test_agent_certificates.py
|
||||
```
|
||||
|
||||
```
|
||||
python tests/test_server_start_zonder_certificaat.py
|
||||
```
|
||||
|
||||
Geen testrunner en geen afhankelijkheden: het zijn losse scripts die 0 teruggeven als alles goed is. Beide
|
||||
horen bij "de suite", dus beide draaien voor een commit die deze app raakt. De uitvoer eindigt met een
|
||||
regel "N goed, M fout". Er is geen watch-modus; de suite kost minder dan een seconde.
|
||||
|
||||
Twee dingen om te weten voordat je een groene uitslag vertrouwt:
|
||||
|
||||
- **sommige toetsen slaan zichzelf over en melden dat.** De vergelijking met `ssl` heeft de
|
||||
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
|
||||
exitcode;
|
||||
- **de suite dekt de agent, niet de pagina.** Alles in `index.html.template` is handwerk in een browser.
|
||||
|
||||
### Architectuurregels
|
||||
|
||||
**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
|
||||
vlagbestand-oplossing.
|
||||
|
||||
**De pagina start altijd, ook zonder certificaat.** Het TLS-blok staat daarom niet in
|
||||
`nginx.conf.template` maar in `stream.conf.template`, en het `command`-blok van de compose zet dat pas in
|
||||
`/var/lib/gate/tls/` als de agent een `cert.conf` geschreven heeft; `nginx.conf` haalt die map met een
|
||||
jokerteken op. Zet het blok niet terug in `nginx.conf`, hoe netjes dat ook staat: nginx weigert te starten
|
||||
als een `listen ssl` geen certificaat heeft, en dan komt de pagina waarop je dat certificaat kiest ook niet
|
||||
omhoog. Dat was de fout in 0.0.3. `tests/test_server_start_zonder_certificaat.py` houdt het dicht.
|
||||
|
||||
**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
|
||||
die het lijkt te doen en pas bij de wallet stukloopt op naamverificatie, en dat is veel lastiger te vinden
|
||||
dan een app die weigert en zegt waarom.
|
||||
|
||||
### Feiten
|
||||
|
||||
- App-id en mapnaam `whatsnext-electrum-gate`.
|
||||
- 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
|
||||
pakket; zie het masterplan **Publicatie-Gate** §4.
|
||||
|
||||
## Evolu Relay
|
||||
|
||||
**Er is nog geen manifest, geen compose en geen code, en die horen er ook nog niet te komen.** Het eerste
|
||||
dat er moet komen is een antwoord, niet een bestand: zie [Docs/CONTINUE_HERE.md](Docs/CONTINUE_HERE.md).
|
||||
Speculatieve pakketbestanden zouden geschreven worden op aannames die het plan **Proefopstelling** juist
|
||||
moet toetsen.
|
||||
|
||||
**De relay-code is niet van ons.** Dit pakketteert `trezor/trezor-suite-sync`; er worden geen wijzigingen
|
||||
aan die software gedaan en er wordt niet bovenstrooms bijgedragen.
|
||||
|
||||
**Er zijn geen tests, want er is geen bron.** Werkte een sessie uitsluitend aan deze app en aan
|
||||
documentatie, dan vervalt stap 4 van het sessie-protocol.
|
||||
|
||||
## Repo-feiten
|
||||
|
||||
- Remote: `https://sc.kamenier-hamer.nl/sysop/UmbrelApps.git`, **SHA-1** en **publiek**. Alle drie zijn
|
||||
eisen en geen voorkeuren: umbreld kloont met isomorphic-git, en die spreekt alleen smart-HTTP, kan alleen
|
||||
SHA-1, en krijgt geen inloggegevens mee.
|
||||
- **Store-id `whatsnext`** in `umbrel-app-store.yml`. Elk app-id moet daarmee beginnen en gelijk zijn aan
|
||||
de mapnaam.
|
||||
- **Aangemaakt op 25-08-2026 zonder historie**, toen Evolu Relay erbij kwam. De voorganger was de repo
|
||||
`ElectrumTLS`; die bevatte één app en zijn naam werd onhoudbaar bij een tweede. De historie is bewust
|
||||
niet meegenomen. Zolang die oude repo op de Git-server staat, staat het domein van de gebruiker daar nog
|
||||
in; zie het plan **Appstore**, `OPEN.md` punt 3.
|
||||
- **De URL is de identiteit van de store.** Verandert hij, dan is het voor umbrelOS een andere store en
|
||||
moet de gebruiker hem opnieuw toevoegen.
|
||||
|
||||
## Taal
|
||||
|
||||
Codecommentaar en documentatie zijn Nederlands. **Zichtbare tekst is Engels**: de pagina, `umbrel-app.yml`,
|
||||
de meldingen die de agent naar de pagina stuurt, en de root-`README.md`, want dat is wat een bezoeker van
|
||||
een publieke app store als eerste ziet. Die grens loopt precies langs "ziet de gebruiker dit": een reden
|
||||
uit `choose()` belandt op het dashboard en is dus Engels, een commentaarregel erboven niet.
|
||||
@@ -0,0 +1,405 @@
|
||||
# Changelog
|
||||
|
||||
Alle belangrijke wijzigingen aan dit project worden hier vastgelegd. Het formaat volgt
|
||||
[Keep a Changelog](https://keepachangelog.com/nl/1.0.0/) en het project volgt
|
||||
[Semantic Versioning](https://semver.org/lang/nl/).
|
||||
|
||||
Wat er nog moet gebeuren staat **niet** hier maar in de plannen; zie [CONTINUE_HERE.md](CONTINUE_HERE.md).
|
||||
Een lijst met geplande features op twee plekken loopt uit elkaar, en dan is geen van beide meer te
|
||||
vertrouwen.
|
||||
|
||||
## [0.0.14] - 2026-08-20
|
||||
|
||||
### Added
|
||||
|
||||
- **`probe` naast `refused` in het activiteitenlog.** Aanleiding was een vraag van de gebruiker over een
|
||||
regel `refused ... status 500` met nul bytes: dat is een TLS-handdruk die niet is afgemaakt, en op een
|
||||
poort die in de router doorgestuurd staat vrijwel altijd een scanner. Elke scan gaf dus een rode regel die
|
||||
suggereerde dat de app iets geweigerd had, terwijl er niets gebeurd was.
|
||||
|
||||
**Het onderscheid ligt bij de bytes, niet bij de duur of de status.** Nul in beide richtingen betekent dat
|
||||
er nooit iets doorgegeven is; die tellers gaan over de doorgegeven verbinding en niet over de handdruk, wat
|
||||
af te lezen was aan de sessies van de gebruiker. Een scan die tien seconden open blijft is nog steeds een
|
||||
scan, en een sessie die na een halve seconde omvalt maar wél verkeer had is nog steeds een storing. Rood
|
||||
blijft dus voor `refused`, en dat is nu het geval waar je iets aan moet doen.
|
||||
|
||||
De notitie bij een probe blijft feitelijk ("nothing exchanged, status 500") en beweert niet dat het de
|
||||
handdruk was; bij status 500 is dat de bijna zekere oorzaak, en bijna is hier niet genoeg. De bytes worden
|
||||
bij nul weggelaten, want "0 B from wallet" naast die notitie is dubbelop en "wallet" is dan ook het
|
||||
verkeerde woord.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Meer ruimte tussen de kaders**, van 1 naar 1,5rem, op verzoek van de gebruiker. De kaders hebben 1,8rem
|
||||
ruimte binnenin en een radius van 28 pixels; met 1rem ertussen was de ruimte tússen twee kaders kleiner
|
||||
dan die erbinnen, en dan plakken ze aan elkaar.
|
||||
|
||||
## [0.0.13] - 2026-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **De twee kaders van die rij groeien en krimpen nu samen.** Dit was de echte oorzaak van het hoogteprobleem
|
||||
dat 0.0.12 alleen kleiner maakte: het logblok stond op `flex: 1 1 auto`, en met basis `auto` telt zijn
|
||||
inhoud mee voor de natuurlijke hoogte van zijn kader. Een log met tien regels duwde de rij dus hoger dan
|
||||
het certificaatkader nodig had, en dat kwam aan de andere kant terug als leegte onder de knop, precies de
|
||||
leegte die verdween als je het uploadblok uitklapte.
|
||||
|
||||
Nu `flex: 1 1 0`: het logblok draagt niets bij aan die natuurlijke hoogte, het certificaatkader bepaalt de
|
||||
rij, en uitklappen laat beide kaders meegroeien. Wat er niet in past scrolt in het logblok, en daar is een
|
||||
log voor. De `max-height` van 620 pixels kon eruit, want de rij is nu begrensd door het kader ernaast.
|
||||
|
||||
**Op één kolom geldt het omgekeerde** en daar staat het log op zijn eigen hoogte, met een plafond van 60%
|
||||
van het venster: er staat dan geen kader naast om de hoogte van te lenen, en een etmaal aan regels zou
|
||||
anders één lange pagina worden.
|
||||
|
||||
## [0.0.12] - 2026-08-20
|
||||
|
||||
### Changed
|
||||
|
||||
- **Het certificaatkader en het activiteitenlog zijn minder hoog.** Gevraagd door de gebruiker. Het
|
||||
mechanisme erachter is het opschrijven waard: de twee kaders staan in één rij met `align-items: stretch`,
|
||||
dus het hoogste bepaalt de hoogte. Het certificaatkader is dat, en alles wat daar aan ruimte overbleef
|
||||
kwam als leegte terug ónder het log ernaast. Krappere marges rond de keuzekop en de knop, minder ruimte om
|
||||
het uploadblok, en de bodem van het logblok van 180 naar 150 pixels.
|
||||
- **De regel over Nginx Proxy Manager is eruit.** Die stond er als bron van certificaten, terwijl die mount
|
||||
nog uitgecommentarieerd in de compose staat omdat het pad niet geverifieerd is; wat er stond kón dus niet
|
||||
waar zijn. De uitleg is nu één regel, en waar een certificaat vandaan komt staat toch al in de optie zelf.
|
||||
|
||||
## [0.0.11] - 2026-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **De kopieerknoppen staan op een telefoon weer op één lijn.** Regressie van 0.0.10: door het omvouwen
|
||||
volgde de knop de tekst, en het ene adres is langer dan het andere. Nu `margin-left: auto` op de knop, en
|
||||
bewust niet `space-between` op de rij: bij de gesplitste weergave staat er ook een notitie naast de
|
||||
waarde, en die hoort daar tegenaan te blijven staan.
|
||||
|
||||
### Changed
|
||||
|
||||
- **De winkeltekst zegt nu wat er níet in het pad zit.** De gebruiker merkte op dat het vermijden van
|
||||
tunneldiensten en VPN-producten bij hem juist de aanleiding voor deze opstelling is, en dat dat goed is
|
||||
voor de marketing. Dat is ook het scherpere argument: een tunneldienst termineert je verkeer onderweg en
|
||||
een mesh-VPN vraagt een account plus een client op elk apparaat, terwijl deze app geen van beide vraagt.
|
||||
Eén alinea in `description`, met de doorgestuurde poort erbij als wat het wél vraagt. Zie
|
||||
[Vergelijkbare-apps.md](Referenties/Vergelijkbare-apps.md), waar ook de Engelse formulering staat voor
|
||||
hergebruik in de PR bij inlevering.
|
||||
- **Het uploadblok staat nu boven de keuzelijst**, tussen de rij "Last reload" en de kop "Choose a
|
||||
certificate". Voorgesteld door de gebruiker om een horizontale lijn uit te sparen: de lijstregel erboven
|
||||
trekt er al een, dus het blok heeft geen eigen `border-top` meer nodig. Het is bovendien de logische
|
||||
volgorde, want wat je uploadt komt in die keuzelijst terecht: eerst toevoegen, dan kiezen.
|
||||
|
||||
### Bevestigd
|
||||
|
||||
- **`0.0.10` na `0.0.9` levert een update op.** Nagekeken op de Umbrel toen de melding gewoon verscheen.
|
||||
Daarmee is de aanname uit 0.0.10 bevestigd en een tekstvergelijking met groter-dan uitgesloten;
|
||||
tweecijferige versiedelen zijn dus veilig. Vastgelegd in
|
||||
[Umbrel-appstore-spec.md](Referenties/Umbrel-appstore-spec.md).
|
||||
- **De mobiele weergave werkt**, gecontroleerd op een telefoon door de gebruiker. Daarmee is 0.0.10
|
||||
geverifieerd op het enige punt dat niet te toetsen was.
|
||||
|
||||
## [0.0.10] - 2026-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Op een telefoon liep de pagina buiten beeld.** Gemeld door de gebruiker. De directe oorzaak was
|
||||
`minmax(420px, 1fr)` op de clientlijst: dat eist een track van minstens 420 pixels, ook op een scherm dat
|
||||
smaller is, dus liep de hele lijst met kopieerknoppen eruit. Nu `minmax(min(420px, 100%), 1fr)`, waarmee
|
||||
de track tot de schermbreedte mag krimpen.
|
||||
- **De lijstregels en de logregels vouwen onder 700 pixels om.** Wat op een brede kaart rechts staat, komt
|
||||
daar onder de titel te staan. Voor de logregels is dat een herziening van een eerdere keuze: die scrollen
|
||||
liever dan dat ze afkappen, en dat blijft zo op een breed scherm, maar op een telefoon betekende het dat
|
||||
je de bytes nooit zag. `overflow-wrap: anywhere` erbij, want `host:poort:protocol` heeft geen spatie en
|
||||
kan nergens afbreken.
|
||||
|
||||
### Changed
|
||||
|
||||
- **De tegel met het aantal dagen tot het certificaat verloopt is eruit**, op verzoek van de gebruiker. De
|
||||
kaart Certificate zegt hetzelfde en noemt de datum, en onder de dertig dagen staat er een melding
|
||||
bovenaan de pagina. **Backend response** neemt de vrijgekomen ruimte en is nu twee kolommen breed; dat is
|
||||
het enige op die rij dat met breedte iets doet, want daar hangt de sparkline in.
|
||||
|
||||
Over het nummer: **0.0.10 en niet 0.1.0**. Dat is alleen een risico als umbreld versies als tekst zou
|
||||
vergelijken, want dan is `0.0.10` kleiner dan `0.0.9`. Elke app in de officiële store gaat ooit van `.9`
|
||||
naar `.10`, dus dat zou daar overal stuklopen; de aanname is dat het een gelijkheids- of semver-vergelijking
|
||||
is. Blijft de update-melding weg, dan is dít de plek om te kijken.
|
||||
|
||||
## [0.0.9] - 2026-08-20
|
||||
|
||||
### Changed
|
||||
|
||||
- **Alles wat de app schrijft staat onder `data/`.** `${APP_DATA_DIR}/runtime` en `.../certs` zijn
|
||||
`${APP_DATA_DIR}/data/runtime` en `.../data/certs` geworden. Gevraagd door de gebruiker met het oog op
|
||||
publicatie in de officiële appstore, en de packaging-documentatie van umbrel zegt het woordelijk:
|
||||
gebruikersstaat, config, uploads en gegenereerde geheimen horen onder `${APP_DATA_DIR}/data/...`, met
|
||||
een `.gitkeep` in de repo voor elke map die bij de eerste start moet bestaan. Die twee `.gitkeep`s staan
|
||||
er nu.
|
||||
|
||||
**Dit vraagt opnieuw kiezen, of opnieuw installeren.** Bij een update kijkt de app niet meer in de oude
|
||||
map, dus zelf geüploade certificaten en de gemaakte keuze zijn weg. Certificaten uit Zoraxy niet.
|
||||
|
||||
### Added
|
||||
|
||||
- **`backupIgnore`** in het manifest, voor `data/runtime/stream.log` en `data/runtime/status.json`. Die
|
||||
twee groeien of veranderen elke minuut en zijn nergens voor nodig in een back-up. Wat er bewust **niet**
|
||||
in staat is `data/runtime/config`: daar zit de certificaatkeuze, en dat is het enige in die map dat niet
|
||||
opnieuw te bedenken is. Paden zijn relatief aan de app-datamap; nagekeken in `app.ts` van umbreld.
|
||||
- Een toets die deze conventie vasthoudt. Bij de volgende mount die iemand toevoegt is dit precies het
|
||||
detail dat je vergeet, en er gaat niets van stuk; het valt pas op bij het inleveren.
|
||||
|
||||
### Het manifest op orde, zonder eigen versienummer
|
||||
|
||||
Later diezelfde dag, en met opzet **geen** verhoging naar 0.0.10: de winkeltekst leest umbrelOS uit de
|
||||
repo-kloon, dus dit is zichtbaar zonder update, en aan de app verandert niets.
|
||||
|
||||
- **De velden staan in de voorgeschreven volgorde.** Die is geen smaak; de packaging-documentatie van de
|
||||
officiële store schrijft hem voor. Wat de spec niet noemt (`icon`, `backupIgnore`) staat nu áchter die
|
||||
reeks, zodat de kop van het bestand letterlijk op orde is en er bij inlevering alleen iets weg hoeft.
|
||||
`icon` staat als laatste, want dat is precies de regel die dan verdwijnt.
|
||||
- **`defaultUsername` en `defaultPassword` zijn eruit.** Ze stonden er leeg in. Deze app heeft geen eigen
|
||||
inlog, de app_proxy van umbrelOS zet er zijn eigen voor, en een leeg veld suggereert dat er iets te
|
||||
vullen valt.
|
||||
- **De release notes zijn opgeschoond, en dat was een echte fout.** Drie versies achter elkaar kwam er een
|
||||
nieuwe kop bovenop terwijl de oude tekst eronder bleef staan: er stond drie keer "Earlier releases" en
|
||||
twee keer dezelfde regel over 0.0.4. Nu is het één verhaal over deze versie en één regel per eerdere
|
||||
versie. Een toets let erop, want niemand leest zijn eigen release notes nog een keer na.
|
||||
- De beschrijving noemt nu ook dat je een eigen certificaat kunt uploaden. Dat kan sinds 0.0.7 en stond
|
||||
nog niet in de winkeltekst.
|
||||
|
||||
## [0.0.8] - 2026-08-20
|
||||
|
||||
### Changed
|
||||
|
||||
- **De pagina draagt dezelfde tagline als de appstore.** Er stond "TLS in front of your own Electrum
|
||||
server", en dat was de achterblijver van de oude naam: bij het hernoemen naar Electrum Gate op
|
||||
19-08-2026 was de reden juist dat de tekst over de opbrengst gaat en niet over TLS als middel. Die
|
||||
maatstaf gold nog niet voor deze regel. Beslist door de gebruiker, die het verschil opmerkte.
|
||||
`tests/test_server_start_zonder_certificaat.py` houdt de twee plekken nu gelijk, want niemand die het
|
||||
manifest aanpast opent daarna de pagina.
|
||||
|
||||
## [0.0.7] - 2026-08-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Een certificaat uploaden via de pagina**, open punt 6 van het plan **Webinterface**. De bron
|
||||
`Own folder` bestond al, maar `${APP_DATA_DIR}/certs` is zonder SSH niet te bereiken, dus die bron was
|
||||
alleen bruikbaar voor wie de shell op durft. Dat was de helft van de reden dat hij bestaat.
|
||||
|
||||
Wat er onder de motorkap voor nodig was, in vier stukken:
|
||||
|
||||
- de mount `${APP_DATA_DIR}/certs` is bij de **agent** beschrijfbaar geworden. Bij nginx blijft hij
|
||||
alleen-lezen: die hoeft er nooit iets neer te zetten;
|
||||
- `GATE_UPLOAD_DIR` zegt welke map dat is. Expliciet en niet afgeleid uit `GATE_CERT_SOURCES`, want
|
||||
welke bron beschrijfbaar is hoort naast de mount te staan die dat toestaat. Leeg zetten schakelt
|
||||
uploaden uit;
|
||||
- een eigen `location` in nginx met een limiet van 96k, want een sleutel plus keten haalt de 1k van
|
||||
`/api/` ruim. Een exacte match, dus die grotere limiet geldt nergens anders;
|
||||
- **de validatie, en dat is het eigenlijke werk.** Het paar gaat eerst onder een `.tmp`-naam naar de
|
||||
doelmap en wordt daar door `ssl.SSLContext.load_cert_chain` geopend. Dat is dezelfde OpenSSL die nginx
|
||||
straks gebruikt, dus wat hier doorkomt komt daar ook door. Zonder die controle levert een verkeerde
|
||||
sleutel een nginx die niet meer herlaadt, en dat is dezelfde klasse storing als waardoor 0.0.3
|
||||
helemaal niet startte. Geweigerd worden verder: rommel in plaats van PEM, een versleutelde sleutel,
|
||||
een verlopen certificaat, en een certificaat zonder hostnaam.
|
||||
|
||||
**De bestandsnaam komt uit het certificaat zelf** en niet uit het verzoek. Dat is geen detail: een naam
|
||||
die van buiten komt moet je tegen padtrucs verdedigen, een naam die je uit het certificaat leest niet.
|
||||
Een hostnaam met `../` erin wordt een platte naam en kan de doelmap niet uit.
|
||||
|
||||
**Uploaden kiest niet.** De nieuwe komt voorgeselecteerd in de lijst en de gebruiker drukt op dezelfde
|
||||
knop als altijd, zodat er precies één plek blijft waar TLS van certificaat wisselt.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Het app-icoon in de kop**, groter, in plaats van het generieke schildje. De pagina valt terug op dat
|
||||
schildje als de mount er niet is, want een gebroken-afbeeldingicoon in de kop is erger dan een generiek
|
||||
merkje. Ook als favicon voor het tabblad.
|
||||
- **De badges in de hoek van de kaders zijn eruit**, op verzoek van de gebruiker. Bij de Electrum-server
|
||||
stond dezelfde meting twee keer in één kader ("answered 7 seconds ago" naast de rij "Last checked"), en
|
||||
bij het activiteitenlog is het log zelf genoeg. Wat er niet verloren gaat is het slechte geval: een
|
||||
backend die niet antwoordt geeft nog steeds de rode melding bovenaan.
|
||||
- **"your choice" staat niet meer achter het actieve certificaat.** De gebruiker merkte op dat het altijd
|
||||
zijn keuze is. Het redenveld blijft voor de gevallen die niet vanzelf spreken, zoals een app die niets
|
||||
koos of er zelf een pakte.
|
||||
- **Een wallet die al verbonden was levert nu ook een `connected`-regel op.** De teller vergeleek met het
|
||||
vorige aantal en zweeg dus over een verbinding die er al hing voor de agent begon, en dat is precies het
|
||||
geval waarin iemand op de pagina komt kijken.
|
||||
- **Trezor Suite staat niet meer als "desktop only".** De gebruiker heeft het op iOS nagekeken en de
|
||||
instelling zit er wel; de documentatie van Trezor sprak zichzelf tegen en had het mis. Zie
|
||||
[Clients.md](Referenties/Clients.md) §3.
|
||||
|
||||
## [0.0.6] - 2026-08-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Een verbindingsteller, dus de pagina laat zien dat er nú een wallet verbonden is.** Dit was het open
|
||||
punt uit §4e van het plan **Webinterface**: nginx `stream` schrijft zijn logregel pas bij het sluiten van
|
||||
een sessie, en een wallet houdt zijn verbinding uren open. Een werkende opstelling zag er daardoor uit
|
||||
als een stille. Het antwoord is `/proc/net/tcp`, geteld door de achtergrondlus van de nginx-container,
|
||||
want die geldt per netwerk-namespace en de agent zit in een andere. Het aantal komt via een bestand in
|
||||
`status.json`, net als bij de herlaadvlag, en de agent zet er een `connected`-regel bij als er één
|
||||
bijkomt. Alleen het aantal: geen adres en geen bronpoort, net als in het log_format.
|
||||
|
||||
### Changed
|
||||
|
||||
- **De certificaatkeuze is een dropdown.** Op verzoek van de gebruiker: met veertien certificaten op de
|
||||
machine was de lijst met keuzerondjes het drukste onderdeel van de pagina, voor iets wat je één keer
|
||||
doet. Alles staat nu in de tekst van de optie, inclusief "expired" en "in use", want opmaak binnen een
|
||||
option verschilt per browser. De keuzeknop eronder blijft.
|
||||
- **Een keuze overleeft een verversing.** De oude lijst zette zichzelf elke ronde terug op wat er in
|
||||
gebruik was, dus een keuze die je net gemaakt had verdween binnen tien seconden.
|
||||
- **`disconnected` heet nu `session ended`, en de details zeggen wat ze betekenen.** De gebruiker vroeg
|
||||
wat die regel betekende, en dat was terecht: er stond een tijd, een duur en twee aantallen bytes, en
|
||||
niets zei dat de tijd het einde is en de duur teruggaat. Ook "up" en "down" zijn eruit; er staat nu bij
|
||||
wie er stuurt.
|
||||
- **Blockstream Green heet nu Blockstream** in de clientlijst en in de winkelbeschrijving; de app is
|
||||
omgedoopt. Gemeld door de gebruiker. De historische entries hieronder houden hun oude naam.
|
||||
- **Vijf stukken tekst eruit**, alle vijf op verzoek van de gebruiker: de voetnoot onder het
|
||||
activiteitenlog, de regel "only ever read, never requested by this app" bij Certificate, de stip voor de
|
||||
badges, de zin dat apps op de Umbrel zelf deze gateway niet nodig hebben, en de uitleg dat de ene wallet
|
||||
één regel wil en de andere twee velden. Wat overblijft bij de verbindingsregels is de SSL-instructie en
|
||||
de waarschuwing over een afwijkende externe poort.
|
||||
|
||||
### Niet gebouwd, met opzet
|
||||
|
||||
- **De `traffic`-regel uit de §4e-tabel.** Bytes van een lopende sessie zijn binnen de container niet af
|
||||
te lezen: `/proc/net/tcp` heeft geen tellers en de interfacetellers van de container zitten vol met
|
||||
dashboard- en backendverkeer. Het zou dus een schatting worden die als meting leest. De sessieregel bij
|
||||
het sluiten geeft de echte aantallen, en de teller geeft de aanwezigheid.
|
||||
|
||||
## [0.0.5] - 2026-08-20
|
||||
|
||||
### Changed
|
||||
|
||||
- **De reden bij meerdere kandidaten noemt het aantal, niet de namen.** Op de Umbrel van de gebruiker
|
||||
vond de agent veertien certificaten, en de melding somde ze alle veertien op. Dat is een muur tekst die
|
||||
zegt wat de keuzelijst eronder al toont, en die lijst is waar je klikt. Het aantal blijft erin, want dat
|
||||
verklaart waarom de app niets koos. De weigering zelf verandert niet: bij twijfel kiest de app niet.
|
||||
|
||||
## [0.0.4] - 2026-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **De app kwam niet omhoog zonder certificaat, en dat was een klem.** Het stream-blok met
|
||||
`listen 50022 ssl` stond in `nginx.conf.template` en includeerde de `cert.conf` van de agent. nginx
|
||||
weigert te starten als dat certificaat er niet is, dus startte ook de web-UI niet, en de web-UI is
|
||||
precies waar je een certificaat kiest. Wie geen reverse proxy draait, of twee kandidaten heeft, zag
|
||||
daardoor niets en had geen weg vooruit. Aangetroffen bij de eerste echte installatie van 0.0.3; de
|
||||
logregels wezen ergens anders heen, want de app_proxy meldde alleen dat de server niet te bereiken was.
|
||||
|
||||
De reparatie: het stream-blok staat nu in `stream.conf.template`, en het `command`-blok van de compose
|
||||
legt het in `/var/lib/gate/tls/` zodra `cert.conf` bestaat. `nginx.conf` haalt die map op met een
|
||||
jokerteken, en een jokerteken dat niets matcht is voor nginx geen fout. Geen certificaat betekent dus
|
||||
geen poort 50022, maar wel een pagina die vertelt waarom en waaruit je kunt kiezen. Een keuze zet de
|
||||
poort erbij met een herlading, zonder herstart.
|
||||
- **Een mislukte herlading brak de herlaadlus af.** Die lus draait onder `set -e`, dus een certificaat dat
|
||||
nginx niet aanneemt liet hem verdwijnen. Daarna werd geen enkele latere wijziging meer opgepikt, zonder
|
||||
dat er iets te zien was.
|
||||
|
||||
### Added
|
||||
|
||||
- **`tests/test_server_start_zonder_certificaat.py`**, dat de bovenstaande reparatie vastlegt. Het stream-
|
||||
blok "netjes" terugzetten in `nginx.conf` brengt de klem terug, en geen van de bestaande toetsen zou dat
|
||||
merken.
|
||||
|
||||
## [0.0.3] - 2026-08-19
|
||||
|
||||
**De nummering begint opnieuw, onder 1.0.** Besloten door de gebruiker bij het opnieuw installeren van de
|
||||
app. De oude nummers, `1.0.0` en daarna `2.0.0` en `2.0.1`, beweerden een rijpheid die er niet was: het
|
||||
ging om één met de hand neergezette installatie op één machine. Het project gaat naar `1.0.0` wanneer het
|
||||
dat verdiend heeft.
|
||||
|
||||
Twee dingen die daarbij horen, zodat dit later niet als een fout leest:
|
||||
|
||||
- **de oude entries blijven staan onder hun oude nummer.** Er wordt niets met terugwerkende kracht
|
||||
omgenummerd; dat zou de commit-historie en deze changelog uit elkaar laten lopen. `0.0.1` en `0.0.2` zijn
|
||||
dus nooit onder die naam uitgeleverd. De `3` is te lezen als "de derde opzet": met de hand neergezet, toen
|
||||
een echte Umbrel-app, en nu deze;
|
||||
- **een stap terug in het nummer is eenmalig en veilig**, want dit is een nieuwe app-id zonder
|
||||
geïnstalleerde voorganger en de gebruiker deïnstalleert de oude app. Vanaf hier moet het nummer altijd
|
||||
omhoog: de spec zegt alleen dat umbreld "de versie vergelijkt", niet of dat een ongelijkheid of een
|
||||
semver-groter-dan is, dus op een omlaaggaand nummer valt niet te bouwen.
|
||||
|
||||
### Changed
|
||||
|
||||
- **De app heet Electrum Gate**, met store-id `whatsnext` en app-id `whatsnext-electrum-gate`. De oude naam
|
||||
beschreef het middel (TLS) en niet wat je ermee kunt. Voor umbrelOS is dit een andere app, dus het vraagt
|
||||
een verwijdering en een herinstallatie.
|
||||
- **De TLS-poort is 50022** in plaats van 50002. Fulcrum bezet 50002 op de host, en met Fulcrum als backend
|
||||
zou de container niet starten; dat is precies het omschakelen dat deze app moet ondersteunen. **Dit vraagt
|
||||
eenmalig een andere doorstuurregel in de router.**
|
||||
- **De web-UI is herbouwd** op het design-systeem, in het Engels, en haalt alles uit `status.json`. De oude
|
||||
pagina zette `Online` als platte tekst in de HTML en had een verzonnen logvenster; hij loog dus precies op
|
||||
het moment dat je hem raadpleegt.
|
||||
- **Het domein en het certificaatpad staan niet meer in de app.** Ze zijn uit `docker-compose.yml` en
|
||||
`nginx.conf.template` verdwenen: de agent bepaalt ze en schrijft een `cert.conf` die nginx includeert.
|
||||
- De teksten in `umbrel-app.yml` gaan nu over de afweging tussen Tor en snelheid, en over voor welke
|
||||
wallets dit nuttig is, in plaats van over TLS als middel.
|
||||
|
||||
### Added
|
||||
|
||||
- **Een `agent`-container** op `python:3-alpine` die `status.json` schrijft, de certificaatmappen scant, de
|
||||
Electrum-server bevraagt en de certificaatkeuze aanneemt. Geen eigen image, dus geen bouwstap en geen
|
||||
registry. Hij herlaadt nginx niet zelf, want dat zou de Docker-socket vragen; hij zet een vlagbestand neer
|
||||
en de nginx-container herlaadt zichzelf.
|
||||
- **Certificaatkeuze op het dashboard**, uit de mappen van Zoraxy of een eigen map. Bij meer dan één
|
||||
kandidaat zonder keuze weigert de app en noemt hij de kandidaten, in plaats van te gokken: een verkeerd
|
||||
certificaat geeft een verbinding die het lijkt te doen en pas bij de wallet stukloopt op naamverificatie.
|
||||
- **Verbindingsregels per client** met een kopieerknop, voor Trezor Suite, Electrum, Sparrow, BlueWallet,
|
||||
Nunchuk, Blockstream Green en BitBoxApp.
|
||||
- **Een activiteitenlog** met een regel per sessie. Zonder client-adres en zonder bronpoort, en zonder regel
|
||||
per protocolaanroep: die zitten in de versleutelde verbinding en ze uitlezen zou precies het verkeer
|
||||
aantasten waarvoor deze app bestaat.
|
||||
- **De eerste tests van dit project**, in `tests/`. Ze toetsen de X.509-lezer van de agent tegen `ssl` op
|
||||
echte certificaten, en de guards rond de certificaatkeuze. Hoe je ze draait staat in `CLAUDE.md`.
|
||||
- Documentatie ondergebracht in `Docs/` volgens de HomeGit-methode. `ARCHITECTURE.md`, `STRUCTURE.md` en
|
||||
`QUICKSTART.md` uit de repo-root zijn opgegaan in `Docs/Referenties/Architectuur-huidig.md`; ze
|
||||
beschreven grotendeels hetzelfde in drie versies.
|
||||
- `Docs/Referenties/Umbrel-appstore-spec.md`: wat umbrelOS van een community app store verwacht, met
|
||||
bronvermelding per feit. Belangrijkste vondsten: umbreld doet géén hostnaamcontrole op de store-URL,
|
||||
dus een eigen Gitea kan de store zijn, mits publiek en over HTTPS; en wisselen tussen Electrs, Fulcrum
|
||||
en ElectrumX vraagt geen eigen mechanisme, want die aliassen zichzelf naar `APP_ELECTRS_*`.
|
||||
- `Docs/Referenties/Clients.md`: welke wallets naar een eigen Electrum-server kunnen wijzen, en wanneer Tor
|
||||
de betere keuze is.
|
||||
|
||||
### Niet geverifieerd
|
||||
|
||||
Hoort erbij, want dit is een release-entry en niet een plan. **De agent heeft bij het uitbrengen van deze
|
||||
versie nog nooit op de Umbrel gedraaid.** Wat er getoetst is, is de X.509-lezer en de keuze-guards, met
|
||||
unittests. De certificaatscan op een echte Zoraxy-map, de Electrum-vraag, het herladen via de vlag en de
|
||||
keuze via de API zijn ongetest in bedrijf.
|
||||
|
||||
## [2.0.1] en [2.0.0] - 2026-08-18
|
||||
|
||||
Uitgeleverd onder de oude naam en de oude nummering; zie de noot bij `0.0.3`. Nooit als entry in deze
|
||||
changelog opgenomen, en dat wordt hier alleen vastgelegd zodat de reeks niet met een gat begint.
|
||||
|
||||
- **2.0.0:** herbouwd als echte Umbrel-app in een eigen community app store, met nginx en de
|
||||
`stream`-module in plaats van stunnel. Daarmee vervielen de installatie bij het starten, de losse
|
||||
certificaatmonitor en de mount van de Docker-socket. Fulcrum en ElectrumX werken sindsdien als backend.
|
||||
- **2.0.1:** de wallet-verbinding lag er na ongeveer tien minuten uit. `proxy_timeout` staat bij nginx
|
||||
standaard op tien minuten en verbrak een stille verbinding; stunnel hanteerde twaalf uur, en dat verschil
|
||||
was bij de overstap over het hoofd gezien. Op 19-08-2026 bevestigd dat de verbinding de nacht doorstond.
|
||||
|
||||
## [1.0.0] - 2024-01-15
|
||||
|
||||
Eerste werkende opzet, met de hand op de Umbrel neergezet.
|
||||
|
||||
### Added
|
||||
|
||||
- SSL/TLS-terminatie via stunnel voor Electrum-verbindingen, op poort 50002.
|
||||
- Certificaatmonitor die elke vijf minuten de mtime van het Zoraxy-certificaat vergelijkt en stunnel
|
||||
herstart bij een wijziging.
|
||||
- Read-only mount van de Zoraxy-certificaten.
|
||||
- Statuspagina op nginx.
|
||||
- `install.sh` en `uninstall.sh` voor installatie op de Umbrel.
|
||||
- Documentatie: README, ARCHITECTURE, QUICKSTART, STRUCTURE.
|
||||
|
||||
---
|
||||
|
||||
**Noot bij de 1.0.0-entry, toegevoegd 18-08-2026.** Deze entry is ingekort. De oorspronkelijke versie
|
||||
noemde onder meer "TLS 1.2+ enforcement", "geen root-rechten in containers" en "minimale
|
||||
Docker-socket-rechten" als geleverde beveiligingskenmerken. Die claims houden geen stand: de
|
||||
stunnel-configuratie legt geen minimale TLS-versie vast, de containers draaien als root, en een
|
||||
read-only mount van de Docker-socket beperkt niets, want wie de socket kan lezen kan containers starten
|
||||
en is daarmee root op de host. Ze zijn geschrapt in plaats van bijgesteld, omdat een changelog-entry
|
||||
beschrijft wat er geleverd is en dit niet geleverd is. De onderliggende punten zijn opgenomen in het plan
|
||||
**Appstore**.
|
||||
@@ -0,0 +1,82 @@
|
||||
# CONTINUE_HERE - actieve plannen, per prioriteit
|
||||
|
||||
> **Dit is een index, geen statusdocument.** Per plan één regel die naar de "Volgende stap" in dat plan
|
||||
> zijn eigen `TAKEN.md` wijst. Status leeft in de checkboxes daar, niet hier.
|
||||
> Status-emoji: ✅ gebouwd · 🔶 deels gedaan / vervolg open · ⬜ nog niet gestart · ⛔ geblokkeerd ·
|
||||
> ⏸ bevroren.
|
||||
>
|
||||
> **Deze repo is één app store met twee apps**, dus elk plan hoort bij één ervan en dat staat in de kolom
|
||||
> App. De prioriteit loopt wél over beide heen: tier A is wat er nu moet gebeuren, ongeacht welke app.
|
||||
>
|
||||
> **Alleen actieve plannen staan in de tiers.** De plannen die nog niet actief zijn staan onderaan in
|
||||
> `Plannen/Masterplannen/`, zonder tier: wannéér zo'n plan aan de beurt komt ligt niet vast, uiteindelijk
|
||||
> worden ze allemaal onder `Actief/` uitgewerkt.
|
||||
|
||||
## De twee apps
|
||||
|
||||
| App | Map | Wat het doet |
|
||||
|-|-|-|
|
||||
| **Electrum Gate** | `whatsnext-electrum-gate/` | TLS-voordeur op je eigen Electrum-server, zodat een wallet van buiten erbij kan zonder Tor. Draait, wordt gebruikt |
|
||||
| **Evolu Relay** | nog niet | De sync-server achter de labels van Trezor Suite, op je eigen Umbrel. Bestaat nog uit plannen |
|
||||
|
||||
## A - Nu (aanbevolen focus)
|
||||
|
||||
| Plan | App | Volgende stap | Status |
|
||||
|-|-|-|-|
|
||||
| [Webinterface](Plannen/Actief/005-Webinterface/TAKEN.md) | Gate | 0.0.10 op een telefoon nakijken, en het uploaden op het niet-gelukkige pad proberen met een sleutel die niet bij het certificaat hoort. Daarna open punt 5: laten controleren of de TLS-poort zélf antwoordt | 🔶 |
|
||||
| [Proefopstelling](Plannen/Actief/007-Proefopstelling/TAKEN.md) | Relay | Controleren of Trezor Suite een eigen sync-server accepteert, en op welk platform. Dat is de goedkoopste weerlegging van het hele project en het kost een minuut in de interface | ⬜ |
|
||||
|
||||
## B - Los oppakbaar (geen blokkade, geen vaste volgorde)
|
||||
|
||||
| Plan | App | Volgende stap | Status |
|
||||
|-|-|-|-|
|
||||
| [Appstore](Plannen/Actief/010-Appstore/TAKEN.md) | Gate | In umbrelOS de oude store verwijderen en `UmbrelApps` toevoegen, en kijken of de geïnstalleerde app dat overleeft. Daarna de repo `ElectrumTLS` weghalen, want daar staat het domein nog in de historie. Verder wacht de herstartcontrole op een herstart die er toch komt | 🔶 |
|
||||
|
||||
## C - Wacht op afhankelijkheid
|
||||
|
||||
| Plan | App | Volgende stap | Wacht op | Status |
|
||||
|-|-|-|-|-|
|
||||
|
||||
## D - Grote fundamentele stap (bewust laatste)
|
||||
|
||||
| Plan | App | Volgende stap | Status |
|
||||
|-|-|-|-|
|
||||
|
||||
## E - Toekomstige kandidaten (nog geen commitment)
|
||||
|
||||
| Plan | App | Waarom nog niet | Status |
|
||||
|-|-|-|-|
|
||||
|
||||
---
|
||||
|
||||
## Masterplannen - nog niet actief, geen tier
|
||||
|
||||
[Plannen/Masterplannen/](Plannen/Masterplannen/) bevat elk plan dat (nog) geen actieve status heeft, elk
|
||||
als één `<Naam>.PLAN.md`. Een plan krijgt pas een map met TAKEN/PROGRESS/OPEN als het werk begint.
|
||||
|
||||
De kolom "afhankelijk van" noemt alleen een **harde** afhankelijkheid; dat is een feit over het plan en
|
||||
geen volgorde-oordeel. De map leegt zichzelf: bij promotie verhuist het bestand naar `Archief/`
|
||||
daaronder, dus deze tabel en de mapinhoud kunnen niet uit elkaar lopen.
|
||||
|
||||
| Plan | App | Afhankelijk van | Waarover het gaat |
|
||||
|-|-|-|-|
|
||||
| [Umbrelapp.PLAN.md](Plannen/Masterplannen/Umbrelapp.PLAN.md) | Relay | Proefopstelling: het aantal containers, de variabelen en de authenticatievraag | Een tweede app-map in deze store, die je in umbrelOS installeert. De ontwerpvraag die alles bepaalt is of de app achter de inlog van umbrelOS kan staan: Trezor Suite is geen browser met een sessiecookie |
|
||||
| [Bereikbaarheid.PLAN.md](Plannen/Masterplannen/Bereikbaarheid.PLAN.md) | Relay | Umbrelapp: er valt niets bereikbaar te maken zolang er niets draait | Synchroniseren buiten het thuisnetwerk, zonder een poort op de router open te zetten. Voorstel is Tailscale; of dat volstaat hangt ervan af of Suite TLS eist |
|
||||
| [Publicatie-Relay.PLAN.md](Plannen/Masterplannen/Publicatie-Relay.PLAN.md) | Relay | Umbrelapp, plus een image die te pinnen valt | Inleveren bij de officiële appstore. Twee dingen kunnen dit blokkeren: een image die niet als multi-arch digest in een registry bestaat, en de eis dat de umbrelOS-inlog aan blijft terwijl de relay een cliënt zonder sessie moet bedienen |
|
||||
| [Publicatie-Gate.PLAN.md](Plannen/Masterplannen/Publicatie-Gate.PLAN.md) | Gate | Appstore: de herstart-controle | **Doel van de gebruiker sinds 20-08-2026:** de app inleveren als standaard-app voor Umbrel. Dat verandert de maatstaf van "hij werkt hier" naar "iemand anders keurt het pakket goed". Het meeste is al goed; wat er nog moet is de images pinnen, het app-id kaal maken en de manifestvelden op orde. Het risico zit niet in die lijst maar in de leesmount op de certificaten van Zoraxy |
|
||||
| [Configuratie.PLAN.md](Plannen/Masterplannen/Configuratie.PLAN.md) | Gate | – | **Grotendeels ingehaald op 19-08-2026** en moet opgeschoond worden voordat promotie nog zin heeft: de agent doet de certificaatbronnen en de keuze al, en het hardgecodeerde domein is uit de compose en uit `nginx.conf.template` verdwenen. Wat er nog in zit is een configuratiebestand voor de poort- en padoverstemmingen, plus de README |
|
||||
|
||||
Cross-plan kennis staat in [KNOWLEDGE.md](KNOWLEDGE.md). Naslag staat niet in deze boom maar in
|
||||
[Referenties/](Referenties/):
|
||||
|
||||
| Document | Voor wie | Waarvoor |
|
||||
|-|-|-|
|
||||
| [Umbrel-appstore-spec.md](Referenties/Umbrel-appstore-spec.md) | beide | Wat umbrelOS van een community app store verwacht, en hoe wisselen tussen Electrs, Fulcrum en ElectrumX werkt. Met bron per feit, zodat dit niet opnieuw uitgezocht hoeft te worden |
|
||||
| [Images-pinnen.md](Referenties/Images-pinnen.md) | beide | De commando's om een image op een index-digest te pinnen, en welke digest uit de uitvoer je moet hebben. Nog niet gedaan, voor geen van beide apps |
|
||||
| [Architectuur-huidig.md](Referenties/Architectuur-huidig.md) | Gate | Hoe de app vandaag op de Umbrel draait, inclusief wat er vast in de code staat |
|
||||
| [Clients.md](Referenties/Clients.md) | Gate | Welke wallets naar een eigen Electrum-server kunnen wijzen, in welke vorm ze het adres willen, en wanneer TLS iets toevoegt boven Tor |
|
||||
| [Vergelijkbare-apps.md](Referenties/Vergelijkbare-apps.md) | Gate | Of dit type app al bestaat voor Umbrel, en waarom niet. De kern voor de PR-tekst bij inlevering: de reverse proxies in de store kunnen een certificaat niet op een TCP-poort zetten |
|
||||
| [Upstream-evolu-relay.md](Referenties/Upstream-evolu-relay.md) | Relay | Wat er over `trezor/trezor-suite-sync` bekend is, met per feit hoe hard het is, en de vier dingen die nog helemaal niet uitgezocht zijn |
|
||||
|
||||
Versiegeschiedenis staat per app: [CHANGELOG-electrum-gate.md](CHANGELOG-electrum-gate.md). Evolu Relay
|
||||
heeft er nog geen, want er is nog geen manifest met een `version`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Duurzame kennis over UmbrelApps
|
||||
|
||||
Cross-plan kennis: dingen die blijven gelden en die geen enkel plan bezit. Wat bij één plan hoort staat
|
||||
daar; wat naslag is staat in [Referenties/](Referenties/). Geldt een stuk maar voor één van de twee apps,
|
||||
dan staat dat in de kop.
|
||||
|
||||
## De app is voor eigen gebruik, niet voor distributie (Electrum Gate)
|
||||
|
||||
Vastgelegd 18-08-2026 op aangeven van de gebruiker. De repo is publiek, maar dat is een **technische
|
||||
voorwaarde en geen publicatiedoel**: umbreld kloont een community app store anoniem en kan geen
|
||||
inloggegevens aanbieden, dus privé kan niet. De app is bedoeld voor één Umbrel, die van de gebruiker.
|
||||
|
||||
Waarom dit opgeschreven staat: het is het soort context dat een latere sessie niet kan afleiden uit de
|
||||
code, en dat zonder vermelding tot werk leidt dat niemand gevraagd heeft. Concreet stuurt het deze
|
||||
afwegingen:
|
||||
|
||||
- **Generiek maken is geen doel op zich.** Configuratie uit de code halen blijft nuttig, maar de reden is
|
||||
dat een domeinwijziging of een poortbotsing anders een zoektocht door drie bestanden wordt, niet dat
|
||||
iemand anders de app moet kunnen draaien. Weeg extra instelbaarheid dus tegen die maatstaf.
|
||||
- **Presentatie mag mager blijven.** Een eigen icoon is nuttig, want dat is de tegel die de gebruiker
|
||||
dagelijks ziet. Gallery-afbeeldingen, uitgebreide release notes en een nette `submitter` zijn dat
|
||||
nauwelijks: die zijn er voor een winkelpagina die niemand bezoekt. Doe wat het manifest verplicht en
|
||||
niet meer.
|
||||
- **Het domein in de publieke historie is een geaccepteerd feit.** De repo is gepusht vóór de opschoning,
|
||||
bewust, met als afweging dat een niet-aangekondigde repo op een eigen server niet gevonden wordt. Haal
|
||||
dat niet opnieuw op als bezwaar en stel er geen werk voor uit; zie het plan **Appstore**, `OPEN.md`
|
||||
punt 3.
|
||||
- **Achterwaartse compatibiliteit is geen eis.** Er zijn geen andere installaties. Een wijziging die de
|
||||
configuratie of de app-id verandert mag gewoon, mits de gebruiker weet dat hij één keer opnieuw moet
|
||||
installeren.
|
||||
|
||||
Wat het **niet** verandert: de eisen van umbrelOS zelf. Prefix in het app-id, `app_proxy`, gepinde images
|
||||
en een geldig manifest zijn geen etiquette maar voorwaarden om te starten. Zie
|
||||
[Referenties/Umbrel-appstore-spec.md](Referenties/Umbrel-appstore-spec.md).
|
||||
@@ -0,0 +1,107 @@
|
||||
# Open punten - Webinterface
|
||||
|
||||
> Beslissingen die nog een **eigenaar** of een **moment** nodig hebben. Wordt een punt een taak, dan
|
||||
> verhuist het naar [TAKEN.md](TAKEN.md).
|
||||
>
|
||||
> **Nummers blijven staan**, ook als een punt beslist is: er kan elders naar verwezen worden. Beslissen
|
||||
> betekent verplaatsen naar de kop hieronder, niet hernummeren.
|
||||
|
||||
## Nog te beslissen
|
||||
|
||||
3. **Komt het activiteitenlog er?**
|
||||
Toegevoegd 19-08-2026, nadat de gebruiker vroeg of er iets over live client-aanroepen te tonen is, en
|
||||
bijgesteld naar logregels onder elkaar. Het ontwerp staat in [PLAN.md](PLAN.md) §4e en de kaart staat
|
||||
al op de pagina in de `no data`-toestand, dus de vorm is te beoordelen zonder dat er iets gebouwd is.
|
||||
|
||||
Wat er beslist moet worden is niet "kan het" maar "is dit het waard, nu het niet is wat er gevraagd
|
||||
werd". Er was gevraagd om **elk verzoek** te loggen, en dat kan niet zonder het Electrum-verkeer van de
|
||||
gebruiker uit te lezen. Wat overblijft is een regel per verbinding en per verkeerspiek. Het vraagt een
|
||||
`log_format` die langs de template-invulling moet, plus een tweede weg naar de bytetellers van een
|
||||
lopende verbinding, want nginx logt een stream-sessie pas bij het sluiten en een wallet houdt hem
|
||||
twaalf uur open.
|
||||
**Moment:** na fase 2, want dan draait de schrijflus toch al · **Eigenaar:** gebruiker
|
||||
|
||||
5. **Wordt er gecontroleerd of de TLS-poort zelf antwoordt?**
|
||||
Toegevoegd 19-08-2026, nadat de badge "Serving this page" eruit ging. De gebruiker wees erop dat die
|
||||
badge niets zei: de pagina wordt nooit getoond aan een wallet die op 50022 verbindt. Mijn gedachte
|
||||
erachter was dat de pagina en de TLS-terminatie in dezelfde container zitten, dus dat de een de ander
|
||||
bewijst, maar dat stond er niet en niemand leest het zo.
|
||||
|
||||
Wat daarbij opvalt en het eigenlijke punt is: **de app controleert wel of de Electrum-server antwoordt,
|
||||
maar niet of hij zelf antwoordt.** Dat is precies zijn enige taak. De agent zou een TLS-verbinding naar
|
||||
de eigen poort kunnen opzetten en de handdruk kunnen afmaken; dat bewijst het luisteren, het certificaat
|
||||
en de doorverbinding in één keer, en het is dezelfde truc waarmee de einddatum van het actieve
|
||||
certificaat te lezen valt.
|
||||
|
||||
Waarom het nog geen taak is: het raakt de agent, en die heeft nog nooit gedraaid. Er is weinig aan om
|
||||
een tweede ongeteste controle op een eerste ongeteste controle te bouwen. Zodra de agent op de Umbrel
|
||||
loopt, is dit de eerstvolgende zinvolle toevoeging aan het dashboard.
|
||||
**Moment:** nadat de agent draait · **Eigenaar:** gebruiker
|
||||
|
||||
7. **Kan Zoraxy of Nginx Proxy Manager verplicht gesteld worden bij de installatie?** Gevraagd door de
|
||||
gebruiker op 20-08-2026, die erbij opmerkte dat sommige apps bij de installatie twee dropdowns tonen.
|
||||
**Technisch antwoord: nee, niet als "de een of de ander".** Nagetrokken in de bron, zie
|
||||
[Umbrel-appstore-spec.md](../../../Referenties/Umbrel-appstore-spec.md) §4, met bronlinks.
|
||||
|
||||
Kort: dat dialoog toont één dropdown **per afhankelijkheid** en niet per keuze. In het schema van
|
||||
umbreld is `dependencies` een platte lijst van app-id's, en wat er in een dropdown staat komt van de
|
||||
andere kant, namelijk elke app die `implements: [<dat id>]` declareert. Het voorbeeld dat de gebruiker
|
||||
zag is `mempool` met `dependencies: [bitcoin, electrs]`. Zoraxy en NPM declareren geen `implements`,
|
||||
dus er is geen gedeelde rol om naar te wijzen, en dat staat in hún manifest en niet in het onze.
|
||||
|
||||
Wat overblijft is Zoraxy hard eisen, en dat is een dropdown met één optie. Wat er nog over te beslissen
|
||||
is, is of dat wenselijk is: het sluit de bron `Own folder` uit, en Zoraxy deïnstalleren zou deze app
|
||||
meesleuren.
|
||||
**Moment:** vrij · **Eigenaar:** gebruiker beslist
|
||||
|
||||
## Beslist
|
||||
|
||||
8. **Welke tagline, en waar?** - **Overal die van de appstore** (20-08-2026, gebruiker).
|
||||
"Your own node from anywhere, without waiting for Tor" staat nu ook op de pagina; daar stond "TLS in
|
||||
front of your own Electrum server".
|
||||
|
||||
De afweging was dat de twee lezers op een ander moment zitten: in de winkel moet de regel iemand
|
||||
overtuigen die de app niet kent, op het dashboard staat iemand die hem al draait. Dat pleit voor twee
|
||||
regels. Wat de doorslag gaf is dat deze afweging al eerder gemaakt is: bij het hernoemen naar Electrum
|
||||
Gate was de reden dat de oude naam het **middel** beschreef en niet de opbrengst. Met die maatstaf was
|
||||
de dashboardregel de achterblijver, niet de winkelregel.
|
||||
|
||||
Een toets houdt de twee plekken nu gelijk. Niet omdat het kan, maar omdat ze ver uit elkaar staan:
|
||||
niemand die het manifest aanpast, opent daarna de pagina.
|
||||
|
||||
6. **Kan een certificaat via de pagina geupload worden?** - **Ja, en gebouwd** (20-08-2026, op verzoek van
|
||||
de gebruiker). Zie [CHANGELOG-electrum-gate.md](../../../CHANGELOG-electrum-gate.md) 0.0.7 voor de vier
|
||||
stukken die het vroeg.
|
||||
|
||||
Twee besluiten die eruit voortkwamen en die het waard zijn om terug te lezen:
|
||||
|
||||
- **de bestandsnaam komt uit het certificaat en niet uit het verzoek.** Daarmee is de hele klasse
|
||||
padtrucs weg zonder dat er iets gefilterd hoeft te worden op invoer die je niet vertrouwt. Wat er
|
||||
niet in het alfabet zit gaat eruit, dus een jokerteken-certificaat voor `*.example.org` wordt
|
||||
`example.org`;
|
||||
- **uploaden kiest niet.** De nieuwe komt voorgeselecteerd in de lijst en de gebruiker drukt op de
|
||||
bestaande knop. Zo blijft er precies één plek waar TLS van certificaat wisselt, en dat is dezelfde
|
||||
reden waarom de app bij twijfel niets kiest.
|
||||
|
||||
Het geheim door een formulier: te verantwoorden omdat het pad achter de inlog van umbrelOS zit en de
|
||||
sleutel in de app-data van de gebruiker zelf landt, met rechten 0600. De agent kapt af op 96k en nginx
|
||||
ook, dus een verzoek dat te groot is komt niet eens bij de validatie.
|
||||
|
||||
|
||||
4. **Wat gebeurt er met de blokken-per-uur-grafiek in het eerste etmaal?** - **Vervallen** (19-08-2026).
|
||||
De vraag ging over een grafiek die er niet meer is: de gebruiker heeft blokken per uur diezelfde dag
|
||||
geschrapt, als grafiek én als tegel. Zie [PLAN.md](PLAN.md) §3.
|
||||
|
||||
1. **Wat toont de pagina als `status.json` ontbreekt?** - **Een neutrale melding, geen fout**
|
||||
(19-08-2026).
|
||||
"No readings yet", met de uitleg dat dit normaal is in de eerste minuten na een installatie of een
|
||||
herstart. Bewust neutraal opgemaakt en niet rood: het bestand ontbreekt in de normale gang van zaken
|
||||
even, en een foutkleur op een normale toestand leert mensen foutkleuren negeren.
|
||||
|
||||
Daarnaast blijft elk afzonderlijk veld op `unknown` staan in plaats van leeg. Een leeg veld leest als
|
||||
"in orde", en dit is de pagina die juist geraadpleegd wordt wanneer er iets mis lijkt.
|
||||
|
||||
2. **Nederlands of Engels?** - **Engels, en alles** (19-08-2026, gebruiker).
|
||||
Dat betekent de pagina én de teksten in `umbrel-app.yml`, want een Engelse pagina onder een
|
||||
Nederlandse winkelbeschrijving is een halve keuze. Codecommentaar blijft Nederlands: de
|
||||
projectafspraak koppelt zichtbare UI-tekst aan de taal van de app, en commentaar niet.
|
||||
@@ -0,0 +1,288 @@
|
||||
# Webinterface - plan
|
||||
|
||||
> Gepromoveerd op 19-08-2026 vanuit `Plannen/Masterplannen/`, omdat het werk begonnen is: de pagina is
|
||||
> herbouwd. Taken staan in [TAKEN.md](TAKEN.md), open punten in [OPEN.md](OPEN.md), de geschiedenis in
|
||||
> [PROGRESS.md](PROGRESS.md).
|
||||
>
|
||||
> **De afhankelijkheid van Configuratie is vervallen.** Die stond er omdat de pagina de ingestelde
|
||||
> waarden moet tonen. In het ontwerp dat er nu ligt haalt de pagina álles uit `status.json` en uit de
|
||||
> umbrel-variabelen, en staat er geen enkele installatiespecifieke waarde meer in het bestand. Daarmee
|
||||
> hoefde Configuratie er niet vóór, en is dit deel van Configuratie fase 2 meteen af.
|
||||
|
||||
## 1. Doel
|
||||
|
||||
De statuspagina toont verzonnen gegevens. De statuswaarden staan als de tekst `Online` in de HTML, het
|
||||
domein staat er hardgecodeerd in en er wordt niets uitgelezen. De pagina meldt dus ook "Online" als
|
||||
stunnel omgevallen is, en dat is erger dan geen pagina: hij wordt geraadpleegd juist wanneer er iets mis
|
||||
lijkt, en geeft dan het verkeerde antwoord.
|
||||
|
||||
Als dit af is, toont de pagina alleen dingen die waar zijn, of hij zegt dat hij het niet weet.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
Vastgesteld op 18-08-2026, na het zien van de pagina in bedrijf.
|
||||
|
||||
- De verzonnen status en het verzonnen logvenster eruit.
|
||||
- **Blokhoogte en backend-status.** De hoogte via het Electrum-protocol, plus welke implementatie actief
|
||||
is en of hij antwoordt. Dit is meteen de echte gezondheidscontrole.
|
||||
- **Certificaat:** geldig tot, resterende dagen, een zichtbare waarschuwing onder de dertig dagen, en het
|
||||
tijdstip van de laatste herlading.
|
||||
- **Reactietijd van de backend**, en de geschiedenis daarvan als sparkline.
|
||||
- Aansluiten op het design-systeem in `HomeGit/Docs/website-design-system.html`: donker-eerst met een
|
||||
lichte variant, tokens voor kleur en radius, en de bestaande componenten `card`, `card-stat`, `badge`,
|
||||
`alert` en `list-item`.
|
||||
- **Verbindingsregels per client**, met kopieerknop. Toegevoegd 19-08-2026 op verzoek van de gebruiker.
|
||||
De ene wallet wil `domein:poort:s` op één regel, de andere wil host en poort in aparte velden met SSL
|
||||
aangevinkt, en juist die kleine verschillen kosten mensen tijd. De rijen komen uit
|
||||
[Referenties/Clients.md](../../Referenties/Clients.md) §4; wie er een bijzet, zet hem daar ook bij.
|
||||
- **Een activiteitenlog**, zie §4e. Herziening van een eerder niet-doel.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Een backend of API.** Zodra de pagina iets moet opvragen, is er een proces nodig dat luistert, en dan
|
||||
is dit geen configuratie-app meer. De weg eromheen staat in §4.
|
||||
- **Live logs, in de zin van een logvenster.** Ruwe logregels in de browser vragen precies de backend uit
|
||||
het vorige punt, en bij een TLS-proxy staan er verbindingsgegevens in. `docker logs` blijft de plek
|
||||
voor de echte logs. Wat er **wel** komt is een uitgedunde samenvatting per sessie; zie §4e. Dat is
|
||||
op 19-08-2026 herzien op verzoek van de gebruiker, en het is een andere zaak dan een logvenster.
|
||||
- **Instellingen bewerken in de pagina.** Uitdrukkelijk zo besloten in het plan **Configuratie**.
|
||||
- **Koersgegevens en koersgrafieken.** Besloten op 18-08-2026, na afweging. Het vraagt een externe API,
|
||||
en dan haalt de browser van iedereen die dit dashboard opent data op bij een derde partij. Bij een
|
||||
zelfgehoste Bitcoin-opstelling is dat precies het soort lek dat deze app juist dichtzet, en umbrelOS
|
||||
heeft er bovendien al widgets voor. **Alles op het dashboard komt uit lokale bronnen.**
|
||||
- **Verbindingen per uur als grafiek.** Kan wel, maar bij één of twee wallets is die grafiek vrijwel
|
||||
leeg. Het activiteitenlog uit §4e dekt dezelfde vraag beter.
|
||||
- **De blokhoogte zelf als grafiek.** Die loopt met ongeveer één per tien minuten omhoog, dus het is
|
||||
altijd dezelfde schuine lijn.
|
||||
- **Blokken per uur, in welke vorm dan ook.** Geschrapt op 19-08-2026 door de gebruiker, nadat het als
|
||||
grafiek én als tegel op de pagina had gestaan. Het antwoordt op een vraag die de gebruiker niet heeft:
|
||||
of de node meekomt met het netwerk is de zorg van de Electrum-server en niet van deze proxy, en de
|
||||
blokhoogte zelf zegt dat al. Gevolg voor het ontwerp: `history` in `status.json` hoeft alleen nog de
|
||||
reactietijd te bewaren, want de sparkline is de enige afnemer.
|
||||
- **De afweging Tor tegenover TLS op het dashboard.** Ook 19-08-2026, door de gebruiker. Die uitleg is
|
||||
positionering en hoort in de winkelbeschrijving in `umbrel-app.yml`, waar iemand staat te kiezen. Wie
|
||||
het dashboard opent, heeft al gekozen en komt iets nakijken. De onderbouwing blijft in
|
||||
[Referenties/Clients.md](../../../Referenties/Clients.md) §1 staan; die voedt nu alleen de marketing.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a0. Wél een backend, en waarom die er alsnog is
|
||||
|
||||
Herzien op 19-08-2026, nadat de gebruiker vroeg of de WebDAV-truc uit §4f van
|
||||
**Configuratie** eigenlijk de gebruikelijke manier is om een backend in een Umbrel-app te bouwen. Het
|
||||
antwoord was nee, en het onderzoek eromheen veranderde deze paragraaf.
|
||||
|
||||
**Wat umbrelOS zelf aanbiedt is één instelling per app: de afhankelijkheidskeuze.** In
|
||||
`AppSettingsSchema` staat `dependencies: z.record(z.string())` en verder niets. Daarom kostte het wisselen
|
||||
tussen Electrs, Fulcrum en ElectrumX twee regels: dát mechanisme bestaat. Er is géén generiek
|
||||
instellingenformulier dat een community-app kan declareren, en de andere umbrel-haken (`hooks/`,
|
||||
`exports.sh`, `*.template`) zijn er voor het opstarten en voor andere apps, niet voor de gebruiker.
|
||||
|
||||
**Apps die wél instelbare configuratie hebben, zijn zelf een backend.** Nginx Proxy Manager, Home
|
||||
Assistant en Jellyfin brengen hun eigen server en hun eigen instellingenscherm mee, in hun eigen image.
|
||||
umbrelOS levert dat scherm niet; de app doet het. De juiste conclusie is dus niet "umbrelOS heeft er iets
|
||||
voor" maar "die apps hebben een backend".
|
||||
|
||||
**Besluit van de gebruiker: een tweede container met een klein programma.** Een officiële
|
||||
`python:3-alpine`, met het script uit `agent.py.template` zodat het bij een update meekomt. Geen eigen
|
||||
image, dus geen bouwstap, geen registry en geen multi-arch-gedoe. Dat laatste is de reden dat een eigen
|
||||
image afvalt, niet de complexiteit van het programma.
|
||||
|
||||
Wat die keuze onderweg oploste, en dat was de eigenlijke aanleiding:
|
||||
|
||||
- **de shell-lus was aan zijn plafond.** Een certificaatdatum lezen, een JSON-RPC-verzoek doen, een
|
||||
geschiedenis bijhouden en een keuze valideren zijn geen dingen die je met `openssl` en `nc` aan elkaar
|
||||
knoopt zonder dat het stil verkeerde antwoorden gaat geven;
|
||||
- **de controles op `openssl`, `nc` en de WebDAV-module vervallen alle drie.** De agent doet dat werk zelf
|
||||
met de standaardbibliotheek, dus de app hangt niet meer af van wat er toevallig in de nginx-image zit;
|
||||
- **het hardgecodeerde domein is weg uit `docker-compose.yml` en `nginx.conf.template`.** De agent
|
||||
schrijft `cert.conf` met de paden die hij gevonden heeft, en nginx doet daar een `include` op. Daarmee
|
||||
is dat deel van **Configuratie** fase 2 af.
|
||||
|
||||
**Wat het kost.** Een tweede container, en een echt API-oppervlak in plaats van geen. Klein, maar niet
|
||||
nul, en daarom staat de verantwoording bij de `location /api/` in `nginx.conf.template` en niet alleen
|
||||
hier.
|
||||
|
||||
**nginx herladen kan de agent niet zelf**, want dat vraagt de Docker-socket en die is er bewust uit. De
|
||||
agent zet een vlagbestand neer en de nginx-container herlaadt zichzelf zodra hij dat ziet. Die lus
|
||||
verplaatst wat eerst de certificaatbewaking was, en doet nu alleen nog dit ene ding.
|
||||
|
||||
### 4a. Statusgegevens uit status.json
|
||||
|
||||
De pagina vraagt nooit iets aan een dienst die kan omvallen: de agent schrijft elke ronde een
|
||||
`status.json`, en de pagina haalt dat op met `fetch` en vult zichzelf. Dat blijft de kern, ook nu er een
|
||||
agent is; het alternatief, een pagina die live naar de backend vraagt, zou de pagina zelf laten hangen
|
||||
als de backend hangt.
|
||||
|
||||
Wat er in staat: het domein en de poort waar de wallet heen moet, het adres en de naam van de backend, de
|
||||
blokhoogte en de reactietijd, de einddatum van het actieve certificaat, de lijst met gevonden
|
||||
certificaten, een geschiedenis van reactietijden over 24 uur, en de logregels. Plus het tijdstip van
|
||||
schrijven, en dat is het belangrijkste veld van allemaal.
|
||||
|
||||
Wat de vorm oplevert:
|
||||
|
||||
- **de pagina kan niet meer liegen over "draait het".** Elk veld heeft een zichtbare onbekend-toestand en
|
||||
nooit een lege waarde, want een leeg veld leest als "in orde". Dit is de pagina die juist geraadpleegd
|
||||
wordt wanneer er iets mis lijkt;
|
||||
- **oude gegevens zijn herkenbaar, en wel bij de waarde zelf.** Onder de blokhoogte staat "as of ... ago",
|
||||
de badge bij de backend gaat van groen naar grijs met "last answered", en de voettekst noemt het
|
||||
tijdstip. Dat is de enige manier waarop de pagina een gestopte agent kan opmerken.
|
||||
|
||||
**Bewust geen melding bovenaan daarvoor.** Die heeft er even gestaan en is er op 19-08-2026 op verzoek
|
||||
van de gebruiker uit. De afweging is het opschrijven waard, want hij geldt voor elke volgende melding
|
||||
die iemand wil toevoegen: een banner die hetzelfde zegt als wat er drie regels lager bij het getal
|
||||
staat, voegt geen informatie toe maar wel gewicht, en dat gewicht gaat ten koste van de meldingen die
|
||||
wél iets nieuws zeggen. Een melding bovenaan is er voor iets wat je nergens anders ziet, zoals een
|
||||
ontbrekend certificaat;
|
||||
- **de lus valt nooit stil.** Een mislukte ronde wordt gelogd en overgeslagen; stoppen zou de pagina op
|
||||
oude gegevens bevriezen zonder dat iemand het ziet.
|
||||
|
||||
### 4a1. De indeling van de pagina
|
||||
|
||||
Vastgesteld door de gebruiker op 19-08-2026, na het bekijken van de eerste versie.
|
||||
|
||||
**De volgorde, van boven naar beneden:** Electrum-server, de drie statustegels, de certificaatkeuze, het
|
||||
activiteitenlog, en onderaan het instellen van je wallet. De regel erachter: **eerst waar je naar kijkt
|
||||
als je iets nakomt, onderaan waar je naar kijkt als je iets instelt.** Dat laatste doe je één keer.
|
||||
|
||||
De laatste twee zijn op verzoek van de gebruiker omgedraaid ten opzichte van de eerste opzet. Dat pakte
|
||||
beter uit dan alleen als voorkeur: het certificaatkader is nu het smalle kader links en het log het brede
|
||||
rechts, waardoor de kolomgrens samenvalt met die van de rij erboven in plaats van ertegenin te lopen. En
|
||||
logregels zijn monospace, dus die hebben de breedte beter nodig dan een lijst met keuzerondjes.
|
||||
|
||||
**Twee kaders zijn er één geworden.** "Point your wallet here" en "Setting up your wallet" stelden dezelfde
|
||||
vraag, dus het verbindingsadres met de kopieerknop staat nu bovenaan het instelkader in plaats van in een
|
||||
eigen kader erboven.
|
||||
|
||||
**Breed, met kaders naast elkaar.** Twaalf kolommen, en de rijen wisselen 5/7 en 7/5 af: de
|
||||
Electrum-server naast de statustegels, het log naast de certificaatkeuze, en het instelkader vol breed.
|
||||
Afgekapt op 1760px, want op een ultrawide monitor levert een kader van 2500px regels op die niemand leest.
|
||||
|
||||
**Elk kader is hetzelfde opgebouwd.** Een `card-head` met een titel in `t-h3`, dan de inhoud. Bijgesteld op
|
||||
19-08-2026 door de gebruiker: de drie statustegels hadden een eigen kleine grijze kop in kapitalen, en dat
|
||||
maakte ze een apart soort ding zonder dat daar een reden voor was. De titel gebruikt de token
|
||||
`--text-primary` en niet letterlijk wit, want in de lichte variant is wit onzichtbaar.
|
||||
|
||||
Wat daarbij vastligt en niet per ongeluk mag verschuiven:
|
||||
|
||||
- **de leesvolgorde in de HTML ís de bedoelde volgorde.** Er staat nergens een `order` of een `row-start`
|
||||
die de opmaak laat afwijken van de bron, dus bij een smal scherm stapelt alles precies zoals het gelezen
|
||||
hoort te worden. Wie hier een kader bijzet, zet het op de goede plek in de HTML en niet op de goede plek
|
||||
in het raster;
|
||||
- **de statustegels rekken mee met hun buur**, met de titel boven en het getal onder, want van zichzelf
|
||||
zijn ze lager dan het kader ernaast en dan staat er een gat rechtsboven. De onderregel van een tegel
|
||||
heeft daarvoor dezelfde hoogte als de sparkline, anders staat het getal van "Backend response" hoger dan
|
||||
de andere twee;
|
||||
- **het logblok scrollt zelf horizontaal in plaats van af te kappen.** Met puntjes zou een smal scherm de
|
||||
bytes stil verbergen, en dat is dezelfde soort onwaarheid als een verzonnen statuswaarde;
|
||||
- **twee kaders naast elkaar zijn even hoog**, en het langste bepaalt de rij. Wat mag meegroeien zegt dat
|
||||
zelf: het logblok en de certificaatlijst. Daardoor is de certificaatlijst een scrollend vak in plaats van
|
||||
een lijst die de pagina oprekt, en dat was nodig: een gedeelde certificatenmap kan er tientallen bevatten.
|
||||
Het aantal staat in de kop, want zodra er een scrollbalk is, is niet meer te zien hoeveel er onder de rand
|
||||
staan.
|
||||
|
||||
**Bewust een lijst en geen dropdown.** De gebruiker vroeg op 19-08-2026 of een dropdown niet netter was,
|
||||
en dat is het visueel ook, maar het botst met wat dit besturingselement moet doen: je kiest hier op de
|
||||
hostnaam waarop je wallet verbindt, en dan wil je bron, naam en resterende dagen naast elkaar kunnen
|
||||
vergelijken. In een dropdown zie je er één per keer, en een verlopen certificaat kan er niet rood in
|
||||
omdat `option`-opmaak per browser verschilt. De hoogtewinst die de dropdown zou opleveren komt er met een
|
||||
scrollend vak ook, dus er hoefde niets ingeleverd te worden;
|
||||
|
||||
- **er is geen voetregel.** Er heeft er een gestaan met "Last reading ... ago" en het versienummer; die is
|
||||
er op verzoek van de gebruiker uit. De leeftijd stond al bij de waarden zelf, dus dat was een derde keer
|
||||
hetzelfde.
|
||||
|
||||
Het **versienummer** is wel gebleven, klein achter de tagline als `v0.0.3`. Dat is een aparte afweging en
|
||||
hij viel andersom uit dan de rest van de voetregel: dit project verloor een keer een dag aan een wijziging
|
||||
die niet uitrolde door een niet-verhoogde `version`, en dan is "welke versie zie ik nu eigenlijk" precies
|
||||
de vraag die je stelt. Het staat in `--text-sec` en niet in `--text-ter`, want die laatste is in de lichte
|
||||
variant `#bbbbbb` op wit en op deze grootte niet te lezen.
|
||||
|
||||
### 4b. Waarschuwen op een aflopend certificaat
|
||||
|
||||
Met de einddatum in `status.json` is dit een vergelijking in de pagina zelf: onder de dertig dagen een
|
||||
opvallende melding, verlopen een duidelijke fout. Dat is de enige echte toevoeging ten opzichte van nu,
|
||||
en hij is goedkoop omdat de datum er toch al is.
|
||||
|
||||
### 4c. Welke backend er gekozen is
|
||||
|
||||
Die volgt uit `${APP_ELECTRS_NODE_IP}` en `${APP_ELECTRS_NODE_PORT}`, maar dat is een IP-adres en geen
|
||||
naam. De adressen liggen per app vast (Electrs op `10.21.21.10`, Fulcrum op `10.21.21.200`, ElectrumX op
|
||||
`10.21.21.199`), dus een vertaaltabel kan er een naam van maken. Dat is aardig, maar het is een tabel die
|
||||
stilzwijgend veroudert als Umbrel de adressen wijzigt. Voorstel: het adres tonen, en de naam alleen als
|
||||
extra wanneer hij in de tabel staat. Dan is de pagina bij veroudering minder informatief in plaats van
|
||||
onwaar.
|
||||
|
||||
### 4e. Het activiteitenlog
|
||||
|
||||
Toegevoegd 19-08-2026, nadat de gebruiker vroeg of er iets over live client-aanroepen te tonen is, en
|
||||
diezelfde dag bijgesteld naar logregels onder elkaar in plaats van een lijst met sessiekaarten.
|
||||
|
||||
**Eerst de grens, want die bepaalt de rest. Een regel per protocolaanroep kan niet.** De wallet doet zijn
|
||||
verzoeken *binnen* één TLS-verbinding die hier getermineerd wordt en daarna als bytestroom naar de backend
|
||||
gaat. nginx `stream` kent geen verzoeken, alleen verbindingen. Ze wél tellen zou betekenen dat de app de
|
||||
Electrum-berichten van de gebruiker uitleest, en dat is precies het verkeer waarvoor deze app bestaat. Dat
|
||||
is geen implementatiedrempel maar een ontwerpgrens, en hij hoort ook op de pagina te staan zodat niemand
|
||||
denkt dat het log iets verzwijgt.
|
||||
|
||||
Wat er wél per regel in kan:
|
||||
|
||||
| Gebeurtenis | Waar het vandaan komt |
|
||||
|-|-|
|
||||
| `connect` | de verbindingsteller is opgelopen sinds de vorige ronde |
|
||||
| ~~`traffic`~~ | vervallen op 20-08-2026, zie punt 2 onderaan deze paragraaf |
|
||||
| `disconnect` | de `access_log`-regel van nginx, met duur en bytes |
|
||||
| `probe` | een sessie die eindigde zonder één byte in beide richtingen. Toegevoegd 20-08-2026: een doorgestuurde poort wordt gescand, en zonder dit onderscheid gaf elke scan een rode `refused`-regel terwijl er niets geweigerd is. Bewust op de bytes en niet op de duur of de status: een scan die tien seconden open blijft is nog steeds een scan, en een echte sessie die na een halve seconde omvalt is nog steeds een storing |
|
||||
| `refused` | de verbinding gaf wél verkeer door en liep daarna stuk. Dit is het geval dat rood mag zijn |
|
||||
| `reload` | het certificaat is gewijzigd en nginx is herladen |
|
||||
| `start` | de container is gestart en luistert |
|
||||
|
||||
Die regels komen als `log` in `status.json`, afgekapt op 24 uur. De pagina toont ze nieuwste bovenaan, in
|
||||
een monospace-blok dat scrollt.
|
||||
|
||||
**Bewust niet gelogd: het IP-adres van de client en de bronpoort.** Dat is precies het soort gegeven dat
|
||||
deze app van het netwerk af houdt, en om te zien dát het werkt is het niet nodig.
|
||||
|
||||
Twee dingen die eerst uitgezocht moeten worden, en die het bouwen kunnen blokkeren:
|
||||
|
||||
1. **Een `log_format` bevat nginx-variabelen met een dollarteken, en dit bestand is een template.** De
|
||||
invulling bij het starten vervangt die en zou ze leegmaken; dat is dezelfde valkuil die nu bovenaan
|
||||
`nginx.conf.template` beschreven staat. De uitweg is de `log_format` niet in het template te zetten
|
||||
maar door het `command`-blok in `docker-compose.yml` te laten wegschrijven als een `include`-bestand,
|
||||
want daar wordt een dollarteken al als `$$` ontsnapt. Te verifiëren.
|
||||
2. **nginx `stream` schrijft zijn regel pas bij het sluiten van de sessie.** Een verbinding die twaalf uur
|
||||
openstaat, verschijnt dus pas na afloop, en dat is de normale toestand van een wallet. Daarom komen
|
||||
`connect` en `traffic` niet uit de log maar uit de lopende verbinding; anders lijkt een actieve wallet
|
||||
afwezig. **Uitgezocht en gebouwd op 20-08-2026, nadat de gebruiker meldde dat hij van zijn verbonden
|
||||
wallet niets terugzag:**
|
||||
|
||||
- **`connect` komt uit `/proc/net/tcp`.** Kolom 2 is het lokale adres met de poort in hex, kolom 4 de
|
||||
toestand; tellen wat op `:C366` staat met toestand `01` geeft het aantal open wallet-verbindingen.
|
||||
Dat bestand geldt **per netwerk-namespace**, en daar zit de bevinding: de agent kan het niet lezen,
|
||||
want hij zit in een andere container. De teller staat daarom in de achtergrondlus van de
|
||||
nginx-container, die het aantal naar een bestand schrijft dat de agent oppikt. Zelfde patroon als de
|
||||
herlaadvlag, en om dezelfde reden: geen Docker-socket. Geteld wordt alleen het aantal;
|
||||
- **`traffic` vervalt.** Bytes van een lopende sessie zijn er niet af te lezen: `/proc/net/tcp` heeft
|
||||
geen tellers, en de interfacetellers van de container bevatten ook het dashboard- en backendverkeer.
|
||||
Wat er dan overblijft is een schatting die als meting leest, en dat is precies wat §4c verbiedt. De
|
||||
sessieregel bij het sluiten geeft de echte aantallen;
|
||||
- **de pagina toont de teller in de badge van het activiteitenlog** en niet als logregel alleen. De
|
||||
vraag is "is mijn wallet nu verbonden", en die hoort niet uit een lijst afgeleid te worden.
|
||||
|
||||
## 5. Het werk
|
||||
|
||||
Staat in [TAKEN.md](TAKEN.md), met de fase-indeling en wat er af is.
|
||||
|
||||
## 6. Open punten
|
||||
|
||||
Staan in [OPEN.md](OPEN.md).
|
||||
|
||||
## 7. Verificatie
|
||||
|
||||
Handmatig, want het gaat om wat er in een browser staat:
|
||||
|
||||
- de pagina toont het werkelijk ingestelde domein en de werkelijke poorten, niet die uit de HTML;
|
||||
- na een certificaatvernieuwing verandert de einddatum op de pagina;
|
||||
- **stunnel of de proxy stoppen laat de pagina niet "Online" tonen.** Dit is de controle die de aanleiding
|
||||
van dit plan afdekt en de enige die per se gedaan moet worden;
|
||||
- een certificaat dat binnen dertig dagen verloopt, geeft een zichtbare waarschuwing.
|
||||
@@ -0,0 +1,339 @@
|
||||
# Voortgang - Webinterface
|
||||
|
||||
## 20-08-2026 - scans zijn geen weigeringen, en meer lucht tussen de kaders
|
||||
|
||||
De gebruiker vroeg wat een logregel `refused ... status 500` met nul bytes betekende. Antwoord: een
|
||||
TLS-handdruk die niet is afgemaakt, en op een doorgestuurde poort vrijwel altijd een scanner. Dat is geen
|
||||
storing, maar het log liep er wel vol met rode regels die suggereerden dat de app iets geweigerd had.
|
||||
|
||||
**Nu een eigen soort `probe`, en de grens ligt bij de bytes.** Niet bij de duur en niet bij de status: een
|
||||
scan die tien seconden open blijft is nog steeds een scan, en een sessie die na een halve seconde omvalt
|
||||
maar wél verkeer had is nog steeds een storing. Dat de bytetellers niets over de handdruk zeggen, bleek uit
|
||||
de sessies van de gebruiker zelf: een mislukte verbinding kwam op 0 en 0 uit terwijl er wél een handdruk
|
||||
geprobeerd is. Rood is nu voorbehouden aan het geval waar je iets aan moet doen.
|
||||
|
||||
Verder de ruimte tussen de kaders van 1 naar 1,5rem. De kaders hebben 1,8rem binnenin, dus de ruimte ertussen
|
||||
was kleiner dan die erbinnen; dat is de reden dat het krap aanvoelde.
|
||||
|
||||
**Geraakt:** `agent.py.template`, `index.html.template`, `umbrel-app.yml` (0.0.14), `PLAN.md` §4e-tabel,
|
||||
`tests/test_agent_certificates.py`. **Tests:** 54 goed 0 fout en 39 goed 0 fout, niets overgeslagen; de
|
||||
indeling van de sessieregels is mutatie-getest met een grens op de duur in plaats van de bytes, en dat viel
|
||||
om zoals het moest.
|
||||
|
||||
## 20-08-2026 - de mobiele opmaak, en een tegel minder
|
||||
|
||||
De pagina liep op een telefoon buiten beeld. **De oorzaak was één regel CSS:** de clientlijst stond op
|
||||
`minmax(420px, 1fr)`, en dat eist een track van minstens 420 pixels ook op een smaller scherm. Nu met
|
||||
`min(420px, 100%)` eromheen. Daarbij vouwen de lijstregels en de logregels onder de 700 pixels om, met de
|
||||
waarde onder de titel in plaats van ernaast.
|
||||
|
||||
Voor de logregels is dat een **herziening** van een eerdere keuze: die scrollen liever dan dat ze afkappen,
|
||||
omdat de kolomvorm dan blijft staan. Dat argument geldt op een breed scherm en daar blijft het zo, maar op
|
||||
een telefoon betekende het dat je de bytes nooit zag.
|
||||
|
||||
Verder is de tegel met het aantal dagen tot het certificaat verloopt eruit, op verzoek van de gebruiker.
|
||||
Dezelfde redenering als bij de badges: de kaart eronder zegt het al, en met een datum erbij. Backend
|
||||
response is nu twee kolommen breed, want de sparkline is het enige op die rij dat met breedte iets doet.
|
||||
|
||||
**Diezelfde dag nagekeken op een telefoon en het ziet er goed uit.** Eén regressie kwam eruit: door het
|
||||
omvouwen volgde de kopieerknop de tekst, en het ene adres is langer dan het andere, dus stonden de knoppen
|
||||
niet op één lijn. Verholpen in **0.0.11** met `margin-left: auto` op de knop, en niet met `space-between` op
|
||||
de rij: bij de gesplitste weergave staat er ook een notitie naast de waarde die daar tegenaan hoort te
|
||||
blijven staan.
|
||||
|
||||
Bijvangst die geen opmaak is: **`0.0.10` na `0.0.9` leverde gewoon een update op.** Daarmee is de aanname
|
||||
uit die release bevestigd en is een tekstvergelijking met groter-dan uitgesloten. Staat in de naslag, want
|
||||
het scheelt de volgende keer een omweg via `0.1.0`.
|
||||
|
||||
**Geraakt:** `index.html.template`, `umbrel-app.yml` (0.0.10 en 0.0.11), `CHANGELOG.md`,
|
||||
`Referenties/Umbrel-appstore-spec.md`. **Tests:** 39 goed 0 fout op de manifest- en configuratietoetsen; aan
|
||||
de agent is niets geraakt.
|
||||
|
||||
## 20-08-2026 - nagekeken in de browser, en dit plan is nu tier A
|
||||
|
||||
Na de verse installatie van 0.0.9 heeft de gebruiker alles nagekeken wat alleen met de hand kan.
|
||||
**Uploaden werkt, kiezen werkt, ook met de certificaten uit Zoraxy**, de keuzelijst bij het installeren zag
|
||||
er goed uit en het versienummer op de pagina klopt. Daarmee is het uploadpad uit 0.0.7 bewezen op het
|
||||
gelukkige pad; alleen de weigering bij een sleutel die niet bij het certificaat hoort is nog niet in een
|
||||
browser gezien.
|
||||
|
||||
**Dit plan is naar tier A gegaan** en van nummer 020 naar 005. Niet omdat er meer werk bij kwam, maar omdat
|
||||
het het enige plan is met werk dat nu te doen is: **Appstore** is in de kern af en wacht op een herstart die
|
||||
niet te plannen is, en **Publicatie** wacht op een publieke repo.
|
||||
|
||||
Eén nieuwe bevinding van de gebruiker om mee te beginnen: in mobiele weergave lopen de verbindingsregels
|
||||
met hun kopieerknoppen en de logregels buiten beeld. Dat laatste is een bewuste keuze geweest (de logregels
|
||||
scrollen liever dan dat ze afgekapt worden), maar op een telefoon is dat het verkeerde antwoord.
|
||||
|
||||
**Geraakt:** alleen documentatie. **Tests:** niet van toepassing.
|
||||
|
||||
## 20-08-2026 - uploaden gebouwd, en de pagina verder uitgekleed
|
||||
|
||||
**Een certificaat uploaden kan nu via de pagina**, open punt 6. Het echte werk zat niet in het formulier
|
||||
maar in de validatie: het paar gaat onder een `.tmp`-naam naar de doelmap en wordt daar door
|
||||
`ssl.SSLContext.load_cert_chain` geopend, dezelfde OpenSSL die nginx straks gebruikt. Zonder die controle
|
||||
levert een verkeerde sleutel een nginx die niet meer herlaadt, en dat is dezelfde klasse storing als
|
||||
waardoor 0.0.3 niet startte. **De bestandsnaam komt uit het certificaat zelf**, dus de hele klasse padtrucs
|
||||
is weg zonder invoerfiltering. Uploaden kiest niet: de nieuwe komt voorgeselecteerd in de lijst.
|
||||
|
||||
Twee dingen die bij het bouwen bijna fout gingen en het opschrijven waard zijn: `os.replace` uit `/tmp`
|
||||
naar `/certs/own` faalt met EXDEV omdat dat een bind-mount is, en `-subj "/CN=../.."` in openssl leest de
|
||||
schuine streep als scheidingsteken, waardoor de padtruc-toets zichzelf stil oversloeg.
|
||||
|
||||
Verder op verzoek van de gebruiker: het app-icoon in de kop in plaats van het schildje, de badges uit de
|
||||
hoek van álle kaders, "your choice" weg achter het actieve certificaat, en Trezor Suite is niet
|
||||
desktop-only. Die laatste is de tweede keer dat leveranciersdocumentatie het aflegt tegen één keer kijken.
|
||||
Bijgevangen: de teller zweeg over een wallet die al verbonden was voordat de agent begon, en dat is precies
|
||||
het geval waarin iemand komt kijken.
|
||||
|
||||
Aan het eind van de dag nog één tekstbesluit: de pagina had een andere tagline dan de appstore, en die van
|
||||
de appstore wint (0.0.8). Zie [OPEN.md](OPEN.md) punt 8; de reden is dezelfde maatstaf als bij het
|
||||
hernoemen van de app, namelijk de opbrengst en niet het middel.
|
||||
|
||||
**Geraakt:** `agent.py.template`, `index.html.template`, `nginx.conf.template`, `docker-compose.yml`,
|
||||
`umbrel-app.yml` (0.0.7 en 0.0.8), `OPEN.md` punt 6 en 8, `Referenties/Clients.md`, beide testbestanden.
|
||||
**Tests:** 48 goed 0 fout en 24 goed 0 fout, niets overgeslagen; de sleutelcontrole en de beschrijfbare
|
||||
mount zijn mutatie-getest. **Niet geverifieerd:** het uploaden in een echte browser.
|
||||
|
||||
## 20-08-2026 - de pagina naast een werkende app, en §4e afgemaakt
|
||||
|
||||
Eerste sessie met een app die echt draait, en dat leverde meteen de bevinding op waar §4e om vroeg: de
|
||||
gebruiker zag van zijn verbonden wallet niets terug. Oorzaak is bekend en staat in het plan, namelijk dat
|
||||
nginx `stream` pas bij het sluiten van een sessie logt. Het open punt was hoe je de lopende verbinding dan
|
||||
wél afleest. **Antwoord: `/proc/net/tcp`, geteld in de nginx-container**, want dat bestand geldt per
|
||||
netwerk-namespace en de agent zit in een andere. Het aantal gaat via een bestand naar de agent, net als de
|
||||
herlaadvlag. **`traffic` is daarbij vervallen:** bytes van een lopende sessie zijn er niet af te lezen, en
|
||||
een schatting die als meting leest is precies wat §4c verbiedt.
|
||||
|
||||
Daarnaast een reeks verzoeken van de gebruiker, alle om dezelfde reden: de pagina was te druk. De
|
||||
certificaatkeuze is een dropdown geworden (met veertien certificaten was die lijst het drukste onderdeel
|
||||
van de pagina, voor iets wat je één keer doet), `disconnected` heet `session ended` met leesbare details
|
||||
erachter, en er zijn vijf stukken tekst geschrapt: de voetnoot onder het log, de regel bij Certificate, de
|
||||
stip in de badges, de zin over apps op de Umbrel zelf en de uitleg over één regel versus twee velden.
|
||||
Bijgevangen bij de dropdown: de oude keuzelijst zette zichzelf elke ronde terug, dus een keuze verdween
|
||||
binnen tien seconden weer. Verder heet Blockstream Green nu Blockstream; die app is omgedoopt.
|
||||
|
||||
**Geraakt:** `index.html.template`, `agent.py.template`, `docker-compose.yml`, `umbrel-app.yml` (0.0.6),
|
||||
`PLAN.md` §4e, `OPEN.md` punt 6 en 7, `Referenties/Clients.md`, `tests/`. **Tests:** 29 goed 0 fout en 19
|
||||
goed 0 fout, niets overgeslagen; de teller en de logregel zijn mutatie-getest. **Niet geverifieerd:** alles
|
||||
in de browser, dus de dropdown, de hoogtes en de badge. Dat is handwerk van de gebruiker.
|
||||
|
||||
## 19-08-2026 - sessie afgesloten; plan zakt naar tier C
|
||||
|
||||
Laatste twee wijzigingen van de dag: certificaat en activiteitenlog omgedraaid, en het losse
|
||||
verbindingsadres met kopieerknop eruit samen met het label "tested clients". Het omdraaien pakte beter uit
|
||||
dan als voorkeur alleen, want de kolomgrens valt nu samen met die van de rij erboven (x=738 in beide rijen)
|
||||
en logregels hebben de breedte beter nodig dan keuzerondjes. Het adres verdween zonder verlies: het staat
|
||||
bij elke clientregel in de vorm die díe client wil, met kopieerknop.
|
||||
|
||||
**Het plan zakt van A naar C**, wachtend op de installatie. De pagina is af voor zover dat zonder de Umbrel
|
||||
kan; al het resterende werk hier begint met kijken of de agent doet wat hij zou moeten doen.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template`, de plannen, `CONTINUE_HERE.md`.
|
||||
**Tests:** 21 goed, 0 fout. Die raken de pagina niet.
|
||||
**Niet geverifieerd:** de agent heeft nooit gedraaid, en ik heb de pagina zelf nooit gezien; het
|
||||
browserpaneel bleef dicht, dus alles is gemeten in plaats van bekeken.
|
||||
|
||||
## 19-08-2026 - voetregel weg, certificaatlijst scrollt, kaders gelijk
|
||||
|
||||
De voetregel met "Last reading ... ago" en het versienummer is eruit; de gebruiker houdt van een rustige
|
||||
interface en de leeftijd stond al bij de waarden zelf. Het **versienummer** is op verzoek dezelfde dag
|
||||
teruggezet, klein achter de tagline als `v0.0.3`: dit project verloor een keer een dag aan een wijziging
|
||||
die niet uitrolde door een niet-verhoogde `version`, en dan is dat precies de vraag die je stelt. Het
|
||||
staat in `--text-sec`, want `--text-ter` is in de lichte variant `#bbbbbb` op wit en op die grootte niet
|
||||
te lezen.
|
||||
|
||||
De gebruiker vroeg of de certificaatlijst een scrollbalk moest krijgen of beter een dropdown werd, en of
|
||||
het logblok en het certificaatkader dan even hoog konden. Het is een scrollende lijst geworden en geen
|
||||
dropdown, want je kiest hier op de hostnaam waarop je wallet verbindt en dan wil je bron, naam en
|
||||
resterende dagen kunnen vergelijken; in een dropdown zie je er één per keer en kan een verlopen
|
||||
certificaat er niet rood in. De hoogtewinst kwam er met een scrollend vak toch, dus er hoefde niets
|
||||
ingeleverd te worden. Het aantal staat nu in de kop, want met een scrollbalk is niet meer te zien hoeveel
|
||||
er onder de rand staan.
|
||||
|
||||
Gemeten met negentien certificaten: 1062px inhoud in een vak van 360px, en de knop blijft eronder staan.
|
||||
Met vier: geen scrollbalk, dus geen leeg vak. In beide gevallen zijn de twee kaders exact even hoog, 773px
|
||||
respectievelijk 636px. Eerst stond het vak op 460px, maar dan werd de rij 873px en vulde die op een
|
||||
1080p-scherm het hele beeld.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` en de plannen.
|
||||
**Tests:** 21 goed, 0 fout; die raken de pagina niet. Nagemeten op 1760px en op mobiel, in beide gevallen
|
||||
zonder horizontale schuifbalk en zonder console-fouten.
|
||||
|
||||
## 19-08-2026 - kaders gelijkgetrokken, en een badge die niets zei
|
||||
|
||||
De drie statustegels hadden een eigen kleine grijze kop in kapitalen en de andere kaders een witte titel.
|
||||
Op verzoek is dat nu overal hetzelfde: een `card-head` met een titel in `t-h3`. Wel met de token
|
||||
`--text-primary` en niet met letterlijk wit, want in de lichte variant is wit onzichtbaar. Bij het
|
||||
nameten bleek het getal van "Backend response" 21px hoger te staan dan de andere twee, omdat daar een
|
||||
sparkline onder hangt in plaats van een tekstregel; de onderregel van een tegel heeft nu dezelfde hoogte
|
||||
als die sparkline.
|
||||
|
||||
De badge "Serving this page" is eruit. De gebruiker wees erop dat die niets zegt: de pagina wordt nooit
|
||||
getoond aan een wallet die op 50022 verbindt. Mijn gedachte erachter was dat de pagina en de
|
||||
TLS-terminatie in dezelfde container zitten en dat de een de ander dus bewijst, maar dat stond er niet en
|
||||
zo leest niemand het. Een badge waarvan de betekenis niet in één zin op te schrijven is, hoort er niet te
|
||||
staan.
|
||||
|
||||
Wat dat wel opleverde, en dat is als open punt 5 vastgelegd: **de app controleert of de Electrum-server
|
||||
antwoordt, maar niet of hij zelf antwoordt**, en dat is zijn enige taak. Een TLS-verbinding naar de eigen
|
||||
poort zou het luisteren, het certificaat en de doorverbinding in één keer bewijzen. Nog geen taak, want
|
||||
het raakt de agent en die heeft nog nooit gedraaid.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` en de plannen.
|
||||
**Tests:** 21 goed, 0 fout; die raken de pagina niet. Gemeten in beide thema's en op 1760px: zeven
|
||||
identieke koppen, drie getallen op één lijn, geen horizontale schuifbalk, geen console-fouten.
|
||||
|
||||
## 19-08-2026 - de vervallen-melding eruit
|
||||
|
||||
De gebruiker vond de melding "Readings are out of date" niet nuttig, en dat klopt: dezelfde mededeling
|
||||
staat al onder de blokhoogte ("as of ... ago"), in de badge bij de Electrum-server die grijs wordt met
|
||||
"last answered", en in de voettekst met het tijdstip. De banner was een vierde keer hetzelfde en wel de
|
||||
hardste.
|
||||
|
||||
De berekening blijft staan, want die stuurt de badge; alleen de melding is weg. Gecontroleerd met een
|
||||
`status.json` van een uur oud: geen enkele melding zichtbaar, badge grijs met "last answered 2 hours ago",
|
||||
"as of 2 hours ago" onder de blokhoogte, en de voettekst met datum en tijd. Er gaat dus geen informatie
|
||||
verloren.
|
||||
|
||||
De afweging staat nu in [PLAN.md](PLAN.md) §4a, omdat hij geldt voor elke volgende melding die iemand wil
|
||||
toevoegen: een banner is er voor iets wat je nergens anders ziet.
|
||||
|
||||
Wat de gebruiker zag was trouwens de preview en niet de app: die `status.json` is een vast bestand dat
|
||||
niets ververst, dus die verloopt altijd als je het tabblad open laat staan.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` en de plannen.
|
||||
**Tests:** 21 goed, 0 fout; die raken de pagina niet.
|
||||
|
||||
## 19-08-2026 - de pagina opnieuw ingedeeld, breed en met kaders naast elkaar
|
||||
|
||||
De gebruiker zag dat "Point your wallet here" en "Setting up your wallet" dezelfde vraag stelden. Die zijn
|
||||
er één geworden, en dat kader is naar onderen verhuisd. De volgorde is nu: Electrum-server, de drie
|
||||
statustegels, het activiteitenlog, de certificaatkeuze, en onderaan het instellen. De regel erachter is de
|
||||
moeite van het opschrijven waard, want hij beslist waar een volgend kader komt: eerst waar je naar kijkt
|
||||
als je iets nakomt, onderaan waar je naar kijkt als je iets instelt.
|
||||
|
||||
Op verzoek ook breed, met kaders naast elkaar: twaalf kolommen, rijen die 5/7 en 7/5 afwisselen, afgekapt
|
||||
op 1760px omdat een kader van 2500px regels oplevert die niemand leest. Belangrijk detail dat bewust zo is
|
||||
gebouwd: er staat geen `order` in de CSS, dus de leesvolgorde in de HTML is de bedoelde volgorde en
|
||||
stapelen op een smal scherm geeft exact dezelfde reeks.
|
||||
|
||||
Twee dingen die het meten opleverde en die anders waren blijven staan. De statustegels waren 146px naast een
|
||||
kader van 293px, dus stond er een gat rechtsboven; ze rekken nu mee en zetten hun inhoud onderaan. En het
|
||||
logblok kapte op een smal scherm de bytes af met puntjes; het scrollt nu zelf horizontaal, want stil
|
||||
verbergen is dezelfde soort onwaarheid als een verzonnen statuswaarde.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` en de plannen.
|
||||
**Tests:** de suite raakt de pagina niet; wel gedraaid en groen (21). Gecontroleerd via de afmetingen van
|
||||
de kaders op 1760px en op mobiel: volgorde gelijk, geen horizontale schuifbalk op de pagina, logblok
|
||||
scrollt zelf, geen console-fouten.
|
||||
**Niet geverifieerd:** ik heb de pagina niet met eigen ogen gezien; het browserpaneel stond dicht, dus dit
|
||||
is gemeten en niet bekeken.
|
||||
|
||||
## 19-08-2026 - een tweede container, en de eerste tests in dit project
|
||||
|
||||
De gebruiker vroeg of de WebDAV-truc de gebruikelijke manier is om een backend in een Umbrel-app te
|
||||
bouwen. Dat was hij niet, en het uitzoeken veranderde het ontwerp. umbrelOS biedt precies één instelling
|
||||
per app, de afhankelijkheidskeuze in `AppSettingsSchema`; apps met instelbare configuratie zijn zelf een
|
||||
backend, in hun eigen image. De gebruiker koos daarop een tweede container met een klein python-programma,
|
||||
uit een `*.template` zodat het bij een update meekomt, zonder eigen image.
|
||||
|
||||
Dat loste meer op dan de keuzelijst. De controles op `openssl`, `nc` en de WebDAV-module zijn alle drie
|
||||
vervallen, want de agent doet dat werk met de standaardbibliotheek. En het hardgecodeerde domein is weg uit
|
||||
`docker-compose.yml` en `nginx.conf.template`: de agent schrijft `cert.conf` en nginx doet daar een
|
||||
`include` op. Daarmee is dat deel van **Configuratie** fase 2 af.
|
||||
|
||||
Twee dingen die het bouwen opleverde. De standaardbibliotheek heeft geen X.509-parser, dus die is er nu:
|
||||
een DER-lezer voor de einddatum en de domeinnamen. Dat is precies het soort code dat niet faalt met een
|
||||
fout maar met een verkeerd antwoord, en een certificaatdatum die er een jaar naast zit valt nooit op.
|
||||
Daarom is hij getoetst tegen `ssl` op 74 echte CA-certificaten, op een levend servercertificaat voor de
|
||||
subjectAltName, en op beide tijdvormen met zelfgebouwde certificaten, want het CA-materiaal gebruikt
|
||||
uitsluitend UTCTime na 2000. Dit project had nog geen suite; die staat nu in `tests/` en `CLAUDE.md`
|
||||
vertelt hoe je hem draait.
|
||||
|
||||
Het tweede: bij het schrijven van de compose bleek mijn eigen `choose` het plan **Configuratie** §4c tegen
|
||||
te spreken. Die koos bij meerdere certificaten "de langst geldige", terwijl daar uitdrukkelijk staat dat er
|
||||
niet gegokt mag worden. Nu weigert de app en noemt de kandidaten, en de pagina zet er een foutmelding
|
||||
boven, want geen certificaat betekent geen TLS.
|
||||
|
||||
**Geraakt:** nieuw `whatsnext-electrum-gate/agent.py.template`, nieuw `tests/`, nieuw `CLAUDE.md`,
|
||||
gewijzigd `docker-compose.yml`, `nginx.conf.template`, `index.html.template`, en de plannen.
|
||||
**Tests:** 21 goed, 0 fout. Mutatietest gedaan op de twee guards: de keuze-validatie en de verloopfilter,
|
||||
allebei met de juiste enkele test die omvalt.
|
||||
**Niet geverifieerd:** de agent heeft nog nooit op de Umbrel gedraaid. Alles over de echte werking, dus de
|
||||
certificaatscan op de Zoraxy-map, de Electrum-vraag, het herladen via de vlag en de keuze via de API, staat
|
||||
nog open.
|
||||
|
||||
## 19-08-2026 - certificaatkeuze op het dashboard
|
||||
|
||||
De gebruiker vroeg hoe je aanwijst welk certificaat van welke app je wilt gebruiken, en koos daarbij voor
|
||||
een keuzelijst op het dashboard boven een sleutel in een configuratiebestand. Dat draait het niet-doel
|
||||
"geen instellingenscherm in de web-UI" terug, en dat is opgeschreven als uitzondering met de reden erbij:
|
||||
dit is de enige instelling waarvan de app de mogelijke waarden zelf al kent, want hij kijkt in de
|
||||
gemounte mappen.
|
||||
|
||||
Terugschrijven kan zonder backend met de WebDAV-module van nginx: één `location` die een `PUT` van
|
||||
hooguit een kilobyte aanneemt, naar `${APP_DATA_DIR}/config/` zodat de keuze een herstart overleeft.
|
||||
Of die module in de image zit, is de eerste van drie controles die nu op de Umbrel moeten gebeuren.
|
||||
|
||||
Twee dingen bewust níet gedaan. De mount voor Nginx Proxy Manager staat uitgecommentarieerd, want het pad
|
||||
is een gok en Docker maakt een ontbrekend bind-mountpad aan; dat zou een lege maphierarchie neerzetten in
|
||||
de app-data van een app die er misschien niet is. En de pagina meldt geen succes na het opslaan: nginx
|
||||
moet het certificaat nog herladen, en dat blijkt pas uit de volgende `status.json`.
|
||||
|
||||
**Geraakt:** `index.html.template`, `nginx.conf.template`, `docker-compose.yml`, en de plannen
|
||||
Webinterface en Configuratie.
|
||||
**Tests:** geen suite. In de lokale render gecontroleerd dat de lijst vier certificaten toont met het
|
||||
verlopen exemplaar als zodanig, dat het actieve aangevinkt staat, en dat de knop pas aangaat bij een
|
||||
andere keuze. **Het wegschrijven zelf is niet geprobeerd**, want daar is de Umbrel voor nodig.
|
||||
|
||||
## 19-08-2026 - dashboard uitgedund na de eerste blik
|
||||
|
||||
De gebruiker heeft de pagina bekeken en er drie dingen af gehaald: de grafiek blokken per uur, de tegel
|
||||
blokken laatste uur, en het kader over Tor tegenover TLS. De eerste twee beantwoorden een vraag over de
|
||||
Electrum-server en niet over deze proxy; de derde is positionering en hoort in de winkelbeschrijving,
|
||||
waar iemand nog staat te kiezen. Er blijven drie tegels over. `history` in `status.json` hoeft daardoor
|
||||
alleen nog de reactietijd te bewaren.
|
||||
|
||||
De badge bij de Electrum-server zei `answering`, en op de vraag wat dat betekende was er geen goed
|
||||
antwoord: het liet in het midden of dat nú gold of ooit. Hij noemt nu de meting en het moment, dus
|
||||
`answered 2 minutes ago`, `no answer 2 minutes ago` of `not checked yet`.
|
||||
|
||||
De activiteitenkaart is omgebouwd tot logregels onder elkaar in monospace, nieuwste bovenaan. Daarbij
|
||||
hoort een grens die op de pagina zelf staat: een regel per protocolaanroep kan niet, want die verzoeken
|
||||
zitten in de versleutelde verbinding en ze tellen zou betekenen dat de app het verkeer van de gebruiker
|
||||
uitleest.
|
||||
|
||||
Kopieerknoppen in een lijstregel verschijnen nu bij hover. Met `opacity` en niet met `display`, zodat ze
|
||||
met de tab-toets bereikbaar blijven; op aanraakschermen staan ze altijd aan.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` en de plannen.
|
||||
**Tests:** geen suite. In de lokale render gecontroleerd: drie tegels, zestien logregels, geen
|
||||
console-fouten, blokkengrafiek en Tor-kader weg.
|
||||
|
||||
## 19-08-2026 - plan werd actief, en de pagina is herbouwd
|
||||
|
||||
Gepromoveerd vanuit `Plannen/Masterplannen/` omdat het werk begon. De oude pagina beweerde `Online` als
|
||||
platte tekst in de HTML, had een verzonnen logvenster en noemde een hardgecodeerd domein. Die is
|
||||
vervangen door een pagina die alles uit `status.json` haalt en elk veld dat hij niet kent zichtbaar op
|
||||
`unknown` laat staan.
|
||||
|
||||
Twee dingen die het ontwerp veranderd hebben ten opzichte van het masterplan. De pagina is verhuisd van
|
||||
`web/index.html` naar `index.html.template` in de app-root: umbreld ververst bij een update alleen een
|
||||
whitelist, dus onder `web/` zou elke latere wijziging stilzwijgend niet aankomen en een herinstallatie
|
||||
kosten. En doordat de pagina niets meer hardgecodeerd heeft, is de afhankelijkheid van **Configuratie**
|
||||
vervallen; dat deel van Configuratie fase 2 is hiermee meteen af.
|
||||
|
||||
Op verzoek van de gebruiker zijn er drie dingen bijgekomen: verbindingsregels per client met een
|
||||
kopieerknop (uit `Referenties/Clients.md` §4), een korte eerlijke uitleg over Tor tegenover TLS, en een
|
||||
kaart voor wallet-activiteit die nu op `no data` staat zodat de vorm beoordeeld kan worden voordat er
|
||||
iets voor gebouwd wordt. Dat laatste is een herziening van het niet-doel "live logs", en de grens ligt
|
||||
bij een samenvatting per sessie zonder client-IP.
|
||||
|
||||
**Geraakt:** `whatsnext-electrum-gate/index.html.template` (was `web/index.html`),
|
||||
`nginx.conf.template`, `docker-compose.yml`, `umbrel-app.yml`.
|
||||
**Tests:** dit project heeft geen suite. Handmatig gecontroleerd in een lokale render met een
|
||||
voorbeeld-`status.json`: beide thema's, alle kaarten, geen console-fouten.
|
||||
**Nog niet geverifieerd:** alles wat een echte `status.json` vraagt, want die wordt nog niet geschreven
|
||||
(fase 2). De kopieerknop is niet achter de app-proxy van umbrelOS geprobeerd, en dat is juist het pad
|
||||
waar de terugval voor http gebruikt wordt.
|
||||
@@ -0,0 +1,210 @@
|
||||
# Taken - Webinterface
|
||||
|
||||
> Prioriteit: **A** | Wacht op: –
|
||||
>
|
||||
> **Van C naar B naar A op 20-08-2026, op één dag.** Eerst verviel de blokkade "de app geïnstalleerd en de
|
||||
> agent draaiend". Aan het eind van die dag is dit het enige plan met werk dat nú te doen is: **Appstore**
|
||||
> heeft alleen nog een herstart nodig die niet te plannen is, en **Publicatie** wacht op een publieke repo.
|
||||
> Het nummer is daarom van 020 naar 005 gegaan; alleen dit plan kreeg een nieuw nummer, Appstore houdt 010.
|
||||
>
|
||||
> Afgezakt van A naar C op 19-08-2026 bij het afsluiten van de sessie. De pagina is af voor zover dat
|
||||
> zonder de Umbrel kan; al het resterende werk in dit plan begint met kijken of de agent doet wat hij zou
|
||||
> moeten doen, en dat kan niet vóór de installatie.
|
||||
|
||||
## Volgende stap
|
||||
|
||||
- [x] **0.0.10 op een telefoon nagekeken. In orde (20-08-2026).** De gebruiker meldt dat het er goed uitziet
|
||||
op mobiel. Eén ding kwam eruit: de kopieerknoppen stonden niet op één lijn, omdat de knop door het
|
||||
omvouwen de lengte van het adres volgde. Verholpen in 0.0.11 met `margin-left: auto` op de knop.
|
||||
Bijvangst: `0.0.10` na `0.0.9` levert wél een update op, dus tweecijferige versiedelen zijn veilig
|
||||
|
||||
- [x] **De agent zien draaien. Gelukt op 20-08-2026, over drie versies.** De agent start en luistert, de
|
||||
pagina laadt, hij vond veertien certificaten in `/certs/zoraxy` en weigerde daarom te kiezen, een
|
||||
keuze op de pagina werd aangenomen, en daarna verbindt een wallet over 50022. De keten waar dit plan
|
||||
op wachtte is dus rond.
|
||||
|
||||
- [ ] **Het uploaden op het niet-gelukkige pad proberen**, het enige stuk van 0.0.7 dat nog niet in een
|
||||
browser gezien is: een sleutel die niet bij het certificaat hoort. Verwacht: een weigering met een
|
||||
leesbare reden, en niets dat achterblijft in `data/certs`. De guard is met echte sleutelparen getoetst
|
||||
en mutatie-getest, dus dit gaat over de weg van de melding naar de pagina en niet over de controle
|
||||
zelf. **Eigenaar: gebruiker**
|
||||
|
||||
- [x] **0.0.7 en 0.0.9 in de browser nagekeken. Gelukt (20-08-2026):** uploaden werkt, kiezen werkt, ook
|
||||
met de certificaten uit Zoraxy, de keuzelijst bij het installeren zag er goed uit en het
|
||||
versienummer op de pagina klopt. Daarmee is het uploadpad uit 0.0.7 bewezen op het gelukkige pad
|
||||
|
||||
- [x] **0.0.6 in de browser nagekeken (20-08-2026).** Ziet er goed uit volgens de gebruiker. Wat eruit
|
||||
kwam: de badges konden weg, "your choice" zei niets, Trezor Suite is niet desktop-only, en
|
||||
Blockstream Green heet Blockstream
|
||||
|
||||
- [x] **De pagina naast een werkende app gelegd (20-08-2026).** Blokhoogte en backend-reactietijd komen
|
||||
binnen. Wat eruit kwam: van een verbonden wallet was niets te zien, de keuzelijst was te druk, en
|
||||
een reeks uitleg-teksten kon weg. Alles verwerkt in 0.0.6
|
||||
|
||||
- [ ] **Het activiteitenlog een etmaal laten lopen** en dan kijken of het klopt: komt er een
|
||||
`connected`-regel bij een nieuwe wallet, en wat staat er na een nacht in. De teller is nieuw en is
|
||||
alleen tegen tijdelijke bestanden getoetst, niet tegen een echte wallet
|
||||
|
||||
Daarna, en niet eerder:
|
||||
|
||||
- [ ] Uitzoeken waar Nginx Proxy Manager op umbrelOS zijn certificaten neerzet. De mount staat
|
||||
uitgecommentarieerd in `docker-compose.yml`: een gok invullen zou Docker een lege maphierarchie
|
||||
laten aanmaken in de app-data van een app die er misschien niet eens is
|
||||
- [ ] Open punt 5: laten controleren of de TLS-poort zélf antwoordt. Dat is nu het enige wat de app niet
|
||||
over zichzelf weet, en het is zijn enige taak
|
||||
- [x] **Open punt 6: een certificaat uploaden via de pagina. Gebouwd in 0.0.7** (20-08-2026). Beschrijfbare
|
||||
mount voor de agent, een eigen nginx-locatie met een grotere limiet, en validatie met
|
||||
`load_cert_chain` voordat er iets geplaatst wordt. De bestandsnaam komt uit het certificaat zelf,
|
||||
dus padtrucs kunnen niet. **Nog niet in een browser geprobeerd**
|
||||
- [ ] Open punt 7: beslissen of Zoraxy een harde afhankelijkheid wordt. "Zoraxy of NPM" kan niet, en dat
|
||||
is op 20-08-2026 in de bron nagetrokken; alleen de wens staat nog open. Zie [OPEN.md](OPEN.md)
|
||||
punt 7
|
||||
- [ ] Fase 5, het activiteitenlog, als de gebruiker het wil
|
||||
|
||||
De drie controles op `openssl`, `nc` en de WebDAV-module zijn **vervallen** met de komst van de agent: die
|
||||
doet dat werk zelf met de standaardbibliotheek. Zie [PLAN.md](PLAN.md) §4a0.
|
||||
|
||||
Daarna, in deze volgorde:
|
||||
|
||||
- [ ] Fase 2 bouwen op de uitkomst van die controle
|
||||
- [ ] Fase 5, als de gebruiker de wallet-activiteit wil
|
||||
|
||||
## Fase 1 - Het liegen eruit
|
||||
|
||||
- [x] De verzonnen statusblokken (`Online`, `Online`) en het verzonnen logvenster weg
|
||||
- [x] Het hardgecodeerde domein weg. De pagina bevat geen enkele installatiespecifieke waarde meer
|
||||
- [x] Taal naar Engels, zie [OPEN.md](OPEN.md) punt 2
|
||||
- [x] Opnieuw opgebouwd op het design-systeem: tokens, `card`, `badge`, `alert`, `list-item`, `btn`,
|
||||
donker met een lichte variant en een schakelaar
|
||||
- [x] **De pagina verhuisd naar `index.html.template` in de app-root.** Onder `web/` zou elke latere
|
||||
wijziging een herinstallatie kosten: umbreld ververst bij een update alleen een whitelist en die
|
||||
kijkt niet in submappen. Als template zit hij in de whitelist én wordt hij bij elke start ingevuld
|
||||
|
||||
## Fase 2 - De agent schrijft `status.json`
|
||||
|
||||
Herzien 19-08-2026: dit is een tweede container met een python-programma geworden in plaats van een
|
||||
shell-lus in de compose. Onderbouwing in [PLAN.md](PLAN.md) §4a0.
|
||||
|
||||
- [x] `agent.py.template`, met de instellingen uit de omgeving in plaats van uit template-invulling.
|
||||
Daardoor staat er geen accolade-variabele in en blijft het geldige Python, dus is het te importeren
|
||||
in een test. De eerste toets in `tests/` controleert precies die aanname
|
||||
- [x] Schrijft `status.json` atomair via een tijdelijk bestand en `os.replace`, want de pagina leest het
|
||||
elke minuut en een half geschreven bestand geeft een lege pagina
|
||||
- [x] Einddatum van het certificaat met een eigen DER-lezer. Getoetst tegen `ssl` op 74 echte
|
||||
CA-certificaten, op een levend servercertificaat, en op beide tijdvormen
|
||||
- [x] Blokhoogte en reactietijd via `blockchain.headers.subscribe` over een verse socket
|
||||
- [x] Een `history`-reeks bijhouden, afgekapt op 24 uur, met alléén de reactietijd. De sparkline is sinds
|
||||
19-08-2026 de enige afnemer
|
||||
- [x] Herkennen welke backend het is aan de hand van het adres, en `unknown` als hij niet in de tabel
|
||||
staat. Zie [PLAN.md](PLAN.md) §4c: liever minder informatief dan onwaar
|
||||
- [x] De lus valt nooit stil: een mislukte ronde wordt gelogd en overgeslagen. Stoppen zou de pagina op
|
||||
oude gegevens bevriezen, en dat is precies het liegen dat dit plan moest afschaffen
|
||||
- [ ] Draaien op de Umbrel. Niets hiervan is buiten de tests uitgevoerd
|
||||
|
||||
## Fase 3 - De pagina vult zichzelf
|
||||
|
||||
- [x] `fetch` op `status.json`, elke minuut
|
||||
- [x] Elk veld heeft een zichtbare onbekend-toestand; nooit een leeg veld, want dat leest als "in orde"
|
||||
- [x] "Laatst bijgewerkt" tonen, en oude gegevens als zodanig laten zien zodra ze ouder zijn dan twee
|
||||
schrijfronden. **Bijgesteld 19-08-2026 op verzoek van de gebruiker:** dat gebeurt niet meer met een
|
||||
melding bovenaan de pagina maar alleen bij de waarden zelf, want die melding zei voor de vierde keer
|
||||
wat er al onder de blokhoogte, in de badge bij de Electrum-server en in de voettekst stond
|
||||
- [x] Leesbare melding als het bestand ontbreekt, zonder dat het op een fout lijkt.
|
||||
Zie [OPEN.md](OPEN.md) punt 1
|
||||
- [ ] Verifiëren tegen een échte `status.json` in plaats van tegen de voorbeeldversie uit de preview
|
||||
|
||||
## Fase 4a - De indeling
|
||||
|
||||
Vastgesteld door de gebruiker op 19-08-2026 na het bekijken van de eerste versie. Ontwerp in
|
||||
[PLAN.md](PLAN.md) §4a1.
|
||||
|
||||
- [x] Volgorde: Electrum-server, drie statustegels, activiteitenlog, certificaatkeuze, instellen van je
|
||||
wallet
|
||||
- [x] "Point your wallet here" opgegaan in "Setting up your wallet"; het waren dezelfde kaders
|
||||
- [x] Breed met een raster van twaalf kolommen, rijen die 5/7 en 7/5 afwisselen, afgekapt op 1760px
|
||||
- [x] De leesvolgorde in de HTML is de bedoelde volgorde, zonder `order` in de CSS, zodat stapelen op een
|
||||
smal scherm dezelfde volgorde geeft. Gecontroleerd op 1760px en op mobiel
|
||||
- [x] Statustegels rekken mee met de hoogte van hun buur, anders staat er een gat rechtsboven
|
||||
- [x] Het logblok scrollt zelf horizontaal in plaats van de regel met puntjes af te kappen
|
||||
- [x] **Alle kaders hetzelfde opgebouwd** (19-08-2026): een `card-head` met een titel in `t-h3`, dan de
|
||||
inhoud. De drie statustegels hadden een eigen kleine grijze kop in kapitalen. De titel gebruikt de
|
||||
token `--text-primary`, niet letterlijk wit, anders is hij onzichtbaar in de lichte variant
|
||||
- [x] De onderregel van een statustegel is even hoog als de sparkline, zodat de drie getallen op één lijn
|
||||
staan. Gecontroleerd: koppen op y=153, getallen op y=311
|
||||
- [x] **De badge "Serving this page" verwijderd** (19-08-2026, op aanwijzing van de gebruiker). Zie
|
||||
[OPEN.md](OPEN.md) punt 5 voor wat er wél op die plek zou horen
|
||||
- [x] **De voetregel verwijderd** (19-08-2026). Daarin stond "Last reading ... ago" plus het versienummer;
|
||||
het eerste stond al bij de waarden zelf
|
||||
- [x] **Het versienummer teruggezet**, klein achter de tagline als `v0.0.3`. Dezelfde dag op verzoek van de
|
||||
gebruiker. In `--text-sec` en niet in `--text-ter`, want die laatste is in de lichte variant `#bbbbbb`
|
||||
op wit en op deze grootte onleesbaar
|
||||
- [x] **De certificaatlijst scrollt**, met het aantal in de kop. Een gedeelde certificatenmap kan er
|
||||
tientallen bevatten. Gecontroleerd met 19 certificaten: 1062px inhoud in een vak van 360px, knop
|
||||
blijft eronder staan
|
||||
- [x] **Het logblok en het certificaatkader zijn even hoog.** Het langste kader van de rij bepaalt de
|
||||
hoogte, en wat mag meegroeien zegt dat zelf met flex. Gemeten: 636px bij vier certificaten, 773px bij
|
||||
negentien, in beide gevallen allebei gelijk
|
||||
- [x] **Certificaat en activiteitenlog omgedraaid** (19-08-2026, op verzoek). Certificaat links en smal,
|
||||
log rechts en breed. Dat pakt twee kanten goed uit: de kolomgrens valt nu samen met die van de rij
|
||||
erboven (gemeten: x=738 in beide rijen), en monospace logregels hebben de breedte beter nodig
|
||||
- [x] **Het losse verbindingsadres met kopieerknop eruit**, plus het label "tested clients". Zonder
|
||||
verlies: hetzelfde adres staat bij elke clientregel, in de vorm die díe client wil, met een
|
||||
kopieerknop erbij. Het instelkader werd daarmee 533px in plaats van 610px
|
||||
- [ ] Nog niet met eigen ogen gezien op een echt breed scherm; gecontroleerd via de afmetingen van de
|
||||
kaders, niet visueel, want het browserpaneel stond dicht
|
||||
|
||||
## Fase 4 - Bruikbaar in plaats van alleen eerlijk
|
||||
|
||||
- [x] Verloopwaarschuwing onder de dertig dagen, en een foutmelding als het certificaat verlopen is
|
||||
- [x] Kopieerknop op het verbindingsadres, met terugval voor http zonder de clipboard-API
|
||||
- [x] Verbindingsregels per client, met kopieerknop per regel. Bron: `Referenties/Clients.md` §4
|
||||
- [x] Kopieerknoppen verschijnen bij hover over de regel. Met `opacity` en niet met `display`, zodat ze
|
||||
met de tab-toets bereikbaar blijven, plus `focus-within` en altijd zichtbaar op aanraakschermen
|
||||
- [ ] De kopieerknop echt uitproberen achter de app-proxy van umbrelOS. Die draait over http, dus de
|
||||
terugval met `execCommand` is daar het pad dat gebruikt wordt en niet de uitzondering. Wat op
|
||||
19-08-2026 lokaal wél bewezen is: de **faalroute** meldt "Press Ctrl+C" in plaats van stil niets te
|
||||
doen. De geslaagde route vraagt een echte muisklik, want de clipboard-API weigert een klik die uit
|
||||
een script komt
|
||||
|
||||
## Fase 4b - De certificaatkeuze
|
||||
|
||||
Besloten 19-08-2026 door de gebruiker: een keuzelijst op het dashboard in plaats van een sleutel in het
|
||||
configuratiebestand. Het ontwerp hoort bij het plan **Configuratie** §4e en §4f; hier staat alleen wat de
|
||||
pagina en de compose ervoor doen.
|
||||
|
||||
- [x] Keuzelijst op de pagina, met per certificaat de bron, de bestandsnaam, het domein en het aantal
|
||||
resterende dagen. Een verlopen certificaat staat er rood bij en wordt niet verborgen: het bestaat,
|
||||
en het kiezen ervan moet een zichtbare vergissing zijn en geen onvindbare
|
||||
- [x] `PUT` naar `api/certificate`, doorgestuurd naar de agent. De WebDAV-truc is vervallen: de agent
|
||||
neemt de keuze aan en kan hem ook controleren, wat WebDAV niet kon
|
||||
- [x] De pagina meldt níet zelf dat het gelukt is. Nginx moet het certificaat nog herladen, en dat blijkt
|
||||
pas uit de volgende `status.json`
|
||||
- [x] De Zoraxy-mount van `/certs` naar `/certs/zoraxy`, en `/certs/own` erbij, zodat er per bron een map
|
||||
is. Beide containers hebben ze nodig: de agent om te kiezen, nginx om het bestand te openen
|
||||
- [x] `certificates` in `status.json`, met per certificaat de bron, de naam, het domein uit het
|
||||
certificaat en de einddatum
|
||||
- [x] **De guard: een id die de agent niet zelf gevonden heeft, wordt geweigerd.** Zowel bij de `PUT` als
|
||||
bij het kiezen. Getest, inclusief de mutatietest
|
||||
- [x] **Bij meerdere kandidaten kiest de app niet.** Bijgesteld nadat bleek dat mijn eerste versie "de
|
||||
langst geldige" pakte, wat het plan **Configuratie** §4c uitdrukkelijk verbiedt: een verkeerd
|
||||
certificaat geeft een verbinding die het lijkt te doen en bij de wallet stukloopt op
|
||||
naamverificatie. De pagina toont dan een foutmelding met de kandidaten erin
|
||||
- [x] De pagina toont bovenaan een foutmelding als er geen certificaat actief is, want dan is er geen TLS
|
||||
en dat is het ergste wat deze app kan overkomen
|
||||
|
||||
## Fase 5 - Het activiteitenlog
|
||||
|
||||
Voorstel, nog niet besloten. Ontwerp in [PLAN.md](PLAN.md) §4e. De kaart staat al op de pagina en toont
|
||||
`no data`, zodat de vorm te beoordelen is voordat er iets voor gebouwd wordt.
|
||||
|
||||
- [x] Vorm: logregels onder elkaar, monospace, nieuwste bovenaan, met de grens erbij vermeld dat een
|
||||
regel per protocolaanroep niet kan zonder het verkeer van de gebruiker uit te lezen
|
||||
- [ ] Uitzoeken of een `log_format` met dollartekens langs de template-invulling te krijgen is, via een
|
||||
`include` die het `command`-blok wegschrijft
|
||||
- [ ] Uitzoeken hoe de bytetellers van een lopende verbinding in de container af te lezen zijn; nginx
|
||||
logt een stream-sessie pas bij het sluiten
|
||||
- [ ] `log` in `status.json`, zonder client-IP en zonder bronpoort
|
||||
|
||||
## Geblokkeerd / wacht op
|
||||
|
||||
- [ ] Niets. De afhankelijkheid van **Configuratie** is vervallen, zie [PLAN.md](PLAN.md)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Open punten - Proefopstelling
|
||||
|
||||
> Beslissingen die nog een **eigenaar** of een **moment** nodig hebben. Staat een punt hier zonder
|
||||
> allebei, dan is dat de eerste fout om op te lossen. Wordt een punt een taak, dan verhuist het naar
|
||||
> [TAKEN.md](TAKEN.md).
|
||||
>
|
||||
> **Nummers blijven staan**, ook als een punt beslist is: er kan elders naar verwezen worden, ook vanuit
|
||||
> codecommentaar. Beslissen betekent verplaatsen naar de kop hieronder, niet hernummeren.
|
||||
|
||||
## Nog te beslissen
|
||||
|
||||
1. **Waar draait de proefopstelling?**
|
||||
Er zijn twee plekken en ze meten niet hetzelfde. Een machine met Docker onder handbereik is het
|
||||
snelst en het makkelijkst opruimen. De Umbrel zelf lijkt dichter bij het doel, maar dat is
|
||||
schijnnauwkeurigheid: je draait dan nog steeds niet als umbrelOS-app, je hebt wel meteen last van
|
||||
poortbotsingen met wat er al draait, en een mislukte poging laat rommel achter op een
|
||||
productiemachine.
|
||||
Voorstel: de losse machine, tenzij er een reden is dat het daar niet kan. De echte controle op de
|
||||
Umbrel hoort bij het masterplan **Umbrelapp**.
|
||||
**Moment:** voor fase 2 · **Eigenaar:** gebruiker
|
||||
|
||||
2. **Welke Trezor Suite telt?**
|
||||
Desktop, web en mobiel zijn drie verschillende programma's, en het is niet gegeven dat ze alle drie
|
||||
een eigen sync-server accepteren. Het antwoord bepaalt wat "het werkt" betekent, en het bepaalt ook
|
||||
het masterplan **Bereikbaarheid**: alleen desktop op het thuisnetwerk vraagt veel minder dan een
|
||||
telefoon onderweg.
|
||||
**Moment:** valt samen met de "Volgende stap" van fase 1 · **Eigenaar:** gebruiker
|
||||
|
||||
3. **Is er een tweede apparaat om mee te synchroniseren?**
|
||||
Fase 3 heeft er een nodig, want dat is de enige controle die bewijst dat de relay doet waarvoor hij
|
||||
bestaat. Een tweede installatie van Suite op dezelfde machine kan misschien ook, maar dat is niet
|
||||
uitgezocht en het is zwakker bewijs: dezelfde machine, hetzelfde netwerk.
|
||||
**Moment:** voor fase 3 · **Eigenaar:** gebruiker
|
||||
|
||||
4. **Wat als de quota-manager verplicht blijkt én zelf een externe dienst nodig heeft?**
|
||||
Dan is dit geen pakketteerprobleem meer. Denkrichtingen, niet in volgorde: de quota-manager mee
|
||||
pakketteren met een minimale configuratie die niets betaalt; uitzoeken of er een schakelaar is die de
|
||||
controle uitzet; of concluderen dat zelf hosten niet bedoeld is en het project hier stoppen. Dat
|
||||
laatste is een geldige uitkomst en zou de goedkoopste zijn die dit plan kan opleveren.
|
||||
**Moment:** zodra fase 1 of fase 2 het antwoord geeft · **Eigenaar:** gebruiker beslist, op basis van
|
||||
wat er dan bekend is
|
||||
|
||||
5. **Wordt de proefopstelling zelf vastgelegd in deze repo?**
|
||||
Een `compose/`-map met de gebruikte compose en een `.env.sample` maakt het herhaalbaar, en dat is
|
||||
veel waard als er over twee weken pas verder gewerkt wordt. Er zit een prijs aan: deze repo wordt
|
||||
publiek, dus er mag geen enkel echt geheim in, en een tweede compose naast die van het pakket kan
|
||||
later verwarren welke de echte is.
|
||||
Voorstel: wel, in een map die duidelijk `proefopstelling/` heet, met uitsluitend een `.env.sample`
|
||||
en nooit een `.env`.
|
||||
**Moment:** bij de eerste geslaagde start in fase 2 · **Eigenaar:** gebruiker
|
||||
@@ -0,0 +1,89 @@
|
||||
# Proefopstelling - plan
|
||||
|
||||
> Ontwerp en afbakening. **Dit bestand lees je zelden**, alleen bij twijfel over scope of architectuur.
|
||||
> Status staat in [TAKEN.md](TAKEN.md), geschiedenis in [PROGRESS.md](PROGRESS.md), onbesliste punten in
|
||||
> [OPEN.md](OPEN.md).
|
||||
|
||||
## 1. Doel
|
||||
|
||||
Uitzoeken of Evolu Relay zelf gehost bruikbaar is voor Trezor Suite, en met welke **minimale** set
|
||||
containers en omgevingsvariabelen. Dat gebeurt lokaal, buiten Umbrel om, want een pakket bouwen voor iets
|
||||
waarvan je niet weet of het draait is de dure volgorde.
|
||||
|
||||
Als dit plan af is, is er één zin die het volgende plan kan aannemen: "de relay draait met deze containers
|
||||
en deze variabelen, en Trezor Suite synchroniseert ermee." Zonder die zin is elk manifest een gok.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
Alles tot en met een label dat op apparaat A gezet wordt en op apparaat B verschijnt, via een relay die op
|
||||
het thuisnetwerk draait en die niets van Trezor nodig heeft.
|
||||
|
||||
Binnen dit plan valt ook het opschrijven van wat er nodig bleek: welke variabelen, welke poorten, welke
|
||||
volumes, en waar de images vandaan komen. Dat is niet de bijvangst maar het eigenlijke product; het
|
||||
volgende plan leest het.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Geen `umbrel-app.yml` en geen `docker-compose.yml` in Umbrel-vorm.** Dat is het masterplan
|
||||
**Umbrelapp**, en het heeft de uitkomst van dit plan nodig.
|
||||
- **Geen bereikbaarheid van buiten, geen TLS, geen Tailscale.** Masterplan **Bereikbaarheid**. Hier
|
||||
volstaat een IP op het eigen netwerk.
|
||||
- **Geen eigen image bouwen of publiceren.** Blijkt dat nodig, dan is dat een bevinding van dit plan en
|
||||
werk van het volgende.
|
||||
- **Niets met quota's of betalen.** De quota-manager is hier een obstakel dat je wegwerkt of moet
|
||||
meenemen, geen functionaliteit die we willen.
|
||||
- **Geen bijdrage aan `trezor/trezor-suite-sync`.** Ook niet als er onderweg iets stuk blijkt.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a. De volgorde is: eerst wat het project kan doden
|
||||
|
||||
Het vooronderzoek zet "clone en draai" als eerste stap. Dat is niet de goedkoopste weerlegging. Er zijn
|
||||
twee aannames waarop dit project stukloopt, en de eerste kost een minuut:
|
||||
|
||||
1. **Kan Trezor Suite überhaupt naar een eigen relay wijzen?** Het vooronderzoek gaat uit van een
|
||||
"Custom server"-veld. Bestaat dat niet in de Suite-versie van de gebruiker, of alleen op een platform
|
||||
dat hij niet gebruikt, dan is er niets te pakketteren. Dit is te controleren in de interface, zonder
|
||||
iets te installeren.
|
||||
2. **Is de quota-manager verplicht?** Zie [Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md)
|
||||
§3. Dit is te lézen in `.env.sample`, de compose en de broncode, en pas daarna te bewijzen door hem weg
|
||||
te laten.
|
||||
|
||||
Pas als die twee goed staan, is "clone en draai" de moeite waard.
|
||||
|
||||
### 4b. Wat er gedraaid wordt
|
||||
|
||||
De compose van Trezor zelf, ongewijzigd waar het kan, met de quota-manager uitgeschakeld. Ongewijzigd is
|
||||
hier een doel op zich: elke aanpassing die je maakt, is een aanpassing waarvan je later niet meer weet of
|
||||
hij nodig was. Werkt het niet zonder wijziging, dan is de wijziging zelf een bevinding.
|
||||
|
||||
### 4c. Wat er opgeschreven wordt
|
||||
|
||||
Per container: image en herkomst, poorten, volumes, en de omgevingsvariabelen die **echt** nodig bleken,
|
||||
niet de hele `.env.sample`. Dat onderscheid is het verschil tussen een manifest van tien regels en een van
|
||||
veertig, en het is achteraf niet meer te maken.
|
||||
|
||||
Daarbij twee dingen die het volgende plan hard nodig heeft en die je alleen hier tegenkomt: **komt de
|
||||
image uit een registry of alleen uit een Dockerfile**, en **hoe authenticeert Suite zich tegen de relay**.
|
||||
Die tweede bepaalt of de app achter de inlog van umbrelOS kan staan; zie het masterplan **Umbrelapp**,
|
||||
§4b.
|
||||
|
||||
## 5. Raakvlakken
|
||||
|
||||
- **Umbrelapp** is hard afhankelijk van dit plan: het aantal containers, de variabelen en de
|
||||
authenticatievraag komen hier vandaan.
|
||||
- **Bereikbaarheid** leunt op één bevinding hier: accepteert Suite een `http://`-adres, of eist het TLS?
|
||||
- **Publicatie** leunt op de herkomst van de image. Zonder een gepinde multi-arch image uit een registry
|
||||
komt dat plan niet van de grond.
|
||||
|
||||
## 6. Verificatie
|
||||
|
||||
Wat als bewijs telt, in oplopende sterkte:
|
||||
|
||||
1. de relay start en blijft draaien zonder quota-manager;
|
||||
2. Trezor Suite accepteert het adres en meldt geen fout;
|
||||
3. een label dat op apparaat A gezet wordt, verschijnt op apparaat B na een synchronisatie. **Dit is de
|
||||
enige die telt.** De eerste twee kunnen slagen terwijl er niets gesynchroniseerd wordt.
|
||||
|
||||
Wat hier per se **niet** bewezen wordt, en wat dus niet als "werkt" gemeld mag worden: gedrag op arm64,
|
||||
overleven van een herstart, gedrag onder umbrelOS, en bereikbaarheid van buiten het thuisnetwerk.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Voortgang - Proefopstelling
|
||||
|
||||
> 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).
|
||||
|
||||
## 25-08-2026 - plan werd actief
|
||||
|
||||
Dit project bestond uit één document, `trezor-suite-sync-umbrel-app-plan.md`, met het vooronderzoek van
|
||||
dezelfde dag erin. Dat is bij het inrichten van de repo over vier plannen verdeeld; dit is het enige dat
|
||||
meteen actief werd, omdat het als enige nu al iets te doen heeft en omdat de andere drie er hard van
|
||||
afhangen. Het origineel staat ongewijzigd in `Plannen/Masterplannen/Archief/Vooronderzoek.PLAN.md`, want
|
||||
het is de bron onder elke regel die met "onderzocht 25-08-2026" gemerkt is.
|
||||
|
||||
**Eén ding is bij het verdelen omgedraaid.** Het vooronderzoek zet "clone en draai" als eerste stap. Er
|
||||
zit een goedkopere weerlegging vóór: of Trezor Suite überhaupt naar een eigen sync-server kan wijzen. Dat
|
||||
kost een minuut in de interface, en is het antwoord nee, dan is er niets te pakketteren. Dat staat nu als
|
||||
"Volgende stap"; de clone is fase 2.
|
||||
|
||||
**De remote is anoniem bereikbaar, en dat is hier een eis en geen hygiëne.** Getest met de
|
||||
credential-helper leeg gezet, want een gewone `ls-remote` slaagt op deze machine ook als de repo dicht
|
||||
staat. De repo is nog leeg, dus dit bewijst bereikbaarheid en nog niet dat umbreld de store-inhoud kan
|
||||
ophalen; die controle hoort bij het masterplan **Umbrelapp**.
|
||||
|
||||
**Geraakt:** de hele `Docs/`-boom, `.gitignore`, `.gitattributes`, `CLAUDE.md`, `README.md`.
|
||||
**Tests:** niet van toepassing, er is nog geen bron. Zie `Docs/README.md`, "Projectspecifiek".
|
||||
@@ -0,0 +1,58 @@
|
||||
# Taken - Proefopstelling
|
||||
|
||||
> Prioriteit: **A** | Afhankelijk van: -
|
||||
>
|
||||
> Actief sinds 25-08-2026, bij het inrichten van dit project. Dit is het enige plan met een tier: de drie
|
||||
> masterplannen wachten allemaal op de uitkomst hiervan.
|
||||
|
||||
## Volgende stap
|
||||
|
||||
- [ ] **Controleren of Trezor Suite een eigen sync-server accepteert, en op welk platform.** Dit is de
|
||||
goedkoopste weerlegging van het hele project en het kost een minuut in de interface. Noteer waar het
|
||||
veld staat, hoe het adres eruit moet zien (met of zonder schema, met of zonder poort), en of het
|
||||
alleen op desktop bestaat. **Eigenaar: gebruiker**
|
||||
|
||||
## Fase 1 - Kan dit überhaupt
|
||||
|
||||
- [ ] Het veld voor een eigen sync-server in Trezor Suite gevonden, met de vorm die het verwacht
|
||||
- [ ] `.env.sample`, `docker-compose.yaml` en de relay-broncode van `trezor/trezor-suite-sync` gelezen op
|
||||
de vraag of de quota-manager verplicht is. **Lezen, nog niet draaien:** als het antwoord in de
|
||||
broncode staat, scheelt dat een halve middag proberen
|
||||
- [ ] Vastgesteld of Trezor een image publiceert of alleen een Dockerfile levert. Zie
|
||||
[Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md) §4 punt 1; dit bepaalt of
|
||||
het masterplan **Publicatie** haalbaar is
|
||||
|
||||
## Fase 2 - De stack lokaal draaien
|
||||
|
||||
- [ ] `trezor/trezor-suite-sync` gekloond
|
||||
- [ ] `docker compose up` met **alleen** Postgres en `evolu-relay`, de quota-manager weggelaten
|
||||
- [ ] De relay antwoordt op poort 4000 en blijft draaien. Blijft hij niet draaien, dan is de foutmelding
|
||||
de bevinding: schrijf hem letterlijk op in [PROGRESS.md](PROGRESS.md)
|
||||
- [ ] **Blocker-check:** is de relay hard afhankelijk van de quota-manager? Zo ja, die erbij en
|
||||
uitzoeken wat hij zelf nodig heeft. Vraagt hij een externe dienst, dan is dat geen taak meer maar
|
||||
open punt 4 in [OPEN.md](OPEN.md)
|
||||
|
||||
## Fase 3 - Echt synchroniseren
|
||||
|
||||
- [ ] Trezor Suite op apparaat A naar `http://<ip-van-de-machine>:4000` laten wijzen
|
||||
- [ ] Een label toevoegen op apparaat A
|
||||
- [ ] Datzelfde label zien verschijnen op apparaat B. **Dit is de enige controle die telt**; de twee
|
||||
hierboven kunnen slagen terwijl er niets gesynchroniseerd wordt
|
||||
- [ ] Geprobeerd wat er gebeurt als de relay even weg is en terugkomt. Niet omdat het nu moet werken,
|
||||
maar omdat het gedrag straks op een Umbrel bij elke update voorkomt
|
||||
|
||||
## Fase 4 - Vastleggen wat het pakket moet worden
|
||||
|
||||
- [ ] Per container opgeschreven: image en herkomst, poorten, volumes, en de variabelen die **echt** nodig
|
||||
bleken. Niet de hele `.env.sample` overnemen
|
||||
- [ ] Opgeschreven hoe Trezor Suite zich tegen de relay authenticeert, of dat het helemaal niet doet. Dit
|
||||
bepaalt of de app achter de inlog van umbrelOS kan staan; zie het masterplan **Umbrelapp**, §4b
|
||||
- [ ] Opgeschreven of Suite een `http://`-adres accepteert of TLS eist. Hier hangt het masterplan
|
||||
**Bereikbaarheid** aan
|
||||
- [ ] [Upstream-evolu-relay.md](../../../Referenties/Upstream-evolu-relay.md) bijgewerkt: alles wat daar
|
||||
als "onderzocht 25-08-2026" staat en nu bevestigd of weerlegd is, met de nieuwe datum erbij
|
||||
- [ ] Het masterplan **Umbrelapp** herzien met wat hier uitkwam, en pas daarna promoveren
|
||||
|
||||
## Geblokkeerd / wacht op
|
||||
|
||||
- [ ] Fase 3 wacht op een tweede apparaat met Trezor Suite. Zie [OPEN.md](OPEN.md) punt 3
|
||||
@@ -0,0 +1,144 @@
|
||||
# Open punten - Appstore
|
||||
|
||||
> Beslissingen die nog een **eigenaar** of een **moment** nodig hebben. Staat een punt hier zonder
|
||||
> allebei, dan is dat de eerste fout om op te lossen. Wordt een punt een taak, dan verhuist het naar
|
||||
> [TAKEN.md](TAKEN.md).
|
||||
>
|
||||
> **Nummers blijven staan**, ook als een punt beslist is: er kan elders naar verwezen worden, ook vanuit
|
||||
> codecommentaar. Beslissen betekent verplaatsen naar de kop hieronder, niet hernummeren.
|
||||
|
||||
## Nog te beslissen
|
||||
|
||||
2. **Wat gebeurt er met de bestaande handmatige installatie?** - **In twee stappen, en de eerste is niet
|
||||
meer uit te stellen** (bijgewerkt 20-08-2026).
|
||||
|
||||
De container is gestopt maar de compose staat er nog: een `alpine:latest` met een `apk add stunnel` bij
|
||||
elke start, met `restart: unless-stopped` en een mount op de certificaten van Zoraxy. Wat er nu aan de
|
||||
orde is:
|
||||
|
||||
- **de container weghalen, nu.** Hij bindt poort **50002**, en dat is precies de poort die Fulcrum op de
|
||||
host wil. Zolang hij bestaat kan het omschakelen naar Fulcrum niet slagen, en dat staat in fase 6 als
|
||||
taak. `restart: unless-stopped` betekent bovendien dat hij een herstart van de Umbrel overleeft;
|
||||
- **de map laten staan tot de herstart-controle gedaan is.** Dat is een compose plus een
|
||||
`entrypoint.sh` en het kost niets. Het is de gedocumenteerde weg terug, en die vervalt pas als
|
||||
bewezen is dat de app na een herstart vanzelf terugkomt. Let op de volgorde: de container mount zijn
|
||||
`entrypoint.sh` uit die map, dus eerst de container weg en dan de map.
|
||||
|
||||
**Moment:** stap 1 nu, stap 2 na de herstart-controle · **Eigenaar:** gebruiker
|
||||
|
||||
4. **Eigen icoon en gallery.**
|
||||
Het manifest wijst nu naar het Electrs-icoon en een screenshot van Electrs in `getumbrel/umbrel-apps`,
|
||||
dus naar andermans bestanden en naar plaatjes van een andere app. Er moet een eigen SVG komen. De
|
||||
gallery is minder dringend: die is er voor een winkelpagina die hier niemand bezoekt, zie
|
||||
[KNOWLEDGE.md](../../../KNOWLEDGE.md). Vraag is wie het icoon maakt en of het in de repo komt of extern
|
||||
gehost wordt.
|
||||
**Moment:** fase 5 · **Eigenaar:** gebruiker
|
||||
|
||||
7. **Hoe wordt de herstart-controle alsnog gedaan?**
|
||||
Dit is de controle waar het hele plan om begon: komt de app na een herstart van de Umbrel vanzelf
|
||||
terug. Hij is op 18-08-2026 uitgesteld omdat de Umbrel een productiemachine is en niet op verzoek
|
||||
herstart wordt. Dat is een geldige reden, maar het betekent wel dat het plan **niet afgerond kan
|
||||
worden**: alles wat er nu ligt is een sterke aanwijzing en geen bewijs.
|
||||
|
||||
Wat er specifiek niet mee getest is, en waar de enige echte twijfel zit: de **volgorde bij het
|
||||
opstarten**. Zoraxy kan later klaar zijn dan deze app, en dan bestaat het certificaat nog niet.
|
||||
|
||||
Bijgewerkt op 20-08-2026: hier stond dat de compose daarvoor een wachtlus heeft. Die is er niet meer,
|
||||
want juist die lus was de klem van 0.0.3. De opvolger is de vangnettak in de herlaadlus, die TLS
|
||||
aanzet zodra `cert.conf` verschijnt zonder dat er een vlag bij hoort. Dat is precies het
|
||||
Zoraxy-is-later-klaar-geval, en het is nog nooit echt voorgekomen; alleen getoetst tegen tijdelijke
|
||||
bestanden.
|
||||
|
||||
**Moment:** bij de eerstvolgende herstart die er toch komt, bijvoorbeeld een umbrelOS-update of een
|
||||
stroomonderbreking. Noteer de uitkomst dan in `PROGRESS.md`. · **Eigenaar:** gebruiker
|
||||
|
||||
9. **Volgt de app-map de conventie van andere apps?** Gevraagd door de gebruiker op 20-08-2026, die
|
||||
opmerkte dat hij bij andere apps geen app-code in `app-data` ziet. Uitgezocht en vastgelegd in
|
||||
[Umbrel-appstore-spec.md](../../../Referenties/Umbrel-appstore-spec.md); de korte versie is dat
|
||||
config-in-app-data een bestaand patroon is (`electrs` mount zijn `torrc` zo) en dat code-in-app-data
|
||||
het gevolg is van geen eigen image bouwen, wat een besluit is en geen ongeluk.
|
||||
|
||||
**Beide vervolgpunten zijn diezelfde dag gedaan in 0.0.9**, nadat de gebruiker zei dat hij de app wil
|
||||
publiceren als standaard-app: de data staat onder `data/` met een `.gitkeep` per map, en `backupIgnore`
|
||||
noemt de sessielog en `status.json`. Niet `data/runtime/config`, want daar zit de certificaatkeuze.
|
||||
|
||||
Wat het uitzoeken daarna opleverde is groter dan dit punt en staat daarom in een eigen masterplan
|
||||
**Publicatie**: de eisen van de officiële store, wat er al aan voldoet, en het risico dat niet in een
|
||||
checklist staat, namelijk de leesmount op de certificaten van Zoraxy.
|
||||
**Moment:** afgerond · **Eigenaar:** –
|
||||
|
||||
## Beslist
|
||||
|
||||
8. **Wordt de repo hernoemd, en wat wordt dan het store-id?** - **Store-id `whatsnext`, app-id
|
||||
`whatsnext-electrum-gate`, repo-naam nog niet** (19-08-2026, gebruiker).
|
||||
|
||||
Het app-id **moet** met het store-id beginnen en de mapnaam moet gelijk zijn aan het app-id (zie
|
||||
[Umbrel-appstore-spec.md](../../../Referenties/Umbrel-appstore-spec.md)). De aanname bij het stellen
|
||||
van deze vraag was dat de repo-naam daar ook in zat, want de store-URL ís de repo-URL. **Dat klopte
|
||||
niet:** het store-id is gewoon een veld in `umbrel-app-store.yml` en staat los van de naam van de
|
||||
repo. Daardoor kon het store-id meteen goed gezet worden en kan de repo-naam wachten.
|
||||
|
||||
De store heet nu naar de maker en niet naar deze ene app. Dat is de variant die later een tweede app
|
||||
toelaat zonder opnieuw te hernoemen, en dat weegt hier zwaarder dan de kosten, want die zijn nul: de
|
||||
gebruiker gaf aan de app zonder bezwaar opnieuw te kunnen installeren.
|
||||
|
||||
**Afgerond op 25-08-2026, en eerder dan gedacht.** Hier stond dat de repo nog `ElectrumTLS` heette en
|
||||
dat het hernoemen zou meeliften op een publieke versie op GitHub. Wat het versnelde was Evolu Relay:
|
||||
umbrelOS leest per store één repo, dus een tweede app dwong de vraag af. De repo is
|
||||
`UmbrelApps` geworden, met `website`, `repo`, `support`, `submission` en `icon` mee, in 0.0.15.
|
||||
|
||||
**Het app-id is niet meegegaan** en dat is de reden dat dit goedkoop was: `whatsnext-electrum-gate`
|
||||
hangt aan het store-id en niet aan de URL. Voor umbrelOS is het dus dezelfde app in een andere store.
|
||||
|
||||
**Wat er wél open blijft, en nu voor het eerst echt aan de orde is:** of een geïnstalleerde app een
|
||||
wisseling van **store-URL** overleeft, of dat de store verwijderd en opnieuw toegevoegd moet worden en
|
||||
de app daarna opnieuw geïnstalleerd. Niet uitgezocht, en niet aannemen dat het meevalt. **Eigenaar:**
|
||||
gebruiker, bij het omzetten van de store in umbrelOS.
|
||||
|
||||
|
||||
1. **Draait `nginx:alpine` met de `stream`- en `stream_ssl`-module?** - **Ja** (18-08-2026).
|
||||
Op de Umbrel gecontroleerd met `docker run --rm nginx:alpine nginx -V`. Aanwezig zijn
|
||||
`--with-stream`, `--with-stream_realip_module`, `--with-stream_ssl_module` en
|
||||
`--with-stream_ssl_preread_module`, alle vier statisch meegebouwd, dus zonder `load_module`.
|
||||
|
||||
Daarmee gaat het ontwerp uit [PLAN.md](PLAN.md) §4c door in de kleine variant: één container die de
|
||||
web-UI serveert én TLS termineert, geen stunnel-installatie bij het starten, geen losse
|
||||
`cert-monitor`, en de Docker-socket-mount vervalt.
|
||||
|
||||
Twee dingen die deze uitkomst meebracht en die het opschrijven waard zijn. nginx wil de **volledige
|
||||
keten** in `ssl_certificate`, terwijl stunnel het gesplitst wilde; de `awk`-splitsing uit
|
||||
`entrypoint.sh` is daarmee overbodig in plaats van overgenomen. En dit antwoord geldt voor de tag
|
||||
`nginx:alpine` van vandaag: bij het pinnen op een digest wordt dezelfde controle op díe digest
|
||||
herhaald, anders is er iets anders bewezen dan er uitgeleverd wordt.
|
||||
|
||||
6. **Hoe komen `entrypoint.sh` en `web/` in `${APP_DATA_DIR}`?** - **Bij installatie automatisch, bij
|
||||
een update niet** (18-08-2026).
|
||||
umbreld doet bij installatie `rsync --archive` van de hele app-map naar `${APP_DATA_DIR}`, dus de
|
||||
mounts in de compose kloppen. Bij een **update** wordt alleen een whitelist ververst:
|
||||
`docker-compose.yml`, `*.template`, `exports.sh`, `torrc`, `hooks` en `umbrel-app.yml`.
|
||||
|
||||
Gevolg dat het ontwerp stuurt: een gewijzigde `entrypoint.sh` bereikt een bestaande installatie nooit,
|
||||
zonder foutmelding. Logica die later nog moet kunnen wijzigen hoort daarom in de compose, in de image,
|
||||
of in een `*.template`-bestand; dat laatste staat in de whitelist én wordt bij elke start met de
|
||||
omgevingsvariabelen ingevuld. Zie de naslag
|
||||
[Umbrel-appstore-spec.md](../../../Referenties/Umbrel-appstore-spec.md).
|
||||
|
||||
3. **Wanneer wordt er voor het eerst gepusht?** - **Meteen, vóór de opschoning** (18-08-2026).
|
||||
De repo staat sindsdien publiek op `https://sc.kamenier-hamer.nl/sysop/ElectrumTLS.git`, met het
|
||||
domein en het certificaatpad er nog in. Publiek zijn was geen keuze (umbreld kloont anoniem), alleen
|
||||
de volgorde was dat. Afweging van de gebruiker: de repo is voor eigen gebruik en wordt niet
|
||||
aangekondigd, dus vindbaarheid is laag.
|
||||
Gevolg dat vastligt: die waarden staan nu in de **historie**. Het plan **Configuratie** haalt ze uit de
|
||||
bestanden, niet uit de historie; dat laatste zou een herschrijving vragen. Behandel het domein dus als
|
||||
bekend, en laat het geen reden zijn om Configuratie uit te stellen of juist te haasten.
|
||||
|
||||
**Bijgewerkt 25-08-2026: die historie is bij de verhuizing achtergebleven.** `UmbrelApps` is als lege
|
||||
repo begonnen met alleen de huidige toestand erin, op verzoek van de gebruiker die de historie niet
|
||||
nodig had. Daarmee vervalt de vaststelling hierboven, maar **pas als de oude repo weg is**: zolang
|
||||
`ElectrumTLS` op de Git-server staat, staat het domein daar nog in de historie. Dat weghalen is dus
|
||||
geen opruimwerk maar het laatste stuk van deze beslissing. **Eigenaar:** gebruiker
|
||||
|
||||
5. **Mag de app-id later nog wijzigen?** - **Ja** (18-08-2026).
|
||||
Er zijn geen andere installaties, dus achterwaartse compatibiliteit is geen eis. Een wijziging kost
|
||||
één keer opnieuw installeren. Genoteerd omdat de prefix-regel anders zwaarder lijkt dan hij is: de
|
||||
keuze `electrumtls-electrum-tls` mag herzien worden zolang dat vóór fase 6 gebeurt.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Appstore - plan
|
||||
|
||||
> Ontwerp en afbakening. **Dit bestand lees je zelden**, alleen bij twijfel over scope of architectuur.
|
||||
> Status staat in [TAKEN.md](TAKEN.md), geschiedenis in [PROGRESS.md](PROGRESS.md), onbesliste punten in
|
||||
> [OPEN.md](OPEN.md).
|
||||
|
||||
## 1. Doel
|
||||
|
||||
De app draait nu als een met de hand neergezette docker-compose die umbrelOS niet kent. Gevolg: na een
|
||||
herstart van de Umbrel, of als een app waar deze van afhangt omvalt, moet er met de hand
|
||||
`docker compose down` en `up` gedaan worden. Dit plan maakt er een echte Umbrel-app van in een eigen
|
||||
community app store: een tegel met icoon, die umbrelOS zelf installeert, start en na een herstart weer
|
||||
opbrengt.
|
||||
|
||||
Als dit af is, is de repo tegelijk de app store: de URL erin plakken in umbrelOS is genoeg om de app te
|
||||
installeren, en een `git push` is genoeg om een update uit te leveren.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
- De repo omzetten naar de vorm die umbrelOS voor een community app store verwacht.
|
||||
- `docker-compose.yml` omzetten naar de moderne vorm: `app_proxy`, geen zelfgebouwd netwerk, geen
|
||||
handmatige host-poorten waar dat niet hoeft.
|
||||
- De Electrum-backend via de afhankelijkheid aanspreken in plaats van via een hardgecodeerde
|
||||
containernaam, zodat Electrs, Fulcrum en ElectrumX alle drie werken.
|
||||
- Een image die gepind kan worden, in plaats van `alpine:latest` met `apk add` bij elke start.
|
||||
- Het manifest compleet en eerlijk maken: eigen icoon, eigen gallery, kloppende velden.
|
||||
- De oude `install.sh` en `uninstall.sh` weghalen.
|
||||
|
||||
De volledige spec waar dit tegenaan moet, met bronvermelding per feit, staat in
|
||||
[Referenties/Umbrel-appstore-spec.md](../../../Referenties/Umbrel-appstore-spec.md). Die is bij het
|
||||
schrijven van dit plan uitgezocht en hoeft niet opnieuw opgezocht te worden.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **De hardgecodeerde waarden eruit halen.** Het domein `sync.kamenier-hamer.nl` en het Zoraxy-pad blijven
|
||||
in dit plan staan zoals ze zijn. Dat is het plan **Configuratie**, en het apart houden is bewust: een
|
||||
commit die tegelijk de structuur omgooit en de configuratie herontwerpt is niet meer na te lezen.
|
||||
- **De web-UI eerlijk maken.** De pagina toont verzonnen status. Dat is het plan **Webinterface**. Hier
|
||||
wordt de pagina alleen verhuisd en aan `app_proxy` gehangen, niet herschreven.
|
||||
- **Meerdere apps in de store.** De store krijgt de vorm die meer apps toelaat, maar er komt er één in.
|
||||
- **Indienen bij de officiële Umbrel App Store.** Een community store is er juist om dat niet te hoeven.
|
||||
Als het later toch aantrekkelijk wordt, is de spec-eis grotendeels dezelfde, dus dit sluit niets af.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a. De repo-vorm
|
||||
|
||||
```
|
||||
UmbrelApps/
|
||||
├── umbrel-app-store.yml id: whatsnext
|
||||
├── whatsnext-electrum-gate/
|
||||
│ ├── umbrel-app.yml id: whatsnext-electrum-gate
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── *.template
|
||||
│ └── data/{certs,runtime}/.gitkeep
|
||||
├── whatsnext-evolu-relay/ komt er bij het masterplan Umbrelapp
|
||||
├── Docs/
|
||||
├── tests/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
De store-id is `whatsnext`. De prefix-eis is hard: mapnaam en manifest-`id` moeten gelijk zijn en allebei
|
||||
met de store-id beginnen.
|
||||
|
||||
**Bijgewerkt op 25-08-2026**, toen de repo een store met meer dan één app werd. Hier stond nog de vorm van
|
||||
18-08-2026 met store-id `electrumtls`; die was al achterhaald door fase 7. Wat de tweede app bewijst is dat
|
||||
de keuze van 19-08 om de store naar de maker te noemen in plaats van naar deze ene app, klopte: er hoefde
|
||||
niets voor te hernoemen.
|
||||
|
||||
### 4b. Backend-onafhankelijk, en waarom dat bijna niets kost
|
||||
|
||||
umbrelOS 1.3 heeft swappable dependencies. Fulcrum en ElectrumX declareren allebei `implements: [electrs]`
|
||||
en hun `exports.sh` aliast `APP_ELECTRS_IP`, `APP_ELECTRS_NODE_IP` en `APP_ELECTRS_NODE_PORT` naar hun
|
||||
eigen waarden. De gebruiker kiest de implementatie in de umbrelOS-instellingen; umbrelOS laadt de
|
||||
`exports.sh` van de gekozen app.
|
||||
|
||||
Deze app hoeft daarvoor dus **geen keuzemechanisme te bouwen**. Het is dit:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
- electrs
|
||||
```
|
||||
|
||||
en in de compose `${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}` gebruiken.
|
||||
|
||||
Twee vallen om te vermijden. De eerste: de huidige compose zet `ELECTRS_HOST=${APP_ELECTRS_IP}`, en dat
|
||||
is de **web-UI-container** van Electrs, niet de Electrum-server; dat moet `APP_ELECTRS_NODE_IP` worden.
|
||||
De tweede: alleen `IP`, `NODE_IP` en `NODE_PORT` worden gealiast, dus alles wat Electrs-specifiek is
|
||||
(zoals `APP_ELECTRS_RPC_HIDDEN_SERVICE`) mag hier niet gebruikt worden.
|
||||
|
||||
### 4c. De image: nginx `stream` in plaats van stunnel
|
||||
|
||||
Het huidige `alpine:latest` plus `apk add stunnel` bij elke start is op drie manieren fout: het is niet
|
||||
te pinnen, het faalt zonder internet, en het maakt de starttijd afhankelijk van een Alpine-mirror. De
|
||||
app-store-eis is een image gepind op de multi-arch index-digest.
|
||||
|
||||
Er zijn drie wegen, en de aanbeveling is de derde:
|
||||
|
||||
1. **Eigen image bouwen** met een `Dockerfile` en een workflow die multi-arch naar een registry duwt.
|
||||
Correct, maar het voegt CI, een registry en een tweede uitleverstroom toe aan een app die verder uit
|
||||
twee shellscripts bestaat.
|
||||
2. **Een bestaande stunnel-image pinnen.** Er is geen onderhouden multi-arch stunnel-image die het
|
||||
vertrouwen waard is. Afgevallen.
|
||||
3. **De officiële `nginx`-image gebruiken en de `stream`-module de TLS-terminatie laten doen.** Die image
|
||||
is multi-arch, wordt onderhouden en is gewoon te pinnen. Dan valt er meer weg dan alleen het
|
||||
bouwprobleem:
|
||||
- dezelfde container serveert de web-UI én termineert TLS, dus van drie containers blijft er één over.
|
||||
**Bijgesteld 19-08-2026:** het zijn er weer twee, want er is een agent bijgekomen. De winst die hier
|
||||
bedoeld werd blijft wel staan: geen Docker-socket, geen installatie bij het starten, en de
|
||||
certificaatwissel is een reload. Zie het plan **Webinterface**, `PLAN.md` §4a0;
|
||||
- een certificaatwissel wordt `nginx -s reload` **binnen** de container, dus de `cert-monitor` heeft de
|
||||
Docker-socket niet meer nodig. Die socket is nu read-only gemonteerd, maar read-only op de
|
||||
Docker-socket beschermt niets: wie de socket kan lezen kan containers starten en is daarmee root op
|
||||
de host. Dat weghalen is de grootste beveiligingswinst in dit plan;
|
||||
- een reload verbreekt bestaande verbindingen niet, een containerherstart wel.
|
||||
|
||||
**Te verifiëren voordat hierop gebouwd wordt:** dat de officiële `nginx:alpine` daadwerkelijk met
|
||||
`--with-stream` en `--with-stream_ssl_module` gebouwd is. Dat is de aanname waar deze hele keuze op
|
||||
rust en hij is in één commando te controleren (`nginx -V`). Klopt hij niet, dan valt dit terug op weg 1.
|
||||
|
||||
De `awk`-splitsing van de certificaat-chain uit `entrypoint.sh` blijft bruikbaar en wordt overgenomen.
|
||||
|
||||
### 4d. Poorten
|
||||
|
||||
`port:` in het manifest is de **web-UI-poort** van de tegel, niet de TLS-poort. Dat staat nu op 50002 en
|
||||
is daarmee fout.
|
||||
|
||||
De TLS-poort blijft een gepubliceerde host-poort; daar helpt `app_proxy` niet, want dat is voor HTTP.
|
||||
Welke poort dat wordt is een open punt in **Configuratie**: Fulcrum bezet host-poort 50002 en botst dus
|
||||
met de huidige keuze.
|
||||
|
||||
### 4e. De eigen Git-server als app store
|
||||
|
||||
De repo staat op `https://sc.kamenier-hamer.nl/sysop/UmbrelApps.git` (tot 25-08-2026:
|
||||
`.../ElectrumTLS.git`). umbreld valideert de URL alleen
|
||||
met de `URL`-constructor en kloont met isomorphic-git; er is geen GitHub-eis. Wat er wél uit die aanroep
|
||||
volgt, en op 18-08-2026 in orde is bevonden:
|
||||
|
||||
- **HTTPS, niet SSH.** In orde.
|
||||
- **Anoniem kloonbaar.** In orde sinds 18-08-2026, maar het kostte moeite en de oorzaak was niet de
|
||||
voor de hand liggende.
|
||||
|
||||
umbrelOS gaf `HTTP Error: 401 Unauthorized` bij het toevoegen van de store. De repo stond op public en
|
||||
`REQUIRE_SIGNIN_VIEW` stond op `false`, en toch weigerde Gitea. De oorzaak was de zichtbaarheid van het
|
||||
**account** `sysop`, die op "limited" stond. **Gitea staat niet toe dat een repo zichtbaarder is dan
|
||||
zijn eigenaar**, dus de repo werd stilzwijgend teruggezet naar "intern", wat voor een niet-ingelogde
|
||||
bezoeker hetzelfde is als privé. De knop "make public" leek te werken maar het label bleef op
|
||||
"intern" staan, en dat is het enige zichtbare spoor.
|
||||
|
||||
Dit is eerst verkeerd beoordeeld, en de manier waaróp is het onthouden waard. Een `git ls-remote` vanaf
|
||||
de werkmachine slaagde, ook met `GIT_TERMINAL_PROMPT=0`, en dat leek bewijs van anonieme toegang. Het
|
||||
was het niet: Git Credential Manager stuurde de bij de push opgeslagen inloggegevens stilzwijgend mee.
|
||||
`GIT_TERMINAL_PROMPT=0` onderdrukt alleen de **vraag** om een wachtwoord, niet het **aanleveren** ervan.
|
||||
De test die het wel aantoont, zet de credential-helper leeg:
|
||||
|
||||
```
|
||||
GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote <url>
|
||||
```
|
||||
|
||||
Die faalt met `could not read Username`, en dat is de toestand die umbreld ziet.
|
||||
- **SHA-1 als objectformaat.** In orde, na het opnieuw aanmaken van de repo. isomorphic-git kan geen
|
||||
SHA-256; zie de naslag.
|
||||
- **Geldig certificaat op `sc.kamenier-hamer.nl`.** Nog niet expliciet gecontroleerd vanaf de Umbrel.
|
||||
- **Alles op de standaardbranch.** In orde: `main`, zowel lokaal als op de remote.
|
||||
- **De URL is de identiteit van de store.** Wijzigt hij, dan ziet umbrelOS een andere store en moet hij
|
||||
opnieuw toegevoegd worden.
|
||||
|
||||
## 5. Raakvlakken
|
||||
|
||||
**Configuratie** herschrijft dezelfde bestanden en wacht op dit plan. Twee dingen komen daarvandaan
|
||||
terug: de TLS-poort (open punt daar, want Fulcrum bezet 50002) en het feit dat het domein na dit plan nog
|
||||
steeds vast in de code staat.
|
||||
|
||||
**Webinterface** wacht op Configuratie en raakt hier alleen de verhuizing van `web/index.html`. Het
|
||||
containerontwerp uit §4c is wel de reden dat dat plan zonder backend kan: de proxy en de webserver worden
|
||||
één container, dus als de pagina geserveerd wordt, draait de proxy ook.
|
||||
|
||||
## 6. Verificatie
|
||||
|
||||
Er zijn geen tests in dit project en die zijn hier ook niet zinvol: de app is configuratie, geen code.
|
||||
Wat er wél moet, en wat per se handmatig op het apparaat gebeurt:
|
||||
|
||||
- de store laat zich in umbrelOS toevoegen en de app verschijnt met icoon en tegel;
|
||||
- installeren via de umbrelOS-interface werkt zonder SSH;
|
||||
- **na `sudo reboot` komt de app vanzelf terug.** Dit is de aanleiding voor het hele plan en dus de
|
||||
belangrijkste controle;
|
||||
- de app komt ook terug nadat Electrs handmatig gestopt en gestart is;
|
||||
- een Electrum-wallet verbindt over TLS en verifieert het certificaat;
|
||||
- omschakelen naar Fulcrum in de umbrelOS-instellingen laat de app werken zonder aanpassing.
|
||||
|
||||
Wat automatisch gecontroleerd kan worden, en de moeite waard is omdat het de fouten vangt die je niet
|
||||
ziet: dat de YAML geldig is, dat het `id` in het manifest gelijk is aan de mapnaam, en dat elke `image:`
|
||||
een `@sha256:`-digest heeft.
|
||||
@@ -0,0 +1,222 @@
|
||||
# Voortgang - Appstore
|
||||
|
||||
> 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).
|
||||
|
||||
## 20-08-2026 - alles van de verse installatie nagekeken; dit plan zakt naar B
|
||||
|
||||
De hele checklist van de herinstallatie is bevestigd door de gebruiker: `data/` met `certs/` en `runtime/`
|
||||
eronder, de pagina komt op zonder gekozen certificaat, `v0.0.9` staat achter de tagline, de keuzelijst voor
|
||||
de Electrum-server verscheen bij het installeren, en daarna werkten kiezen én uploaden.
|
||||
|
||||
**Daarmee is dit plan in de kern af en gaat het van A naar B.** Alles waar het om begon is bewezen: de app
|
||||
is een echte umbrelOS-app, hij komt op uit een schone installatie, en een wallet van buiten verbindt over
|
||||
TLS. Wat er nog staat is niet te plannen (de herstartcontrole) of hoort bij het masterplan **Publicatie**.
|
||||
**Webinterface** is daarmee tier A geworden en van 020 naar 005 hernummerd; dit plan houdt 010.
|
||||
|
||||
**Geraakt:** alleen documentatie. **Tests:** niet van toepassing.
|
||||
|
||||
## 20-08-2026 - verse installatie van 0.0.9, en die werkte
|
||||
|
||||
De gebruiker heeft de app gedeïnstalleerd en opnieuw geïnstalleerd in plaats van geüpdatet, zodat de
|
||||
app-data schoon is. **Dat werkte, en het bewijst iets wat op een bestaande installatie niet te bewijzen
|
||||
was:** de pagina komt omhoog op een installatie waar nog nooit een certificaat gekozen is. Dat is precies
|
||||
de situatie waarin 0.0.3 stukliep, en de reparatie uit 0.0.4 was tot nu toe alleen getoetst op een
|
||||
installatie die die keuze al had.
|
||||
|
||||
Wat er nog níet uit volgt: of de twee `.gitkeep`-bestanden hun werk deden. Een werkende app bewijst dat
|
||||
niet, want Docker maakt een ontbrekende bind-mount-map zelf aan; het verschil zit alleen op schijf. Staat
|
||||
als losse taak, want het is een eis voor het masterplan **Publicatie**.
|
||||
|
||||
**Geraakt:** alleen documentatie. **Tests:** niet van toepassing.
|
||||
|
||||
## 20-08-2026 - data onder data/, en een doel dat de maatstaf verandert
|
||||
|
||||
De gebruiker vroeg of het klopt dat alle runtime-bestanden in de installatiemap staan, want bij andere apps
|
||||
ziet hij daar geen app-code. **Dat klopt, en het verschil zit niet in umbrelOS:** voor elke app wordt de
|
||||
hele app-map naar `app-data` gekopieerd, maar andere apps hebben hun code in een image. Config uit
|
||||
`app-data` mounten is wél een bestaand patroon; `electrs` doet zijn `torrc` precies zo, en `torrc` en
|
||||
`*.template` staan met naam in de update-whitelist. Bijvangst: de invulling gebeurt met `envsubst`, en dat
|
||||
is de onderbouwing van de architectuurregel over accolade-variabelen.
|
||||
|
||||
Waar deze app echt afweek was de plaats van de data, en dat is in **0.0.9** verholpen: `runtime/` en
|
||||
`certs/` staan nu onder `data/`, met een `.gitkeep` per map. Aanleiding was het antwoord op de vervolgvraag:
|
||||
**de gebruiker wil de app publiceren als standaard-app voor Umbrel.** De packaging-documentatie van umbrel
|
||||
zegt woordelijk dat gebruikersstaat onder `${APP_DATA_DIR}/data/...` hoort, dus dit was geen smaak.
|
||||
|
||||
Dat doel verandert de maatstaf van "hij werkt hier" naar "iemand anders keurt het pakket goed", en dat is
|
||||
een eigen plan geworden: **Publicatie**, als masterplan. Het meeste blijkt al goed; wat er nog moet is de
|
||||
images pinnen, het app-id kaal maken en de manifestvelden op orde brengen. Het risico staat niet in die
|
||||
lijst: deze app leest de certificaatmap van een andere app, en dat is precies waar een review over valt.
|
||||
|
||||
**Geraakt:** `docker-compose.yml`, `umbrel-app.yml` (0.0.9 en `backupIgnore`), twee `.gitkeep`-bestanden,
|
||||
`Referenties/Umbrel-appstore-spec.md`, `OPEN.md` punt 7 en 9, nieuw
|
||||
`Plannen/Masterplannen/Publicatie.PLAN.md`, `tests/test_server_start_zonder_certificaat.py`.
|
||||
**Tests:** 48 goed 0 fout en 33 goed 0 fout, niets overgeslagen; de data-conventie is mutatie-getest.
|
||||
|
||||
## 20-08-2026 - de app werkt: pagina, keuze, TLS en een wallet van buiten
|
||||
|
||||
**De reparatie werkt.** Na de uitrol van 0.0.4 laadt het dashboard, met de melding "No certificate in use"
|
||||
en de reden van de agent erbij. De agent vond **veertien certificaten in Zoraxy** en koos daarom niets; dat
|
||||
is de weigering die zo bedoeld is, en het antwoord op de vraag die de log van 0.0.3 openliet.
|
||||
|
||||
Wat de gebruiker er meteen van zei: die reden somde alle veertien domeinnamen op, en dat is een muur tekst
|
||||
die zegt wat de keuzelijst eronder al toont. **0.0.5** geeft alleen het aantal. De weigering zelf blijft.
|
||||
|
||||
**En daarna liep het hele pad.** Een certificaat kiezen op de pagina werkt: de agent nam de keuze aan, nginx
|
||||
kwam met poort 50022 door, en na het aanpassen van de doorstuurregel in de router verbindt een wallet van
|
||||
buiten. **Daarmee is fase 6 in de kern klaar** en is de app voor het eerst als umbrelOS-app af.
|
||||
|
||||
**Geraakt:** `agent.py.template` (`choose`), `umbrel-app.yml` (0.0.5), `tests/test_agent_certificates.py`,
|
||||
`CHANGELOG.md`. **Tests:** 21 goed 0 fout en 15 goed 0 fout, niets overgeslagen; de ingekorte reden is
|
||||
mutatie-getest. Nog open op de Umbrel: de herstartcontrole en het omschakelen naar Fulcrum.
|
||||
|
||||
## 20-08-2026 - 0.0.3 geinstalleerd, geen UI, en de oorzaak was het ontwerp
|
||||
|
||||
**De app is geinstalleerd en er kwam geen pagina.** De log van `app_proxy` meldde alleen dat de server niet
|
||||
te bereiken was, en die van `server` herhaalde "Wachten tot de agent een certificaat gekozen heeft". Dat is
|
||||
geen storing maar een klem in het ontwerp: het stream-blok met `listen 50022 ssl` includeerde de `cert.conf`
|
||||
van de agent, nginx weigert te starten zonder dat certificaat, dus ook de pagina kwam niet omhoog. En de
|
||||
pagina is precies waar je dat certificaat kiest.
|
||||
|
||||
**Verholpen in 0.0.4** door het TLS-deel naar `stream.conf.template` te verplaatsen; het `command`-blok legt
|
||||
dat pas in `/var/lib/gate/tls/` als `cert.conf` bestaat, en `nginx.conf` haalt die map met een jokerteken op.
|
||||
Meegenomen: een mislukte `nginx -s reload` brak de herlaadlus af, want die draait onder `set -e`.
|
||||
|
||||
Wat de log wél bewees: de agent start en luistert (`[gate] api listening on port 8000`), en Tor bootstrapt.
|
||||
Wat de agent in de certificaatmappen vond, is nog onbekend; dat leest de pagina van 0.0.4 straks voor.
|
||||
|
||||
**Geraakt:** `nginx.conf.template`, `docker-compose.yml`, nieuw `stream.conf.template`, `umbrel-app.yml`
|
||||
(0.0.4 en release notes), `CLAUDE.md`, `CHANGELOG.md`, nieuw
|
||||
`tests/test_server_start_zonder_certificaat.py`. **Tests:** 21 goed 0 fout en 15 goed 0 fout, niets
|
||||
overgeslagen. De reparatie zelf is **niet** geverifieerd: dat vraagt de uitrol op de Umbrel.
|
||||
|
||||
## 19-08-2026 - sessie afgesloten; dit plan is de enige tier A
|
||||
|
||||
Bij de prioriteitsherziening blijft **Appstore de enige tier A**, en de reden is dat er precies één
|
||||
handeling openstaat waar al het andere achter wacht: de app installeren. **Webinterface is naar C gezakt**;
|
||||
dat plan is af voor zover het zonder de Umbrel kan.
|
||||
|
||||
Wat er van de gebruiker moet, in deze volgorde: de doorstuurregel in de router naar 50022, dan de oude app
|
||||
verwijderen, de store-URL opnieuw laten ophalen en `whatsnext-electrum-gate` installeren. Versie staat op
|
||||
0.0.3 en de release notes kloppen, dus daar hoeft niets meer aan.
|
||||
|
||||
Ook opgemerkt en als taak vastgelegd: het plan **Configuratie** is grotendeels ingehaald door het werk van
|
||||
vandaag. De agent doet de certificaatbronnen en de keuze al, en het hardgecodeerde domein is uit de compose
|
||||
en uit `nginx.conf.template` verdwenen. Dat plan moet dus eerst opgeschoond worden voordat promotie nog
|
||||
zin heeft, anders lijkt het groter dan het is.
|
||||
|
||||
**Geraakt:** alleen documentatie in deze entry.
|
||||
**Tests:** 21 goed, 0 fout.
|
||||
|
||||
## 19-08-2026 - hernoemd naar Electrum Gate, fase 7 grotendeels af
|
||||
|
||||
De wallet-verbinding had de nacht doorstaan, dus de `proxy_timeout`-fix is bewezen en de storing waar de
|
||||
sessie van gisteren mee begon is verklaard en verholpen.
|
||||
|
||||
De app heet nu **Electrum Gate**, met store-id `whatsnext` en app-id `whatsnext-electrum-gate`. Bij het
|
||||
voorleggen van die keuze zat een fout in mijn eigen redenering: ik nam aan dat de repo-naam het store-id
|
||||
bepaalt, omdat de store-URL de repo-URL is. Dat is niet zo, het store-id is gewoon een veld. Daardoor kon
|
||||
het meteen goed en kan de repo-naam wachten tot er een publieke versie op GitHub komt.
|
||||
|
||||
Twee dingen die er vooruitlopend op andere plannen bij konden, omdat er tóch opnieuw geïnstalleerd wordt:
|
||||
de TLS-poort is 50022 in plaats van 50002, wat de botsing met Fulcrum wegneemt, en de web-UI is
|
||||
herbouwd. **Dat vraagt eenmalig een aanpassing van de doorstuurregel in de router**, anders komt een
|
||||
wallet van buiten niet meer binnen.
|
||||
|
||||
De tagline en de beschrijving zijn Engels geworden en gaan nu over de afweging Tor tegenover snelheid, in
|
||||
plaats van over TLS als middel.
|
||||
|
||||
**Geraakt:** `umbrel-app-store.yml`, de app-map (hernoemd), `umbrel-app.yml`, `docker-compose.yml`,
|
||||
`nginx.conf.template`, `README.md` en de plannen.
|
||||
**Tests:** dit project heeft geen suite. **Niets hiervan is op de Umbrel gedraaid**: de nieuwe app is nog
|
||||
niet geïnstalleerd, en `version` staat nog op 2.0.1 omdat de gebruiker de UI eerst wilde zien.
|
||||
|
||||
## 18-08-2026 - storing na tien minuten, en de besluiten over naam en dashboard
|
||||
|
||||
Kort na de installatie viel de wallet-verbinding weg. De container bleef onafgebroken draaien
|
||||
(`status=running`, `restarts=0`, schone nginx-log), wat de container als oorzaak uitsloot en één
|
||||
verdachte overliet: `proxy_timeout` in een `stream`-blok staat standaard op tien minuten, en een
|
||||
Electrum-wallet houdt een langlopende, grotendeels stille verbinding open. **Dit was een regressie van
|
||||
diezelfde dag:** stunnel hanteerde twaalf uur, en dat verschil is bij de vertaling naar nginx niet
|
||||
opgemerkt. Nu op twaalf uur, met `proxy_socket_keepalive`.
|
||||
|
||||
Meegenomen omdat ze bij het uitzoeken bovenkwamen: nginx draaide als achtergrondproces onder een shell,
|
||||
die daardoor PID 1 was en signalen niet doorgaf, en het script eindigde met exitcode 0 als nginx omviel,
|
||||
waardoor `restart: on-failure` niet zou ingrijpen. Nginx is nu met `exec` het hoofdproces.
|
||||
|
||||
De gebruiker vroeg of umbrelOS een update wel zou zien, en dat was de goede vraag: het `version`-veld
|
||||
stond nog op 2.0.0, dus de fix was nooit uitgerold. Zonder melding en zonder fout. Na 2.0.1 kwam de
|
||||
update binnen en werkte hij, wat meteen de hele uitleverketen aantoont.
|
||||
|
||||
Aan het eind besloten: de app gaat **Electrum Gateway** heten, want de oude naam beschreef het middel en
|
||||
niet de waarde. En het dashboard krijgt alleen lokale gegevens; koersdata valt af omdat de browser van
|
||||
elke bezoeker dan bij een derde partij aanklopt.
|
||||
|
||||
**Geraakt:** `docker-compose.yml`, `nginx.conf.template`, `umbrel-app.yml`, en de plannen.
|
||||
**Tests:** niet van toepassing. **De `proxy_timeout`-fix is nog niet bewezen**: die brak pas na tien
|
||||
minuten stilte, dus dat vraagt een langere observatie. Ook de herstart-controle staat nog open.
|
||||
|
||||
## 18-08-2026 - geinstalleerd op de Umbrel, wallet verbindt
|
||||
|
||||
De app is als Umbrel-app geïnstalleerd en een Electrum-wallet krijgt over TLS direct data terug. Dat
|
||||
bewijst het hele pad in één keer: het `stream`-blok, het certificaat, en de verbinding naar de
|
||||
Electrum-server via `APP_ELECTRS_NODE_IP`.
|
||||
|
||||
De weg erheen kostte drie omwegen die allemaal buiten de app zelf lagen. De Gitea-repo was als SHA-256
|
||||
aangemaakt terwijl isomorphic-git alleen SHA-1 kan. Daarna weigerde Gitea anonieme toegang, en de oorzaak
|
||||
bleek niet de repo maar de zichtbaarheid van het **account**: Gitea staat niet toe dat een repo
|
||||
zichtbaarder is dan zijn eigenaar en zet hem dan zwijgend terug naar "intern". En de installatie faalde
|
||||
drie keer op `Bind for 0.0.0.0:50002 failed: port is already allocated`, omdat de oude handmatige
|
||||
container nog draaide. Dat laatste was de voorspelde en gewenste faalrichting: de bestaande dienst bleef
|
||||
gewoon werken.
|
||||
|
||||
Eén les die het onthouden waard is: een `git ls-remote` vanaf de eigen machine bewees niets over anonieme
|
||||
toegang, want de credential-helper stuurde de opgeslagen inloggegevens stilzwijgend mee. Dat leidde tot
|
||||
een verkeerde diagnose die pas onderuitging met `-c credential.helper=`.
|
||||
|
||||
**Geraakt:** niets in de app; alleen documentatie en de configuratie van de Gitea-server.
|
||||
**Tests:** niet van toepassing. Wallet-verbinding handmatig geverifieerd. **De herstart-controle staat
|
||||
nog open**, en dat is de aanleiding van het hele plan.
|
||||
|
||||
## 18-08-2026 - fases 1 tot en met 5 gebouwd
|
||||
|
||||
De repo heeft de vorm van een community app store, de compose is omgezet naar
|
||||
`app_proxy`, de backend loopt via de afhankelijkheid, en het manifest klopt met een eigen icoon.
|
||||
|
||||
Twee dingen die het uitzoekwerk opleverde en die het ontwerp veranderd hebben. De controle op de Umbrel
|
||||
bevestigde dat `nginx:alpine` alle vier de stream-modules meebrengt, waardoor drie containers er één
|
||||
werden en de Docker-socket kon vervallen; dat was de grootste beveiligingswinst en hij was gratis. En
|
||||
umbreld blijkt bij een **update** alleen een whitelist te verversen (`docker-compose.yml`, `*.template`,
|
||||
`exports.sh`, `torrc`, `hooks`, `umbrel-app.yml`), terwijl bij installatie de hele map wordt gekopieerd.
|
||||
Daarom staat de nginx-configuratie in een `*.template` en niet in een los script: anders had een `git
|
||||
push` stilzwijgend niets gedaan bij een bestaande installatie.
|
||||
|
||||
Ook bleek de `awk`-splitsing van de certificaatketen niet overgenomen te hoeven worden maar juist fout
|
||||
te zijn: nginx wil de volledige keten in `ssl_certificate`, waar stunnel hem gesplitst wilde.
|
||||
|
||||
**Geraakt:** `umbrel-app-store.yml`, `electrumtls-electrum-tls/` (compose, manifest, `nginx.conf.template`,
|
||||
`icon.png`), verwijderd zijn `entrypoint.sh`, `cert-watch.sh`, `install.sh` en `uninstall.sh`.
|
||||
**Tests:** niet van toepassing, dit project heeft er geen. **Niets is op de Umbrel geïnstalleerd of
|
||||
gedraaid**; dat is fase 6.
|
||||
|
||||
## 18-08-2026 - plan werd actief
|
||||
|
||||
Dit plan is als eerste actief geworden omdat het de aanleiding van de hele sessie oplost: de app draait
|
||||
als een handmatig neergezette docker-compose die umbrelOS niet kent, en moet na elke herstart of na een
|
||||
storing in een afhankelijkheid met de hand opgestart worden.
|
||||
|
||||
De repo is dezelfde dag onder versiebeheer gebracht en gepusht naar
|
||||
`https://sc.kamenier-hamer.nl/sysop/ElectrumTLS.git`. Dat kostte één omweg: de Gitea-repo was als SHA-256
|
||||
aangemaakt, en de push faalde met een melding die de oorzaak niet noemt. Bij het uitzoeken bleek het
|
||||
probleem groter dan de push, want umbreld kloont met isomorphic-git en die kan uitsluitend SHA-1. De repo
|
||||
is opnieuw aangemaakt als SHA-1.
|
||||
|
||||
Twee dingen die het uitzoekwerk opleverde en die het werk kleiner maken dan gedacht. umbrelOS doet géén
|
||||
hostnaamcontrole op de store-URL, dus een eigen Gitea kan de app store zijn. En wisselen tussen Electrs,
|
||||
Fulcrum en ElectrumX vraagt geen eigen mechanisme: die declareren `implements: [electrs]` en aliassen
|
||||
zichzelf naar `APP_ELECTRS_*`, dus twee regels volstaan. Dat was oorspronkelijk een apart plan waard en
|
||||
is nu fase 3.
|
||||
|
||||
**Geraakt:** promotie vanuit `Plannen/Masterplannen/Appstore.PLAN.md`; nog geen app-bestanden.
|
||||
**Tests:** niet van toepassing, dit project heeft er geen. Er is nog niets op de Umbrel geverifieerd.
|
||||
@@ -0,0 +1,174 @@
|
||||
# Taken - Appstore
|
||||
|
||||
> Prioriteit: **B** | Afhankelijk van: –
|
||||
>
|
||||
> **Van A naar B op 20-08-2026**, bij de prioriteitsherziening na de verse installatie van 0.0.9. Alles
|
||||
> waar dit plan om begon is af: de app is een echte umbrelOS-app, hij komt op uit een schone installatie,
|
||||
> en een wallet van buiten verbindt over TLS. Wat er nog staat is niet te plannen (de herstartcontrole
|
||||
> wacht op een herstart die er toch komt) of hoort bij het masterplan **Publicatie**. Het plan houdt zijn
|
||||
> nummer 010; alleen **Webinterface** kreeg een nieuw nummer bij de promotie naar A.
|
||||
|
||||
## Volgende stap
|
||||
|
||||
- [x] **De maphiërarchie na een verse installatie nagekeken. In orde (20-08-2026):** in
|
||||
`~/umbrel/app-data/whatsnext-electrum-gate/` staat `data/` met `certs/` en `runtime/` eronder. Dat is
|
||||
de vorm die de appstore-eis vraagt, en samen met de twee `.gitkeep`-bestanden in de repo is dat punt
|
||||
van het masterplan **Publicatie** dus af. Wat deze controle niet onderscheidt is wie de mappen
|
||||
aanmaakte, `rsync` of Docker; voor de eis maakt dat niet uit, want die gaat over wat er in de repo
|
||||
staat
|
||||
|
||||
- [x] **0.0.9 opnieuw geïnstalleerd in plaats van geüpdatet. Gelukt (20-08-2026).** De gebruiker koos een
|
||||
deïnstallatie plus verse installatie omdat de app-data dan schoon is, en dat werkte. Daarmee is
|
||||
bewezen wat op een bestaande installatie niet te bewijzen was: **de pagina komt omhoog op een
|
||||
installatie waar nog nooit een certificaat gekozen is.** Dat is precies de situatie waarin 0.0.3
|
||||
stukliep
|
||||
- [ ] **Na `sudo reboot` controleren dat de app vanzelf terugkomt.** Dit is de aanleiding voor het hele
|
||||
plan en nu het enige wat er in de kern nog openstaat; zie fase 6. **Eigenaar: gebruiker, op een
|
||||
moment dat het kan**
|
||||
|
||||
- [x] **De doorstuurregel in de router naar poort 50022 gezet (20-08-2026).** Nodig omdat de app niet meer
|
||||
op 50002 luistert; zonder deze regel komt een wallet van buiten niet binnen
|
||||
- [x] **Een certificaat kiezen op de pagina, en daarmee het hele pad. Gelukt (20-08-2026):** de keuze werd
|
||||
aangenomen, nginx kwam met poort 50022 door, en een wallet van buiten verbindt. De app doet waarvoor
|
||||
hij bestaat
|
||||
|
||||
- [x] **0.0.4 uitrollen en kijken of de pagina nu komt. Gelukt (20-08-2026).** De pagina laadt, de
|
||||
melding "No certificate in use" staat er met de reden van de agent, en de veertien certificaten uit
|
||||
Zoraxy staan als keuzelijst. Daarmee is de klem uit 0.0.3 bewezen verholpen
|
||||
|
||||
- [x] De oude app `electrumtls-electrum-tls` verwijderen in umbrelOS, de store-URL opnieuw laten ophalen,
|
||||
en `whatsnext-electrum-gate` installeren. **Gedaan (20-08-2026), en het leverde meteen een fout op:**
|
||||
er kwam geen UI. Oorzaak was geen installatiefout maar een ontwerpfout, verholpen in 0.0.4, zie
|
||||
[PROGRESS.md](PROGRESS.md) en [CHANGELOG-electrum-gate.md](../../../CHANGELOG-electrum-gate.md)
|
||||
|
||||
Daarna, in deze volgorde:
|
||||
|
||||
- [x] `version` op **0.0.3** en de release notes herschreven (19-08-2026). Niet 3.0.0: de gebruiker heeft
|
||||
de nummering opnieuw onder 1.0 gezet, omdat 1.0.0 en 2.0.x een rijpheid beweerden die er niet was.
|
||||
De stap terug in het nummer kan omdat de oude app gedeïnstalleerd wordt en dit een nieuwe app-id is,
|
||||
dus er is geen geïnstalleerd manifest om tegen te vergelijken. **Vanaf hier moet het nummer altijd
|
||||
omhoog**; zie [CHANGELOG-electrum-gate.md](../../../CHANGELOG-electrum-gate.md)
|
||||
- [x] Controleren of de wallet-verbinding de nacht heeft doorstaan. **Gelukt (19-08-2026):** de
|
||||
verbinding stond er 's ochtends nog. Daarmee is de `proxy_timeout`-fix bewezen en de storing
|
||||
verklaard en verholpen
|
||||
- [x] Het plan **Webinterface** promoveren naar `Actief/`; het werk daaraan is begonnen
|
||||
- [ ] Het plan **Configuratie** herzien vóórdat het gepromoveerd wordt. Bij het afsluiten van de sessie op
|
||||
19-08-2026 bleek het grotendeels ingehaald: de agent doet §4e en §4f al, en het hardgecodeerde
|
||||
domein is uit `docker-compose.yml` én uit `nginx.conf.template` verdwenen, dus fase 2 is op de
|
||||
README na klaar. Wat er nog echt in zit, is kleiner dan het plan suggereert. Eerst opschonen, dan
|
||||
beslissen of promotie nog nodig is
|
||||
|
||||
## Fase 1 - De repo-vorm
|
||||
|
||||
- [x] `umbrel-app-store.yml` aanmaken met `id: electrumtls` en een `name`
|
||||
- [x] Map `electrumtls-electrum-tls/` aanmaken en de app-bestanden erheen verplaatsen
|
||||
- [x] `id` in `umbrel-app.yml` op `electrumtls-electrum-tls` zetten, gelijk aan de mapnaam
|
||||
- [x] `install.sh` en `uninstall.sh` verwijderen; umbreld beheert dit onder umbrelOS 1.x
|
||||
- [x] Verplaatsen en inhoudelijk wijzigen in **twee** commits houden, anders is de verhuizing niet als
|
||||
"alleen verplaatst" na te lezen
|
||||
|
||||
## Fase 2 - De compose
|
||||
|
||||
- [x] `app_proxy`-service toevoegen met `APP_HOST: electrumtls-electrum-tls_server_1`
|
||||
- [x] Het externe `umbrel_main_network` en het top-level `networks:`-blok verwijderen
|
||||
- [x] `version: "3.7"` weghalen; die sleutel is verouderd
|
||||
- [x] Certificaatmount op `${UMBREL_ROOT}/app-data/zoraxy/...` in plaats van een absoluut pad
|
||||
- [x] De handmatige `${APP_ELECTRUM_TLS_WEB_PORT}`-plaatshouder eruit; de web-UI loopt via `app_proxy`
|
||||
- [x] **Uitzoeken hoe `entrypoint.sh`, `cert-watch.sh` en `web/` in `${APP_DATA_DIR}` terechtkomen.**
|
||||
Bij installatie kopieert umbreld de hele app-map met `rsync --archive`, dus de mounts kloppen. Bij
|
||||
een **update** wordt alleen een whitelist ververst; zie [OPEN.md](OPEN.md) punt 6
|
||||
- [ ] Logica die later nog moet kunnen wijzigen uit `entrypoint.sh` halen en in de compose of in een
|
||||
`*.template` zetten, anders levert een `git push` geen update op bij een bestaande installatie
|
||||
|
||||
## Fase 3 - Backend-onafhankelijk
|
||||
|
||||
- [x] `dependencies: [electrs]` in het manifest laten staan en controleren
|
||||
- [x] `ELECTRS_HOST` van `${APP_ELECTRS_IP}` naar `${APP_ELECTRS_NODE_IP}` (nu wijst hij naar de
|
||||
web-UI-container van Electrs, niet naar de Electrum-server)
|
||||
- [x] `ELECTRS_PORT` van de vaste 50001 naar `${APP_ELECTRS_NODE_PORT}`
|
||||
- [x] Controleren dat er nergens een Electrs-specifieke variabele gebruikt wordt; alleen `IP`, `NODE_IP`
|
||||
en `NODE_PORT` worden door Fulcrum en ElectrumX gealiast
|
||||
- [x] De terugval `electrs_electrs_1` in `entrypoint.sh` vervangen door hard stoppen met een melding;
|
||||
een oude containernaam als standaard laat de app draaien terwijl hij naar niets wijst
|
||||
- [ ] Op de Umbrel verifiëren: omschakelen naar Fulcrum en controleren dat het zonder aanpassing werkt
|
||||
(staat ook in fase 6)
|
||||
|
||||
## Fase 4 - De image
|
||||
|
||||
- [x] Uitkomst van de "Volgende stap" verwerken: nginx `stream`, of terugvallen op een eigen image
|
||||
- [ ] Image pinnen op de multi-arch index-digest, geverifieerd met `docker buildx imagetools inspect`
|
||||
(kan alleen op de Umbrel; staat als TODO in de compose). **Sinds 20-08-2026 ook de grootste
|
||||
openstaande eis voor het masterplan Publicatie**, want de officiële store eist
|
||||
`repo:versie@sha256:<digest>` met beide architecturen erin. Blijft hier staan tot dat plan actief
|
||||
wordt; een taak op twee plekken loopt uit elkaar
|
||||
- [x] `apk add` bij het starten verwijderen
|
||||
- [x] De `cert-monitor`-service samenvoegen met de proxy en de Docker-socket-mount schrappen
|
||||
- [x] Certificaatwissel omzetten naar een reload binnen de container in plaats van een containerherstart
|
||||
- [x] `entrypoint.sh` en `cert-watch.sh` verwijderen; de logica staat nu in de compose en in
|
||||
`nginx.conf.template`, die allebei bij een update wél ververst worden
|
||||
|
||||
## Fase 5 - Het manifest
|
||||
|
||||
- [x] `port` op de web-UI-poort zetten in plaats van 50002
|
||||
- [x] Eigen icoon toevoegen; verwijst nu naar het Electrs-icoon in andermans repo
|
||||
- [x] `gallery` invullen of tot het minimum beperken; het wijst nu naar een screenshot van Electrs
|
||||
- [x] `developer`, `website`, `repo`, `support`, `submitter` en `submission` kloppend maken
|
||||
- [x] `releaseNotes` en `version` bijwerken
|
||||
- [x] `tagline` en `description` bijwerken; die noemden stunnel, dat er niet meer is
|
||||
- [x] Controleren dat Gitea `icon.png` als afbeelding serveert. **Ja**, na het omzetten van de
|
||||
accountzichtbaarheid: 33,9 kB als `image/png`. De eerdere 401 en 404 kwamen van dezelfde oorzaak
|
||||
als de mislukte kloon, dus er is geen `data:`-URI nodig
|
||||
|
||||
## Fase 6 - Installeren en verifiëren
|
||||
|
||||
- [x] De bestaande handmatige installatie stoppen; die hield poort 50002 vast en liet de installatie drie
|
||||
keer falen. Stond in `~/umbrel/home/Containers/ElectrumTLS`
|
||||
- [x] Store-URL toevoegen in umbrelOS en de app installeren
|
||||
- [x] Verbinden met een Electrum-wallet over TLS. Werkt: de wallet krijgt direct data terug, wat het hele
|
||||
pad bewijst, dus TLS-terminatie, certificaat en de verbinding naar de Electrum-server
|
||||
- [ ] **Na `sudo reboot` controleren dat de app vanzelf terugkomt.** Dit is de aanleiding voor het plan
|
||||
en de enige controle die er nog echt toe doet. **Uitgesteld op 18-08-2026:** de Umbrel is een
|
||||
productiemachine en wordt niet op verzoek herstart. Zie [OPEN.md](OPEN.md) punt 7
|
||||
- [ ] Vervanger die nu wél kan: de app stoppen en starten via umbrelOS. Dat gebruikt hetzelfde
|
||||
`app-script start`-pad als het opstarten na een herstart, maar bewijst niet de volgorde ten
|
||||
opzichte van Zoraxy
|
||||
- [ ] Controleren dat de app terugkomt nadat Electrs gestopt en gestart is
|
||||
- [ ] **De oude stunnel-container weghalen, vóór de Fulcrum-test.** Hij bindt poort 50002 en dat is de
|
||||
poort die Fulcrum op de host wil, dus die test kan niet slagen zolang hij bestaat. Met
|
||||
`docker compose -f ~/umbrel/home/Containers/ElectrumTLS/docker-compose.yml down`. Het externe
|
||||
`umbrel_main_network` blijft daarbij staan; dat verwijdert compose nooit. **Eigenaar: gebruiker**
|
||||
- [ ] Omschakelen naar Fulcrum in de umbrelOS-instellingen en controleren dat het zonder aanpassing werkt
|
||||
- [ ] Pas ná een geslaagde herstart de oude map `~/umbrel/home/Containers/ElectrumTLS` opruimen; tot dan
|
||||
is dat de terugweg. Let op de volgorde: de container mount zijn `entrypoint.sh` uit die map, dus
|
||||
eerst de container weg en dan de map
|
||||
|
||||
## Fase 7 - Naam en identiteit
|
||||
|
||||
Toegevoegd 18-08-2026. De naam beschreef het middel (TLS) en niet wat je ermee kunt.
|
||||
|
||||
- [x] Naam wordt **Electrum Gate**. Bijgesteld op 19-08-2026; op 18-08-2026 stond hier nog Electrum
|
||||
Gateway. TLS is uit de naam en uit de tagline verdwenen; die gaat nu over de afweging Tor tegenover
|
||||
snelheid
|
||||
- [x] Store-id `whatsnext`, app-id en mapnaam `whatsnext-electrum-gate`. Zie [OPEN.md](OPEN.md) punt 8
|
||||
- [x] `developer` en `submitter` op `WhatsNext?` (19-08-2026)
|
||||
- [x] `tagline` en `description` herschreven, in het Engels, met de Tor-afweging en de lijst met clients
|
||||
waarvoor de app nuttig is
|
||||
- [ ] Eigen icoon van de gebruiker inbouwen, in de kleuren van het design-systeem. Het huidige
|
||||
`icon.png` staat er nog en is niet het definitieve
|
||||
- [x] **De repo hernoemd naar `UmbrelApps`, met `icon`, `website`, `repo`, `support` en `submission` mee
|
||||
(25-08-2026, in 0.0.15).** Niet uitgesteld tot GitHub, zoals hier stond, maar afgedwongen door Evolu
|
||||
Relay: umbrelOS leest per store één repo, dus een tweede app maakte een naam naar deze ene app
|
||||
onhoudbaar. Het **app-id** ging niet mee en dat hoefde ook niet; dat hangt aan het store-id en niet
|
||||
aan de URL
|
||||
- [ ] **In umbrelOS de oude store verwijderen en de nieuwe URL toevoegen.** Hier valt te zien of een
|
||||
geïnstalleerde app een wisseling van store-URL overleeft, en dat is **niet uitgezocht**; ga er niet
|
||||
van uit. Zie [OPEN.md](OPEN.md) punt 8. **Eigenaar: gebruiker**
|
||||
- [ ] Pas daarna de repo `ElectrumTLS` op de Git-server weghalen. Dat is niet alleen opruimen: zolang hij
|
||||
bestaat, staat het domein nog in een publieke historie. Zie [OPEN.md](OPEN.md) punt 3.
|
||||
**Eigenaar: gebruiker**
|
||||
|
||||
## Geblokkeerd / wacht op
|
||||
|
||||
- [x] Keuze van de TLS-poort - **vervallen 19-08-2026**. Open punt 1 van het plan **Configuratie** is
|
||||
beslist op 50022 en meteen doorgevoerd, omdat de app toch opnieuw geïnstalleerd wordt. Daarmee is
|
||||
ook de botsing met Fulcrum weg en kan de omschakelcontrole in fase 3 en fase 6 echt gedaan worden
|
||||
@@ -0,0 +1,213 @@
|
||||
# Appstore - masterplan
|
||||
|
||||
> **Status: nog niet actief.** Dit is één bestand en dat is bewust: er is nog geen `TAKEN.md`,
|
||||
> `PROGRESS.md` of `OPEN.md`, want er wordt nog niet aan gewerkt. Bij promotie naar
|
||||
> `Plannen/Actief/NNN-Appstore/` worden de paragrafen hieronder over die vier bestanden verdeeld; zie
|
||||
> `HomeGit/Docs/Werkproces.md` §1b.
|
||||
>
|
||||
> Afhankelijk van: –
|
||||
|
||||
## 1. Doel
|
||||
|
||||
De app draait nu als een met de hand neergezette docker-compose die umbrelOS niet kent. Gevolg: na een
|
||||
herstart van de Umbrel, of als een app waar deze van afhangt omvalt, moet er met de hand
|
||||
`docker compose down` en `up` gedaan worden. Dit plan maakt er een echte Umbrel-app van in een eigen
|
||||
community app store: een tegel met icoon, die umbrelOS zelf installeert, start en na een herstart weer
|
||||
opbrengt.
|
||||
|
||||
Als dit af is, is de repo tegelijk de app store: de URL erin plakken in umbrelOS is genoeg om de app te
|
||||
installeren, en een `git push` is genoeg om een update uit te leveren. De repo staat op de eigen
|
||||
Git-server, `https://sc.kamenier-hamer.nl/sysop/ElectrumTLS.git`, en dat kan: umbreld doet geen enkele
|
||||
controle op de hostnaam. Zie §4e voor wat dat wél afdwingt.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
- De repo omzetten naar de vorm die umbrelOS voor een community app store verwacht.
|
||||
- `docker-compose.yml` omzetten naar de moderne vorm: `app_proxy`, geen zelfgebouwd netwerk, geen
|
||||
handmatige host-poorten waar dat niet hoeft.
|
||||
- De Electrum-backend via de afhankelijkheid aanspreken in plaats van via een hardgecodeerde
|
||||
containernaam, zodat Electrs, Fulcrum en ElectrumX alle drie werken.
|
||||
- Een image die gepind kan worden, in plaats van `alpine:latest` met `apk add` bij elke start.
|
||||
- Het manifest compleet en eerlijk maken: eigen icoon, eigen gallery, kloppende velden.
|
||||
- De oude `install.sh` en `uninstall.sh` weghalen.
|
||||
|
||||
De volledige spec waar dit tegenaan moet, met bronvermelding per feit, staat in
|
||||
[Referenties/Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md). Die is bij het schrijven
|
||||
van dit plan uitgezocht en hoeft niet opnieuw opgezocht te worden.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **De hardgecodeerde waarden eruit halen.** Het domein `sync.kamenier-hamer.nl` en het Zoraxy-pad blijven
|
||||
in dit plan staan zoals ze zijn. Dat is het plan **Configuratie**, en het apart houden is bewust: een
|
||||
commit die tegelijk de structuur omgooit en de configuratie herontwerpt is niet meer na te lezen.
|
||||
Wel een harde koppeling: **de repo gaat pas naar GitHub als Configuratie af is**, want anders staat een
|
||||
persoonlijk domein en de indeling van een privé-server in een publieke repo. Zie open punt 3.
|
||||
- **De web-UI eerlijk maken.** De pagina toont verzonnen status. Dat is het plan **Webinterface**. Hier
|
||||
wordt de pagina alleen verhuisd en aan `app_proxy` gehangen, niet herschreven.
|
||||
- **Meerdere apps in de store.** De store krijgt de vorm die meer apps toelaat, maar er komt er één in.
|
||||
- **Indienen bij de officiële Umbrel App Store.** Een community store is er juist om dat niet te hoeven.
|
||||
Als het later toch aantrekkelijk wordt, is de spec-eis grotendeels dezelfde, dus dit sluit niets af.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a. De repo-vorm
|
||||
|
||||
```
|
||||
ElectrumTLS/
|
||||
├── umbrel-app-store.yml id: electrumtls
|
||||
├── electrumtls-electrum-tls/
|
||||
│ ├── umbrel-app.yml id: electrumtls-electrum-tls
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── entrypoint.sh
|
||||
│ └── web/index.html
|
||||
├── Docs/
|
||||
├── README.md
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
De store-id is `electrumtls`, gekozen op 18-08-2026. De prefix-eis is hard: mapnaam en manifest-`id`
|
||||
moeten gelijk zijn en allebei met de store-id beginnen.
|
||||
|
||||
### 4b. Backend-onafhankelijk, en waarom dat bijna niets kost
|
||||
|
||||
umbrelOS 1.3 heeft swappable dependencies. Fulcrum en ElectrumX declareren allebei `implements: [electrs]`
|
||||
en hun `exports.sh` aliast `APP_ELECTRS_IP`, `APP_ELECTRS_NODE_IP` en `APP_ELECTRS_NODE_PORT` naar hun
|
||||
eigen waarden. De gebruiker kiest de implementatie in de umbrelOS-instellingen; umbrelOS laadt de
|
||||
`exports.sh` van de gekozen app.
|
||||
|
||||
Deze app hoeft daarvoor dus **geen keuzemechanisme te bouwen**. Het is dit:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
- electrs
|
||||
```
|
||||
|
||||
en in de compose `${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}` gebruiken.
|
||||
|
||||
Twee vallen om te vermijden. De eerste: de huidige compose zet `ELECTRS_HOST=${APP_ELECTRS_IP}`, en dat
|
||||
is de **web-UI-container** van Electrs, niet de Electrum-server; dat moet `APP_ELECTRS_NODE_IP` worden.
|
||||
De tweede: alleen `IP`, `NODE_IP` en `NODE_PORT` worden gealiast, dus alles wat Electrs-specifiek is
|
||||
(zoals `APP_ELECTRS_RPC_HIDDEN_SERVICE`) mag hier niet gebruikt worden.
|
||||
|
||||
### 4c. De image: nginx `stream` in plaats van stunnel
|
||||
|
||||
Het huidige `alpine:latest` plus `apk add stunnel` bij elke start is op drie manieren fout: het is niet
|
||||
te pinnen, het faalt zonder internet, en het maakt de starttijd afhankelijk van een Alpine-mirror. De
|
||||
app-store-eis is een image gepind op de multi-arch index-digest.
|
||||
|
||||
Er zijn drie wegen, en de aanbeveling is de derde:
|
||||
|
||||
1. **Eigen image bouwen** met een `Dockerfile` en een GitHub Actions-workflow die multi-arch naar GHCR
|
||||
duwt. Correct, maar het voegt CI, een registry en een tweede uitleverstroom toe aan een app die verder
|
||||
uit twee shellscripts bestaat.
|
||||
2. **Een bestaande stunnel-image pinnen.** Er is geen onderhouden multi-arch stunnel-image die het
|
||||
vertrouwen waard is. Afgevallen.
|
||||
3. **De officiële `nginx`-image gebruiken en de `stream`-module de TLS-terminatie laten doen.** Die image
|
||||
is multi-arch, wordt onderhouden en is gewoon te pinnen. Dan valt er meer weg dan alleen het
|
||||
bouwprobleem:
|
||||
- dezelfde container serveert de web-UI én termineert TLS, dus van drie containers blijft er één over;
|
||||
- een certificaatwissel wordt `nginx -s reload` **binnen** de container, dus de `cert-monitor` heeft de
|
||||
Docker-socket niet meer nodig. Die socket is nu read-only gemonteerd, maar read-only op de
|
||||
Docker-socket beschermt niets: wie de socket kan lezen kan containers starten en is daarmee root op
|
||||
de host. Dat weghalen is de grootste beveiligingswinst in dit plan;
|
||||
- een reload verbreekt bestaande verbindingen niet, een containerherstart wel.
|
||||
|
||||
**Te verifiëren voordat hierop gebouwd wordt:** dat de officiële `nginx:alpine` daadwerkelijk met
|
||||
`--with-stream` en `--with-stream_ssl_module` gebouwd is. Dat is de aanname waar deze hele keuze op
|
||||
rust en hij is in één commando te controleren (`nginx -V`). Klopt hij niet, dan valt dit terug op weg 1.
|
||||
|
||||
De `awk`-splitsing van de certificaat-chain uit `entrypoint.sh` blijft bruikbaar en wordt overgenomen.
|
||||
|
||||
### 4e. De eigen Git-server als app store
|
||||
|
||||
De repo komt op `https://sc.kamenier-hamer.nl/sysop/ElectrumTLS.git`. umbreld valideert de URL alleen met
|
||||
de `URL`-constructor en kloont met isomorphic-git; er is geen GitHub-eis. Wat er wél uit die aanroep volgt
|
||||
en wat dus getest moet worden:
|
||||
|
||||
- **HTTPS, niet SSH.** De URL hierboven heeft de goede vorm.
|
||||
- **Anoniem kloonbaar.** umbreld geeft geen inloggegevens mee, dus de repo moet in Gitea op publiek
|
||||
staan. Een privérepo werkt niet, en er is geen omweg.
|
||||
- **Geldig certificaat op `sc.kamenier-hamer.nl`.** Node valideert de keten. Loopt die host al via
|
||||
Zoraxy met Let's Encrypt, dan is dit in orde, maar het is het controleren waard voordat het klonen
|
||||
onverklaarbaar faalt.
|
||||
- **Alles op de standaardbranch.** Er wordt met `depth: 1, singleBranch: true` gekloond, dus een tag of
|
||||
tweede branch levert niets op.
|
||||
- **De URL is de identiteit van de store.** Wijzigt hij, dan ziet umbrelOS een andere store en moet hij
|
||||
opnieuw toegevoegd worden. Kies hem dus één keer goed.
|
||||
|
||||
### 4d. Poorten
|
||||
|
||||
`port:` in het manifest is de **web-UI-poort** van de tegel, niet de TLS-poort. Dat staat nu op 50002 en
|
||||
is daarmee fout.
|
||||
|
||||
De TLS-poort blijft een gepubliceerde host-poort; daar helpt `app_proxy` niet, want dat is voor HTTP.
|
||||
Welke poort dat wordt is een open punt in **Configuratie**: Fulcrum bezet host-poort 50002 en botst dus
|
||||
met de huidige keuze.
|
||||
|
||||
## 5. Het werk in grote lijnen
|
||||
|
||||
**Fase 1 - De repo-vorm.** `umbrel-app-store.yml` erbij, de app-bestanden naar
|
||||
`electrumtls-electrum-tls/`, het app-id met prefix, `install.sh` en `uninstall.sh` eruit. Uitkomst: de
|
||||
repo heeft de vorm die umbrelOS herkent.
|
||||
|
||||
**Fase 2 - De compose.** `app_proxy` erin, het externe `umbrel_main_network` en de handmatige
|
||||
`APP_..._PORT`-plaatshouders eruit, de mounts op `${APP_DATA_DIR}` en `${UMBREL_ROOT}`. Uitkomst: een
|
||||
compose die umbrelOS zelf kan draaien.
|
||||
|
||||
**Fase 3 - Backend-onafhankelijk.** `dependencies: [electrs]` en `${APP_ELECTRS_NODE_IP}` /
|
||||
`${APP_ELECTRS_NODE_PORT}`. Uitkomst: de app werkt met Electrs, Fulcrum en ElectrumX zonder aanpassing.
|
||||
|
||||
**Fase 4 - De image.** `nginx:alpine` gepind op digest, `stream`-configuratie in plaats van stunnel,
|
||||
`cert-monitor` samengevoegd en de Docker-socket eruit. Uitkomst: één gepinde container, geen `apk add`
|
||||
bij start, geen socket.
|
||||
|
||||
**Fase 5 - Het manifest.** Eigen icoon en gallery in de repo, `port` op de web-UI-poort, `developer`,
|
||||
`repo`, `support`, `submitter` en `submission` kloppend, `website` naar de eigen repo. Uitkomst: een
|
||||
tegel die er klopt uitziet en niet naar andermans plaatjes wijst.
|
||||
|
||||
**Fase 6 - Installeren en verifiëren.** Store toevoegen in umbrelOS, installeren, en de dingen nalopen
|
||||
die alleen op het apparaat te zien zijn: start hij mee na een herstart, komt hij terug als Electrs
|
||||
omvalt, en werkt de TLS-verbinding vanaf een echte wallet.
|
||||
|
||||
## 6. Open punten
|
||||
|
||||
1. **Draait `nginx:alpine` met de `stream`- en `stream_ssl`-module?** De hele keuze uit §4c hangt hierop.
|
||||
**Moment:** als eerste taak van fase 4, vóór er iets herschreven wordt. **Eigenaar:** uitvoerder.
|
||||
|
||||
2. **Wat gebeurt er met een bestaande installatie?** Er draait nu een handmatig neergezette
|
||||
`electrum-tls` op de Umbrel. Die moet met de hand weg voordat de echte app geïnstalleerd wordt, anders
|
||||
vecht hij om de poort. Hoort daar een korte migratie-aanwijzing bij in de README, of doet de gebruiker
|
||||
dat eenmalig zelf? **Moment:** bij fase 6. **Eigenaar:** gebruiker.
|
||||
|
||||
3. **Wanneer wordt er voor het eerst gepusht?** **Beslist op 18-08-2026: meteen.** De repo staat sinds
|
||||
die dag publiek op `https://sc.kamenier-hamer.nl/sysop/ElectrumTLS.git`, met de
|
||||
installatiespecifieke waarden er nog in. Dat de repo publiek moest, stond niet ter discussie: umbreld
|
||||
kloont anoniem. Alleen de volgorde was een keuze, en die is bewust vóór **Configuratie** gevallen.
|
||||
|
||||
Het gevolg dat vastligt: **`sync.kamenier-hamer.nl` en het certificaatpad staan nu in de publieke
|
||||
historie.** Ze eruit halen in Configuratie haalt ze uit de bestanden, niet uit de historie; daarvoor
|
||||
zou de historie herschreven moeten worden, en dat is bij een repo die anderen al gekloond kunnen
|
||||
hebben geen schoonmaak maar een breuk. Behandel het domein dus als bekend, en laat het geen argument
|
||||
worden om Configuratie uit te stellen: de winst daarvan zit nu in herbruikbaarheid, niet meer in
|
||||
geheimhouding.
|
||||
|
||||
4. **Eigen icoon.** Het manifest wijst nu naar het Electrs-icoon in andermans repo. Er moet een eigen
|
||||
SVG komen en een gallery-plaatje. Wie maakt die, en waar staan ze (in de repo of extern gehost)?
|
||||
**Moment:** fase 5. **Eigenaar:** gebruiker.
|
||||
|
||||
## 7. Verificatie
|
||||
|
||||
Er zijn geen tests in dit project en die zijn hier ook niet zinvol: de app is configuratie, geen code.
|
||||
Wat er wél moet, en wat per se handmatig op het apparaat gebeurt:
|
||||
|
||||
- de store laat zich in umbrelOS toevoegen en de app verschijnt met icoon en tegel;
|
||||
- installeren via de umbrelOS-interface werkt zonder SSH;
|
||||
- **na `sudo reboot` komt de app vanzelf terug.** Dit is de aanleiding voor het hele plan en dus de
|
||||
belangrijkste controle;
|
||||
- de app komt ook terug nadat Electrs handmatig gestopt en gestart is;
|
||||
- een Electrum-wallet verbindt over TLS en verifieert het certificaat;
|
||||
- omschakelen naar Fulcrum in de umbrelOS-instellingen laat de app werken zonder aanpassing.
|
||||
|
||||
Wat automatisch gecontroleerd kan worden, en de moeite waard is omdat het de fouten vangt die je niet
|
||||
ziet: dat de YAML geldig is, dat het `id` in het manifest gelijk is aan de mapnaam, en dat elke `image:`
|
||||
een `@sha256:`-digest heeft.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Plan: Trezor Suite Sync als Umbrel App
|
||||
|
||||
## Status quo (onderzocht 25 aug 2026)
|
||||
- Trezor's custom-sync-server heet **Evolu Relay**, onderdeel van de repo `trezor/trezor-suite-sync`.
|
||||
- De repo bevat twee services: **evolu-relay** (poort 4000, de eigenlijke sync-relay) en **quota-manager** (poort 4001, betaal/quota-server) plus een **Postgres**-database.
|
||||
- Er bestaat een `Dockerfile` en `docker-compose.yaml` in de repo — dus containeriseren is al gedeeltelijk gedaan door Trezor zelf.
|
||||
- **Geen bestaande Umbrel-app**: niet in de officiële store (`getumbrel/umbrel-apps`), en ik heb geen relevante custom store gevonden die 'm aanbiedt.
|
||||
- Conclusie: je zou de eerste zijn. Dat is haalbaar, want Umbrel-apps zijn in de kern "een `umbrel-app.yml` manifest + een `docker-compose.yml`" bovenop een al bestaande Docker-gebaseerde app.
|
||||
|
||||
## Doel
|
||||
Een installeerbare Umbrel-app die de Evolu Relay (en evt. quota-manager) draait, zodat je in Trezor Suite bij "Custom server" een lokale/eigen URL kunt invullen die naar je Umbrel wijst.
|
||||
|
||||
## Belangrijke open vraag eerst
|
||||
De quota-manager lijkt bedoeld voor **Trezor's eigen betaalde/quota-gebaseerde hosting** (het noemt een "Payment Server" en Notion API-spec). Voor puur privégebruik op je eigen Umbrel heb je waarschijnlijk **alleen de evolu-relay** nodig, zonder quota-manager. Dit moet je bevestigen door de `.env.sample` en broncode door te nemen — mogelijk verwacht de relay wél een werkende quota-manager-verbinding om te draaien. Zet dit als eerste stap in je proof-of-concept.
|
||||
|
||||
## Stap 1 — Proof of concept (lokaal, buiten Umbrel om)
|
||||
1. Clone `trezor/trezor-suite-sync`.
|
||||
2. Draai `docker compose up` zoals in de README, met alleen Postgres + evolu-relay (probeer quota-manager eerst weg te laten).
|
||||
3. Test of Trezor Suite (desktop) succesvol labels kan syncen naar `http://<jouw-ip>:4000`.
|
||||
4. Documenteer welke environment variables daadwerkelijk nodig zijn (uit `.env.sample`).
|
||||
5. **Blocker-check:** als de relay hard afhankelijk blijkt van de quota-manager, moet die ook mee gepakketteerd worden.
|
||||
|
||||
## Stap 2 — Umbrel App Framework structuur opzetten
|
||||
Volgens `getumbrel/umbrel-apps` heeft elke app een vaste structuur:
|
||||
```
|
||||
trezor-suite-sync/
|
||||
├── umbrel-app.yml # manifest: naam, versie, poort, categorie, beschrijving
|
||||
├── docker-compose.yml # services, aangepast voor Umbrel's netwerkconventies
|
||||
└── exports.sh (optioneel) # env vars die andere apps kunnen gebruiken
|
||||
```
|
||||
Aandachtspunten bij het omzetten van Trezor's eigen `docker-compose.yaml`:
|
||||
- Alle services moeten achter Umbrel's **`app_proxy`** draaien (voor routing/auth), tenzij je zelf auth afhandelt (zoals bv. Gitea/Budibase doen met `PROXY_AUTH_ADD: "false"`).
|
||||
- Poorten mogen niet botsen met andere geïnstalleerde apps — kies een vast, uniek poortnummer.
|
||||
- Data (Postgres-volumes) moet in Umbrel's `${APP_DATA_DIR}`-conventie staan zodat backups/updates werken.
|
||||
- Vervang eventuele Kubernetes-specifieke config (`.k8s/` map) — die is niet relevant voor Umbrel, puur Docker Compose telt.
|
||||
|
||||
## Stap 3 — HTTPS / bereikbaarheid van buitenaf
|
||||
Trezor Suite (desktop/mobiel, ook onderweg) moet de relay kunnen bereiken:
|
||||
- **Alleen thuisnetwerk:** lokaal IP + poort volstaat, geen HTTPS nodig als je Suite ook alleen thuis gebruikt.
|
||||
- **Ook buitenshuis:** dan heb je een manier nodig om je Umbrel veilig van buitenaf te bereiken — bijvoorbeeld via **Tailscale** (Umbrel heeft hier al een officiële app voor, zoals ook bij hun Nostr-relay-app wordt geadviseerd) of via een reverse proxy met een eigen domein + TLS-certificaat.
|
||||
- Aanbevolen aanpak: begin met Tailscale, dat is de weg van de minste weerstand en vermijdt dat je zelf poorten moet open zetten op je router.
|
||||
|
||||
## Stap 4 — Testen
|
||||
- Test op echte hardware: Raspberry Pi 5, x86-systeem, of Umbrel Home (zoals Umbrel's eigen testrichtlijnen voorschrijven).
|
||||
- Test het volledige label-sync-scenario: label toevoegen op device A, checken of het verschijnt op device B na sync.
|
||||
- Test update-/herstart-gedrag: overleeft de Postgres-data een app-herstart of Umbrel-OS-update?
|
||||
|
||||
## Stap 5 — Distributie: officieel vs. eigen store
|
||||
| Optie | Voor | Nadeel |
|
||||
|---|---|---|
|
||||
| **PR naar `getumbrel/umbrel-apps`** | Bereikt alle Umbrel-gebruikers, officieel gereviewd | Moet aan Umbrel's kwaliteitseisen voldoen, review kan lang duren, mogelijk willen ze afstemming met Trezor zelf |
|
||||
| **Eigen custom app store** (zoals `dentropy/dentropys-umbrel-appstore`) | Snel live, volledige controle | Alleen bereikbaar via handmatige CLI-toevoeging (`sudo ~/umbrel/scripts/repo add <url>`), kleiner bereik |
|
||||
|
||||
**Advies:** begin met een eigen custom store/repo voor je eigen gebruik en testen. Als het stabiel werkt, overweeg een PR naar de officiële store — lees eerst `AGENTS.md` in `getumbrel/umbrel-apps` voor de exacte richtlijnen.
|
||||
|
||||
## Risico's / dingen om in de gaten te houden
|
||||
- Trezor kan de relay-architectuur wijzigen (het is een vrij nieuw, actief project — laatste release v0.1.8, april 2026).
|
||||
- Quota-manager suggereert mogelijk een businessmodel rond gehoste sync; zelf-hosten omzeilt dat, maar controleer of er geen impliciete afhankelijkheden zijn (bv. licenties, rate-limits ingebakken in de relay-code).
|
||||
- Data is end-to-end versleuteld volgens Trezor (client-side), dus zelf-hosten van de relay verandert niets aan de privacy-garanties — het haalt alleen Trezor's eigen server uit de vergelijking.
|
||||
|
||||
## Volgende concrete actie
|
||||
Begin met **Stap 1**: lokaal draaien zonder Umbrel, en uitzoeken of quota-manager verplicht is. Dat bepaalt of dit een simpel 1-service-pakket wordt of een 3-service-stack (relay + quota-manager + postgres).
|
||||
@@ -0,0 +1,76 @@
|
||||
# Bereikbaarheid - masterplan
|
||||
|
||||
> **App: Evolu Relay.** Status: nog niet actief. Dit is één bestand en dat is bewust: er wordt nog niet
|
||||
> aan gewerkt. Bij
|
||||
> promotie naar `Plannen/Actief/NNN-Bereikbaarheid/` worden de paragrafen hieronder over de vier
|
||||
> bestanden verdeeld; zie `HomeGit/Docs/Werkproces.md` §1b.
|
||||
>
|
||||
> Afhankelijk van: **Umbrelapp**. Er valt niets bereikbaar te maken zolang er niets draait. Eén bevinding
|
||||
> uit **Proefopstelling** stuurt dit plan wel al: accepteert Trezor Suite een `http://`-adres, of eist het
|
||||
> TLS?
|
||||
|
||||
## 1. Doel
|
||||
|
||||
Trezor Suite ook buiten het thuisnetwerk laten synchroniseren met de eigen relay, zonder dat daarvoor een
|
||||
poort op de router open hoeft.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
De weg van een apparaat onderweg naar de relay op de Umbrel, en wat daarvoor op de Umbrel en op het
|
||||
apparaat geregeld moet worden. Inclusief de vraag wat er gebeurt als die weg wegvalt: een client die
|
||||
buiten het netwerk niets kan synchroniseren, moet dat binnen het netwerk nog wel gewoon doen.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Geen poort openzetten op de router als het te vermijden is.** Een sync-relay die rechtstreeks aan het
|
||||
internet hangt is een ander soort ding dan een relay op je eigen netwerk, en niets in dit project vraagt
|
||||
erom.
|
||||
- **Geen eigen certificaatbeheer bouwen.** Bestaat dat al op deze Umbrel, dan gebruiken we het. Zo niet,
|
||||
dan is dat een reden om voor de weg te kiezen die geen certificaat nodig heeft.
|
||||
- **Geen dienst van derden in het datapad die het verkeer termineert.** Dat is precies wat zelf hosten
|
||||
moest oplossen.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
Twee wegen, en ze sluiten elkaar niet uit.
|
||||
|
||||
**Tailscale.** umbrelOS heeft er een officiële app voor, en het is de weg van de minste weerstand: geen
|
||||
poort open, geen certificaat, geen domein. De prijs is een account bij een derde partij en een client op
|
||||
elk apparaat dat mee wil doen. Voor het datapad is dat geen bezwaar, want het verkeer loopt versleuteld
|
||||
tussen de apparaten zelf en niet via die partij, maar de coördinatie loopt er wel langs.
|
||||
|
||||
**Een reverse proxy met een eigen domein en TLS.** Zwaarder op te zetten, maar er is één ding dat het
|
||||
makkelijker maakt dan het lijkt: op deze Umbrel draait al een reverse proxy die certificaten beheert. Dat
|
||||
is dezelfde die Electrum Gate gebruikt. Het nadeel blijft dat er dan wél een poort open moet.
|
||||
|
||||
**Voorstel: beginnen met Tailscale**, en de reverse proxy pas overwegen als er een apparaat is dat geen
|
||||
Tailscale-client kan draaien.
|
||||
|
||||
Wat dit plan stuurt en wat hier nu nog onbekend is: **eist Trezor Suite een `https://`-adres?** Zo ja, dan
|
||||
valt de kale Tailscale-route weg of moet er alsnog een certificaat bij. Dat antwoord komt uit
|
||||
**Proefopstelling**, fase 4.
|
||||
|
||||
## 5. Het werk in grote lijnen
|
||||
|
||||
**Fase 1 - de keuze onderbouwen.** Vastleggen welke apparaten mee moeten doen en of Suite TLS eist.
|
||||
Uitkomst: één gekozen weg, met de reden erbij.
|
||||
|
||||
**Fase 2 - opzetten.** De gekozen weg inrichten op de Umbrel en op één apparaat.
|
||||
|
||||
**Fase 3 - verifiëren onderweg.** Synchroniseren vanaf een verbinding die niet het thuisnetwerk is, en
|
||||
daarna controleren dat het thuis nog steeds werkt.
|
||||
|
||||
## 6. Open punten
|
||||
|
||||
1. **Welke apparaten moeten buitenshuis kunnen synchroniseren?** Alleen een laptop is iets anders dan een
|
||||
telefoon, en het bepaalt of Tailscale volstaat.
|
||||
**Moment:** fase 1 · **Eigenaar:** gebruiker
|
||||
2. **Eist Trezor Suite TLS?** Zie hierboven.
|
||||
**Moment:** komt uit Proefopstelling · **Eigenaar:** volgt uit dat plan
|
||||
|
||||
## 7. Verificatie
|
||||
|
||||
Synchroniseren vanaf een mobiel netwerk, dus niet vanaf de wifi thuis. Dat is de enige test die telt; een
|
||||
test op het eigen netwerk met een externe naam kan slagen op een router die het verkeer naar binnen lust.
|
||||
|
||||
Daarna de tegenproef: werkt het thuis nog steeds, en werkt het nog als de gekozen weg wegvalt.
|
||||
@@ -0,0 +1,217 @@
|
||||
# Configuratie - masterplan
|
||||
|
||||
> **Status: nog niet actief.** Dit is één bestand en dat is bewust: er is nog geen `TAKEN.md`,
|
||||
> `PROGRESS.md` of `OPEN.md`, want er wordt nog niet aan gewerkt. Bij promotie naar
|
||||
> `Plannen/Actief/NNN-Configuratie/` worden de paragrafen hieronder over die vier bestanden verdeeld; zie
|
||||
> `HomeGit/Docs/Werkproces.md` §1b.
|
||||
>
|
||||
> Afhankelijk van: het plan **Appstore**, omdat dit dezelfde bestanden herschrijft en die eerst hun
|
||||
> nieuwe vorm moeten hebben.
|
||||
|
||||
## 1. Doel
|
||||
|
||||
De app is nu op één installatie toegesneden: het domein `sync.kamenier-hamer.nl`, het pad naar de
|
||||
certificaten van Zoraxy en de poortnummers staan in de scripts en de compose. Daardoor kan hij niet
|
||||
gedeeld worden, en, belangrijker, kan de repo niet publiek staan zonder de indeling van een privéserver
|
||||
mee te publiceren. Dat laatste is geen theoretisch bezwaar: een community app store **moet** anoniem
|
||||
kloonbaar zijn, anders kan umbrelOS hem niet ophalen.
|
||||
|
||||
Als dit af is, staat elke installatiespecifieke waarde in één configuratiebestand en bevat de repo zelf
|
||||
niets persoonlijks meer.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
- Alle vaste waarden uit de scripts, de compose en de web-UI halen: domein, certificaatpad,
|
||||
certificaatbestandsnamen, TLS-poort, backend-poort.
|
||||
- Eén configuratiebestand met verstandige standaardwaarden, dat bij een eerste start wordt aangemaakt als
|
||||
het er nog niet is.
|
||||
- De certificaatbron volledig instelbaar maken, met Zoraxy als standaard. Zo besloten op 18-08-2026.
|
||||
**Uitgebreid op 19-08-2026** op verzoek van de gebruiker: er moeten drie bronnen zijn, en er moet
|
||||
binnen zo'n bron een certificaat te **kiezen** zijn. Zie §4e.
|
||||
- ~~Beslissen welke TLS-poort de standaard wordt, gezien de botsing met Fulcrum.~~ **Beslist op
|
||||
19-08-2026: 50022.** De app draait er al op; zie open punt 1.
|
||||
- Documenteren wat er ingesteld kan worden, in de README die de gebruiker op de repo-pagina ziet.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Een instellingenscherm in de web-UI**, met **één uitzondering: de keuze van het certificaat.**
|
||||
Op 18-08-2026 was dit een heel niet-doel: instelbaar in een configuratiebestand is genoeg, en een
|
||||
formulier dat configuratie wegschrijft vraagt een backend. Op 19-08-2026 heeft de gebruiker voor de
|
||||
certificaatkeuze het tegendeel gekozen, en dat is te verdedigen omdat het de enige instelling is waar
|
||||
de app de mogelijke waarden zélf al kent: hij kijkt in de gemounte mappen. Een keuzelijst met wat
|
||||
gevonden is, is dan iets anders dan een configuratieformulier. De rest blijft in het bestand. Hoe de
|
||||
keuze wordt weggeschreven staat in §4f.
|
||||
- **Zelf certificaten aanvragen.** ACME, Let's Encrypt en verlenging blijven bij Zoraxy. Deze app leest
|
||||
alleen. Dat is de hele reden dat hij zo klein kan blijven.
|
||||
- **Meerdere domeinen of meerdere backends tegelijk.** Eén certificaat, één backend. Zolang daar geen
|
||||
concrete aanleiding voor is, is dat onnodige complexiteit.
|
||||
- **De poort die van buiten open staat.** Die is in de router doorgestuurd en staat daar op een ander
|
||||
nummer; dat is netwerkbeheer en niet iets wat deze app kan of moet weten.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a. Waar de configuratie staat
|
||||
|
||||
In `${APP_DATA_DIR}/config/`. Dat is de map die umbrelOS aan de app toewijst, die een herinstallatie van
|
||||
de app overleeft en die in de back-up meegaat. De repo bevat alleen een sjabloon; het werkelijke bestand
|
||||
wordt bij de eerste start aangemaakt als het ontbreekt, en daarna nooit meer overschreven. Anders wist een
|
||||
app-update de instellingen van de gebruiker, en dat is precies het soort fout dat je pas maanden later
|
||||
merkt.
|
||||
|
||||
Vorm: een `.env`-achtig bestand met `SLEUTEL=waarde`, want dat is zonder hulpmiddelen te lezen door een
|
||||
shellscript en met de hand te bewerken over SSH. YAML zou een parser vragen die er nu niet is.
|
||||
|
||||
### 4b. Wat er instelbaar wordt
|
||||
|
||||
| Sleutel | Standaard | Waarvoor |
|
||||
|-|-|-|
|
||||
| `TLS_DOMAIN` | leeg, dan automatisch detecteren | de naam waar het certificaat op staat |
|
||||
| `CERT_DIR` | het Zoraxy-certificatenpad | de map met certificaat en sleutel |
|
||||
| `CERT_FILE` | `${TLS_DOMAIN}.pem` | naam van het certificaat, voor bronnen die anders benoemen |
|
||||
| `KEY_FILE` | `${TLS_DOMAIN}.key` | naam van de sleutel |
|
||||
| `TLS_PORT` | open punt 1 | de poort waarop TLS binnenkomt |
|
||||
| `CHECK_INTERVAL` | 300 | seconden tussen twee certificaatcontroles |
|
||||
|
||||
De backend komt hier bewust **niet** in te staan: die volgt uit `${APP_ELECTRS_NODE_IP}` en
|
||||
`${APP_ELECTRS_NODE_PORT}`, die umbrelOS aanlevert op grond van de app die de gebruiker als
|
||||
Electrum-server gekozen heeft. Zie het plan **Appstore**, en
|
||||
[Referenties/Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md) §4 voor waarom dat werkt.
|
||||
|
||||
### 4c. Automatisch detecteren van het domein
|
||||
|
||||
Staat `TLS_DOMAIN` leeg, dan zoekt de app in `CERT_DIR` naar een `.pem` met een gelijknamige `.key`
|
||||
ernaast. Is er precies één paar, dan is dat het. Zijn het er meer, dan stopt de app met een leesbare
|
||||
foutmelding die de gevonden namen noemt en vraagt om `TLS_DOMAIN` in te vullen.
|
||||
|
||||
Bewust **niet** "pak de nieuwste". Bij meer certificaten is de nieuwste een gok, en een verkeerd
|
||||
certificaat kiezen levert een verbinding op die het lijkt te doen maar bij de wallet op een
|
||||
naamsverificatiefout stukloopt. Dat is lastiger te vinden dan een app die netjes weigert te starten.
|
||||
|
||||
### 4d. Het certificaat lezen zonder een pad vast te leggen
|
||||
|
||||
Het Zoraxy-pad is nu twee keer als absoluut pad in de compose gemonteerd. Dat wordt
|
||||
`${UMBREL_ROOT}/app-data/zoraxy/...`, wat hetzelfde oplevert maar niet aanneemt waar Umbrel staat.
|
||||
|
||||
Blijft over dat de mount in `docker-compose.yml` staat en de configuratie in een bestand: een gebruiker
|
||||
die `CERT_DIR` naar iets buiten die mount wijst, ziet de map niet in de container. Dat moet in de README,
|
||||
en de foutmelding moet het noemen. Het alternatief, de hele `${UMBREL_ROOT}/app-data` monteren, geeft de
|
||||
app leestoegang tot de gegevens van elke andere app en dat weegt niet op tegen het gemak.
|
||||
|
||||
### 4e. Drie certificaatbronnen, met een keuze binnen de bron
|
||||
|
||||
Toegevoegd 19-08-2026 op verzoek van de gebruiker. De app moet kunnen terugvallen op meer dan Zoraxy:
|
||||
|
||||
| `CERT_SOURCE` | Waar de app kijkt | Wie beheert de vernieuwing |
|
||||
|-|-|-|
|
||||
| `zoraxy` (standaard) | `${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs` | Zoraxy |
|
||||
| `npm` | de certificatenmap van Nginx Proxy Manager | Nginx Proxy Manager |
|
||||
| `own` | `${APP_DATA_DIR}/certs` | de gebruiker, met de hand |
|
||||
| `custom` | wat er in `CERT_DIR` staat | onbekend |
|
||||
|
||||
Twee dingen die dit groter maken dan "nog een pad erbij", en die het ontwerp sturen:
|
||||
|
||||
- **Het is een keuze binnen een map, niet alleen een keuze van een map.** Zoraxy en Nginx Proxy Manager
|
||||
beheren de certificaten van *alle* diensten op die machine, dus er staan er meestal meerdere. Welke van
|
||||
die certificaten deze app moet gebruiken, is een aparte instelling. De detectie uit §4c is daarmee de
|
||||
uitzondering en niet de regel: hij werkt alleen als er precies één paar staat, en dat is bij een
|
||||
gedeelde certificatenmap juist zelden zo. Vandaar het harde weigeren bij meerdere treffers, met de
|
||||
gevonden namen in de melding, want dát is de lijst waar de gebruiker uit kiest.
|
||||
- **Nginx Proxy Manager benoemt anders.** Waar Zoraxy `<domein>.pem` en `<domein>.key` schrijft, zet NPM
|
||||
zijn certificaten in genummerde mappen (`npm-<n>/fullchain.pem` en `privkey.pem`), dus het domein staat
|
||||
níet in de naam. De detectie op naam werkt daar dus niet en de nummer-naar-domein-koppeling zit in de
|
||||
database van NPM, die deze app niet mag lezen. **Voorlopige aanname, te verifiëren op de Umbrel voordat
|
||||
dit gebouwd wordt.** Voor `npm` wordt de instelling daarom waarschijnlijk de map en niet het domein, en
|
||||
leest de app het domein uít het certificaat in plaats van uit de bestandsnaam.
|
||||
|
||||
Gevolg voor de compose: elke bron die gemount moet worden, moet dat vooraf zijn, want een pad instellen
|
||||
naar iets wat niet gemount is levert een map op die de container niet ziet. Drie read-only mounts dus
|
||||
(Zoraxy, NPM, en de eigen map), en `custom` werkt alleen binnen een van die drie. Dat hoort in de README
|
||||
en in de foutmelding, net als in §4d.
|
||||
|
||||
Sleutels die hierbij horen, bovenop de tabel in §4b: `CERT_SOURCE` met `zoraxy` als standaard, en
|
||||
`CERT_NAME` voor de keuze binnen de bron. De twee samen vormen de id die §4f gebruikt, in de vorm
|
||||
`bron/naam`.
|
||||
|
||||
### 4f. De keuze wegschrijven vanaf het dashboard
|
||||
|
||||
Besloten 19-08-2026 door de gebruiker: de keuze gaat via een keuzelijst op het dashboard en niet via het
|
||||
configuratiebestand. Zie de uitzondering in §3.
|
||||
|
||||
De app kent de mogelijke waarden zelf, want ze staan in de gemounte mappen. De agent zet ze als
|
||||
`certificates` in `status.json`, met per certificaat de bron, de bestandsnaam, het domein uit het
|
||||
certificaat en de einddatum. De pagina toont die lijst met een keuzerondje en stuurt de gekozen id terug.
|
||||
|
||||
**Terugschrijven gaat via de agent**, een `PUT` op `api/certificate` die nginx doorstuurt. Dat is de
|
||||
tweede container uit het plan **Webinterface** §4a0, waar ook staat waarom die er is.
|
||||
|
||||
Onderweg is dit twee keer van vorm veranderd, en de tussenstap is het opschrijven waard omdat hij eruit
|
||||
zag als de goedkoopste oplossing:
|
||||
|
||||
- **eerst: de WebDAV-module van nginx.** Eén `location` met `dav_methods PUT` zet het bestand neer, geen
|
||||
extra proces, geen taal erbij. Nadeel dat het onderuit haalde: WebDAV kan alleen een bestand neerzetten
|
||||
en niets controleren. De validatie moest dan alsnog in de leeslus, en dan valideer je iets wat er al
|
||||
staat in plaats van het te weigeren;
|
||||
- **nu: de agent.** Die neemt de `PUT` aan, vergelijkt de id met wat hij zelf gevonden heeft, en weigert
|
||||
met een leesbare fout als het niet klopt. Dat is dezelfde guard, maar op de plek waar hij hoort.
|
||||
|
||||
Waarom dit ook nu geen echt instellingenformulier is: de mogelijke waarden komen uit de gemounte mappen en
|
||||
niet uit invoer van de gebruiker, dus er is niets vrij te typen. Het pad hangt achter de app-proxy van
|
||||
umbrelOS, die er zijn eigen inlog voor zet, en de TLS-poort staat er los van. Het ergste wat een geslaagde
|
||||
aanroep kan doen is een ander, ook bestaand, certificaat kiezen.
|
||||
|
||||
**De keuze staat in `${APP_DATA_DIR}/runtime/config/selected-cert`**, dus buiten de container, zodat hij
|
||||
een herstart en een app-update overleeft. Dat is de eis uit §4a en hij geldt hier net zo goed.
|
||||
|
||||
## 5. Het werk in grote lijnen
|
||||
|
||||
**Fase 1 - Het configuratiebestand.** Sjabloon, aanmaken bij eerste start, inlezen in het startscript,
|
||||
standaardwaarden. Uitkomst: de app leest zijn instellingen uit één bestand.
|
||||
|
||||
**Fase 2 - De waarden eruit.** Domein, paden en poorten uit `entrypoint.sh`, `cert-watch.sh`,
|
||||
`docker-compose.yml` en `web/index.html` vervangen door de ingelezen waarden. Uitkomst: `grep` op
|
||||
`kamenier` in de repo levert niets meer op buiten `Docs/`.
|
||||
|
||||
**Fase 3 - Automatisch detecteren en falen.** Detectie van het certificaatpaar, en leesbare fouten bij
|
||||
nul of meer dan één treffer. Uitkomst: een verse installatie werkt zonder iets in te vullen, en een
|
||||
onduidelijke situatie stopt met een bruikbare melding.
|
||||
|
||||
**Fase 4 - Documenteren.** De instellingen in de README, met de valkuil uit §4d. Uitkomst: iemand anders
|
||||
kan de app installeren zonder de scripts te lezen.
|
||||
|
||||
## 6. Open punten
|
||||
|
||||
1. **Welke TLS-poort wordt de standaard?** - **50022** (19-08-2026, gebruiker).
|
||||
50002 is de conventie voor Electrum over SSL, maar Fulcrum bezet die op de host, dus met Fulcrum erbij
|
||||
zou de app niet starten, en dat is precies het omschakelen dat deze app moet ondersteunen. De poort
|
||||
naar buiten is toch al een andere, want die staat in de router doorgestuurd, dus de conventie weegt
|
||||
hier licht.
|
||||
|
||||
Al doorgevoerd in `docker-compose.yml` en `nginx.conf.template`, vooruitlopend op dit plan, omdat de
|
||||
app op 19-08-2026 toch opnieuw geïnstalleerd werd. **Let op: dit vraagt eenmalig een aanpassing van de
|
||||
doorstuurregel in de router**, anders komt een wallet van buiten niet meer binnen. De wallet zelf
|
||||
merkt er niets van, want die gebruikt het externe poortnummer.
|
||||
|
||||
2. **Wat gebeurt er als het certificaat verdwijnt terwijl de app draait?** Nu wacht het startscript er bij
|
||||
de start op, maar tijdens bedrijf is er geen gedrag afgesproken. Doorgaan met het oude certificaat in
|
||||
het geheugen is waarschijnlijk het beste, maar het moet een besluit zijn en geen toeval. **Moment:**
|
||||
bij fase 3. **Eigenaar:** uitvoerder.
|
||||
|
||||
3. **Blijft `Docs/` in de publieke repo staan?** De repo staat sinds 18-08-2026 publiek, mét het domein
|
||||
en het certificaatpad in de historie. Daarmee is de geheimhoudingsvraag vervallen: `Docs/` alsnog uit
|
||||
de repo halen verbergt niets meer.
|
||||
|
||||
Wat overblijft is een presentatievraag, en die is kleiner: de naslag beschrijft de indeling van één
|
||||
specifieke server, wat voor een lezer van een publieke app store ruis is. Voorstel: laten staan, en de
|
||||
installatiespecifieke voorbeelden in `Referenties/Architectuur-huidig.md` vervangen door de
|
||||
configuratiesleutels zodra fase 2 klaar is. Dan documenteert de naslag het mechanisme in plaats van
|
||||
één installatie. **Moment:** bij fase 4, samen met de README. **Eigenaar:** gebruiker.
|
||||
|
||||
## 7. Verificatie
|
||||
|
||||
- Een verse installatie zonder configuratiebestand start en detecteert het certificaat zelf.
|
||||
- `TLS_DOMAIN` invullen overstemt de detectie.
|
||||
- Twee certificaatparen in de map leveren een foutmelding op die beide namen noemt, en geen willekeurige
|
||||
keuze.
|
||||
- Een app-update overschrijft een bestaand configuratiebestand niet. Dit is de belangrijkste controle,
|
||||
want dit is het soort fout dat pas bij de volgende update zichtbaar wordt.
|
||||
- `grep -ri kamenier` over de repo levert buiten `Docs/` niets op.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Publicatie-Gate - masterplan
|
||||
|
||||
> **App: Electrum Gate.** Er is ook een **Publicatie-Relay**; inleveren bij de officiële store is per app
|
||||
> en de twee plannen delen alleen de eisen, niet het werk. Hernoemd op 25-08-2026, toen Evolu Relay bij
|
||||
> deze store kwam; daarvoor heette dit plan **Publicatie**.
|
||||
|
||||
## 1. Waarom dit een eigen plan is
|
||||
|
||||
Op 20-08-2026 zei de gebruiker dat hij de app uiteindelijk als standaard-app voor Umbrel wil publiceren.
|
||||
Dat verandert wat "af" betekent. Tot nu toe was de maatstaf "hij werkt op deze Umbrel", en de app haalt die
|
||||
sinds diezelfde dag. De maatstaf van dit plan is een andere: **iemand anders keurt het pakket goed**, tegen
|
||||
eisen die niet van ons zijn.
|
||||
|
||||
Daarom staat dit los van het plan **Appstore**. Dat plan gaat over de app als umbrelOS-app en over de
|
||||
eigen community store, en die twee doelen zijn haalbaar zonder dat er ooit iemand meekijkt. Twee taken uit
|
||||
dat plan verhuizen hier inhoudelijk naartoe (de images pinnen, de repo hernoemen); ze blijven daar staan
|
||||
tot dit plan actief wordt, want een taak op twee plekken loopt uit elkaar.
|
||||
|
||||
## 2. Wat er af is
|
||||
|
||||
Niet alles hoeft nog te gebeuren. Bij het uitzoeken op 20-08-2026 bleek een deel al goed, en dat is geen
|
||||
toeval: de eisen zijn dezelfde als die van de spec die dit project vanaf 18-08 als naslag bijhoudt.
|
||||
|
||||
- `app_proxy` met alleen omgevingsvariabelen, geen eigen poorten;
|
||||
- de umbrelOS-inlog staat aan en er is geen `PROXY_AUTH_WHITELIST`, dus ook het API-pad zit erachter;
|
||||
- `gallery: []`, wat voor een nieuw pakket precies goed is;
|
||||
- alle gebruikersstaat onder `${APP_DATA_DIR}/data/...`, met een `.gitkeep` per map (sinds 0.0.9);
|
||||
- de manifestvelden in de voorgeschreven volgorde, met een toets erop, en `icon` als laatste regel zodat
|
||||
dat de enige is die bij inlevering weg hoeft (20-08-2026);
|
||||
- niets wordt buiten `${APP_DATA_DIR}` geschreven;
|
||||
- geen Docker-socket, geen privileged container, geen host-netwerk.
|
||||
|
||||
## 3. Wat er nog moet, in volgorde van moeilijkheid
|
||||
|
||||
1. **De images pinnen.** `python:3-alpine` en `nginx:alpine` staan kaal in de compose. Het moet
|
||||
`repo:versie@sha256:<digest>` worden, met `linux/amd64` én `linux/arm64` in de manifest-lijst. Dit kan
|
||||
alleen op de Umbrel en het is de grootste openstaande eis. Bijkomend voordeel dat losstaat van
|
||||
publicatie: een gepinde image maakt de app reproduceerbaar, en dat was al een doel van het plan
|
||||
**Appstore**, fase 4.
|
||||
|
||||
**Niet nu doen.** Besloten met de gebruiker op 20-08-2026: zolang er nog gedraaid, verbeterd en getest
|
||||
wordt, kost een pin alleen werk. Elke keer dat je een nieuwere basisimage wilt, moet je opnieuw pinnen,
|
||||
en tot de inlevering levert het niets op wat er nu ontbreekt.
|
||||
|
||||
De commando's staan in [Images-pinnen.md](../../Referenties/Images-pinnen.md), met de drie dingen die
|
||||
erbij stil mis kunnen gaan: een tag die meebeweegt, de digest van één architectuur in plaats van die van
|
||||
de index, en de aanname dat pinnen een eenmalige handeling is. Werkwijze: de gebruiker draait het
|
||||
commando op de Umbrel en plakt de uitvoer, waarna de pin in `docker-compose.yml` gaat. Dat is een commit
|
||||
met een versieverhoging, want de compose wordt daadwerkelijk uitgerold.
|
||||
2. **Het app-id kaal maken.** `whatsnext-electrum-gate` wordt `electrum-gate`, en de mapnaam mee. Het
|
||||
voorvoegsel is een eis van een community store, niet van de officiële. Let op: een id-wijziging is voor
|
||||
umbrelOS een andere app, dus dat is opnieuw installeren. De gebruiker heeft daar op 20-08-2026 geen
|
||||
bezwaar tegen.
|
||||
3. **`icon` weghalen**, en dat kan pas op het moment van inleveren: zolang dit een eigen store is moet het
|
||||
icoon er juist in. De volgorde zelf is op 20-08-2026 al goed gezet, en het icoon staat als laatste regel
|
||||
zodat dit één verwijdering is. Wat er dan ook nog moet: `submission` naar de PR-URL laten wijzen.
|
||||
4. **Drie tot vijf schermafbeeldingen aanleveren.** Niet zelf opmaken: het store-team maakt de
|
||||
promo-afbeeldingen (achtergrond met het plaatje erop) en daarom zien ze er allemaal hetzelfde uit. Wie
|
||||
het wél zelf wil, levert 1440 bij 900 in PNG. Details en de vorm voor een eigen store staan in
|
||||
[Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md).
|
||||
|
||||
Wat er op moet: het dashboard met echte gegevens, de certificaatkeuze open met meerdere kandidaten, en
|
||||
de verbindingsregels per wallet. Dat laatste is waar iemand voor komt en het eerste is het bewijs dat het
|
||||
werkt. **Met plaatsvervangende hostnamen**, want een echte schermafbeelding van deze machine zet veertien
|
||||
subdomeinen van de gebruiker op een publieke winkelpagina.
|
||||
|
||||
5. **De repo verhuizen naar een publieke plek** met de URL-velden mee. Staat nu als taak in **Appstore**
|
||||
fase 7, en is hier een voorwaarde: `repo`, `website`, `support` en `submission` moeten naar iets wijzen
|
||||
dat een reviewer kan openen.
|
||||
6. **De herstart-controle afronden.** Geen formele eis, maar wel de enige controle die dit project nooit
|
||||
heeft kunnen doen, en het is een slecht idee om iets in te leveren waarvan je dat niet weet. Zie
|
||||
**Appstore**, open punt 7.
|
||||
|
||||
## 3b. Bestaat dit al, en wat zeggen we in de PR
|
||||
|
||||
Uitgezocht op 20-08-2026, want dat is de eerste vraag die een reviewer stelt. Antwoord: **nee**, en de
|
||||
onderbouwing is scherper dan "ik heb niets gevonden". De reverse proxies die al in de store staan, Nginx
|
||||
Proxy Manager op kop, kunnen een certificaat wel op HTTP zetten maar niet op een gewone TCP-poort; daar
|
||||
staat bij NPM een openstaand verzoek voor. Een Electrum-wallet praat geen HTTP, dus die apps lossen dit
|
||||
niet op. Met bronnen in
|
||||
[Vergelijkbare-apps.md](../../Referenties/Vergelijkbare-apps.md).
|
||||
|
||||
Die ene zin is de kern van de PR-tekst. Reken er daarnaast op dat er gevraagd wordt waarom dit geen VPN of
|
||||
tunnel is. Het antwoord staat in datzelfde document en is **geen** afweging maar een verschil in soort: een
|
||||
tunneldienst zit in het pad en termineert het verkeer daar, een mesh-VPN vraagt een account, een
|
||||
coördinatieserver en een client op elk apparaat, en deze app vraagt geen van beide. De Engelse formulering
|
||||
staat er ook, en sinds 0.0.11 in de `description` van het manifest.
|
||||
|
||||
## 4. Het risico dat niet in een checklist staat
|
||||
|
||||
De app leest de certificaatmap van een ándere app: `${UMBREL_ROOT}/app-data/zoraxy/...` staat alleen-lezen
|
||||
gemount. Dat is toegestaan (de regel gaat over schrijven), maar het is het meest ongebruikelijke aan dit
|
||||
pakket en het is precies het soort ding waar een review over valt.
|
||||
|
||||
Wat het antwoord daarop wordt, is nog niet beslist. Denkrichtingen, niet in volgorde:
|
||||
|
||||
- laten staan en uitleggen. Het is alleen-lezen, het is de kern van wat de app doet, en het alternatief is
|
||||
dat iedere gebruiker zijn certificaat met de hand kopieert;
|
||||
- het uploadpad uit 0.0.7 is er al en werkt zonder die mount. De app is dus bruikbaar zonder Zoraxy, en dat
|
||||
maakt de mount een gemak in plaats van een voorwaarde;
|
||||
- vragen vóór het inleveren in plaats van erna. Een issue in `umbrel-apps` kost minder dan een afgewezen PR.
|
||||
|
||||
## 5. Onderbouwing
|
||||
|
||||
De eisen staan niet in de README van `umbrel-apps` maar in de skill-documentatie waar die naar verwijst.
|
||||
Met bron per regel, plus wat deze app er nu van doet, in
|
||||
[Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md).
|
||||
@@ -0,0 +1,100 @@
|
||||
# Publicatie-Relay - masterplan
|
||||
|
||||
> **App: Evolu Relay.** Er is ook een **Publicatie-Gate**; inleveren bij de officiële store is per app en
|
||||
> de twee plannen delen alleen de eisen, niet het werk.
|
||||
>
|
||||
> Status: nog niet actief, en dit is het plan dat het langst mag wachten. Bij promotie naar
|
||||
> `Plannen/Actief/NNN-Publicatie-Relay/` worden de paragrafen hieronder over de vier bestanden verdeeld;
|
||||
> zie `HomeGit/Docs/Werkproces.md` §1b.
|
||||
>
|
||||
> Afhankelijk van: **Umbrelapp**, en van een image die te pinnen valt. Zie §4.
|
||||
|
||||
## 1. Doel
|
||||
|
||||
De app inleveren in de officiële Umbrel-appstore, zodat er niet eerst een store-URL geplakt hoeft te
|
||||
worden. Dat verandert de maatstaf: niet "hij werkt hier" maar "iemand anders keurt het pakket goed",
|
||||
tegen eisen die niet van ons zijn.
|
||||
|
||||
Het vooronderzoek noemde dit als keuze tussen een eigen store en de officiële. Die keuze is er niet echt:
|
||||
je begint sowieso met een eigen store, want anders valt er niets te testen. De vraag is alleen of je
|
||||
daarna inlevert.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
Alles wat er tussen een werkend pakket in een eigen store en een aanvaarde bijdrage aan
|
||||
`getumbrel/umbrel-apps` zit. Het pakket zelf is het masterplan **Umbrelapp**.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Geen functionaliteit erbij om de app aantrekkelijker te maken.** Wat er niet in zit omdat we het niet
|
||||
nodig hebben, hoeft er niet in omdat een winkelpagina er beter van wordt.
|
||||
- **Geen afstemming met Trezor**, tenzij een reviewer erom vraagt. Zie open punt 3.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
De eisen staan niet in de README van `umbrel-apps` maar in de skill-documentatie waar die naar verwijst.
|
||||
Ze zijn uitgeschreven, met bron per regel, in
|
||||
[Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md). Wat daarvan hier het zwaarst weegt:
|
||||
|
||||
1. **Elke image gepind als `repo:versie@sha256:<digest>`, met `linux/amd64` én `linux/arm64`.** Dit is de
|
||||
eis waar dit plan op kan stranden, en het is geen kwestie van uitvoeren: hij vraagt dat de image
|
||||
überhaupt in een registry bestaat, voor beide architecturen. Publiceert Trezor er geen, dan bouw en
|
||||
publiceer je zelf, en dan lever je een pakket in dat naar je eigen image wijst. Dat is een doorlopende
|
||||
verplichting en een reviewer zal ernaar vragen.
|
||||
2. **Een kaal app-id, dus zonder store-voorvoegsel.** Dat betekent voor de gebruiker één keer opnieuw
|
||||
installeren, want voor umbrelOS is een ander id een andere app.
|
||||
3. **`icon` weglaten en `gallery` leeg**, en dat kan pas op het moment van inleveren: zolang het een eigen
|
||||
store is, moet het icoon er juist in. Zet `icon` daarom als laatste regel van het manifest, dan is dat
|
||||
één verwijdering.
|
||||
4. **Drie tot vijf schermafbeeldingen**, 1440 bij 900 in PNG, of gewone schermafbeeldingen waarna het
|
||||
store-team de opmaak doet.
|
||||
5. **De inlog van umbrelOS aan laten staan.** Dit is voor deze app geen formaliteit maar de kern van het
|
||||
risico; zie §5.
|
||||
|
||||
## 5. Het risico dat niet in een checklist staat
|
||||
|
||||
Electrum Gate heeft er één, een leesmount in de map van een andere app. Dit pakket heeft er ook één, en
|
||||
een andere: **de relay moet bereikbaar zijn voor een cliënt die geen umbrelOS-sessie heeft.** De
|
||||
inlevereisen zeggen dat de inlog aan moet blijven en dat een `PROXY_AUTH_WHITELIST` alleen smal mag zijn,
|
||||
en alleen voor paden die geen cookie kunnen sturen.
|
||||
|
||||
Of dat hier lukt, hangt volledig af van hoe de relay zelf authenticeert. Doet hij dat niet, dan lever je
|
||||
een pakket in met een open eindpunt, en dan is dit geen presentatiekwestie meer maar een ontwerpkwestie.
|
||||
Dat is de reden dat dit plan achter **Umbrelapp** staat en niet ernaast: het antwoord komt daarvandaan, en
|
||||
zonder dat antwoord is inleveren zinloos.
|
||||
|
||||
Wat wél in ons voordeel werkt: deze app leest niets buiten zijn eigen map, heeft geen Docker-socket, geen
|
||||
privileged container en geen afhankelijkheid van een andere app. Dat is een schoner pakket dan Electrum
|
||||
Gate.
|
||||
|
||||
## 6. Het werk in grote lijnen
|
||||
|
||||
**Fase 1 - de harde eisen halen.** Images pinnen, app-id kaal maken, manifestvelden in de voorgeschreven
|
||||
volgorde. Uitkomst: een pakket dat op de checklist niets meer rood heeft.
|
||||
|
||||
**Fase 2 - de presentatie.** Schermafbeeldingen met plaatsvervangende gegevens, en een `description` die
|
||||
zegt waarom dit bestaat en niet wat het technisch is.
|
||||
|
||||
**Fase 3 - de vraag vóór de PR.** Het punt uit §5 voorleggen als issue in `umbrel-apps`. Een issue kost
|
||||
minder dan een afgewezen PR.
|
||||
|
||||
**Fase 4 - inleveren en de review doorlopen.**
|
||||
|
||||
## 7. Open punten
|
||||
|
||||
1. **Willen we dit eigenlijk?** Een eigen store werkt en kost niets. Inleveren betekent een pakket
|
||||
onderhouden voor onbekende gebruikers, en als de image van onszelf is, betekent het ook die
|
||||
onderhouden. Dit is een echte keuze en geen vanzelfsprekend eindpunt.
|
||||
**Moment:** als **Umbrelapp** af is en de app een tijd gedraaid heeft · **Eigenaar:** gebruiker
|
||||
2. **Als de image zelf gebouwd moet worden, waar komt hij te staan?** En wie verhoogt hem als Trezor een
|
||||
nieuwe versie uitbrengt?
|
||||
**Moment:** valt samen met punt 1 · **Eigenaar:** gebruiker
|
||||
3. **Afstemmen met Trezor?** Een reviewer kan vragen of Trezor hierachter staat, omdat het hun software is
|
||||
en hun naam op de tegel.
|
||||
**Moment:** fase 3 · **Eigenaar:** gebruiker
|
||||
|
||||
## 8. Verificatie
|
||||
|
||||
Deze is anders dan bij de andere plannen: het bewijs is de aanvaarde PR. Wat er vóór die tijd te
|
||||
controleren valt, is dat elke regel van de checklist in §4 met een commando of een bestand te staven is,
|
||||
en dat de app na het kaal maken van het app-id nog steeds vanaf nul installeert.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Umbrelapp - masterplan
|
||||
|
||||
> **App: Evolu Relay.** Status: nog niet actief. Dit is één bestand en dat is bewust: er is nog geen
|
||||
> `TAKEN.md`, `PROGRESS.md` of `OPEN.md`, want er wordt nog niet aan gewerkt. Bij promotie naar
|
||||
> `Plannen/Actief/NNN-Umbrelapp/` worden de paragrafen hieronder over die vier bestanden verdeeld; zie
|
||||
> `HomeGit/Docs/Werkproces.md` §1b.
|
||||
>
|
||||
> Afhankelijk van: **Proefopstelling**. Het aantal containers, de variabelen en de authenticatievraag
|
||||
> komen daar vandaan, en zonder die antwoorden is elk manifest een gok.
|
||||
|
||||
## 1. Doel
|
||||
|
||||
Van een stack die lokaal draait naar een tweede app in deze store, die je in umbrelOS installeert en die
|
||||
daarna vanzelf terugkomt. Concreet: een map `whatsnext-evolu-relay/` met een `umbrel-app.yml` en een
|
||||
`docker-compose.yml`, geïnstalleerd op de Umbrel van de gebruiker, met Trezor Suite die erop
|
||||
synchroniseert.
|
||||
|
||||
## 2. Afbakening
|
||||
|
||||
Het pakket en de installatie: manifest, compose, en de controles op het apparaat zelf. Ook het opruimen
|
||||
van wat de eigen opzet van Trezor meebrengt en Umbrel niet wil, zoals de `.k8s/`-map en een eigen
|
||||
netwerkblok.
|
||||
|
||||
De store zelf valt hier **buiten**: die bestaat al en serveert Electrum Gate. Wat er nog aan moet gebeuren
|
||||
is één map erbij.
|
||||
|
||||
## 3. Niet-doelen
|
||||
|
||||
- **Geen bereikbaarheid van buiten het thuisnetwerk.** Masterplan **Bereikbaarheid**. Dit plan is af als
|
||||
het op het eigen netwerk werkt.
|
||||
- **Geen inlevering bij de officiële store.** Masterplan **Publicatie-Relay**. Dat is een andere maatstaf:
|
||||
niet "hij werkt hier" maar "iemand anders keurt het goed".
|
||||
- **Geen wijzigingen aan de relay zelf.** Wat Trezor levert, draaien we; wat het niet kan, kan het niet.
|
||||
|
||||
## 4. Ontwerp
|
||||
|
||||
### 4a. De vorm is bekend, de inhoud niet
|
||||
|
||||
De repo-vorm van een community store, de app-proxy, de whitelist bij updates en de regel dat een wijziging
|
||||
zonder verhoging van `version` niet wordt uitgerold: dat staat allemaal al opgeschreven, met bron, in
|
||||
[Umbrel-appstore-spec.md](../../Referenties/Umbrel-appstore-spec.md), en het hoeft niet opnieuw uitgezocht
|
||||
te worden. Electrum Gate in dezelfde repo is bovendien een werkend voorbeeld van elke regel daaruit.
|
||||
|
||||
Wat hieronder staat is wat daar **niet** uit volgt, omdat die spec is opgeschreven vanuit een app die op
|
||||
vier punten een ander geval is.
|
||||
|
||||
### 4b. De cliënt is geen browser, en dat raakt de app-proxy
|
||||
|
||||
**Dit is de ontwerpvraag die alles bepaalt**, en de reden dat dit plan wacht in plaats van alvast te
|
||||
beginnen aan een manifest.
|
||||
|
||||
Electrum Gate heeft een dashboard dat een mens in een browser opent, en dat mag dus gewoon achter de inlog
|
||||
van umbrelOS staan. Trezor Suite is geen browser met een sessiecookie. Komt de relay achter `app_proxy` met
|
||||
de standaardinstelling te staan, dan krijgt Suite een inlogpagina in plaats van de relay, en dat is geen
|
||||
configuratiefoutje maar het einde van het pad.
|
||||
|
||||
Twee bekende uitwegen, allebei met een prijs:
|
||||
|
||||
1. **`PROXY_AUTH_ADD: "false"`** op de proxy-service, zoals Gitea en Budibase in de officiële store doen.
|
||||
Dan doet de app zijn eigen authenticatie, en de vraag wordt meteen: **dóét deze relay dat?** Zo niet,
|
||||
dan zet je een open eindpunt op je Umbrel;
|
||||
2. **een eigen `ports:` op de service**, zoals Electrum Gate met 50022 doet. Voor een niet-web-poort is dat
|
||||
normaal en de inlevereisen noemen het ook zo. De poort moet dan wel uniek zijn op de host, en dat is
|
||||
hier een echte controle: op deze machine draaien al Electrum Gate, een Electrum-server en een reverse
|
||||
proxy.
|
||||
|
||||
Welke van de twee het wordt, hangt af van hoe Suite zich tegen de relay authenticeert. Dat antwoord komt
|
||||
uit **Proefopstelling**, fase 4.
|
||||
|
||||
### 4c. Er is geen web-UI, en `port` is een verplicht veld
|
||||
|
||||
`port` in het manifest is de poort die umbrelOS voor de tegel gebruikt, dus een web-UI-poort. Deze app
|
||||
heeft er geen. Wat andere apps zonder interface daarmee doen is niet uitgezocht; zie open punt 1.
|
||||
|
||||
### 4d. Er zit een database in de stack
|
||||
|
||||
Nieuw ten opzichte van Electrum Gate, dat niets bewaart.
|
||||
|
||||
- **De Postgres-datamap hoort onder `${APP_DATA_DIR}/data/postgres`**, niet in een naamloos Docker-volume.
|
||||
Anders overleeft de data een herinstallatie niet en zit hij niet in de back-up van umbrelOS.
|
||||
- **Het wachtwoord hoort niet in de repo.** umbrelOS levert `${APP_PASSWORD}` en `${APP_SEED}` aan, per
|
||||
installatie afgeleid. Een literal in de compose is in een publieke repo een gepubliceerd wachtwoord.
|
||||
- **`backupIgnore` verdient een overweging, en het antwoord is hier waarschijnlijk "niets".** De database
|
||||
ís de waarde van deze app. Dat staat er expliciet omdat Electrum Gate het veld wél gebruikt, en
|
||||
overnemen uit gewoonte zou hier precies het verkeerde weglaten.
|
||||
|
||||
### 4e. Geen afhankelijkheden, en niets om te vervangen
|
||||
|
||||
Electrum Gate declareert `dependencies: [electrs]` en leunt op het wisselmechanisme met Fulcrum en
|
||||
ElectrumX. Deze app staat op zichzelf: geen `dependencies`, geen `implements`, en geen leesmount in de map
|
||||
van een andere app. Dat maakt het pakket eenvoudiger en het haalt meteen het grootste review-risico weg dat
|
||||
Electrum Gate wél heeft.
|
||||
|
||||
### 4f. De image is niet van ons, en misschien bestaat hij niet
|
||||
|
||||
Electrum Gate draait op `nginx:alpine` en `python:3-alpine`, twee images die gewoon in een registry staan
|
||||
en die je alleen nog hoeft te pinnen. Hier is niet bekend of Trezor een image publiceert of alleen een
|
||||
Dockerfile levert. Is het alleen een Dockerfile, dan bouw en publiceer je zelf, en dan ben je onderhouder
|
||||
van een image geworden. **Proefopstelling** fase 1 levert het antwoord; zie
|
||||
[Upstream-evolu-relay.md](../../Referenties/Upstream-evolu-relay.md) §4 punt 1.
|
||||
|
||||
## 5. Het werk in grote lijnen
|
||||
|
||||
**Fase 1 - de app-map.** `whatsnext-evolu-relay/` naast `whatsnext-electrum-gate/`, met een
|
||||
`umbrel-app.yml` waarvan het `id` gelijk is aan de mapnaam. Uitkomst: umbrelOS toont een tweede tegel in
|
||||
de store, ook al doet de app nog niets.
|
||||
|
||||
**Fase 2 - de compose.** De `docker-compose.yaml` van Trezor omzetten: `app_proxy`, geen eigen netwerkblok,
|
||||
data onder `${APP_DATA_DIR}/data/`, geheimen uit umbrelOS-variabelen, en `.k8s/` blijft waar het is.
|
||||
Uitkomst: een compose die op de Umbrel start.
|
||||
|
||||
**Fase 3 - het manifest afmaken.** Velden in de voorgeschreven volgorde, een eigen icoon, en een antwoord
|
||||
op de `port`-vraag uit §4c.
|
||||
|
||||
**Fase 4 - installeren en verifiëren op de Umbrel.** Installeren, Suite laten synchroniseren, en daarna de
|
||||
controles die je alleen op het apparaat kunt doen: overleeft de data een update, komt de app terug na een
|
||||
herstart, en botst er geen poort met wat er al draait.
|
||||
|
||||
## 6. Open punten
|
||||
|
||||
1. **Wat wordt `port` in het manifest?** Dat veld is de web-UI-poort voor de tegel en deze app heeft geen
|
||||
web-UI. Wat andere apps zonder interface daarmee doen is niet uitgezocht.
|
||||
**Moment:** fase 3 · **Eigenaar:** uit te zoeken bij het schrijven van het manifest
|
||||
2. **Komt er een statuspagina?** Electrum Gate heeft er een en die bleek in de praktijk het nuttigste deel
|
||||
van de app. Hier zou dat kunnen: draait de relay, hoe groot is de database, wanneer was de laatste
|
||||
synchronisatie. Het is echter een eigen container en een eigen onderhoudslast, en het is geen voorwaarde
|
||||
om te kunnen synchroniseren. Voordeel dat pas sinds de samenvoeging bestaat: de pagina van Electrum Gate
|
||||
staat in dezelfde repo en is grotendeels over te nemen.
|
||||
**Moment:** pas overwegen als fase 4 geslaagd is · **Eigenaar:** gebruiker
|
||||
3. **Blijft de quota-manager buiten het pakket?** Als **Proefopstelling** uitwijst dat het kan, dan ja.
|
||||
Blijkt hij verplicht, dan verandert dit plan van vorm en hoort dit punt opnieuw beslist te worden.
|
||||
**Moment:** bij promotie van dit plan · **Eigenaar:** volgt uit Proefopstelling
|
||||
|
||||
## 7. Verificatie
|
||||
|
||||
Alles behalve het laatste punt is op een laptop te controleren; het laatste punt is precies waarom dit plan
|
||||
bestaat.
|
||||
|
||||
- YAML geldig, `id` in het manifest gelijk aan de mapnaam, elke `image:` met een `@sha256:`-digest. Dat
|
||||
zijn de fouten die je anders pas op het apparaat merkt, en ze zijn automatisch te controleren;
|
||||
- **op de Umbrel:** installeren, synchroniseren met Suite, en daarna een herstart van de app en van het
|
||||
apparaat. Meld per controle of hij gedaan is, en meld ook expliciet welke niet;
|
||||
- **en één die alleen bij een tweede app in een bestaande store bestaat:** dat het toevoegen van deze app
|
||||
Electrum Gate niet raakt. Een kapot manifest in de ene map mag de andere niet meeslepen; of umbrelOS dat
|
||||
netjes doet is niet uitgezocht.
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Docs - documentatiewortel van UmbrelApps
|
||||
|
||||
Dit is de **enige** documentatieboom in de repo, en dit is de **enige** README erin: alle regels over
|
||||
waar iets hoort staan hieronder. De onderbouwing van de methode staat in `HomeGit/Docs/`.
|
||||
|
||||
```
|
||||
Docs/
|
||||
├── CONTINUE_HERE.md - dunne index: per prioriteitstier naar de "volgende stap" van elk actief plan
|
||||
├── KNOWLEDGE.md - duurzame cross-plan kennis
|
||||
├── CHANGELOG-<app>.md - versiegeschiedenis, per app
|
||||
├── Plannen/
|
||||
│ ├── Actief/NNN-<Plan>/ - PLAN.md (ontwerp) · TAKEN.md (checklist) · PROGRESS.md (log) · OPEN.md
|
||||
│ ├── Archief/<Plan>/ - afgerond; hele map verhuist hierheen, nummer vervalt
|
||||
│ └── Masterplannen/ - nog niet actief; elk plan als één <Naam>.PLAN.md
|
||||
│ └── Archief/ - opgevolgd; hier wordt nooit in bewerkt
|
||||
└── Referenties/ - uitsluitend naslag: dingen waar je rekening mee moet houden
|
||||
```
|
||||
|
||||
De map `Plannen/Archief/` bestaat nog niet; lege mappen overleven git toch niet. Hij komt er zodra er
|
||||
iets in gaat.
|
||||
|
||||
## Eén boom, twee apps
|
||||
|
||||
Deze repo is één community app store met twee apps erin, en er is **één** documentatiewortel voor allebei.
|
||||
Dat is geen concessie maar de reden dat het samengevoegd is: de spec van de appstore, de manier om images
|
||||
te pinnen en de manier van werken zijn voor beide apps dezelfde, en die stonden eerst in twee repo's naast
|
||||
elkaar.
|
||||
|
||||
Wat dat vraagt:
|
||||
|
||||
- **elk plan hoort bij één app**, en dat staat in de kolom App van [CONTINUE_HERE.md](CONTINUE_HERE.md) en
|
||||
in de kop van het plan zelf. De prioriteit loopt wél over beide heen: tier A is wat er nu moet gebeuren,
|
||||
ongeacht welke app;
|
||||
- **botst een plannaam, dan krijgt hij een achtervoegsel met de app**, en anders niet. Inleveren bij de
|
||||
officiële store is per app, dus daar staan **Publicatie-Gate** en **Publicatie-Relay**. De rest houdt
|
||||
gewoon zijn naam;
|
||||
- **hetzelfde geldt voor `CHANGELOG`**, want `version` staat per manifest;
|
||||
- **een referentie zegt voor wie hij is.** Sommige gelden voor beide apps, sommige voor één.
|
||||
|
||||
## Waar hoort iets
|
||||
|
||||
Twee vragen, in deze volgorde:
|
||||
|
||||
1. **Is het naslag** (geen status, hoort niet bij één plan, wordt niet per sessie bijgewerkt)? Dan
|
||||
`Referenties/`.
|
||||
2. **Anders is het een plan, en de status bepaalt de plek:** nog niet actief →
|
||||
`Plannen/Masterplannen/<Naam>.PLAN.md`; er wordt aan gewerkt → `Plannen/Actief/NNN-<Naam>/`;
|
||||
afgerond → `Plannen/Archief/<Naam>/`.
|
||||
|
||||
Twijfel? De test is: zit er werk aan vast dat ooit af moet zijn? Zo ja, dan is het een plan, ook als er
|
||||
voorlopig niemand aan begint.
|
||||
|
||||
## Een plan begint als één bestand
|
||||
|
||||
Schrijf een nieuw plan als `Plannen/Masterplannen/<Naam>.PLAN.md`: doel, afbakening, niet-doelen, het
|
||||
werk in grote lijnen, open punten. Pas als het werk écht begint, promoveer je het naar
|
||||
`Plannen/Actief/NNN-<Naam>/` met de vier bestanden. De promotie-stappen staan in
|
||||
`HomeGit/Docs/Werkproces.md` §1b.
|
||||
|
||||
Een masterplan heeft geen tier en geen nummer. Wat het wél kan hebben is een harde afhankelijkheid, en
|
||||
die staat in de masterplannen-tabel van [CONTINUE_HERE.md](CONTINUE_HERE.md).
|
||||
|
||||
## Het nummer
|
||||
|
||||
`010-`, `020-`, in stappen van tien zodat er altijd een `015-` tussen kan. Drie cijfers. Het nummer is de
|
||||
aanbevolen volgorde, **niet** de tier: die staat in de prioriteits-header van `TAKEN.md`.
|
||||
|
||||
**Het nummer bestaat op twee plekken: de mapnaam en `CONTINUE_HERE.md`.** Verwijs overal elders met de
|
||||
naam van het plan: "zie het plan **Appstore**, `TAKEN.md` Fase 2". Binnen een plan link je relatief.
|
||||
|
||||
De reeks loopt over beide apps heen en is dus niet per app aaneengesloten. Dat is de bedoeling: er is één
|
||||
volgorde van werken, niet twee.
|
||||
|
||||
## Sessie-protocol
|
||||
|
||||
**Starten:** `CONTINUE_HERE.md` → tier A → `Plannen/Actief/NNN-<Plan>/TAKEN.md` ("Volgende stap").
|
||||
`PLAN.md` alleen bij twijfel over scope of ontwerp.
|
||||
|
||||
**Afsluiten:** `TAKEN.md` bijwerken, korte entry in `PROGRESS.md`, tier en de indexregel controleren,
|
||||
gewijzigde bestanden opsommen.
|
||||
|
||||
## Projectspecifiek
|
||||
|
||||
**Alle documentatie staat hier, ook wat elders vaak in de root ligt.** In de repo-root blijft alleen
|
||||
`README.md`, en die is geen documentatie maar de **voordeur**: Gitea toont hem op de repo-pagina, en dat
|
||||
is voor een publieke app store het eerste wat iemand ziet. Hij hoort kort te blijven en door te verwijzen
|
||||
naar deze map. De oude `ARCHITECTURE.md`, `STRUCTURE.md` en `QUICKSTART.md` uit de root zijn opgegaan in
|
||||
[Referenties/Architectuur-huidig.md](Referenties/Architectuur-huidig.md); ze beschreven grotendeels
|
||||
hetzelfde in drie versies.
|
||||
|
||||
**Er zijn tests, maar alleen voor Electrum Gate**, en ze dekken de agent en het manifest. Hoe ze draaien
|
||||
staat in [../CLAUDE.md](../CLAUDE.md). Voor Evolu Relay is er nog geen bron en dus ook niets te testen;
|
||||
stap 4 van het sessie-protocol vervalt zolang een sessie alleen aan die app werkt.
|
||||
|
||||
**Wat sowieso automatisch te controleren is** en de moeite waard blijft, en dat geldt straks voor beide
|
||||
app-mappen: dat de YAML geldig is, dat het `id` in `umbrel-app.yml` gelijk is aan de mapnaam, en dat elke
|
||||
`image:` een `@sha256:`-digest heeft. Dat zijn precies de fouten die je op het apparaat pas merkt als de
|
||||
app niet start.
|
||||
|
||||
**"Gebouwd" en "werkend" liggen hier verder uit elkaar dan gebruikelijk.** Elk plan heeft daarom een
|
||||
paragraaf Verificatie met de controles die **op de Umbrel zelf** gedaan moeten worden. Meld die uitslag,
|
||||
inclusief wat er níet gecontroleerd is.
|
||||
|
||||
**De repo wordt publiek.** Dat is geen keuze maar een eis: umbreld kloont een community app store anoniem
|
||||
en kan geen inloggegevens aanbieden. Houd daar rekening mee bij wat je opschrijft. Evolu Relay krijgt een
|
||||
Postgres, en dan is er meer weg te houden dan bij Electrum Gate: geen wachtwoord in de compose, geen
|
||||
`.env`, geen dump.
|
||||
|
||||
**Regeleindes zijn LF, afgedwongen door `.gitattributes`.** Dat is hier geen netheid: de configuratie
|
||||
draait in een Linux-container, en een CRLF achter een `#!/bin/sh` laat een script stuklopen op een
|
||||
foutmelding die nergens naar het echte probleem wijst.
|
||||
@@ -0,0 +1,100 @@
|
||||
# De huidige opzet: hoe het vandaag in elkaar zit
|
||||
|
||||
Naslag. Wat er ís, zodat een latere sessie het niet hoeft te reconstrueren. Wat er mis mee is en wat eraan
|
||||
gebeurt staat in de plannen.
|
||||
|
||||
**Let op het onderscheid dat dit document maakt**, want het is op 19-08-2026 het belangrijkste feit over
|
||||
deze app: er is verschil tussen wat er in de repo staat en wat er op de Umbrel draait. Dat verschil is
|
||||
groot, en het per ongeluk gelijkstellen leidt tot conclusies die niet kloppen.
|
||||
|
||||
| | In de repo | Draait op de Umbrel |
|
||||
|-|-|-|
|
||||
| Versie | 0.0.3 | 2.0.1, en die app wordt gedeïnstalleerd |
|
||||
| App-id | `whatsnext-electrum-gate` | `electrumtls-electrum-tls` |
|
||||
| TLS-poort | 50022 | 50002 |
|
||||
| Containers | `server` plus `agent` | alleen `server` |
|
||||
| Certificaat | door de agent gekozen uit de gevonden mappen | vast pad naar `sync.kamenier-hamer.nl` |
|
||||
| Dashboard | leest `status.json` | verzonnen waarden, hardgecodeerd domein |
|
||||
|
||||
Alles in de kolom "in de repo" is **ongetest buiten de unittests**. De vorige versie draait en een wallet
|
||||
verbindt er over TLS mee; dat is bewezen. De nieuwe niet.
|
||||
|
||||
## 1. Wat het doet
|
||||
|
||||
Een Electrum-server spreekt onversleuteld TCP. Een wallet buiten het netwerk wil TLS. Deze app zet daar
|
||||
nginx met de `stream`-module tussen: die accepteert TLS, termineert het, en praat plat door naar de
|
||||
Electrum-server die de gebruiker in umbrelOS gekozen heeft.
|
||||
|
||||
```
|
||||
Electrum-wallet ──TLS 50022──▶ nginx stream ──plat──▶ Electrs, Fulcrum of ElectrumX
|
||||
▲
|
||||
cert.conf, geschreven door de agent
|
||||
```
|
||||
|
||||
Het certificaat komt van een reverse proxy die op dezelfde Umbrel al Let's Encrypt-certificaten beheert.
|
||||
Deze app leest die mappen alleen; hij vraagt zelf niets aan en vernieuwt niets.
|
||||
|
||||
## 2. De twee containers
|
||||
|
||||
**`server`** (`nginx:alpine`) doet twee dingen. In het `http`-blok serveert hij het dashboard op poort 80,
|
||||
achter de `app_proxy` van umbrelOS, plus `status.json` en een doorstuur naar de API van de agent. In het
|
||||
`stream`-blok termineert hij TLS op 50022. Zijn `command` schrijft bij het starten het `log_format` voor de
|
||||
sessielog weg, wacht tot de agent een certificaat gekozen heeft, en draait daarna een lus die nginx
|
||||
herlaadt zodra de agent daarom vraagt.
|
||||
|
||||
**`agent`** (`python:3-alpine`) draait `agent.py` en doet elke minuut een ronde: de certificaatmappen
|
||||
scannen, de keuze toepassen, de Electrum-server bevragen voor blokhoogte en reactietijd, de sessielog van
|
||||
nginx uitlezen, en `status.json` schrijven. Daarnaast luistert hij op poort 8000 voor de certificaatkeuze
|
||||
van het dashboard.
|
||||
|
||||
**Waarom de agent nginx niet zelf herlaadt.** Dat zou de Docker-socket vragen, en die is er bewust uit
|
||||
gehaald: hij geeft root-toegang tot de host. In plaats daarvan schrijft de agent `cert.conf` plus een
|
||||
vlagbestand, en herlaadt de nginx-container zichzelf. Een reload verbreekt bestaande wallet-verbindingen
|
||||
niet; een containerherstart wel.
|
||||
|
||||
## 3. De gedeelde map
|
||||
|
||||
Beide containers hebben `${APP_DATA_DIR}/runtime` gemount op `/var/lib/gate`. Dat is het enige raakvlak
|
||||
tussen de twee, en het is met opzet een map met bestanden en geen protocol.
|
||||
|
||||
| Bestand | Wie schrijft | Waarvoor |
|
||||
|-|-|-|
|
||||
| `status.json` | agent | alles wat het dashboard toont |
|
||||
| `cert.conf` | agent | de `ssl_certificate`-regels die nginx includeert |
|
||||
| `reload` | agent | vraagt nginx om een herlading; wordt `reload.done` |
|
||||
| `stream-log.conf` | nginx | het `log_format`, hier omdat een dollarteken niet in een template kan |
|
||||
| `stream.log` | nginx | een regel per afgesloten sessie, zonder client-adres |
|
||||
| `config/selected-cert` | agent | de keuze van de gebruiker, overleeft een herstart |
|
||||
|
||||
## 4. Wat er nog hardgecodeerd is
|
||||
|
||||
Sinds 19-08-2026 bijna niets meer in de app zelf. De domeinnaam en het certificaatpad zijn uit
|
||||
`docker-compose.yml` en `nginx.conf.template` verdwenen: de agent bepaalt ze.
|
||||
|
||||
Wat er nog staat, en waar:
|
||||
|
||||
- **de mounts van de certificaatbronnen** in `docker-compose.yml`, want een pad instellen naar iets wat
|
||||
niet gemount is levert een map op die de container niet ziet. Nginx Proxy Manager staat er
|
||||
uitgecommentarieerd bij, omdat het pad niet geverifieerd is;
|
||||
- **de poortnummers** 50022 en 3850;
|
||||
- **de vertaaltabel van backend-IP naar naam** in de agent, met `unknown` als terugval;
|
||||
- **de repo-URL's** in `umbrel-app.yml`. Die horen daar; ze wijzen naar de repo.
|
||||
|
||||
Het plan **Configuratie** haalt de eerste twee naar een configuratiebestand.
|
||||
|
||||
## 5. Hoe het geïnstalleerd wordt
|
||||
|
||||
Als community app store: de gebruiker plakt de repo-URL in umbrelOS en installeert de app. umbreld kloont
|
||||
anoniem met isomorphic-git, dus de repo moet publiek en SHA-1 zijn. Bij installatie wordt de hele app-map
|
||||
gekopieerd; **bij een update alleen een whitelist**, en dat stuurt het hele ontwerp. Zie
|
||||
[Umbrel-appstore-spec.md](Umbrel-appstore-spec.md).
|
||||
|
||||
## 6. Wat er goed aan is, en dat is het onthouden waard
|
||||
|
||||
- **geen Docker-socket**, dus geen root-toegang tot de host;
|
||||
- **geen eigen image**, dus geen bouwstap, geen registry en geen multi-arch-gedoe. Beide containers
|
||||
draaien op een officiële image met configuratie uit een template;
|
||||
- **niets wordt bij het starten geïnstalleerd.** De vorige opzet deed `apk add stunnel` bij élke start;
|
||||
- **de app vraagt geen certificaten aan.** ACME blijft bij de reverse proxy, en dat is de reden dat deze
|
||||
app zo klein kan blijven;
|
||||
- **een certificaatvernieuwing is een reload**, dus wallet-verbindingen blijven staan.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Wie kan deze app gebruiken
|
||||
|
||||
Naslag. Welke wallets en programma's naar een eigen Electrum-server kunnen wijzen, in welke vorm ze het
|
||||
adres willen, en wanneer TLS daarbij werkelijk iets toevoegt. Uitgezocht op 18-08-2026.
|
||||
|
||||
Dit stuurt drie dingen: de beschrijving in de app store, de verbindingsregels op het dashboard (zie het
|
||||
plan **Webinterface**), en de vraag of TLS voor een bepaalde client eigenlijk wel het juiste antwoord is.
|
||||
|
||||
## 1. Waarom TLS, en niet gewoon Tor
|
||||
|
||||
**De privacywinst zit in de eigen server, niet in het transport.** Verbindt een wallet met een publieke
|
||||
Electrum-server, dan vraagt hij daar de geschiedenis van al zijn adressen op, en daarmee weet die server
|
||||
welke adressen en welk saldo bij één gebruiker horen. Dat is de grootste weggever in het dagelijks gebruik
|
||||
van een wallet, en die verdwijnt zodra je je eigen server draait. Dat geldt ongeacht of je over Tor of
|
||||
over TLS verbindt, en het is dus de eigenlijke reden dat deze opzet bestaat.
|
||||
|
||||
Wat daarná overblijft is een afweging over metadata, en die is kleiner. **Bijna elke client hieronder kan
|
||||
ook over Tor.** Een onion-adres is versleuteld, vraagt geen certificaat, geen open poort in de router en geen
|
||||
domeinnaam. Voor privacy is het zelfs beter.
|
||||
|
||||
TLS is dus geen vervanging van Tor maar een andere afweging, en hij wint op deze punten:
|
||||
|
||||
- **Snelheid.** Een Tor-verbinding voegt honderden milliseconden per verzoek toe, en dat merk je bij het
|
||||
synchroniseren van een wallet met veel adressen.
|
||||
- **Betrouwbaarheid op mobiel.** Tor op een telefoon is gevoelig voor slaapstanden en wisselende
|
||||
netwerken; een gewone TLS-verbinding niet.
|
||||
- **Netwerken die Tor blokkeren.** Bedrijfsnetwerken en sommige providers.
|
||||
- **Geen handmatige verificatie.** Bij een publiek vertrouwd certificaat controleert de client de naam
|
||||
zelf. Bij een zelfondertekend certificaat moet de gebruiker een vingerafdruk overtypen, en dat is
|
||||
precies de stap die mensen overslaan.
|
||||
|
||||
Waar TLS **niet** in voorziet: het verbergt niet dat er verkeer is, en je domeinnaam is publiek zichtbaar
|
||||
in het certificaattransparantielogboek. Wie dat wil vermijden, hoort Tor te gebruiken.
|
||||
|
||||
## 2. De clients
|
||||
|
||||
| Client | Kan naar eigen Electrum-server | Vorm van het adres | Opmerking |
|
||||
|-|-|-|-|
|
||||
| **Trezor Suite** (desktop en mobiel) | ja | `host:poort:protocol`, met `s` voor SSL | Instellingen → Netwerken → tandwiel bij Bitcoin. Zie §3 |
|
||||
| **Sparrow Wallet** | ja | host en poort apart, met een keuze voor SSL | De keuze voor gevorderden op de desktop; kan ook rechtstreeks naar Bitcoin Core |
|
||||
| **Electrum** (desktop en Android) | ja | `host:poort:s` | De referentie-implementatie van het protocol |
|
||||
| **BlueWallet** | ja | host en poort, in de instellingen | Mobiel; hier weegt het snelheidsargument tegenover Tor het zwaarst |
|
||||
| **Nunchuk** | ja | host en poort | Gericht op multisig, op alle platforms |
|
||||
| **BitBoxApp** | ja | via de geavanceerde instellingen | Begeleidende app bij de BitBox-hardwarewallet |
|
||||
| **Blockstream** | ja | host en poort, met een Tor-schakelaar | Heeft een expliciete optie voor een eigen Electrum-server. Heette Blockstream Green; de app is omgedoopt, gemeld door de gebruiker op 20-08-2026 |
|
||||
|
||||
**Niet van toepassing**, en het opschrijven waard zodat de vraag niet terugkomt: **Wasabi** en
|
||||
**Samourai** gebruiken hun eigen achterkant en spreken geen Electrum-protocol. **Specter Desktop** praat
|
||||
rechtstreeks met Bitcoin Core via RPC. Hardwarewallets als **Coldcard**, **Keystone** en **Passport**
|
||||
verbinden nooit zelf, maar via een van de programma's hierboven.
|
||||
|
||||
**Binnen je eigen Umbrel is deze app niet nodig.** Apps als mempool en btc-rpc-explorer praten
|
||||
rechtstreeks met Electrs over het interne netwerk. Deze app is uitsluitend voor clients **buiten** je
|
||||
netwerk.
|
||||
|
||||
## 3. Trezor Suite, want die is het meest specifiek
|
||||
|
||||
De vorm is `host:poort:protocol`, waarbij de laatste letter het protocol kiest:
|
||||
|
||||
```
|
||||
sync.kamenier-hamer.nl:50002:s TLS, dus via deze app
|
||||
192.168.1.100:50001:t plat TCP, alleen binnen het eigen netwerk
|
||||
```
|
||||
|
||||
`s` is SSL/TLS, `t` is onversleuteld TCP. Een `.onion`-adres kan ook, want Suite heeft Tor ingebouwd.
|
||||
|
||||
**De tegenspraak in Trezors documentatie is opgelost, en niet in het voordeel van de documentatie.** Er
|
||||
stond hier dat een eigen achterkant alleen in de desktopversie kan, met de aantekening dat elders in
|
||||
diezelfde documentatie een keuzelijst voor mobiel beschreven staat. **De gebruiker heeft het op 20-08-2026
|
||||
op iOS nagekeken: de instelling zit er, dus de mobiele app kan het wel.** Android is niet nagekeken en
|
||||
zal vermoedelijk hetzelfde doen; er staat daarom "also in the mobile app" op het dashboard en niet
|
||||
"iOS en Android".
|
||||
|
||||
Dit is het tweede geval in dit project waarin de documentatie van een leverancier het aflegt tegen één
|
||||
keer kijken. Waard om te onthouden bij de andere regels in de tabel hierboven: die komen uit
|
||||
documentatie, niet uit een test.
|
||||
|
||||
## 4. Wat dit betekent voor het dashboard
|
||||
|
||||
De verbindingsregel is per client net anders, en juist die kleine verschillen kosten mensen tijd. Het
|
||||
dashboard kan ze kant-en-klaar tonen met een kopieerknop, in plaats van alleen het domein en de poort:
|
||||
|
||||
- Trezor Suite: `sync.kamenier-hamer.nl:50002:s`
|
||||
- Electrum: `sync.kamenier-hamer.nl:50002:s`
|
||||
- Sparrow, BlueWallet, Nunchuk, Green: host en poort apart, met SSL aangevinkt
|
||||
|
||||
Dat is goedkoop om te bouwen, want de waarden staan na het plan **Configuratie** toch al in de
|
||||
configuratie.
|
||||
|
||||
## 5. Bronnen
|
||||
|
||||
Geraadpleegd op 18-08-2026:
|
||||
|
||||
- Trezor Knowledge Base, "Connect Trezor Suite to your own node" en "Custom backend in Trezor Suite"
|
||||
- Trezor, "Self-hosted full node via Electrum server in Trezor Suite App"
|
||||
- Documentatie van Sparrow Wallet
|
||||
- Start9 en RaspiBolt, overzichten van wallets die naar een eigen node kunnen wijzen
|
||||
- Coldcard, "Compatible Wallets and Tools"
|
||||
@@ -0,0 +1,98 @@
|
||||
# Images pinnen: de commando's
|
||||
|
||||
Naslag, geen plan. Wanneer dit gedaan wordt en waarom staat in het masterplan **Publicatie**; hier staan
|
||||
alleen de commando's en wat je uit de uitvoer moet halen.
|
||||
|
||||
Alles hieronder draait **op de Umbrel**, want daar staat Docker. Lukt een commando niet, zet er dan `sudo`
|
||||
voor.
|
||||
|
||||
## Waarom het niet één commando is
|
||||
|
||||
De eis is `repo:versie@sha256:<digest>`, en daar zitten drie dingen in die elk apart mis kunnen gaan:
|
||||
|
||||
- **de tag moet specifiek zijn.** `python:3-alpine` beweegt mee met elke nieuwe 3.x en is ook mét digest
|
||||
niet toegestaan. Stap 1 zoekt uit welke versie er nu achter zit;
|
||||
- **de digest moet die van de index zijn**, niet die van één architectuur. Een platform-digest werkt op de
|
||||
machine waar je hem ophaalt en breekt op de andere; een Umbrel Home is arm64 en een zelfbouw meestal
|
||||
amd64;
|
||||
- **beide architecturen moeten erin zitten.** Dat lees je in dezelfde uitvoer af.
|
||||
|
||||
## Stap 1: welke versie zit er achter de bewegende tag
|
||||
|
||||
```bash
|
||||
docker run --rm python:3-alpine python -V
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run --rm nginx:alpine nginx -v
|
||||
```
|
||||
|
||||
De uitvoer noemt bijvoorbeeld `Python 3.13.7` en `nginx version: nginx/1.27.4`. Gebruik daarvan alleen het
|
||||
eerste deel in de tag, dus `python:3.13-alpine` en `nginx:1.27-alpine`, en niet de derde cijfergroep: een
|
||||
patchversie-tag bestaat niet altijd, en de tweede cijfergroep is specifiek genoeg om niet mee te bewegen
|
||||
met een nieuwe hoofdversie.
|
||||
|
||||
## Stap 2: de index-digest van die tag
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect python:3.13-alpine
|
||||
```
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect nginx:1.27-alpine
|
||||
```
|
||||
|
||||
Twee dingen uit die uitvoer, en let op welke digest je pakt:
|
||||
|
||||
```
|
||||
Name: docker.io/library/python:3.13-alpine
|
||||
MediaType: application/vnd.oci.image.index.v1+json
|
||||
Digest: sha256:AAAA... <- deze, de bovenste
|
||||
Manifests:
|
||||
Name: docker.io/library/python:3.13-alpine@sha256:BBBB...
|
||||
Platform: linux/amd64 <- deze moet erin staan
|
||||
Name: docker.io/library/python:3.13-alpine@sha256:CCCC...
|
||||
Platform: linux/arm64 <- en deze ook
|
||||
```
|
||||
|
||||
De bovenste `Digest:` is de manifest-lijst en die hoort in de compose. De digests bij `Manifests:` zijn per
|
||||
platform; dat is precies de verwisseling die pas op de andere architectuur opvalt.
|
||||
|
||||
## Stap 2b: als `buildx` er niet is
|
||||
|
||||
```bash
|
||||
docker pull python:3.13-alpine
|
||||
```
|
||||
|
||||
```bash
|
||||
docker image inspect --format '{{index .RepoDigests 0}}' python:3.13-alpine
|
||||
```
|
||||
|
||||
Een `docker pull` van een tag met meerdere architecturen zet de index-digest in `RepoDigests`, dus dit
|
||||
levert dezelfde waarde op als stap 2.
|
||||
|
||||
**Maar het bewijst niets over de architecturen, en dat is hier geen theoretisch punt.** De Umbrel van de
|
||||
gebruiker is amd64 (`OS: Linux 6.12.85+deb13-amd64`, uit de nginx-startlog van 20-08-2026). Een `docker pull`
|
||||
daar haalt de amd64-variant en zegt niets over arm64, terwijl arm64 wel een eis is en de helft van de
|
||||
Umbrels erop draait. Gebruik dus stap 2 als het kan, en zie deze terugval als "de digest opzoeken", niet als
|
||||
"de eis controleren".
|
||||
|
||||
Terzijde, uit diezelfde log: achter `nginx:alpine` zat op 20-08-2026 **nginx 1.31.4**. Stap 1 hoeft daarvoor
|
||||
dus niet eens gedraaid te worden zolang de app draait; `docker logs` van de server-container noemt de versie
|
||||
bij elke start.
|
||||
|
||||
## Stap 3: de pin in de repo
|
||||
|
||||
Plak de uitvoer van stap 2 in de sessie. De pin gaat dan in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
image: python:3.13-alpine@sha256:AAAA...
|
||||
```
|
||||
|
||||
Dat is een gewone commit **met een versieverhoging** in `umbrel-app.yml`, want de compose staat in de
|
||||
update-whitelist en wordt dus daadwerkelijk uitgerold.
|
||||
|
||||
## Stap 4: het is onderhoud, geen eenmalige handeling
|
||||
|
||||
Een gepinde image krijgt geen beveiligingsupdates meer tot iemand de pin verhoogt. Deze vier stappen horen
|
||||
daarom bij elke release opnieuw gedaan te worden, niet één keer bij de inlevering.
|
||||
@@ -0,0 +1,528 @@
|
||||
# Umbrel community app store: de spec waar deze repo aan moet voldoen
|
||||
|
||||
Naslag, geen plan. Dit is wat umbrelOS verwacht van een repo die als **community app store** dient, plus
|
||||
het mechanisme waarmee een app van Electrum-backend kan wisselen. Uitgezocht op 18-08-2026 tegen de
|
||||
bronnen die onderaan staan; elk feit hieronder komt uit code of een manifest in die repo's, niet uit een
|
||||
blogpost.
|
||||
|
||||
> **Geldt voor beide apps in deze repo.** Dit document is op 18-08-2026 geschreven toen er één app was, en
|
||||
> op sommige plekken is Electrum Gate nog het voorbeeld. De regels zelf zijn eigenschappen van umbrelOS en
|
||||
> gelden onverkort voor Evolu Relay. De uitzondering is §5, "Wat deze repo nu níet heeft": dat is een
|
||||
> momentopname van 18-08-2026 en gaat alleen over Electrum Gate. Wat er voor de relay ánders ligt, staat in
|
||||
> het masterplan **Umbrelapp** §4.
|
||||
|
||||
## 1. De repo-vorm
|
||||
|
||||
Een community app store is een gewone GitHub-repo met deze vorm:
|
||||
|
||||
```
|
||||
<repo-root>/
|
||||
├── umbrel-app-store.yml # id + name van de store
|
||||
├── <store-id>-<app-id>/ # één map per app
|
||||
│ ├── umbrel-app.yml
|
||||
│ └── docker-compose.yml
|
||||
└── <store-id>-<andere-app>/
|
||||
```
|
||||
|
||||
`umbrel-app-store.yml` heeft precies twee velden:
|
||||
|
||||
```yaml
|
||||
id: whatsnext
|
||||
name: WhatsNext?
|
||||
```
|
||||
|
||||
Het `id` is een **verplichte prefix voor elk app-id in de store**. Een store met `id: whatsnext` die een
|
||||
app `electrum-gate` bevat, heeft dus een map `whatsnext-electrum-gate/` en in het manifest
|
||||
`id: whatsnext-electrum-gate`. Mapnaam en manifest-id moeten gelijk zijn.
|
||||
|
||||
Dat is meteen de reden dat het store-id niet naar één app genoemd moet worden: het zit in het id van
|
||||
elke app die er ooit bij komt, en een app-id wijzigen is voor umbrelOS een andere app. Toen Evolu Relay
|
||||
er op 25-08-2026 bij kwam, hoefde er daarom niets te hernoemen.
|
||||
|
||||
De gebruiker voegt de store toe door de URL van de repo in de umbrelOS-interface te plakken. Er is geen
|
||||
review, geen submissie en geen wachttijd; dat is precies de reden om deze weg te kiezen.
|
||||
|
||||
### De repo hoeft niet op GitHub te staan
|
||||
|
||||
De documentatie van Umbrel praat consequent over GitHub, maar de code doet dat niet. In
|
||||
`app-repository.ts` van umbreld is de enige controle op de URL deze:
|
||||
|
||||
```ts
|
||||
function isValidUrl(url: string) {
|
||||
try {
|
||||
void new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Geen hostnaamcontrole, geen `github.com`. Klonen gaat met **isomorphic-git**, een implementatie in
|
||||
JavaScript:
|
||||
|
||||
```ts
|
||||
await git.clone({
|
||||
fs: fse,
|
||||
http,
|
||||
url: this.url,
|
||||
dir: temporaryPath,
|
||||
depth: 1,
|
||||
singleBranch: true,
|
||||
})
|
||||
```
|
||||
|
||||
Een eigen Gitea of Forgejo kan dus de app store zijn. Daar zitten wel drie voorwaarden aan die uit deze
|
||||
aanroep volgen en die niet in de documentatie staan:
|
||||
|
||||
1. **HTTPS, geen SSH.** isomorphic-git spreekt het smart-HTTP-protocol. Een `git@host:pad`-URL werkt niet;
|
||||
het moet `https://host/gebruiker/repo.git` zijn.
|
||||
2. **Anoniem kloonbaar.** Er wordt geen `onAuth` meegegeven, dus umbreld heeft geen manier om
|
||||
inloggegevens aan te bieden. De repo moet publiek leesbaar zijn. Dat is meteen het antwoord op de
|
||||
vraag of een privérepo kan: nee. Lukt het niet, dan meldt umbrelOS `HTTP Error: 401 Unauthorized` bij
|
||||
het toevoegen van de store.
|
||||
|
||||
**Test dit niet met een gewone `git ls-remote`.** Op een machine die ooit naar die repo gepusht heeft,
|
||||
levert een credential-helper de opgeslagen inloggegevens stilzwijgend aan en slaagt de test ten
|
||||
onrechte. Ook met `GIT_TERMINAL_PROMPT=0`, want dat onderdrukt alleen de vráág om een wachtwoord. De
|
||||
test die de toestand van umbreld nabootst:
|
||||
|
||||
```sh
|
||||
GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote https://host/gebruiker/repo.git
|
||||
```
|
||||
|
||||
Slaagt die, dan kan umbreld het ook. Faalt hij met `could not read Username`, dan vraagt de Git-server
|
||||
om een aanmelding. Bij Gitea zijn daar **drie** onafhankelijke oorzaken voor, en ze moeten alle drie
|
||||
goed staan:
|
||||
|
||||
1. **de zichtbaarheid van de repo**, onder Settings;
|
||||
2. **`REQUIRE_SIGNIN_VIEW`** in de serverconfiguratie, die ook publieke repo's achter een aanmelding
|
||||
zet;
|
||||
3. **de zichtbaarheid van het account of de organisatie die eigenaar is**, in te stellen onder Site
|
||||
Administration → User Accounts → Edit → Visibility.
|
||||
|
||||
Die derde is de valstrik, en hij kostte hier de meeste tijd. **Gitea staat niet toe dat een repo
|
||||
zichtbaarder is dan zijn eigenaar.** Staat het account op "limited", dan wordt een repo die je op
|
||||
public zet stilzwijgend teruggezet naar "intern", wat voor een niet-ingelogde bezoeker hetzelfde is
|
||||
als privé. Er komt geen foutmelding; het enige spoor is dat het label na het opslaan op "intern"
|
||||
blijft staan.
|
||||
|
||||
Handig om te weten bij het zoeken: de configuratie van het SynoCommunity-pakket voor Synology heet
|
||||
**`conf.ini`** en niet `app.ini`. Het draaiende proces noemt het echte pad, en dat is sneller dan
|
||||
zoeken:
|
||||
|
||||
```sh
|
||||
ps -ef | grep -i "[g]itea"
|
||||
```
|
||||
3. **Een geldig TLS-certificaat.** Node valideert de keten. Een zelfondertekend certificaat op de
|
||||
Git-server laat het klonen falen.
|
||||
4. **SHA-1 als objectformaat, geen SHA-256.** isomorphic-git berekent object-id's uitsluitend met SHA-1.
|
||||
In `src/utils/shasum.js` staat `import Hash from 'sha.js/sha1.js'` en verder
|
||||
`crypto.subtle.digest('SHA-1', buffer)`; er is geen algoritmeparameter en geen tweede pad. Een repo die
|
||||
met `--object-format=sha256` is aangemaakt, is voor umbreld dus onleesbaar.
|
||||
|
||||
Dit is het opschrijven waard omdat het pas laat zichtbaar wordt. Gitea biedt SHA-256 bij het aanmaken
|
||||
van een repo gewoon als keuze aan, lokaal werkt alles, en de eerste `git push` vanaf een SHA-1-repo
|
||||
faalt met `fatal: the receiving end does not support this repository's hash algorithm`, wat de
|
||||
werkelijke oorzaak niet noemt. Achteraf omzetten kan niet met een instelling: dat is een nieuwe repo
|
||||
plus `git fast-export` naar `git fast-import`. Gevonden op 18-08-2026, bij de eerste push van deze
|
||||
repo.
|
||||
|
||||
Verder is `depth: 1, singleBranch: true` het vermelden waard: alleen de **standaardbranch** wordt
|
||||
opgehaald. De store-inhoud moet daar staan, en een tag of tweede branch doet niets.
|
||||
|
||||
Waar de kloon terechtkomt volgt uit `cleanUrl()`: hostnaam tot de eerste punt, gebruiker en repo uit het
|
||||
pad, plus de eerste acht tekens van de SHA-256 van de URL. Voor
|
||||
`https://sc.kamenier-hamer.nl/sysop/UmbrelApps.git` wordt dat `sysop-umbrelapps-sc-<hash>`. Praktisch
|
||||
gevolg: **de URL is de identiteit van de store.** Verander je hem, dan is het voor umbrelOS een andere
|
||||
store en moet de gebruiker opnieuw toevoegen.
|
||||
|
||||
## 2. Het manifest
|
||||
|
||||
Het schema staat in `packages/umbreld/source/modules/apps/schema.ts` van umbrelOS. Runtime-validatie is
|
||||
daar op dit moment **uitgeschakeld** (`AppManifestSchema.parse` staat uitgecommentarieerd, er wordt een
|
||||
cast gedaan), dus een fout manifest geeft geen nette foutmelding maar vreemd gedrag. Reden te meer om het
|
||||
schema hier op te schrijven.
|
||||
|
||||
Velden die het schema kent, met de type-eis:
|
||||
|
||||
| Veld | Type | Opmerking |
|
||||
|-|-|-|
|
||||
| `manifestVersion` | semver | `1` volstaat; `1.1` is nodig zodra je `hooks/` gebruikt |
|
||||
| `id` | string | gelijk aan de mapnaam, met store-prefix |
|
||||
| `name`, `tagline`, `category`, `version` | string | `version` is vrij tekst, geen semver-eis |
|
||||
| `port` | integer | de **web-UI-poort** die umbrelOS voor de tegel gebruikt, niet je TCP-poort |
|
||||
| `description`, `support` | string | verplicht in het schema |
|
||||
| `website` | URL | moet een geldige URL zijn |
|
||||
| `gallery` | lijst van strings | verplicht in het schema, mag naar externe URL's wijzen |
|
||||
| `icon` | string | optioneel in het schema, maar zonder icoon geen herkenbare tegel |
|
||||
| `dependencies` | lijst van strings | app-id's; zie §4 |
|
||||
| `implements` | lijst van strings | welk app-id deze app kan **vervangen**; zie §4 |
|
||||
| `developer`, `submitter`, `submission`, `repo` | string/URL | optioneel |
|
||||
| `releaseNotes`, `path`, `defaultUsername`, `defaultPassword` | string | optioneel |
|
||||
| `deterministicPassword`, `torOnly`, `optimizedForUmbrelHome`, `disabled` | boolean | optioneel |
|
||||
| `installSize` | integer | in bytes |
|
||||
| `widgets` | lijst | vorm nog niet vastgelegd in het schema |
|
||||
| `backupIgnore` | lijst van strings | paden die niet in de back-up meegaan |
|
||||
| `permissions`, `defaultShell` | | optioneel |
|
||||
|
||||
## 3. De compose
|
||||
|
||||
Twee dingen die de huidige opzet van deze repo níet doet.
|
||||
|
||||
**`app_proxy` in plaats van eigen poorten en netwerken.** umbrelOS genereert zelf een proxy-service; je
|
||||
vult alleen in waar hij heen moet wijzen. De hostnaam is `<app-id>_<compose-service>_1`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app_proxy:
|
||||
environment:
|
||||
# De vorm is: <app-id>_<docker-service-naam>_1
|
||||
APP_HOST: electrumtls-electrum-tls_web_1
|
||||
APP_PORT: 80
|
||||
```
|
||||
|
||||
Een eigen `networks:`-blok op topniveau hoort er niet: de skill in `getumbrel/umbrel-apps` zegt letterlijk
|
||||
dat je `networks: default:` per service alleen gebruikt wanneer je een geteste statische IP of alias nodig
|
||||
hebt. Het `umbrel_main_network` handmatig als extern netwerk aanhaken is de oude 0.5-manier.
|
||||
|
||||
**Images gepind op digest.** De regel uit dezelfde skill: pin elke image als
|
||||
`registry/repo:versie-of-commit@sha256:<digest>`, houd tag en digest samen, en gebruik de **multi-arch
|
||||
index-digest**, niet de architectuurspecifieke. Te controleren met:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect <image>:<tag>
|
||||
```
|
||||
|
||||
Zowel `linux/amd64` als `linux/arm64` moeten erin zitten. Niet toegestaan: `latest`, meebewegende
|
||||
branch-tags, en een digest zonder tag.
|
||||
|
||||
Dit raakt deze app hard: hij draait nu op `alpine:latest` plus een `apk add stunnel` bij élke start. Dat
|
||||
is niet reproduceerbaar, het faalt zonder internet, en het is per definitie niet te pinnen.
|
||||
|
||||
### Hoe bestanden bij de app terechtkomen, en waarom dat bij een update anders gaat
|
||||
|
||||
Dit is niet gedocumenteerd en het is de valkuil met de langste terugverdientijd. Uitgezocht op
|
||||
18-08-2026 in `apps.ts` en het script `legacy-compat/app-script` van umbreld.
|
||||
|
||||
**Bij installatie wordt de hele app-map gekopieerd** naar `${APP_DATA_DIR}`:
|
||||
|
||||
```ts
|
||||
await $`rsync --archive --verbose --exclude ".gitkeep" ${appTemplatePath}/. ${appDataDirectory}`
|
||||
```
|
||||
|
||||
Alles wat in de app-map staat, komt dus mee: scripts, configuratie, een `web/`-map. Een compose die
|
||||
`${APP_DATA_DIR}/entrypoint.sh` mount, werkt daardoor gewoon.
|
||||
|
||||
**Bij een update wordt alleen een whitelist opnieuw gekopieerd.** In `app-script` staat:
|
||||
|
||||
```sh
|
||||
UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"
|
||||
UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"
|
||||
```
|
||||
|
||||
Alles daarbuiten wordt bij een update **niet** ververst. Een gewijzigde `entrypoint.sh` of
|
||||
`web/index.html` bereikt een bestaande installatie dus nooit; de gebruiker ziet zijn oude versie en er is
|
||||
geen foutmelding. Alleen een verwijdering en herinstallatie brengt het over.
|
||||
|
||||
**Gevolg voor het ontwerp.** Zet logica die je later nog wilt kunnen wijzigen op een van deze plekken:
|
||||
|
||||
1. **in `docker-compose.yml` zelf**, bijvoorbeeld als een inline `command:`-blok. De compose staat in de
|
||||
whitelist;
|
||||
2. **in de image**, als je er toch een bouwt;
|
||||
3. **in een `*.template`-bestand.** Dit is de nette umbrel-manier en hij doet twee dingen tegelijk: het
|
||||
bestand staat in de whitelist, én `template_app` verwerkt bij elke start elk
|
||||
`${APP_DATA_DIR}/*.template` naar dezelfde naam zonder de extensie, met de omgevingsvariabelen
|
||||
ingevuld. Een `entrypoint.sh.template` wordt dus `entrypoint.sh` mét `${APP_ELECTRS_NODE_IP}` er al in
|
||||
ingevuld.
|
||||
|
||||
Wat je op grond hiervan **niet** moet doen: een los shellscript naast de compose zetten en aannemen dat
|
||||
een `git push` het uitlevert.
|
||||
|
||||
### Wat er in `app-data` staat, en waarom deze app er anders uitziet dan andere
|
||||
|
||||
Nagetrokken op 20-08-2026, nadat de gebruiker opmerkte dat hij bij andere apps geen app-code in de
|
||||
app-map ziet en dat die map daar eerder voor data lijkt te zijn. Dat klopt, en het verschil zit niet in
|
||||
umbrelOS maar in wat een app zelf in zijn map zet.
|
||||
|
||||
**Voor élke app geldt dat de hele app-map naar `app-data` gekopieerd wordt.** Dus ook bij andere apps
|
||||
staan `docker-compose.yml`, `umbrel-app.yml`, `exports.sh` en `hooks/` in
|
||||
`~/umbrel/app-data/<app-id>/`. Wat daar bij hen níet staat is hun programmacode, want die zit in een
|
||||
Docker-image uit een registry. Bij ons staat die er wel: de agent, de nginx-config en de pagina worden uit
|
||||
`app-data` gemount, omdat deze app geen eigen image bouwt.
|
||||
|
||||
**Config-bestanden uit de app-map mounten is een bestaand patroon, ook bij first-party apps.** Uit
|
||||
`electrs/docker-compose.yml`, letterlijk:
|
||||
|
||||
```yaml
|
||||
- ${APP_DATA_DIR}/torrc:/etc/tor/torrc:ro
|
||||
- "${APP_DATA_DIR}/data/electrs:/data"
|
||||
```
|
||||
|
||||
Dat is precies de vorm die deze app ook gebruikt. Het bewijs dat het bedoeld is, staat in de whitelist
|
||||
hieronder: `torrc` en `*.template` staan er met naam in, en `template_app` verwerkt bij elke start
|
||||
`${app_data_dir}/*.template` met **`envsubst`**. Dat laatste is ook de reden achter de architectuurregel in
|
||||
`CLAUDE.md`: `envsubst` vervangt élke `${NAAM}`, ook een die niet bestaat, en die wordt dan leeg.
|
||||
|
||||
**Waar deze app wél van de conventie afwijkt: de data.** Andere apps zetten hun persistente data onder een
|
||||
submap, `${APP_DATA_DIR}/data/...`, zoals in de regels hierboven. Deze app gebruikt
|
||||
`${APP_DATA_DIR}/runtime` en `${APP_DATA_DIR}/certs`, dus naast de code in plaats van eronder. Dat werkt,
|
||||
maar het is niet de vorm die iemand verwacht die andere apps kent.
|
||||
|
||||
Wat de code in `app-data` verder betekent, en dat is de prijs die bewust betaald is:
|
||||
|
||||
- **wijzigen kan alleen via de whitelist.** Vandaar dat hier alles een `*.template` is;
|
||||
- **het gaat mee in de back-up**, want `app-data` is wat umbrelOS bewaart. Voor de code is dat ruis en
|
||||
voor `runtime/` ook; `certs/` hoort er juist wél in. Het manifest heeft een veld `backupIgnore` om dat
|
||||
te sturen, en dat wordt hier nog niet gebruikt;
|
||||
- **er is geen bouwstap en geen registry**, en geen `apk add` bij het starten. Dat was de reden om het zo
|
||||
te doen, en die staat in het plan **Appstore**, fase 4.
|
||||
|
||||
Bronnen: [`electrs/docker-compose.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/electrs/docker-compose.yml),
|
||||
[`mempool/docker-compose.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/mempool/docker-compose.yml),
|
||||
[`app-script`](https://raw.githubusercontent.com/getumbrel/umbrel/master/packages/umbreld/source/modules/apps/legacy-compat/app-script).
|
||||
|
||||
### De inlevereisen van de officiële appstore
|
||||
|
||||
Opgehaald op 20-08-2026 toen de gebruiker zei dat hij de app uiteindelijk als standaard-app wil
|
||||
publiceren. De eisen staan niet in de README van `umbrel-apps` maar in de skill-documentatie die daar naar
|
||||
verwijst:
|
||||
[`.claude/skills/umbrel-package-app/SKILL.md`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/.claude/skills/umbrel-package-app/SKILL.md).
|
||||
Wat daar staat en wat deze app ervan doet:
|
||||
|
||||
| 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` |
|
||||
| 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 |
|
||||
| `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 |
|
||||
| `icon` weglaten bij inlevering; iconen worden apart gehost | Nu wél gevuld, en dat moet ook zolang dit een eigen store is. Staat daarom als **laatste** regel van het manifest: bij inlevering is dat de enige die weg hoeft |
|
||||
| `app_proxy` met alleen omgevingsvariabelen, geen eigen `ports:` | Klopt. De `ports:` van 50022 staat op de server-service, en dat is normaal voor een niet-web-poort |
|
||||
| Umbrel-inlog aan laten staan; `PROXY_AUTH_WHITELIST` alleen smal en voor paden die geen cookie kunnen sturen | Klopt: er is geen whitelist, dus ook `/api/` zit achter de inlog. Dat is de onderbouwing onder het uploadpad |
|
||||
| Alle gebruikersstaat, config, uploads en geheimen onder `${APP_DATA_DIR}/data/...`, met een `.gitkeep` per map die bij de eerste start moet bestaan | Klopt sinds 0.0.9 |
|
||||
| Niet buiten `${APP_DATA_DIR}` schrijven | Klopt. Wel **lezen** buiten: `${UMBREL_ROOT}/app-data/zoraxy/...` staat alleen-lezen gemount, en dat is het meest ongebruikelijke aan deze app. Reken op een vraag daarover bij de review |
|
||||
|
||||
### Iconen en de drie promo-afbeeldingen
|
||||
|
||||
Uitgezocht op 20-08-2026, nadat de gebruiker opmerkte dat de afbeeldingen bovenaan een app in de store er
|
||||
allemaal hetzelfde uitzien: een achtergrond met een plaatje van de app erop. Dat klopt, en de reden is dat
|
||||
**het store-team ze zelf maakt**. Ze staan dan ook niet in de app-repo maar in
|
||||
[`umbrel-apps-gallery`](https://github.com/getumbrel/umbrel-apps-gallery).
|
||||
|
||||
**In de officiële store zijn het kale bestandsnamen.** Uit `electrs/umbrel-app.yml`:
|
||||
|
||||
```yaml
|
||||
gallery:
|
||||
- 1.jpg
|
||||
- 2.jpg
|
||||
- 3.jpg
|
||||
- 4.jpg
|
||||
```
|
||||
|
||||
Wat er van een inzender gevraagd wordt, uit de inleverdraden: **1440 bij 900 pixels, PNG, drie tot vijf
|
||||
stuks**, of drie tot vijf gewone schermafbeeldingen waarna het team de opmaak doet. Gecombineerd met de
|
||||
regel uit de packaging-documentatie ("set `gallery: []` for new packages") is de praktische route dus:
|
||||
schermafbeeldingen aanleveren, veld leeg laten. Het veld zelf blijft op zijn plek in de volgorde staan;
|
||||
alleen de inhoud is leeg.
|
||||
|
||||
**In een eigen store zijn het absolute URL's**, net als het icoon. Uit het voorbeeld in de
|
||||
community-store-template:
|
||||
|
||||
```yaml
|
||||
icon: https://svgur.com/i/mvA.svg
|
||||
gallery:
|
||||
- https://i.imgur.com/yyVG0Jb.jpeg
|
||||
- https://i.imgur.com/yyVG0Jb.jpeg
|
||||
- https://i.imgur.com/yyVG0Jb.jpeg
|
||||
```
|
||||
|
||||
Voor deze store kan dat dus met dezelfde truc als `icon` nu doet: de bestanden in de repo zetten en er met
|
||||
hun raw-URL naar wijzen.
|
||||
|
||||
**Eén waarschuwing die niet over formaten gaat.** Een schermafbeelding van de certificaatkeuze op deze
|
||||
machine toont veertien hostnamen van de gebruiker, en een van de verbindingsregels toont zijn domein. Voor
|
||||
een publieke winkelpagina horen daar plaatsvervangende namen in. Dat is een andere afweging dan de
|
||||
certificate-transparency-logs die in de app-beschrijving staan: die zijn per certificaat op te zoeken, een
|
||||
winkelpagina zet de hele lijst bij elkaar.
|
||||
|
||||
Bronnen: [`electrs/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/electrs/umbrel-app.yml),
|
||||
[voorbeeld uit de community-store-template](https://raw.githubusercontent.com/getumbrel/umbrel-community-app-store/master/sparkles-hello-world/umbrel-app.yml),
|
||||
[`umbrel-apps-gallery`](https://github.com/getumbrel/umbrel-apps-gallery).
|
||||
|
||||
### Hoe umbrelOS een update ziet
|
||||
|
||||
Via het veld **`version`** in `umbrel-app.yml`. umbreld haalt de repo periodiek op en vergelijkt de
|
||||
versie in de store met die van het geïnstalleerde manifest in `${APP_DATA_DIR}/umbrel-app.yml`. Dat
|
||||
verklaart ook waarom `umbrel-app.yml` als **laatste** wordt gekopieerd bij een update
|
||||
(`UPDATE_FILES_WHITELIST_POST`): het geïnstalleerde manifest is de administratie van wat er staat, en dat
|
||||
mag pas bijgewerkt worden als de rest binnen is.
|
||||
|
||||
**Hoe die vergelijking werkt, voor zover het uitmaakt: `0.0.10` na `0.0.9` levert een update op.** Op
|
||||
20-08-2026 gecontroleerd op de Umbrel van de gebruiker. Dat sluit een tekstvergelijking met groter-dan uit,
|
||||
want daarin is `0.0.10` kleiner dan `0.0.9`. Het is dus een gelijkheids- of semver-vergelijking, en over
|
||||
tweecijferige versiedelen hoef je je geen zorgen te maken. Over een **omlaaggaand** nummer nog steeds wel:
|
||||
dat blijft ongetoetst.
|
||||
|
||||
**Praktische regel die hieruit volgt: een wijziging zonder versieverhoging wordt nooit uitgerold.** Er
|
||||
komt geen melding en geen fout; umbrelOS ziet simpelweg hetzelfde nummer en doet niets. Bij elke
|
||||
functionele wijziging hoort dus een nieuwe `version`, ook bij een kleine reparatie in de compose of in
|
||||
een template.
|
||||
|
||||
### Beschikbare omgevingsvariabelen
|
||||
|
||||
| Variabele | Betekenis |
|
||||
|-|-|
|
||||
| `APP_ID` | app-id uit manifest en mapnaam |
|
||||
| `APP_VERSION` | het `version`-veld |
|
||||
| `APP_DATA_DIR` | `${UMBREL_ROOT}/app-data/<app-id>` |
|
||||
| `APP_MANIFEST_FILE` | pad naar het geïnstalleerde `umbrel-app.yml` |
|
||||
| `UMBREL_ROOT` | de Umbrel-datawortel op de host |
|
||||
| `DEVICE_HOSTNAME` | apparaatnaam zonder `.local` |
|
||||
| `DEVICE_DOMAIN_NAME` | het `.local`-domein van het apparaat |
|
||||
| `APP_DOMAIN` | het lokale `.local`-domein voor deze app |
|
||||
| `APP_PROXY_HOSTNAME`, `APP_PROXY_PORT` | hostnaam en poort van de gegenereerde proxy |
|
||||
| `NETWORK_IP` | basis-IP van het Umbrel-dockernetwerk, met `/16` voor het subnet |
|
||||
| `TOR_PROXY_IP`, `TOR_PROXY_PORT` | de SOCKS-proxy van Umbrel |
|
||||
| `TOR_DATA_DIR` | Tor-datamap op de host |
|
||||
| `APP_HIDDEN_SERVICE` | het onion-adres van de app |
|
||||
| `APP_SEED`, `APP_PASSWORD` | deterministisch per installatie afgeleide geheimen |
|
||||
|
||||
Daarnaast krijgt een app de exports van zijn **afhankelijkheden** (§4).
|
||||
|
||||
## 4. Wisselen tussen Electrs, Fulcrum en ElectrumX
|
||||
|
||||
Dit is het antwoord op de vraag "moet de gebruiker kunnen kiezen tussen Electrs en Fulcrum": umbrelOS
|
||||
1.3 doet dat al, en een app hoeft er niets voor te bouwen.
|
||||
|
||||
**Het mechanisme.** Een app declareert een afhankelijkheid op een app-id, en een andere app kan zeggen
|
||||
dat hij die rol vervult:
|
||||
|
||||
```yaml
|
||||
# fulcrum/umbrel-app.yml en electrumx/umbrel-app.yml
|
||||
implements:
|
||||
- electrs
|
||||
```
|
||||
|
||||
In `AppSettingsSchema` staat `dependencies: z.record(z.string())`: per app wordt bewaard welke
|
||||
implementatie de gebruiker voor welke afhankelijkheid gekozen heeft. umbrelOS laadt vervolgens de
|
||||
`exports.sh` van de **gekozen** app.
|
||||
|
||||
**Waarom dat werkt zonder aanpassing aan de afnemer.** De vervangers aliassen zichzelf naar de
|
||||
Electrs-namen. Uit `fulcrum/exports.sh`:
|
||||
|
||||
```sh
|
||||
export APP_FULCRUM_IP="10.21.22.200"
|
||||
export APP_FULCRUM_NODE_IP="10.21.21.200"
|
||||
export APP_FULCRUM_NODE_PORT="50002"
|
||||
|
||||
for var in IP NODE_IP NODE_PORT; do
|
||||
electrs_var="APP_ELECTRS_${var}"
|
||||
fulcrum_var="APP_FULCRUM_${var}"
|
||||
...
|
||||
export "$electrs_var"="${!electrs_var:=${!fulcrum_var}}"
|
||||
done
|
||||
```
|
||||
|
||||
`electrumx/exports.sh` doet exact hetzelfde met `APP_ELECTRUMX_*`. Ter vergelijking, `electrs/exports.sh`:
|
||||
|
||||
```sh
|
||||
export APP_ELECTRS_IP="10.21.22.4"
|
||||
export APP_ELECTRS_NODE_IP="10.21.21.10"
|
||||
export APP_ELECTRS_NODE_PORT="50001"
|
||||
```
|
||||
|
||||
**Wat dat voor deze app betekent, in twee regels:**
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
- electrs
|
||||
```
|
||||
|
||||
en in de compose `${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}` gebruiken in plaats van een
|
||||
hardgecodeerde containernaam. Daarmee werkt de app met alle drie de backends en kiest de gebruiker in de
|
||||
umbrelOS-instellingen.
|
||||
|
||||
**Let op: alleen `IP`, `NODE_IP` en `NODE_PORT` worden gealiast.** Alles wat Electrs-specifiek is, zoals
|
||||
`APP_ELECTRS_RPC_HIDDEN_SERVICE`, bestaat niet bij Fulcrum. Gebruik die dus niet.
|
||||
|
||||
### Het keuzedialoog bij de installatie, en wat er niet mee kan
|
||||
|
||||
Nagetrokken op 20-08-2026 in de bron, nadat de gebruiker meldde dat hij bij sommige apps twee dropdowns
|
||||
krijgt en vroeg of "Zoraxy of Nginx Proxy Manager" ook zo kan. Dat dialoog bestaat, maar het werkt anders
|
||||
dan het lijkt, en het verschil is precies wat de vraag beantwoordt.
|
||||
|
||||
**Eén dropdown per afhankelijkheid, niet per keuze.** In het schema van umbreld staat:
|
||||
|
||||
```ts
|
||||
dependencies: z.array(z.string()).optional(),
|
||||
implements: z.array(z.string()).optional(),
|
||||
```
|
||||
|
||||
Een afhankelijkheid is dus één app-id en kan zelf géén lijst met alternatieven zijn; een geneste lijst
|
||||
bestaat niet in dit schema. Wat er in zo'n dropdown staat, komt van de andere kant: elke app die
|
||||
`implements: [<dat id>]` declareert, verschijnt erin. Twee dropdowns betekent dus twee afhankelijkheden.
|
||||
Het voorbeeld dat de gebruiker zag is `mempool`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
- bitcoin
|
||||
- electrs
|
||||
```
|
||||
|
||||
Dat geeft twee dropdowns, en de inhoud van de tweede is precies waar §4 hierboven over gaat: Electrs,
|
||||
Fulcrum en ElectrumX declareren allemaal `implements: [electrs]`.
|
||||
|
||||
**Gevolg voor "Zoraxy of NPM": dat kan niet.** Nagekeken in beide manifesten in de officiële appstore:
|
||||
`zoraxy` en `nginx-proxy-manager` declareren **geen** `implements`, dus er is geen gedeelde rol waarop een
|
||||
afhankelijkheid kan wijzen. Er is ook geen weg omheen aan onze kant: `implements` staat in hún manifest en
|
||||
niet in het onze. Wat wél kan is één van de twee hard eisen (`dependencies: [electrs, zoraxy]`), en dat is
|
||||
dan een dropdown met één optie erin.
|
||||
|
||||
Bronnen: [`schema.ts`](https://raw.githubusercontent.com/getumbrel/umbrel/master/packages/umbreld/source/modules/apps/schema.ts),
|
||||
[`mempool/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/mempool/umbrel-app.yml),
|
||||
[`zoraxy/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/zoraxy/umbrel-app.yml),
|
||||
[`nginx-proxy-manager/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/nginx-proxy-manager/umbrel-app.yml).
|
||||
|
||||
### Poortbotsing: 50002
|
||||
|
||||
De poorten die de drie backends op de **host** publiceren:
|
||||
|
||||
| App | Host-poort |
|
||||
|-|-|
|
||||
| Electrs | 50001 |
|
||||
| Fulcrum | 50002 |
|
||||
| ElectrumX | 50001 intern, 50003 als publieke poort |
|
||||
|
||||
Deze app publiceert nu zelf 50002 voor stunnel. Met Fulcrum geïnstalleerd botst dat en start de app niet.
|
||||
De keuze hierover staat als open punt in het plan **Configuratie**.
|
||||
|
||||
## 5. Wat deze repo nu níet heeft
|
||||
|
||||
Puntsgewijs, zodat het plan **Appstore** hier direct op kan leunen:
|
||||
|
||||
1. geen `umbrel-app-store.yml`;
|
||||
2. geen app-submap, alles ligt in de repo-root;
|
||||
3. app-id `electrum-tls` mist de store-prefix;
|
||||
4. geen `app_proxy`-service; wel handmatige host-poorten en een extern `umbrel_main_network`;
|
||||
5. images op `latest`, stunnel via `apk add` bij elke start;
|
||||
6. `dependencies: [electrs]` staat er wel, maar de compose gebruikt `${APP_ELECTRS_IP}` als host terwijl
|
||||
`${APP_ELECTRS_NODE_IP}` bedoeld is: `APP_ELECTRS_IP` is de web-UI-container van Electrs, niet de
|
||||
Electrum-server;
|
||||
7. `icon` en `gallery` wijzen naar de assets van de Electrs-app in `getumbrel/umbrel-apps`, dus naar
|
||||
andermans bestanden en naar een plaatje van een andere app;
|
||||
8. `port: 50002` staat op de TCP-poort terwijl dat veld de web-UI-poort van de tegel is;
|
||||
9. `submitter` en `submission` verwijzen naar `getumbrel/umbrel-apps`, wat voor een community store niet
|
||||
klopt;
|
||||
10. `install.sh` en `uninstall.sh` kopiëren naar `/home/umbrel/umbrel/apps/`, wat onder umbrelOS 1.x door
|
||||
`umbreld` beheerd wordt.
|
||||
|
||||
## 6. Bronnen
|
||||
|
||||
Alles hierboven komt uit deze bestanden, geraadpleegd op 18-08-2026:
|
||||
|
||||
- `getumbrel/umbrel-community-app-store`, README en de voorbeeld-app `sparkles-hello-world/`
|
||||
- `getumbrel/umbrel-apps`, `AGENTS.md` en `.claude/skills/umbrel-package-app/SKILL.md`
|
||||
- `getumbrel/umbrel-apps`, de mappen `electrs/`, `fulcrum/` en `electrumx/`: manifest, compose en
|
||||
`exports.sh`
|
||||
- `getumbrel/umbrel`, `packages/umbreld/source/modules/apps/schema.ts` en `app-repository.ts`
|
||||
- umbrelOS 1.3 release notes over swappable dependencies
|
||||
@@ -0,0 +1,72 @@
|
||||
# Upstream: `trezor/trezor-suite-sync` en Evolu Relay
|
||||
|
||||
Naslag, geen plan. Wat er over de bovenstroomse repo bekend is, plus per feit hoe hard het is. Dat
|
||||
onderscheid staat er expres in: **alles hieronder komt uit het vooronderzoek van 25-08-2026 en is in deze
|
||||
repo nog niet nagetrokken tegen de bron.** Het plan **Proefopstelling** doet precies dat, en de eerste
|
||||
sessie die daar iets van bevestigt of weerlegt, werkt dit document bij met de datum erbij.
|
||||
|
||||
## 1. Wat het is
|
||||
|
||||
Trezor Suite kan labels en accountnamen synchroniseren tussen apparaten. De server die dat doet heet in de
|
||||
interface "custom sync server"; het onderdeel zelf heet **Evolu Relay** en staat in de repo
|
||||
`trezor/trezor-suite-sync`.
|
||||
|
||||
Evolu is een local-first synchronisatielaag: de client houdt zijn eigen kopie bij en de relay is een
|
||||
doorgeefluik voor versleutelde wijzigingen. Dat is ook waarom zelf hosten hier kán zonder dat je iets aan
|
||||
de beveiliging opgeeft: volgens Trezor is de data client-side end-to-end versleuteld, dus de relay ziet
|
||||
niets leesbaars. **Zelf hosten haalt Trezor uit de vergelijking, het verandert de privacygaranties niet.**
|
||||
|
||||
## 2. Wat de repo bevat
|
||||
|
||||
| Onderdeel | Poort | Waarvoor | Hardheid |
|
||||
|-|-|-|-|
|
||||
| `evolu-relay` | 4000 | de eigenlijke sync-relay, dit is wat we nodig hebben | onderzocht 25-08-2026 |
|
||||
| `quota-manager` | 4001 | quota- en betaalserver, noemt een "Payment Server" en een Notion API-spec | onderzocht 25-08-2026 |
|
||||
| Postgres | standaard | opslag achter de relay | onderzocht 25-08-2026 |
|
||||
| `Dockerfile` en `docker-compose.yaml` | | Trezor containeriseert zelf al | onderzocht 25-08-2026 |
|
||||
| `.k8s/` | | Kubernetes-configuratie, voor Umbrel niet relevant | onderzocht 25-08-2026 |
|
||||
|
||||
Laatste release genoemd in het vooronderzoek: **v0.1.8, april 2026**. Dat maakt het een jong en actief
|
||||
project, en dat is een risico dat verder gaat dan een versienummer: de architectuur kan nog schuiven.
|
||||
|
||||
## 3. De vraag die alles bepaalt
|
||||
|
||||
**Is de quota-manager verplicht?** De naam en de beschrijving wijzen op Trezor's eigen gehoste,
|
||||
quota-gebaseerde dienst, en voor privégebruik op één Umbrel is dat niet iets wat je wilt draaien. Maar
|
||||
"bedoeld voor" is geen "optioneel": als de relay bij het starten een verbinding met de quota-manager
|
||||
verwacht, moet hij mee in het pakket.
|
||||
|
||||
Het verschil in uitkomst is groot genoeg om het als eerste te beantwoorden:
|
||||
|
||||
- **niet verplicht** → een pakket van twee containers, relay plus Postgres;
|
||||
- **wel verplicht** → drie containers, plus de vraag wat de quota-manager zelf nodig heeft. Als dat een
|
||||
Notion-sleutel of een betaalprovider is, is dat geen pakketteerprobleem meer maar een blokkade.
|
||||
|
||||
Waar je het antwoord vindt zonder te draaien: `.env.sample`, de compose van Trezor zelf, en de plek in de
|
||||
broncode van de relay waar de quota-manager wordt aangeroepen. Waar je het antwoord bewijst: hem starten
|
||||
zonder.
|
||||
|
||||
## 4. Wat er nog helemaal niet uitgezocht is
|
||||
|
||||
Deze vier staan hier omdat ze het pakket bepalen en omdat het vooronderzoek er niets over zegt. Ze zijn
|
||||
geen taak (dat zijn ze in **Proefopstelling** en **Umbrelapp**), maar een lezer moet niet denken dat de
|
||||
tabel hierboven het hele plaatje is.
|
||||
|
||||
1. **Publiceert Trezor een image in een registry, of is er alleen een Dockerfile?** Dit is de vraag met de
|
||||
grootste gevolgen na de quota-manager. Umbrel wil een image gepind op
|
||||
`repo:versie@sha256:<digest>`, met `linux/amd64` én `linux/arm64` erin. Alleen een Dockerfile betekent
|
||||
dat je zelf bouwt en zelf publiceert, en dan ben je onderhouder van een image geworden.
|
||||
2. **Draait het op arm64?** De helft van de Umbrels is een Raspberry Pi. Een image die alleen amd64 kent,
|
||||
valt daar om, en dat merk je pas op het apparaat.
|
||||
3. **Hoe authenticeert Trezor Suite zich tegen de relay, en over welk protocol praat het?** Dit bepaalt of
|
||||
de app achter de inlog van umbrelOS kan staan. Zie het masterplan **Umbrelapp**, §4b.
|
||||
4. **Accepteert Trezor Suite een `http://`-adres, of eist het TLS?** Daar hangt het masterplan
|
||||
**Bereikbaarheid** aan.
|
||||
|
||||
## 5. Bronnen
|
||||
|
||||
Het vooronderzoek van 25-08-2026 staat ongewijzigd in
|
||||
[Vooronderzoek.PLAN.md](../Plannen/Masterplannen/Archief/Vooronderzoek.PLAN.md); daar is elke regel
|
||||
hierboven vandaan gekomen. De onderliggende bronnen (de repo van Trezor zelf, de release-pagina) zijn nog
|
||||
niet opnieuw geraadpleegd; zet de URL erbij zodra dat gebeurt, zoals de andere referenties in deze map dat
|
||||
doen.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Bestaat dit al voor Umbrel?
|
||||
|
||||
Uitgezocht op 20-08-2026 op verzoek van de gebruiker, met het oog op inlevering in de officiële appstore.
|
||||
De korte versie: **nee, en de reden dat het niet bestaat is opvallend concreet.**
|
||||
|
||||
## Wat er in de officiële store staat en in de buurt komt
|
||||
|
||||
Uit de categorie Networking: **Nginx Proxy Manager** en **OpenResty Manager** ("Expose your services easily
|
||||
and securely"), **Cloudflare Tunnel**, **NetBird**, en verder Pi-hole, AdGuard en dat soort dingen.
|
||||
**Zoraxy** staat er ook (deze app leest zijn certificaatmap). Dat zijn allemaal reverse proxies voor HTTP,
|
||||
of tunnels.
|
||||
|
||||
Geen van die apps doet wat deze app doet: **TLS termineren op een gewone TCP-poort** en het verkeer plat
|
||||
doorzetten naar de Electrum-server.
|
||||
|
||||
## Het beslissende feit
|
||||
|
||||
Nginx Proxy Manager is de populairste van dat rijtje en zit onder de motorkap op precies dezelfde techniek
|
||||
als deze app, namelijk het `stream`-blok van nginx. **Maar zijn beheerinterface kan er geen certificaat op
|
||||
zetten.** Daar staat een openstaand verzoek voor, en de werkwijze die mensen ondertussen gebruiken is met de
|
||||
hand een bestand als `/data/nginx/stream/6.conf` bijwerken, wat de interface bij de volgende wijziging
|
||||
overschrijft.
|
||||
|
||||
De onderliggende nginx kán het wel; dat is precies wat deze app doet, en het is ook waarom de app zo klein
|
||||
kon blijven. Wat hij toevoegt is niet de techniek maar het beheer eromheen: certificaten vinden, weigeren
|
||||
bij twijfel, een keuze onthouden, en herladen bij een vernieuwing zonder verbindingen te verbreken.
|
||||
|
||||
## Dat de vraag bestaat, is ook zichtbaar
|
||||
|
||||
Op het forum van Umbrel staan meerdere draden die letterlijk hierom vragen: "On Umbrel Home, how to enable
|
||||
SSL for Electrs?", "Can't connect to Electrs over Clearnet/HTTPS" en "How can I turn on https?". De
|
||||
antwoorden daarin zijn handwerk: een reverse proxy ervoor zetten, of een VPS met een WireGuard-tunnel.
|
||||
|
||||
## De vergelijking met een tunnel of VPN, en waarom die juist vóór deze app pleit
|
||||
|
||||
Wie dit probleem heeft, kan ook een tunnel of een VPN nemen: **Cloudflare Tunnel**, **NetBird**, Tailscale,
|
||||
WireGuard. Reken erop dat een reviewer daarnaar vraagt.
|
||||
|
||||
Hier stond eerst dat dat een afweging is. **Dat was te vriendelijk voor die alternatieven**, en de gebruiker
|
||||
wees daar op 20-08-2026 op: het vermijden van dat soort producten is bij hem juist de aanleiding voor deze
|
||||
opstelling. Dat is een sterker en preciezer argument, en het is ook feitelijk:
|
||||
|
||||
- **een tunneldienst zit in het pad.** Wie zijn verkeer door zo'n dienst laat lopen, laat het daar
|
||||
termineren; dat is niet een bijwerking maar de manier waarop het werkt;
|
||||
- **een mesh-VPN vraagt een account en een coördinatieserver**, en een client op elk apparaat dat je
|
||||
gebruikt. Op een telefoon betekent dat vaak dat álle verkeer die kant op gaat;
|
||||
- **deze app vraagt geen van beide.** Je eigen certificaat, je eigen domein, je eigen poort, en aan de
|
||||
andere kant een wallet die TLS al spreekt. Dat is precies de lijst in de app: Trezor Suite, Sparrow,
|
||||
BlueWallet, Nunchuk, Blockstream, BitBoxApp;
|
||||
- wat het wél vraagt is één doorgestuurde poort in de router, en dat staat zo in de beschrijving.
|
||||
|
||||
Tor blijft de privacyvriendelijkere weg en staat als zodanig in de beschrijving van de app zelf. Deze app is
|
||||
de andere kant van díe afweging en doet niet alsof hij die niet heeft.
|
||||
|
||||
**In het Engels, voor hergebruik in de winkeltekst en de PR** (staat sinds 0.0.11 ook in `description`):
|
||||
|
||||
> Nothing in the middle. The connection runs from your wallet straight to your own node, encrypted with a
|
||||
> certificate you already own, for a domain you already control. There is no account to create, no tunnel
|
||||
> service that terminates your traffic along the way, and no client to install on every device you use: the
|
||||
> wallets below already speak TLS, they only need an address. What it does ask of you is one forwarded port
|
||||
> on your router.
|
||||
|
||||
## Wat dit betekent voor de inlevering
|
||||
|
||||
De niche is echt en scherp te omschrijven, en dat hoort in de PR-tekst: *de reverse proxies in deze store
|
||||
kunnen certificaten wel op HTTP zetten, maar niet op een TCP-poort, en een Electrum-wallet praat geen HTTP.*
|
||||
|
||||
Bronnen:
|
||||
[Networking-categorie van de appstore](https://apps.umbrel.com/category/networking),
|
||||
[NPM-verzoek om stream-SSL-terminatie](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/2542),
|
||||
[nginx over SSL-terminatie voor TCP](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/),
|
||||
[forumdraad over SSL voor Electrs](https://community.umbrel.com/t/on-umbrel-home-how-to-enable-ssl-for-electrs/24909),
|
||||
[forumdraad over Electrs via clearnet](https://community.umbrel.com/t/cant-connect-to-electrs-over-clearnet-https/13579).
|
||||
@@ -0,0 +1,80 @@
|
||||
# UmbrelApps
|
||||
|
||||
A community app store for [Umbrel](https://umbrel.com), store id `whatsnext`. Add it in umbrelOS under
|
||||
**App Store → Community App Stores**, using the clone URL of this repository.
|
||||
|
||||
## Apps
|
||||
|
||||
### Electrum Gate
|
||||
|
||||
Reach your own Electrum server from outside your network, over TLS. An Electrum server speaks plain TCP;
|
||||
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.
|
||||
|
||||
Two containers, both on an off-the-shelf image, both configured from a `*.template`.
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
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
|
||||
reload keeps existing wallet connections alive.
|
||||
|
||||
| Port | For |
|
||||
|-|-|
|
||||
| 50022 | TLS for Electrum wallets. **Not** the conventional 50002: Fulcrum occupies that on the host, and with Fulcrum as the backend the container would not start |
|
||||
| 3850 | the web UI, through the umbrelOS app proxy |
|
||||
|
||||
Electrs, Fulcrum and ElectrumX all work, switchable in the umbrelOS settings: the app declares the
|
||||
dependency and uses the address it is handed. Details, with sources, in
|
||||
[Docs/Referenties/Umbrel-appstore-spec.md](Docs/Referenties/Umbrel-appstore-spec.md) §4.
|
||||
|
||||
**Tor or TLS.** The privacy win is in running your own server, and you have that the moment you do. Tor
|
||||
remains the better choice for privacy; TLS wins on speed, on mobile, and on networks that block Tor. The
|
||||
trade-off is written out in [Docs/Referenties/Clients.md](Docs/Referenties/Clients.md) §1.
|
||||
|
||||
### Evolu Relay
|
||||
|
||||
> **Not packaged yet.** Plans only, no manifest and no compose.
|
||||
|
||||
Trezor Suite syncs labels and account names between devices, and by default that runs through a server
|
||||
operated by Trezor. That server is open source and called Evolu Relay. The plan is to package it here so
|
||||
the sync runs through your own machine. Trezor states the data is end to end encrypted client-side, so
|
||||
self-hosting does not change that guarantee, it only takes Trezor out of the picture.
|
||||
|
||||
Two questions decide the shape of the package, and both are open: whether Trezor Suite can point at a
|
||||
custom sync server at all, and whether the quota manager (part of Trezor's own paid hosting) is required.
|
||||
See [Docs/Referenties/Upstream-evolu-relay.md](Docs/Referenties/Upstream-evolu-relay.md).
|
||||
|
||||
## Documentatie
|
||||
|
||||
Alles staat in **[Docs/](Docs/README.md)**. Begin bij
|
||||
**[Docs/CONTINUE_HERE.md](Docs/CONTINUE_HERE.md)**; dat is de index die naar de volgende stap wijst.
|
||||
|
||||
| Waar je heen wilt | Waar het staat |
|
||||
|-|-|
|
||||
| Wat er nu speelt en wat de volgende stap is | [Docs/CONTINUE_HERE.md](Docs/CONTINUE_HERE.md) |
|
||||
| Wat umbrelOS van een app store verwacht | [Docs/Referenties/Umbrel-appstore-spec.md](Docs/Referenties/Umbrel-appstore-spec.md) |
|
||||
| Hoe Electrum Gate vandaag in elkaar zit | [Docs/Referenties/Architectuur-huidig.md](Docs/Referenties/Architectuur-huidig.md) |
|
||||
| Welke wallets hierheen kunnen wijzen, en wanneer Tor beter is | [Docs/Referenties/Clients.md](Docs/Referenties/Clients.md) |
|
||||
| Wat er over de sync-server van Trezor bekend is | [Docs/Referenties/Upstream-evolu-relay.md](Docs/Referenties/Upstream-evolu-relay.md) |
|
||||
| Versiegeschiedenis van Electrum Gate | [Docs/CHANGELOG-electrum-gate.md](Docs/CHANGELOG-electrum-gate.md) |
|
||||
| Waar documentatie hoort | [Docs/README.md](Docs/README.md) |
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
python tests/test_agent_certificates.py
|
||||
```
|
||||
|
||||
Losse scripts, geen afhankelijkheden. Let op de regels met `OVERGESLAGEN`: die toetsen hebben de
|
||||
certificaatwinkel van het besturingssysteem of netwerk nodig, en zijn dan niet bewezen. Ze dekken Electrum
|
||||
Gate; voor Evolu Relay is er nog geen bron.
|
||||
|
||||
## Licentie
|
||||
|
||||
De apps zijn dunne lagen om bestaande onderdelen: nginx en Alpine Linux (BSD/MIT) voor Electrum Gate, en
|
||||
straks de relay van Trezor, die zijn eigen licentie houdt. Voor de verpakking zelf is nog geen licentie
|
||||
gekozen.
|
||||
@@ -0,0 +1,625 @@
|
||||
"""Toetst de certificaatlezer en de keuze-guard van de agent.
|
||||
|
||||
Waarom deze test bestaat: de agent leest een X.509-certificaat met een eigen
|
||||
DER-lezer, omdat de standaardbibliotheek er geen heeft en de app bij het starten
|
||||
niets bijinstalleert. Zo'n lezer faalt niet met een foutmelding maar met een
|
||||
verkeerd antwoord, en een certificaatdatum die er een jaar naast zit valt nooit
|
||||
op. Daarom wordt hij hier vergeleken met `ssl`, dat een onafhankelijke
|
||||
implementatie is.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_agent_certificates.py
|
||||
|
||||
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen
|
||||
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat;
|
||||
de eerste toets hieronder controleert precies dat.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Vóór de imports, want anders is het te laat: Python legt bytecode naast
|
||||
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de
|
||||
# app-map. Een keer is zo'n .pyc meegegaan in een commit.
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import importlib.machinery # noqa: E402
|
||||
import importlib.util # noqa: E402
|
||||
import os # noqa: E402
|
||||
import shutil # noqa: E402
|
||||
import socket # noqa: E402
|
||||
import ssl # noqa: E402
|
||||
import subprocess # noqa: E402
|
||||
import tempfile # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
TEMPLATE = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate", "agent.py.template")
|
||||
|
||||
|
||||
def load_agent():
|
||||
"""Laadt agent.py.template als module.
|
||||
|
||||
Met een expliciete loader, want importlib kijkt normaal naar de extensie en
|
||||
.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)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class Uitslag:
|
||||
def __init__(self):
|
||||
self.goed = 0
|
||||
self.fout = []
|
||||
|
||||
def check(self, naam, gelukt, uitleg=""):
|
||||
if gelukt:
|
||||
self.goed += 1
|
||||
else:
|
||||
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||
|
||||
def rapport(self):
|
||||
print()
|
||||
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||
for f in self.fout:
|
||||
print(" FOUT: " + f)
|
||||
return 0 if not self.fout else 1
|
||||
|
||||
|
||||
def der(tag, inhoud):
|
||||
"""Eén DER-element, met de lengte in korte of lange vorm."""
|
||||
if len(inhoud) < 0x80:
|
||||
return bytes([tag, len(inhoud)]) + inhoud
|
||||
lengte = len(inhoud).to_bytes((len(inhoud).bit_length() + 7) // 8, "big")
|
||||
return bytes([tag, 0x80 | len(lengte)]) + lengte + inhoud
|
||||
|
||||
|
||||
def nep_certificaat(tijdtag, tijdtekst):
|
||||
"""Het kleinste certificaat waarin not_after zijn validity vindt."""
|
||||
tijd = der(tijdtag, tijdtekst.encode("ascii"))
|
||||
return der(0x30, der(0x30, der(0x30, tijd + tijd)))
|
||||
|
||||
|
||||
def test_template_is_invulbaar_zonder_schade(u):
|
||||
"""De aanname waar deze hele test op rust."""
|
||||
with open(TEMPLATE, "r", encoding="utf-8") as f:
|
||||
inhoud = f.read()
|
||||
u.check("template bevat geen accolade-variabelen",
|
||||
"${" not in inhoud,
|
||||
"umbreld zou die invullen en de Python-code slopen")
|
||||
|
||||
|
||||
def test_einddatum_gelijk_aan_ssl(agent, u):
|
||||
"""De echte certificaten uit de certificaatwinkel van het besturingssysteem.
|
||||
|
||||
Dit is de belangrijkste toets, want `ssl` levert hier het antwoordblad. Zijn
|
||||
er geen certificaten te vinden, dan wordt dit overgeslagen en gemeld, in
|
||||
plaats van stil als geslaagd geteld.
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
geparseerd = ctx.get_ca_certs(binary_form=False)
|
||||
ders = ctx.get_ca_certs(binary_form=True)
|
||||
if not ders or len(ders) != len(geparseerd):
|
||||
print("OVERGESLAGEN: geen CA-certificaten beschikbaar op dit systeem, "
|
||||
"dus de vergelijking met ssl is niet gedaan")
|
||||
return
|
||||
|
||||
afwijkingen = []
|
||||
for i, d in enumerate(ders):
|
||||
verwacht = int(ssl.cert_time_to_seconds(geparseerd[i]["notAfter"]))
|
||||
try:
|
||||
gekregen = agent.not_after(d)
|
||||
except Exception as e: # noqa: BLE001
|
||||
afwijkingen.append("cert %d wierp %r" % (i, e))
|
||||
continue
|
||||
if gekregen != verwacht:
|
||||
afwijkingen.append("cert %d: verwacht %d, kreeg %d"
|
||||
% (i, verwacht, gekregen))
|
||||
u.check("einddatum van %d CA-certificaten gelijk aan ssl" % len(ders),
|
||||
not afwijkingen, "; ".join(afwijkingen[:3]))
|
||||
|
||||
|
||||
def test_beide_tijdvormen(agent, u):
|
||||
"""UTCTime en GeneralizedTime, en beide takken van de eeuwregel.
|
||||
|
||||
Het CA-materiaal gebruikt uitsluitend UTCTime met jaren na 2000, dus zonder
|
||||
deze zelfgebouwde certificaten blijven twee takken ongetoetst.
|
||||
"""
|
||||
gevallen = [
|
||||
(0x18, "20700101000000Z", "Jan 1 00:00:00 2070 GMT"),
|
||||
(0x18, "99991231235959Z", "Dec 31 23:59:59 9999 GMT"),
|
||||
(0x17, "491231235959Z", "Dec 31 23:59:59 2049 GMT"),
|
||||
(0x17, "500101000000Z", "Jan 1 00:00:00 1950 GMT"),
|
||||
]
|
||||
for tag, tekst, ssl_tekst in gevallen:
|
||||
verwacht = int(ssl.cert_time_to_seconds(ssl_tekst))
|
||||
gekregen = agent.not_after(nep_certificaat(tag, tekst))
|
||||
u.check("tijd %s" % tekst, gekregen == verwacht,
|
||||
"verwacht %d, kreeg %d" % (verwacht, gekregen))
|
||||
|
||||
|
||||
def test_tijd_zonder_zulu_faalt_hard(agent, u):
|
||||
"""Het niet-gelukkige pad: een tijd die DER niet toestaat.
|
||||
|
||||
Dit is met opzet een uitzondering en geen benadering. Een certificaatdatum die
|
||||
er een uur naast zit, valt nooit op.
|
||||
"""
|
||||
slecht = nep_certificaat(0x18, "20700101000000")
|
||||
try:
|
||||
agent.not_after(slecht)
|
||||
u.check("tijd zonder Z faalt", False, "er kwam een antwoord uit")
|
||||
except ValueError:
|
||||
u.check("tijd zonder Z faalt", True)
|
||||
except Exception as e: # noqa: BLE001
|
||||
u.check("tijd zonder Z faalt", False, "verkeerde uitzondering: %r" % e)
|
||||
|
||||
|
||||
def test_certificaat_zonder_validity_faalt(agent, u):
|
||||
"""Een certificaat waar niets in staat, mag geen datum opleveren."""
|
||||
leeg = der(0x30, der(0x30, der(0x02, b"\x01")))
|
||||
try:
|
||||
agent.not_after(leeg)
|
||||
u.check("geen validity faalt", False, "er kwam een antwoord uit")
|
||||
except ValueError:
|
||||
u.check("geen validity faalt", True)
|
||||
|
||||
|
||||
def test_live_certificaat_met_san(agent, u):
|
||||
"""Een echt servercertificaat, want CA-certificaten hebben geen dNSName.
|
||||
|
||||
Zonder netwerk wordt dit overgeslagen en blijft subjectAltName ongetoetst.
|
||||
Dat is gemeld, niet verzwegen.
|
||||
"""
|
||||
try:
|
||||
with ssl.create_default_context().wrap_socket(
|
||||
socket.create_connection(("example.com", 443), timeout=6),
|
||||
server_hostname="example.com",
|
||||
) as s:
|
||||
binair = s.getpeercert(binary_form=True)
|
||||
info = s.getpeercert()
|
||||
except Exception as e: # noqa: BLE001
|
||||
print("OVERGESLAGEN: geen verbinding voor de SAN-toets (%s), "
|
||||
"subjectAltName blijft dus ongetoetst" % e)
|
||||
return
|
||||
|
||||
verwacht = sorted(v for k, v in info.get("subjectAltName", ()) if k == "DNS")
|
||||
gekregen = sorted(agent.dns_names(binair))
|
||||
u.check("subjectAltName gelijk aan ssl", gekregen == verwacht,
|
||||
"verwacht %r, kreeg %r" % (verwacht[:3], gekregen[:3]))
|
||||
u.check("einddatum van een levend certificaat gelijk aan ssl",
|
||||
agent.not_after(binair) == int(ssl.cert_time_to_seconds(info["notAfter"])))
|
||||
|
||||
|
||||
def test_keuze_negeert_onbekende_id(agent, u):
|
||||
"""De guard. Dit is het pad waar een fout stil TLS zou uitschakelen.
|
||||
|
||||
Wijst de keuze naar iets wat niet bestaat, dan mag de app niet doen alsof.
|
||||
Hij valt terug op wat er wel is, en meldt dat.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
agent.CONFIG_DIR = tmp
|
||||
agent.SELECTED_FILE = os.path.join(tmp, "selected-cert")
|
||||
|
||||
nu = 2_000_000_000
|
||||
certs = [
|
||||
{"id": "zoraxy/kort", "source": "zoraxy", "name": "kort",
|
||||
"cert": "/x", "key": "/y", "not_after": nu + 86400},
|
||||
{"id": "zoraxy/lang", "source": "zoraxy", "name": "lang",
|
||||
"cert": "/a", "key": "/b", "not_after": nu + 400 * 86400},
|
||||
]
|
||||
|
||||
agent.write_selection("zoraxy/bestaat-niet")
|
||||
gekozen, waarom = agent.choose(certs)
|
||||
u.check("onbekende keuze wordt niet gehonoreerd",
|
||||
gekozen is None, "koos %r" % (gekozen and gekozen["id"]))
|
||||
u.check("en er wordt ook niet zomaar een ander gepakt",
|
||||
"2 certificates found" in waarom, "waarom was %r" % waarom)
|
||||
# De reden noemt het aantal en niet de namen. Op de Umbrel van de
|
||||
# gebruiker zijn dat er veertien, en die opsomming maakte van de melding
|
||||
# een muur tekst die zegt wat de keuzelijst eronder al toont.
|
||||
u.check("de reden somt de kandidaten niet op",
|
||||
"zoraxy/kort" not in waarom and "zoraxy/lang" not in waarom,
|
||||
"waarom was %r" % waarom)
|
||||
|
||||
# Eén kandidaat is geen keuze, dus die mag wel automatisch.
|
||||
gekozen, waarom = agent.choose(certs[:1])
|
||||
u.check("bij precies één certificaat kiest de app zelf",
|
||||
gekozen is not None and gekozen["id"] == "zoraxy/kort",
|
||||
"koos %r" % (gekozen and gekozen["id"]))
|
||||
|
||||
agent.write_selection("zoraxy/kort")
|
||||
gekozen, waarom = agent.choose(certs)
|
||||
u.check("een bestaande keuze wint van de automaat",
|
||||
gekozen["id"] == "zoraxy/kort", "koos %r" % gekozen["id"])
|
||||
|
||||
onleesbaar = [dict(certs[0], unreadable="stuk")]
|
||||
gekozen, waarom = agent.choose(onleesbaar)
|
||||
u.check("een onleesbaar certificaat wordt niet gekozen",
|
||||
gekozen is None, "koos %r" % (gekozen and gekozen["id"]))
|
||||
|
||||
|
||||
def test_keuze_zonder_geldig_certificaat(agent, u):
|
||||
"""Alles verlopen: dan liever niets dan een verlopen certificaat opdringen."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
agent.CONFIG_DIR = tmp
|
||||
agent.SELECTED_FILE = os.path.join(tmp, "selected-cert")
|
||||
verlopen = [{"id": "own/oud", "source": "own", "name": "oud",
|
||||
"cert": "/a", "key": "/b", "not_after": 1}]
|
||||
gekozen, waarom = agent.choose(verlopen)
|
||||
u.check("alles verlopen levert geen keuze op", gekozen is None)
|
||||
u.check("en de reden wordt genoemd", "expired" in waarom,
|
||||
"waarom was %r" % waarom)
|
||||
|
||||
gekozen, waarom = agent.choose([])
|
||||
u.check("een lege lijst levert geen keuze op", gekozen is None)
|
||||
|
||||
|
||||
def test_paren_vinden(agent, u):
|
||||
"""De drie naamvormen van de bronnen, in een echte map."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# Zoraxy-vorm
|
||||
open(os.path.join(tmp, "a.example.org.pem"), "w").close()
|
||||
open(os.path.join(tmp, "a.example.org.key"), "w").close()
|
||||
# Een certificaat zonder sleutel hoort niet mee te komen
|
||||
open(os.path.join(tmp, "b.example.org.pem"), "w").close()
|
||||
# Nginx Proxy Manager-vorm
|
||||
os.mkdir(os.path.join(tmp, "npm-7"))
|
||||
open(os.path.join(tmp, "npm-7", "fullchain.pem"), "w").close()
|
||||
open(os.path.join(tmp, "npm-7", "privkey.pem"), "w").close()
|
||||
# Een map zonder sleutel hoort ook niet mee te komen
|
||||
os.mkdir(os.path.join(tmp, "npm-8"))
|
||||
open(os.path.join(tmp, "npm-8", "fullchain.pem"), "w").close()
|
||||
|
||||
namen = sorted(n for n, _, _ in agent._pairs_in(tmp))
|
||||
u.check("alleen volledige paren", namen == ["a.example.org", "npm-7"],
|
||||
"vond %r" % namen)
|
||||
|
||||
u.check("een map die niet bestaat levert een lege lijst",
|
||||
agent._pairs_in(os.path.join(tmp, "weg")) == [])
|
||||
|
||||
|
||||
def openssl_paar(map_, naam, cn, dagen=365, wachtwoord=None, san=True):
|
||||
"""Een echt zelfondertekend paar. Geeft (certpad, keypad) of None.
|
||||
|
||||
Echt en niet zelfgebouwd, want de controle die hier getoetst wordt is die van
|
||||
OpenSSL zelf: hoort deze sleutel bij dit certificaat. Een met de hand
|
||||
geknutseld DER-certificaat heeft geen sleutel en bewijst daar dus niets over.
|
||||
"""
|
||||
if not shutil.which("openssl"):
|
||||
return None
|
||||
cert = os.path.join(map_, naam + ".crt")
|
||||
key = os.path.join(map_, naam + ".key")
|
||||
opdracht = [
|
||||
"openssl", "req", "-x509", "-newkey", "rsa:2048",
|
||||
"-keyout", key, "-out", cert, "-days", str(dagen),
|
||||
# De schuine streep is in -subj het scheidingsteken tussen velden, dus een
|
||||
# hostnaam die er zelf een bevat moet ontsnapt worden. Zonder dit sloeg de
|
||||
# padtruc-toets zichzelf stil over, en dat is precies de toets die er moet
|
||||
# zijn.
|
||||
"-subj", "/CN=" + cn.replace("/", "\\/"),
|
||||
]
|
||||
if san:
|
||||
opdracht += ["-addext", "subjectAltName=DNS:" + cn]
|
||||
if wachtwoord:
|
||||
opdracht += ["-passout", "pass:" + wachtwoord]
|
||||
else:
|
||||
opdracht += ["-nodes"]
|
||||
klaar = subprocess.run(opdracht, capture_output=True)
|
||||
if klaar.returncode != 0:
|
||||
# De laatste regel, want openssl vult stderr met voortgangspuntjes en de
|
||||
# echte reden staat onderaan.
|
||||
regels = [r for r in klaar.stderr.decode("utf-8", "replace").splitlines()
|
||||
if r.strip()]
|
||||
print("OVERGESLAGEN: openssl gaf een fout bij %s (%s)"
|
||||
% (naam, regels[-1][:160] if regels else "geen melding"))
|
||||
return None
|
||||
return cert, key
|
||||
|
||||
|
||||
class Tijdreis:
|
||||
"""Een tijdmodule die alleen over `time()` liegt.
|
||||
|
||||
Nodig om de vervaldatum-guard te toetsen: een certificaat dat al verlopen ís
|
||||
kan openssl 3.1 niet maken, en de datum vooruitzetten kan wel.
|
||||
"""
|
||||
|
||||
def __init__(self, echt, wanneer):
|
||||
self._echt = echt
|
||||
self._wanneer = wanneer
|
||||
|
||||
def time(self):
|
||||
return self._wanneer
|
||||
|
||||
def __getattr__(self, naam):
|
||||
return getattr(self._echt, naam)
|
||||
|
||||
|
||||
def test_upload(agent, u):
|
||||
"""Het uploadpad, en vooral wat het weigert.
|
||||
|
||||
Dit is het enige pad waarlangs iets van buiten bestanden neerzet, dus de
|
||||
toetsen gaan over de niet-gelukkige gevallen. De duurste fout die hier
|
||||
voorkomen moet worden is een sleutel die niet bij het certificaat hoort: die
|
||||
levert een nginx op die niet meer herlaadt, en dat is dezelfde klasse storing
|
||||
als waardoor 0.0.3 helemaal niet startte.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bron = os.path.join(tmp, "bron")
|
||||
doel = os.path.join(tmp, "own")
|
||||
os.makedirs(bron)
|
||||
agent.UPLOAD_DIR = doel
|
||||
agent.SOURCES = [("Own folder", doel)]
|
||||
|
||||
# Uit zonder map. Eerst, want dit hoort niet stil door te vallen naar een
|
||||
# pad dat toevallig bestaat.
|
||||
agent.UPLOAD_DIR = ""
|
||||
naam, fout = agent.accept_upload("x", "y")
|
||||
u.check("zonder uploadmap weigert de agent",
|
||||
naam is None and "switched off" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
agent.UPLOAD_DIR = doel
|
||||
|
||||
naam, fout = agent.accept_upload("hallo", "-----BEGIN PRIVATE KEY-----")
|
||||
u.check("rommel in plaats van een certificaat wordt geweigerd",
|
||||
naam is None and "does not contain a certificate" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
|
||||
naam, fout = agent.accept_upload(
|
||||
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----", "hallo")
|
||||
u.check("rommel in plaats van een sleutel wordt geweigerd",
|
||||
naam is None and "does not contain a private key" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
|
||||
u.check("een weigering laat niets achter in de doelmap",
|
||||
not os.path.isdir(doel) or os.listdir(doel) == [],
|
||||
"er staat: %r" % (os.path.isdir(doel) and os.listdir(doel)))
|
||||
|
||||
paar = openssl_paar(bron, "goed", "gate.example.org")
|
||||
if paar is None:
|
||||
print("OVERGESLAGEN: geen openssl, dus het gelukkige pad en de "
|
||||
"sleutelcontrole van deze toets blijven ongetoetst")
|
||||
return
|
||||
cert_tekst = open(paar[0], encoding="ascii").read()
|
||||
key_tekst = open(paar[1], encoding="ascii").read()
|
||||
|
||||
# De belangrijkste toets van dit bestand na de datumvergelijking: een
|
||||
# sleutel van een ánder paar hoort hier te stranden.
|
||||
ander = openssl_paar(bron, "ander", "andere.example.org")
|
||||
naam, fout = agent.accept_upload(cert_tekst,
|
||||
open(ander[1], encoding="ascii").read())
|
||||
u.check("een sleutel van een ander paar wordt geweigerd",
|
||||
naam is None and "does not go with" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
u.check("en ook dan blijft de doelmap leeg",
|
||||
not os.path.isdir(doel) or os.listdir(doel) == [],
|
||||
"er staat: %r" % (os.path.isdir(doel) and os.listdir(doel)))
|
||||
|
||||
# Een versleutelde sleutel: nginx kan die niet openen zonder wachtwoord.
|
||||
met_slot = openssl_paar(bron, "slot", "slot.example.org",
|
||||
wachtwoord="geheim")
|
||||
if met_slot:
|
||||
naam, fout = agent.accept_upload(
|
||||
open(met_slot[0], encoding="ascii").read(),
|
||||
open(met_slot[1], encoding="ascii").read())
|
||||
u.check("een versleutelde sleutel wordt geweigerd",
|
||||
naam is None and "encrypted" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
|
||||
# Verlopen. De klok gaat vooruit in plaats van het certificaat achteruit.
|
||||
echt = agent.time
|
||||
agent.time = Tijdreis(echt, echt.time() + 400 * 86400)
|
||||
try:
|
||||
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
|
||||
finally:
|
||||
agent.time = echt
|
||||
u.check("een verlopen certificaat wordt geweigerd",
|
||||
naam is None and "expired on" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
|
||||
# Een certificaat zonder bruikbare naam: er is dan niets om een wallet
|
||||
# tegen te vergelijken, dus plaatsen heeft geen zin.
|
||||
naamloos = openssl_paar(bron, "naamloos", "!!!", san=False)
|
||||
if naamloos:
|
||||
naam, fout = agent.accept_upload(
|
||||
open(naamloos[0], encoding="ascii").read(),
|
||||
open(naamloos[1], encoding="ascii").read())
|
||||
u.check("een certificaat zonder hostnaam wordt geweigerd",
|
||||
naam is None and "no host name" in (fout or ""),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
|
||||
# En dan het gelukkige pad.
|
||||
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
|
||||
u.check("een geldig paar wordt aangenomen",
|
||||
naam == "gate.example.org" and fout is None,
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
u.check("en staat als paar in de doelmap",
|
||||
sorted(os.listdir(doel)) == ["gate.example.org.key",
|
||||
"gate.example.org.pem"],
|
||||
"er staat: %r" % sorted(os.listdir(doel)))
|
||||
u.check("de scanner vindt het meteen",
|
||||
[n for n, _, _ in agent._pairs_in(doel)] == ["gate.example.org"])
|
||||
u.check("de id verwijst naar de bron uit de omgeving",
|
||||
agent.upload_id(naam) == "Own folder/gate.example.org",
|
||||
"gaf %r" % agent.upload_id(naam))
|
||||
|
||||
# Nog een keer hetzelfde: dat vervangt, en laat geen .tmp achter. Een
|
||||
# vernieuwd certificaat is precies waarom dit pad bestaat.
|
||||
naam, fout = agent.accept_upload(cert_tekst, key_tekst)
|
||||
u.check("hetzelfde nog eens vervangt zonder klagen",
|
||||
naam == "gate.example.org" and fout is None,
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
u.check("en er blijft geen tijdelijk bestand liggen",
|
||||
not [n for n in os.listdir(doel) if n.endswith(".tmp")],
|
||||
"er staat: %r" % sorted(os.listdir(doel)))
|
||||
|
||||
# Een naam met padtrucs erin kan niet uit het doel ontsnappen, want de
|
||||
# naam komt uit het certificaat en niet uit het verzoek.
|
||||
stout = openssl_paar(bron, "stout", "../../etc/nginx/evil", san=False)
|
||||
if stout:
|
||||
naam, fout = agent.accept_upload(
|
||||
open(stout[0], encoding="ascii").read(),
|
||||
open(stout[1], encoding="ascii").read())
|
||||
u.check("een hostnaam met padtekens levert een platte naam op",
|
||||
fout is None and "/" not in (naam or "/") and ".." not in (naam or ".."),
|
||||
"gaf %r, %r" % (naam, fout))
|
||||
u.check("en het bestand staat in de doelmap zelf",
|
||||
fout is not None or os.path.isfile(
|
||||
os.path.join(doel, naam + ".pem")))
|
||||
|
||||
|
||||
def test_sessieregels_indelen(agent, u):
|
||||
"""De drie soorten sessieregels uit de nginx-log.
|
||||
|
||||
Het onderscheid tussen `probe` en `refused` is het punt: een doorgestuurde
|
||||
poort wordt gescand, en zonder dat onderscheid gaf elke scan een rode regel
|
||||
"refused" terwijl er niets geweigerd is. De grens ligt bij de bytes en niet bij
|
||||
de duur of de status, en dat is precies wat hier vastligt.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
agent.STREAM_LOG = os.path.join(tmp, "stream.log")
|
||||
with open(agent.STREAM_LOG, "w", encoding="utf-8") as f:
|
||||
# Een echte sessie, een scan, en een verbinding die wél iets
|
||||
# doorgaf en daarna omviel.
|
||||
f.write("2026-08-20T12:39:29+02:00 200 95300 218000 240.5\n")
|
||||
f.write("2026-08-20T12:38:29+02:00 500 0 0 1.001\n")
|
||||
f.write("2026-08-20T12:40:29+02:00 502 120 0 0.5\n")
|
||||
# Een scan die lang open bleef staan: nog steeds een scan.
|
||||
f.write("2026-08-20T12:41:29+02:00 500 0 0 45.0\n")
|
||||
f.write("rommel die geen sessieregel is\n")
|
||||
|
||||
regels = agent.read_stream_log(0)
|
||||
soorten = [r["event"] for r in regels]
|
||||
u.check("de drie soorten komen er in de juiste orde uit",
|
||||
soorten == ["disconnect", "probe", "refused", "probe"],
|
||||
"gaf %r" % soorten)
|
||||
|
||||
u.check("een onleesbare regel wordt overgeslagen", len(regels) == 4,
|
||||
"gaf %d regels" % len(regels))
|
||||
|
||||
probe = regels[1]
|
||||
u.check("de scan houdt zijn status in de notitie",
|
||||
probe.get("note") == "nothing exchanged, status 500",
|
||||
"gaf %r" % probe.get("note"))
|
||||
u.check("en zijn bytes staan op nul",
|
||||
probe["bytes_in"] == 0 and probe["bytes_out"] == 0)
|
||||
|
||||
stuk = regels[2]
|
||||
u.check("een sessie die wél verkeer had heet refused",
|
||||
stuk.get("note") == "session ended with status 502",
|
||||
"gaf %r" % stuk.get("note"))
|
||||
|
||||
echt = regels[0]
|
||||
# 240 en niet 241: round() in Python rondt een halve naar even af.
|
||||
u.check("een geslaagde sessie heeft geen notitie nodig",
|
||||
"note" not in echt and echt["seconds"] == 240,
|
||||
"gaf %r" % echt)
|
||||
|
||||
|
||||
def test_open_verbindingen(agent, u):
|
||||
"""De verbindingsteller, en de logregel die eruit volgt.
|
||||
|
||||
Dit gaat via `build_status`, dus via de ronde die de agent echt draait, en
|
||||
niet via een interne helper. Twee dingen moeten kloppen die makkelijk stil
|
||||
fout gaan: een teller die er niet is mag geen nul worden, en een
|
||||
connect-regel moet de volgende ronde overleven. Die laatste zat er eerst
|
||||
naast: `build_status` neemt de eigen gebeurtenissen van de vorige ronde over
|
||||
aan de hand van een opsomming van soorten, en `connect` stond er niet in.
|
||||
Een regel die na een minuut weer verdwijnt is nutteloos.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
agent.SOURCES = []
|
||||
agent.ELECTRUM_HOST = "" # dan faalt de probe meteen, zonder netwerk
|
||||
agent.STATE_DIR = tmp
|
||||
agent.CONFIG_DIR = os.path.join(tmp, "config")
|
||||
agent.SELECTED_FILE = os.path.join(agent.CONFIG_DIR, "selected-cert")
|
||||
agent.STATUS_FILE = os.path.join(tmp, "status.json")
|
||||
agent.CERT_CONF = os.path.join(tmp, "cert.conf")
|
||||
agent.RELOAD_FLAG = os.path.join(tmp, "reload")
|
||||
agent.STREAM_LOG = os.path.join(tmp, "stream.log")
|
||||
agent.SESSIONS_FILE = os.path.join(tmp, "sessions")
|
||||
|
||||
def ronde():
|
||||
status = agent.build_status([])
|
||||
agent.write_status(status)
|
||||
return status
|
||||
|
||||
# Geen tellerbestand: dan hoort er niets in de status te staan. Nul zou
|
||||
# hier "geen wallet verbonden" beweren op grond van niets.
|
||||
status = ronde()
|
||||
u.check("zonder teller geen bewering over verbindingen",
|
||||
"open_connections" not in status["tls"],
|
||||
"stond er: %r" % status["tls"])
|
||||
|
||||
def zet_teller(tekst):
|
||||
with open(agent.SESSIONS_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(tekst)
|
||||
|
||||
zet_teller("0\n")
|
||||
status = ronde()
|
||||
u.check("een teller van nul komt wel in de status",
|
||||
status["tls"].get("open_connections") == 0)
|
||||
u.check("nul verbindingen levert geen connect-regel op",
|
||||
not [e for e in status["log"] if e.get("event") == "connect"])
|
||||
|
||||
zet_teller("1\n")
|
||||
status = ronde()
|
||||
connects = [e for e in status["log"] if e.get("event") == "connect"]
|
||||
u.check("een verbinding erbij levert één connect-regel op",
|
||||
len(connects) == 1, "regels: %r" % connects)
|
||||
u.check("en die regel noemt hoeveel er nu openstaan",
|
||||
connects and connects[0].get("note") == "1 open now",
|
||||
"regel: %r" % (connects[0] if connects else None))
|
||||
|
||||
# Zelfde aantal, dus geen nieuwe regel. En de vorige moet blijven staan:
|
||||
# dit is de toets die de vergeten opsomming vond.
|
||||
status = ronde()
|
||||
connects = [e for e in status["log"] if e.get("event") == "connect"]
|
||||
u.check("een gelijk aantal levert geen tweede regel op",
|
||||
len(connects) == 1, "regels: %r" % connects)
|
||||
|
||||
zet_teller("rommel")
|
||||
status = ronde()
|
||||
u.check("een onleesbare teller wordt niets, niet nul",
|
||||
"open_connections" not in status["tls"],
|
||||
"stond er: %r" % status["tls"])
|
||||
u.check("en de eerdere connect-regel staat er nog",
|
||||
len([e for e in status["log"] if e.get("event") == "connect"]) == 1)
|
||||
|
||||
# Van onbekend naar een aantal hoort ook een regel op te leveren. Dit is
|
||||
# het geval "de wallet hing er al voordat de agent begon", en dat is
|
||||
# precies wanneer iemand op de pagina komt kijken. Zonder deze regel blijft
|
||||
# het log dan uren stil.
|
||||
zet_teller("2\n")
|
||||
status = ronde()
|
||||
connects = [e for e in status["log"] if e.get("event") == "connect"]
|
||||
u.check("na een onbekend aantal levert een verbinding alsnog een regel op",
|
||||
len(connects) == 2, "regels: %r" % connects)
|
||||
u.check("en die noemt het aantal dat er nu staat",
|
||||
connects[-1].get("note") == "2 open now",
|
||||
"regel: %r" % connects[-1])
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
test_template_is_invulbaar_zonder_schade(u)
|
||||
agent = load_agent()
|
||||
test_einddatum_gelijk_aan_ssl(agent, u)
|
||||
test_beide_tijdvormen(agent, u)
|
||||
test_tijd_zonder_zulu_faalt_hard(agent, u)
|
||||
test_certificaat_zonder_validity_faalt(agent, u)
|
||||
test_live_certificaat_met_san(agent, u)
|
||||
test_keuze_negeert_onbekende_id(agent, u)
|
||||
test_keuze_zonder_geldig_certificaat(agent, u)
|
||||
test_paren_vinden(agent, u)
|
||||
# Deze twee als laatste, want ze verzetten de module-instellingen naar een
|
||||
# tijdelijke map.
|
||||
test_upload(agent, u)
|
||||
test_sessieregels_indelen(agent, u)
|
||||
test_open_verbindingen(agent, u)
|
||||
return u.rapport()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Toetst dat de pagina omhoog komt zonder certificaat.
|
||||
|
||||
Waarom deze test bestaat. In 0.0.3 startte de app niet: het stream-blok van
|
||||
nginx had een 'listen 50022 ssl' met een include naar de cert.conf van de agent,
|
||||
en nginx weigert te starten als dat certificaat er niet is. Daardoor kwam ook de
|
||||
web-UI niet omhoog, en dat is precies de pagina waarop je een certificaat kiest.
|
||||
Wie geen reverse proxy draait, of twee kandidaten heeft, zag dus niets en kon
|
||||
niets. De symptomen wezen ergens anders heen: de app_proxy meldde alleen dat de
|
||||
server niet te bereiken was.
|
||||
|
||||
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
|
||||
de compose pas in /var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf
|
||||
haalt die map op met een jokerteken, en een jokerteken dat niets matcht is voor
|
||||
nginx geen fout. Eén iemand die dat blok "netjes" terugzet in nginx.conf en de
|
||||
klem is terug, zonder dat een van de andere toetsen iets merkt.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_server_start_zonder_certificaat.py
|
||||
|
||||
Er staan inmiddels een paar toetsen bij die niet over het starten gaan maar wel
|
||||
over 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,
|
||||
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.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import os # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate")
|
||||
|
||||
NGINX_CONF = os.path.join(APP, "nginx.conf.template")
|
||||
STREAM_CONF = os.path.join(APP, "stream.conf.template")
|
||||
COMPOSE = os.path.join(APP, "docker-compose.yml")
|
||||
MANIFEST = os.path.join(APP, "umbrel-app.yml")
|
||||
PAGINA = os.path.join(APP, "index.html.template")
|
||||
|
||||
|
||||
class Uitslag:
|
||||
def __init__(self):
|
||||
self.goed = 0
|
||||
self.fout = []
|
||||
|
||||
def check(self, naam, gelukt, uitleg=""):
|
||||
if gelukt:
|
||||
self.goed += 1
|
||||
else:
|
||||
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||
|
||||
def rapport(self):
|
||||
print()
|
||||
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||
for f in self.fout:
|
||||
print(" FOUT: " + f)
|
||||
return 0 if not self.fout else 1
|
||||
|
||||
|
||||
def lees(pad):
|
||||
with open(pad, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def zonder_commentaar(tekst):
|
||||
"""De regels die nginx daadwerkelijk uitvoert.
|
||||
|
||||
Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die
|
||||
noemt juist de dingen die er niet meer mogen staan.
|
||||
"""
|
||||
regels = []
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if not kaal or kaal.startswith("#"):
|
||||
continue
|
||||
regels.append(kaal)
|
||||
return regels
|
||||
|
||||
|
||||
def test_pagina_hangt_niet_aan_een_certificaat(u):
|
||||
"""Geen TLS-directive in nginx.conf, want die blokkeert de start."""
|
||||
regels = zonder_commentaar(lees(NGINX_CONF))
|
||||
|
||||
u.check("nginx.conf heeft geen listen met ssl",
|
||||
not any("listen" in r and "ssl" in r for r in regels),
|
||||
"een 'listen ssl' zonder certificaat laat nginx niet starten, "
|
||||
"en dan is er ook geen pagina om er een te kiezen")
|
||||
|
||||
u.check("nginx.conf includet cert.conf niet rechtstreeks",
|
||||
not any("cert.conf" in r for r in regels),
|
||||
"een include van een bestand dat er nog niet is, is een startfout")
|
||||
|
||||
u.check("nginx.conf haalt het stream-blok met een jokerteken op",
|
||||
any(r.startswith("include") and "/var/lib/gate/tls/*" in r
|
||||
for r in regels),
|
||||
"zonder jokerteken is een lege map alsnog een startfout")
|
||||
|
||||
u.check("de pagina zelf luistert nog wel",
|
||||
any(r.startswith("listen 80") for r in regels))
|
||||
|
||||
|
||||
def test_stream_blok_staat_apart(u):
|
||||
"""Het TLS-deel is compleet, en het is een template."""
|
||||
u.check("stream.conf.template 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):
|
||||
return
|
||||
|
||||
tekst = lees(STREAM_CONF)
|
||||
regels = zonder_commentaar(tekst)
|
||||
|
||||
u.check("stream.conf luistert op 50022 met ssl",
|
||||
any("listen 50022 ssl" in r for r in regels))
|
||||
u.check("stream.conf includet de cert.conf van de agent",
|
||||
any("include /var/lib/gate/cert.conf" in r for r in regels))
|
||||
u.check("stream.conf includet het log_format",
|
||||
any("include /var/lib/gate/stream-log.conf" in r for r in regels))
|
||||
u.check("stream.conf verwijst naar de backend uit de omgeving",
|
||||
any("proxy_pass ${APP_ELECTRS_NODE_IP}" in r for r in regels))
|
||||
|
||||
# Dezelfde architectuurregel als voor de andere templates: umbreld vult elke
|
||||
# accolade-variabele in, dus een nginx-variabele met een dollarteken wordt
|
||||
# hier stil leeggemaakt. Vandaar dat het log_format uit een ander bestand
|
||||
# komt.
|
||||
losse_dollars = []
|
||||
for regel in regels:
|
||||
pos = regel.find("$")
|
||||
while pos >= 0:
|
||||
if regel[pos + 1:pos + 2] != "{":
|
||||
losse_dollars.append(regel)
|
||||
break
|
||||
pos = regel.find("$", pos + 1)
|
||||
u.check("stream.conf heeft geen nginx-variabelen",
|
||||
not losse_dollars,
|
||||
"de template-invulling maakt die leeg: %r" % losse_dollars[:2])
|
||||
|
||||
|
||||
def test_compose_wacht_niet_op_de_agent(u):
|
||||
"""Het command-blok start nginx altijd, en zet TLS erbij als het kan."""
|
||||
tekst = lees(COMPOSE)
|
||||
regels = zonder_commentaar(tekst)
|
||||
|
||||
u.check("geen wachtlus op cert.conf",
|
||||
not any("! -f /var/lib/gate/cert.conf" in r and "while" in r
|
||||
for r in regels),
|
||||
"dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, "
|
||||
"geen pagina, dus geen manier om een certificaat te kiezen")
|
||||
|
||||
u.check("stream.conf is in de server gemount",
|
||||
any("stream.conf:/etc/nginx/stream.conf" in r for r in regels),
|
||||
"anders is er niets om in /var/lib/gate/tls/ te zetten")
|
||||
|
||||
u.check("het stream-blok wordt weggezet als cert.conf bestaat",
|
||||
any("cp /etc/nginx/stream.conf /var/lib/gate/tls/" in r
|
||||
for r in regels))
|
||||
|
||||
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),
|
||||
"een ingetrokken keuze moet de poort ook echt sluiten")
|
||||
|
||||
u.check("een mislukte herlading breekt de wachtlus niet af",
|
||||
any("if ! nginx -s reload" in r for r in regels),
|
||||
"onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer "
|
||||
"een wijziging op")
|
||||
|
||||
|
||||
def test_uploadpad(u):
|
||||
"""De drie dingen buiten de agent die uploaden mogelijk maken.
|
||||
|
||||
Alle drie kunnen ze stil wegvallen: een mount die weer `:ro` wordt geeft een
|
||||
foutmelding pas als iemand iets uploadt, en een te kleine limiet in nginx
|
||||
geeft een 413 die niets uitlegt.
|
||||
"""
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
nginx = zonder_commentaar(lees(NGINX_CONF))
|
||||
|
||||
eigen = [r for r in compose if "/certs/own" in r and r.startswith("-")]
|
||||
u.check("de eigen certificaatmap is twee keer gemount",
|
||||
len(eigen) == 2, "gevonden: %r" % eigen)
|
||||
u.check("en precies één daarvan is beschrijfbaar",
|
||||
len([r for r in eigen if not r.endswith(":ro")]) == 1,
|
||||
"zonder :ro kan de agent niets neerzetten, met :ro overal ook niet: %r"
|
||||
% eigen)
|
||||
|
||||
u.check("GATE_UPLOAD_DIR staat in de omgeving van de agent",
|
||||
any(r.startswith("GATE_UPLOAD_DIR:") for r in compose),
|
||||
"leeg of afwezig betekent: uploaden staat uit")
|
||||
|
||||
u.check("nginx heeft een eigen locatie voor de upload",
|
||||
any("location = /api/certificate/upload" in r for r in nginx))
|
||||
# De limiet moet groter zijn dan die van /api/, anders kapt nginx het verzoek
|
||||
# af voordat de agent er iets over kan zeggen.
|
||||
limieten = []
|
||||
for regel in nginx:
|
||||
if regel.startswith("client_max_body_size"):
|
||||
limieten.append(regel.rstrip(";").split()[-1])
|
||||
u.check("en een grotere limiet dan het gewone api-pad",
|
||||
len(limieten) == 2 and limieten[0] != limieten[1],
|
||||
"gevonden limieten: %r" % limieten)
|
||||
|
||||
|
||||
def test_verbindingsteller(u):
|
||||
"""De teller in de nginx-container, en de poort in hex.
|
||||
|
||||
Hoort hier omdat het om hetzelfde `command`-blok gaat. Het gevaarlijke deel
|
||||
is de hex: de teller zoekt in /proc/net/tcp op de poort in hexadecimale vorm,
|
||||
en verandert de TLS-poort ooit, dan telt hij stil nul. Een teller die altijd
|
||||
nul zegt, ziet eruit als "geen wallet verbonden" en niet als een fout.
|
||||
"""
|
||||
tekst = lees(COMPOSE)
|
||||
regels = zonder_commentaar(tekst)
|
||||
|
||||
teller = [r for r in regels if "/proc/net/tcp" in r]
|
||||
u.check("de teller leest /proc/net/tcp", bool(teller),
|
||||
"dat is de enige plek waar de open verbindingen van deze container "
|
||||
"staan; de agent zit in een andere netwerk-namespace")
|
||||
|
||||
u.check("en schrijft het aantal weg voor de agent",
|
||||
any("/var/lib/gate/sessions" in r for r in regels))
|
||||
|
||||
# De poort uit de omgeving van de agent is de waarheid; de teller moet
|
||||
# dezelfde poort in hex zoeken.
|
||||
poorten = [r for r in regels if r.startswith("GATE_TLS_PORT:")]
|
||||
u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1,
|
||||
"gevonden: %r" % poorten)
|
||||
if not poorten or not teller:
|
||||
return
|
||||
|
||||
poort = int(poorten[0].split(":", 1)[1].strip().strip('"'))
|
||||
hexpoort = "%X" % poort
|
||||
# Over de regels heen zoeken, want de awk-regel staat achter een
|
||||
# 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),
|
||||
(":" + hexpoort) in " ".join(regels),
|
||||
"nergens in het command-blok staat :%s, dus telt de teller nul"
|
||||
% hexpoort)
|
||||
|
||||
|
||||
def test_data_onder_data(u):
|
||||
"""Alles wat de app zelf schrijft staat onder `data/`.
|
||||
|
||||
De conventie van de officiele appstore, afgelezen aan echte apps: electrs
|
||||
mount `${APP_DATA_DIR}/data/electrs`, mempool `${APP_DATA_DIR}/data`. Deze app
|
||||
had `runtime/` en `certs/` naast de templates staan. Verplaatst op 20-08-2026
|
||||
met het oog op publicatie.
|
||||
|
||||
Waarom er een toets op staat: bij de volgende mount die iemand toevoegt is dit
|
||||
precies het detail dat je vergeet, en niets gaat er stuk van. Het valt pas op
|
||||
bij het inleveren.
|
||||
"""
|
||||
regels = zonder_commentaar(lees(COMPOSE))
|
||||
mounts = [r[1:].strip() for r in regels
|
||||
if r.startswith("-") and "${APP_DATA_DIR}" in r]
|
||||
u.check("er zijn mounts uit de app-datamap", bool(mounts))
|
||||
|
||||
fout = []
|
||||
for mount in mounts:
|
||||
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/"):
|
||||
fout.append(host)
|
||||
u.check("elke map die de app zelf vult staat onder data/",
|
||||
not fout, "deze niet: %r" % fout)
|
||||
|
||||
u.check("de gedeelde toestand staat onder data/",
|
||||
any("${APP_DATA_DIR}/data/runtime:" in m for m in mounts))
|
||||
u.check("de eigen certificaatmap staat onder data/",
|
||||
any("${APP_DATA_DIR}/data/certs:" in m for m in mounts))
|
||||
|
||||
# En het manifest moet die paden ook noemen, anders wijst backupIgnore naar
|
||||
# iets wat niet bestaat en gaat de sessielog alsnog mee in elke back-up.
|
||||
manifest = lees(MANIFEST)
|
||||
u.check("backupIgnore staat in het manifest", "backupIgnore:" in manifest)
|
||||
for pad in ("data/runtime/stream.log", "data/runtime/status.json"):
|
||||
u.check("backupIgnore noemt %s" % pad, pad in manifest)
|
||||
|
||||
|
||||
# De volgorde die de packaging-documentatie van de officiele appstore voorschrijft.
|
||||
# Wat de spec niet noemt, zoals icon en backupIgnore, hoort daarachter en niet
|
||||
# ertussen: dan is de kop van het manifest letterlijk goed.
|
||||
MANIFEST_ORDE = [
|
||||
"manifestVersion", "id", "category", "name", "version", "tagline",
|
||||
"description", "releaseNotes", "developer", "website", "dependencies",
|
||||
"repo", "support", "port", "gallery", "path",
|
||||
]
|
||||
|
||||
|
||||
def manifest_velden():
|
||||
"""De veldnamen op het hoogste niveau, in de volgorde van het bestand."""
|
||||
velden = []
|
||||
for regel in lees(MANIFEST).splitlines():
|
||||
if not regel or regel[0] in " #-":
|
||||
continue
|
||||
if ":" in regel:
|
||||
velden.append(regel.split(":", 1)[0])
|
||||
return velden
|
||||
|
||||
|
||||
def test_manifest_volgorde(u):
|
||||
"""De velden staan in de voorgeschreven volgorde.
|
||||
|
||||
Geen smaak: de officiele appstore schrijft deze volgorde voor. Een toets
|
||||
hierop omdat een nieuw veld standaard onderaan of middenin belandt, en er
|
||||
niets van stukgaat; het valt pas op bij het inleveren.
|
||||
"""
|
||||
velden = manifest_velden()
|
||||
kern = [v for v in velden if v in MANIFEST_ORDE]
|
||||
|
||||
ontbreekt = [v for v in MANIFEST_ORDE if v not in velden]
|
||||
u.check("alle voorgeschreven velden staan in het manifest",
|
||||
not ontbreekt, "mist: %r" % ontbreekt)
|
||||
u.check("en in de voorgeschreven volgorde",
|
||||
kern == MANIFEST_ORDE, "de volgorde is nu: %r" % kern)
|
||||
|
||||
# De extra's horen achter de voorgeschreven reeks. Zo is de kop van het
|
||||
# bestand letterlijk op orde en hoeft er bij inlevering alleen iets weg.
|
||||
if kern == MANIFEST_ORDE and velden:
|
||||
laatste_kern = velden.index(MANIFEST_ORDE[-1])
|
||||
extra_ervoor = [v for v in velden[:laatste_kern] if v not in MANIFEST_ORDE]
|
||||
u.check("en wat de spec niet noemt staat erachter",
|
||||
not extra_ervoor, "deze staan ertussen: %r" % extra_ervoor)
|
||||
|
||||
u.check("de lege standaard-inloggegevens zijn eruit",
|
||||
"defaultUsername" not in velden and "defaultPassword" not in velden,
|
||||
"deze app heeft geen eigen inlog, dus een leeg veld zegt niets")
|
||||
|
||||
|
||||
def test_releasenotes_hebben_een_geschiedenis(u):
|
||||
"""Eén verhaal over deze versie, dan één regel per eerdere versie.
|
||||
|
||||
Deze toets bestaat om een echte fout: drie versies achter elkaar kwam er een
|
||||
nieuwe kop bovenop de release notes terwijl de oude tekst eronder bleef staan.
|
||||
In 0.0.9 stond er daardoor drie keer "Earlier releases" en twee keer dezelfde
|
||||
regel over 0.0.4. Dat is zichtbare tekst in de appstore, en niemand leest zijn
|
||||
eigen release notes nog een keer na.
|
||||
"""
|
||||
tekst = lees(MANIFEST)
|
||||
u.check("er staat precies een keer een geschiedenis-kop",
|
||||
tekst.count("Earlier releases:") == 1,
|
||||
"gevonden: %d keer" % tekst.count("Earlier releases:"))
|
||||
|
||||
# Elke eerdere versie mag maar een keer genoemd worden.
|
||||
dubbel = []
|
||||
for versie in ("0.0.4", "0.0.5", "0.0.6", "0.0.7", "0.0.8"):
|
||||
aantal = tekst.count("\n " + versie + " ")
|
||||
if aantal > 1:
|
||||
dubbel.append((versie, aantal))
|
||||
u.check("en geen versie wordt twee keer beschreven",
|
||||
not dubbel, "dubbel: %r" % dubbel)
|
||||
|
||||
|
||||
def test_tagline_is_overal_dezelfde(u):
|
||||
"""De regel onder de app-naam staat op twee plekken en moet gelijk zijn.
|
||||
|
||||
Besloten door de gebruiker op 20-08-2026, nadat hij zag dat de winkel en de
|
||||
pagina iets anders zeiden. Eén app, één regel. Deze toets bestaat omdat de
|
||||
twee plekken ver uit elkaar staan: niemand die de tagline in het manifest
|
||||
aanpast, opent daarna de pagina.
|
||||
"""
|
||||
tagline = None
|
||||
for regel in lees(MANIFEST).splitlines():
|
||||
if regel.startswith("tagline:"):
|
||||
tagline = regel.split(":", 1)[1].strip().strip('"')
|
||||
break
|
||||
|
||||
u.check("het manifest heeft een tagline", bool(tagline))
|
||||
if not tagline:
|
||||
return
|
||||
|
||||
u.check("en de pagina gebruikt woordelijk dezelfde regel",
|
||||
tagline in lees(PAGINA),
|
||||
"de pagina noemt hem niet: %r" % tagline)
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
test_pagina_hangt_niet_aan_een_certificaat(u)
|
||||
test_stream_blok_staat_apart(u)
|
||||
test_compose_wacht_niet_op_de_agent(u)
|
||||
test_verbindingsteller(u)
|
||||
test_uploadpad(u)
|
||||
test_data_onder_data(u)
|
||||
test_manifest_volgorde(u)
|
||||
test_releasenotes_hebben_een_geschiedenis(u)
|
||||
test_tagline_is_overal_dezelfde(u)
|
||||
return u.rapport()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
id: whatsnext
|
||||
name: WhatsNext?
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,247 @@
|
||||
services:
|
||||
# umbrelOS genereert deze service zelf; wij vullen alleen in waar hij heen moet
|
||||
# wijzen. De hostnaam heeft de vorm <app-id>_<compose-service>_1.
|
||||
app_proxy:
|
||||
environment:
|
||||
APP_HOST: whatsnext-electrum-gate_server_1
|
||||
APP_PORT: 80
|
||||
|
||||
# De agent. Hij schrijft status.json, leest de certificaten, bevraagt de
|
||||
# Electrum-server en neemt de certificaatkeuze aan.
|
||||
#
|
||||
# Waarom een tweede container en niet een shell-lus in de server hieronder:
|
||||
# dit werk is inmiddels een programma. Een certificaatdatum uitlezen, een
|
||||
# JSON-RPC-verzoek doen, een geschiedenis bijhouden en een keuze valideren zijn
|
||||
# geen dingen die je met openssl en nc aan elkaar knoopt zonder dat het stil
|
||||
# verkeerde antwoorden gaat geven. Bijkomend voordeel: de app hangt niet meer
|
||||
# af van de vraag of die twee gereedschappen in de nginx-image zitten.
|
||||
agent:
|
||||
# TODO (fase 4 van het plan Appstore): pinnen op de multi-arch index-digest,
|
||||
# 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
|
||||
environment:
|
||||
# Het adres van de Electrum-server die de gebruiker in umbrelOS gekozen
|
||||
# heeft. umbrelOS vult dit in op grond van de afhankelijkheid hieronder,
|
||||
# dus dit werkt met Electrs, Fulcrum en ElectrumX.
|
||||
GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}
|
||||
GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}
|
||||
GATE_TLS_PORT: "50022"
|
||||
GATE_INTERVAL: "60"
|
||||
GATE_API_PORT: "8000"
|
||||
GATE_STATE_DIR: /var/lib/gate
|
||||
# Bron-naam=map. Een bron die niet gemount is, bestaat niet en wordt
|
||||
# overgeslagen; dat is de normale toestand voor een reverse proxy die de
|
||||
# gebruiker niet draait.
|
||||
GATE_CERT_SOURCES: "Zoraxy=/certs/zoraxy,Own folder=/certs/own"
|
||||
# De enige beschrijfbare bron, en dus de map waarin een upload van de
|
||||
# pagina landt. Expliciet en niet afgeleid uit de lijst hierboven: welke
|
||||
# bron beschrijfbaar is, hoort te staan naast de mount die dat toestaat.
|
||||
# Leeg zetten schakelt uploaden uit.
|
||||
GATE_UPLOAD_DIR: /certs/own
|
||||
PYTHONUNBUFFERED: "1"
|
||||
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
|
||||
# gekozen certificaat-id. Beide containers zitten hierin.
|
||||
#
|
||||
# Onder data/ en niet naast de templates, want dat is wat andere apps doen:
|
||||
# electrs mount ${APP_DATA_DIR}/data/electrs, mempool ${APP_DATA_DIR}/data.
|
||||
# Verplaatst op 20-08-2026 met het oog op publicatie in de officiele store.
|
||||
- ${APP_DATA_DIR}/data/runtime:/var/lib/gate
|
||||
# De certificaatbronnen. Zoraxy alleen lezen: dat zijn de certificaten van
|
||||
# een andere app en die raakt deze app niet aan.
|
||||
- ${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs:/certs/zoraxy:ro
|
||||
# De eigen map is de enige die beschrijfbaar is, want hier landt een upload
|
||||
# van de pagina. Alleen de agent; de nginx-container houdt hem alleen-lezen,
|
||||
# want die hoeft er nooit iets neer te zetten.
|
||||
- ${APP_DATA_DIR}/data/certs:/certs/own
|
||||
#
|
||||
# Nginx Proxy Manager hoort hier als derde bron bij en staat er bewust nog
|
||||
# niet in. Het pad hieronder is een gok en niet geverifieerd, en een
|
||||
# bind-mount naar een pad dat niet bestaat laat Docker het aanmaken. Dat zou
|
||||
# een lege maphierarchie neerzetten in de app-data van een app die
|
||||
# misschien niet eens geinstalleerd is, en die rommel blijft daar staan.
|
||||
# Eerst op de Umbrel controleren waar NPM zijn certificaten neerzet, dan
|
||||
# deze regel aanzetten en de naam toevoegen aan GATE_CERT_SOURCES.
|
||||
#
|
||||
# - ${UMBREL_ROOT}/app-data/nginx-proxy-manager/data/letsencrypt/live:/certs/npm:ro
|
||||
command:
|
||||
- python
|
||||
- /app/agent.py
|
||||
|
||||
server:
|
||||
image: nginx:alpine
|
||||
restart: on-failure
|
||||
# De agent moet er zijn voordat nginx start, want de proxy_pass naar
|
||||
# http://agent:8000 wordt bij het starten opgelost en een onbekende naam
|
||||
# laat nginx afbreken. Op de cert.conf van de agent wordt niet gewacht: de
|
||||
# pagina komt hoe dan ook omhoog, zie het command-blok hieronder.
|
||||
depends_on:
|
||||
- agent
|
||||
ports:
|
||||
# De TLS-poort voor Electrum-wallets. Dit is de enige poort die deze app
|
||||
# zelf publiceert; de web-UI loopt via app_proxy.
|
||||
#
|
||||
# 50022 en niet de conventionele 50002, omdat Fulcrum die op de host
|
||||
# bezet: met Fulcrum als backend zou de container niet starten, en dat is
|
||||
# precies het omschakelen dat deze app moet ondersteunen. De poort naar
|
||||
# buiten is toch al een andere, want die staat in de router doorgestuurd.
|
||||
# Beslist 19-08-2026, open punt 1 van het plan Configuratie.
|
||||
- "50022:50022"
|
||||
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
|
||||
# status.json, en schrijft de sessielog.
|
||||
- ${APP_DATA_DIR}/data/runtime:/var/lib/gate
|
||||
# De certificaten zelf moeten ook hier gemount zijn: de agent bepaalt
|
||||
# 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
|
||||
- ${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;'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
# nginx-configuratie voor Electrum Gate.
|
||||
#
|
||||
# Dit bestand is een template en dat is bewust. umbrelOS ververst bij een update
|
||||
# alleen een whitelist van bestanden, en *.template staat daarin; een gewoon
|
||||
# bestand naast de compose zou na een update ongewijzigd blijven staan. Daarnaast
|
||||
# vult umbrelOS bij elke start de omgevingsvariabelen hieronder in en schrijft het
|
||||
# resultaat naast dit bestand weg als nginx.conf.
|
||||
#
|
||||
# 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;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
# ── De web-UI ────────────────────────────────────────────────────────────────
|
||||
# Wordt niet rechtstreeks gepubliceerd: umbrelOS zet er zijn eigen app_proxy
|
||||
# voor, en die verwijst naar poort 80 van deze container.
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log off;
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# status.json wordt door de agent in de gedeelde map geschreven, niet in
|
||||
# de webroot. Geen cache: de pagina vraagt hem juist op om te zien hoe
|
||||
# oud de gegevens zijn.
|
||||
location = /status.json {
|
||||
alias /var/lib/gate/status.json;
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
# De API van de agent. Het enige pad waarlangs iets van buiten de
|
||||
# instellingen van de app raakt, en dat is de certificaatkeuze.
|
||||
#
|
||||
# Waarom dit te verantwoorden is op een app waarvan de kleinheid het punt
|
||||
# is: het pad hangt achter de app_proxy van umbrelOS, die er zijn eigen
|
||||
# inlog voor zet, de TLS-poort staat er los van, en de agent accepteert
|
||||
# alleen een certificaat-id die hij zelf in de gemounte mappen gevonden
|
||||
# heeft. Het ergste wat een geslaagde aanroep doet is een ander, ook
|
||||
# bestaand, certificaat kiezen.
|
||||
#
|
||||
# Geen proxy_set_header hier, en dat is geen vergetelheid: die zouden een
|
||||
# nginx-variabele vragen, en die haalt de template-invulling weg. De
|
||||
# agent heeft ze niet nodig.
|
||||
location /api/ {
|
||||
proxy_pass http://agent:8000;
|
||||
client_max_body_size 1k;
|
||||
}
|
||||
|
||||
# Het uploaden van een certificaat, en het enige pad met een grotere
|
||||
# limiet: een sleutel plus keten haalt de 1k van hierboven ruim. Een
|
||||
# exacte match, dus deze regel wint van /api/ hierboven en de grotere
|
||||
# limiet geldt nergens anders. De agent kapt zelf ook af, op 96k; deze
|
||||
# limiet is de eerste zeef en niet de enige.
|
||||
location = /api/certificate/upload {
|
||||
proxy_pass http://agent:8000;
|
||||
client_max_body_size 96k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── De TLS-terminatie ────────────────────────────────────────────────────────
|
||||
# Die staat niet hier maar in stream.conf.template, en dat is geen ordening maar
|
||||
# de reparatie van een klem. nginx weigert te starten als een 'listen ... ssl'
|
||||
# geen certificaat heeft; stond het stream-blok hier, dan kwam met dat blok ook
|
||||
# de pagina hierboven niet omhoog zolang de agent geen certificaat gekozen had.
|
||||
# En 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
|
||||
# zodra cert.conf bestaat, en haalt het weg als dat niet zo is. Een include met
|
||||
# een jokerteken die nergens op uitkomt is voor nginx geen fout, dus zonder
|
||||
# certificaat start deze server zonder poort 50022 en met een pagina die
|
||||
# vertelt waarom.
|
||||
include /var/lib/gate/tls/*.conf;
|
||||
@@ -0,0 +1,61 @@
|
||||
# De TLS-terminatie van Electrum Gate, als los bestand, en dat is de hele truc.
|
||||
#
|
||||
# nginx weigert te starten als een 'listen ... ssl' geen certificaat heeft, en
|
||||
# welk certificaat dat wordt weet alleen de agent. Zolang dit blok in nginx.conf
|
||||
# stond, kwam de server dus niet omhoog voordat er een keuze lag, en daarmee ook
|
||||
# de web-UI niet: precies de pagina waarop die keuze gemaakt wordt. Een gebruiker
|
||||
# zonder Zoraxy, of met twee kandidaten, zag daardoor helemaal niets en had geen
|
||||
# enkele weg vooruit. Zo aangetroffen in 0.0.3.
|
||||
#
|
||||
# Nu staat het blok hier, en het command-blok van docker-compose.yml zet dit
|
||||
# bestand pas in /var/lib/gate/tls/ zodra de agent cert.conf geschreven heeft.
|
||||
# nginx.conf haalt die map op met een jokerteken, en een jokerteken dat niets
|
||||
# matcht is voor nginx geen fout. Geen certificaat betekent dus: geen
|
||||
# stream-blok, geen poort 50022, maar wel een werkende pagina die vertelt wat
|
||||
# eraan mankeert.
|
||||
#
|
||||
# Dit is een template om dezelfde reden als de andere: bij een update ververst
|
||||
# umbrelOS alleen een whitelist, en *.template staat daarin. Let daarom ook hier
|
||||
# op het dollarteken: elke ${...} wordt bij het starten ingevuld, dus geen
|
||||
# nginx-variabelen in dit bestand. Het log_format, dat er niet zonder kan, staat
|
||||
# in stream-log.conf en wordt door het command-blok weggeschreven.
|
||||
|
||||
stream {
|
||||
# Het log_format en de access_log, geschreven door het command-blok in
|
||||
# docker-compose.yml.
|
||||
include /var/lib/gate/stream-log.conf;
|
||||
|
||||
server {
|
||||
# 50022 en niet 50002; de reden staat bij de ports-regel in
|
||||
# docker-compose.yml.
|
||||
listen 50022 ssl;
|
||||
|
||||
# ssl_certificate en ssl_certificate_key, geschreven door de agent 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
|
||||
# in dit bestand. Dat dit bestand bestaat betekent dat cert.conf er is:
|
||||
# het command-blok zet het er pas dan neer.
|
||||
#
|
||||
# nginx wil de VOLLEDIGE keten in ssl_certificate, dus het bestand van
|
||||
# Zoraxy kan er ongewijzigd in. Splitsen in een servercertificaat en een
|
||||
# CA-deel, zoals de oude stunnel-opzet deed, is hier juist fout.
|
||||
include /var/lib/gate/cert.conf;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
# Een Electrum-wallet houdt een langlopende verbinding open die het
|
||||
# grootste deel van de tijd stil is. De standaardwaarde van
|
||||
# proxy_timeout is tien minuten, en die verbreekt zo'n verbinding dus
|
||||
# zonder aanleiding; de wallet merkt dat pas als hij iets wil en meldt
|
||||
# dan dat er geen data is. stunnel hanteerde hier twaalf uur, en dat
|
||||
# verschil is bij de overstap naar nginx over het hoofd gezien.
|
||||
proxy_timeout 12h;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
# Houdt de verbinding naar de backend levend en laat een verbroken
|
||||
# verbinding sneller opvallen dan pas bij het volgende verzoek.
|
||||
proxy_socket_keepalive on;
|
||||
|
||||
proxy_pass ${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
# De veldvolgorde hieronder is niet vrij: de packaging-documentatie van de
|
||||
# officiele appstore schrijft hem voor. Zie de tabel in
|
||||
# Docs/Referenties/Umbrel-appstore-spec.md, en de toets in
|
||||
# tests/test_server_start_zonder_certificaat.py die hem vasthoudt.
|
||||
#
|
||||
# Wat de spec niet noemt, staat onderaan dit bestand en niet ertussen. Dan is de
|
||||
# voorgeschreven kop letterlijk goed en hoeft er bij inlevering alleen iets weg.
|
||||
manifestVersion: 1
|
||||
id: whatsnext-electrum-gate
|
||||
category: bitcoin
|
||||
name: Electrum Gate
|
||||
version: "0.0.15"
|
||||
tagline: Your own node from anywhere, without waiting for Tor
|
||||
description: >-
|
||||
The privacy win is already yours: you run the Electrum server. A public one gets asked for
|
||||
the history of every address in your wallet, and that tells it which addresses and which
|
||||
balance belong to one person. Your own server never reports back.
|
||||
|
||||
|
||||
What is left is a trade. Tor is the more private way in, and every wallet below speaks it.
|
||||
But it adds hundreds of milliseconds to every request, it drops when a phone sleeps or
|
||||
changes network, and plenty of networks block it outright. Electrum Gate is the other side
|
||||
of that trade: a TLS front door on your node. Fast, no fingerprint to type over, and it
|
||||
works on any network you happen to be on.
|
||||
|
||||
|
||||
It reuses the Let's Encrypt certificate your reverse proxy already manages, so there is
|
||||
nothing to request and nothing to renew. A renewal is picked up on its own, without
|
||||
dropping connections that are already open. You can also upload a certificate you manage
|
||||
yourself, if you do not run a reverse proxy on this machine.
|
||||
|
||||
|
||||
Nothing in the middle. The connection runs from your wallet straight to your own node,
|
||||
encrypted with a certificate you already own, for a domain you already control. There is no
|
||||
account to create, no tunnel service that terminates your traffic along the way, and no
|
||||
client to install on every device you use: the wallets below already speak TLS, they only
|
||||
need an address. What it does ask of you is one forwarded port on your router.
|
||||
|
||||
|
||||
Useful for wallets connecting from outside your home: Trezor Suite, Electrum, Sparrow,
|
||||
BlueWallet, Nunchuk, Blockstream and BitBoxApp. Apps on the Umbrel itself do not need
|
||||
it, they already reach the Electrum server directly.
|
||||
|
||||
|
||||
The dashboard hands you the exact line each of those wallets asks for, and shows block
|
||||
height, certificate expiry and whether your Electrum server is answering. Electrs, Fulcrum
|
||||
and ElectrumX all work, switchable in the umbrelOS settings.
|
||||
|
||||
|
||||
What it does not do: hide that the traffic exists. Your domain is public in the certificate
|
||||
transparency logs. If that matters more to you than speed, use Tor instead.
|
||||
|
||||
# Eén verhaal over deze versie, dan één regel per eerdere versie. Niet meer dan
|
||||
# dat, en dat staat hier omdat het drie versies achter elkaar fout ging: bij elke
|
||||
# release kwam er een nieuwe kop bovenop terwijl de oude tekst eronder bleef
|
||||
# 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.
|
||||
releaseNotes: >-
|
||||
The app store this app comes from has moved to a new address, and every link in this listing now
|
||||
points there. The app itself is unchanged. If you added the store at its old address, remove it in
|
||||
umbrelOS and add the new one.
|
||||
|
||||
|
||||
The move is because the store now carries a second app, so naming it after this one no longer made
|
||||
sense.
|
||||
|
||||
|
||||
Earlier releases:
|
||||
|
||||
|
||||
0.0.14 stopped calling a scan from the internet a refusal. Those now read "probe", in grey, and red
|
||||
is kept for a session that did carry traffic and then broke. The panels also got more room between
|
||||
them.
|
||||
|
||||
|
||||
0.0.13 made the certificate panel and the activity log grow and shrink together, so opening the
|
||||
upload section no longer leaves a gap under the button.
|
||||
|
||||
|
||||
0.0.12 tightened the spacing in the certificate panel, and dropped a line claiming certificates
|
||||
are read from Nginx Proxy Manager.
|
||||
|
||||
|
||||
0.0.11 lined up the copy buttons along the right edge on a phone, and moved the upload section
|
||||
above the certificate picker.
|
||||
|
||||
|
||||
0.0.10 made the dashboard work on a phone, and dropped the tile counting the days left on your
|
||||
certificate.
|
||||
|
||||
|
||||
0.0.9 moved everything the app writes into the "data" folder inside the app folder, which is
|
||||
where the App Store expects an app to keep its state.
|
||||
|
||||
|
||||
0.0.8 gave the dashboard the same line as this listing, instead of describing TLS as a means.
|
||||
|
||||
|
||||
0.0.7 added uploading your own certificate, with the key checked against the certificate before
|
||||
anything is stored.
|
||||
|
||||
|
||||
0.0.6 made a connected wallet visible while it is connected, turned the certificate picker into
|
||||
a dropdown, and rewrote the session line in the activity log.
|
||||
|
||||
|
||||
0.0.5 shortened the message you get when more than one certificate was found.
|
||||
|
||||
|
||||
0.0.4 fixed an install that showed no dashboard at all.
|
||||
|
||||
developer: "WhatsNext?"
|
||||
website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
|
||||
dependencies:
|
||||
- electrs
|
||||
repo: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
|
||||
support: https://sc.kamenier-hamer.nl/sysop/UmbrelApps/issues
|
||||
# De poort waarop umbrelOS de web-UI van deze app aanbiedt, niet de TLS-poort.
|
||||
# Die staat in docker-compose.yml en is 50022.
|
||||
port: 3850
|
||||
# Leeg laten bij een nieuw pakket; de plaatjes doet het store-team zelf.
|
||||
gallery: []
|
||||
path: ""
|
||||
submitter: "WhatsNext?"
|
||||
submission: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
|
||||
|
||||
# ── Hieronder staat wat de voorgeschreven volgorde niet noemt ────────────────
|
||||
#
|
||||
# defaultUsername en defaultPassword stonden hier met een lege waarde en zijn
|
||||
# eruit. Deze app heeft geen eigen inlog; de app_proxy van umbrelOS zet er zijn
|
||||
# eigen voor. Een leeg veld zegt niets en suggereert dat er iets te vullen valt.
|
||||
|
||||
# Wat niet in de back-up hoeft. Paden zijn relatief aan de app-datamap en
|
||||
# ondersteunen een jokerteken; nagekeken in app.ts van umbreld op 20-08-2026.
|
||||
#
|
||||
# Alleen de twee die groeien of elke minuut veranderen. Wat er bewust NIET bij
|
||||
# staat is data/runtime/config: daar zit de certificaatkeuze van de gebruiker, en
|
||||
# die is het enige in deze map dat niet opnieuw te bedenken is.
|
||||
backupIgnore:
|
||||
- data/runtime/stream.log
|
||||
- data/runtime/status.json
|
||||
|
||||
# Het icoon hoort bij een eigen store en moet bij inlevering in de officiele
|
||||
# store juist weg: die host iconen apart. Daarom staat het als laatste, want dan
|
||||
# is dit de enige regel die eruit moet en blijft de rest letterlijk op orde.
|
||||
icon: https://sc.kamenier-hamer.nl/sysop/UmbrelApps/raw/branch/main/whatsnext-electrum-gate/icon.png
|
||||
Reference in New Issue
Block a user