From dd367728f64ae1437c479b911262463e34ecc7aa Mon Sep 17 00:00:00 2001 From: beckline Date: Mon, 16 Feb 2026 01:36:42 +0300 Subject: [PATCH] ui: show backend traffic audit in dialog log --- selective-vpn-gui/api_client.py | 25 ++++++++++++++++++++++ selective-vpn-gui/dashboard_controller.py | 4 ++++ selective-vpn-gui/traffic_mode_dialog.py | 26 +++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/selective-vpn-gui/api_client.py b/selective-vpn-gui/api_client.py index d17b565..02df939 100644 --- a/selective-vpn-gui/api_client.py +++ b/selective-vpn-gui/api_client.py @@ -173,6 +173,15 @@ class TrafficAppProfileSaveResult: profile: Optional[TrafficAppProfile] = None +@dataclass(frozen=True) +class TrafficAudit: + ok: bool + message: str + now: str + pretty: str + issues: List[str] + + @dataclass(frozen=True) class TrafficCandidateSubnet: @@ -1041,6 +1050,22 @@ class ApiClient: stderr="", ) + def traffic_audit_get(self) -> TrafficAudit: + data = cast( + Dict[str, Any], + self._json(self._request("GET", "/api/v1/traffic/audit")) or {}, + ) + raw_issues = data.get("issues") or [] + if not isinstance(raw_issues, list): + raw_issues = [] + return TrafficAudit( + ok=bool(data.get("ok", False)), + message=strip_ansi(str(data.get("message") or "").strip()), + now=str(data.get("now") or "").strip(), + pretty=strip_ansi(str(data.get("pretty") or "").strip()), + issues=[strip_ansi(str(x)).strip() for x in raw_issues if str(x).strip()], + ) + # DNS / SmartDNS def dns_upstreams_get(self) -> DnsUpstreams: data = cast(Dict[str, Any], self._json(self._request("GET", "/api/v1/dns-upstreams")) or {}) diff --git a/selective-vpn-gui/dashboard_controller.py b/selective-vpn-gui/dashboard_controller.py index 7a3e13a..e195111 100644 --- a/selective-vpn-gui/dashboard_controller.py +++ b/selective-vpn-gui/dashboard_controller.py @@ -37,6 +37,7 @@ from api_client import ( TrafficAppMarkItem, TrafficAppProfile, TrafficAppProfileSaveResult, + TrafficAudit, TrafficInterfaces, TrafficModeStatus, TraceDump, @@ -767,6 +768,9 @@ class DashboardController: def traffic_app_profile_delete(self, id: str) -> CmdResult: return self.client.traffic_app_profile_delete(id) + def traffic_audit(self) -> TrafficAudit: + return self.client.traffic_audit_get() + def routes_nft_progress_from_event(self, ev: Event) -> RoutesNftProgressView: """ Превращает Event(kind='routes_nft_progress') в удобную модель diff --git a/selective-vpn-gui/traffic_mode_dialog.py b/selective-vpn-gui/traffic_mode_dialog.py index 5f953a1..f1970c7 100644 --- a/selective-vpn-gui/traffic_mode_dialog.py +++ b/selective-vpn-gui/traffic_mode_dialog.py @@ -580,6 +580,16 @@ RU: Восстанавливает маршруты/nft из последнег self.txt_app.setReadOnly(True) tab_log = QWidget() tab_log_layout = QVBoxLayout(tab_log) + row_log = QHBoxLayout() + self.btn_app_audit = QPushButton("Run audit") + self.btn_app_audit.setToolTip( + "EN: Runs backend traffic audit (duplicates + nft consistency) and prints it here.\n" + "RU: Запускает backend-аудит трафика (дубли + nft консистентность) и печатает сюда." + ) + self.btn_app_audit.clicked.connect(self.on_app_audit) + row_log.addWidget(self.btn_app_audit) + row_log.addStretch(1) + tab_log_layout.addLayout(row_log) tab_log_layout.addWidget(self.txt_app, stretch=1) self.apps_tabs.addTab(tab_log, "Log") @@ -1742,6 +1752,22 @@ RU: Применяет policy-rules и проверяет health. При оши self._safe(work, title="App picker error") + def on_app_audit(self) -> None: + def work() -> None: + audit = self.ctrl.traffic_audit() + pretty = (getattr(audit, "pretty", "") or "").strip() + if not pretty: + pretty = f"ok={getattr(audit, 'ok', False)} message={getattr(audit, 'message', '')}" + self._append_app_log("[audit]\n" + pretty) + issues = list(getattr(audit, "issues", []) or []) + ok = bool(getattr(audit, "ok", False)) and len(issues) == 0 + if issues: + self._set_action_status(f"Audit: issues={len(issues)}", ok=False) + else: + self._set_action_status("Audit: OK", ok=True) + + self._safe(work, title="Traffic audit error") + def _run_systemd_unit(self, cmdline: str, *, unit: str) -> tuple[str, str]: args = shlex.split(cmdline or "") if not args: