Files
anonymisation/gui_v6/tabs/tab_about.py
Domi31tls 6a0a5811a5 fix(gui): retours Dom GUI V6 — thème, Administration, Règles, aide
Cinq retours utilisateur sur l'exécutable Windows GUI V6.

- Thème : `_render()` vidait les widgets mais conservait le cache
  `_tab_frames`/`_visible_tab` → l'onglet Utilisation se vidait (TclError
  sur widget détruit) au changement de thème. Reset du cache dans
  `_render()` → onglet actif recréé proprement.
- Onglet principal « Configuration » → « Administration » (clé interne
  inchangée).
- Sous-onglet « Règles  2 » → « Règles » (le « 2 » était un badge non
  câblé).
- Actions de maquette non câblées (Partage Export/Import, Règles Nouvelle
  règle/Recharger/Tester/Fermer) désactivées + suffixe « (à venir) » via
  `_mockup_button` : plus aucune action morte qui semble fonctionner.
- Aide « ? » restaurée (façon V5) : `ui_kit.HelpButton`/`help_button`
  réutilisable ouvrant une fenêtre d'aide en français simple, posée sur
  Utilisation, Administration (Réglages/Masquage/Partage/Règles) et
  À propos. Partage : phrase visible + aide expliquant qu'on partage les
  réglages, jamais les documents patients.

`tests/unit/test_gui_v6_app_shell.py` : régression thème, libellés,
présence d'aide, navigation. 228 tests unit OK (0 régression), self-test
GUI V6 OK. V5/moteur/app_aivanov non touchés, aucune dépendance ajoutée.
Verdict Qwen requis avant push/build/diffusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:39:53 +02:00

