Dokumentiert nachträglich eine bereits deployte, live-verifizierte Session-Arbeit: De-/Reprovisionieren (serverseitiger Credential-Entzug, Agent-Self-Heal mit deprovisioned_at-Gate), Löschen als Soft Delete/Archivierung, Hardware-/Log-Leseendpunkte. Bislang komplett undokumentiert in platform-docs gewesen. 09-data-model-v1.md um devices.deprovisioned_at/archived_at ergänzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.1 KiB
ADR-0013: Geräte-Lebenszyklus-Aktionen im Kundenportal
Status: Beschlossen Datum: 10.08.2026
Kontext
Die Geräteliste im Kundenportal (/geraete, siehe ADR-0011) zeigte bislang nur Hostname, Kennung, letzten Check-in und einen Link zum Auftragskatalog. Kund:innen fehlte jede Möglichkeit, Hardware-Informationen oder die Installations-/Aktionshistorie eines Geräts einzusehen, oder ein Gerät zu deaktivieren beziehungsweise loszuwerden, ohne den Betreiber zu kontaktieren.
Für „De-Provisionieren" und „Löschen" waren zwei Fragen zu klären, die reale Konsequenzen auf im Feld laufende Geräte haben:
- Was passiert technisch mit einem deprovisionierten Gerät — wird der laufende Agent aktiv ferngesteuert deinstalliert, oder wird ihm nur serverseitig der Zugriff entzogen?
- Ist „Löschen" ein harter, unwiderruflicher Datenbank-Delete, oder eine Archivierung?
Entscheidung
De-Provisionieren ist ein rein serverseitiger Credential-Entzug, keine aktive Fernabmeldung. Das Agent-Secret des Geräts wird ungültig gemacht (agent_secret_hash = NULL, deprovisioned_at = now()); der laufende Agent auf dem Gerät selbst wird nicht kontaktiert und deinstalliert sich nicht — er bekommt beim nächsten Check-in schlicht 401 unauthorized und bleibt bis zur Reprovisionierung untätig liegen. Historie (Hardware-Snapshots, Aktions-Log) bleibt vollständig erhalten. Diese Variante wurde einer aktiven Fernabmeldung vorgezogen, weil letztere eine neue, deutlich invasivere Fähigkeit im provisioning-agent gebraucht hätte (Uninstall-Kommando auf echten Kundengeräten), mit entsprechend höherem Test- und Sicherheitsaufwand.
Damit „Erneut provisionieren" ein deprovisioniertes, aber weiterhin laufendes Gerät ohne physischen Eingriff reaktivieren kann, wurde der Agent (provisioning-agent/agent.py) um einen Self-Heal-Mechanismus erweitert: bekommt der Agent beim Check-in 401, ruft er einmalig /api/v1/agent/bootstrap erneut auf (dieser Endpoint kannte von Anfang an keine andere Authentifizierung als die device_id selbst — bestehendes Design, kein neues Risiko) und übernimmt bei Erfolg das neue Secret. Damit dieser Mechanismus die De-Provisionierung nicht selbst aushebelt — ein deprovisioniertes Gerät würde sich sonst beim nächsten Poll-Zyklus einfach automatisch selbst reaktivieren, unabhängig davon, ob „Erneut provisionieren" im Portal je ausgelöst wurde —, prüft agent_bootstrap seit dieser Änderung zusätzlich deprovisioned_at IS NULL. Erst das explizite Löschen von deprovisioned_at (die Reprovisionieren-Aktion) macht einen erneuten Bootstrap-Aufruf überhaupt erfolgreich. Dieses Gate wurde bei der Live-Verifikation nachträglich ergänzt, nachdem der ursprüngliche Entwurf diese Lücke übersehen hatte.
Löschen ist Soft Delete (Archivierung), kein Hard Delete. Ein „gelöschtes" Gerät bekommt archived_at = now() gesetzt, verschwindet aus der aktiven Geräteliste (fetch_devices_for_organization()/fetch_all_devices() filtern archived_at IS NULL), Datensatz und Historie bleiben aber vollständig in der Datenbank erhalten. Kein separater „Wiederherstellen"-Endpunkt in dieser ersten Ausbaustufe — die Zeile bleibt bei Bedarf über die Datenbank erreichbar.
Zusätzlich neu: Lese-Endpunkte für Hardware-Informationen (GET /api/v1/devices/{id}/hardware, jüngster hardware_snapshots-Eintrag samt Netzwerk-Interfaces/Storage-Devices) und Installations-/Aktions-Log (GET /api/v1/devices/{id}/events, bestehende device_merkmal_events-Tabelle aus ADR-0010, hier erstmals für Kund:innen sichtbar gemacht statt nur intern verwendet).
Alle Aktionen sind über ein Pico-natives Dropdown (<details class="dropdown">) in der Geräteliste erreichbar, ohne zusätzliches JavaScript — konsistent mit dem Rest von kundenplattform, das durchgehend serverseitig rendert.
Konsequenzen
devices bekommt zwei unabhängige, nullable Zeitstempel-Spalten (deprovisioned_at, archived_at) statt eines einzelnen Status-Enums — ein Gerät kann deprovisioniert und trotzdem noch in der aktiven Liste sichtbar sein, während „archiviert" es komplett ausblendet; ein einzelnes Enum hätte diese unabhängigen Zustände entweder verboten oder umständlich kombinieren müssen.
agent_bootstrap verweigert seit dieser Änderung Geräte mit gesetztem deprovisioned_at — bestehende, funktionierende Erstinstallationen sind davon nicht betroffen, da deprovisioned_at bei neu angelegten Geräten immer NULL ist.
Der Self-Heal-Mechanismus im Agenten ist die einzige Änderung dieses ADRs, die tatsächlich auf bereits im Feld laufenden Geräten ausgeführten Code betrifft (nicht nur auf anode) — entsprechend wurde er vor dem Live-Rollout gezielt gegen ein deprovisioniertes/reprovisioniertes Testgerät durchgespielt, nicht nur gegen die API-Antwort geprüft.
Die Aktionen sind bislang nur im Kundenportal (/geraete) umgesetzt, nicht im internen Admin-Bereich (/admin/geraete) — letzterer nutzt für den Auftragskatalog bereits denselben Reuse-Pattern (admin_geraete.py importiert aus routers/geraete.py), eine spätere Erweiterung ist entsprechend günstig.
Details zum Datenmodell: 09-data-model-v1.md.