Het pakket omgebouwd: kale relay, statuspagina, poorten omgedraaid

De app-map is niet langer de relay van Trezor met een Postgres en een
quota-manager ernaast, maar de relay van Evolu met onze eigen allowlist eromheen.
Drie containers werden relay, agent en nginx; het wachtwoord en de database zijn
verdwenen.

De poorten zijn omgedraaid en dat is de kern. Tot 0.0.2 hing de relay achter
app_proxy en moest de inlog van umbrelOS dus uit, waardoor een statuspagina net zo
onbeschermd zou zijn als de relay zelf. Nu hangt de pagina daar met de inlog aan,
en publiceert de relay zijn eigen poort 3852 waar Zoraxy met TLS naartoe wijst.
Dat is hetzelfde patroon als Electrum Gate met 50022. Niet 4000 op de host, want
dat is een veelgebruikte poort en een botsing merk je pas als de app niet start.

De agent beslist niets: hij leest wat het relay-proces heeft opgeschreven en legt
opdrachten in een postbus die de relay zelf leegmaakt. Twee processen die in
dezelfde allowlist schrijven is een wedloop die je een keer per jaar treft en dan
niet kunt reproduceren. Hij weigert ook een tweede opdracht zolang de vorige er
nog ligt, want overschrijven zou er stil een laten verdwijnen.

De pagina volgt het ontwerpsysteem van Electrum Gate, zonder de Google
Fonts-verwijzing daaruit: een app op een Umbrel hoort niet te wachten op een
lettertype van buiten. Alles is met stringoptelling geschreven en zonder enig
dollarteken, want umbreld haalt elke template door envsubst en zou een
JavaScript-template-literal stilzwijgend leegmaken.

De image is door de gebruiker gebouwd en geduwd; de compose is gepind op tag plus
digest. Manifest naar 0.2.0, met een beschrijving en releaseNotes die kloppen met
wat er nu draait in plaats van met het vorige pakket.

Wat hier NIET mee bewezen is, en dat is meer dan gebruikelijk: de agent is alleen
op syntaxis gecontroleerd, de pagina is nooit gerenderd, en of een opdracht van de
pagina daadwerkelijk bij het relay-proces aankomt is niet gemeten. Dat kan alleen
op het apparaat en staat als taak.

