feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,650 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import {
|
||||
dashLatency,
|
||||
greTunnels as mockGreTunnels,
|
||||
pingProbes,
|
||||
servers as mockServers,
|
||||
traffic as mockTraffic,
|
||||
vxlanTunnels as mockVxlan,
|
||||
type GreTunnel,
|
||||
type Server,
|
||||
} from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { listEvents } from "@/shared/api/events"
|
||||
import type { EventItem } from "@mmapp/contracts/events"
|
||||
import { buildLatencySeriesByProbeSource } from "@/lib/dashboard-latency"
|
||||
import {
|
||||
buildDashboardInternetPath,
|
||||
resolveDefaultRouteLookup,
|
||||
type InternetPathViewModel,
|
||||
} from "@/lib/dashboard-internet-path"
|
||||
|
||||
import { updateDashboardSource, type DashboardSource, type DashboardSources } from "@/lib/dashboard-source-state"
|
||||
|
||||
import {can,ensureAuthConfig} from "@/lib/auth"
|
||||
import {parseDashboardPath,parseDashboardFilters,parseDashboardSpeedProbes,parseDashboardWanRuntime} from "@/lib/dashboard-path-response"
|
||||
import {parseDashboardGre,parseDashboardWireGuard,parseDashboardVxlan,type DashboardGreTunnel} from "@/lib/dashboard-overlays"
|
||||
import type {DashboardProbe} from "@/lib/dashboard-probes"
|
||||
import {parseDashboardServers,parseDashboardProbes,type DashboardServerRow} from "@/lib/dashboard-readers"
|
||||
import {parseDashboardTraffic,type DashboardTraffic} from "@/lib/dashboard-traffic"
|
||||
import {parseDashboardBgp,type DashboardBgp} from "@/lib/dashboard-bgp"
|
||||
import {dashboardRequestPermission} from "@/lib/dashboard-permissions"
|
||||
async function dashboardRequest<T>(path:string,work:()=>Promise<T>,method="GET"):Promise<T>{
|
||||
await ensureAuthConfig();const permission=dashboardRequestPermission(method,path)
|
||||
if(!can(permission))throw Error("Нет доступа к источнику: "+permission)
|
||||
const result=await work();if(!can(permission))throw Error("Доступ к источнику отозван: "+permission)
|
||||
return result
|
||||
}
|
||||
|
||||
const MOCK_DASH_STARS_LS = "mm:dashboard-probe-ids"
|
||||
const UPTIME_PROBES_CHANGED = "mm:uptime-probes-changed"
|
||||
|
||||
function makeApiFetch(backendUrl: string) {
|
||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return dashboardRequest(path,()=>requestJson<T>(backendUrl,path,init),init?.method??"GET")
|
||||
}
|
||||
}
|
||||
|
||||
function readMockDashboardStarIds(): Set<string> {
|
||||
if (typeof window === "undefined") return new Set()
|
||||
try {
|
||||
const raw = localStorage.getItem(MOCK_DASH_STARS_LS)
|
||||
const arr = raw ? (JSON.parse(raw) as unknown) : []
|
||||
return new Set(Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [])
|
||||
} catch {
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
type BackendServerRow=DashboardServerRow
|
||||
|
||||
type ApiGreTunnelRow=DashboardGreTunnel
|
||||
|
||||
export type OverlayKind = "gre" | "wg" | "vxlan"
|
||||
|
||||
export interface OverlayItem {
|
||||
id: string
|
||||
name: string
|
||||
kind: OverlayKind
|
||||
href: string
|
||||
status: "up" | "down" | "degraded"
|
||||
}
|
||||
|
||||
export interface AttentionRow {
|
||||
id: string
|
||||
title: string
|
||||
hint: string
|
||||
href: string
|
||||
tone: "destructive" | "warning"
|
||||
}
|
||||
|
||||
export type LatencyBlock =
|
||||
| { kind: "loading" }
|
||||
| { kind: "empty"; message: string }
|
||||
| { kind: "mock"; series: Record<string, (number|null)[]>; labels?: Record<string, string>; subtitle: string }
|
||||
| { kind: "live"; series: Record<string, (number|null)[]>; labels?: Record<string, string>; subtitle: string }
|
||||
|
||||
function apiGreToGreTunnel(t: ApiGreTunnelRow): GreTunnel {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
serverId: String(t.serverId),
|
||||
localAddress: t.localAddress,
|
||||
remoteAddress: t.remoteAddress,
|
||||
localInnerIp: t.localInnerIp,
|
||||
remoteInnerIp: t.remoteInnerIp,
|
||||
poolId: t.poolId || "live",
|
||||
ipsec: null,
|
||||
mtu: t.mtu,
|
||||
keepaliveInterval: t.keepaliveInterval,
|
||||
keepaliveRetries: t.keepaliveRetries,
|
||||
dscp: t.dscp,
|
||||
clampTcpMss: t.clampTcpMss,
|
||||
allowFastPath: t.allowFastPath,
|
||||
comment: t.comment,
|
||||
enabled: t.enabled,
|
||||
status: t.status,
|
||||
}
|
||||
}
|
||||
|
||||
function mapBackendToServer(s: BackendServerRow): Server {
|
||||
const wanUplinks = Array.isArray(s.wanUplinks)
|
||||
? s.wanUplinks
|
||||
.filter((w) => typeof w === "object" && w != null)
|
||||
.map((w, idx) => ({
|
||||
id: String(w.id || `wan-${s.id}-${idx + 1}`),
|
||||
name: String(w.name || `WAN${idx + 1}`),
|
||||
isp: String(w.isp || "—"),
|
||||
iface: String(w.iface || ""),
|
||||
ip: String(w.ip || ""),
|
||||
maxDl: Math.max(1, Math.round(Number(w.maxDl) || 100)),
|
||||
maxUl: Math.max(1, Math.round(Number(w.maxUl) || 100)),
|
||||
}))
|
||||
: []
|
||||
return {
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
host: s.host,
|
||||
model: s.model ?? "—",
|
||||
os: s.os ?? "—",
|
||||
site: s.site || "—",
|
||||
country: s.country || "UN",
|
||||
asn: s.asn,
|
||||
type: s.type,
|
||||
enabled: s.enabled,
|
||||
status: s.status ?? "unknown",
|
||||
latency: s.latency != null ? Math.round(s.latency) : null,
|
||||
sessions: s.sessions,
|
||||
wanUplinks,
|
||||
}
|
||||
}
|
||||
|
||||
function mockOverlayItems(): OverlayItem[] {
|
||||
const gre: OverlayItem[] = mockGreTunnels.map((t) => ({
|
||||
id: JSON.stringify(["gre", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "gre",
|
||||
href: "/gre?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
const wg: OverlayItem[] = mockServers.flatMap((s) =>
|
||||
(s.wireGuardIfaces ?? []).map((iface) => ({
|
||||
id: JSON.stringify(["wg", s.id, iface.id]),
|
||||
name: iface.name,
|
||||
kind: "wg" as const,
|
||||
href: "/wireguard?server=" + encodeURIComponent(s.id) + "&interface=" + encodeURIComponent(iface.id),
|
||||
status: iface.status,
|
||||
})),
|
||||
)
|
||||
const vx: OverlayItem[] = mockVxlan.map((t) => ({
|
||||
id: JSON.stringify(["vxlan", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "vxlan",
|
||||
href: "/vxlan?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
return [...gre, ...wg, ...vx]
|
||||
}
|
||||
|
||||
export function useDashboardLive() {
|
||||
const pathname = usePathname()
|
||||
const { mode, backendUrl, prefsHydrated } = useDataSource()
|
||||
const isLive = prefsHydrated && mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [liveProbes, setLiveProbes] = useState<DashboardProbe[] | null>(null)
|
||||
const [liveServers, setLiveServers] = useState<Server[] | null>(null)
|
||||
const [overlayItems, setOverlayItems] = useState<OverlayItem[] | null>(null)
|
||||
const [bgp, setBgp] = useState<DashboardBgp | null>(null)
|
||||
const [trafficSeries, setTrafficSeries] = useState<DashboardTraffic | null>(null)
|
||||
const [recentEvents, setRecentEvents] = useState<EventItem[]>([])
|
||||
const [eventsError, setEventsError] = useState<string | null>(null)
|
||||
const [eventsLoading, setEventsLoading] = useState(false)
|
||||
const [internetPath, setInternetPath] = useState<InternetPathViewModel | null>(null)
|
||||
const [internetPathLoading, setInternetPathLoading] = useState(false)
|
||||
const [internetPathError, setInternetPathError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [mockDashEpoch, setMockDashEpoch] = useState(0)
|
||||
|
||||
const [sources, setSources] = useState<DashboardSources>({})
|
||||
const snapshotRequest = useRef(0)
|
||||
const eventsRequest = useRef(0)
|
||||
const recordSource = useCallback((source: DashboardSource, error: string | null, sampledAt?: string) => {
|
||||
setSources(previous => updateDashboardSource(previous, source, error, Date.now(), sampledAt))
|
||||
}, [])
|
||||
|
||||
const fetchSnapshot = useCallback(async (_silent: boolean) => {
|
||||
if (!isLive) return
|
||||
const request = ++snapshotRequest.current
|
||||
const current = () => request === snapshotRequest.current
|
||||
setLoading(true)
|
||||
setInternetPathLoading(true)
|
||||
try {
|
||||
await ensureAuthConfig();if(!current())return
|
||||
if(!can("mm:servers:read"))setLiveServers(null)
|
||||
if(!can("mm:uptime:read"))setLiveProbes(null)
|
||||
if(!can("mm:bgp:read"))setBgp(null)
|
||||
if(!can("mm:traffic:read"))setTrafficSeries(null)
|
||||
if(!can("mm:network:read"))setInternetPath(null)
|
||||
setOverlayItems(previous=>previous?.filter(item=>can(item.kind==="gre"?"mm:filters:read":item.kind==="wg"?"mm:network:read":"mm:dashboard:read"))??null)
|
||||
const [overviewRes, serversRes, fr, br, ipRes, greRes, wgRes, trafficRes, vxRes] = await Promise.allSettled([
|
||||
apiFetch<unknown>("/api/uptime/overview?range=1h").then(parseDashboardProbes),
|
||||
apiFetch<unknown>("/api/servers").then(parseDashboardServers),
|
||||
apiFetch<unknown>("/api/filters/rules").then(parseDashboardFilters),
|
||||
apiFetch<unknown>("/api/bgp/catalog").then(parseDashboardBgp),
|
||||
apiFetch<unknown>("/api/internet-path/latest").then(parseDashboardPath),
|
||||
apiFetch<unknown>("/api/filters/gre-tunnels").then(parseDashboardGre),
|
||||
apiFetch<unknown>("/api/wireguard").then(parseDashboardWireGuard),
|
||||
apiFetch<unknown>("/api/traffic/servers?range=1h").then(parseDashboardTraffic),
|
||||
apiFetch<unknown>("/api/vxlan").then(parseDashboardVxlan),
|
||||
])
|
||||
|
||||
if (!current()) return
|
||||
const results = { probes: overviewRes, servers: serversRes, bgp: br, gre: greRes, wg: wgRes, traffic: trafficRes, vxlan: vxRes }
|
||||
for (const [key, result] of Object.entries(results)) {
|
||||
recordSource(key as DashboardSource, result.status === "rejected"
|
||||
? result.reason instanceof Error ? result.reason.message : "Источник недоступен" : null,key==="traffic"&&trafficRes.status==="fulfilled"?trafficRes.value.sampledAt??undefined:key==="probes"&&overviewRes.status==="fulfilled"?overviewRes.value.sampledAt??undefined:undefined)
|
||||
}
|
||||
const failures = Object.values(results).filter(result => result.status === "rejected").length
|
||||
setError(failures ? "Не все источники обновлены. Последние полученные данные сохранены." : null)
|
||||
|
||||
if (overviewRes.status === "fulfilled") {
|
||||
setLiveProbes(overviewRes.value.probes)
|
||||
}
|
||||
|
||||
let serversMapped: Server[] = []
|
||||
if (serversRes.status === "fulfilled") {
|
||||
serversMapped = serversRes.value.map(mapBackendToServer)
|
||||
setLiveServers(serversMapped)
|
||||
}
|
||||
|
||||
if (br.status === "fulfilled") setBgp(br.value)
|
||||
|
||||
const greItems: OverlayItem[] =
|
||||
greRes.status === "fulfilled"
|
||||
? (greRes.value.tunnels ?? []).map((t) => ({
|
||||
id: JSON.stringify(["gre", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "gre" as const,
|
||||
href: "/gre?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
: []
|
||||
const wgItems: OverlayItem[] =
|
||||
wgRes.status === "fulfilled"
|
||||
? wgRes.value.interfaces.map((iface) => ({
|
||||
id: JSON.stringify(["wg", iface.serverId, iface.id]),
|
||||
name: iface.name,
|
||||
kind: "wg" as const,
|
||||
href: "/wireguard?server=" + encodeURIComponent(iface.serverId) + "&interface=" + encodeURIComponent(iface.id),
|
||||
status: iface.status,
|
||||
}))
|
||||
: []
|
||||
const vxItems: OverlayItem[] =
|
||||
vxRes.status === "fulfilled"
|
||||
? (vxRes.value.tunnels ?? []).map((t) => ({
|
||||
id: JSON.stringify(["vxlan", t.serverId, t.id]),
|
||||
name: t.name,
|
||||
kind: "vxlan" as const,
|
||||
href: "/vxlan?server=" + encodeURIComponent(t.serverId) + "&tunnel=" + encodeURIComponent(t.id),
|
||||
status: t.status,
|
||||
}))
|
||||
: []
|
||||
setOverlayItems(previous => [
|
||||
...(greRes.status === "fulfilled" ? greItems : previous?.filter(item => item.kind === "gre") ?? []),
|
||||
...(wgRes.status === "fulfilled" ? wgItems : previous?.filter(item => item.kind === "wg") ?? []),
|
||||
...(vxRes.status === "fulfilled" ? vxItems : previous?.filter(item => item.kind === "vxlan") ?? []),
|
||||
])
|
||||
|
||||
if (trafficRes.status === "fulfilled") setTrafficSeries(trafficRes.value)
|
||||
|
||||
const greMapped =
|
||||
greRes.status === "fulfilled" ? (greRes.value.tunnels ?? []).map(apiGreToGreTunnel) : []
|
||||
|
||||
if (ipRes.status === "rejected") throw ipRes.reason
|
||||
const snap = ipRes.value.snapshot
|
||||
if (snap || serversMapped.length > 0) {
|
||||
if (snap) {
|
||||
setInternetPath(
|
||||
buildDashboardInternetPath({
|
||||
servers: snap.servers.map(mapBackendToServer),
|
||||
greTunnels: (snap.greTunnels ?? []).map(apiGreToGreTunnel),
|
||||
probes: snap.speedProbes ?? [],
|
||||
filtersRulesets: snap.filtersRulesets ?? [],
|
||||
routeLookupByServerId: snap.routeLookupByServerId ?? {},
|
||||
wanRuntimeByHomeId: snap.wanRuntimeByHomeId ?? {},
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (serversRes.status === "rejected" || fr.status === "rejected" || greRes.status === "rejected") {
|
||||
throw new Error("Не удалось получить исходные данные Internet path")
|
||||
}
|
||||
let lookupFailed = false
|
||||
const checkedFetch: typeof apiFetch = async (path, init) => {
|
||||
if (!current()) throw new Error("Обновление заменено новым запросом")
|
||||
try { return await apiFetch(path, init) }
|
||||
catch (error) { lookupFailed = true; throw error }
|
||||
}
|
||||
const filterRulesets = fr.status === "fulfilled" ? fr.value.rulesets : []
|
||||
const homes = serversMapped.filter((s) => s.type === "home-router" && s.enabled)
|
||||
const lookups = await Promise.all(
|
||||
homes.map(async (h) => ({
|
||||
id: h.id,
|
||||
lookup: await resolveDefaultRouteLookup(checkedFetch, h.id),
|
||||
})),
|
||||
)
|
||||
if (!current()) return
|
||||
const wanRuntimeRows = await Promise.all(
|
||||
homes.map(async (h) => {
|
||||
try {
|
||||
const rt = await checkedFetch<unknown>(`/api/servers/${h.id}/wan-runtime`).then(parseDashboardWanRuntime)
|
||||
return { id: h.id, runtime: rt }
|
||||
} catch {
|
||||
lookupFailed=true
|
||||
return { id: h.id, runtime: null }
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (!current()) return
|
||||
const speedRes = await checkedFetch<unknown>("/api/uptime/speed-probes").then(parseDashboardSpeedProbes).catch(error=>{lookupFailed=true;throw Error("Не удалось обновить все данные Internet path: "+(error instanceof Error?error.message:String(error)))})
|
||||
if (!current()) return
|
||||
if (lookupFailed) throw new Error("Не удалось обновить все данные Internet path")
|
||||
const lookupById = Object.fromEntries(lookups.map((x) => [x.id, x.lookup]))
|
||||
const wanRuntimeById = Object.fromEntries(wanRuntimeRows.map((x) => [x.id, x.runtime]))
|
||||
setInternetPath(
|
||||
buildDashboardInternetPath({
|
||||
servers: serversMapped,
|
||||
greTunnels: greMapped,
|
||||
probes: speedRes.probes ?? [],
|
||||
filtersRulesets: filterRulesets,
|
||||
routeLookupByServerId: lookupById,
|
||||
wanRuntimeByHomeId: wanRuntimeById,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (serversRes.status === "rejected") throw serversRes.reason
|
||||
setInternetPath(null)
|
||||
}
|
||||
recordSource("internetPath", null, snap?.sampledAt)
|
||||
setInternetPathError(null)
|
||||
} catch (e) {
|
||||
if (!current()) return
|
||||
const msg = e instanceof Error ? e.message : "Не удалось загрузить дашборд"
|
||||
setInternetPathError(msg)
|
||||
recordSource("internetPath", msg)
|
||||
} finally {
|
||||
if (current()) {
|
||||
setLoading(false)
|
||||
setInternetPathLoading(false)
|
||||
}
|
||||
}
|
||||
}, [apiFetch, isLive, recordSource])
|
||||
|
||||
const fetchRecentEvents = useCallback(async (_silent: boolean) => {
|
||||
if (!isLive) {
|
||||
setRecentEvents([])
|
||||
setEventsError(null)
|
||||
return
|
||||
}
|
||||
const request = ++eventsRequest.current
|
||||
const current = () => request === eventsRequest.current
|
||||
setEventsLoading(true)
|
||||
try {
|
||||
const rows = await dashboardRequest("/api/events",()=>listEvents(backendUrl, { limit: 6 }))
|
||||
if (!current()) return
|
||||
setRecentEvents(rows)
|
||||
recordSource("events", null)
|
||||
setEventsError(null)
|
||||
} catch (err) {
|
||||
if (!current()) return
|
||||
const message = err instanceof Error ? err.message : "Не удалось загрузить события"
|
||||
recordSource("events", message)
|
||||
setEventsError(message)
|
||||
} finally {
|
||||
if (current()) setEventsLoading(false)
|
||||
}
|
||||
}, [backendUrl, isLive, recordSource])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
queueMicrotask(() => {
|
||||
if (cancelled) return
|
||||
setLiveProbes(null)
|
||||
setLiveServers(null)
|
||||
setOverlayItems(null)
|
||||
setBgp(null)
|
||||
setTrafficSeries(null)
|
||||
setError(null)
|
||||
setInternetPath(null)
|
||||
setInternetPathError(null)
|
||||
setRecentEvents([])
|
||||
setEventsError(null)
|
||||
setSources({})
|
||||
setLoading(false)
|
||||
setEventsLoading(false)
|
||||
setInternetPathLoading(false)
|
||||
void fetchSnapshot(false)
|
||||
void fetchRecentEvents(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
snapshotRequest.current += 1
|
||||
eventsRequest.current += 1
|
||||
}
|
||||
}, [fetchSnapshot, fetchRecentEvents])
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
queueMicrotask(() => {
|
||||
void fetchRecentEvents(true)
|
||||
})
|
||||
}, 20_000)
|
||||
return () => clearInterval(id)
|
||||
}, [fetchRecentEvents])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLive) return
|
||||
const id = setInterval(() => {
|
||||
queueMicrotask(() => {
|
||||
void fetchSnapshot(true)
|
||||
})
|
||||
}, 60_000)
|
||||
return () => clearInterval(id)
|
||||
}, [isLive, fetchSnapshot])
|
||||
|
||||
useEffect(() => {
|
||||
const bumpMock = () => setMockDashEpoch((x) => x + 1)
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === MOCK_DASH_STARS_LS) bumpMock()
|
||||
}
|
||||
const onVis = () => {
|
||||
if (document.visibilityState === "visible") bumpMock()
|
||||
}
|
||||
const onUptimeChanged = () => {
|
||||
bumpMock()
|
||||
if (isLive) void fetchSnapshot(true)
|
||||
}
|
||||
window.addEventListener(UPTIME_PROBES_CHANGED, onUptimeChanged)
|
||||
window.addEventListener("storage", onStorage)
|
||||
document.addEventListener("visibilitychange", onVis)
|
||||
return () => {
|
||||
window.removeEventListener(UPTIME_PROBES_CHANGED, onUptimeChanged)
|
||||
window.removeEventListener("storage", onStorage)
|
||||
document.removeEventListener("visibilitychange", onVis)
|
||||
}
|
||||
}, [isLive, fetchSnapshot])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setMockDashEpoch((x) => x + 1))
|
||||
}, [pathname])
|
||||
|
||||
const servers = useMemo(() => {
|
||||
if (!prefsHydrated) return []
|
||||
if (!isLive) return mockServers
|
||||
return liveServers ?? []
|
||||
}, [prefsHydrated, isLive, liveServers])
|
||||
|
||||
const mockActiveProbes = useMemo(() => {
|
||||
void mockDashEpoch
|
||||
const stars = readMockDashboardStarIds()
|
||||
return pingProbes.filter((p) => p.enabled && stars.has(p.id))
|
||||
}, [mockDashEpoch])
|
||||
|
||||
const activeProbes = useMemo(() => {
|
||||
if (!prefsHydrated) return []
|
||||
if (!isLive) return mockActiveProbes
|
||||
if (liveProbes === null) return []
|
||||
return liveProbes.filter((p) => p.enabled && p.showOnDashboard === true)
|
||||
}, [prefsHydrated, isLive, liveProbes, mockActiveProbes])
|
||||
|
||||
const enabledProbes = useMemo(() => {
|
||||
if (!prefsHydrated) return []
|
||||
if (!isLive) return pingProbes.filter((p) => p.enabled)
|
||||
return liveProbes?.filter((p) => p.enabled) ?? []
|
||||
}, [prefsHydrated, isLive, liveProbes])
|
||||
|
||||
const overlay = useMemo(() => {
|
||||
const items = !prefsHydrated ? [] : isLive ? (overlayItems ?? []) : mockOverlayItems()
|
||||
const up = items.filter((i) => i.status === "up").length
|
||||
const down = items.filter((i) => i.status !== "up").length
|
||||
return { items, total: items.length, up, down }
|
||||
}, [prefsHydrated, isLive, overlayItems])
|
||||
|
||||
const traffic = useMemo(() => {
|
||||
if (!prefsHydrated) return null
|
||||
if (!isLive) {
|
||||
return {
|
||||
rx: mockTraffic.rx,
|
||||
tx: mockTraffic.tx,
|
||||
rxNow: mockTraffic.rx[mockTraffic.rx.length - 1] ?? 0,
|
||||
txNow: mockTraffic.tx[mockTraffic.tx.length - 1] ?? 0,
|
||||
demo: true,observationsKnown:true,sampledAt:null,
|
||||
}
|
||||
}
|
||||
if (!trafficSeries) return null
|
||||
return { ...trafficSeries, demo: false }
|
||||
}, [prefsHydrated, isLive, trafficSeries])
|
||||
|
||||
const latency: LatencyBlock = useMemo(() => {
|
||||
if (!prefsHydrated) return { kind: "loading" }
|
||||
if (!isLive) {
|
||||
return {
|
||||
kind: "mock",
|
||||
series: dashLatency,
|
||||
subtitle: "Последние 60 минут · демо",
|
||||
}
|
||||
}
|
||||
if (liveProbes === null && loading) return { kind: "loading" }
|
||||
if (!liveProbes?.length) {
|
||||
return {
|
||||
kind: "empty",
|
||||
message: "Нет данных проб. Откройте мониторинг и проверьте сборщик uptime.",
|
||||
}
|
||||
}
|
||||
const { series, labels } = buildLatencySeriesByProbeSource(liveProbes, liveServers ?? [], {
|
||||
maxServers: 8,
|
||||
points: 60,
|
||||
})
|
||||
if (Object.keys(series).length === 0) {
|
||||
return {
|
||||
kind: "empty",
|
||||
message: "Нет включённых проб с историей RTT. Включите пробы на странице «Мониторинг».",
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: "live",
|
||||
series,
|
||||
labels,
|
||||
subtitle: "Средний измеренный RTT · 1 ч · до 8 узлов",
|
||||
}
|
||||
}, [prefsHydrated, isLive, liveProbes, loading, liveServers])
|
||||
|
||||
const attentionServers: AttentionRow[] = useMemo(() => {
|
||||
return servers
|
||||
.filter((s) => s.enabled && s.status !== "online")
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.name,
|
||||
hint: s.status === "unknown" ? "Статус неизвестен" : s.status === "degraded" ? "Ухудшение" : "Не в сети",
|
||||
href: "/servers?server=" + encodeURIComponent(s.id),
|
||||
tone: s.status === "degraded" || s.status === "unknown" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
}, [servers])
|
||||
|
||||
const attentionOverlay: AttentionRow[] = useMemo(() => {
|
||||
return overlay.items
|
||||
.filter((i) => i.status !== "up")
|
||||
.map((i) => ({
|
||||
id: i.id,
|
||||
title: i.name,
|
||||
hint: i.status === "degraded" ? "Ухудшение" : "Не работает",
|
||||
href: i.href,
|
||||
tone: i.status === "degraded" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
}, [overlay.items])
|
||||
|
||||
const attentionProbes: AttentionRow[] = useMemo(() => {
|
||||
return enabledProbes
|
||||
.filter((p) => p.status === "down" || p.status === "warn" || p.status === "unknown")
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.name,
|
||||
hint: p.status === "unknown" ? "Статус неизвестен" : p.status === "warn" ? "Предупреждение" : "Ошибка",
|
||||
href: "/uptime?probe=" + encodeURIComponent(p.id),
|
||||
tone: p.status === "warn" || p.status === "unknown" ? ("warning" as const) : ("destructive" as const),
|
||||
}))
|
||||
}, [enabledProbes])
|
||||
|
||||
const onlineCount = servers.filter((s) => s.status === "online").length
|
||||
const probeDown = enabledProbes.filter((p) => p.status === "down").length
|
||||
const probeWarn = enabledProbes.filter((p) => p.status === "warn").length
|
||||
const dataPending = isLive && loading && liveServers === null && liveProbes === null
|
||||
const kpiLoading = !prefsHydrated || dataPending
|
||||
|
||||
const probesSubtitle = !prefsHydrated
|
||||
? "Загрузка…"
|
||||
: !isLive
|
||||
? mockActiveProbes.length > 0
|
||||
? `${mockActiveProbes.length} на дашборде · демо`
|
||||
: "Нет проб на дашборде · отметьте ★ в мониторинге"
|
||||
: sources.probes?.error && liveProbes === null
|
||||
? "Данные проб недоступны"
|
||||
: activeProbes.length > 0
|
||||
? `${activeProbes.length} на дашборде · 1 ч`
|
||||
: "Нет проб на дашборде · отметьте ★ в мониторинге"
|
||||
|
||||
return {
|
||||
prefsHydrated,
|
||||
isLive,
|
||||
loading: kpiLoading,
|
||||
error,
|
||||
sources,
|
||||
refreshing: isLive && (loading || eventsLoading),
|
||||
availability: {
|
||||
servers: !isLive || liveServers !== null,
|
||||
probes: !isLive || liveProbes !== null,
|
||||
overlay: !isLive || ["gre", "wg", "vxlan"].every(key => sources[key as DashboardSource]?.receivedAt != null),
|
||||
},
|
||||
reliable: {
|
||||
servers: !isLive || (liveServers !== null && !sources.servers?.error && liveServers.every(s=>s.status!=="unknown")),
|
||||
probes: !isLive || (liveProbes !== null && !sources.probes?.error && liveProbes.filter(p=>p.enabled).every(p=>p.status!=="unknown")),
|
||||
overlay: !isLive || ["gre", "wg", "vxlan"].every(key => sources[key as DashboardSource]?.receivedAt != null && !sources[key as DashboardSource]?.error),
|
||||
},
|
||||
retry: () => {
|
||||
void fetchSnapshot(false)
|
||||
void fetchRecentEvents(false)
|
||||
},
|
||||
servers,
|
||||
activeProbes,
|
||||
overlay,
|
||||
bgp: isLive ? bgp : { prefixSum: 8432, establishedCount: 3 },
|
||||
traffic,
|
||||
onlineCount,
|
||||
offlineCount:servers.filter(s=>s.status==="offline").length,
|
||||
degradedServerCount:servers.filter(s=>s.status==="degraded").length,
|
||||
unknownServerCount:servers.filter(s=>s.status==="unknown").length,
|
||||
totalServers: servers.length,
|
||||
probeDown,
|
||||
probeWarn,
|
||||
probeCount: enabledProbes.length,
|
||||
unknownProbeCount:enabledProbes.filter(p=>p.status==="unknown").length,
|
||||
latency,
|
||||
recentEvents,
|
||||
eventsLoading,
|
||||
eventsError,
|
||||
internetPath,
|
||||
internetPathLoading: isLive && internetPathLoading && !internetPath,
|
||||
internetPathError,
|
||||
attentionServers,
|
||||
attentionOverlay,
|
||||
attentionProbes,
|
||||
probesSubtitle,
|
||||
probesLoading: isLive && loading && liveProbes === null,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user