# 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`.

## 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).
