feat(gui): binding licence-poste souple (P0-6/D-20.4, affichage sans blocage)

This commit is contained in:
2026-06-25 17:52:07 +02:00
parent dc0554e694
commit 9e87cb3122
4 changed files with 54 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ from gui_v6 import theme as theme_mod
from gui_v6 import ui_kit from gui_v6 import ui_kit
from gui_v6.config_state import ConfigState from gui_v6.config_state import ConfigState
from gui_v6.license_client import LicenseClient, LicenseStatus 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_about import AboutTab
from gui_v6.tabs.tab_config import ConfigTab from gui_v6.tabs.tab_config import ConfigTab
from gui_v6.tabs.tab_usage import UsageTab 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) 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 = [ _TABS = [
("use", "📄 Utilisation"), ("use", "📄 Utilisation"),
("cfg", "⚙️ Administration"), ("cfg", "⚙️ Administration"),
@@ -89,7 +110,8 @@ class AnonymisationApp(ctk.CTk):
def _safe_local_status(self) -> LicenseStatus: def _safe_local_status(self) -> LicenseStatus:
try: try:
return self._license_client.local_status() status = self._license_client.local_status()
return bound_local_status(status, default_machine_id())
except Exception: except Exception:
return LicenseStatus.unavailable() return LicenseStatus.unavailable()

View File

@@ -33,6 +33,7 @@ _STATUS_LABELS = {
"revoked": "Poste révoqué", "revoked": "Poste révoqué",
"invalid": "Licence invalide", "invalid": "Licence invalide",
"unavailable": "Serveur de licence indisponible", "unavailable": "Serveur de licence indisponible",
"autre_poste": "Licence liée à un autre poste",
"none": "Aucune licence", "none": "Aucune licence",
} }

View File

@@ -113,6 +113,7 @@ _STATUS_TOKEN = {
"revoked": "danger", "revoked": "danger",
"invalid": "danger", "invalid": "danger",
"unavailable": "warning", "unavailable": "warning",
"autre_poste": "warning",
"none": "text_muted", "none": "text_muted",
} }

View File

@@ -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