This commit is contained in:
Denozordec
2026-05-03 11:16:07 +07:00
parent ce00c4c671
commit bdb9b72fac
66 changed files with 9553 additions and 1547 deletions
+3 -141
View File
@@ -116,16 +116,6 @@ interface LiveTrafficServer {
txSeries: number[]
}
interface TrafficSettingsDto {
enabled: boolean
intervalSec: number
retentionDays: number
lastCollectedAt: string | null
lastDurationMs: number | null
lastError: string | null
collectorRunning: boolean
}
interface LiveTrafficInterface {
name: string
running: boolean
@@ -787,10 +777,6 @@ export default function TrafficPage() {
const [liveServers, setLiveServers] = useState<ServerTraffic[]>([])
const [liveBusy, setLiveBusy] = useState(false)
const [liveError, setLiveError] = useState<string | null>(null)
const [settingsBusy, setSettingsBusy] = useState(false)
const [trafficSettings, setTrafficSettings] = useState<TrafficSettingsDto | null>(null)
const [intervalDraft, setIntervalDraft] = useState("30")
const [retentionDraft, setRetentionDraft] = useState("14")
const [serverIfaces, setServerIfaces] = useState<LiveTrafficInterface[]>([])
const [selectedIface, setSelectedIface] = useState("__all__")
const [hideDisabledIfaces, setHideDisabledIfaces] = useState(true)
@@ -834,30 +820,16 @@ export default function TrafficPage() {
}
}, [apiFetch, isLive])
const loadTrafficSettings = useCallback(async () => {
if (!isLive) return
try {
const s = await apiFetch<TrafficSettingsDto>("/api/traffic/settings")
setTrafficSettings(s)
setIntervalDraft(String(s.intervalSec))
setRetentionDraft(String(s.retentionDays))
} catch (e) {
setLiveError(e instanceof Error ? e.message : "Не удалось загрузить настройки сбора")
}
}, [apiFetch, isLive])
useEffect(() => {
if (!isLive) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLiveServers([])
setLiveError(null)
setTrafficSettings(null)
return
}
if (groupMode !== "servers") setGroupMode("servers")
void loadLiveTraffic(range)
void loadTrafficSettings()
}, [isLive, groupMode, range, loadLiveTraffic, loadTrafficSettings])
}, [isLive, groupMode, range, loadLiveTraffic])
const activeServerTraffic = isLive ? liveServers : serverTraffic
const visibleIfaces = useMemo(
@@ -974,28 +946,6 @@ export default function TrafficPage() {
>
<RefreshCwIcon className={cn("size-4", isLive && liveBusy && "animate-spin")} />Обновить
</Button>
{isLive && (
<Button
variant="outline"
size="sm"
onClick={async () => {
setSettingsBusy(true)
setLiveError(null)
try {
await apiFetch("/api/traffic/collect-now", { method: "POST" })
await loadLiveTraffic(range)
await loadTrafficSettings()
} catch (e) {
setLiveError(e instanceof Error ? e.message : "Не удалось выполнить сбор")
} finally {
setSettingsBusy(false)
}
}}
disabled={settingsBusy}
>
<ActivityIcon className={cn("size-4", settingsBusy && "animate-pulse")} />Собрать сейчас
</Button>
)}
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
</>
}
@@ -1020,94 +970,6 @@ export default function TrafficPage() {
<div className="flex-1 overflow-y-auto p-6">
<div className="flex flex-col gap-5">
{isLive && trafficSettings && (
<Card>
<CardContent className="pt-4 pb-4 px-4">
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<span className="text-[11px] text-muted-foreground">Сбор статистики</span>
<div className="flex rounded-md border border-input overflow-hidden h-8">
<button
className={cn("px-3 text-xs", trafficSettings.enabled ? "bg-emerald-600 text-white" : "text-muted-foreground hover:bg-muted")}
onClick={async () => {
setSettingsBusy(true)
try {
await apiFetch("/api/traffic/settings", { method: "PUT", body: JSON.stringify({ enabled: true }) })
await loadTrafficSettings()
} finally { setSettingsBusy(false) }
}}
disabled={settingsBusy}
>Вкл</button>
<button
className={cn("px-3 text-xs border-l border-input", !trafficSettings.enabled ? "bg-muted-foreground text-white" : "text-muted-foreground hover:bg-muted")}
onClick={async () => {
setSettingsBusy(true)
try {
await apiFetch("/api/traffic/settings", { method: "PUT", body: JSON.stringify({ enabled: false }) })
await loadTrafficSettings()
} finally { setSettingsBusy(false) }
}}
disabled={settingsBusy}
>Выкл</button>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-[11px] text-muted-foreground">Интервал (сек)</span>
<input
value={intervalDraft}
onChange={(e) => setIntervalDraft(e.target.value)}
className="h-8 w-24 text-xs bg-muted/50 border border-border rounded-md px-2"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-[11px] text-muted-foreground">Хранение (дней)</span>
<input
value={retentionDraft}
onChange={(e) => setRetentionDraft(e.target.value)}
className="h-8 w-24 text-xs bg-muted/50 border border-border rounded-md px-2"
/>
</div>
<Button
size="sm"
variant="outline"
className="h-8"
disabled={settingsBusy}
onClick={async () => {
setSettingsBusy(true)
try {
await apiFetch("/api/traffic/settings", {
method: "PUT",
body: JSON.stringify({
intervalSec: Number.parseInt(intervalDraft, 10) || 30,
retentionDays: Number.parseInt(retentionDraft, 10) || 14,
}),
})
await loadTrafficSettings()
} catch (e) {
setLiveError(e instanceof Error ? e.message : "Не удалось сохранить настройки")
} finally {
setSettingsBusy(false)
}
}}
>
Сохранить сбор
</Button>
<div className="ml-auto text-xs text-muted-foreground">
<p>Последний сбор: {trafficSettings.lastCollectedAt ? new Date(trafficSettings.lastCollectedAt).toLocaleString("ru-RU") : "—"}</p>
<p>Длительность: {trafficSettings.lastDurationMs != null ? `${trafficSettings.lastDurationMs} мс` : "—"}</p>
<p className={cn(trafficSettings.lastError ? "text-destructive" : "")}>
{trafficSettings.lastError ? `Ошибка: ${trafficSettings.lastError}` : "Ошибок нет"}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* ── summary stat cards ── */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
@@ -1206,11 +1068,11 @@ export default function TrafficPage() {
>
Все
</button>
{visibleIfaces.map((iface) => {
{visibleIfaces.map((iface, index) => {
const active = selectedIface === iface.name
return (
<button
key={iface.name}
key={`${iface.name}:${index}`}
onClick={() => setSelectedIface(iface.name)}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",