diff --git a/backend/src/routes/internet-path.ts b/backend/src/routes/internet-path.ts index 8fa1e19a..bed3ad2f 100644 --- a/backend/src/routes/internet-path.ts +++ b/backend/src/routes/internet-path.ts @@ -1,5 +1,5 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" -import { readStoredInternetPathPayload } from "../services/internet-path-snapshot.js" +import { mapStoredInternetPathResponse } from "../services/internet-path-snapshot.js" import { refreshScheduler } from "../services/scheduler.js" import { getInternetPathSettings, @@ -48,16 +48,11 @@ const internetPathRoutes: FastifyPluginAsyncZod = async (app) => { app.get("/internet-path/latest", async (_req, reply) => { const row = await getLatestInternetPathSnapshot() if (!row) return reply.send({ snapshot: null }) - let payload: object try { - payload = readStoredInternetPathPayload(row.payloadJson) + return reply.send(mapStoredInternetPathResponse(row)) } catch { return reply.code(502).send({error: "Сохранённый снимок Internet path повреждён"}) } - return reply.send({ - snapshot: payload, - sampledAt: row.sampledAt, - }) }) } diff --git a/backend/src/services/internet-path-snapshot.test.ts b/backend/src/services/internet-path-snapshot.test.ts index ed10595b..4eaa8023 100644 --- a/backend/src/services/internet-path-snapshot.test.ts +++ b/backend/src/services/internet-path-snapshot.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict" -import {collectConfirmedInternetPath, readInternetPathRows, readStoredInternetPathPayload} from "./internet-path-snapshot.js" +import {collectConfirmedInternetPath, readInternetPathRows, readStoredInternetPathPayload, mapStoredInternetPathResponse} from "./internet-path-snapshot.js" const servers=[{id:1,type:"home-router",enabled:true},{id:2,type:"home-router",enabled:false},{id:3,type:"exit-node",enabled:true}] const sampledAt="2026-10-04T00:00:00Z" const defaults={servers:async()=>servers,gre:async()=>[],filters:async()=>[],speed:async()=>[],route:async(id:number)=>{assert.equal(id,1);return {gateway:null,routingMark:null}},wan:async(id:number)=>{assert.equal(id,1);return {defaultGateway:null,defaultInterface:null,uplinks:[]}}} @@ -26,3 +26,12 @@ for(const [fields,row] of [[ ["dst-address"],{"message":"denied"} ],[["interface assert.deepEqual(readInternetPathRows([],"routes",["dst-address"]),[]) assert.equal(readInternetPathRows([{ "dst-address":"0.0.0.0/0",active:"true" }],"routes",["dst-address"]).length,1) assert.equal(readInternetPathRows([{ ".id":"*1",name:"gre", "local-address":"0.0.0.0","remote-address":"192.0.2.2" }],"gre",[".id","name","local-address","remote-address"]).length,1) + +for (const stored of ["2026-10-04 07:00:00+07", "2026-10-04T00:00:00Z"]) { + const response=mapStoredInternetPathResponse({payloadJson:good,sampledAt:stored}) + assert.equal(response.sampledAt,"2026-10-04T00:00:00.000Z") + assert.deepEqual(response.snapshot,good) +} +for (const invalid of ["", "bad", null]) assert.throws(()=>mapStoredInternetPathResponse({payloadJson:good,sampledAt:invalid as string})) +assert.throws(()=>mapStoredInternetPathResponse({payloadJson:"bad",sampledAt})) +console.log("internet path HTTP timestamp: PostgreSQL/ISO instant preserved; invalid metadata rejected") diff --git a/backend/src/services/internet-path-snapshot.ts b/backend/src/services/internet-path-snapshot.ts index d7893323..82455e9d 100644 --- a/backend/src/services/internet-path-snapshot.ts +++ b/backend/src/services/internet-path-snapshot.ts @@ -30,3 +30,9 @@ export function readStoredInternetPathPayload(raw: unknown): object { if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("Сохранённый снимок Internet path повреждён") return payload } + +/** PostgreSQL string-mode timestamps must be normalized at the HTTP boundary. */ +export function mapStoredInternetPathResponse(row: { payloadJson: unknown; sampledAt: string }) { + if (typeof row.sampledAt !== "string" || !row.sampledAt.trim()) throw new Error("Сохранённая дата Internet path повреждена") + return { snapshot: readStoredInternetPathPayload(row.payloadJson), sampledAt: new Date(row.sampledAt).toISOString() } +} diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index a09f58dc..c6ff696a 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -34,6 +34,18 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в описанных границах:12.3.83,main1186/1186,context221/221. Этап остаётся в работе: стенд12.5.1 подготовлен,bootstrap/сценарии12.5 не выполнены. | +## 12.5.3 — нормализация даты Internet path на границе API (2026-10-07) + +Выявленный live-smoke дефект исправлен в mapper/GET latest: внешний PostgreSQL sampledAt преобразуется вISO без смены instant;payload не изменяется. Неверная дата/payload отклоняются502. Строгийfrontend DTO не ослаблен. Pure collector/mapper tests прошли,включаяPostgreSQL+07/ISO/invalid/null/blank,contracts/backend build passed. На сервере пока работает прежний образ;блок в работе до пересборки и live проверки. + +## 12.5.2 — сборка Docker и запуск приложения на тестовом сервере (2026-10-07) + +Собраны backend/frontend из закреплённого коммита4d88b97d490b2342aa4de81d093a0b8d4902e8d4,через чистые build contexts по CI main. Архив8MiB не содержит локальных.env/лицензии/ключей/node_modules. Образы mmapp-test/backend:ux-4d88b97 и mmapp-test/frontend:ux-4d88b97 созданы на сервере без push;version1.0.0-test.4d88b97,revision label сохранён. Контейнеры запущены наloopback;backend/PostgreSQL healthy,health напрямую/черезfrontend status:ok. SSH-туннель3000/8000 открыт для проверки. + +Реальный PostgreSQL применил9миграций,включая0008/0009;CHECK разрешает5статусов outbox и resolution_version>=0. После restartbackend совпали контрольный uptime_settings(id1/retention14) и список миграций;health восстановился. Пустые servers/probes/history прочитаны через API. [Первый live browser smoke](ux-ui/acceptance/test-server-smoke-2026-10-07.json):3экрана200,безJSerrors/API>=400,320px fits,безHTTP-моков. [Снимок320px](ux-ui/screens/test-server-speed-320-2026-10-07.png) просмотрен. + +Smoke обнаружил ошибку Internet path приHTTP200: внешний sampledAt пришёл строкой PostgreSQL вместоISO;корректный snapshot отклонён строгимfrontend. Общий smoke не является полной приёмкой: UI-source error сохранён в отчёте. Следующий12.5.3 — нормализация producer timestamp,пересборка и повтор live-smoke. PG backup/restore/concurrency/native/integrations не проверены. Внешний PG-1 проверен bootstrap/restart/schema/control-row в указанной границе. + ## 12.5.1 — подготовка удалённого тестового Docker-стенда (2026-10-07) По прямому поручению пользователя подготовлен временный сервер161.104.46.222,root,Debian13.7 trixie/x86_64. До установки Docker отсутствовал;1CPU/1973MiB RAM,23GiB свободно. Установлены Docker Engine29.8.2,Compose5.6.0,Buildx0.37.1 из официального apt repository,git/python3 и4GiB swap. Docker systemd enabled/active,hello-world passed. SSH и сетевые настройки подключения сохранены. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 2669f98f..92a263b9 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -960,3 +960,15 @@ Main1186/1186 на9680a64 и эта context матрица теперь вклю Создан /opt/mmapp-test (0700),Compose по main с отдельными именами/project/data,ограничением logs,loopback3000/8000 и фиксированными тегами. Пароль SSH не сохранён в репозитории; PG/JWT secrets сгенерированы на сервере в.env0600 без вывода. Compose config --quiet passed; SHA256 совпал с локальным шаблоном. Реальный PostgreSQL18.6 запущен,healthy и SELECT version() passed; порт БД не опубликован. [Паспорт и дальнейшие команды](ux-ui-test-server.md), [Compose](../deploy/docker-compose.test.yml), [env-шаблон](../deploy/env.test.example). Это подготовка инфраструктуры, не приёмка PG-1/restore/native: backend/frontend ещё не собраны и не запущены, bootstrap не выполнен. Следующий12.5.2 — чистые CI build contexts,закреплённые образы без push,bootstrap/health и начало PG-протокола. Локально docker CLI отсутствует; серверный Docker готов. Публикация Gitea/CI не выполнялась; локальный коммит без push. Ресурсы стенда не представляют production performance. + +## 12.5.2 — сборка Docker и запуск приложения на тестовом сервере (2026-10-07) + +Собраны backend/frontend из закреплённого коммита4d88b97d490b2342aa4de81d093a0b8d4902e8d4,через чистые build contexts по CI main. Архив8MiB не содержит локальных.env/лицензии/ключей/node_modules. Образы mmapp-test/backend:ux-4d88b97 и mmapp-test/frontend:ux-4d88b97 созданы на сервере без push;version1.0.0-test.4d88b97,revision label сохранён. Контейнеры запущены наloopback;backend/PostgreSQL healthy,health напрямую/черезfrontend status:ok. SSH-туннель3000/8000 открыт для проверки. + +Реальный PostgreSQL применил9миграций,включая0008/0009;CHECK разрешает5статусов outbox и resolution_version>=0. После restartbackend совпали контрольный uptime_settings(id1/retention14) и список миграций;health восстановился. Пустые servers/probes/history прочитаны через API. [Первый live browser smoke](ux-ui/acceptance/test-server-smoke-2026-10-07.json):3экрана200,безJSerrors/API>=400,320px fits,безHTTP-моков. [Снимок320px](ux-ui/screens/test-server-speed-320-2026-10-07.png) просмотрен. + +Smoke обнаружил ошибку Internet path приHTTP200: внешний sampledAt пришёл строкой PostgreSQL вместоISO;корректный snapshot отклонён строгимfrontend. Общий smoke не является полной приёмкой: UI-source error сохранён в отчёте. Следующий12.5.3 — нормализация producer timestamp,пересборка и повтор live-smoke. PG backup/restore/concurrency/native/integrations не проверены. Внешний PG-1 проверен bootstrap/restart/schema/control-row в указанной границе. + +## 12.5.3 — нормализация даты Internet path на границе API (2026-10-07) + +Выявленный live-smoke дефект исправлен в mapper/GET latest: внешний PostgreSQL sampledAt преобразуется вISO без смены instant;payload не изменяется. Неверная дата/payload отклоняются502. Строгийfrontend DTO не ослаблен. Pure collector/mapper tests прошли,включаяPostgreSQL+07/ISO/invalid/null/blank,contracts/backend build passed. На сервере пока работает прежний образ;блок в работе до пересборки и live проверки. diff --git a/docs/ux-ui/acceptance/test-server-smoke-2026-10-07.json b/docs/ux-ui/acceptance/test-server-smoke-2026-10-07.json new file mode 100644 index 00000000..a141d5e5 --- /dev/null +++ b/docs/ux-ui/acceptance/test-server-smoke-2026-10-07.json @@ -0,0 +1,234 @@ +{ + "sourceHead": "4d88b97d490b2342aa4de81d093a0b8d4902e8d4", + "server": "161.104.46.222", + "url": "http://localhost:3000 via SSH tunnel", + "mockedResponses": false, + "results": [ + { + "route": "/dashboard", + "status": 200, + "mainVisible": true, + "text": "Боковое меню\nОбзор\nДашборд\nСистема\nAPI доступен\nНе обновлены источники: 1\nПоследние полученные данные сохранены. Если источник ещё не загрузился, его показатели неизвестны.\nИсточники и время получения\nСерверы: Получено 07.10.2026, 21:19:46\nПробы: Получено 07.10.2026, 21:19:46\nBGP: Получено 07.10.2026, 21:19:46\nGRE: Получено 07.10.2026, 21:19:46\nWir" + }, + { + "route": "/uptime?tab=speed", + "status": 200, + "mainVisible": true, + "text": "Боковое меню\nОбзор\nМониторинг\nПауза\nОбновить\nНовая speed-проба\nСистема\nAPI доступен\nLive (backend)\nПинг-пробы\n0\nРесурсы\n0\nСкорость\n0\n\nSpeed-пробы: получено 07.10.2026, 21:19:55\n\nИстория тестов: получено 07.10.2026, 21:19:55\n\nSpeed-пробы\n0 шт\nТестов выполнено\n0 run\nМакс TX\n—\nМакс RX\n—\n\nНет speed-проб\n\nСоздайте пробу через кнопку «Новая speed-проба» " + }, + { + "route": "/servers", + "status": 200, + "mainVisible": true, + "text": "Боковое меню\nУправление\nСерверы\nПроверить все\nДобавить сервер\nСистема\nAPI доступен\nВсего серверов\n0\nОнлайн\n0\nJH + Exit Node\n0\nHome Router\n0\nВсе\n0\nJumpHost\n0\nExit Node\n0\nHome Router\n0\nФильтры\n0 серверов\nНет серверов\nДобавьте первый MikroTik-сервер для мониторинга" + } + ], + "mobileFits": true, + "pageErrors": [], + "apiReads": [ + { + "path": "/api/auth/config", + "status": 200 + }, + { + "path": "/api/filters/gre-tunnels", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/events", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/api/filters/rules", + "status": 200 + }, + { + "path": "/api/bgp/catalog", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/uptime/overview", + "status": 200 + }, + { + "path": "/api/internet-path/latest", + "status": 200 + }, + { + "path": "/api/filters/gre-tunnels", + "status": 200 + }, + { + "path": "/api/traffic/servers", + "status": 200 + }, + { + "path": "/api/vxlan", + "status": 200 + }, + { + "path": "/api/wireguard", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + }, + { + "path": "/api/auth/config", + "status": 200 + }, + { + "path": "/api/filters/gre-tunnels", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/uptime/overview", + "status": 200 + }, + { + "path": "/api/uptime/speed-probes", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/uptime/speed-test/runs", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + }, + { + "path": "/api/auth/config", + "status": 200 + }, + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/filters/gre-tunnels", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + }, + { + "path": "/api/auth/config", + "status": 200 + }, + { + "path": "/api/filters/gre-tunnels", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/servers", + "status": 200 + }, + { + "path": "/api/uptime/overview", + "status": 200 + }, + { + "path": "/api/uptime/speed-probes", + "status": 200 + }, + { + "path": "/health", + "status": 200 + }, + { + "path": "/api/sidebar-counts", + "status": 200 + }, + { + "path": "/api/uptime/speed-test/runs", + "status": 200 + }, + { + "path": "/api/evobgp/settings", + "status": 200 + } + ] +} diff --git a/docs/ux-ui/screens/test-server-speed-320-2026-10-07.png b/docs/ux-ui/screens/test-server-speed-320-2026-10-07.png new file mode 100644 index 00000000..c6ffd20b Binary files /dev/null and b/docs/ux-ui/screens/test-server-speed-320-2026-10-07.png differ