server-up/CHANGELOG.md
Ramon f8274ebeff
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 17m46s
v0.10.20-beta - self-update, tags, tokens, --base-dir en --doctor
- bijwerken vanuit de interface kon nooit werken: selfupdate schreef SU_TAG naar
  <werkmap>/.env, maar die map is nergens in de container gemount. De
  helper-container mount hem wel en zet de tag nu
- een vastgepinde versie kon niet bijgewerkt worden: `git reset --hard
  origin/<tag>` bestaat niet. Resetten gaat nu naar FETCH_HEAD, wat voor takken
  en tags allebei klopt
- de token stond in .git/config en in ps; hij gaat nu via een bestand met modus
  0600 dat na afloop weer weg is (git via een credential-helper, curl via
  --config). Schema en host komen uit bron_url, want git zoekt op exact die
  combinatie
- nieuwe optie --base-dir PAD: de hoofdmap bij de installatie zetten in plaats
  van achteraf in .env, waarna je de data alsnog moet verhuizen. Relatieve
  paden en systeemmappen worden geweigerd
- nieuwe actie --doctor: een installatie doorlichten zonder iets te wijzigen —
  container, bereikbaarheid op het ingestelde adres, het account versus SU_UID
  in .env, of BASE_DIR echt gekoppeld is, de rechten van .env en de vrije
  ruimte. Exitcode 1 bij fouten
- de CI draait --doctor na elke deploy tegen de echte installatie

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
2026-08-03 00:26:52 +02:00

