# ADR-0021: Agent-Standardintervall 6h+Jitter, Debug-Modus, Health-Monitoring **Status:** Beschlossen **Datum:** 26.08.2026 ## Kontext Der Provisioning-Agent (`provisioning-agent/agent.py`) checkte bislang alle 5 Minuten ein (`AGENT_POLL_INTERVAL_SECONDS`, fest über eine Server-Umgebungsvariable) — sinnvoll für die Entwicklungsphase, aber bei einer wachsenden Geräteflotte unnötig häufig für den Normalbetrieb. Es gab außerdem keine Möglichkeit, ein einzelnes Gerät gezielt kurzfristig engmaschiger abzufragen (z.B. um eine frisch zugewiesene Rolle zeitnah zu beobachten), ohne das Intervall global zu ändern. Und: der Agent sammelte nur einmalig bei der Installation eine statische Hardware-Inventur (`hardware_snapshots`, siehe `10_hardware.sh`) — es gab keine periodische Sicht auf den *aktuellen* Gerätezustand (Diskbelegung, Auslastung, Uptime). Der Boot-Checkin selbst (Agent meldet sich direkt beim Start) war entgegen der ursprünglichen Annahme bereits vorhanden — `agent.py:main()` ruft `run_once()` grundsätzlich vor der ersten `sleep()`-Anweisung auf, daher hier nicht angefasst. ## Entscheidung **Standardintervall:** `AGENT_POLL_INTERVAL_SECONDS`-Default von 300 auf 21600 (6 Stunden) angehoben. **Jitter** kommt bewusst aus dem Agenten selbst, nicht vom Server (der kennt keinen sinnvollen Zufallszustand pro Gerät): `agent.py`s `main()`-Loop addiert 0–30 Minuten zufällig auf das zurückgelieferte Intervall, aber **nur** wenn es „groß" ist (`JITTER_THRESHOLD_SECONDS = 3600`) — der kurze Debug-Modus (60s) und der Fallback-Retry bei Fehlern (`FALLBACK_INTERVAL_SECONDS = 60`) bleiben bewusst unverjittert, sonst würde Jitter genau den Zweck des schnellen Debug-Zyklus untergraben. **Debug-Modus:** neue Spalte `devices.debug_mode_until` (Migration `0022_agent_debug_mode_und_health.sql`) ist der einzige Zustand — kein separates Enum, kein Cron-Job zum Zurücksetzen. `agent_checkin()` vergleicht bei jeder Anfrage `debug_mode_until` mit der aktuellen Zeit und liefert `poll_interval_seconds` entsprechend (60s im Debug-Modus, sonst 6h) — das automatische Zurückfallen nach Ablauf passiert rein durch Zeitvergleich, ohne dass irgendetwas aktiv zurückgesetzt werden muss. Aktivieren (`POST /api/v1/devices/{id}/debug-mode`, setzt `now()+6h`) und Deaktivieren (`DELETE`, setzt `NULL`) sind eigene, dünne Endpunkte, gegen `require_service_token()` wie üblich — Organisationszugehörigkeitsprüfung bleibt Aufgabe der aufrufenden Kundenplattform (`find_device_in_organization()`), nicht dieses Endpunkts. Bewusst **kein zusätzliches Rechte-Gate**: jeder eingeloggte Nutzer der eigenen Organisation darf den Debug-Modus für die eigenen Geräte umschalten, gleiches Muster wie die bestehenden Auftragskatalog-Aktionen. **Health-Monitoring:** sechs neue, nullable `devices`-Spalten (`health_disk_used_percent`, `health_ram_used_percent`, `health_uptime_seconds`, `health_load_1m/5m/15m`, `health_collected_at`) — halten bewusst nur den jeweils letzten Stand, keine wachsende Historie (konsistent mit dem Plattenplatz-Bewusstsein an anderer Stelle, siehe `iso_builds`-Aufräumung). Der Agent sammelt die Werte bei jedem Checkin rein über Python-Stdlib (`shutil.disk_usage("/")`, `/proc/meminfo`, `/proc/uptime`, `os.getloadavg()`) — keine neue Abhängigkeit, jeder Einzelwert einzeln per `try/except`, damit ein fehlschlagender Wert (z.B. Load Average auf einem ungewöhnlichen System) die übrigen nicht verhindert. Bewusst ausgeschlossen: Temperatur/SMART-Werte — hätten zusätzliche Pakete/Berechtigungen gebraucht, war nicht Teil der 25./26.08.-Planung. `agent_checkin()` schreibt `agent_last_checkin` und die Health-Felder in einem kombinierten `UPDATE ... RETURNING debug_mode_until` statt separater Round-Trips. Fehlt das `health`-Objekt in einem einzelnen Checkin (z.B. Sammel-Fehler im Agenten), bleiben die zuvor gespeicherten Health-Werte unverändert stehen statt auf `NULL` zurückzufallen — ein `CASE WHEN` pro Spalte statt eines bedingungslosen Überschreibens. `fetch_devices_for_organization()`/`fetch_all_devices()` liefern `debug_mode_until` und `health` jetzt mit aus (gemeinsame SQL-Spaltenliste `GERAETE_HEALTH_SPALTEN_SQL`, gemeinsame Aufbereitung `_health_felder_aus_row()`) — Kundenplattform braucht dafür keinen zusätzlichen Request, das Device-Dict aus der ohnehin schon vorhandenen Besitz-Validierung (`find_device_in_organization()`) trägt die Felder direkt mit. **Kundenplattform-UI:** neue Sektion „Debug-Modus" auf der Auftragskatalog-Seite (Kunden- **und** Admin-Bereich, gleiches `zurueck_url`-Präfix-Muster wie die bestehenden Select/Deselect-Formulare aus ADR-0015), neue Sektion „Aktueller Zustand (letzter Checkin)" auf der Hardware-Seite (klar getrennt von der bestehenden statischen Install-Zeit-Inventur), Debug-Badge in beiden Geräte-Listen, und „Zuletzt gemeldet" jetzt auch auf der Log-Seite (vorher nur in der Geräteliste sichtbar, nicht in der Detail-Historie). ## Konsequenzen Migration `0022_agent_debug_mode_und_health.sql` (8 neue `devices`-Spalten, alle nullable, kein Backfill nötig). Kein neuer Cron/Scheduler-Prozess für den Debug-Modus-Rückfall — reine Zeitvergleichslogik im bestehenden Checkin-Pfad. Live auf anode verifiziert: `agent_checkin()` mit Health-Payload durchlaufen, `debug_mode_until` korrekt in `poll_interval_seconds` übersetzt (Debug-Endpunkt aktiviert → nächster Checkin liefert 60s, nach Ablauf/Deaktivieren wieder 21600s). `collect_health()` gegen ein echtes Linux-System (enterprise) getestet, liefert plausible Werte ohne Zusatzpaket. UI-Änderungen lokal mit gemocktem Kontext gerendert (Debug-Toggle in beiden Zuständen, Health-Sektion mit/ohne Daten, Log-Seite mit Zeitstempel).