Files
anonymisation/gui_v6/tabs/tab_about.py
Domi31tls 34c681b791 feat(gui): GUI V6 G4 — alignement visuel sur la maquette v6 (option A)
Refonte de la couche présentation pour reprendre docs/ui_mockup_v6.html, sans
changer de techno UI ni la logique G1-G3.

- theme.py : 4 thèmes aux tokens EXACTS de la maquette (sombre #1a1a2e/#16213e/
  #e94560, clair, médical, neutre), palette complète + status_color.
- ui_kit.py (nouveau) : composants stylés (Card titrée, boutons primary/secondary/
  success/pilule, StatCard, ToggleRow) appliquant la palette.
- app.py : shell étroit, header identité + version + statut licence + liseré accent,
  barre d'onglets custom (plus de CTkTabview brut), navigation par recréation,
  changement de thème à chaud.
- tab_usage : carte Apparence (sélecteur de thème), dropzone stylée, grille formats,
  barre d'actions, progression à étapes + journal, résultats en cartes statistiques.
- tab_config : sous-navigation Réglages/Masquage/Partage/Règles ; Réglages câblé au
  ConfigState (profil, moteurs NER, dossier sortie).
- tab_about : grille d'informations + bloc licence (logique inchangée).

Logique inchangée : engine_bridge, config_state, license_client/store, runner.
Tests : +9 (theme). self-test exit 0, 55 tests gui_v6, 202 tests/unit (0 régression).
Smoke construction headless (Xvfb) : 3 onglets × 4 thèmes rendus sans erreur.
Pas de pywebview, aucun .exe.

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

134 lines
5.4 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
_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
# 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))