traffic: expose runtime appmarks items + show in gui
This commit is contained in:
@@ -138,6 +138,21 @@ class TrafficAppMarksResult:
|
||||
timeout_sec: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrafficAppMarkItem:
|
||||
id: int
|
||||
target: str # vpn|direct
|
||||
cgroup: str
|
||||
cgroup_rel: str
|
||||
level: int
|
||||
unit: str
|
||||
command: str
|
||||
app_key: str
|
||||
added_at: str
|
||||
expires_at: str
|
||||
remaining_sec: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrafficAppProfile:
|
||||
id: str
|
||||
@@ -839,6 +854,43 @@ class ApiClient:
|
||||
message=str(data.get("message") or ""),
|
||||
)
|
||||
|
||||
def traffic_appmarks_items(self) -> List[TrafficAppMarkItem]:
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(self._request("GET", "/api/v1/traffic/appmarks/items")) or {},
|
||||
)
|
||||
raw = data.get("items") or []
|
||||
if not isinstance(raw, list):
|
||||
raw = []
|
||||
|
||||
out: List[TrafficAppMarkItem] = []
|
||||
for it in raw:
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
try:
|
||||
mid = int(it.get("id", 0) or 0)
|
||||
except Exception:
|
||||
mid = 0
|
||||
tgt = str(it.get("target") or "").strip().lower()
|
||||
if mid <= 0 or tgt not in ("vpn", "direct"):
|
||||
continue
|
||||
out.append(
|
||||
TrafficAppMarkItem(
|
||||
id=mid,
|
||||
target=tgt,
|
||||
cgroup=str(it.get("cgroup") or "").strip(),
|
||||
cgroup_rel=str(it.get("cgroup_rel") or "").strip(),
|
||||
level=int(it.get("level", 0) or 0),
|
||||
unit=str(it.get("unit") or "").strip(),
|
||||
command=str(it.get("command") or "").strip(),
|
||||
app_key=str(it.get("app_key") or "").strip(),
|
||||
added_at=str(it.get("added_at") or "").strip(),
|
||||
expires_at=str(it.get("expires_at") or "").strip(),
|
||||
remaining_sec=int(it.get("remaining_sec", 0) or 0),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
def traffic_appmarks_apply(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -34,6 +34,7 @@ from api_client import (
|
||||
TrafficCandidates,
|
||||
TrafficAppMarksResult,
|
||||
TrafficAppMarksStatus,
|
||||
TrafficAppMarkItem,
|
||||
TrafficAppProfile,
|
||||
TrafficAppProfileSaveResult,
|
||||
TrafficInterfaces,
|
||||
@@ -715,6 +716,9 @@ class DashboardController:
|
||||
def traffic_appmarks_status(self) -> TrafficAppMarksStatus:
|
||||
return self.client.traffic_appmarks_status()
|
||||
|
||||
def traffic_appmarks_items(self) -> List[TrafficAppMarkItem]:
|
||||
return self.client.traffic_appmarks_items()
|
||||
|
||||
def traffic_appmarks_apply(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -393,6 +393,44 @@ RU: Восстанавливает маршруты/nft из последнег
|
||||
|
||||
tab_apps_layout.addWidget(run_group)
|
||||
|
||||
marks_group = QGroupBox("Active runtime marks (TTL)")
|
||||
marks_layout = QVBoxLayout(marks_group)
|
||||
|
||||
marks_row = QHBoxLayout()
|
||||
self.btn_marks_refresh = QPushButton("Refresh marks")
|
||||
self.btn_marks_refresh.setToolTip(
|
||||
"EN: Reload active runtime marks from backend (prunes expired).\n"
|
||||
"RU: Обновить активные runtime-метки из backend (просроченные удаляются)."
|
||||
)
|
||||
self.btn_marks_refresh.clicked.connect(self.refresh_appmarks_items)
|
||||
marks_row.addWidget(self.btn_marks_refresh)
|
||||
|
||||
self.btn_marks_unmark = QPushButton("Unmark selected")
|
||||
self.btn_marks_unmark.setToolTip(
|
||||
"EN: Remove routing marks for selected items (does not necessarily stop the app).\n"
|
||||
"RU: Удалить метки маршрутизации для выбранных элементов (не обязательно останавливает приложение)."
|
||||
)
|
||||
self.btn_marks_unmark.clicked.connect(self.on_appmarks_unmark_selected)
|
||||
marks_row.addWidget(self.btn_marks_unmark)
|
||||
|
||||
marks_row.addStretch(1)
|
||||
marks_layout.addLayout(marks_row)
|
||||
|
||||
self.lst_marks = QListWidget()
|
||||
self.lst_marks.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.lst_marks.setToolTip(
|
||||
"EN: Active runtime marks. Stored by backend with TTL.\n"
|
||||
"RU: Активные runtime-метки. Хранятся backend с TTL."
|
||||
)
|
||||
self.lst_marks.setFixedHeight(140)
|
||||
marks_layout.addWidget(self.lst_marks)
|
||||
|
||||
self.lbl_marks = QLabel("Active marks: —")
|
||||
self.lbl_marks.setStyleSheet("color: gray;")
|
||||
marks_layout.addWidget(self.lbl_marks)
|
||||
|
||||
tab_apps_layout.addWidget(marks_group)
|
||||
|
||||
scopes_group = QGroupBox("Active svpn units (systemd --user)")
|
||||
scopes_layout = QVBoxLayout(scopes_group)
|
||||
|
||||
@@ -546,6 +584,7 @@ RU: Применяет policy-rules и проверяет health. При оши
|
||||
QtCore.QTimer.singleShot(0, self.refresh_state)
|
||||
QtCore.QTimer.singleShot(0, self.refresh_app_profiles)
|
||||
QtCore.QTimer.singleShot(0, self.refresh_appmarks_counts)
|
||||
QtCore.QTimer.singleShot(0, self.refresh_appmarks_items)
|
||||
QtCore.QTimer.singleShot(0, self.refresh_running_scopes)
|
||||
|
||||
# EN: Auto-refresh runtime marks/units while dialog is open.
|
||||
@@ -561,6 +600,7 @@ RU: Применяет policy-rules и проверяет health. При оши
|
||||
try:
|
||||
# Only refresh units list when Apps(runtime) tab is visible.
|
||||
if int(self.tabs.currentIndex() or 0) == 1:
|
||||
self.refresh_appmarks_items(quiet=True)
|
||||
self.refresh_running_scopes(quiet=True)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1116,6 +1156,96 @@ RU: Применяет policy-rules и проверяет health. При оши
|
||||
except Exception as e:
|
||||
self.lbl_app_counts.setText(f"Marks: error: {e}")
|
||||
|
||||
def refresh_appmarks_items(self, quiet: bool = False) -> None:
|
||||
def work() -> None:
|
||||
items = list(self.ctrl.traffic_appmarks_items() or [])
|
||||
self.lst_marks.clear()
|
||||
|
||||
vpn = 0
|
||||
direct = 0
|
||||
for it in items:
|
||||
tgt = (getattr(it, "target", "") or "").strip().lower()
|
||||
if tgt == "vpn":
|
||||
vpn += 1
|
||||
elif tgt == "direct":
|
||||
direct += 1
|
||||
|
||||
mid = int(getattr(it, "id", 0) or 0)
|
||||
app_key = (getattr(it, "app_key", "") or "").strip()
|
||||
unit = (getattr(it, "unit", "") or "").strip()
|
||||
cmd = (getattr(it, "command", "") or "").strip()
|
||||
rem = int(getattr(it, "remaining_sec", 0) or 0)
|
||||
|
||||
rem_h = rem // 3600
|
||||
rem_m = (rem % 3600) // 60
|
||||
rem_s = rem % 60
|
||||
rem_txt = f"{rem_h:02d}:{rem_m:02d}:{rem_s:02d}"
|
||||
|
||||
label = f"{tgt} {app_key or unit or mid} (ttl {rem_txt})"
|
||||
q = QListWidgetItem(label)
|
||||
q.setToolTip(
|
||||
(
|
||||
f"id: {mid}\n"
|
||||
f"target: {tgt}\n"
|
||||
f"app_key: {app_key}\n"
|
||||
f"unit: {unit}\n"
|
||||
f"remaining: {rem}s\n\n"
|
||||
f"{cmd}"
|
||||
).strip()
|
||||
)
|
||||
q.setData(QtCore.Qt.UserRole, it)
|
||||
self.lst_marks.addItem(q)
|
||||
|
||||
self.lbl_marks.setText(f"Active marks: {len(items)} (VPN={vpn}, Direct={direct})")
|
||||
self.btn_marks_unmark.setEnabled(self.lst_marks.count() > 0)
|
||||
|
||||
if quiet:
|
||||
try:
|
||||
work()
|
||||
except Exception as e:
|
||||
self.lbl_marks.setText(f"Active marks: error: {e}")
|
||||
return
|
||||
|
||||
self._safe(work, title="Refresh marks error")
|
||||
|
||||
def on_appmarks_unmark_selected(self) -> None:
|
||||
sel = list(self.lst_marks.selectedItems() or [])
|
||||
if not sel:
|
||||
return
|
||||
|
||||
# Convert selection to (target,id).
|
||||
pairs: list[tuple[str, int]] = []
|
||||
for it in sel:
|
||||
obj = it.data(QtCore.Qt.UserRole)
|
||||
tgt = (getattr(obj, "target", "") or "").strip().lower()
|
||||
mid = int(getattr(obj, "id", 0) or 0)
|
||||
if tgt in ("vpn", "direct") and mid > 0:
|
||||
pairs.append((tgt, mid))
|
||||
|
||||
if not pairs:
|
||||
return
|
||||
|
||||
def work() -> None:
|
||||
if QMessageBox.question(
|
||||
self,
|
||||
"Unmark selected",
|
||||
"Remove routing marks for selected items?\n\n"
|
||||
+ "\n".join([f"{t}:{i}" for (t, i) in pairs[:20]])
|
||||
+ ("\n..." if len(pairs) > 20 else ""),
|
||||
) != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
for (tgt, mid) in pairs:
|
||||
res = self.ctrl.traffic_appmarks_apply(op="del", target=tgt, cgroup=str(mid))
|
||||
if not res.ok:
|
||||
raise RuntimeError(res.message or f"unmark failed: {tgt}:{mid}")
|
||||
|
||||
self._set_action_status(f"Unmarked: {len(pairs)} item(s)", ok=True)
|
||||
self.refresh_appmarks_items(quiet=True)
|
||||
self.refresh_appmarks_counts()
|
||||
|
||||
self._safe(work, title="Unmark selected error")
|
||||
|
||||
def _refresh_last_scope_ui(self) -> None:
|
||||
unit = (self._last_app_unit or "").strip()
|
||||
target = (self._last_app_target or "").strip().lower()
|
||||
|
||||
Reference in New Issue
Block a user