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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Harmen
2026-09-07 20:20:42 +02:00
co-authored by Claude Fable 5.1
parent 7b3804df89
commit 973b24a23f
25 changed files with 981 additions and 410 deletions
File diff suppressed because it is too large Load Diff
+42 -170
View File
@@ -6,21 +6,31 @@ services:
APP_HOST: whatsnext-electrum-gate_server_1
APP_PORT: 80
# Beide containers draaien dezelfde image, gebouwd door tools/electrum-gate/
# build.sh. Wat erin zit staat in de Dockerfile daar: nginx met de pagina en het
# stream-blok, en python3 met de agent. Tot 0.0.29 stonden die bestanden hier
# als *.template en draaiden ze op python:3-alpine en nginx:alpine; waarom dat
# veranderd is staat in het plan Eigenimage, PLAN.md §3.
#
# De tag hoort gelijk te zijn aan VERSION in build.sh, en er hoort een digest
# achter zodra de image geduwd is. Let op de valstrik uit KNOWLEDGE.md: een
# oude digest onder een nieuwe tag levert stilzwijgend de oude image, dus laat
# de digest er even van af zolang de nieuwe niet geduwd is.
# De agent. Hij schrijft status.json, leest de certificaten, bevraagt de
# 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.
# Waarom een tweede container en niet een lus in de server hieronder: dit werk
# is 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.
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
image: sc.kamenier-hamer.nl/sysop/electrum-gate:0.1.0
restart: on-failure
command:
- python3
- /app/agent.py
environment:
# Het adres van de Electrum-server die de gebruiker in umbrelOS gekozen
# heeft. umbrelOS vult dit in op grond van de afhankelijkheid hieronder,
@@ -28,6 +38,10 @@ services:
GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}
GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}
GATE_TLS_PORT: "50022"
# De versie uit het manifest, voor de kop van de pagina. Die stond tot
# 0.0.29 rechtstreeks in de pagina, ingevuld door umbreld; nu de pagina in
# de image zit, geeft de agent hem door via status.json.
GATE_APP_VERSION: ${APP_VERSION}
# De zelfcontrole: de agent verbindt met de TLS-poort van de server
# hieronder en maakt de handdruk af. Niet localhost: dat is een andere
# container en dus een andere netwerk-namespace.
@@ -57,19 +71,13 @@ services:
# 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.
# Onder data/, 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.
@@ -88,17 +96,18 @@ services:
# 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
# nginx: de pagina op poort 80 achter de app_proxy, en de TLS-poort 50022 zodra
# de agent een certificaat gekozen heeft. Het startscript is entrypoint.sh in de
# image; tot 0.0.29 stond dat hier als command-blok van ruim honderd regels, met
# elk dollarteken verdubbeld. Wat het doet staat daar uitgelegd.
server:
image: nginx:alpine
image: sc.kamenier-hamer.nl/sysop/electrum-gate:0.1.0
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.
# De agent moet er zijn voordat nginx start, want de proxy_pass naar de agent
# 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 entrypoint.sh.
depends_on:
- agent
ports:
@@ -111,22 +120,13 @@ services:
# 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"
environment:
# Dezelfde twee als bij de agent. entrypoint.sh vult ze in het stream-blok
# in, want nginx kan in een proxy_pass geen omgevingsvariabele lezen. Tot
# 0.0.29 deed umbreld dat via stream.conf.template.
GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}
GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}
volumes:
# 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
@@ -134,131 +134,3 @@ services:
# 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;'
File diff suppressed because it is too large Load Diff
@@ -1,97 +0,0 @@
# 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.
#
# De volledige containernaam en niet de servicenaam 'agent'. Dat stond er
# tot 0.0.23 wel, en het werkte zolang deze app de enige was met een
# service van die naam. Sinds Evolu Relay er is, is dat niet meer waar:
# beide apps zitten op hetzelfde Docker-netwerk, allebei met een 'agent'
# op poort 8000, en Docker verdeelt de naam dan over de twee. Op
# 28-08-2026 gemeten op het apparaat: van tien verzoeken kwamen er vijf
# bij de verkeerde app uit. De vorm <app-id>_<service>_1 is uniek per app;
# het is dezelfde les die in de compose al stond over de naam 'server'.
location /api/ {
proxy_pass http://whatsnext-electrum-gate_agent_1: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://whatsnext-electrum-gate_agent_1: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;
@@ -1,61 +0,0 @@
# 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};
}
}
+11 -1
View File
@@ -9,7 +9,7 @@ manifestVersion: 1
id: whatsnext-electrum-gate
category: bitcoin
name: Electrum Gate
version: "0.0.29"
version: "0.1.0"
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
@@ -56,6 +56,16 @@ description: >-
# 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 now ships as its own image instead of mounting its program files into two stock images. Nothing
changes in what it does; what changes is where the code lives. The app data folder now holds only your
data: the certificate you chose, the one you uploaded, and the status the page reads. The startup logic
that used to be a hundred lines of shell inside the compose file is a plain script inside the image, and
the app icon updates along with everything else, which it could not before.
Everything in 0.0.29:
The glow behind the page is gone. It arrived in 0.0.25 and it did what it was meant to do, but a status
page is something you glance at, and a coloured wash over the whole window is a lot of decoration for a
glance. The panels keep their edge, their shadow and their top light, so they still read as panels; only