From 9e87cb3122943b1a50d1e183b2fa0eefa41a559e Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Thu, 25 Jun 2026 17:52:07 +0200 Subject: [PATCH] feat(gui): binding licence-poste souple (P0-6/D-20.4, affichage sans blocage) --- gui_v6/app.py | 24 ++++++++++++++++++- gui_v6/tabs/tab_about.py | 1 + gui_v6/theme.py | 1 + tests/unit/test_gui_v6_license_binding.py | 29 +++++++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_gui_v6_license_binding.py diff --git a/gui_v6/app.py b/gui_v6/app.py index 4124a76..4cf306c 100644 --- a/gui_v6/app.py +++ b/gui_v6/app.py @@ -20,6 +20,7 @@ from gui_v6 import theme as theme_mod from gui_v6 import ui_kit from gui_v6.config_state import ConfigState from gui_v6.license_client import LicenseClient, LicenseStatus +from gui_v6.machine_id import default_machine_id from gui_v6.tabs.tab_about import AboutTab from gui_v6.tabs.tab_config import ConfigTab from gui_v6.tabs.tab_usage import UsageTab @@ -32,6 +33,26 @@ def resolve_portal_url() -> str: return os.environ.get("ANON_PORTAL_URL", DEFAULT_PORTAL_URL) +def bound_local_status(status: LicenseStatus, local_machine_id: str) -> LicenseStatus: + """Annoter le statut licence selon le binding poste. + + Souple (décision D1) : on N'EMPÊCHE PAS le traitement. Si la licence locale + est valide mais liée à un autre ``machine_id`` que le poste courant (ex. + ``license.json`` copié), on le **signale** par un statut non valide d'affichage. + """ + if status.valid and status.machine_id and status.machine_id != local_machine_id: + return LicenseStatus( + valid=False, + status="autre_poste", + message="Licence liée à un autre poste", + expires_at=status.expires_at, + grace_days=status.grace_days, + machine_id=status.machine_id, + license_ref=status.license_ref, + ) + return status + + _TABS = [ ("use", "📄 Utilisation"), ("cfg", "⚙️ Administration"), @@ -89,7 +110,8 @@ class AnonymisationApp(ctk.CTk): def _safe_local_status(self) -> LicenseStatus: try: - return self._license_client.local_status() + status = self._license_client.local_status() + return bound_local_status(status, default_machine_id()) except Exception: return LicenseStatus.unavailable() diff --git a/gui_v6/tabs/tab_about.py b/gui_v6/tabs/tab_about.py index f69f9f4..6385d8e 100644 --- a/gui_v6/tabs/tab_about.py +++ b/gui_v6/tabs/tab_about.py @@ -33,6 +33,7 @@ _STATUS_LABELS = { "revoked": "Poste révoqué", "invalid": "Licence invalide", "unavailable": "Serveur de licence indisponible", + "autre_poste": "Licence liée à un autre poste", "none": "Aucune licence", } diff --git a/gui_v6/theme.py b/gui_v6/theme.py index bd17318..e423bf7 100644 --- a/gui_v6/theme.py +++ b/gui_v6/theme.py @@ -113,6 +113,7 @@ _STATUS_TOKEN = { "revoked": "danger", "invalid": "danger", "unavailable": "warning", + "autre_poste": "warning", "none": "text_muted", } diff --git a/tests/unit/test_gui_v6_license_binding.py b/tests/unit/test_gui_v6_license_binding.py new file mode 100644 index 0000000..20469a4 --- /dev/null +++ b/tests/unit/test_gui_v6_license_binding.py @@ -0,0 +1,29 @@ +from gui_v6.app import bound_local_status +from gui_v6.license_client import LicenseStatus + + +def test_binding_flags_other_machine(): + st = LicenseStatus(valid=True, status="active", machine_id="AAAA1111") + out = bound_local_status(st, "BBBB2222") + assert out.valid is False + assert out.status == "autre_poste" + assert "autre poste" in out.message.lower() + + +def test_binding_ok_same_machine(): + st = LicenseStatus(valid=True, status="active", machine_id="AAAA1111") + out = bound_local_status(st, "AAAA1111") + assert out.valid is True + assert out.status == "active" + + +def test_binding_noop_without_machine_id(): + st = LicenseStatus(valid=True, status="active", machine_id=None) + assert bound_local_status(st, "AAAA1111").valid is True + + +def test_binding_passes_through_invalid_status(): + st = LicenseStatus(valid=False, status="expired", machine_id="OTHER") + out = bound_local_status(st, "AAAA1111") + assert out.status == "expired" + assert out.valid is False