fix(traffic): сохранять точность объёмов и обозначать бинарные единицы
This commit is contained in:
1 parent
2358ecef60
commit
2b3d583e3b
29 files changed
+1452
-39
No files matched your search
@@ -19,7 +19,7 @@ import {
|
||||
ArrowUpIcon, ArrowDownIcon, ActivityIcon, UsersIcon, CableIcon, ServerIcon, SearchIcon, GitBranchIcon, PlusIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { fmtGB, fmtRate } from "@/lib/fmt-rate"
|
||||
import { fmtGiB, fmtRate } from "@/lib/fmt-rate"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { useTrafficLive } from "@/hooks/use-traffic-live"
|
||||
import { useFlowLive } from "@/hooks/use-flow-live"
|
||||
@@ -86,6 +86,7 @@ function flowEmptyHint(stats: FlowStatsDto | null, collectorAlive?: boolean): st
|
||||
// ─── data model ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface BoundIfaceTraffic {
|
||||
volumeUnit?: "GiB"
|
||||
observations?: TrafficObservationMeta
|
||||
id: string
|
||||
bindingId: string
|
||||
@@ -113,6 +114,7 @@ interface BoundIfaceTraffic {
|
||||
}
|
||||
|
||||
interface ServerTraffic {
|
||||
volumeUnit?: "GiB"
|
||||
observations?: TrafficObservationMeta
|
||||
id: string
|
||||
name: string
|
||||
@@ -132,6 +134,7 @@ interface ServerTraffic {
|
||||
}
|
||||
|
||||
interface UserTraffic {
|
||||
volumeUnit?: "GiB"
|
||||
observations?: TrafficObservationMeta
|
||||
id: string
|
||||
login: string
|
||||
@@ -150,6 +153,7 @@ interface UserTraffic {
|
||||
}
|
||||
|
||||
interface LiveTrafficServer {
|
||||
volumeUnit?: "GiB"
|
||||
observations?: TrafficObservationMeta
|
||||
id: string
|
||||
name: string
|
||||
@@ -497,8 +501,8 @@ function IfaceRow({ c, showServer = false }: { c: BoundIfaceTraffic; showServer?
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 text-right leading-tight text-[10px] text-muted-foreground w-[72px]">
|
||||
<p className="tabular-nums">{fmtGB(c.rxTotal)}</p>
|
||||
<p className="tabular-nums">{fmtGB(c.txTotal)}</p>
|
||||
<p className="tabular-nums">{fmtGiB(c.rxTotal)}</p>
|
||||
<p className="tabular-nums">{fmtGiB(c.txTotal)}</p>
|
||||
</div>
|
||||
<span className={cn(
|
||||
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium tabular-nums",
|
||||
@@ -555,18 +559,18 @@ function TotalsRow({ rxTotal, txTotal, rxSeries, txSeries }: {
|
||||
rxTotal: number|null; txTotal: number|null; rxSeries: (number|null)[]; txSeries: (number|null)[]
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-4 [&>div]:min-w-0 [&_svg]:max-w-full">
|
||||
<div role="group" aria-label="Учтённый объём по измерениям" className="mt-4 pt-4 border-t grid grid-cols-2 gap-4 [&>div]:min-w-0 [&_svg]:max-w-full">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Учтено RX по замерам</p>
|
||||
<p className="text-lg font-semibold tabular-nums">
|
||||
{fmtGB(rxTotal)}
|
||||
{fmtGiB(rxTotal)}
|
||||
</p>
|
||||
<Sparkline data={rxSeries} width={180} height={28} color="var(--chart-rx)" filled />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-1">Учтено TX по замерам</p>
|
||||
<p className="text-lg font-semibold tabular-nums">
|
||||
{fmtGB(txTotal)}
|
||||
{fmtGiB(txTotal)}
|
||||
</p>
|
||||
<Sparkline data={txSeries} width={180} height={28} color="var(--chart-tx)" filled />
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts",
|
||||
"test:traffic-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",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../services/traffic-users.js"
|
||||
|
||||
interface TrafficServerDto {
|
||||
volumeUnit: "GiB"
|
||||
observations?:TrafficObservations
|
||||
id: string
|
||||
name: string
|
||||
@@ -95,6 +96,7 @@ function buildServerTraffic(
|
||||
): TrafficServerDto {
|
||||
const built = buildTrafficFromSamples(rows, rangeStartMs, rangeEndMs, onlyInterface,undefined,true)
|
||||
return {
|
||||
volumeUnit: "GiB",
|
||||
id: String(s.id),
|
||||
name: s.name || s.host,
|
||||
site: s.site || "—",
|
||||
|
||||
@@ -58,6 +58,7 @@ const samples: TrafficSampleLike[] = [
|
||||
const built = buildTrafficFromSamples(samples, start, end)
|
||||
assert.ok(built.rxPeak > 0, "peak RX from delta")
|
||||
assert.equal(built.rxNow, 1, "last valid delta after reset skip")
|
||||
assert.equal(built.rxTotalGiB,3_750_000/1024**3,"only observed positive delta counts; reset is not volume")
|
||||
assert.ok(built.rxSeries.some((v) => v > 0), "bucket series not flat")
|
||||
assert.ok(built.rxPeak <= 1.1, "lo excluded from peak")
|
||||
|
||||
|
||||
@@ -216,8 +216,8 @@ export function buildTrafficFromSamples(
|
||||
txNow: last?.tx ?? 0,
|
||||
rxPeak,
|
||||
txPeak,
|
||||
rxTotalGiB: Number((rxBytesDelta / (1024 ** 3)).toFixed(1)),
|
||||
txTotalGiB: Number((txBytesDelta / (1024 ** 3)).toFixed(1)),
|
||||
rxTotalGiB: rxBytesDelta / (1024 ** 3),
|
||||
txTotalGiB: txBytesDelta / (1024 ** 3),
|
||||
sessions,
|
||||
rxSeries,
|
||||
txSeries,
|
||||
@@ -268,8 +268,6 @@ export function mergeBuiltTraffic(parts: BuiltTrafficSeries[]): BuiltTrafficSeri
|
||||
acc.txSeries[i] += p.txSeries[i] ?? 0
|
||||
}
|
||||
}
|
||||
acc.rxTotalGiB = Number(acc.rxTotalGiB.toFixed(1))
|
||||
acc.txTotalGiB = Number(acc.txTotalGiB.toFixed(1))
|
||||
return parts.every(p => p.observations) ? { ...acc, observations: mergeTrafficObservations(parts.map(p => p.observations!)) } : acc
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { listUsers } from "../modules/users/service/users-service.js"
|
||||
@@ -13,6 +12,7 @@ import { applyTrafficFreshness } from "./traffic-freshness.js"
|
||||
import type { TrafficObservations } from "./traffic-rate.js"
|
||||
|
||||
export interface BoundIfaceTrafficDto {
|
||||
volumeUnit: "GiB"
|
||||
observations?: TrafficObservations
|
||||
id: string
|
||||
bindingId: string
|
||||
@@ -40,6 +40,7 @@ export interface BoundIfaceTrafficDto {
|
||||
}
|
||||
|
||||
export interface UserTrafficDto {
|
||||
volumeUnit: "GiB"
|
||||
observations?: TrafficObservations
|
||||
id: string
|
||||
login: string
|
||||
@@ -59,6 +60,7 @@ export interface UserTrafficDto {
|
||||
|
||||
function seriesFromBuilt(built: BuiltTrafficSeries) {
|
||||
return {
|
||||
volumeUnit: "GiB" as const,
|
||||
observations: built.observations,
|
||||
rxNow: built.rxNow,
|
||||
txNow: built.txNow,
|
||||
@@ -71,12 +73,6 @@ function seriesFromBuilt(built: BuiltTrafficSeries) {
|
||||
}
|
||||
}
|
||||
|
||||
async function serverStatus(serverId: number): Promise<"online" | "offline"> {
|
||||
const row = (await db.select().from(servers).where(eq(servers.id, serverId)).limit(1))[0]
|
||||
if (!row?.enabled) return "offline"
|
||||
return "online"
|
||||
}
|
||||
|
||||
export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: number): Promise<UserTrafficDto[]> {
|
||||
const sinceIso = new Date(rangeStartMs).toISOString()
|
||||
const users = await listUsers()
|
||||
@@ -122,7 +118,7 @@ export async function buildUserTrafficList(rangeStartMs: number, rangeEndMs: num
|
||||
serverSite: b.serverSite,
|
||||
serverCountry: b.serverCountry,
|
||||
...seriesFromBuilt(built),
|
||||
status: running && (await serverStatus(b.serverId)) === "online" ? "online" : "offline",
|
||||
status: running && server?.enabled ? "online" : "offline",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Fastify from "fastify"
|
||||
import { pool, closePool } from "../db/index.js"
|
||||
import { initDatabase } from "../db/bootstrap.js"
|
||||
import trafficRoutes from "../routes/traffic.js"
|
||||
const database=process.env.MMAPP_OWN_SQL_DB
|
||||
if(!database)console.log("traffic volume SQL: skipped; explicit owned database required")
|
||||
else {
|
||||
assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,"/"+database)
|
||||
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
|
||||
const prefix="own-volume180-"+Date.now()+"-",userId=prefix+"user",at=new Date(Date.now()-5000).toISOString(),updatedAt=new Date(Date.now()-3600_000).toISOString()
|
||||
let serverId:number|undefined,cases=0
|
||||
const app=Fastify();await app.register(trafficRoutes)
|
||||
const get=async(path:string)=>{const r=await app.inject({method:"GET",url:path});assert.equal(r.statusCode,200);return r.json()}
|
||||
const put=async(name:string,offset:number,bytes:number)=>pool.query("INSERT INTO traffic_samples(server_id,interface_name,sampled_at,rx_bytes,tx_bytes,flags) VALUES($1,$2,$3,$4,$4,5)",[serverId,name,new Date(Date.parse(at)+offset).toISOString(),bytes])
|
||||
try {
|
||||
await initDatabase()
|
||||
serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,updated_at) VALUES($1,'127.0.0.1',1,false,true,'fixture','fixture',$2) RETURNING id",[prefix+"server",updatedAt])).rows[0].id)
|
||||
await pool.query("INSERT INTO traffic_settings(id,enabled,interval_sec) VALUES(1,true,30) ON CONFLICT(id) DO UPDATE SET enabled=true,interval_sec=30")
|
||||
await pool.query("INSERT INTO traffic_collection_state(server_id,server_updated_at,last_attempt_at,last_succeeded_at,status) VALUES($1,$2,$3,$3,'ok')",[serverId,updatedAt,at])
|
||||
await pool.query("INSERT INTO app_users(id,login,name) VALUES($1,$2,'Volume fixture')",[userId,prefix+"login"])
|
||||
await pool.query("INSERT INTO user_interface_bindings(id,user_id,server_id,interface_name,interface_type) VALUES($1,$2,$3,'ether1','ether')",[prefix+"binding",userId,serverId])
|
||||
for(const bytes of[1,1024,1024**2,1024**3]){
|
||||
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await put("ether1",-30_000,0);await put("ether1",0,bytes)
|
||||
const s=(await get(`/traffic/servers/${serverId}?range=1h`)).server
|
||||
assert.equal(s.volumeUnit,"GiB");assert.equal(s.rxTotal,bytes/1024**3);assert.equal(s.observations.rxNow,true)
|
||||
const user=(await get("/traffic/users?range=1h")).users.find((u:{id:string})=>u.id===userId)
|
||||
assert.equal(user.volumeUnit,"GiB");assert.equal(user.rxTotal,s.rxTotal)
|
||||
const iface=(await get("/traffic/bound-interfaces?range=1h")).interfaces.find((i:{userId:string})=>i.userId===userId)
|
||||
assert.equal(iface.volumeUnit,"GiB");assert.equal(iface.rxTotal,s.rxTotal);cases++
|
||||
}
|
||||
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await pool.query("DELETE FROM user_interface_bindings WHERE user_id=$1",[userId])
|
||||
for(let i=0;i<100;i++){const name=`ether${i}`;await put(name,-30_000,0);await put(name,0,1024**2);await pool.query("INSERT INTO user_interface_bindings(id,user_id,server_id,interface_name,interface_type) VALUES($1,$2,$3,$4,'ether')",[prefix+"binding"+i,userId,serverId,name])}
|
||||
let s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.rxTotal,100/1024)
|
||||
const user=(await get("/traffic/users?range=1h")).users.find((u:{id:string})=>u.id===userId);assert.equal(user.rxTotal,100/1024);cases++
|
||||
await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await put("ether1",-90_000,0);await put("ether1",-60_000,1024**2);await put("ether1",-30_000,512);await put("ether1",0,1024)
|
||||
s=(await get(`/traffic/servers/${serverId}?range=1h`)).server;assert.equal(s.rxTotal,(1024**2+512)/1024**3);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()}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
|
||||
|
||||
@@ -696,7 +696,7 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
|
||||
@@ -2070,3 +2070,24 @@ Owned Node22/PG18.6 profile test:traffic-rate24570ms без skip:9 counters/sour
|
||||
[Настоящие CHR SSE](ux-ui/acceptance/traffic-live-native-2026-10-12.json) получили первый полный sample на обоих sourceId1/2, затем streams отменены: native interface GET + monitor-traffic read-only POST. Пример краткого наблюдения194 RX0.031584/TX0.002016Mbps,104 RX/TX0.002112Mbps; это не SLA/capacity/WAN proof. Пароли/конфигурация не менялись, messages0. [Production browser1440/320](ux-ui/acceptance/traffic-live-deployed-2026-10-12.json): actual live labels/receipt provenance,3APIs200,no overflow/pageerrors. Lab AUTH_REQUIRED=false; JWT/RBAC scopes не объявлены полностью принятыми по этой smoke.
|
||||
|
||||
Следующий180 — volume precision/binary units. Read-only baseline уже показал: measured1KiB/1MiB API round to0;100×1MiB также aggregate0;1GiB подписан1.0GB. Baseline сохранён в ignored tmp/volume180-baseline.json и пока не выдан за исправление. Sparse intervals/peer completeness/identity recreation/restore/multi-instance/scale и внешние integrations остаются12.5. Локальный коммит без push.
|
||||
|
||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||
|
||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||
|
||||
DTO серверов, пользователей и привязок объявляет volumeUnit=GiB. Frontend принимает эту единицу явно, absent unit оставляет volume — без потери known speed/history; unsupportedGB marker отклоняет DTO. fmtGiB отображает Б/КиБ/МиБ/ГиБ/ТиБ/ПиБ, positive ниже1B как<1B, true0 как0B. Число округляется только при форматировании. Existing rate formatter остаётся decimal Кбит/Мбит/Гбит, binary объёмы не смешиваются с decimal rates. TotalsRow получил semantic group label. ReUI layout/theme/components сохранены. Удалён N+1 server-status SELECT на каждую binding: используется уже прочитанный catalog snapshot.
|
||||
|
||||
| Состояние | Схема | Пример |
|
||||
| --- | --- | --- |
|
||||
| Small positive bytes | bytes → exact GiB DTO → подходящая binary unit | [ПК](ux-ui/screens/traffic-volume-1440-2026-10-12.png), [320](ux-ui/screens/traffic-volume-320-2026-10-12.png), API-fixture component capture |
|
||||
| Old response without unit | unknown unit → —, speeds/history retained | E2E actual API path fixture |
|
||||
|
||||
Backend formulas/volume assertion (known positive delta counts; reset does not), frontend unit boundary/unknown/zero/projection, TypeScript/backend build/ESLint passed.18 E2E passed36.3s (2new volume+10detail+6mobile), screenshot delta2 passed10.5s не новые cases; final widget captures просмотрены. Новые positive fixtures явно объявляют GiB. Existing user8 GRE/VXLAN screenshots не тронуты.
|
||||
|
||||
Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP SSE+6new volume SQL cases, без SQL skip. Новые6 — storage/API GET для1B/1KiB/1MiB/1GiB на server/user/binding;100-part aggregate100MiB; reset lower-bound1MiB+512B. Seeded marker5 rows — собственные SQL fixtures, не физические device counts; отсутствие RouterOS calls в volume-only case не объявлено native acceptance. OwnDB mm_accept_166_1791816469068/ограниченная role/container удалены, absence verified. [SQL](ux-ui/acceptance/traffic-volume-sql-2026-10-12.json).
|
||||
|
||||
[Source452/522](ux-ui/acceptance/traffic-volume-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-volume-compiled-2026-10-12.json) SHA256 parity passed. Обе production builds прошли. Frontend ux-volume180 staged with backend ux-live179 [interop](ux-ui/acceptance/traffic-volume-interop-2026-10-12.json): volumes без unit unknown до backend update. Затем both ux-volume180/version1.0.0-test.volume180 [runtime](ux-ui/acceptance/traffic-volume-runtime-2026-10-12.json) healthy/running/imageIds. Только.env tags, rollback both ux-live179; no secret env copy/new migration. Старую вкладку следует перезагрузить для новых unit labels.
|
||||
|
||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||
|
||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-12
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
|
||||
|
||||
@@ -371,6 +371,6 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
+22
-1
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -38,6 +38,27 @@
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. |
|
||||
|
||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||
|
||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||
|
||||
DTO серверов, пользователей и привязок объявляет volumeUnit=GiB. Frontend принимает эту единицу явно, absent unit оставляет volume — без потери known speed/history; unsupportedGB marker отклоняет DTO. fmtGiB отображает Б/КиБ/МиБ/ГиБ/ТиБ/ПиБ, positive ниже1B как<1B, true0 как0B. Число округляется только при форматировании. Existing rate formatter остаётся decimal Кбит/Мбит/Гбит, binary объёмы не смешиваются с decimal rates. TotalsRow получил semantic group label. ReUI layout/theme/components сохранены. Удалён N+1 server-status SELECT на каждую binding: используется уже прочитанный catalog snapshot.
|
||||
|
||||
| Состояние | Схема | Пример |
|
||||
| --- | --- | --- |
|
||||
| Small positive bytes | bytes → exact GiB DTO → подходящая binary unit | [ПК](ux-ui/screens/traffic-volume-1440-2026-10-12.png), [320](ux-ui/screens/traffic-volume-320-2026-10-12.png), API-fixture component capture |
|
||||
| Old response without unit | unknown unit → —, speeds/history retained | E2E actual API path fixture |
|
||||
|
||||
Backend formulas/volume assertion (known positive delta counts; reset does not), frontend unit boundary/unknown/zero/projection, TypeScript/backend build/ESLint passed.18 E2E passed36.3s (2new volume+10detail+6mobile), screenshot delta2 passed10.5s не новые cases; final widget captures просмотрены. Новые positive fixtures явно объявляют GiB. Existing user8 GRE/VXLAN screenshots не тронуты.
|
||||
|
||||
Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP SSE+6new volume SQL cases, без SQL skip. Новые6 — storage/API GET для1B/1KiB/1MiB/1GiB на server/user/binding;100-part aggregate100MiB; reset lower-bound1MiB+512B. Seeded marker5 rows — собственные SQL fixtures, не физические device counts; отсутствие RouterOS calls в volume-only case не объявлено native acceptance. OwnDB mm_accept_166_1791816469068/ограниченная role/container удалены, absence verified. [SQL](ux-ui/acceptance/traffic-volume-sql-2026-10-12.json).
|
||||
|
||||
[Source452/522](ux-ui/acceptance/traffic-volume-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-volume-compiled-2026-10-12.json) SHA256 parity passed. Обе production builds прошли. Frontend ux-volume180 staged with backend ux-live179 [interop](ux-ui/acceptance/traffic-volume-interop-2026-10-12.json): volumes без unit unknown до backend update. Затем both ux-volume180/version1.0.0-test.volume180 [runtime](ux-ui/acceptance/traffic-volume-runtime-2026-10-12.json) healthy/running/imageIds. Только.env tags, rollback both ux-live179; no secret env copy/new migration. Старую вкладку следует перезагрузить для новых unit labels.
|
||||
|
||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||
|
||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||
|
||||
## 12.5.179 — достоверный live-трафик, остановка и переподключение (2026-10-12)
|
||||
|
||||
parseMonitorTraffic больше не превращает empty/missing/malformed/negative/prefix values в0. Требуются именованные уникальные интерфейсы и обе конечные неотрицательные скорости; для aggregate — полный expected set актуальных активных интерфейсов, loopback исключён. Duplicate/wrong/missing interface и неполный ответ дают error. Настоящий measured0 сохраняется. Дельты и Mbps больше не округляются до хранения/передачи: positive1byte/30s не исчезает; legacy bigint rxBps/txBps округляются только при insert, read consumers используют counters. sum snapshot сохраняет полную точность. UI positive меньше разрешения отображается «<1 Кбит/с», а не fake idle0. Это не подтверждение максимальной производительности интерфейса.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-12
|
||||
|
||||
Это актуальная матрица после 12.5.179. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.180. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-live179`, frontend `ux-live179`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-volume180`, frontend `ux-volume180`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||
| --- | --- | --- |
|
||||
@@ -18,7 +18,7 @@
|
||||
| Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята |
|
||||
| Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты |
|
||||
| Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, native IPv6 ULA flow pipeline passed145 (IPv4 transport), public IPv6 WAN/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения |
|
||||
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; monitoring history172/ownSQL26/compiled1020/deployed passed; Ping known/error173/ownHTTP-SQL8/85uniqueE2E/native4/4/deployed passed; traffic confidence174 baseline reproduced/local guard passed; owned SQL9/9358ms/deploy175/1024compiled/live smoke passed; legacy history/provenance open; compound RTT176 local/native replay2 passed, SQL10/deploy177/1024compiled/live smoke passed; history178/legacy provenance/current TTL/manual truth/SQL9+14/35unique E2E/native GET/deployed passed; live179 complete/disable-config/bounded fallback/ownSQL10/nativeSSE2/liveUI2 passed; volume precision/units180 next; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
|
||||
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; monitoring history172/ownSQL26/compiled1020/deployed passed; Ping known/error173/ownHTTP-SQL8/85uniqueE2E/native4/4/deployed passed; traffic confidence174 baseline reproduced/local guard passed; owned SQL9/9358ms/deploy175/1024compiled/live smoke passed; legacy history/provenance open; compound RTT176 local/native replay2 passed, SQL10/deploy177/1024compiled/live smoke passed; history178/legacy provenance/current TTL/manual truth/SQL9+14/35unique E2E/native GET/deployed passed; live179 complete/disable-config/bounded fallback/ownSQL10/nativeSSE2/liveUI2 passed; volume180 exact/unit marker/ownSQL6/18E2E/API/liveUI/deployed passed; binding status freshness181 next; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
|
||||
|
||||
Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
|
||||
|
||||
@@ -985,3 +985,24 @@ Owned Node22/PG18.6 profile test:traffic-rate24570ms без skip:9 counters/sour
|
||||
[Настоящие CHR SSE](ux-ui/acceptance/traffic-live-native-2026-10-12.json) получили первый полный sample на обоих sourceId1/2, затем streams отменены: native interface GET + monitor-traffic read-only POST. Пример краткого наблюдения194 RX0.031584/TX0.002016Mbps,104 RX/TX0.002112Mbps; это не SLA/capacity/WAN proof. Пароли/конфигурация не менялись, messages0. [Production browser1440/320](ux-ui/acceptance/traffic-live-deployed-2026-10-12.json): actual live labels/receipt provenance,3APIs200,no overflow/pageerrors. Lab AUTH_REQUIRED=false; JWT/RBAC scopes не объявлены полностью принятыми по этой smoke.
|
||||
|
||||
Следующий180 — volume precision/binary units. Read-only baseline уже показал: measured1KiB/1MiB API round to0;100×1MiB также aggregate0;1GiB подписан1.0GB. Baseline сохранён в ignored tmp/volume180-baseline.json и пока не выдан за исправление. Sparse intervals/peer completeness/identity recreation/restore/multi-instance/scale и внешние integrations остаются12.5. Локальный коммит без push.
|
||||
|
||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||
|
||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||
|
||||
DTO серверов, пользователей и привязок объявляет volumeUnit=GiB. Frontend принимает эту единицу явно, absent unit оставляет volume — без потери known speed/history; unsupportedGB marker отклоняет DTO. fmtGiB отображает Б/КиБ/МиБ/ГиБ/ТиБ/ПиБ, positive ниже1B как<1B, true0 как0B. Число округляется только при форматировании. Existing rate formatter остаётся decimal Кбит/Мбит/Гбит, binary объёмы не смешиваются с decimal rates. TotalsRow получил semantic group label. ReUI layout/theme/components сохранены. Удалён N+1 server-status SELECT на каждую binding: используется уже прочитанный catalog snapshot.
|
||||
|
||||
| Состояние | Схема | Пример |
|
||||
| --- | --- | --- |
|
||||
| Small positive bytes | bytes → exact GiB DTO → подходящая binary unit | [ПК](ux-ui/screens/traffic-volume-1440-2026-10-12.png), [320](ux-ui/screens/traffic-volume-320-2026-10-12.png), API-fixture component capture |
|
||||
| Old response without unit | unknown unit → —, speeds/history retained | E2E actual API path fixture |
|
||||
|
||||
Backend formulas/volume assertion (known positive delta counts; reset does not), frontend unit boundary/unknown/zero/projection, TypeScript/backend build/ESLint passed.18 E2E passed36.3s (2new volume+10detail+6mobile), screenshot delta2 passed10.5s не новые cases; final widget captures просмотрены. Новые positive fixtures явно объявляют GiB. Existing user8 GRE/VXLAN screenshots не тронуты.
|
||||
|
||||
Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP SSE+6new volume SQL cases, без SQL skip. Новые6 — storage/API GET для1B/1KiB/1MiB/1GiB на server/user/binding;100-part aggregate100MiB; reset lower-bound1MiB+512B. Seeded marker5 rows — собственные SQL fixtures, не физические device counts; отсутствие RouterOS calls в volume-only case не объявлено native acceptance. OwnDB mm_accept_166_1791816469068/ограниченная role/container удалены, absence verified. [SQL](ux-ui/acceptance/traffic-volume-sql-2026-10-12.json).
|
||||
|
||||
[Source452/522](ux-ui/acceptance/traffic-volume-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-volume-compiled-2026-10-12.json) SHA256 parity passed. Обе production builds прошли. Frontend ux-volume180 staged with backend ux-live179 [interop](ux-ui/acceptance/traffic-volume-interop-2026-10-12.json): volumes без unit unknown до backend update. Затем both ux-volume180/version1.0.0-test.volume180 [runtime](ux-ui/acceptance/traffic-volume-runtime-2026-10-12.json) healthy/running/imageIds. Только.env tags, rollback both ux-live179; no secret env copy/new migration. Старую вкладку следует перезагрузить для новых unit labels.
|
||||
|
||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||
|
||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||
+22
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-12: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -3375,3 +3375,24 @@ Owned Node22/PG18.6 profile test:traffic-rate24570ms без skip:9 counters/sour
|
||||
[Настоящие CHR SSE](ux-ui/acceptance/traffic-live-native-2026-10-12.json) получили первый полный sample на обоих sourceId1/2, затем streams отменены: native interface GET + monitor-traffic read-only POST. Пример краткого наблюдения194 RX0.031584/TX0.002016Mbps,104 RX/TX0.002112Mbps; это не SLA/capacity/WAN proof. Пароли/конфигурация не менялись, messages0. [Production browser1440/320](ux-ui/acceptance/traffic-live-deployed-2026-10-12.json): actual live labels/receipt provenance,3APIs200,no overflow/pageerrors. Lab AUTH_REQUIRED=false; JWT/RBAC scopes не объявлены полностью принятыми по этой smoke.
|
||||
|
||||
Следующий180 — volume precision/binary units. Read-only baseline уже показал: measured1KiB/1MiB API round to0;100×1MiB также aggregate0;1GiB подписан1.0GB. Baseline сохранён в ignored tmp/volume180-baseline.json и пока не выдан за исправление. Sparse intervals/peer completeness/identity recreation/restore/multi-instance/scale и внешние integrations остаются12.5. Локальный коммит без push.
|
||||
|
||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||
|
||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||
|
||||
DTO серверов, пользователей и привязок объявляет volumeUnit=GiB. Frontend принимает эту единицу явно, absent unit оставляет volume — без потери known speed/history; unsupportedGB marker отклоняет DTO. fmtGiB отображает Б/КиБ/МиБ/ГиБ/ТиБ/ПиБ, positive ниже1B как<1B, true0 как0B. Число округляется только при форматировании. Existing rate formatter остаётся decimal Кбит/Мбит/Гбит, binary объёмы не смешиваются с decimal rates. TotalsRow получил semantic group label. ReUI layout/theme/components сохранены. Удалён N+1 server-status SELECT на каждую binding: используется уже прочитанный catalog snapshot.
|
||||
|
||||
| Состояние | Схема | Пример |
|
||||
| --- | --- | --- |
|
||||
| Small positive bytes | bytes → exact GiB DTO → подходящая binary unit | [ПК](ux-ui/screens/traffic-volume-1440-2026-10-12.png), [320](ux-ui/screens/traffic-volume-320-2026-10-12.png), API-fixture component capture |
|
||||
| Old response without unit | unknown unit → —, speeds/history retained | E2E actual API path fixture |
|
||||
|
||||
Backend formulas/volume assertion (known positive delta counts; reset does not), frontend unit boundary/unknown/zero/projection, TypeScript/backend build/ESLint passed.18 E2E passed36.3s (2new volume+10detail+6mobile), screenshot delta2 passed10.5s не новые cases; final widget captures просмотрены. Новые positive fixtures явно объявляют GiB. Existing user8 GRE/VXLAN screenshots не тронуты.
|
||||
|
||||
Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP SSE+6new volume SQL cases, без SQL skip. Новые6 — storage/API GET для1B/1KiB/1MiB/1GiB на server/user/binding;100-part aggregate100MiB; reset lower-bound1MiB+512B. Seeded marker5 rows — собственные SQL fixtures, не физические device counts; отсутствие RouterOS calls в volume-only case не объявлено native acceptance. OwnDB mm_accept_166_1791816469068/ограниченная role/container удалены, absence verified. [SQL](ux-ui/acceptance/traffic-volume-sql-2026-10-12.json).
|
||||
|
||||
[Source452/522](ux-ui/acceptance/traffic-volume-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-volume-compiled-2026-10-12.json) SHA256 parity passed. Обе production builds прошли. Frontend ux-volume180 staged with backend ux-live179 [interop](ux-ui/acceptance/traffic-volume-interop-2026-10-12.json): volumes без unit unknown до backend update. Затем both ux-volume180/version1.0.0-test.volume180 [runtime](ux-ui/acceptance/traffic-volume-runtime-2026-10-12.json) healthy/running/imageIds. Только.env tags, rollback both ux-live179; no secret env copy/new migration. Старую вкладку следует перезагрузить для новых unit labels.
|
||||
|
||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||
|
||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Сервер161.104.46.222: завершён12.5.179 — live complete payload/expected interfaces/auth/disable-config guard; scoped retry/retain cause/latest sample, abort cleanup/receipt timestamp, positive tiny rates. OwnSQL9+14+10/24570ms,7E2E passed13.3s,types/build/ESLint/1036compiled parity. Both ux-live179 healthy/running,2 actual CHR SSE samples+live UI2 passed; ownDB/role/container removed. Следующий180 — точность и binary units учтённых объёмов: baseline positive1KiB/1MiB rounded0 и1GiB mislabeledGB подтверждён, ещё не исправлен.12.5 открыта.
|
||||
Сервер161.104.46.222: завершён12.5.180 — точные учтённые объёмы без early rounding, explicit GiB API/КиБ-МиБ-ГиБ UI, unknown unit→—, N+1 server-status query removed. OwnSQL9+14+10+6/31474ms,18E2E/36.3s/types/build/ESLint/1036compiled parity. Both ux-volume180 healthy/running/API2/real-live browser2 passed; ownDB/role/container removed. Следующий181 — проверить freshness статусов привязок и peer/interface flags;12.5 открыта.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -14,7 +14,7 @@
|
||||
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
|
||||
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
|
||||
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
|
||||
| Образы приложения | mmapp-test/backend:ux-live179 и mmapp-test/frontend:ux-live179; запущены |
|
||||
| Образы приложения | mmapp-test/backend:ux-volume180 и mmapp-test/frontend:ux-volume180; запущены |
|
||||
|
||||
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
|
||||
|
||||
@@ -1801,3 +1801,24 @@ Owned Node22/PG18.6 profile test:traffic-rate24570ms без skip:9 counters/sour
|
||||
[Настоящие CHR SSE](ux-ui/acceptance/traffic-live-native-2026-10-12.json) получили первый полный sample на обоих sourceId1/2, затем streams отменены: native interface GET + monitor-traffic read-only POST. Пример краткого наблюдения194 RX0.031584/TX0.002016Mbps,104 RX/TX0.002112Mbps; это не SLA/capacity/WAN proof. Пароли/конфигурация не менялись, messages0. [Production browser1440/320](ux-ui/acceptance/traffic-live-deployed-2026-10-12.json): actual live labels/receipt provenance,3APIs200,no overflow/pageerrors. Lab AUTH_REQUIRED=false; JWT/RBAC scopes не объявлены полностью принятыми по этой smoke.
|
||||
|
||||
Следующий180 — volume precision/binary units. Read-only baseline уже показал: measured1KiB/1MiB API round to0;100×1MiB также aggregate0;1GiB подписан1.0GB. Baseline сохранён в ignored tmp/volume180-baseline.json и пока не выдан за исправление. Sparse intervals/peer completeness/identity recreation/restore/multi-instance/scale и внешние integrations остаются12.5. Локальный коммит без push.
|
||||
|
||||
## 12.5.180 — точность учтённых объёмов и бинарные единицы (2026-10-12)
|
||||
|
||||
Read-only [baseline](ux-ui/acceptance/traffic-volume-baseline-2026-10-12.json) подтверждён до правки: measured1KiB/1MiB превращались в API0 и0.0GB;100×1MiB также aggregate0;1GiB выдавался с decimal labelGB. Убрано toFixed(1) из buildTrafficFromSamples/mergeBuiltTraffic: округление отдельных слагаемых больше не теряет суммарный трафик. Учёт остаётся по known соседним counter deltas, reset/unknown не добавляют выдуманные bytes. Отсутствующий baseline и coverage gaps по-прежнему ограничивают полноту периода.
|
||||
|
||||
DTO серверов, пользователей и привязок объявляет volumeUnit=GiB. Frontend принимает эту единицу явно, absent unit оставляет volume — без потери known speed/history; unsupportedGB marker отклоняет DTO. fmtGiB отображает Б/КиБ/МиБ/ГиБ/ТиБ/ПиБ, positive ниже1B как<1B, true0 как0B. Число округляется только при форматировании. Existing rate formatter остаётся decimal Кбит/Мбит/Гбит, binary объёмы не смешиваются с decimal rates. TotalsRow получил semantic group label. ReUI layout/theme/components сохранены. Удалён N+1 server-status SELECT на каждую binding: используется уже прочитанный catalog snapshot.
|
||||
|
||||
| Состояние | Схема | Пример |
|
||||
| --- | --- | --- |
|
||||
| Small positive bytes | bytes → exact GiB DTO → подходящая binary unit | [ПК](ux-ui/screens/traffic-volume-1440-2026-10-12.png), [320](ux-ui/screens/traffic-volume-320-2026-10-12.png), API-fixture component capture |
|
||||
| Old response without unit | unknown unit → —, speeds/history retained | E2E actual API path fixture |
|
||||
|
||||
Backend formulas/volume assertion (known positive delta counts; reset does not), frontend unit boundary/unknown/zero/projection, TypeScript/backend build/ESLint passed.18 E2E passed36.3s (2new volume+10detail+6mobile), screenshot delta2 passed10.5s не новые cases; final widget captures просмотрены. Новые positive fixtures явно объявляют GiB. Existing user8 GRE/VXLAN screenshots не тронуты.
|
||||
|
||||
Owned Node22/PG18.6 test:traffic-rate31474ms:9 counters+14history+10actual TCP SSE+6new volume SQL cases, без SQL skip. Новые6 — storage/API GET для1B/1KiB/1MiB/1GiB на server/user/binding;100-part aggregate100MiB; reset lower-bound1MiB+512B. Seeded marker5 rows — собственные SQL fixtures, не физические device counts; отсутствие RouterOS calls в volume-only case не объявлено native acceptance. OwnDB mm_accept_166_1791816469068/ограниченная role/container удалены, absence verified. [SQL](ux-ui/acceptance/traffic-volume-sql-2026-10-12.json).
|
||||
|
||||
[Source452/522](ux-ui/acceptance/traffic-volume-source-2026-10-12.json)/[compiled1036](ux-ui/acceptance/traffic-volume-compiled-2026-10-12.json) SHA256 parity passed. Обе production builds прошли. Frontend ux-volume180 staged with backend ux-live179 [interop](ux-ui/acceptance/traffic-volume-interop-2026-10-12.json): volumes без unit unknown до backend update. Затем both ux-volume180/version1.0.0-test.volume180 [runtime](ux-ui/acceptance/traffic-volume-runtime-2026-10-12.json) healthy/running/imageIds. Только.env tags, rollback both ux-live179; no secret env copy/new migration. Старую вкладку следует перезагрузить для новых unit labels.
|
||||
|
||||
[Actual volume API](ux-ui/acceptance/traffic-volume-api-2026-10-12.json) показывает GiB и неокруглённые quantities обоих CHR из штатных collector rows; этот checker не отправлял новых native commands. [Browser1440/320](ux-ui/acceptance/traffic-volume-deployed-2026-10-12.json):3APIs200, actual live/receipt labels/provenance, no errors/overflow. Рабочая конфигурация/пароли не менялись, messages0; auto collectors остаются штатными. Это не WAN byte billing/capacity/SLA proof и не exact arithmetic acceptance за пределами safe integer/scale bounds.
|
||||
|
||||
Следующий181 — freshness статуса привязанных интерфейсов: enabled device+old running flag не должен объявлять online после failed/stale poll; peer enabled не доказывает handshake. Проверить native flag provenance и scalar shape, не переписывать legacy rows по догадке. Coverage/peer completeness/recreated interfaces/restore/multi-instance/scale/external integrations остаются12.5. Локальный коммит без push.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"version": "1.0.0-test.volume180",
|
||||
"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.013485109433531761,
|
||||
"txTotalGiB": 0.001898050308227539,
|
||||
"currentState": "current"
|
||||
},
|
||||
{
|
||||
"sourceId": "2",
|
||||
"volumeUnit": "GiB",
|
||||
"rxTotalGiB": 0.0021370751783251762,
|
||||
"txTotalGiB": 0.0023917602375149727,
|
||||
"currentState": "current"
|
||||
}
|
||||
],
|
||||
"newRouterOSCommands": 0,
|
||||
"passwordChanges": 0,
|
||||
"messages": 0
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"method": "Read-only current source reproduction: positive measured deltas are rounded before aggregation; binary quantity carries decimal label",
|
||||
"cases": [
|
||||
{
|
||||
"bytes": 1024,
|
||||
"actualGiB": 9.5367431640625e-7,
|
||||
"apiTotal": 0,
|
||||
"display": "0.0 ГБ",
|
||||
"observed": true
|
||||
},
|
||||
{
|
||||
"bytes": 1048576,
|
||||
"actualGiB": 0.0009765625,
|
||||
"apiTotal": 0,
|
||||
"display": "0.0 ГБ",
|
||||
"observed": true
|
||||
},
|
||||
{
|
||||
"bytes": 1073741824,
|
||||
"actualGiB": 1,
|
||||
"apiTotal": 1,
|
||||
"display": "1.0 ГБ",
|
||||
"observed": true
|
||||
},
|
||||
{
|
||||
"bytes": 1000000000,
|
||||
"actualGiB": 0.9313225746154785,
|
||||
"apiTotal": 0.9,
|
||||
"display": "0.9 ГБ",
|
||||
"observed": true
|
||||
}
|
||||
],
|
||||
"merged100MiB": {
|
||||
"apiTotal": 0,
|
||||
"actualGiB": 0.09765625
|
||||
},
|
||||
"fixed": false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"runtime": {
|
||||
"backend": "ux-volume180",
|
||||
"frontend": "ux-volume180"
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"path": "/api/traffic/servers",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/api/traffic/users",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/api/traffic/bound-interfaces",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"width": 1440,
|
||||
"trafficServers200": true,
|
||||
"attemptProvenanceVisible": true,
|
||||
"actualLiveSample": true,
|
||||
"backendReceiptLabel": true,
|
||||
"pageErrors": 0,
|
||||
"noOverflow": true
|
||||
},
|
||||
{
|
||||
"width": 320,
|
||||
"trafficServers200": true,
|
||||
"attemptProvenanceVisible": true,
|
||||
"actualLiveSample": true,
|
||||
"backendReceiptLabel": true,
|
||||
"pageErrors": 0,
|
||||
"noOverflow": true
|
||||
}
|
||||
],
|
||||
"bounds": [
|
||||
"Read-only smoke; error/stale/legacy branches proven by owned SQL and UI fixtures",
|
||||
"No manual production scheduler/config/password/message writes"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"newFrontend": "ux-volume180",
|
||||
"oldBackend": "ux-live179",
|
||||
"readerAccepted": true,
|
||||
"serverCount": 2,
|
||||
"pageErrors": 0,
|
||||
"bound": "Additive observed DTO reader compatibility; legacy numeric-only paths verified separately as unknown"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"secretEnvCopied": false,
|
||||
"rollbackTags": {
|
||||
"MMAPP_IMAGE_TAG": "ux-live179",
|
||||
"BACKEND_IMAGE_TAG": "ux-live179"
|
||||
},
|
||||
"services": {
|
||||
"backend": {
|
||||
"image": "mmapp-test/backend:ux-volume180",
|
||||
"imageId": "sha256:225a7a5613eb3cdc15f1b7d4e8f54c4c0dc6c4395c74769eaed9ca9296a6cca1",
|
||||
"state": "running",
|
||||
"health": "healthy",
|
||||
"matchedVerifiedImage": true
|
||||
},
|
||||
"frontend": {
|
||||
"image": "mmapp-test/frontend:ux-volume180",
|
||||
"imageId": "sha256:93b132c1ab859c5511275e14e95a74325909002cf28ae6cb053d3e362f0ae391",
|
||||
"state": "running",
|
||||
"health": null,
|
||||
"matchedVerifiedImage": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"matched": {
|
||||
"backend": 452,
|
||||
"frontend": 522
|
||||
},
|
||||
"different": [],
|
||||
"missing": []
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"passed": true,
|
||||
"profiles": [
|
||||
{
|
||||
"date": "2026-10-12",
|
||||
"block": "12.5.180",
|
||||
"database": "mm_accept_166_1791816469068",
|
||||
"scope": "ephemeral role and precreated owned database; no copied production credentials",
|
||||
"tests": [
|
||||
{
|
||||
"command": "npm --prefix backend run test:traffic-rate",
|
||||
"passed": true,
|
||||
"durationMs": 31474,
|
||||
"ownedTrafficSqlCases": 9,
|
||||
"ownedHistorySqlCases": 14,
|
||||
"ownedLiveSqlCases": 10,
|
||||
"ownedVolumeSqlCases": 6,
|
||||
"postgresUnavailableSkip": false
|
||||
}
|
||||
],
|
||||
"scopedRoleVerified": true,
|
||||
"passed": true,
|
||||
"databaseRemoved": true,
|
||||
"roleRemoved": true,
|
||||
"testContainerRemoved": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -1,6 +1,11 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { fmtRate } from "./fmt-rate.js"
|
||||
import { fmtRate, fmtGiB } from "./fmt-rate.js"
|
||||
assert.equal(fmtRate(null),"—");assert.equal(fmtRate(undefined),"—");assert.equal(fmtRate(0),"0 Кбит/с")
|
||||
assert.equal(fmtRate(0.000001),"<1 Кбит/с");assert.equal(fmtRate(0.001),"1 Кбит/с")
|
||||
assert.equal(fmtRate(1),"1.00 Мбит/с");assert.equal(fmtRate(1000),"1.00 Гбит/с")
|
||||
console.log("rate formatting unknown/zero/positive-small/units tests ok")
|
||||
assert.equal(fmtGiB(null),"—");assert.equal(fmtGiB(0),"0 Б")
|
||||
assert.equal(fmtGiB(1/1024**3),"1 Б");assert.equal(fmtGiB(1/1024**2),"1 КиБ")
|
||||
assert.equal(fmtGiB(1/1024),"1 МиБ");assert.equal(fmtGiB(1),"1 ГиБ");assert.equal(fmtGiB(1024),"1 ТиБ")
|
||||
assert.equal(fmtGiB(100/1024),"100 МиБ");assert.equal(fmtGiB(Number.MAX_VALUE),"—")
|
||||
console.log("binary traffic volume precision/unit boundary tests ok")
|
||||
+11
-4
@@ -10,10 +10,17 @@ export function fmtRate(v: number | null | undefined): string {
|
||||
return `${Math.round(v)} Мбит/с`
|
||||
}
|
||||
|
||||
export function fmtGB(v: number | null | undefined): string {
|
||||
if (v == null) return "—"
|
||||
if (v >= 1000) return `${(v / 1000).toFixed(2)} ТБ`
|
||||
return `${v.toFixed(1)} ГБ`
|
||||
const binaryFormatter = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 })
|
||||
/** Traffic API volumeUnit=GiB: preserve binary units and never format positive bytes as zero. */
|
||||
export function fmtGiB(v: number | null | undefined): string {
|
||||
if (v == null || !Number.isFinite(v) || v < 0) return "—"
|
||||
const bytes = v * 1024 ** 3
|
||||
if (!Number.isFinite(bytes)) return "—"
|
||||
if (bytes === 0) return "0 Б"
|
||||
if (bytes < 1) return "<1 Б"
|
||||
const units = ["Б", "КиБ", "МиБ", "ГиБ", "ТиБ", "ПиБ"]
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
|
||||
return `${binaryFormatter.format(bytes / 1024 ** index)} ${units[index]}`
|
||||
}
|
||||
|
||||
export function formatBytes(n: number): string {
|
||||
|
||||
@@ -5,6 +5,9 @@ const row={rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal:0,txTotal:0,rxSeries:[0,0],
|
||||
const obs={rxNow:true,txNow:true,rxSeries:[false,true],txSeries:[false,true],sampledAt,currentState:"current",expiresAt}
|
||||
assert.equal(projectTrafficObservations(row,at).rxNow,null)
|
||||
const known=projectTrafficObservations({...row,observations:obs},at);assert.equal(known.rxNow,0);assert.deepEqual(known.rxSeries,[null,0])
|
||||
assert.equal(known.rxTotal,null)
|
||||
assert.equal(projectTrafficObservations({...row,volumeUnit:"GiB",observations:obs},at).rxTotal,0)
|
||||
assert.throws(()=>projectTrafficObservations({...row,volumeUnit:"GB",observations:obs},at))
|
||||
const stale=projectTrafficObservations({...row,observations:obs},at+90_001);assert.equal(stale.rxNow,null);assert.equal(stale.observations?.currentState,"stale");assert.deepEqual(stale.rxSeries,[null,0])
|
||||
assert.equal(projectTrafficObservations({...row,observations:{...obs,currentState:"error"}},at).rxNow,null)
|
||||
for(const change of [{rxNow:-1},{observations:{...obs,sampledAt:"bad"}},{observations:{...obs,sampledAt:null}},{observations:{...obs,rxSeries:[]}}])assert.throws(()=>projectTrafficObservations({...row,...change},at))
|
||||
|
||||
@@ -5,7 +5,7 @@ const meta = z.object({ rxNow: z.boolean(), txNow: z.boolean(), rxSeries: z.arra
|
||||
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>
|
||||
const metrics = z.object({ rxNow: rate, txNow: rate, rxPeak: rate, txPeak: rate, rxTotal: rate, txTotal: rate,
|
||||
const metrics = z.object({ volumeUnit: z.literal("GiB").optional(), rxNow: rate, txNow: rate, rxPeak: rate, txPeak: rate, rxTotal: rate, txTotal: rate,
|
||||
rxSeries: z.array(rate), txSeries: z.array(rate), observations: meta.optional() }).refine(r =>
|
||||
(!r.observations || r.observations.rxSeries.length === r.rxSeries.length && r.observations.txSeries.length === r.txSeries.length &&
|
||||
(!(r.observations.rxNow || r.observations.txNow || r.observations.rxSeries.some(Boolean) || r.observations.txSeries.some(Boolean)) || r.observations.sampledAt !== null)))
|
||||
@@ -18,8 +18,8 @@ export function projectTrafficObservations(raw: unknown, now = Date.now()) {
|
||||
const current = !o?.currentState || o.currentState === "current"
|
||||
const rxSeries = r.rxSeries.map((v, i) => o?.rxSeries[i] ? v : null)
|
||||
const txSeries = r.txSeries.map((v, i) => o?.txSeries[i] ? v : null)
|
||||
return { observations: o, rxNow: current && o?.rxNow ? r.rxNow : null, txNow: current && o?.txNow ? r.txNow : null,
|
||||
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,
|
||||
rxTotal: rxSeries.some(v => v !== null) ? r.rxTotal : null, txTotal: txSeries.some(v => v !== null) ? r.txTotal : null }
|
||||
rxTotal: r.volumeUnit && rxSeries.some(v => v !== null) ? r.rxTotal : null, txTotal: r.volumeUnit && txSeries.some(v => v !== null) ? r.txTotal : null }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export function observedTrafficFixturePayload(value: unknown): unknown {
|
||||
if (!value || typeof value !== "object") return value
|
||||
const r = Object.fromEntries(Object.entries(value).map(([key, v]) => [key, observedTrafficFixturePayload(v)]))
|
||||
if ("rxNow" in r && "txNow" in r && "rxTotal" in r && "txTotal" in r && Array.isArray(r.rxSeries) && Array.isArray(r.txSeries) && !("observations" in r)) {
|
||||
r.volumeUnit = "GiB"
|
||||
r.observations = { rxNow: true, txNow: true, sampledAt: "2026-10-12T12:00:00Z",
|
||||
rxSeries: r.rxSeries.map(v => typeof v === "number" && Number.isFinite(v) && v >= 0),
|
||||
txSeries: r.txSeries.map(v => typeof v === "number" && Number.isFinite(v) && v >= 0) }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
for(const width of[1440,320])test(`Объём трафика: положительные байты/бинарные единицы/неизвестная единица ${width}`,async({page})=>{
|
||||
await page.setViewportSize({width,height:1000});let unit=true,rxTotal=1/1024,txTotal=1/1024**3
|
||||
const server=()=>({id:"1",name:"Объём CHR",site:"TEST",country:"RU",status:"online",rxNow:0,txNow:0,rxPeak:0,txPeak:0,rxTotal,txTotal,rxSeries:[0,0],txSeries:[0,0],sessions:1,...(unit?{volumeUnit:"GiB"}:{}),observations:{rxNow:true,txNow:true,rxSeries:[false,true],txSeries:[false,true],sampledAt:"2026-10-12T12:00:00Z"}})
|
||||
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:"Fixture без live"}})
|
||||
return r.fulfill({json:path==="/api/auth/config"?{required:false}:path==="/api/traffic/servers"?{servers:[server()]}:path==="/api/traffic/servers/1"?{server:server()}:path.endsWith("/interfaces")||path==="/api/traffic/bound-interfaces"?{interfaces:[]}:path==="/api/traffic/users"?{users:[]}:path==="/api/servers"?[]:{}})
|
||||
});await page.goto("/traffic")
|
||||
const volume=page.getByRole("group",{name:"Учтённый объём по измерениям"})
|
||||
await expect(volume).toContainText("1 МиБ");await expect(volume).toContainText("1 Б")
|
||||
await volume.screenshot({path:`docs/ux-ui/screens/traffic-volume-${width}-2026-10-12.png`})
|
||||
rxTotal=1;txTotal=1024;await page.getByRole("button",{name:"15м",exact:true}).click()
|
||||
await expect(volume).toContainText("1 ГиБ");await expect(volume).toContainText("1 ТиБ")
|
||||
unit=false;await page.getByRole("button",{name:"24ч",exact:true}).click()
|
||||
await expect(volume.getByText("—",{exact:true})).toHaveCount(2)
|
||||
unit=true;rxTotal=0;txTotal=0;await page.getByRole("button",{name:"4ч",exact:true}).click()
|
||||
await expect(volume.getByText("0 Б",{exact:true})).toHaveCount(2)
|
||||
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
|
||||
})
|
||||
Reference in new issue
Block a user