fix(traffic): ограничивать доверие скорости после пропусков
This commit is contained in:
1 parent
9603048bd8
commit
493c30bebc
21 files changed
+1367
-22
No files matched your search
@@ -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 && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-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 && tsx src/services/traffic-gap.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",
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ function sampleRate(
|
|||||||
rxBytes: number,
|
rxBytes: number,
|
||||||
txBytes: number,
|
txBytes: number,
|
||||||
nowMs: number,
|
nowMs: number,
|
||||||
|
maxIntervalMs: number,
|
||||||
): { rxBps: number; txBps: number; measured: boolean } {
|
): { rxBps: number; txBps: number; measured: boolean } {
|
||||||
return observedTrafficRate(prevWave.get(key), rxBytes, txBytes, nowMs)
|
return observedTrafficRate(prevWave.get(key), rxBytes, txBytes, nowMs, maxIntervalMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrafficCollectorState {
|
export interface TrafficCollectorState {
|
||||||
@@ -122,7 +123,8 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await getSettingsRow()
|
const settings = await getSettingsRow()
|
||||||
|
const maxIntervalMs = Math.max(5, settings?.intervalSec ?? 30) * 3000
|
||||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||||
|
|
||||||
for (const srv of enabledServers) {
|
for (const srv of enabledServers) {
|
||||||
@@ -146,7 +148,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
const txBytes = toNum(i["tx-byte"])
|
const txBytes = toNum(i["tx-byte"])
|
||||||
const running = parseTrafficFlag(i.running)
|
const running = parseTrafficFlag(i.running)
|
||||||
const disabled = parseTrafficFlag(i.disabled)
|
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, maxIntervalMs)
|
||||||
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
if (shouldIncludeIface(interfaceName, running, disabled)) {
|
||||||
includedInterfaces++
|
includedInterfaces++
|
||||||
totalsMeasured &&= measured
|
totalsMeasured &&= measured
|
||||||
@@ -177,6 +179,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
|
|||||||
rxBytes,
|
rxBytes,
|
||||||
txBytes,
|
txBytes,
|
||||||
nowMs,
|
nowMs,
|
||||||
|
maxIntervalMs,
|
||||||
)
|
)
|
||||||
rows.push({
|
rows.push({
|
||||||
serverId: srv.id,
|
serverId: srv.id,
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ export function mergeTrafficObservations(parts: TrafficObservations[]): TrafficO
|
|||||||
return times.length && times.every(Number.isFinite) ? new Date(Math.min(...times)).toISOString() : null
|
return times.length && times.every(Number.isFinite) ? new Date(Math.min(...times)).toISOString() : null
|
||||||
}
|
}
|
||||||
const states: TrafficCurrentState[] = ["error", "pending", "stale", "disabled", "unknown"]
|
const states: TrafficCurrentState[] = ["error", "pending", "stale", "disabled", "unknown"]
|
||||||
|
const intervals = parts.map(p => p.measurementIntervalMs)
|
||||||
return { rxNow: !!parts.length && parts.every(p => p.rxNow), txNow: !!parts.length && parts.every(p => p.txNow),
|
return { rxNow: !!parts.length && parts.every(p => p.rxNow), txNow: !!parts.length && parts.every(p => p.txNow),
|
||||||
rxSeries: Array.from({ length: parts[0]?.rxSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.rxSeries[i])),
|
rxSeries: Array.from({ length: parts[0]?.rxSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.rxSeries[i])),
|
||||||
txSeries: Array.from({ length: parts[0]?.txSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.txSeries[i])),
|
txSeries: Array.from({ length: parts[0]?.txSeries.length ?? 60 }, (_, i) => !!parts.length && parts.every(p => p.txSeries[i])),
|
||||||
sampledAt: oldest("sampledAt"), expiresAt: oldest("expiresAt"), lastAttemptAt: oldest("lastAttemptAt"), lastSucceededAt: oldest("lastSucceededAt"),
|
sampledAt: oldest("sampledAt"), expiresAt: oldest("expiresAt"), lastAttemptAt: oldest("lastAttemptAt"), lastSucceededAt: oldest("lastSucceededAt"),
|
||||||
currentState: parts.length ? states.find(state => parts.some(p => p.currentState === state)) ?? "current" : "unknown",
|
currentState: parts.length ? states.find(state => parts.some(p => p.currentState === state)) ?? "current" : "unknown",
|
||||||
error: parts.find(p => p.error)?.error ?? null,
|
error: parts.find(p => p.error)?.error ?? null,
|
||||||
|
...(intervals.some(v => v !== undefined) ? {measurementIntervalMs: intervals.every(v => typeof v === "number" && Number.isFinite(v) && v > 0) ? Math.max(...intervals as number[]) : null} : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function applyTrafficFreshness(observations: TrafficObservations,
|
export function applyTrafficFreshness(observations: TrafficObservations,
|
||||||
@@ -35,6 +37,8 @@ export function applyTrafficFreshness(observations: TrafficObservations,
|
|||||||
const period = Math.max(5, settings?.intervalSec ?? 30) * 1000
|
const period = Math.max(5, settings?.intervalSec ?? 30) * 1000
|
||||||
const time = (raw: string | Date | null | undefined) => raw instanceof Date ? raw.getTime() : Date.parse(raw ?? "")
|
const time = (raw: string | Date | null | undefined) => raw instanceof Date ? raw.getTime() : Date.parse(raw ?? "")
|
||||||
const attemptMs = time(attempt?.lastAttemptAt), successMs = time(attempt?.lastSucceededAt)
|
const attemptMs = time(attempt?.lastAttemptAt), successMs = time(attempt?.lastSucceededAt)
|
||||||
|
const interval = observations.measurementIntervalMs
|
||||||
|
const intervalUnknown = interval !== undefined && (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0 || interval > 3 * period)
|
||||||
let currentState: TrafficCurrentState = "unknown"
|
let currentState: TrafficCurrentState = "unknown"
|
||||||
if (!server.enabled || settings?.enabled === false) currentState = "disabled"
|
if (!server.enabled || settings?.enabled === false) currentState = "disabled"
|
||||||
else if (settings && attempt && time(attempt.serverUpdatedAt) === time(server.updatedAt)) {
|
else if (settings && attempt && time(attempt.serverUpdatedAt) === time(server.updatedAt)) {
|
||||||
@@ -42,7 +46,7 @@ export function applyTrafficFreshness(observations: TrafficObservations,
|
|||||||
else if (attempt.status === "pending") currentState = "pending"
|
else if (attempt.status === "pending") currentState = "pending"
|
||||||
else if (attempt.status === "error") currentState = "error"
|
else if (attempt.status === "error") currentState = "error"
|
||||||
else if (attempt.status === "ok" && successMs === attemptMs &&
|
else if (attempt.status === "ok" && successMs === attemptMs &&
|
||||||
Date.parse(observations.sampledAt ?? "") === successMs && (observations.rxNow || observations.txNow)) currentState = "current"
|
!intervalUnknown && Date.parse(observations.sampledAt ?? "") === successMs && (observations.rxNow || observations.txNow)) currentState = "current"
|
||||||
}
|
}
|
||||||
return { ...observations,
|
return { ...observations,
|
||||||
expiresAt: Number.isFinite(attemptMs + 3 * period) && Math.abs(attemptMs + 3 * period) <= 8.64e15 ? new Date(attemptMs + 3 * period).toISOString() : null,
|
expiresAt: Number.isFinite(attemptMs + 3 * period) && Math.abs(attemptMs + 3 * period) <= 8.64e15 ? new Date(attemptMs + 3 * period).toISOString() : null,
|
||||||
@@ -50,6 +54,6 @@ export function applyTrafficFreshness(observations: TrafficObservations,
|
|||||||
txNow: currentState === "current" && observations.txNow,
|
txNow: currentState === "current" && observations.txNow,
|
||||||
currentState, lastAttemptAt: Number.isFinite(attemptMs) ? new Date(attemptMs).toISOString() : null,
|
currentState, lastAttemptAt: Number.isFinite(attemptMs) ? new Date(attemptMs).toISOString() : null,
|
||||||
lastSucceededAt: Number.isFinite(successMs) ? new Date(successMs).toISOString() : null,
|
lastSucceededAt: Number.isFinite(successMs) ? new Date(successMs).toISOString() : null,
|
||||||
error: attempt?.error ?? null,
|
error: attempt?.error ?? (intervalUnknown ? "Интервал между измерениями слишком велик или неизвестен. История и учтённый объём сохранены; для текущей скорости нужны соседние свежие измерения." : null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import {buildTrafficFromSamples} from './traffic-rate.js'
|
||||||
|
import {applyTrafficFreshness,mergeTrafficObservations} from './traffic-freshness.js'
|
||||||
|
import {observedTrafficRate} from './traffic-observation.js'
|
||||||
|
const now=Date.parse('2026-10-13T00:00:00Z'),iso=new Date(now).toISOString(),server={enabled:true,updatedAt:iso},settings={enabled:true,intervalSec:30},attempt={serverUpdatedAt:iso,lastAttemptAt:iso,lastSucceededAt:iso,status:'ok',error:null}
|
||||||
|
for(const dt of[30_000,90_000,90_001,3600_000,86400_000]){
|
||||||
|
const rows=[0,1].map(i=>({interfaceName:'ether1',sampledAt:new Date(now-(1-i)*dt).toISOString(),rxBytes:i*1024**2,txBytes:i*1024**2,rxBps:0,txBps:0,running:true,disabled:false,countersKnown:true}))
|
||||||
|
const built=buildTrafficFromSamples(rows,now-86400_000,now,undefined,undefined,true),o=applyTrafficFreshness(built.observations!,server,settings,attempt,now)
|
||||||
|
assert.equal(o.rxNow,dt<=90_000);assert.equal(o.txNow,dt<=90_000);assert.equal(built.rxTotalGiB,1/1024);assert(o.rxSeries.some(Boolean));assert.equal(o.measurementIntervalMs,dt)
|
||||||
|
const rate=observedTrafficRate(rows[0],rows[1].rxBytes,rows[1].txBytes,now,90_000);assert.equal(rate.measured,dt<=90_000)
|
||||||
|
if(dt>90_000)assert(o.error?.includes('Интервал'))
|
||||||
|
}
|
||||||
|
const base={rxNow:true,txNow:true,rxSeries:[true],txSeries:[true],sampledAt:iso}
|
||||||
|
for(const interval of[null,0,-1,NaN,Infinity])assert.equal(applyTrafficFreshness({...base,measurementIntervalMs:interval},server,settings,attempt,now).rxNow,false)
|
||||||
|
assert.equal(mergeTrafficObservations([{...base,measurementIntervalMs:30_000},{...base,measurementIntervalMs:120_000}]).measurementIntervalMs,120_000)
|
||||||
|
assert.equal(mergeTrafficObservations([{...base,measurementIntervalMs:30_000},base]).measurementIntervalMs,null)
|
||||||
|
assert.equal(applyTrafficFreshness({...base,measurementIntervalMs:120_000},server,settings,{...attempt,status:'error',error:'401'},now).error,'401')
|
||||||
|
console.log('traffic gap threshold/history/volume/merge/collector guards passed')
|
||||||
@@ -17,10 +17,10 @@ export function parseTrafficCounter(raw: unknown): number {
|
|||||||
|
|
||||||
export function observedTrafficRate(
|
export function observedTrafficRate(
|
||||||
previous: { rxBytes: number; txBytes: number; sampledAt: string } | undefined,
|
previous: { rxBytes: number; txBytes: number; sampledAt: string } | undefined,
|
||||||
rxBytes: number, txBytes: number, nowMs: number,
|
rxBytes: number, txBytes: number, nowMs: number, maxIntervalMs = Infinity,
|
||||||
): { rxBps: number; txBps: number; measured: boolean } {
|
): { rxBps: number; txBps: number; measured: boolean } {
|
||||||
const prevMs = previous ? Date.parse(previous.sampledAt) : NaN
|
const prevMs = previous ? Date.parse(previous.sampledAt) : NaN
|
||||||
if (!previous || ![previous.rxBytes, previous.txBytes, rxBytes, txBytes].every(v => Number.isSafeInteger(v) && v >= 0)) {
|
if (!previous || nowMs - prevMs > maxIntervalMs || ![previous.rxBytes, previous.txBytes, rxBytes, txBytes].every(v => Number.isSafeInteger(v) && v >= 0)) {
|
||||||
return { rxBps: 0, txBps: 0, measured: false }
|
return { rxBps: 0, txBps: 0, measured: false }
|
||||||
}
|
}
|
||||||
const rx = previous ? rateBpsFromDelta(previous.rxBytes, rxBytes, prevMs, nowMs) : null
|
const rx = previous ? rateBpsFromDelta(previous.rxBytes, rxBytes, prevMs, nowMs) : null
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface RatePoint {
|
|||||||
txMbps: number
|
txMbps: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrafficObservations {rxNow:boolean;txNow:boolean;rxSeries:boolean[];txSeries:boolean[];sampledAt:string|null;expiresAt?:string|null;
|
export interface TrafficObservations {rxNow:boolean;txNow:boolean;rxSeries:boolean[];txSeries:boolean[];sampledAt:string|null;expiresAt?:string|null;measurementIntervalMs?:number|null;
|
||||||
currentState?: import("./traffic-freshness.js").TrafficCurrentState;lastAttemptAt?:string|null;lastSucceededAt?:string|null;error?:string|null}
|
currentState?: import("./traffic-freshness.js").TrafficCurrentState;lastAttemptAt?:string|null;lastSucceededAt?:string|null;error?:string|null}
|
||||||
|
|
||||||
export interface BuiltTrafficSeries {
|
export interface BuiltTrafficSeries {
|
||||||
@@ -143,7 +143,7 @@ export function buildTrafficFromSamples(
|
|||||||
|
|
||||||
const rxPoints: Array<{ t: number; v: number }> = []
|
const rxPoints: Array<{ t: number; v: number }> = []
|
||||||
const txPoints: Array<{ t: number; v: number }> = []
|
const txPoints: Array<{ t: number; v: number }> = []
|
||||||
const byTs = new Map<number, { rx: number; tx: number;rxObserved:boolean;txObserved:boolean;count:number }>()
|
const byTs = new Map<number, { rx: number; tx: number;rxObserved:boolean;txObserved:boolean;count:number;intervalMs:number }>()
|
||||||
let includedSeries=0,lastAttemptAt:number|null=null
|
let includedSeries=0,lastAttemptAt:number|null=null
|
||||||
|
|
||||||
let rxBytesDelta = 0
|
let rxBytesDelta = 0
|
||||||
@@ -190,7 +190,8 @@ export function buildTrafficFromSamples(
|
|||||||
if (rxBps == null && txBps == null) continue
|
if (rxBps == null && txBps == null) continue
|
||||||
const rxMbps = bpsToMbps(rxBps ?? 0)
|
const rxMbps = bpsToMbps(rxBps ?? 0)
|
||||||
const txMbps = bpsToMbps(txBps ?? 0)
|
const txMbps = bpsToMbps(txBps ?? 0)
|
||||||
const acc = byTs.get(t1) ?? { rx: 0, tx: 0,rxObserved:true,txObserved:true,count:0 }
|
const acc = byTs.get(t1) ?? { rx: 0, tx: 0,rxObserved:true,txObserved:true,count:0,intervalMs:0 }
|
||||||
|
acc.intervalMs = Math.max(acc.intervalMs, t1 - t0)
|
||||||
acc.count++;acc.rxObserved&&=rxBps!==null;acc.txObserved&&=txBps!==null
|
acc.count++;acc.rxObserved&&=rxBps!==null;acc.txObserved&&=txBps!==null
|
||||||
acc.rx += rxMbps
|
acc.rx += rxMbps
|
||||||
acc.tx += txMbps
|
acc.tx += txMbps
|
||||||
@@ -226,6 +227,7 @@ export function buildTrafficFromSamples(
|
|||||||
rxSeries:bucketAvg([...byTs].map(([t,v])=>({t,v:Number(v.rxObserved&&v.count===includedSeries&&t>=rangeStartMs&&t<=rangeEndMs)})),rangeStartMs,rangeEndMs).map(v=>v===1),
|
rxSeries:bucketAvg([...byTs].map(([t,v])=>({t,v:Number(v.rxObserved&&v.count===includedSeries&&t>=rangeStartMs&&t<=rangeEndMs)})),rangeStartMs,rangeEndMs).map(v=>v===1),
|
||||||
txSeries:bucketAvg([...byTs].map(([t,v])=>({t,v:Number(v.txObserved&&v.count===includedSeries&&t>=rangeStartMs&&t<=rangeEndMs)})),rangeStartMs,rangeEndMs).map(v=>v===1),
|
txSeries:bucketAvg([...byTs].map(([t,v])=>({t,v:Number(v.txObserved&&v.count===includedSeries&&t>=rangeStartMs&&t<=rangeEndMs)})),rangeStartMs,rangeEndMs).map(v=>v===1),
|
||||||
sampledAt:lastAttemptAt===null?null:new Date(lastAttemptAt).toISOString(),
|
sampledAt:lastAttemptAt===null?null:new Date(lastAttemptAt).toISOString(),
|
||||||
|
measurementIntervalMs:lastTs===lastAttemptAt?last?.intervalMs??null:null,
|
||||||
}}:{}),
|
}}:{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ else{
|
|||||||
denyPeer=true;peerVariant="valid";let partial=await collectTrafficOnce();assert.equal(partial.servers[0].ok,true);assert(partial.servers[0].warning?.includes("401"));rows=await bindings();assert(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.error.includes("401"));cases++
|
denyPeer=true;peerVariant="valid";let partial=await collectTrafficOnce();assert.equal(partial.servers[0].ok,true);assert(partial.servers[0].warning?.includes("401"));rows=await bindings();assert(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.error.includes("401"));cases++
|
||||||
const manualPartial=await app.inject({method:"POST",url:"/traffic/collect-now"});assert.equal(manualPartial.statusCode,200);assert.equal(manualPartial.json().ok,true);assert.equal(manualPartial.json().warnings.length,1);assert(manualPartial.json().lastError.includes("401"));
|
const manualPartial=await app.inject({method:"POST",url:"/traffic/collect-now"});assert.equal(manualPartial.statusCode,200);assert.equal(manualPartial.json().ok,true);assert.equal(manualPartial.json().warnings.length,1);assert(manualPartial.json().lastError.includes("401"));
|
||||||
denyPeer=false;partial=await collectTrafficOnce();assert.equal(partial.servers[0].warning,undefined);assert.equal((await pool.query("SELECT error FROM traffic_collection_state WHERE server_id=$1",[serverId])).rows[0].error,null);cases++
|
denyPeer=false;partial=await collectTrafficOnce();assert.equal(partial.servers[0].warning,undefined);assert.equal((await pool.query("SELECT error FROM traffic_collection_state WHERE server_id=$1",[serverId])).rows[0].error,null);cases++
|
||||||
|
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1 AND sampled_at<>(SELECT max(sampled_at) FROM traffic_samples WHERE server_id=$1)",[serverId]);
|
||||||
|
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++;
|
||||||
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++
|
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`)
|
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()}
|
}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()}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ else {
|
|||||||
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.rxTotal,2048/1024**3);assert(s.observations.rxSeries.some(Boolean));assert(s.rxPeak>0);assert.equal(s.sessions,1);
|
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.rxTotal,2048/1024**3);assert(s.observations.rxSeries.some(Boolean));assert(s.rxPeak>0);assert.equal(s.sessions,1);
|
||||||
const summary=(await get("/traffic/servers?range=1h")).servers.find((r:{id:string})=>r.id===String(serverId));assert.equal(summary.rxTotal,s.rxTotal);cases++
|
const summary=(await get("/traffic/servers?range=1h")).servers.find((r:{id:string})=>r.id===String(serverId));assert.equal(summary.rxTotal,s.rxTotal);cases++
|
||||||
}
|
}
|
||||||
|
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await put("ether1",-120_000,0);await put("ether1",0,1024**2);
|
||||||
|
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.rxTotal,1/1024);assert.equal(s.observations.rxNow,false);assert.equal(s.observations.measurementIntervalMs,120_000);assert(s.observations.error.includes("Интервал"));assert(s.observations.rxSeries.some(Boolean));cases++;
|
||||||
|
await pool.query("UPDATE traffic_samples SET sampled_at=$1 WHERE server_id=$2 AND sampled_at=$3",[new Date(Date.parse(at)-30_000).toISOString(),serverId,at]);await put("ether1",0,2*1024**2);
|
||||||
|
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.observations.rxNow,true);assert.equal(s.observations.measurementIntervalMs,30_000);assert.equal(s.rxTotal,2/1024);cases++;
|
||||||
console.log(`traffic volume owned SQL: ${cases} cases; exact bytes/binary unit/server/user/binding/100-part aggregate/reset passed`)
|
console.log(`traffic volume owned SQL: ${cases} cases; exact bytes/binary unit/server/user/binding/100-part aggregate/reset passed`)
|
||||||
}finally{await app.close();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()}
|
}finally{await app.close();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
|
# UX/UI: протокол внешней приёмки 12.5
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
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.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
|
|
||||||
@@ -2184,3 +2184,21 @@ Backend TypeScript/build, [source455/524](ux-ui/acceptance/traffic-membership-so
|
|||||||
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
||||||
|
|
||||||
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
||||||
|
|
||||||
|
## 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 сохраняются.
|
||||||
|
|
||||||
|
Collector использует тот же предел в observedTrafficRate: после длинного пропуска snapshot не содержит sumRxMbps/sumTxMbps как измеренную текущую скорость. Следующая нормальная пара восстанавливает current, включая measured0. Native SSE monitor-traffic не изменён; native link status не зависит от длительности counter pair. Legacy вручную сформированная meta без interval сохраняет прежнюю совместимость; production builder выдаёт interval/null явно. Это не подтверждение native identity или полного billing за период.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|30s/90s | свежая пара → current | unit threshold |
|
||||||
|
|90.001s/1h/24h | history+known bytes → текущая скорость неизвестна | unit, own SQL, UI1440/320 |
|
||||||
|
| Новый poll после пропуска | нормальная пара → current0 допустим | own collector/SQL recovery |
|
||||||
|
|
||||||
|
Unit threshold/invalid interval/merge/collector guards, backend/application TypeScript и ESLint passed.6 E2E14.7s (2new gap+2binding freshness+2peer warnings) passed. UI использует существующий ReUI Alert/base-nova без новой темы или компонентов. [Owned SQL](ux-ui/acceptance/traffic-gap-sql-2026-10-13.json):58 cases9counter+14history+10TCP SSE+10volume+15status/37008ms, no unavailable skip.3 новые cases: API сохраняет1MiB/history при120s gap, API recovery30s возвращает current и2MiB, actual fixture collector long gap не выдаёт current sum и второй poll подтверждает0. Первоначальный SQL запуск выявил fixture duplicate PK при переносе всех исторических timestamps; fixture оставляет только последнюю волну перед сдвигом. Ошибка не выдана за passed, обе временные базы/роли удалены. Последняя ownDB mm_accept_166_1791827374886/role/container удалена и отсутствие проверено.
|
||||||
|
|
||||||
|
[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.
|
||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
# План реализации UX/UI по пятичасовым окнам
|
# План реализации UX/UI по пятичасовым окнам
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
Общий план проекта 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.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
Этапы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.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
+19
-1
@@ -1,6 +1,6 @@
|
|||||||
# UX/UI: журнал реализации
|
# UX/UI: журнал реализации
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
Общий план: [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,24 @@
|
|||||||
| 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.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 сохраняются.
|
||||||
|
|
||||||
|
Collector использует тот же предел в observedTrafficRate: после длинного пропуска snapshot не содержит sumRxMbps/sumTxMbps как измеренную текущую скорость. Следующая нормальная пара восстанавливает current, включая measured0. Native SSE monitor-traffic не изменён; native link status не зависит от длительности counter pair. Legacy вручную сформированная meta без interval сохраняет прежнюю совместимость; production builder выдаёт interval/null явно. Это не подтверждение native identity или полного billing за период.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|30s/90s | свежая пара → current | unit threshold |
|
||||||
|
|90.001s/1h/24h | history+known bytes → текущая скорость неизвестна | unit, own SQL, UI1440/320 |
|
||||||
|
| Новый poll после пропуска | нормальная пара → current0 допустим | own collector/SQL recovery |
|
||||||
|
|
||||||
|
Unit threshold/invalid interval/merge/collector guards, backend/application TypeScript и ESLint passed.6 E2E14.7s (2new gap+2binding freshness+2peer warnings) passed. UI использует существующий ReUI Alert/base-nova без новой темы или компонентов. [Owned SQL](ux-ui/acceptance/traffic-gap-sql-2026-10-13.json):58 cases9counter+14history+10TCP SSE+10volume+15status/37008ms, no unavailable skip.3 новые cases: API сохраняет1MiB/history при120s gap, API recovery30s возвращает current и2MiB, actual fixture collector long gap не выдаёт current sum и второй poll подтверждает0. Первоначальный SQL запуск выявил fixture duplicate PK при переносе всех исторических timestamps; fixture оставляет только последнюю волну перед сдвигом. Ошибка не выдана за passed, обе временные базы/роли удалены. Последняя ownDB mm_accept_166_1791827374886/role/container удалена и отсутствие проверено.
|
||||||
|
|
||||||
|
[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.185 — граница доверия скорости после пропусков: baseline (2026-10-12)
|
## 12.5.185 — граница доверия скорости после пропусков: baseline (2026-10-12)
|
||||||
|
|
||||||
Блок начат; реализация и приёмка не завершены. [Чистый read-only пример](ux-ui/acceptance/traffic-gap-baseline-2026-10-12.json) проверил один и тот же достоверный delta1MiB между двумя counters с интервалами30s,90s,1h,24h. Учтённый объём во всех случаях1MiB — корректное свойство counter difference. Но observations.rxNow=true также для среднего за1h/24h; длительность последнего интервала пока не влияет на доверие текущей скорости. Это не native monitor-traffic: среднее за интервал не доказывает instantaneous rate в конце интервала.4 baseline cases не являются4 новыми принятыми production сценариями.
|
Блок начат; реализация и приёмка не завершены. [Чистый read-only пример](ux-ui/acceptance/traffic-gap-baseline-2026-10-12.json) проверил один и тот же достоверный delta1MiB между двумя counters с интервалами30s,90s,1h,24h. Учтённый объём во всех случаях1MiB — корректное свойство counter difference. Но observations.rxNow=true также для среднего за1h/24h; длительность последнего интервала пока не влияет на доверие текущей скорости. Это не native monitor-traffic: среднее за интервал не доказывает instantaneous rate в конце интервала.4 baseline cases не являются4 новыми принятыми production сценариями.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Это актуальная матрица после 12.5.185. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
Это актуальная матрица после 12.5.185. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||||
|
|
||||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-history184`, 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-gap185`, frontend `ux-warning183`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||||
|
|
||||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -1099,3 +1099,21 @@ Backend TypeScript/build, [source455/524](ux-ui/acceptance/traffic-membership-so
|
|||||||
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
||||||
|
|
||||||
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
||||||
|
|
||||||
|
## 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 сохраняются.
|
||||||
|
|
||||||
|
Collector использует тот же предел в observedTrafficRate: после длинного пропуска snapshot не содержит sumRxMbps/sumTxMbps как измеренную текущую скорость. Следующая нормальная пара восстанавливает current, включая measured0. Native SSE monitor-traffic не изменён; native link status не зависит от длительности counter pair. Legacy вручную сформированная meta без interval сохраняет прежнюю совместимость; production builder выдаёт interval/null явно. Это не подтверждение native identity или полного billing за период.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|30s/90s | свежая пара → current | unit threshold |
|
||||||
|
|90.001s/1h/24h | history+known bytes → текущая скорость неизвестна | unit, own SQL, UI1440/320 |
|
||||||
|
| Новый poll после пропуска | нормальная пара → current0 допустим | own collector/SQL recovery |
|
||||||
|
|
||||||
|
Unit threshold/invalid interval/merge/collector guards, backend/application TypeScript и ESLint passed.6 E2E14.7s (2new gap+2binding freshness+2peer warnings) passed. UI использует существующий ReUI Alert/base-nova без новой темы или компонентов. [Owned SQL](ux-ui/acceptance/traffic-gap-sql-2026-10-13.json):58 cases9counter+14history+10TCP SSE+10volume+15status/37008ms, no unavailable skip.3 новые cases: API сохраняет1MiB/history при120s gap, API recovery30s возвращает current и2MiB, actual fixture collector long gap не выдаёт current sum и второй poll подтверждает0. Первоначальный SQL запуск выявил fixture duplicate PK при переносе всех исторических timestamps; fixture оставляет только последнюю волну перед сдвигом. Ошибка не выдана за passed, обе временные базы/роли удалены. Последняя ownDB mm_accept_166_1791827374886/role/container удалена и отсутствие проверено.
|
||||||
|
|
||||||
|
[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.
|
||||||
+19
-1
@@ -1,6 +1,6 @@
|
|||||||
# Этап 12: итоговая приёмка UX/UI
|
# Этап 12: итоговая приёмка UX/UI
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-12: завершён12.5.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.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 открыта.
|
||||||
|
|
||||||
План: [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.
|
||||||
|
|
||||||
@@ -3489,3 +3489,21 @@ Backend TypeScript/build, [source455/524](ux-ui/acceptance/traffic-membership-so
|
|||||||
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
||||||
|
|
||||||
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
||||||
|
|
||||||
|
## 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 сохраняются.
|
||||||
|
|
||||||
|
Collector использует тот же предел в observedTrafficRate: после длинного пропуска snapshot не содержит sumRxMbps/sumTxMbps как измеренную текущую скорость. Следующая нормальная пара восстанавливает current, включая measured0. Native SSE monitor-traffic не изменён; native link status не зависит от длительности counter pair. Legacy вручную сформированная meta без interval сохраняет прежнюю совместимость; production builder выдаёт interval/null явно. Это не подтверждение native identity или полного billing за период.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|30s/90s | свежая пара → current | unit threshold |
|
||||||
|
|90.001s/1h/24h | history+known bytes → текущая скорость неизвестна | unit, own SQL, UI1440/320 |
|
||||||
|
| Новый poll после пропуска | нормальная пара → current0 допустим | own collector/SQL recovery |
|
||||||
|
|
||||||
|
Unit threshold/invalid interval/merge/collector guards, backend/application TypeScript и ESLint passed.6 E2E14.7s (2new gap+2binding freshness+2peer warnings) passed. UI использует существующий ReUI Alert/base-nova без новой темы или компонентов. [Owned SQL](ux-ui/acceptance/traffic-gap-sql-2026-10-13.json):58 cases9counter+14history+10TCP SSE+10volume+15status/37008ms, no unavailable skip.3 новые cases: API сохраняет1MiB/history при120s gap, API recovery30s возвращает current и2MiB, actual fixture collector long gap не выдаёт current sum и второй poll подтверждает0. Первоначальный SQL запуск выявил fixture duplicate PK при переносе всех исторических timestamps; fixture оставляет только последнюю волну перед сдвигом. Ошибка не выдана за passed, обе временные базы/роли удалены. Последняя ownDB mm_accept_166_1791827374886/role/container удалена и отсутствие проверено.
|
||||||
|
|
||||||
|
[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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Тестовый сервер UX/UI — 2026-10-10
|
# Тестовый сервер UX/UI — 2026-10-10
|
||||||
|
|
||||||
Сервер161.104.46.222: завершён12.5.184, backend ux-history184/frontend ux-warning183 healthy. Начат185: baseline4 воспроизводит current mask для среднего counter rate за30s/90s/1h/24h; это не исправлено и не принято. Следующий шаг — ограничить доверие текущей counter-derived скорости длительностью интервала, сохраняя подтверждённые bytes/history.12.5 открыта.
|
Сервер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 открыта.
|
||||||
|
|
||||||
| Параметр | Проверенное состояние |
|
| Параметр | Проверенное состояние |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -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-history184 и mmapp-test/frontend:ux-warning183; запущены |
|
| Образы приложения | mmapp-test/backend:ux-gap185 и mmapp-test/frontend:ux-warning183; запущены |
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@@ -708,7 +708,7 @@ flowchart LR
|
|||||||
|
|
||||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||||
|
|
||||||
Актуальное продолжение 2026-10-09: 12.5.77 завершил укрупнённый основной блок IPsec: достоверное чтение, запрет опроса выключенных устройств, совместимый RouterOS7 мастер, раздельные ID клиентов, безопаснее удаление, URL устройства/вкладки, Docker обеих частей и реальная приёмка. Local30/30 + context6/6 + backend67, live browser4/4, PSK и сертификатный защищённый GRE/Ping3/3 на двух CHR; cleanup/DB controls passed. Runtime backend/frontend ux-ipsec77, version1.0.0-test.ipsec77. Следующий12.5.78 — остаточная профильная приёмка IPsec: сопоставление active-peer/online, выдача mode-config, поздние ошибки операций и история восстановления; затем маршрутизация. Этап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 открыта.
|
||||||
|
|
||||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||||
|
|
||||||
@@ -1915,3 +1915,21 @@ Backend TypeScript/build, [source455/524](ux-ui/acceptance/traffic-membership-so
|
|||||||
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
Точный следующий шаг: передавать длительность последней пары measurements в provenance и ограничить current counter-derived rate согласованным пределом относительно collector period. Аналогичный guard нужен в snapshot collector, чтобы новое чтение после длительного сбоя не объявляло старое среднее текущей скоростью. Подтвердить normal/threshold/long gap/recovery/reset на собственном SQL/HTTP и UI. Не подменять average/unknown нулём и не менять actual SSE monitor rate. Отдельно остаётся неоднозначность пересоздания интерфейса с прежним именем и увеличившимся counter: существующая sample identity не доказывает непрерывность.
|
||||||
|
|
||||||
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
Стенд остаётся backend ux-history184/frontend ux-warning183, source/runtime184 не менялись. Аудит не обращался к CHR/DB, writes0/native commands0, пароли/конфигурация не менялись. Состояние сохранено локальным коммитом без push для продолжения185 после сброса лимита.
|
||||||
|
|
||||||
|
## 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 сохраняются.
|
||||||
|
|
||||||
|
Collector использует тот же предел в observedTrafficRate: после длинного пропуска snapshot не содержит sumRxMbps/sumTxMbps как измеренную текущую скорость. Следующая нормальная пара восстанавливает current, включая measured0. Native SSE monitor-traffic не изменён; native link status не зависит от длительности counter pair. Legacy вручную сформированная meta без interval сохраняет прежнюю совместимость; production builder выдаёт interval/null явно. Это не подтверждение native identity или полного billing за период.
|
||||||
|
|
||||||
|
| Состояние | Схема | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|30s/90s | свежая пара → current | unit threshold |
|
||||||
|
|90.001s/1h/24h | history+known bytes → текущая скорость неизвестна | unit, own SQL, UI1440/320 |
|
||||||
|
| Новый poll после пропуска | нормальная пара → current0 допустим | own collector/SQL recovery |
|
||||||
|
|
||||||
|
Unit threshold/invalid interval/merge/collector guards, backend/application TypeScript и ESLint passed.6 E2E14.7s (2new gap+2binding freshness+2peer warnings) passed. UI использует существующий ReUI Alert/base-nova без новой темы или компонентов. [Owned SQL](ux-ui/acceptance/traffic-gap-sql-2026-10-13.json):58 cases9counter+14history+10TCP SSE+10volume+15status/37008ms, no unavailable skip.3 новые cases: API сохраняет1MiB/history при120s gap, API recovery30s возвращает current и2MiB, actual fixture collector long gap не выдаёт current sum и второй poll подтверждает0. Первоначальный SQL запуск выявил fixture duplicate PK при переносе всех исторических timestamps; fixture оставляет только последнюю волну перед сдвигом. Ошибка не выдана за passed, обе временные базы/роли удалены. Последняя ownDB mm_accept_166_1791827374886/role/container удалена и отсутствие проверено.
|
||||||
|
|
||||||
|
[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.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-13",
|
||||||
|
"block": "12.5.185",
|
||||||
|
"version": "1.0.0-test.gap185",
|
||||||
|
"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.013670094311237335,
|
||||||
|
"txTotalGiB": 0.0017840750515460968,
|
||||||
|
"currentState": "current"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sourceId": "2",
|
||||||
|
"volumeUnit": "GiB",
|
||||||
|
"rxTotalGiB": 0.0023259855806827545,
|
||||||
|
"txTotalGiB": 0.0025926968082785606,
|
||||||
|
"currentState": "current"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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.185",
|
||||||
|
"runtime": {
|
||||||
|
"backend": "ux-gap185",
|
||||||
|
"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,25 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-13",
|
||||||
|
"block": "12.5.185",
|
||||||
|
"secretEnvCopied": false,
|
||||||
|
"rollbackTags": {
|
||||||
|
"MMAPP_IMAGE_TAG": "ux-warning183",
|
||||||
|
"BACKEND_IMAGE_TAG": "ux-history184"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"backend": {
|
||||||
|
"image": "mmapp-test/backend:ux-gap185",
|
||||||
|
"imageId": "sha256:b4c6ef706f07aa4be6131314e9690d5b72d8e6ee7e9f0bfea5ad8233d92a6caa",
|
||||||
|
"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.185",
|
||||||
|
"matched": {
|
||||||
|
"backend": 456,
|
||||||
|
"frontend": 524
|
||||||
|
},
|
||||||
|
"different": [],
|
||||||
|
"missing": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-10-13",
|
||||||
|
"block": "12.5.185",
|
||||||
|
"passed": true,
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"date": "2026-10-13",
|
||||||
|
"block": "12.5.185",
|
||||||
|
"database": "mm_accept_166_1791827374886",
|
||||||
|
"scope": "ephemeral role and precreated owned database; no copied production credentials",
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"command": "npm --prefix backend run test:traffic-rate",
|
||||||
|
"passed": true,
|
||||||
|
"durationMs": 37008,
|
||||||
|
"ownedTrafficSqlCases": 9,
|
||||||
|
"ownedHistorySqlCases": 14,
|
||||||
|
"ownedLiveSqlCases": 10,
|
||||||
|
"ownedVolumeSqlCases": 10,
|
||||||
|
"ownedStatusSqlCases": 15,
|
||||||
|
"postgresUnavailableSkip": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopedRoleVerified": true,
|
||||||
|
"passed": true,
|
||||||
|
"databaseRemoved": true,
|
||||||
|
"roleRemoved": true,
|
||||||
|
"testContainerRemoved": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { test, expect, type Page } from "@playwright/test"
|
||||||
|
const at = new Date("2026-10-12T12:00:00Z")
|
||||||
|
async function setup(page: Page, state: "error" | "current" | "legacy", status = "online") {
|
||||||
|
let calls = 0
|
||||||
|
const observations = { rxNow: state === "current", txNow: state === "current", rxSeries: [false, true], txSeries: [false, true],
|
||||||
|
sampledAt: at.toISOString(), lastAttemptAt: at.toISOString(), lastSucceededAt: at.toISOString(),
|
||||||
|
expiresAt: new Date(at.getTime()+90_000).toISOString(), currentState: state === "error" || state === "legacy" ? "unknown" : state,
|
||||||
|
error: state === "error" ? "Интервал между измерениями слишком велик. История и учтённый объём сохранены." : null }
|
||||||
|
const server = { id: "1", name: "История CHR", site: "TEST", country: "RU", status,
|
||||||
|
rxNow: state === "current" ? 0 : 17, txNow: 0, rxPeak: 5, txPeak: 6, rxTotal: 1, txTotal: 2, sessions: 1,
|
||||||
|
rxSeries: [0,5], txSeries: [0,6], ...(state !== "legacy" ? {observations} : {}) }
|
||||||
|
await page.clock.install({time:at})
|
||||||
|
await page.route("**/health", r=>r.fulfill({json:{ok:true}}))
|
||||||
|
await page.route("**/api/**", r=> {
|
||||||
|
const path = new URL(r.request().url()).pathname
|
||||||
|
if(path.endsWith("/live")) return r.fulfill({status:503,json:{error:"Live недоступен"}})
|
||||||
|
if(path==="/api/traffic/servers")calls++
|
||||||
|
return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/traffic/servers"?{servers:[server]}:
|
||||||
|
path==="/api/traffic/servers/1"?{server}:path.endsWith("/interfaces")||path==="/api/traffic/bound-interfaces"?{interfaces:[]}:
|
||||||
|
path==="/api/traffic/users"?{users:[]}:path==="/api/servers"?[]:{}})
|
||||||
|
})
|
||||||
|
await page.goto("/traffic")
|
||||||
|
await expect(page.getByRole("heading",{name:"История CHR",exact:true})).toBeVisible()
|
||||||
|
return ()=>calls
|
||||||
|
}
|
||||||
|
for(const width of[1440,320])test(`Длительный пропуск: история сохранена, текущая скорость неизвестна ${width}`,async({page})=>{
|
||||||
|
await page.setViewportSize({width,height:1000});await setup(page,"error")
|
||||||
|
await expect(page.getByRole("status").filter({hasText:"Скорость пока не измерена"})).toContainText("Интервал между измерениями слишком велик")
|
||||||
|
await expect(page.getByLabel("Скорость выбранного сервера").getByText("—",{exact:true})).toHaveCount(2)
|
||||||
|
await expect(page.getByRole("region",{name:"История RX/TX"})).toBeVisible()
|
||||||
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user