feat(gui): câbler l'envoi de la télémétrie d'usage en fin de run
Le module usage_telemetry est maintenant réellement branché : la GUI V6 envoie les statistiques au portail après chaque run (les stats web restaient vides sans cela). - processing_runner : RunSummary porte une liste DocResult (ordinal, page_count via page_count_for, status, duration_ms, extension) — peuplée dans la boucle. Aucun nom/chemin de fichier. - usage_telemetry : report_run_summary(summary, base_url, license_ref, machine_id, session, ...) construit le payload depuis le RunSummary et l'envoie (non bloquant). N'envoie RIEN sans license_ref. Spool JSONL si échec réseau. - tab_usage : _finish() déclenche l'envoi en thread daemon (jamais bloquant pour l'UI ni le run). - app : fournit le reporter à UsageTab avec le contexte licence (base_url du LicenseClient, license_ref via local_status, machine_id, app_version). Tests : RunSummary.documents peuplé (0 chemin) ; report_run_summary (payload correct, réseau KO → spool sans crash, pas d'envoi sans licence) ; _finish appelle le reporter. 252 tests unit OK (0 régression), self-test OK. V5/moteur/app_aivanov intacts, 0 dépendance. Aucun build/push sans GO Dom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@ arrêt coopératif (entre deux documents).
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
from gui_batch_paths import build_batch_output_dir, list_supported_documents
|
||||
from gui_v6.usage_telemetry import page_count_for
|
||||
|
||||
# process_fn(doc_path, out_dir) -> dict de sortie (ignoré par le runner).
|
||||
ProcessFn = Callable[[Path, Path], dict]
|
||||
@@ -61,6 +63,20 @@ def discover_documents(input_path, extensions: Optional[Sequence[str]] = None) -
|
||||
return []
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocResult:
|
||||
"""Détail anonymisé d'un document traité (pour la télémétrie d'usage).
|
||||
|
||||
RGPD : aucun nom ni chemin de fichier — uniquement des métadonnées.
|
||||
"""
|
||||
|
||||
ordinal: int
|
||||
page_count: Optional[int]
|
||||
status: str # "success" | "failed"
|
||||
duration_ms: Optional[int]
|
||||
extension: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunSummary:
|
||||
"""Résultat d'un run : compteurs et erreurs par document."""
|
||||
@@ -70,6 +86,7 @@ class RunSummary:
|
||||
failed: int = 0
|
||||
stopped: bool = False
|
||||
errors: list = field(default_factory=list) # list[tuple[str, str]] (nom, message)
|
||||
documents: list = field(default_factory=list) # list[DocResult] (anonymisé)
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
@@ -148,6 +165,11 @@ class ProcessingRunner:
|
||||
break
|
||||
if on_progress:
|
||||
on_progress(index - 1, summary.total, doc.name)
|
||||
# Détails anonymisés pour la télémétrie (jamais le nom/chemin).
|
||||
extension = doc.suffix.lstrip(".").lower() or None
|
||||
page_count = page_count_for(doc)
|
||||
started = time.monotonic()
|
||||
status = "success"
|
||||
try:
|
||||
if input_path.is_dir():
|
||||
doc_out = build_batch_output_dir(root_dir, out_root, doc)
|
||||
@@ -158,9 +180,19 @@ class ProcessingRunner:
|
||||
summary.succeeded += 1
|
||||
log(f"OK : {doc.name}")
|
||||
except Exception as exc: # un échec n'interrompt pas le lot
|
||||
status = "failed"
|
||||
summary.failed += 1
|
||||
summary.errors.append((doc.name, str(exc)))
|
||||
log(f"ÉCHEC : {doc.name} — {exc}")
|
||||
summary.documents.append(
|
||||
DocResult(
|
||||
ordinal=index - 1,
|
||||
page_count=page_count,
|
||||
status=status,
|
||||
duration_ms=int((time.monotonic() - started) * 1000),
|
||||
extension=extension,
|
||||
)
|
||||
)
|
||||
if on_progress:
|
||||
on_progress(index, summary.total, doc.name)
|
||||
return summary
|
||||
|
||||
Reference in New Issue
Block a user