From 53c1bfbd3bbcaa5a6011447de646f4aa1aa9f93a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 11:53:48 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=20=D0=BF=D0=B8=D0=BD=D0=B3-=D0=BF=D1=80=D0=BE=D0=B1=20?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=BC=D0=B0=D1=80=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/uptime/page.tsx | 101 ++++++++++-------- backend/src/routes/ping-probes-write.test.ts | 52 +++++++++ backend/src/routes/ping-probes-write.ts | 16 +++ backend/src/routes/uptime.ts | 35 +----- .../src/services/ping-probe-replacement.ts | 21 ++++ backend/src/services/uptime-collector.ts | 58 +++------- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 12 ++- docs/ux-ui-stage-12.md | 11 ++ docs/ux-ui/screens/ping-catalog-error-320.png | Bin 0 -> 65340 bytes lib/dashboard-probes.ts | 2 + packages/contracts/src/index.ts | 2 + packages/contracts/src/uptime-probes.ts | 12 +++ tests/e2e/backend-switch.e2e.ts | 2 +- tests/e2e/uptime-links.spec.ts | 39 +++++++ 15 files changed, 243 insertions(+), 122 deletions(-) create mode 100644 backend/src/routes/ping-probes-write.test.ts create mode 100644 backend/src/routes/ping-probes-write.ts create mode 100644 backend/src/services/ping-probe-replacement.ts create mode 100644 docs/ux-ui/screens/ping-catalog-error-320.png create mode 100644 packages/contracts/src/uptime-probes.ts diff --git a/app/(main)/uptime/page.tsx b/app/(main)/uptime/page.tsx index 40d336e4..0658782f 100644 --- a/app/(main)/uptime/page.tsx +++ b/app/(main)/uptime/page.tsx @@ -1,6 +1,8 @@ "use client" -import { speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema } from "@mmapp/contracts" +import { dashboardProbeOverviewSchema } from "@/lib/dashboard-probes" + +import { speedHistoryResponseSchema, speedProbesResponseSchema, speedProbesWriteSchema, speedProbesSavedSchema, pingProbesWriteSchema, probeCatalogSavedSchema } from "@mmapp/contracts" import probeStyles from "./probe-rows.module.css" @@ -250,7 +252,7 @@ function TypeChip({ type }: { type: "jump-host" | "exit-node" | "home-router" }) function StatChip({ label, value, color, active, onClick, }: { - label: string; value: number; color?: "emerald" | "amber" | "red"; active?: boolean; onClick?: () => void + label: string; value: number | string; color?: "emerald" | "amber" | "red"; active?: boolean; onClick?: () => void }) { return ( {tab === "probes" && ( - )} @@ -2052,7 +2056,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {stats.down} )} - {probes.length} + {pingCatalogKnown ? probes.length : "—"} ) : t === "resources" ? ( @@ -2365,8 +2369,19 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {/* ── probes tab ── */} {tab === "probes" && <> + {liveApi && !pingCatalogReady &&
+ + +
} - {opError && ( + {opError && pingCatalogReady && (
@@ -2392,10 +2407,10 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {/* ── summary + server filter ── */}
- - cycleStatusFilter("up")} /> - cycleStatusFilter("warn")} /> - cycleStatusFilter("down")} /> + + cycleStatusFilter("up")} /> + cycleStatusFilter("warn")} /> + cycleStatusFilter("down")} />
@@ -2453,7 +2468,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId

- {filtered.length} из {probes.length} проб + {pingCatalogKnown ? filtered.length + " из " + probes.length + " проб" : "Каталог проб пока неизвестен"} · звезда — блок «Активные пробы» @@ -2464,7 +2479,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId {/* ── probe groups ── */}

