Ein Sub-Lizenzserver (role='license_server') meldet jetzt seinen
kompletten eigenen Kunden-/Ticket-/Lizenzstand an den Master, der ihn
als Ueberblick auf einer neuen Kunden-Detailseite anzeigt: unter dem
Kunden, der die Lizenzserver-Lizenz haelt, erscheinen dessen eigene
Kunden mit ihren Tickets/Lizenzen -- genau wie gewuenscht.
Transport: huckepack auf dem bereits bestehenden 6-Stunden-Heartbeat
(licensing.py: neues optionales 'report'-Feld in
build_client_request(), signiert wie jedes andere Feld -- ein
Angreifer ohne den privaten Lizenzschluessel kann keinen gefaelschten
Bericht unterschieben). Zusaetzlich sofortiger Best-Effort-Report per
Hintergrund-Thread nach jeder lokalen Ticket-/Kunden-/Lizenz-Aenderung
(_report_to_master_async()), damit der Master nicht bis zu 6h auf
Aktualitaet warten muss. Funktioniert automatisch auch ueber den
Offline-Code-Weg (/licenses/manual-code), da beide Transportwege
denselben _process_heartbeat()-Code nutzen.
- Drei neue, rein informative Tabellen (reported_sub_customers/
_tickets/_licenses), verankert an source_license_id (die Lizenz,
die den Sub-Lizenzserver identifiziert). Ein Bericht ist immer ein
VOLLER Schnappschuss -- bei jedem Empfang werden alle bisherigen
Zeilen dieser source_license_id ersetzt (kein Diffing noetig, ein
beim Sub-Lizenzserver geloeschtes Ticket verschwindet dadurch
automatisch korrekt auch hier).
- _build_own_issuance_report(): baut den Schnappschuss auf der
Sub-Lizenzserver-Seite.
- _ingest_sub_report(): uebernimmt ihn auf der Master-Seite, NUR wenn
row['role']=='license_server' fuer die absendende Lizenz (Verteid-
igung in der Tiefe -- row['role'] steht in der Master-DB fest, nicht
im Client-Request, ein gewoehnlicher Kunde kann sich also nicht als
Lizenzserver ausgeben).
- Neue Kunden-Detailseite (/customers/<id>, customer_detail.html):
Stammdaten, eigene Tickets bei uns, und -- falls dieser Kunde eine
aktive role='license_server'-Lizenz haelt -- die gemeldeten eigenen
Kunden mit Tickets/Lizenzen (Ampelfarbe/Resttage ueber dieselbe
_license_dict_for_display()-Funktion wie ueberall sonst). Kunden-
liste verlinkt jetzt auf die Detailseite, zeigt 'Lizenzserver'-Badge.
Getestet: licensing.py isoliert (report signiert+verifiziert,
Manipulation bricht die Signatur, unveraendertes Verhalten ohne
report-Argument), _ingest_sub_report() isoliert (Erstbericht korrekt
uebernommen, Zweitbericht ersetzt korrekt inkl. verschwindender
Tickets, Isolation zwischen mehreren source_license_id sauber).
ZUSAETZLICH ein vollstaendiger Zwei-Prozess-Integrationstest ueber
echtes HTTP (zwei unabhaengige, native laufende Instanzen: 'Master' +
'Sub-Lizenzserver' auf unterschiedlichen Ports): Ticket mit
role=license_server angelegt, Lizenz ausgestellt, auf der zweiten
Instanz hochgeladen und ONLINE aktiviert, dort ein eigener Endkunde
samt Ticket/Lizenz angelegt, automatischer Report ausgeloest --
Master-Kundendetailseite zeigt den gemeldeten Endkunden mit korrektem
Ticket-Typ/Laufzeit/Status und aktuellem Heartbeat-Zeitstempel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>