fix(traffic): считать общий пик по совпадающим интервалам

This commit is contained in:
Denozordec committed 2026-10-13 13:58:56 +07:00
1 parent dbc3b2c947
commit 7b88416415
21 files changed
+1390 -27

No files matched your search

+1 -1
View File
@@ -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 && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.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 && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.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",
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict'
import {buildTrafficFromSamples, mergeBuiltTraffic} from './traffic-rate.js'
const at = Date.parse('2026-10-13T00:00:00Z')
const rows = (name:string, bytes:number[], times=[0,30_000,60_000], known=[false,true,true]) => bytes.map((value,i)=>({
interfaceName:name,sampledAt:new Date(at+times[i]).toISOString(),rxBytes:value,txBytes:value/2,
rxBps:0,txBps:0,running:true,disabled:false,countersKnown:true,continuityKnown:known[i],nativeCounterIdentity:name,
}))
const build = (r:ReturnType<typeof rows>) => buildTrafficFromSamples(r,at,at+60_000,undefined,undefined,true)
const a=build(rows('a',[0,375_000_000,375_000_000]))
const b=build(rows('b',[0,0,375_000_000]))
const separate=mergeBuiltTraffic([a,b])
assert.equal(separate.rxPeak,100);assert.equal(separate.txPeak,50)
assert.equal(separate.observations!.rxPeak,true)
assert.equal(separate.rxTotalGiB,a.rxTotalGiB+b.rxTotalGiB)
assert.equal(mergeBuiltTraffic([a,a]).rxPeak,200)
assert.equal(mergeBuiltTraffic([separate,separate]).rxPeak,200,'nested merge preserves raw intervals')
assert.equal(mergeBuiltTraffic([build(rows('z',[0,0,0])),build(rows('z2',[0,0,0]))]).observations!.rxPeak,true,'measured zero is known')
const missing=build(rows('missing',[0,0,375_000_000],[0,30_000,60_000],[false,false,false]))
assert.equal(mergeBuiltTraffic([a,missing]).observations!.rxPeak,false)
const jitter=build(rows('jitter',[0,0,375_000_000],[1,30_001,59_999]))
assert.equal(mergeBuiltTraffic([a,jitter]).observations!.rxPeak,false,'sharing a chart bucket does not establish a matching wave')
const differentStart=build(rows('start',[0,375_000_000],[15_000,30_000]))
assert.equal(mergeBuiltTraffic([a,differentStart]).observations!.rxPeak,false,'same end with different interval is not simultaneous')
const recovery=build(rows('recovery',[0,0,375_000_000],[0,30_000,60_000],[false,false,true]))
assert.equal(mergeBuiltTraffic([a,recovery]).rxPeak,100)
assert.equal(mergeBuiltTraffic([a,recovery]).observations!.rxPeak,true)
assert.equal(mergeBuiltTraffic([a,build([])]).observations!.rxPeak,false)
assert.equal(mergeBuiltTraffic([a,{...a}]).observations!.rxPeak,false,'serialized legacy parts have no raw interval proof')
assert(!JSON.stringify(separate).includes('rxKnown'),'internal points are not DTO fields')
console.log('aggregate peak: separate/coincident/nested/zero/missing/jitter/interval/recovery/empty/legacy/serialization passed')
+40 -9
View File
@@ -25,6 +25,7 @@ export interface RatePoint {
}
export interface TrafficObservations {rxNow:boolean;txNow:boolean;rxSeries:boolean[];txSeries:boolean[];sampledAt:string|null;expiresAt?:string|null;measurementIntervalMs?:number|null;
rxPeak?:boolean;txPeak?:boolean;
currentState?: import("./traffic-freshness.js").TrafficCurrentState;lastAttemptAt?:string|null;lastSucceededAt?:string|null;error?:string|null}
export interface BuiltTrafficSeries {
@@ -40,6 +41,14 @@ export interface BuiltTrafficSeries {
txSeries: number[]
}
type PeakWave = { start: number; rx: number; tx: number; rxKnown: boolean; txKnown: boolean }
// Raw intervals stay internal: DTO serialization must not expose or round these points.
const peakWaves = new WeakMap<BuiltTrafficSeries, Map<number, PeakWave>>()
function retainPeakWaves(built: BuiltTrafficSeries, waves: Map<number, PeakWave>): BuiltTrafficSeries {
peakWaves.set(built, waves)
return built
}
export interface MonitorLiveSample {
rxMbps: number
txMbps: number
@@ -129,7 +138,7 @@ export function buildTrafficFromSamples(
txSeries: Array.from({ length: SERIES_POINTS }, () => 0),
}
const noObservations:TrafficObservations={rxNow:false,txNow:false,rxSeries:Array(SERIES_POINTS).fill(false),txSeries:Array(SERIES_POINTS).fill(false),sampledAt:null}
if (rows.length === 0) return includeObservations?{...empty,observations:noObservations}:empty
if (rows.length === 0) return retainPeakWaves(includeObservations?{...empty,observations:{...noObservations,rxPeak:false,txPeak:false}}:empty,new Map())
const bySeries = new Map<string, TrafficSampleLike[]>()
for (const r of rows) {
@@ -145,7 +154,7 @@ export function buildTrafficFromSamples(
const rxPoints: 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;intervalMs:number }>()
const byTs = new Map<number, { rx: number; tx: number;rxObserved:boolean;txObserved:boolean;count:number;intervalMs:number;start:number;sameStart:boolean }>()
let includedSeries=0,lastAttemptAt:number|null=null
const membership = new Map<number, {count:number; declared:boolean; active:number}>()
let latestMembershipAt = -Infinity
@@ -205,7 +214,8 @@ export function buildTrafficFromSamples(
if (rxBps == null && txBps == null) continue
const rxMbps = bpsToMbps(rxBps ?? 0)
const txMbps = bpsToMbps(txBps ?? 0)
const acc = byTs.get(t1) ?? { rx: 0, tx: 0,rxObserved:true,txObserved:true,count:0,intervalMs:0 }
const acc = byTs.get(t1) ?? { rx: 0, tx: 0,rxObserved:true,txObserved:true,count:0,intervalMs:0,start:t0,sameStart:true }
acc.sameStart &&= acc.start === t0
acc.intervalMs = Math.max(acc.intervalMs, t1 - t0)
acc.count++;acc.rxObserved&&=rxBps!==null;acc.txObserved&&=txBps!==null
acc.rx += rxMbps
@@ -228,12 +238,18 @@ export function buildTrafficFromSamples(
const last = lastTs != null ? byTs.get(lastTs) : undefined
const peak = (points: Array<{t:number;v:number}>, side: "rxObserved"|"txObserved") => points.reduce((m,p) => {
const wave = byTs.get(p.t)!
return p.t >= rangeStartMs && p.t <= rangeEndMs && wave[side] && wave.count === expectedAt(p.t) ? Math.max(m,p.v) : m
return p.t >= rangeStartMs && p.t <= rangeEndMs && wave.sameStart && wave[side] && wave.count === expectedAt(p.t) ? Math.max(m,p.v) : m
},0)
const rxPeak = peak(rxPoints,"rxObserved")
const txPeak = peak(txPoints,"txObserved")
return {
const waves = new Map<number, PeakWave>()
for (const [t, wave] of byTs) {
if (t < rangeStartMs || t > rangeEndMs) continue
const complete = wave.sameStart && wave.count === expectedAt(t)
waves.set(t,{start:wave.start,rx:wave.rx,tx:wave.tx,rxKnown:complete&&wave.rxObserved,txKnown:complete&&wave.txObserved})
}
return retainPeakWaves({
rxNow: last?.rx ?? 0,
txNow: last?.tx ?? 0,
rxPeak,
@@ -250,8 +266,10 @@ export function buildTrafficFromSamples(
txSeries:bucketAvg([...byTs].map(([t,v])=>({t,v:Number(v.txObserved&&v.count===expectedAt(t)&&t>=rangeStartMs&&t<=rangeEndMs)})),rangeStartMs,rangeEndMs).map(v=>v===1),
sampledAt:lastAttemptAt===null?null:new Date(lastAttemptAt).toISOString(),
measurementIntervalMs:lastTs===lastAttemptAt?last?.intervalMs??null:null,
rxPeak:[...waves.values()].some(w=>w.rxKnown),
txPeak:[...waves.values()].some(w=>w.txKnown),
}}:{}),
}
},waves)
}
export function mergeBuiltTraffic(parts: BuiltTrafficSeries[]): BuiltTrafficSeries {
@@ -281,8 +299,6 @@ export function mergeBuiltTraffic(parts: BuiltTrafficSeries[]): BuiltTrafficSeri
for (const p of parts) {
acc.rxNow += p.rxNow
acc.txNow += p.txNow
acc.rxPeak += p.rxPeak
acc.txPeak += p.txPeak
acc.rxTotalGiB += p.rxTotalGiB
acc.txTotalGiB += p.txTotalGiB
acc.sessions += p.sessions
@@ -291,7 +307,22 @@ export function mergeBuiltTraffic(parts: BuiltTrafficSeries[]): BuiltTrafficSeri
acc.txSeries[i] += p.txSeries[i] ?? 0
}
}
return parts.every(p => p.observations) ? { ...acc, observations: mergeTrafficObservations(parts.map(p => p.observations!)) } : acc
const waves = new Map<number, PeakWave>()
const first = peakWaves.get(parts[0])
for (const [t, wave] of first ?? []) {
const matching = parts.map(p => peakWaves.get(p)?.get(t))
if (!matching.every(w => w && w.start === wave.start)) continue
const rows = matching as PeakWave[]
const combined = {start:wave.start,rx:rows.reduce((n,w)=>n+w.rx,0),tx:rows.reduce((n,w)=>n+w.tx,0),
rxKnown:rows.every(w=>w.rxKnown),txKnown:rows.every(w=>w.txKnown)}
waves.set(t,combined)
if (combined.rxKnown) acc.rxPeak = Math.max(acc.rxPeak,combined.rx)
if (combined.txKnown) acc.txPeak = Math.max(acc.txPeak,combined.tx)
}
return retainPeakWaves(parts.every(p => p.observations) ? { ...acc, observations: {
...mergeTrafficObservations(parts.map(p => p.observations!)),
rxPeak:[...waves.values()].some(w=>w.rxKnown),txPeak:[...waves.values()].some(w=>w.txKnown),
} } : acc,waves)
}
export function parseMonitorTraffic(
@@ -59,6 +59,19 @@ else {
for(const[name,offset,bytes]of[["ether1",-90_000,0],["ether1",-60_000,0],["ether1",-30_000,1024**2],["new",-30_000,0],["ether1",0,1024**2],["new",0,0]] as const)await put(name,offset,bytes);
await pool.query("UPDATE traffic_samples SET flags=61,native_counter_identity=interface_name WHERE server_id=$1",[serverId]);await pool.query("UPDATE traffic_samples SET flags=45 WHERE server_id=$1 AND interface_name='new' AND sampled_at=$2",[serverId,new Date(Date.parse(at)-30_000).toISOString()]);
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.observations.rxNow,true);assert.equal(s.rxPeak,0);assert.equal(s.txPeak,0);assert.equal(s.rxTotal,1/1024);assert(s.observations.rxSeries.some(Boolean));cases++;
await pool.query("DELETE FROM user_interface_bindings WHERE user_id=$1",[userId])
for(const name of ['peakA','peakB'])await pool.query("INSERT INTO user_interface_bindings(id,user_id,server_id,interface_name,interface_type) VALUES($1,$2,$3,$4,'ether')",[prefix+name,userId,serverId,name])
for(const mode of ['separate','coincident','jitter']){
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId])
for(const [name,offset,bytes] of [['peakA',-60_000,0],['peakA',-30_000,375_000_000],['peakA',0,375_000_000],
['peakB',-60_000,0],['peakB',-30_000,mode==='coincident'?375_000_000:0],['peakB',0,375_000_000]] as const)
await put(name,offset+(mode==='jitter'&&name==='peakB'?-1:0),bytes)
const aggregate=(await get('/traffic/users?range=1h')).users.find((u:{id:string})=>u.id===userId)
assert.equal(aggregate.rxPeak,mode==='jitter'?0:mode==='coincident'?200:100)
assert.equal(aggregate.observations.rxPeak,mode!=='jitter');assert.equal(aggregate.observations.txPeak,mode!=='jitter')
assert.equal(aggregate.rxTotal,750_000_000/1024**3);assert(aggregate.observations.rxSeries.some(Boolean))
assert.equal(aggregate.interfaces.length,2);assert(aggregate.interfaces.every((i:{rxPeak:number})=>i.rxPeak===100));cases++
}
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()}
}
+20 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.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: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
@@ -2296,3 +2296,21 @@ Unit проверил partial high point → последующая complete0 wa
Точный следующий шаг: общий peak вычислять из подтверждённых совпадающих временных срезов, сохраняя исходные timestamp/confidence внутренних parts; не складывать индивидуальные maxima. При отсутствии совместимого времени показывать неизвестное значение, сохраняя volumes/history/current metadata. Проверить разные времена peaks, совпадающий peak, missing contributor, jitter/неполные bins и recovery. Не подменять raw point peak максимумом bucket average без явной семантики. Нужны meaningful own SQL/API/UI cases перед production rollout.
Стенд остаётся backend ux-peak190/frontend ux-warning183. Source/runtime190 и real CHR/config/passwords unchanged; messages0. После191 — scoped control затронутых разделов и одна согласованная общая регрессия; внешние Portal/Telegram/client OS/multi-WAN/scale требуют доступов/сценариев из матрицы.12.5 открыта. Локальный commit без push сохраняет точное продолжение.
## 12.5.191 — подтверждённый общий пик привязок: реализация и приёмка (2026-10-13)
Исправлен baseline ниже: индивидуальные максимумы в разные моменты больше не складываются. A=100Mbps на первой волне и B=100Mbps на следующей дают общий пик100, совпадающие интервалы —200. Сумма byte deltas/учтённый объём сохранена. Пик является максимумом подтверждённых средних скоростей соседних счётчиков за совпадающие интервалы, а не мгновенным максимумом линии или максимумом chart bucket average.
Исходные интервалы и confidence parts хранятся во внутреннем WeakMap и не попадают в DTO/JSON. Merge, включая вложенный, требует совпадения начала и конца интервала и подтверждённых вкладов всех выбранных привязок для каждого направления. Jitter, отсутствие part, несовместимые интервалы или сериализованные parts без исходных точек не доказывают общий пик: observations.rxPeak/txPeak=false, UI показывает «—», а не ложный ноль. Подтверждённый ноль остаётся0. Для старого backend новый reader сохраняет прежнюю совместимость при отсутствии новых optional flags; interop проверен до переключения backend. История, объём и freshness metadata сохраняются. Данные разных серверов не интерполируются и не сдвигаются ради создания искусственного общего пика.
Изменены traffic-rate.ts, lib/traffic-observations.ts и профильные тесты. ReUI PRO/base-nova/Frame/KpiStatGrid переиспользуются, vendor/theme/layout не менялись. Проверены different/coincident peaks, nested merge, measured zero, missing/empty/legacy parts, jitter внутри одинаковых chart bins, одинаковый конец с разным началом и recovery. Реальный PostgreSQL/API fixture добавляет3 сценария пользователя с двумя привязками, включая неизменный объём750000000bytes. Все71 профильных SQL-проверок прошли41228ms без postgres-unavailable skip; собственные временная БД, роль и контейнер удалены.
Backend build и frontend TypeScript passed. Все77 уникальных E2E трафика прошли1.6m; начальные4 focused cases22.4s входят в77 и повторно не считаются. Production backend/frontend build passed; source manifests совпали461 backend+524 frontend файлов,1040 compiled backend artifacts совпали. Runtime backend/frontend ux-peak191, PostgreSQL healthy; frontend healthcheck отсутствует, работа подтверждена HTTP/browser. До backend rollout новый frontend принят с backend190. Actual API200 и fresh measurements обоих CHR, live UI1440/320 без page errors/overflow. Native конфигурация/пароли не менялись, messages0.
Протоколы: [SQL](ux-ui/acceptance/traffic-peak191-sql191-result.json), [source](ux-ui/acceptance/traffic-peak191-source191-result.json), [compiled](ux-ui/acceptance/traffic-peak191-compiled191-result.json), [runtime](ux-ui/acceptance/traffic-peak191-runtime191-result.json), [API](ux-ui/acceptance/traffic-peak191-peak191-api-result.json), [E2E](ux-ui/acceptance/traffic-peak-e2e-2026-10-13.json), [interop](ux-ui/acceptance/traffic-peak-interop-2026-10-13.json), [live UI](ux-ui/acceptance/traffic-peak-deployed-2026-10-13.json).
| Референс | Схема |
| --- | --- |
| [ReUI Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Совпадающие интервалы всех привязок → подтверждённая сумма → максимум периода; нет совпадения → «—», объём сохранён |
Следующий192: одна согласованная общая backend-регрессия; фиксировать реальные skipped/external ветки отдельно. Последующий объём определяется результатом этой проверки и матрицей внешней приёмки. Portal/Telegram/client OS/multi-WAN/scale не приняты по fixture-тестам.12.5 открыта.
+3 -3
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-12
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.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: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
+20 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -36,7 +36,25 @@
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
| 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–191 и ограничения ниже. Runtime backend/frontend ux-peak191. |
## 12.5.191 — подтверждённый общий пик привязок: реализация и приёмка (2026-10-13)
Исправлен baseline ниже: индивидуальные максимумы в разные моменты больше не складываются. A=100Mbps на первой волне и B=100Mbps на следующей дают общий пик100, совпадающие интервалы —200. Сумма byte deltas/учтённый объём сохранена. Пик является максимумом подтверждённых средних скоростей соседних счётчиков за совпадающие интервалы, а не мгновенным максимумом линии или максимумом chart bucket average.
Исходные интервалы и confidence parts хранятся во внутреннем WeakMap и не попадают в DTO/JSON. Merge, включая вложенный, требует совпадения начала и конца интервала и подтверждённых вкладов всех выбранных привязок для каждого направления. Jitter, отсутствие part, несовместимые интервалы или сериализованные parts без исходных точек не доказывают общий пик: observations.rxPeak/txPeak=false, UI показывает «—», а не ложный ноль. Подтверждённый ноль остаётся0. Для старого backend новый reader сохраняет прежнюю совместимость при отсутствии новых optional flags; interop проверен до переключения backend. История, объём и freshness metadata сохраняются. Данные разных серверов не интерполируются и не сдвигаются ради создания искусственного общего пика.
Изменены traffic-rate.ts, lib/traffic-observations.ts и профильные тесты. ReUI PRO/base-nova/Frame/KpiStatGrid переиспользуются, vendor/theme/layout не менялись. Проверены different/coincident peaks, nested merge, measured zero, missing/empty/legacy parts, jitter внутри одинаковых chart bins, одинаковый конец с разным началом и recovery. Реальный PostgreSQL/API fixture добавляет3 сценария пользователя с двумя привязками, включая неизменный объём750000000bytes. Все71 профильных SQL-проверок прошли41228ms без postgres-unavailable skip; собственные временная БД, роль и контейнер удалены.
Backend build и frontend TypeScript passed. Все77 уникальных E2E трафика прошли1.6m; начальные4 focused cases22.4s входят в77 и повторно не считаются. Production backend/frontend build passed; source manifests совпали461 backend+524 frontend файлов,1040 compiled backend artifacts совпали. Runtime backend/frontend ux-peak191, PostgreSQL healthy; frontend healthcheck отсутствует, работа подтверждена HTTP/browser. До backend rollout новый frontend принят с backend190. Actual API200 и fresh measurements обоих CHR, live UI1440/320 без page errors/overflow. Native конфигурация/пароли не менялись, messages0.
Протоколы: [SQL](ux-ui/acceptance/traffic-peak191-sql191-result.json), [source](ux-ui/acceptance/traffic-peak191-source191-result.json), [compiled](ux-ui/acceptance/traffic-peak191-compiled191-result.json), [runtime](ux-ui/acceptance/traffic-peak191-runtime191-result.json), [API](ux-ui/acceptance/traffic-peak191-peak191-api-result.json), [E2E](ux-ui/acceptance/traffic-peak-e2e-2026-10-13.json), [interop](ux-ui/acceptance/traffic-peak-interop-2026-10-13.json), [live UI](ux-ui/acceptance/traffic-peak-deployed-2026-10-13.json).
| Референс | Схема |
| --- | --- |
| [ReUI Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Совпадающие интервалы всех привязок → подтверждённая сумма → максимум периода; нет совпадения → «—», объём сохранён |
Следующий192: одна согласованная общая backend-регрессия; фиксировать реальные skipped/external ветки отдельно. Последующий объём определяется результатом этой проверки и матрицей внешней приёмки. Portal/Telegram/client OS/multi-WAN/scale не приняты по fixture-тестам.12.5 открыта.
## 12.5.191 — общий пик нескольких привязок: baseline (2026-10-13)
+20 -2
View File
@@ -1,10 +1,10 @@
# Оставшаяся внешняя приёмка — 2026-10-13
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Это актуальная матрица после 12.5.191. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-peak190`, 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/frontend `ux-peak191`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
| Раздел | Подтверждено | Осталось / условие завершения |
| --- | --- | --- |
@@ -1212,3 +1212,21 @@ Unit проверил partial high point → последующая complete0 wa
Точный следующий шаг: общий peak вычислять из подтверждённых совпадающих временных срезов, сохраняя исходные timestamp/confidence внутренних parts; не складывать индивидуальные maxima. При отсутствии совместимого времени показывать неизвестное значение, сохраняя volumes/history/current metadata. Проверить разные времена peaks, совпадающий peak, missing contributor, jitter/неполные bins и recovery. Не подменять raw point peak максимумом bucket average без явной семантики. Нужны meaningful own SQL/API/UI cases перед production rollout.
Стенд остаётся backend ux-peak190/frontend ux-warning183. Source/runtime190 и real CHR/config/passwords unchanged; messages0. После191 — scoped control затронутых разделов и одна согласованная общая регрессия; внешние Portal/Telegram/client OS/multi-WAN/scale требуют доступов/сценариев из матрицы.12.5 открыта. Локальный commit без push сохраняет точное продолжение.
## 12.5.191 — подтверждённый общий пик привязок: реализация и приёмка (2026-10-13)
Исправлен baseline ниже: индивидуальные максимумы в разные моменты больше не складываются. A=100Mbps на первой волне и B=100Mbps на следующей дают общий пик100, совпадающие интервалы —200. Сумма byte deltas/учтённый объём сохранена. Пик является максимумом подтверждённых средних скоростей соседних счётчиков за совпадающие интервалы, а не мгновенным максимумом линии или максимумом chart bucket average.
Исходные интервалы и confidence parts хранятся во внутреннем WeakMap и не попадают в DTO/JSON. Merge, включая вложенный, требует совпадения начала и конца интервала и подтверждённых вкладов всех выбранных привязок для каждого направления. Jitter, отсутствие part, несовместимые интервалы или сериализованные parts без исходных точек не доказывают общий пик: observations.rxPeak/txPeak=false, UI показывает «—», а не ложный ноль. Подтверждённый ноль остаётся0. Для старого backend новый reader сохраняет прежнюю совместимость при отсутствии новых optional flags; interop проверен до переключения backend. История, объём и freshness metadata сохраняются. Данные разных серверов не интерполируются и не сдвигаются ради создания искусственного общего пика.
Изменены traffic-rate.ts, lib/traffic-observations.ts и профильные тесты. ReUI PRO/base-nova/Frame/KpiStatGrid переиспользуются, vendor/theme/layout не менялись. Проверены different/coincident peaks, nested merge, measured zero, missing/empty/legacy parts, jitter внутри одинаковых chart bins, одинаковый конец с разным началом и recovery. Реальный PostgreSQL/API fixture добавляет3 сценария пользователя с двумя привязками, включая неизменный объём750000000bytes. Все71 профильных SQL-проверок прошли41228ms без postgres-unavailable skip; собственные временная БД, роль и контейнер удалены.
Backend build и frontend TypeScript passed. Все77 уникальных E2E трафика прошли1.6m; начальные4 focused cases22.4s входят в77 и повторно не считаются. Production backend/frontend build passed; source manifests совпали461 backend+524 frontend файлов,1040 compiled backend artifacts совпали. Runtime backend/frontend ux-peak191, PostgreSQL healthy; frontend healthcheck отсутствует, работа подтверждена HTTP/browser. До backend rollout новый frontend принят с backend190. Actual API200 и fresh measurements обоих CHR, live UI1440/320 без page errors/overflow. Native конфигурация/пароли не менялись, messages0.
Протоколы: [SQL](ux-ui/acceptance/traffic-peak191-sql191-result.json), [source](ux-ui/acceptance/traffic-peak191-source191-result.json), [compiled](ux-ui/acceptance/traffic-peak191-compiled191-result.json), [runtime](ux-ui/acceptance/traffic-peak191-runtime191-result.json), [API](ux-ui/acceptance/traffic-peak191-peak191-api-result.json), [E2E](ux-ui/acceptance/traffic-peak-e2e-2026-10-13.json), [interop](ux-ui/acceptance/traffic-peak-interop-2026-10-13.json), [live UI](ux-ui/acceptance/traffic-peak-deployed-2026-10-13.json).
| Референс | Схема |
| --- | --- |
| [ReUI Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Совпадающие интервалы всех привязок → подтверждённая сумма → максимум периода; нет совпадения → «—», объём сохранён |
Следующий192: одна согласованная общая backend-регрессия; фиксировать реальные skipped/external ветки отдельно. Последующий объём определяется результатом этой проверки и матрицей внешней приёмки. Portal/Telegram/client OS/multi-WAN/scale не приняты по fixture-тестам.12.5 открыта.
+19 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -3601,3 +3601,21 @@ Unit проверил partial high point → последующая complete0 wa
Точный следующий шаг: общий peak вычислять из подтверждённых совпадающих временных срезов, сохраняя исходные timestamp/confidence внутренних parts; не складывать индивидуальные maxima. При отсутствии совместимого времени показывать неизвестное значение, сохраняя volumes/history/current metadata. Проверить разные времена peaks, совпадающий peak, missing contributor, jitter/неполные bins и recovery. Не подменять raw point peak максимумом bucket average без явной семантики. Нужны meaningful own SQL/API/UI cases перед production rollout.
Стенд остаётся backend ux-peak190/frontend ux-warning183. Source/runtime190 и real CHR/config/passwords unchanged; messages0. После191 — scoped control затронутых разделов и одна согласованная общая регрессия; внешние Portal/Telegram/client OS/multi-WAN/scale требуют доступов/сценариев из матрицы.12.5 открыта. Локальный commit без push сохраняет точное продолжение.
## 12.5.191 — подтверждённый общий пик привязок: реализация и приёмка (2026-10-13)
Исправлен baseline ниже: индивидуальные максимумы в разные моменты больше не складываются. A=100Mbps на первой волне и B=100Mbps на следующей дают общий пик100, совпадающие интервалы —200. Сумма byte deltas/учтённый объём сохранена. Пик является максимумом подтверждённых средних скоростей соседних счётчиков за совпадающие интервалы, а не мгновенным максимумом линии или максимумом chart bucket average.
Исходные интервалы и confidence parts хранятся во внутреннем WeakMap и не попадают в DTO/JSON. Merge, включая вложенный, требует совпадения начала и конца интервала и подтверждённых вкладов всех выбранных привязок для каждого направления. Jitter, отсутствие part, несовместимые интервалы или сериализованные parts без исходных точек не доказывают общий пик: observations.rxPeak/txPeak=false, UI показывает «—», а не ложный ноль. Подтверждённый ноль остаётся0. Для старого backend новый reader сохраняет прежнюю совместимость при отсутствии новых optional flags; interop проверен до переключения backend. История, объём и freshness metadata сохраняются. Данные разных серверов не интерполируются и не сдвигаются ради создания искусственного общего пика.
Изменены traffic-rate.ts, lib/traffic-observations.ts и профильные тесты. ReUI PRO/base-nova/Frame/KpiStatGrid переиспользуются, vendor/theme/layout не менялись. Проверены different/coincident peaks, nested merge, measured zero, missing/empty/legacy parts, jitter внутри одинаковых chart bins, одинаковый конец с разным началом и recovery. Реальный PostgreSQL/API fixture добавляет3 сценария пользователя с двумя привязками, включая неизменный объём750000000bytes. Все71 профильных SQL-проверок прошли41228ms без postgres-unavailable skip; собственные временная БД, роль и контейнер удалены.
Backend build и frontend TypeScript passed. Все77 уникальных E2E трафика прошли1.6m; начальные4 focused cases22.4s входят в77 и повторно не считаются. Production backend/frontend build passed; source manifests совпали461 backend+524 frontend файлов,1040 compiled backend artifacts совпали. Runtime backend/frontend ux-peak191, PostgreSQL healthy; frontend healthcheck отсутствует, работа подтверждена HTTP/browser. До backend rollout новый frontend принят с backend190. Actual API200 и fresh measurements обоих CHR, live UI1440/320 без page errors/overflow. Native конфигурация/пароли не менялись, messages0.
Протоколы: [SQL](ux-ui/acceptance/traffic-peak191-sql191-result.json), [source](ux-ui/acceptance/traffic-peak191-source191-result.json), [compiled](ux-ui/acceptance/traffic-peak191-compiled191-result.json), [runtime](ux-ui/acceptance/traffic-peak191-runtime191-result.json), [API](ux-ui/acceptance/traffic-peak191-peak191-api-result.json), [E2E](ux-ui/acceptance/traffic-peak-e2e-2026-10-13.json), [interop](ux-ui/acceptance/traffic-peak-interop-2026-10-13.json), [live UI](ux-ui/acceptance/traffic-peak-deployed-2026-10-13.json).
| Референс | Схема |
| --- | --- |
| [ReUI Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Совпадающие интервалы всех привязок → подтверждённая сумма → максимум периода; нет совпадения → «—», объём сохранён |
Следующий192: одна согласованная общая backend-регрессия; фиксировать реальные skipped/external ветки отдельно. Последующий объём определяется результатом этой проверки и матрицей внешней приёмки. Portal/Telegram/client OS/multi-WAN/scale не приняты по fixture-тестам.12.5 открыта.
+20 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -708,7 +708,7 @@ flowchart LR
## 12.5.77 — основной блок IPsec (2026-10-09)
Актуальное продолжение 2026-10-13: завершены185–190, backend ux-peak190/frontend ux-warning183 healthy; последний ownSQL68/38553ms/API/browser2 passed. Начат191 — baseline sum individual peaks200Mbps против complete simultaneous wave100Mbps; это не исправлено и не принято. Следующий шаг — temporal aggregate peak с confidence при несовпадающих timestamps.12.5 открыта.
Актуальное продолжение 2026-10-13: завершён191 — общий пик привязок по совпадающим подтверждённым интервалам; backend/frontend ux-peak191 развёрнуты. ownSQL71/41228ms, E2E77/1.6m, source985/compiled1040/API/live UI2 passed. Начата192 — общая backend-регрессия в отдельной временной БД.12.5 открыта.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
@@ -2027,3 +2027,21 @@ Unit проверил partial high point → последующая complete0 wa
Точный следующий шаг: общий peak вычислять из подтверждённых совпадающих временных срезов, сохраняя исходные timestamp/confidence внутренних parts; не складывать индивидуальные maxima. При отсутствии совместимого времени показывать неизвестное значение, сохраняя volumes/history/current metadata. Проверить разные времена peaks, совпадающий peak, missing contributor, jitter/неполные bins и recovery. Не подменять raw point peak максимумом bucket average без явной семантики. Нужны meaningful own SQL/API/UI cases перед production rollout.
Стенд остаётся backend ux-peak190/frontend ux-warning183. Source/runtime190 и real CHR/config/passwords unchanged; messages0. После191 — scoped control затронутых разделов и одна согласованная общая регрессия; внешние Portal/Telegram/client OS/multi-WAN/scale требуют доступов/сценариев из матрицы.12.5 открыта. Локальный commit без push сохраняет точное продолжение.
## 12.5.191 — подтверждённый общий пик привязок: реализация и приёмка (2026-10-13)
Исправлен baseline ниже: индивидуальные максимумы в разные моменты больше не складываются. A=100Mbps на первой волне и B=100Mbps на следующей дают общий пик100, совпадающие интервалы —200. Сумма byte deltas/учтённый объём сохранена. Пик является максимумом подтверждённых средних скоростей соседних счётчиков за совпадающие интервалы, а не мгновенным максимумом линии или максимумом chart bucket average.
Исходные интервалы и confidence parts хранятся во внутреннем WeakMap и не попадают в DTO/JSON. Merge, включая вложенный, требует совпадения начала и конца интервала и подтверждённых вкладов всех выбранных привязок для каждого направления. Jitter, отсутствие part, несовместимые интервалы или сериализованные parts без исходных точек не доказывают общий пик: observations.rxPeak/txPeak=false, UI показывает «—», а не ложный ноль. Подтверждённый ноль остаётся0. Для старого backend новый reader сохраняет прежнюю совместимость при отсутствии новых optional flags; interop проверен до переключения backend. История, объём и freshness metadata сохраняются. Данные разных серверов не интерполируются и не сдвигаются ради создания искусственного общего пика.
Изменены traffic-rate.ts, lib/traffic-observations.ts и профильные тесты. ReUI PRO/base-nova/Frame/KpiStatGrid переиспользуются, vendor/theme/layout не менялись. Проверены different/coincident peaks, nested merge, measured zero, missing/empty/legacy parts, jitter внутри одинаковых chart bins, одинаковый конец с разным началом и recovery. Реальный PostgreSQL/API fixture добавляет3 сценария пользователя с двумя привязками, включая неизменный объём750000000bytes. Все71 профильных SQL-проверок прошли41228ms без postgres-unavailable skip; собственные временная БД, роль и контейнер удалены.
Backend build и frontend TypeScript passed. Все77 уникальных E2E трафика прошли1.6m; начальные4 focused cases22.4s входят в77 и повторно не считаются. Production backend/frontend build passed; source manifests совпали461 backend+524 frontend файлов,1040 compiled backend artifacts совпали. Runtime backend/frontend ux-peak191, PostgreSQL healthy; frontend healthcheck отсутствует, работа подтверждена HTTP/browser. До backend rollout новый frontend принят с backend190. Actual API200 и fresh measurements обоих CHR, live UI1440/320 без page errors/overflow. Native конфигурация/пароли не менялись, messages0.
Протоколы: [SQL](ux-ui/acceptance/traffic-peak191-sql191-result.json), [source](ux-ui/acceptance/traffic-peak191-source191-result.json), [compiled](ux-ui/acceptance/traffic-peak191-compiled191-result.json), [runtime](ux-ui/acceptance/traffic-peak191-runtime191-result.json), [API](ux-ui/acceptance/traffic-peak191-peak191-api-result.json), [E2E](ux-ui/acceptance/traffic-peak-e2e-2026-10-13.json), [interop](ux-ui/acceptance/traffic-peak-interop-2026-10-13.json), [live UI](ux-ui/acceptance/traffic-peak-deployed-2026-10-13.json).
| Референс | Схема |
| --- | --- |
| [ReUI Stats12](https://reui.io/preview/base/stats-12?ref=mcp) | Совпадающие интервалы всех привязок → подтверждённая сумма → максимум периода; нет совпадения → «—», объём сохранён |
Следующий192: одна согласованная общая backend-регрессия; фиксировать реальные skipped/external ветки отдельно. Последующий объём определяется результатом этой проверки и матрицей внешней приёмки. Portal/Telegram/client OS/multi-WAN/scale не приняты по fixture-тестам.12.5 открыта.
@@ -1,9 +1,9 @@
{
"date": "2026-10-13",
"block": "12.5.190",
"block": "12.5.191",
"runtime": {
"backend": "ux-peak190",
"frontend": "ux-warning183"
"backend": "ux-peak191",
"frontend": "ux-peak191"
},
"apis": [
{
@@ -0,0 +1,16 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"command": "playwright test traffic-.*\\.spec\\.ts",
"uniqueCases": 77,
"passed": 77,
"duration": "1.6m",
"focusedInitialRun": {
"passed": 4,
"durationSeconds": 22.4,
"includedInUniqueCount": true
},
"scope": "Ordinary HTTP API fixture path, not built-in mock mode; separate deployed read-only smoke",
"frontendTypeScript": true,
"backendBuild": true
}
@@ -0,0 +1,10 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"newFrontend": "ux-peak191",
"oldBackend": "ux-peak190",
"readerAccepted": true,
"serverCount": 2,
"pageErrors": 0,
"bound": "Additive observed DTO reader compatibility; legacy numeric-only paths verified separately as unknown"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,26 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"version": "1.0.0-test.peak191",
"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.012759584002196789,
"txTotalGiB": 0.0008463365957140923,
"currentState": "current"
},
{
"sourceId": "2",
"volumeUnit": "GiB",
"rxTotalGiB": 0.0011972729116678238,
"txTotalGiB": 0.0014407914131879807,
"currentState": "current"
}
],
"newRouterOSCommands": 0,
"passwordChanges": 0,
"messages": 0
}
@@ -0,0 +1,25 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"secretEnvCopied": false,
"rollbackTags": {
"MMAPP_IMAGE_TAG": "ux-warning183",
"BACKEND_IMAGE_TAG": "ux-peak190"
},
"services": {
"backend": {
"image": "mmapp-test/backend:ux-peak191",
"imageId": "sha256:f6db37afa8263cf16ddb955938c7c20796b49570a08f8f706803b94e30812fb7",
"state": "running",
"health": "healthy",
"matchedVerifiedImage": true
},
"frontend": {
"image": "mmapp-test/frontend:ux-peak191",
"imageId": "sha256:1d9f85dbb5c791f09dad2863041d0fae5a291b02f5e73a5e96185a7866c673c7",
"state": "running",
"health": null,
"matchedVerifiedImage": true
}
}
}
@@ -0,0 +1,10 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"matched": {
"backend": 461,
"frontend": 524
},
"different": [],
"missing": []
}
@@ -0,0 +1,31 @@
{
"date": "2026-10-13",
"block": "12.5.191",
"passed": true,
"profiles": [
{
"date": "2026-10-13",
"block": "12.5.191",
"database": "mm_accept_166_1791874191164",
"scope": "ephemeral role and precreated owned database; no copied production credentials",
"tests": [
{
"command": "npm --prefix backend run test:traffic-rate",
"passed": true,
"durationMs": 41228,
"ownedTrafficSqlCases": 9,
"ownedHistorySqlCases": 14,
"ownedLiveSqlCases": 10,
"ownedVolumeSqlCases": 17,
"ownedStatusSqlCases": 21,
"postgresUnavailableSkip": false
}
],
"scopedRoleVerified": true,
"passed": true,
"databaseRemoved": true,
"roleRemoved": true,
"testContainerRemoved": true
}
]
}
+4
View File
@@ -12,3 +12,7 @@ const stale=projectTrafficObservations({...row,observations:obs},at+90_001);asse
assert.equal(projectTrafficObservations({...row,observations:{...obs,currentState:"error"}},at).rxNow,null)
for(const change of [{rxNow:-1},{observations:{...obs,sampledAt:"bad"}},{observations:{...obs,sampledAt:null}},{observations:{...obs,rxSeries:[]}}])assert.throws(()=>projectTrafficObservations({...row,...change},at))
console.log("traffic observations nullable/zero/gaps/expiry guards tests ok")
const noPeak=projectTrafficObservations({...row,volumeUnit:"GiB",observations:{...obs,rxPeak:false,txPeak:false}},at)
assert.equal(noPeak.rxPeak,null);assert.equal(noPeak.txPeak,null);assert.equal(noPeak.rxTotal,0)
assert.equal(projectTrafficObservations({...row,observations:{...obs,rxPeak:true,txPeak:true}},at).rxPeak,0)
console.log('aggregate peak confidence overrides bucket confidence; measured zero and volumes retained')
+3 -2
View File
@@ -2,6 +2,7 @@ import { z } from "zod"
const timestamp = z.string().refine(v => Number.isFinite(Date.parse(v))).nullable()
const rate = z.number().finite().nonnegative().nullable()
const meta = z.object({ rxNow: z.boolean(), txNow: z.boolean(), rxSeries: z.array(z.boolean()), txSeries: z.array(z.boolean()),
rxPeak: z.boolean().optional(), txPeak: z.boolean().optional(),
sampledAt: timestamp, expiresAt: timestamp.optional(), lastAttemptAt: timestamp.optional(), lastSucceededAt: timestamp.optional(), error: z.string().nullable().optional(),
currentState: z.enum(["current", "unknown", "pending", "error", "stale", "disabled"]).optional() })
export type TrafficObservationMeta = z.infer<typeof meta>
@@ -19,7 +20,7 @@ export function projectTrafficObservations(raw: unknown, now = Date.now()) {
const rxSeries = r.rxSeries.map((v, i) => o?.rxSeries[i] ? v : null)
const txSeries = r.txSeries.map((v, i) => o?.txSeries[i] ? v : null)
return { observations: o, volumeUnit: r.volumeUnit, rxNow: current && o?.rxNow ? r.rxNow : null, txNow: current && o?.txNow ? r.txNow : null,
rxSeries, txSeries, rxPeak: rxSeries.some(v => v !== null) ? r.rxPeak : null,
txPeak: txSeries.some(v => v !== null) ? r.txPeak : null,
rxSeries, txSeries, rxPeak: (o?.rxPeak ?? rxSeries.some(v => v !== null)) ? r.rxPeak : null,
txPeak: (o?.txPeak ?? txSeries.some(v => v !== null)) ? r.txPeak : null,
rxTotal: r.volumeUnit && rxSeries.some(v => v !== null) ? r.rxTotal : null, txTotal: r.volumeUnit && txSeries.some(v => v !== null) ? r.txTotal : null }
}
+25
View File
@@ -0,0 +1,25 @@
import {test,expect} from '@playwright/test'
for(const width of [1440,320])test(`Пик трафика: неизвестный интервал, подтверждённый пик и ноль ${width}`,async({page})=>{
await page.setViewportSize({width,height:1000})
let peakKnown=false,peak=100
const server=()=>({id:'1',name:'Пик CHR',site:'TEST',country:'RU',status:'online',volumeUnit:'GiB',sessions:1,
rxNow:0,txNow:0,rxPeak:peak,txPeak:peak,rxTotal:1,txTotal:1,rxSeries:[0,100],txSeries:[0,100],
observations:{rxNow:true,txNow:true,rxPeak:peakKnown,txPeak:peakKnown,rxSeries:[false,true],txSeries:[false,true],sampledAt:'2026-10-13T00:00:00Z'}})
await page.route('**/health',r=>r.fulfill({json:{ok:true}}))
await page.route('**/api/**',r=>{
const p=new URL(r.request().url()).pathname
if(p.endsWith('/live'))return r.fulfill({status:503,json:{error:'Fixture без live'}})
return r.fulfill({json:p==='/api/auth/config'?{required:false}:p==='/api/traffic/servers'?{servers:[server()]}:
p==='/api/traffic/servers/1'?{server:server()}:p.endsWith('/interfaces')||p==='/api/traffic/bound-interfaces'?{interfaces:[]}:
p==='/api/traffic/users'?{users:[]}:p==='/api/servers'?[]:{}})
})
await page.goto('/traffic')
const peakCard=page.locator('[data-slot="frame-panel"]').filter({has:page.getByText('Пик RX',{exact:true})}).first()
await expect(peakCard).toContainText('—')
await expect(page.getByRole('group',{name:'Учтённый объём по измерениям'})).toContainText('1 ГиБ')
peakKnown=true;await page.getByRole('button',{name:'15м',exact:true}).click()
await expect(peakCard).toContainText('100')
peak=0;await page.getByRole('button',{name:'24ч',exact:true}).click()
await expect(peakCard).toContainText('0')
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
})