# 2026-11-09

## Mobi-Router: Standort veraltet, Online falsch

Problem: Standort auf der Karte nicht aktuell, Online/Offline falsch.

Gelesen (Router-Weboberfläche RUT955 und ioBroker-REST, nichts geändert):

- Router „Data to Server“: Sammlung `dp` (Client-ID `rtrmobi`, Topic `rtrmobi/gpsdata`, Periode 60 s) mit Eingang `gpsdata` (Typ Modbus, JSON, Segment count 1, Send as object); Sammlung `lte` (Client-ID `rtrmobi_lte`, Topic `rtrmobi/lte`). MQTT-Publisher aus.
- Modbus TCP Client `gpsdata` (127.0.0.1:502, alle 30 s): `latitude` Register 144, `longitude` Register 146.
- ioBroker: `mqtt.0.rtrmobi.gpsdata` aktuell, enthält aber nur `longitude`. `0_userdata.0.dp-smartsolutions.mobi.geolocation.latitude/longitude` zuletzt vor ca. 27 Tagen geändert.
- `mqtt.0.info.clients.rtrmobi` = false, `rtrmobi_lte` = true, alle Client-Flags zuletzt beim Adapter-Neustart vor ca. 37 h gesetzt.

Ursache: Mit Segment count 1 geht pro Nachricht nur ein Register raus, `latitude` kommt nicht an; das Geo-Skript bekommt kein vollständiges Paar. Die App wertet Online über `info.clients.rtrmobi` aus, das Flag spiegelt nicht, ob Daten fließen.

Zusätzlich: ioBroker-Skript `script.js.ksb.gps_mobi` erwartet noch `{"longitude":"[lat],[lon]"}` und scheitert am heutigen Format `{"gpsdata":{"name":"longitude","data":"[7.03]"}}`, darum stehen `geolocation.*` still.

### Behoben (App)

- `ContainerApiService::getApiDataByContainerId()` liest zusätzlich `mqtt.0.{router}.*` (Router-ID aus der `rtr_reachable`-URL). `rtr_reachable` = true, wenn `lte` oder `gpsdata` in den letzten 5 Minuten gemeldet haben; neu `rtr_last_seen`. Damit stimmen Karte, Anlagenansicht, TV-Wand und Live-Dashboard ohne eigene Änderungen.
- `geo_lat`/`geo_lng` direkt aus `gpsdata` (`parseGpsPayload()` kennt alle drei Formate). Einzeln gelieferte Werte merkt sich der App-Cache 7 Tage, bis beide da sind; sonst bleibt der ioBroker-Wert.
- Live geprüft: Mobi online (letzte Meldung 11:15:14), Position 50.709896 / 7.032893; Narwali und Narwali 2 offline (letzte Meldung 28.11.2025 bzw. 08.07.2026).
- Router und ioBroker nicht verändert. Ort/Region (Text) kommen weiter aus dem kaputten Skript.

Dateien: `src/Service/ContainerApiService.php`, `config/swimmonitor/changelog.yaml`, `docs/TODO.md`.

## Mobi 2: Modbus und Data-to-Server wie Mobi 1

Problem: Mobi 2 (RUT955 `192.168.55.9`) sollte Modbus und MQTT wie Mobi 1 (`192.168.55.6`) bekommen, mit eigenen Client-IDs, damit sich die Router am Broker nicht stoßen.

Entscheidung / Stand auf dem Router (WebUI, Firmware RUT9_R_00.07.06.21):

- GPS an. Modbus TCP Server an, Port 502, Device ID 1, persistent.
- Modbus TCP Client `gpsdata` 127.0.0.1:502, Periode 30 s, Timeout 5 s, an. Requests: `latitude` Register 144 / `longitude` Register 146, 32-bit float Byte order 1,2,3,4, Function 3, Count 2, Brackets an.
- Data to Server Sammlung `dp`: MQTT `192.168.1.2:1883`, Topic `rtrmobi2/gpsdata`, Client-ID `rtrmobi2`, User `iobroker`, Periode 60 s, Eingang `gpsdata` Modbus JSON, Segment count 1, Send as object an.
- Sammlung `lte`: MQTT derselbe Broker, Topic `rtrmobi2/lte`, Client-ID `rtrmobi2_lte`, Eingang `lte` Typ GSM JSON, Periode 60 s. Beide Sammlungen Enable an.
- MQTT-Publisher auf Mobi 1 unverändert aus. Firmware-Update auf Mobi 2 nicht ausgeführt.

