From 1275dd7b9b51c569f2468591f64e280128c5b872 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 12 Oct 2026 20:23:07 +0700 Subject: [PATCH] =?UTF-8?q?fix(traffic):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B8=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BE=D1=81=D1=82=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=83=D1=8E=20=D1=81=D0=B2=D0=B5=D0=B6=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/traffic/page.tsx | 166 +-- .../drizzle/0011_traffic_collection_state.sql | 8 + backend/package.json | 2 +- backend/src/db/schema.ts | 9 + backend/src/db/traffic-flags.test.ts | 8 +- backend/src/db/traffic-flags.ts | 6 +- backend/src/routes/traffic.ts | 66 +- .../alert-engine/signals-from-collectors.ts | 16 +- .../services/scheduler-recorded-run.test.ts | 8 +- .../src/services/scheduler-recorded-run.ts | 3 + .../src/services/traffic-collection-state.ts | 27 + backend/src/services/traffic-collector.ts | 16 +- .../src/services/traffic-freshness.test.ts | 21 + backend/src/services/traffic-freshness.ts | 45 + .../src/services/traffic-history-sql.test.ts | 61 + .../src/services/traffic-observation.test.ts | 1 + backend/src/services/traffic-observation.ts | 3 + backend/src/services/traffic-rate.ts | 26 +- backend/src/services/traffic-users.ts | 13 +- components/reui-kit/traffic-mini-chart.tsx | 6 +- .../traffic/traffic-observation-status.tsx | 18 + docs/ux-ui-external-acceptance.md | 34 +- docs/ux-ui-plan.md | 6 +- docs/ux-ui-progress.md | 32 +- docs/ux-ui-remaining-acceptance.md | 36 +- docs/ux-ui-stage-12.md | 32 +- docs/ux-ui-test-server.md | 34 +- .../traffic-history-compiled-2026-10-12.json | 1042 +++++++++++++++++ .../traffic-history-deployed-2026-10-12.json | 42 + .../traffic-history-interop-2026-10-12.json | 10 + .../traffic-history-native-2026-10-12.json | 29 + .../traffic-history-runtime-2026-10-12.json | 25 + .../traffic-history-source-2026-10-12.json | 10 + .../traffic-history-sql-2026-10-12.json | 49 + .../traffic-history-error-1440-2026-10-12.png | Bin 0 -> 132358 bytes .../traffic-history-error-320-2026-10-12.png | Bin 0 -> 48662 bytes lib/fmt-rate.ts | 6 +- lib/traffic-observations.test.ts | 11 + lib/traffic-observations.ts | 25 + tests/e2e/traffic-catalog.spec.ts | 11 +- tests/e2e/traffic-details.spec.ts | 13 +- tests/e2e/traffic-history-confidence.spec.ts | 55 + tests/e2e/traffic-ipv6-labels.spec.ts | 7 +- tests/e2e/traffic-list.spec.ts | 13 +- tests/e2e/traffic-live.spec.ts | 7 +- tests/e2e/traffic-mobile.spec.ts | 5 +- tests/e2e/traffic-observation-fixture.ts | 12 + tests/e2e/traffic-response-validation.spec.ts | 11 +- 48 files changed, 1899 insertions(+), 187 deletions(-) create mode 100644 backend/drizzle/0011_traffic_collection_state.sql create mode 100644 backend/src/services/traffic-collection-state.ts create mode 100644 backend/src/services/traffic-freshness.test.ts create mode 100644 backend/src/services/traffic-freshness.ts create mode 100644 backend/src/services/traffic-history-sql.test.ts create mode 100644 components/traffic/traffic-observation-status.tsx create mode 100644 docs/ux-ui/acceptance/traffic-history-compiled-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-deployed-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-interop-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-native-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-runtime-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-source-2026-10-12.json create mode 100644 docs/ux-ui/acceptance/traffic-history-sql-2026-10-12.json create mode 100644 docs/ux-ui/screens/traffic-history-error-1440-2026-10-12.png create mode 100644 docs/ux-ui/screens/traffic-history-error-320-2026-10-12.png create mode 100644 lib/traffic-observations.test.ts create mode 100644 lib/traffic-observations.ts create mode 100644 tests/e2e/traffic-history-confidence.spec.ts create mode 100644 tests/e2e/traffic-observation-fixture.ts diff --git a/app/(main)/traffic/page.tsx b/app/(main)/traffic/page.tsx index b4630ff4..d5c6cebb 100644 --- a/app/(main)/traffic/page.tsx +++ b/app/(main)/traffic/page.tsx @@ -2,6 +2,8 @@ import { useState, useMemo, useEffect, type ReactNode } from "react" import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert" +import { projectTrafficObservations, type TrafficObservationMeta } from "@/lib/traffic-observations" +import { TrafficObservationStatus } from "@/components/traffic/traffic-observation-status" import { TrafficDataTime } from "@/components/traffic/traffic-data-time" import { PageHeader } from "@/components/page-header" import { Frame, FramePanel } from "@/components/reui/frame" @@ -84,6 +86,7 @@ function flowEmptyHint(stats: FlowStatsDto | null, collectorAlive?: boolean): st // ─── data model ─────────────────────────────────────────────────────────────── interface BoundIfaceTraffic { + observations?: TrafficObservationMeta id: string bindingId: string userId: string @@ -98,75 +101,79 @@ interface BoundIfaceTraffic { serverName: string serverSite: string serverCountry: string - rxNow: number - txNow: number - rxPeak: number - txPeak: number - rxTotal: number - txTotal: number - rxSeries: number[] - txSeries: number[] + rxNow: number | null + txNow: number | null + rxPeak: number | null + txPeak: number | null + rxTotal: number | null + txTotal: number | null + rxSeries: (number|null)[] + txSeries: (number|null)[] status: "online" | "offline" } interface ServerTraffic { + observations?: TrafficObservationMeta id: string name: string site: string country: string status: "online" | "offline" | "degraded" - rxNow: number - txNow: number - rxPeak: number - txPeak: number - rxTotal: number - txTotal: number + rxNow: number | null + txNow: number | null + rxPeak: number | null + txPeak: number | null + rxTotal: number | null + txTotal: number | null sessions: number - rxSeries: number[] - txSeries: number[] + rxSeries: (number|null)[] + txSeries: (number|null)[] boundIfaces: BoundIfaceTraffic[] } interface UserTraffic { + observations?: TrafficObservationMeta id: string login: string displayName: string role: string active?: boolean interfaces: BoundIfaceTraffic[] - rxNow: number - txNow: number - rxPeak: number - txPeak: number - rxTotal: number - txTotal: number - rxSeries: number[] - txSeries: number[] + rxNow: number | null + txNow: number | null + rxPeak: number | null + txPeak: number | null + rxTotal: number | null + txTotal: number | null + rxSeries: (number|null)[] + txSeries: (number|null)[] } interface LiveTrafficServer { + observations?: TrafficObservationMeta id: string name: string site: string country: string status: "online" | "offline" | "degraded" - rxNow: number - txNow: number - rxPeak: number - txPeak: number - rxTotal: number - txTotal: number + rxNow: number | null + txNow: number | null + rxPeak: number | null + txPeak: number | null + rxTotal: number | null + txTotal: number | null sessions: number - rxSeries: number[] - txSeries: number[] + rxSeries: (number|null)[] + txSeries: (number|null)[] } interface LiveTrafficInterface { + observations?: TrafficObservationMeta name: string running: boolean disabled: boolean - rxNow: number - txNow: number + rxNow: number | null + txNow: number | null } function makeApiFetch(backendUrl: string) { @@ -175,6 +182,13 @@ function makeApiFetch(backendUrl: string) { } } +function compareRates(a: number|null, b: number|null, direction: "asc"|"desc") { + if (a === null && b === null) return 0 + if (a === null) return direction === "desc" ? -1 : 1 + if (b === null) return direction === "desc" ? 1 : -1 + return a - b +} + function hashSeed(s: string): number { let h = 0 for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0 @@ -278,14 +292,14 @@ function buildUserTraffic( clients: BoundIfaceTraffic[], active = true, ): UserTraffic { - const rxNow = clients.reduce((a, c) => a + c.rxNow, 0) - const txNow = clients.reduce((a, c) => a + c.txNow, 0) - const rxPeak = clients.reduce((a, c) => a + c.rxPeak, 0) - const txPeak = clients.reduce((a, c) => a + c.txPeak, 0) - const rxTotal = clients.reduce((a, c) => a + c.rxTotal, 0) - const txTotal = clients.reduce((a, c) => a + c.txTotal, 0) - const rxSeries = clients.reduce((a, c) => addSeries(a, c.rxSeries), Array(60).fill(0) as number[]) - const txSeries = clients.reduce((a, c) => addSeries(a, c.txSeries), Array(60).fill(0) as number[]) + const rxNow = clients.reduce((a, c) => a + (c.rxNow ?? 0), 0) + const txNow = clients.reduce((a, c) => a + (c.txNow ?? 0), 0) + const rxPeak = clients.reduce((a, c) => a + (c.rxPeak ?? 0), 0) + const txPeak = clients.reduce((a, c) => a + (c.txPeak ?? 0), 0) + const rxTotal = clients.reduce((a, c) => a + (c.rxTotal ?? 0), 0) + const txTotal = clients.reduce((a, c) => a + (c.txTotal ?? 0), 0) + const rxSeries = clients.reduce((a, c) => addSeries(a, c.rxSeries.map(v => v ?? 0)), Array(60).fill(0) as number[]) + const txSeries = clients.reduce((a, c) => addSeries(a, c.txSeries.map(v => v ?? 0)), Array(60).fill(0) as number[]) return { id, login, displayName, role, active, interfaces: clients, rxNow, txNow, rxPeak, txPeak, rxTotal, txTotal, rxSeries, txSeries } } @@ -538,21 +552,21 @@ function OfflinePlaceholder({ text = "Нет данных — объект не } function TotalsRow({ rxTotal, txTotal, rxSeries, txSeries }: { - rxTotal: number; txTotal: number; rxSeries: number[]; txSeries: number[] + rxTotal: number|null; txTotal: number|null; rxSeries: (number|null)[]; txSeries: (number|null)[] }) { return (
-

Получено за период

+

Учтено RX по замерам

- {rxTotal.toFixed(1)} ГБ + {fmtGB(rxTotal)}

-

Отправлено за период

+

Учтено TX по замерам

- {txTotal.toFixed(1)} ГБ + {fmtGB(txTotal)}

@@ -583,7 +597,8 @@ function ServerDetail({ {sel.site} - {sel.status === "offline" ? : } + +
{sel.serverSite} - {sel.status === "offline" ? : } +
} label="RX" value={fmtRate(sel.rxNow)} /> } label="TX" value={fmtRate(sel.txNow)} /> @@ -738,24 +753,16 @@ const toLiveServer = (s: LiveTrafficServer): ServerTraffic => { site: s.site || "—", country: s.country || "UN", status: s.status, - rxNow: s.rxNow, - txNow: s.txNow, - rxPeak: s.rxPeak, - txPeak: s.txPeak, - rxTotal: s.rxTotal, - txTotal: s.txTotal, sessions: s.sessions, - rxSeries: s.rxSeries, - txSeries: s.txSeries, boundIfaces: [], + ...projectTrafficObservations(s), } } const toLiveBound = (c: BoundIfaceTraffic): BoundIfaceTraffic => ({ ...c, interfaceType: (["ether", "gre", "wg", "other"].includes(c.interfaceType) ? c.interfaceType : "other") as InterfaceType, - rxSeries: c.rxSeries, - txSeries: c.txSeries, + ...projectTrafficObservations(c), }) const toLiveUser = (u: UserTraffic & { interfaces?: BoundIfaceTraffic[] }): UserTraffic => { @@ -764,14 +771,19 @@ const toLiveUser = (u: UserTraffic & { interfaces?: BoundIfaceTraffic[] }): User ...u, role: ROLE_LABEL[u.role as keyof typeof ROLE_LABEL] ?? u.role, interfaces: ifaces, - rxSeries: u.rxSeries, - txSeries: u.txSeries, + ...projectTrafficObservations(u), } } export default function TrafficPage() { const { mode, backendUrl } = useDataSource() const isLive = mode === "live" + const [trafficClock, setTrafficClock] = useState(() => Date.now()) + useEffect(() => { + if (!isLive) return + const timer = setInterval(() => setTrafficClock(Date.now()), 10_000) + return () => clearInterval(timer) + }, [isLive]) const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl]) const [groupMode, setGroupMode] = useState("servers") @@ -945,9 +957,9 @@ export default function TrafficPage() { void listServers(backendUrl).then(setCatalogServers).catch(() => setCatalogServers([])) }, [isLive, backendUrl]) - const activeServerTraffic = useMemo(() => isLive ? (trafficReady ? liveServers : []) : serverTraffic, [isLive, trafficReady, liveServers]) - const activeUserTraffic = useMemo(() => isLive ? (trafficReady ? liveUsers : []) : userTraffic, [isLive, trafficReady, liveUsers]) - const activeBoundIfaces = useMemo(() => isLive ? (trafficReady ? liveBoundIfaces : []) : boundIfaces, [isLive, trafficReady, liveBoundIfaces]) + const activeServerTraffic = useMemo(() => isLive ? (trafficReady ? liveServers.map(s => ({ ...s, ...projectTrafficObservations(s, trafficClock) })) : []) : serverTraffic, [isLive, trafficReady, liveServers, trafficClock]) + const activeUserTraffic = useMemo(() => isLive ? (trafficReady ? liveUsers.map(u => ({ ...u, ...projectTrafficObservations(u, trafficClock), interfaces: u.interfaces.map(toLiveBound) })) : []) : userTraffic, [isLive, trafficReady, liveUsers, trafficClock]) + const activeBoundIfaces = useMemo(() => isLive ? (trafficReady ? liveBoundIfaces.map(c => ({ ...c, ...projectTrafficObservations(c, trafficClock) })) : []) : boundIfaces, [isLive, trafficReady, liveBoundIfaces, trafficClock]) const visibleIfaces = useMemo( () => hideDisabledIfaces ? serverIfaces.filter((i) => !i.disabled && i.running) : serverIfaces, [serverIfaces, hideDisabledIfaces], @@ -1022,8 +1034,8 @@ export default function TrafficPage() { .filter(s => !q || s.name.toLowerCase().includes(q) || s.site.toLowerCase().includes(q)) .sort((a, b) => { let v = 0 - if (sortField === "rx") v = a.rxNow - b.rxNow - else if (sortField === "tx") v = a.txNow - b.txNow + if (sortField === "rx") v = compareRates(a.rxNow, b.rxNow, sortDir) + else if (sortField === "tx") v = compareRates(a.txNow, b.txNow, sortDir) else if (sortField === "name") v = a.name.localeCompare(b.name) else if (sortField === "sessions") v = a.sessions - b.sessions return sortDir === "desc" ? -v : v @@ -1035,8 +1047,8 @@ export default function TrafficPage() { .filter(u => !q || u.login.toLowerCase().includes(q) || u.displayName.toLowerCase().includes(q)) .sort((a, b) => { let v = 0 - if (sortField === "rx") v = a.rxNow - b.rxNow - else if (sortField === "tx") v = a.txNow - b.txNow + if (sortField === "rx") v = compareRates(a.rxNow, b.rxNow, sortDir) + else if (sortField === "tx") v = compareRates(a.txNow, b.txNow, sortDir) else if (sortField === "name") v = a.login.localeCompare(b.login) else if (sortField === "sessions") v = a.interfaces.length - b.interfaces.length return sortDir === "desc" ? -v : v @@ -1052,8 +1064,8 @@ export default function TrafficPage() { || c.userLogin.toLowerCase().includes(q)) .sort((a, b) => { let v = 0 - if (sortField === "rx") v = a.rxNow - b.rxNow - else if (sortField === "tx") v = a.txNow - b.txNow + if (sortField === "rx") v = compareRates(a.rxNow, b.rxNow, sortDir) + else if (sortField === "tx") v = compareRates(a.txNow, b.txNow, sortDir) else if (sortField === "name") v = a.interfaceName.localeCompare(b.interfaceName) return sortDir === "desc" ? -v : v }) @@ -1065,8 +1077,8 @@ export default function TrafficPage() { .filter((c) => !q || c.name.toLowerCase().includes(q) || c.subtitle.toLowerCase().includes(q) || c.site.toLowerCase().includes(q)) .sort((a, b) => { let v = 0 - if (sortField === "rx") v = a.rxNow - b.rxNow - else if (sortField === "tx") v = a.txNow - b.txNow + if (sortField === "rx") v = compareRates(a.rxNow, b.rxNow, sortDir) + else if (sortField === "tx") v = compareRates(a.txNow, b.txNow, sortDir) else if (sortField === "name") v = a.name.localeCompare(b.name) else if (sortField === "sessions") v = a.sessions - b.sessions return sortDir === "desc" ? -v : v @@ -1081,7 +1093,7 @@ export default function TrafficPage() { const displayedFlow = currentFlows && !liveError ? (flowLiveSample ?? currentAnalytics?.data ?? null) : null const selServer = useMemo(() => activeServerTraffic.find(s => s.id === selectedId) ?? activeServerTraffic[0], [selectedId, activeServerTraffic]) - const detailServer = isLive ? currentDetail?.server : selServer + const detailServer = isLive ? (currentDetail?.server ? { ...currentDetail.server, ...projectTrafficObservations(currentDetail.server, trafficClock) } : null) : selServer const selUser = useMemo(() => activeUserTraffic.find(u => u.id === selectedId) ?? activeUserTraffic[0], [selectedId, activeUserTraffic]) const selIface = useMemo(() => activeBoundIfaces.find(c => c.id === selectedId) ?? activeBoundIfaces[0], [selectedId, activeBoundIfaces]) @@ -1090,10 +1102,10 @@ export default function TrafficPage() { : effectiveMode === "ifaces" ? activeBoundIfaces : activeServerTraffic - const totalRx = kpiSource.reduce((a, s) => a + s.rxNow, 0) - const totalTx = kpiSource.reduce((a, s) => a + s.txNow, 0) - const peakRx = kpiSource.reduce((a, s) => Math.max(a, s.rxPeak), 0) - const peakTx = kpiSource.reduce((a, s) => Math.max(a, s.txPeak), 0) + const totalRx = kpiSource.every(s => s.rxNow !== null) ? kpiSource.reduce((a, s) => a + (s.rxNow ?? 0), 0) : null + const totalTx = kpiSource.every(s => s.txNow !== null) ? kpiSource.reduce((a, s) => a + (s.txNow ?? 0), 0) : null + const peakRx = kpiSource.every(s => s.rxPeak !== null) ? kpiSource.reduce((a, s) => Math.max(a, s.rxPeak ?? 0), 0) : null + const peakTx = kpiSource.every(s => s.txPeak !== null) ? kpiSource.reduce((a, s) => Math.max(a, s.txPeak ?? 0), 0) : null const visibleSortFields = SORT_FIELDS.filter(s => !s.modesOnly || s.modesOnly.includes(effectiveMode)) const ingestLine = flowIngestLine(flowStats) diff --git a/backend/drizzle/0011_traffic_collection_state.sql b/backend/drizzle/0011_traffic_collection_state.sql new file mode 100644 index 00000000..f5c5245f --- /dev/null +++ b/backend/drizzle/0011_traffic_collection_state.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS traffic_collection_state ( + server_id BIGINT PRIMARY KEY REFERENCES servers(id) ON DELETE CASCADE, + server_updated_at TIMESTAMPTZ NOT NULL, + last_attempt_at TIMESTAMPTZ NOT NULL, + last_succeeded_at TIMESTAMPTZ, + status TEXT NOT NULL CHECK(status IN ('pending', 'ok', 'error')), + error TEXT +); diff --git a/backend/package.json b/backend/package.json index e1268137..8f50dff9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,7 @@ "test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts", "test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts", "test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts", - "test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-observation-sql.test.ts", + "test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts", "test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts", "test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts", "test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts", diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 22807bd7..ab78f791 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -341,6 +341,15 @@ export const uptimeSettings = pgTable("uptime_settings", { updatedAt: ts("updated_at").notNull().defaultNow(), }) +export const trafficCollectionState = pgTable("traffic_collection_state", { + serverId: intPkRef().primaryKey().references(() => servers.id, { onDelete: "cascade" }), + serverUpdatedAt: ts("server_updated_at").notNull(), + lastAttemptAt: ts("last_attempt_at").notNull(), + lastSucceededAt: ts("last_succeeded_at"), + status: text("status").notNull(), + error: text("error"), +}) + export const uptimeProbes = pgTable("uptime_probes", { id: text("id").primaryKey(), srcServerId: bigint("src_server_id", { mode: "number" }).notNull() diff --git a/backend/src/db/traffic-flags.test.ts b/backend/src/db/traffic-flags.test.ts index 1fd05e48..06db8ea3 100644 --- a/backend/src/db/traffic-flags.test.ts +++ b/backend/src/db/traffic-flags.test.ts @@ -12,8 +12,10 @@ assert.equal(encodeTrafficFlags(true, true), 3) assert.equal(encodeTrafficFlags(false, false), 0) assert.equal(trafficFlagRunning(1), true) assert.equal(trafficFlagDisabled(1), false) -assert.deepEqual(decodeTrafficSampleFlags(1), { running: true, disabled: false }) -assert.deepEqual(decodeTrafficSampleFlags(0), { running: false, disabled: false }) -assert.deepEqual(decodeTrafficSampleFlags(undefined), { running: false, disabled: false }) +assert.deepEqual(decodeTrafficSampleFlags(1), { running: true, disabled: false, countersKnown: false }) +assert.deepEqual(decodeTrafficSampleFlags(0), { running: false, disabled: false, countersKnown: false }) +assert.deepEqual(decodeTrafficSampleFlags(undefined), { running: false, disabled: false, countersKnown: false }) +assert.equal(encodeTrafficFlags(true,false,true),5) +assert.deepEqual(decodeTrafficSampleFlags(5), {running:true,disabled:false,countersKnown:true}) console.log("traffic-flags.test.ts: ok") diff --git a/backend/src/db/traffic-flags.ts b/backend/src/db/traffic-flags.ts index a0a813bc..4c1da1e5 100644 --- a/backend/src/db/traffic-flags.ts +++ b/backend/src/db/traffic-flags.ts @@ -1,8 +1,10 @@ export const TRAFFIC_FLAG_RUNNING = 1 export const TRAFFIC_FLAG_DISABLED = 2 +export const TRAFFIC_FLAG_COUNTERS_KNOWN = 4 -export function encodeTrafficFlags(running: boolean, disabled: boolean): number { +export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false): number { return (running ? TRAFFIC_FLAG_RUNNING : 0) | (disabled ? TRAFFIC_FLAG_DISABLED : 0) + | (countersKnown ? TRAFFIC_FLAG_COUNTERS_KNOWN : 0) } export function trafficFlagRunning(flags: number | null | undefined): boolean { @@ -16,9 +18,11 @@ export function trafficFlagDisabled(flags: number | null | undefined): boolean { export function decodeTrafficSampleFlags(flags: number | null | undefined): { running: boolean disabled: boolean + countersKnown: boolean } { return { running: trafficFlagRunning(flags), disabled: trafficFlagDisabled(flags), + countersKnown: ((Number(flags) || 0) & TRAFFIC_FLAG_COUNTERS_KNOWN) !== 0, } } diff --git a/backend/src/routes/traffic.ts b/backend/src/routes/traffic.ts index a8c71995..72ed2a74 100644 --- a/backend/src/routes/traffic.ts +++ b/backend/src/routes/traffic.ts @@ -5,32 +5,28 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" import { db } from "../db/index.js" import { serverSnapshots, servers } from "../db/schema.js" import { - collectTrafficOnce, getTrafficCollectorState, getTrafficSettings, readServerSamplesInRange, updateTrafficSettings, } from "../services/traffic-collector.js" -import { scheduleAlertEngineAfterDataCollectors } from "../services/alert-collector-hooks.js" -import { refreshScheduler } from "../services/scheduler.js" +import { refreshScheduler, runSchedulerJobNow } from "../services/scheduler.js" import { appendEvent } from "../modules/events/service/events-service.js" import { MikrotikClient } from "../services/mikrotik.js" import { getEnabledServerById } from "../services/wireguard-live.js" +import { readTrafficCollectionContext } from "../services/traffic-collection-state.js" +import { applyTrafficFreshness } from "../services/traffic-freshness.js" import { - bpsToMbps, buildTrafficFromSamples, type TrafficObservations, isLoopbackName, parseMonitorTraffic, - rateBpsFromDelta, } from "../services/traffic-rate.js" import { buildBoundInterfaceTraffic, buildUserTrafficList, } from "../services/traffic-users.js" -type SnapshotRow = typeof serverSnapshots.$inferSelect - interface TrafficServerDto { observations?:TrafficObservations id: string @@ -53,8 +49,8 @@ interface TrafficInterfaceDto { name: string running: boolean disabled: boolean - rxNow: number - txNow: number + rxNow: number | null + txNow: number | null } const LIVE_TICK_MS = 1500 @@ -96,6 +92,7 @@ function buildServerTraffic( rangeStartMs: number, rangeEndMs: number, onlyInterface?: string, + context?: Awaited>, ): TrafficServerDto { const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, onlyInterface,undefined,true) return { @@ -104,7 +101,7 @@ function buildServerTraffic( site: s.site || "—", country: s.country || "UN", status, - observations:built.observations, + observations:built.observations && applyTrafficFreshness(built.observations, s, context?.settings, context?.states.get(s.id), rangeEndMs), rxNow: built.rxNow, txNow: built.txNow, rxPeak: built.rxPeak, @@ -122,23 +119,6 @@ async function snapshotStatus(serverId: number): Promise { app.post("/traffic/collect-now", async (_req, reply) => { try { - await collectTrafficOnce() - scheduleAlertEngineAfterDataCollectors() + const receipt = await runSchedulerJobNow("traffic") + if (!receipt.result || receipt.result.job !== "traffic") return reply.status(502).send({ ok: false, error: receipt.error ?? "Нет подтверждённого результата сбора" }) + const result = receipt.result + if (result.skipped) return reply.status(409).send({ error: "Сбор трафика уже выполняется" }) + const failures = result.servers.filter(s => !s.ok) + if (result.fatalError || failures.length) { + const error = result.fatalError ?? failures.map(s => `${s.name}: ${s.error ?? "Ошибка опроса"}`).join("; ") + await appendEvent({ level: "warning", eventType: "traffic.collect.manual.failed", sourceModule: "traffic", + title: "Сбор трафика завершён с ошибками", message: error }) + return reply.status(502).send({ ok: false, error, failures, succeeded: result.servers.filter(s => s.ok).map(s => s.serverId) }) + } const updated = await getTrafficSettings() await appendEvent({ level: "info", @@ -287,6 +276,8 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => { lastError: updated.lastError || null, }) } catch (error) { + if (typeof error === "object" && error && "statusCode" in error && error.statusCode === 409) + return reply.status(409).send({ error: error instanceof Error ? error.message : "Сбор трафика уже выполняется" }) const msg = error instanceof Error ? error.message : String(error) await appendEvent({ level: "critical", @@ -306,9 +297,10 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => { const rangeStartMs = rangeEndMs - minutes * 60_000 const sinceIso = new Date(rangeStartMs).toISOString() const allServers = await db.select().from(servers).where(eq(servers.enabled, true)) + const context = await readTrafficCollectionContext() const data = await Promise.all(allServers.map(async (s): Promise => { const rows = await readServerSamplesInRange(s.id, sinceIso) - return buildServerTraffic(s, await snapshotStatus(s.id), rows, rangeStartMs, rangeEndMs) + return buildServerTraffic(s, await snapshotStatus(s.id), rows, rangeStartMs, rangeEndMs, undefined, context) })) return reply.send({ servers: data }) @@ -385,9 +377,10 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => { const sinceIso = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() const rows = await readServerSamplesInRange(serverId, sinceIso) + const context = await readTrafficCollectionContext(), end = Date.now() const byIface = new Map() for (const r of rows) { - if (isLoopbackName(r.interfaceName)) continue + if (isLoopbackName(r.interfaceName) || r.peerPublicKey) continue const arr = byIface.get(r.interfaceName) ?? [] arr.push(r) byIface.set(r.interfaceName, arr) @@ -395,19 +388,19 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => { const interfaces: TrafficInterfaceDto[] = [...byIface.entries()].map(([name, arr]) => { const sorted = arr.sort((a, b) => a.sampledAt.localeCompare(b.sampledAt)) const last = sorted[sorted.length - 1] - const prev = sorted[sorted.length - 2] if (!last) { return { name, running: false, disabled: true, rxNow: 0, txNow: 0 } } - const now = ifaceNowMbps(prev, last) + const built = buildTrafficFromSamples(sorted, Date.parse(sinceIso), end, name, undefined, true) + const observed = applyTrafficFreshness(built.observations!, server, context.settings, context.states.get(server.id), end) return { name, running: Boolean(last.running), disabled: Boolean(last.disabled), - rxNow: now.rxNow, - txNow: now.txNow, + rxNow: observed.rxNow ? built.rxNow : null, + txNow: observed.txNow ? built.txNow : null, } - }).sort((a, b) => (b.rxNow + b.txNow) - (a.rxNow + a.txNow)) + }).sort((a, b) => ((b.rxNow ?? -1) + (b.txNow ?? -1)) - ((a.rxNow ?? -1) + (a.txNow ?? -1))) return reply.send({ interfaces }) }) @@ -432,6 +425,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => { rangeStartMs, rangeEndMs, q.iface && q.iface !== "__all__" ? q.iface : undefined, + await readTrafficCollectionContext(), ) return reply.send({ server: data }) }) diff --git a/backend/src/services/alert-engine/signals-from-collectors.ts b/backend/src/services/alert-engine/signals-from-collectors.ts index 12ecf3f2..b0353908 100644 --- a/backend/src/services/alert-engine/signals-from-collectors.ts +++ b/backend/src/services/alert-engine/signals-from-collectors.ts @@ -4,6 +4,8 @@ import { collectGreSignalsFromRuns } from "./gre-signals.js" import { readGreBgpRunSnapshots } from "../gre-bgp-snapshot-history.js" import { collectSourceSignals, probePeriodMs, type CollectorRunRow, type CollectorSourceConfig } from "./collector-source-signals.js" import type { SignalSnapshot } from "./types.js" +import type { TrafficAttempt } from "../traffic-freshness.js" +import { applyTrafficFreshness } from "../traffic-freshness.js" export type CollectorRead = (query: string, params?: unknown[]) => Promise export type CollectorSourceSnapshot = Pick @@ -18,7 +20,7 @@ async function readRuns(read: CollectorRead, job: string, limit: number, since?: /** Read-only: do not seed defaults or load credentials while building alert facts. */ export async function readCollectorSourceSnapshot(clock: () => number = Date.now, read: CollectorRead = dbAll): Promise { const now = clock() - const [servers, probes, uptime, rest, traffic] = await Promise.all([ + const [servers, probes, uptime, rest, traffic, attempts] = await Promise.all([ read("SELECT id, name, host, enabled, updated_at AS \"updatedAt\" FROM servers"), read(`SELECT id, name, target, src_server_id AS "srcServerId", src_interface AS "srcInterface", enabled, interval_sec AS "intervalSec", updated_at AS "updatedAt" FROM uptime_probes`), @@ -26,6 +28,8 @@ export async function readCollectorSourceSnapshot(clock: () => number = Date.now interval_sec AS "intervalSec", probe_interval_sec AS "probeIntervalSec" FROM uptime_settings WHERE id = 1`), read>('SELECT enabled, interval_sec AS "intervalSec" FROM servers_api_ping_settings WHERE id = 1'), read>('SELECT enabled, interval_sec AS "intervalSec" FROM traffic_settings WHERE id = 1'), + read(`SELECT server_id AS "serverId",server_updated_at AS "serverUpdatedAt", + last_attempt_at AS "lastAttemptAt",last_succeeded_at AS "lastSucceededAt",status,error FROM traffic_collection_state`), ]) const config: CollectorSourceConfig = { servers, probes, uptime: uptime[0], rest: rest[0], traffic: traffic[0] } const periods = probes.filter(p => p.enabled).map(p => probePeriodMs(config, p)).filter(Number.isFinite) @@ -36,9 +40,17 @@ export async function readCollectorSourceSnapshot(clock: () => number = Date.now readRuns(read, "uptime_ping", 4096, new Date(now - probeWindowMs).toISOString()), readRuns(read, "traffic", 8), ]) + const facts = collectSourceSignals(config, { resources: resourceRuns, rest: restRuns, ping: pingRuns, traffic: trafficRuns }, now) + facts.trafficByServer = facts.trafficByServer.filter(signal => { + const server = servers.find(s => (s.name || s.host) === signal.serverName) + const attempt = server && attempts.find(a => a.serverId === Number(server.id)) + if (!attempt) return true // Existing scheduler provenance remains readable until this device's first new attempt. + return !!server && applyTrafficFreshness({ rxNow: true, txNow: true, rxSeries: [], txSeries: [], sampledAt: signal.sampledAt }, + { enabled: server.enabled === true, updatedAt: server.updatedAt ?? "" }, traffic[0], attempt, now).currentState === "current" + }) return { sampledAt: new Date(now).toISOString(), - ...collectSourceSignals(config, { resources: resourceRuns, rest: restRuns, ping: pingRuns, traffic: trafficRuns }, now), + ...facts, } } diff --git a/backend/src/services/scheduler-recorded-run.test.ts b/backend/src/services/scheduler-recorded-run.test.ts index e08e16cc..6fa20b2c 100644 --- a/backend/src/services/scheduler-recorded-run.test.ts +++ b/backend/src/services/scheduler-recorded-run.test.ts @@ -55,9 +55,15 @@ test("partial backups, renew and alert errors are explicit while confirmed resul assert.match(schedulerSnapshotError({ v: 1, job: "alert_engine", sampledAt: "now", rulesChecked: 1, standaloneFires: 0, groupFires: 0, skippedNoTelegram: false, errors: ["send failed"] })!, /send failed/) }) test("valid offline/down observations are not collector errors", () => { - assert.equal(schedulerSnapshotError({ v: 1, job: "traffic", sampledAt: "now", servers: [{ serverId: 1, name: "router", host: "host", ok: false, error: "offline" }] }), null) + assert.equal(schedulerSnapshotError({ v: 1, job: "traffic", sampledAt: "now", servers: [{ serverId: 1, name: "router", host: "host", ok: true, sumRxMbps: 0, sumTxMbps: 0 }] }), null) assert.equal(schedulerSnapshotError({ v: 1, job: "uptime_ping", sampledAt: "now", probes: [{ probeId: "probe", name: "probe", target: "host", srcServerId: 1, srcServerName: "router", srcInterface: "ether1", ok: false, rttMs: null, lossPct: 100, status: "down" }] }), null) }) +test("traffic command errors are limitations; partial measurements remain in the receipt",async()=>{ + const snapshot={v:1 as const,job:"traffic" as const,sampledAt:"now",servers:[{serverId:1,name:"router",host:"host",ok:false,error:"401"},{serverId:2,name:"second",host:"second",ok:true,sumRxMbps:0,sumTxMbps:0}]} + const h=harness(snapshot as never),receipt=await runRecordedSchedulerJob(h.deps) + assert.equal(receipt.status,"error");assert.match(receipt.error!,/router: 401/) + assert.equal(receipt.result,snapshot);assert.equal(h.records.length,1);assert.equal(h.events[0].level,"warning") +}) test("skipped/not-due snapshots preserve their reason and do not announce completed work", async () => { for (const snapshot of [backup({ due: false }), backup({ skipped: true })]) { const h = harness(snapshot); const receipt = await runRecordedSchedulerJob(h.deps) diff --git a/backend/src/services/scheduler-recorded-run.ts b/backend/src/services/scheduler-recorded-run.ts index 68554200..4c9a1a13 100644 --- a/backend/src/services/scheduler-recorded-run.ts +++ b/backend/src/services/scheduler-recorded-run.ts @@ -20,6 +20,9 @@ export function schedulerSnapshotError(snapshot: SchedulerRunSnapshot | null): s if (!snapshot) return null if ("fatalError" in snapshot && snapshot.fatalError) return snapshot.fatalError const errors = "errors" in snapshot ? [...(snapshot.errors ?? [])] : [] + if (snapshot.job === "traffic") for (const server of snapshot.servers) { + if (!server.ok) errors.push(`${server.name}: ${server.error ?? "Ошибка опроса трафика"}`) + } if (snapshot.job === "uptime_speed") { for (const run of snapshot.runs) if (!run.ok && run.error) errors.push(run.error) } diff --git a/backend/src/services/traffic-collection-state.ts b/backend/src/services/traffic-collection-state.ts new file mode 100644 index 00000000..59758a6a --- /dev/null +++ b/backend/src/services/traffic-collection-state.ts @@ -0,0 +1,27 @@ +import { and, eq, lte } from "drizzle-orm" +import { db } from "../db/index.js" +import { trafficCollectionState, trafficSettings, type Server } from "../db/schema.js" + +export async function recordTrafficCollectionAttempt(server: Pick, + status: "pending" | "ok" | "error", sampledAt: string, error?: string) { + if (status === "pending") { + const next = { serverId: server.id, serverUpdatedAt: server.updatedAt, + lastAttemptAt: sampledAt, status, error: null } + await db.insert(trafficCollectionState).values(next).onConflictDoUpdate({ + target: trafficCollectionState.serverId, set: next, + setWhere: lte(trafficCollectionState.lastAttemptAt, sampledAt), + }) + } else { + await db.update(trafficCollectionState).set({ status, error: error?.slice(0, 1024) ?? null, + ...(status === "ok" ? { lastSucceededAt: sampledAt } : {}) }).where(and( + eq(trafficCollectionState.serverId, server.id), eq(trafficCollectionState.lastAttemptAt, sampledAt), + )) + } +} + +/** Read-only: does not create default settings or contact a device. */ +export async function readTrafficCollectionContext() { + const [states, settings] = await Promise.all([db.select().from(trafficCollectionState), + db.select().from(trafficSettings).where(eq(trafficSettings.id, 1)).limit(1)]) + return { states: new Map(states.map(s => [s.serverId, s])), settings: settings[0] } +} diff --git a/backend/src/services/traffic-collector.ts b/backend/src/services/traffic-collector.ts index 11e5f77e..19af9f12 100644 --- a/backend/src/services/traffic-collector.ts +++ b/backend/src/services/traffic-collector.ts @@ -8,6 +8,7 @@ import { MikrotikClient } from "./mikrotik.js" import { bpsToMbps, shouldIncludeIface } from "./traffic-rate.js" import { observedTrafficRate, parseTrafficCounter } from "./traffic-observation.js" import { rememberServerIfaces } from "./traffic-flow-ifindex.js" +import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js" interface RosIfaceTraffic { ".id"?: string @@ -86,6 +87,7 @@ async function readPreviousWave(serverId: number): Promise [`${r.interfaceName}\0${r.peerPublicKey ?? ""}`, r])) + return new Map(rows.filter(r => decodeTrafficSampleFlags(r.flags).countersKnown) + .map((r) => [`${r.interfaceName}\0${r.peerPublicKey ?? ""}`, r])) } export async function collectTrafficOnce(): Promise { @@ -110,7 +113,6 @@ export async function collectTrafficOnce(): Promise { collecting = true const startedAt = Date.now() const now = sampledAt - const settings = await getSettingsRow() const snapshot: TrafficRunSnapshot = { v: SCHEDULER_RUN_SNAPSHOT_VERSION, job: "traffic", @@ -119,11 +121,13 @@ export async function collectTrafficOnce(): Promise { } try { + await getSettingsRow() const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true)) for (const srv of enabledServers) { const name = srv.name || srv.host try { + await recordTrafficCollectionAttempt(srv, "pending", now) const client = MikrotikClient.fromServer(srv) const ifaces = await client.get("/interface") rememberServerIfaces(srv.id, ifaces) @@ -155,7 +159,7 @@ export async function collectTrafficOnce(): Promise { txBytes, rxBps, txBps, - flags: encodeTrafficFlags(running, disabled), + flags: encodeTrafficFlags(running, disabled, true), } }) try { @@ -184,7 +188,7 @@ export async function collectTrafficOnce(): Promise { txBytes, rxBps, txBps, - flags: encodeTrafficFlags(running, disabled), + flags: encodeTrafficFlags(running, disabled, true), }) } } catch { @@ -193,6 +197,7 @@ export async function collectTrafficOnce(): Promise { if (rows.length > 0) { await db.insert(trafficSamples).values(rows) } + await recordTrafficCollectionAttempt(srv, "ok", now) snapshot.servers.push({ serverId: srv.id, name, @@ -205,6 +210,7 @@ export async function collectTrafficOnce(): Promise { } : {}), }) } catch (err) { + await recordTrafficCollectionAttempt(srv, "error", now, err instanceof Error ? err.message : String(err)) snapshot.servers.push({ serverId: srv.id, name, @@ -218,7 +224,7 @@ export async function collectTrafficOnce(): Promise { await db.update(trafficSettings).set({ lastCollectedAt: now, lastDurationMs: Date.now() - startedAt, - lastError: "", + lastError: snapshot.servers.filter(s => !s.ok).map(s => `${s.name}: ${s.error ?? "Ошибка опроса"}`).join("; "), updatedAt: now, }).where(eq(trafficSettings.id, 1)) } catch (e) { diff --git a/backend/src/services/traffic-freshness.test.ts b/backend/src/services/traffic-freshness.test.ts new file mode 100644 index 00000000..a06dda20 --- /dev/null +++ b/backend/src/services/traffic-freshness.test.ts @@ -0,0 +1,21 @@ +import assert from "node:assert/strict" +import { applyTrafficFreshness, mergeTrafficObservations } from "./traffic-freshness.js" +const now = Date.parse("2026-10-12T12:00:00Z"), iso = (offset=0) => new Date(now+offset).toISOString() +const observed = {rxNow:true,txNow:true,rxSeries:[false,true],txSeries:[false,true],sampledAt:iso()} +const server={enabled:true,updatedAt:iso(-3600_000)},settings={enabled:true,intervalSec:30} +const attempt={serverUpdatedAt:server.updatedAt,lastAttemptAt:iso(),lastSucceededAt:iso(),status:"ok",error:null} +const check=(s=server,c=settings,a:typeof attempt|undefined=attempt,o=observed,t=now)=>applyTrafficFreshness(o,s,c,a,t) +assert.equal(check().currentState,"current");assert.equal(check().rxNow,true) +for(const status of ["error","pending"]) {const r=check(server,settings,{...attempt,status});assert.equal(r.currentState,status);assert.equal(r.rxNow,false);assert.deepEqual(r.rxSeries,observed.rxSeries)} +assert.equal(applyTrafficFreshness(observed,server,settings,undefined,now).currentState,"unknown") +assert.equal(check({...server,updatedAt:iso(1)}).currentState,"unknown") +assert.equal(check({...server,enabled:false}).currentState,"disabled") +assert.equal(check(server,{...settings,enabled:false}).currentState,"disabled") +assert.equal(check(server,settings,attempt,observed,now+90_000).currentState,"current") +assert.equal(check(server,settings,attempt,observed,now+90_001).currentState,"stale") +assert.equal(check(server,settings,{...attempt,lastAttemptAt:iso(30_001)}).currentState,"stale") +assert.equal(check(server,settings,attempt,{...observed,sampledAt:iso(-1000)}).currentState,"unknown") +const mixed=mergeTrafficObservations([check(),check(server,settings,{...attempt,status:"error",error:"401"} as typeof attempt)]) +assert.equal(mixed.rxNow,false);assert.equal(mixed.currentState,"error");assert.deepEqual(mixed.rxSeries,[false,true]) +assert.equal(mergeTrafficObservations([]).rxNow,false) +console.log("traffic freshness/source barrier/merge tests ok") diff --git a/backend/src/services/traffic-freshness.ts b/backend/src/services/traffic-freshness.ts new file mode 100644 index 00000000..476332ee --- /dev/null +++ b/backend/src/services/traffic-freshness.ts @@ -0,0 +1,45 @@ +import type { TrafficObservations } from "./traffic-rate.js" + +export type TrafficCurrentState = "current" | "unknown" | "pending" | "error" | "stale" | "disabled" +export interface TrafficAttempt { + serverUpdatedAt: string | Date; lastAttemptAt: string | Date; lastSucceededAt: string | Date | null; status: string; error: string | null +} +export function mergeTrafficObservations(parts: TrafficObservations[]): TrafficObservations { + const oldest = (key: "sampledAt" | "lastAttemptAt" | "lastSucceededAt" | "expiresAt") => { + const times = parts.map(p => Date.parse(p[key] ?? "")) + return times.length && times.every(Number.isFinite) ? new Date(Math.min(...times)).toISOString() : null + } + const states: TrafficCurrentState[] = ["error", "pending", "stale", "disabled", "unknown"] + return { rxNow: !!parts.length && parts.every(p => p.rxNow), txNow: !!parts.length && parts.every(p => p.txNow), + rxSeries: Array.from({ length: parts[0]?.rxSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.rxSeries[i])), + txSeries: Array.from({ length: parts[0]?.txSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.txSeries[i])), + sampledAt: oldest("sampledAt"), expiresAt: oldest("expiresAt"), lastAttemptAt: oldest("lastAttemptAt"), lastSucceededAt: oldest("lastSucceededAt"), + currentState: parts.length ? states.find(state => parts.some(p => p.currentState === state)) ?? "current" : "unknown", + error: parts.find(p => p.error)?.error ?? null, + } +} +export function applyTrafficFreshness(observations: TrafficObservations, + server: { enabled: boolean; updatedAt: string | Date }, + settings: { enabled: boolean; intervalSec: number } | undefined, + attempt: TrafficAttempt | undefined, now: number): TrafficObservations { + const period = Math.max(5, settings?.intervalSec ?? 30) * 1000 + const time = (raw: string | Date | null | undefined) => raw instanceof Date ? raw.getTime() : Date.parse(raw ?? "") + const attemptMs = time(attempt?.lastAttemptAt), successMs = time(attempt?.lastSucceededAt) + let currentState: TrafficCurrentState = "unknown" + if (!server.enabled || settings?.enabled === false) currentState = "disabled" + else if (settings && attempt && time(attempt.serverUpdatedAt) === time(server.updatedAt)) { + if (!Number.isFinite(attemptMs) || attemptMs > now + period || now - attemptMs > 3 * period) currentState = "stale" + else if (attempt.status === "pending") currentState = "pending" + else if (attempt.status === "error") currentState = "error" + else if (attempt.status === "ok" && successMs === attemptMs && + Date.parse(observations.sampledAt ?? "") === successMs && (observations.rxNow || observations.txNow)) currentState = "current" + } + return { ...observations, + expiresAt: Number.isFinite(attemptMs + 3 * period) && Math.abs(attemptMs + 3 * period) <= 8.64e15 ? new Date(attemptMs + 3 * period).toISOString() : null, + rxNow: currentState === "current" && observations.rxNow, + txNow: currentState === "current" && observations.txNow, + currentState, lastAttemptAt: Number.isFinite(attemptMs) ? new Date(attemptMs).toISOString() : null, + lastSucceededAt: Number.isFinite(successMs) ? new Date(successMs).toISOString() : null, + error: attempt?.error ?? null, + } +} diff --git a/backend/src/services/traffic-history-sql.test.ts b/backend/src/services/traffic-history-sql.test.ts new file mode 100644 index 00000000..82e9c557 --- /dev/null +++ b/backend/src/services/traffic-history-sql.test.ts @@ -0,0 +1,61 @@ +import assert from "node:assert/strict" +import { createServer } from "node:http" +import Fastify from "fastify" +import { pool, closePool } from "../db/index.js" +import { initDatabase } from "../db/bootstrap.js" +import { collectTrafficOnce } from "./traffic-collector.js" +import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js" +import { buildSignalSnapshotFromCollectors } from "./alert-engine/signals-from-collectors.js" +import trafficRoutes from "../routes/traffic.js" +import { clearAlertCollectorHooksTimer } from "./alert-collector-hooks.js" + +const database=process.env.MMAPP_OWN_SQL_DB +if(!database) console.log("traffic history SQL: skipped; explicit owned database required") +else { + assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,"/"+database) + assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database) + const prefix="own-history178-"+Date.now()+"-",userId=prefix+"user" + let serverId:number|undefined,deny=false,counter="1000",cases=0,requests=0 + const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;res.end('{"message":"Unauthorized"}')}else res.end(JSON.stringify(req.url?.includes("/wireguard/")?[]:[{name:"ether1",running:"true",disabled:"false","rx-byte":counter,"tx-byte":counter}]))}) + await new Promise(resolve=>http.listen(0,"127.0.0.1",resolve));const address=http.address();assert(address&&typeof address==="object") + const app=Fastify();await app.register(trafficRoutes) + const get=async(path:string)=>{const r=await app.inject({method:"GET",url:path});assert.equal(r.statusCode,200);return r.json()} + try { + await initDatabase() + const updatedAt=new Date(Date.now()-3_600_000).toISOString() + serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,updated_at) VALUES($1,'127.0.0.1',$2,false,true,'fixture','fixture',$3) RETURNING id",[prefix+"server",address.port,updatedAt])).rows[0].id) + await pool.query("INSERT INTO traffic_settings(id,enabled,interval_sec) VALUES(1,true,30) ON CONFLICT(id) DO UPDATE SET enabled=true,interval_sec=30") + await pool.query("INSERT INTO app_users(id,login,name) VALUES($1,$2,$3)",[userId,prefix+"login","History fixture"]) + await pool.query("INSERT INTO user_interface_bindings(id,user_id,server_id,interface_name,interface_type) VALUES($1,$2,$3,'ether1','ether')",[prefix+"binding",userId,serverId]) + for(const offset of[-60_000,-30_000])await pool.query("INSERT INTO traffic_samples(server_id,interface_name,sampled_at,rx_bytes,tx_bytes,flags) VALUES($1,'ether1',$2,1000,1000,1)",[serverId,new Date(Date.now()+offset).toISOString()]) + const detail=async()=> (await get(`/traffic/servers/${serverId}?range=1h`)).server + let dto=await detail();assert.equal(dto.observations.rxNow,false);assert(dto.observations.rxSeries.every((v:boolean)=>!v));cases++ + await collectTrafficOnce();dto=await detail();assert.equal(dto.observations.rxNow,false);cases++ + await pool.query("DELETE FROM traffic_samples WHERE server_id=$1 AND (flags & 4)=0",[serverId]) + await pool.query("UPDATE traffic_samples SET sampled_at=$1 WHERE server_id=$2",[new Date(Date.now()-30_000).toISOString(),serverId]) + counter="3751000";const success=await collectTrafficOnce();dto=await detail();assert.equal(dto.observations.currentState,"current");assert.equal(dto.observations.rxNow,true);assert(dto.rxNow>0.9);cases++ + await pool.query("INSERT INTO scheduler_runs(id,job_key,started_at,finished_at,status,result_json) VALUES($1,'traffic',$2,$3,'ok',$4::jsonb)",[prefix+"run",success.sampledAt,new Date().toISOString(),JSON.stringify(success)]) + assert.equal((await buildSignalSnapshotFromCollectors()).trafficByServer.length,1);cases++ + const succeeded=dto.observations.lastSucceededAt + deny=true;await collectTrafficOnce();dto=await detail();assert.equal(dto.observations.currentState,"error");assert.equal(dto.observations.rxNow,false);assert.equal(dto.observations.lastSucceededAt,succeeded);assert(dto.observations.rxSeries.some(Boolean));cases++ + assert.deepEqual((await buildSignalSnapshotFromCollectors()).trafficByServer,[]);cases++ + const users=(await get("/traffic/users?range=1h")).users, bound=(await get("/traffic/bound-interfaces?range=1h")).interfaces + assert.equal(users.find((u:{id:string})=>u.id===userId).observations.currentState,"error");assert.equal(bound.find((b:{userId:string})=>b.userId===userId).observations.rxNow,false);cases++ + const interfaces=(await get(`/traffic/servers/${serverId}/interfaces`)).interfaces;assert.equal(interfaces[0].rxNow,null);cases++ + const manual=await app.inject({method:"POST",url:"/traffic/collect-now"});assert.equal(manual.statusCode,502);assert.equal(manual.json().ok,false) + assert((await pool.query("SELECT status FROM scheduler_runs WHERE job_key='traffic' ORDER BY finished_at DESC LIMIT 1")).rows[0].status==="error");cases++ + deny=false;counter="3751000";const recovery=await app.inject({method:"POST",url:"/traffic/collect-now"});assert.equal(recovery.statusCode,200);dto=await detail();assert.equal(dto.observations.currentState,"current");assert.equal(dto.rxNow,0);cases++ + await pool.query("UPDATE traffic_collection_state SET last_attempt_at=$1,last_succeeded_at=$1 WHERE server_id=$2",[new Date(Date.now()-100_000).toISOString(),serverId]);dto=await detail();assert.equal(dto.observations.currentState,"stale");assert.equal(dto.observations.rxNow,false);assert(dto.observations.rxSeries.some(Boolean));cases++ + await recordTrafficCollectionAttempt({id:serverId,updatedAt},"pending",new Date().toISOString());dto=await detail();assert.equal(dto.observations.currentState,"pending");cases++ + await pool.query("UPDATE servers SET updated_at=$1 WHERE id=$2",[new Date(Date.now()+1).toISOString(),serverId]);dto=await detail();assert.equal(dto.observations.currentState,"unknown");cases++ + await pool.query("UPDATE servers SET enabled=false WHERE id=$1",[serverId]);const before=requests;await collectTrafficOnce();assert.equal(requests,before);dto=await detail();assert.equal(dto.observations.currentState,"disabled");cases++ + console.log(`traffic history owned SQL: ${cases} cases; legacy/current/failure/history/users/interfaces/manual/source/stale/pending/config/disabled passed`) + } finally { + clearAlertCollectorHooksTimer() + await app.close();await new Promise(resolve=>http.close(()=>resolve())) + await pool.query("DELETE FROM app_users WHERE id=$1",[userId]) + await pool.query("DELETE FROM scheduler_runs WHERE id LIKE $1",[prefix+"%"]) + if(serverId!==undefined){await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await pool.query("DELETE FROM servers WHERE id=$1",[serverId])} + await closePool() + } +} diff --git a/backend/src/services/traffic-observation.test.ts b/backend/src/services/traffic-observation.test.ts index a84af8bd..d29e27d4 100644 --- a/backend/src/services/traffic-observation.test.ts +++ b/backend/src/services/traffic-observation.test.ts @@ -45,6 +45,7 @@ const context = vm.createContext({ SCHEDULER_RUN_SNAPSHOT_VERSION: 1, servers: {}, trafficSamples: {}, trafficSettings: {}, eq: () => ({}), getSettingsRow: async () => ({}), readPreviousWave: async () => wave, rememberServerIfaces: () => {}, encodeTrafficFlags: () => 1, + recordTrafficCollectionAttempt: async () => {}, MikrotikClient: { fromServer: () => ({ get: async (path: string) => path === "/interface" ? [{ name: "ether1", running: String(included), disabled: "false", "rx-byte": counters, "tx-byte": counters }] : [] }) }, db: { diff --git a/backend/src/services/traffic-observation.ts b/backend/src/services/traffic-observation.ts index 6a67982a..b80e2020 100644 --- a/backend/src/services/traffic-observation.ts +++ b/backend/src/services/traffic-observation.ts @@ -15,6 +15,9 @@ export function observedTrafficRate( rxBytes: number, txBytes: number, nowMs: number, ): { rxBps: number; txBps: number; measured: boolean } { const prevMs = previous ? Date.parse(previous.sampledAt) : NaN + if (!previous || ![previous.rxBytes, previous.txBytes, rxBytes, txBytes].every(v => Number.isSafeInteger(v) && v >= 0)) { + return { rxBps: 0, txBps: 0, measured: false } + } const rx = previous ? rateBpsFromDelta(previous.rxBytes, rxBytes, prevMs, nowMs) : null const tx = previous ? rateBpsFromDelta(previous.txBytes, txBytes, prevMs, nowMs) : null // Stored rates retain the legacy numeric shape; observed totals require both deltas. diff --git a/backend/src/services/traffic-rate.ts b/backend/src/services/traffic-rate.ts index 32ed81af..1f9c2c58 100644 --- a/backend/src/services/traffic-rate.ts +++ b/backend/src/services/traffic-rate.ts @@ -1,4 +1,5 @@ /** Чистые формулы трафика: дельты счётчиков, корзины series, monitor-traffic. */ +import { mergeTrafficObservations } from "./traffic-freshness.js" export const SERIES_POINTS = 60 @@ -12,6 +13,7 @@ export interface TrafficSampleLike { txBps: number running: boolean disabled: boolean + countersKnown?: boolean } export interface RatePoint { @@ -20,7 +22,8 @@ export interface RatePoint { txMbps: number } -export interface TrafficObservations {rxNow:boolean;txNow:boolean;rxSeries:boolean[];txSeries:boolean[];sampledAt:string|null} +export interface TrafficObservations {rxNow:boolean;txNow:boolean;rxSeries:boolean[];txSeries:boolean[];sampledAt:string|null;expiresAt?:string|null; + currentState?: import("./traffic-freshness.js").TrafficCurrentState;lastAttemptAt?:string|null;lastSucceededAt?:string|null;error?:string|null} export interface BuiltTrafficSeries { observations?:TrafficObservations @@ -161,7 +164,7 @@ export function buildTrafficFromSamples( continue } - const sorted = [...arr].sort((a, b) => a.sampledAt.localeCompare(b.sampledAt)) + const sorted = [...arr].sort((a, b) => Date.parse(a.sampledAt) - Date.parse(b.sampledAt)) const last = sorted[sorted.length - 1] if (!last) continue if (!allowList && (!last.running || last.disabled)) continue @@ -169,14 +172,6 @@ export function buildTrafficFromSamples( includedSeries++ if (last.running && !last.disabled) sessions += 1 - const first = sorted[0] - if (first) { - const dRx = last.rxBytes - first.rxBytes - const dTx = last.txBytes - first.txBytes - rxBytesDelta += dRx >= 0 ? dRx : last.rxBytes - txBytesDelta += dTx >= 0 ? dTx : last.txBytes - } - for (let i = 1; i < sorted.length; i++) { const prev = sorted[i - 1] const cur = sorted[i] @@ -185,8 +180,13 @@ export function buildTrafficFromSamples( const t0 = parseIsoMs(prev.sampledAt) const t1 = parseIsoMs(cur.sampledAt) if(Number.isFinite(Date.parse(cur.sampledAt))&&t1>=rangeStartMs&&t1<=rangeEndMs)lastAttemptAt=Math.max(lastAttemptAt??t1,t1) - const rxBps = rateBpsFromDelta(prev.rxBytes, cur.rxBytes, t0, t1) - const txBps = rateBpsFromDelta(prev.txBytes, cur.txBytes, t0, t1) + const countersKnown = prev.countersKnown !== false && cur.countersKnown !== false + const rxBps = countersKnown ? rateBpsFromDelta(prev.rxBytes, cur.rxBytes, t0, t1) : null + const txBps = countersKnown ? rateBpsFromDelta(prev.txBytes, cur.txBytes, t0, t1) : null + if (t1 >= rangeStartMs && t1 <= rangeEndMs) { + if (rxBps !== null) rxBytesDelta += cur.rxBytes - prev.rxBytes + if (txBps !== null) txBytesDelta += cur.txBytes - prev.txBytes + } if (rxBps == null && txBps == null) continue const rxMbps = bpsToMbps(rxBps ?? 0) const txMbps = bpsToMbps(txBps ?? 0) @@ -269,7 +269,7 @@ export function mergeBuiltTraffic(parts: BuiltTrafficSeries[]): BuiltTrafficSeri } acc.rxTotalGiB = Number(acc.rxTotalGiB.toFixed(1)) acc.txTotalGiB = Number(acc.txTotalGiB.toFixed(1)) - return acc + return parts.every(p => p.observations) ? { ...acc, observations: mergeTrafficObservations(parts.map(p => p.observations!)) } : acc } export function parseMonitorTraffic( diff --git a/backend/src/services/traffic-users.ts b/backend/src/services/traffic-users.ts index 79053d76..8ac48053 100644 --- a/backend/src/services/traffic-users.ts +++ b/backend/src/services/traffic-users.ts @@ -8,8 +8,12 @@ import { mergeBuiltTraffic, type BuiltTrafficSeries, } from "./traffic-rate.js" +import { readTrafficCollectionContext } from "./traffic-collection-state.js" +import { applyTrafficFreshness } from "./traffic-freshness.js" +import type { TrafficObservations } from "./traffic-rate.js" export interface BoundIfaceTrafficDto { + observations?: TrafficObservations id: string bindingId: string userId: string @@ -36,6 +40,7 @@ export interface BoundIfaceTrafficDto { } export interface UserTrafficDto { + observations?: TrafficObservations id: string login: string displayName: string @@ -54,6 +59,7 @@ export interface UserTrafficDto { function seriesFromBuilt(built: BuiltTrafficSeries) { return { + observations: built.observations, rxNow: built.rxNow, txNow: built.txNow, rxPeak: built.rxPeak, @@ -74,6 +80,8 @@ async function serverStatus(serverId: number): Promise<"online" | "offline"> { export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: number): Promise { const sinceIso = new Date(rangeStartMs).toISOString() const users = await listUsers() + const [context, allServers] = await Promise.all([readTrafficCollectionContext(), db.select().from(servers)]) + const catalog = new Map(allServers.map(s => [s.id, s])) const sampleCache = new Map>>() return Promise.all(users.map(async (user) => { @@ -87,7 +95,10 @@ export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: num sampleCache.set(b.serverId, rows) } const peerKey = b.peerPublicKey ?? "" - const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, b.interfaceName, peerKey) + const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, b.interfaceName, peerKey, true) + const server = catalog.get(b.serverId) + if (built.observations) built.observations = applyTrafficFreshness(built.observations, + server ?? { enabled: false, updatedAt: "" }, context.settings, context.states.get(b.serverId), rangeEndMs) parts.push(built) const last = [...rows.filter((r) => r.interfaceName === b.interfaceName && (r.peerPublicKey ?? "") === peerKey, diff --git a/components/reui-kit/traffic-mini-chart.tsx b/components/reui-kit/traffic-mini-chart.tsx index 4d7568c9..14ee2928 100644 --- a/components/reui-kit/traffic-mini-chart.tsx +++ b/components/reui-kit/traffic-mini-chart.tsx @@ -1,14 +1,14 @@ import { Fragment } from "react" /** Compact RX/TX history shared by traffic cards. Gaps remain gaps. */ -export function TrafficMiniChart({ rx, tx, height = 44 }: { rx: number[]; tx: number[]; height?: number }) { - const valid = (value: number) => Number.isFinite(value) && value >= 0 +export function TrafficMiniChart({ rx, tx, height = 44 }: { rx: (number|null)[]; tx: (number|null)[]; height?: number }) { + const valid = (value: number|null): value is number => value !== null && Number.isFinite(value) && value >= 0 const values = [...rx, ...tx].filter(valid) if (!values.length) return

Нет истории трафика

const width = 300 const count = Math.max(rx.length, tx.length) const max = Math.max(...values, 1) * 1.1 - const segments = (series: number[]) => { + const segments = (series: (number|null)[]) => { const result: Array> = [] let current: Array<{ x: number; y: number }> = [] series.forEach((value, index) => { diff --git a/components/traffic/traffic-observation-status.tsx b/components/traffic/traffic-observation-status.tsx new file mode 100644 index 00000000..aeb646b3 --- /dev/null +++ b/components/traffic/traffic-observation-status.tsx @@ -0,0 +1,18 @@ +import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert" +import type { TrafficObservationMeta } from "@/lib/traffic-observations" + +export function TrafficObservationStatus({ observations: o, unknown }: { observations?: TrafficObservationMeta; unknown: boolean }) { + const labels = { error: "Ошибка опроса трафика", pending: "Опрос трафика выполняется", stale: "Измерения трафика устарели", + disabled: "Опрос трафика отключён", unknown: "Скорость пока не измерена", current: "Скорость пока не измерена" } + const warning = unknown || o?.currentState && o.currentState !== "current" + return <> + {warning && + {labels[o?.currentState ?? "unknown"]} + {o?.error || "История сохранена. Для текущей скорости нужны достоверные свежие измерения."} + } + {o?.lastAttemptAt &&

+ Последняя попытка: + {o.lastSucceededAt && <> · успешный сбор: } +

} + +} diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 2c71bb1e..bcfba366 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md). @@ -696,7 +696,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -2017,3 +2017,33 @@ Ping ownSQL173 расширен двумя controls: summary53ms900us сохра [Live UI1440/320](ux-ui/acceptance/ping-duration-deployed-2026-10-12.json) прошёл: uptime overview200, unknown filter, no overflow/pageerrors. Actual production probes catalog пустой — эта smoke проверка не изображает main compound/error sample; они подтверждены отдельной ownSQL. Новых native Ping/config/password/Telegram операций не выполнялось. Пароли CHR не менялись. Исправления Ping173/176 и traffic174–175 теперь совместно доступны на тестовом backend; UI layout не менялся. Следующий178 — read-only audit traffic history freshness при failed collection и legacy invalid-counter provenance; не объявлять старые неоднозначные строки исправленными по новому producer guard. Source unconfirmed/missing diagnostics, summary loss/count consistency, scale/multi-backend, физические WAN и внешние интеграции остаются условиями12.5. Журнал/план синхронизированы; локальный коммит без push. Новых блоков до следующего окна не начинать. + +## 12.5.178 — достоверная история трафика, ручной сбор и свежесть UI (2026-10-12) + +Подтверждены source gaps: исторические deltas могли выдавать старую скорость как текущую после failed collection; manual collect-now игнорировал snapshot failures и публиковал success. Добавлена migration0011/traffic_collection_state (одна актуальная запись на server): pending до HTTP, ok/error после записи, lastAttemptAt/lastSucceededAt и конфигурационный updatedAt fingerprint. Pending/error не стирают successful history. Completion обновляет только свою lastAttemptAt; pending не переписывает более новую попытку. Это ограничивает гонки записей, но не является распределённым lock/полной приёмкой нескольких backend. + +Freshness reader read-only: enabled device/source, совпадение config fingerprint, state.ok и exact latest sample/success/attempt, TTL3periods (min5s), future tolerance1period. Error/pending/stale/disabled/config change/unknown маскируют current RX/TX; historical observed masks сохраняются. API серверов, клиентов и привязок получает observations/provenance; interface inventory больше не использует stored-rate fallback и не смешивает WG peer rows с interface counters. Source facts дополнительно учитывают новую attempt barrier, поэтому manual error/pending не оставляет старое traffic alarm активным. Для устройств без новой state пока читается прежняя scheduler provenance; первая новая попытка создаёт authoritative barrier. Не считать этот переход доказательством точности каждого старого snapshot. + +В flags зарезервирован bit4 countersKnown, выставляется только новым strict collector. Старые/imported rows не переписаны и не объявлены достоверными; previous wave отбирает known counters. Исторический builder не строит deltas между legacy/unknown counters. Учтённые bytes суммируются по подтверждённым соседним дельтам; reset не добавляет произвольный новый counter как объём. Это lower-bound accounted volume, не утверждение полного покрытия периода. В UI подпись «Учтено RX/TX по замерам»; неизвестные скорости/пики/объёмы — —, chart gaps — null. Genuine measured0 сохраняется. Сортировка держит unknown после measured values. Dashboard existing decoder продолжает использовать masks. + +Manual collect-now теперь через runSchedulerJobNow/общую запись результата и execution lock. Busy409; fatal/partial failures502 c failures/succeeded и сохранённым scheduler error; confirmed success200. Scheduler traffic ok=false теперь execution limitation, а genuine0 — success; valid offline/down других collectors не изменены. Ошибка setup больше не оставляет collecting=true навсегда: getSettingsRow находится внутри try/finally. Existing event/journal-unknown semantics сохранены. + +Frontend projectTrafficObservations валидирует metadata/метрики, маскирует legacy numeric-only DTO, допускает RX/TX histories разной длины при корректных собственных masks. local10s clock переводит expired DTO в stale без новых сетевых запросов. Видны причина failed collection, последняя попытка и successful collection (не выдаётся за подтверждение measured rate). Offline object не скрывает сохранившуюся историю. ReUI Frame/installed Alert/KpiStatGrid/chart-23 adaptation и theme/base-nova сохранены; новые blocks/vendor components не устанавливались. + +| Сценарий | Эталон и схема | Артефакт | +| --- | --- | --- | +| Ошибка опроса | Существующая ReUI Alert → current — → сохранённая RX/TX история | [ПК1440](ux-ui/screens/traffic-history-error-1440-2026-10-12.png), [телефон320](ux-ui/screens/traffic-history-error-320-2026-10-12.png), API-fixture | +| Genuine0 → TTL | Подтверждённая0 → expiry → —, история остаётся | E2E clock/control; без clock-based refetch | +| Legacy counters | Нет происхождения → unknown → новая baseline → measured delta | Owned SQL/HTTP collector path | + +Backend test:traffic-rate прошёл: existing formulas, producer body/in-memory boundaries, strict counter/rate guards, freshness/merge. Frontend projection/dashboard unit tests, traffic flags, scheduler11, application-source TypeScript/backend production build/ESLint passed (unused obsolete ifaceNow helper удалён). Первоначальная freshness unit ошиблась default-param undefined, исправлена на прямой missing-state вызов; backend type adapter учитывает PG Date/string. Scheduler regression assertion разделена: traffic command failure является ошибкой исполнения, не device offline observation. + +35 unique E2E eventually passed: initial31=27pass/4fail выявили лишнее требование равной RX/TX длины и положительные fixtures без explicit observations; malformed negative gap fixture заменена правильным null. Delta14=13pass/1fail выявил shorthand-json fixture без metadata; исправлено, delta1 passed. Остальные изменённые positive fixtures21 passed29.5s. Новые4 cases error1440/320/history retained even offline, true0/TTL/no refetch, numeric legacy unknown прошли. Screenshot-only scoped repeats2×2 для видимого warning/history внутри main scroller не считаются новыми сценариями; финальные изображения просмотрены. Existing8 user GRE/VXLAN dirty screenshots не тронуты. Full UI1260/backend24 не повторялись без необходимости. + +Owned PG18.6/Node22: test:traffic-rate15709ms,9 counter/source +14 history controls (legacy/first/current/error/old-alarm barrier/users/interfaces/manual failure/success/stale/pending/config/disabled). Real MikrotikClient к loopback HTTP simulator, real SQL + Fastify GET/POST; не physical CHR fault. Common test:alert-engine20075ms:26 source/history SQL/GRE8/actual stability. Второй runner имел старую command label в report; corrected по actual command/harness и asserts before public recording, профиль не подменён повторным traffic run. Own DBs mm_accept_166_1791809970201 иmm_accept_166_1791810266923/ограниченные roles/test containers удалены; отсутствие проверено каталогами/docker. Production secrets не копировались. [SQL](ux-ui/acceptance/traffic-history-sql-2026-10-12.json). + +[Source448 backend+521 frontend](ux-ui/acceptance/traffic-history-source-2026-10-12.json)/[1032 compiled](ux-ui/acceptance/traffic-history-compiled-2026-10-12.json) совпали SHA256. Обе production builds прошли (frontend185s Next phase); frontend ux-history178 staged with old backend ux-rtt177 [interop](ux-ui/acceptance/traffic-history-interop-2026-10-12.json), затем backend ux-history178/version1.0.0-test.history178. [Runtime](ux-ui/acceptance/traffic-history-runtime-2026-10-12.json) healthy/running/verified imageIds; в.env менялись только tags, rollback ux-rtt177/ux-ping173 без копии секретного env. Migration0011 подтверждена main PG. Новый frontend следует перезагрузить: прежняя вкладка не получает новый truth reader автоматически. Additive storage не гарантирует truthful interpretation старым UI. + +[Native read-only](ux-ui/acceptance/traffic-history-native-2026-10-12.json): оба CHR194.226.121.150/104.165.244.7, GET identity/interface200, по2 interfaces, counters valid;4 native GET, no routerConfigWrites/password changes/messages. Это не loss/speed/WAN proof. [Live smoke1440/320+3 APIs](ux-ui/acceptance/traffic-history-deployed-2026-10-12.json) подтверждает actual provenance/UI/overflow/pageerrors0; error/legacy branches — отдельно fixtures/ownSQL. Main DB read-only только для native checker; schema migration и штатный scheduler writes были частью deploy. + +Следующий179 — inspect/fix live monitor-traffic empty/partial/malformed rates (текущий parseMonitorTraffic заменяет их нулями), per-interface fallback и disabled-after-stream-start polling. Не объявлять этот путь принятым по history guards. Peer collection completeness, long sparse intervals, interface recreation/identity, snapshot after restore, scale/multiple instances и external integrations остаются условиями12.5. Локальный коммит без push. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 7b0c5974..67066bf9 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-12 -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. @@ -371,6 +371,6 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 13a7b72f..db41fc4a 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,36 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. | +## 12.5.178 — достоверная история трафика, ручной сбор и свежесть UI (2026-10-12) + +Подтверждены source gaps: исторические deltas могли выдавать старую скорость как текущую после failed collection; manual collect-now игнорировал snapshot failures и публиковал success. Добавлена migration0011/traffic_collection_state (одна актуальная запись на server): pending до HTTP, ok/error после записи, lastAttemptAt/lastSucceededAt и конфигурационный updatedAt fingerprint. Pending/error не стирают successful history. Completion обновляет только свою lastAttemptAt; pending не переписывает более новую попытку. Это ограничивает гонки записей, но не является распределённым lock/полной приёмкой нескольких backend. + +Freshness reader read-only: enabled device/source, совпадение config fingerprint, state.ok и exact latest sample/success/attempt, TTL3periods (min5s), future tolerance1period. Error/pending/stale/disabled/config change/unknown маскируют current RX/TX; historical observed masks сохраняются. API серверов, клиентов и привязок получает observations/provenance; interface inventory больше не использует stored-rate fallback и не смешивает WG peer rows с interface counters. Source facts дополнительно учитывают новую attempt barrier, поэтому manual error/pending не оставляет старое traffic alarm активным. Для устройств без новой state пока читается прежняя scheduler provenance; первая новая попытка создаёт authoritative barrier. Не считать этот переход доказательством точности каждого старого snapshot. + +В flags зарезервирован bit4 countersKnown, выставляется только новым strict collector. Старые/imported rows не переписаны и не объявлены достоверными; previous wave отбирает known counters. Исторический builder не строит deltas между legacy/unknown counters. Учтённые bytes суммируются по подтверждённым соседним дельтам; reset не добавляет произвольный новый counter как объём. Это lower-bound accounted volume, не утверждение полного покрытия периода. В UI подпись «Учтено RX/TX по замерам»; неизвестные скорости/пики/объёмы — —, chart gaps — null. Genuine measured0 сохраняется. Сортировка держит unknown после measured values. Dashboard existing decoder продолжает использовать masks. + +Manual collect-now теперь через runSchedulerJobNow/общую запись результата и execution lock. Busy409; fatal/partial failures502 c failures/succeeded и сохранённым scheduler error; confirmed success200. Scheduler traffic ok=false теперь execution limitation, а genuine0 — success; valid offline/down других collectors не изменены. Ошибка setup больше не оставляет collecting=true навсегда: getSettingsRow находится внутри try/finally. Existing event/journal-unknown semantics сохранены. + +Frontend projectTrafficObservations валидирует metadata/метрики, маскирует legacy numeric-only DTO, допускает RX/TX histories разной длины при корректных собственных masks. local10s clock переводит expired DTO в stale без новых сетевых запросов. Видны причина failed collection, последняя попытка и successful collection (не выдаётся за подтверждение measured rate). Offline object не скрывает сохранившуюся историю. ReUI Frame/installed Alert/KpiStatGrid/chart-23 adaptation и theme/base-nova сохранены; новые blocks/vendor components не устанавливались. + +| Сценарий | Эталон и схема | Артефакт | +| --- | --- | --- | +| Ошибка опроса | Существующая ReUI Alert → current — → сохранённая RX/TX история | [ПК1440](ux-ui/screens/traffic-history-error-1440-2026-10-12.png), [телефон320](ux-ui/screens/traffic-history-error-320-2026-10-12.png), API-fixture | +| Genuine0 → TTL | Подтверждённая0 → expiry → —, история остаётся | E2E clock/control; без clock-based refetch | +| Legacy counters | Нет происхождения → unknown → новая baseline → measured delta | Owned SQL/HTTP collector path | + +Backend test:traffic-rate прошёл: existing formulas, producer body/in-memory boundaries, strict counter/rate guards, freshness/merge. Frontend projection/dashboard unit tests, traffic flags, scheduler11, application-source TypeScript/backend production build/ESLint passed (unused obsolete ifaceNow helper удалён). Первоначальная freshness unit ошиблась default-param undefined, исправлена на прямой missing-state вызов; backend type adapter учитывает PG Date/string. Scheduler regression assertion разделена: traffic command failure является ошибкой исполнения, не device offline observation. + +35 unique E2E eventually passed: initial31=27pass/4fail выявили лишнее требование равной RX/TX длины и положительные fixtures без explicit observations; malformed negative gap fixture заменена правильным null. Delta14=13pass/1fail выявил shorthand-json fixture без metadata; исправлено, delta1 passed. Остальные изменённые positive fixtures21 passed29.5s. Новые4 cases error1440/320/history retained even offline, true0/TTL/no refetch, numeric legacy unknown прошли. Screenshot-only scoped repeats2×2 для видимого warning/history внутри main scroller не считаются новыми сценариями; финальные изображения просмотрены. Existing8 user GRE/VXLAN dirty screenshots не тронуты. Full UI1260/backend24 не повторялись без необходимости. + +Owned PG18.6/Node22: test:traffic-rate15709ms,9 counter/source +14 history controls (legacy/first/current/error/old-alarm barrier/users/interfaces/manual failure/success/stale/pending/config/disabled). Real MikrotikClient к loopback HTTP simulator, real SQL + Fastify GET/POST; не physical CHR fault. Common test:alert-engine20075ms:26 source/history SQL/GRE8/actual stability. Второй runner имел старую command label в report; corrected по actual command/harness и asserts before public recording, профиль не подменён повторным traffic run. Own DBs mm_accept_166_1791809970201 иmm_accept_166_1791810266923/ограниченные roles/test containers удалены; отсутствие проверено каталогами/docker. Production secrets не копировались. [SQL](ux-ui/acceptance/traffic-history-sql-2026-10-12.json). + +[Source448 backend+521 frontend](ux-ui/acceptance/traffic-history-source-2026-10-12.json)/[1032 compiled](ux-ui/acceptance/traffic-history-compiled-2026-10-12.json) совпали SHA256. Обе production builds прошли (frontend185s Next phase); frontend ux-history178 staged with old backend ux-rtt177 [interop](ux-ui/acceptance/traffic-history-interop-2026-10-12.json), затем backend ux-history178/version1.0.0-test.history178. [Runtime](ux-ui/acceptance/traffic-history-runtime-2026-10-12.json) healthy/running/verified imageIds; в.env менялись только tags, rollback ux-rtt177/ux-ping173 без копии секретного env. Migration0011 подтверждена main PG. Новый frontend следует перезагрузить: прежняя вкладка не получает новый truth reader автоматически. Additive storage не гарантирует truthful interpretation старым UI. + +[Native read-only](ux-ui/acceptance/traffic-history-native-2026-10-12.json): оба CHR194.226.121.150/104.165.244.7, GET identity/interface200, по2 interfaces, counters valid;4 native GET, no routerConfigWrites/password changes/messages. Это не loss/speed/WAN proof. [Live smoke1440/320+3 APIs](ux-ui/acceptance/traffic-history-deployed-2026-10-12.json) подтверждает actual provenance/UI/overflow/pageerrors0; error/legacy branches — отдельно fixtures/ownSQL. Main DB read-only только для native checker; schema migration и штатный scheduler writes были частью deploy. + +Следующий179 — inspect/fix live monitor-traffic empty/partial/malformed rates (текущий parseMonitorTraffic заменяет их нулями), per-interface fallback и disabled-after-stream-start polling. Не объявлять этот путь принятым по history guards. Peer collection completeness, long sparse intervals, interface recreation/identity, snapshot after restore, scale/multiple instances и external integrations остаются условиями12.5. Локальный коммит без push. + ## 12.5.177 — SQL-регрессия составного RTT и обновление стенда (2026-10-12) Ping ownSQL173 расширен двумя controls: summary53ms900us сохраняется как RTT54ms/loss0; raw53bad без подтверждённых counts/loss сохраняется как unknown/null/error. Остальные8 controls сохранены:401/empty/manual/actual100%/zero/API/legacy exact provenance/disabled no outbound. Node22/PG18.6 полный test:probes прошёл24383ms,10 SQL cases без skip. Тесты используют настоящий MikrotikClient к локальному HTTP simulator и действующий collector/PG observations; физический RouterOS malformed response не создавался. Native replay2 выполнен локально176; в удалённом test image evidence file отсутствует и replay явно skipped, не выдан за повторную physical проверку. [SQL](ux-ui/acceptance/ping-duration-sql-2026-10-12.json). diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index 2628ea73..c8a38b11 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,8 +1,8 @@ # Оставшаяся внешняя приёмка — 2026-10-12 -Это актуальная матрица после 12.5.177. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.178. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-rtt177`, frontend `ux-ping173`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-history178`, frontend `ux-history178`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -18,7 +18,7 @@ | Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята | | Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты | | Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, native IPv6 ULA flow pipeline passed145 (IPv4 transport), public IPv6 WAN/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения | -| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; monitoring history172/ownSQL26/compiled1020/deployed passed; Ping known/error173/ownHTTP-SQL8/85uniqueE2E/native4/4/deployed passed; traffic confidence174 baseline reproduced/local guard passed; owned SQL9/9358ms/deploy175/1024compiled/live smoke passed; legacy history/provenance open; compound RTT176 local/native replay2 passed, SQL10/deploy177/1024compiled/live smoke passed; traffic history178 audit next; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | +| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; monitoring history172/ownSQL26/compiled1020/deployed passed; Ping known/error173/ownHTTP-SQL8/85uniqueE2E/native4/4/deployed passed; traffic confidence174 baseline reproduced/local guard passed; owned SQL9/9358ms/deploy175/1024compiled/live smoke passed; legacy history/provenance open; compound RTT176 local/native replay2 passed, SQL10/deploy177/1024compiled/live smoke passed; history178/legacy provenance/current TTL/manual truth/SQL9+14/35unique E2E/native GET/deployed passed; live incomplete/disable-stream179 next; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение. @@ -932,3 +932,33 @@ Ping ownSQL173 расширен двумя controls: summary53ms900us сохра [Live UI1440/320](ux-ui/acceptance/ping-duration-deployed-2026-10-12.json) прошёл: uptime overview200, unknown filter, no overflow/pageerrors. Actual production probes catalog пустой — эта smoke проверка не изображает main compound/error sample; они подтверждены отдельной ownSQL. Новых native Ping/config/password/Telegram операций не выполнялось. Пароли CHR не менялись. Исправления Ping173/176 и traffic174–175 теперь совместно доступны на тестовом backend; UI layout не менялся. Следующий178 — read-only audit traffic history freshness при failed collection и legacy invalid-counter provenance; не объявлять старые неоднозначные строки исправленными по новому producer guard. Source unconfirmed/missing diagnostics, summary loss/count consistency, scale/multi-backend, физические WAN и внешние интеграции остаются условиями12.5. Журнал/план синхронизированы; локальный коммит без push. Новых блоков до следующего окна не начинать. + +## 12.5.178 — достоверная история трафика, ручной сбор и свежесть UI (2026-10-12) + +Подтверждены source gaps: исторические deltas могли выдавать старую скорость как текущую после failed collection; manual collect-now игнорировал snapshot failures и публиковал success. Добавлена migration0011/traffic_collection_state (одна актуальная запись на server): pending до HTTP, ok/error после записи, lastAttemptAt/lastSucceededAt и конфигурационный updatedAt fingerprint. Pending/error не стирают successful history. Completion обновляет только свою lastAttemptAt; pending не переписывает более новую попытку. Это ограничивает гонки записей, но не является распределённым lock/полной приёмкой нескольких backend. + +Freshness reader read-only: enabled device/source, совпадение config fingerprint, state.ok и exact latest sample/success/attempt, TTL3periods (min5s), future tolerance1period. Error/pending/stale/disabled/config change/unknown маскируют current RX/TX; historical observed masks сохраняются. API серверов, клиентов и привязок получает observations/provenance; interface inventory больше не использует stored-rate fallback и не смешивает WG peer rows с interface counters. Source facts дополнительно учитывают новую attempt barrier, поэтому manual error/pending не оставляет старое traffic alarm активным. Для устройств без новой state пока читается прежняя scheduler provenance; первая новая попытка создаёт authoritative barrier. Не считать этот переход доказательством точности каждого старого snapshot. + +В flags зарезервирован bit4 countersKnown, выставляется только новым strict collector. Старые/imported rows не переписаны и не объявлены достоверными; previous wave отбирает known counters. Исторический builder не строит deltas между legacy/unknown counters. Учтённые bytes суммируются по подтверждённым соседним дельтам; reset не добавляет произвольный новый counter как объём. Это lower-bound accounted volume, не утверждение полного покрытия периода. В UI подпись «Учтено RX/TX по замерам»; неизвестные скорости/пики/объёмы — —, chart gaps — null. Genuine measured0 сохраняется. Сортировка держит unknown после measured values. Dashboard existing decoder продолжает использовать masks. + +Manual collect-now теперь через runSchedulerJobNow/общую запись результата и execution lock. Busy409; fatal/partial failures502 c failures/succeeded и сохранённым scheduler error; confirmed success200. Scheduler traffic ok=false теперь execution limitation, а genuine0 — success; valid offline/down других collectors не изменены. Ошибка setup больше не оставляет collecting=true навсегда: getSettingsRow находится внутри try/finally. Existing event/journal-unknown semantics сохранены. + +Frontend projectTrafficObservations валидирует metadata/метрики, маскирует legacy numeric-only DTO, допускает RX/TX histories разной длины при корректных собственных masks. local10s clock переводит expired DTO в stale без новых сетевых запросов. Видны причина failed collection, последняя попытка и successful collection (не выдаётся за подтверждение measured rate). Offline object не скрывает сохранившуюся историю. ReUI Frame/installed Alert/KpiStatGrid/chart-23 adaptation и theme/base-nova сохранены; новые blocks/vendor components не устанавливались. + +| Сценарий | Эталон и схема | Артефакт | +| --- | --- | --- | +| Ошибка опроса | Существующая ReUI Alert → current — → сохранённая RX/TX история | [ПК1440](ux-ui/screens/traffic-history-error-1440-2026-10-12.png), [телефон320](ux-ui/screens/traffic-history-error-320-2026-10-12.png), API-fixture | +| Genuine0 → TTL | Подтверждённая0 → expiry → —, история остаётся | E2E clock/control; без clock-based refetch | +| Legacy counters | Нет происхождения → unknown → новая baseline → measured delta | Owned SQL/HTTP collector path | + +Backend test:traffic-rate прошёл: existing formulas, producer body/in-memory boundaries, strict counter/rate guards, freshness/merge. Frontend projection/dashboard unit tests, traffic flags, scheduler11, application-source TypeScript/backend production build/ESLint passed (unused obsolete ifaceNow helper удалён). Первоначальная freshness unit ошиблась default-param undefined, исправлена на прямой missing-state вызов; backend type adapter учитывает PG Date/string. Scheduler regression assertion разделена: traffic command failure является ошибкой исполнения, не device offline observation. + +35 unique E2E eventually passed: initial31=27pass/4fail выявили лишнее требование равной RX/TX длины и положительные fixtures без explicit observations; malformed negative gap fixture заменена правильным null. Delta14=13pass/1fail выявил shorthand-json fixture без metadata; исправлено, delta1 passed. Остальные изменённые positive fixtures21 passed29.5s. Новые4 cases error1440/320/history retained even offline, true0/TTL/no refetch, numeric legacy unknown прошли. Screenshot-only scoped repeats2×2 для видимого warning/history внутри main scroller не считаются новыми сценариями; финальные изображения просмотрены. Existing8 user GRE/VXLAN dirty screenshots не тронуты. Full UI1260/backend24 не повторялись без необходимости. + +Owned PG18.6/Node22: test:traffic-rate15709ms,9 counter/source +14 history controls (legacy/first/current/error/old-alarm barrier/users/interfaces/manual failure/success/stale/pending/config/disabled). Real MikrotikClient к loopback HTTP simulator, real SQL + Fastify GET/POST; не physical CHR fault. Common test:alert-engine20075ms:26 source/history SQL/GRE8/actual stability. Второй runner имел старую command label в report; corrected по actual command/harness и asserts before public recording, профиль не подменён повторным traffic run. Own DBs mm_accept_166_1791809970201 иmm_accept_166_1791810266923/ограниченные roles/test containers удалены; отсутствие проверено каталогами/docker. Production secrets не копировались. [SQL](ux-ui/acceptance/traffic-history-sql-2026-10-12.json). + +[Source448 backend+521 frontend](ux-ui/acceptance/traffic-history-source-2026-10-12.json)/[1032 compiled](ux-ui/acceptance/traffic-history-compiled-2026-10-12.json) совпали SHA256. Обе production builds прошли (frontend185s Next phase); frontend ux-history178 staged with old backend ux-rtt177 [interop](ux-ui/acceptance/traffic-history-interop-2026-10-12.json), затем backend ux-history178/version1.0.0-test.history178. [Runtime](ux-ui/acceptance/traffic-history-runtime-2026-10-12.json) healthy/running/verified imageIds; в.env менялись только tags, rollback ux-rtt177/ux-ping173 без копии секретного env. Migration0011 подтверждена main PG. Новый frontend следует перезагрузить: прежняя вкладка не получает новый truth reader автоматически. Additive storage не гарантирует truthful interpretation старым UI. + +[Native read-only](ux-ui/acceptance/traffic-history-native-2026-10-12.json): оба CHR194.226.121.150/104.165.244.7, GET identity/interface200, по2 interfaces, counters valid;4 native GET, no routerConfigWrites/password changes/messages. Это не loss/speed/WAN proof. [Live smoke1440/320+3 APIs](ux-ui/acceptance/traffic-history-deployed-2026-10-12.json) подтверждает actual provenance/UI/overflow/pageerrors0; error/legacy branches — отдельно fixtures/ownSQL. Main DB read-only только для native checker; schema migration и штатный scheduler writes были частью deploy. + +Следующий179 — inspect/fix live monitor-traffic empty/partial/malformed rates (текущий parseMonitorTraffic заменяет их нулями), per-interface fallback и disabled-after-stream-start polling. Не объявлять этот путь принятым по history guards. Peer collection completeness, long sparse intervals, interface recreation/identity, snapshot after restore, scale/multiple instances и external integrations остаются условиями12.5. Локальный коммит без push. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 4a286f68..1c1007d1 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-12: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Актуальное продолжение 2026-10-12: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -3322,3 +3322,33 @@ Ping ownSQL173 расширен двумя controls: summary53ms900us сохра [Live UI1440/320](ux-ui/acceptance/ping-duration-deployed-2026-10-12.json) прошёл: uptime overview200, unknown filter, no overflow/pageerrors. Actual production probes catalog пустой — эта smoke проверка не изображает main compound/error sample; они подтверждены отдельной ownSQL. Новых native Ping/config/password/Telegram операций не выполнялось. Пароли CHR не менялись. Исправления Ping173/176 и traffic174–175 теперь совместно доступны на тестовом backend; UI layout не менялся. Следующий178 — read-only audit traffic history freshness при failed collection и legacy invalid-counter provenance; не объявлять старые неоднозначные строки исправленными по новому producer guard. Source unconfirmed/missing diagnostics, summary loss/count consistency, scale/multi-backend, физические WAN и внешние интеграции остаются условиями12.5. Журнал/план синхронизированы; локальный коммит без push. Новых блоков до следующего окна не начинать. + +## 12.5.178 — достоверная история трафика, ручной сбор и свежесть UI (2026-10-12) + +Подтверждены source gaps: исторические deltas могли выдавать старую скорость как текущую после failed collection; manual collect-now игнорировал snapshot failures и публиковал success. Добавлена migration0011/traffic_collection_state (одна актуальная запись на server): pending до HTTP, ok/error после записи, lastAttemptAt/lastSucceededAt и конфигурационный updatedAt fingerprint. Pending/error не стирают successful history. Completion обновляет только свою lastAttemptAt; pending не переписывает более новую попытку. Это ограничивает гонки записей, но не является распределённым lock/полной приёмкой нескольких backend. + +Freshness reader read-only: enabled device/source, совпадение config fingerprint, state.ok и exact latest sample/success/attempt, TTL3periods (min5s), future tolerance1period. Error/pending/stale/disabled/config change/unknown маскируют current RX/TX; historical observed masks сохраняются. API серверов, клиентов и привязок получает observations/provenance; interface inventory больше не использует stored-rate fallback и не смешивает WG peer rows с interface counters. Source facts дополнительно учитывают новую attempt barrier, поэтому manual error/pending не оставляет старое traffic alarm активным. Для устройств без новой state пока читается прежняя scheduler provenance; первая новая попытка создаёт authoritative barrier. Не считать этот переход доказательством точности каждого старого snapshot. + +В flags зарезервирован bit4 countersKnown, выставляется только новым strict collector. Старые/imported rows не переписаны и не объявлены достоверными; previous wave отбирает known counters. Исторический builder не строит deltas между legacy/unknown counters. Учтённые bytes суммируются по подтверждённым соседним дельтам; reset не добавляет произвольный новый counter как объём. Это lower-bound accounted volume, не утверждение полного покрытия периода. В UI подпись «Учтено RX/TX по замерам»; неизвестные скорости/пики/объёмы — —, chart gaps — null. Genuine measured0 сохраняется. Сортировка держит unknown после measured values. Dashboard existing decoder продолжает использовать masks. + +Manual collect-now теперь через runSchedulerJobNow/общую запись результата и execution lock. Busy409; fatal/partial failures502 c failures/succeeded и сохранённым scheduler error; confirmed success200. Scheduler traffic ok=false теперь execution limitation, а genuine0 — success; valid offline/down других collectors не изменены. Ошибка setup больше не оставляет collecting=true навсегда: getSettingsRow находится внутри try/finally. Existing event/journal-unknown semantics сохранены. + +Frontend projectTrafficObservations валидирует metadata/метрики, маскирует legacy numeric-only DTO, допускает RX/TX histories разной длины при корректных собственных masks. local10s clock переводит expired DTO в stale без новых сетевых запросов. Видны причина failed collection, последняя попытка и successful collection (не выдаётся за подтверждение measured rate). Offline object не скрывает сохранившуюся историю. ReUI Frame/installed Alert/KpiStatGrid/chart-23 adaptation и theme/base-nova сохранены; новые blocks/vendor components не устанавливались. + +| Сценарий | Эталон и схема | Артефакт | +| --- | --- | --- | +| Ошибка опроса | Существующая ReUI Alert → current — → сохранённая RX/TX история | [ПК1440](ux-ui/screens/traffic-history-error-1440-2026-10-12.png), [телефон320](ux-ui/screens/traffic-history-error-320-2026-10-12.png), API-fixture | +| Genuine0 → TTL | Подтверждённая0 → expiry → —, история остаётся | E2E clock/control; без clock-based refetch | +| Legacy counters | Нет происхождения → unknown → новая baseline → measured delta | Owned SQL/HTTP collector path | + +Backend test:traffic-rate прошёл: existing formulas, producer body/in-memory boundaries, strict counter/rate guards, freshness/merge. Frontend projection/dashboard unit tests, traffic flags, scheduler11, application-source TypeScript/backend production build/ESLint passed (unused obsolete ifaceNow helper удалён). Первоначальная freshness unit ошиблась default-param undefined, исправлена на прямой missing-state вызов; backend type adapter учитывает PG Date/string. Scheduler regression assertion разделена: traffic command failure является ошибкой исполнения, не device offline observation. + +35 unique E2E eventually passed: initial31=27pass/4fail выявили лишнее требование равной RX/TX длины и положительные fixtures без explicit observations; malformed negative gap fixture заменена правильным null. Delta14=13pass/1fail выявил shorthand-json fixture без metadata; исправлено, delta1 passed. Остальные изменённые positive fixtures21 passed29.5s. Новые4 cases error1440/320/history retained even offline, true0/TTL/no refetch, numeric legacy unknown прошли. Screenshot-only scoped repeats2×2 для видимого warning/history внутри main scroller не считаются новыми сценариями; финальные изображения просмотрены. Existing8 user GRE/VXLAN dirty screenshots не тронуты. Full UI1260/backend24 не повторялись без необходимости. + +Owned PG18.6/Node22: test:traffic-rate15709ms,9 counter/source +14 history controls (legacy/first/current/error/old-alarm barrier/users/interfaces/manual failure/success/stale/pending/config/disabled). Real MikrotikClient к loopback HTTP simulator, real SQL + Fastify GET/POST; не physical CHR fault. Common test:alert-engine20075ms:26 source/history SQL/GRE8/actual stability. Второй runner имел старую command label в report; corrected по actual command/harness и asserts before public recording, профиль не подменён повторным traffic run. Own DBs mm_accept_166_1791809970201 иmm_accept_166_1791810266923/ограниченные roles/test containers удалены; отсутствие проверено каталогами/docker. Production secrets не копировались. [SQL](ux-ui/acceptance/traffic-history-sql-2026-10-12.json). + +[Source448 backend+521 frontend](ux-ui/acceptance/traffic-history-source-2026-10-12.json)/[1032 compiled](ux-ui/acceptance/traffic-history-compiled-2026-10-12.json) совпали SHA256. Обе production builds прошли (frontend185s Next phase); frontend ux-history178 staged with old backend ux-rtt177 [interop](ux-ui/acceptance/traffic-history-interop-2026-10-12.json), затем backend ux-history178/version1.0.0-test.history178. [Runtime](ux-ui/acceptance/traffic-history-runtime-2026-10-12.json) healthy/running/verified imageIds; в.env менялись только tags, rollback ux-rtt177/ux-ping173 без копии секретного env. Migration0011 подтверждена main PG. Новый frontend следует перезагрузить: прежняя вкладка не получает новый truth reader автоматически. Additive storage не гарантирует truthful interpretation старым UI. + +[Native read-only](ux-ui/acceptance/traffic-history-native-2026-10-12.json): оба CHR194.226.121.150/104.165.244.7, GET identity/interface200, по2 interfaces, counters valid;4 native GET, no routerConfigWrites/password changes/messages. Это не loss/speed/WAN proof. [Live smoke1440/320+3 APIs](ux-ui/acceptance/traffic-history-deployed-2026-10-12.json) подтверждает actual provenance/UI/overflow/pageerrors0; error/legacy branches — отдельно fixtures/ownSQL. Main DB read-only только для native checker; schema migration и штатный scheduler writes были частью deploy. + +Следующий179 — inspect/fix live monitor-traffic empty/partial/malformed rates (текущий parseMonitorTraffic заменяет их нулями), per-interface fallback и disabled-after-stream-start polling. Не объявлять этот путь принятым по history guards. Peer collection completeness, long sparse intervals, interface recreation/identity, snapshot after restore, scale/multiple instances и external integrations остаются условиями12.5. Локальный коммит без push. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 2d3e1267..1a9e2d36 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Сервер161.104.46.222: завершён12.5.177 — parser176 проверен real HTTP client→owned PG→GET observations:10 SQL cases/test:probes24383ms, compound54/garbage unknown; ownDB/role/container удалены. Backend ux-rtt177 healthy/frontend ux-ping173,1024 compiled parity/2live UI passed. Следующий178 — аудит failed traffic attempts/history freshness и legacy counters;12.5 открыта. +Сервер161.104.46.222: завершён12.5.178 — traffic history/current freshness и legacy counter provenance, manual truth/journal, masks/TTL UI серверов/клиентов/привязок. OwnSQL9+14/15709ms иsource26+GRE8/20075ms,35 unique E2E eventually passed,build/types/ESLint/1032compiled parity. Both ux-history178 healthy/running, migration0011 applied/2native CHR GET/live UI2 passed; ownDBs/roles/containers removed. Следующий179 — live monitor-traffic complete payload и disable-after-stream-start guard;12.5 открыта. | Параметр | Проверенное состояние | | --- | --- | @@ -14,7 +14,7 @@ | PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed | | Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data | | Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска | -| Образы приложения | mmapp-test/backend:ux-rtt177 и mmapp-test/frontend:ux-ping173; запущены | +| Образы приложения | mmapp-test/backend:ux-history178 и mmapp-test/frontend:ux-history178; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -1748,3 +1748,33 @@ Ping ownSQL173 расширен двумя controls: summary53ms900us сохра [Live UI1440/320](ux-ui/acceptance/ping-duration-deployed-2026-10-12.json) прошёл: uptime overview200, unknown filter, no overflow/pageerrors. Actual production probes catalog пустой — эта smoke проверка не изображает main compound/error sample; они подтверждены отдельной ownSQL. Новых native Ping/config/password/Telegram операций не выполнялось. Пароли CHR не менялись. Исправления Ping173/176 и traffic174–175 теперь совместно доступны на тестовом backend; UI layout не менялся. Следующий178 — read-only audit traffic history freshness при failed collection и legacy invalid-counter provenance; не объявлять старые неоднозначные строки исправленными по новому producer guard. Source unconfirmed/missing diagnostics, summary loss/count consistency, scale/multi-backend, физические WAN и внешние интеграции остаются условиями12.5. Журнал/план синхронизированы; локальный коммит без push. Новых блоков до следующего окна не начинать. + +## 12.5.178 — достоверная история трафика, ручной сбор и свежесть UI (2026-10-12) + +Подтверждены source gaps: исторические deltas могли выдавать старую скорость как текущую после failed collection; manual collect-now игнорировал snapshot failures и публиковал success. Добавлена migration0011/traffic_collection_state (одна актуальная запись на server): pending до HTTP, ok/error после записи, lastAttemptAt/lastSucceededAt и конфигурационный updatedAt fingerprint. Pending/error не стирают successful history. Completion обновляет только свою lastAttemptAt; pending не переписывает более новую попытку. Это ограничивает гонки записей, но не является распределённым lock/полной приёмкой нескольких backend. + +Freshness reader read-only: enabled device/source, совпадение config fingerprint, state.ok и exact latest sample/success/attempt, TTL3periods (min5s), future tolerance1period. Error/pending/stale/disabled/config change/unknown маскируют current RX/TX; historical observed masks сохраняются. API серверов, клиентов и привязок получает observations/provenance; interface inventory больше не использует stored-rate fallback и не смешивает WG peer rows с interface counters. Source facts дополнительно учитывают новую attempt barrier, поэтому manual error/pending не оставляет старое traffic alarm активным. Для устройств без новой state пока читается прежняя scheduler provenance; первая новая попытка создаёт authoritative barrier. Не считать этот переход доказательством точности каждого старого snapshot. + +В flags зарезервирован bit4 countersKnown, выставляется только новым strict collector. Старые/imported rows не переписаны и не объявлены достоверными; previous wave отбирает known counters. Исторический builder не строит deltas между legacy/unknown counters. Учтённые bytes суммируются по подтверждённым соседним дельтам; reset не добавляет произвольный новый counter как объём. Это lower-bound accounted volume, не утверждение полного покрытия периода. В UI подпись «Учтено RX/TX по замерам»; неизвестные скорости/пики/объёмы — —, chart gaps — null. Genuine measured0 сохраняется. Сортировка держит unknown после measured values. Dashboard existing decoder продолжает использовать masks. + +Manual collect-now теперь через runSchedulerJobNow/общую запись результата и execution lock. Busy409; fatal/partial failures502 c failures/succeeded и сохранённым scheduler error; confirmed success200. Scheduler traffic ok=false теперь execution limitation, а genuine0 — success; valid offline/down других collectors не изменены. Ошибка setup больше не оставляет collecting=true навсегда: getSettingsRow находится внутри try/finally. Existing event/journal-unknown semantics сохранены. + +Frontend projectTrafficObservations валидирует metadata/метрики, маскирует legacy numeric-only DTO, допускает RX/TX histories разной длины при корректных собственных masks. local10s clock переводит expired DTO в stale без новых сетевых запросов. Видны причина failed collection, последняя попытка и successful collection (не выдаётся за подтверждение measured rate). Offline object не скрывает сохранившуюся историю. ReUI Frame/installed Alert/KpiStatGrid/chart-23 adaptation и theme/base-nova сохранены; новые blocks/vendor components не устанавливались. + +| Сценарий | Эталон и схема | Артефакт | +| --- | --- | --- | +| Ошибка опроса | Существующая ReUI Alert → current — → сохранённая RX/TX история | [ПК1440](ux-ui/screens/traffic-history-error-1440-2026-10-12.png), [телефон320](ux-ui/screens/traffic-history-error-320-2026-10-12.png), API-fixture | +| Genuine0 → TTL | Подтверждённая0 → expiry → —, история остаётся | E2E clock/control; без clock-based refetch | +| Legacy counters | Нет происхождения → unknown → новая baseline → measured delta | Owned SQL/HTTP collector path | + +Backend test:traffic-rate прошёл: existing formulas, producer body/in-memory boundaries, strict counter/rate guards, freshness/merge. Frontend projection/dashboard unit tests, traffic flags, scheduler11, application-source TypeScript/backend production build/ESLint passed (unused obsolete ifaceNow helper удалён). Первоначальная freshness unit ошиблась default-param undefined, исправлена на прямой missing-state вызов; backend type adapter учитывает PG Date/string. Scheduler regression assertion разделена: traffic command failure является ошибкой исполнения, не device offline observation. + +35 unique E2E eventually passed: initial31=27pass/4fail выявили лишнее требование равной RX/TX длины и положительные fixtures без explicit observations; malformed negative gap fixture заменена правильным null. Delta14=13pass/1fail выявил shorthand-json fixture без metadata; исправлено, delta1 passed. Остальные изменённые positive fixtures21 passed29.5s. Новые4 cases error1440/320/history retained even offline, true0/TTL/no refetch, numeric legacy unknown прошли. Screenshot-only scoped repeats2×2 для видимого warning/history внутри main scroller не считаются новыми сценариями; финальные изображения просмотрены. Existing8 user GRE/VXLAN dirty screenshots не тронуты. Full UI1260/backend24 не повторялись без необходимости. + +Owned PG18.6/Node22: test:traffic-rate15709ms,9 counter/source +14 history controls (legacy/first/current/error/old-alarm barrier/users/interfaces/manual failure/success/stale/pending/config/disabled). Real MikrotikClient к loopback HTTP simulator, real SQL + Fastify GET/POST; не physical CHR fault. Common test:alert-engine20075ms:26 source/history SQL/GRE8/actual stability. Второй runner имел старую command label в report; corrected по actual command/harness и asserts before public recording, профиль не подменён повторным traffic run. Own DBs mm_accept_166_1791809970201 иmm_accept_166_1791810266923/ограниченные roles/test containers удалены; отсутствие проверено каталогами/docker. Production secrets не копировались. [SQL](ux-ui/acceptance/traffic-history-sql-2026-10-12.json). + +[Source448 backend+521 frontend](ux-ui/acceptance/traffic-history-source-2026-10-12.json)/[1032 compiled](ux-ui/acceptance/traffic-history-compiled-2026-10-12.json) совпали SHA256. Обе production builds прошли (frontend185s Next phase); frontend ux-history178 staged with old backend ux-rtt177 [interop](ux-ui/acceptance/traffic-history-interop-2026-10-12.json), затем backend ux-history178/version1.0.0-test.history178. [Runtime](ux-ui/acceptance/traffic-history-runtime-2026-10-12.json) healthy/running/verified imageIds; в.env менялись только tags, rollback ux-rtt177/ux-ping173 без копии секретного env. Migration0011 подтверждена main PG. Новый frontend следует перезагрузить: прежняя вкладка не получает новый truth reader автоматически. Additive storage не гарантирует truthful interpretation старым UI. + +[Native read-only](ux-ui/acceptance/traffic-history-native-2026-10-12.json): оба CHR194.226.121.150/104.165.244.7, GET identity/interface200, по2 interfaces, counters valid;4 native GET, no routerConfigWrites/password changes/messages. Это не loss/speed/WAN proof. [Live smoke1440/320+3 APIs](ux-ui/acceptance/traffic-history-deployed-2026-10-12.json) подтверждает actual provenance/UI/overflow/pageerrors0; error/legacy branches — отдельно fixtures/ownSQL. Main DB read-only только для native checker; schema migration и штатный scheduler writes были частью deploy. + +Следующий179 — inspect/fix live monitor-traffic empty/partial/malformed rates (текущий parseMonitorTraffic заменяет их нулями), per-interface fallback и disabled-after-stream-start polling. Не объявлять этот путь принятым по history guards. Peer collection completeness, long sparse intervals, interface recreation/identity, snapshot after restore, scale/multiple instances и external integrations остаются условиями12.5. Локальный коммит без push. diff --git a/docs/ux-ui/acceptance/traffic-history-compiled-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-compiled-2026-10-12.json new file mode 100644 index 00000000..0f2b14ec --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-compiled-2026-10-12.json @@ -0,0 +1,1042 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "backendVersion": "1.0.0-test.history178", + "normalization": "CRLF to LF", + "matched": [ + "backend/dist/services/traffic-collection-state.js", + "backend/dist/services/traffic-collection-state.js.map", + "backend/dist/services/traffic-collection-state.d.ts", + "backend/dist/services/traffic-collection-state.d.ts.map", + "backend/dist/services/traffic-freshness.js", + "backend/dist/services/traffic-freshness.js.map", + "backend/dist/services/traffic-freshness.d.ts", + "backend/dist/services/traffic-freshness.d.ts.map", + "backend/dist/config.js", + "backend/dist/config.js.map", + "backend/dist/config.d.ts", + "backend/dist/config.d.ts.map", + "backend/dist/constants/ping-probe.js", + "backend/dist/constants/ping-probe.js.map", + "backend/dist/constants/ping-probe.d.ts", + "backend/dist/constants/ping-probe.d.ts.map", + "backend/dist/db/bootstrap.js", + "backend/dist/db/bootstrap.js.map", + "backend/dist/db/bootstrap.d.ts", + "backend/dist/db/bootstrap.d.ts.map", + "backend/dist/db/index.js", + "backend/dist/db/index.js.map", + "backend/dist/db/index.d.ts", + "backend/dist/db/index.d.ts.map", + "backend/dist/db/json.js", + "backend/dist/db/json.js.map", + "backend/dist/db/json.d.ts", + "backend/dist/db/json.d.ts.map", + "backend/dist/db/migrate.js", + "backend/dist/db/migrate.js.map", + "backend/dist/db/migrate.d.ts", + "backend/dist/db/migrate.d.ts.map", + "backend/dist/db/operation-lock.js", + "backend/dist/db/operation-lock.js.map", + "backend/dist/db/operation-lock.d.ts", + "backend/dist/db/operation-lock.d.ts.map", + "backend/dist/db/partitions.js", + "backend/dist/db/partitions.js.map", + "backend/dist/db/partitions.d.ts", + "backend/dist/db/partitions.d.ts.map", + "backend/dist/db/schema.js", + "backend/dist/db/schema.js.map", + "backend/dist/db/schema.d.ts", + "backend/dist/db/schema.d.ts.map", + "backend/dist/db/sql-bind.js", + "backend/dist/db/sql-bind.js.map", + "backend/dist/db/sql-bind.d.ts", + "backend/dist/db/sql-bind.d.ts.map", + "backend/dist/db/sqlite-import.js", + "backend/dist/db/sqlite-import.js.map", + "backend/dist/db/sqlite-import.d.ts", + "backend/dist/db/sqlite-import.d.ts.map", + "backend/dist/db/traffic-flags.js", + "backend/dist/db/traffic-flags.js.map", + "backend/dist/db/traffic-flags.d.ts", + "backend/dist/db/traffic-flags.d.ts.map", + "backend/dist/index.js", + "backend/dist/index.js.map", + "backend/dist/index.d.ts", + "backend/dist/index.d.ts.map", + "backend/dist/lib/permissions.js", + "backend/dist/lib/permissions.js.map", + "backend/dist/lib/permissions.d.ts", + "backend/dist/lib/permissions.d.ts.map", + "backend/dist/managed-markers.js", + "backend/dist/managed-markers.js.map", + "backend/dist/managed-markers.d.ts", + "backend/dist/managed-markers.d.ts.map", + "backend/dist/modules/events/repository/events-repository.js", + "backend/dist/modules/events/repository/events-repository.js.map", + "backend/dist/modules/events/repository/events-repository.d.ts", + "backend/dist/modules/events/repository/events-repository.d.ts.map", + "backend/dist/modules/events/service/events-service.js", + "backend/dist/modules/events/service/events-service.js.map", + "backend/dist/modules/events/service/events-service.d.ts", + "backend/dist/modules/events/service/events-service.d.ts.map", + "backend/dist/modules/servers/mapper/servers-mapper.js", + "backend/dist/modules/servers/mapper/servers-mapper.js.map", + "backend/dist/modules/servers/mapper/servers-mapper.d.ts", + "backend/dist/modules/servers/mapper/servers-mapper.d.ts.map", + "backend/dist/modules/servers/repository/servers-repository.js", + "backend/dist/modules/servers/repository/servers-repository.js.map", + "backend/dist/modules/servers/repository/servers-repository.d.ts", + "backend/dist/modules/servers/repository/servers-repository.d.ts.map", + "backend/dist/modules/servers/service/servers-service.js", + "backend/dist/modules/servers/service/servers-service.js.map", + "backend/dist/modules/servers/service/servers-service.d.ts", + "backend/dist/modules/servers/service/servers-service.d.ts.map", + "backend/dist/modules/users/iface-type.js", + "backend/dist/modules/users/iface-type.js.map", + "backend/dist/modules/users/iface-type.d.ts", + "backend/dist/modules/users/iface-type.d.ts.map", + "backend/dist/modules/users/interface-inventory.js", + "backend/dist/modules/users/interface-inventory.js.map", + "backend/dist/modules/users/interface-inventory.d.ts", + "backend/dist/modules/users/interface-inventory.d.ts.map", + "backend/dist/modules/users/peer-bind.js", + "backend/dist/modules/users/peer-bind.js.map", + "backend/dist/modules/users/peer-bind.d.ts", + "backend/dist/modules/users/peer-bind.d.ts.map", + "backend/dist/modules/users/repository/users-repository.js", + "backend/dist/modules/users/repository/users-repository.js.map", + "backend/dist/modules/users/repository/users-repository.d.ts", + "backend/dist/modules/users/repository/users-repository.d.ts.map", + "backend/dist/modules/users/service/users-service.js", + "backend/dist/modules/users/service/users-service.js.map", + "backend/dist/modules/users/service/users-service.d.ts", + "backend/dist/modules/users/service/users-service.d.ts.map", + "backend/dist/plugins/auth.js", + "backend/dist/plugins/auth.js.map", + "backend/dist/plugins/auth.d.ts", + "backend/dist/plugins/auth.d.ts.map", + "backend/dist/plugins/database-request-gate.js", + "backend/dist/plugins/database-request-gate.js.map", + "backend/dist/plugins/database-request-gate.d.ts", + "backend/dist/plugins/database-request-gate.d.ts.map", + "backend/dist/routes/alerts.js", + "backend/dist/routes/alerts.js.map", + "backend/dist/routes/alerts.d.ts", + "backend/dist/routes/alerts.d.ts.map", + "backend/dist/routes/backups.js", + "backend/dist/routes/backups.js.map", + "backend/dist/routes/backups.d.ts", + "backend/dist/routes/backups.d.ts.map", + "backend/dist/routes/bgp.js", + "backend/dist/routes/bgp.js.map", + "backend/dist/routes/bgp.d.ts", + "backend/dist/routes/bgp.d.ts.map", + "backend/dist/routes/certificates.js", + "backend/dist/routes/certificates.js.map", + "backend/dist/routes/certificates.d.ts", + "backend/dist/routes/certificates.d.ts.map", + "backend/dist/routes/containers.js", + "backend/dist/routes/containers.js.map", + "backend/dist/routes/containers.d.ts", + "backend/dist/routes/containers.d.ts.map", + "backend/dist/routes/events.js", + "backend/dist/routes/events.js.map", + "backend/dist/routes/events.d.ts", + "backend/dist/routes/events.d.ts.map", + "backend/dist/routes/evobgp-catalog.js", + "backend/dist/routes/evobgp-catalog.js.map", + "backend/dist/routes/evobgp-catalog.d.ts", + "backend/dist/routes/evobgp-catalog.d.ts.map", + "backend/dist/routes/evobgp-settings.js", + "backend/dist/routes/evobgp-settings.js.map", + "backend/dist/routes/evobgp-settings.d.ts", + "backend/dist/routes/evobgp-settings.d.ts.map", + "backend/dist/routes/evobgp.js", + "backend/dist/routes/evobgp.js.map", + "backend/dist/routes/evobgp.d.ts", + "backend/dist/routes/evobgp.d.ts.map", + "backend/dist/routes/exec.js", + "backend/dist/routes/exec.js.map", + "backend/dist/routes/exec.d.ts", + "backend/dist/routes/exec.d.ts.map", + "backend/dist/routes/filters.js", + "backend/dist/routes/filters.js.map", + "backend/dist/routes/filters.d.ts", + "backend/dist/routes/filters.d.ts.map", + "backend/dist/routes/firewall.js", + "backend/dist/routes/firewall.js.map", + "backend/dist/routes/firewall.d.ts", + "backend/dist/routes/firewall.d.ts.map", + "backend/dist/routes/geoip.js", + "backend/dist/routes/geoip.js.map", + "backend/dist/routes/geoip.d.ts", + "backend/dist/routes/geoip.d.ts.map", + "backend/dist/routes/gre.js", + "backend/dist/routes/gre.js.map", + "backend/dist/routes/gre.d.ts", + "backend/dist/routes/gre.d.ts.map", + "backend/dist/routes/internet-path.js", + "backend/dist/routes/internet-path.js.map", + "backend/dist/routes/internet-path.d.ts", + "backend/dist/routes/internet-path.d.ts.map", + "backend/dist/routes/ipsec.js", + "backend/dist/routes/ipsec.js.map", + "backend/dist/routes/ipsec.d.ts", + "backend/dist/routes/ipsec.d.ts.map", + "backend/dist/routes/network.js", + "backend/dist/routes/network.js.map", + "backend/dist/routes/network.d.ts", + "backend/dist/routes/network.d.ts.map", + "backend/dist/routes/ospf.js", + "backend/dist/routes/ospf.js.map", + "backend/dist/routes/ospf.d.ts", + "backend/dist/routes/ospf.d.ts.map", + "backend/dist/routes/ping-probes-write.js", + "backend/dist/routes/ping-probes-write.js.map", + "backend/dist/routes/ping-probes-write.d.ts", + "backend/dist/routes/ping-probes-write.d.ts.map", + "backend/dist/routes/probes.js", + "backend/dist/routes/probes.js.map", + "backend/dist/routes/probes.d.ts", + "backend/dist/routes/probes.d.ts.map", + "backend/dist/routes/recursive-routes.js", + "backend/dist/routes/recursive-routes.js.map", + "backend/dist/routes/recursive-routes.d.ts", + "backend/dist/routes/recursive-routes.d.ts.map", + "backend/dist/routes/scheduler.js", + "backend/dist/routes/scheduler.js.map", + "backend/dist/routes/scheduler.d.ts", + "backend/dist/routes/scheduler.d.ts.map", + "backend/dist/routes/servers-api-ping.js", + "backend/dist/routes/servers-api-ping.js.map", + "backend/dist/routes/servers-api-ping.d.ts", + "backend/dist/routes/servers-api-ping.d.ts.map", + "backend/dist/routes/servers.js", + "backend/dist/routes/servers.js.map", + "backend/dist/routes/servers.d.ts", + "backend/dist/routes/servers.d.ts.map", + "backend/dist/routes/sidebar-counts.js", + "backend/dist/routes/sidebar-counts.js.map", + "backend/dist/routes/sidebar-counts.d.ts", + "backend/dist/routes/sidebar-counts.d.ts.map", + "backend/dist/routes/speed-probes-write.js", + "backend/dist/routes/speed-probes-write.js.map", + "backend/dist/routes/speed-probes-write.d.ts", + "backend/dist/routes/speed-probes-write.d.ts.map", + "backend/dist/routes/speed-test.js", + "backend/dist/routes/speed-test.js.map", + "backend/dist/routes/speed-test.d.ts", + "backend/dist/routes/speed-test.d.ts.map", + "backend/dist/routes/statistics.js", + "backend/dist/routes/statistics.js.map", + "backend/dist/routes/statistics.d.ts", + "backend/dist/routes/statistics.d.ts.map", + "backend/dist/routes/system-database.js", + "backend/dist/routes/system-database.js.map", + "backend/dist/routes/system-database.d.ts", + "backend/dist/routes/system-database.d.ts.map", + "backend/dist/routes/traffic-flow-purge.js", + "backend/dist/routes/traffic-flow-purge.js.map", + "backend/dist/routes/traffic-flow-purge.d.ts", + "backend/dist/routes/traffic-flow-purge.d.ts.map", + "backend/dist/routes/traffic-flow-settings.js", + "backend/dist/routes/traffic-flow-settings.js.map", + "backend/dist/routes/traffic-flow-settings.d.ts", + "backend/dist/routes/traffic-flow-settings.d.ts.map", + "backend/dist/routes/traffic-flow.js", + "backend/dist/routes/traffic-flow.js.map", + "backend/dist/routes/traffic-flow.d.ts", + "backend/dist/routes/traffic-flow.d.ts.map", + "backend/dist/routes/traffic.js", + "backend/dist/routes/traffic.js.map", + "backend/dist/routes/traffic.d.ts", + "backend/dist/routes/traffic.d.ts.map", + "backend/dist/routes/uptime.js", + "backend/dist/routes/uptime.js.map", + "backend/dist/routes/uptime.d.ts", + "backend/dist/routes/uptime.d.ts.map", + "backend/dist/routes/users.js", + "backend/dist/routes/users.js.map", + "backend/dist/routes/users.d.ts", + "backend/dist/routes/users.d.ts.map", + "backend/dist/routes/vxlan.js", + "backend/dist/routes/vxlan.js.map", + "backend/dist/routes/vxlan.d.ts", + "backend/dist/routes/vxlan.d.ts.map", + "backend/dist/routes/wireguard.js", + "backend/dist/routes/wireguard.js.map", + "backend/dist/routes/wireguard.d.ts", + "backend/dist/routes/wireguard.d.ts.map", + "backend/dist/scripts/migrate-sqlite-to-pg.js", + "backend/dist/scripts/migrate-sqlite-to-pg.js.map", + "backend/dist/scripts/migrate-sqlite-to-pg.d.ts", + "backend/dist/scripts/migrate-sqlite-to-pg.d.ts.map", + "backend/dist/scripts/rebuild-flow-facts.js", + "backend/dist/scripts/rebuild-flow-facts.js.map", + "backend/dist/scripts/rebuild-flow-facts.d.ts", + "backend/dist/scripts/rebuild-flow-facts.d.ts.map", + "backend/dist/services/acme-cloudflare.js", + "backend/dist/services/acme-cloudflare.js.map", + "backend/dist/services/acme-cloudflare.d.ts", + "backend/dist/services/acme-cloudflare.d.ts.map", + "backend/dist/services/alert-collector-hooks.js", + "backend/dist/services/alert-collector-hooks.js.map", + "backend/dist/services/alert-collector-hooks.d.ts", + "backend/dist/services/alert-collector-hooks.d.ts.map", + "backend/dist/services/alert-engine/bgp-signals.js", + "backend/dist/services/alert-engine/bgp-signals.js.map", + "backend/dist/services/alert-engine/bgp-signals.d.ts", + "backend/dist/services/alert-engine/bgp-signals.d.ts.map", + "backend/dist/services/alert-engine/collector-context-reader.js", + "backend/dist/services/alert-engine/collector-context-reader.js.map", + "backend/dist/services/alert-engine/collector-context-reader.d.ts", + "backend/dist/services/alert-engine/collector-context-reader.d.ts.map", + "backend/dist/services/alert-engine/collector-source-signals.js", + "backend/dist/services/alert-engine/collector-source-signals.js.map", + "backend/dist/services/alert-engine/collector-source-signals.d.ts", + "backend/dist/services/alert-engine/collector-source-signals.d.ts.map", + "backend/dist/services/alert-engine/confirm-stability.js", + "backend/dist/services/alert-engine/confirm-stability.js.map", + "backend/dist/services/alert-engine/confirm-stability.d.ts", + "backend/dist/services/alert-engine/confirm-stability.d.ts.map", + "backend/dist/services/alert-engine/cooldown.js", + "backend/dist/services/alert-engine/cooldown.js.map", + "backend/dist/services/alert-engine/cooldown.d.ts", + "backend/dist/services/alert-engine/cooldown.d.ts.map", + "backend/dist/services/alert-engine/decision-engine.js", + "backend/dist/services/alert-engine/decision-engine.js.map", + "backend/dist/services/alert-engine/decision-engine.d.ts", + "backend/dist/services/alert-engine/decision-engine.d.ts.map", + "backend/dist/services/alert-engine/evaluate-rule.js", + "backend/dist/services/alert-engine/evaluate-rule.js.map", + "backend/dist/services/alert-engine/evaluate-rule.d.ts", + "backend/dist/services/alert-engine/evaluate-rule.d.ts.map", + "backend/dist/services/alert-engine/gre-signals.js", + "backend/dist/services/alert-engine/gre-signals.js.map", + "backend/dist/services/alert-engine/gre-signals.d.ts", + "backend/dist/services/alert-engine/gre-signals.d.ts.map", + "backend/dist/services/alert-engine/groups.js", + "backend/dist/services/alert-engine/groups.js.map", + "backend/dist/services/alert-engine/groups.d.ts", + "backend/dist/services/alert-engine/groups.d.ts.map", + "backend/dist/services/alert-engine/monitoring-history.js", + "backend/dist/services/alert-engine/monitoring-history.js.map", + "backend/dist/services/alert-engine/monitoring-history.d.ts", + "backend/dist/services/alert-engine/monitoring-history.d.ts.map", + "backend/dist/services/alert-engine/outbox-execution.js", + "backend/dist/services/alert-engine/outbox-execution.js.map", + "backend/dist/services/alert-engine/outbox-execution.d.ts", + "backend/dist/services/alert-engine/outbox-execution.d.ts.map", + "backend/dist/services/alert-engine/outbox.js", + "backend/dist/services/alert-engine/outbox.js.map", + "backend/dist/services/alert-engine/outbox.d.ts", + "backend/dist/services/alert-engine/outbox.d.ts.map", + "backend/dist/services/alert-engine/prev-live-store.js", + "backend/dist/services/alert-engine/prev-live-store.js.map", + "backend/dist/services/alert-engine/prev-live-store.d.ts", + "backend/dist/services/alert-engine/prev-live-store.d.ts.map", + "backend/dist/services/alert-engine/rule-matcher.js", + "backend/dist/services/alert-engine/rule-matcher.js.map", + "backend/dist/services/alert-engine/rule-matcher.d.ts", + "backend/dist/services/alert-engine/rule-matcher.d.ts.map", + "backend/dist/services/alert-engine/run-once.js", + "backend/dist/services/alert-engine/run-once.js.map", + "backend/dist/services/alert-engine/run-once.d.ts", + "backend/dist/services/alert-engine/run-once.d.ts.map", + "backend/dist/services/alert-engine/server-signal-identity.js", + "backend/dist/services/alert-engine/server-signal-identity.js.map", + "backend/dist/services/alert-engine/server-signal-identity.d.ts", + "backend/dist/services/alert-engine/server-signal-identity.d.ts.map", + "backend/dist/services/alert-engine/signal-ingestor.js", + "backend/dist/services/alert-engine/signal-ingestor.js.map", + "backend/dist/services/alert-engine/signal-ingestor.d.ts", + "backend/dist/services/alert-engine/signal-ingestor.d.ts.map", + "backend/dist/services/alert-engine/signals-from-collectors.js", + "backend/dist/services/alert-engine/signals-from-collectors.js.map", + "backend/dist/services/alert-engine/signals-from-collectors.d.ts", + "backend/dist/services/alert-engine/signals-from-collectors.d.ts.map", + "backend/dist/services/alert-engine/snapshot.js", + "backend/dist/services/alert-engine/snapshot.js.map", + "backend/dist/services/alert-engine/snapshot.d.ts", + "backend/dist/services/alert-engine/snapshot.d.ts.map", + "backend/dist/services/alert-engine/source-watermark.js", + "backend/dist/services/alert-engine/source-watermark.js.map", + "backend/dist/services/alert-engine/source-watermark.d.ts", + "backend/dist/services/alert-engine/source-watermark.d.ts.map", + "backend/dist/services/alert-engine/state-machine.js", + "backend/dist/services/alert-engine/state-machine.js.map", + "backend/dist/services/alert-engine/state-machine.d.ts", + "backend/dist/services/alert-engine/state-machine.d.ts.map", + "backend/dist/services/alert-engine/target-issues.js", + "backend/dist/services/alert-engine/target-issues.js.map", + "backend/dist/services/alert-engine/target-issues.d.ts", + "backend/dist/services/alert-engine/target-issues.d.ts.map", + "backend/dist/services/alert-engine/telegram-emoji.js", + "backend/dist/services/alert-engine/telegram-emoji.js.map", + "backend/dist/services/alert-engine/telegram-emoji.d.ts", + "backend/dist/services/alert-engine/telegram-emoji.d.ts.map", + "backend/dist/services/alert-engine/types.js", + "backend/dist/services/alert-engine/types.js.map", + "backend/dist/services/alert-engine/types.d.ts", + "backend/dist/services/alert-engine/types.d.ts.map", + "backend/dist/services/alerts-config-execution.js", + "backend/dist/services/alerts-config-execution.js.map", + "backend/dist/services/alerts-config-execution.d.ts", + "backend/dist/services/alerts-config-execution.d.ts.map", + "backend/dist/services/alerts-outbox-overview.js", + "backend/dist/services/alerts-outbox-overview.js.map", + "backend/dist/services/alerts-outbox-overview.d.ts", + "backend/dist/services/alerts-outbox-overview.d.ts.map", + "backend/dist/services/alerts-outbox-retry.js", + "backend/dist/services/alerts-outbox-retry.js.map", + "backend/dist/services/alerts-outbox-retry.d.ts", + "backend/dist/services/alerts-outbox-retry.d.ts.map", + "backend/dist/services/alerts-overview.js", + "backend/dist/services/alerts-overview.js.map", + "backend/dist/services/alerts-overview.d.ts", + "backend/dist/services/alerts-overview.d.ts.map", + "backend/dist/services/alerts-revision.js", + "backend/dist/services/alerts-revision.js.map", + "backend/dist/services/alerts-revision.d.ts", + "backend/dist/services/alerts-revision.d.ts.map", + "backend/dist/services/alerts-service.js", + "backend/dist/services/alerts-service.js.map", + "backend/dist/services/alerts-service.d.ts", + "backend/dist/services/alerts-service.d.ts.map", + "backend/dist/services/alerts-telegram-delivery.js", + "backend/dist/services/alerts-telegram-delivery.js.map", + "backend/dist/services/alerts-telegram-delivery.d.ts", + "backend/dist/services/alerts-telegram-delivery.d.ts.map", + "backend/dist/services/alerts-telegram-send.js", + "backend/dist/services/alerts-telegram-send.js.map", + "backend/dist/services/alerts-telegram-send.d.ts", + "backend/dist/services/alerts-telegram-send.d.ts.map", + "backend/dist/services/alerts-telegram-settings.js", + "backend/dist/services/alerts-telegram-settings.js.map", + "backend/dist/services/alerts-telegram-settings.d.ts", + "backend/dist/services/alerts-telegram-settings.d.ts.map", + "backend/dist/services/backup-deletion.js", + "backend/dist/services/backup-deletion.js.map", + "backend/dist/services/backup-deletion.d.ts", + "backend/dist/services/backup-deletion.d.ts.map", + "backend/dist/services/backup-dto.js", + "backend/dist/services/backup-dto.js.map", + "backend/dist/services/backup-dto.d.ts", + "backend/dist/services/backup-dto.d.ts.map", + "backend/dist/services/backup-job-execution.js", + "backend/dist/services/backup-job-execution.js.map", + "backend/dist/services/backup-job-execution.d.ts", + "backend/dist/services/backup-job-execution.d.ts.map", + "backend/dist/services/backup-prune.js", + "backend/dist/services/backup-prune.js.map", + "backend/dist/services/backup-prune.d.ts", + "backend/dist/services/backup-prune.d.ts.map", + "backend/dist/services/backup-restore.js", + "backend/dist/services/backup-restore.js.map", + "backend/dist/services/backup-restore.d.ts", + "backend/dist/services/backup-restore.d.ts.map", + "backend/dist/services/backup-scheduler-collector.js", + "backend/dist/services/backup-scheduler-collector.js.map", + "backend/dist/services/backup-scheduler-collector.d.ts", + "backend/dist/services/backup-scheduler-collector.d.ts.map", + "backend/dist/services/backup-scheduler-run.js", + "backend/dist/services/backup-scheduler-run.js.map", + "backend/dist/services/backup-scheduler-run.d.ts", + "backend/dist/services/backup-scheduler-run.d.ts.map", + "backend/dist/services/backup-service.js", + "backend/dist/services/backup-service.js.map", + "backend/dist/services/backup-service.d.ts", + "backend/dist/services/backup-service.d.ts.map", + "backend/dist/services/backup-storage-update.js", + "backend/dist/services/backup-storage-update.js.map", + "backend/dist/services/backup-storage-update.d.ts", + "backend/dist/services/backup-storage-update.d.ts.map", + "backend/dist/services/bandwidth-test-rates.js", + "backend/dist/services/bandwidth-test-rates.js.map", + "backend/dist/services/bandwidth-test-rates.d.ts", + "backend/dist/services/bandwidth-test-rates.d.ts.map", + "backend/dist/services/bandwidth-test-status.js", + "backend/dist/services/bandwidth-test-status.js.map", + "backend/dist/services/bandwidth-test-status.d.ts", + "backend/dist/services/bandwidth-test-status.d.ts.map", + "backend/dist/services/bgp-catalog-poll.js", + "backend/dist/services/bgp-catalog-poll.js.map", + "backend/dist/services/bgp-catalog-poll.d.ts", + "backend/dist/services/bgp-catalog-poll.d.ts.map", + "backend/dist/services/bgp-parse-sessions.js", + "backend/dist/services/bgp-parse-sessions.js.map", + "backend/dist/services/bgp-parse-sessions.d.ts", + "backend/dist/services/bgp-parse-sessions.d.ts.map", + "backend/dist/services/bgp-peers-live.js", + "backend/dist/services/bgp-peers-live.js.map", + "backend/dist/services/bgp-peers-live.d.ts", + "backend/dist/services/bgp-peers-live.d.ts.map", + "backend/dist/services/btest-node-locks.js", + "backend/dist/services/btest-node-locks.js.map", + "backend/dist/services/btest-node-locks.d.ts", + "backend/dist/services/btest-node-locks.d.ts.map", + "backend/dist/services/certificate-issue-dto.js", + "backend/dist/services/certificate-issue-dto.js.map", + "backend/dist/services/certificate-issue-dto.d.ts", + "backend/dist/services/certificate-issue-dto.d.ts.map", + "backend/dist/services/certificate-issue-execution.js", + "backend/dist/services/certificate-issue-execution.js.map", + "backend/dist/services/certificate-issue-execution.d.ts", + "backend/dist/services/certificate-issue-execution.d.ts.map", + "backend/dist/services/certificate-issue-request.js", + "backend/dist/services/certificate-issue-request.js.map", + "backend/dist/services/certificate-issue-request.d.ts", + "backend/dist/services/certificate-issue-request.d.ts.map", + "backend/dist/services/certificate-issue-runner.js", + "backend/dist/services/certificate-issue-runner.js.map", + "backend/dist/services/certificate-issue-runner.d.ts", + "backend/dist/services/certificate-issue-runner.d.ts.map", + "backend/dist/services/certificate-parse.js", + "backend/dist/services/certificate-parse.js.map", + "backend/dist/services/certificate-parse.d.ts", + "backend/dist/services/certificate-parse.d.ts.map", + "backend/dist/services/certificate-renew-collector.js", + "backend/dist/services/certificate-renew-collector.js.map", + "backend/dist/services/certificate-renew-collector.d.ts", + "backend/dist/services/certificate-renew-collector.d.ts.map", + "backend/dist/services/certificates-service.js", + "backend/dist/services/certificates-service.js.map", + "backend/dist/services/certificates-service.d.ts", + "backend/dist/services/certificates-service.d.ts.map", + "backend/dist/services/certificates-state.js", + "backend/dist/services/certificates-state.js.map", + "backend/dist/services/certificates-state.d.ts", + "backend/dist/services/certificates-state.d.ts.map", + "backend/dist/services/complete-count.js", + "backend/dist/services/complete-count.js.map", + "backend/dist/services/complete-count.d.ts", + "backend/dist/services/complete-count.d.ts.map", + "backend/dist/services/config-apply-plan.js", + "backend/dist/services/config-apply-plan.js.map", + "backend/dist/services/config-apply-plan.d.ts", + "backend/dist/services/config-apply-plan.d.ts.map", + "backend/dist/services/config-revisions.js", + "backend/dist/services/config-revisions.js.map", + "backend/dist/services/config-revisions.d.ts", + "backend/dist/services/config-revisions.d.ts.map", + "backend/dist/services/containers-live.js", + "backend/dist/services/containers-live.js.map", + "backend/dist/services/containers-live.d.ts", + "backend/dist/services/containers-live.d.ts.map", + "backend/dist/services/containers-state.js", + "backend/dist/services/containers-state.js.map", + "backend/dist/services/containers-state.d.ts", + "backend/dist/services/containers-state.d.ts.map", + "backend/dist/services/database-background-work.js", + "backend/dist/services/database-background-work.js.map", + "backend/dist/services/database-background-work.d.ts", + "backend/dist/services/database-background-work.d.ts.map", + "backend/dist/services/database-request-gate.js", + "backend/dist/services/database-request-gate.js.map", + "backend/dist/services/database-request-gate.d.ts", + "backend/dist/services/database-request-gate.d.ts.map", + "backend/dist/services/entity-snapshots.js", + "backend/dist/services/entity-snapshots.js.map", + "backend/dist/services/entity-snapshots.d.ts", + "backend/dist/services/entity-snapshots.d.ts.map", + "backend/dist/services/evobgp-catalog.js", + "backend/dist/services/evobgp-catalog.js.map", + "backend/dist/services/evobgp-catalog.d.ts", + "backend/dist/services/evobgp-catalog.d.ts.map", + "backend/dist/services/firewall-live.js", + "backend/dist/services/firewall-live.js.map", + "backend/dist/services/firewall-live.d.ts", + "backend/dist/services/firewall-live.d.ts.map", + "backend/dist/services/firewall-poll.js", + "backend/dist/services/firewall-poll.js.map", + "backend/dist/services/firewall-poll.d.ts", + "backend/dist/services/firewall-poll.d.ts.map", + "backend/dist/services/geoip-settings.js", + "backend/dist/services/geoip-settings.js.map", + "backend/dist/services/geoip-settings.d.ts", + "backend/dist/services/geoip-settings.d.ts.map", + "backend/dist/services/geoip-update-collector.js", + "backend/dist/services/geoip-update-collector.js.map", + "backend/dist/services/geoip-update-collector.d.ts", + "backend/dist/services/geoip-update-collector.d.ts.map", + "backend/dist/services/gre-bgp-snapshot-collector.js", + "backend/dist/services/gre-bgp-snapshot-collector.js.map", + "backend/dist/services/gre-bgp-snapshot-collector.d.ts", + "backend/dist/services/gre-bgp-snapshot-collector.d.ts.map", + "backend/dist/services/gre-bgp-snapshot-history.js", + "backend/dist/services/gre-bgp-snapshot-history.js.map", + "backend/dist/services/gre-bgp-snapshot-history.d.ts", + "backend/dist/services/gre-bgp-snapshot-history.d.ts.map", + "backend/dist/services/gre-bgp-timing.js", + "backend/dist/services/gre-bgp-timing.js.map", + "backend/dist/services/gre-bgp-timing.d.ts", + "backend/dist/services/gre-bgp-timing.d.ts.map", + "backend/dist/services/gre-catalog-poll.js", + "backend/dist/services/gre-catalog-poll.js.map", + "backend/dist/services/gre-catalog-poll.d.ts", + "backend/dist/services/gre-catalog-poll.d.ts.map", + "backend/dist/services/gre-comment.js", + "backend/dist/services/gre-comment.js.map", + "backend/dist/services/gre-comment.d.ts", + "backend/dist/services/gre-comment.d.ts.map", + "backend/dist/services/gre-live.js", + "backend/dist/services/gre-live.js.map", + "backend/dist/services/gre-live.d.ts", + "backend/dist/services/gre-live.d.ts.map", + "backend/dist/services/gre-tunnels-live.js", + "backend/dist/services/gre-tunnels-live.js.map", + "backend/dist/services/gre-tunnels-live.d.ts", + "backend/dist/services/gre-tunnels-live.d.ts.map", + "backend/dist/services/internet-path-collector.js", + "backend/dist/services/internet-path-collector.js.map", + "backend/dist/services/internet-path-collector.d.ts", + "backend/dist/services/internet-path-collector.d.ts.map", + "backend/dist/services/internet-path-snapshot.js", + "backend/dist/services/internet-path-snapshot.js.map", + "backend/dist/services/internet-path-snapshot.d.ts", + "backend/dist/services/internet-path-snapshot.d.ts.map", + "backend/dist/services/ipsec-ca.js", + "backend/dist/services/ipsec-ca.js.map", + "backend/dist/services/ipsec-ca.d.ts", + "backend/dist/services/ipsec-ca.d.ts.map", + "backend/dist/services/ipsec-certificate-dn.js", + "backend/dist/services/ipsec-certificate-dn.js.map", + "backend/dist/services/ipsec-certificate-dn.d.ts", + "backend/dist/services/ipsec-certificate-dn.d.ts.map", + "backend/dist/services/ipsec-cleanup.js", + "backend/dist/services/ipsec-cleanup.js.map", + "backend/dist/services/ipsec-cleanup.d.ts", + "backend/dist/services/ipsec-cleanup.d.ts.map", + "backend/dist/services/ipsec-config.js", + "backend/dist/services/ipsec-config.js.map", + "backend/dist/services/ipsec-config.d.ts", + "backend/dist/services/ipsec-config.d.ts.map", + "backend/dist/services/ipsec-live.js", + "backend/dist/services/ipsec-live.js.map", + "backend/dist/services/ipsec-live.d.ts", + "backend/dist/services/ipsec-live.d.ts.map", + "backend/dist/services/ipsec-operation.js", + "backend/dist/services/ipsec-operation.js.map", + "backend/dist/services/ipsec-operation.d.ts", + "backend/dist/services/ipsec-operation.d.ts.map", + "backend/dist/services/ipsec-ros.js", + "backend/dist/services/ipsec-ros.js.map", + "backend/dist/services/ipsec-ros.d.ts", + "backend/dist/services/ipsec-ros.d.ts.map", + "backend/dist/services/mikrotik.js", + "backend/dist/services/mikrotik.js.map", + "backend/dist/services/mikrotik.d.ts", + "backend/dist/services/mikrotik.d.ts.map", + "backend/dist/services/monitoring-server-catalog.js", + "backend/dist/services/monitoring-server-catalog.js.map", + "backend/dist/services/monitoring-server-catalog.d.ts", + "backend/dist/services/monitoring-server-catalog.d.ts.map", + "backend/dist/services/ospf-catalog-poll.js", + "backend/dist/services/ospf-catalog-poll.js.map", + "backend/dist/services/ospf-catalog-poll.d.ts", + "backend/dist/services/ospf-catalog-poll.d.ts.map", + "backend/dist/services/ospf-effective-disabled.js", + "backend/dist/services/ospf-effective-disabled.js.map", + "backend/dist/services/ospf-effective-disabled.d.ts", + "backend/dist/services/ospf-effective-disabled.d.ts.map", + "backend/dist/services/ospf-observed-values.js", + "backend/dist/services/ospf-observed-values.js.map", + "backend/dist/services/ospf-observed-values.d.ts", + "backend/dist/services/ospf-observed-values.d.ts.map", + "backend/dist/services/ospf-optimizer-identity.js", + "backend/dist/services/ospf-optimizer-identity.js.map", + "backend/dist/services/ospf-optimizer-identity.d.ts", + "backend/dist/services/ospf-optimizer-identity.d.ts.map", + "backend/dist/services/ospf-optimizer-metrics.js", + "backend/dist/services/ospf-optimizer-metrics.js.map", + "backend/dist/services/ospf-optimizer-metrics.d.ts", + "backend/dist/services/ospf-optimizer-metrics.d.ts.map", + "backend/dist/services/ospf-optimizer-plan.js", + "backend/dist/services/ospf-optimizer-plan.js.map", + "backend/dist/services/ospf-optimizer-plan.d.ts", + "backend/dist/services/ospf-optimizer-plan.d.ts.map", + "backend/dist/services/ospf-route-parse.js", + "backend/dist/services/ospf-route-parse.js.map", + "backend/dist/services/ospf-route-parse.d.ts", + "backend/dist/services/ospf-route-parse.d.ts.map", + "backend/dist/services/ping-probe-replacement.js", + "backend/dist/services/ping-probe-replacement.js.map", + "backend/dist/services/ping-probe-replacement.d.ts", + "backend/dist/services/ping-probe-replacement.d.ts.map", + "backend/dist/services/poller.js", + "backend/dist/services/poller.js.map", + "backend/dist/services/poller.d.ts", + "backend/dist/services/poller.d.ts.map", + "backend/dist/services/recursive-apply.js", + "backend/dist/services/recursive-apply.js.map", + "backend/dist/services/recursive-apply.d.ts", + "backend/dist/services/recursive-apply.d.ts.map", + "backend/dist/services/recursive-route-catalog.js", + "backend/dist/services/recursive-route-catalog.js.map", + "backend/dist/services/recursive-route-catalog.d.ts", + "backend/dist/services/recursive-route-catalog.d.ts.map", + "backend/dist/services/recursive-route-validation.js", + "backend/dist/services/recursive-route-validation.js.map", + "backend/dist/services/recursive-route-validation.d.ts", + "backend/dist/services/recursive-route-validation.d.ts.map", + "backend/dist/services/ros-metric-parse.js", + "backend/dist/services/ros-metric-parse.js.map", + "backend/dist/services/ros-metric-parse.d.ts", + "backend/dist/services/ros-metric-parse.d.ts.map", + "backend/dist/services/ros-ops.js", + "backend/dist/services/ros-ops.js.map", + "backend/dist/services/ros-ops.d.ts", + "backend/dist/services/ros-ops.d.ts.map", + "backend/dist/services/s3-backup-client.js", + "backend/dist/services/s3-backup-client.js.map", + "backend/dist/services/s3-backup-client.d.ts", + "backend/dist/services/s3-backup-client.d.ts.map", + "backend/dist/services/scheduler-recorded-run.js", + "backend/dist/services/scheduler-recorded-run.js.map", + "backend/dist/services/scheduler-recorded-run.d.ts", + "backend/dist/services/scheduler-recorded-run.d.ts.map", + "backend/dist/services/scheduler-running.js", + "backend/dist/services/scheduler-running.js.map", + "backend/dist/services/scheduler-running.d.ts", + "backend/dist/services/scheduler-running.d.ts.map", + "backend/dist/services/scheduler.js", + "backend/dist/services/scheduler.js.map", + "backend/dist/services/scheduler.d.ts", + "backend/dist/services/scheduler.d.ts.map", + "backend/dist/services/serial-operation.js", + "backend/dist/services/serial-operation.js.map", + "backend/dist/services/serial-operation.d.ts", + "backend/dist/services/serial-operation.d.ts.map", + "backend/dist/services/servers-rest-ping-collector.js", + "backend/dist/services/servers-rest-ping-collector.js.map", + "backend/dist/services/servers-rest-ping-collector.d.ts", + "backend/dist/services/servers-rest-ping-collector.d.ts.map", + "backend/dist/services/speed-history-dto.js", + "backend/dist/services/speed-history-dto.js.map", + "backend/dist/services/speed-history-dto.d.ts", + "backend/dist/services/speed-history-dto.d.ts.map", + "backend/dist/services/speed-probe-dto.js", + "backend/dist/services/speed-probe-dto.js.map", + "backend/dist/services/speed-probe-dto.d.ts", + "backend/dist/services/speed-probe-dto.d.ts.map", + "backend/dist/services/speed-probe-replacement.js", + "backend/dist/services/speed-probe-replacement.js.map", + "backend/dist/services/speed-probe-replacement.d.ts", + "backend/dist/services/speed-probe-replacement.d.ts.map", + "backend/dist/services/speed-run-error-message.js", + "backend/dist/services/speed-run-error-message.js.map", + "backend/dist/services/speed-run-error-message.d.ts", + "backend/dist/services/speed-run-error-message.d.ts.map", + "backend/dist/services/speed-run-persistence.js", + "backend/dist/services/speed-run-persistence.js.map", + "backend/dist/services/speed-run-persistence.d.ts", + "backend/dist/services/speed-run-persistence.d.ts.map", + "backend/dist/services/statistics-aggregate.js", + "backend/dist/services/statistics-aggregate.js.map", + "backend/dist/services/statistics-aggregate.d.ts", + "backend/dist/services/statistics-aggregate.d.ts.map", + "backend/dist/services/system-database-backup.js", + "backend/dist/services/system-database-backup.js.map", + "backend/dist/services/system-database-backup.d.ts", + "backend/dist/services/system-database-backup.d.ts.map", + "backend/dist/services/system-database-cache.js", + "backend/dist/services/system-database-cache.js.map", + "backend/dist/services/system-database-cache.d.ts", + "backend/dist/services/system-database-cache.d.ts.map", + "backend/dist/services/system-database-operation.js", + "backend/dist/services/system-database-operation.js.map", + "backend/dist/services/system-database-operation.d.ts", + "backend/dist/services/system-database-operation.d.ts.map", + "backend/dist/services/system-database-partition-restore.js", + "backend/dist/services/system-database-partition-restore.js.map", + "backend/dist/services/system-database-partition-restore.d.ts", + "backend/dist/services/system-database-partition-restore.d.ts.map", + "backend/dist/services/system-database-restore-execution.js", + "backend/dist/services/system-database-restore-execution.js.map", + "backend/dist/services/system-database-restore-execution.d.ts", + "backend/dist/services/system-database-restore-execution.d.ts.map", + "backend/dist/services/traffic-collector.js", + "backend/dist/services/traffic-collector.js.map", + "backend/dist/services/traffic-collector.d.ts", + "backend/dist/services/traffic-collector.d.ts.map", + "backend/dist/services/traffic-flow-analytics.js", + "backend/dist/services/traffic-flow-analytics.js.map", + "backend/dist/services/traffic-flow-analytics.d.ts", + "backend/dist/services/traffic-flow-analytics.d.ts.map", + "backend/dist/services/traffic-flow-apps.js", + "backend/dist/services/traffic-flow-apps.js.map", + "backend/dist/services/traffic-flow-apps.d.ts", + "backend/dist/services/traffic-flow-apps.d.ts.map", + "backend/dist/services/traffic-flow-brands.js", + "backend/dist/services/traffic-flow-brands.js.map", + "backend/dist/services/traffic-flow-brands.d.ts", + "backend/dist/services/traffic-flow-brands.d.ts.map", + "backend/dist/services/traffic-flow-classify.js", + "backend/dist/services/traffic-flow-classify.js.map", + "backend/dist/services/traffic-flow-classify.d.ts", + "backend/dist/services/traffic-flow-classify.d.ts.map", + "backend/dist/services/traffic-flow-collector-ipc.js", + "backend/dist/services/traffic-flow-collector-ipc.js.map", + "backend/dist/services/traffic-flow-collector-ipc.d.ts", + "backend/dist/services/traffic-flow-collector-ipc.d.ts.map", + "backend/dist/services/traffic-flow-collector-worker.js", + "backend/dist/services/traffic-flow-collector-worker.js.map", + "backend/dist/services/traffic-flow-collector-worker.d.ts", + "backend/dist/services/traffic-flow-collector-worker.d.ts.map", + "backend/dist/services/traffic-flow-dedup.js", + "backend/dist/services/traffic-flow-dedup.js.map", + "backend/dist/services/traffic-flow-dedup.d.ts", + "backend/dist/services/traffic-flow-dedup.d.ts.map", + "backend/dist/services/traffic-flow-dest.js", + "backend/dist/services/traffic-flow-dest.js.map", + "backend/dist/services/traffic-flow-dest.d.ts", + "backend/dist/services/traffic-flow-dest.d.ts.map", + "backend/dist/services/traffic-flow-engine.js", + "backend/dist/services/traffic-flow-engine.js.map", + "backend/dist/services/traffic-flow-engine.d.ts", + "backend/dist/services/traffic-flow-engine.d.ts.map", + "backend/dist/services/traffic-flow-facts-filter.js", + "backend/dist/services/traffic-flow-facts-filter.js.map", + "backend/dist/services/traffic-flow-facts-filter.d.ts", + "backend/dist/services/traffic-flow-facts-filter.d.ts.map", + "backend/dist/services/traffic-flow-facts-rebuild.js", + "backend/dist/services/traffic-flow-facts-rebuild.js.map", + "backend/dist/services/traffic-flow-facts-rebuild.d.ts", + "backend/dist/services/traffic-flow-facts-rebuild.d.ts.map", + "backend/dist/services/traffic-flow-facts.js", + "backend/dist/services/traffic-flow-facts.js.map", + "backend/dist/services/traffic-flow-facts.d.ts", + "backend/dist/services/traffic-flow-facts.d.ts.map", + "backend/dist/services/traffic-flow-geoip.js", + "backend/dist/services/traffic-flow-geoip.js.map", + "backend/dist/services/traffic-flow-geoip.d.ts", + "backend/dist/services/traffic-flow-geoip.d.ts.map", + "backend/dist/services/traffic-flow-host-file-builders.js", + "backend/dist/services/traffic-flow-host-file-builders.js.map", + "backend/dist/services/traffic-flow-host-file-builders.d.ts", + "backend/dist/services/traffic-flow-host-file-builders.d.ts.map", + "backend/dist/services/traffic-flow-host-files.js", + "backend/dist/services/traffic-flow-host-files.js.map", + "backend/dist/services/traffic-flow-host-files.d.ts", + "backend/dist/services/traffic-flow-host-files.d.ts.map", + "backend/dist/services/traffic-flow-ifaces.js", + "backend/dist/services/traffic-flow-ifaces.js.map", + "backend/dist/services/traffic-flow-ifaces.d.ts", + "backend/dist/services/traffic-flow-ifaces.d.ts.map", + "backend/dist/services/traffic-flow-ifindex.js", + "backend/dist/services/traffic-flow-ifindex.js.map", + "backend/dist/services/traffic-flow-ifindex.d.ts", + "backend/dist/services/traffic-flow-ifindex.d.ts.map", + "backend/dist/services/traffic-flow-ingest.js", + "backend/dist/services/traffic-flow-ingest.js.map", + "backend/dist/services/traffic-flow-ingest.d.ts", + "backend/dist/services/traffic-flow-ingest.d.ts.map", + "backend/dist/services/traffic-flow-ip.js", + "backend/dist/services/traffic-flow-ip.js.map", + "backend/dist/services/traffic-flow-ip.d.ts", + "backend/dist/services/traffic-flow-ip.d.ts.map", + "backend/dist/services/traffic-flow-key-execution.js", + "backend/dist/services/traffic-flow-key-execution.js.map", + "backend/dist/services/traffic-flow-key-execution.d.ts", + "backend/dist/services/traffic-flow-key-execution.d.ts.map", + "backend/dist/services/traffic-flow-map-exporter.js", + "backend/dist/services/traffic-flow-map-exporter.js.map", + "backend/dist/services/traffic-flow-map-exporter.d.ts", + "backend/dist/services/traffic-flow-map-exporter.d.ts.map", + "backend/dist/services/traffic-flow-map-hops.js", + "backend/dist/services/traffic-flow-map-hops.js.map", + "backend/dist/services/traffic-flow-map-hops.d.ts", + "backend/dist/services/traffic-flow-map-hops.d.ts.map", + "backend/dist/services/traffic-flow-overlay.js", + "backend/dist/services/traffic-flow-overlay.js.map", + "backend/dist/services/traffic-flow-overlay.d.ts", + "backend/dist/services/traffic-flow-overlay.d.ts.map", + "backend/dist/services/traffic-flow-parse.js", + "backend/dist/services/traffic-flow-parse.js.map", + "backend/dist/services/traffic-flow-parse.d.ts", + "backend/dist/services/traffic-flow-parse.d.ts.map", + "backend/dist/services/traffic-flow-planes.js", + "backend/dist/services/traffic-flow-planes.js.map", + "backend/dist/services/traffic-flow-planes.d.ts", + "backend/dist/services/traffic-flow-planes.d.ts.map", + "backend/dist/services/traffic-flow-purge-execution.js", + "backend/dist/services/traffic-flow-purge-execution.js.map", + "backend/dist/services/traffic-flow-purge-execution.d.ts", + "backend/dist/services/traffic-flow-purge-execution.d.ts.map", + "backend/dist/services/traffic-flow-ripe.js", + "backend/dist/services/traffic-flow-ripe.js.map", + "backend/dist/services/traffic-flow-ripe.d.ts", + "backend/dist/services/traffic-flow-ripe.d.ts.map", + "backend/dist/services/traffic-flow-settings.js", + "backend/dist/services/traffic-flow-settings.js.map", + "backend/dist/services/traffic-flow-settings.d.ts", + "backend/dist/services/traffic-flow-settings.d.ts.map", + "backend/dist/services/traffic-flow-topology.js", + "backend/dist/services/traffic-flow-topology.js.map", + "backend/dist/services/traffic-flow-topology.d.ts", + "backend/dist/services/traffic-flow-topology.d.ts.map", + "backend/dist/services/traffic-flow-worker-shutdown.js", + "backend/dist/services/traffic-flow-worker-shutdown.js.map", + "backend/dist/services/traffic-flow-worker-shutdown.d.ts", + "backend/dist/services/traffic-flow-worker-shutdown.d.ts.map", + "backend/dist/services/traffic-observation.js", + "backend/dist/services/traffic-observation.js.map", + "backend/dist/services/traffic-observation.d.ts", + "backend/dist/services/traffic-observation.d.ts.map", + "backend/dist/services/traffic-rate.js", + "backend/dist/services/traffic-rate.js.map", + "backend/dist/services/traffic-rate.d.ts", + "backend/dist/services/traffic-rate.d.ts.map", + "backend/dist/services/traffic-users.js", + "backend/dist/services/traffic-users.js.map", + "backend/dist/services/traffic-users.d.ts", + "backend/dist/services/traffic-users.d.ts.map", + "backend/dist/services/uptime-collector.js", + "backend/dist/services/uptime-collector.js.map", + "backend/dist/services/uptime-collector.d.ts", + "backend/dist/services/uptime-collector.d.ts.map", + "backend/dist/services/uptime-probe-observations.js", + "backend/dist/services/uptime-probe-observations.js.map", + "backend/dist/services/uptime-probe-observations.d.ts", + "backend/dist/services/uptime-probe-observations.d.ts.map", + "backend/dist/services/uptime-speed-service.js", + "backend/dist/services/uptime-speed-service.js.map", + "backend/dist/services/uptime-speed-service.d.ts", + "backend/dist/services/uptime-speed-service.d.ts.map", + "backend/dist/services/vxlan-live.js", + "backend/dist/services/vxlan-live.js.map", + "backend/dist/services/vxlan-live.d.ts", + "backend/dist/services/vxlan-live.d.ts.map", + "backend/dist/services/wg-keys.js", + "backend/dist/services/wg-keys.js.map", + "backend/dist/services/wg-keys.d.ts", + "backend/dist/services/wg-keys.d.ts.map", + "backend/dist/services/wireguard-config.js", + "backend/dist/services/wireguard-config.js.map", + "backend/dist/services/wireguard-config.d.ts", + "backend/dist/services/wireguard-config.d.ts.map", + "backend/dist/services/wireguard-delete.js", + "backend/dist/services/wireguard-delete.js.map", + "backend/dist/services/wireguard-delete.d.ts", + "backend/dist/services/wireguard-delete.d.ts.map", + "backend/dist/services/wireguard-import-apply.js", + "backend/dist/services/wireguard-import-apply.js.map", + "backend/dist/services/wireguard-import-apply.d.ts", + "backend/dist/services/wireguard-import-apply.d.ts.map", + "backend/dist/services/wireguard-import-revision.js", + "backend/dist/services/wireguard-import-revision.js.map", + "backend/dist/services/wireguard-import-revision.d.ts", + "backend/dist/services/wireguard-import-revision.d.ts.map", + "backend/dist/services/wireguard-import.js", + "backend/dist/services/wireguard-import.js.map", + "backend/dist/services/wireguard-import.d.ts", + "backend/dist/services/wireguard-import.d.ts.map", + "backend/dist/services/wireguard-live.js", + "backend/dist/services/wireguard-live.js.map", + "backend/dist/services/wireguard-live.d.ts", + "backend/dist/services/wireguard-live.d.ts.map", + "backend/dist/services/wireguard-observed-values.js", + "backend/dist/services/wireguard-observed-values.js.map", + "backend/dist/services/wireguard-observed-values.d.ts", + "backend/dist/services/wireguard-observed-values.d.ts.map", + "backend/dist/services/wireguard-ros.js", + "backend/dist/services/wireguard-ros.js.map", + "backend/dist/services/wireguard-ros.d.ts", + "backend/dist/services/wireguard-ros.d.ts.map", + "backend/dist/test/pg.js", + "backend/dist/test/pg.js.map", + "backend/dist/test/pg.d.ts", + "backend/dist/test/pg.d.ts.map", + "backend/dist/types/scheduler-run-snapshot.js", + "backend/dist/types/scheduler-run-snapshot.js.map", + "backend/dist/types/scheduler-run-snapshot.d.ts", + "backend/dist/types/scheduler-run-snapshot.d.ts.map", + "backend/dist/types/server.js", + "backend/dist/types/server.js.map", + "backend/dist/types/server.d.ts", + "backend/dist/types/server.d.ts.map", + "backend/dist/utils/abort-signals.js", + "backend/dist/utils/abort-signals.js.map", + "backend/dist/utils/abort-signals.d.ts", + "backend/dist/utils/abort-signals.d.ts.map", + "backend/dist/utils/ros-src-address.js", + "backend/dist/utils/ros-src-address.js.map", + "backend/dist/utils/ros-src-address.d.ts", + "backend/dist/utils/ros-src-address.d.ts.map", + "backend/dist/utils/server-id.js", + "backend/dist/utils/server-id.js.map", + "backend/dist/utils/server-id.d.ts", + "backend/dist/utils/server-id.d.ts.map", + "packages/contracts/dist/alerts.js", + "packages/contracts/dist/alerts.js.map", + "packages/contracts/dist/alerts.d.ts", + "packages/contracts/dist/alerts.d.ts.map", + "packages/contracts/dist/backups.js", + "packages/contracts/dist/backups.js.map", + "packages/contracts/dist/backups.d.ts", + "packages/contracts/dist/backups.d.ts.map", + "packages/contracts/dist/certificates.js", + "packages/contracts/dist/certificates.js.map", + "packages/contracts/dist/certificates.d.ts", + "packages/contracts/dist/certificates.d.ts.map", + "packages/contracts/dist/containers.js", + "packages/contracts/dist/containers.js.map", + "packages/contracts/dist/containers.d.ts", + "packages/contracts/dist/containers.d.ts.map", + "packages/contracts/dist/events.js", + "packages/contracts/dist/events.js.map", + "packages/contracts/dist/events.d.ts", + "packages/contracts/dist/events.d.ts.map", + "packages/contracts/dist/evobgp.js", + "packages/contracts/dist/evobgp.js.map", + "packages/contracts/dist/evobgp.d.ts", + "packages/contracts/dist/evobgp.d.ts.map", + "packages/contracts/dist/geoip.js", + "packages/contracts/dist/geoip.js.map", + "packages/contracts/dist/geoip.d.ts", + "packages/contracts/dist/geoip.d.ts.map", + "packages/contracts/dist/index.js", + "packages/contracts/dist/index.js.map", + "packages/contracts/dist/index.d.ts", + "packages/contracts/dist/index.d.ts.map", + "packages/contracts/dist/ipsec.js", + "packages/contracts/dist/ipsec.js.map", + "packages/contracts/dist/ipsec.d.ts", + "packages/contracts/dist/ipsec.d.ts.map", + "packages/contracts/dist/ospf-optimizer.js", + "packages/contracts/dist/ospf-optimizer.js.map", + "packages/contracts/dist/ospf-optimizer.d.ts", + "packages/contracts/dist/ospf-optimizer.d.ts.map", + "packages/contracts/dist/servers.js", + "packages/contracts/dist/servers.js.map", + "packages/contracts/dist/servers.d.ts", + "packages/contracts/dist/servers.d.ts.map", + "packages/contracts/dist/statistics.js", + "packages/contracts/dist/statistics.js.map", + "packages/contracts/dist/statistics.d.ts", + "packages/contracts/dist/statistics.d.ts.map", + "packages/contracts/dist/traffic-flow.js", + "packages/contracts/dist/traffic-flow.js.map", + "packages/contracts/dist/traffic-flow.d.ts", + "packages/contracts/dist/traffic-flow.d.ts.map", + "packages/contracts/dist/uptime-probes.js", + "packages/contracts/dist/uptime-probes.js.map", + "packages/contracts/dist/uptime-probes.d.ts", + "packages/contracts/dist/uptime-probes.d.ts.map", + "packages/contracts/dist/uptime-speed.js", + "packages/contracts/dist/uptime-speed.js.map", + "packages/contracts/dist/uptime-speed.d.ts", + "packages/contracts/dist/uptime-speed.d.ts.map", + "packages/contracts/dist/users.js", + "packages/contracts/dist/users.js.map", + "packages/contracts/dist/users.d.ts", + "packages/contracts/dist/users.d.ts.map", + "packages/contracts/dist/wireguard.js", + "packages/contracts/dist/wireguard.js.map", + "packages/contracts/dist/wireguard.d.ts", + "packages/contracts/dist/wireguard.d.ts.map" + ], + "missing": [], + "different": [] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/traffic-history-deployed-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-deployed-2026-10-12.json new file mode 100644 index 00000000..a679c081 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-deployed-2026-10-12.json @@ -0,0 +1,42 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "runtime": { + "backend": "ux-history178", + "frontend": "ux-history178" + }, + "apis": [ + { + "path": "/api/traffic/servers", + "status": 200 + }, + { + "path": "/api/traffic/users", + "status": 200 + }, + { + "path": "/api/traffic/bound-interfaces", + "status": 200 + } + ], + "cases": [ + { + "width": 1440, + "trafficServers200": true, + "attemptProvenanceVisible": true, + "pageErrors": 0, + "noOverflow": true + }, + { + "width": 320, + "trafficServers200": true, + "attemptProvenanceVisible": true, + "pageErrors": 0, + "noOverflow": true + } + ], + "bounds": [ + "Read-only smoke; error/stale/legacy branches proven by owned SQL and UI fixtures", + "No manual production scheduler/config/password/message writes" + ] +} diff --git a/docs/ux-ui/acceptance/traffic-history-interop-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-interop-2026-10-12.json new file mode 100644 index 00000000..f49322c2 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-interop-2026-10-12.json @@ -0,0 +1,10 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "newFrontend": "ux-history178", + "oldBackend": "ux-rtt177", + "readerAccepted": true, + "serverCount": 2, + "pageErrors": 0, + "bound": "Additive observed DTO reader compatibility; legacy numeric-only paths verified separately as unknown" +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/traffic-history-native-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-native-2026-10-12.json new file mode 100644 index 00000000..ae4519a4 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-native-2026-10-12.json @@ -0,0 +1,29 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "version": "1.0.0-test.history178", + "readOnlyDatabase": true, + "cases": [ + { + "sourceId": 1, + "target": "194.226.121.150", + "identityRead": true, + "interfaceRead": true, + "interfaceCount": 2, + "countersValid": true + }, + { + "sourceId": 2, + "target": "104.165.244.7", + "identityRead": true, + "interfaceRead": true, + "interfaceCount": 2, + "countersValid": true + } + ], + "dbWrites": 0, + "routerConfigWrites": 0, + "messages": 0, + "migrationApplied": true, + "passed": true +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/traffic-history-runtime-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-runtime-2026-10-12.json new file mode 100644 index 00000000..2f91f3e8 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-runtime-2026-10-12.json @@ -0,0 +1,25 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "secretEnvCopied": false, + "rollbackTags": { + "MMAPP_IMAGE_TAG": "ux-ping173", + "BACKEND_IMAGE_TAG": "ux-rtt177" + }, + "services": { + "backend": { + "image": "mmapp-test/backend:ux-history178", + "imageId": "sha256:da0ff45a8f7e5b2fcfb98b6c17161c7dcb9776e9dadbd0770f35e95cbad89aaf", + "state": "running", + "health": "healthy", + "matchedVerifiedImage": true + }, + "frontend": { + "image": "mmapp-test/frontend:ux-history178", + "imageId": "sha256:320a81a42a84af52fe200c9464d01105811d0d4a952654690471f14824cd1739", + "state": "running", + "health": null, + "matchedVerifiedImage": true + } + } +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/traffic-history-source-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-source-2026-10-12.json new file mode 100644 index 00000000..9e5e2bde --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-source-2026-10-12.json @@ -0,0 +1,10 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "matched": { + "backend": 448, + "frontend": 521 + }, + "different": [], + "missing": [] +} \ No newline at end of file diff --git a/docs/ux-ui/acceptance/traffic-history-sql-2026-10-12.json b/docs/ux-ui/acceptance/traffic-history-sql-2026-10-12.json new file mode 100644 index 00000000..c2256901 --- /dev/null +++ b/docs/ux-ui/acceptance/traffic-history-sql-2026-10-12.json @@ -0,0 +1,49 @@ +{ + "date": "2026-10-12", + "block": "12.5.178", + "passed": true, + "profiles": [ + { + "date": "2026-10-12", + "block": "12.5.178", + "database": "mm_accept_166_1791809970201", + "scope": "ephemeral role and precreated owned database; no copied production credentials", + "tests": [ + { + "command": "npm --prefix backend run test:traffic-rate", + "passed": true, + "durationMs": 15709, + "ownedTrafficSqlCases": 9, + "ownedHistorySqlCases": 14, + "postgresUnavailableSkip": false + } + ], + "scopedRoleVerified": true, + "passed": true, + "databaseRemoved": true, + "roleRemoved": true, + "testContainerRemoved": true + }, + { + "date": "2026-10-12", + "block": "12.5.178", + "database": "mm_accept_166_1791810266923", + "scope": "ephemeral role and precreated owned database; no copied production credentials", + "tests": [ + { + "command": "npm --prefix backend run test:alert-engine", + "passed": true, + "durationMs": 20075, + "ownedSourceSqlCases": 26, + "ownedGreSqlRows": 8, + "postgresUnavailableSkip": false + } + ], + "scopedRoleVerified": true, + "passed": true, + "databaseRemoved": true, + "roleRemoved": true, + "testContainerRemoved": true + } + ] +} \ No newline at end of file diff --git a/docs/ux-ui/screens/traffic-history-error-1440-2026-10-12.png b/docs/ux-ui/screens/traffic-history-error-1440-2026-10-12.png new file mode 100644 index 0000000000000000000000000000000000000000..df86619cede9e6a1309b7bd44ac511a109671386 GIT binary patch literal 132358 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

w`U@>L99Yq4ow1z;R0%Buh~p;-Lv(i?~0 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 literal 0 HcmV?d00001 diff --git a/docs/ux-ui/screens/traffic-history-error-320-2026-10-12.png b/docs/ux-ui/screens/traffic-history-error-320-2026-10-12.png new file mode 100644 index 0000000000000000000000000000000000000000..0c4e28e76cd8b2ab6bb43a375ea2cc088ae29bda GIT binary patch literal 48662 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

b0d=x3ohZSgdKPxbjM%KnbE9H=IZux}tVDO>q z$Y{I9e+89bfuYxX?s>4{v?${^{{gl+u*^_QQBqSgVa;Q4;syC__~v3aZL4AOApcU8 z(ydt6*)<8iVXHoZL#Gfwzd8t4acvYVpzsb=?N$nVb$fBZ`liywM(4~**y65Qg7c;YIsH%w<2CvI;!a$8%gM8&E|;r!s#Ikw+`s6%Y^^wk zwOppw8y`M=iQs=mG!NYaS#i64Wwt-@sSs1d>odP@eC|6;R0*YIXZZeg$T_c#*Nb0Q zL?IA4bTQr%0lNXKWjdCHGR0wTr%PP{m>;5=)O_lY6pA-ex7u6aTzu#S)`sf~CF%>b8ej<7pqhPny(pkx0 zt)mP^ngRKK`useo5>^2}wf4j23#a}Rz-5d|JCACq6QMI`CS*`ah>0Ad2g>F`#EjlT zPoyBo)5bRauEu%2ndleRw=7+dkS$)?r`f}aZ$rb!mZ5I`fs&NUO)ha2ezbQtaugVv z@oiRP$OifceI2Ax_^c%ru;&K{7l)@F*VFGp5EW0=K=f#P>|Hyz;i$;xZ_49;r5kKb zn-Zj_K5TuO+->^qkBafq*wu3zK?ulFwa(3L z7gm8UP4q<(CnM9VI=Z#BigsLdvAR{(<%UF9C`YPWxC5wA$yd|WvzPqWy5IEVjZ=`% zbE>oG>azQG6_#tUYBL9naPfMRqrK1h7?q=&!R0yCkVCZHVR%-ozIWNL1eXQlKAL%Vhkzh(XXmW?ElK2*Pz9E zi!Mb{oG%M;PW#KQUU#O=I?v&Y^HK?cQ_e`YF2ajNK@1}!)5-@Y#3clRh0F15<(mHX zcYg$kh%&uK1m2cTRTacn3f}x}437e_PuTQsDZOKx*=sPJ$)Aw#B!eYcrRp2%tAW#B z@OhA17@~+iyZ@ulA0J&1Q7l9Y(9FLY@J4m}&z5a^}Fx@?@{(9!UHn$J#>6t##V0P-OFrt9p z@T+%J_#PfDt~0#qcw}F3wRcC#MBkbc7iWX)9zKj56tO$HidZDb=^B~_ww2jlyq;ND_ujoBFU5R&8DiO=XPNKUUSv3*Lw-*m zhJ0^xh+9A58e;n-C-Kld--c)gMZMh@T~t}FME-qV$K#j7InlGMs%ldGqntGFg}vkZ z2Sqn_PLbIStEc8H%4Xlzl(hqg{>Y*l=KwwA6#mDewOBA*4BdWOV>;=9~%#1 zo69=viw73b4Vyw&=)PF*SGR9*443TO+&IpdIiq8FQyXhZxm2X|chhU&+YupRd5RKq z^yuu9-}O(&`D{;z`B_^9KnV=l{Uw=onuY{H-IfL$X&h&B`=%~WbrR8<-pNe{zKSk$ ziH^(lpB>xpUVk8K&n+%Fo)H>JCgAILoJLwNH)L*7S`RxaJR+n|R0sg5rJaeq&(-kyE&APuhQv+7 z&yO@QlL%#+(E0|}p9B^0`3H|THIgPy1P$W7o^UV5-^XnijhW*iDG)3czzG%eIzUND zsr_;wCN8M=|7?Q~SoLWrEGQ@tsTLNd+i10KXl!&^D2*$QvDqeVJ@BJjNx7)WPRhjHU;hF5WjLZ` zC07?9T$oyOSX)b5%N|=)S36vL@K2{h=c<7S@Z+rwWukPMC${U>s%{bD;s-IYvi3|YZ&;b3os-6KgQ_MdO_t{+aZ~1ruiKM{%_vQ zzptz8&%)*fSALsFd9+h;514T$QA487ZoaH(tn3Vcw46jlW%$ZXbhtmZa2du1Ax8f0 zeBIlQzdS62v=c#9-Ave?xH`&foX-#U4(xyudldC-VzsvK~nr`1%y~UQrOofJf znGDz(#|VDB!+ai^wf%VPONa^Od2U>&uT0=;_6h;yBHaCg1wYcwgSqPqV-A=8KJ;!u ztlf`l3KS#-8(g-Y6fRObJm!D?c%5qD$`Li&zr{EVzdK7o7@ewGiv_mSCamzi20gHW z>886H^+z@!Z~JM8HcMO1kPidLE;&JuRnHjsv8fvHxvK$##Q_bQ*RQdIEYkP*Pp!`h zT&BV84zct{HP)oo+%xx^SkL=wi}RG7=ZjR!3Kw9fy>9TDAtrmcs+fr!$z8Y1pEd>A z+olu~ntyK>8G~qO_)rleiQ%w6dP#Kw0cYh@=dbOkRvbm6I2@G9FhOn>Tbd9eDSN~V z{T7nOI+l9W@Y43{7J*`+8AJwKFOIYZh2DqVI9r|K_CAmGm6Srh*Q@^EHEgp3*deUD zLs>%nP%bi0NL0qw_*TnV1vdU*vU`oyVA%!u$yG|MG03)uH#6a5gl{{?oaC~6f(JZY zOnIRr+S{&i`_`KvqEb?AzZMm&_HOTJ{PP4fGE(`|0<-;mxxzLkkyw-2VzqJ4-BBlA zkl^9 z2(P4_{Qj%sHT=SUzi8Grvy3m_*>}@xp@vgA>$e<<$P%fq9qtr1U1;Wbxa#AjhIA2< zrAW>@aRv`VcJ6ap4#dr|k*=;G+RH!V<1ed^hx=SljIRT`Y4+KRb6bQ zI!XcE_A+8R2hZko^7Lub!nN~KOyx@1U)In4@hYnVF5F%{R+cl>*Cr!A=x#sOj6hQS zlLc4^#(lQqL2-2~d38NTWFs?S6$FKyCJ(4U`47+#6lm5kSklKy*Mzk0ylX^uz5WsI z6#=sc?Z^nt1(a6_q=pNT42ENrrxj*oiP*Z!zFAV%i2Vi}hK&kCLeuDG$^mgbC1DJ* zZM(zwz$rmOl%vs}fc0FYZ4%R~%qK4=d6{#COf(_G!{YEYJc`3NgfDJJ-mkU_< z7!IpKAJzsaA!!fu6S=jD+!2&7vdgU{f|xBXf9SZnSl3itobO}|y98emXcC*|r|EnQ z<0k~K$6UFhtn4_ay*kafYgXZ6UX19}Y!}0(lPmdS*S9!eE2jtUM6~oApl>oPNggUD zMt9(s#=BWhw)G>B=GWc=0!HxAVmz@fDE{y~W;9oa?;BmY+1&Ecg!xU%^mFauNXYBn z|2-ZB4Gf9l#Jm>f;8Pstf)^l}WXrcb3ayVKVn#EA_($v9L^?}tO@+ka@?LJ=glvje zY{5U(UA3Jc4{2ky{N1hIfR0(VUO zp9T1tM@dK%U<4b!E-abNTMY18oe3(pxDFSO z_=dN?_t)l{)3$LQ*OBIg?HFlKV-PLmcK4#bIuD=FdwVe6VoEdyNyzdv*UR4>FzN^rh_)*2ad@P?VwM^k(MQ0H}Af>r!(4EaK`6H)T zv7XM@3^20QXy6lcKuwkUy?5Lf>}=ea8WJk{U4&SYnt|h}a&T-7S!a#+N7KG+1CGim zyk+%vH_kjA}N`+Z?aWqaZlX4oPd&w{OKZE9t`*;Ec(vydQ)3eKdk7bH4Y6Gw^aSHm-h1jYBU zj0hN>Wv-F87X&>NJBYRO-hzyU%5#{rccqPY@|O~=^oF)7%K9f{e4P&NW&9(Z-|6Ja zXV>ImXXnqOm@L&#$=WnP{f9mUh}&y_Se510i<^66>nkFs4NP$0aCL`vIB$3tcjMG+ z){#ZXWNiGsntUujQyrw7daCv~w)?SF*SZ|j4t|30C@tsu<8ML~k>A?dd7*u=gl#)WzpY6YTE5J@i>U40 zrMP2Adan89PnIq8i2GqaxHsm=ZiwB2!#yx!4DmEFVY}JeVAuYPv&RqP;bn3<#!0;B z1j%!mSc(eW{A?I+O!U<#ljgh3CS)EVgo)0sd%fF@BxgBdTM+-7ON*o?RiJ>;*)CPD z?ym86bp(a1Qnp(mmj&r`X-p5B@eRdP+xhPc&)OFx6Nnpy%p`s{I3_^ID!lYT#CFT^ zv4gvR0kTIquN(bXzX$PrbVGUZf;<<-Oc@?EYYtfJ{D(PnJMUyE2>o4cy+|^TQ>c#e zibqw%`|1rDvm{NEQ+tawXiHUH@@Fv-yv@!M-D#gFMoICJQuE79MPc8?wHf#PLU$hPhuDZofW0~U%ZUS2eGh2 z+;h>Lr-?oZOvY@Q$e!mR*{f{}+VK}$BnnnrzVhM)bS)c~g(Tea#VYFEjc&!+ldt48 zUG1wbuuP!wOFbLZXK0UcDqN?*SJ^))ye(sz1sZ3>k=Ou_BNIH?VvELvdp}MT!BoDb}Ski9+wO?5-=;mz<57{VO*&a>xU{lh~N zlQ|{pos>GOO1DnarK@rBPG31ft|^Wtk&ZT2S}rs^s2oU`eYW;BQ7oIm4nsTm?4$Eb z&ewWS?ZJRtYB(CW5LvE%Re6D+LpY-)joszhpvgNVT=$Fkm z8d$uIOn=)V0)He>zp4UPTsaY(QJYi@2m-2S$*a^DlYX9Zrs6(J!_=!v7skKpoEAt` z6TPdBc$x?2Fi9)elHWkhicU3%hqnfi;1B+9a{XIPna%Au?S_3KAItGkS6dbr+}5C?#Ww|4*>}6)2M6D%JVW>30|j z6vp3yQkW+c%m$gcY4-0{i|QC3w|9MnrSqj6YwhCQ5i2z{C2XgxsE9&;CX%=OR|Z4x zl7KIFKQ-T5^ftkR;BY>Cw95C}44zL~E#1&y#+3%c?d<(%g}zzb&Y9I|0XFws6kVq; z+=hLQC?WBV^E~K89MlG-D8Yqh>x@51MXf&uvtkG#zjr$Fu>OHsr?QfkSSYmp-c zLQ*#7m+3b*Qy8?|e5M7q+cdn1hN@GllMp7S!b2;3SW&zlE8L>?{lEU3-{iK$T2DL3 z{vrwOs_&0NKSySM-a1tAC}lSuX(PHzFImab^mx!Vz9{FvaFN=_RKQ5wQbSG~YjW`? zKa;5T2o6p4ZjY(pJ(JzkMeBd_C~bI59kBg)IcUdK+IJ5QOeJCH#$LzS55Z2vx7jpQ!omz=9ADkI<=Qw%}@mYY@ShYMTnBV2+OOk>f^Wx#l2~ zy(4jTO1~jlJ{ql@0rPj!@o6@(T<6Sjcd4rZsNG(sC|YtNGOzP?!V?^eNrA#evYYRR z2~?GKu@Ay|02v@Sz1vjFi)hm~Ykp3|JJ@g3X;=N59$ZQ&%TlKHP0{n1K*d1yx1(2) z(yEW!_)2Spx9o#;wE)T@G1oZ4Bz{qG3@Umgb}U~x-~O19}1 z(H=0Gx?)w-(Ol54v6l9?tr||Xe7K4_*_Oqsm&iQv6twf0t2|o~RbS}e043APHl9lU+ zl!u=Apr7d<>±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 literal 0 HcmV?d00001 diff --git a/lib/fmt-rate.ts b/lib/fmt-rate.ts index a0b41e76..db121dd6 100644 --- a/lib/fmt-rate.ts +++ b/lib/fmt-rate.ts @@ -1,5 +1,6 @@ /** Формат скорости: Кбит/с при < 1 Мбит/с, иначе Мбит/с / Гбит/с. */ -export function fmtRate(v: number): string { +export function fmtRate(v: number | null | undefined): string { + if (v == null) return "—" if (!Number.isFinite(v) || v <= 0) return "0 Кбит/с" if (v >= 1000) return `${(v / 1000).toFixed(2)} Гбит/с` if (v < 1) return `${Math.round(v * 1000)} Кбит/с` @@ -8,7 +9,8 @@ export function fmtRate(v: number): string { return `${Math.round(v)} Мбит/с` } -export function fmtGB(v: number): string { +export function fmtGB(v: number | null | undefined): string { + if (v == null) return "—" if (v >= 1000) return `${(v / 1000).toFixed(2)} ТБ` return `${v.toFixed(1)} ГБ` } diff --git a/lib/traffic-observations.test.ts b/lib/traffic-observations.test.ts new file mode 100644 index 00000000..49d02fee --- /dev/null +++ b/lib/traffic-observations.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict" +import { projectTrafficObservations } from "./traffic-observations.js" +const at=Date.parse("2026-10-12T12:00:00Z"),sampledAt=new Date(at).toISOString(),expiresAt=new Date(at+90_000).toISOString() +const row={rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal:0,txTotal:0,rxSeries:[0,0],txSeries:[0,0]} +const obs={rxNow:true,txNow:true,rxSeries:[false,true],txSeries:[false,true],sampledAt,currentState:"current",expiresAt} +assert.equal(projectTrafficObservations(row,at).rxNow,null) +const known=projectTrafficObservations({...row,observations:obs},at);assert.equal(known.rxNow,0);assert.deepEqual(known.rxSeries,[null,0]) +const stale=projectTrafficObservations({...row,observations:obs},at+90_001);assert.equal(stale.rxNow,null);assert.equal(stale.observations?.currentState,"stale");assert.deepEqual(stale.rxSeries,[null,0]) +assert.equal(projectTrafficObservations({...row,observations:{...obs,currentState:"error"}},at).rxNow,null) +for(const change of [{rxNow:-1},{observations:{...obs,sampledAt:"bad"}},{observations:{...obs,sampledAt:null}},{observations:{...obs,rxSeries:[]}}])assert.throws(()=>projectTrafficObservations({...row,...change},at)) +console.log("traffic observations nullable/zero/gaps/expiry guards tests ok") diff --git a/lib/traffic-observations.ts b/lib/traffic-observations.ts new file mode 100644 index 00000000..84153a6f --- /dev/null +++ b/lib/traffic-observations.ts @@ -0,0 +1,25 @@ +import { z } from "zod" +const timestamp = z.string().refine(v => Number.isFinite(Date.parse(v))).nullable() +const rate = z.number().finite().nonnegative().nullable() +const meta = z.object({ rxNow: z.boolean(), txNow: z.boolean(), rxSeries: z.array(z.boolean()), txSeries: z.array(z.boolean()), + sampledAt: timestamp, expiresAt: timestamp.optional(), lastAttemptAt: timestamp.optional(), lastSucceededAt: timestamp.optional(), error: z.string().nullable().optional(), + currentState: z.enum(["current", "unknown", "pending", "error", "stale", "disabled"]).optional() }) +export type TrafficObservationMeta = z.infer +const metrics = z.object({ rxNow: rate, txNow: rate, rxPeak: rate, txPeak: rate, rxTotal: rate, txTotal: rate, + rxSeries: z.array(rate), txSeries: z.array(rate), observations: meta.optional() }).refine(r => + (!r.observations || r.observations.rxSeries.length === r.rxSeries.length && r.observations.txSeries.length === r.txSeries.length && + (!(r.observations.rxNow || r.observations.txNow || r.observations.rxSeries.some(Boolean) || r.observations.txSeries.some(Boolean)) || r.observations.sampledAt !== null))) +/** Receipt of numeric legacy DTO values cannot establish an observed measurement. */ +export function projectTrafficObservations(raw: unknown, now = Date.now()) { + const parsed = metrics.safeParse(raw) + if (!parsed.success) throw new Error("Некорректные измерения трафика") + const r = parsed.data, o = r.observations && { ...r.observations } + if (o && o.expiresAt && Date.parse(o.expiresAt) < now && o.currentState === "current") o.currentState = "stale" + const current = !o?.currentState || o.currentState === "current" + const rxSeries = r.rxSeries.map((v, i) => o?.rxSeries[i] ? v : null) + const txSeries = r.txSeries.map((v, i) => o?.txSeries[i] ? v : null) + return { observations: o, rxNow: current && o?.rxNow ? r.rxNow : null, txNow: current && o?.txNow ? r.txNow : null, + rxSeries, txSeries, rxPeak: rxSeries.some(v => v !== null) ? r.rxPeak : null, + txPeak: txSeries.some(v => v !== null) ? r.txPeak : null, + rxTotal: rxSeries.some(v => v !== null) ? r.rxTotal : null, txTotal: txSeries.some(v => v !== null) ? r.txTotal : null } +} diff --git a/tests/e2e/traffic-catalog.spec.ts b/tests/e2e/traffic-catalog.spec.ts index 6155cae3..ea18753f 100644 --- a/tests/e2e/traffic-catalog.spec.ts +++ b/tests/e2e/traffic-catalog.spec.ts @@ -1,3 +1,4 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect } from '@playwright/test' for (const outcome of ['success', 'error'] as const) { @@ -6,23 +7,23 @@ for (const outcome of ['success', 'error'] as const) { const gate = new Promise(resolve => { release = resolve }) const individual: string[] = [] const server = { id: '1', name: 'Подтверждённый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] } - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', async route => { const path = new URL(route.request().url()).pathname if (/^\/api\/traffic\/servers\/[^/]+/.test(path)) individual.push(path) if (path === '/api/traffic/servers') { await gate - if (outcome === 'error') return route.fulfill({ status: 503, json: { error: 'Каталог временно недоступен' } }) - return route.fulfill({ json: { servers: [server] } }) + if (outcome === 'error') return route.fulfill({ status: 503, json: observedTrafficFixturePayload({ error: 'Каталог временно недоступен' }) }) + return route.fulfill({ json: observedTrafficFixturePayload({ servers: [server] }) }) } - if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} }) + if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) }) const json = path === '/api/auth/config' ? { required: false } : path === '/api/traffic/servers/1' ? { server } : path === '/api/traffic/servers/1/interfaces' ? { interfaces: [] } : path === '/api/traffic/users' ? { users: [] } : path === '/api/traffic/bound-interfaces' ? { interfaces: [] } : path === '/api/servers' ? [] : {} - return route.fulfill({ json }) + return route.fulfill({ json: observedTrafficFixturePayload(json) }) }) const catalog = page.waitForRequest(r => new URL(r.url()).pathname === '/api/traffic/servers') await page.goto('/traffic') diff --git a/tests/e2e/traffic-details.spec.ts b/tests/e2e/traffic-details.spec.ts index 092f756b..e6c7b493 100644 --- a/tests/e2e/traffic-details.spec.ts +++ b/tests/e2e/traffic-details.spec.ts @@ -1,18 +1,19 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect, type Page } from '@playwright/test' async function setup(page: Page) { const control = { holdRange: '', failIface: false, series: undefined as Array | undefined, txSeries: undefined as number[] | undefined, release: () => {} } const gate = new Promise(resolve => { control.release = resolve }) const server = { id: '1', name: 'Первый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] } - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', async route => { const url = new URL(route.request().url()), path = url.pathname - if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} }) + if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) }) if (path === '/api/traffic/servers/1') { const range = url.searchParams.get('range'), iface = url.searchParams.get('iface') if (control.holdRange && range === control.holdRange) await gate - if (control.failIface && iface === 'ether2') return route.fulfill({ status: 500, json: { error: 'Интерфейс временно недоступен' } }) - return route.fulfill({ json: { server: { ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } } }) + if (control.failIface && iface === 'ether2') return route.fulfill({ status: 500, json: observedTrafficFixturePayload({ error: 'Интерфейс временно недоступен' }) }) + return route.fulfill({ json: observedTrafficFixturePayload({ server: { ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } }) }) } const json = path === '/api/auth/config' ? { required: false } : path === '/api/traffic/servers' ? { servers: [{ ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries }] } @@ -20,7 +21,7 @@ async function setup(page: Page) { : path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] } : path === '/api/traffic/users' ? { users: [] } : path === '/api/servers' ? [] : {} - await route.fulfill({ json }) + await route.fulfill({ json: observedTrafficFixturePayload(json) }) }) await page.goto('/traffic') await expect(page.getByRole('heading', { name: 'Детали 1h __all__', exact: true })).toBeVisible() @@ -118,7 +119,7 @@ test('Карточка трафика: пустая история, одна т await page.getByRole('button', { name: '15м', exact: true }).click() await expect(card.locator('circle')).toHaveCount(1) await expect(card.locator('circle')).toHaveAttribute('cx', '150') - control.series = [2, 3, -1, 4, 5] + control.series = [2, 3, null, 4, 5] control.txSeries = [0, 0, 0, 0, 0] await page.getByRole('button', { name: '4ч', exact: true }).click() await expect(card.locator('[data-direction="rx"] polyline')).toHaveCount(2) diff --git a/tests/e2e/traffic-history-confidence.spec.ts b/tests/e2e/traffic-history-confidence.spec.ts new file mode 100644 index 00000000..8c969b35 --- /dev/null +++ b/tests/e2e/traffic-history-confidence.spec.ts @@ -0,0 +1,55 @@ +import { test, expect, type Page } from "@playwright/test" +const at = new Date("2026-10-12T12:00:00Z") +async function setup(page: Page, state: "error" | "current" | "legacy", status = "online") { + let calls = 0 + const observations = { rxNow: state === "current", txNow: state === "current", rxSeries: [false, true], txSeries: [false, true], + sampledAt: at.toISOString(), lastAttemptAt: at.toISOString(), lastSucceededAt: at.toISOString(), + expiresAt: new Date(at.getTime()+90_000).toISOString(), currentState: state === "legacy" ? "unknown" : state, + error: state === "error" ? "CHR: HTTP 401 — опрос не выполнен" : null } + const server = { id: "1", name: "История CHR", site: "TEST", country: "RU", status, + rxNow: state === "current" ? 0 : 17, txNow: 0, rxPeak: 5, txPeak: 6, rxTotal: 1, txTotal: 2, sessions: 1, + rxSeries: [0,5], txSeries: [0,6], ...(state !== "legacy" ? {observations} : {}) } + await page.clock.install({time:at}) + await page.route("**/health", r=>r.fulfill({json:{ok:true}})) + await page.route("**/api/**", r=> { + const path = new URL(r.request().url()).pathname + if(path.endsWith("/live")) return r.fulfill({status:503,json:{error:"Live недоступен"}}) + if(path==="/api/traffic/servers")calls++ + return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/traffic/servers"?{servers:[server]}: + path==="/api/traffic/servers/1"?{server}:path.endsWith("/interfaces")||path==="/api/traffic/bound-interfaces"?{interfaces:[]}: + path==="/api/traffic/users"?{users:[]}:path==="/api/servers"?[]:{}}) + }) + await page.goto("/traffic") + await expect(page.getByRole("heading",{name:"История CHR",exact:true})).toBeVisible() + return ()=>calls +} +for(const width of [1440,320]) test(`Ошибка опроса: история сохранена, текущая скорость неизвестна ${width}`,async({page})=>{ + await page.setViewportSize({width,height:1000});await setup(page,"error","offline") + await expect(page.getByRole("status").filter({hasText:"Ошибка опроса трафика"})).toContainText("HTTP 401") + await expect(page.getByRole("region",{name:"История RX/TX"})).toBeVisible() + await expect(page.getByLabel("Скорость выбранного сервера")).not.toContainText("17.0") + await expect(page.getByLabel("Скорость выбранного сервера").getByText("—",{exact:true})).toHaveCount(2) + await expect(page.getByLabel("Попытка опроса трафика")).toBeVisible() + expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true) + await page.getByRole("status").filter({hasText:"Ошибка опроса трафика"}).scrollIntoViewIfNeeded() + await page.getByRole("status").filter({hasText:"Ошибка опроса трафика"}).evaluate(el => { + let parent = el.parentElement + while (parent && (!/auto|scroll/.test(getComputedStyle(parent).overflowY) || parent.scrollHeight <= parent.clientHeight)) parent = parent.parentElement + if (parent) parent.scrollTop += el.getBoundingClientRect().top - parent.getBoundingClientRect().top - 24 + }) + await page.screenshot({path:`docs/ux-ui/screens/traffic-history-error-${width}-2026-10-12.png`,fullPage:true}) +}) +test("Измеренный ноль сохраняется; по TTL скорость становится неизвестной без подмены истории",async({page})=>{ + const calls=await setup(page,"current") + await expect(page.getByLabel("Скорость выбранного сервера").getByText("0 Кбит/с",{exact:true})).toHaveCount(2) + const before=calls();await page.clock.fastForward(100_001) + await expect(page.getByRole("status").filter({hasText:"Измерения трафика устарели"})).toBeVisible() + await expect(page.getByLabel("Скорость выбранного сервера").getByText("—",{exact:true})).toHaveCount(2) + await expect(page.getByRole("region",{name:"История RX/TX"})).toBeVisible();expect(calls()).toBe(before) +}) +test("Legacy numeric DTO без происхождения не изображает измеренную историю",async({page})=>{ + await setup(page,"legacy") + await expect(page.getByText("Нет исторических измерений за выбранный период",{exact:true})).toBeVisible() + await expect(page.getByRole("status").filter({hasText:"Скорость пока не измерена"})).toBeVisible() + await expect(page.getByLabel("Скорость выбранного сервера")).not.toContainText("17.0") +}) diff --git a/tests/e2e/traffic-ipv6-labels.spec.ts b/tests/e2e/traffic-ipv6-labels.spec.ts index 63cd438c..9454d95c 100644 --- a/tests/e2e/traffic-ipv6-labels.spec.ts +++ b/tests/e2e/traffic-ipv6-labels.spec.ts @@ -1,3 +1,4 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect } from '@playwright/test' for (const width of [1440, 320]) { @@ -10,10 +11,10 @@ for (const width of [1440, 320]) { { src: '10.0.0.1', dst: '8.8.8.8', srcPort: 50002, dstPort: 443, internetPeer: '8.8.8.8', internetPeerPort: 443, proto: 6, protoName: 'TCP', application: 'HTTPS' }, ].map(row => ({ ...row, serverId: '1', serverName: 'IPv6 exporter', clientIp: row.src, inIface: 'ether1', bps: 1000, bytes: 1000, packets: 10 })) const stats = { bpsNow: 0, bytes: 4000, packets: 40, conversations: 4, uniqueSrc: 4, uniqueDst: 4, topCategory: 'Прочее', topProto: 'ICMPv6', rxSeries: [], txSeries: [], interfaces: [], ifaces: [], live: false, timeline: [], applications: [], categories: [], services: [], countries: [], asns: [], protocols: [], sources: [], destinations: [], conversationsList: rows, dedupApplied: false } - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', route => { const path = new URL(route.request().url()).pathname - if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} }) + if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) }) const json = path === '/api/auth/config' ? { required: false } : path === '/api/traffic/flow/analytics' ? stats : path === '/api/traffic/flow/exporters' ? { exporters: [{ id: '1', name: 'IPv6 exporter', subtitle: '', site: 'TEST', country: '', status: 'online', rxNow: 0, txNow: 0, sessions: 4, bytes: 4000, rxSeries: [], txSeries: [] }] } @@ -23,7 +24,7 @@ for (const width of [1440, 320]) { : path === '/api/traffic/flow/clients' ? { clients: [] } : path.endsWith('/interfaces') ? { interfaces: [] } : path === '/api/servers' ? [] : {} - return route.fulfill({ json }) + return route.fulfill({ json: observedTrafficFixturePayload(json) }) }) await page.goto('/traffic') await page.getByRole('group', { name: 'Разделы трафика' }).getByRole('button', { name: 'Потоки', exact: true }).click() diff --git a/tests/e2e/traffic-list.spec.ts b/tests/e2e/traffic-list.spec.ts index da68a784..f48534e7 100644 --- a/tests/e2e/traffic-list.spec.ts +++ b/tests/e2e/traffic-list.spec.ts @@ -1,22 +1,23 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect, type Page } from '@playwright/test' async function setup(page: Page) { const control = { holdRange: '', failList: false, release: () => {} } const gate = new Promise(resolve => { control.release = resolve }) const server = { id: '1', name: 'Первый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] } - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', async route => { const url = new URL(route.request().url()), path = url.pathname - if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} }) + if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) }) if (path === '/api/traffic/servers/1') { const range = url.searchParams.get('range'), iface = url.searchParams.get('iface') - return route.fulfill({ json: { server: { ...server, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } } }) + return route.fulfill({ json: observedTrafficFixturePayload({ server: { ...server, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } }) }) } if (path === '/api/traffic/servers') { const range = url.searchParams.get('range') if (control.holdRange === range) await gate - if (control.failList) return route.fulfill({ status: 500, json: { error: 'Сводка временно недоступна' } }) - return route.fulfill({ json: { servers: [{ ...server, name: 'Список ' + range }] } }) + if (control.failList) return route.fulfill({ status: 500, json: observedTrafficFixturePayload({ error: 'Сводка временно недоступна' }) }) + return route.fulfill({ json: observedTrafficFixturePayload({ servers: [{ ...server, name: 'Список ' + range }] }) }) } const json = path === '/api/auth/config' ? { required: false } : path === '/api/traffic/servers' ? { servers: [server] } @@ -24,7 +25,7 @@ async function setup(page: Page) { : path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] } : path === '/api/traffic/users' ? { users: [] } : path === '/api/servers' ? [] : {} - await route.fulfill({ json }) + await route.fulfill({ json: observedTrafficFixturePayload(json) }) }) await page.goto('/traffic') await expect(page.getByRole('heading', { name: 'Детали 1h __all__', exact: true })).toBeVisible() diff --git a/tests/e2e/traffic-live.spec.ts b/tests/e2e/traffic-live.spec.ts index 7ebdc4a7..aca0f755 100644 --- a/tests/e2e/traffic-live.spec.ts +++ b/tests/e2e/traffic-live.spec.ts @@ -1,15 +1,16 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { installOpenLiveStream, resumeLiveStream, sendLiveHeartbeat } from "./live-stream-fixture" import { test, expect } from '@playwright/test' for (const variant of ['closed', 'invalid', 'http-error', 'silent', 'awaiting'] as const) { test(`Трафик: SSE ${variant} не выдаёт старое измерение за live`, async ({ page }) => { const server = { id: '1', name: 'Тестовый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] } - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', route => { const url = new URL(route.request().url()) const path = url.pathname if (path.endsWith('/live')) { - if (variant === 'http-error' || url.searchParams.get('iface') === 'ether2') return route.fulfill({ status: 503, json: { error: 'Поток недоступен' } }) + if (variant === 'http-error' || url.searchParams.get('iface') === 'ether2') return route.fulfill({ status: 503, json: observedTrafficFixturePayload({ error: 'Поток недоступен' }) }) const sample = { rxMbps: 17, txMbps: 19, at: '2026-09-23T12:00:00Z' } const body = 'event: sample\ndata: ' + JSON.stringify(sample) + '\n\n' + (variant === 'invalid' ? 'event: sample\ndata: {"rxMbps":null,"txMbps":-1}\n\n' : '') return route.fulfill({ contentType: 'text/event-stream', body }) @@ -21,7 +22,7 @@ for (const variant of ['closed', 'invalid', 'http-error', 'silent', 'awaiting'] : path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] } : path === '/api/traffic/users' ? { users: [] } : path === '/api/servers' ? [] : {} - return route.fulfill({ json }) + return route.fulfill({ json: observedTrafficFixturePayload(json) }) }) if (variant === 'silent' || variant === 'awaiting') await installOpenLiveStream(page, '/api/traffic/servers/1/live', { rxMbps: 17, txMbps: 19, at: '2026-09-23T12:00:00Z' }, variant !== 'awaiting') const received = new Date('2026-09-28T06:00:00Z') diff --git a/tests/e2e/traffic-mobile.spec.ts b/tests/e2e/traffic-mobile.spec.ts index 0fae27e6..01a2b83a 100644 --- a/tests/e2e/traffic-mobile.spec.ts +++ b/tests/e2e/traffic-mobile.spec.ts @@ -1,3 +1,4 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect, type Page } from '@playwright/test' const longName = 'customer-with-a-very-long-unbroken-identifier-12345678901234567890' @@ -5,7 +6,7 @@ async function setup(page: Page, empty = false) { const metrics = { rxNow: 2, txNow: 3, rxPeak: 4, txPeak: 5, rxTotal: 100, txTotal: 200, rxSeries: [1, 2], txSeries: [2, 3] } const iface = { ...metrics, id: 'i1', bindingId: 'b1', userId: 'u1', userLogin: longName, userName: longName, interfaceName: longName, interfaceType: 'gre', comment: longName, serverId: '1', serverName: longName, serverSite: 'TEST', serverCountry: 'RU', status: 'online' } const user = { ...metrics, id: 'u1', login: longName, displayName: longName, role: 'viewer', interfaces: [iface] } - await page.route('**/health', r => r.fulfill({ json: { ok: true } })) + await page.route('**/health', r => r.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', route => { const path = new URL(route.request().url()).pathname const json = path === '/api/auth/config' ? { required: false } @@ -14,7 +15,7 @@ async function setup(page: Page, empty = false) { : path === '/api/traffic/bound-interfaces' ? { interfaces: empty ? [] : [iface] } : path.endsWith('/interfaces') ? { interfaces: [] } : path === '/api/servers' ? [] : {} - return route.fulfill({ json }) + return route.fulfill({ json: observedTrafficFixturePayload(json) }) }) await page.goto('/traffic') } diff --git a/tests/e2e/traffic-observation-fixture.ts b/tests/e2e/traffic-observation-fixture.ts new file mode 100644 index 00000000..24186a73 --- /dev/null +++ b/tests/e2e/traffic-observation-fixture.ts @@ -0,0 +1,12 @@ +/** Positive fixtures explicitly declare measured rates; legacy absence is tested separately. */ +export function observedTrafficFixturePayload(value: unknown): unknown { + if (Array.isArray(value)) return value.map(observedTrafficFixturePayload) + if (!value || typeof value !== "object") return value + const r = Object.fromEntries(Object.entries(value).map(([key, v]) => [key, observedTrafficFixturePayload(v)])) + if ("rxNow" in r && "txNow" in r && "rxTotal" in r && "txTotal" in r && Array.isArray(r.rxSeries) && Array.isArray(r.txSeries) && !("observations" in r)) { + r.observations = { rxNow: true, txNow: true, sampledAt: "2026-10-12T12:00:00Z", + rxSeries: r.rxSeries.map(v => typeof v === "number" && Number.isFinite(v) && v >= 0), + txSeries: r.txSeries.map(v => typeof v === "number" && Number.isFinite(v) && v >= 0) } + } + return r +} diff --git a/tests/e2e/traffic-response-validation.spec.ts b/tests/e2e/traffic-response-validation.spec.ts index 484422f5..1ab76349 100644 --- a/tests/e2e/traffic-response-validation.spec.ts +++ b/tests/e2e/traffic-response-validation.spec.ts @@ -1,3 +1,4 @@ +import { observedTrafficFixturePayload } from "./traffic-observation-fixture" import { test, expect } from '@playwright/test' for (const variant of ['missing-sessions', 'negative-series', 'monthly-missing', 'monthly-mismatch'] as const) { @@ -9,7 +10,7 @@ for (const variant of ['missing-sessions', 'negative-series', 'monthly-missing', const stats = { bpsNow: 1000000, bytes: 200, packets: 2, conversations: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', topCategory: 'Восстановлено', rxSeries: [1], txSeries: [2], applications: [], protocols: [], sources: [], destinations: [], interfaces: [], conversationsList: [], ifaces: [], live: false } const card = { id: '1', name: 'Экспортёр DTO', subtitle: '', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, sessions: 1, bytes: 200, rxSeries: [1], txSeries: [2] } page.on('pageerror', error => errors.push(error.message)) - await page.route('**/health', route => route.fulfill({ json: { ok: true } })) + await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) })) await page.route('**/api/**', route => { const url = new URL(route.request().url()), path = url.pathname if (path === '/api/traffic/flow/analytics' || path === '/api/traffic/flow/monthly') { @@ -18,13 +19,13 @@ for (const variant of ['missing-sessions', 'negative-series', 'monthly-missing', const month = url.searchParams.get('month')! const data = bad && variant === 'monthly-missing' ? { month, bytes: 1500 } : { month: bad && variant === 'monthly-mismatch' ? (month === '2026-01' ? '2026-02' : '2026-01') : month, bytes: 1500, countries: [], services: [], asns: [] } - return route.fulfill({ json: data }) + return route.fulfill({ json: observedTrafficFixturePayload(data) }) } const data = bad && variant === 'missing-sessions' ? { ...stats, conversationsList: undefined } : bad && variant === 'negative-series' ? { ...stats, rxSeries: [-1] } : stats - return route.fulfill({ json: data }) + return route.fulfill({ json: observedTrafficFixturePayload(data) }) } - if (path.endsWith('/live')) return route.fulfill({ status: 503, json: {} }) + if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) }) const json = path === '/api/auth/config' ? { required: false } : path === '/api/traffic/flow/exporters' ? { exporters: [card] } : path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 200, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true } @@ -33,7 +34,7 @@ for (const variant of ['missing-sessions', 'negative-series', 'monthly-missing', : path === '/api/traffic/flow/clients' ? { clients: [] } : path.endsWith('/interfaces') ? { interfaces: [] } : path === '/api/servers' ? [{ id: 1, name: card.name, host: '127.0.0.1', port: 80, useSsl: false, verifySsl: false, username: 'admin', password: '', type: 'jump-host', site: 'TEST', country: 'RU', asn: '', comment: '', enabled: true, lanSubnet: '', wanUplinks: [], createdAt: '', updatedAt: '', status: null, latency: null, os: null, model: null, uptime: null, cpuLoad: null, freeMemory: null, totalMemory: null, identityName: null, sessions: 0, polledAt: null }] : {} - return route.fulfill({ json }) + return route.fulfill({ json: observedTrafficFixturePayload(json) }) }) await page.goto('/traffic') await page.getByRole('group', { name: 'Разделы трафика' }).getByRole('button', { name: 'Потоки', exact: true }).click()