1861 lines
92 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# v0.10.20-beta — Bijwerken vanuit de interface, en een script dat nakijkt
Het tweede deel van de controle op het installatiepad.
**Bijwerken vanuit de interface kon nooit werken.** `selfupdate` schreef
`SU_TAG` naar `<werkmap>/.env` — maar die map is nergens in de container
gemount. Er zijn maar drie mounts: de docker-socket, het `su-data`-volume en
`BASE_DIR`. De installatiemap zit daar niet bij, dus schrijven mislukte. Dat
viel niet op omdat een zelfgebouwd image de knop toch al uitschakelt; zette je
`SU_IMAGE` naar een registry — de manier die `docs/updates.md` noemt om hem aan
te zetten — dan faalde hij altijd. De helper-container mount die map wél en
draait als root, dus die zet de tag nu.
**Een vastgepinde versie kon nooit bijgewerkt worden.** `--branch v0.9.00-beta`
installeerde prima, maar bijwerken deed `git reset --hard origin/v0.9.00-beta`
en tags krijgen geen `origin/`-ref: "fatal: ambiguous argument". Het resetten
gaat nu naar `FETCH_HEAD`, wat voor takken en tags allebei klopt. De test liet
zien dat het na een tag-installatie óók voor takken stuk was.
**De token stond in `.git/config` en in `ps`.** In de URL schrijft git hem
verbatim in de config, waar hij blijft staan; als argument staat hij in
`/proc/<pid>/cmdline` en leest elke gebruiker op de server hem met `ps`. Dat is
precies waarom dit script `--admin-password` weigert. Hij gaat nu via een
bestand met modus 0600 dat na afloop weer weg is — git leest het via een
credential-helper, curl via `--config`; alleen het pad staat op de opdrachtregel.
Twee nieuwe opties:
- **`--base-dir PAD`** zet de hoofdmap voor stacks, appdata en backups bij de
installatie, in plaats van achteraf in `.env` — waarna je de data alsnog moet
verhuizen. Relatieve paden en systeemmappen worden geweigerd.
- **`--doctor`** licht een bestaande installatie door zonder iets te wijzigen:
container, bereikbaarheid *op het ingestelde adres*, onder welk account hij
draait versus wat er in `.env` staat, of `BASE_DIR` echt gekoppeld is (bestaan
is niet genoeg), de rechten van `.env`, en hoeveel ruimte er over is. Exitcode
1 bij fouten. De CI draait hem na elke deploy tegen de echte installatie —
ruim honderd regels shell die je nergens anders getest krijgt.
# v0.10.10-beta — Het installatiescript loog op drie plekken
Een controle van het hele installatiepad leverde drie fouten op die alledrie op
`curl … | sh` liggen — de regel die de README noemt.
**Vragen werden niet gesteld en beantwoordden zichzelf met "ja".** `vraag()`
keek naar stdin, en bij `curl … | sh` is dat de pipe. Alle andere prompts in het
script gebruiken `/dev/tty`, dat gewoon bereikbaar is. Gevolg: "Nu installeren
via het officiële script van docker.com?" werd overgeslagen en met ja
beantwoord, `--uninstall` brak je installatie af zonder bevestiging, en de vraag
of je een beheerdersaccount wilde bleef ongesteld terwijl de vervolgprompt
("Gebruikersnaam:") wél verscheen. Er is nu één `terminal_beschikbaar()` die
alle vijf de plekken gebruiken.
**Met een eigen bind-adres mislukte de healthcheck én het beheerdersaccount.**
Compose publiceert op `$BIND`, maar het script vroeg altijd `127.0.0.1`. Met
`--bind 10.0.20.5` luistert daar niets, dus meldde het "reageerde niet binnen
anderhalve minuut" en daarna "Server Up was niet bereikbaar; account niet
aangemaakt" — terwijl alles draaide. Je hield een installatie over zonder
account, precies het venster waar het script voor waarschuwt. Het controleadres
volgt nu `$BIND`, met blokhaken om IPv6.
**Een mislukte update meldde zich als geslaagd.** `wacht_op_gereed || true`
gevolgd door `goed "Bijgewerkt."` maakte een kapotte update niet te
onderscheiden van een goede, ook niet aan de exitcode. Nu stopt hij met een
foutmelding en de weg terug. Bij een verse installatie gaat hij wél door — die
aanwijzingen zijn juist dán nuttig — maar het slot zegt eerlijk dat er niet
geantwoord is en de exitcode is 1.
Verder:
- `curl` kreeg tijdslimieten bij het pollen. Zonder die limieten bleef hij
hangen op een adres waar niets naartoe routeert en betekende het aantal
pogingen niets. Het aantal is nu ook instelbaar met `SU_WACHT_POGINGEN`, voor
trage machines.
- Het slot zei nog "Server Up beheert Docker als root" en noemt nu onder welk
account het draait.
Onderweg gevonden en meteen verholpen: de eerste versie van
`terminal_beschikbaar()` gebruikte `{ : </dev/tty; }`. `:` is een *special
builtin*, en een mislukte redirect daarop beëindigt volgens POSIX de hele shell
— exit 2, zonder melding, nog vóór de eerste stap. Dat brak elke installatie
zonder terminal. De openpoging staat nu in een subshell.
# v0.10.00-beta — Server Up draait niet meer als root
Er stond `user: "0:0"` in `docker-compose.yml` en nergens waarom. Dat is nu een
keuze die je bij de installatie maakt.
**Bij het installeren wordt gevraagd onder welk account het moet draaien**: een
nieuw systeemaccount `serverup` (aanbevolen, wordt aangemaakt zonder shell en
zonder wachtwoord), het account waarmee je werkt, een bestaand account uit een
lijst, of root zoals voorheen. Met `--user NAAM` of `--yes` sla je de vraag over.
Een bestaande installatie krijgt de vraag één keer bij `--update`; daarna blijft
je keuze staan.
**De container start nog steeds als root en zakt daarna af.** Dat moet: Docker
maakt het `su-data`-volume als root aan, en een container die meteen als een
gewone gebruiker start komt niet eens tot zijn configuratie. Het nieuwe
`docker-entrypoint.sh` zet `/data`, `stacks` en `backups` klaar en gebruikt dan
`setpriv` — niet `gosu`, want een proces dat met `setuid` afzakt verliest zijn
capabilities, en die zijn hier nodig. `appdata` blijft ongemoeid: die mappen zijn
van de apps zelf, en een `chown` daaroverheen breekt precies de containers die
als hun eigen uid draaien.
Het afgezakte proces houdt `CHOWN`, `DAC_OVERRIDE` en `FOWNER`. Alle drie zitten
al in de standaardset van Docker, dus de container krijgt geen enkel recht bij —
er gaan er alleen af. Wat dat wél en níét oplevert staat in
`docs/beveiliging.md`; kort gezegd beschermt het tegen bugs en ongelukken, niet
tegen iemand die de interface overneemt. Die heeft de docker-socket, en dat
blijft root. Een socket-proxy helpt daar niet tegen, omdat het dóél van deze app
is om containers met willekeurige bind mounts aan te maken.
**Twee rechten-fouten die hierdoor aan het licht kwamen, zijn ook verholpen:**
- Het installatieformulier zette `PUID`/`PGID` op wat het sjabloon toevallig
noemde — 44 sjablonen staan blind op `1000`. Nu krijgen ze de ids waaronder
Server Up draait, zodat appdata van hem is en hij hem kan inpakken.
- Eén onleesbaar bestand liet de héle backup falen: de `tar.add` van de
appdata-map zat in dezelfde `try` als de rest. Nu gaat de rest gewoon mee en
komt wat ontbreekt in het log én in de metadata (`skipped`) te staan. Een
archief dat compleet lijkt maar het niet is, is erger dan een archief dat zegt
wat het mist.
- Terugzetten gaf alles aan wie uitpakte: `tarfile` met `filter="data"` (PEP 706)
laat uid en gid uit het archief vallen. De appdata krijgt nu de `PUID`/`PGID`
uit de metadata terug, anders die van Server Up zelf.
De CI controleert vanaf nu in een echte container dat `setpriv` bestaat, dat er
wordt afgezakt, dat de gid van de docker-socket meekomt, dat `/data` schrijfbaar
is, en dat `SU_UID=0` root laat blijven.
**Terug naar de oude situatie:** zet `SU_UID=0` en `SU_GID=0` in je `.env` en
draai `docker compose up -d`.
# v0.9.00-beta — Eén hoofdmap, en je data mag mee verhuizen
**Server Up stelde `/opt/serverup` voor, wat je ook instelde.** Dat was geen
weergavefout maar drie fouten tegelijk:
- `LIBRARY_DIR`, `DATA_DIR` en `BACKUP_DIR` stonden hard op `/opt/serverup/*` en
trokken zich niets aan van de `BASE_DIR` die `docker-compose.yml` wél kent. Ze
worden nu afgeleid van `BASE_DIR`, en compose geeft die variabele voortaan ook
aan de container door — die stond alleen in de mount, dus je kon `/srv`
koppelen terwijl de app `/opt/serverup` bleef voorstellen.
- 266 van de 286 sjablonen hebben `/opt/serverup/appdata` als standaard voor
`appdata_dir`. Het installatieformulier overschreef wel de tijdzone met die
van de server, maar niet de appdata-map. Wat je ook had ingesteld, je data
belandde in `/opt`. Dat is nu één plek in `_annotate_fields()`, en daarmee
meteen goed voor alle sjablonen.
- `.env.example` en de documentatie noemden `/opt/serverup` als vast gegeven.
**Instellingen → Paden werkt nu met één hoofdmap.** Typ je er een, dan verschijnen
de drie submappen eronder als voorstel, elk met een knop om hem over te nemen, en
één knop **Alles overnemen**. De velden blijven los aanpasbaar. `/api/paths/check`
controleert onderweg of Server Up er überhaupt bij kán: de container ziet alleen
wat via `BASE_DIR` gekoppeld is, en daarbuiten schrijft hij naar zijn eigen
overlay — dat lijkt te lukken en levert daarna lege apps op. Ligt je map erbuiten,
dan noemt de melding de regel voor `.env` en het commando, in plaats van "kon niet
opslaan".
**Een gewijzigd pad biedt aan de data mee te verhuizen.** Alleen de instelling
aanpassen liet je data staan waar hij stond, en elke stack wees nog naar zijn oude
appdata-map. De verhuizing stopt de draaiende stacks, kopieert met een
voortgangsbalk, controleert aantal en omvang, schrijft de paden om in
`docker-compose.yml`, `.env` en de metadata van elke stack, start de stacks weer
op de nieuwe plek — **en ruimt pas daarna het oude op**. Klopt de controle niet,
dan blijft het origineel staan en is de melding de fout. Het omschrijven kijkt
naar hele pad-segmenten, zodat `/opt/serverup` vervangen `/opt/serverup-oud` met
rust laat.
**Rechten én eigenaar gaan mee.** `shutil.copy2` neemt de rechten over maar niet
de eigenaar, en Server Up draait als root — verhuisde appdata werd dus root:root,
waarna een container die als PUID 1000 draait niet meer bij zijn eigen gegevens
kon. De linuxserver-images zetten `/config` bij het starten terug, maar postgres,
Nextcloud en Immich lopen er gewoon op vast. De verhuizing chownt nu elk bestand,
elke map, elke symlink én de hoofdmap zelf naar de eigenaar van het origineel.
Lukt dat niet, dan gaat de verhuizing door met een waarschuwing in het log.
Jobs kregen daarvoor een `voortgang()`; de terminal toont de balk boven het log.
# v0.8.82-beta — "Poort {port} is vrij" stond er letterlijk
De poortcontrole werd aangesloten op vertaalsleutels die er al waren, en twee
daarvan verwachten een plaatshouder die niet werd meegegeven: `port_free` wil
`{port}`, `port_in_use` wil `{port}` én `{by}`. In beeld stond dus letterlijk
"Poort {port} is vrij".
Er draait nu een test die elke `t('sleutel', { … })` in de interface naloopt en
vergelijkt met de plaatshouders in de Nederlandse tekst — in beide richtingen,
en ook voor aanroepen zónder variabelen. Dat laatste was nodig: de eerste versie
van die test keek alleen naar aanroepen die wél iets meegaven, en liet de fout
waar hij voor gemaakt was gewoon passeren.
# v0.8.81-beta — Poorten kiezen zonder gokken, en installeren op een telefoon
**De poortcontrole zat er wel, maar werd nooit gevraagd.** `/api/ports/check`
bestaat en kijkt naar wat Docker publiceert én naar wat er rechtstreeks op de
host luistert — dat laatste is precies de botsing die je anders pas ziet als de
stack niet start. Het installatieformulier riep hem alleen nergens aan. Er stond
hooguit een regeltje als de stándaardwaarde bezet was; typte je zelf iets, dan
hoorde je niets meer.
Een poortveld heeft nu knoppen om een stapje omhoog of omlaag te doen, een
vergrootglas om te controleren, en een melding eronder: vrij, of bezet en door
welke container, met een knop om meteen naar de eerstvolgende vrije poort te
springen. De controle loopt ook zodra je een stap opent, dus je ziet het vóórdat
je iets typt. Kan de host niet uitgelezen worden, dan zegt de melding dat "vrij"
een aanname is in plaats van het te verzwijgen.
De knoppen zitten alleen op velden die ook echt een poort zijn — `puid` en `pgid`
zijn ook getallen. En het scherm *Instellingen wijzigen* van een bestaande stack
deed de controle bij het openen niet mee; dat is hetzelfde formulier, dus dat
hoort er ook bij.
**Installeren op een telefoon.** De installatiemodal is daar nu schermvullend in
plaats van een venstertje met marges, met de kop en de knoppenbalk vast en alleen
de inhoud die scrollt. Vorige en Volgende vullen samen de breedte in plaats van
op een rare plek af te breken. En omdat er dan geen achtergrond meer is om op te
tikken, zit er een sluitknop in de kop — die was er niet, en zonder deze
wijziging was je er niet uit gekomen.
# v0.8.70-beta — De store blijft soepel, en achtentwintig apps erbij
**Typen in het zoekveld werd traag.** `filteredStore()` is een methode, geen
gecachte getter, en werd per repo drie keer per reactieve tick aangeroepen: in de
`x-show`, in de `x-for` en via de teller in het filtermenu. Elke aanroep liep alle
apps langs met een `toLowerCase()` op naam, map, omschrijving en elke tag — bij
iedere toetsaanslag opnieuw. De catalogus ging deze reeks van 142 naar 286 apps,
en dat begon te merken.
Het filteren gebeurt nu één keer per filterstand en wordt vastgehouden tot je iets
verandert. Daarnaast staan er nog maar zestig kaarten tegelijk in beeld, met een
knop **Nog N tonen** eronder; die limiet valt terug zodra je het filter of de
zoekterm wijzigt, anders sta je na één zoekactie voor de rest van de sessie naar
alles te kijken. En het zoekveld wacht 200 ms voordat het filtert.
**Achtentwintig apps** in de hoeken die het dunst waren:
- **Beveiliging:** 2FAuth (tweestapscodes naast je kluis, niet erin), Passbolt
(wachtwoorden delen in een team), OpenBao, Trivy
- **Ontwikkeling:** Verdaccio, Docker Registry, MkDocs Material
- **AI:** LiteLLM, Flowise, Speaches
- **Spellen:** Pelican Panel, Gameyfin, EmulatorJS
- **3D-printen:** Mainsail en Fluidd — de twee interfaces voor Klipper, waar
OctoPrint niets mee doet
- **Productiviteit:** Kanboard, Wekan, Filestash
- **Documenten:** Linkwarden (bladwijzers mét een gearchiveerde kopie), Slash,
RSSHub
- **Communicatie:** Apprise (één API naar honderd meldingsdiensten), Mailpit
(vangt de mail van je eigen apps op)
- **Verder:** Dashy, Plausible, NetBird, LibreSpeed, PinePods
Alle 318 images in de catalogus zijn opnieuw tegen hun registry gecontroleerd.
# v0.8.60-beta — Backups bevatten nu je gegevens, en Pangolin hoort erbij
**Een backup gaf je niets terug.** `backups.create()` maakte een tar van de
stackmap: de compose, de `.env` en de metadata. `LIBRARY_DIR` is
`/opt/serverup/stacks` en de appdata staat in `/opt/serverup/appdata` — buren,
geen genestelde mappen. Je maakte dus een backup, kreeg een groen vinkje, en
hield bij terugzetten een lege app over. De documentatie noemde als uitweg dat je
data wél meegaat "als die map binnen je stackmap zit", maar met de
standaardinstellingen zit hij er juist buiten.
Een archief heeft nu drie takken: de stackmap, de appdata-mappen van die stack,
en een dump per database. Die dump is geen dubbelop — een tar van een draaiende
PostgreSQL is een momentopname van bestanden die tijdens het inpakken
veranderden. Bij terugzetten komt de dump erin nádat de container draait, met een
wachtlus omdat een database na het starten niet meteen verbindingen aanneemt.
Stond de container tijdens de backup uit, dan staat dat als waarschuwing in het
log; stil overslaan zou het ergste geval opleveren, een archief dat compleet
lijkt.
Nieuwe instelling **Appdata meenemen** (`BACKUP_APPDATA`, standaard aan). Uit
zetten is nodig bij een mediabibliotheek of fotoarchief: daar loopt dit in de
honderden gigabytes en is restic of borg het betere middel.
**Pangolin en Newt staan nu in de store.** Server Up had al een complete
Pangolin-integratie — een instellingenpagina, een publiceerknop per stack — maar
je moest Pangolin zelf ergens anders vandaan halen. Nu staan de tunnelclient
(Newt) en de server zelf (pangolin + gerbil + traefik, met de configuratie erbij)
in de catalogus.
**En er is een stap voor in de setup-wizard**, na de git-stap: koppelen aan een
Pangolin die je al hebt, er hier een neerzetten, of overslaan. Bij de eerste
keuze kun je meteen Newt erbij installeren. Terzijde: de wizard hing zijn
afhandeling aan stapnummers, dus met een stap ertussen ging de git-stap mis. Die
vergelijkingen gaan nu op de naam van de stap.
**Waarschuwing op de stackpagina.** `depends_on` meldde alleen bij het
installeren wat er ontbrak. Haal je Mosquitto later weg, dan viel Zigbee2MQTT
stil zonder dat iets zei waarom. Dat staat nu als badge op de stackkaart, met de
reden in de tooltip en een doorklik naar de store.
# v0.8.50-beta — Leesbare namen, zichtbaar wat al draait, en bruikbaar op een telefoon
**Het auditlog toonde ruwe id's.** In de kolommen stond `store`, `reconfigure`,
`credentials_view` — de sleutels waarmee de code logt, niet iets wat je leest. Die
hebben nu Nederlandse namen ("App Store", "Opnieuw ingesteld", "Inloggegevens
bekeken"), ook in de filterlijsten erboven, en de kolomkoppen zijn niet langer
hardgecodeerd Nederlands maar vertaald. Een categorie die we niet kennen — van een
module bijvoorbeeld — valt terug op de ruwe waarde in plaats van leeg te blijven.
Er draait een test die de broncode afloopt en elke `audit.log(...)` zonder
vertaling aanwijst. Een modulepagina heette in de titelbalk letterlijk
`mod:mijnmodule`; die toont nu de naam van de module.
**De store zag niet dat een app al draaide.** Er zat een klein groen vinkje naast
de installeerknop, en dat werkte alleen als je de stack precies zo had genoemd als
het sjabloon: `sonarr` of `sonarr-iets`. Noemde je hem `media-tv`, dan bleef de
store volhouden dat Sonarr nog niet geïnstalleerd was. Nu leest hij `.serverup.json`
uit — dezelfde `source` die het herconfigureerscherm al gebruikte — en toont een
duidelijk **Geïnstalleerd**-label met de naam van elke draaiende installatie,
waar je op kunt klikken om er meteen naartoe te gaan. De knop eronder zegt dan
"Nog een keer" in plaats van "Installeren".
**Op een telefoon was de stackpagina niet te doen.** Elke stack had twaalf
icoonknoppen zonder tekst naast elkaar, wat op een smal scherm drie rijen
plaatjes werd. Daar staan nu starten, herstarten en logs, plus een menu met de
rest — mét namen erbij. Op een breed scherm verandert er niets. De rij van twintig
categorieknoppen boven de stacks is hetzelfde menu geworden als in de app store,
en "Updates controleren" verliest op smalle schermen zijn label.
# v0.8.40-beta — De filter in de app store is een menu geworden
**Twee rijen knoppen vulden een half scherm.** Boven de app store stonden twintig
categorieknoppen en veertien labelknoppen, allemaal tegelijk uitgeklapt. Bij 256
apps is dat geen filter meer maar een muur waar je langs moet scrollen voordat je
de eerste app ziet.
Het is nu één knop **Filter** met een menu eronder. Categorieën staan in een
raster van twee kolommen met hun eigen kleur en het aantal apps erachter, labels
hebben een eigen zoekveld, en onderaan staat hoeveel apps er overblijven met een
knop om alles in één keer te wissen. Het menu gaat dicht als je ernaast klikt of
op Escape drukt.
**En de labelfilter werkte maar half.** De rij was afgekapt op veertien labels,
terwijl er 297 in de catalogus zitten. De overige 283 waren gewoon niet te kiezen
— je zag ze wel op de app-kaarten staan, maar erop filteren kon niet. Het menu
zoekt nu in de volledige lijst; zonder zoekterm toont het de veertig meest
gebruikte, en een label dat je gekozen hebt blijft zichtbaar ook als het zeldzaam
is.
**Wat je aan hebt staan zie je zonder het menu te openen:** actieve filters staan
als chips naast de knop, met een kruisje om ze los weg te halen. En het zoekveld
heeft een wisknop gekregen.
**Nieuwe test op de HTML zelf.** De interface is één sjabloon van ruim
drieduizend regels met Alpine-templates die in elkaar zitten. Eén `</div>`
verkeerd en de rest van de pagina schuift in de verkeerde tak — wat je pas merkt
als je die kant op klikt. Er draait nu een test die het hele sjabloon op
gebalanceerde tags controleert, naast de bestaande die de JavaScript parseert.
# v0.8.30-beta — Sjablonen kunnen zeggen wat ze nodig hebben, en vijftien domotica-apps
**Een app kon niet zeggen dat hij een andere nodig had.** Het `connect`-veld
wijst naar een andere app en vult een adres in, maar zegt niets over noodzaak.
Daardoor kon je Zigbee2MQTT installeren zonder MQTT-broker, Alertmanager zonder
Prometheus en Grafana Alloy zonder Loki. Alle drie starten ze, doen niets, en de
reden staat nergens.
Sjablonen kunnen dat nu wél zeggen, met `depends_on` in de metadata. Het
installatiescherm toont vóór het invullen wat er ontbreekt, met de reden erbij en
een knop die je meteen naar dat sjabloon brengt. **Advies, geen blokkade** — je
kunt de broker of Home Assistant net zo goed op een andere machine hebben staan.
De reden is verplicht: een waarschuwing zonder uitleg klik je weg.
Meteen toegepast op acht sjablonen waar het gemis tot nu toe onzichtbaar was:
Zigbee2MQTT, Alloy, Alertmanager, node-exporter, cAdvisor, Collabora, Double Take
en Soularr. Bewust níét op Element, open-webui, Exportarr, Unpackerr en ONLYOFFICE:
die werken met meerdere alternatieven, en `depends_on` kent geen "een van deze".
**Vijftien domotica-apps.** Van 12 naar 27 in die categorie — vooral het spul dat
*om* Home Assistant heen hoort:
- **Stemassistent**, tot nu toe volledig afwezig: Piper (tekst naar spraak),
Whisper (spraak naar tekst) en openWakeWord (het activeerwoord). Samen maken ze
de spraakbediening van HA compleet zonder dat er iets naar een clouddienst gaat.
- **Matter Server** en **Matter Hub**, plus **AppDaemon** voor automatiseringen in
Python
- **go2rtc** (camerastreams) en **Double Take** (gezichtsherkenning op Frigate)
- **Domoticz** en **openHAB** als alternatief voor Home Assistant zelf
- Voor de Nederlandse opstelling: **DSMR-reader** leest de P1-poort van je slimme
meter uit, **rtl_433** vangt de 433 MHz-sensoren van weerstations en regenmeters
op en zet ze op MQTT
- **Traccar** (GPS-tracking), **Snapcast** (audio door het hele huis) en **EMQX**
(MQTT-broker met webinterface)
Drie daarvan hebben een fysiek apparaat nodig — een P1-kabel, een SDR-stick, een
Zigbee-dongle. Die krijgen een veld met uitleg over hoe je het juiste pad vindt,
in plaats van een gok die stilzwijgend faalt.
# v0.8.20-beta — Prometheus startte niet, en 23 apps erbij
**Prometheus stond kapot in de catalogus.** Het sjabloon koppelde een lege map op
`/etc/prometheus` en startte met `--config.file=/etc/prometheus/prometheus.yml`
een bestand dat er nooit kwam. Prometheus stopt daar meteen op. En zelfs mét
configuratie viel er niets te meten: er was in de hele catalogus geen
node-exporter en geen cAdvisor. Er zit nu een startconfiguratie bij, met
koppelvelden naar allebei.
Datzelfde patroon zat op meer plekken, en dat is waar de meeste apps van deze
ronde vandaan komen — niet nieuwe categorieën, maar de ontbrekende helft van wat
er al stond:
- **node-exporter**, **cAdvisor** en **Alertmanager** maken Prometheus en Grafana
pas bruikbaar
- **Loki** en **Grafana Alloy** bewaren logs; Dozzle liet alleen zien wat er nú
gebeurt, en na een herstart was de oorzaak weg
- **Unbound** zoekt DNS zelf op. Pi-hole en AdGuard blokkeerden wel reclame, maar
stuurden de rest gewoon door naar Google of Cloudflare
**Domeinen die ontbraken:** evcc (zonnepanelen, thuisbatterij en laadpaal op
elkaar afstemmen), Healthchecks (merkt dat een cronjob níét liep — Uptime Kuma
merkt alleen dat iets uit ligt), Infisical (geheimen voor je applicaties, waar
Vaultwarden voor mensen is), Music Assistant en pgAdmin.
**Verder:** Crafty Controller, Manyfold, ErsatzTV, FitTrackee, ComfyUI, Omada
Controller, en de zwaardere Zabbix, Graylog, Wazuh, NetBox, Seafile en OpenCloud.
Van 218 naar 241 apps. Monitoring gaat van 18 naar 27.
**Een controle die zichzelf tegensprak.** De image-controle draaide met zestien
verzoeken tegelijk en meldde negen ontbrekende lscr.io-images. Alle negen bleken
te bestaan: registries knijpen af, en een afgeknepen verzoek ziet er precies zo
uit als een image dat er niet is. Bijna had ik negen werkende sjablonen
"gerepareerd". Die controle zit nu in `tools/controleer_images.py`, met vier
tegelijk en opnieuw proberen bij twijfel, en draait als stap in CI.
# v0.8.10-beta — Vervangers voor big tech, en sleutels in het juiste formaat
**Een sleutel is niet zomaar een willekeurige string.** De generator maakte
altijd url-veilige base64, en dat is precies wat Laravel, Homarr en LibreChat
níét accepteren. Laravel wil `base64:` ervoor met standaard-base64; Homarr wil 64
hexadecimale tekens; LibreChat wil er twee, van 64 en 32. Een waarde in het
verkeerde formaat levert een container op die niet start, met een foutmelding die
nergens naar de sleutel wijst. Sjablonen kunnen nu `secret_format` (`hex` of
`laravel`) en `secret_bytes` opgeven.
Daarmee vielen twee fouten uit de vorige versie op: **Homarr** en **LibreChat**
kregen sleutels die ze hadden geweigerd. Rechtgezet. En **Snipe-IT** en **Invoice
Ninja**, die vorige ronde afvielen omdat hun APP_KEY niet te genereren was, staan
er nu wel in.
**Negentien vervangers voor big tech.** De catalogus gaat van 197 naar 218 apps:
| In plaats van | Nu |
|---|---|
| Google Agenda en Contacten | Baïkal, Radicale |
| Google Translate | LibreTranslate |
| Google Docs | CryptPad, Collabora Online, ONLYOFFICE |
| Google Forms | Formbricks |
| Google Meet en Zoom | Jitsi Meet |
| YouTube | PeerTube, Invidious, Tube Archivist |
| Twitch | Owncast |
| X (Twitter) | Mastodon |
| Instagram | Pixelfed |
| Reddit | Lemmy |
| Calendly | Cal.com |
| Doodle | Rallly |
| DocuSign | DocuSeal |
| Figma | Penpot |
Communicatie gaat daarmee van 12 naar 16 apps en productiviteit van 21 naar 31 —
de twee hoeken waar zelfhosten het meest oplevert en de catalogus het dunst was.
**Wat er niet in ging.** Bij een paar apps staat een waarschuwing in de
omschrijving in plaats van dat ik doe alsof het vanzelf gaat: Mastodon en PeerTube
leggen hun domein vast zodra andere servers je kennen, CryptPad heeft echt twee
verschillende domeinen nodig om zijn sandbox te laten werken, en LibreTranslate
haalt bij de eerste start zijn taalmodellen op.
# v0.8.00-beta — 55 apps erbij, en drie kapotte images gevonden
**De catalogus stond scheef.** Na het ARR-werk telde media 42 apps en downloaden
19, terwijl communicatie er 4 had, AI 4, financiën 3 en foto's 2. Wie de store
opende voor iets anders dan media, vond het vaak niet. Er zijn nu 55 apps bij, van
142 naar 197, gekozen op wat er in 2026 daadwerkelijk gedraaid wordt.
**Drie nieuwe categorieën:** Statistiek (Umami, Matomo), Zakelijk (Odoo,
FreeScout, EspoCRM) en Spellen (RomM, Minecraft) — hoeken die er helemaal niet
waren.
**Homelab-gereedschap:** Dockge, Komodo, Glance, Gatus, Scrutiny, Backrest,
Headscale, Zoraxy, CrowdSec, NetAlertX, Pocket ID en Authentik.
**Communicatie** gaat van 4 naar 12 met Mattermost, Synapse, Element, Listmonk,
Mumble, Roundcube en Stalwart. **Notities en documenten:** Outline, Trilium,
SilverBullet, Readeck, Docmost en Kiwix. Verder LibreChat, AnythingLLM, LocalAI,
Ghostfolio, Wallos, Maybe, Lychee, Piwigo, Homebox, wger, Donetick, Dawarich,
Gitea, Adminer, code-server, Woodpecker, Semaphore, Opengist, MinIO, Pingvin
Share, Leantime, PrivateBin, Emby en ConvertX.
**Drie apps in de catalogus waren niet te installeren.** De controle die bij het
ARR-werk Huntarr ontmaskerde, is nu over alle 213 images in de catalogus gehaald.
Daaruit kwamen drie stille fouten: Forgejo wees naar `:latest` terwijl dat project
helemaal geen latest-tag publiceert, Planka naar een tag `1` die niet bestaat, en
Baby Buddy naar `ghcr.io/babybuddy/babybuddy` — een pad dat er niet is, terwijl
het sjabloon met zijn PUID/PGID al voor de linuxserver-variant geschreven was. Alle
drie rechtgezet.
**Twee testregels waren te krap.** Een `config.yaml` in `files/` die geen
compose-bestand is, viel tussen wal en schip: hij werd niet als compose gescand en
overgeslagen omdat hij op `.yaml` eindigt — velden die alleen dáár gebruikt werden,
gingen door voor ongebruikt. En een verplicht geheim dat de gebruiker ergens anders
vandaan haalt, stond in een handmatige lijst met veldnamen. Dat is nu een vlag in
het sjabloon zelf: `"eigen_invoer": true`.
**Nieuwe controles.** Zesendertig apps brengen inmiddels hun eigen database mee.
Daarvoor geldt nu: elke extra container draagt de servicenaam als voorvoegsel
(anders botsen twee installaties van dezelfde app), geen enkel databasewachtwoord
staat vast in een sjabloon, en er wacht altijd iemand met `depends_on` op de
database.
# v0.7.90-beta — De ARR-stack uit elkaar, en netwerkopties per container
**Elk *arr-onderdeel is nu een losse app.** Wie alleen Sonarr wil, hoefde eerst
de hele ARR-stack te nemen. Er staan nu zesenveertig losse sjablonen in de store
— Sonarr, Radarr, Prowlarr, Bazarr, Gluetun, en alles daaromheen — en de
gecombineerde stack blijft bestaan als "alles in één keer". Het zijn dezelfde
containers: beide verpakkingen komen uit één bron (`tools/arr_sjablonen.py`) en
een test draait die opnieuw en vergelijkt het resultaat, zodat ze niet stilletjes
uit elkaar groeien. Die test vond meteen zijn eerste geval: los koppelde
qBittorrent zijn interne poort aan de hostpoort die jij koos, wat niet meer klopt
zodra hij achter een VPN zit.
**Hardlinks werkten niet, en dat was niet zichtbaar.** Elke app kreeg losse
mounts — `/tv`, `/movies`, `/downloads`. Voor Sonarr zijn dat twee verschillende
bestandssystemen, ook al staat alles op dezelfde schijf, dus werd élke voltooide
download naar je bibliotheek *gekopieerd* in plaats van gelinkt: traag, tijdelijk
alles dubbel, en je torrent stopte met seeden zodra je opruimde. De beschrijving
waarschuwde wel voor "zet ze op hetzelfde bestandssysteem", maar de mounts maakten
het alsnog onmogelijk. Alles deelt nu één map die als `/data` binnenkomt, met
`UMASK=002` zodat de apps bij elkaars bestanden kunnen.
**Negentien lege kaarten.** De schakelaar van een onderdeel stond hard in de stap
*Instellingen*, terwijl je hem ook in de kop van zijn eigen groep in *Basis* zag
staan: elk onderdeel had twee schakelaars in twee schermen, en *Instellingen*
bestond voor het grootste deel uit kaarten zonder inhoud. Er is nu een eerste
stap **Onderdelen** waar alle schakelaars bij elkaar staan, gegroepeerd per soort
— bij negenenveertig onderdelen scheelt dat nogal. Wat je uitzet verdwijnt uit de
volgende stappen in plaats van als lege kaart te blijven staan.
**Een eigen IP-adres ging per stack, niet per container.** Bij een app met één
container klopt dat. Bij een stack met twintig webinterfaces kreeg de eerste
container het adres en raakten **alle andere hun poortmappings kwijt** zonder er
iets voor terug te krijgen — daarna waren ze nergens meer te bereiken. Je wijst
nu per container een adres toe; wie er geen krijgt, houdt gewoon zijn poort.
Hetzelfde gold voor Pangolin, dat één URL per stack maakte op de eerste poort die
het tegenkwam: dat gaat nu ook per container.
**Huntarr is eruit.** Het project werd in februari 2026 offline gehaald nadat er
onder meer authenticatie-bypasses en API-sleutels in platte tekst waren gevonden;
de auteur heeft de repo verwijderd. Het image dat in het sjabloon stond bestaat
niet meer — een controle langs alle registries bevestigde dat. Vervangen door
NeutArr, de fork van de laatste schone versie. In diezelfde ronde bleek
Maintainerr verhuisd naar een andere organisatie.
**Nieuw in de catalogus:** Overseerr, Cleanuparr, Decluttarr, Janitorr,
cross-seed, Configarr, Profilarr, Byparr, Kometa, Tautulli, Jellystat, Wizarr,
SuggestArr, Trailarr, Lingarr, NZBHydra2, Deluge, Transmission, slskd, Soularr,
Mylar3, Kapowarr, Pulsarr, Prefetcharr, Posterizarr, Notifiarr, Exportarr,
Autopulse, Requestrr, Homarr en NeutArr. Alle images zijn tegen hun registry
gecontroleerd.
**Twee mappen die allebei "data" heetten.** `data_dir` was de map waar een app
zijn eigen spullen bewaart; daar kwam nu een gedeelde `/data` naast. Die heten nu
`appdata_dir` en `data_root`, in alle sjablonen. En qBittorrent stond standaard op
poort 8080, samen met zes andere apps; die staat nu op 8097.
# v0.7.80-beta — De knop Volgende, tijdzones, en netwerken per groepje
**De knop Volgende deed niets.** Alpine haalt een boolean-attribuut alleen weg
bij `null`, `undefined` of `false` — het getal `0` zét het juist. Mijn
`:disabled="stepMissing().length"` gaf bij een compleet ingevuld formulier `0`,
en daarmee stond de knop permanent uit. De logica klopte, de binding niet.
Dat was met losse logica niet te vinden, dus er draait nu een test die de echte
modal met Alpine in een DOM rendert en er daadwerkelijk op klikt. Die vangt
precies dit soort bindingsfouten. Hij slaat zichzelf over waar `jsdom` ontbreekt.
**Tijdzones komen van de server.** In alle 77 sjablonen die er een hebben stond
`Europe/Amsterdam` hard ingevuld — dat klopt alleen toevallig. Er is nu een
instelling **Tijdzone** die standaard de tijdzone van je server overneemt (uit
`TZ`, `/etc/timezone` of `/etc/localtime`), en die wordt als voorstel gebruikt.
De negentien apps zonder tijdzoneveld krijgen `TZ` er automatisch bij, want die
draaiden tot nu toe in UTC en zetten dus overal het verkeerde tijdstip neer.
**Poortcontrole kijkt nu ook naar de host.** `docker ps` toont alleen wat Docker
publiceert; een nginx of DNS-server die rechtstreeks op de host draait bleef
onzichtbaar terwijl je er net zo goed mee botst. Die worden nu meegeteld, via
`/proc/net/tcp` in een hulpcontainer met `--network host`. Lukt dat niet, dan
zegt het antwoord `host_checked: false` — "vrij" is dan een aanname.
**Niet alles hoeft meer aan alles vast.** Er was één gedeeld netwerk waar élke
app aan hing. Je kiest nu per app aan welke netwerken hij meedoet, en kunt er bij
het installeren zelf een maken — met een eigen naam of een voorgestelde
(`su-<appnaam>`). Dat netwerk verschijnt daarna bij elke andere app in de lijst.
Een app kan aan meerdere netwerken tegelijk hangen.
**Advies bij IP-adressen.** Vul je een adres in, dan wordt gecontroleerd of het
binnen het subnet valt, binnen je ingestelde bereik, of het al aan een container
is toegekend, en of de host het recent op het LAN heeft gezien — dat laatste uit
de ARP-tabel, en dat vangt een fysiek apparaat met een DHCP-adres. Klopt er iets
niet, dan staat erbij wát en krijg je een vrij adres voorgesteld.
**Nieuw: Pangolin.** Onder Inloggegevens kun je met één klik een publiek adres
voor een app aanmaken via een [Pangolin](https://github.com/fosrl/pangolin)-tunnel,
zonder poorten open te zetten. Zie [`docs/pangolin.md`](docs/pangolin.md).
> Deze integratie is getest tegen een nagebouwde API, niet tegen een echte
> Pangolin-server — die heb ik hier niet. De API veranderde bovendien in
> versie 1.9 (een resource hing eerst onder een site en staat nu los), dus
> beide routes worden geprobeerd en bij een fout krijg je beide meldingen te
> zien.
2369 tests groen.
---
# v0.7.70-beta — Installeren via een invulmenu, geheimen naar .env
**Het installatieformulier is een invulmenu geworden.** Eén lange lijst werkte
slecht aan beide uiteinden: de mediane app heeft vijf velden in één groep, en de
ARR-stack tweeënvijftig in eenentwintig groepen. Nu zijn er vaste stappen —
Basis, Verbinden, Instellingen, Toegang, Controleren — waarbij een lege stap
wordt overgeslagen. Een eenvoudige app krijgt er dus drie, de ARR-stack vijf.
**Volgende** controleert de verplichte velden van díé stap en noemt wat er nog
leeg is. Tot nu toe kwam die melding pas als je op Installeren drukte. De
stapindicator is klikbaar om terug te springen.
**Geavanceerde opties zitten achter één schakelaar** die op elke stap zichtbaar
is, in plaats van een uitklapblok per groep dat je steeds opnieuw moest zoeken.
Een badge toont hoeveel velden verborgen zijn. Er is ook een schakelaar **Alles
op één pagina** voor wie het oude gedrag wil; beide keuzes worden onthouden.
**Geheimen gaan naar `.env`.** Wachtwoorden en sleutels staan niet meer in het
compose-bestand maar als `${NAAM}` met de waarde in `.env`, geschreven met
rechten `0600`. Poorten, paden en tijdzone blijven leesbaar in compose, zodat je
nog steeds in één bestand kunt zien wat er draait. Ook `.serverup.json` bevat
geen geheimen meer en krijgt `0600` — daar stonden ze tot nu toe wereldleesbaar
in.
Eerlijk over de grens hiervan: compose vult `${...}` in bij het inlezen, dus
`docker inspect` toont de waarde nog steeds. Wat je wint is dat het
compose-bestand deelbaar wordt en dat alles op één afgeschermde plek staat.
`docs/beveiliging.md` legt uit wanneer je verder wilt gaan met `secrets:` van
Compose, en waarom een externe kluis hier een slecht idee is.
**Compose en `.env` zijn nu ook vóór het installeren te bewerken.** De laatste
stap toont het gerenderde resultaat met de waarden die je hebt ingevuld; met
Bewerken pas je het aan en gaat jouw versie mee in plaats van het sjabloon.
Ongeldige YAML wordt geweigerd voordat er een map is aangemaakt. De editor voor
bestaande stacks heeft tabbladen gekregen (je moest hem sluiten en opnieuw
openen om van compose naar `.env` te gaan) en biedt aan te herstarten na
opslaan. De `.env`-editor weigert nu een regel zonder `=` — compose negeert die
stilzwijgend, waarna er ineens een wachtwoord mist — en schrijft een auditregel.
**Nieuw: Inloggegevens per app.** Een knop op de stackkaart toont het adres, de
gebruikersnaam en het wachtwoord, met een oog-knop en een kopieerknop. Bij een
gegenereerd wachtwoord kon je dat nergens meer terugvinden. Apps die je bij het
eerste bezoek zelf een account laten aanmaken (Vaultwarden, Immich) melden dat.
Alleen beheerders kunnen erbij en elk bekijken komt in het auditlog.
**Herstel van een regressie uit v0.7.60.** Ik had de genereerknop toen aan
`generate` gehangen in plaats van aan `secret`, waardoor vijftien velden hun knop
kwijtraakten. Acht daarvan zijn inlogwachtwoorden (InfluxDB, Kimai, Linkding,
Miniflux, PhotoPrism, Pi-hole, Technitium, ChangeDetection) en die hebben hem
terug. De zeven waar een verzonnen waarde juist fout is — een WireGuard-
privésleutel, een Beszel-agentsleutel, tokens uit een andere app — houden geen
knop.
Bij het uitzoeken bleek ook dat ik in v0.7.60 twee dingen verkeerd heb gemeld.
De genereerknop wérkte destijds wel (de vlag werd gezet in `app.py`, niet in
`boilerplates.py` waar ik keek), en er wás al een compose- en `.env`-editor.
De logica van het invulmenu wordt nu getest door de echte component uit
`index.html` in Node uit te voeren; die test slaat zichzelf over waar Node
ontbreekt, zoals in het bouwimage. 2295 tests groen.
---
# v0.7.60-beta — App-instellingen die niet meer stilzwijgend misgaan
Aanleiding: Homepage weigerde na installatie met **"Host validation failed"**.
De standaardwaarde van `allowed_hosts` noemde poort 3001 terwijl het poortveld
op 3002 stond — en `localhost` werkt sowieso niet als je je server op zijn
IP-adres benadert. Dat bleek geen los geval, dus alle 96 sjablonen zijn
nagelopen.
**De dobbelsteenknop bestond niet.** Zeventien apps zeggen in hun uitleg
"gebruik de dobbelsteenknop voor een willekeurige waarde". Die knop hing af van
een veld `secret` dat de backend nooit meestuurde, dus hij verscheen nooit — en
wachtwoorden stonden als gewone leesbare tekst in het formulier. Nu worden
geheimen herkend, en 27 velden die je toch nooit zelf typt (databasewachtwoorden,
JWT- en versleutelingssleutels) worden meteen ingevuld met een willekeurige
waarde. Inlogwachtwoorden krijgen alleen de knop: die moet je zelf noteren.
De knop hangt nu aan "hier past een willekeurige waarde" en niet aan "dit is
geheim" — voor een WireGuard-privésleutel of een token uit een andere app is
iets verzonnens juist fout.
**Verplichte velden werden niet gecontroleerd.** Het formulier zette een
sterretje achter een verplicht veld en hield verder niets tegen: je kon een app
installeren met een leeg databasewachtwoord of een lege sleutel. De container
start dan niet, of draait met een leeg geheim. Dat wordt nu geweigerd, bij
installeren én bij het achteraf wijzigen van instellingen, met de melding welke
velden nog leeg zijn. Velden achter een uitgeschakelde groepsschakelaar tellen
niet mee.
**Acht apps hadden een vast wachtwoord in het sjabloon**: `immich_db_pass`,
`ghost_db_pass` en soortgelijke voor iedereen die het installeert, en `admin` als
beheerderswachtwoord van Grafana en Gotify. De databasewachtwoorden worden nu
gegenereerd; bij Grafana en Gotify moet je zelf een wachtwoord kiezen.
**Wachtwoorden stonden in het installatielog**, dat in de interface zichtbaar is
en vaak in een bugmelding geplakt wordt. Die worden nu gemaskeerd.
Verder rechtgezet:
- `baserow`, `hedgedoc` en `ntfy` hadden dezelfde poortfout als Homepage: de
URL in de standaardwaarde wees naar een poort waar niets luistert, waardoor
links en terugkeer-na-inloggen niet werkten.
- Zes velden stonden in het formulier maar kwamen nergens terecht: `data_dir`
bij Keycloak, MeTube, Miniflux en TeslaMate (die gebruiken een Docker-volume,
dus je gegevens stonden niet waar je dacht), en `puid`/`pgid` bij FreshRSS,
die het image helemaal niet kent.
- De tijdzone van Prometheus werd wel gevraagd maar niet doorgegeven.
Nieuw is `tests/test_app_templates.py`: elke app wordt met zijn
standaardwaarden gerenderd en gecontroleerd op ongedefinieerde variabelen,
velden zonder werking, niet-gedeclareerde volumes, `depends_on` naar een
onbekende service, dubbele containernamen, poortconflicten binnen een stack,
poortnummers in standaardwaarden die niet kloppen met het poortveld, en vaste
wachtwoorden. 2236 tests groen.
---
# v0.7.50-beta — Je hoort het als er iets misgaat
Het onbewaakte deel was de zwakke plek. Geplande backups, updatechecks en de
zelf-update draaien 's nachts zonder toezicht; ging er iets mis, dan kwam dat
alleen in het auditlog terecht. Een backup die drie weken stilletjes faalt,
ontdek je op het slechtst denkbare moment.
**Meldingen** via ntfy, een webhook of e-mail — alle drie met wat er al in
Server Up zat, dus geen extra software. Je krijgt bericht bij een mislukte
backup, een archief dat niet leesbaar blijkt, een vastgelopen geplande taak,
weinig schijfruimte, een beschikbare update en een container die zou moeten
draaien maar dat niet doet. Standaard staan alleen de storingen aan.
Eén bericht per ronde, niet één per stack: bij een volle schijf faalt alles
tegelijk en dan wil je geen twintig meldingen. Bij weinig ruimte en gestopte
containers wordt alleen de overgang gemeld, anders krijg je elk uur hetzelfde.
Er is een testknop die eerst je instellingen opslaat, zodat je test wat je net
hebt ingevuld. Een storing in het meldingskanaal kan de taak die de melding
veroorzaakte nooit alsnog laten omvallen.
**Schijfruimte** wordt nu bewaakt. Vóór elke backup wordt gekeken of het er
redelijkerwijs in past; zo niet, dan wordt hij geweigerd in plaats van
halverwege afgebroken — een half archief ziet er in een lijst compleet uit.
Daarbovenop blijft `DISK_MIN_FREE_GB` gereserveerd, want een volle schijf op een
Docker-host breekt alles en niet alleen Server Up. In de backuplijst staat per
schijf een balkje, dat rood kleurt onder `DISK_WARN_PCT`.
**Backups worden gecontroleerd.** Elk nieuw archief wordt meteen helemaal
uitgelezen: dat controleert de tar-structuur én de gzip-checksum. Die checksum
staat aan het eind, dus een archief dat halverwege is afgebroken valt door de
mand terwijl het in een directorylisting compleet lijkt. Er is ook een diepe
variant die echt uitpakt naar een tijdelijke map, met dezelfde beperkingen als
een echt herstel — de enige manier om te weten dat terugzetten werkt vóórdat je
het nodig hebt. Achter elk archief staat een schildje: groen als het gelezen is,
rood met de foutmelding als het beschadigd is.
**Backups kunnen de deur uit.** Er is een downloadknop, en met
`BACKUP_OFFSITE_DIR` wordt elke nieuwe backup gekopieerd naar een tweede
bestemming: een gemounte schijf, NFS- of SMB-share. Tot nu toe stonden backups
uitsluitend op dezelfde schijf als de data die ze moeten beschermen, en kon je
ze er niet eens afhalen. Het kopiëren gaat via een `.part`-bestand dat daarna
hernoemd wordt, en valt de mount weg — dan bestaat het pad vaak nog als lege map
op je systeemschijf — dan wordt eerst gekeken of er ruimte is, zodat je niet
ongemerkt de verkeerde schijf vult.
Het token en het SMTP-wachtwoord zijn write-only, net als de git- en
registry-tokens: instellen kan, teruglezen niet.
Nieuw: [`docs/meldingen.md`](docs/meldingen.md), en `docs/backups.md` is
uitgebreid met controleren, schijfruimte, de tweede bestemming en downloaden.
Getest tegen een draaiende server met een echte ontvanger voor de meldingen:
een gehalveerd archief wordt herkend, een volle schijf weigert de backup zonder
iets achter te laten, het gedownloade bestand is een uitpakbare tarball, en een
padtruc in de bestandsnaam levert een 400 op. 1064 tests groen.
---
# v0.7.40-beta — Account en bereikbaarheid meteen goed
Na het installeren was je nog niet klaar: je moest zelf naar de webinterface om
een beheerdersaccount te claimen, en stond `BIND` op `127.0.0.1`, dan kón je daar
niet eens bij zonder eerst `.env` aan te passen. Het script regelt nu allebei.
**Beheerdersaccount.** Op een terminal vraagt het script of je er meteen één wil,
en zo ja om een naam en wachtwoord (twee keer, met de echo uit). Automatisch gaat
het met `--admin NAAM` plus `--admin-password-file PAD` of `SU_ADMIN_PASSWORD`.
Is er geen wachtwoordbron én geen terminal, dan maakt het script er zelf een van
24 tekens en zet die op het scherm.
Het wachtwoord kan bewust *niet* als optie mee: `--admin-password` weigert met
uitleg. Opdrachtregelargumenten zijn voor elke gebruiker op de server zichtbaar
met `ps` en blijven in je shell-geschiedenis staan. Onderweg naar de server gaat
het via stdin naar `curl`, niet als argument.
Nieuw is ook **`--create-admin`**: alleen het account aanmaken, bij een
installatie die al draait. Voor als het de eerste keer is misgegaan of je het
hebt overgeslagen. De poort komt daarbij uit `.env`.
**Bereikbaarheid.** Het script vraagt nu waarop de interface moet luisteren:
alleen deze server, het hele netwerk (met het gedetecteerde adres erbij), of een
adres dat je zelf opgeeft. Installeer je opnieuw over een bestaande map, dan is
je huidige instelling het uitgangspunt en houdt enter die vast.
Daarbij zat een fout die je zelf tegenkwam: een bestaande `.env` bleef *altijd*
ongemoeid, ook als je `--bind` meegaf. De installatie leek dan te lukken terwijl
de server op het oude adres bleef luisteren. Nu wordt doorgevoerd wat je
expliciet meegeeft, en blijft de rest staan. Andersom rekent het script bij
`--update` voortaan met wat er écht in `.env` staat: draaide je op poort 8080,
dan wachtte het daarvoor op poort 5000 en meldde het het verkeerde adres.
Verder: `--dry-run` riep `sudo docker` aan en vroeg dus om een wachtwoord,
terwijl het net beloofd had niets te doen — zonder terminal liep het daar vast.
Een proefdraai spreekt de Docker-daemon nu niet meer aan. En een ongeldig
bind-adres of een poort buiten 165535 wordt meteen geweigerd in plaats van als
een onbegrijpelijke fout uit `docker compose` terug te komen.
De vragen worden allemaal vooraf gesteld, zodat je kunt weglopen terwijl het
image gebouwd wordt.
Getest met een echte pseudo-terminal voor de vragen, en tegen een echt
draaiende server voor het aanmaken van het account — inclusief een wachtwoord
vol aanhalingstekens en backslashes, een tweede account dat geweigerd hoort te
worden, en een server die niet reageert.
---
# v0.7.30 — Installeren met één regel
```bash
curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install.sh | sh
```
Het script controleert je systeem, biedt aan Docker te installeren als dat
ontbreekt, haalt de code op, maakt `.env` aan, bouwt het image, start de
container en wacht tot `/healthz` antwoordt. Daarna toont het waar je terecht
kunt — met de waarschuwing dat je meteen een beheerdersaccount moet aanmaken.
Opties: `--dir`, `--port`, `--bind`, `--branch`, `--token`, `--update`,
`--uninstall`, `--yes` en `--dry-run`.
**`--dry-run`** toont de hele gang van zaken zonder iets te wijzigen. Handig om
te zien wat er gaat gebeuren voordat je een script met root-rechten loslaat op
je server; `docs/installeren.md` zet die stap dan ook vooraan.
**Bijwerken** gaat met `sh /opt/server-up/install.sh --update`: nieuwe code
ophalen, opnieuw bouwen, herstarten. Je `.env`, stacks en gegevens blijven staan.
**Verwijderen** stopt de container maar laat je gegevens met rust, en vertelt
daarna hoe je die alsnog opruimt.
Het script gebruikt alleen `sudo` waar dat nodig is: kun je zelf al in de
doelmap schrijven én docker aanroepen, dan blijft alles onder je eigen account.
Nieuw: `README.md` verwijst er nu naar, plus `docs/installeren.md` met alle
opties en een probleemoplostabel.
Getest onder `sh`, `dash` en `bash`, met een testsuite die controleert dat er
geen bashismen in sluipen, dat een typefout in de opties netjes stopt, en dat
een proefdraai echt niets aanmaakt. Twee dingen die daarbij naar boven kwamen:
de wachtlus gaf anderhalve minuut lang geen enkel teken van leven (nu puntjes),
en `docker compose --project-directory` zoekt zonder `-f` het compose-bestand
alsnog in de huidige map — wat bij `curl | sh` je thuismap is.
Een derde kwam pas op de bouwserver boven: ontbrak `curl`, dan stopte ook een
proefdraai meteen. Juist dan wil je zien wat er gaat gebeuren, dus dat is nu een
waarschuwing in plaats van een harde fout — net als bij een ontbrekende Docker.
Dit is de eerste stabiele release sinds v0.4.60. Alles uit de v0.5-, v0.6- en
v0.7-beta's zit erin: inloggen met rollen, backups, netwerken met een eigen
IP-adres, 96 apps met categorieën, apps onderling koppelen, instellingen
achteraf wijzigen, en zelf bijwerken. Draaide je nog v0.4.60, lees dan eerst
`docs/beveiliging.md`: daar stond de interface nog open zonder login, en na het
bijwerken moet je meteen een beheerdersaccount aanmaken.
---
# v0.7.20-beta — Instellingen wijzigen, koppelen achteraf, en een defect uit v0.7.00
## 🔴 Gedeeld netwerk botste met `network_mode`
De koppeling uit v0.7.00 hing élke service aan het gedeelde netwerk. Maar een
service met `network_mode` deelt al andermans namespace, en docker compose
weigert die combinatie. Met de standaardkeuze "Verbinden met andere apps" waren
**Homebridge**, **Scrypted**, **Beszel** en de **ARR-stack met Gluetun** daardoor
niet te installeren.
Services met `network_mode` worden nu overgeslagen. Een test past het gedeelde
netwerk toe over alle 96 apps en controleert dat die combinatie nooit ontstaat.
## ⚙️ Instellingen wijzigen na installatie
Je koos bij het installeren netjes een poort, map en wachtwoord — maar daarna was
er geen weg terug naar dat formulier. Voor een andere poort moest je het
compose-bestand met de hand bewerken.
De knop **Instellingen** op elke stackkaart opent hetzelfde formulier, voorgevuld
met wat je destijds hebt gekozen. Opslaan maakt eerst een backup, rendert de
stack opnieuw, valideert en herstart. Loopt het valideren mis, dan wordt de
backup automatisch teruggezet.
Stacks die zijn geïnstalleerd voordat Server Up die keuzes bewaarde, hebben de
knop uit staan met uitleg waarom.
## 🔗 Bestaande stacks alsnog koppelen
Het gedeelde netwerk gold alleen voor nieuwe installaties. Een knop op de
stackkaart zet een bestaande stack er alsnog op, valideert en herstart. Wordt de
compose ongeldig, dan wordt de wijziging teruggedraaid.
## 🔍 Audit-log doorzoekbaar
Het log toonde de laatste tweehonderd regels zonder filter, terwijl er inmiddels
logins, rolwijzigingen, backups en updates in landen. Nu filteren op bron, actie,
status en periode, met vrij zoeken door referentie, details en IP-adres, en
doorbladeren voorbij de eerste honderd.
## 📖 Documentatie
- **`README.md`** — die was er niet. Wat Server Up is, installeren, het eerste
account, en waar je verder moet kijken.
- **`docs/apps-maken.md`** — het volledige templateformaat: velden, geavanceerde
opties, uitleg per veld, apps aan elkaar koppelen, onderdelen optioneel maken
met groepsschakelaars, en de valkuilen (named volumes, poorten, `network_mode`).
- **`docs/README.md`** — index over de zeven documenten.
De changelog-secties voor `v0.5.44`, `v0.5.45` en `v0.5.46` ontbraken; omdat
`release.yml` de notes daaruit haalt, zou een tag op die versies een lege release
opleveren. Aangevuld, met een test die afdwingt dat het huidige `VERSION` altijd
een sectie heeft.
---
# v0.7.10-beta — Categorieën met kleur, in de app store en bij je apps
Alle 96 apps zijn ingedeeld in zeventien categorieën, elk met een eigen kleur en
pictogram. Een app mag in meerdere categorieën zitten: Frigate staat onder
Domotica én Media, Immich onder Foto's én Opslag, Kavita onder Media én
Documenten. Negenendertig apps vallen onder meer dan één.
| | Categorie | | Categorie |
|---|---|---|---|
| 🟣 | Media | 🔵 | Netwerk |
| 🩷 | Foto's | 🔴 | Beveiliging |
| 🟢 | Downloaden | 🟠 | Opslag & backup |
| 🟩 | Domotica | 🩵 | Documenten |
| 🟦 | 3D-printen | 🟦 | Productiviteit |
| 🔷 | Communicatie | 🟩 | Financiën |
| 🌸 | Gezin & huishouden | 🟧 | Monitoring |
| 🩵 | Ontwikkeling | 🟪 | AI |
| ⬜ | Systeembeheer | | |
**In de app store** staat een gekleurde categoriebalk met per categorie het
aantal apps. Klikken filtert; het bestaande zoekveld en de tagfilters werken
ernaast gewoon door. Elke app-kaart toont zijn categorieën als gekleurde badge,
en klikken op zo'n badge filtert er meteen op.
**Bij je geïnstalleerde apps** staat dezelfde balk, met alleen de categorieën die
je daadwerkelijk draait. Elke stackkaart heeft een gekleurde rand in de kleur van
zijn eerste categorie, plus dezelfde klikbare badges.
Apps uit externe repo's (zoals de ChristianLempa-boilerplates) geven geen
categorieën op. Die worden afgeleid uit hun tags en omschrijving, zodat ook zij
een kleur en een plek in het filter krijgen in plaats van als naamloze groep
buiten de indeling te vallen.
Tests bewaken dat elke app minstens één geldige categorie heeft, dat de kleuren
onderling verschillen, en dat er geen categorie in het filter staat waar geen
enkele app onder valt.
---
# v0.7.00-beta — Apps koppelen, eenvoudiger instellen, geavanceerde opties verborgen
## 🔗 Apps kunnen elkaar nu bereiken
Elke stack draaide als eigen compose-project en kreeg daarmee zijn eigen
netwerk. Twee stacks konden elkaar dus niet op naam vinden: Zigbee2MQTT zag de
Mosquitto-broker uit een andere stack simpelweg niet.
Bij het installeren staat nu **"Verbinden met andere apps"** aan. De stack komt
dan op een gedeeld bridge-netwerk (`serverup`), waar containers elkaar op naam
oplossen. Het netwerk wordt aangemaakt zodra de eerste stack het nodig heeft.
## 🎯 Kiezen in plaats van typen
Een veld dat naar een andere app verwijst, toont voortaan een **keuzelijst met
wat je al hebt draaien** in plaats van een leeg tekstveld. Installeer je
Zigbee2MQTT en heb je Mosquitto al staan, dan staat `mqtt://mosquitto:1883` er
meteen ingevuld. Hetzelfde voor Open WebUI dat je Ollama vindt.
Staat de app er nog niet, dan zegt het veld dat — met de mogelijkheid om alsnog
handmatig een adres in te vullen.
## 💬 Uitleg per instelling
Achter elk veld met toelichting staat een **info-icoon**. Klikken opent een blok
met de volledige uitleg en de standaardwaarde. Voor de instellingen die de
meeste vragen oproepen is die uitleg uitgeschreven — waarom je downloadmap op
hetzelfde bestandssysteem moet staan als je mediamap, wat `shm_size` bij Frigate
doet, waarom Caddy poort 80 nodig heeft ook als je alles via HTTPS aanbiedt.
## 🎛️ Geavanceerde opties uit het zicht
Velden die je meestal met rust laat — database-wachtwoorden, PUID/PGID,
USB-paden, bewaartermijnen — zitten nu achter een uitklapblok **"Geavanceerde
opties"** met een teller erbij. Het installatieformulier begint daardoor met
alleen de vragen die er echt toe doen.
Templates kunnen `advanced: true` op een veld of op een hele groep zetten. Een
test bewaakt dat er per app altijd zichtbare velden overblijven, zodat niemand
per ongeluk een volledig leeg formulier maakt.
---
# v0.6.20-beta — 31 apps erbij na een rondgang langs de verzamelsites
Van 65 naar 96 apps. Selectie op basis van awesome-selfhosted, selfh.st,
Perfect Media Server en een paar overzichtslijsten van 2026, aangevuld met wat
er in de bestaande collectie nog ontbrak.
**Monitoring** — Prometheus (de tegenhanger van Grafana, die er al stond),
InfluxDB, Netdata, Beszel
**Reverse proxy** — Caddy, dat zijn certificaten volledig zelf regelt
**Notities en kennis** — Memos, HedgeDoc, Wiki.js, Excalidraw, Karakeep
**Data en dashboards** — NocoDB, Baserow, Metabase
**Identiteit** — Authelia en Keycloak, allebei bruikbaar met de
reverse-proxy-SSO van Server Up zelf
**Media** — Komga, PhotoPrism, MeTube, Pinchflat
**Meldingen** — ntfy
**Productiviteit** — Planka, Focalboard, Kimai
**Locatie en voertuig** — OwnTracks, TeslaMate
**Hulpmiddelen** — Shlink, CyberChef, LanguageTool, Kopia
**AI** — Ollama en Open WebUI, allebei volledig lokaal
De images van de nieuwere apps (Beszel, Karakeep, Pinchflat) zijn nagetrokken
bij de bron in plaats van uit het hoofd opgeschreven; van Karakeep is de
officiële compose overgenomen, inclusief de meilisearch- en chrome-containers
die hij nodig heeft.
Vijftien poortbotsingen met bestaande apps zijn automatisch rechtgezet. Caddy
deelt 80 en 443 met Traefik en Nginx Proxy Manager — alternatieven voor dezelfde
taak, dus dat staat in de allowlist.
---
# v0.6.10-beta — ARR-stack met een schakelaar per onderdeel
De ARR-stack installeerde altijd dezelfde zeven containers. Nu kies je per
onderdeel wat je wilt, uit twintig:
**Media beheren** — Sonarr (series), Radarr (films), Lidarr (muziek), Readarr
(boeken), Whisparr
**Indexers en ondersteuning** — Prowlarr, Jackett, FlareSolverr, Bazarr
(ondertitels)
**Downloaden** — qBittorrent, SABnzbd, NZBGet, en Gluetun om al het
downloadverkeer door een VPN te leiden
**Verzoeken en aanvullingen** — Jellyseerr, Recyclarr (TRaSH-profielen),
Unpackerr, Tdarr (hercoderen), Autobrr, Maintainerr, Huntarr
Standaard staan Sonarr, Radarr, Prowlarr, Bazarr, qBittorrent en Jellyseerr aan;
de rest zet je zelf bij.
## Gluetun schakelt de downloadclients om
Zet je Gluetun aan, dan verhuizen qBittorrent, SABnzbd en NZBGet naar diens
netwerknamespace en verliezen ze hun eigen poortmapping — precies zoals het
hoort, want anders lekt hun verkeer om de VPN heen. Hun webinterfaces bereik je
dan via de poorten van de Gluetun-container.
Daarbij zat een val: qBittorrent en SABnzbd luisteren allebei standaard op 8080,
en achter één VPN-container botsen ze. qBittorrent draait nu intern op 8090.
## Groepsschakelaars werkten niet
Een groepsschakelaar in template.json was zelf geen variabele en kreeg dus nooit
een startwaarde. Elk schakelbaar onderdeel stond daardoor standaard uit — een
stack met schakelaars zou leeg binnenkomen. Ze worden nu als veld meegegeven met
een instelbare standaardstand, en de interface toont ze alleen als schakelaar in
de kop van de groep.
---
# v0.6.00-beta — 29 apps erbij, en een bug die zes bestaande apps onstartbaar maakte
## 🐛 Named volumes verdwenen uit het compose-bestand
Bij het bouwen van de nieuwe apps liep ik tegen iets aan dat er al langer zat.
`_drop_empty_mappings` ruimt sleutels op die een `<% if %>`-blok leeg
achterlaat. Maar een named volume wordt gedeclareerd als een kale sleutel zonder
waarde:
```yaml
volumes:
immich_pgdata:
```
Die werd als "lege mapping" opgeruimd, waarna ook het bovenliggende `volumes:`
wegviel. De services verwezen vervolgens naar een volume dat nergens meer
gedeclareerd stond, en `docker compose up` weigert dat met *"refers to undefined
volume"*.
**Immich, Ghost, Miniflux, Paperless-ngx, Unifi Network en Vikunja waren
daardoor niet installeerbaar via Server Up.** Alleen bekende compose-sleutels
worden nu opgeruimd; een volumedeclaratie blijft staan.
## 📦 29 nieuwe apps
**Domotica** — Zigbee2MQTT, Mosquitto MQTT, ESPHome, Node-RED, Z-Wave JS UI,
Homebridge, Scrypted, Frigate NVR
**3D-printen** — Bambuddy (Bambu Lab, zonder cloud), OctoPrint, Spoolman
**Gezin en huishouden** — Baby Buddy, Grocy, Tandoor Recipes, Firefly III
**Media** — Audiobookshelf, Navidrome, Kavita, Calibre-Web, Jellyseerr
**Kennis** — BookStack, FreshRSS, Wallabag
**Netwerk en beheer** — WireGuard Easy, Homepage, SearXNG, Duplicati,
Watchtower, Technitium DNS
Daarmee staat de teller op 65 apps.
## ✅ Testsuite over de echte templates
Nieuwe `tests/test_apps.py` rendert **elke** app met de echte engine en
controleert de uitkomst: geldige YAML, elke service heeft een image of build,
named volumes zijn gedeclareerd, geen onvervangen `<< variabelen >>`, en geen
onbedoeld dubbele standaardpoorten. Die laatste vond zeven botsingen tussen de
nieuwe apps en bestaande; die zijn rechtgezet. Botsingen die logisch zijn
(Traefik en Nginx Proxy Manager op 80/443, AdGuard en Pi-hole op 53) staan met
uitleg in een allowlist.
---
# v0.5.50-beta — Containers die nergens te vinden waren
Het dashboard meldde 13 actieve containers en 17 images, terwijl je die nergens
terugzag. Dat kwam doordat de cijfers en de lijst uit twee verschillende bronnen
komen:
* de **tellers** komen van `docker info` en gaan over de hele Docker-daemon
* de **stacklijst** toont alleen mappen in `LIBRARY_DIR` met een compose-bestand
Alles wat je buiten Server Up om had gestart — je Forgejo, de act_runner,
containers van vóór je Server Up ging gebruiken — telde dus wel mee maar was
onzichtbaar en onbedienbaar. Een beheertool die dingen telt die hij niet toont,
is verwarrend.
## Wat er nu is
- **Overzicht van alle containers op de host.** Onder de stacklijst staat een
blok "Overige containers" met alles wat niet bij een beheerde stack hoort,
inclusief image, poorten en compose-project. Starten, stoppen, herstarten en
logs bekijken kan direct.
- **De dashboardteller vertelt nu het hele verhaal**: onder "actief" staat
hoeveel daarvan via Server Up loopt.
- De eigen container is gemarkeerd als "deze app" en kan niet via dit blok
gestopt worden — daarvoor is de herstartknop in de instellingen, die dat
netjes afhandelt in plaats van zichzelf halverwege een verzoek te stoppen.
- Een viewer ziet de lijst wel maar kan niets bedienen.
## Verholpen tijdens het bouwen
Containers zonder poorten én zonder compose-labels vielen weg uit de lijst als
ze toevallig als laatste stonden: `strip()` haalde de lege velden aan het eind
van de uitvoer weg, waarna die regel te weinig kolommen leek te hebben. Gevonden
door de test die precies zo'n container als laatste zet.
---
# v0.5.46-beta — Deploy strandde op een .env van een andere gebruiker
De stap die instellingen klaarzet faalde op `touch: cannot touch '.env':
Permission denied`. Het bestand was met sudo aangemaakt en dus van root, terwijl
de runner als gewone gebruiker draait — waarmee de hele deploy afbrak op iets
dat geen blokkade hoort te zijn.
- `SU_TAG` gaat nu altijd via de omgeving naar `docker compose`. Wegschrijven in
`.env` blijft de voorkeur omdat het een handmatige `docker compose up`
overleeft, maar is niet meer nodig om te kunnen deployen.
- Is `.env` niet schrijfbaar, dan meldt de log wie de eigenaar is en welk
`chown`-commando dat rechtzet, en loopt de deploy door.
---
# v0.5.45-beta — Wizard-knop toegevoegd
De wizard was alleen zichtbaar bij een verse installatie: zodra hij een keer was
afgerond, was er geen enkele manier meer om hem te openen. De vertaalsleutels
`wizard_open` en `wizard_reset` bestonden al en `/api/wizard/reset` werkte ook,
maar de knop is nooit gebouwd — ook niet in v0.4.60.
- Blok **Setup Wizard** onder Instellingen met "Wizard openen" en "Opnieuw
uitvoeren".
- Sluitknop in de wizard zelf; die kon je eerder alleen verlaten door hem
helemaal af te ronden.
- De staptitels lopen nu via `t()` in plaats van harde Nederlandse teksten.
---
# v0.5.44-beta — Gedetecteerd netwerk verdween uit het formulier
Zodra je een netwerk had toegevoegd, was het blok "gevonden op deze server" leeg
bij een volgende poging: subnetten die al geconfigureerd waren werden uit de
suggesties gefilterd. Dat pakt verkeerd uit — je kunt prima een tweede netwerk op
hetzelfde subnet willen met een andere range, en zonder dat blok lijkt het
formulier kapot.
- Gedetecteerde netwerken blijven staan en tonen "al toegevoegd" in plaats van
te verdwijnen.
- Levert de detectie niets op zonder dat er een fout is, dan staat er nu uitleg
in plaats van een leeg blok.
---
# v0.5.43-beta — Van-tot omrekenen naar een CIDR-blok
"Ik wil van 10.0.20.200 tot .254" is een volstrekt redelijke wens, maar bestaat
niet als CIDR-notatie: dat zijn 55 adressen, en Docker accepteert alleen
uitgelijnde blokken van 2, 4, 8, 16 … adressen.
Onder het IP-range-veld zit nu een **van-tot-hulp**. Vul de eerste en laatste
gewenste adressen in en Server Up rekent uit welke blokken in de buurt komen:
* **dekt alles** — het kleinste blok dat je hele wens omvat (pakt er onderaan
wat bij)
* **past binnen** — het grootste blok dat volledig binnen je wens valt
Bij `.200` t/m `.254` levert dat `10.0.20.192/26` op: 62 adressen, `.193` t/m
`.254`. Klikken vult het veld in.
---
# v0.5.42-beta — Netwerk toevoegen: twee blokkades weg
Een netwerk aanmaken lukte niet, met wisselend gedrag: soms een foutmelding dat
de IP-range buiten het subnet lag terwijl dat aantoonbaar niet zo was, soms geen
melding maar toch geen resultaat. Het bleken twee losse oorzaken.
## De foutmelding die niet klopte
De beoordeling gaat af bij elke toetsaanslag. Tijdens het intypen van `/28` ga
je langs `/2`, en `10.0.20.240/2` normaliseert naar `0.0.0.0/2` — inderdaad
buiten je subnet. Kwam dat antwoord ná het antwoord voor de volledige waarde
binnen, dan bleef die foutmelding staan en bleef de knop uitgeschakeld.
Er zit nu een volgnummer op: alleen het antwoord bij de laatste vraag telt. En
de knop wordt niet meer uitgeschakeld op de clientstatus — bij het opslaan
beslist de server, zodat een verouderde beoordeling je nooit kan blokkeren.
## De range die Docker weigerde
`10.0.20.200/28` heeft hostbits gezet. Python leest dat soepel als
`10.0.20.192/28`, maar Docker is streng en antwoordt *"has host bits set"*. Wij
valideerden soepel en stuurden vervolgens de ruwe tekst door, dus de interface
keurde iets goed dat bij het aanmaken alsnog strandde.
Subnet en IP-range worden nu omgezet naar hun canonieke vorm voordat ze naar
Docker gaan én voordat ze worden opgeslagen, met een waarschuwing die laat zien
wat er van je invoer gemaakt is. `.200/28` is trouwens een logische invoer — je
bedoelt "vanaf .200" — maar een /28 begint nu eenmaal op een veelvoud van 16.
---
# v0.5.41-beta — Netwerk toevoegen liep vast tijdens het typen
Een netwerk toevoegen gaf een JSON-parsefout in de browser.
Oorzaak: de live beoordeling die bij elke toetsaanslag afgaat, bouwde een lijst
van alle bruikbare adressen in het bereik. Tijdens het intypen van
`192.168.1.0/24` is de tussenstand `192.168.1.0/2` een volkomen geldig
netwerk — met 1.073.741.824 adressen. Het verzoek liep daarop vast en de browser
kreeg geen JSON meer terug.
- `review()` rekent eerste adres, laatste adres en aantal nu uit in plaats van
ze op te sommen. Antwoord binnen 0,05 seconde bij elk prefix.
- `suggest_range()` had hetzelfde probleem: die somde alle /28-blokken op, wat
bij een `/8` ruim een miljoen blokken zijn. Rekent nu van achteren naar voren.
- Waarschuwing bij een bereik van meer dan 4096 adressen, want dat is vrijwel
altijd een typefout in het prefix.
- Ook `.env` van de deploy-map werd bij elke deploy gewist door `rsync --delete`,
omdat het bestand in `.gitignore` staat en dus niet in de checkout zit. Daardoor
raakte je `BIND` telkens kwijt. `--exclude='.env'` toegevoegd.
- Nieuw `.env.example` met alle instellingen bij elkaar en uitleg per sleutel.
---
# v0.5.40-beta — Netwerken instellen zonder uitzoekwerk
## 🔍 Automatische detectie
Bij **Instellingen → Netwerken → Netwerk** toont Server Up nu bovenaan het
netwerk dat je server zelf gebruikt: interface, subnet, gateway en een
voorgestelde vrije IP-range. Eén klik vult het hele formulier.
De detectie start heel kort een container die de netwerknamespace van de host
deelt en leest daar de routetabel. Dat is nodig omdat Server Up in een eigen
namespace zit: de vorige versie las `/sys/class/net` binnen de container en
toonde daardoor de bridge-interface van de container zelf in plaats van de
netwerkkaart van de host — een lijstje dat er goed uitzag maar niet klopte.
Lukt detecteren niet (geen docker-socket, ouder image), dan staat er een nette
melding en vul je het als vanouds handmatig in.
## 💡 Live meedenken bij het invullen
Terwijl je typt controleert Server Up de combinatie en toont wat het oplevert:
*"14 adressen beschikbaar: 192.168.1.241 tot en met 192.168.1.254"*. Klopt er
iets niet, dan staat er in gewone taal bij waarom — een range buiten het subnet,
een gateway die je beter buiten het bereik kunt houden, of een range zo klein
dat er maar een paar stacks in passen.
Een knop stelt een vrij blok voor op basis van het subnet, met de gateway
ontweken.
## 📖 Uitleg waar je hem nodig hebt
In het formulier zit een uitklapbaar blok dat uitlegt hoe subnet, gateway,
IP-range en host-interface zich tot elkaar verhouden, met een concreet voorbeeld.
`docs/netwerken.md` heeft dezelfde uitleg met een schema van de adresverdeling,
een tabel met veelgebruikte ranges, en één punt dat er echt uit moet springen:
**het instellen van de IP-range in Server Up doet niets aan je router** — je moet
daar zelf het DHCP-bereik verkleinen, anders blijven dubbele adressen mogelijk.
---
# v0.5.30-beta — Rollen, app-store-filter en twee planningsfouten
## 👥 Rollen per gebruiker
Tot nu toe had elk account volledige toegang. Nu drie rollen:
| Rol | Mag |
|---|---|
| **Beheerder** | alles |
| **Operator** | stacks, containers en backups beheren — niet de instellingen, gebruikers, repo's, netwerken of modules |
| **Alleen lezen** | alleen bekijken |
- Afgedwongen in de `before_request`-guard, niet alleen in de interface.
- Je eigen wachtwoord wijzigen mag iedereen, ongeacht rol.
- De laatste beheerder kan niet gedegradeerd of verwijderd worden.
- Wie via de reverse proxy binnenkomt zonder eigen account krijgt de rol uit
`AUTH.proxy_role`; bestaat er wél een lokaal account met die naam, dan wint
dat account.
- Accounts van vóór deze versie hebben geen rol en gelden als beheerder.
## 🔒 Rechtenescalatie verholpen
Bij het wijzigen van een wachtwoord werd het hele gebruikersrecord vervangen,
waardoor het `role`-veld wegviel. Omdat een ontbrekende rol als beheerder geldt
(nodig voor bestaande accounts), kon **elke operator of viewer zichzelf tot
beheerder promoveren door zijn eigen wachtwoord te wijzigen.** Gevonden bij het
naspelen van de rollenflow; de rol blijft nu behouden en een test dekt het af.
## ⏰ Geplande backups draaiden nooit
De scheduler stempelde ook taken af die zichzelf hadden overgeslagen. De
backup-taak bewaakt zelf of het het ingestelde uur is, dus die werd afgestempeld
op het moment van de eerste tick — waarna het volgende moment 24 uur later op
precies dat verkeerde tijdstip viel. Resultaat: de geplande backup kwam nooit
aan de beurt.
Een taak stempelt nu alleen af als hij `True` teruggeeft. De backup-taak draait
daardoor op een kort interval en bepaalt zelf wanneer het zover is, zodat een
gewijzigde planning ook meteen werkt zonder herstart. Voor wekelijkse backups is
`BACKUP_SCHEDULE_DAY` toegevoegd.
## 🔎 Zoeken en filteren in de app store
Zoekveld over naam, omschrijving en tags, plus klikbare tagfilters (de veertien
meestgebruikte, op frequentie gesorteerd). Repo's zonder treffers vallen weg.
## 🔧 Overig
- Opstartcontrole op de meegeleverde front-end-bestanden. Mislukt het downloaden
tijdens de image-build, dan laadde de interface zonder opmaak terwijl de
server prima leek te draaien; nu staat er een duidelijke melding in het log.
- 227 tests.
---
# v0.5.20-beta — Vertalingen, SSO-scherm, complete backups en app-updates
## 🌍 Vertalingen en SSO
- De schermen uit v0.5.x (login, Beveiliging, Netwerken, Updates,
containerpaneel) gebruikten harde Nederlandse teksten terwijl de rest van de
interface via `t()` loopt. Op Engels gaf dat een mengelmoes. Nu volledig
vertaald: 208 sleutels in `nl` en `en`.
- `t()` ondersteunt plaatshouders: `t('port_taken', {port: 8080})`. Zo blijven
zinnen heel in plaats van in losse stukjes geknipt.
- Nieuw instelscherm voor **reverse-proxy-SSO** onder Instellingen →
Beveiliging: modus lokaal/proxy/beide, identiteitsheader en de vertrouwde
proxy-adressen. De backend bestond al maar was alleen met `curl` te bereiken.
- Nieuwe test bewaakt dat `nl` en `en` dezelfde sleutels houden, dat de
plaatshouders in beide talen gelijk zijn, en dat de interface geen sleutels
gebruikt die nergens gedefinieerd staan.
## 💾 Backups compleet
Backup was één `tar`-commando waarvan de returncode genegeerd werd — een
mislukte backup gold als succes, en terugzetten kon alleen met de hand.
- **Terugzetten** met één klik: stack stoppen, huidige map opzij, uitpakken,
starten. Mislukt het uitpakken, dan komt de oude situatie terug.
- **Bewaarbeleid**: aantal per stack en/of maximale leeftijd. De nieuwste
backup van een stack blijft altijd staan.
- **Geplande backups**, dagelijks of wekelijks, via een eigen planner in de
applicatie (geen cron). Een gemiste ronde loopt bij de eerstvolgende
gelegenheid alsnog.
- **Automatisch een backup vóór het bijwerken of verwijderen** van een stack.
Mislukt die, dan gaat de actie door — anders kun je een kapotte stack niet
meer opruimen.
- Uitpakken gebeurt met `tarfile` en `filter="data"`: absolute paden en
`..`-ingangen worden geweigerd. Met een kaal `tar xzf` kon een geprepareerd
archief buiten de stackmap schrijven.
- Backups binnen dezelfde seconde overschreven elkaar; namen krijgen nu een
teller.
- `docs/backups.md` legt uit wat er wél en niet in zit — de compose- en
`.env`-bestanden, **niet** de Docker-volumes met de eigenlijke appdata.
## 📦 Updates per app
- Server Up controleert nu ook of er nieuwere images zijn voor de
geïnstalleerde stacks, met een badge op de stackkaart en een knop om te
controleren. Bijwerken doet de bestaande update-knop.
- De vergelijking loopt via de Registry API v2 (`Docker-Content-Digest`), niet
via `docker manifest inspect`: dat laatste geeft per platform een aparte
digest terug terwijl `RepoDigests` de digest van de manifest-list bevat, wat
bij elk multi-arch image permanent "update beschikbaar" zou opleveren.
- Drie statussen per service — `update`, `current` en `unknown`. Een lokaal
gebouwd image, een nog niet gepulld image of een onbereikbare registry levert
`unknown` op en dus géén badge, zodat er nooit ten onrechte een update wordt
gemeld.
- Dagelijkse achtergrondcheck via dezelfde planner; resultaten 6 uur gecached.
---
# v0.5.10-beta — Update-systeem met kanalen en één-klik bijwerken
## 🔄 Kanalen
Server Up kent nu twee update-kanalen in plaats van een vinkje "pre-releases
meenemen":
- **stable** — alleen echte releases (`v0.5.10`)
- **beta** — ook pre-releases (`v0.5.10-beta1`), die `release.yml` automatisch
als zodanig markeert bij een tag met een streepje
Een release telt hoger dan zijn eigen beta (`0.5.10-beta1` < `0.5.10`), dus wie
op beta zit krijgt de definitieve versie alsnog aangeboden. De oude instelling
`UPDATE_INCLUDE_PRERELEASE` migreert automatisch naar het beta-kanaal.
## ⬇️ Bijwerken vanuit de interface
Nieuw: **Nu bijwerken** haalt het image uit je Forgejo container-registry en
vervangt de eigen container.
- Omdat een container zichzelf niet kan hercreëren, doet Server Up alleen het
voorwerk (image ophalen, `SU_TAG` wegschrijven) en laat het de hercreatie over
aan een korte helper-container die op het nieuwe image draait — dat bevat de
docker- en compose-CLI al.
- De deploy-map wordt uitgelezen uit de compose-labels van de eigen container,
niet geraden. Ontbreken die labels, draait de container niet vanaf een
registry-image, of is de docker-socket er niet, dan meldt de interface
precies waaróm bijwerken niet kan in plaats van iets te proberen.
- De vorige tag wordt onthouden, met een **terugrolknop** in de instellingen.
- Voortgang van de `docker pull` loopt via het bestaande job-logvenster; daarna
pollt de browser `/healthz` tot de nieuwe versie leeft.
## 🔧 Overig
- Update-check wordt een uur gecached; **Controleren** forceert een verse check.
Eerder deed elke paginalading een netwerkverzoek.
- Laatst-gecontroleerd-tijdstip zichtbaar in de interface.
- Registry-inloggegevens instelbaar voor een privé registry. Het token wordt net
als de git-tokens nooit teruggegeven door de API (alleen een `has_`-vlag) en
leeg laten betekent "ongewijzigd".
- `docker-compose.yml` gebruikt `${SU_IMAGE:-server-up}:${SU_TAG:-latest}`, zodat
image en tag los instelbaar zijn. Zonder `SU_IMAGE` blijft alles werken zoals
voorheen (lokaal bouwen).
- `deploy-prod.yml` bouwt én pusht het image in dezelfde job wanneer `SU_IMAGE`
ingesteld is. Bewust niet als aparte workflow: bij één runner zou de deploy
wachten op een build die zelf nog in de wachtrij staat.
- `build.yml` is nu alleen handmatig, voor het herbouwen van een specifieke tag.
- `docs/updates.md` toegevoegd: hoe het werkt, de complete Forgejo-instelling
(registry, tokens, variables, secrets, runners), de release-procedure voor
zowel stable als beta, terugrollen en een probleemoplostabel.
---
# v0.5.00-beta — Authenticatie, beveiliging en eigen IP-adressen
> **Let op bij het bijwerken.** Server Up heeft nu een login. Open na het
> bijwerken meteen de webinterface en maak een beheerdersaccount aan — zolang
> dat niet gebeurd is, kan iedereen die de pagina bereikt het account claimen.
> De poort wordt voortaan standaard op `127.0.0.1` gebonden; zet `BIND=0.0.0.0`
> in je `.env` als je er van buiten de server bij moet (liefst achter een
> reverse proxy met TLS — zie `docs/beveiliging.md`).
## 🔐 Authenticatie
Tot nu toe was elke `/api/*`-route open. Omdat Server Up de docker-socket als
root gebruikt, betekende dat: wie de poort kon bereiken, had root op de host.
- Lokale accounts met scrypt-gehashte wachtwoorden, sessiecookie
(`HttpOnly`, `SameSite=Strict`) en lockout na vijf mislukte pogingen.
- Optionele SSO via een reverse-proxy-header (Authelia/Authentik/Cloudflare
Access). De header wordt alleen vertrouwd vanaf een geconfigureerd proxy-IP.
- Loginscherm en eerste-account-setup in de interface; gebruikersbeheer en
wachtwoord wijzigen onder Instellingen → Beveiliging.
## 🛡️ Beveiligingsfixes
- **CSRF**: elke mutatie vereist een `X-CSRF-Token`-header. Routes die de
toestand wijzigen accepteren geen `GET` meer — `/api/docker/restart` was
eerder met een `<img>`-tag vanaf een willekeurige website te triggeren.
- **Path traversal**: `/api/store/install` controleerde de instantienaam niet;
`"instance": "../../…"` schreef buiten de library. Alle stack-, instantie- en
repo-namen lopen nu door één `safe_name()`-validatie.
- **Tokenlek**: git-tokens werden teruggegeven door `/api/repos` en
`/api/settings`. Die zijn vervangen door een `has_token`-vlag; opslaan met een
leeg veld wist het bestaande token niet meer.
- **Git-URL's**: alleen `http(s)://`, `ssh://` en `git@host:pad`. Git's
`ext::`-transport voert een shell-commando uit en wordt nu geweigerd.
- **Templates** renderen in een Jinja2-sandbox (server-side template injection).
- **SSH**: host-keys worden geverifieerd (`accept-new` + `/data/known_hosts`);
eerder stond `StrictHostKeyChecking=no`, waarmee elke MITM onzichtbaar was.
- **Modules**: repo's worden niet meer automatisch bij elke start gepulld
(`AUTO_SYNC_ON_BOOT`, standaard uit) en kunnen per repo op een commit worden
vastgezet.
- Productie-WSGI-server (waitress) i.p.v. de Flask-ontwikkelserver, limiet op
request-grootte, `ProxyFix`, en CSP/`X-Frame-Options`/`nosniff`/
`Referrer-Policy`-headers.
- Tailwind, Alpine en htmx worden meegeleverd in plaats van vanaf een CDN
geladen; Google Fonts is eruit. De interface werkt nu ook offline.
- `config.json` en de sleutel staan op 0600.
## 🌐 Stacks met een eigen IP-adres
Nieuw: geef een stack een eigen adres in je LAN in plaats van poorten op de host
(macvlan/ipvlan). Geen poortconflicten meer, apps op hun eigen standaardpoort, en
je kunt per app firewallen.
- Netwerkbeheer onder Instellingen → Netwerken (driver, host-interface, subnet,
gateway, optionele IP-range).
- Bij het installeren kies je "Poorten op de host" of een netwerk; Server Up
stelt het eerstvolgende vrije adres voor en houdt toegekende adressen vast.
- Het gerenderde compose-bestand wordt automatisch omgezet: poortmappings eruit,
netwerk met `ipv4_address` erin. De templates in `apps/` blijven ongewijzigd.
- Uitleg en valkuilen (waaronder de shim-interface die de host nodig heeft om
zijn eigen macvlan-containers te bereiken) staan in `docs/netwerken.md`.
## 🧩 Beheer per container
De stackkaart klapt uit naar de losse containers: per container starten, stoppen,
herstarten, logs bekijken en live CPU-/geheugengebruik.
## 📦 Veiliger installeren
- Poortvelden krijgen een vrij poortnummer voorgesteld — `next_free_port()`
bestond al maar werd nergens gebruikt. Bezette poorten worden gemeld.
- Velden voor tokens en wachtwoorden krijgen een genereerknop.
- Compose wordt gevalideerd (`docker compose config`) vóór het wegschrijven, dus
een typefout in de editor maakt een draaiende stack niet meer onstartbaar.
## 🔧 Overig
- Testsuite met pytest (112 tests), ook als stap in beide deploy-workflows.
- Audit-log gebruikt één gedeelde SQLite-verbinding — elke job lekte eerder een
file descriptor. Joblogs worden afgekapt op 2000 regels.
- Het `VERSION`-bestand is de enige bron voor het versienummer.
- Lichte `/healthz` voor de healthcheck in plaats van `docker info`.
- `fix-config.sh` verwijderd: bevatte een hardgecodeerd intern IP en
overschreef de configuratie van de gebruiker.
---
# v0.4.60 — Tweecijferig patch-nummer
## Versiebeleid vanaf v0.4.60
Het patch-deel (laatste cijfer) gebruikt nu twee cijfers en loopt in tientallen:
`0.4.60`, `0.4.61`, … `0.4.69`, `0.4.70`. Blijft semver-compatibel (het deel
wordt als geheel getal vergeleken, dus `0.4.60` > `0.4.6`).
---
# v0.4.6 — Kaal versienummer op alle builds
## Wijziging in v0.4.6
De deploy-workflows tonen nu overal het kale versienummer uit het `VERSION`-
bestand (of de tagnaam), zonder `-dev`/`-rc.<sha>`-suffix. Eenvoudiger te lezen;
bump bij elke wijziging gewoon `VERSION`.
---
# v0.4.5 — Releases & in-app update-check
## Nieuw in v0.4.5
### 🏷️ Automatische releases bij een tag
Push je een `v*`-tag, dan maakt de nieuwe `release.yml`-workflow automatisch een
Forgejo-**Release** aan met notes uit de bijbehorende `CHANGELOG.md`-sectie plus
een commit-overzicht sinds de vorige tag. Pre-release tags (bv. `v0.8.4-beta1`)
worden als pre-release gemarkeerd. Zo krijg je een Releases-pagina met duidelijke
changelog per versie, vergelijkbaar met GitHub Releases.
### 🔔 In-app "update beschikbaar"
Server Up vergelijkt de draaiende versie met de laatste release via de Forgejo/
GitHub Releases-API (semver-vergelijking, pre-releases optioneel). Is er een
nieuwere versie, dan verschijnt een melding in de topbar en een blok in
Instellingen → Updates met versie, release-notes en een link. Instelbaar via
`UPDATE_API_URL` (of env `SU_UPDATE_API`) en de optie "pre-releases meenemen".
### 🔖 Versiebeleid
Bump bij elke wijziging het centrale `VERSION`-bestand; tags zijn de bron voor
release-versies (`vX.Y.Z`, of `-beta`/`-rc` voor pre-releases).
---
# v0.4.4 — Nette versienummers op alle builds
## Nieuw in v0.4.4
Centraal `VERSION`-bestand (semver) is nu de bron voor het versienummer. De
deploy-workflows lezen het en bouwen:
- tag `v0.4.4``0.4.4` (productie, toont `v0.4.4`)
- push naar `main``0.4.4-rc.<sha>`
- push naar `dev``0.4.4-dev.<sha>`
Zo zie je voortaan het échte versienummer in de topbar i.p.v. alleen de
git-SHA (`vdev-1eab7c07`). Bump bij elke wijziging alleen nog `VERSION` (en voor
de zekerheid de fallback in `app.py`/`index.html` voor lokale runs).
---
# v0.4.3 — Auto-logo's legacy-apps + fix lege Docker Images
## Nieuw / fixes in v0.4.3
### 🖼️ Automatische logo's voor legacy-apps
Legacy-apps (eigen `app.json`/`stack.json`-stacks) krijgen nu automatisch een
logo via de dashboard-icons CDN, afgeleid uit de app-naam (bv. "Nextcloud" →
`nextcloud.png`). Een expliciete logo-URL in de metadata wint; bestaat het
geraden icoon niet, dan valt de UI terug op het emoji-icoon. Geldt voor zowel
de App Store als geïnstalleerde stacks.
### 🐛 Fix: Docker Images-pagina was leeg
De afbeeldingenlijst gebruikte `:key="img.id"`, maar meerdere tags kunnen
dezelfde image-ID delen → dubbele Alpine-keys waardoor de tabel niet rendert
(en de "geen images"-melding ook niet, want er waren wél images). De `x-for`
gebruikt nu de index als key.
---
# v0.4.2 — App-logo's bij stacks
## Nieuw in v0.4.2
Geïnstalleerde stacks tonen nu het app-logo (of een emoji-icoon als fallback),
zowel op het dashboard als op de Stacks-pagina. Bij installatie wordt het
logo/icoon van de bron-app opgeslagen in `.serverup.json` in de stack-map.
Bestaande installaties krijgen hun logo via een naam-match met de App Store,
dus ze hoeven niet opnieuw geïnstalleerd te worden. De store toonde al logo's
voor boilerplate-apps (via de selfhst/dashboard-icons CDN).
---
# v0.4.1 — Fix: styling werd niet toegepast
## Fix bovenop v0.4.0
De volledige Tailwind-stylesheet faalde stil omdat `.modal` via `@apply` de
eigen CSS-animatieklasse `anim` toepaste. Tailwind's `@apply` accepteert alleen
Tailwind-utilities, geen losse CSS-klassen, waardoor de hele Play-CDN-compilatie
afbrak en de pagina ongestyled (kaal HTML) werd geladen. De animatie staat nu
als gewone CSS-regel (`.anim, .modal { animation: … }`) los van `@apply`.
---
# v0.4.0 — Volledig nieuw UI-ontwerp (Modern SaaS)
## Nieuw in v0.4.0
### 🎨 Compleet herontworpen interface
Volledig nieuw, licht en ruim "Modern SaaS"-ontwerp (Inter-font, indigo accent,
zachte schaduwen, ronde 2xl-kaarten). Nieuwe app-shell: verticale sidebar met
merk-header, gegroepeerde navigatie met actieve indicator en live status-footer;
slanke sticky topbar met dynamische paginatitel en status-chips. Dashboard,
stacks, app store, images-/audittabellen, instellingen, modals, wizard, toasts
en het log-paneel zijn allemaal opnieuw vormgegeven. Inklapbaar menu en grote
touch-targets blijven behouden; volledig mobielvriendelijk.
### 🌗 Thema volgt systeemvoorkeur
Standaard volgt het thema de OS-voorkeur (licht/donker) en reageert live op
wijzigingen. Handmatige override via Auto / Light / Dark in Instellingen.
---
# v0.3.1 — UI-restyle + taalmodule (add-on)
## Nieuw in v0.3.1
### 🎨 Grondige UI-restyle
Grotere, touch-vriendelijke knoppen (min. 44px), ruimere spacing, grotere
typografie en kaarten. Layout gecentreerd met max-breedte voor meer lucht op
grote schermen. Volledig mobielvriendelijk.
### 📐 Inklapbaar menu (ook op desktop)
De hamburger klapt de sidebar nu ook op desktop in/uit; voorkeur wordt onthouden
(`localStorage`), hoofdinhoud en log-paneel schuiven mee. Op mobiel een
tap-to-close overlay.
### 🌐 Taalmodule als add-on
Nieuwe sectie in Instellingen → "Taalmodule": talen toevoegen, bewerken,
verwijderen en ingebouwde talen (NL/EN) dupliceren als startpunt. Editor toont
per sleutel de Engelse referentie met zoekfilter. Backend-endpoints:
`POST/DELETE /api/i18n`, `GET /api/i18n/keys`, `GET /api/i18n/<code>/raw`.
Toegevoegde talen komen als JSON in `translations/` en zijn direct beschikbaar.
---
# v0.3.01 — Boilerplates fixes + git-driven versioning
## Fixes bovenop v0.3.0
### 🔁 Auto-migratie van default-repos voor upgraders
Bestaande installaties (vanaf v0.2.x) hadden de Boilerplates-repo niet zichtbaar
in de App Store omdat hun `config.json` in de `su-data` volume al bestond en de
nieuwe `DEFAULTS["APP_REPOS"]` daardoor werd overschreven.
`core/__init__.py` `load()` doet nu een eenmalige migratie: ontbrekende
default-repos worden bij opstart aangevuld op basis van id, en gemarkeerd in
`MIGRATIONS_DONE: ["v0.3.0_default_repos"]`. Wordt direct gepersisteerd.
Verwijdert een gebruiker de Boilerplates-repo expliciet, dan komt-ie niet
automatisch terug.
### 🧹 YAML-poetsstap na boilerplate-render
Bij stacks waar de meeste optionele groepen uit staan (Authentik, Nextcloud,
etc.) liet de Jinja-render verlaten mapping-sleutels achter (`volumes:`,
`networks:`, etc. zonder kinderen). Docker-compose faalt daarop met *"block
sequence entries are not allowed in this context"*.
`core/boilerplates.py` `_tidy()` heeft nu een `_drop_empty_mappings()` substep
die in meerdere passes mapping-sleutels verwijdert die alleen worden gevolgd
door whitespace/commentaar of een sibling op gelijke/lagere indent. Werkt in
cascade.
### 🏷️ Versie komt nu uit git
`Dockerfile` accepteert `ARG SU_VERSION=dev` en bakt die in `ENV SU_VERSION` +
OCI image-label. Met `docker build --build-arg SU_VERSION=$(git describe ...)`
weet het image zijn eigen versie. De Server Up UI toont automatisch de juiste
waarde, ongeacht wat de productie-compose meegeeft.
Forgejo Actions kan een tag-push automatisch verwerken — zie
`server-up-deploy/README.md`.
## Migratie vanaf v0.3.0
```bash
docker compose build --no-cache
docker compose up -d
```
---
# v0.3.0 — UI rebuild + Boilerplates support
## Hoogtepunten
### 🎨 Nieuwe UI (Tailwind + Alpine + HTMX)
- `templates/index.html` is volledig herschreven. De handgeschreven CSS
(`--bg/--s1/...` variabelen, ad-hoc grid-classes) is vervangen door
Tailwind utility-classes met een gematchte donker/licht-palette.
- Statebeheer via Alpine.js: één `app()` component bovenop het hele document,
geen `$=document.getElementById`-spaghetti meer.
- HTMX is geladen voor toekomstige server-rendered partials. Het bestaande
fetch-RPC patroon blijft werken; HTMX kan progressief worden ingezet.
- Modals, toasts, terminal-overlay en first-run wizard zitten allemaal in
één Alpine-state — geen losse globale variabelen meer.
- Mobiele sidebar gedraagt zich nu correct (slide-in i.p.v. layout-flip).
### 🧩 ChristianLempa Boilerplates ondersteund
Server Up herkent nu twee stack-formaten naast elkaar:
| Formaat | Detectie | Bron |
|---|---|---|
| **Compose** (origineel) | `compose.yml` / `docker-compose.yml` (+ optioneel `stack.json`) | bes-r/server-up |
| **Boilerplate** (nieuw) | `template.json` + `files/` | ChristianLempa/boilerplates-library |
Nieuw bestand `app/core/boilerplates.py`:
- `is_boilerplate(d)` — detectie
- `metadata(d)` — converteert `template.json["metadata"]` (incl. selfhst-icons) naar Server-Up formaat
- `fields(d)` — flattened variable-schema voor de install-modal
- `render_to_dir(src, dest, values)` — rendert `files/*.yaml` met de Jinja-achtige
`<< var >>` + `<%- if expr %>` syntax die de boilerplates gebruiken (Jinja2 met
custom delimiters). Niet-tekst bestanden worden verbatim gekopieerd.
`app/core/git.py``_scan_compose_dirs` herkent beide formaten en zet
`format: "boilerplate"` in de stack-entry zodat de UI er een badge bij kan tonen.
`app/app.py`:
- `_find_stack_src` accepteert ook boilerplate-mappen.
- `POST /api/store/preview` retourneert voor boilerplates het variabelen-schema
(`fields`) plus een gerenderde preview met defaults.
- `POST /api/store/install` met body `{values: {...}}` rendert de templates
voor je voordat de stack gestart wordt.
### ⚙️ Default-repos
Een nieuwe Server Up komt nu uit de doos met twee app-repositories:
1. `bes-r/server-up` — eigen stacks, submap `apps`
2. `ChristianLempa/boilerplates-library` — community templates, submap `compose`
### 📦 Versie / Dockerfile
- `SU_VERSION` = `0.3.0` in `Dockerfile` en `docker-compose.yml`
- `requirements.txt`: Jinja2 expliciet toegevoegd (was al een Flask-dep)
## Migratie vanaf v0.2.29
- Build opnieuw: `docker compose up -d --build --no-cache`
- Bestaande stacks blijven werken (legacy formaat is intact).
- De Boilerplates-repo wordt automatisch toegevoegd voor verse installs.
Bestaande gebruikers kunnen de repo handmatig toevoegen via
Instellingen → Git Repositories met submap `compose`.
## Bekende beperkingen
- Boilerplate-templates met onbekende custom-filters of complexe Ansible-style
conditionals kunnen falen — de fout verschijnt in het terminal-paneel.
- `volumes:` blokken die conditioneel zijn (`<%- if volume_mode == 'local' %>`)
werken; complexere render-logica (loops over services) is nog niet getest.
- HTMX is geladen maar de meeste interacties draaien nog op fetch-RPC. Verdere
migratie naar server-rendered partials kan stapsgewijs in volgende releases.