Betriebshandbuch¶
Alles, was jemand wissen muss, der das Gerät im Museum am Laufen hält. Die Bedienung steht in der Anleitung für das Museumsteam; hier steht die Technik.
Auf einem echten Pi noch nicht erprobt. Die Dateien unter
deploy/pi/sind sorgfältig geschrieben und syntaktisch geprüft, aber nie gelaufen — es gab beim Bauen kein Gerät. Was davon zuerst hakt, gehört in diese Datei, sobald der Pi dasteht.
Einen neuen Pi einrichten¶
Raspberry Pi OS Lite (64 Bit), kein Desktop. Dann:
sudo git clone <repo> /opt/kiekmap
sudo sh /opt/kiekmap/deploy/pi/setup-pi.sh
Das Skript installiert cage, Chromium und Docker, legt den Benutzer kiekmap an, richtet den
Kiosk-Dienst und die USB-Regel ein und schaltet die Bildschirmabschaltung ab. Danach nennt es die
vier Schritte, die es nicht selbst tun kann: .env anlegen, PIN setzen, Kartendaten kopieren,
Container starten.
Kartendaten kommen vom Entwicklungsrechner, nicht vom Pi. make tiles und make places
brauchen Internet und Rechenzeit; auf den Pi gehören nur die Ergebnisse:
rsync -a frontend/public/tiles/ pi:/opt/kiekmap/frontend/public/tiles/
rsync -a data/places.json pi:/opt/kiekmap/data/places.json
Das Wappen kommt denselben Weg. Im Repo liegt nur ein Platzhalter — ein Gemeindewappen darf dort nicht liegen, siehe decisions.md, Punkt 21. Auf dem Gerät gehört das echte hin:
rsync -a wappen.png pi:/opt/kiekmap/frontend/public/logo.png
Danach das Frontend neu bauen (make prod baut die Images ohnehin neu) — die Datei wird beim Bau
in das Abbild aufgenommen, nicht zur Laufzeit gelesen. Das Holmer Wappen liegt unter
~/Developer/Museum/Wappen/holm-wappen.png auf dem Entwicklungsrechner; Quelle und
Rechtelage stehen in adaption.de.md, Abschnitt „Wappen einsetzen".
Was beim Einschalten passiert¶
Etwa 20 Sekunden, in dieser Reihenfolge:
- Docker startet. Die Container laufen mit
restart: unless-stoppedvon selbst hoch. Beim ersten Start nach einem Update laufen die Alembic-Migrationen — deshalb kann er länger dauern. kiekmap-kiosk.servicewartet auf/api/health. Ohne das Warten sähen die ersten Besucher ein paar Sekunden lang eine Fehlerseite — und die bliebe stehen, weil Chromium nicht von allein neu lädt. Nach fünf Minuten startet der Dienst trotzdem: eine Fehlerseite, die jemand sieht und meldet, ist besser als ein schwarzer Bildschirm.cage -- chromium --kioskübernimmt den Bildschirm. Frisches Browserprofil bei jedem Start, damit nach einem Stromausfall nichts von gestern übrig ist.- Stürzt Chromium ab, startet systemd ihn neu (
Restart=always, 5 s Pause).
Woran man erkennt, dass etwas hakt:
systemctl status kiekmap-kiosk # läuft der Kiosk?
journalctl -u kiekmap-kiosk -n 50 # warum nicht?
cd /opt/kiekmap/deploy && docker compose ps
curl -sf http://localhost/api/health && echo " API antwortet"
Wartungsausgang¶
Der Kiosk kennt keine Tastenkombination zum Beenden — das ist Absicht, damit ein Besucher die Ausstellung nicht versehentlich verlässt. Der Weg hinaus geht über SSH:
sudo systemctl stop kiekmap-kiosk # Bildschirm wird schwarz, Dienste laufen weiter
sudo systemctl start kiekmap-kiosk # zurück in die Karte
Für Arbeiten am Gerät selbst genügt meist der Admin-Bereich über das Wappen — Fotos pflegen, hochladen, sichern. SSH braucht man für Updates und Fehlersuche.
Update ohne Internet¶
Auf dem Entwicklungsrechner einen Ordner für den Stick bauen:
make release to=/Volumes/STICK/kiekmap-update
make release to=/Volumes/STICK/kiekmap-update map=1 # falls sich die Region geändert hat
Das Ziel baut beide Abbilder, sichert sie als images.tar und schreibt die version-Datei
daneben. Es bricht ab, wenn der Arbeitsbaum nicht sauber ist oder der passende Tag fehlt — ein
Stick, der zu keinem Commit gehört, ist ein Jahr später nicht mehr zuzuordnen.
Vorher also: make version v=0.9.0, committen, git tag -s v0.9.0 -m v0.9.0.
Von Hand waren das vier Befehle. Der, den man vergisst, schreibt die version-Datei: Die Abbilder
laden, KIEKMAP_VERSION bleibt in der .env stehen, und der nächste Start zieht das alte
Abbild wieder hoch. Das Gerät läuft dann mit der alten Software und sagt es nirgends.
Am Pi:
sudo sh /opt/kiekmap/deploy/pi/update.sh /media/STICK/kiekmap-update
Das Skript liest die Abbilder ein, trägt die Version in die .env, tauscht Kartendaten und
Ortsindex, startet die Container neu und wartet, bis die API antwortet. Der Bestand wird nicht
angefasst — Fotos und Angaben bleiben, wo sie sind.
Zwei Feinheiten stecken darin: Die Kartendaten werden erst danebengelegt und dann umbenannt, damit ein abgebrochenes Kopieren keine halbe Kartendatei hinterlässt. Und der Ortsindex wird ausdrücklich neu eingelesen — beim Start lädt das Backend ihn nur, wenn die Tabelle leer ist.
SD-Karte klonen¶
Die vollständige Sicherung des Geräts, inklusive Betriebssystem. Einmal nach der Einrichtung und nach jedem größeren Update:
# Pi herunterfahren, Karte in den Entwicklungsrechner:
sudo dd if=/dev/rdiskN bs=4m | gzip > holm-pi-2026-07-29.img.gz
Das ersetzt die Sicherung im Admin-Bereich nicht — die läuft im laufenden Betrieb und sichert den Bestand. Der Klon sichert das eingerichtete Gerät.
Bildschirm bleibt schwarz¶
In dieser Reihenfolge:
systemctl status kiekmap-kiosk— läuft der Dienst?journalctl -u kiekmap-kiosk -n 50— meldet cage etwas? „unable to open primary DRM device" heißt: Die Sitzung hat kein Ausgabegerät. Dann fehlt eine der vier ZeilenPAMName,TTYPath,StandardInput,UtmpIdentifierin der Unit, oder der Benutzer ist nicht in den Gruppenvideoundrender.docker compose ps— laufen die Container? Wenn nicht:docker compose logs backend.- Nach zehn Minuten schwarz, obwohl vorher alles lief:
consoleblank=0fehlt in dercmdline.txt(setztsetup-pi.sh, wirkt erst nach einem Neustart).
Fehlersuche kurz¶
| Beobachtung | Erster Verdacht |
|---|---|
| Karte ohne Beschriftung | frontend/public/basemaps/ fehlt — make tiles lief nicht |
| Karte grau, keine Kacheln | frontend/public/tiles/map.pmtiles fehlt oder ist halb kopiert |
| Ortssuche findet nichts | data/places.json fehlt, oder python -m app.cli places lief nicht |
| „Hilf mit" meldet stumm Fehler | Regionsprüfung ohne data/region.json — make tiles legt sie mit ab |
| Anzeige normal, aber nichts lässt sich speichern | Schema veraltet. Seit August 2026 zieht die Wiederherstellung es selbst nach — siehe unten |
| USB-Stick erscheint nicht | udev-Regel oder :rshared — siehe unten |
| Anmeldung lehnt jede PIN ab | KIEKMAP_ADMIN_PIN_HASH leer; der Bereich sagt das im Klartext |
| Importierte Fotos ohne Schlagwort oder Bildnachweis | Eine Einstellung erreicht den Container nicht — siehe unten |
Die PIN für den Admin-Bereich einrichten¶
cd backend && .venv/bin/python -m app.cli pin
Der Befehl fragt die PIN zweimal ab und gibt die Zeile aus, die in die .env gehört. Die PIN
selbst wird nirgends gespeichert; vergessen heißt neu setzen. Danach den Dienst neu starten.
Ist keine PIN eingerichtet, sagt das Zahlenfeld genau das — es lehnt nicht stumm jede Eingabe ab. Nach fünf Fehlversuchen sperrt es für eine Minute. Die Sitzung endet nach 30 Minuten ohne Bedienung; jede Aktion schiebt sie hinaus, und ein Neustart des Dienstes beendet jede Sitzung.
Einstellungen im Containerbetrieb¶
Die .env im Projektverzeichnis ist auch im Betrieb die Stelle, an der Einstellungen stehen. Sie
liegt bewusst nicht im Abbild — das Abbild ist die Software, die .env ist der Ort — und wird
in deploy/docker-compose.yml als env_file eingelesen. Wer dort
etwas ändert, startet danach die Container neu:
cd /opt/kiekmap && docker compose up -d
Die Sprache des Geräts steht ebenfalls hier:
KIEKMAP_LANGUAGE=de # oder en
Sie schaltet Besucheransicht, Verwaltung, Meldungen und Datumsbeschriftung um. Ein neuer Bau ist
nicht nötig — nach dem Neustart der Container gilt der neue Wert. Ein anderer Wert als de oder
en bricht den Start ab, statt still auf Deutsch zurückzufallen; im Protokoll steht dann eine
Zeile von Pydantic. Mehr in adaption.de.md.
Vier Werte setzt die Compose-Datei selbst, und die gewinnen über die .env:
KIEKMAP_DATA_DIR, KIEKMAP_MEDIA_DIR, KIEKMAP_CORS_ORIGINS und der Ort des PIN-Hashes. Sie
beschreiben den Container, nicht den Ort — innen heißen die Verzeichnisse immer /data und
/media, gleichgültig wo sie außen liegen. Ein KIEKMAP_MEDIA_DIR=/Volumes in der .env des
Entwicklungsmacs stört den Betrieb deshalb nicht.
Warum das hier steht: Bis zum 14. August 2026 reichte die Compose-Datei nur einzelne Werte durch. Die übrigen fielen im Container still auf ihre Vorgaben zurück, und das traf ausgerechnet den Import: Fotos kamen an, aber ohne Schlagwort, ohne Bildnachweis und ohne Herkunftsangabe. Nichts schlug fehl, nichts stand im Protokoll. Wer heute eine neue Einstellung einführt, muss nichts weiter tun — sie kommt von selbst durch; geprüft ist das sowohl über den Eingangsordner als auch über den Stapel-Upload der Verwaltung.
USB-Sticks sichtbar machen¶
Raspberry Pi OS Lite hat keinen Desktop und damit keinen Automounter: Ein eingesteckter Stick taucht von allein nirgends auf. Der Admin-Bereich sähe nie einen und meldete ewig „Bitte USB-Stick einstecken".
sudo install -m 755 deploy/pi/kiekmap-usb-mount /usr/local/sbin/
sudo install -m 644 deploy/pi/99-kiekmap-usb.rules /etc/udev/rules.d/
sudo udevadm control --reload
Prüfen: Stick einstecken, dann
ls /media && findmnt /media/*
Zwei Fallstricke stecken darin, beide still:
Der Container sieht den Stick nicht. Ein Docker-Bind-Mount zeigt nur, was beim Start des
Containers schon eingehängt war. Ein später eingesteckter Stick bleibt unsichtbar — ohne
Fehlermeldung, der Ordner ist einfach leer. Dagegen steht :rshared an der Zeile /media:/media
in deploy/docker-compose.yml. Fehlt es, hilft auch kein Neustart
des Containers zur richtigen Zeit.
Der Stick ist da, aber schreibgeschützt. FAT- und exFAT-Sticks kennen keine Besitzer; ohne
uid=1000 beim Einhängen gehören sie root, und der Dienst (UID 1000, siehe
backend/Dockerfile) darf nicht schreiben. Das Skript setzt die Option — der Admin-Bereich
blendet solche Laufwerke aber ohnehin aus, statt einen Knopf anzubieten, der später scheitert.
Auf dem Mac zum Entwickeln: KIEKMAP_MEDIA_DIR=/Volumes in die .env. Ein Prüfvolumen
entsteht mit
hdiutil create -size 200m -fs "HFS+" -volname TESTSTICK teststick.dmg && hdiutil attach teststick.dmg
In
/Volumesliegt immer ein Symlink auf/, benannt nach dem internen Volume — das legt macOS selbst an. Bis zum 14. August 2026 galt er als Datenträger, und die Sicherung landete dahinter, im laufenden Datenverzeichnis. Seither werden Symlinks übersprungen (decisions.md, Punkt 40); die Liste ist auf einem Mac ohne angestecktes Laufwerk jetzt leer, und genau das ist richtig.
Der Schemastand einer zurückgespielten Sicherung¶
Seit dem 15. August 2026 regelt das die Wiederherstellung selbst — dieser Abschnitt beschreibt, wie, und was zu tun ist, wenn doch etwas hakt. Der kurze Weg für das Team steht in der Anleitung.
Warum es überhaupt eine Frage ist. Eine Sicherung enthält kiekmap.db genau so, wie die Datei
damals aussah — mitsamt ihrem Schemastand in der Tabelle alembic_version. Beim Zurückspielen wird
die Datei im Ganzen ausgetauscht (_swap_in in services/backup/restore.py); danach hängt
sich das laufende Programm nur neu an sie (_reopen_database). Migrationen laufen dabei nicht von
selbst: Sie laufen beim Start (backend/docker-entrypoint.sh), und eine Wiederherstellung ist
kein Start.
Was jetzt passiert, und die Reihenfolge ist der ganze Punkt (services/schema.py):
| Die Sicherung ist … | … und dann |
|---|---|
| älter als das Programm | wird nach dem Tausch alembic upgrade head gefahren. Auf dem Balken steht „Der Schemastand wird nachgezogen" |
| auf dem gleichen Stand | passiert nichts, der Aufruf ist folgenlos |
| neuer als das Programm | bricht ab, bevor irgendetwas getauscht ist — der Bestand auf dem Gerät bleibt unangetastet |
Die Ablehnung kommt vor dem Tausch, und das ist keine Feinheit: Eine Sicherung, die dieses Programm nicht lesen kann, darf das Gerät nicht halb ersetzt zurücklassen. Nach der Ablehnung liegt das Archiv weiterhin im Eingangsordner, der Arbeitsordner ist aufgeräumt.
Bei einer zu neuen Sicherung: erst das Programm aktualisieren, dann einspielen. Siehe Update ohne Internet.
Nachsehen, wo etwas steht¶
docker compose exec backend python -c "import sqlite3; print(sqlite3.connect('/data/kiekmap.db').execute('select * from alembic_version').fetchone())"
docker compose exec backend alembic heads
Stimmen die beiden Werte nicht überein, ist das Schema nicht auf Stand. Das ist bei Schreibfehlern im Betrieb der erste Blick, und der Zustand ist von außen nicht zu sehen: Die Ausstellung zeigt Fotos, Karte und Zeitleiste wie immer, nur jeder Schreibzugriff scheitert mit HTTP 500. Auf dem Entwicklungsrechner dasselbe ohne Container:
sqlite3 data/kiekmap.db "select * from alembic_version;"
cd backend && .venv/bin/alembic heads
Und die Reparatur von Hand:
make migrate
Wo die Sicherung liegt¶
Auf dem Stick im Ordner kiekmap-backup/:
kiekmap-backup/
backup.json Datum, Anzahl, Ortsname
kiekmap.db die Angaben, mit VACUUM INTO konsistent herausgeschrieben
photos/ die Originale, nach ihrem Hash abgelegt
thumbs/ die Vorschaubilder
region.json Kartenausschnitt
places.json Ortsverzeichnis
Ordner statt Archiv: Eine abgebrochene Sicherung ist so teilweise brauchbar statt komplett wertlos, und die Bilder lassen sich an jedem Rechner ansehen.
Nach einer Wiederherstellung liegt der bisherige Stand unter data/before-<Datum>/ — inklusive
Datenbank und Write-Ahead-Log. Er wird nie automatisch gelöscht. Wenn feststeht, dass alles stimmt:
rm -rf data/before-2026-07-29-1115
Das ist der einzige Ort, an dem die SD-Karte unbemerkt volllaufen kann.
Das Gerät für einen anderen Ort einrichten: adaption.de.md.