fix(alerts): объяснить отключённые и неоднозначные цели оповещений

This commit is contained in:
Denozordec committed 2026-10-12 15:47:16 +07:00
1 parent db7105119d
commit d3f9a120e6
23 files changed
+1429 -16

No files matched your search

+3
View File
@@ -10,6 +10,7 @@ import {useAlertsTargets} from "@/hooks/use-alerts-targets"
import {AlertsOutboxPanel} from "@/components/alerts-outbox-panel"
import {AlertsTelegramForm} from "@/components/alerts-telegram-form"
import {AlertsTargetsStatus} from "@/components/alerts-targets-status"
import {AlertsTargetIssues} from "@/components/alerts-target-issues"
import { useDataSource } from "@/lib/data-source"
import { PageHeader } from "@/components/page-header"
import { FormToggle } from "@/components/form-kit"
@@ -157,6 +158,7 @@ interface AlertsGreTunnelRow {
}
interface AlertsMeta {
targetIssues?: AlertsResponse["meta"]["targetIssues"]
servers: string[]
greClients: string[]
probeTargets: string[]
@@ -2531,6 +2533,7 @@ function AlertsContent() {
{config.busy?<p role="status" className="text-xs">Ожидаем подтверждение правил и групп…</p>:null}
{config.unknown&&!config.busy?<div className="space-y-2"><p role="status" className="text-xs">Результат сохранения правил неизвестен. Запрос мог выполниться; проверьте правила и группы в БД перед повтором. Обновление списка не разрешает повтор автоматически.</p><Button type="button" size="sm" variant="outline" className="h-auto whitespace-normal" disabled={!config.enabled||operationBusy} onClick={config.review}>Я проверил правила — разрешить новый запрос</Button></div>:null}
{known?<>
{isLive && <AlertsTargetIssues issues={meta?.targetIssues} />}
{/* ── summary chip bar ── */}
<div className="flex items-center gap-2 flex-wrap">
@@ -3,6 +3,7 @@ import { pool, closePool } from "../../db/index.js"
import { initDatabase } from "../../db/bootstrap.js"
import { buildSignalSnapshotFromCollectors } from "./signals-from-collectors.js"
import { evaluateRule } from "./evaluate-rule.js"
import { getAlertsMeta } from "../alerts-service.js"
import type { ApiAlertRule } from "../alerts-service.js"
const database = process.env.MMAPP_OWN_SQL_DB
@@ -41,12 +42,14 @@ else {
let signals = await build()
assert.equal(signals.servers.length, 2); assert.equal(signals.servers[0].prevStatus, "online")
assert.equal(signals.trafficByServer[0].rxMbps, 0); assert.equal(signals.probes.length, 2); cases++
assert.deepEqual((await getAlertsMeta()).targetIssues, [])
const offlineRule = { id: "owned", name: "owned", type: "server", targets: [server().name], conditions: ["offline"], enabled: true } as ApiAlertRule
assert.ok(evaluateRule(offlineRule, signals))
await pool.query("UPDATE uptime_settings SET resources_enabled=false,ping_enabled=false WHERE id=1")
await pool.query("UPDATE traffic_settings SET enabled=false WHERE id=1")
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.probes, []); assert.deepEqual(signals.trafficByServer, []); cases++
assert((await getAlertsMeta()).targetIssues?.every(issue => issue.reason === "disabled-source"))
await pool.query("UPDATE uptime_settings SET resources_enabled=true,ping_enabled=true WHERE id=1")
await pool.query("UPDATE traffic_settings SET enabled=true WHERE id=1")
await pool.query("UPDATE servers SET enabled=false WHERE id=$1", [ids[0]])
@@ -54,6 +57,7 @@ else {
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [ids[0]])
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server().name, ids[1]])
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.trafficByServer, []); cases++
assert.deepEqual((await getAlertsMeta()).targetIssues?.filter(issue => issue.reason === "ambiguous").map(issue => issue.kind), ["server", "traffic"])
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server(1).name, ids[1]])
for (const broken of [null, "{", { ...snapshot("uptime_resources", []), servers: {} }, snapshot("uptime_resources", [])]) {
@@ -0,0 +1,33 @@
import type { AlertTargetIssue } from "@mmapp/contracts/alerts"
import type { CollectorSourceConfig } from "./collector-source-signals.js"
/** Configuration diagnostics only. This does not claim fresh samples or Telegram delivery. */
export function collectAlertTargetIssues(config: CollectorSourceConfig): AlertTargetIssue[] {
const out: AlertTargetIssue[] = []
const servers = new Map<string, typeof config.servers>()
for (const server of config.servers) {
const key = (server.name || server.host).trim().toLowerCase()
if (key) servers.set(key, [...(servers.get(key) ?? []), server])
}
const serverOn = (config.uptime?.resourcesEnabled ?? config.uptime?.enabled) === true || config.rest?.enabled === true
for (const group of servers.values()) {
const active = group.filter(s => s.enabled === true), target = (group[0].name || group[0].host).trim()
for (const [kind, sourceOn] of [["server", serverOn], ["traffic", config.traffic?.enabled === true]] as const) {
const reason = !active.length ? "disabled-object" : active.length > 1 ? "ambiguous" : !sourceOn ? "disabled-source" : null
if (reason) out.push({ kind, target, reason })
}
}
const probes = new Map<string, typeof config.probes>()
for (const probe of config.probes) {
if (!probe.name.trim() || !probe.target.trim()) continue
const key = `${probe.name.trim()} → ${probe.target.trim()}`
probes.set(key, [...(probes.get(key) ?? []), probe])
}
for (const [target, group] of probes) {
const active = group.filter(p => p.enabled && config.servers.some(s => s.enabled === true && Number(s.id) === p.srcServerId))
const sourceOn = (config.uptime?.pingEnabled ?? config.uptime?.enabled) === true
const reason = !active.length ? "disabled-object" : active.length > 1 ? "ambiguous" : !sourceOn ? "disabled-source" : null
if (reason) out.push({ kind: "probe", target, reason })
}
return out
}
@@ -3,6 +3,8 @@ import assert from "node:assert/strict"
import {test} from "node:test"
import type {AlertsResponse} from "@mmapp/contracts/alerts"
import {readAlertsOverview,alertsOverviewTransaction,buildAlertsMeta} from "./alerts-overview.js"
import {collectAlertTargetIssues} from "./alert-engine/target-issues.js"
import type {CollectorSourceConfig} from "./alert-engine/collector-source-signals.js"
const empty:AlertsResponse={revision:alertsConfigRevision({groups:[],rules:[]}),telegram:{chatId:"",tokenConfigured:false,connected:false,messageThreadId:null},groups:[],rules:[],history:[],meta:buildAlertsMeta([],[])}
test("Alerts overview uses one context with repeatable read/read only and rejects partial snapshots",async()=>{
const context={snapshot:structuredClone(empty)},calls:string[]=[];let transactions=0
@@ -17,3 +19,21 @@ test("Alerts metadata exposes existing probe keys without synthesizing per-serve
const withoutProbes=buildAlertsMeta(servers,[]);assert.deepEqual(withoutProbes.probeTargets,[]);assert.deepEqual(withoutProbes.rttLossTargets,[]);assert.equal(withoutProbes.serversDetail[0]?.type,"")
const actual=buildAlertsMeta(servers,[{name:"probe-fixture",target:"1.1.1.1"}]);assert.deepEqual(actual.probeTargets,["probe-fixture → 1.1.1.1"]);assert.deepEqual(actual.rttLossTargets,actual.probeTargets);assert.equal(JSON.stringify(actual).includes("8.8.8.8"),false)
})
test("Target diagnostics explain ambiguous and disabled configuration without claiming live status",()=>{
const config:CollectorSourceConfig={servers:[{id:1,name:"CHR",host:"one",enabled:true},{id:2,name:" chr ",host:"two",enabled:true}],probes:[],uptime:{enabled:true,resourcesEnabled:true,pingEnabled:true,intervalSec:15,probeIntervalSec:15},rest:{enabled:false,intervalSec:120},traffic:{enabled:true,intervalSec:30}}
assert.deepEqual(collectAlertTargetIssues(config),[{kind:"server",target:"CHR",reason:"ambiguous"},{kind:"traffic",target:"CHR",reason:"ambiguous"}])
const unique={...config,servers:config.servers.map((s,index)=>({...s,name:"server"+index}))}
assert.deepEqual(collectAlertTargetIssues(unique),[])
assert.deepEqual(collectAlertTargetIssues({...config,servers:[config.servers[0],{...config.servers[1],enabled:false}]}),[])
const disabled=collectAlertTargetIssues({...unique,servers:unique.servers.map(s=>({...s,enabled:false}))});assert.equal(disabled.length,4);assert(disabled.every(i=>i.reason==="disabled-object"))
const sourceOff={...unique,uptime:{...unique.uptime!,resourcesEnabled:false},traffic:{...unique.traffic!,enabled:false}}
assert(collectAlertTargetIssues(sourceOff).every(i=>i.reason==="disabled-source"))
assert.deepEqual(collectAlertTargetIssues({...sourceOff,rest:{...sourceOff.rest!,enabled:true}}).map(i=>i.kind),["traffic","traffic"])
const probe={id:"probe",name:"ping",target:"target",srcServerId:1,srcInterface:"",enabled:true,intervalSec:15}
const withProbe={...unique,probes:[probe,{...probe,id:"second"}]}
assert.deepEqual(collectAlertTargetIssues(withProbe),[{kind:"probe",target:"ping → target",reason:"ambiguous"}])
assert.deepEqual(collectAlertTargetIssues({...withProbe,probes:[probe,{...probe,id:"second",enabled:false}]}),[])
assert.equal(collectAlertTargetIssues({...unique,probes:[probe],uptime:{...unique.uptime!,pingEnabled:false}})[0].reason,"disabled-source")
const current=buildAlertsMeta(unique.servers.map(s=>({...s,site:null,country:null,type:null})),[probe],unique)
assert.deepEqual(current.targetIssues,[]);assert.equal("password" in current,false)
})
+4 -2
View File
@@ -1,5 +1,7 @@
import {alertsConfigRevision} from "./alerts-revision.js"
import {alertsResponseSchema,type AlertsResponse} from "@mmapp/contracts/alerts"
import {collectAlertTargetIssues} from "./alert-engine/target-issues.js"
import type {CollectorSourceConfig} from "./alert-engine/collector-source-signals.js"
export const alertsOverviewTransaction={isolationLevel:"repeatable read",accessMode:"read only"} as const
interface Dependencies<C>{transaction:(work:(context:C)=>Promise<AlertsResponse>,options:typeof alertsOverviewTransaction)=>Promise<AlertsResponse>;telegram:(context:C)=>Promise<unknown>;groups:(context:C)=>Promise<unknown>;rules:(context:C)=>Promise<unknown>;history:(context:C)=>Promise<unknown>;meta:(context:C)=>Promise<unknown>}
export async function readAlertsOverview<C>(deps:Dependencies<C>):Promise<AlertsResponse>{
@@ -14,8 +16,8 @@ export async function readAlertsOverview<C>(deps:Dependencies<C>):Promise<Alerts
},alertsOverviewTransaction)
}
export function buildAlertsMeta(servers:{name:string|null;host:string;site:string|null;country:string|null;type:string|null}[],probes:{name:string;target:string}[]){
export function buildAlertsMeta(servers:{name:string|null;host:string;site:string|null;country:string|null;type:string|null}[],probes:{name:string;target:string}[],config?:CollectorSourceConfig){
const labels=servers.map(server=>server.name?.trim()||server.host)
const probeTargets=probes.map(probe=>probe.name+" → "+probe.target)
return {servers:labels,greClients:labels,probeTargets,rttLossTargets:probeTargets,trafficServers:labels,serversDetail:servers.map(server=>({name:server.name?.trim()||"",host:server.host.trim(),site:server.site?.trim()||"",country:server.country?.trim()||"",type:server.type??""}))}
return {servers:labels,greClients:labels,probeTargets,rttLossTargets:probeTargets,trafficServers:labels,serversDetail:servers.map(server=>({name:server.name?.trim()||"",host:server.host.trim(),site:server.site?.trim()||"",country:server.country?.trim()||"",type:server.type??""})),...(config?{targetIssues:collectAlertTargetIssues(config)}:{})}
}
+11 -2
View File
@@ -15,6 +15,9 @@ import {
alertTelegramSettings,
servers,
uptimeProbes,
uptimeSettings,
serversApiPingSettings,
trafficSettings,
} from "../db/schema.js"
export type ApiAlertGroup = {
@@ -495,6 +498,8 @@ export async function listMergedHistory(persistLimit = 40, liveCap = 25,context:
export async function getAlertsMeta(context:AlertsReadContext=db) {
const srvRows = await context
.select({
id: servers.id,
enabled: servers.enabled,
name: servers.name,
host: servers.host,
site: servers.site,
@@ -502,8 +507,12 @@ export async function getAlertsMeta(context:AlertsReadContext=db) {
type: servers.type,
})
.from(servers)
const probes=await context.select({name:uptimeProbes.name,target:uptimeProbes.target}).from(uptimeProbes)
return buildAlertsMeta(srvRows,probes)
const probes=await context.select({id:uptimeProbes.id,name:uptimeProbes.name,target:uptimeProbes.target,enabled:uptimeProbes.enabled,
srcServerId:uptimeProbes.srcServerId,srcInterface:uptimeProbes.srcInterface,intervalSec:uptimeProbes.intervalSec}).from(uptimeProbes)
const uptime=(await context.select().from(uptimeSettings).where(eq(uptimeSettings.id,1)).limit(1))[0]
const rest=(await context.select().from(serversApiPingSettings).where(eq(serversApiPingSettings.id,1)).limit(1))[0]
const traffic=(await context.select().from(trafficSettings).where(eq(trafficSettings.id,1)).limit(1))[0]
return buildAlertsMeta(srvRows,probes,{servers:srvRows,probes,uptime,rest,traffic})
}
export async function getAlertsOverview(){
+41
View File
@@ -0,0 +1,41 @@
import Link from "next/link"
import { TriangleAlertIcon } from "lucide-react"
import type { AlertTargetIssue } from "@mmapp/contracts/alerts"
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
const kinds = { server: "Доступность сервера", traffic: "Трафик", probe: "Ping-проба" }
const reasons = {
ambiguous: "Несколько включённых объектов имеют эту подпись. События не подтверждаются.",
"disabled-object": "Объект или его сервер выключен. Опрос не выполняется.",
"disabled-source": "Сбор данных этого типа выключен.",
}
export function AlertsTargetIssues({ issues }: { issues: AlertTargetIssue[] | undefined }) {
if (!issues?.length) return null
const grouped = new Map<string, { target: string; reason: AlertTargetIssue["reason"]; kinds: string[] }>()
for (const issue of issues) {
const key = `${issue.reason}\0${issue.target}`
const group = grouped.get(key) ?? { target: issue.target, reason: issue.reason, kinds: [] }
if (!group.kinds.includes(kinds[issue.kind])) group.kinds.push(kinds[issue.kind])
grouped.set(key, group)
}
return <Alert role="status" variant="warning" aria-label="Проблемы целей оповещений">
<TriangleAlertIcon />
<AlertTitle>Некоторые цели оповещений требуют проверки</AlertTitle>
<AlertDescription>
<details open={grouped.size <= 5}>
<summary className="cursor-pointer">Проверить цели: {grouped.size}</summary>
<ul className="mt-2 space-y-2">
{[...grouped].map(([key, group]) => <li key={key} className="[overflow-wrap:anywhere]">
<strong>{group.target}</strong> · {group.kinds.join(" / ")} — {reasons[group.reason]}
</li>)}
</ul>
</details>
{issues.some(i => i.reason === "ambiguous") && <p>Задайте разные имена объектов, затем обновите цели существующих правил.</p>}
<p className="mt-2 flex flex-wrap gap-x-4 gap-y-1">
<Link className="underline underline-offset-4" href="/servers">Проверить серверы</Link>
{issues.some(i => i.kind === "probe") && <Link className="underline underline-offset-4" href="/uptime">Проверить пробы</Link>}
{issues.some(i => i.reason === "disabled-source") && <Link className="underline underline-offset-4" href="/data-collection">Проверить сбор данных</Link>}
</p>
</AlertDescription>
</Alert>
}
+24 -2
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
@@ -696,7 +696,7 @@ flowchart LR
## 12.5.77 — основной блок IPsec (2026-10-09)
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
@@ -1908,3 +1908,25 @@ Readonly production PG builder подтвердил1 группу одинако
Протоколы: [SQL16/cleanup](ux-ui/acceptance/collector-sources-sql-2026-10-12.json), [source433](ux-ui/acceptance/collector-sources-source-2026-10-12.json), [compiled1008](ux-ui/acceptance/collector-sources-compiled-2026-10-12.json), [readonly production](ux-ui/acceptance/collector-sources-production-2026-10-12.json), [runtime](ux-ui/acceptance/collector-sources-runtime-2026-10-12.json), [API/UI](ux-ui/acceptance/collector-sources-deployed-2026-10-12.json).
Следующий171 — понятная диагностика неоднозначных/отключённых целей оповещений: silent suppression безопасен для engine, но пользователь должен понимать причину отсутствия события. Миграция name-based rules на stable ID, scale/multi-instance/physical WAN/внешние Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
## 12.5.171 — понятные причины недоступности целей оповещений (2026-10-12)
При conservative suppression170 пользователь должен понимать, почему событие не подтверждается. Добавлен чистый collectAlertTargetIssues и optional public meta.targetIssues: kind server/traffic/probe, подпись и причина ambiguous/disabled-object/disabled-source. Диагностика строится из текущих настроек внутри того же repeatable read/read only transaction, что основной overview; getters defaults не создают. Учитываются dedicated source switches, enabled REST как разрешённый availability source по конфигурации, enabled устройства/пробы, duplicate labels и fallback имени на host. Это диагностика конфигурации; она не заявляет свежесть измерений, online устройств или фактическую доставку Telegram. Поля credentials не выбираются и не публикуются. Engine правила, target representation и права не изменены.
На /alerts добавлен существующий ReUI/shadcn Alert warning в base-nova/Frame композиции без изменения vendor files/theme. Строки доступности и трафика одной цели объединены; причина и дальнейшее действие читаются вместе. Длинный список раскрывается native details/summary клавиатурой, большой список изначально свёрнут; длинные подписи переносятся на телефоне. Ссылки ведут к серверам, пробам и сбору данных. При устранении причины warning исчезает после подтверждённого обновления; legacy API без optional поля не подменяется догадкой. Имена двух CHR и targets действующих правил автоматически не переписывались.
| Эталон | Доработка | Проверка |
| --- | --- | --- |
| Существующие Alert и Frame проекта | Подпись → причина → понятный переход → обновление | [ПК1440](ux-ui/screens/alerts-target-issues-1440-2026-10-12.png), [телефон320](ux-ui/screens/alerts-target-issues-320-2026-10-12.png) |
Локально прошли12 скриптов test:alerts-telegram (unit/transaction/config/queue/HTTP/JWT), application-source TypeScript, backend build и ESLint. Полный root tsc сначала захватил ignored tmp с давними test/build artifacts и выдал их ошибки; source check с исключением tmp/backend/packages прошёл, contracts/backend проверены собственной сборкой. Root tsconfig не менялся.42 unique E2E:22 readers/targets/pickers/rules/new diagnostics за44.7s;19 history/outbox/Telegram за30.6s;1 новый keyboard/long-label mobile за6.2s. Реальная отправка Telegram отсутствовала: HTTP пути подменены тестовыми receipts. Последняя проверка хронологии старым check-progress168 ожидала прежний header168; сам сохранённый historical slice имел12 dated blocks/35 существующих evidence links. Это устаревшее ожидание helper, не удаление журнала; актуальная сверка обновлена отдельно.
Owned Node22/PostgreSQL18.6 profile passed19540ms:16 source SQL cases плюс проверки actual getAlertsMeta (known empty, disabled-source reasons, ambiguous server/traffic kinds),8 GRE/BGP SQL rows и actual stability;PG unavailable skips0. Временная own DB/ограниченная роль mm_accept_166_1791794042576 и test container удалены. Main fixtures/RouterOS writes/messages0. Обе production сборки прошли; source434 backend+518 frontend inputs и1012 emitted backend/contracts files совпали SHA256 (text CRLF→LF, binary unchanged). Полную UI1260/context221/backend24 regression без новой причины не повторяли.
Развёртывание staged: frontend ux-targets171/version1.0.0-test.targets171 сначала принял реальный прежний API ux-sources170 без targetIssues; затем обновлён backend ux-targets171. На старом cached frontend строгий прежний DTO не знает additive поле: после обновления backend следует перезагрузить страницу для получения совместимого frontend. Менялись только image tags в existing.env; rollback tags сохранены отдельно без копирования secret env. Backend/PG healthy; frontend running/HTTP/browser подтверждён, отдельного healthcheck нет. Запущенные imageIds совпадают с проверенными образами; после cleanup3 основных running containers.
2 real UI1440/320 показали настоящий CHR warning:1 сгруппированная подпись,2 affected kinds, ссылка /servers, overflow/pageerrors/API writes0. Scoped axe нового блока WCAG2A/AA violations0, full WCAG certification не заявляется. Снимки просмотрены; исходные пользовательские GRE/VXLAN images сохранены без staging/revert. Настройки/имена/пароли CHR и исходные правила не менялись.
Протоколы: [SQL/cleanup](ux-ui/acceptance/alert-target-issues-sql-2026-10-12.json), [source](ux-ui/acceptance/alert-target-issues-source-2026-10-12.json), [compiled](ux-ui/acceptance/alert-target-issues-compiled-2026-10-12.json), [interop](ux-ui/acceptance/alert-target-issues-interop-2026-10-12.json), [runtime](ux-ui/acceptance/alert-target-issues-runtime-2026-10-12.json), [live UI](ux-ui/acceptance/alert-target-issues-deployed-2026-10-12.json).
Следующий172 — отдельная обнаруженная ветка: синтезированный monitoring history в alerts-service читает raw latest samples, пока не применяет enabled/freshness/current-error guards170 и требует проверки реального PG timestamp→public string на positive sample. Source finding не является уже выполненным исправлением или доказанной runtime ошибкой. Stable-ID target migration, отсутствующие цели удалённых правил, physical WAN/scale/multi-instance и реальные Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
+3 -3
View File
@@ -1,12 +1,12 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-12. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
## Актуальная точка продолжения — 2026-10-12
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
@@ -371,6 +371,6 @@ flowchart LR
## 12.5.77 — основной блок IPsec (2026-10-09)
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
+23 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -38,6 +38,28 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–168 и ограничения ниже. Runtime backend ux-month149/frontend ux-sessions150. |
## 12.5.171 — понятные причины недоступности целей оповещений (2026-10-12)
При conservative suppression170 пользователь должен понимать, почему событие не подтверждается. Добавлен чистый collectAlertTargetIssues и optional public meta.targetIssues: kind server/traffic/probe, подпись и причина ambiguous/disabled-object/disabled-source. Диагностика строится из текущих настроек внутри того же repeatable read/read only transaction, что основной overview; getters defaults не создают. Учитываются dedicated source switches, enabled REST как разрешённый availability source по конфигурации, enabled устройства/пробы, duplicate labels и fallback имени на host. Это диагностика конфигурации; она не заявляет свежесть измерений, online устройств или фактическую доставку Telegram. Поля credentials не выбираются и не публикуются. Engine правила, target representation и права не изменены.
На /alerts добавлен существующий ReUI/shadcn Alert warning в base-nova/Frame композиции без изменения vendor files/theme. Строки доступности и трафика одной цели объединены; причина и дальнейшее действие читаются вместе. Длинный список раскрывается native details/summary клавиатурой, большой список изначально свёрнут; длинные подписи переносятся на телефоне. Ссылки ведут к серверам, пробам и сбору данных. При устранении причины warning исчезает после подтверждённого обновления; legacy API без optional поля не подменяется догадкой. Имена двух CHR и targets действующих правил автоматически не переписывались.
| Эталон | Доработка | Проверка |
| --- | --- | --- |
| Существующие Alert и Frame проекта | Подпись → причина → понятный переход → обновление | [ПК1440](ux-ui/screens/alerts-target-issues-1440-2026-10-12.png), [телефон320](ux-ui/screens/alerts-target-issues-320-2026-10-12.png) |
Локально прошли12 скриптов test:alerts-telegram (unit/transaction/config/queue/HTTP/JWT), application-source TypeScript, backend build и ESLint. Полный root tsc сначала захватил ignored tmp с давними test/build artifacts и выдал их ошибки; source check с исключением tmp/backend/packages прошёл, contracts/backend проверены собственной сборкой. Root tsconfig не менялся.42 unique E2E:22 readers/targets/pickers/rules/new diagnostics за44.7s;19 history/outbox/Telegram за30.6s;1 новый keyboard/long-label mobile за6.2s. Реальная отправка Telegram отсутствовала: HTTP пути подменены тестовыми receipts. Последняя проверка хронологии старым check-progress168 ожидала прежний header168; сам сохранённый historical slice имел12 dated blocks/35 существующих evidence links. Это устаревшее ожидание helper, не удаление журнала; актуальная сверка обновлена отдельно.
Owned Node22/PostgreSQL18.6 profile passed19540ms:16 source SQL cases плюс проверки actual getAlertsMeta (known empty, disabled-source reasons, ambiguous server/traffic kinds),8 GRE/BGP SQL rows и actual stability;PG unavailable skips0. Временная own DB/ограниченная роль mm_accept_166_1791794042576 и test container удалены. Main fixtures/RouterOS writes/messages0. Обе production сборки прошли; source434 backend+518 frontend inputs и1012 emitted backend/contracts files совпали SHA256 (text CRLF→LF, binary unchanged). Полную UI1260/context221/backend24 regression без новой причины не повторяли.
Развёртывание staged: frontend ux-targets171/version1.0.0-test.targets171 сначала принял реальный прежний API ux-sources170 без targetIssues; затем обновлён backend ux-targets171. На старом cached frontend строгий прежний DTO не знает additive поле: после обновления backend следует перезагрузить страницу для получения совместимого frontend. Менялись только image tags в existing.env; rollback tags сохранены отдельно без копирования secret env. Backend/PG healthy; frontend running/HTTP/browser подтверждён, отдельного healthcheck нет. Запущенные imageIds совпадают с проверенными образами; после cleanup3 основных running containers.
2 real UI1440/320 показали настоящий CHR warning:1 сгруппированная подпись,2 affected kinds, ссылка /servers, overflow/pageerrors/API writes0. Scoped axe нового блока WCAG2A/AA violations0, full WCAG certification не заявляется. Снимки просмотрены; исходные пользовательские GRE/VXLAN images сохранены без staging/revert. Настройки/имена/пароли CHR и исходные правила не менялись.
Протоколы: [SQL/cleanup](ux-ui/acceptance/alert-target-issues-sql-2026-10-12.json), [source](ux-ui/acceptance/alert-target-issues-source-2026-10-12.json), [compiled](ux-ui/acceptance/alert-target-issues-compiled-2026-10-12.json), [interop](ux-ui/acceptance/alert-target-issues-interop-2026-10-12.json), [runtime](ux-ui/acceptance/alert-target-issues-runtime-2026-10-12.json), [live UI](ux-ui/acceptance/alert-target-issues-deployed-2026-10-12.json).
Следующий172 — отдельная обнаруженная ветка: синтезированный monitoring history в alerts-service читает raw latest samples, пока не применяет enabled/freshness/current-error guards170 и требует проверки реального PG timestamp→public string на positive sample. Source finding не является уже выполненным исправлением или доказанной runtime ошибкой. Stable-ID target migration, отсутствующие цели удалённых правил, physical WAN/scale/multi-instance и реальные Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
## 12.5.170 — достоверность серверных, пробных и трафиковых сигналов (2026-10-12)
Завершён оставшийся объём169 в одном coherent backend блоке. Новый collector-source-signals отделяет чтение от чистого построения фактов. Builder читает текущие настройки и identity без seeding getters и без credentials: resourcesEnabled/pingEnabled имеют тот же fallback на legacy enabled, что scheduler; REST/traffic используют собственные enabled. Выбираются последние завершённые ok/error runs, включая NULL/error JSON, вместо only-ok history. Текущие server/traffic keys берутся только из newest run: empty/error/malformed/unknown не восстанавливают старую цель. Частичная ошибочная коллекция сохраняет известные строки других устройств. ServerID/name/host/enabled/duplicate guards169 сохранены; пустое имя может использовать host, как collector. producer sample и completion проверяются по3 effective periods, future tolerance1 period; минимумы5s resources/traffic и10s REST повторяют scheduler. Resource неизвестное значение не становится offline; Traffic требует ok=true и конечные неотрицательные RX/TX, отсутствующее/ошибка не становится0. Подтверждённый нулевой трафик остаётся0.
+25 -3
View File
@@ -1,8 +1,8 @@
# Оставшаяся внешняя приёмка — 2026-10-12
Это актуальная матрица после 12.5.170. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Это актуальная матрица после 12.5.171. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-sources170`, frontend `ux-bfd158`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-targets171`, frontend `ux-targets171`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
| Раздел | Подтверждено | Осталось / условие завершения |
| --- | --- | --- |
@@ -18,7 +18,7 @@
| Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята |
| Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты |
| Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, native IPv6 ULA flow pipeline passed145 (IPv4 transport), public IPv6 WAN/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения |
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171 pending; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate passed141; IPv6 special ranges/cache passed142; unknown RIPE coordinates passed143; IPv6 synthetic UDP/worker/SQL passed144; native CHR ULA/IPFIX/API/UI passed145; ICMPv6/IPv6 endpoint labels passed146; HTTP DTO validation passed147; scoped flow catalog/serverId guards passed148; strict monthly calendar passed149; Sessions labels passed150; application GRE static IPv6 lifecycle151 passed; WireGuard static IPv6 lifecycle152 и disabled SQL/API gates passed; OSPF/BFD boundary/warning153 passed; OSPFv3 routes154 accepted in recorded bounds; BGP IPv6 basic155 passed; bigint ID/SQL guards156 passed; общая UI1260/context221/backend24 regression157 passed; IPv6 BFD correlation158 profile/deployed passed; native OSPFv3/BFDv6 boundary/recovery159 passed; BGP IPv6/BFD withdrawal/recovery160 passed; selected BGP/alert unknown guards161 replay/profiles/deployed passed (SQL stability skipped); private4-byte ASN162 native/API/UI passed; duplicate BGP alert keys163 replay/profiles/deployed passed; nullable selected BGP164 replay/profiles/API/UI passed; collector165 parallel/current guards/read-only PG passed; positive partial/error SQL166/full24 with PG passed (optional network SQL not repeated); GRE/BGP freshness167/SQL8 passed; other source audit168 выявил identity/disabled/freshness gaps; локальный server/traffic identity guard169 passed без SQL/deploy, source/per-probe/freshness170/ownSQL16/compiled1008/deployed passed; source diagnostics171/42E2E/ownSQL metadata/live2 passed; synthesized monitoring history guards172 pending; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию |
Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение.
@@ -823,3 +823,25 @@ Readonly production PG builder подтвердил1 группу одинако
Протоколы: [SQL16/cleanup](ux-ui/acceptance/collector-sources-sql-2026-10-12.json), [source433](ux-ui/acceptance/collector-sources-source-2026-10-12.json), [compiled1008](ux-ui/acceptance/collector-sources-compiled-2026-10-12.json), [readonly production](ux-ui/acceptance/collector-sources-production-2026-10-12.json), [runtime](ux-ui/acceptance/collector-sources-runtime-2026-10-12.json), [API/UI](ux-ui/acceptance/collector-sources-deployed-2026-10-12.json).
Следующий171 — понятная диагностика неоднозначных/отключённых целей оповещений: silent suppression безопасен для engine, но пользователь должен понимать причину отсутствия события. Миграция name-based rules на stable ID, scale/multi-instance/physical WAN/внешние Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
## 12.5.171 — понятные причины недоступности целей оповещений (2026-10-12)
При conservative suppression170 пользователь должен понимать, почему событие не подтверждается. Добавлен чистый collectAlertTargetIssues и optional public meta.targetIssues: kind server/traffic/probe, подпись и причина ambiguous/disabled-object/disabled-source. Диагностика строится из текущих настроек внутри того же repeatable read/read only transaction, что основной overview; getters defaults не создают. Учитываются dedicated source switches, enabled REST как разрешённый availability source по конфигурации, enabled устройства/пробы, duplicate labels и fallback имени на host. Это диагностика конфигурации; она не заявляет свежесть измерений, online устройств или фактическую доставку Telegram. Поля credentials не выбираются и не публикуются. Engine правила, target representation и права не изменены.
На /alerts добавлен существующий ReUI/shadcn Alert warning в base-nova/Frame композиции без изменения vendor files/theme. Строки доступности и трафика одной цели объединены; причина и дальнейшее действие читаются вместе. Длинный список раскрывается native details/summary клавиатурой, большой список изначально свёрнут; длинные подписи переносятся на телефоне. Ссылки ведут к серверам, пробам и сбору данных. При устранении причины warning исчезает после подтверждённого обновления; legacy API без optional поля не подменяется догадкой. Имена двух CHR и targets действующих правил автоматически не переписывались.
| Эталон | Доработка | Проверка |
| --- | --- | --- |
| Существующие Alert и Frame проекта | Подпись → причина → понятный переход → обновление | [ПК1440](ux-ui/screens/alerts-target-issues-1440-2026-10-12.png), [телефон320](ux-ui/screens/alerts-target-issues-320-2026-10-12.png) |
Локально прошли12 скриптов test:alerts-telegram (unit/transaction/config/queue/HTTP/JWT), application-source TypeScript, backend build и ESLint. Полный root tsc сначала захватил ignored tmp с давними test/build artifacts и выдал их ошибки; source check с исключением tmp/backend/packages прошёл, contracts/backend проверены собственной сборкой. Root tsconfig не менялся.42 unique E2E:22 readers/targets/pickers/rules/new diagnostics за44.7s;19 history/outbox/Telegram за30.6s;1 новый keyboard/long-label mobile за6.2s. Реальная отправка Telegram отсутствовала: HTTP пути подменены тестовыми receipts. Последняя проверка хронологии старым check-progress168 ожидала прежний header168; сам сохранённый historical slice имел12 dated blocks/35 существующих evidence links. Это устаревшее ожидание helper, не удаление журнала; актуальная сверка обновлена отдельно.
Owned Node22/PostgreSQL18.6 profile passed19540ms:16 source SQL cases плюс проверки actual getAlertsMeta (known empty, disabled-source reasons, ambiguous server/traffic kinds),8 GRE/BGP SQL rows и actual stability;PG unavailable skips0. Временная own DB/ограниченная роль mm_accept_166_1791794042576 и test container удалены. Main fixtures/RouterOS writes/messages0. Обе production сборки прошли; source434 backend+518 frontend inputs и1012 emitted backend/contracts files совпали SHA256 (text CRLF→LF, binary unchanged). Полную UI1260/context221/backend24 regression без новой причины не повторяли.
Развёртывание staged: frontend ux-targets171/version1.0.0-test.targets171 сначала принял реальный прежний API ux-sources170 без targetIssues; затем обновлён backend ux-targets171. На старом cached frontend строгий прежний DTO не знает additive поле: после обновления backend следует перезагрузить страницу для получения совместимого frontend. Менялись только image tags в existing.env; rollback tags сохранены отдельно без копирования secret env. Backend/PG healthy; frontend running/HTTP/browser подтверждён, отдельного healthcheck нет. Запущенные imageIds совпадают с проверенными образами; после cleanup3 основных running containers.
2 real UI1440/320 показали настоящий CHR warning:1 сгруппированная подпись,2 affected kinds, ссылка /servers, overflow/pageerrors/API writes0. Scoped axe нового блока WCAG2A/AA violations0, full WCAG certification не заявляется. Снимки просмотрены; исходные пользовательские GRE/VXLAN images сохранены без staging/revert. Настройки/имена/пароли CHR и исходные правила не менялись.
Протоколы: [SQL/cleanup](ux-ui/acceptance/alert-target-issues-sql-2026-10-12.json), [source](ux-ui/acceptance/alert-target-issues-source-2026-10-12.json), [compiled](ux-ui/acceptance/alert-target-issues-compiled-2026-10-12.json), [interop](ux-ui/acceptance/alert-target-issues-interop-2026-10-12.json), [runtime](ux-ui/acceptance/alert-target-issues-runtime-2026-10-12.json), [live UI](ux-ui/acceptance/alert-target-issues-deployed-2026-10-12.json).
Следующий172 — отдельная обнаруженная ветка: синтезированный monitoring history в alerts-service читает raw latest samples, пока не применяет enabled/freshness/current-error guards170 и требует проверки реального PG timestamp→public string на positive sample. Source finding не является уже выполненным исправлением или доказанной runtime ошибкой. Stable-ID target migration, отсутствующие цели удалённых правил, physical WAN/scale/multi-instance и реальные Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
+23 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-12: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Актуальное продолжение 2026-10-12: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -3213,3 +3213,25 @@ Readonly production PG builder подтвердил1 группу одинако
Протоколы: [SQL16/cleanup](ux-ui/acceptance/collector-sources-sql-2026-10-12.json), [source433](ux-ui/acceptance/collector-sources-source-2026-10-12.json), [compiled1008](ux-ui/acceptance/collector-sources-compiled-2026-10-12.json), [readonly production](ux-ui/acceptance/collector-sources-production-2026-10-12.json), [runtime](ux-ui/acceptance/collector-sources-runtime-2026-10-12.json), [API/UI](ux-ui/acceptance/collector-sources-deployed-2026-10-12.json).
Следующий171 — понятная диагностика неоднозначных/отключённых целей оповещений: silent suppression безопасен для engine, но пользователь должен понимать причину отсутствия события. Миграция name-based rules на stable ID, scale/multi-instance/physical WAN/внешние Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
## 12.5.171 — понятные причины недоступности целей оповещений (2026-10-12)
При conservative suppression170 пользователь должен понимать, почему событие не подтверждается. Добавлен чистый collectAlertTargetIssues и optional public meta.targetIssues: kind server/traffic/probe, подпись и причина ambiguous/disabled-object/disabled-source. Диагностика строится из текущих настроек внутри того же repeatable read/read only transaction, что основной overview; getters defaults не создают. Учитываются dedicated source switches, enabled REST как разрешённый availability source по конфигурации, enabled устройства/пробы, duplicate labels и fallback имени на host. Это диагностика конфигурации; она не заявляет свежесть измерений, online устройств или фактическую доставку Telegram. Поля credentials не выбираются и не публикуются. Engine правила, target representation и права не изменены.
На /alerts добавлен существующий ReUI/shadcn Alert warning в base-nova/Frame композиции без изменения vendor files/theme. Строки доступности и трафика одной цели объединены; причина и дальнейшее действие читаются вместе. Длинный список раскрывается native details/summary клавиатурой, большой список изначально свёрнут; длинные подписи переносятся на телефоне. Ссылки ведут к серверам, пробам и сбору данных. При устранении причины warning исчезает после подтверждённого обновления; legacy API без optional поля не подменяется догадкой. Имена двух CHR и targets действующих правил автоматически не переписывались.
| Эталон | Доработка | Проверка |
| --- | --- | --- |
| Существующие Alert и Frame проекта | Подпись → причина → понятный переход → обновление | [ПК1440](ux-ui/screens/alerts-target-issues-1440-2026-10-12.png), [телефон320](ux-ui/screens/alerts-target-issues-320-2026-10-12.png) |
Локально прошли12 скриптов test:alerts-telegram (unit/transaction/config/queue/HTTP/JWT), application-source TypeScript, backend build и ESLint. Полный root tsc сначала захватил ignored tmp с давними test/build artifacts и выдал их ошибки; source check с исключением tmp/backend/packages прошёл, contracts/backend проверены собственной сборкой. Root tsconfig не менялся.42 unique E2E:22 readers/targets/pickers/rules/new diagnostics за44.7s;19 history/outbox/Telegram за30.6s;1 новый keyboard/long-label mobile за6.2s. Реальная отправка Telegram отсутствовала: HTTP пути подменены тестовыми receipts. Последняя проверка хронологии старым check-progress168 ожидала прежний header168; сам сохранённый historical slice имел12 dated blocks/35 существующих evidence links. Это устаревшее ожидание helper, не удаление журнала; актуальная сверка обновлена отдельно.
Owned Node22/PostgreSQL18.6 profile passed19540ms:16 source SQL cases плюс проверки actual getAlertsMeta (known empty, disabled-source reasons, ambiguous server/traffic kinds),8 GRE/BGP SQL rows и actual stability;PG unavailable skips0. Временная own DB/ограниченная роль mm_accept_166_1791794042576 и test container удалены. Main fixtures/RouterOS writes/messages0. Обе production сборки прошли; source434 backend+518 frontend inputs и1012 emitted backend/contracts files совпали SHA256 (text CRLF→LF, binary unchanged). Полную UI1260/context221/backend24 regression без новой причины не повторяли.
Развёртывание staged: frontend ux-targets171/version1.0.0-test.targets171 сначала принял реальный прежний API ux-sources170 без targetIssues; затем обновлён backend ux-targets171. На старом cached frontend строгий прежний DTO не знает additive поле: после обновления backend следует перезагрузить страницу для получения совместимого frontend. Менялись только image tags в existing.env; rollback tags сохранены отдельно без копирования secret env. Backend/PG healthy; frontend running/HTTP/browser подтверждён, отдельного healthcheck нет. Запущенные imageIds совпадают с проверенными образами; после cleanup3 основных running containers.
2 real UI1440/320 показали настоящий CHR warning:1 сгруппированная подпись,2 affected kinds, ссылка /servers, overflow/pageerrors/API writes0. Scoped axe нового блока WCAG2A/AA violations0, full WCAG certification не заявляется. Снимки просмотрены; исходные пользовательские GRE/VXLAN images сохранены без staging/revert. Настройки/имена/пароли CHR и исходные правила не менялись.
Протоколы: [SQL/cleanup](ux-ui/acceptance/alert-target-issues-sql-2026-10-12.json), [source](ux-ui/acceptance/alert-target-issues-source-2026-10-12.json), [compiled](ux-ui/acceptance/alert-target-issues-compiled-2026-10-12.json), [interop](ux-ui/acceptance/alert-target-issues-interop-2026-10-12.json), [runtime](ux-ui/acceptance/alert-target-issues-runtime-2026-10-12.json), [live UI](ux-ui/acceptance/alert-target-issues-deployed-2026-10-12.json).
Следующий172 — отдельная обнаруженная ветка: синтезированный monitoring history в alerts-service читает raw latest samples, пока не применяет enabled/freshness/current-error guards170 и требует проверки реального PG timestamp→public string на positive sample. Source finding не является уже выполненным исправлением или доказанной runtime ошибкой. Stable-ID target migration, отсутствующие цели удалённых правил, physical WAN/scale/multi-instance и реальные Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
+24 -2
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10
Сервер161.104.46.222: завершён12.5.170 — source enabled/current/unknown/freshness/per-probe guards, включая169. Own PG16 source cases +GRE/BGP8/stability passed; temporary DB/role/container removed. Source433/compiled1008 parity; backend ux-sources170 healthy, frontend ux-bfd158 прежний;6 real UI/4 API reads passed, оба CHR доступны. Одноимённые server/traffic targets подавлены, имена/пароли не менялись. Следующий171 — показать неоднозначные/отключённые цели оповещений в UI и объяснить отсутствие подтверждённого сигнала;12.5 открыт.
Сервер161.104.46.222: завершён12.5.171 — объяснение неоднозначных/отключённых целей в оповещениях, ReUI Alert без vendor changes. Backend profile/42E2E/ownPG16+metadata/compiled1012/source434+518 passed;2 live warning screens/scoped axe passed. Frontend сначала проверен с old API, затем оба ux-targets171 развёрнуты;backend/PG healthy. DB/role/container удалены. Следующий172 — исправить синтезированный журнал monitoring: raw sample readers ещё игнорируют disabled/freshness и Date needs positive SQL verification;12.5 открыт.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -14,7 +14,7 @@
| PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed |
| Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data |
| Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска |
| Образы приложения | mmapp-test/backend:ux-sources170 и mmapp-test/frontend:ux-bfd158; запущены |
| Образы приложения | mmapp-test/backend:ux-targets171 и mmapp-test/frontend:ux-targets171; запущены |
Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example).
@@ -1639,3 +1639,25 @@ Readonly production PG builder подтвердил1 группу одинако
Протоколы: [SQL16/cleanup](ux-ui/acceptance/collector-sources-sql-2026-10-12.json), [source433](ux-ui/acceptance/collector-sources-source-2026-10-12.json), [compiled1008](ux-ui/acceptance/collector-sources-compiled-2026-10-12.json), [readonly production](ux-ui/acceptance/collector-sources-production-2026-10-12.json), [runtime](ux-ui/acceptance/collector-sources-runtime-2026-10-12.json), [API/UI](ux-ui/acceptance/collector-sources-deployed-2026-10-12.json).
Следующий171 — понятная диагностика неоднозначных/отключённых целей оповещений: silent suppression безопасен для engine, но пользователь должен понимать причину отсутствия события. Миграция name-based rules на stable ID, scale/multi-instance/physical WAN/внешние Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
## 12.5.171 — понятные причины недоступности целей оповещений (2026-10-12)
При conservative suppression170 пользователь должен понимать, почему событие не подтверждается. Добавлен чистый collectAlertTargetIssues и optional public meta.targetIssues: kind server/traffic/probe, подпись и причина ambiguous/disabled-object/disabled-source. Диагностика строится из текущих настроек внутри того же repeatable read/read only transaction, что основной overview; getters defaults не создают. Учитываются dedicated source switches, enabled REST как разрешённый availability source по конфигурации, enabled устройства/пробы, duplicate labels и fallback имени на host. Это диагностика конфигурации; она не заявляет свежесть измерений, online устройств или фактическую доставку Telegram. Поля credentials не выбираются и не публикуются. Engine правила, target representation и права не изменены.
На /alerts добавлен существующий ReUI/shadcn Alert warning в base-nova/Frame композиции без изменения vendor files/theme. Строки доступности и трафика одной цели объединены; причина и дальнейшее действие читаются вместе. Длинный список раскрывается native details/summary клавиатурой, большой список изначально свёрнут; длинные подписи переносятся на телефоне. Ссылки ведут к серверам, пробам и сбору данных. При устранении причины warning исчезает после подтверждённого обновления; legacy API без optional поля не подменяется догадкой. Имена двух CHR и targets действующих правил автоматически не переписывались.
| Эталон | Доработка | Проверка |
| --- | --- | --- |
| Существующие Alert и Frame проекта | Подпись → причина → понятный переход → обновление | [ПК1440](ux-ui/screens/alerts-target-issues-1440-2026-10-12.png), [телефон320](ux-ui/screens/alerts-target-issues-320-2026-10-12.png) |
Локально прошли12 скриптов test:alerts-telegram (unit/transaction/config/queue/HTTP/JWT), application-source TypeScript, backend build и ESLint. Полный root tsc сначала захватил ignored tmp с давними test/build artifacts и выдал их ошибки; source check с исключением tmp/backend/packages прошёл, contracts/backend проверены собственной сборкой. Root tsconfig не менялся.42 unique E2E:22 readers/targets/pickers/rules/new diagnostics за44.7s;19 history/outbox/Telegram за30.6s;1 новый keyboard/long-label mobile за6.2s. Реальная отправка Telegram отсутствовала: HTTP пути подменены тестовыми receipts. Последняя проверка хронологии старым check-progress168 ожидала прежний header168; сам сохранённый historical slice имел12 dated blocks/35 существующих evidence links. Это устаревшее ожидание helper, не удаление журнала; актуальная сверка обновлена отдельно.
Owned Node22/PostgreSQL18.6 profile passed19540ms:16 source SQL cases плюс проверки actual getAlertsMeta (known empty, disabled-source reasons, ambiguous server/traffic kinds),8 GRE/BGP SQL rows и actual stability;PG unavailable skips0. Временная own DB/ограниченная роль mm_accept_166_1791794042576 и test container удалены. Main fixtures/RouterOS writes/messages0. Обе production сборки прошли; source434 backend+518 frontend inputs и1012 emitted backend/contracts files совпали SHA256 (text CRLF→LF, binary unchanged). Полную UI1260/context221/backend24 regression без новой причины не повторяли.
Развёртывание staged: frontend ux-targets171/version1.0.0-test.targets171 сначала принял реальный прежний API ux-sources170 без targetIssues; затем обновлён backend ux-targets171. На старом cached frontend строгий прежний DTO не знает additive поле: после обновления backend следует перезагрузить страницу для получения совместимого frontend. Менялись только image tags в existing.env; rollback tags сохранены отдельно без копирования secret env. Backend/PG healthy; frontend running/HTTP/browser подтверждён, отдельного healthcheck нет. Запущенные imageIds совпадают с проверенными образами; после cleanup3 основных running containers.
2 real UI1440/320 показали настоящий CHR warning:1 сгруппированная подпись,2 affected kinds, ссылка /servers, overflow/pageerrors/API writes0. Scoped axe нового блока WCAG2A/AA violations0, full WCAG certification не заявляется. Снимки просмотрены; исходные пользовательские GRE/VXLAN images сохранены без staging/revert. Настройки/имена/пароли CHR и исходные правила не менялись.
Протоколы: [SQL/cleanup](ux-ui/acceptance/alert-target-issues-sql-2026-10-12.json), [source](ux-ui/acceptance/alert-target-issues-source-2026-10-12.json), [compiled](ux-ui/acceptance/alert-target-issues-compiled-2026-10-12.json), [interop](ux-ui/acceptance/alert-target-issues-interop-2026-10-12.json), [runtime](ux-ui/acceptance/alert-target-issues-runtime-2026-10-12.json), [live UI](ux-ui/acceptance/alert-target-issues-deployed-2026-10-12.json).
Следующий172 — отдельная обнаруженная ветка: синтезированный monitoring history в alerts-service читает raw latest samples, пока не применяет enabled/freshness/current-error guards170 и требует проверки реального PG timestamp→public string на positive sample. Source finding не является уже выполненным исправлением или доказанной runtime ошибкой. Stable-ID target migration, отсутствующие цели удалённых правил, physical WAN/scale/multi-instance и реальные Telegram/Portal/client OS остаются отдельными условиями12.5. Локальный коммит без push.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,39 @@
{
"date": "2026-10-12",
"block": "12.5.171",
"runtime": {
"backend": "ux-targets171",
"frontend": "ux-targets171"
},
"api200": true,
"ambiguousKindCount": 2,
"cases": [
{
"width": 1440,
"warningVisible": true,
"duplicateGrouped": true,
"serverLink": true,
"noOverflow": true,
"scopedAxeViolations": [],
"pageErrors": [],
"apiWrites": [],
"screenshot": "docs/ux-ui/screens/alerts-target-issues-1440-2026-10-12.png"
},
{
"width": 320,
"warningVisible": true,
"duplicateGrouped": true,
"serverLink": true,
"noOverflow": true,
"scopedAxeViolations": [],
"pageErrors": [],
"apiWrites": [],
"screenshot": "docs/ux-ui/screens/alerts-target-issues-320-2026-10-12.png"
}
],
"bounds": [
"Read-only real duplicate CHR metadata; no rename/native mutation/password change/message",
"Scoped axe of new warning is not full WCAG certification",
"Disabled configuration covered by owned SQL and API-path UI fixtures; live configuration preserved"
]
}
@@ -0,0 +1,9 @@
{
"date": "2026-10-12",
"block": "12.5.171",
"newFrontend": "ux-targets171",
"oldBackend": "ux-sources170",
"optionalDiagnosticMissing": true,
"readerAccepted": true,
"unknownDiagnosticNotInvented": true
}
@@ -0,0 +1,30 @@
{
"date": "2026-10-12",
"block": "12.5.171",
"services": {
"backend": {
"image": "mmapp-test/backend:ux-targets171",
"imageId": "sha256:754b7420ca35657fb566cec97cf37322320787363e1787bb4ce89aa0bfb256ed",
"matchedVerifiedImage": true,
"state": "running",
"health": "healthy"
},
"frontend": {
"image": "mmapp-test/frontend:ux-targets171",
"imageId": "sha256:050987e82c1ab5571f4c3dc8692a22f8ba703881f699c802f0168d90ab74652e",
"matchedVerifiedImage": true,
"state": "running",
"health": null
}
},
"rollbackTags": {
"MMAPP_IMAGE_TAG": "ux-bfd158",
"BACKEND_IMAGE_TAG": "ux-sources170"
},
"secretEnvCopied": false,
"runningContainers": [
"mmapp-test-backend",
"mmapp-test-frontend",
"mmapp-test-postgres"
]
}
@@ -0,0 +1,11 @@
{
"date": "2026-10-12",
"block": "12.5.171",
"matched": {
"backend": 434,
"frontend": 518
},
"different": [],
"missing": [],
"sourceBase": "db7105119d0c9a3b1dee254d8e3d6ac45c97bba0"
}
@@ -0,0 +1,23 @@
{
"date": "2026-10-12",
"block": "12.5.171",
"database": "mm_accept_166_1791794042576",
"scope": "ephemeral role and precreated owned database; no copied production credentials",
"tests": [
{
"command": "npm --prefix backend run test:alert-engine",
"passed": true,
"durationMs": 19540,
"ownedHistorySqlRows": 8,
"ownedSourceSqlCases": 16,
"sqlConfirmStabilityPassed": true,
"postgresUnavailableSkip": false
}
],
"scopedRoleVerified": true,
"passed": true,
"databaseRemoved": true,
"roleRemoved": true,
"testContainerRemoved": true,
"metadataSqlAssertionsPassed": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+5
View File
@@ -93,9 +93,14 @@ export const alertRuleResponseSchema = z.object({
confirmStabilitySec:z.number().int().min(0).max(86400).nullable(),recoveryMode:recoveryModeSchema,recoveryStabilitySec:z.number().int().min(0).max(86400).nullable(),chatId:z.string(),lastFiredAt:alertTimestampSchema.nullable(),
}).strict()
export const alertHistoryResponseSchema = z.object({id:z.string().min(1),ruleName:z.string(),severity:alertSeveritySchema,message:z.string(),sent:z.boolean(),firedAt:alertTimestampSchema,source:z.enum(["db","live"])}).strict()
export const alertTargetIssueSchema = z.object({
kind:z.enum(["server","traffic","probe"]),target:z.string().min(1),reason:z.enum(["ambiguous","disabled-object","disabled-source"]),
}).strict()
export type AlertTargetIssue=z.output<typeof alertTargetIssueSchema>
export const alertsMetaResponseSchema = z.object({
servers:z.array(z.string()),greClients:z.array(z.string()),probeTargets:z.array(z.string()),rttLossTargets:z.array(z.string()),trafficServers:z.array(z.string()),
serversDetail:z.array(z.object({name:z.string(),host:z.string(),site:z.string(),country:z.string(),type:z.string()}).strict()),
targetIssues:z.array(alertTargetIssueSchema).optional(),
}).strict()
const alertsConfigResponseShape = {groups:z.array(alertGroupSchema.strict()),rules:z.array(alertRuleResponseSchema)}
function validConfig(value:{groups:{id:string}[];rules:{id:string;groupId:string|null}[]}):boolean {
+52
View File
@@ -0,0 +1,52 @@
import {test,expect,type Page} from "@playwright/test"
import {emptyAlerts,emptyOutbox} from "./fixtures/alerts"
import type {AlertTargetIssue} from "@mmapp/contracts/alerts"
async function setup(page:Page,reply:()=>unknown){
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}))
await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname
if(path==="/api/auth/config")return r.fulfill({json:{required:false}})
if(path==="/api/alerts")return r.fulfill({json:reply()})
if(path==="/api/alerts/outbox")return r.fulfill({json:emptyOutbox})
if(path==="/api/servers"||path==="/api/bgp/sessions")return r.fulfill({json:[]})
if(path==="/api/filters/gre-tunnels")return r.fulfill({json:{tunnels:[]}})
return r.fulfill({status:503,json:{error:"outside diagnostic fixture"}})
})
await page.goto("/alerts")
}
for(const width of[1440,320])test(`Alerts targets: ambiguity is explained and grouped at ${width}`,async({page})=>{
await page.setViewportSize({width,height:1000})
const issues:AlertTargetIssue[]=[{kind:"server",target:"CHR",reason:"ambiguous"},{kind:"traffic",target:"CHR",reason:"ambiguous"}]
let current=issues
await setup(page,()=>({...emptyAlerts,meta:{...emptyAlerts.meta,targetIssues:current}}))
const warning=page.getByRole("status",{name:"Проблемы целей оповещений"})
await expect(warning).toBeVisible();await expect(warning.getByText("CHR",{exact:true})).toHaveCount(1)
await expect(warning).toContainText("Несколько включённых объектов")
await expect(warning).toContainText("обновите цели существующих правил")
await expect(warning.getByRole("link",{name:"Проверить серверы"})).toHaveAttribute("href","/servers")
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true)
current=[];await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(warning).toHaveCount(0)
})
test("Alerts targets: disabled source and probe explain the next action",async({page})=>{
await setup(page,()=>({...emptyAlerts,meta:{...emptyAlerts.meta,targetIssues:[{kind:"traffic",target:"router",reason:"disabled-source"},{kind:"probe",target:"probe → target",reason:"disabled-object"}]}}))
const warning=page.getByRole("status",{name:"Проблемы целей оповещений"})
await expect(warning).toContainText("Сбор данных этого типа выключен")
await expect(warning).toContainText("Объект или его сервер выключен")
await expect(warning.getByRole("link",{name:"Проверить сбор данных"})).toHaveAttribute("href","/data-collection")
await expect(warning.getByRole("link",{name:"Проверить пробы"})).toHaveAttribute("href","/uptime")
})
test("Alerts targets: older valid API does not invent diagnostics",async({page})=>{
await setup(page,()=>emptyAlerts)
await expect(page.getByText("Всего: 0",{exact:true})).toBeVisible()
await expect(page.getByRole("status",{name:"Проблемы целей оповещений"})).toHaveCount(0)
})
test("Alerts targets: many long labels remain compact and keyboard accessible on phone",async({page})=>{
await page.setViewportSize({width:320,height:1000})
const targets=Array.from({length:6},(_,index)=>`target-${index}-${"x".repeat(180)}`)
await setup(page,()=>({...emptyAlerts,meta:{...emptyAlerts.meta,targetIssues:targets.map(target=>({kind:"server",target,reason:"ambiguous"}))}}))
const warning=page.getByRole("status",{name:"Проблемы целей оповещений"})
await expect(warning.getByText(targets[0],{exact:true})).toBeHidden()
const summary=warning.locator("summary");await summary.focus();await page.keyboard.press("Enter")
await expect(warning.getByText(targets[0],{exact:true})).toBeVisible()
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true)
await page.keyboard.press("Enter");await expect(warning.getByText(targets[0],{exact:true})).toBeHidden()
})