Tests: alle vier groen (32, 54, 39 en 60 goed, 0 fout).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harmen
2026-08-28 11:22:32 +02:00
co-authored by Claude Opus 5
parent a738f6c0f1
commit 576acd20dd
8 changed files with 963 additions and 147 deletions
@@ -3,6 +3,23 @@
> 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).
## 28-08-2026 (avond) - het pakket is omgebouwd: relay, agent en pagina
De app-map is nu de kale relay met onze allowlist eromheen. De poorten zijn omgedraaid: de statuspagina
hangt achter `app_proxy` mét de inlog van umbrelOS, de relay publiceert 3852 waar Zoraxy met TLS naartoe
gaat wijzen. Postgres, het wachtwoord en de quota-manager zijn eruit.
De gebruiker heeft de image gebouwd en naar het Gitea-register geduwd; de compose is gepind op tag plus
digest. Manifest naar 0.2.0, met een beschrijving en releaseNotes die niet langer over Trezor's relay en
een quota-manager gaan.
**Alles van vanavond is ongetest gedrag.** De agent is alleen op syntaxis gecontroleerd, de pagina is nooit
gerenderd, en of een opdracht van de pagina bij het relay-proces aankomt is niet gemeten. Dat is de
eerstvolgende taak en die kan alleen op het apparaat.
**Geraakt:** de hele app-map, `tools/evolu-relay/build.sh` en de documentatie van dit plan.
**Tests:** alle vier groen (32, 54, 39 en 60 goed, 0 fout).
## 28-08-2026 (later) - het relay-programma staat, met een test erop
`tools/evolu-relay/src/` bevat nu een eigen programma dat `createRelay` uit `@evolu/nodejs` aanroept met
+11 -7
View File
@@ -63,13 +63,17 @@ eromheen, want de compose hangt af van wat dat programma nodig heeft.
- [x] **`tools/evolu-relay/build.sh` omgeschreven (28-08-2026):** bouwt de eigen `Dockerfile` in plaats van
de repo van Trezor te klonen. Git is niet meer nodig, de pin zit nu in `package.json`, en `VERSION`
in het script hoort gelijk te zijn aan `version` in het manifest
- [ ] **De compose omzetten:** pagina achter `app_proxy` met de inlog aan, relay op een eigen `ports:`.
Postgres, wachtwoord en quota-manager eruit. Zie [PLAN.md](PLAN.md) §4h
- [ ] **De statuspagina bouwen**, met de pagina en de agent van Electrum Gate als vertrekpunt en met
hetzelfde ontwerpsysteem (`HomeGit/Docs/website-design-system.html`), maar zonder de Google
Fonts-verwijzing daaruit. Toont of de relay draait, welke eigenaars bekend en toegelaten zijn, de
omvang van de database en het laatste schrijfmoment. Draagt de schakelaar voor nieuwe eigenaars en de
wisknop per eigenaar
- [x] **De compose omgezet (28-08-2026):** pagina achter `app_proxy` mét de inlog, relay op host-poort
3852. Postgres, wachtwoord en quota-manager eruit; drie containers werden relay, agent en nginx. De
image is gepind op tag plus digest
- [x] **De statuspagina gebouwd (28-08-2026):** `index.html.template`, `nginx.conf.template` en
`agent.py.template`, met het ontwerpsysteem van Electrum Gate en zonder de Google Fonts-verwijzing.
Toont of de relay draait, de omvang en het laatste schrijfmoment van de database, het adres dat je in
Suite invult, en de eigenaars in drie lijsten met knoppen om te blokkeren, alsnog toe te laten of te
vergeten. **Nog nooit in een browser gezien**
- [ ] **De pagina en de agent op het apparaat verifiëren.** Alles hierboven is ongetest gedrag: de agent is
alleen op syntaxis gecontroleerd, de pagina is nooit gerenderd, en of de opdrachten daadwerkelijk bij
het relay-proces aankomen is niet gemeten. **Eigenaar: gebruiker**
- [ ] **De app opnieuw installeren in plaats van updaten.** De gebruiker heeft de oude installatie op
28-08-2026 weggehaald; dat is ook de nette weg, want niet elk bestand bereikt een bestaande
installatie via een update
+259
View File
@@ -0,0 +1,259 @@
# ═══════════════════════════════════════════════════════════════════════════════
# De agent van Evolu Relay.
#
# Hij bedient de statuspagina en doet verder niets: lezen wat het relay-proces
# heeft opgeschreven, en opdrachten van de pagina in de postbus leggen.
#
# Waarom de agent niet zelf beslist wie er binnen mag: dat beleid hoort bij het
# proces dat de verbindingen aanneemt, en dat is de relay. Twee processen die in
# dezelfde allowlist schrijven is een wedloop die je een keer per jaar treft en
# dan niet kunt reproduceren. De agent schrijft daarom uitsluitend command.json,
# en het relay-proces past hem toe en ruimt hem op.
#
# LET OP: umbreld haalt dit bestand bij elke start door envsubst. Er mag dus geen
# dollarteken in staan, ook niet in een regex of een tekst. Een accolade-variabele
# die niet bestaat wordt leeg, en dat sloopt Python-code zonder foutmelding.
# tests/test_appstore_vorm.py controleert dat.
# ═══════════════════════════════════════════════════════════════════════════════
import json
import os
import socket
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
STATE_DIR = Path(os.environ.get("RELAY_STATE_DIR", "/var/lib/relay"))
OWNERS_FILE = STATE_DIR / "owners.json"
COMMAND_FILE = STATE_DIR / "command.json"
DATABASE_FILE = STATE_DIR / "evolu-relay.db"
RELAY_HOST = os.environ.get("RELAY_HOST", "")
RELAY_PORT = int(os.environ.get("RELAY_PORT", "4000"))
PUBLIC_PORT = int(os.environ.get("RELAY_PUBLIC_PORT", "3852"))
API_PORT = int(os.environ.get("RELAY_API_PORT", "8000"))
PROBE_INTERVAL = int(os.environ.get("RELAY_PROBE_INTERVAL", "15"))
# Wat de pagina mag vragen. Expliciet en niet doorgeven wat er binnenkomt: dit
# bestand wordt door een ander proces uitgevoerd, en een onbekende actie hoort
# hier te stranden en niet daar.
ALLOWED_ACTIONS = ("set-learning", "block", "allow", "forget")
MAX_BODY_BYTES = 4096
MAX_OWNER_ID_LENGTH = 256
# Door de achtergrondlus bijgewerkt, door de webserver gelezen. Een dict wordt in
# zijn geheel vervangen en nooit ter plekke aangepast, zodat een lezer altijd een
# samenhangend beeld heeft zonder slot.
probe = {"reachable": None, "checked": None}
def relay_reachable():
"""Kan de relay een TCP-verbinding aannemen.
Bewust niet meer dan dat. De relay is een WebSocket-server en antwoordt niet
op een gewoon verzoek; een handdruk nabouwen om de pagina groen te krijgen is
meer code dan het waard is. Wat dit wel uitsluit is de meest voorkomende
storing: het proces is omgevallen.
"""
if not RELAY_HOST:
return None
try:
with socket.create_connection((RELAY_HOST, RELAY_PORT), timeout=3):
return True
except OSError:
return False
def probe_loop():
global probe
while True:
# In zijn geheel vervangen en niet ter plekke aanpassen: een lezer ziet
# dan altijd een samenhangend beeld, zonder dat er een slot nodig is.
probe = {
"reachable": relay_reachable(),
"checked": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
time.sleep(PROBE_INTERVAL)
def read_owners():
"""De allowlist zoals het relay-proces hem heeft achtergelaten."""
try:
with OWNERS_FILE.open("r", encoding="utf-8") as handle:
data = json.load(handle)
except FileNotFoundError:
# Nog nooit geschreven. Dat is de normale toestand vlak na een
# installatie: het relay-proces schrijft pas bij de eerste wijziging.
return {"state": None, "problem": "nog-niet-aangemaakt"}
except (OSError, ValueError):
return {"state": None, "problem": "onleesbaar"}
if not isinstance(data, dict):
return {"state": None, "problem": "onleesbaar"}
return {"state": data, "problem": None}
def database_facts():
try:
stat = DATABASE_FILE.stat()
except OSError:
return {"bytes": None, "modified": None}
return {
"bytes": stat.st_size,
"modified": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(stat.st_mtime)),
}
def build_status():
owners = read_owners()
state = owners["state"] or {}
return {
"relay": {
"reachable": probe["reachable"],
"checked": probe["checked"],
"publicPort": PUBLIC_PORT,
},
"owners": {
"problem": owners["problem"],
# Ontbreekt de staat, dan is 'learning' onbekend en niet 'false'. De
# pagina hoort dat verschil te tonen: onbekend is een reden om te
# kijken, uit is een keuze.
"learning": state.get("learning"),
"allowed": [
entry
for entry in state.get("owners", [])
if isinstance(entry, dict) and entry.get("allowed") is True
],
"blocked": [
entry
for entry in state.get("owners", [])
if isinstance(entry, dict) and entry.get("allowed") is False
],
"rejected": [
entry for entry in state.get("rejected", []) if isinstance(entry, dict)
],
},
"database": database_facts(),
# Ligt er nog een opdracht, dan heeft het relay-proces hem nog niet
# opgepakt. De pagina kan dat tonen in plaats van te doen alsof er niets
# gebeurd is.
"pendingCommand": COMMAND_FILE.exists(),
}
def valid_command(payload):
"""Geeft de opdracht terug, of een foutmelding.
Streng aan deze kant, want dit is de enige plek waar iets van buiten in de
postbus belandt.
"""
if not isinstance(payload, dict):
return None, "geen object"
action = payload.get("action")
if action not in ALLOWED_ACTIONS:
return None, "onbekende actie"
if action == "set-learning":
value = payload.get("value")
if not isinstance(value, bool):
return None, "waarde moet true of false zijn"
return {"action": action, "value": value}, None
owner_id = payload.get("ownerId")
if not isinstance(owner_id, str) or not owner_id or len(owner_id) > MAX_OWNER_ID_LENGTH:
return None, "ontbrekende of te lange ownerId"
return {"action": action, "ownerId": owner_id}, None
def write_command(command):
"""Legt de opdracht in de postbus.
Eerst een tijdelijk bestand en dan hernoemen: het relay-proces kijkt op zijn
eigen moment en mag geen half bestand aantreffen.
"""
STATE_DIR.mkdir(parents=True, exist_ok=True)
temporary = COMMAND_FILE.with_suffix(".json.tmp")
with temporary.open("w", encoding="utf-8") as handle:
json.dump(command, handle)
handle.write("\n")
temporary.replace(COMMAND_FILE)
class Handler(BaseHTTPRequestHandler):
# De standaardregel van BaseHTTPRequestHandler noemt de naam van de server en
# de Python-versie. Dat hoeft niemand te weten.
server_version = "evolu-relay-agent"
sys_version = ""
def log_message(self, format, *args):
# Geen toegangslog. Elke regel zou het adres van de bezoeker bevatten en
# de pagina zit achter de inlog van umbrelOS; er valt niets te zien wat
# het bewaren waard is.
return
def _send(self, status, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path.rstrip("/") in ("/api/status", "/status"):
self._send(200, build_status())
return
self._send(404, {"error": "onbekend pad"})
def do_POST(self):
if self.path.rstrip("/") not in ("/api/command", "/command"):
self._send(404, {"error": "onbekend pad"})
return
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
self._send(400, {"error": "lengte ontbreekt"})
return
if length <= 0 or length > MAX_BODY_BYTES:
self._send(400, {"error": "lege of te grote opdracht"})
return
try:
payload = json.loads(self.rfile.read(length).decode("utf-8"))
except (UnicodeDecodeError, ValueError):
self._send(400, {"error": "onleesbare opdracht"})
return
command, problem = valid_command(payload)
if command is None:
self._send(400, {"error": problem})
return
# Eén opdracht tegelijk. Ligt er nog een, dan zou schrijven hem stil
# overschrijven en verdwijnt de vorige zonder dat iemand het merkt.
if COMMAND_FILE.exists():
self._send(409, {"error": "vorige opdracht is nog niet verwerkt"})
return
try:
write_command(command)
except OSError as error:
self._send(500, {"error": "opdracht kon niet worden weggeschreven: " + str(error)})
return
self._send(202, {"accepted": command})
def main():
threading.Thread(target=probe_loop, daemon=True).start()
ThreadingHTTPServer(("0.0.0.0", API_PORT), Handler).serve_forever()
if __name__ == "__main__":
main()
+111 -117
View File
@@ -1,135 +1,129 @@
# ═══════════════════════════════════════════════════════════════════════════════
# Evolu Relay - de sync-server van Trezor Suite op je eigen Umbrel.
# Evolu Relay - je eigen sync-server voor de labels van Trezor Suite.
#
# Drie containers, twee images. De relay en de quota-manager komen uit dezélfde
# image met een ander `command`: bovenstrooms is het één codebase met meerdere
# startscripts. Dat is nagetrokken in de Dockerfile en de README van Trezor; zie
# Docs/Referenties/Upstream-evolu-relay.md §2.
# Drie containers: de relay, een agent voor de statuspagina, en nginx die die
# pagina serveert. Geen Postgres en geen quota-manager meer; die hoorden bij de
# relay van Trezor, en die is als zelf-gehoste relay in de kern onbruikbaar omdat
# de cliënt bij een eigen relay-URL nooit een eigenaar registreert. Zie het plan
# Umbrelapp, OPEN.md punt 6, en de proef in PLAN.md §6a.
#
# De poortindeling is omgedraaid ten opzichte van 25-08-2026, en dat is de kern
# van dit bestand:
#
# - de PAGINA hangt achter app_proxy, mét de inlog van umbrelOS. Vroeger stond de
# relay daar en moest de inlog dus uit, waardoor een statuspagina net zo
# onbeschermd zou zijn als de relay zelf;
# - de RELAY publiceert zijn eigen poort, waar Zoraxy met TLS naartoe wijst.
# Trezor Suite is geen browser met een sessiecookie en zou achter de inlog een
# inlogpagina krijgen in plaats van de relay.
#
# Dat is hetzelfde patroon als Electrum Gate in deze store: de web-UI via de
# proxy, het protocol op een eigen poort. Zie PLAN.md §4h.
# ═══════════════════════════════════════════════════════════════════════════════
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.
#
# PROXY_AUTH_ADD staat hier bewust NIET op "false". Dat stond er tot 0.0.2 wel,
# toen de relay hierachter hing. Nu wijst dit naar de statuspagina, en die hoort
# juist achter de inlog. Zet het niet terug zonder §4h te lezen.
app_proxy:
environment:
APP_HOST: whatsnext-evolu-relay_relay_1
APP_PORT: 4000
# Trezor Suite is geen browser met een sessiecookie. Achter de inlog van
# umbrelOS zou het een inlogpagina krijgen in plaats van de relay, en dat
# is niet op te lossen aan onze kant.
#
# Dit is geen omweg: het is het patroon dat de eigen nostr-relay-app van
# Umbrel gebruikt, om precies dezelfde reden. Ook Gitea en Budibase in de
# officiele store doen het zo.
#
# De prijs staat er wel bij: wie deze poort kan bereiken, bereikt de relay
# zonder aanmelding. Wat de schade beperkt is dat de relay zelf elke
# eigenaar weigert die geen limietenrij in de database heeft. Zet deze app
# dus niet zonder meer open naar het internet; zie het masterplan
# Bereikbaarheid.
#
# En let op de tweede weg naar binnen, die je niet ziet in deze compose:
# umbrelOS maakt per app een Tor hidden service. Zonder inlog betekent dat
# bereikbaar vanaf het internet, alleen beschermd doordat het .onion-adres
# onraadbaar is. Bij Electrum Gate is dat geen bezwaar, want daar zit de
# inlog van umbrelOS ervoor; hier is die er juist uit. Zet de hidden service
# dus uit in umbrelOS tenzij je hem bewust wilt. Op 25-08-2026 stond hij bij
# de eerste installatie gewoon aan.
PROXY_AUTH_ADD: "false"
APP_HOST: whatsnext-evolu-relay_server_1
APP_PORT: 80
relay:
# Gebouwd uit de broncode van Trezor door tools/evolu-relay/build.sh, en
# geduwd naar het Gitea-register op dezelfde server als deze store. Dat moet:
# umbreld haalt élke image op via de Docker Engine API (docker-modem) en niet
# via compose, dus een lokaal gebouwde tag is voor hem onbereikbaar en
# `pull_policy` wordt nooit gelezen. Op 25-08-2026 op het apparaat
# vastgesteld; zie Docs/Referenties/Umbrel-appstore-spec.md.
# Gebouwd uit tools/evolu-relay/ door build.sh en geduwd naar het
# Gitea-register op dezelfde server als deze store. Dat moet: umbreld haalt
# élke image op via de Docker Engine API en niet via compose, dus een lokaal
# gebouwde tag is voor hem onbereikbaar en de installatie faalt met
# "pull access denied". Op 25-08-2026 op het apparaat vastgesteld.
#
# De digest hoort erbij en niet alleen de tag: een tag kan opnieuw geduwd
# worden en dan draait er iets anders dan hier staat.
# worden en dan draait er iets anders dan hier staat. Staan ze allebei, dan
# bepaalt de digest wat er gehaald wordt en is de tag alleen leesbaarheid.
# Houd de tag gelijk aan `version` in het manifest en aan VERSION in build.sh.
#
# Let op: dit is de digest van één architectuur (amd64), want er is alleen
# amd64 geduwd. Voor de officiele store moet het een multi-arch index-digest
# zijn met arm64 erin; zie het masterplan Publicatie-Relay.
image: sc.kamenier-hamer.nl/sysop/evolu-relay:c03a204@sha256:2fe1e9e90e5cbc12d5ea6363df03bb132fddec0150283063b9103a09bdbe853a
# Geduwd op 28-08-2026. Let op: dit is één architectuur (amd64), want er is
# alleen amd64 gebouwd. Voor de officiele store hoort er een multi-arch
# index-digest met arm64 in; zie het masterplan Publicatie-Relay.
image: sc.kamenier-hamer.nl/sysop/evolu-relay:0.2.0@sha256:5b2b4228c24a7e8a12a99869ca39729f47a0a01d6a346c623b22d6e62f2918c2
restart: on-failure
depends_on:
db:
condition: service_healthy
# Bovenstrooms staat `CMD ["yarn", "start"]` in de Dockerfile, met daarbij een
# eigen commentaar dat het misschien een van de twee specifieke scripts had
# moeten zijn. Daarom hier expliciet, en niet vertrouwen op de standaard.
command: ["yarn", "start-evolu-relay"]
ports:
# De relay zelf. Dit is de poort waar Zoraxy met TLS naartoe wijst, en de
# enige die deze app publiceert; de pagina loopt via app_proxy.
#
# 3852 op de host en niet 4000: dat laatste is een veelgebruikte poort en
# een botsing merk je pas als de app niet start. Dezelfde les als bij
# Electrum Gate, dat om die reden 50022 gebruikt in plaats van 50002.
# Binnen de container blijft het 4000, want dat is wat de relay verwacht.
- "3852:4000"
environment:
RELAY_PORT: "4000"
HEALTH_PORT: "4002"
# dev of prod. Volgens .env.sample zet prod authenticatie aan; in de code
# bepaalt die vlag alleen het logniveau en staan de autorisatiecontroles
# onvoorwaardelijk aan. Een van de twee is achterhaald en dat is nog niet
# uitgezocht. Hier staat prod, want dat is de veilige kant van die
# onduidelijkheid: als het iets aanzet, willen we dat het aanstaat.
SERVER_ENV: prod
POSTGRES_GATE_HOST: whatsnext-evolu-relay_db_1
POSTGRES_GATE_PORT: "5432"
POSTGRES_GATE_USER: suite-sync
POSTGRES_GATE_DB: suite-sync-gate
# Door umbrelOS per installatie afgeleid. Nooit een letterlijke waarde:
# deze repo is publiek.
POSTGRES_GATE_PASSWORD: ${APP_PASSWORD}
POSTGRES_GATE_SSL: "false"
quota-manager:
# Zelfde image als relay, ander commando. Tag en digest moeten dus gelijk
# blijven aan die hierboven.
image: sc.kamenier-hamer.nl/sysop/evolu-relay:c03a204@sha256:2fe1e9e90e5cbc12d5ea6363df03bb132fddec0150283063b9103a09bdbe853a
restart: on-failure
depends_on:
db:
condition: service_healthy
command: ["yarn", "start-quota-manager"]
# Waarom deze container er is, terwijl hij bij Trezor bij hun betaalde
# hosting hoort: de relay weigert elke eigenaar zonder rij in de
# limietentabel, en dit is wat die rijen aanmaakt. Er is geen HTTP-koppeling
# tussen de twee; ze delen alleen de database. Zie Upstream-evolu-relay.md §3.
environment:
QUOTA_MANAGER_PORT: "4001"
HEALTH_PORT: "4012"
SERVER_ENV: prod
POSTGRES_GATE_HOST: whatsnext-evolu-relay_db_1
POSTGRES_GATE_PORT: "5432"
POSTGRES_GATE_USER: suite-sync
POSTGRES_GATE_DB: suite-sync-gate
POSTGRES_GATE_PASSWORD: ${APP_PASSWORD}
POSTGRES_GATE_SSL: "false"
db:
# TODO pinnen op de multi-arch index-digest, met
# `docker buildx imagetools inspect postgres:17-alpine`. Kan alleen op een
# machine met Docker; staat als taak in het plan Umbrelapp.
#
# Niet `postgres` kaal zoals de compose van Trezor doet: dat is `latest` en
# dus niet reproduceerbaar, en een grote-versiesprong van Postgres migreert
# zijn datamap niet vanzelf. Dan start de database niet meer en is de data
# alleen met handwerk terug te halen.
image: postgres:17-alpine
restart: on-failure
environment:
POSTGRES_USER: suite-sync
POSTGRES_DB: suite-sync-gate
POSTGRES_PASSWORD: ${APP_PASSWORD}
# Een submap en niet de wortel van de mount. Postgres weigert een datamap
# die al iets anders bevat, en een mount heeft op sommige bestandssystemen
# een lost+found.
PGDATA: /var/lib/postgresql/data/pgdata
# Per schrijfactie en niet per eigenaar; zo is isOwnerWithinQuota bij Evolu
# bedoeld. Eén labelwijziging is klein, dus een schrijfactie hierboven is
# eerder een fout of misbruik dan normaal gebruik. Zie PLAN.md §4g.
RELAY_MAX_WRITE_BYTES: "1048576"
volumes:
# De database van de relay én onze allowlist ernaast, in dezelfde map. Het
# programma doet chdir naar `data`, net als de relay van Evolu zelf, dus dit
# pad ligt vast.
#
# Onder data/ en niet in een naamloos Docker-volume: dit is wat umbrelOS
# bewaart en meeneemt in de back-up. In een volume zou de database een
# bewaart en meeneemt in de back-up. In een volume zou de synchronisatie een
# herinstallatie van de app niet overleven.
- ${APP_DATA_DIR}/data/postgres:/var/lib/postgresql/data
healthcheck:
# Op de eigen database en gebruiker, niet op `-d postgres` zoals
# bovenstrooms: die database bestaat hier niet en dan is de controle groen
# terwijl het verkeerde antwoord gegeven wordt.
test: ["CMD-SHELL", "pg_isready -U suite-sync -d suite-sync-gate"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
- ${APP_DATA_DIR}/data/relay:/app/data
# De agent bedient de statuspagina. Hij beslist niets: hij leest wat het
# relay-proces heeft opgeschreven en legt opdrachten in een postbus die de relay
# zelf leegmaakt. Twee processen die in dezelfde allowlist schrijven is een
# wedloop die je een keer per jaar treft en dan niet kunt reproduceren.
agent:
# TODO pinnen op de multi-arch index-digest, met
# `docker buildx imagetools inspect python:3-alpine`. Geldt ook voor nginx
# hieronder; staat als taak in het plan Umbrelapp.
image: python:3-alpine
restart: on-failure
environment:
RELAY_STATE_DIR: /var/lib/relay
# De volledige containernaam en niet de servicenaam 'relay'. Bij Electrum
# Gate gaf de korte naam op het apparaat een geweigerde verbinding: de naam
# loste wél op, maar naar een container van een andere app op hetzelfde
# netwerk. De vorm <app-id>_<service>_1 is uniek per app.
RELAY_HOST: whatsnext-evolu-relay_relay_1
RELAY_PORT: "4000"
# Alleen om op de pagina het adres te tonen dat je in Trezor Suite invult.
# Moet gelijk zijn aan de hostpoort hierboven.
RELAY_PUBLIC_PORT: "3852"
RELAY_API_PORT: "8000"
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
# Dezelfde map als de relay. De agent moet hier kunnen schrijven, want de
# opdrachten van de pagina landen hier.
- ${APP_DATA_DIR}/data/relay:/var/lib/relay
command:
- python
- /app/agent.py
server:
image: nginx:alpine
restart: on-failure
# De agent moet er zijn voordat nginx start: de proxy_pass naar
# http://agent:8000 wordt bij het starten opgelost en een onbekende naam laat
# nginx afbreken.
depends_on:
- agent
volumes:
# Beide door umbrelOS ingevuld uit een .template bij het starten.
- ${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.
- ${APP_DATA_DIR}/icon.png:/usr/share/nginx/html/icon.png:ro
+480
View File
@@ -0,0 +1,480 @@
<!DOCTYPE html>
<!--
═══════════════════════════════════════════════════════════════════════════════
De statuspagina van Evolu Relay.
Zichtbare tekst is Engels: dit is een publieke app store en dit is wat een
bezoeker ziet. Commentaar is Nederlands.
LET OP, en dit is de valstrik van dit bestand: umbreld haalt elke *.template
bij het starten door envsubst. Er mag dus GEEN dollarteken in staan. Dat sluit
ook JavaScript-template-literals uit, want die gebruiken accolades achter een
dollarteken en zouden stilzwijgend leeg worden. Alles hieronder plakt strings
dus met een plus aan elkaar. tests/test_appstore_vorm.py houdt dit dicht.
Het ontwerp volgt HomeGit/Docs/website-design-system.html, net als Electrum
Gate, zodat de twee apps naast elkaar op hetzelfde dashboard herkenbaar zijn.
Wat er bewust NIET uit overgenomen is: de verwijzing naar Google Fonts. Een app
op een Umbrel hoort niet te wachten op een lettertype van buiten.
═══════════════════════════════════════════════════════════════════════════════
-->
<html lang="en" data-theme="dark" data-accent="orange">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Evolu Relay</title>
<link rel="icon" href="icon.png">
<style>
:root {
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-full: 999px;
--font-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', ui-monospace, Menlo, Consolas, monospace;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
[data-theme="dark"][data-accent="orange"] {
--bg: #080808;
--bg-elevated: #181818;
--bg-raised: #242424;
--border: rgba(255,255,255,0.18);
--text-primary: #f2f2f2;
--text-sec: #a3a3a3;
--text-ter: #6a6a6a;
--accent: #FF6B00;
--accent-soft: rgba(255,107,0,0.16);
--pos: #FF8124;
--pos-bg: rgba(255,107,0,0.18);
--neg: #ff5a5a;
--neg-bg: rgba(255,69,69,0.18);
--neutral-bg: rgba(255,255,255,0.08);
}
[data-theme="light"][data-accent="orange"] {
--bg: #ffffff;
--bg-elevated: #f7f7f7;
--bg-raised: #f0f0f0;
--border: rgba(0,0,0,0.12);
--text-primary: #0a0a0a;
--text-sec: #888888;
--text-ter: #bbbbbb;
--accent: #E05500;
--accent-soft: rgba(224,85,0,0.08);
--pos: #D04800;
--pos-bg: rgba(224,85,0,0.08);
--neg: #cc2222;
--neg-bg: rgba(204,34,34,0.08);
--neutral-bg: rgba(0,0,0,0.04);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 24px 16px 64px;
background: var(--bg);
color: var(--text-primary);
font-family: var(--font-base);
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 780px; margin: 0 auto; }
header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
header img { width: 40px; height: 40px; border-radius: var(--radius-sm); }
header h1 { font-size: 1.35rem; margin: 0; font-weight: 700; letter-spacing: -0.01em; }
header .spacer { flex: 1; }
.sub { color: var(--text-sec); margin: 0 0 24px; font-size: 0.92rem; }
.theme-toggle {
background: var(--bg-raised);
border: 1px solid var(--border);
color: var(--text-sec);
border-radius: var(--radius-full);
padding: 6px 14px;
font: inherit;
font-size: 0.8rem;
cursor: pointer;
}
.card {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 16px;
}
.card h2 {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-ter);
margin: 0 0 14px;
font-weight: 600;
}
.stat-row { display: flex; flex-wrap: wrap; gap: 24px; }
.stat { min-width: 130px; }
.stat-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-ter); }
.stat-val { font-size: 1.15rem; font-weight: 600; margin-top: 2px; }
.stat-val.mono { font-family: var(--font-mono); font-size: 0.95rem; }
.pill {
display: inline-block;
padding: 3px 10px;
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 600;
}
.pill.ok { background: var(--pos-bg); color: var(--pos); }
.pill.bad { background: var(--neg-bg); color: var(--neg); }
.pill.unknown { background: var(--neutral-bg); color: var(--text-sec); }
.url-box {
font-family: var(--font-mono);
font-size: 0.85rem;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
overflow-wrap: anywhere;
color: var(--accent);
}
.hint { color: var(--text-sec); font-size: 0.85rem; margin-top: 10px; }
.switch-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.switch-row .grow { flex: 1; min-width: 200px; }
button.action {
background: var(--accent-soft);
border: 1px solid var(--accent);
color: var(--accent);
border-radius: var(--radius-full);
padding: 8px 18px;
font: inherit;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s var(--ease-out);
}
button.action:hover:not(:disabled) { transform: translateY(-1px); }
button.action:disabled { opacity: 0.45; cursor: default; }
button.action.quiet { background: transparent; border-color: var(--border); color: var(--text-sec); }
button.action.danger { background: var(--neg-bg); border-color: var(--neg); color: var(--neg); }
.owner {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.owner:first-of-type { border-top: none; }
.owner .id {
font-family: var(--font-mono);
font-size: 0.8rem;
overflow-wrap: anywhere;
flex: 1;
min-width: 220px;
}
.owner .meta { color: var(--text-ter); font-size: 0.75rem; }
.owner .buttons { display: flex; gap: 8px; }
.empty { color: var(--text-ter); font-size: 0.88rem; font-style: italic; }
.notice {
border-radius: var(--radius-md);
padding: 12px 14px;
margin-bottom: 16px;
font-size: 0.88rem;
display: none;
}
.notice.show { display: block; }
.notice.warn { background: var(--neg-bg); color: var(--neg); }
.notice.info { background: var(--accent-soft); color: var(--accent); }
footer { color: var(--text-ter); font-size: 0.78rem; text-align: center; margin-top: 32px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<img src="icon.png" alt="">
<h1>Evolu Relay</h1>
<span class="spacer"></span>
<button class="theme-toggle" id="theme">Light</button>
</header>
<p class="sub">Your own sync server for Trezor Suite labels. Nothing leaves your machine.</p>
<div class="notice" id="notice"></div>
<div class="card">
<h2>Relay</h2>
<div class="stat-row">
<div class="stat">
<div class="stat-label">Status</div>
<div class="stat-val"><span class="pill unknown" id="relay-state">checking</span></div>
</div>
<div class="stat">
<div class="stat-label">Stored data</div>
<div class="stat-val mono" id="db-size">-</div>
</div>
<div class="stat">
<div class="stat-label">Last write</div>
<div class="stat-val mono" id="db-modified">-</div>
</div>
</div>
</div>
<div class="card">
<h2>Point Trezor Suite here</h2>
<div class="url-box" id="relay-url">-</div>
<p class="hint">
In Trezor Suite, open the developer settings and set the custom relay URL. Leave the quota manager
URL empty: Suite ignores it once a custom relay is set. The address must start with http or https;
Suite rejects ws.
</p>
</div>
<div class="card">
<h2>New owners</h2>
<div class="switch-row">
<div class="grow">
<div class="stat-val"><span class="pill unknown" id="learning-state">unknown</span></div>
<p class="hint" id="learning-hint">
While this is open, the next device that connects is accepted and remembered. Close it once your
own devices are paired.
</p>
</div>
<button class="action" id="learning-toggle" disabled>Loading</button>
</div>
</div>
<div class="card">
<h2>Accepted owners</h2>
<div id="allowed"><p class="empty">None yet.</p></div>
</div>
<div class="card">
<h2>Blocked owners</h2>
<div id="blocked"><p class="empty">None.</p></div>
</div>
<div class="card">
<h2>Refused attempts</h2>
<div id="rejected"><p class="empty">None.</p></div>
<p class="hint">
Devices that tried to connect while new owners were closed. Only the twenty most recent are kept.
</p>
</div>
<footer>Evolu Relay on umbrelOS &middot; WhatsNext?</footer>
</div>
<script>
(function () {
'use strict';
var root = document.documentElement;
var noticeBox = document.getElementById('notice');
var busy = false;
// ── Thema ──────────────────────────────────────────────────────────────────
var themeButton = document.getElementById('theme');
var applyTheme = function (theme) {
root.setAttribute('data-theme', theme);
themeButton.textContent = theme === 'dark' ? 'Light' : 'Dark';
try { localStorage.setItem('evolu-relay-theme', theme); } catch (error) { /* privémodus */ }
};
try {
var saved = localStorage.getItem('evolu-relay-theme');
if (saved === 'light' || saved === 'dark') applyTheme(saved);
} catch (error) { /* privémodus */ }
themeButton.addEventListener('click', function () {
applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
// ── Hulpjes ────────────────────────────────────────────────────────────────
var notice = function (text, kind) {
noticeBox.textContent = text;
noticeBox.className = 'notice show ' + (kind || 'info');
};
var clearNotice = function () { noticeBox.className = 'notice'; };
var formatBytes = function (bytes) {
if (typeof bytes !== 'number') return '-';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' kB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
};
var formatTime = function (value) {
if (typeof value !== 'string') return 'never';
var when = new Date(value);
if (isNaN(when.getTime())) return 'never';
return when.toLocaleString();
};
var setPill = function (element, text, kind) {
element.textContent = text;
element.className = 'pill ' + kind;
};
var shorten = function (ownerId) {
if (ownerId.length <= 24) return ownerId;
return ownerId.slice(0, 12) + '…' + ownerId.slice(-8);
};
// ── Opdrachten ─────────────────────────────────────────────────────────────
var send = function (command) {
if (busy) return;
busy = true;
clearNotice();
fetch('api/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(command)
}).then(function (response) {
return response.json().then(function (body) { return { ok: response.ok, body: body }; });
}).then(function (result) {
if (!result.ok) {
notice('The relay refused that: ' + (result.body.error || 'unknown reason'), 'warn');
return;
}
notice('Sent. The relay applies changes within a few seconds.', 'info');
}).catch(function () {
notice('Could not reach the agent.', 'warn');
}).then(function () {
busy = false;
window.setTimeout(refresh, 2500);
});
};
var ownerRow = function (entry, buttons) {
var row = document.createElement('div');
row.className = 'owner';
var id = document.createElement('div');
id.className = 'id';
id.textContent = shorten(entry.id);
id.title = entry.id;
row.appendChild(id);
var meta = document.createElement('div');
meta.className = 'meta';
var parts = [];
if (entry.firstSeen) parts.push('first seen ' + formatTime(entry.firstSeen));
if (entry.lastSeen) parts.push('last seen ' + formatTime(entry.lastSeen));
if (entry.attempts) parts.push(entry.attempts + ' attempts');
meta.textContent = parts.join(' · ');
row.appendChild(meta);
var holder = document.createElement('div');
holder.className = 'buttons';
buttons.forEach(function (spec) {
var button = document.createElement('button');
button.className = 'action ' + (spec.style || 'quiet');
button.textContent = spec.label;
button.addEventListener('click', function () { send(spec.command(entry.id)); });
holder.appendChild(button);
});
row.appendChild(holder);
return row;
};
var fillList = function (element, entries, emptyText, buttons) {
element.textContent = '';
if (!entries.length) {
var empty = document.createElement('p');
empty.className = 'empty';
empty.textContent = emptyText;
element.appendChild(empty);
return;
}
entries.forEach(function (entry) { element.appendChild(ownerRow(entry, buttons)); });
};
// ── De ronde ───────────────────────────────────────────────────────────────
var refresh = function () {
fetch('api/status', { cache: 'no-store' }).then(function (response) {
return response.json();
}).then(function (status) {
var relayPill = document.getElementById('relay-state');
if (status.relay.reachable === true) setPill(relayPill, 'running', 'ok');
else if (status.relay.reachable === false) setPill(relayPill, 'not responding', 'bad');
else setPill(relayPill, 'unknown', 'unknown');
document.getElementById('db-size').textContent = formatBytes(status.database.bytes);
document.getElementById('db-modified').textContent = formatTime(status.database.modified);
document.getElementById('relay-url').textContent =
'http://' + window.location.hostname + ':' + status.relay.publicPort;
var learningPill = document.getElementById('learning-state');
var toggle = document.getElementById('learning-toggle');
if (status.owners.learning === true) {
setPill(learningPill, 'open', 'ok');
toggle.textContent = 'Close';
toggle.disabled = false;
} else if (status.owners.learning === false) {
setPill(learningPill, 'closed', 'unknown');
toggle.textContent = 'Open';
toggle.disabled = false;
} else {
setPill(learningPill, 'unknown', 'unknown');
toggle.textContent = 'Loading';
toggle.disabled = true;
}
toggle.onclick = function () {
send({ action: 'set-learning', value: status.owners.learning !== true });
};
fillList(document.getElementById('allowed'), status.owners.allowed, 'None yet.', [
{ label: 'Block', style: 'danger', command: function (id) { return { action: 'block', ownerId: id }; } },
{ label: 'Forget', command: function (id) { return { action: 'forget', ownerId: id }; } }
]);
fillList(document.getElementById('blocked'), status.owners.blocked, 'None.', [
{ label: 'Allow', command: function (id) { return { action: 'allow', ownerId: id }; } },
{ label: 'Forget', command: function (id) { return { action: 'forget', ownerId: id }; } }
]);
fillList(document.getElementById('rejected'), status.owners.rejected, 'None.', [
{ label: 'Allow', command: function (id) { return { action: 'allow', ownerId: id }; } }
]);
if (status.owners.problem === 'onleesbaar') {
notice('The owner list could not be read and was moved aside. New owners are refused until you '
+ 'open them again above.', 'warn');
} else if (status.pendingCommand) {
notice('A command is waiting for the relay to pick it up.', 'info');
} else if (noticeBox.className.indexOf('warn') === -1) {
clearNotice();
}
}).catch(function () {
setPill(document.getElementById('relay-state'), 'agent unreachable', 'bad');
});
};
refresh();
window.setInterval(refresh, 5000);
})();
</script>
</body>
</html>
+58
View File
@@ -0,0 +1,58 @@
# nginx-configuratie voor Evolu Relay.
#
# Dit bestand serveert alleen de statuspagina. De relay zelf zit er niet achter:
# die publiceert zijn eigen poort in docker-compose.yml, waar Zoraxy met TLS
# naartoe wijst. Dat is de omkering ten opzichte van het pakket van 25-08-2026,
# waar de relay achter de app-proxy stond en de inlog dus uit moest; zie het plan
# Umbrelapp, PLAN.md §4h.
#
# Dit is een template, en dat is bewust: umbrelOS ververst bij een update alleen
# een whitelist van bestanden, en *.template staat daarin. Bij elke start vult
# umbrelOS de omgevingsvariabelen in en schrijft het resultaat 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
# accolade-variabele en zou die stilzwijgend leegmaken. Daarom staat access_log
# uit.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
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;
# De API van de agent: de status lezen en een opdracht in de postbus
# leggen. Dit is het enige pad waarlangs iets van buiten de instellingen
# van de app raakt.
#
# Waarom dat hier te verantwoorden is: dit pad hangt achter de app_proxy
# van umbrelOS, die er zijn eigen inlog voor zet. De relay staat op een
# andere poort en is hierlangs niet te bereiken. En het ergste wat een
# geslaagde aanroep doet is de leerstand omzetten of een eigenaar
# blokkeren, en dat is precies waar de pagina voor is.
#
# Geen proxy_set_header, 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;
# Een opdracht is een handvol bytes. De agent kapt zelf ook af, op
# 4k; dit is de eerste zeef en niet de enige.
client_max_body_size 4k;
}
}
}
+27 -23
View File
@@ -11,7 +11,7 @@ manifestVersion: 1
id: whatsnext-evolu-relay
category: bitcoin
name: Evolu Relay
version: "0.0.2"
version: "0.2.0"
tagline: Sync your Trezor Suite labels through your own machine
description: >-
Trezor Suite can sync the labels and account names you give your addresses across your
@@ -24,43 +24,47 @@ description: >-
can see that you are syncing at all.
This is Trezor's own relay, not a reimplementation: it is built from the source they publish
as trezor/trezor-suite-sync, pinned to a specific commit.
This runs the relay from the Evolu project, the sync layer Trezor Suite is built on, taken
straight from the package they publish. Nothing about it is reimplemented here. What this app
adds is the part a self-hosted relay needs and an open one does not: control over who may use
it.
The tile opens a status page. It shows whether the relay is running, how much it stores, and
which devices are allowed. New devices are accepted one at a time: the first owner that
connects is remembered, and you close the door again once your own devices are paired. Any
owner can be blocked or removed later from that same page.
Point Trezor Suite at this Umbrel to use it. Away from home you will need a way in, such as
Tailscale or a reverse proxy with your own domain.
Two things to know before you install. The relay has no web interface, so the tile opens the
relay itself rather than a page for you to read. And it only serves an owner that has a
storage limit registered in its database, which is what the included quota manager is for;
if syncing is refused, that registration is the place to look.
releaseNotes: >-
First release that can actually be installed. 0.0.1 pointed at an image that only existed
on the machine it was built on, and the install failed before anything started: umbrel
fetches every image from a registry, so a local build is out of reach. The image now comes
from a registry and is pinned to its digest.
A different relay, and a much smaller app. 0.0.2 packaged the relay Trezor runs for their own
hosted service, with a quota manager and a PostgreSQL database beside it. That combination
cannot work on your own machine: Suite skips the quota manager as soon as you point it at a
relay of your own, while that relay refuses every owner the quota manager never registered.
Packages Trezor's Evolu Relay, the quota manager it needs, and a PostgreSQL database, all on
your own machine.
This release uses the relay from the Evolu project instead, which Trezor Suite talks to
directly. Measured, not assumed: Suite sent labels to it and they arrived. The database and
the quota manager are gone, and three containers became one relay plus a status page.
Built from source pinned to commit c03a204 of trezor/trezor-suite-sync. There is no image
published by Trezor, so the image is built from their Dockerfile; see tools/evolu-relay in
the app store repository.
New: a status page behind your umbrelOS login, and control over who may sync. The first owner
that connects is remembered and accepted; after that you close the door, and anything new is
refused until you open it again.
Not verified yet: whether Trezor Suite accepts this address, and what has to happen before
an owner is allowed to sync.
Not verified yet: reading back from a second device, and whether the iOS app can use a relay
of your own at all.
developer: "WhatsNext?"
website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
repo: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
support: https://sc.kamenier-hamer.nl/sysop/UmbrelApps/issues
# De poort waarop umbrelOS deze app aanbiedt, en tegelijk de poort waar Trezor
# Suite naartoe wijst: de app-proxy staat met PROXY_AUTH_ADD op "false", dus wat
# hier binnenkomt gaat rechtstreeks naar de relay. Zie docker-compose.yml.
# De poort waarop umbrelOS deze app aanbiedt. Sinds 0.1.0 is dat de STATUSPAGINA
# en niet de relay: de app-proxy zet er de inlog van umbrelOS voor, en daar hoort
# een pagina wel achter en een relay niet. Trezor Suite wijst naar 3852, die de
# relay zelf publiceert. Zie docker-compose.yml en het plan Umbrelapp, PLAN.md §4h.
#
# Niet 4000, de eigen poort van de relay: die is een veelgebruikte poort en een
# botsing op de host merk je pas als de app niet start. Dat heeft bij Electrum