diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index 8387a1a..7300639 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.8.18" +version = "2.8.19" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/services/issuance.py b/apps/tesm-license/src/tesm_license/services/issuance.py index f7ffa7c..f3f427a 100644 --- a/apps/tesm-license/src/tesm_license/services/issuance.py +++ b/apps/tesm-license/src/tesm_license/services/issuance.py @@ -779,6 +779,11 @@ def display_state(row: dict[str, Any], *, now: datetime | None = None) -> dict[s Grenzen: mehr als 90 Tage = gruen, 30 bis 90 Tage = orange, unter 30 Tage (auch abgelaufen) = rot. Unbefristete Lizenzen sind immer gruen. + + Gezaehlt wird in **angefangenen** Tagen (``lic.tage_bis``) -- dieselbe + Rechnung, die auch der Client meldet. Die Vergleiche sind darauf + abgestimmt: 90 volle Tage und eine Stunde sind "mehr als 90" und damit + gruen, 29 Tage und eine Stunde sind "unter 30" und damit rot. """ moment = now or datetime.now(timezone.utc) result = { @@ -802,11 +807,18 @@ def display_state(row: dict[str, Any], *, now: datetime | None = None) -> dict[s return {**result, "color": "red"} delta = expires - moment - days_left = int(delta.total_seconds() // 86400) + # Dieselbe Rechnung wie im Client -- nicht eine zweite daneben. Hier stand + # eine abgerundete, und damit zeigte dieselbe Lizenz auf dieser Seite + # "21 Tage", waehrend der Abruf beim Kunden "22" meldete. + days_left = lic.tage_bis(expires, now=moment) expired = delta.total_seconds() <= 0 if expired: color = "red" - elif days_left < RED_BELOW_DAYS: + elif days_left <= RED_BELOW_DAYS: + # "<=", weil ``days_left`` **angefangene** Tage zaehlt: eine Lizenz mit + # 29 Tagen und einer Stunde steht als 30 da und liegt trotzdem unter + # der Schwelle von 30 vollen Tagen. Mit "<" waere sie orange -- die + # Warnung kaeme einen Tag zu spaet, und zwar genau am kritischen Rand. color = "red" elif days_left <= GREEN_ABOVE_DAYS: color = "orange" diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index c0bc4e3..bf9bc6a 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.8.19" +version = "2.8.20" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/licensing_client.py b/packages/tesm-core/src/tesm_core/licensing_client.py index c827b7f..e7a73bf 100644 --- a/packages/tesm-core/src/tesm_core/licensing_client.py +++ b/packages/tesm-core/src/tesm_core/licensing_client.py @@ -193,7 +193,18 @@ def zustand_als_auskunft(state: "LicenseState") -> dict[str, Any]: "days_left": (status.days_left if status and not status.lifetime else None), "expiring_soon": bool(status.expiring_soon) if status else False, "grace_active": bool(status.grace_active) if status else False, - "modules": sorted(state.modules_in_use), + # Drei Angaben, weil zwei davon verschieden sein koennen und die + # Verwechslung teuer ist: "modules" ist, was die **Lizenz** freigibt -- + # dieselbe Liste, die der Lizenzserver zeigt. Was davon vor Ort + # abgeschaltet wurde, steht daneben. + # + # Vorher stand hier allein das Laufende. Beim Kunden fehlte darin + # "dhcp", waehrend die Lizenzseite es auswies -- es war lediglich unter + # Verwaltung -> System abgeschaltet. Aus der Antwort ging das nicht + # hervor, und damit sah es nach einem Lizenzproblem aus. + "modules": sorted(state.modules), + "modules_active": sorted(state.modules_in_use), + "modules_off": sorted(state.modules_off), "customer": state.customer_name, "last_heartbeat_at": state.last_heartbeat_at, "error": state.error, diff --git a/packages/tesm-licensing/pyproject.toml b/packages/tesm-licensing/pyproject.toml index 7f97cdb..1f79d89 100644 --- a/packages/tesm-licensing/pyproject.toml +++ b/packages/tesm-licensing/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-licensing" -version = "2.7.7" +version = "2.7.8" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = ["cryptography>=42"] diff --git a/packages/tesm-licensing/src/tesm_licensing/__init__.py b/packages/tesm-licensing/src/tesm_licensing/__init__.py index 6e666bd..55ea3f2 100644 --- a/packages/tesm-licensing/src/tesm_licensing/__init__.py +++ b/packages/tesm-licensing/src/tesm_licensing/__init__.py @@ -46,6 +46,7 @@ from .license import ( key_id, license_status, parse_iso, + tage_bis, split_bundle, utcnow, verify_license, @@ -103,6 +104,7 @@ __all__ = [ "machine_identity", "new_nonce", "parse_iso", + "tage_bis", "sign", "split_bundle", "system_fingerprint", diff --git a/packages/tesm-licensing/src/tesm_licensing/license.py b/packages/tesm-licensing/src/tesm_licensing/license.py index 7ebd714..0e706bd 100644 --- a/packages/tesm-licensing/src/tesm_licensing/license.py +++ b/packages/tesm-licensing/src/tesm_licensing/license.py @@ -221,6 +221,27 @@ def verify_license( ) +def tage_bis(expires_at: datetime, *, now: datetime | None = None) -> int: + """Restlaufzeit in **angefangenen** Tagen. + + Aufgerundet, und das ist eine Entscheidung: eine Lizenz, die in zwei + Stunden ablaeuft, hat "1 Tag" und nicht "0". Null bedeutet hier + ausschliesslich *abgelaufen* -- eine gueltige Lizenz, die mit "0 Tage" + dasteht, liest sich wie eine tote. + + **Es gibt genau diese eine Rechnung.** Der Lizenzserver hatte eine zweite, + abgerundete: dieselbe Lizenz zeigte in seiner Oberflaeche "21 Tage" und im + Abruf beim Kunden "22". Zwei Zahlen fuer dieselbe Frage sind schlimmer als + eine ungenaue -- sie kosten jemanden einen Nachmittag mit der Suche nach + einem Fehler, den es nicht gibt. + """ + moment = now or utcnow() + sekunden = int((expires_at - moment).total_seconds()) + if sekunden <= 0: + return 0 + return -(-sekunden // 86400) + + def license_status( document: Mapping[str, Any], *, @@ -278,7 +299,7 @@ def license_status( days_left = 0 days_since_expiry = int((moment - expires_at).total_seconds() // 86400) else: - days_left = -(-int(delta.total_seconds()) // 86400) + days_left = tage_bis(expires_at, now=moment) days_since_expiry = None grace_active = bool(