- {grouped.length === 0 && ( + {pingCatalogKnown && grouped.length === 0 && (

Пробы не найдены

@@ -2560,7 +2575,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId !p.enabled && "opacity-40", )} > - +
toggleDashboardStar(p.id)} + disabled={probeSaving || !canWrite || !pingCatalogReady} onClick={() => toggleDashboardStar(p.id)} title={p.showOnDashboard ? "Убрать с дашборда" : "Показать на дашборде"} aria-label={p.showOnDashboard ? "Убрать с дашборда" : "Показать на дашборде"} aria-pressed={p.showOnDashboard === true} @@ -2641,12 +2656,12 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
@@ -2690,7 +2705,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId })} - + diff --git a/backend/src/routes/ping-probes-write.test.ts b/backend/src/routes/ping-probes-write.test.ts new file mode 100644 index 00000000..7622f7bc --- /dev/null +++ b/backend/src/routes/ping-probes-write.test.ts @@ -0,0 +1,52 @@ +import assert from "node:assert/strict" +import Fastify from "fastify" +import { SignJWT } from "jose" +import { createPingProbesWriteRoute } from "./ping-probes-write.js" +import { pingProbesWriteSchema } from "@mmapp/contracts" +import { replacePingProbes, type PingProbePatch, type PingProbeStore } from "../services/ping-probe-replacement.js" +const probe = { id: "p1", srcServerId: "1", name: " Проверка ", target: "192.0.2.2", enabled: false, showOnDashboard: true } +let transactions = 0, refreshes = 0, failWrite = false, failScheduler = false +let rows = new Map([["old", { ...pingProbesWriteSchema.parse({ probes: [probe] }).probes[0], id: "old", probeFilter: "main", sortOrder: 0, updatedAt: "old", intervalSec: 60 }]]) +let history = new Map([["old", [42]]]) +const store: PingProbeStore = { transaction: async run => { + transactions++; const draft = new Map(rows), samples = new Map(history) + await run({ listIds: async () => [...draft.keys()], remove: async id => { draft.delete(id); samples.delete(id) }, upsert: async patch => { + draft.set(patch.id, { ...draft.get(patch.id), ...patch }) + if (failWrite) throw new Error("write failed") + } }) + rows = draft; history = samples +} } +const app = Fastify(); await app.register(createPingProbesWriteRoute({ replace: rows => replacePingProbes(store, rows), refreshScheduler: async () => { refreshes++; if (failScheduler) throw new Error("scheduler failed") } }), { prefix: "/api" }) +const put = (payload: unknown) => app.inject({ method: "PUT", url: "/api/uptime/probes", headers: { "content-type": "application/json" }, payload: JSON.stringify(payload) }) +for (const payload of [null, {}, { probes: null }, { probes: {} }, { probes: [probe, probe] }, ...[ + { id: "" }, { srcServerId: "1junk" }, { srcServerId: 0 }, { srcServerId: 9007199254740992 }, { name: " " }, { target: " " }, + { enabled: undefined }, { enabled: "true" }, { showOnDashboard: "false" }, { filter: false }, { srcInterface: null }, + { intervalSec: -1 }, { intervalSec: "10junk" }, { intervalSec: 3.5 }, { intervalSec: 2147483648 }, +].map(patch => ({ probes: [probe, { ...probe, id: "p2", ...patch }] }))]) assert.equal((await put(payload)).statusCode, 400) +assert.equal(transactions, 0); assert.equal(refreshes, 0); assert.deepEqual(history.get("old"), [42]) +failWrite = true; assert.equal((await put({ probes: [probe] })).statusCode, 500) +assert.ok(rows.has("old")); assert.deepEqual(history.get("old"), [42]); assert.equal(refreshes, 0) +failWrite = false; assert.equal((await put({ probes: [{ ...probe, intervalSec: 60 }] })).statusCode, 200) +assert.equal(rows.get("p1")?.name, "Проверка"); assert.equal(rows.get("p1")?.enabled, false); assert.equal(history.has("old"), false) +history.set("p1", [0, 42]); await put({ probes: [{ ...probe, showOnDashboard: undefined }] }) +assert.equal(rows.get("p1")?.intervalSec, 60); assert.equal(rows.get("p1")?.showOnDashboard, true); assert.deepEqual(history.get("p1"), [0, 42]) +for (const intervalSec of [3, 0, "", null]) { + await put({ probes: [{ ...probe, intervalSec }] }); assert.equal(rows.get("p1")?.intervalSec, intervalSec === 3 ? 5 : 0) +} +failScheduler = true; const saved = await put({ probes: [{ ...probe, name: "Изменена" }] }) +assert.equal(saved.statusCode, 200); assert.equal(saved.json().ok, true); assert.match(saved.json().warning, /Ping-пробы сохранены/); assert.equal(rows.get("p1")?.name, "Изменена") +failScheduler = false; await put({ probes: [probe, { ...probe, id: "p2", name: "Вторая" }] }) +assert.deepEqual([...rows.values()].map(row => [row.id, row.sortOrder]), [["p1", 0], ["p2", 1]]) +assert.equal((await put({ probes: [] })).statusCode, 200); assert.equal(rows.size, 0); assert.equal(history.size, 0) +await app.close(); console.log("Ping probes HTTP/DI: whole request validated before mutation; modeled atomic rollback including history; omitted interval preserved; explicit delete and scheduler warning") +process.env.AUTH_REQUIRED = "true"; process.env.AUTH_JWT_SECRET = "ping-probes-test-secret"; process.env.AUTH_ISSUER = "https://auth.test.local"; process.env.NODE_ENV = "test" +const { default: authPlugin, requireAuth } = await import("../plugins/auth.js") +const secured = Fastify(); await secured.register(authPlugin); secured.addHook("preHandler", requireAuth) +let writes = 0; await secured.register(createPingProbesWriteRoute({ replace: async () => { writes++ }, refreshScheduler: async () => undefined }), { prefix: "/api" }) +const send = (token?: string) => secured.inject({ method: "PUT", url: "/api/uptime/probes", payload: { probes: [] }, headers: token ? { authorization: "Bearer " + token } : {} }) +assert.equal((await send()).statusCode, 401); assert.equal(writes, 0) +for (const permission of ["mm:uptime:read", "mm:servers:write", "mm:uptime:write"]) { + const token = await new SignJWT({ sub: "test", apps: ["mm"], permissions: [permission] }).setProtectedHeader({ alg: "HS256" }).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("ping-probes-test-secret")) + assert.equal((await send(token)).statusCode, permission === "mm:uptime:write" ? 200 : 403) +} +assert.equal(writes, 1); await secured.close(); console.log("Ping probes RBAC: unauthorized requests have no storage side effects") diff --git a/backend/src/routes/ping-probes-write.ts b/backend/src/routes/ping-probes-write.ts new file mode 100644 index 00000000..a2610090 --- /dev/null +++ b/backend/src/routes/ping-probes-write.ts @@ -0,0 +1,16 @@ +import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" +import { pingProbesWriteSchema, type PingProbeWrite } from "@mmapp/contracts" +export function createPingProbesWriteRoute(deps: { replace: (probes: PingProbeWrite[]) => Promise; refreshScheduler: () => Promise }): FastifyPluginAsyncZod { + return async app => { + app.put("/uptime/probes", async (req, reply) => { + const parsed = pingProbesWriteSchema.safeParse(req.body) + if (!parsed.success) return reply.code(400).send({ error: "Список Ping-проб некорректен. Проверьте устройства, имена, цели и уникальность записей. Изменения не сохранены." }) + await deps.replace(parsed.data.probes) + try { await deps.refreshScheduler(); return reply.send({ ok: true }) } + catch (error) { + req.log.error({ err: error }, "Ping probes saved; scheduler refresh failed") + return reply.send({ ok: true, warning: "Ping-пробы сохранены, но расписание не обновлено. Проверьте состояние сборщика данных." }) + } + }) + } +} diff --git a/backend/src/routes/uptime.ts b/backend/src/routes/uptime.ts index 38f2bcc7..00a26cba 100644 --- a/backend/src/routes/uptime.ts +++ b/backend/src/routes/uptime.ts @@ -1,3 +1,4 @@ +import { createPingProbesWriteRoute } from "./ping-probes-write.js" import { createSpeedProbesWriteRoute } from "./speed-probes-write.js" import { replaceSpeedProbes } from "../services/speed-probe-replacement.js" import { mapSpeedProbeRead } from "../services/speed-probe-dto.js" @@ -217,39 +218,7 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => { refreshScheduler, })) - app.put("/uptime/probes", async (req, reply) => { - const body = req.body as { - probes?: Array<{ - id?: string - srcServerId?: string | number - srcInterface?: string - name?: string - target?: string - filter?: string - enabled?: boolean - showOnDashboard?: boolean - intervalSec?: number | string - }> - } - const normalized = (body.probes ?? []) - .map((p, i) => ({ - id: p.id || `p-${Date.now()}-${i}`, - srcServerId: Number.parseInt(String(p.srcServerId ?? ""), 10), - srcInterface: String(p.srcInterface ?? "").trim(), - name: String(p.name ?? "").trim(), - target: String(p.target ?? "").trim(), - probeFilter: String(p.filter ?? "—"), - enabled: p.enabled !== false, - showOnDashboard: p.showOnDashboard === true, - intervalSec: p.intervalSec == null || p.intervalSec === "" - ? undefined - : Number.parseInt(String(p.intervalSec), 10), - })) - .filter((p) => Number.isFinite(p.srcServerId) && p.name && p.target) - await replaceProbes(normalized) - await refreshScheduler() - return reply.send({ ok: true }) - }) + await app.register(createPingProbesWriteRoute({ replace: replaceProbes, refreshScheduler })) app.patch("/uptime/probes/:id", async (req, reply) => { const params = req.params as { id?: string } diff --git a/backend/src/services/ping-probe-replacement.ts b/backend/src/services/ping-probe-replacement.ts new file mode 100644 index 00000000..0ee4df44 --- /dev/null +++ b/backend/src/services/ping-probe-replacement.ts @@ -0,0 +1,21 @@ +import type { PingProbeWrite } from "@mmapp/contracts" +export type PingProbePatch = Omit & { probeFilter: string; sortOrder: number; updatedAt: string } +export type PingProbeTransaction = { + listIds: () => Promise + remove: (id: string) => Promise + upsert: (row: PingProbePatch, createdAt: string) => Promise +} +export type PingProbeStore = { transaction: (run: (tx: PingProbeTransaction) => Promise) => Promise } +export async function replacePingProbes(store: PingProbeStore, probes: PingProbeWrite[], now = new Date().toISOString()): Promise { + await store.transaction(async tx => { + const nextIds = new Set(probes.map(row => row.id)) + for (const id of await tx.listIds()) if (!nextIds.has(id)) await tx.remove(id) + for (const [sortOrder, row] of probes.entries()) { + const { filter, intervalSec, showOnDashboard, ...config } = row + await tx.upsert({ ...config, probeFilter: filter, sortOrder, updatedAt: now, + ...(intervalSec === undefined ? {} : { intervalSec: intervalSec > 0 ? Math.max(5, intervalSec) : 0 }), + ...(showOnDashboard === undefined ? {} : { showOnDashboard }), + }, now) + } + }) +} diff --git a/backend/src/services/uptime-collector.ts b/backend/src/services/uptime-collector.ts index dd511495..0d589c2d 100644 --- a/backend/src/services/uptime-collector.ts +++ b/backend/src/services/uptime-collector.ts @@ -1,4 +1,6 @@ -import { and, asc, eq, gte, inArray, max, or } from "drizzle-orm" +import { replacePingProbes } from "./ping-probe-replacement.js" +import type { PingProbeWrite } from "@mmapp/contracts" +import { and, asc, eq, gte, inArray, max, or, sql } from "drizzle-orm" import { db } from "../db/index.js" import { servers, @@ -467,49 +469,19 @@ export async function updateProbeShowOnDashboard(probeId: string, showOnDashboar return r.length > 0 } -export async function replaceProbes(rows: Array<{ - id: string - srcServerId: number - srcInterface: string - name: string - target: string - probeFilter: string - enabled: boolean - showOnDashboard?: boolean - intervalSec?: number -}>) { - const now = new Date().toISOString() - const existing = await db.select({ id: uptimeProbes.id }).from(uptimeProbes) - const nextIds = new Set(rows.map((r) => r.id)) - - for (const p of existing) { - if (nextIds.has(p.id)) continue - await db.delete(uptimeProbes).where(eq(uptimeProbes.id, p.id)) - } - - for (let i = 0; i < rows.length; i += 1) { - const r = rows[i] - const intervalSec = r.intervalSec != null && r.intervalSec > 0 ? Math.max(5, Math.floor(r.intervalSec)) : 0 - const patch = { - srcServerId: r.srcServerId, - srcInterface: r.srcInterface || "", - name: r.name, - target: r.target, - probeFilter: r.probeFilter || "—", - enabled: r.enabled, - showOnDashboard: r.showOnDashboard ?? false, - intervalSec, - sortOrder: i, - updatedAt: now, - } - const updated = await db.update(uptimeProbes).set(patch).where(eq(uptimeProbes.id, r.id)).returning({ id: uptimeProbes.id }) - if (updated.length > 0) continue - await db.insert(uptimeProbes).values({ - id: r.id, - ...patch, - createdAt: now, +export async function replaceProbes(rows: PingProbeWrite[]) { + await replacePingProbes({ transaction: run => db.transaction(async tx => { + await tx.execute(sql`select pg_advisory_xact_lock(hashtext('uptime-ping-probes'))`) + await run({ + listIds: async () => (await tx.select({ id: uptimeProbes.id }).from(uptimeProbes)).map(row => row.id), + remove: async id => { await tx.delete(uptimeProbes).where(eq(uptimeProbes.id, id)) }, + upsert: async (row, createdAt) => { + const { id, ...patch } = row + await tx.insert(uptimeProbes).values({ id, ...patch, createdAt }) + .onConflictDoUpdate({ target: uptimeProbes.id, set: patch }) + }, }) - } + }) }, rows) } export async function readProbeSamplesSince(sinceIso: string) { diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index cf30fa3a..ed1fa96b 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -4,7 +4,7 @@ ## Актуальная точка продолжения — 2026-10-04 -Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. Следующий блок12.3.26:strict atomic Ping write и copy об удалении истории; затем12.3.27 WireGuard missing/zero bytes и network grids; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. +Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. 12.3.18 принят локально:users1000/поиск всего списка/детали и ошибка refresh/32 browser/build. 12.3.19 принят локально:backups1000/все мобильные действия/возврат focus/39+4 browser/build. 12.3.20 принят локально:независимые speed GET/cache/freshness/58+4 browser/build. 12.3.21 принят локально:strict history DTO/null rates от DB до UI/60+5 browser/unit4/builds. 12.4.3 принят локально:users/backups delta context36 на a558412. 12.3.22 принят локально:shared speed-probes DTO/context8/60+6+47 browser/unit5/builds. 12.3.23 принят локально:strict write DTO/atomic replacement/confirmed scheduler warning/65 browser/HTTP DI JWT/builds. 12.3.24 принят локально:OSPF routes1000/50 DOM/page20/cache/320px/22+1 browser/build. 12.3.25 принят локально:BFD1000/50 DOM/cache/320px/12 browser/build. 12.4.4 принят локально:context213/25 файлов и unit36 прошли на5b2a291. 12.4.5 принят локально:main1110/171 прошли на21859be. 12.3.26 принят локально:strict atomic Ping write/подтверждение каталога и сохранения/история удаления/62 browser/context11/DI6/builds. Следующий блок12.3.27: WireGuard missing/zero bytes и network grids; далее network grids и полная main/context регрессия.12.5 отдельно. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются. Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index bbe6b3b4..7e1caf10 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -29,13 +29,23 @@ | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | -| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13–12.3.25 приняты локально в описанных границах;12.4.4 context213 и12.4.5 main1110 прошли, остаются Ping write/network grids/внешняя приёмка | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | В работе;12.1/12.2.1–12.2.10 и12.3.1–12.3.5 и12.3.7–12.3.12 приняты в описанных локальных границах;12.4.1 main регрессия и профильный повтор записаны;12.4.2 context205 и unit28 прошли;12.3.13–12.3.25 приняты локально в описанных границах;12.4.4 context213 и12.4.5 main1110 прошли, 12.3.26 Ping write принят локально; остаются WireGuard counters/network grids/внешняя приёмка | +## 12.3.26 — атомарная запись каталога Ping-проб (2026-10-04) + +Shared write DTO проверяет весь список до DB: обязательный массив, уникальные ID, безопасные ID устройств, непустые имена/цели, boolean и целые интервалы. Некорректные строки не отбрасываются с последующим удалением других проб. Замена выполняется в PostgreSQL transaction с advisory lock; конфигурация обновляется отдельно от измерений. Оmitted interval/dashboard сохраняются для существующих строк, явный пустой список означает удаление. Ошибка refreshScheduler после commit возвращает подтверждённое ok:true с предупреждением. + +UI проверяет Ping-каталог до публикации, сохраняет последний успешный список при ошибке и запрещает замену неподтверждённого каталога. Первоначально неизвестные количества показаны как —. PUT требует подтверждённый ответ; malformed ack сохраняет черновик. Один ReUI Alert показывает причину и время последнего успешного списка. Диалог удаления точно описывает каскадное удаление Ping history; speed history сохраняется. Снимок320px: [Ping catalog error](ux-ui/screens/ping-catalog-error-320.png). + +Проверки: uptime-links62/62; context uptime/backend-switch11/11; pure/HTTP DI6/6 (включая JWT401/403, отсутствие mutations до validation и моделированный rollback с историей). TypeScript без incremental и ESLint прошли (9 исходных warnings Uptime); backend и frontend production build прошли. ReUI Alert API/usage проверены, primitives/base-nova не изменены. Fixture backend-switch дополнен реальным backend полем showOnDashboard. Общая регрессия1110/213 относится к предыдущим HEAD, после delta целиком ещё не повторена. + +Реальный PG rollback/advisory lock/RouterOS не проверены; DI не заменяет внешнюю12.5. Whole-catalog PUT остаётся last writer wins без revision/ETag. Resources entries остаются legacy DTO (проверена только array оболочка). Следующий блок12.3.27: WireGuard missing/zero RX/TX в деталях и таблице пиров; далее network grids. Коммит локальный, без push. + ## 12.4.5 — повтор основной регрессии (2026-10-04) На HEAD21859be полный playwright.config.ts дал1110 из1110 passed/171 файл за26.6min;0 unexpected/0 skipped/0 flaky, Edge/workers1/retries0/maxFailures12. Прежние3 устаревшие fixtures из12.4.1 исправлены и теперь прошли в общей матрице. Включены последние speed read/write и большие OSPF/BFD профили, двухтемная доступность оболочки, права, responsive и сквозные API scenarios. Доказательство: docs/ux-ui/acceptance/main-repeat-2026-10-04.json с точным HEAD и результатами по файлам. Context213/25 и unit36 приняты отдельно в12.4.4. Исторические PNG, пересозданные этим прогоном, восстановлены по явному проверенному списку; новые принятые OSPF/BFD снимки сохранены. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 6120cacf..19291e2b 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -380,3 +380,14 @@ Backend заменяет каталог в одной Drizzle transaction с pg_ Следующий блок12.3.26: строгий Ping write DTO до DB, атомарная замена, защита записи неизвестного каталога, подтверждение scheduler warning и точная copy о каскадном удалении истории. Затем12.3.27 WireGuard missing/zero counters и оставшиеся network grids. Внешняя12.5 отдельно; коммит локальный без push. + +## 12.3.26 — атомарная запись каталога Ping-проб (2026-10-04) + +Shared write DTO проверяет весь список до DB: обязательный массив, уникальные ID, безопасные ID устройств, непустые имена/цели, boolean и целые интервалы. Некорректные строки не отбрасываются с последующим удалением других проб. Замена выполняется в PostgreSQL transaction с advisory lock; конфигурация обновляется отдельно от измерений. Оmitted interval/dashboard сохраняются для существующих строк, явный пустой список означает удаление. Ошибка refreshScheduler после commit возвращает подтверждённое ok:true с предупреждением. + +UI проверяет Ping-каталог до публикации, сохраняет последний успешный список при ошибке и запрещает замену неподтверждённого каталога. Первоначально неизвестные количества показаны как —. PUT требует подтверждённый ответ; malformed ack сохраняет черновик. Один ReUI Alert показывает причину и время последнего успешного списка. Диалог удаления точно описывает каскадное удаление Ping history; speed history сохраняется. Снимок320px: [Ping catalog error](ux-ui/screens/ping-catalog-error-320.png). + +Проверки: uptime-links62/62; context uptime/backend-switch11/11; pure/HTTP DI6/6 (включая JWT401/403, отсутствие mutations до validation и моделированный rollback с историей). TypeScript без incremental и ESLint прошли (9 исходных warnings Uptime); backend и frontend production build прошли. ReUI Alert API/usage проверены, primitives/base-nova не изменены. Fixture backend-switch дополнен реальным backend полем showOnDashboard. Общая регрессия1110/213 относится к предыдущим HEAD, после delta целиком ещё не повторена. + +Реальный PG rollback/advisory lock/RouterOS не проверены; DI не заменяет внешнюю12.5. Whole-catalog PUT остаётся last writer wins без revision/ETag. Resources entries остаются legacy DTO (проверена только array оболочка). Следующий блок12.3.27: WireGuard missing/zero RX/TX в деталях и таблице пиров; далее network grids. Коммит локальный, без push. + diff --git a/docs/ux-ui/screens/ping-catalog-error-320.png b/docs/ux-ui/screens/ping-catalog-error-320.png new file mode 100644 index 0000000000000000000000000000000000000000..511824b566790f29c7e310b1ec9b47a2a692a17c GIT binary patch literal 65340 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.enabled),times=active.map(p=>p.sampledAt).filter((v):v is string=>!!v) return {probes,sampledAt:active.length&×.length===active.length?new Date(times.reduce((oldest,v)=>Math.min(oldest,Date.parse(v)),Infinity)).toISOString():null} } + +export { overview as dashboardProbeOverviewSchema } diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 4deabc0f..9177d031 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -15,3 +15,5 @@ export * from "./ospf-optimizer.js" export * from "./evobgp.js" export * from "./uptime-speed.js" + +export * from "./uptime-probes.js" diff --git a/packages/contracts/src/uptime-probes.ts b/packages/contracts/src/uptime-probes.ts new file mode 100644 index 00000000..a0578b38 --- /dev/null +++ b/packages/contracts/src/uptime-probes.ts @@ -0,0 +1,12 @@ +import { z } from "zod" +const serverId = z.union([z.number(), z.string().regex(/^[1-9][0-9]*$/).transform(Number)]).pipe(z.number().int().positive().max(Number.MAX_SAFE_INTEGER)) +const interval = z.union([z.number(), z.string().regex(/^[0-9]+$/).transform(Number), z.literal("").transform(() => 0), z.null().transform(() => 0)]).pipe(z.number().int().min(0).max(2147483647)) +export const pingProbeWriteSchema = z.object({ + id: z.string().trim().min(1), srcServerId: serverId, + srcInterface: z.string().trim().default(""), name: z.string().trim().min(1), target: z.string().trim().min(1), + filter: z.string().trim().transform(value => value || "—").default("—"), enabled: z.boolean(), + showOnDashboard: z.boolean().optional(), intervalSec: interval.optional(), +}) +export const pingProbesWriteSchema = z.object({ probes: z.array(pingProbeWriteSchema).refine(rows => new Set(rows.map(row => row.id)).size === rows.length, "Повторный ID Ping-пробы") }) +export const probeCatalogSavedSchema = z.object({ ok: z.literal(true), warning: z.string().min(1).optional() }) +export type PingProbeWrite = z.infer diff --git a/tests/e2e/backend-switch.e2e.ts b/tests/e2e/backend-switch.e2e.ts index 0e2069b0..9af23934 100644 --- a/tests/e2e/backend-switch.e2e.ts +++ b/tests/e2e/backend-switch.e2e.ts @@ -19,7 +19,7 @@ for (const status of [200, 500]) { } } const servers = [{ id: 1, name: isNew ? 'Новый источник' : 'Старый источник', host: '192.0.2.1', type: 'exit-node', enabled: true, country: 'RU', site: 'TEST', status: 'online', wanUplinks: [] }] - const probes = [{ id: 'p1', srcServerId: '1', name: isNew ? 'Новая проверка' : 'Старая проверка', target: '192.0.2.10', filter: 'main', rtt: 10, loss: 0, status: 'up', series: [10], enabled: true }] + const probes = [{ id: 'p1', srcServerId: '1', name: isNew ? 'Новая проверка' : 'Старая проверка', target: '192.0.2.10', filter: 'main', rtt: 10, loss: 0, status: 'up', series: [10], enabled: true, showOnDashboard: false }] const json = url.pathname === '/api/auth/config' ? { required: false } : url.pathname === '/api/servers' ? servers : url.pathname === '/api/uptime/overview' ? { probes, resources: [] } diff --git a/tests/e2e/uptime-links.spec.ts b/tests/e2e/uptime-links.spec.ts index f02cd5b5..e109ed1f 100644 --- a/tests/e2e/uptime-links.spec.ts +++ b/tests/e2e/uptime-links.spec.ts @@ -724,3 +724,42 @@ for (const malformed of [false, true]) test("Speed save requires confirmation an } expect(calls).toBe(1) }) + +for (const malformed of [false, true]) test("Ping save requires confirmation and separates scheduler failure/" + malformed, async ({ page }) => { + await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write") + await page.getByRole("button", { name: "Пауза", exact: true }).click() + let calls = 0 + await page.route("**/api/uptime/probes", route => { if (route.request().method() !== "PUT") return route.fallback(); calls++; return route.fulfill({ json: malformed ? {} : { ok: true, warning: "Ping-пробы сохранены, но расписание не обновлено. Проверьте состояние сборщика данных." } }) }) + await page.getByRole("button", { name: "Редактировать пробу", exact: true }).click() + const dialog = page.getByRole("dialog"), input = dialog.getByPlaceholder("youtube.com") + await input.fill("Черновик") + await dialog.getByRole("button", { name: "Сохранить", exact: true }).click() + if (malformed) { await expect(dialog.getByRole("alert")).toContainText("Ответ о сохранении Ping-проб не подтверждён"); await expect(input).toHaveValue("Черновик") } + else { await expect(dialog).toBeHidden(); await expect(page.getByRole("status").filter({ hasText: "Ping-пробы сохранены, но расписание" })).toBeVisible() } + expect(calls).toBe(1) +}) +test("Ping catalog: malformed initial/refresh disables replacement, recovery and history deletion copy", async ({ page }, info) => { + const control = await setup(page, "/uptime?probe=p1", 0, false, "mm:uptime:write") + await page.getByRole("button", { name: "Пауза", exact: true }).click() + let malformed = true + await page.route("**/api/uptime/overview*", route => malformed ? route.fulfill({ json: { probes: [{ id: "broken" }], resources: [] } }) : route.fallback()) + await page.reload(); await page.getByRole("button", { name: "Пауза", exact: true }).click() + const warning = page.getByRole("alert", { name: "Чтение Ping-проб", exact: true }) + await expect(warning).toContainText("Данные пока неизвестны") + await expect(page.getByRole("button", { name: "Новая проба", exact: true }).first()).toBeDisabled() + await expect(page.getByText("Пробы не найдены", { exact: true })).toHaveCount(0) + malformed = false; await page.getByRole("button", { name: "Обновить данные", exact: true }).click() + await expect(warning).toHaveCount(0); await expect(page.getByRole("button", { name: "Редактировать пробу", exact: true })).toHaveCount(1) + malformed = true; await page.getByRole("button", { name: "Обновить данные", exact: true }).click() + await expect(warning).toContainText("Показан последний успешный список") + await expect(page.getByRole("button", { name: "Редактировать пробу", exact: true }).first()).toBeDisabled() + await page.setViewportSize({ width: 320, height: 1000 }) + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) + await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }) + await page.screenshot({ path: info.outputPath("ping-catalog-error-320.png"), animations: "disabled" }) + malformed = false; await page.getByRole("button", { name: "Обновить данные", exact: true }).click() + await page.getByRole("button", { name: "Удалить пробу", exact: true }).first().click() + await expect(page.getByRole("alertdialog")).toContainText("Вместе с ней будет удалена история её измерений") + await page.getByRole("alertdialog").getByRole("button", { name: "Отмена", exact: true }).click() + expect(control.writes).toEqual([]) +})