fix(traffic): подтверждать свежесть статусов привязанных интерфейсов
This commit is contained in:
1 parent
2b3d583e3b
commit
b4d0733cfa
27 files changed
+1463
-36
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useMemo, useEffect, type ReactNode } from "react"
|
import { useState, useMemo, useEffect, type ReactNode } from "react"
|
||||||
import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert"
|
import { Alert, AlertTitle, AlertDescription } from "@/components/reui/alert"
|
||||||
import { projectTrafficObservations, type TrafficObservationMeta } from "@/lib/traffic-observations"
|
import { projectTrafficObservations, type TrafficObservationMeta } from "@/lib/traffic-observations"
|
||||||
|
import { projectTrafficBindingStatus } from "@/lib/traffic-binding-status"
|
||||||
import { TrafficObservationStatus } from "@/components/traffic/traffic-observation-status"
|
import { TrafficObservationStatus } from "@/components/traffic/traffic-observation-status"
|
||||||
import { TrafficDataTime } from "@/components/traffic/traffic-data-time"
|
import { TrafficDataTime } from "@/components/traffic/traffic-data-time"
|
||||||
import { PageHeader } from "@/components/page-header"
|
import { PageHeader } from "@/components/page-header"
|
||||||
@@ -86,6 +87,8 @@ function flowEmptyHint(stats: FlowStatsDto | null, collectorAlive?: boolean): st
|
|||||||
// ─── data model ───────────────────────────────────────────────────────────────
|
// ─── data model ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface BoundIfaceTraffic {
|
interface BoundIfaceTraffic {
|
||||||
|
statusObservedAt?: string|null
|
||||||
|
statusExpiresAt?: string|null
|
||||||
volumeUnit?: "GiB"
|
volumeUnit?: "GiB"
|
||||||
observations?: TrafficObservationMeta
|
observations?: TrafficObservationMeta
|
||||||
id: string
|
id: string
|
||||||
@@ -110,7 +113,7 @@ interface BoundIfaceTraffic {
|
|||||||
txTotal: number | null
|
txTotal: number | null
|
||||||
rxSeries: (number|null)[]
|
rxSeries: (number|null)[]
|
||||||
txSeries: (number|null)[]
|
txSeries: (number|null)[]
|
||||||
status: "online" | "offline"
|
status: "online" | "offline" | "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerTraffic {
|
interface ServerTraffic {
|
||||||
@@ -508,7 +511,7 @@ function IfaceRow({ c, showServer = false }: { c: BoundIfaceTraffic; showServer?
|
|||||||
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium tabular-nums",
|
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium tabular-nums",
|
||||||
c.status === "online" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground",
|
c.status === "online" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground",
|
||||||
)}>
|
)}>
|
||||||
{c.status === "online" ? "online" : "offline"}
|
{c.status === "online" ? "Работает" : c.status === "unknown" ? "Нет данных" : "Неактивен"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -767,6 +770,7 @@ const toLiveBound = (c: BoundIfaceTraffic): BoundIfaceTraffic => ({
|
|||||||
...c,
|
...c,
|
||||||
interfaceType: (["ether", "gre", "wg", "other"].includes(c.interfaceType) ? c.interfaceType : "other") as InterfaceType,
|
interfaceType: (["ether", "gre", "wg", "other"].includes(c.interfaceType) ? c.interfaceType : "other") as InterfaceType,
|
||||||
...projectTrafficObservations(c),
|
...projectTrafficObservations(c),
|
||||||
|
status: projectTrafficBindingStatus(c.status,c.statusObservedAt,c.statusExpiresAt),
|
||||||
})
|
})
|
||||||
|
|
||||||
const toLiveUser = (u: UserTraffic & { interfaces?: BoundIfaceTraffic[] }): UserTraffic => {
|
const toLiveUser = (u: UserTraffic & { interfaces?: BoundIfaceTraffic[] }): UserTraffic => {
|
||||||
@@ -965,7 +969,7 @@ export default function TrafficPage() {
|
|||||||
|
|
||||||
const activeServerTraffic = useMemo(() => isLive ? (trafficReady ? liveServers.map(s => ({ ...s, ...projectTrafficObservations(s, trafficClock) })) : []) : serverTraffic, [isLive, trafficReady, liveServers, trafficClock])
|
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 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 activeBoundIfaces = useMemo(() => isLive ? (trafficReady ? liveBoundIfaces.map(c => ({ ...c, ...projectTrafficObservations(c, trafficClock), status: projectTrafficBindingStatus(c.status,c.statusObservedAt,c.statusExpiresAt,trafficClock) })) : []) : boundIfaces, [isLive, trafficReady, liveBoundIfaces, trafficClock])
|
||||||
const visibleIfaces = useMemo(
|
const visibleIfaces = useMemo(
|
||||||
() => hideDisabledIfaces ? serverIfaces.filter((i) => !i.disabled && i.running) : serverIfaces,
|
() => hideDisabledIfaces ? serverIfaces.filter((i) => !i.disabled && i.running) : serverIfaces,
|
||||||
[serverIfaces, hideDisabledIfaces],
|
[serverIfaces, hideDisabledIfaces],
|
||||||
@@ -1099,7 +1103,7 @@ export default function TrafficPage() {
|
|||||||
const displayedFlow = currentFlows && !liveError ? (flowLiveSample ?? currentAnalytics?.data ?? null) : null
|
const displayedFlow = currentFlows && !liveError ? (flowLiveSample ?? currentAnalytics?.data ?? null) : null
|
||||||
|
|
||||||
const selServer = useMemo(() => activeServerTraffic.find(s => s.id === selectedId) ?? activeServerTraffic[0], [selectedId, activeServerTraffic])
|
const selServer = useMemo(() => activeServerTraffic.find(s => s.id === selectedId) ?? activeServerTraffic[0], [selectedId, activeServerTraffic])
|
||||||
const detailServer = isLive ? (currentDetail?.server ? { ...currentDetail.server, ...projectTrafficObservations(currentDetail.server, trafficClock) } : null) : selServer
|
const detailServer = isLive ? (currentDetail?.server ? { ...currentDetail.server, ...projectTrafficObservations(currentDetail.server, trafficClock), boundIfaces: currentDetail.server.boundIfaces.map(toLiveBound) } : null) : selServer
|
||||||
const selUser = useMemo(() => activeUserTraffic.find(u => u.id === selectedId) ?? activeUserTraffic[0], [selectedId, activeUserTraffic])
|
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])
|
const selIface = useMemo(() => activeBoundIfaces.find(c => c.id === selectedId) ?? activeBoundIfaces[0], [selectedId, activeBoundIfaces])
|
||||||
|
|
||||||
|
|||||||
@@ -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: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: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: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-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-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-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-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: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: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",
|
"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",
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ assert.equal(encodeTrafficFlags(true, true), 3)
|
|||||||
assert.equal(encodeTrafficFlags(false, false), 0)
|
assert.equal(encodeTrafficFlags(false, false), 0)
|
||||||
assert.equal(trafficFlagRunning(1), true)
|
assert.equal(trafficFlagRunning(1), true)
|
||||||
assert.equal(trafficFlagDisabled(1), false)
|
assert.equal(trafficFlagDisabled(1), false)
|
||||||
assert.deepEqual(decodeTrafficSampleFlags(1), { running: true, disabled: false, countersKnown: false })
|
assert.deepEqual(decodeTrafficSampleFlags(1), { running: true, disabled: false, countersKnown: false, statusKnown:false })
|
||||||
assert.deepEqual(decodeTrafficSampleFlags(0), { running: false, disabled: false, countersKnown: false })
|
assert.deepEqual(decodeTrafficSampleFlags(0), { running: false, disabled: false, countersKnown: false, statusKnown:false })
|
||||||
assert.deepEqual(decodeTrafficSampleFlags(undefined), { running: false, disabled: false, countersKnown: false })
|
assert.deepEqual(decodeTrafficSampleFlags(undefined), { running: false, disabled: false, countersKnown: false, statusKnown:false })
|
||||||
assert.equal(encodeTrafficFlags(true,false,true),5)
|
assert.equal(encodeTrafficFlags(true,false,true),5)
|
||||||
assert.deepEqual(decodeTrafficSampleFlags(5), {running:true,disabled:false,countersKnown:true})
|
assert.deepEqual(decodeTrafficSampleFlags(5), {running:true,disabled:false,countersKnown:true,statusKnown:false})
|
||||||
|
assert.deepEqual(decodeTrafficSampleFlags(encodeTrafficFlags(true,false,true,true)),{running:true,disabled:false,countersKnown:true,statusKnown:true})
|
||||||
|
|
||||||
console.log("traffic-flags.test.ts: ok")
|
console.log("traffic-flags.test.ts: ok")
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
export const TRAFFIC_FLAG_RUNNING = 1
|
export const TRAFFIC_FLAG_RUNNING = 1
|
||||||
export const TRAFFIC_FLAG_DISABLED = 2
|
export const TRAFFIC_FLAG_DISABLED = 2
|
||||||
export const TRAFFIC_FLAG_COUNTERS_KNOWN = 4
|
export const TRAFFIC_FLAG_COUNTERS_KNOWN = 4
|
||||||
|
export const TRAFFIC_FLAG_STATUS_KNOWN = 8
|
||||||
|
|
||||||
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false): number {
|
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false, statusKnown = false): number {
|
||||||
return (running ? TRAFFIC_FLAG_RUNNING : 0) | (disabled ? TRAFFIC_FLAG_DISABLED : 0)
|
return (running ? TRAFFIC_FLAG_RUNNING : 0) | (disabled ? TRAFFIC_FLAG_DISABLED : 0)
|
||||||
| (countersKnown ? TRAFFIC_FLAG_COUNTERS_KNOWN : 0)
|
| (countersKnown ? TRAFFIC_FLAG_COUNTERS_KNOWN : 0)
|
||||||
|
| (statusKnown ? TRAFFIC_FLAG_STATUS_KNOWN : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trafficFlagRunning(flags: number | null | undefined): boolean {
|
export function trafficFlagRunning(flags: number | null | undefined): boolean {
|
||||||
@@ -19,10 +21,12 @@ export function decodeTrafficSampleFlags(flags: number | null | undefined): {
|
|||||||
running: boolean
|
running: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
countersKnown: boolean
|
countersKnown: boolean
|
||||||
|
statusKnown: boolean
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
running: trafficFlagRunning(flags),
|
running: trafficFlagRunning(flags),
|
||||||
disabled: trafficFlagDisabled(flags),
|
disabled: trafficFlagDisabled(flags),
|
||||||
countersKnown: ((Number(flags) || 0) & TRAFFIC_FLAG_COUNTERS_KNOWN) !== 0,
|
countersKnown: ((Number(flags) || 0) & TRAFFIC_FLAG_COUNTERS_KNOWN) !== 0,
|
||||||
|
statusKnown: ((Number(flags) || 0) & TRAFFIC_FLAG_STATUS_KNOWN) !== 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { trafficBindingStatus } from "./traffic-freshness.js"
|
||||||
|
import { parseTrafficFlag } from "./traffic-observation.js"
|
||||||
|
const now=Date.parse("2026-10-12T12:00:00Z"),iso=new Date(now).toISOString()
|
||||||
|
const last={sampledAt:iso,countersKnown:true,statusKnown:true,running:true,disabled:false}
|
||||||
|
const server={enabled:true,updatedAt:iso},settings={enabled:true,intervalSec:30},attempt={serverUpdatedAt:iso,lastAttemptAt:iso,lastSucceededAt:iso,status:"ok",error:null}
|
||||||
|
assert.equal(trafficBindingStatus(last,false,server,settings,attempt,now),"online")
|
||||||
|
assert.equal(trafficBindingStatus({...last,statusKnown:false},false,server,settings,attempt,now),"unknown")
|
||||||
|
assert.equal(trafficBindingStatus({...last,disabled:true},false,server,settings,attempt,now),"offline")
|
||||||
|
assert.equal(trafficBindingStatus(last,true,server,settings,attempt,now),"unknown")
|
||||||
|
assert.equal(trafficBindingStatus({...last,disabled:true},true,server,settings,attempt,now),"offline")
|
||||||
|
for(const status of["error","pending"])assert.equal(trafficBindingStatus(last,false,server,settings,{...attempt,status},now),"unknown")
|
||||||
|
assert.equal(trafficBindingStatus(last,false,server,settings,attempt,now+90_001),"unknown")
|
||||||
|
assert.equal(trafficBindingStatus(last,false,{...server,enabled:false},settings,attempt,now),"unknown")
|
||||||
|
for(const value of[true,"true","yes"])assert.equal(parseTrafficFlag(value),true)
|
||||||
|
for(const value of[false,"false","no"])assert.equal(parseTrafficFlag(value),false)
|
||||||
|
for(const value of[undefined,null,"bad",{},0,1])assert.throws(()=>parseTrafficFlag(value))
|
||||||
|
console.log("binding native status provenance/TTL/peer configuration/boolean guards tests ok")
|
||||||
@@ -6,7 +6,7 @@ import type { TrafficRunSnapshot } from "../types/scheduler-run-snapshot.js"
|
|||||||
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
|
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
|
||||||
import { MikrotikClient } from "./mikrotik.js"
|
import { MikrotikClient } from "./mikrotik.js"
|
||||||
import { bpsToMbps, shouldIncludeIface } from "./traffic-rate.js"
|
import { bpsToMbps, shouldIncludeIface } from "./traffic-rate.js"
|
||||||
import { observedTrafficRate, parseTrafficCounter } from "./traffic-observation.js"
|
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||||
import { rememberServerIfaces } from "./traffic-flow-ifindex.js"
|
import { rememberServerIfaces } from "./traffic-flow-ifindex.js"
|
||||||
import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js"
|
import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js"
|
||||||
|
|
||||||
@@ -133,16 +133,18 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
rememberServerIfaces(srv.id, ifaces)
|
rememberServerIfaces(srv.id, ifaces)
|
||||||
const prevWave = await readPreviousWave(srv.id)
|
const prevWave = await readPreviousWave(srv.id)
|
||||||
const nowMs = Date.parse(now)
|
const nowMs = Date.parse(now)
|
||||||
|
if (!Array.isArray(ifaces) || ifaces.some(i => typeof i.name !== "string" || !i.name.trim()) || new Set(ifaces.map(i => i.name)).size !== ifaces.length)
|
||||||
|
throw new Error("RouterOS вернул некорректный каталог интерфейсов")
|
||||||
let sumRxMbps = 0
|
let sumRxMbps = 0
|
||||||
let sumTxMbps = 0
|
let sumTxMbps = 0
|
||||||
let includedInterfaces = 0
|
let includedInterfaces = 0
|
||||||
let totalsMeasured = true
|
let totalsMeasured = true
|
||||||
const rows = ifaces.map((i) => {
|
const rows = ifaces.map((i) => {
|
||||||
const interfaceName = i.name ?? "unknown"
|
const interfaceName = i.name!
|
||||||
const rxBytes = toNum(i["rx-byte"])
|
const rxBytes = toNum(i["rx-byte"])
|
||||||
const txBytes = toNum(i["tx-byte"])
|
const txBytes = toNum(i["tx-byte"])
|
||||||
const running = (i.running ?? "false") === "true"
|
const running = parseTrafficFlag(i.running)
|
||||||
const disabled = (i.disabled ?? "false") === "true"
|
const disabled = parseTrafficFlag(i.disabled)
|
||||||
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs)
|
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs)
|
||||||
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
||||||
includedInterfaces++
|
includedInterfaces++
|
||||||
@@ -159,7 +161,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
txBytes,
|
txBytes,
|
||||||
rxBps: Math.round(rxBps),
|
rxBps: Math.round(rxBps),
|
||||||
txBps: Math.round(txBps),
|
txBps: Math.round(txBps),
|
||||||
flags: encodeTrafficFlags(running, disabled, true),
|
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
@@ -170,7 +172,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
if (!interfaceName || !peerPublicKey) continue
|
if (!interfaceName || !peerPublicKey) continue
|
||||||
const rxBytes = toNum(p.rx)
|
const rxBytes = toNum(p.rx)
|
||||||
const txBytes = toNum(p.tx)
|
const txBytes = toNum(p.tx)
|
||||||
const disabled = (p.disabled ?? "false") === "true" || p.disabled === "yes"
|
const disabled = parseTrafficFlag(p.disabled)
|
||||||
const running = !disabled
|
const running = !disabled
|
||||||
const { rxBps, txBps } = sampleRate(
|
const { rxBps, txBps } = sampleRate(
|
||||||
prevWave,
|
prevWave,
|
||||||
@@ -188,7 +190,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
txBytes,
|
txBytes,
|
||||||
rxBps: Math.round(rxBps),
|
rxBps: Math.round(rxBps),
|
||||||
txBps: Math.round(txBps),
|
txBps: Math.round(txBps),
|
||||||
flags: encodeTrafficFlags(running, disabled, true),
|
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ export type TrafficCurrentState = "current" | "unknown" | "pending" | "error" |
|
|||||||
export interface TrafficAttempt {
|
export interface TrafficAttempt {
|
||||||
serverUpdatedAt: string | Date; lastAttemptAt: string | Date; lastSucceededAt: string | Date | null; status: string; error: string | null
|
serverUpdatedAt: string | Date; lastAttemptAt: string | Date; lastSucceededAt: string | Date | null; status: string; error: string | null
|
||||||
}
|
}
|
||||||
|
/** Interface running is a native state; enabled WG peer is only configuration, not handshake proof. */
|
||||||
|
export function trafficBindingStatus(last: { sampledAt: string; countersKnown: boolean; statusKnown: boolean; running: boolean; disabled: boolean } | undefined,
|
||||||
|
peer: boolean, server: { enabled: boolean; updatedAt: string | Date },
|
||||||
|
settings: { enabled: boolean; intervalSec: number } | undefined, attempt: TrafficAttempt | undefined, now: number): "online" | "offline" | "unknown" {
|
||||||
|
if (!last?.countersKnown || !last.statusKnown) return "unknown"
|
||||||
|
const observed = applyTrafficFreshness({ rxNow: true, txNow: true, rxSeries: [], txSeries: [], sampledAt: last.sampledAt }, server, settings, attempt, now)
|
||||||
|
if (observed.currentState !== "current") return "unknown"
|
||||||
|
if (last.disabled || !last.running) return "offline"
|
||||||
|
return peer ? "unknown" : "online"
|
||||||
|
}
|
||||||
export function mergeTrafficObservations(parts: TrafficObservations[]): TrafficObservations {
|
export function mergeTrafficObservations(parts: TrafficObservations[]): TrafficObservations {
|
||||||
const oldest = (key: "sampledAt" | "lastAttemptAt" | "lastSucceededAt" | "expiresAt") => {
|
const oldest = (key: "sampledAt" | "lastAttemptAt" | "lastSucceededAt" | "expiresAt") => {
|
||||||
const times = parts.map(p => Date.parse(p[key] ?? ""))
|
const times = parts.map(p => Date.parse(p[key] ?? ""))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import assert from "node:assert/strict"
|
import assert from "node:assert/strict"
|
||||||
import { observedTrafficRate, parseTrafficCounter } from "./traffic-observation.js"
|
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||||
import { collectSourceSignals } from "./alert-engine/collector-source-signals.js"
|
import { collectSourceSignals } from "./alert-engine/collector-source-signals.js"
|
||||||
import fs from "node:fs"
|
import fs from "node:fs"
|
||||||
import vm from "node:vm"
|
import vm from "node:vm"
|
||||||
@@ -41,7 +41,7 @@ assert.equal(ast.statements.filter(n => ts.isFunctionDeclaration(n) && n.name &&
|
|||||||
let inserted: unknown[] = [], wave = new Map<string, typeof previous>(), counters: unknown = "1000"
|
let inserted: unknown[] = [], wave = new Map<string, typeof previous>(), counters: unknown = "1000"
|
||||||
let included = true
|
let included = true
|
||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
Date, observedTrafficRate, parseTrafficCounter, bpsToMbps, shouldIncludeIface,
|
Date, observedTrafficRate, parseTrafficCounter, parseTrafficFlag, bpsToMbps, shouldIncludeIface,
|
||||||
SCHEDULER_RUN_SNAPSHOT_VERSION: 1, servers: {}, trafficSamples: {}, trafficSettings: {}, eq: () => ({}),
|
SCHEDULER_RUN_SNAPSHOT_VERSION: 1, servers: {}, trafficSamples: {}, trafficSettings: {}, eq: () => ({}),
|
||||||
getSettingsRow: async () => ({}), readPreviousWave: async () => wave, rememberServerIfaces: () => {},
|
getSettingsRow: async () => ({}), readPreviousWave: async () => wave, rememberServerIfaces: () => {},
|
||||||
encodeTrafficFlags: () => 1,
|
encodeTrafficFlags: () => 1,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { rateBpsFromDelta } from "./traffic-rate.js"
|
import { rateBpsFromDelta } from "./traffic-rate.js"
|
||||||
|
export function parseTrafficFlag(raw: unknown): boolean {
|
||||||
|
if (raw === true || raw === "true" || raw === "yes") return true
|
||||||
|
if (raw === false || raw === "false" || raw === "no") return false
|
||||||
|
throw new Error("RouterOS вернул неизвестное состояние интерфейса")
|
||||||
|
}
|
||||||
|
|
||||||
/** RouterOS byte counters must be exact nonnegative integers, never fallback zero. */
|
/** RouterOS byte counters must be exact nonnegative integers, never fallback zero. */
|
||||||
export function parseTrafficCounter(raw: unknown): number {
|
export function parseTrafficCounter(raw: unknown): number {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 trafficRoutes from "../routes/traffic.js"
|
||||||
|
const database=process.env.MMAPP_OWN_SQL_DB
|
||||||
|
if(!database)console.log("traffic status 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-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false
|
||||||
|
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:peerDisabled}]:[{name:"ether1",running:flag,disabled:false,"rx-byte":"1000","tx-byte":"1000"}]))})
|
||||||
|
await new Promise<void>(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 bindings=async()=>{const r=await app.inject({method:"GET",url:"/traffic/bound-interfaces?range=1h"});assert.equal(r.statusCode,200);return r.json().interfaces.filter((i:{userId:string})=>i.userId===userId)}
|
||||||
|
try{
|
||||||
|
await initDatabase();const updated=new Date(Date.now()-3600_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,updated])).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,'Status fixture')",[userId,prefix+"login"])
|
||||||
|
for(const[name,key]of[["ether1",""],["wg1","fixturekey"]])await pool.query("INSERT INTO user_interface_bindings(id,user_id,server_id,interface_name,interface_type,peer_public_key) VALUES($1,$2,$3,$4,$5,$6)",[prefix+name,userId,serverId,name,key?"wg":"ether",key])
|
||||||
|
await collectTrafficOnce();let rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").status,"online");assert(rows[0].statusObservedAt&&rows[0].statusExpiresAt);cases++
|
||||||
|
assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1").status,"unknown");cases++
|
||||||
|
peerDisabled=true;await collectTrafficOnce();rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1").status,"offline");cases++
|
||||||
|
flag=false;await collectTrafficOnce();rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").status,"offline");cases++
|
||||||
|
flag="bad";let snap=await collectTrafficOnce();assert.equal(snap.servers[0].ok,false);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||||
|
flag=true;deny=true;await collectTrafficOnce();rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||||
|
deny=false;await collectTrafficOnce();await pool.query("UPDATE traffic_samples SET flags=flags & 7 WHERE server_id=$1",[serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||||
|
await collectTrafficOnce();await pool.query("UPDATE traffic_collection_state SET last_attempt_at=$1 WHERE server_id=$2",[new Date(Date.now()-100_000).toISOString(),serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||||
|
await pool.query("UPDATE servers SET enabled=false WHERE id=$1",[serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
|
||||||
|
console.log(`traffic status owned SQL: ${cases} cases; first-wave native flags/peer config/invalid/error/legacy/stale/disabled passed`)
|
||||||
|
}finally{await app.close();await new Promise<void>(resolve=>http.close(()=>resolve()));await pool.query("DELETE FROM app_users WHERE id=$1",[userId]);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()}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type BuiltTrafficSeries,
|
type BuiltTrafficSeries,
|
||||||
} from "./traffic-rate.js"
|
} from "./traffic-rate.js"
|
||||||
import { readTrafficCollectionContext } from "./traffic-collection-state.js"
|
import { readTrafficCollectionContext } from "./traffic-collection-state.js"
|
||||||
import { applyTrafficFreshness } from "./traffic-freshness.js"
|
import { applyTrafficFreshness, trafficBindingStatus } from "./traffic-freshness.js"
|
||||||
import type { TrafficObservations } from "./traffic-rate.js"
|
import type { TrafficObservations } from "./traffic-rate.js"
|
||||||
|
|
||||||
export interface BoundIfaceTrafficDto {
|
export interface BoundIfaceTrafficDto {
|
||||||
@@ -36,7 +36,9 @@ export interface BoundIfaceTrafficDto {
|
|||||||
txTotal: number
|
txTotal: number
|
||||||
rxSeries: number[]
|
rxSeries: number[]
|
||||||
txSeries: number[]
|
txSeries: number[]
|
||||||
status: "online" | "offline"
|
status: "online" | "offline" | "unknown"
|
||||||
|
statusObservedAt: string | null
|
||||||
|
statusExpiresAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserTrafficDto {
|
export interface UserTrafficDto {
|
||||||
@@ -99,9 +101,9 @@ export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: num
|
|||||||
const last = [...rows.filter((r) =>
|
const last = [...rows.filter((r) =>
|
||||||
r.interfaceName === b.interfaceName && (r.peerPublicKey ?? "") === peerKey,
|
r.interfaceName === b.interfaceName && (r.peerPublicKey ?? "") === peerKey,
|
||||||
)]
|
)]
|
||||||
.sort((a, c) => a.sampledAt.localeCompare(c.sampledAt))
|
.sort((a, c) => Date.parse(a.sampledAt) - Date.parse(c.sampledAt))
|
||||||
.at(-1)
|
.at(-1)
|
||||||
const running = Boolean(last?.running) && !last?.disabled
|
const status = trafficBindingStatus(last, !!peerKey, server ?? {enabled:false,updatedAt:""}, context.settings, context.states.get(b.serverId), rangeEndMs)
|
||||||
interfaces.push({
|
interfaces.push({
|
||||||
id: `${b.userId}:${b.serverId}:${b.interfaceName}:${peerKey || "_iface"}`,
|
id: `${b.userId}:${b.serverId}:${b.interfaceName}:${peerKey || "_iface"}`,
|
||||||
bindingId: b.id,
|
bindingId: b.id,
|
||||||
@@ -118,7 +120,9 @@ export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: num
|
|||||||
serverSite: b.serverSite,
|
serverSite: b.serverSite,
|
||||||
serverCountry: b.serverCountry,
|
serverCountry: b.serverCountry,
|
||||||
...seriesFromBuilt(built),
|
...seriesFromBuilt(built),
|
||||||
status: running && server?.enabled ? "online" : "offline",
|
status,
|
||||||
|
statusObservedAt: status !== "unknown" && last ? new Date(last.sampledAt).toISOString() : null,
|
||||||
|
statusExpiresAt: status !== "unknown" ? built.observations?.expiresAt ?? null : null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# UX/UI: протокол внешней приёмки 12.5
|
# UX/UI: протокол внешней приёмки 12.5
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;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).
|
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)
|
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
|
|
||||||
@@ -2091,3 +2091,27 @@ Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP S
|
|||||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||||
|
|
||||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||||
|
|
||||||
|
## 12.5.181 — достоверность статусов привязанных интерфейсов (2026-10-12)
|
||||||
|
|
||||||
|
Статус привязки теперь требует свежего успешного опроса текущей конфигурации устройства и подтверждённых native флагов. Ошибка, pending, disabled source, истёкший TTL или legacy row без status-known marker дают «Нет данных». Первое корректное измерение интерфейса может подтвердить его running, даже когда ещё недостаточно соседних counters для скорости. История и объёмы сохраняются.
|
||||||
|
|
||||||
|
Producer строго принимает true/false и native строки true/false/yes/no; missing/invalid flags больше не подменяются false. Новый бит8 status-known добавляется только новым успешным измерениям; bit4 counter-known не меняется, legacy rows не переписываются. Interface inventory проверяет имена/дубликаты. Включённый WG peer означает конфигурацию, поэтому даёт unknown без handshake proof; disabled peer — «Неактивен». Это не проверка реального VPN-туннеля.
|
||||||
|
|
||||||
|
DTO привязок содержит statusObservedAt/statusExpiresAt. Frontend проверяет provenance и TTL, обновляет состояние по своему clock даже без повторной загрузки; прежний payload без дат не считается подтверждением. Общие ReUI layout/base-nova и компоненты сохранены.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Native running + свежий успешный poll | native flag → known marker → TTL → Работает | own SQL и UI1440/320 |
|
||||||
|
| Ошибка / старый flag / истёкший TTL | последнее измерение → Нет данных | own SQL и UI1440/320 |
|
||||||
|
| Enabled WireGuard peer | configuration flag → Нет данных | own SQL; handshake не заявлен |
|
||||||
|
|
||||||
|
TypeScript/backend build/ESLint и профильные unit проверки прошли.8 E2E15.7s (2new+6mobile), после последней правки final delta2 passed10s. Один повтор первоначально не запустился из-за Windows sandbox/SWC access denied; корректный разрешённый запуск прошёл, это не UI failure.
|
||||||
|
|
||||||
|
[Own PG SQL](ux-ui/acceptance/traffic-status-sql-2026-10-12.json):48 cases9counter+14history+10TCP SSE+6volume+9status,36163ms, без unavailable skip. Новые9 проверяют настоящий collector→SQL→GET bindings на локальном HTTP RouterOS fixture: первое измерение, enabled/disabled peer, inactive interface, malformed flag,401,legacy bit,stale и disabled source. OwnDB mm_accept_166_1791818856762/ограниченная role/container удалены, отсутствие проверено. Первый запуск harness имел неверный путь распаковки; тесты не начались, его временная база/роль также удалены; пути исправлены до приведённого успешного результата.
|
||||||
|
|
||||||
|
[Source454/524](ux-ui/acceptance/traffic-status-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-status-compiled-2026-10-12.json) SHA256 parity, обе production builds прошли. Frontend ux-status181 сначала проверен с backend ux-volume180 [interop](ux-ui/acceptance/traffic-status-interop-2026-10-12.json); затем [both ux-status181](ux-ui/acceptance/traffic-status-runtime-2026-10-12.json) healthy/running/imageIds. Только .env tags, rollback ux-volume180, без копирования secret env и новой миграции.
|
||||||
|
|
||||||
|
[Native GET](ux-ui/acceptance/traffic-status-native-2026-10-12.json): оба CHR доступны, interface counters и boolean flags валидны; DB read-only, RouterOS config writes0/password changes0/messages0. [Browser1440/320](ux-ui/acceptance/traffic-status-deployed-2026-10-12.json): actual API/live/receipt labels без ошибок/overflow. Положительные binding branches доказаны собственными SQL/UI fixtures, не выданы за существующие пользовательские привязки стенда. Вкладку следует перезагрузить.
|
||||||
|
|
||||||
|
Следующий182 — полнота peer-среза при optional endpoint failure и пропуски истории. Interface running не доказывает end-to-end connectivity, enabled peer не доказывает handshake. Coverage/recreated identity/restore/multi-instance/scale/external integrations остаются открыты в12.5. Локальный коммит без push.
|
||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
# План реализации UX/UI по пятичасовым окнам
|
# План реализации UX/UI по пятичасовым окнам
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||||
|
|
||||||
## Актуальная точка продолжения — 2026-10-12
|
## Актуальная точка продолжения — 2026-10-12
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
|
Этапы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)
|
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
+25
-1
@@ -1,6 +1,6 @@
|
|||||||
# UX/UI: журнал реализации
|
# UX/UI: журнал реализации
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||||
|
|
||||||
@@ -38,6 +38,30 @@
|
|||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. |
|
||||||
|
|
||||||
|
## 12.5.181 — достоверность статусов привязанных интерфейсов (2026-10-12)
|
||||||
|
|
||||||
|
Статус привязки теперь требует свежего успешного опроса текущей конфигурации устройства и подтверждённых native флагов. Ошибка, pending, disabled source, истёкший TTL или legacy row без status-known marker дают «Нет данных». Первое корректное измерение интерфейса может подтвердить его running, даже когда ещё недостаточно соседних counters для скорости. История и объёмы сохраняются.
|
||||||
|
|
||||||
|
Producer строго принимает true/false и native строки true/false/yes/no; missing/invalid flags больше не подменяются false. Новый бит8 status-known добавляется только новым успешным измерениям; bit4 counter-known не меняется, legacy rows не переписываются. Interface inventory проверяет имена/дубликаты. Включённый WG peer означает конфигурацию, поэтому даёт unknown без handshake proof; disabled peer — «Неактивен». Это не проверка реального VPN-туннеля.
|
||||||
|
|
||||||
|
DTO привязок содержит statusObservedAt/statusExpiresAt. Frontend проверяет provenance и TTL, обновляет состояние по своему clock даже без повторной загрузки; прежний payload без дат не считается подтверждением. Общие ReUI layout/base-nova и компоненты сохранены.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Native running + свежий успешный poll | native flag → known marker → TTL → Работает | own SQL и UI1440/320 |
|
||||||
|
| Ошибка / старый flag / истёкший TTL | последнее измерение → Нет данных | own SQL и UI1440/320 |
|
||||||
|
| Enabled WireGuard peer | configuration flag → Нет данных | own SQL; handshake не заявлен |
|
||||||
|
|
||||||
|
TypeScript/backend build/ESLint и профильные unit проверки прошли.8 E2E15.7s (2new+6mobile), после последней правки final delta2 passed10s. Один повтор первоначально не запустился из-за Windows sandbox/SWC access denied; корректный разрешённый запуск прошёл, это не UI failure.
|
||||||
|
|
||||||
|
[Own PG SQL](ux-ui/acceptance/traffic-status-sql-2026-10-12.json):48 cases9counter+14history+10TCP SSE+6volume+9status,36163ms, без unavailable skip. Новые9 проверяют настоящий collector→SQL→GET bindings на локальном HTTP RouterOS fixture: первое измерение, enabled/disabled peer, inactive interface, malformed flag,401,legacy bit,stale и disabled source. OwnDB mm_accept_166_1791818856762/ограниченная role/container удалены, отсутствие проверено. Первый запуск harness имел неверный путь распаковки; тесты не начались, его временная база/роль также удалены; пути исправлены до приведённого успешного результата.
|
||||||
|
|
||||||
|
[Source454/524](ux-ui/acceptance/traffic-status-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-status-compiled-2026-10-12.json) SHA256 parity, обе production builds прошли. Frontend ux-status181 сначала проверен с backend ux-volume180 [interop](ux-ui/acceptance/traffic-status-interop-2026-10-12.json); затем [both ux-status181](ux-ui/acceptance/traffic-status-runtime-2026-10-12.json) healthy/running/imageIds. Только .env tags, rollback ux-volume180, без копирования secret env и новой миграции.
|
||||||
|
|
||||||
|
[Native GET](ux-ui/acceptance/traffic-status-native-2026-10-12.json): оба CHR доступны, interface counters и boolean flags валидны; DB read-only, RouterOS config writes0/password changes0/messages0. [Browser1440/320](ux-ui/acceptance/traffic-status-deployed-2026-10-12.json): actual API/live/receipt labels без ошибок/overflow. Положительные binding branches доказаны собственными SQL/UI fixtures, не выданы за существующие пользовательские привязки стенда. Вкладку следует перезагрузить.
|
||||||
|
|
||||||
|
Следующий182 — полнота peer-среза при optional endpoint failure и пропуски истории. Interface running не доказывает end-to-end connectivity, enabled peer не доказывает handshake. Coverage/recreated identity/restore/multi-instance/scale/external integrations остаются открыты в12.5. Локальный коммит без push.
|
||||||
|
|
||||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||||
|
|
||||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Оставшаяся внешняя приёмка — 2026-10-12
|
# Оставшаяся внешняя приёмка — 2026-10-12
|
||||||
|
|
||||||
Это актуальная матрица после 12.5.180. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
Это актуальная матрица после 12.5.181. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||||
|
|
||||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-volume180`, frontend `ux-volume180`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-status181`, frontend `ux-status181`; 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 миграция не принята |
|
| 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 не приняты |
|
| 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/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения |
|
| 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; history178/legacy provenance/current TTL/manual truth/SQL9+14/35unique E2E/native GET/deployed passed; live179 complete/disable-config/bounded fallback/ownSQL10/nativeSSE2/liveUI2 passed; volume180 exact/unit marker/ownSQL6/18E2E/API/liveUI/deployed passed; binding status freshness181 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; live179 complete/disable-config/bounded fallback/ownSQL10/nativeSSE2/liveUI2 passed; volume180 exact/unit marker/ownSQL6/18E2E/API/liveUI/deployed passed; binding status181 native/ownSQL9/8E2E/deployed passed; peer slice completeness182 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). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
|
Доказательства и точные ограничения: [полная история этапа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). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
|
||||||
|
|
||||||
@@ -1006,3 +1006,27 @@ Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP S
|
|||||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||||
|
|
||||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||||
|
|
||||||
|
## 12.5.181 — достоверность статусов привязанных интерфейсов (2026-10-12)
|
||||||
|
|
||||||
|
Статус привязки теперь требует свежего успешного опроса текущей конфигурации устройства и подтверждённых native флагов. Ошибка, pending, disabled source, истёкший TTL или legacy row без status-known marker дают «Нет данных». Первое корректное измерение интерфейса может подтвердить его running, даже когда ещё недостаточно соседних counters для скорости. История и объёмы сохраняются.
|
||||||
|
|
||||||
|
Producer строго принимает true/false и native строки true/false/yes/no; missing/invalid flags больше не подменяются false. Новый бит8 status-known добавляется только новым успешным измерениям; bit4 counter-known не меняется, legacy rows не переписываются. Interface inventory проверяет имена/дубликаты. Включённый WG peer означает конфигурацию, поэтому даёт unknown без handshake proof; disabled peer — «Неактивен». Это не проверка реального VPN-туннеля.
|
||||||
|
|
||||||
|
DTO привязок содержит statusObservedAt/statusExpiresAt. Frontend проверяет provenance и TTL, обновляет состояние по своему clock даже без повторной загрузки; прежний payload без дат не считается подтверждением. Общие ReUI layout/base-nova и компоненты сохранены.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Native running + свежий успешный poll | native flag → known marker → TTL → Работает | own SQL и UI1440/320 |
|
||||||
|
| Ошибка / старый flag / истёкший TTL | последнее измерение → Нет данных | own SQL и UI1440/320 |
|
||||||
|
| Enabled WireGuard peer | configuration flag → Нет данных | own SQL; handshake не заявлен |
|
||||||
|
|
||||||
|
TypeScript/backend build/ESLint и профильные unit проверки прошли.8 E2E15.7s (2new+6mobile), после последней правки final delta2 passed10s. Один повтор первоначально не запустился из-за Windows sandbox/SWC access denied; корректный разрешённый запуск прошёл, это не UI failure.
|
||||||
|
|
||||||
|
[Own PG SQL](ux-ui/acceptance/traffic-status-sql-2026-10-12.json):48 cases9counter+14history+10TCP SSE+6volume+9status,36163ms, без unavailable skip. Новые9 проверяют настоящий collector→SQL→GET bindings на локальном HTTP RouterOS fixture: первое измерение, enabled/disabled peer, inactive interface, malformed flag,401,legacy bit,stale и disabled source. OwnDB mm_accept_166_1791818856762/ограниченная role/container удалены, отсутствие проверено. Первый запуск harness имел неверный путь распаковки; тесты не начались, его временная база/роль также удалены; пути исправлены до приведённого успешного результата.
|
||||||
|
|
||||||
|
[Source454/524](ux-ui/acceptance/traffic-status-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-status-compiled-2026-10-12.json) SHA256 parity, обе production builds прошли. Frontend ux-status181 сначала проверен с backend ux-volume180 [interop](ux-ui/acceptance/traffic-status-interop-2026-10-12.json); затем [both ux-status181](ux-ui/acceptance/traffic-status-runtime-2026-10-12.json) healthy/running/imageIds. Только .env tags, rollback ux-volume180, без копирования secret env и новой миграции.
|
||||||
|
|
||||||
|
[Native GET](ux-ui/acceptance/traffic-status-native-2026-10-12.json): оба CHR доступны, interface counters и boolean flags валидны; DB read-only, RouterOS config writes0/password changes0/messages0. [Browser1440/320](ux-ui/acceptance/traffic-status-deployed-2026-10-12.json): actual API/live/receipt labels без ошибок/overflow. Положительные binding branches доказаны собственными SQL/UI fixtures, не выданы за существующие пользовательские привязки стенда. Вкладку следует перезагрузить.
|
||||||
|
|
||||||
|
Следующий182 — полнота peer-среза при optional endpoint failure и пропуски истории. Interface running не доказывает end-to-end connectivity, enabled peer не доказывает handshake. Coverage/recreated identity/restore/multi-instance/scale/external integrations остаются открыты в12.5. Локальный коммит без push.
|
||||||
+25
-1
@@ -1,6 +1,6 @@
|
|||||||
# Этап 12: итоговая приёмка UX/UI
|
# Этап 12: итоговая приёмка UX/UI
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Актуальное продолжение 2026-10-12: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||||
|
|
||||||
@@ -3396,3 +3396,27 @@ Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP S
|
|||||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||||
|
|
||||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||||
|
|
||||||
|
## 12.5.181 — достоверность статусов привязанных интерфейсов (2026-10-12)
|
||||||
|
|
||||||
|
Статус привязки теперь требует свежего успешного опроса текущей конфигурации устройства и подтверждённых native флагов. Ошибка, pending, disabled source, истёкший TTL или legacy row без status-known marker дают «Нет данных». Первое корректное измерение интерфейса может подтвердить его running, даже когда ещё недостаточно соседних counters для скорости. История и объёмы сохраняются.
|
||||||
|
|
||||||
|
Producer строго принимает true/false и native строки true/false/yes/no; missing/invalid flags больше не подменяются false. Новый бит8 status-known добавляется только новым успешным измерениям; bit4 counter-known не меняется, legacy rows не переписываются. Interface inventory проверяет имена/дубликаты. Включённый WG peer означает конфигурацию, поэтому даёт unknown без handshake proof; disabled peer — «Неактивен». Это не проверка реального VPN-туннеля.
|
||||||
|
|
||||||
|
DTO привязок содержит statusObservedAt/statusExpiresAt. Frontend проверяет provenance и TTL, обновляет состояние по своему clock даже без повторной загрузки; прежний payload без дат не считается подтверждением. Общие ReUI layout/base-nova и компоненты сохранены.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Native running + свежий успешный poll | native flag → known marker → TTL → Работает | own SQL и UI1440/320 |
|
||||||
|
| Ошибка / старый flag / истёкший TTL | последнее измерение → Нет данных | own SQL и UI1440/320 |
|
||||||
|
| Enabled WireGuard peer | configuration flag → Нет данных | own SQL; handshake не заявлен |
|
||||||
|
|
||||||
|
TypeScript/backend build/ESLint и профильные unit проверки прошли.8 E2E15.7s (2new+6mobile), после последней правки final delta2 passed10s. Один повтор первоначально не запустился из-за Windows sandbox/SWC access denied; корректный разрешённый запуск прошёл, это не UI failure.
|
||||||
|
|
||||||
|
[Own PG SQL](ux-ui/acceptance/traffic-status-sql-2026-10-12.json):48 cases9counter+14history+10TCP SSE+6volume+9status,36163ms, без unavailable skip. Новые9 проверяют настоящий collector→SQL→GET bindings на локальном HTTP RouterOS fixture: первое измерение, enabled/disabled peer, inactive interface, malformed flag,401,legacy bit,stale и disabled source. OwnDB mm_accept_166_1791818856762/ограниченная role/container удалены, отсутствие проверено. Первый запуск harness имел неверный путь распаковки; тесты не начались, его временная база/роль также удалены; пути исправлены до приведённого успешного результата.
|
||||||
|
|
||||||
|
[Source454/524](ux-ui/acceptance/traffic-status-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-status-compiled-2026-10-12.json) SHA256 parity, обе production builds прошли. Frontend ux-status181 сначала проверен с backend ux-volume180 [interop](ux-ui/acceptance/traffic-status-interop-2026-10-12.json); затем [both ux-status181](ux-ui/acceptance/traffic-status-runtime-2026-10-12.json) healthy/running/imageIds. Только .env tags, rollback ux-volume180, без копирования secret env и новой миграции.
|
||||||
|
|
||||||
|
[Native GET](ux-ui/acceptance/traffic-status-native-2026-10-12.json): оба CHR доступны, interface counters и boolean flags валидны; DB read-only, RouterOS config writes0/password changes0/messages0. [Browser1440/320](ux-ui/acceptance/traffic-status-deployed-2026-10-12.json): actual API/live/receipt labels без ошибок/overflow. Положительные binding branches доказаны собственными SQL/UI fixtures, не выданы за существующие пользовательские привязки стенда. Вкладку следует перезагрузить.
|
||||||
|
|
||||||
|
Следующий182 — полнота peer-среза при optional endpoint failure и пропуски истории. Interface running не доказывает end-to-end connectivity, enabled peer не доказывает handshake. Coverage/recreated identity/restore/multi-instance/scale/external integrations остаются открыты в12.5. Локальный коммит без push.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Тестовый сервер UX/UI — 2026-10-10
|
# Тестовый сервер UX/UI — 2026-10-10
|
||||||
|
|
||||||
Сервер161.104.46.222: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
Сервер161.104.46.222: завершён12.5.181 — подтверждённые свежие статусы привязок, strict native boolean flags и отдельный unknown для enabled WireGuard peer. OwnSQL48/36163ms,8E2E/15.7s+final delta2/10s/types/build/ESLint/1036compiled parity. Both ux-status181 healthy/running/native2/browser2; ownDB/role/container удалены. Следующий182 — полнота peer-среза и история пропусков;12.5 открыта.
|
||||||
|
|
||||||
| Параметр | Проверенное состояние |
|
| Параметр | Проверенное состояние |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
|
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
|
||||||
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
|
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
|
||||||
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
|
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
|
||||||
| Образы приложения | mmapp-test/backend:ux-volume180 и mmapp-test/frontend:ux-volume180; запущены |
|
| Образы приложения | mmapp-test/backend:ux-status181 и mmapp-test/frontend:ux-status181; запущены |
|
||||||
|
|
||||||
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
|
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
|
||||||
|
|
||||||
@@ -1822,3 +1822,27 @@ Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP S
|
|||||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||||
|
|
||||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||||
|
|
||||||
|
## 12.5.181 — достоверность статусов привязанных интерфейсов (2026-10-12)
|
||||||
|
|
||||||
|
Статус привязки теперь требует свежего успешного опроса текущей конфигурации устройства и подтверждённых native флагов. Ошибка, pending, disabled source, истёкший TTL или legacy row без status-known marker дают «Нет данных». Первое корректное измерение интерфейса может подтвердить его running, даже когда ещё недостаточно соседних counters для скорости. История и объёмы сохраняются.
|
||||||
|
|
||||||
|
Producer строго принимает true/false и native строки true/false/yes/no; missing/invalid flags больше не подменяются false. Новый бит8 status-known добавляется только новым успешным измерениям; bit4 counter-known не меняется, legacy rows не переписываются. Interface inventory проверяет имена/дубликаты. Включённый WG peer означает конфигурацию, поэтому даёт unknown без handshake proof; disabled peer — «Неактивен». Это не проверка реального VPN-туннеля.
|
||||||
|
|
||||||
|
DTO привязок содержит statusObservedAt/statusExpiresAt. Frontend проверяет provenance и TTL, обновляет состояние по своему clock даже без повторной загрузки; прежний payload без дат не считается подтверждением. Общие ReUI layout/base-nova и компоненты сохранены.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Native running + свежий успешный poll | native flag → known marker → TTL → Работает | own SQL и UI1440/320 |
|
||||||
|
| Ошибка / старый flag / истёкший TTL | последнее измерение → Нет данных | own SQL и UI1440/320 |
|
||||||
|
| Enabled WireGuard peer | configuration flag → Нет данных | own SQL; handshake не заявлен |
|
||||||
|
|
||||||
|
TypeScript/backend build/ESLint и профильные unit проверки прошли.8 E2E15.7s (2new+6mobile), после последней правки final delta2 passed10s. Один повтор первоначально не запустился из-за Windows sandbox/SWC access denied; корректный разрешённый запуск прошёл, это не UI failure.
|
||||||
|
|
||||||
|
[Own PG SQL](ux-ui/acceptance/traffic-status-sql-2026-10-12.json):48 cases9counter+14history+10TCP SSE+6volume+9status,36163ms, без unavailable skip. Новые9 проверяют настоящий collector→SQL→GET bindings на локальном HTTP RouterOS fixture: первое измерение, enabled/disabled peer, inactive interface, malformed flag,401,legacy bit,stale и disabled source. OwnDB mm_accept_166_1791818856762/ограниченная role/container удалены, отсутствие проверено. Первый запуск harness имел неверный путь распаковки; тесты не начались, его временная база/роль также удалены; пути исправлены до приведённого успешного результата.
|
||||||
|
|
||||||
|
[Source454/524](ux-ui/acceptance/traffic-status-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-status-compiled-2026-10-12.json) SHA256 parity, обе production builds прошли. Frontend ux-status181 сначала проверен с backend ux-volume180 [interop](ux-ui/acceptance/traffic-status-interop-2026-10-12.json); затем [both ux-status181](ux-ui/acceptance/traffic-status-runtime-2026-10-12.json) healthy/running/imageIds. Только .env tags, rollback ux-volume180, без копирования secret env и новой миграции.
|
||||||
|
|
||||||
|
[Native GET](ux-ui/acceptance/traffic-status-native-2026-10-12.json): оба CHR доступны, interface counters и boolean flags валидны; DB read-only, RouterOS config writes0/password changes0/messages0. [Browser1440/320](ux-ui/acceptance/traffic-status-deployed-2026-10-12.json): actual API/live/receipt labels без ошибок/overflow. Положительные binding branches доказаны собственными SQL/UI fixtures, не выданы за существующие пользовательские привязки стенда. Вкладку следует перезагрузить.
|
||||||
|
|
||||||
|
Следующий182 — полнота peer-среза при optional endpoint failure и пропуски истории. Interface running не доказывает end-to-end connectivity, enabled peer не доказывает handshake. Coverage/recreated identity/restore/multi-instance/scale/external integrations остаются открыты в12.5. Локальный коммит без push.
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"runtime": {
|
||||||
|
"backend": "ux-status181",
|
||||||
|
"frontend": "ux-status181"
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"actualLiveSample": true,
|
||||||
|
"backendReceiptLabel": true,
|
||||||
|
"pageErrors": 0,
|
||||||
|
"noOverflow": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"width": 320,
|
||||||
|
"trafficServers200": true,
|
||||||
|
"attemptProvenanceVisible": true,
|
||||||
|
"actualLiveSample": true,
|
||||||
|
"backendReceiptLabel": 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"newFrontend": "ux-status181",
|
||||||
|
"oldBackend": "ux-volume180",
|
||||||
|
"readerAccepted": true,
|
||||||
|
"serverCount": 2,
|
||||||
|
"pageErrors": 0,
|
||||||
|
"bound": "Additive observed DTO reader compatibility; legacy numeric-only paths verified separately as unknown"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"version": "1.0.0-test.status181",
|
||||||
|
"readOnlyDatabase": true,
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"sourceId": 1,
|
||||||
|
"target": "194.226.121.150",
|
||||||
|
"identityRead": true,
|
||||||
|
"interfaceRead": true,
|
||||||
|
"interfaceCount": 2,
|
||||||
|
"countersValid": true,
|
||||||
|
"nativeBooleanFlagsValidated": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sourceId": 2,
|
||||||
|
"target": "104.165.244.7",
|
||||||
|
"identityRead": true,
|
||||||
|
"interfaceRead": true,
|
||||||
|
"interfaceCount": 2,
|
||||||
|
"countersValid": true,
|
||||||
|
"nativeBooleanFlagsValidated": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dbWrites": 0,
|
||||||
|
"routerConfigWrites": 0,
|
||||||
|
"messages": 0,
|
||||||
|
"migrationApplied": true,
|
||||||
|
"passed": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"secretEnvCopied": false,
|
||||||
|
"rollbackTags": {
|
||||||
|
"MMAPP_IMAGE_TAG": "ux-volume180",
|
||||||
|
"BACKEND_IMAGE_TAG": "ux-volume180"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"backend": {
|
||||||
|
"image": "mmapp-test/backend:ux-status181",
|
||||||
|
"imageId": "sha256:28dffe2d6c13e3b34ef6f3b9ece3de051f84b9688f5a47fc67e96ae8cafd6d9e",
|
||||||
|
"state": "running",
|
||||||
|
"health": "healthy",
|
||||||
|
"matchedVerifiedImage": true
|
||||||
|
},
|
||||||
|
"frontend": {
|
||||||
|
"image": "mmapp-test/frontend:ux-status181",
|
||||||
|
"imageId": "sha256:6d0930bd7d656dea9e81b88d0a7333758d6a1f35dc4e9833b5d89d74bfa81bd5",
|
||||||
|
"state": "running",
|
||||||
|
"health": null,
|
||||||
|
"matchedVerifiedImage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"matched": {
|
||||||
|
"backend": 454,
|
||||||
|
"frontend": 524
|
||||||
|
},
|
||||||
|
"different": [],
|
||||||
|
"missing": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"passed": true,
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"date": "2026-10-12",
|
||||||
|
"block": "12.5.181",
|
||||||
|
"database": "mm_accept_166_1791818856762",
|
||||||
|
"scope": "ephemeral role and precreated owned database; no copied production credentials",
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"command": "npm --prefix backend run test:traffic-rate",
|
||||||
|
"passed": true,
|
||||||
|
"durationMs": 36163,
|
||||||
|
"ownedTrafficSqlCases": 9,
|
||||||
|
"ownedHistorySqlCases": 14,
|
||||||
|
"ownedLiveSqlCases": 10,
|
||||||
|
"ownedVolumeSqlCases": 6,
|
||||||
|
"ownedStatusSqlCases": 9,
|
||||||
|
"postgresUnavailableSkip": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopedRoleVerified": true,
|
||||||
|
"passed": true,
|
||||||
|
"databaseRemoved": true,
|
||||||
|
"roleRemoved": true,
|
||||||
|
"testContainerRemoved": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { projectTrafficBindingStatus } from "./traffic-binding-status.js"
|
||||||
|
const at=Date.parse("2026-10-12T12:00:00Z"),s=new Date(at).toISOString(),expiry=new Date(at+90_000).toISOString()
|
||||||
|
assert.equal(projectTrafficBindingStatus("online",s,expiry,at),"online")
|
||||||
|
assert.equal(projectTrafficBindingStatus("offline",s,expiry,at),"offline")
|
||||||
|
assert.equal(projectTrafficBindingStatus("online",undefined,undefined,at),"unknown")
|
||||||
|
assert.equal(projectTrafficBindingStatus("online",s,expiry,at+90_001),"unknown")
|
||||||
|
assert.equal(projectTrafficBindingStatus("online",s,"bad",at),"unknown")
|
||||||
|
console.log("binding status legacy/expiry projection tests ok")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function projectTrafficBindingStatus(status: unknown, observedAt: unknown, expiresAt: unknown, now = Date.now()): "online" | "offline" | "unknown" {
|
||||||
|
if ((status !== "online" && status !== "offline") || typeof observedAt !== "string" || typeof expiresAt !== "string") return "unknown"
|
||||||
|
const observed = Date.parse(observedAt), expires = Date.parse(expiresAt)
|
||||||
|
if (!Number.isFinite(observed) || !Number.isFinite(expires) || observed >= expires || now > expires || observed > now + (expires-observed)/3) return "unknown"
|
||||||
|
return status
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import {test,expect} from "@playwright/test"
|
||||||
|
import {observedTrafficFixturePayload} from "./traffic-observation-fixture"
|
||||||
|
for(const width of[1440,320])test(`Статус интерфейса: fresh/expired/legacy ${width}`,async({page})=>{
|
||||||
|
const at=new Date("2026-10-12T12:00:00Z");await page.clock.install({time:at});await page.setViewportSize({width,height:1000});let legacy=false
|
||||||
|
const iface=()=>({id:"i1",bindingId:"b1",userId:"u1",userLogin:"user",userName:"User",interfaceName:"ether1",interfaceType:"ether",comment:"",serverId:"1",serverName:"CHR",serverSite:"TEST",serverCountry:"RU",status:"online",...(!legacy?{statusObservedAt:at.toISOString(),statusExpiresAt:new Date(at.getTime()+90_000).toISOString()}:{}),rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal:0,txTotal:0,rxSeries:[0,0],txSeries:[0,0]})
|
||||||
|
await page.route("**/health",r=>r.fulfill({json:{ok:true}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;return r.fulfill({json:observedTrafficFixturePayload(path==="/api/auth/config"?{required:false}:path==="/api/traffic/servers"?{servers:[]}:path==="/api/traffic/users"?{users:[{id:"u1",login:"user",displayName:"User",role:"viewer",interfaces:[iface()],rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal:0,txTotal:0,rxSeries:[0,0],txSeries:[0,0]}]}:path==="/api/traffic/bound-interfaces"?{interfaces:[iface()]}:path==="/api/servers"?[]:{})})})
|
||||||
|
await page.goto("/traffic");await page.getByRole("group",{name:"Разделы трафика"}).getByRole("button",{name:"Клиенты",exact:true}).click()
|
||||||
|
await expect(page.getByText("Работает",{exact:true})).toBeVisible();await page.clock.fastForward(100_001)
|
||||||
|
await expect(page.getByText("Работает",{exact:true})).toHaveCount(0);await expect(page.getByText("Нет данных",{exact:true})).toBeVisible()
|
||||||
|
legacy=true;await page.getByRole("button",{name:"15м",exact:true}).click();await expect(page.getByText("Нет данных",{exact:true})).toBeVisible()
|
||||||
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user