Ursache, warum in ioBroker noch nichts liegt: Ping von Mobi 2 nach `192.168.1.2` geht (5/5, ~50–60 ms). `mqtt.0.info.clients` kennt aber weder `rtrmobi2` noch `rtrmobi2_lte`; `mqtt.0.rtrmobi2.*` existiert nicht. Mobi 1 sendet weiter (`rtrmobi` / `rtrmobi_lte` true). Syslog Mobi 2: `data_sender` um 11:44 mit `Config option 'plugin' is required` / `Failed to init output plugin '5'` / `Error while parsing config section '4'` beendet, später nur noch `Signal received, exiting` ohne sauberen Start. Das Passwortfeld der Teltonika-UI zeigt denselben Platzhalter wie auf Mobi 1; bei einer neuen Sammlung wird der Platzhalter als Wert gespeichert.

App-Code nicht geändert. `ContainerApiService` liest weiterhin nur `rtrmobi` für Container 3.

Dateien: nur Router-WebUI; Doku `docs/TODO.md`, `docs/journal/2026-11-09.md`.

## Wartelisten-Claim-URL: kein 500, Angebot ungültig

Live geprüft: `GET /appointments/waiting-list/claim/{token}` antwortet **200**. Text aus `claim_invalid.html.twig` („Platzangebot ist nicht mehr verfügbar“). Token ungültig, abgelaufen oder schon eingelöst.

Die Seite erbt weiter von `base.html.twig` (altes SwimMonitor-Menü, CDN, Webpack-Entry `app`), nicht vom Layout 2.0 `swimmonitor/base.html.twig`. Die Design-Umstellung hat Buchungs-/Claim-Seiten bewusst nicht umgebaut, nur die Menülinks im alten Base. Deshalb sieht der Eltern-Link nach Login/Container/Admin aus, ist aber kein Serverfehler.

Produktives `entrypoints.json` hat den Schlüssel `app` doppelt (`app_js` und `app_css`); PHP behält den letzten. Das erklärt fehlendes App-JS, nicht den 200-Text. PHP-Log vom früheren 500 ohne Serverzugriff nicht einsehbar.

## Wartelisten-Claim: gültiges Angebot lieferte 500

Problem: Nach der Diagnose der Live-URL (ungültiges Angebot → 200) sollte ein echter Claim lokal durchgespielt werden.

Ursache: `GET /appointments/waiting-list/claim/{token}` mit gültigem Token rendert `claim.html.twig`. Dort stand `{% if error %}` ohne `is defined`. Lokal läuft Symfony als `prod` mit `debug: true`; Twig wirft dann „Variable error does not exist“ → HTTP 500. Dieselbe Seite mit abgelaufenem Token nutzt `claim_invalid.html.twig` und bleibt 200.

Nachtrag: Produktion läuft ohne Debug (404 ohne `X-Debug-*`-Header und ohne Symfony-Fehlerseite). Dort ist `strict_variables` aus, `{% if error %}` wirft also nicht. Der Template-Fehler erklärt den gemeldeten Live-500 deshalb nicht; dessen Ursache bleibt ohne Server-Log offen.

