platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
from typing import cast
from api_client import CmdResult, Event
from .views import ActionView, RoutesNftProgressView, RoutesResolveSummaryView, ServiceAction
class RoutesControllerMixin:
def routes_service_action(self, action: str) -> ActionView:
act = action.strip().lower()
if act not in ("start", "stop", "restart"):
raise ValueError(f"Invalid routes action: {action}")
res = self.client.routes_service(cast(ServiceAction, act))
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_clear(self) -> ActionView:
res = self.client.routes_clear()
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_cache_restore(self) -> ActionView:
res = self.client.routes_cache_restore()
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_precheck_debug(self, run_now: bool = True) -> ActionView:
res = self.client.routes_precheck_debug(run_now=run_now)
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_fix_policy_route(self) -> ActionView:
res = self.client.routes_fix_policy_route()
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_timer_enabled(self) -> bool:
st = self.client.routes_timer_get()
return bool(st.enabled)
def routes_timer_set(self, enabled: bool) -> ActionView:
res = self.client.routes_timer_set(bool(enabled))
return ActionView(ok=res.ok, pretty_text=self._pretty_cmd(res))
def routes_resolve_summary_view(self) -> RoutesResolveSummaryView:
dump = self.client.trace_get("full")
lines = list(getattr(dump, "lines", []) or [])
line = ""
for raw in reversed(lines):
s = str(raw or "")
if "resolve summary:" in s:
line = s
break
if not line:
return RoutesResolveSummaryView(
available=False,
text="Resolve summary: no data yet",
recheck_text="Timeout recheck: —",
color="gray",
recheck_color="gray",
)
tail = line.split("resolve summary:", 1)[1]
pairs: dict[str, int] = {}
for m in re.finditer(r"([a-zA-Z0-9_]+)=(-?\d+)", tail):
k = str(m.group(1) or "").strip().lower()
try:
pairs[k] = int(m.group(2))
except Exception:
continue
unique_ips = int(pairs.get("unique_ips", 0))
direct_ips = int(pairs.get("direct_ips", 0))
wildcard_ips = int(pairs.get("wildcard_ips", 0))
unresolved = int(pairs.get("unresolved", 0))
unresolved_live = int(pairs.get("unresolved_live", 0))
unresolved_suppressed = int(pairs.get("unresolved_suppressed", 0))
q_hits = int(pairs.get("quarantine_hits", 0))
dns_attempts = int(pairs.get("dns_attempts", 0))
dns_timeout = int(pairs.get("dns_timeout", 0))
dns_cooldown_skips = int(pairs.get("dns_cooldown_skips", 0))
live_batch_target = int(pairs.get("live_batch_target", 0))
live_batch_deferred = int(pairs.get("live_batch_deferred", 0))
live_batch_p1 = int(pairs.get("live_batch_p1", 0))
live_batch_p2 = int(pairs.get("live_batch_p2", 0))
live_batch_p3 = int(pairs.get("live_batch_p3", 0))
live_batch_nxheavy_pct = int(pairs.get("live_batch_nxheavy_pct", 0))
live_batch_nxheavy_skip = int(pairs.get("live_batch_nxheavy_skip", 0))
r_checked = int(pairs.get("timeout_recheck_checked", 0))
r_recovered = int(pairs.get("timeout_recheck_recovered", 0))
r_recovered_ips = int(pairs.get("timeout_recheck_recovered_ips", 0))
r_still_timeout = int(pairs.get("timeout_recheck_still_timeout", 0))
r_now_nx = int(pairs.get("timeout_recheck_now_nxdomain", 0))
r_now_tmp = int(pairs.get("timeout_recheck_now_temporary", 0))
text = (
f"Resolve: ips={unique_ips} (direct={direct_ips}, wildcard={wildcard_ips}, "
f"+recheck_ips={r_recovered_ips}) | unresolved={unresolved} "
f"(live={unresolved_live}, suppressed={unresolved_suppressed}) | "
f"quarantine_hits={q_hits} | dns_timeout={dns_timeout} "
f"| cooldown_skips={dns_cooldown_skips} | attempts={dns_attempts} "
f"| live_batch={live_batch_target} deferred={live_batch_deferred} "
f"(p1={live_batch_p1}, p2={live_batch_p2}, p3={live_batch_p3}, nx_pct={live_batch_nxheavy_pct}, nx_skip={live_batch_nxheavy_skip})"
)
recheck_text = (
f"Timeout recheck: checked={r_checked} recovered={r_recovered} "
f"still_timeout={r_still_timeout} now_nxdomain={r_now_nx} now_temporary={r_now_tmp}"
)
color = "green" if unresolved < 4000 else ("#b58900" if unresolved < 10000 else "red")
if dns_timeout > 500 and color == "green":
color = "#b58900"
if live_batch_p3 > 0 and (live_batch_p1+live_batch_p2) > 0:
ratio = float(live_batch_p3) / float(live_batch_p1 + live_batch_p2 + live_batch_p3)
if ratio > 0.8:
color = "#b58900" if color == "green" else color
if ratio > 0.95:
color = "red"
recheck_color = "green" if r_still_timeout <= 20 else ("#b58900" if r_still_timeout <= 100 else "red")
return RoutesResolveSummaryView(
available=True,
text=text,
recheck_text=recheck_text,
color=color,
recheck_color=recheck_color,
)
def routes_nft_progress_from_event(self, ev: Event) -> RoutesNftProgressView:
"""
Превращает Event(kind='routes_nft_progress') в удобную модель
для прогресс-бара/лейбла.
"""
payload = (
getattr(ev, "data", None)
or getattr(ev, "payload", None)
or getattr(ev, "extra", None)
or {}
)
if not isinstance(payload, dict):
payload = {}
try:
percent = int(payload.get("percent", 0))
except Exception:
percent = 0
msg = str(payload.get("message", "")) if payload is not None else ""
if not msg:
msg = "Updating nft set…"
active = 0 <= percent < 100
return RoutesNftProgressView(
percent=percent,
message=msg,
active=active,
)
# -------- DNS / SmartDNS --------