feat(gui): câblage upload diagnostics en fin de run (E3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 10:44:02 +02:00
parent 8eb8cf9999
commit 8f9107a27f
3 changed files with 76 additions and 0 deletions

View File

@@ -193,6 +193,7 @@ class AnonymisationApp(ctk.CTk):
on_theme_change=self.set_theme, on_theme_change=self.set_theme,
current_theme=self._theme_name, current_theme=self._theme_name,
usage_reporter=self._report_usage, usage_reporter=self._report_usage,
diag_reporter=self._report_diagnostics,
) )
if key == "cfg": if key == "cfg":
return ConfigTab(self._content, palette=p, state=self._config, config_path=self._user_config_path) return ConfigTab(self._content, palette=p, state=self._config, config_path=self._user_config_path)
@@ -243,6 +244,36 @@ class AnonymisationApp(ctk.CTk):
except Exception: except Exception:
pass pass
def _report_diagnostics(self, summary) -> None:
"""Envoie les diagnostics 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 diagnostics
from gui_v6.logging_setup import log_file_path
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()
spool = log_file_path().parent / "diagnostics_spool.jsonl"
diagnostics.report_run_diagnostics(
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,
spool_path=spool,
)
except Exception:
pass
def _show(self, key: str) -> None: def _show(self, key: str) -> None:
self._active = key self._active = key
self._refresh_tabbar() self._refresh_tabbar()

View File

@@ -68,6 +68,7 @@ class UsageTab(ctk.CTkFrame):
on_theme_change=None, on_theme_change=None,
current_theme: str = theme_mod.DEFAULT_THEME, current_theme: str = theme_mod.DEFAULT_THEME,
usage_reporter=None, usage_reporter=None,
diag_reporter=None,
**kwargs, **kwargs,
): ):
self._p = palette or theme_mod.get_palette(current_theme) self._p = palette or theme_mod.get_palette(current_theme)
@@ -80,6 +81,9 @@ class UsageTab(ctk.CTkFrame):
# Callback(summary) appelé en fin de run pour la télémétrie d'usage # Callback(summary) appelé en fin de run pour la télémétrie d'usage
# (envoi non bloquant, injecté par l'app avec le contexte licence). # (envoi non bloquant, injecté par l'app avec le contexte licence).
self._usage_reporter = usage_reporter self._usage_reporter = usage_reporter
# Callback(summary) appelé en fin de run pour les diagnostics RGPD
# (envoi non bloquant, injecté par l'app avec le contexte licence).
self._diag_reporter = diag_reporter
self._input_path: Path | None = None self._input_path: Path | None = None
self._output_dir: Path | None = None self._output_dir: Path | None = None
@@ -320,6 +324,7 @@ class UsageTab(ctk.CTkFrame):
self._show_results(summary) self._show_results(summary)
self._show_failure_hint(summary) self._show_failure_hint(summary)
self._send_usage_telemetry(summary) self._send_usage_telemetry(summary)
self._send_diagnostics(summary)
def _send_usage_telemetry(self, summary) -> None: def _send_usage_telemetry(self, summary) -> None:
"""Envoie la télémétrie d'usage en fin de run, sans bloquer l'UI ni le run.""" """Envoie la télémétrie d'usage en fin de run, sans bloquer l'UI ni le run."""
@@ -335,6 +340,20 @@ class UsageTab(ctk.CTkFrame):
threading.Thread(target=work, daemon=True).start() threading.Thread(target=work, daemon=True).start()
def _send_diagnostics(self, summary) -> None:
"""Envoie les diagnostics en fin de run, sans bloquer l'UI ni le run."""
reporter = self._diag_reporter
if reporter is None:
return
def work():
try:
reporter(summary)
except Exception:
pass # un échec diagnostic ne doit jamais remonter
threading.Thread(target=work, daemon=True).start()
def _show_results(self, summary) -> None: def _show_results(self, summary) -> None:
p = self._p p = self._p
for w in self._stats_row.winfo_children(): for w in self._stats_row.winfo_children():

View File

@@ -108,3 +108,29 @@ def test_flush_spool_sends_and_clears(tmp_path):
sent = diagnostics.flush_spool(spool, diagnostics.DiagnosticsClient( sent = diagnostics.flush_spool(spool, diagnostics.DiagnosticsClient(
"https://app.aivanov.eu", session=_FakeSession(status_code=200))) "https://app.aivanov.eu", session=_FakeSession(status_code=200)))
assert sent == 2 and not spool.exists() assert sent == 2 and not spool.exists()
def test_tab_send_diagnostics_calls_reporter():
import threading
from gui_v6.tabs.tab_usage import UsageTab
tab = object.__new__(UsageTab) # pas de Tk : on teste juste le helper
seen = {}
done = threading.Event()
def reporter(summary):
seen["summary"] = summary
done.set()
tab._diag_reporter = reporter
tab._send_diagnostics(SimpleNamespace(documents=[], failed=0))
assert done.wait(timeout=2.0)
assert seen["summary"] is not None
def test_tab_send_diagnostics_noop_without_reporter():
from gui_v6.tabs.tab_usage import UsageTab
tab = object.__new__(UsageTab)
tab._diag_reporter = None
tab._send_diagnostics(SimpleNamespace(documents=[])) # ne lève pas