Ablauf: `app:waitlist:e2e-test --system=swim --skip-mail --stop-before-claim` (Availability #477, Entry #1283). Nach dem Twig-Fix GET 200 (Formular), POST mit Extrafeld „Testkind“ → „Ihr Platz wurde gebucht.“, erneuter GET → ungültig. Testdaten wieder gelöscht.

Dateien: `templates/waiting_list/claim.html.twig`, `src/Controller/WaitingListController.php`, `src/Command/WaitlistE2eTestCommand.php`, `docs/CRONJOBS.md`, `docs/TODO.md`.

## Live-500 beim Wartelisten-Claim: Notiz zu lang

Auf dem Server gelesen (SSH `hetznerksb`, nur lesend; Projekt liegt unter `/var/www/html/ksbdueren`, Log `shared/var/log/prod.log`):

- 07.11.2026 12:05:08, 12:05:35, 12:06:39, 12:07:23: vier `request.CRITICAL` mit `SQLSTATE[22001] … Data too long for column 'notes'`. Kein anderer Fehler dieser Art im Log.
- `AppointmentWaitlistService::claim()` setzt `appointment.notes` auf `'Buchung über Warteliste: ' . <Wartelisten-Notiz>`; `appointment.notes` ist `VARCHAR(255)`, `waiting_list.notes` länger.
- Eintrag #1234 (Kurs 467 „Mobi Bonn Wassergewöhnung Mo 17–18 Uhr 6–8 Jahre“): Notiz 242 Zeichen, benachrichtigt, Angebot heute weg. 25 + 242 = 267 Zeichen → Insert scheitert, Transaktion rollt zurück, Seite 500. Nach Ablauf des Angebots hat am 08.11. 22:25 ein anderer Eintrag den Platz übernommen (Termin #3539). Der aufgerufene Link zeigt deshalb heute „nicht mehr verfügbar“.
- Bestätigt durch die vom Nutzer weitergeleitete Angebotsmail: Token `968fad8a…`, Kurs 467, versandt 07.11. 07:09, reserviert bis 08.11. 07:09; Beschwerde der Kundin 12:10. Name passt zu #1234. Für Kurs 467 gibt es keinen Termin dieser Familie.
- Dieselbe Familie steht mit gleicher langer Notiz auf acht Wartelisten: #1228–#1235 (Kurse 461–470, 237–242 Zeichen). Jedes weitere Angebot an sie scheitert genauso.

Nichts auf dem Server geändert.

### Behoben (lokal)

`Appointment::$notes` ist jetzt `TEXT` (Migration `Version20261109140000`: `appointment.notes` → `LONGTEXT`; `down` kürzt erst auf 255). Lokal: Dry-Run zeigte nur diese Migration, danach angewendet. Claim mit 242-Zeichen-Notiz: 200, „Ihr Platz wurde gebucht“, Notiz mit 267 Zeichen gespeichert; Testdaten (Availability #479) gelöscht. `schema:update --dump-sql` zeigt für `appointment.notes` keine Abweichung mehr (übrige Index-Abweichungen bestanden schon vorher).

Dateien: `src/Entity/Appointment.php`, `migrations/Version20261109140000.php`, `config/swimmonitor/changelog.yaml`, `docs/TODO.md`.

## Lilian Labs: Warum seit April keine Daten

Server (nur lesend): Cronjob läuft (letzter Lauf 18.04.2026 04:00), `lilian_labs_config.token_expiry` 18.04.2026 11:05, Token zuletzt am 18.03. eingetragen. 531 Messungen, letzte vom 17.04.2026. Seitdem bricht `syncAllPoints()` mit „Token ist abgelaufen“ ab, ohne dass jemand benachrichtigt wird.

Öffentliches Frontend `manager.lilianlabs.com` gelesen: Anmeldung nur über `POST auth/login` mit E-Mail, Passwort und `multiFactorAuthCode` (6-stelliger Code, wird bei jedem Login zugeschickt). Kein Refresh-Endpunkt, kein API-Key, kein „Gerät merken“. Der Token gilt laut `sessionExpiresAt` etwa 30 Tage.

Nichts geändert; Entscheidung über den Weg steht aus.

### Nachgeladen (Server)

Nutzer hat den Token erneuert (gültig bis 10.12.2026) und selbst einen 30-Tage-Abruf gestartet. Danach auf dem Server `php bin/console lilian-labs:fetch-data --days=210` (kein `--dry-run` vorhanden, Duplikat-Schutz an): 256 neu, 20 übersprungen. Alle neuen Messungen: Köln Buchheim, Messgerät 3 Beckenrand, bis 15.10.2026 (neuere gibt es bei Lilian nicht). Kall endet am 14.04.2026. „Messeinheit“ von Messgerät 2 und 3 antwortet mit HTTP 400 (eventuell gelöschte Punkte). Cache-Warnungen „Permission denied“ beim Aufruf als `pohl` harmlos, keine Dateien unter `pohl` angelegt. Aktueller Release auf dem Server: `20261109130305`.

Container-Zuordnung: `water_measurement` hat keinen Container, nur `location` (Ortsname aus Lilian). Der Wasservergleich ordnet über `lilian_labs_config.influx_mappings` zu (Server: Bayrol → Kall, mobi → Köln Mengenicher Strasse); Köln Buchheim und Blankenheim sind dort nicht zugeordnet.

### Messungen → Container (lokal)

Zuordnung laut Nutzer: Blankenheim und Kall → Narwali2, Köln Mengenicher Strasse und Köln Buchheim → Mobi; Narwali (Bayrol) hat keinen Lilian-Standort. `water_measurement.container_id` (FK, `ON DELETE SET NULL`), `lilian_labs_config.location_containers` (JSON Standort → Container-ID), Migration `Version20261109150000` belegt die Zuordnung über die Containernamen vor. Import setzt den Container, `lilian-labs:assign-containers [--dry-run]` ordnet Bestand neu zu, Speichern der Konfiguration ebenso. Admin: Feld „Zuordnung Container“, Spalte „Container“ in der Messwertliste.

Stolperstein: Lilian liefert „Köln Mengenicher Strasse “ mit Leerzeichen am Ende; die Zuordnung vergleicht getrimmt. Lokal: 790 Messungen zugeordnet (Mobi 610, Narwali2 180), zweiter Lauf ändert nichts. Nach der Migration war `cache:clear` nötig, sonst kannte Doctrine das neue Feld nicht.

Widerspruch offen: `influx_mappings` ordnet die Bayrol-Werte (Container Narwali) Kall zu, laut Nutzer stand in Kall Narwali2. Nicht geändert.

Dateien: `src/Entity/WaterMeasurement.php`, `src/Entity/LilianLabsConfig.php`, `src/Service/LilianLabsApiService.php`, `src/Command/AssignLilianLabsContainersCommand.php`, `src/Controller/Admin/LilianLabsController.php`, `templates/admin/lilian_labs/config.html.twig`, `templates/admin/lilian_labs/measurements.html.twig`, `migrations/Version20261109150000.php`, `docs/CRONJOBS.md`, `config/swimmonitor/changelog.yaml`.

## Wartelisten-Claim-Seiten ohne Menü

Wunsch: Eltern-Link ohne SwimMonitor-Navigation. Neues Layout `waiting_list/_layout.html.twig` (Karte auf grauem Verlauf wie Verifizierung/Storno, ohne Menü, Breadcrumb, Footer, `noindex`). Claim, Erfolg und Ungültig erben davon. Lokal mit Angebot #1284 geprüft: HTML enthält kein Menü mehr; Testdaten (Availability #478) gelöscht.

Dateien: `templates/waiting_list/_layout.html.twig`, `templates/waiting_list/claim.html.twig`, `templates/waiting_list/claim_success.html.twig`, `templates/waiting_list/claim_invalid.html.twig`, `config/swimmonitor/changelog.yaml` (2.4.2).

## Lilian: Anmeldung in der Konfiguration statt Browser-Konsole

Wunsch: die Messdaten auf dem eigenen Server holen, ohne das Token aus der Entwicklerkonsole von `app.lilianlabs.com` zu kopieren.

Entscheidung: Die Konfiguration speichert E-Mail und Passwort. „Code anfordern“ ruft `POST https://api.lilianlabs.com/auth/login` auf. Lilian schickt den Einmalcode, der Code wird beim Speichern mitgeschickt und nicht abgelegt. Das Passwort liegt mit `sodium` und dem App-Secret verschlüsselt. Ein stiller Login im Cron bleibt aus: es gibt keinen Refresh, und jeder Login will einen neuen Code. Solange das Token gilt (etwa 30 Tage), synchronisiert der Cron weiter wie bisher.

Nicht umgesetzt: Flags, mit denen die App die Zwei-Faktor-Abfrage überspringt.

Geprüft: `tests/Service/LilianLabsApiServiceTest.php` (Anmeldung setzt das Token, ohne Code bleibt das alte Token). Die Admin-Seite im Browser nicht geöffnet.

Dateien: `src/Entity/LilianLabsConfig.php`, `src/Service/LilianLabsApiService.php`, `src/Controller/Admin/LilianLabsController.php`, `templates/admin/lilian_labs/config.html.twig`, `templates/admin/lilian_labs/index.html.twig`, `public/js/pages/lilian-labs.js`, `migrations/Version20261109160000.php`, `tests/Service/LilianLabsApiServiceTest.php`, `config/swimmonitor/changelog.yaml` (2.5.0).
