diff --git a/gui_v6/app.py b/gui_v6/app.py index 13b3130..c2b8209 100644 --- a/gui_v6/app.py +++ b/gui_v6/app.py @@ -193,6 +193,7 @@ class AnonymisationApp(ctk.CTk): on_theme_change=self.set_theme, current_theme=self._theme_name, usage_reporter=self._report_usage, + diag_reporter=self._report_diagnostics, ) if key == "cfg": 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: 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: self._active = key self._refresh_tabbar() diff --git a/gui_v6/tabs/tab_usage.py b/gui_v6/tabs/tab_usage.py index 92d3822..b362ca5 100644 --- a/gui_v6/tabs/tab_usage.py +++ b/gui_v6/tabs/tab_usage.py @@ -68,6 +68,7 @@ class UsageTab(ctk.CTkFrame): on_theme_change=None, current_theme: str = theme_mod.DEFAULT_THEME, usage_reporter=None, + diag_reporter=None, **kwargs, ): 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 # (envoi non bloquant, injecté par l'app avec le contexte licence). 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._output_dir: Path | None = None @@ -320,6 +324,7 @@ class UsageTab(ctk.CTkFrame): self._show_results(summary) self._show_failure_hint(summary) self._send_usage_telemetry(summary) + self._send_diagnostics(summary) 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.""" @@ -335,6 +340,20 @@ class UsageTab(ctk.CTkFrame): 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: p = self._p for w in self._stats_row.winfo_children(): diff --git a/tests/unit/test_gui_v6_diagnostics.py b/tests/unit/test_gui_v6_diagnostics.py index ba89773..f41ff81 100644 --- a/tests/unit/test_gui_v6_diagnostics.py +++ b/tests/unit/test_gui_v6_diagnostics.py @@ -108,3 +108,29 @@ def test_flush_spool_sends_and_clears(tmp_path): sent = diagnostics.flush_spool(spool, diagnostics.DiagnosticsClient( "https://app.aivanov.eu", session=_FakeSession(status_code=200))) 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