fix(ui): показывать свежесть срезов Firewall

This commit is contained in:
Denozordec committed 2026-09-30 11:04:57 +07:00
1 parent c2159508d5
commit 4b01f20558
6 files changed
+118 -4

No files matched your search

+23 -1
View File
@@ -22,6 +22,7 @@ import {
} from "@/lib/data"
import { useDataSource } from "@/lib/data-source"
import { firewallResponseSchema, firewallServersSchema, mergeFirewallRows, type FirewallFailure, type FirewallBackendServer } from "@/lib/firewall-response"
import { updateFirewallSliceTimes, firewallTimeRange, type FirewallSliceTime } from "@/lib/firewall-freshness"
import { requestJson } from "@/shared/api/http-client"
import { Frame, FramePanel } from "@/components/reui/frame"
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
@@ -1935,6 +1936,7 @@ function FirewallPageInner() {
const [dataLoading, setDataLoading] = useState(false)
const [dataError, setDataError] = useState<string | null>(null)
const [hasLoaded, setHasLoaded] = useState(false)
const [sliceTimes, setSliceTimes] = useState<FirewallSliceTime[]>([])
const [failures, setFailures] = useState<FirewallFailure[]>([])
const loadEpoch = useRef(0)
useEffect(() => () => { loadEpoch.current += 1 }, [backendUrl, isLive])
@@ -1990,6 +1992,8 @@ function FirewallPageInner() {
setLiveRules(previous => mergeFirewallRows(previous, firewall.data.rules, firewall.data.failures, enabledIds))
setLiveAddrLists(previous => mergeFirewallRows(previous, firewall.data.addressLists, firewall.data.failures, enabledIds, true))
setFailures(firewall.data.failures)
const receivedAt = new Date().toISOString()
setSliceTimes(previous => updateFirewallSliceTimes(previous, [...enabledIds], firewall.data.failures, receivedAt))
setHasLoaded(true)
} catch (e) {
if (epoch !== loadEpoch.current) return
@@ -2054,6 +2058,7 @@ function FirewallPageInner() {
setLiveRules([])
setLiveAddrLists([])
setHasLoaded(false)
setSliceTimes([])
setFailures([])
setDataError(null)
setDataLoading(false)
@@ -2151,6 +2156,8 @@ function FirewallPageInner() {
const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : []
const firewallIncomplete = scopedFailures.length > 0
const scopedSliceTimes = sliceTimes.filter(slice => (selectedServerId === ALL_SERVERS_ID || slice.serverId === selectedServerId) && slice.family === ipFamily)
const freshness = firewallTimeRange(scopedSliceTimes)
const totalEnabled = familyRules.filter((r) => r.enabled).length
const totalHits = familyRules.reduce((s, r) => s + r.hits, 0)
const dropRules = familyRules.filter((r) => r.action === "drop" || r.action === "reject").length
@@ -2572,10 +2579,25 @@ function FirewallPageInner() {
</span>
</div>
{isLive && <div aria-label="Свежесть данных Firewall" className="text-xs text-muted-foreground">
{dataLoading && <p role="status">{hasLoaded ? "Обновление данных Firewall…" : "Загрузка данных Firewall…"}</p>}
<p title="Время получения данных в приложении; не время измерения на RouterOS">
{freshness.oldest ? <>Получено в приложении: <time dateTime={freshness.oldest}>{new Date(freshness.oldest).toLocaleString("ru-RU")}</time>
{freshness.newest !== freshness.oldest && <> — <time dateTime={freshness.newest}>{new Date(freshness.newest!).toLocaleString("ru-RU")}</time></>}
</> : hasLoaded && liveServers.every(server => !server.enabled) ? "Нет включённых устройств для опроса" : "Время получения срезов неизвестно"}
{freshness.unknown > 0 && <> · Не получено срезов: {freshness.unknown}</>}
</p>
</div>}
{firewallIncomplete && <div role="alert" className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3 text-sm">
<p className="font-medium">Не все срезы Firewall получены</p>
<p>Сохранены последние данные недоступных срезов; их состояние неизвестно. Обновите данные перед изменениями.</p>
{scopedFailures.map((failure, index) => <p key={index}>{[failure.serverName || failure.serverId, failure.family, failure.table, failure.error].filter(Boolean).join(" · ")}</p>)}
{scopedFailures.map((failure, index) => {
const range = firewallTimeRange(scopedSliceTimes.filter(slice => slice.serverId === failure.serverId && (!failure.family || slice.family === failure.family) && (!failure.table || slice.table === failure.table)))
return <p key={index}>{[failure.serverName || failure.serverId, failure.family, failure.table, failure.error].filter(Boolean).join(" · ")} · Последние данные: {range.oldest
? <><time dateTime={range.oldest}>{new Date(range.oldest).toLocaleString("ru-RU")}</time>
{range.newest !== range.oldest && <> — <time dateTime={range.newest}>{new Date(range.newest!).toLocaleString("ru-RU")}</time></>}
</> : "ещё не получены"}{range.unknown > 0 && <> · Не получено срезов: {range.unknown}</>}</p>
})}
</div>}
{moveStatus && <p role="status" className="text-sm text-muted-foreground">{moveStatus}</p>}
{moveError && <p role="alert" className="text-sm text-destructive">{moveError}</p>}