fix(ui): не подменять неполные сетевые счётчики нулями
This commit is contained in:
1 parent
587dd94f82
commit
1deccf862c
13 files changed
+84
-99
No files matched your search
@@ -4,6 +4,7 @@
|
|||||||
"description": "MikroTik Manager backend — Fastify + Drizzle + PostgreSQL",
|
"description": "MikroTik Manager backend — Fastify + Drizzle + PostgreSQL",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test:sidebar-counts": "tsx src/services/complete-count.test.ts && tsx src/routes/sidebar-counts.test.ts",
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch src/index.ts",
|
||||||
"build": "npm run build -w @mmapp/contracts && tsc",
|
"build": "npm run build -w @mmapp/contracts && tsc",
|
||||||
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
|
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import Fastify from "fastify"
|
||||||
|
import {createSidebarCountsRoutes} from "./sidebar-counts.js"
|
||||||
|
const core={servers:0,filterRules:0,uptimeProbes:0,uptimeSpeedProbes:0,monitoringItems:0,recursiveRoutes:0,users:0};let malformed=false
|
||||||
|
const app=Fastify();await app.register(createSidebarCountsRoutes({core:async()=>malformed?{}:core,certificates:async()=>undefined,wireguard:async()=>0,ipsec:async()=>{throw Error("offline")},bgp:async()=>undefined,vxlan:async()=>2,containers:async()=>undefined}),{prefix:"/api"})
|
||||||
|
let response=await app.inject({method:"GET",url:"/api/sidebar-counts"});assert.equal(response.statusCode,200);assert.equal(response.headers["cache-control"],"no-store");assert.deepEqual(response.json(),{...core,wireguard:0,vxlan:2});malformed=true;response=await app.inject({method:"GET",url:"/api/sidebar-counts"});assert.equal(response.statusCode,500);assert.equal(response.json().servers,undefined);await app.close();console.log("Sidebar counts HTTP: unknown optional omitted, known zero, malformed core rejected")
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||||
|
import {z} from "zod"
|
||||||
|
import {boundedCompleteCount,completeCollectionCount} from "../services/complete-count.js"
|
||||||
import { count } from "drizzle-orm"
|
import { count } from "drizzle-orm"
|
||||||
import { listCertificatesFromServers } from "../services/certificates-service.js"
|
import { listCertificatesFromServers } from "../services/certificates-service.js"
|
||||||
import { countWireGuardInterfaces } from "../services/wireguard-live.js"
|
import { countWireGuardInterfaces } from "../services/wireguard-live.js"
|
||||||
@@ -18,43 +20,23 @@ import { listUsers } from "../modules/users/service/users-service.js"
|
|||||||
|
|
||||||
async function tableCount(table: typeof servers | typeof filterRules | typeof uptimeProbes | typeof uptimeSpeedProbes | typeof recursiveRoutes): Promise<number> {
|
async function tableCount(table: typeof servers | typeof filterRules | typeof uptimeProbes | typeof uptimeSpeedProbes | typeof recursiveRoutes): Promise<number> {
|
||||||
const rows = await db.select({ n: count() }).from(table)
|
const rows = await db.select({ n: count() }).from(table)
|
||||||
return rows[0]?.n ?? 0
|
const value=rows[0]?.n
|
||||||
|
if(typeof value!=="number"||!Number.isSafeInteger(value)||value<0)throw Error("Счётчик БД не подтверждён")
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
|
const countSchema=z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
||||||
app.get("/sidebar-counts", async (_req, reply) => {
|
const coreSchema=z.object({servers:countSchema,filterRules:countSchema,uptimeProbes:countSchema,uptimeSpeedProbes:countSchema,monitoringItems:countSchema,recursiveRoutes:countSchema,users:countSchema}).strict().refine(value=>value.monitoringItems===value.uptimeProbes+value.uptimeSpeedProbes)
|
||||||
const serversTotal = await tableCount(servers)
|
interface Dependencies{core:()=>Promise<unknown>;certificates:()=>Promise<number|undefined>;wireguard:()=>Promise<number|undefined>;ipsec:()=>Promise<number|undefined>;bgp:()=>Promise<number|undefined>;vxlan:()=>Promise<number|undefined>;containers:()=>Promise<number|undefined>}
|
||||||
const filterRulesTotal = await tableCount(filterRules)
|
export const defaultSidebarCountsDependencies:Dependencies={
|
||||||
const uptimeProbesTotal = await tableCount(uptimeProbes)
|
core:async()=>{const [serversTotal,filterRulesTotal,uptimeProbesTotal,uptimeSpeedProbesTotal,recursiveRoutesTotal,users]=await Promise.all([tableCount(servers),tableCount(filterRules),tableCount(uptimeProbes),tableCount(uptimeSpeedProbes),tableCount(recursiveRoutes),listUsers()]);return {servers:serversTotal,filterRules:filterRulesTotal,uptimeProbes:uptimeProbesTotal,uptimeSpeedProbes:uptimeSpeedProbesTotal,monitoringItems:uptimeProbesTotal+uptimeSpeedProbesTotal,recursiveRoutes:recursiveRoutesTotal,users:users.length}},
|
||||||
const uptimeSpeedProbesTotal = await tableCount(uptimeSpeedProbes)
|
certificates:()=>boundedCompleteCount(listCertificatesFromServers,value=>completeCollectionCount(value,"certificates")),wireguard:countWireGuardInterfaces,ipsec:countIpsecClients,bgp:countBgpSessions,vxlan:countVxlanTunnels,containers:countContainers,
|
||||||
const recursiveRoutesTotal = await tableCount(recursiveRoutes)
|
|
||||||
const [certRes, wireguardTotal, ipsecTotal, bgpTotal, vxlanTotal, containersTotal] = await Promise.all([
|
|
||||||
listCertificatesFromServers(),
|
|
||||||
countWireGuardInterfaces().catch(() => 0),
|
|
||||||
countIpsecClients().catch(() => 0),
|
|
||||||
countBgpSessions().catch(() => 0),
|
|
||||||
countVxlanTunnels().catch(() => 0),
|
|
||||||
countContainers().catch(() => 0),
|
|
||||||
])
|
|
||||||
const certificatesTotal = certRes.certificates.length
|
|
||||||
const usersTotal = (await listUsers()).length
|
|
||||||
|
|
||||||
return reply.send({
|
|
||||||
servers: serversTotal,
|
|
||||||
filterRules: filterRulesTotal,
|
|
||||||
uptimeProbes: uptimeProbesTotal,
|
|
||||||
uptimeSpeedProbes: uptimeSpeedProbesTotal,
|
|
||||||
monitoringItems: uptimeProbesTotal + uptimeSpeedProbesTotal,
|
|
||||||
recursiveRoutes: recursiveRoutesTotal,
|
|
||||||
certificates: certificatesTotal,
|
|
||||||
wireguard: wireguardTotal,
|
|
||||||
ipsec: ipsecTotal,
|
|
||||||
users: usersTotal,
|
|
||||||
bgpSessions: bgpTotal,
|
|
||||||
vxlan: vxlanTotal,
|
|
||||||
containers: containersTotal,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
export function createSidebarCountsRoutes(deps:Dependencies=defaultSidebarCountsDependencies):FastifyPluginAsyncZod{return async app=>{
|
||||||
export default sidebarCountsRoutes
|
app.get("/sidebar-counts",async(_req,reply)=>{
|
||||||
|
const [core,certificates,wireguard,ipsec,bgpSessions,vxlan,containers]=await Promise.all([deps.core(),boundedCompleteCount(deps.certificates,x=>x),boundedCompleteCount(deps.wireguard,x=>x),boundedCompleteCount(deps.ipsec,x=>x),boundedCompleteCount(deps.bgp,x=>x),boundedCompleteCount(deps.vxlan,x=>x),boundedCompleteCount(deps.containers,x=>x)])
|
||||||
|
const parsed=coreSchema.safeParse(core);if(!parsed.success)return reply.status(500).send({error:"Основные счётчики не подтверждены"})
|
||||||
|
return reply.header("Cache-Control","no-store").send({...parsed.data,certificates,wireguard,ipsec,bgpSessions,vxlan,containers})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
export default createSidebarCountsRoutes()
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||||
|
import {pollBgpCatalog} from "./bgp-catalog-poll.js"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
import type { BgpSessionRead } from "../types/server.js"
|
import type { BgpSessionRead } from "../types/server.js"
|
||||||
import { parseBgpSessions } from "./bgp-parse-sessions.js"
|
import { parseBgpSessions,parseBgpCatalogSessions } from "./bgp-parse-sessions.js"
|
||||||
import { MikrotikClient } from "./mikrotik.js"
|
import { MikrotikClient } from "./mikrotik.js"
|
||||||
|
|
||||||
/** Как в UI `/alerts`: `serverName: peerName`. */
|
/** Как в UI `/alerts`: `serverName: peerName`. */
|
||||||
@@ -29,15 +31,6 @@ export async function fetchBgpSessionsForAlerts(): Promise<BgpSessionRead[]> {
|
|||||||
return results.flat()
|
return results.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countBgpSessions(): Promise<number> {
|
export async function countBgpSessions():Promise<number|undefined>{
|
||||||
try {
|
return boundedCompleteCount(async()=>{const enabled=await db.select().from(servers).where(eq(servers.enabled,true));return pollBgpCatalog(enabled,async server=>parseBgpCatalogSessions(server,await MikrotikClient.fromServer(server).getBgpSessions()))},value=>completeCollectionCount(value,"sessions"))
|
||||||
const result = await Promise.race([
|
|
||||||
fetchBgpSessionsForAlerts(),
|
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
|
||||||
])
|
|
||||||
if (!result) return 0
|
|
||||||
return result.length
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import {test} from "node:test"
|
||||||
|
import {boundedCompleteCount,completeCollectionCount,completeContainerCount} from "./complete-count.js"
|
||||||
|
test("complete counts distinguish known zero, partial and missing failure metadata",()=>{
|
||||||
|
assert.equal(completeCollectionCount({rows:[],failures:[]},"rows"),0)
|
||||||
|
for(const raw of [{rows:[]},{rows:[],failures:[{}]},{rows:null,failures:[]}])assert.equal(completeCollectionCount(raw,"rows"),undefined)
|
||||||
|
assert.equal(completeContainerCount({servers:[]}),0);assert.equal(completeContainerCount({servers:[{availability:"available",containers:[{}],failures:[{resource:"envs"}]}]}),1)
|
||||||
|
for(const availability of ["unsupported","error"])assert.equal(completeContainerCount({servers:[{availability,containers:[],failures:[]}]}),undefined)
|
||||||
|
})
|
||||||
|
test("bounded reader omits thrown, malformed and timed out counts without publishing late success",async()=>{
|
||||||
|
assert.equal(await boundedCompleteCount(async()=>0,x=>x),0);assert.equal(await boundedCompleteCount(async()=>{throw Error("offline")},x=>x),undefined)
|
||||||
|
for(const value of [-1,NaN,Infinity,1.5])assert.equal(await boundedCompleteCount(async()=>value,x=>x),undefined)
|
||||||
|
let release!:(value:number)=>void;const gate=new Promise<number>(resolve=>release=resolve);assert.equal(await boundedCompleteCount(()=>gate,x=>x,5),undefined);release(9)
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/** Unknown or partial results are omitted from the API, never represented as zero. */
|
||||||
|
export async function boundedCompleteCount<T>(read:()=>Promise<T>,select:(value:T)=>number|undefined,timeoutMs=8000):Promise<number|undefined>{
|
||||||
|
let timer:ReturnType<typeof setTimeout>|undefined
|
||||||
|
try{return await Promise.race([Promise.resolve().then(read).then(select).then(value=>typeof value==="number"&&Number.isSafeInteger(value)&&value>=0?value:undefined).catch(()=>undefined),new Promise<undefined>(resolve=>{timer=setTimeout(()=>resolve(undefined),timeoutMs)})])}finally{if(timer!==undefined)clearTimeout(timer)}
|
||||||
|
}
|
||||||
|
export function completeCollectionCount(raw:unknown,key:string):number|undefined{
|
||||||
|
if(typeof raw!=="object"||raw===null)return undefined
|
||||||
|
const value=raw as Record<string,unknown>
|
||||||
|
if(!Array.isArray(value.failures)||value.failures.length||!Array.isArray(value[key]))return undefined
|
||||||
|
return value[key].length
|
||||||
|
}
|
||||||
|
export function completeContainerCount(raw:unknown):number|undefined{
|
||||||
|
if(typeof raw!=="object"||raw===null||!("servers" in raw)||!Array.isArray(raw.servers))return undefined
|
||||||
|
let count=0
|
||||||
|
for(const row of raw.servers){if(typeof row!=="object"||row===null||row.availability!=="available"||!Array.isArray(row.containers)||!Array.isArray(row.failures)||row.failures.some((failure:unknown)=>typeof failure!=="object"||failure===null||!("resource" in failure)||failure.resource==="containers"))return undefined;count+=row.containers.length}
|
||||||
|
return count
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {boundedCompleteCount,completeContainerCount} from "./complete-count.js"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
@@ -171,18 +172,7 @@ export async function listContainersForServer(server: ServerRow): Promise<Contai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countContainers(): Promise<number> {
|
export async function countContainers():Promise<number|undefined>{return boundedCompleteCount(()=>listContainersState(),completeContainerCount)}
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
listContainers(),
|
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
|
||||||
])
|
|
||||||
if (!result) return 0
|
|
||||||
return result.length
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeRosId(rosId: string): string {
|
function encodeRosId(rosId: string): string {
|
||||||
return encodeURIComponent(rosId)
|
return encodeURIComponent(rosId)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
@@ -576,18 +577,7 @@ export async function listIpsec(opts?: { serverId?: string }): Promise<IpsecList
|
|||||||
return { servers: summaries, clients, failures }
|
return { servers: summaries, clients, failures }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countIpsecClients(): Promise<number> {
|
export async function countIpsecClients():Promise<number|undefined>{return boundedCompleteCount(()=>listIpsec(),value=>completeCollectionCount(value,"clients"))}
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
listIpsec(),
|
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
|
||||||
])
|
|
||||||
if (!result) return 0
|
|
||||||
return result.clients.length
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnabledIpsecServerById(serverId: string | number) {
|
export async function getEnabledIpsecServerById(serverId: string | number) {
|
||||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
@@ -128,15 +129,4 @@ export async function listVxlanTunnelsForServer(server: ServerRow): Promise<Vxla
|
|||||||
return fetchVxlanForServer(server)
|
return fetchVxlanForServer(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countVxlanTunnels(): Promise<number> {
|
export async function countVxlanTunnels():Promise<number|undefined>{return boundedCompleteCount(()=>listVxlanResult(),value=>completeCollectionCount(value,"tunnels"))}
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
listVxlanTunnels(),
|
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
|
||||||
])
|
|
||||||
if (!result) return 0
|
|
||||||
return result.length
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {boundedCompleteCount,completeCollectionCount} from "./complete-count.js"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { db } from "../db/index.js"
|
import { db } from "../db/index.js"
|
||||||
import { servers } from "../db/schema.js"
|
import { servers } from "../db/schema.js"
|
||||||
@@ -279,18 +280,7 @@ export async function listWireGuardInterfaces(opts?: {
|
|||||||
return { interfaces: results.flat(), failures }
|
return { interfaces: results.flat(), failures }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countWireGuardInterfaces(): Promise<number> {
|
export async function countWireGuardInterfaces():Promise<number|undefined>{return boundedCompleteCount(()=>listWireGuardInterfaces({ includePrivateKey: false }),value=>completeCollectionCount(value,"interfaces"))}
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
await listWireGuardInterfaces({ includePrivateKey: false }),
|
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 8_000)),
|
|
||||||
])
|
|
||||||
if (!result) return 0
|
|
||||||
return result.interfaces.length
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnabledServerById(serverId: string | number) {
|
export async function getEnabledServerById(serverId: string | number) {
|
||||||
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
const id = typeof serverId === "number" ? serverId : Number.parseInt(String(serverId), 10)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Актуальная точка продолжения — 2026-10-03
|
## Актуальная точка продолжения — 2026-10-03
|
||||||
|
|
||||||
Этап 11Б в работе; последний проверенный блок 11Б.4.4.3.7; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
Этап 11Б в работе; последний проверенный блок 11Б.4.4.3.8; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.4.3.9: исправить семантику выбора объектов/условий и один scroll owner в Alerts Sheet; затем обозначить неподдерживаемое BGP-prefix событие, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует сценария остановки отправителя.
|
||||||
|
|
||||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.4.3.7; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.4.3.8; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +36,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Блок 11Б.4.4.3.8 — честные backend счётчики каталогов (2026-10-03)
|
||||||
|
|
||||||
|
- WireGuard/IPsec/VXLAN/BGP/certificates count берётся только из полного результата с явным failures:[]; containers — из нового typed state с available containers на всех устройствах. Legacy BGP/container list с swallowed errors не используется для count. Known empty даёт 0; timeout/throw/partial/malformed count даёт undefined, который HTTP не выдаёт как числовое значение.
|
||||||
|
- Общий bounded helper очищает timer и не публикует поздний результат после deadline. WireGuard больше не await-ит list до старта timeout. Core DB count не подменяет отсутствующий aggregate row нулём; HTTP строго проверяет core и monitoring sum, ошибки core дают 500; optional failures не уничтожают другие числа. No-store response, no payload/secret echo. Правила сетевого применения не менялись.
|
||||||
|
- 2 count helper DI + HTTP unknown/zero/core-malformed проверки, backend build, containers/live-maps профиль и lint прошли; browser sidebar/dashboard limited rights 7/7 прошли. Count functions имеют единственного consumer — sidebar-counts; callers проверены сборкой.
|
||||||
|
- Реальный RouterOS/PG не проверен. Deadline ограничивает ожидание результата; не отменяет уже начатый RouterOS polling внутри legacy reader. Counts разных каталогов не являются общей атомарной сетевой transaction. При partial refresh sidebar сохраняет ранее подтверждённый optional badge; первоначальное missing значение скрыто.
|
||||||
|
- Следующий точный шаг 11Б.4.4.3.9: исправить семантику выбора объектов/условий и один scroll owner в Alerts Sheet; затем обозначить неподдерживаемое BGP-prefix событие, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует сценария остановки отправителя.
|
||||||
|
|
||||||
## Блок 11Б.4.4.3.7 — права каждого источника дашборда (2026-10-03)
|
## Блок 11Б.4.4.3.7 — права каждого источника дашборда (2026-10-03)
|
||||||
|
|
||||||
- Все запросы useDashboardLive проходят ensureAuthConfig и per-path/method gate до запроса и публикации, включая standalone WireGuard/events. Dashboard:read не выдаёт servers/uptime/filters/BGP/network/traffic rights; fallback diagnostic POST требует servers:write как на backend. Unknown route не получает разрешение по умолчанию. Отозванные источники очищаются перед обновлением.
|
- Все запросы useDashboardLive проходят ensureAuthConfig и per-path/method gate до запроса и публикации, включая standalone WireGuard/events. Dashboard:read не выдаёт servers/uptime/filters/BGP/network/traffic rights; fallback diagnostic POST требует servers:write как на backend. Unknown route не получает разрешение по умолчанию. Отозванные источники очищаются перед обновлением.
|
||||||
|
|||||||
@@ -308,3 +308,7 @@ UI manual unknown retry принят локально: explicit duplicate risk,
|
|||||||
### Продолжение 2026-10-03 — 11Б.4.4.3.7
|
### Продолжение 2026-10-03 — 11Б.4.4.3.7
|
||||||
|
|
||||||
Dashboard per-request permissions принят локально: mapper parity13paths, limited JWT2, browser18/typecheck/lint прошли. Предыдущая regression107 завершилась успешно. Legacy dashboard DTO/fallback не переприняты. Следующий блок 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
Dashboard per-request permissions принят локально: mapper parity13paths, limited JWT2, browser18/typecheck/lint прошли. Предыдущая regression107 завершилась успешно. Legacy dashboard DTO/fallback не переприняты. Следующий блок 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||||
|
|
||||||
|
### Продолжение 2026-10-03 — 11Б.4.4.3.8
|
||||||
|
|
||||||
|
Backend sidebar optional counts не выдают timeout/partial/error как0, используют complete typed catalogs; helper2/HTTP/build/containers/live-maps/lint/browser7 прошли. Сетевой опрос после deadline не отменяется; PG/RouterOS внешняя приёмка нужна. Следующий блок 11Б.4.4.3.9: исправить семантику выбора объектов/условий и один scroll owner в Alerts Sheet; затем обозначить неподдерживаемое BGP-prefix событие, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует сценария остановки отправителя.
|
||||||
Reference in new issue
Block a user