fix(traffic): подтверждать идентичность соседних счётчиков
This commit is contained in:
1 parent
493c30bebc
commit
b57ae316ba
27 files changed
+1444
-36
No files matched your search
@@ -0,0 +1 @@
|
||||
ALTER TABLE traffic_samples ADD COLUMN IF NOT EXISTS native_counter_identity TEXT;
|
||||
@@ -19,7 +19,7 @@
|
||||
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
|
||||
"test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts",
|
||||
"test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts",
|
||||
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-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 && tsx src/services/traffic-gap.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 && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts",
|
||||
"test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
|
||||
"test:users": "tsx src/modules/users/iface-type.test.ts && tsx src/modules/users/bindings.test.ts && tsx src/modules/users/interface-inventory.test.ts && tsx src/routes/users-catalog.test.ts",
|
||||
"test:pg": "tsx src/db/pg-pool.test.ts && tsx src/db/sql-bind.test.ts && tsx src/db/sqlite-json.test.ts && tsx src/db/traffic-flags.test.ts && tsx src/db/pg-schema.test.ts && tsx src/services/config-revisions.test.ts",
|
||||
|
||||
@@ -310,6 +310,7 @@ export const flowAsnMeta = pgTable("flow_asn_meta", {
|
||||
})
|
||||
|
||||
export const trafficSamples = pgTable("traffic_samples", {
|
||||
nativeCounterIdentity: text("native_counter_identity"),
|
||||
serverId: intPkRef().references(() => servers.id, { onDelete: "cascade" }),
|
||||
interfaceName: text("interface_name").notNull(),
|
||||
peerPublicKey: text("peer_public_key").notNull().default(""),
|
||||
|
||||
@@ -2,11 +2,19 @@ export const TRAFFIC_FLAG_RUNNING = 1
|
||||
export const TRAFFIC_FLAG_DISABLED = 2
|
||||
export const TRAFFIC_FLAG_COUNTERS_KNOWN = 4
|
||||
export const TRAFFIC_FLAG_STATUS_KNOWN = 8
|
||||
export const TRAFFIC_FLAG_CONTINUITY_KNOWN = 16
|
||||
export const TRAFFIC_FLAG_CONTINUITY_RECORDED = 32
|
||||
|
||||
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false, statusKnown = false): number {
|
||||
export function encodeTrafficFlags(running: boolean, disabled: boolean, countersKnown = false, statusKnown = false, continuityKnown?: boolean): number {
|
||||
return (running ? TRAFFIC_FLAG_RUNNING : 0) | (disabled ? TRAFFIC_FLAG_DISABLED : 0)
|
||||
| (countersKnown ? TRAFFIC_FLAG_COUNTERS_KNOWN : 0)
|
||||
| (statusKnown ? TRAFFIC_FLAG_STATUS_KNOWN : 0)
|
||||
| (continuityKnown === undefined ? 0 : TRAFFIC_FLAG_CONTINUITY_RECORDED | (continuityKnown ? TRAFFIC_FLAG_CONTINUITY_KNOWN : 0))
|
||||
}
|
||||
|
||||
/** Undefined retains the documented legacy boundary; new producers explicitly mark both outcomes. */
|
||||
export function trafficCounterContinuity(flags: number): boolean | undefined {
|
||||
return flags & TRAFFIC_FLAG_CONTINUITY_RECORDED ? !!(flags & TRAFFIC_FLAG_CONTINUITY_KNOWN) : undefined
|
||||
}
|
||||
|
||||
export function trafficFlagRunning(flags: number | null | undefined): boolean {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { and, asc, desc, eq, gte } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers, trafficSamples, trafficSettings } from "../db/schema.js"
|
||||
import { decodeTrafficSampleFlags, encodeTrafficFlags } from "../db/traffic-flags.js"
|
||||
import { decodeTrafficSampleFlags, encodeTrafficFlags, trafficCounterContinuity } from "../db/traffic-flags.js"
|
||||
import type { TrafficRunSnapshot } from "../types/scheduler-run-snapshot.js"
|
||||
import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { bpsToMbps, shouldIncludeIface } from "./traffic-rate.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag, nativeTrafficCounterIdentity } from "./traffic-observation.js"
|
||||
import { rememberServerIfaces } from "./traffic-flow-ifindex.js"
|
||||
import { recordTrafficCollectionAttempt } from "./traffic-collection-state.js"
|
||||
import { parseTrafficPeers } from "./traffic-peer-observation.js"
|
||||
@@ -38,14 +38,15 @@ function waveKey(interfaceName: string, peerPublicKey = ""): string {
|
||||
}
|
||||
|
||||
function sampleRate(
|
||||
prevWave: Map<string, { rxBytes: number; txBytes: number; sampledAt: string }>,
|
||||
prevWave: Map<string, { rxBytes: number; txBytes: number; sampledAt: string; nativeCounterIdentity: string|null }>,
|
||||
key: string,
|
||||
rxBytes: number,
|
||||
txBytes: number,
|
||||
nowMs: number,
|
||||
maxIntervalMs: number,
|
||||
identity: string|null,
|
||||
): { rxBps: number; txBps: number; measured: boolean } {
|
||||
return observedTrafficRate(prevWave.get(key), rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
return observedTrafficRate(identity !== null && prevWave.get(key)?.nativeCounterIdentity === identity ? prevWave.get(key) : undefined, rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
}
|
||||
|
||||
export interface TrafficCollectorState {
|
||||
@@ -75,7 +76,7 @@ async function getSettingsRow() {
|
||||
return (await db.select().from(trafficSettings).where(eq(trafficSettings.id, 1)).limit(1))[0]
|
||||
}
|
||||
|
||||
async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes: number; txBytes: number; sampledAt: string }>> {
|
||||
async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes: number; txBytes: number; sampledAt: string; nativeCounterIdentity: string|null }>> {
|
||||
const last = (await db
|
||||
.select({ sampledAt: trafficSamples.sampledAt })
|
||||
.from(trafficSamples)
|
||||
@@ -90,6 +91,7 @@ async function readPreviousWave(serverId: number): Promise<Map<string, { rxBytes
|
||||
rxBytes: trafficSamples.rxBytes,
|
||||
txBytes: trafficSamples.txBytes,
|
||||
flags: trafficSamples.flags,
|
||||
nativeCounterIdentity: trafficSamples.nativeCounterIdentity,
|
||||
sampledAt: trafficSamples.sampledAt,
|
||||
})
|
||||
.from(trafficSamples)
|
||||
@@ -144,11 +146,13 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
let totalsMeasured = true
|
||||
const rows = ifaces.map((i) => {
|
||||
const interfaceName = i.name!
|
||||
const nativeCounterIdentity = nativeTrafficCounterIdentity(srv.updatedAt,i[".id"])
|
||||
const continuityKnown = nativeCounterIdentity !== null && prevWave.get(waveKey(interfaceName))?.nativeCounterIdentity === nativeCounterIdentity
|
||||
const rxBytes = toNum(i["rx-byte"])
|
||||
const txBytes = toNum(i["tx-byte"])
|
||||
const running = parseTrafficFlag(i.running)
|
||||
const disabled = parseTrafficFlag(i.disabled)
|
||||
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||
const { rxBps, txBps, measured } = sampleRate(prevWave, waveKey(interfaceName), rxBytes, txBytes, nowMs, maxIntervalMs, nativeCounterIdentity)
|
||||
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
||||
includedInterfaces++
|
||||
totalsMeasured &&= measured
|
||||
@@ -164,7 +168,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
rxBps: Math.round(rxBps),
|
||||
txBps: Math.round(txBps),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true, continuityKnown),
|
||||
nativeCounterIdentity,
|
||||
}
|
||||
})
|
||||
let peerWarning: string | undefined
|
||||
@@ -172,6 +177,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
const peers = await client.get<RosWgPeerTraffic[]>("/interface/wireguard/peers")
|
||||
for (const p of parseTrafficPeers(peers)) {
|
||||
const { interfaceName, peerPublicKey, rxBytes, txBytes, disabled } = p
|
||||
const nativeCounterIdentity = nativeTrafficCounterIdentity(srv.updatedAt,p.nativeId)
|
||||
const continuityKnown = nativeCounterIdentity !== null && prevWave.get(waveKey(interfaceName,peerPublicKey))?.nativeCounterIdentity === nativeCounterIdentity
|
||||
const running = !disabled
|
||||
const { rxBps, txBps } = sampleRate(
|
||||
prevWave,
|
||||
@@ -180,6 +187,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
nowMs,
|
||||
maxIntervalMs,
|
||||
nativeCounterIdentity,
|
||||
)
|
||||
rows.push({
|
||||
serverId: srv.id,
|
||||
@@ -190,7 +198,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
||||
txBytes,
|
||||
rxBps: Math.round(rxBps),
|
||||
txBps: Math.round(txBps),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true),
|
||||
flags: encodeTrafficFlags(running, disabled, true, true, continuityKnown),
|
||||
nativeCounterIdentity,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -292,5 +301,5 @@ export async function readServerSamplesInRange(serverId: number, sinceIso: strin
|
||||
gte(trafficSamples.sampledAt, sinceIso),
|
||||
))
|
||||
.orderBy(asc(trafficSamples.sampledAt))
|
||||
return rows.map((r) => ({ ...r, ...decodeTrafficSampleFlags(r.flags) }))
|
||||
return rows.map((r) => ({ ...r, ...decodeTrafficSampleFlags(r.flags), continuityKnown: trafficCounterContinuity(r.flags) }))
|
||||
}
|
||||
@@ -16,7 +16,7 @@ else {
|
||||
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
|
||||
const prefix="own-history178-"+Date.now()+"-",userId=prefix+"user"
|
||||
let serverId:number|undefined,deny=false,counter="1000",cases=0,requests=0
|
||||
const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;res.end('{"message":"Unauthorized"}')}else res.end(JSON.stringify(req.url?.includes("/wireguard/")?[]:[{name:"ether1",running:"true",disabled:"false","rx-byte":counter,"tx-byte":counter}]))})
|
||||
const http=createServer((req,res)=>{requests++;res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;res.end('{"message":"Unauthorized"}')}else res.end(JSON.stringify(req.url?.includes("/wireguard/")?[]:[{".id":"*1",name:"ether1",running:"true",disabled:"false","rx-byte":counter,"tx-byte":counter}]))})
|
||||
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 get=async(path:string)=>{const r=await app.inject({method:"GET",url:path});assert.equal(r.statusCode,200);return r.json()}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {nativeTrafficCounterIdentity} from './traffic-observation.js'
|
||||
import {buildTrafficFromSamples} from './traffic-rate.js'
|
||||
import {encodeTrafficFlags,trafficCounterContinuity} from '../db/traffic-flags.js'
|
||||
const at=Date.parse('2026-10-13T00:00:00Z'),iso=new Date(at).toISOString(),identity=nativeTrafficCounterIdentity(iso,'*a')!
|
||||
assert.equal(identity,nativeTrafficCounterIdentity(new Date(at),'*A'))
|
||||
for(const id of[undefined,null,'','bad','*z',{},1])assert.equal(nativeTrafficCounterIdentity(iso,id),null)
|
||||
assert.equal(trafficCounterContinuity(13),undefined);assert.equal(trafficCounterContinuity(encodeTrafficFlags(true,false,true,true,false)),false);assert.equal(trafficCounterContinuity(encodeTrafficFlags(true,false,true,true,true)),true)
|
||||
const first={interfaceName:'ether1',sampledAt:new Date(at-30_000).toISOString(),rxBytes:1000,txBytes:1000,rxBps:0,txBps:0,running:true,disabled:false,countersKnown:true,nativeCounterIdentity:identity,continuityKnown:false}
|
||||
for(const nextIdentity of[identity,nativeTrafficCounterIdentity(iso,'*b'),nativeTrafficCounterIdentity(new Date(at+1),'*a'),null]){
|
||||
const cur={...first,sampledAt:iso,rxBytes:2000,txBytes:2000,nativeCounterIdentity:nextIdentity,continuityKnown:nextIdentity===identity}
|
||||
const built=buildTrafficFromSamples([first,cur],at-30_000,at,undefined,undefined,true)
|
||||
assert.equal(built.observations!.rxNow,nextIdentity===identity);assert.equal(built.rxTotalGiB,nextIdentity===identity?1000/1024**3:0)
|
||||
}
|
||||
console.log('native counter identity/config/legacy marker/pair guards passed')
|
||||
@@ -15,8 +15,8 @@ else {
|
||||
let serverId: number | undefined, counters: unknown = "1000", second = false, cases = 0, requests = 0
|
||||
const http = createServer((req, res) => {
|
||||
requests++; res.setHeader("Content-Type", "application/json")
|
||||
const rows = [{ name: "ether1", running: "true", disabled: "false", "rx-byte": counters, "tx-byte": counters }]
|
||||
if (second) rows.push({ name: "ether2", running: "true", disabled: "false", "rx-byte": "1000", "tx-byte": "1000" })
|
||||
const rows = [{ ".id":"*1",name: "ether1", running: "true", disabled: "false", "rx-byte": counters, "tx-byte": counters }]
|
||||
if (second) rows.push({ ".id":"*2",name: "ether2", running: "true", disabled: "false", "rx-byte": "1000", "tx-byte": "1000" })
|
||||
res.end(JSON.stringify(req.url?.includes("/wireguard/") ? [] : rows))
|
||||
})
|
||||
await new Promise<void>(resolve => http.listen(0, "127.0.0.1", resolve))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {parseTrafficPeers} from "./traffic-peer-observation.js"
|
||||
import assert from "node:assert/strict"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag } from "./traffic-observation.js"
|
||||
import { observedTrafficRate, parseTrafficCounter, parseTrafficFlag, nativeTrafficCounterIdentity } from "./traffic-observation.js"
|
||||
import { collectSourceSignals } from "./alert-engine/collector-source-signals.js"
|
||||
import fs from "node:fs"
|
||||
import vm from "node:vm"
|
||||
@@ -38,18 +39,18 @@ const ast = ts.createSourceFile("traffic-collector.ts", collector, ts.ScriptTarg
|
||||
const functions = ast.statements.filter(n => ts.isFunctionDeclaration(n) && n.name &&
|
||||
["waveKey", "sampleRate", "toNum", "collectTrafficOnce"].includes(n.name.text)).map(n => n.getText(ast)).join("\n")
|
||||
assert.equal(ast.statements.filter(n => ts.isFunctionDeclaration(n) && n.name && n.name.text === "collectTrafficOnce").length, 1)
|
||||
let inserted: unknown[] = [], wave = new Map<string, typeof previous>(), counters: unknown = "1000"
|
||||
let inserted: unknown[] = [], wave = new Map<string, typeof previous & {nativeCounterIdentity?:string|null}>(), counters: unknown = "1000"
|
||||
let included = true
|
||||
const context = vm.createContext({
|
||||
Date, observedTrafficRate, parseTrafficCounter, parseTrafficFlag, bpsToMbps, shouldIncludeIface,
|
||||
Date, nativeTrafficCounterIdentity,parseTrafficPeers, observedTrafficRate, parseTrafficCounter, parseTrafficFlag, bpsToMbps, shouldIncludeIface,
|
||||
SCHEDULER_RUN_SNAPSHOT_VERSION: 1, servers: {}, trafficSamples: {}, trafficSettings: {}, eq: () => ({}),
|
||||
getSettingsRow: async () => ({}), readPreviousWave: async () => wave, rememberServerIfaces: () => {},
|
||||
encodeTrafficFlags: () => 1,
|
||||
recordTrafficCollectionAttempt: async () => {},
|
||||
MikrotikClient: { fromServer: () => ({ get: async (path: string) => path === "/interface" ?
|
||||
[{ name: "ether1", running: String(included), disabled: "false", "rx-byte": counters, "tx-byte": counters }] : [] }) },
|
||||
[{ ".id":"*1",name: "ether1", running: String(included), disabled: "false", "rx-byte": counters, "tx-byte": counters }] : [] }) },
|
||||
db: {
|
||||
select: () => ({ from: () => ({ where: async () => [{ id: 1, name: "audit", host: "audit.example", enabled: true }] }) }),
|
||||
select: () => ({ from: () => ({ where: async () => [{ updatedAt:iso,id: 1, name: "audit", host: "audit.example", enabled: true }] }) }),
|
||||
insert: () => ({ values: async (rows: unknown[]) => { inserted.push(...rows) } }),
|
||||
update: () => ({ set: () => ({ where: async () => {} }) }),
|
||||
},
|
||||
@@ -58,7 +59,7 @@ vm.runInContext(ts.transpileModule("let collecting=false;\n" + functions.replace
|
||||
"\nglobalThis.run=collectTrafficOnce;", { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText, context)
|
||||
let snapshot = await context.run()
|
||||
assert.equal(snapshot.servers[0].ok, true); assert.equal(snapshot.servers[0].sumRxMbps, undefined); assert.equal(inserted.length, 1)
|
||||
wave = new Map([["ether1\0", { ...previous, sampledAt: new Date(Date.now() - 30_000).toISOString() }]])
|
||||
wave = new Map([["ether1\0", { ...previous, nativeCounterIdentity:nativeTrafficCounterIdentity(iso,"*1"),sampledAt: new Date(Date.now() - 30_000).toISOString() }]])
|
||||
snapshot = await context.run(); assert.equal(snapshot.servers[0].sumRxMbps, 0)
|
||||
counters = "10"; snapshot = await context.run(); assert.equal(snapshot.servers[0].sumRxMbps, undefined)
|
||||
counters = "bad"; inserted = []; snapshot = await context.run()
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { rateBpsFromDelta } from "./traffic-rate.js"
|
||||
export function nativeTrafficCounterIdentity(updatedAt: string | Date, nativeId: unknown): string | null {
|
||||
const at = updatedAt instanceof Date ? updatedAt.getTime() : Date.parse(updatedAt)
|
||||
if (!Number.isFinite(at) || typeof nativeId !== "string" || !/^\*[0-9a-f]+$/i.test(nativeId)) return null
|
||||
return JSON.stringify([new Date(at).toISOString(), nativeId.toUpperCase()])
|
||||
}
|
||||
export function parseTrafficFlag(raw: unknown): boolean {
|
||||
if (raw === true || raw === "true" || raw === "yes") return true
|
||||
if (raw === false || raw === "false" || raw === "no") return false
|
||||
|
||||
@@ -13,6 +13,6 @@ export function parseTrafficPeers(raw: unknown) {
|
||||
const key = `${interfaceName}\0${peerPublicKey}`
|
||||
if (seen.has(key)) throw new Error("Повторное измерение WireGuard peer")
|
||||
seen.add(key)
|
||||
return { interfaceName, peerPublicKey, rxBytes: parseTrafficCounter(p.rx), txBytes: parseTrafficCounter(p.tx), disabled: parseTrafficFlag(p.disabled) }
|
||||
return { interfaceName, peerPublicKey, nativeId: p[".id"], rxBytes: parseTrafficCounter(p.rx), txBytes: parseTrafficCounter(p.tx), disabled: parseTrafficFlag(p.disabled) }
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export interface TrafficSampleLike {
|
||||
running: boolean
|
||||
disabled: boolean
|
||||
countersKnown?: boolean
|
||||
continuityKnown?: boolean
|
||||
nativeCounterIdentity?: string | null
|
||||
}
|
||||
|
||||
export interface RatePoint {
|
||||
@@ -180,7 +182,8 @@ export function buildTrafficFromSamples(
|
||||
const t0 = parseIsoMs(prev.sampledAt)
|
||||
const t1 = parseIsoMs(cur.sampledAt)
|
||||
if(Number.isFinite(Date.parse(cur.sampledAt))&&t1>=rangeStartMs&&t1<=rangeEndMs)lastAttemptAt=Math.max(lastAttemptAt??t1,t1)
|
||||
const countersKnown = prev.countersKnown !== false && cur.countersKnown !== false
|
||||
const sameIdentity = (prev.nativeCounterIdentity ?? null) === (cur.nativeCounterIdentity ?? null)
|
||||
const countersKnown = prev.countersKnown !== false && cur.countersKnown !== false && cur.continuityKnown !== false && sameIdentity
|
||||
const rxBps = countersKnown ? rateBpsFromDelta(prev.rxBytes, cur.rxBytes, t0, t1) : null
|
||||
const txBps = countersKnown ? rateBpsFromDelta(prev.txBytes, cur.txBytes, t0, t1) : null
|
||||
if (t1 >= rangeStartMs && t1 <= rangeEndMs) {
|
||||
|
||||
@@ -9,8 +9,8 @@ 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,peerVariant="valid",denyPeer=false
|
||||
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny || denyPeer && req.url?.includes("/wireguard/")){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:peerDisabled},...(peerVariant==="partial"?[{interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{name:"ether1",running:flag,disabled:false,"rx-byte":"1000","tx-byte":"1000"}]))})
|
||||
const prefix="own-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false,peerVariant="valid",denyPeer=false,ifaceId:unknown="*1",counterValue="1000",peerId="*2",peerCounter="1000"
|
||||
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny || denyPeer && req.url?.includes("/wireguard/")){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{".id":peerId,interface:"wg1","public-key":"fixturekey",rx:peerCounter,tx:peerCounter,disabled:peerDisabled},...(peerVariant==="partial"?[{".id":peerId,interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{".id":ifaceId,name:"ether1",running:flag,disabled:false,"rx-byte":counterValue,"tx-byte":counterValue}]))})
|
||||
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{
|
||||
@@ -39,6 +39,14 @@ else{
|
||||
await pool.query("UPDATE traffic_samples SET sampled_at=$1 WHERE server_id=$2",[new Date(Date.now()-120_000).toISOString(),serverId]);
|
||||
let gap=await collectTrafficOnce();assert.equal(gap.servers[0].ok,true);assert.equal(gap.servers[0].sumRxMbps,undefined);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,false);
|
||||
gap=await collectTrafficOnce();assert.equal(gap.servers[0].sumRxMbps,0);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,true);cases++;
|
||||
for(const change of["native-id","config","missing-id"]){
|
||||
counterValue=String(Number(counterValue)+1000);
|
||||
if(change==="native-id")ifaceId="*10";else if(change==="missing-id")ifaceId=undefined;else await pool.query("UPDATE servers SET updated_at=$1 WHERE id=$2",[new Date().toISOString(),serverId]);
|
||||
const switched=await collectTrafficOnce();assert.equal(switched.servers[0].sumRxMbps,undefined);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,false);assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").rxTotal,0);cases++;
|
||||
if(change!=="missing-id"){const next=await collectTrafficOnce();assert.equal(next.servers[0].sumRxMbps,0);rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.rxNow,true)}
|
||||
}
|
||||
ifaceId="*10";await collectTrafficOnce();await collectTrafficOnce();peerId="*20";peerCounter="2000";await collectTrafficOnce();rows=await bindings();let peer=rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1");assert.equal(peer.observations.rxNow,false);assert.equal(peer.rxTotal,0);
|
||||
await collectTrafficOnce();rows=await bindings();peer=rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1");assert.equal(peer.observations.rxNow,true);assert.equal(peer.rxNow,0);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()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
|
||||
|
||||
@@ -696,7 +696,7 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
|
||||
@@ -2202,3 +2202,25 @@ Unit threshold/invalid interval/merge/collector guards, backend/application Type
|
||||
[Source456/524](ux-ui/acceptance/traffic-gap-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-gap-compiled-2026-10-13.json) SHA256 parity. Backend production ux-gap185 [healthy/imageIds](ux-ui/acceptance/traffic-gap-runtime-2026-10-13.json), frontend ux-warning183 без исходных изменений/повторной сборки. Только backend.env tag, rollback backend ux-history184/frontend ux-warning183; secret env copy0, migration0. [Actual API](ux-ui/acceptance/traffic-gap-api-2026-10-13.json)/[production UI1440/320](ux-ui/acceptance/traffic-gap-deployed-2026-10-13.json) passed с actual live и backend receipt labels, no errors/overflow. Checker не отправлял новых native commands и не создавал отказ на CHR. Password/config/message changes0.
|
||||
|
||||
Следующий186 — provenance native interface/peer identity и конфигурации источника: positive counter difference по одному прежнему имени не доказывает непрерывность после пересоздания/смены устройства. Старую историю не переписывать по догадке. Общая12.5 и внешние зависимости остаются открыты. Локальный коммит без push.
|
||||
|
||||
## 12.5.186 — подтверждение идентичности пары счётчиков (2026-10-13)
|
||||
|
||||
Имя интерфейса больше не является единственным основанием новой counter pair. Nullable native_counter_identity хранит нормализованные source.updatedAt и native RouterOS.id (*HEX). Producer подтверждает continuity только когда identity непустая и совпадает с предыдущей строкой того же interface/peer key. Новый native ID, конфигурация источника или missing/invalid ID дают первую pair unknown; следующий подтверждённый poll восстанавливает измерение. Для WG используются peer.id и существующий public-key/iface series key, не ID самого интерфейса.
|
||||
|
||||
Биты16/32 различают confirmed continuity и сам факт проверки новым producer; running/disabled/counter/status markers не меняют смысла. Builder исключает неподтверждённую или несовпадающую pair из новых rates/bytes, сохраняя остальные подтверждённые исторические пары. Первый корректный counter остаётся baseline для следующей pair, даже если его собственная предыдущая continuity неизвестна. Legacy rows не переписываются: absent new marker/native identity оставляет прежнюю явно ограниченную историю. Это не доказательство непрерывности legacy, повторного использования того же native ID после перезагрузки или физической доступности.
|
||||
|
||||
| Изменение | Схема результата | Проверка |
|
||||
| --- | --- | --- |
|
||||
| Same name, новый native ID | old counter → unknown boundary → новый baseline → confirmed0 | own SQL interface+peer |
|
||||
| Config version изменилась | не переносить counter pair между источниками → normal next poll | own SQL |
|
||||
| Missing native ID | новые pair unknown, без guessed zero/bytes | own SQL/unit |
|
||||
|
||||
Миграция0012 добавляет nullable TEXT к partitioned traffic_samples, включая существующие partitions; нет массового UPDATE/переписывания flags. Downgrade старого reader может игнорировать новый столбец, migration removal не выполнялась. Реальный backup/restore с новой колонкой — следующий187, пока не принят данным блоком.
|
||||
|
||||
Unit identity/config/legacy-marker/pair guards, backend/application TypeScript и diff checks passed. [Own PG SQL](ux-ui/acceptance/traffic-identity-sql-2026-10-13.json):62 cases9counter+14history+10TCP SSE+10volume+19status/37613ms, no skip.4 новые cases: interface ID replacement/config version/missing ID/peer ID replacement с positive counters, unknown boundary, no extra bytes и последующим measured0. Fixtures явно объявляют native IDs; VM actual-collector unit получил настоящий identity/peer parser dependencies вместо отсутствовавших globals. Первая попытка остановилась до SQL scenarios на прежнем VM fixture; временная база/role удалены. Последний namespace mm_accept_166_1791828561251/role/container удалён, absence verified.
|
||||
|
||||
[Source458/524](ux-ui/acceptance/traffic-identity-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-identity-compiled-2026-10-13.json) SHA256 parity. [Backend ux-identity186](ux-ui/acceptance/traffic-identity-runtime-2026-10-13.json) healthy/imageIds; frontend ux-warning183 без изменений/новой сборки. Only backend.env tag, rollback backend ux-gap185/frontend ux-warning183; additive migration остаётся. Secret env copy0.
|
||||
|
||||
[Native GET](ux-ui/acceptance/traffic-identity-native-2026-10-13.json): оба CHR доступны, interface native IDs/flags/counters валидны;0012 applied. Peers фактически0 — positive peer replacement доказан изолированным fixture, не реальным VPN-туннелем. DB read-only/RouterOS config writes0/password changes0/messages0. [Initial API](ux-ui/acceptance/traffic-identity-api-2026-10-13.json) ожидаемо unknown при первом baseline после нового writer; [штатное восстановление](ux-ui/acceptance/traffic-identity-recovery-2026-10-13.json) подтвердило current/rxKnown/txKnown обоих CHR с30s interval без manual collector invocation. [Production UI1440/320](ux-ui/acceptance/traffic-identity-deployed-2026-10-13.json) actual live/receipt labels/no errors/overflow. Native SSE не изменён; предшествующие6E2E185 UI guard checks не пересчитываются как новые186.
|
||||
|
||||
Следующий187 — own-schema archive restore для nullable native counter identity/continuity flags и старого schema archive с повторным bootstrap migration. Потом отдельная приёмка многопроцессной координации/масштаба/внешних доступов.12.5 открыта. Локальный коммит без push.
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-12
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
|
||||
|
||||
@@ -371,6 +371,6 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
+23
-1
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -38,6 +38,28 @@
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. |
|
||||
|
||||
## 12.5.186 — подтверждение идентичности пары счётчиков (2026-10-13)
|
||||
|
||||
Имя интерфейса больше не является единственным основанием новой counter pair. Nullable native_counter_identity хранит нормализованные source.updatedAt и native RouterOS.id (*HEX). Producer подтверждает continuity только когда identity непустая и совпадает с предыдущей строкой того же interface/peer key. Новый native ID, конфигурация источника или missing/invalid ID дают первую pair unknown; следующий подтверждённый poll восстанавливает измерение. Для WG используются peer.id и существующий public-key/iface series key, не ID самого интерфейса.
|
||||
|
||||
Биты16/32 различают confirmed continuity и сам факт проверки новым producer; running/disabled/counter/status markers не меняют смысла. Builder исключает неподтверждённую или несовпадающую pair из новых rates/bytes, сохраняя остальные подтверждённые исторические пары. Первый корректный counter остаётся baseline для следующей pair, даже если его собственная предыдущая continuity неизвестна. Legacy rows не переписываются: absent new marker/native identity оставляет прежнюю явно ограниченную историю. Это не доказательство непрерывности legacy, повторного использования того же native ID после перезагрузки или физической доступности.
|
||||
|
||||
| Изменение | Схема результата | Проверка |
|
||||
| --- | --- | --- |
|
||||
| Same name, новый native ID | old counter → unknown boundary → новый baseline → confirmed0 | own SQL interface+peer |
|
||||
| Config version изменилась | не переносить counter pair между источниками → normal next poll | own SQL |
|
||||
| Missing native ID | новые pair unknown, без guessed zero/bytes | own SQL/unit |
|
||||
|
||||
Миграция0012 добавляет nullable TEXT к partitioned traffic_samples, включая существующие partitions; нет массового UPDATE/переписывания flags. Downgrade старого reader может игнорировать новый столбец, migration removal не выполнялась. Реальный backup/restore с новой колонкой — следующий187, пока не принят данным блоком.
|
||||
|
||||
Unit identity/config/legacy-marker/pair guards, backend/application TypeScript и diff checks passed. [Own PG SQL](ux-ui/acceptance/traffic-identity-sql-2026-10-13.json):62 cases9counter+14history+10TCP SSE+10volume+19status/37613ms, no skip.4 новые cases: interface ID replacement/config version/missing ID/peer ID replacement с positive counters, unknown boundary, no extra bytes и последующим measured0. Fixtures явно объявляют native IDs; VM actual-collector unit получил настоящий identity/peer parser dependencies вместо отсутствовавших globals. Первая попытка остановилась до SQL scenarios на прежнем VM fixture; временная база/role удалены. Последний namespace mm_accept_166_1791828561251/role/container удалён, absence verified.
|
||||
|
||||
[Source458/524](ux-ui/acceptance/traffic-identity-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-identity-compiled-2026-10-13.json) SHA256 parity. [Backend ux-identity186](ux-ui/acceptance/traffic-identity-runtime-2026-10-13.json) healthy/imageIds; frontend ux-warning183 без изменений/новой сборки. Only backend.env tag, rollback backend ux-gap185/frontend ux-warning183; additive migration остаётся. Secret env copy0.
|
||||
|
||||
[Native GET](ux-ui/acceptance/traffic-identity-native-2026-10-13.json): оба CHR доступны, interface native IDs/flags/counters валидны;0012 applied. Peers фактически0 — positive peer replacement доказан изолированным fixture, не реальным VPN-туннелем. DB read-only/RouterOS config writes0/password changes0/messages0. [Initial API](ux-ui/acceptance/traffic-identity-api-2026-10-13.json) ожидаемо unknown при первом baseline после нового writer; [штатное восстановление](ux-ui/acceptance/traffic-identity-recovery-2026-10-13.json) подтвердило current/rxKnown/txKnown обоих CHR с30s interval без manual collector invocation. [Production UI1440/320](ux-ui/acceptance/traffic-identity-deployed-2026-10-13.json) actual live/receipt labels/no errors/overflow. Native SSE не изменён; предшествующие6E2E185 UI guard checks не пересчитываются как новые186.
|
||||
|
||||
Следующий187 — own-schema archive restore для nullable native counter identity/continuity flags и старого schema archive с повторным bootstrap migration. Потом отдельная приёмка многопроцессной координации/масштаба/внешних доступов.12.5 открыта. Локальный коммит без push.
|
||||
|
||||
## 12.5.185 — доверие текущей скорости после длительного пропуска (2026-10-13)
|
||||
|
||||
Завершает baseline185 от2026-10-12. Builder сообщает measurementIntervalMs последней пары counter samples; для агрегата используется максимальный интервал всех составляющих, отсутствие interval в одной из новых составляющих даёт null. applyTrafficFreshness разрешает текущую counter-derived скорость только при положительном конечном interval≤3×max(5s,configured period). Это тот же запас3периода, что у TTL. При периоде30s граница90s включительно; более длинный или неизвестный интервал даёт unknown и понятную причину. Историческое среднее и подтверждённые counter bytes сохраняются.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-12
|
||||
|
||||
Это актуальная матрица после 12.5.185. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.186. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-gap185`, frontend `ux-warning183`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-identity186`, frontend `ux-warning183`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||
| --- | --- | --- |
|
||||
@@ -1117,3 +1117,25 @@ Unit threshold/invalid interval/merge/collector guards, backend/application Type
|
||||
[Source456/524](ux-ui/acceptance/traffic-gap-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-gap-compiled-2026-10-13.json) SHA256 parity. Backend production ux-gap185 [healthy/imageIds](ux-ui/acceptance/traffic-gap-runtime-2026-10-13.json), frontend ux-warning183 без исходных изменений/повторной сборки. Только backend.env tag, rollback backend ux-history184/frontend ux-warning183; secret env copy0, migration0. [Actual API](ux-ui/acceptance/traffic-gap-api-2026-10-13.json)/[production UI1440/320](ux-ui/acceptance/traffic-gap-deployed-2026-10-13.json) passed с actual live и backend receipt labels, no errors/overflow. Checker не отправлял новых native commands и не создавал отказ на CHR. Password/config/message changes0.
|
||||
|
||||
Следующий186 — provenance native interface/peer identity и конфигурации источника: positive counter difference по одному прежнему имени не доказывает непрерывность после пересоздания/смены устройства. Старую историю не переписывать по догадке. Общая12.5 и внешние зависимости остаются открыты. Локальный коммит без push.
|
||||
|
||||
## 12.5.186 — подтверждение идентичности пары счётчиков (2026-10-13)
|
||||
|
||||
Имя интерфейса больше не является единственным основанием новой counter pair. Nullable native_counter_identity хранит нормализованные source.updatedAt и native RouterOS.id (*HEX). Producer подтверждает continuity только когда identity непустая и совпадает с предыдущей строкой того же interface/peer key. Новый native ID, конфигурация источника или missing/invalid ID дают первую pair unknown; следующий подтверждённый poll восстанавливает измерение. Для WG используются peer.id и существующий public-key/iface series key, не ID самого интерфейса.
|
||||
|
||||
Биты16/32 различают confirmed continuity и сам факт проверки новым producer; running/disabled/counter/status markers не меняют смысла. Builder исключает неподтверждённую или несовпадающую pair из новых rates/bytes, сохраняя остальные подтверждённые исторические пары. Первый корректный counter остаётся baseline для следующей pair, даже если его собственная предыдущая continuity неизвестна. Legacy rows не переписываются: absent new marker/native identity оставляет прежнюю явно ограниченную историю. Это не доказательство непрерывности legacy, повторного использования того же native ID после перезагрузки или физической доступности.
|
||||
|
||||
| Изменение | Схема результата | Проверка |
|
||||
| --- | --- | --- |
|
||||
| Same name, новый native ID | old counter → unknown boundary → новый baseline → confirmed0 | own SQL interface+peer |
|
||||
| Config version изменилась | не переносить counter pair между источниками → normal next poll | own SQL |
|
||||
| Missing native ID | новые pair unknown, без guessed zero/bytes | own SQL/unit |
|
||||
|
||||
Миграция0012 добавляет nullable TEXT к partitioned traffic_samples, включая существующие partitions; нет массового UPDATE/переписывания flags. Downgrade старого reader может игнорировать новый столбец, migration removal не выполнялась. Реальный backup/restore с новой колонкой — следующий187, пока не принят данным блоком.
|
||||
|
||||
Unit identity/config/legacy-marker/pair guards, backend/application TypeScript и diff checks passed. [Own PG SQL](ux-ui/acceptance/traffic-identity-sql-2026-10-13.json):62 cases9counter+14history+10TCP SSE+10volume+19status/37613ms, no skip.4 новые cases: interface ID replacement/config version/missing ID/peer ID replacement с positive counters, unknown boundary, no extra bytes и последующим measured0. Fixtures явно объявляют native IDs; VM actual-collector unit получил настоящий identity/peer parser dependencies вместо отсутствовавших globals. Первая попытка остановилась до SQL scenarios на прежнем VM fixture; временная база/role удалены. Последний namespace mm_accept_166_1791828561251/role/container удалён, absence verified.
|
||||
|
||||
[Source458/524](ux-ui/acceptance/traffic-identity-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-identity-compiled-2026-10-13.json) SHA256 parity. [Backend ux-identity186](ux-ui/acceptance/traffic-identity-runtime-2026-10-13.json) healthy/imageIds; frontend ux-warning183 без изменений/новой сборки. Only backend.env tag, rollback backend ux-gap185/frontend ux-warning183; additive migration остаётся. Secret env copy0.
|
||||
|
||||
[Native GET](ux-ui/acceptance/traffic-identity-native-2026-10-13.json): оба CHR доступны, interface native IDs/flags/counters валидны;0012 applied. Peers фактически0 — positive peer replacement доказан изолированным fixture, не реальным VPN-туннелем. DB read-only/RouterOS config writes0/password changes0/messages0. [Initial API](ux-ui/acceptance/traffic-identity-api-2026-10-13.json) ожидаемо unknown при первом baseline после нового writer; [штатное восстановление](ux-ui/acceptance/traffic-identity-recovery-2026-10-13.json) подтвердило current/rxKnown/txKnown обоих CHR с30s interval без manual collector invocation. [Production UI1440/320](ux-ui/acceptance/traffic-identity-deployed-2026-10-13.json) actual live/receipt labels/no errors/overflow. Native SSE не изменён; предшествующие6E2E185 UI guard checks не пересчитываются как новые186.
|
||||
|
||||
Следующий187 — own-schema archive restore для nullable native counter identity/continuity flags и старого schema archive с повторным bootstrap migration. Потом отдельная приёмка многопроцессной координации/масштаба/внешних доступов.12.5 открыта. Локальный коммит без push.
|
||||
+23
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -3507,3 +3507,25 @@ Unit threshold/invalid interval/merge/collector guards, backend/application Type
|
||||
[Source456/524](ux-ui/acceptance/traffic-gap-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-gap-compiled-2026-10-13.json) SHA256 parity. Backend production ux-gap185 [healthy/imageIds](ux-ui/acceptance/traffic-gap-runtime-2026-10-13.json), frontend ux-warning183 без исходных изменений/повторной сборки. Только backend.env tag, rollback backend ux-history184/frontend ux-warning183; secret env copy0, migration0. [Actual API](ux-ui/acceptance/traffic-gap-api-2026-10-13.json)/[production UI1440/320](ux-ui/acceptance/traffic-gap-deployed-2026-10-13.json) passed с actual live и backend receipt labels, no errors/overflow. Checker не отправлял новых native commands и не создавал отказ на CHR. Password/config/message changes0.
|
||||
|
||||
Следующий186 — provenance native interface/peer identity и конфигурации источника: positive counter difference по одному прежнему имени не доказывает непрерывность после пересоздания/смены устройства. Старую историю не переписывать по догадке. Общая12.5 и внешние зависимости остаются открыты. Локальный коммит без push.
|
||||
|
||||
## 12.5.186 — подтверждение идентичности пары счётчиков (2026-10-13)
|
||||
|
||||
Имя интерфейса больше не является единственным основанием новой counter pair. Nullable native_counter_identity хранит нормализованные source.updatedAt и native RouterOS.id (*HEX). Producer подтверждает continuity только когда identity непустая и совпадает с предыдущей строкой того же interface/peer key. Новый native ID, конфигурация источника или missing/invalid ID дают первую pair unknown; следующий подтверждённый poll восстанавливает измерение. Для WG используются peer.id и существующий public-key/iface series key, не ID самого интерфейса.
|
||||
|
||||
Биты16/32 различают confirmed continuity и сам факт проверки новым producer; running/disabled/counter/status markers не меняют смысла. Builder исключает неподтверждённую или несовпадающую pair из новых rates/bytes, сохраняя остальные подтверждённые исторические пары. Первый корректный counter остаётся baseline для следующей pair, даже если его собственная предыдущая continuity неизвестна. Legacy rows не переписываются: absent new marker/native identity оставляет прежнюю явно ограниченную историю. Это не доказательство непрерывности legacy, повторного использования того же native ID после перезагрузки или физической доступности.
|
||||
|
||||
| Изменение | Схема результата | Проверка |
|
||||
| --- | --- | --- |
|
||||
| Same name, новый native ID | old counter → unknown boundary → новый baseline → confirmed0 | own SQL interface+peer |
|
||||
| Config version изменилась | не переносить counter pair между источниками → normal next poll | own SQL |
|
||||
| Missing native ID | новые pair unknown, без guessed zero/bytes | own SQL/unit |
|
||||
|
||||
Миграция0012 добавляет nullable TEXT к partitioned traffic_samples, включая существующие partitions; нет массового UPDATE/переписывания flags. Downgrade старого reader может игнорировать новый столбец, migration removal не выполнялась. Реальный backup/restore с новой колонкой — следующий187, пока не принят данным блоком.
|
||||
|
||||
Unit identity/config/legacy-marker/pair guards, backend/application TypeScript и diff checks passed. [Own PG SQL](ux-ui/acceptance/traffic-identity-sql-2026-10-13.json):62 cases9counter+14history+10TCP SSE+10volume+19status/37613ms, no skip.4 новые cases: interface ID replacement/config version/missing ID/peer ID replacement с positive counters, unknown boundary, no extra bytes и последующим measured0. Fixtures явно объявляют native IDs; VM actual-collector unit получил настоящий identity/peer parser dependencies вместо отсутствовавших globals. Первая попытка остановилась до SQL scenarios на прежнем VM fixture; временная база/role удалены. Последний namespace mm_accept_166_1791828561251/role/container удалён, absence verified.
|
||||
|
||||
[Source458/524](ux-ui/acceptance/traffic-identity-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-identity-compiled-2026-10-13.json) SHA256 parity. [Backend ux-identity186](ux-ui/acceptance/traffic-identity-runtime-2026-10-13.json) healthy/imageIds; frontend ux-warning183 без изменений/новой сборки. Only backend.env tag, rollback backend ux-gap185/frontend ux-warning183; additive migration остаётся. Secret env copy0.
|
||||
|
||||
[Native GET](ux-ui/acceptance/traffic-identity-native-2026-10-13.json): оба CHR доступны, interface native IDs/flags/counters валидны;0012 applied. Peers фактически0 — positive peer replacement доказан изолированным fixture, не реальным VPN-туннелем. DB read-only/RouterOS config writes0/password changes0/messages0. [Initial API](ux-ui/acceptance/traffic-identity-api-2026-10-13.json) ожидаемо unknown при первом baseline после нового writer; [штатное восстановление](ux-ui/acceptance/traffic-identity-recovery-2026-10-13.json) подтвердило current/rxKnown/txKnown обоих CHR с30s interval без manual collector invocation. [Production UI1440/320](ux-ui/acceptance/traffic-identity-deployed-2026-10-13.json) actual live/receipt labels/no errors/overflow. Native SSE не изменён; предшествующие6E2E185 UI guard checks не пересчитываются как новые186.
|
||||
|
||||
Следующий187 — own-schema archive restore для nullable native counter identity/continuity flags и старого schema archive с повторным bootstrap migration. Потом отдельная приёмка многопроцессной координации/масштаба/внешних доступов.12.5 открыта. Локальный коммит без push.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Сервер161.104.46.222: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -14,7 +14,7 @@
|
||||
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
|
||||
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
|
||||
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
|
||||
| Образы приложения | mmapp-test/backend:ux-gap185 и mmapp-test/frontend:ux-warning183; запущены |
|
||||
| Образы приложения | mmapp-test/backend:ux-identity186 и mmapp-test/frontend:ux-warning183; запущены |
|
||||
|
||||
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
|
||||
|
||||
@@ -708,7 +708,7 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.185 — current counter rate ограничен3периодами; long gap→unknown без потери history/known bytes; collector snapshot также не выдаёт long average как current. OwnSQL58/37008ms/6E2E14.7s/types/build/ESLint/source456+524/1040compiled/API/browser2 passed. Backend ux-gap185 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий186 — native identity/config continuity;12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13: завершён12.5.186 — native counter identity+source config, declared continuity flags, migration0012 без переписывания legacy. OwnSQL62/37613ms/types/build/source458+524/1040compiled/native2/recovery2/browser2 passed. Backend ux-identity186 healthy/frontend ux-warning183; ownDB/role/container удалены. Следующий187 — совместимость backup/restore после nullable-column migration;12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
|
||||
@@ -1933,3 +1933,25 @@ Unit threshold/invalid interval/merge/collector guards, backend/application Type
|
||||
[Source456/524](ux-ui/acceptance/traffic-gap-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-gap-compiled-2026-10-13.json) SHA256 parity. Backend production ux-gap185 [healthy/imageIds](ux-ui/acceptance/traffic-gap-runtime-2026-10-13.json), frontend ux-warning183 без исходных изменений/повторной сборки. Только backend.env tag, rollback backend ux-history184/frontend ux-warning183; secret env copy0, migration0. [Actual API](ux-ui/acceptance/traffic-gap-api-2026-10-13.json)/[production UI1440/320](ux-ui/acceptance/traffic-gap-deployed-2026-10-13.json) passed с actual live и backend receipt labels, no errors/overflow. Checker не отправлял новых native commands и не создавал отказ на CHR. Password/config/message changes0.
|
||||
|
||||
Следующий186 — provenance native interface/peer identity и конфигурации источника: positive counter difference по одному прежнему имени не доказывает непрерывность после пересоздания/смены устройства. Старую историю не переписывать по догадке. Общая12.5 и внешние зависимости остаются открыты. Локальный коммит без push.
|
||||
|
||||
## 12.5.186 — подтверждение идентичности пары счётчиков (2026-10-13)
|
||||
|
||||
Имя интерфейса больше не является единственным основанием новой counter pair. Nullable native_counter_identity хранит нормализованные source.updatedAt и native RouterOS.id (*HEX). Producer подтверждает continuity только когда identity непустая и совпадает с предыдущей строкой того же interface/peer key. Новый native ID, конфигурация источника или missing/invalid ID дают первую pair unknown; следующий подтверждённый poll восстанавливает измерение. Для WG используются peer.id и существующий public-key/iface series key, не ID самого интерфейса.
|
||||
|
||||
Биты16/32 различают confirmed continuity и сам факт проверки новым producer; running/disabled/counter/status markers не меняют смысла. Builder исключает неподтверждённую или несовпадающую pair из новых rates/bytes, сохраняя остальные подтверждённые исторические пары. Первый корректный counter остаётся baseline для следующей pair, даже если его собственная предыдущая continuity неизвестна. Legacy rows не переписываются: absent new marker/native identity оставляет прежнюю явно ограниченную историю. Это не доказательство непрерывности legacy, повторного использования того же native ID после перезагрузки или физической доступности.
|
||||
|
||||
| Изменение | Схема результата | Проверка |
|
||||
| --- | --- | --- |
|
||||
| Same name, новый native ID | old counter → unknown boundary → новый baseline → confirmed0 | own SQL interface+peer |
|
||||
| Config version изменилась | не переносить counter pair между источниками → normal next poll | own SQL |
|
||||
| Missing native ID | новые pair unknown, без guessed zero/bytes | own SQL/unit |
|
||||
|
||||
Миграция0012 добавляет nullable TEXT к partitioned traffic_samples, включая существующие partitions; нет массового UPDATE/переписывания flags. Downgrade старого reader может игнорировать новый столбец, migration removal не выполнялась. Реальный backup/restore с новой колонкой — следующий187, пока не принят данным блоком.
|
||||
|
||||
Unit identity/config/legacy-marker/pair guards, backend/application TypeScript и diff checks passed. [Own PG SQL](ux-ui/acceptance/traffic-identity-sql-2026-10-13.json):62 cases9counter+14history+10TCP SSE+10volume+19status/37613ms, no skip.4 новые cases: interface ID replacement/config version/missing ID/peer ID replacement с positive counters, unknown boundary, no extra bytes и последующим measured0. Fixtures явно объявляют native IDs; VM actual-collector unit получил настоящий identity/peer parser dependencies вместо отсутствовавших globals. Первая попытка остановилась до SQL scenarios на прежнем VM fixture; временная база/role удалены. Последний namespace mm_accept_166_1791828561251/role/container удалён, absence verified.
|
||||
|
||||
[Source458/524](ux-ui/acceptance/traffic-identity-source-2026-10-13.json)/[compiled1040](ux-ui/acceptance/traffic-identity-compiled-2026-10-13.json) SHA256 parity. [Backend ux-identity186](ux-ui/acceptance/traffic-identity-runtime-2026-10-13.json) healthy/imageIds; frontend ux-warning183 без изменений/новой сборки. Only backend.env tag, rollback backend ux-gap185/frontend ux-warning183; additive migration остаётся. Secret env copy0.
|
||||
|
||||
[Native GET](ux-ui/acceptance/traffic-identity-native-2026-10-13.json): оба CHR доступны, interface native IDs/flags/counters валидны;0012 applied. Peers фактически0 — positive peer replacement доказан изолированным fixture, не реальным VPN-туннелем. DB read-only/RouterOS config writes0/password changes0/messages0. [Initial API](ux-ui/acceptance/traffic-identity-api-2026-10-13.json) ожидаемо unknown при первом baseline после нового writer; [штатное восстановление](ux-ui/acceptance/traffic-identity-recovery-2026-10-13.json) подтвердило current/rxKnown/txKnown обоих CHR с30s interval без manual collector invocation. [Production UI1440/320](ux-ui/acceptance/traffic-identity-deployed-2026-10-13.json) actual live/receipt labels/no errors/overflow. Native SSE не изменён; предшествующие6E2E185 UI guard checks не пересчитываются как новые186.
|
||||
|
||||
Следующий187 — own-schema archive restore для nullable native counter identity/continuity flags и старого schema archive с повторным bootstrap migration. Потом отдельная приёмка многопроцессной координации/масштаба/внешних доступов.12.5 открыта. Локальный коммит без push.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"version": "1.0.0-test.identity186",
|
||||
"passed": true,
|
||||
"method": "Read-only actual application volume API; no new native RouterOS commands; exact edge values covered separately by owned SQL",
|
||||
"cases": [
|
||||
{
|
||||
"sourceId": "1",
|
||||
"volumeUnit": "GiB",
|
||||
"rxTotalGiB": 0.013450461439788342,
|
||||
"txTotalGiB": 0.0018185554072260857,
|
||||
"currentState": "unknown"
|
||||
},
|
||||
{
|
||||
"sourceId": "2",
|
||||
"volumeUnit": "GiB",
|
||||
"rxTotalGiB": 0.002372242510318756,
|
||||
"txTotalGiB": 0.002620793879032135,
|
||||
"currentState": "unknown"
|
||||
}
|
||||
],
|
||||
"newRouterOSCommands": 0,
|
||||
"passwordChanges": 0,
|
||||
"messages": 0
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"runtime": {
|
||||
"backend": "ux-identity186",
|
||||
"frontend": "ux-warning183"
|
||||
},
|
||||
"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,37 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"version": "1.0.0-test.identity186",
|
||||
"readOnlyDatabase": true,
|
||||
"cases": [
|
||||
{
|
||||
"sourceId": 1,
|
||||
"target": "194.226.121.150",
|
||||
"identityRead": true,
|
||||
"interfaceRead": true,
|
||||
"interfaceCount": 2,
|
||||
"countersValid": true,
|
||||
"nativeBooleanFlagsValidated": true,
|
||||
"peerSliceValidated": true,
|
||||
"peerCount": 0,
|
||||
"nativeCounterIdsValidated": true
|
||||
},
|
||||
{
|
||||
"sourceId": 2,
|
||||
"target": "104.165.244.7",
|
||||
"identityRead": true,
|
||||
"interfaceRead": true,
|
||||
"interfaceCount": 2,
|
||||
"countersValid": true,
|
||||
"nativeBooleanFlagsValidated": true,
|
||||
"peerSliceValidated": true,
|
||||
"peerCount": 0,
|
||||
"nativeCounterIdsValidated": true
|
||||
}
|
||||
],
|
||||
"dbWrites": 0,
|
||||
"routerConfigWrites": 0,
|
||||
"messages": 0,
|
||||
"migrationApplied": true,
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"method": "Read-only actual API after automatic polls; no manual collector invocation",
|
||||
"cases": [
|
||||
{
|
||||
"sourceId": "1",
|
||||
"state": "current",
|
||||
"rxKnown": true,
|
||||
"txKnown": true,
|
||||
"measurementIntervalMs": 30000
|
||||
},
|
||||
{
|
||||
"sourceId": "2",
|
||||
"state": "current",
|
||||
"rxKnown": true,
|
||||
"txKnown": true,
|
||||
"measurementIntervalMs": 30000
|
||||
}
|
||||
],
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"secretEnvCopied": false,
|
||||
"rollbackTags": {
|
||||
"MMAPP_IMAGE_TAG": "ux-warning183",
|
||||
"BACKEND_IMAGE_TAG": "ux-gap185"
|
||||
},
|
||||
"services": {
|
||||
"backend": {
|
||||
"image": "mmapp-test/backend:ux-identity186",
|
||||
"imageId": "sha256:0856c19bdcba4f8514b582f610073fc24249fbafee4972b2ea0f29f6b00d3fd5",
|
||||
"state": "running",
|
||||
"health": "healthy",
|
||||
"matchedVerifiedImage": true
|
||||
},
|
||||
"frontend": {
|
||||
"image": "mmapp-test/frontend:ux-warning183",
|
||||
"imageId": "sha256:4cf0549a212bfdc75c0107f3725686a9a9746a46ed6493ac73f608a0a335a586",
|
||||
"state": "running",
|
||||
"health": null,
|
||||
"matchedVerifiedImage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"matched": {
|
||||
"backend": 458,
|
||||
"frontend": 524
|
||||
},
|
||||
"different": [],
|
||||
"missing": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"passed": true,
|
||||
"profiles": [
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.186",
|
||||
"database": "mm_accept_166_1791828561251",
|
||||
"scope": "ephemeral role and precreated owned database; no copied production credentials",
|
||||
"tests": [
|
||||
{
|
||||
"command": "npm --prefix backend run test:traffic-rate",
|
||||
"passed": true,
|
||||
"durationMs": 37613,
|
||||
"ownedTrafficSqlCases": 9,
|
||||
"ownedHistorySqlCases": 14,
|
||||
"ownedLiveSqlCases": 10,
|
||||
"ownedVolumeSqlCases": 10,
|
||||
"ownedStatusSqlCases": 19,
|
||||
"postgresUnavailableSkip": false
|
||||
}
|
||||
],
|
||||
"scopedRoleVerified": true,
|
||||
"passed": true,
|
||||
"databaseRemoved": true,
|
||||
"roleRemoved": true,
|
||||
"testContainerRemoved": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user