252 lines
9.0 KiB
Python
252 lines
9.0 KiB
Python
"""Shell de la GUI V6 (G4 — alignement maquette).
|
||
|
||
Reproduit l'identité de ``docs/ui_mockup_v6.html`` : shell étroit, header avec
|
||
identité produit + version + statut licence + liseré accent, barre d'onglets
|
||
custom (pas CTkTabview brut), navigation par panneaux mis en cache après leur
|
||
première ouverture visible, changement de thème à chaud. La logique (runner
|
||
moteur, config, licence) est inchangée.
|
||
|
||
La fenêtre n'est créée qu'à l'instanciation de :class:`AnonymisationApp`.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Optional
|
||
|
||
import customtkinter as ctk
|
||
|
||
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
|
||
|
||
DEFAULT_PORTAL_URL = "https://app.aivanov.eu"
|
||
|
||
|
||
def resolve_portal_url() -> str:
|
||
"""URL du portail : env ``ANON_PORTAL_URL`` sinon défaut prod."""
|
||
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"),
|
||
("about", "ℹ️ À propos"),
|
||
]
|
||
|
||
|
||
class AnonymisationApp(ctk.CTk):
|
||
def __init__(
|
||
self,
|
||
license_client: Optional[LicenseClient] = None,
|
||
theme_name: str = theme_mod.DEFAULT_THEME,
|
||
) -> None:
|
||
super().__init__()
|
||
self._theme_name = theme_name
|
||
self._license_client = license_client or LicenseClient(resolve_portal_url())
|
||
self._config = ConfigState()
|
||
self._active = "use"
|
||
self._tab_buttons: dict = {}
|
||
self._tab_frames: dict = {}
|
||
self._visible_tab = None
|
||
|
||
self.title("Pseudonymisation de vos documents — bêta")
|
||
self.geometry("820x880")
|
||
self.minsize(720, 680)
|
||
self._render()
|
||
|
||
# -- thème / rendu ----------------------------------------------------
|
||
|
||
def set_theme(self, name: str) -> None:
|
||
self._theme_name = name
|
||
self._render()
|
||
|
||
def _render(self) -> None:
|
||
self._palette = theme_mod.apply_theme(self._theme_name)
|
||
p = self._palette
|
||
try:
|
||
self.configure(fg_color=p["bg"])
|
||
except Exception:
|
||
pass
|
||
for child in self.winfo_children():
|
||
child.destroy()
|
||
# Les frames d'onglets mis en cache étaient des enfants détruits ci-dessus :
|
||
# on vide le cache pour que ``_show`` recrée proprement l'onglet actif
|
||
# (sinon on re-packe un widget mort → onglet vide / TclError au changement de thème).
|
||
self._tab_frames = {}
|
||
self._visible_tab = None
|
||
self._build_header(p)
|
||
self._build_tabsbar(p)
|
||
self._content = ctk.CTkScrollableFrame(self, fg_color=p["bg"])
|
||
self._content.pack(fill="both", expand=True)
|
||
self._show(self._active)
|
||
|
||
# -- header -----------------------------------------------------------
|
||
|
||
def _safe_local_status(self) -> LicenseStatus:
|
||
try:
|
||
status = self._license_client.local_status()
|
||
return bound_local_status(status, default_machine_id())
|
||
except Exception:
|
||
return LicenseStatus.unavailable()
|
||
|
||
def _build_header(self, p: dict) -> None:
|
||
header = ctk.CTkFrame(self, fg_color=p["card"], corner_radius=0)
|
||
header.pack(fill="x")
|
||
identity = ctk.CTkFrame(header, fg_color="transparent")
|
||
identity.pack(side="left", padx=16, pady=10)
|
||
ctk.CTkLabel(
|
||
identity, text="🛡️ aivanonym", text_color=p["text"], font=ui_kit.font(18, "bold")
|
||
).pack(side="left")
|
||
ctk.CTkLabel(
|
||
identity,
|
||
text="bêta",
|
||
text_color="#ffffff",
|
||
fg_color=p["primary"],
|
||
corner_radius=8,
|
||
font=ui_kit.font(10, "bold"),
|
||
).pack(side="left", padx=(8, 0), ipadx=6, ipady=1)
|
||
|
||
status = self._safe_local_status()
|
||
ctk.CTkLabel(
|
||
header,
|
||
text=f"licence : {status.status}",
|
||
text_color=theme_mod.status_color(self._theme_name, status.status),
|
||
font=ui_kit.font(11),
|
||
).pack(side="right", padx=(8, 16))
|
||
ctk.CTkLabel(
|
||
header, text="v6.0", text_color=p["text_muted"], font=ui_kit.font(11)
|
||
).pack(side="right", padx=4)
|
||
|
||
# Liseré accent sous le header (border-bottom 3px primary).
|
||
ctk.CTkFrame(self, fg_color=p["primary"], height=3, corner_radius=0).pack(fill="x")
|
||
|
||
# -- barre d'onglets --------------------------------------------------
|
||
|
||
def _build_tabsbar(self, p: dict) -> None:
|
||
bar = ctk.CTkFrame(self, fg_color=p["card"], corner_radius=0)
|
||
bar.pack(fill="x")
|
||
self._tab_buttons = {}
|
||
for key, label in _TABS:
|
||
active = key == self._active
|
||
btn = ctk.CTkButton(
|
||
bar,
|
||
text=label,
|
||
command=lambda k=key: self._show(k),
|
||
fg_color="transparent",
|
||
hover_color=p["card_border"],
|
||
text_color=p["primary"] if active else p["text_dim"],
|
||
font=ui_kit.font(13, "bold" if active else "normal"),
|
||
corner_radius=0,
|
||
width=10,
|
||
)
|
||
btn.pack(side="left", padx=4, pady=4)
|
||
self._tab_buttons[key] = btn
|
||
|
||
def _refresh_tabbar(self) -> None:
|
||
p = self._palette
|
||
for key, btn in self._tab_buttons.items():
|
||
active = key == self._active
|
||
btn.configure(
|
||
text_color=p["primary"] if active else p["text_dim"],
|
||
font=ui_kit.font(13, "bold" if active else "normal"),
|
||
)
|
||
|
||
# -- contenu ----------------------------------------------------------
|
||
|
||
def _create_tab(self, key: str):
|
||
p = self._palette
|
||
status = self._safe_local_status()
|
||
if key == "use":
|
||
return UsageTab(
|
||
self._content,
|
||
palette=p,
|
||
config_provider=lambda: self._config,
|
||
on_theme_change=self.set_theme,
|
||
current_theme=self._theme_name,
|
||
usage_reporter=self._report_usage,
|
||
)
|
||
if key == "cfg":
|
||
return ConfigTab(self._content, palette=p, state=self._config)
|
||
return AboutTab(
|
||
self._content,
|
||
palette=p,
|
||
status=status,
|
||
theme_name=self._theme_name,
|
||
license_client=self._license_client,
|
||
)
|
||
|
||
# -- télémétrie d'usage -----------------------------------------------
|
||
|
||
def _usage_session(self):
|
||
if getattr(self, "_usage_http_session", None) is None:
|
||
try:
|
||
import requests
|
||
|
||
self._usage_http_session = requests.Session()
|
||
except Exception:
|
||
self._usage_http_session = None
|
||
return self._usage_http_session
|
||
|
||
def _report_usage(self, summary) -> None:
|
||
"""Envoie la télémétrie d'usage en fin de run (non bloquant, best-effort).
|
||
|
||
N'envoie rien si aucune licence locale valide. Ne lève jamais.
|
||
"""
|
||
try:
|
||
from gui_v6 import __version__ as gui_version
|
||
from gui_v6 import usage_telemetry
|
||
from gui_v6.machine_id import default_machine_id
|
||
|
||
session = self._usage_session()
|
||
if session is None:
|
||
return
|
||
status = self._safe_local_status()
|
||
base_url = getattr(self._license_client, "_base_url", "") or resolve_portal_url()
|
||
usage_telemetry.report_run_summary(
|
||
summary,
|
||
base_url=base_url,
|
||
license_ref=getattr(status, "license_ref", None),
|
||
machine_id=default_machine_id(),
|
||
session=session,
|
||
app_name="gui_v6",
|
||
app_version=gui_version,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
def _show(self, key: str) -> None:
|
||
self._active = key
|
||
self._refresh_tabbar()
|
||
if self._visible_tab is not None:
|
||
self._tab_frames[self._visible_tab].pack_forget()
|
||
if key not in self._tab_frames:
|
||
self._tab_frames[key] = self._create_tab(key)
|
||
self._tab_frames[key].pack(fill="both", expand=True)
|
||
self._visible_tab = key
|