2 Commits
Author SHA1 Message Date
rothman d0f3206ff4 Release 1.2.0
CI / test-bookworm (push) Successful in 28s
CI / test-latest (push) Successful in 33s
Release / package (push) Successful in 29s
Release / image (push) Successful in 46m7s
2026-11-05 23:26:41 +01:00
rothmanandClaude Opus 5.5 b0840d240e Daynivo names for the files on the SD card and the command line tool
CI / test-bookworm (push) Successful in 25s
CI / test-latest (push) Successful in 31s
- daynivo-config.json, daynivo-authorized_keys and daynivo-network-reset
  on the boot partition; errors go to daynivo-config.error.txt.
- daynivo-kiosk-ctl, with voxip-kiosk-ctl as a symlink to it.
- The voxip-* names keep working, so existing SD cards and instructions
  stay valid; when both config files are present the Daynivo one is
  applied last.
- Internal names (services, /etc/voxip-kiosk, user voxip) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:25:42 +01:00
18 changed files with 109 additions and 77 deletions

No files matched your search

+17 -17
View File
@@ -31,7 +31,7 @@ hardware staat in [`docs/HARDWARE-TEST.md`](docs/HARDWARE-TEST.md).
3. Optioneel, in Raspberry Pi Imager → *OS-aanpassingen*: wifi, gebruiker 3. Optioneel, in Raspberry Pi Imager → *OS-aanpassingen*: wifi, gebruiker
met wachtwoord en/of SSH-sleutel, tijdzone. Zonder aanpassingen heeft de met wachtwoord en/of SSH-sleutel, tijdzone. Zonder aanpassingen heeft de
image **geen** bruikbaar wachtwoord en staat SSH uit. image **geen** bruikbaar wachtwoord en staat SSH uit.
4. Optioneel: zet een `voxip-config.json` op de bootpartitie (zie 4. Optioneel: zet een `daynivo-config.json` op de bootpartitie (zie
[Uitrol bij meerdere klanten](#uitrol-bij-meerdere-klanten)). [Uitrol bij meerdere klanten](#uitrol-bij-meerdere-klanten)).
5. Start de Pi. Bij de eerste boot wordt de rootpartitie automatisch 5. Start de Pi. Bij de eerste boot wordt de rootpartitie automatisch
vergroot tot de hele SD-kaart. Daarna verschijnt een setupscherm met de vergroot tot de hele SD-kaart. Daarna verschijnt een setupscherm met de
@@ -191,14 +191,14 @@ browser blijft draaien. *Chromium herstarten* herstart alleen de browser.
### Commandoregel (via SSH) ### Commandoregel (via SSH)
```bash ```bash
sudo voxip-kiosk-ctl status # overzicht sudo daynivo-kiosk-ctl status # overzicht
sudo voxip-kiosk-ctl setup-code # setupcode voor de eerste configuratie sudo daynivo-kiosk-ctl setup-code # setupcode voor de eerste configuratie
sudo voxip-kiosk-ctl reset-password # wachtwoord vergeten: nieuwe setupcode sudo daynivo-kiosk-ctl reset-password # wachtwoord vergeten: nieuwe setupcode
sudo voxip-kiosk-ctl set-url https://… # URL wijzigen sudo daynivo-kiosk-ctl set-url https://… # URL wijzigen
sudo voxip-kiosk-ctl reload # pagina herladen sudo daynivo-kiosk-ctl reload # pagina herladen
sudo voxip-kiosk-ctl restart-browser # Chromium herstarten sudo daynivo-kiosk-ctl restart-browser # Chromium herstarten
sudo voxip-kiosk-ctl health # healthcheck-rapport sudo daynivo-kiosk-ctl health # healthcheck-rapport
sudo voxip-kiosk-ctl maintenance on|off # healthcheck tijdelijk niets laten herstellen sudo daynivo-kiosk-ctl maintenance on|off # healthcheck tijdelijk niets laten herstellen
journalctl -u voxip-kiosk-browser -u voxip-kiosk-agent -f journalctl -u voxip-kiosk-browser -u voxip-kiosk-agent -f
``` ```
@@ -221,7 +221,7 @@ journalctl -u voxip-kiosk-browser -u voxip-kiosk-agent -f
| `admin_allow_public` | `false` | ook toegang vanaf publieke IP-adressen (afgeraden) | | `admin_allow_public` | `false` | ook toegang vanaf publieke IP-adressen (afgeraden) |
| `auto_update` | `false` | elke nacht een nieuwe release installeren | | `auto_update` | `false` | elke nacht een nieuwe release installeren |
Wijzigingen via de webinterface of `voxip-kiosk-ctl` worden atomair Wijzigingen via de webinterface of `daynivo-kiosk-ctl` worden atomair
opgeslagen, met een `.bak` van de vorige versie. Een URL-wijziging gaat in opgeslagen, met een `.bak` van de vorige versie. Een URL-wijziging gaat in
zonder dat Chromium herstart. Scherminstellingen gelden na een herstart van zonder dat Chromium herstart. Scherminstellingen gelden na een herstart van
Chromium (een vinkje in de webinterface doet dat direct). Chromium (een vinkje in de webinterface doet dat direct).
@@ -231,15 +231,15 @@ Chromium (een vinkje in de webinterface doet dat direct).
Eén image voor alle klanten. Per apparaat zet je na het flashen op de Eén image voor alle klanten. Per apparaat zet je na het flashen op de
bootpartitie (`bootfs`, zichtbaar op Windows en macOS): bootpartitie (`bootfs`, zichtbaar op Windows en macOS):
- **`voxip-config.json`**: instellingen voor deze kiosk, plus optioneel - **`daynivo-config.json`**: instellingen voor deze kiosk, plus optioneel
`hostname` en `wifi_country`. Een voorbeeld staat als `hostname` en `wifi_country`. Een voorbeeld staat als
`voxip-config.example.json` op de bootpartitie. `daynivo-config.example.json` op de bootpartitie.
- **`voxip-authorized_keys`**: openbare SSH-sleutels. SSH gaat dan aan, alleen - **`daynivo-authorized_keys`**: openbare SSH-sleutels. SSH gaat dan aan, alleen
met sleutel-login. met sleutel-login.
- Wifi en een eigen gebruiker stel je in via Raspberry Pi Imager. - Wifi en een eigen gebruiker stel je in via Raspberry Pi Imager.
De bestanden worden bij het opstarten verwerkt en hernoemd naar De bestanden worden bij het opstarten verwerkt en hernoemd naar
`*.applied`, of naar `*.rejected` met de reden in `voxip-config.error.txt`. `*.applied`, of naar `*.rejected` met de reden in `daynivo-config.error.txt`.
### Daynivo-schermen ### Daynivo-schermen
@@ -270,8 +270,8 @@ make dev # http://127.0.0.1:8080 met tijdelijke mappen in .dev/
| Symptoom | Oplossing | | Symptoom | Oplossing |
|---|---| |---|---|
| Zwart scherm na opstarten | `sudo voxip-kiosk-ctl status`. Kijk in de journal van `voxip-kiosk-x`/`voxip-kiosk-browser`. Staat de monitor aan vóór de Pi? | | Zwart scherm na opstarten | `sudo daynivo-kiosk-ctl status`. Kijk in de journal van `voxip-kiosk-x`/`voxip-kiosk-browser`. Staat de monitor aan vóór de Pi? |
| Beheerinterface onbereikbaar na netwerkwijziging | Wacht 3 minuten (automatische rollback) of zet `voxip-network-reset` op de bootpartitie en herstart. | | Beheerinterface onbereikbaar na netwerkwijziging | Wacht 3 minuten (automatische rollback) of zet `daynivo-network-reset` op de bootpartitie en herstart. |
| Wachtwoord vergeten | `sudo voxip-kiosk-ctl reset-password` via SSH, of verwijder `/etc/voxip-kiosk/admin.json` met de SD-kaart in een Linux-pc. | | Wachtwoord vergeten | `sudo daynivo-kiosk-ctl reset-password` via SSH, of verwijder `/etc/voxip-kiosk/admin.json` met de SD-kaart in een Linux-pc. |
| `kiosk.local` werkt niet | Gebruik het IP-adres (staat op het setupscherm en in je router). Windows heeft soms Bonjour/mDNS nodig. Geef iedere kiosk een unieke hostname. | | `kiosk.local` werkt niet | Gebruik het IP-adres (staat op het setupscherm en in je router). Windows heeft soms Bonjour/mDNS nodig. Geef iedere kiosk een unieke hostname. |
| Onderspanning in het dashboard | Gebruik de officiële 5,1 V/2,5 A-voeding. Onderspanning veroorzaakt crashes en SD-kaartcorruptie. | | Onderspanning in het dashboard | Gebruik de officiële 5,1 V/2,5 A-voeding. Onderspanning veroorzaakt crashes en SD-kaartcorruptie. |
+1 -1
View File
@@ -1 +1 @@
1.1.0 1.2.0
File renamed without changes.
+5 -5
View File
@@ -12,7 +12,7 @@ samenwerken en waarom bepaalde keuzes zijn gemaakt.
- Lokale webbeheerinterface (*Daynivo Kiosk Manager*) op poort 8080. - Lokale webbeheerinterface (*Daynivo Kiosk Manager*) op poort 8080.
- Geen klantspecifieke code: alles wat per klant verschilt staat in - Geen klantspecifieke code: alles wat per klant verschilt staat in
`/etc/voxip-kiosk/config.json` en kan bij uitrol via de bootpartitie `/etc/voxip-kiosk/config.json` en kan bij uitrol via de bootpartitie
worden meegegeven (`voxip-config.json`). worden meegegeven (`daynivo-config.json`).
## Overzicht ## Overzicht
@@ -104,7 +104,7 @@ Zolang er geen beheerderswachtwoord is:
adressen van de beheerinterface; adressen van de beheerinterface;
3. kan een wachtwoord alleen ingesteld worden met die code. 3. kan een wachtwoord alleen ingesteld worden met die code.
Wie fysiek bij het scherm staat (of via SSH `voxip-kiosk-ctl setup-code` Wie fysiek bij het scherm staat (of via SSH `daynivo-kiosk-ctl setup-code`
draait) kan het apparaat dus claimen, iemand anders op het netwerk niet. draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
### Netwerkwijzigingen met rollback ### Netwerkwijzigingen met rollback
@@ -119,7 +119,7 @@ draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
5. Valt de stroom uit tijdens die periode, dan zet 5. Valt de stroom uit tijdens die periode, dan zet
`voxip-network-guard.service` de snapshot terug bij het opstarten, nog `voxip-network-guard.service` de snapshot terug bij het opstarten, nog
vóór NetworkManager start. vóór NetworkManager start.
6. Noodherstel: een leeg bestand `voxip-network-reset` op de bootpartitie 6. Noodherstel: een leeg bestand `daynivo-network-reset` op de bootpartitie
verwijdert bij de volgende boot alle Daynivo-netwerkprofielen (terug naar verwijdert bij de volgende boot alle Daynivo-netwerkprofielen (terug naar
DHCP). DHCP).
@@ -165,7 +165,7 @@ draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
(toegestaan: privé, link-local, loopback, CGNAT/VPN 100.64.0.0/10). (toegestaan: privé, link-local, loopback, CGNAT/VPN 100.64.0.0/10).
- Optioneel HTTPS (zelfondertekend certificaat, automatisch aangemaakt). - Optioneel HTTPS (zelfondertekend certificaat, automatisch aangemaakt).
- SSH staat in de image standaard uit en wordt alleen ingeschakeld met - SSH staat in de image standaard uit en wordt alleen ingeschakeld met
credentials uit Raspberry Pi Imager of `voxip-authorized_keys` op de credentials uit Raspberry Pi Imager of `daynivo-authorized_keys` op de
bootpartitie (dan alleen sleutel-login). bootpartitie (dan alleen sleutel-login).
- Strikte security-headers en CSP (`default-src 'self'`). - Strikte security-headers en CSP (`default-src 'self'`).
@@ -192,7 +192,7 @@ daynivo-kiosk-os/
│ ├── netinfo.py netwerkstatus (zonder rechten) │ ├── netinfo.py netwerkstatus (zonder rechten)
│ ├── sysinfo.py CPU, temperatuur, geheugen, opslag, IP │ ├── sysinfo.py CPU, temperatuur, geheugen, opslag, IP
│ ├── services.py systemd- en journal-status │ ├── services.py systemd- en journal-status
│ ├── ctl.py CLI voor SSH-beheer (voxip-kiosk-ctl) │ ├── ctl.py CLI voor SSH-beheer (daynivo-kiosk-ctl)
│ └── web/ Flask-app, templates, statische bestanden │ └── web/ Flask-app, templates, statische bestanden
├── www/ lokale kioskpagina's (start, offline, setup) ├── www/ lokale kioskpagina's (start, offline, setup)
├── systemd/ unit-bestanden ├── systemd/ unit-bestanden
+8 -8
View File
@@ -47,14 +47,14 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
(zie Logboek → Kiosk-agent). (zie Logboek → Kiosk-agent).
- [ ] Screenshot op de Kiosk-pagina toont wat er op het scherm staat. - [ ] Screenshot op de Kiosk-pagina toont wat er op het scherm staat.
- [ ] Daynivo-URL met `?token=`: het token is overal gemaskeerd - [ ] Daynivo-URL met `?token=`: het token is overal gemaskeerd
(dashboard, `voxip-kiosk-ctl status`, `journalctl -u voxip-kiosk-agent`). (dashboard, `daynivo-kiosk-ctl status`, `journalctl -u voxip-kiosk-agent`).
## 4. Herstel ## 4. Herstel
- [ ] `sudo pkill -9 chromium`: binnen 15 s is de pagina terug. Het logboek - [ ] `sudo pkill -9 chromium`: binnen 15 s is de pagina terug. Het logboek
toont de herstart. toont de herstart.
- [ ] `sudo pkill -9 Xorg`: X en Chromium komen binnen 20 s terug. - [ ] `sudo pkill -9 Xorg`: X en Chromium komen binnen 20 s terug.
- [ ] Crashloop: `sudo voxip-kiosk-ctl set-url https://localhost:1/` en - [ ] Crashloop: `sudo daynivo-kiosk-ctl set-url https://localhost:1/` en
daarna 7× `sudo pkill -9 chromium` binnen 2 minuten. De service komt daarna 7× `sudo pkill -9 chromium` binnen 2 minuten. De service komt
in de fouttoestand (start-limit). De healthcheck logt een fout en start in de fouttoestand (start-limit). De healthcheck logt een fout en start
hem na ongeveer 5 minuten opnieuw. Er is geen eindeloze stille lus. hem na ongeveer 5 minuten opnieuw. Er is geen eindeloze stille lus.
@@ -82,7 +82,7 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
- [ ] **Rollback na stroomuitval:** herhaal, trek binnen 3 minuten de stroom - [ ] **Rollback na stroomuitval:** herhaal, trek binnen 3 minuten de stroom
eruit. Na het opstarten zijn de oude instellingen actief. eruit. Na het opstarten zijn de oude instellingen actief.
- [ ] **Noodreset:** stel iets onbereikbaars in en bevestig het. Zet - [ ] **Noodreset:** stel iets onbereikbaars in en bevestig het. Zet
`voxip-network-reset` op de bootpartitie en start: weer DHCP. `daynivo-network-reset` op de bootpartitie en start: weer DHCP.
## 6. Systeem ## 6. Systeem
@@ -104,7 +104,7 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
zonder ingrijpen terug in de kiosk, zonder "herstellen"-melding. zonder ingrijpen terug in de kiosk, zonder "herstellen"-melding.
- [ ] 72 uur continu draaien met verversinterval 5 min. Daarna: geheugen - [ ] 72 uur continu draaien met verversinterval 5 min. Daarna: geheugen
< 80% (dashboard), geen onderspanning, `journalctl -p err -b` zonder < 80% (dashboard), geen onderspanning, `journalctl -p err -b` zonder
terugkerende fouten, `voxip-kiosk-ctl status` alles *Actief*. terugkerende fouten, `daynivo-kiosk-ctl status` alles *Actief*.
- [ ] SD-kaartschrijfacties: `cat /proc/diskstats | grep mmcblk0` op t=0 en - [ ] SD-kaartschrijfacties: `cat /proc/diskstats | grep mmcblk0` op t=0 en
na 1 uur stationair. Het verschil in geschreven sectoren (kolom 10) is na 1 uur stationair. Het verschil in geschreven sectoren (kolom 10) is
minder dan 50 MB per uur. minder dan 50 MB per uur.
@@ -112,11 +112,11 @@ Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
## 8. Uitrol en updates ## 8. Uitrol en updates
- [ ] Nieuwe kaart met `voxip-config.json` (url, device_name, hostname, - [ ] Nieuwe kaart met `daynivo-config.json` (url, device_name, hostname,
rotation) en `voxip-authorized_keys` op de bootpartitie: bij de eerste rotation) en `daynivo-authorized_keys` op de bootpartitie: bij de eerste
boot toegepast, hernoemd naar `.applied`, SSH alleen met sleutel. boot toegepast, hernoemd naar `.applied`, SSH alleen met sleutel.
- [ ] Ongeldige `voxip-config.json` (http-URL): `.rejected` en - [ ] Ongeldige `daynivo-config.json` (http-URL): `.rejected` en
`voxip-config.error.txt` met de reden. De kiosk start met de standaardconfiguratie. `daynivo-config.error.txt` met de reden. De kiosk start met de standaardconfiguratie.
- [ ] `sudo ./update.sh` vanuit een nieuwere checkout: versie in het dashboard - [ ] `sudo ./update.sh` vanuit een nieuwere checkout: versie in het dashboard
verhoogd, configuratie en wachtwoord ongewijzigd, back-up in `/var/backups`. verhoogd, configuratie en wachtwoord ongewijzigd, back-up in `/var/backups`.
- [ ] Systeem → Software-updates → *Nu controleren* met een nieuwere release - [ ] Systeem → Software-updates → *Nu controleren* met een nieuwere release
+5 -5
View File
@@ -4,10 +4,10 @@ Daynivo Kiosk OS — bestanden op deze bootpartitie
Je kunt een kiosk vooraf instellen door vóór de eerste start bestanden op Je kunt een kiosk vooraf instellen door vóór de eerste start bestanden op
deze partitie te zetten (bijvoorbeeld direct na het flashen). Ze worden bij deze partitie te zetten (bijvoorbeeld direct na het flashen). Ze worden bij
het opstarten verwerkt en daarna hernoemd naar *.applied (of *.rejected bij het opstarten verwerkt en daarna hernoemd naar *.applied (of *.rejected bij
een fout; de reden staat dan in voxip-config.error.txt). een fout; de reden staat dan in daynivo-config.error.txt).
voxip-config.json daynivo-config.json
Instellingen voor deze kiosk. Zie voxip-config.example.json. Toegestaan: Instellingen voor deze kiosk. Zie daynivo-config.example.json. Toegestaan:
alle sleutels uit /etc/voxip-kiosk/config.json plus "hostname" en alle sleutels uit /etc/voxip-kiosk/config.json plus "hostname" en
"wifi_country". Voorbeeld: "wifi_country". Voorbeeld:
@@ -18,12 +18,12 @@ voxip-config.json
"rotation": 90 "rotation": 90
} }
voxip-authorized_keys daynivo-authorized_keys
Openbare SSH-sleutels (één per regel). SSH wordt dan ingeschakeld, alleen Openbare SSH-sleutels (één per regel). SSH wordt dan ingeschakeld, alleen
met sleutel-login, voor de beheerder (voxipadmin of de gebruiker die je met sleutel-login, voor de beheerder (voxipadmin of de gebruiker die je
in Raspberry Pi Imager hebt aangemaakt). in Raspberry Pi Imager hebt aangemaakt).
voxip-network-reset daynivo-network-reset
Leeg bestand. Verwijdert bij de volgende start alle via de Leeg bestand. Verwijdert bij de volgende start alle via de
beheerinterface ingestelde netwerkprofielen (terug naar DHCP). Gebruik beheerinterface ingestelde netwerkprofielen (terug naar DHCP). Gebruik
dit als de kiosk na een netwerkwijziging onbereikbaar is. dit als de kiosk na een netwerkwijziging onbereikbaar is.
File renamed without changes.
+1 -1
View File
@@ -20,4 +20,4 @@ EOF_CHROOT
# Instructions next to config.txt for whoever opens the boot partition. # Instructions next to config.txt for whoever opens the boot partition.
install -m 0644 files/voxip-kiosk/image/boot-README.txt "${ROOTFS_DIR}/boot/firmware/VOXIP-LEESMIJ.txt" install -m 0644 files/voxip-kiosk/image/boot-README.txt "${ROOTFS_DIR}/boot/firmware/VOXIP-LEESMIJ.txt"
install -m 0644 files/voxip-kiosk/image/voxip-config.example.json "${ROOTFS_DIR}/boot/firmware/voxip-config.example.json" install -m 0644 files/voxip-kiosk/image/daynivo-config.example.json "${ROOTFS_DIR}/boot/firmware/daynivo-config.example.json"
+4 -2
View File
@@ -138,7 +138,9 @@ rm -rf "$LIB/python.old"
for bin in voxip-kiosk-agent voxip-kiosk-browser voxip-kiosk-healthcheck voxip-kiosk-helper voxip-kiosk-manager voxip-kiosk-updater voxip-kiosk-x; do for bin in voxip-kiosk-agent voxip-kiosk-browser voxip-kiosk-healthcheck voxip-kiosk-helper voxip-kiosk-manager voxip-kiosk-updater voxip-kiosk-x; do
install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin" install -m 0755 -o root -g root "$SRC/bin/$bin" "$LIB/$bin"
done done
install -m 0755 -o root -g root "$SRC/bin/voxip-kiosk-ctl" /usr/local/sbin/voxip-kiosk-ctl install -m 0755 -o root -g root "$SRC/bin/daynivo-kiosk-ctl" /usr/local/sbin/daynivo-kiosk-ctl
# The name from before the rename to Daynivo keeps working.
ln -sfn daynivo-kiosk-ctl /usr/local/sbin/voxip-kiosk-ctl
rm -rf "$SHARE/www" rm -rf "$SHARE/www"
install -d -m 0755 "$SHARE/www" install -d -m 0755 "$SHARE/www"
@@ -340,4 +342,4 @@ if [[ ! -f $ETC/admin.json ]]; then
echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)" echo " Setupcode: $(cat "$ETC/setup-code") (staat ook op het scherm)"
fi fi
fi fi
echo " Status: sudo voxip-kiosk-ctl status" echo " Status: sudo daynivo-kiosk-ctl status"
+3 -3
View File
@@ -1,4 +1,4 @@
"""voxip-kiosk-ctl — command-line management over SSH (run with sudo).""" """daynivo-kiosk-ctl — command-line management over SSH (run with sudo)."""
from __future__ import annotations from __future__ import annotations
@@ -33,7 +33,7 @@ def cmd_status(args) -> int:
print(f"Apparaat: {config['device_name']}") print(f"Apparaat: {config['device_name']}")
print(f"URL: {config['url'] if args.show_secrets else mask_url(config['url'])}") print(f"URL: {config['url'] if args.show_secrets else mask_url(config['url'])}")
print(f"Beheer: {'https' if config['admin_https'] else 'http'}://{os.uname().nodename}.local:{config['admin_port']}") print(f"Beheer: {'https' if config['admin_https'] else 'http'}://{os.uname().nodename}.local:{config['admin_port']}")
print(f"Wachtwoord: {'ingesteld' if paths.admin_file.exists() else 'NOG NIET INGESTELD (setupcode: sudo voxip-kiosk-ctl setup-code)'}") print(f"Wachtwoord: {'ingesteld' if paths.admin_file.exists() else 'NOG NIET INGESTELD (setupcode: sudo daynivo-kiosk-ctl setup-code)'}")
for key, info in services.summary().items(): for key, info in services.summary().items():
print(f"{info['unit']:<34} {info['label']} (herstarts: {info['restarts']})") print(f"{info['unit']:<34} {info['label']} (herstarts: {info['restarts']})")
if agent: if agent:
@@ -138,7 +138,7 @@ def cmd_version(_args) -> int:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="voxip-kiosk-ctl", description="Beheer van Daynivo Kiosk OS vanaf de commandoregel.") parser = argparse.ArgumentParser(prog="daynivo-kiosk-ctl", description="Beheer van Daynivo Kiosk OS vanaf de commandoregel.")
sub = parser.add_subparsers(dest="command", required=True) sub = parser.add_subparsers(dest="command", required=True)
p = sub.add_parser("status", help="toon de status") p = sub.add_parser("status", help="toon de status")
p.add_argument("--show-secrets", action="store_true", help="toon de volledige URL inclusief token") p.add_argument("--show-secrets", action="store_true", help="toon de volledige URL inclusief token")
+28 -21
View File
@@ -33,9 +33,11 @@ UPDATE_UNIT = "voxip-kiosk-update-now"
UPDATER_PATH = "/usr/lib/voxip-kiosk/voxip-kiosk-updater" UPDATER_PATH = "/usr/lib/voxip-kiosk/voxip-kiosk-updater"
ACTIVATE_UNIT = "voxip-network-activate" ACTIVATE_UNIT = "voxip-network-activate"
BOOT_DIRS = (Path("/boot/firmware"), Path("/boot")) BOOT_DIRS = (Path("/boot/firmware"), Path("/boot"))
RESET_TRIGGER = "voxip-network-reset" # Files on the boot partition. The voxip-* names are from before the rename to
IMPORT_CONFIG = "voxip-config.json" # Daynivo and keep working, so existing SD cards and instructions stay valid.
IMPORT_KEYS = "voxip-authorized_keys" RESET_TRIGGERS = ("daynivo-network-reset", "voxip-network-reset")
IMPORT_CONFIGS = ("voxip-config.json", "daynivo-config.json") # applied in this order: the Daynivo name wins
IMPORT_KEYS = ("voxip-authorized_keys", "daynivo-authorized_keys")
SSH_KEY_TYPES = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-") SSH_KEY_TYPES = ("ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-")
# Verbs the web interface may call through sudo. Everything else is only # Verbs the web interface may call through sudo. Everything else is only
@@ -306,15 +308,16 @@ class Helper:
self.network_restore(boot=True) self.network_restore(boot=True)
result["rolled_back"] = True result["rolled_back"] = True
boot = self._boot_dir() boot = self._boot_dir()
trigger = boot / RESET_TRIGGER if boot else None triggers = [boot / name for name in RESET_TRIGGERS if (boot / name).exists()] if boot else []
if trigger and trigger.exists(): if triggers:
stash = self.paths.network_state_dir / f"disabled-{int(time.time())}" stash = self.paths.network_state_dir / f"disabled-{int(time.time())}"
stash.mkdir(parents=True, mode=0o700) stash.mkdir(parents=True, mode=0o700)
for iface in netconfig.INTERFACES: for iface in netconfig.INTERFACES:
src = self._keyfile_path(iface) src = self._keyfile_path(iface)
if src.exists(): if src.exists():
shutil.move(str(src), str(stash / src.name)) shutil.move(str(src), str(stash / src.name))
trigger.unlink() for trigger in triggers:
trigger.unlink()
log(syslog.LOG_WARNING, f"Netwerk-reset uitgevoerd; Daynivo-profielen verplaatst naar {stash}") log(syslog.LOG_WARNING, f"Netwerk-reset uitgevoerd; Daynivo-profielen verplaatst naar {stash}")
result["reset"] = True result["reset"] = True
return result return result
@@ -335,12 +338,14 @@ class Helper:
if boot is None: if boot is None:
return {"imported": []} return {"imported": []}
imported = [] imported = []
conf = boot / IMPORT_CONFIG for name in IMPORT_CONFIGS:
if conf.exists(): conf = boot / name
if not conf.exists():
continue
try: try:
data = json.loads(conf.read_text(encoding="utf-8-sig")) data = json.loads(conf.read_text(encoding="utf-8-sig"))
if not isinstance(data, dict): if not isinstance(data, dict):
raise HelperError("voxip-config.json moet een JSON-object zijn") raise HelperError(f"{name} moet een JSON-object zijn")
data = dict(data) data = dict(data)
host = data.pop("hostname", None) host = data.pop("hostname", None)
country = data.pop("wifi_country", None) country = data.pop("wifi_country", None)
@@ -351,22 +356,24 @@ class Helper:
self.set_hostname(str(host)) self.set_hostname(str(host))
if country: if country:
self.set_wifi_country(str(country)) self.set_wifi_country(str(country))
conf.rename(conf.with_name(IMPORT_CONFIG + ".applied")) conf.rename(conf.with_name(name + ".applied"))
imported.append(IMPORT_CONFIG) imported.append(name)
log(syslog.LOG_NOTICE, "Configuratie geïmporteerd vanaf de bootpartitie") log(syslog.LOG_NOTICE, f"Configuratie geïmporteerd vanaf de bootpartitie ({name})")
except (HelperError, cfg.ConfigError, v.ValidationError, ValueError, OSError) as exc: except (HelperError, cfg.ConfigError, v.ValidationError, ValueError, OSError) as exc:
(boot / "voxip-config.error.txt").write_text(f"{exc}\n", encoding="utf-8") (boot / name.replace(".json", ".error.txt")).write_text(f"{exc}\n", encoding="utf-8")
conf.rename(conf.with_name(IMPORT_CONFIG + ".rejected")) conf.rename(conf.with_name(name + ".rejected"))
log(syslog.LOG_ERR, f"voxip-config.json geweigerd: {exc}") log(syslog.LOG_ERR, f"{name} geweigerd: {exc}")
keys = boot / IMPORT_KEYS for name in IMPORT_KEYS:
if keys.exists(): keys = boot / name
if not keys.exists():
continue
try: try:
self._import_ssh_keys(keys.read_text(encoding="utf-8-sig")) self._import_ssh_keys(keys.read_text(encoding="utf-8-sig"))
keys.rename(keys.with_name(IMPORT_KEYS + ".applied")) keys.rename(keys.with_name(name + ".applied"))
imported.append(IMPORT_KEYS) imported.append(name)
except (HelperError, OSError) as exc: except (HelperError, OSError) as exc:
keys.rename(keys.with_name(IMPORT_KEYS + ".rejected")) keys.rename(keys.with_name(name + ".rejected"))
log(syslog.LOG_ERR, f"SSH-sleutels geweigerd: {exc}") log(syslog.LOG_ERR, f"SSH-sleutels geweigerd ({name}): {exc}")
return {"imported": imported} return {"imported": imported}
def _import_ssh_keys(self, text: str) -> None: def _import_ssh_keys(self, text: str) -> None:
+1 -1
View File
@@ -12,6 +12,6 @@
</label> </label>
<button type="submit" class="btn btn-primary btn-block">Inloggen</button> <button type="submit" class="btn btn-primary btn-block">Inloggen</button>
</form> </form>
<p class="muted small">Wachtwoord vergeten? Log via SSH in en voer <code>sudo voxip-kiosk-ctl reset-password</code> uit.</p> <p class="muted small">Wachtwoord vergeten? Log via SSH in en voer <code>sudo daynivo-kiosk-ctl reset-password</code> uit.</p>
</section> </section>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -94,7 +94,7 @@
<h2>Herstel bij een onbereikbare kiosk</h2> <h2>Herstel bij een onbereikbare kiosk</h2>
<ul class="plain"> <ul class="plain">
<li>Iedere wijziging wordt na {{ rollback_seconds // 60 }} minuten automatisch teruggedraaid als je hem niet bevestigt, ook als de stroom tussendoor uitvalt.</li> <li>Iedere wijziging wordt na {{ rollback_seconds // 60 }} minuten automatisch teruggedraaid als je hem niet bevestigt, ook als de stroom tussendoor uitvalt.</li>
<li>Noodherstel: zet een leeg bestand <code>voxip-network-reset</code> op de bootpartitie van de SD-kaart en start de Pi. Alle Daynivo-netwerkprofielen worden dan verwijderd (terug naar DHCP).</li> <li>Noodherstel: zet een leeg bestand <code>daynivo-network-reset</code> op de bootpartitie van de SD-kaart en start de Pi. Alle Daynivo-netwerkprofielen worden dan verwijderd (terug naar DHCP).</li>
</ul> </ul>
</section> </section>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -4,7 +4,7 @@
<section class="card card-narrow"> <section class="card card-narrow">
<h1>Welkom bij Daynivo Kiosk Manager</h1> <h1>Welkom bij Daynivo Kiosk Manager</h1>
<p>Stel eerst een uniek beheerderswachtwoord in. Je hebt daarvoor de <strong>setupcode</strong> nodig die op het <p>Stel eerst een uniek beheerderswachtwoord in. Je hebt daarvoor de <strong>setupcode</strong> nodig die op het
scherm van de kiosk staat (of via SSH: <code>sudo voxip-kiosk-ctl setup-code</code>).</p> scherm van de kiosk staat (of via SSH: <code>sudo daynivo-kiosk-ctl setup-code</code>).</p>
{% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %} {% if error %}<div class="alert alert-error" role="alert">{{ error }}</div>{% endif %}
<form method="post" class="form"> <form method="post" class="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
+4
View File
@@ -3,6 +3,10 @@ Description=Daynivo configuratie en SSH-sleutels importeren vanaf de bootpartiti
Documentation=file:///usr/share/doc/voxip-kiosk/README.md Documentation=file:///usr/share/doc/voxip-kiosk/README.md
After=local-fs.target After=local-fs.target
Before=voxip-kiosk-manager.service voxip-kiosk-browser.service ssh.service Before=voxip-kiosk-manager.service voxip-kiosk-browser.service ssh.service
ConditionPathExistsGlob=|/boot/firmware/daynivo-config.json
ConditionPathExistsGlob=|/boot/firmware/daynivo-authorized_keys
ConditionPathExistsGlob=|/boot/daynivo-config.json
ConditionPathExistsGlob=|/boot/daynivo-authorized_keys
ConditionPathExistsGlob=|/boot/firmware/voxip-config.json ConditionPathExistsGlob=|/boot/firmware/voxip-config.json
ConditionPathExistsGlob=|/boot/firmware/voxip-authorized_keys ConditionPathExistsGlob=|/boot/firmware/voxip-authorized_keys
ConditionPathExistsGlob=|/boot/voxip-config.json ConditionPathExistsGlob=|/boot/voxip-config.json
+28 -9
View File
@@ -169,35 +169,54 @@ def test_boot_guard_rolls_back_and_honours_reset(helper, paths, boot):
assert not any(c[0] == "nmcli" for c in helper.run.commands), "NetworkManager is not running yet at boot" assert not any(c[0] == "nmcli" for c in helper.run.commands), "NetworkManager is not running yet at boot"
(paths.nm_connections_dir / "voxip-wlan0.nmconnection").write_text("x") (paths.nm_connections_dir / "voxip-wlan0.nmconnection").write_text("x")
(boot / "voxip-network-reset").write_text("") (boot / "daynivo-network-reset").write_text("")
assert helper.network_boot_guard() == {"reset": True} assert helper.network_boot_guard() == {"reset": True}
assert not (paths.nm_connections_dir / "voxip-wlan0.nmconnection").exists() assert not (paths.nm_connections_dir / "voxip-wlan0.nmconnection").exists()
assert not (boot / "voxip-network-reset").exists() assert not (boot / "daynivo-network-reset").exists()
assert old.exists() assert old.exists()
def test_boot_guard_still_honours_the_old_reset_name(helper, paths, boot):
(paths.nm_connections_dir / "voxip-eth0.nmconnection").write_text("x")
(boot / "voxip-network-reset").write_text("")
assert helper.network_boot_guard() == {"reset": True}
assert not (paths.nm_connections_dir / "voxip-eth0.nmconnection").exists()
assert not (boot / "voxip-network-reset").exists()
def test_boot_import_applies_config(helper, paths, boot): def test_boot_import_applies_config(helper, paths, boot):
(boot / "voxip-config.json").write_text(json.dumps({"device_name": "Hal", "url": "https://klant.example.nl/display/hal?token=x", (boot / "daynivo-config.json").write_text(json.dumps({"device_name": "Hal", "url": "https://klant.example.nl/display/hal?token=x",
"hostname": "hal", "rotation": 90})) "hostname": "hal", "rotation": 90}))
assert helper.boot_import() == {"imported": ["voxip-config.json"]} assert helper.boot_import() == {"imported": ["daynivo-config.json"]}
conf = load_config(paths.config_file) conf = load_config(paths.config_file)
assert conf["device_name"] == "Hal" and conf["rotation"] == 90 and conf["url"].startswith("https://klant") assert conf["device_name"] == "Hal" and conf["rotation"] == 90 and conf["url"].startswith("https://klant")
assert (helper.etc / "hostname").read_text() == "hal\n" assert (helper.etc / "hostname").read_text() == "hal\n"
assert (boot / "daynivo-config.json.applied").exists()
def test_boot_import_still_accepts_the_old_names(helper, paths, boot):
(boot / "voxip-config.json").write_text(json.dumps({"device_name": "Oud", "rotation": 180}))
(boot / "daynivo-config.json").write_text(json.dumps({"device_name": "Nieuw"}))
assert helper.boot_import() == {"imported": ["voxip-config.json", "daynivo-config.json"]}
conf = load_config(paths.config_file)
assert conf["device_name"] == "Nieuw" and conf["rotation"] == 180 # the Daynivo file is applied last
assert (boot / "voxip-config.json.applied").exists() assert (boot / "voxip-config.json.applied").exists()
def test_boot_import_rejects_invalid_config(helper, paths, boot): def test_boot_import_rejects_invalid_config(helper, paths, boot):
before = paths.config_file.read_text() before = paths.config_file.read_text()
(boot / "voxip-config.json").write_text(json.dumps({"url": "http://onveilig.example"})) (boot / "daynivo-config.json").write_text(json.dumps({"url": "http://onveilig.example"}))
helper.boot_import() helper.boot_import()
assert paths.config_file.read_text() == before assert paths.config_file.read_text() == before
assert (boot / "voxip-config.json.rejected").exists() assert (boot / "daynivo-config.json.rejected").exists()
assert "HTTPS" in (boot / "voxip-config.error.txt").read_text() assert "HTTPS" in (boot / "daynivo-config.error.txt").read_text()
def test_boot_import_rejects_non_key_lines(helper, boot): def test_boot_import_rejects_non_key_lines(helper, boot):
(boot / "voxip-authorized_keys").write_text("dit is geen sleutel\n") (boot / "daynivo-authorized_keys").write_text("dit is geen sleutel\n")
(boot / "voxip-authorized_keys").write_text("ook geen sleutel\n")
helper.boot_import() helper.boot_import()
assert (boot / "daynivo-authorized_keys.rejected").exists()
assert (boot / "voxip-authorized_keys.rejected").exists() assert (boot / "voxip-authorized_keys.rejected").exists()
+1 -1
View File
@@ -33,7 +33,7 @@ systemctl daemon-reload
echo "==> Bestanden verwijderen" echo "==> Bestanden verwijderen"
rm -rf /usr/lib/voxip-kiosk /usr/share/voxip-kiosk /usr/share/doc/voxip-kiosk rm -rf /usr/lib/voxip-kiosk /usr/share/voxip-kiosk /usr/share/doc/voxip-kiosk
rm -f /usr/local/sbin/voxip-kiosk-ctl rm -f /usr/local/sbin/daynivo-kiosk-ctl /usr/local/sbin/voxip-kiosk-ctl
rm -f /etc/sudoers.d/voxip-kiosk rm -f /etc/sudoers.d/voxip-kiosk
rm -f /etc/systemd/journald.conf.d/voxip-kiosk.conf /etc/systemd/system.conf.d/voxip-watchdog.conf rm -f /etc/systemd/journald.conf.d/voxip-kiosk.conf /etc/systemd/system.conf.d/voxip-watchdog.conf
rm -f /etc/logrotate.d/voxip-kiosk /etc/avahi/services/voxip-kiosk.service rm -f /etc/logrotate.d/voxip-kiosk /etc/avahi/services/voxip-kiosk.service
+1 -1
View File
@@ -44,7 +44,7 @@
li.textContent = u; li.textContent = u;
list.appendChild(li); list.appendChild(li);
}); });
document.getElementById("code").textContent = params.get("code") || "zie: sudo voxip-kiosk-ctl setup-code"; document.getElementById("code").textContent = params.get("code") || "zie: sudo daynivo-kiosk-ctl setup-code";
</script> </script>
</body> </body>
</html> </html>