154 lines
6.3 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Onglet « À propos » de la GUI V6 (G4 — alignement maquette).
Grille d'informations (agrid) + bloc licence (activation par clef, vérification).
La logique licence est inchangée : aucun token journalisé, aucun appel réseau au
démarrage (seul ``local_status`` est lu).
"""
from __future__ import annotations
from typing import Optional
import customtkinter as ctk
from gui_v6 import __version__ as GUI_VERSION
from gui_v6 import theme as theme_mod
from gui_v6 import ui_kit
from gui_v6.license_client import LicenseClient, LicenseStatus
from gui_v6.machine_id import default_machine_id
_HELP_ABOUT = (
"Cet écran affiche la version de l'application, les moteurs utilisés et "
"l'identifiant de ce poste.\n\n"
"La licence s'active avec une clef fournie par votre administrateur. "
"L'activation se fait sans envoyer aucun document : seule la clef est vérifiée.\n\n"
"Le traitement des documents reste 100 % local sur ce poste."
)
_STATUS_LABELS = {
"active": "Licence active",
"grace": "Licence en période de grâce",
"expired": "Licence expirée",
"revoked": "Poste révoqué",
"invalid": "Licence invalide",
"unavailable": "Serveur de licence indisponible",
"none": "Aucune licence",
}
def _build_info() -> str:
try:
import build_info # type: ignore
return f"{getattr(build_info, 'BUILD_COMMIT', '?')} ({getattr(build_info, 'BUILD_BRANCH', '?')})"
except Exception:
return "indisponible"
class AboutTab(ctk.CTkFrame):
def __init__(
self,
master,
status: Optional[LicenseStatus] = None,
theme_name: str = theme_mod.DEFAULT_THEME,
license_client: Optional[LicenseClient] = None,
palette: dict | None = None,
**kwargs,
) -> None:
self._p = palette or theme_mod.get_palette(theme_name)
super().__init__(master, fg_color=self._p["bg"], **kwargs)
self._theme_name = theme_name
self._client = license_client
self._machine_id = default_machine_id()
self._status = status or LicenseStatus.none()
self._build()
def _build(self) -> None:
p = self._p
# Bandeau d'introduction + aide « ? »
intro = ctk.CTkFrame(self, fg_color="transparent")
intro.pack(fill="x", padx=14, pady=(12, 0))
ctk.CTkLabel(
intro,
text="Informations sur l'application et activation de votre licence.",
text_color=p["text_dim"],
font=ui_kit.font(12),
anchor="w",
).pack(side="left", padx=(2, 6))
ui_kit.help_button(intro, p, _HELP_ABOUT, title="À propos / Licence").pack(side="right", padx=2)
# Grille d'informations
info = ui_kit.Card(self, p, title=" Informations")
info.pack(fill="x", padx=14, pady=(14, 7))
grid = ctk.CTkFrame(info, fg_color="transparent")
grid.pack(fill="x", padx=16, pady=(0, 14))
items = [
("🏷️", "Version", f"Interface V6 — {GUI_VERSION}"),
("📅", "Build", _build_info()),
("🧠", "Moteurs NER", "CamemBERT · EDS-Pseudo · GLiNER"),
("🔒", "Traitement", "100 % local — aucune donnée transmise"),
("📚", "Gazetteers", "INSEE 219K · FINESS 108K · BDPM 7K"),
("📁", "Formats", "PDF · DOCX · ODT · RTF · TXT · Images"),
("🖥️", "Poste", self._machine_id),
]
for col in (0, 1):
grid.grid_columnconfigure(col, weight=1)
for idx, (icon, key, val) in enumerate(items):
cell = ctk.CTkFrame(grid, fg_color="transparent")
cell.grid(row=idx // 2, column=idx % 2, sticky="w", padx=4, pady=5)
ctk.CTkLabel(cell, text=icon, font=ui_kit.font(18)).pack(side="left", padx=(0, 8))
txt = ctk.CTkFrame(cell, fg_color="transparent")
txt.pack(side="left")
ctk.CTkLabel(txt, text=key.upper(), text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w")
ctk.CTkLabel(txt, text=val, text_color=p["text"], font=ui_kit.font(12, "bold"), anchor="w").pack(anchor="w")
# Bloc licence
lic = ui_kit.Card(self, p, title="🔑 Licence")
lic.pack(fill="x", padx=14, pady=7)
self._status_label = ctk.CTkLabel(lic, text="", text_color=p["text"], font=ui_kit.font(13), anchor="w", justify="left")
self._status_label.pack(anchor="w", padx=16, pady=(0, 8))
block = ctk.CTkFrame(lic, fg_color="transparent")
block.pack(fill="x", padx=16, pady=(0, 14))
self._key_entry = ctk.CTkEntry(block, width=240, placeholder_text="Clef d'activation",
fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"])
self._key_entry.pack(side="left", padx=(0, 8))
self._activate_btn = ui_kit.primary_button(block, p, "Activer", command=self._activate)
self._activate_btn.pack(side="left", padx=(0, 8))
self._check_btn = ui_kit.secondary_button(block, p, "Vérifier", command=self._check)
self._check_btn.pack(side="left")
if self._client is None:
self._activate_btn.configure(state="disabled")
self._check_btn.configure(state="disabled")
self.set_status(self._status)
def set_status(self, status: LicenseStatus) -> None:
self._status = status
label = _STATUS_LABELS.get(status.status, status.status)
text = f"État : {label}"
if status.expires_at:
text += f" · expire le {status.expires_at}"
if status.message:
text += f"\n{status.message}"
self._status_label.configure(text=text, text_color=theme_mod.status_color(self._theme_name, status.status))
def _activate(self) -> None:
if self._client is None:
return
token = self._key_entry.get().strip()
if not token:
return
status = self._client.activate(token, self._machine_id)
self.set_status(status)
self._key_entry.delete(0, "end")
def _check(self) -> None:
if self._client is None:
return
ref = self._status.license_ref
if not ref:
self.set_status(LicenseStatus.none("Aucune licence à vérifier"))
return
self.set_status(self._client.check(ref, self._machine_id))