From 5919af7f9b2d5477a8003ad2e6d48951bdd72af5 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 7 Oct 2026 14:19:13 +0700 Subject: [PATCH] =?UTF-8?q?fix(ospf):=20=D1=81=D0=BE=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BD=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/ospf.ts | 12 ++---------- .../src/services/ospf-optimizer-metrics.test.ts | 16 +++++++++++++--- backend/src/services/ospf-optimizer-metrics.ts | 14 +++++++++++++- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 2 +- docs/ux-ui-stage-12.md | 9 +++++++++ 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/backend/src/routes/ospf.ts b/backend/src/routes/ospf.ts index 98111e38..8538eaab 100644 --- a/backend/src/routes/ospf.ts +++ b/backend/src/routes/ospf.ts @@ -12,7 +12,7 @@ import type { import { parseOspfGateway, parseOspfRouteType, parseOspfCatalogRouteType } from "../services/ospf-route-parse.js" import { z } from "zod" import {resolveOptimizerRouterOwners} from "../services/ospf-optimizer-identity.js" -import {readOspfOptimizerObserved} from "../services/ospf-optimizer-metrics.js" +import {readOspfOptimizerObserved,readOspfOptimizerModelSpeeds} from "../services/ospf-optimizer-metrics.js" import type {OspfOptimizerObserved} from "@mmapp/contracts" import {readEditableOspfTemplates,validateOspfOptimizationPlan} from "../services/ospf-optimizer-plan.js" import { observedInteger, observedDurationMs, observedBoolean } from "../services/ospf-observed-values.js" @@ -531,15 +531,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number): ? Math.min(995, Math.round(localLatencyMs + await getLatestSnapshotLatencyMs(dstServerId))) : localLatencyMs - const dlMbps = - bestProbe?.lastTxAvgMbps != null - ? Math.max(1, Math.round(bestProbe.lastTxAvgMbps)) - : speed.dlMbps - - const ulMbps = - bestProbe?.lastRxAvgMbps != null - ? Math.max(1, Math.round(bestProbe.lastRxAvgMbps)) - : speed.ulMbps + const {dlMbps,ulMbps} = readOspfOptimizerModelSpeeds(bestProbe,speed) return { id: iface.id, diff --git a/backend/src/services/ospf-optimizer-metrics.test.ts b/backend/src/services/ospf-optimizer-metrics.test.ts index 3a01880a..8f0c41d7 100644 --- a/backend/src/services/ospf-optimizer-metrics.test.ts +++ b/backend/src/services/ospf-optimizer-metrics.test.ts @@ -1,14 +1,24 @@ import assert from "node:assert/strict" -import {readOspfOptimizerObserved} from "./ospf-optimizer-metrics.js" +import {readOspfOptimizerObserved,readOspfProbeSpeeds,readOspfOptimizerModelSpeeds} from "./ospf-optimizer-metrics.js" const probe={id:"p",lastPingRttMs:0,lastPingAt:"2020-01-01 00:00:00+00",lastTxAvgMbps:0,lastRxAvgMbps:null} const traffic={interfaceName:"ether1",rxBps:1234567,txBps:0,sampledAt:"2020-01-02 00:00:00+00"} const observed=readOspfOptimizerObserved(probe,traffic) assert.deepEqual(observed.ping,{value:0,source:"speed-probe",sourceId:"p",measuredAt:"2020-01-01T00:00:00.000Z"}) -assert.equal(observed.dl.value,0);assert.equal(observed.dl.source,"speed-probe");assert.equal(observed.dl.measuredAt,null) -assert.equal(observed.ul.value,0);assert.equal(observed.ul.source,"interface-traffic");assert.equal(observed.ul.measuredAt,"2020-01-02T00:00:00.000Z") +assert.equal(observed.ul.value,0);assert.equal(observed.ul.source,"speed-probe");assert.equal(observed.ul.measuredAt,null) +assert.equal(observed.dl.value,1.234567);assert.equal(observed.dl.source,"interface-traffic");assert.equal(observed.dl.measuredAt,"2020-01-02T00:00:00.000Z") assert.equal(readOspfOptimizerObserved(undefined,traffic).dl.value,1.234567) assert.deepEqual(readOspfOptimizerObserved(undefined,undefined),{ping:{value:null,source:"unknown",sourceId:null,measuredAt:null},dl:{value:null,source:"unknown",sourceId:null,measuredAt:null},ul:{value:null,source:"unknown",sourceId:null,measuredAt:null}}) assert.throws(()=>readOspfOptimizerObserved({...probe,lastPingRttMs:-1},traffic)) assert.throws(()=>readOspfOptimizerObserved({...probe,lastPingAt:"invalid"},traffic)) assert.throws(()=>readOspfOptimizerObserved(undefined,{...traffic,txBps:-1})) console.log("OSPF observed metrics: raw zero/null/fractions, provenance/old timestamps, no speed date inference; no DB/RouterOS") + +const asymmetric={...probe,lastTxAvgMbps:30.25,lastRxAvgMbps:80.75} +assert.deepEqual(readOspfProbeSpeeds(asymmetric),{dlMbps:80.75,ulMbps:30.25}) +assert.deepEqual(readOspfOptimizerModelSpeeds(asymmetric,{dlMbps:12,ulMbps:34}),{dlMbps:81,ulMbps:30}) +const asymmetricObserved=readOspfOptimizerObserved(asymmetric,traffic) +assert.equal(asymmetricObserved.dl.value,80.75);assert.equal(asymmetricObserved.ul.value,30.25) +assert.deepEqual(readOspfOptimizerModelSpeeds(probe,{dlMbps:12,ulMbps:34}),{dlMbps:12,ulMbps:1}) +assert.deepEqual(readOspfOptimizerModelSpeeds(undefined,{dlMbps:12,ulMbps:34}),{dlMbps:12,ulMbps:34}) +assert.deepEqual(readOspfProbeSpeeds({...probe,lastTxAvgMbps:null,lastRxAvgMbps:0}),{dlMbps:0,ulMbps:null}) +console.log("OSPF client RX→DL/TX→UL: asymmetric decimals, explicit zero, unknown, preserved model floor/rounding/fallback") diff --git a/backend/src/services/ospf-optimizer-metrics.ts b/backend/src/services/ospf-optimizer-metrics.ts index 7f87006e..fce7efd6 100644 --- a/backend/src/services/ospf-optimizer-metrics.ts +++ b/backend/src/services/ospf-optimizer-metrics.ts @@ -3,14 +3,26 @@ interface Probe {id:string;lastPingRttMs:number|null;lastPingAt:string|null;last interface Traffic {interfaceName:string;rxBps:number;txBps:number;sampledAt:string} const unknown=()=>({value:null,source:"unknown" as const,sourceId:null,measuredAt:null}) function timestamp(value:string|null){return value===null?null:new Date(value).toISOString()} +/** Probes are selected with this router as the bandwidth-test client. */ +export function readOspfProbeSpeeds(probe: Pick | undefined) { + return {dlMbps:probe?.lastRxAvgMbps??null,ulMbps:probe?.lastTxAvgMbps??null} +} +export function readOspfOptimizerModelSpeeds(probe: Probe | undefined, fallback: {dlMbps:number;ulMbps:number}) { + const speeds=readOspfProbeSpeeds(probe) + return { + dlMbps:speeds.dlMbps===null?fallback.dlMbps:Math.max(1,Math.round(speeds.dlMbps)), + ulMbps:speeds.ulMbps===null?fallback.ulMbps:Math.max(1,Math.round(speeds.ulMbps)), + } +} export function readOspfOptimizerObserved(probe:Probe|undefined,traffic:Traffic|undefined):OspfOptimizerObserved{ function speed(value:number|null|undefined,field:"rxBps"|"txBps"){ if(value!=null&&probe)return {value,source:"speed-probe" as const,sourceId:probe.id,measuredAt:null} if(traffic)return {value:traffic[field]/1_000_000,source:"interface-traffic" as const,sourceId:traffic.interfaceName,measuredAt:timestamp(traffic.sampledAt)} return unknown() } + const speeds=readOspfProbeSpeeds(probe) return ospfOptimizerObservedSchema.parse({ ping:probe?.lastPingRttMs!=null?{value:probe.lastPingRttMs,source:"speed-probe",sourceId:probe.id,measuredAt:timestamp(probe.lastPingAt)}:unknown(), - dl:speed(probe?.lastTxAvgMbps,"rxBps"),ul:speed(probe?.lastRxAvgMbps,"txBps"), + dl:speed(speeds.dlMbps,"rxBps"),ul:speed(speeds.ulMbps,"txBps"), }) } diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index ffd99eae..03db129e 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,6 +1,6 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-07: 12.3.74 завершён: backend проверяет native terminal status перед Ping/DB success write,16pure и backend build. Следующий12.3.75: OSPF RX/TX mapping; затем76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно. +Актуальное продолжение 2026-10-07: 12.3.75 завершён: OSPF raw/model используют client RX→DL/TX→UL,4backend suites и build. Следующий12.3.76: mobile pinned-column readable geometry. После этого обновить профильную и общую регрессию; команды/права не менять,12.5 отдельно. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-04. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index e658571d..a60c127d 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-07: 12.3.74 завершён: backend проверяет native terminal status перед Ping/DB success write,16pure и backend build. Следующий12.3.75: OSPF RX/TX mapping; затем76mobile pinned-column readable geometry. Команды/права не менять,12.5 отдельно. +Актуальное продолжение 2026-10-07: 12.3.75 завершён: OSPF raw/model используют client RX→DL/TX→UL,4backend suites и build. Следующий12.3.76: mobile pinned-column readable geometry. После этого обновить профильную и общую регрессию; команды/права не менять,12.5 отдельно. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index f58c9805..72148674 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -826,3 +826,12 @@ CPU final:1000routes13.3ms;100000(10×100×100)49.7ms;100000(100×10×100)44.0ms 16 pure tests (3status,4rates,9scheduler) и backend build passed. Проверка placement/callers по source: HTTP и scheduled collection используют общий execute, их catch вызывает прежний failure recorder, который сохраняет старые probe averages и пишет error run с nullable rates. Полная DB/network orchestration не запускалась; это source review и pure guard tests, а не внешняя приёмка. Команда bandwidth-test и её параметры, locks, routing rules, permissions и DB schema не менялись. Источник native statuses: [RouterOS CLI bandwidth-test](https://manual.mikrotik.com/docs/cli-reference/tool/bandwidth-test/). Последний reported done допускает последующие rows без status, но любые failure markers отклоняются даже после completion. Реальный REST shape/версия RouterOS и PG writes требуют12.5. Следующие75direction OSPF и76mobile geometry. + + +## 12.3.75 — направление скоростных входов OSPF + +2026-10-07. Источник выбирается через srcServerId текущего роутера, поэтому единый pure getter читает RX как DL и TX как UL. Getter используется raw observed и model speeds; route builder вызывает модельный helper вместо прежнего обратного сопоставления. Raw zero/null/fraction/source/date сохраняются. Модельные округление, floor1 и traffic fallback сохранены; formula Cost, pingWeight, unique probe assignment и порядок операций не менялись. Рекомендации Cost могут измениться из-за исправленных asymmetric inputs. Traffic fallback RX→DL/TX→UL уже был верен. Дата speed остаётсяnull, Ping date не подставляется. + +Четыре backend suites passed: pure metrics (новые asymmetric80.75/30.25,0/null,rounding/floor/fallback), identity, plan и HTTP optimizer с DI/in-memory writes. Backend build passed. Source review подтвердил фильтрацию по srcServerId и вызов helper в builder. Реальный DB builder и RouterOS не исполнялись; HTTP suite подменяет buildOptimizationPlan и отдельно проверяет отсутствие операций до валидного плана. Frontend не менялся, повторный browser/build для него не требовался. + +Следующий76: на320px проверить видимость всей соседней metric cell за pinnedWAN, исправить column sizing при подтверждённой проблеме. Общая main/context регрессия прежних HEAD требует обновления позже;12.5 остаётся внешней.