fix(alerts): отличать неизвестные данные от пустого списка правил
This commit is contained in:
1 parent
9bc3a5db77
commit
3710fc6aab
5 files changed
+54
-29
No files matched your search
+35
-27
@@ -39,6 +39,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { Flag, countryName } from "@/components/flag"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {getAlerts} from "@/shared/api/alerts"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -2351,18 +2352,25 @@ function AddRuleSheet({
|
||||
|
||||
type SeverityFilter = AlertSeverity | "all"
|
||||
|
||||
export default function AlertsPage() {
|
||||
export default function AlertsPage(){const {mode,backendUrl}=useDataSource();return <AlertsContent key={JSON.stringify([mode,backendUrl])}/>}
|
||||
|
||||
function AlertsContent() {
|
||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
|
||||
|
||||
const [rules, setRules] = useState<AlertRule[]>(INIT_RULES)
|
||||
const [groups, setGroups] = useState<AlertGroup[]>(INIT_GROUPS)
|
||||
const [history, setHistory] = useState<HistoryEntry[]>(INIT_HISTORY)
|
||||
const [tg, setTg] = useState<TelegramConfig>(INIT_TG)
|
||||
const [rules, setRules] = useState<AlertRule[]>(isLive?[]:INIT_RULES)
|
||||
const [groups, setGroups] = useState<AlertGroup[]>(isLive?[]:INIT_GROUPS)
|
||||
const [history, setHistory] = useState<HistoryEntry[]>(isLive?[]:INIT_HISTORY)
|
||||
const [tg, setTg] = useState<TelegramConfig>(isLive?{token:"",chatId:"",messageThreadId:"",connected:false}:INIT_TG)
|
||||
const [tokenConfigured, setTokenConfigured] = useState(false)
|
||||
const [meta, setMeta] = useState<AlertsMeta | null>(null)
|
||||
|
||||
const [known,setKnown]=useState(!isLive)
|
||||
const [receivedAt,setReceivedAt]=useState<string|null>(null)
|
||||
const pageActive=useRef(true),reader=useRef<AbortController|null>(null),revision=useRef(0)
|
||||
useEffect(()=>{pageActive.current=true;return()=>{pageActive.current=false;reader.current?.abort()}},[])
|
||||
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
const [pendingPresetReplaceId, setPendingPresetReplaceId] = useState<string | null>(null)
|
||||
const [sheetSnap, setSheetSnap] = useState<RuleFormSnapshot | null>(null)
|
||||
@@ -2407,6 +2415,7 @@ export default function AlertsPage() {
|
||||
}, [isLive, meta])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if(!pageActive.current)return
|
||||
if (!isLive) {
|
||||
setRules(INIT_RULES)
|
||||
setGroups(INIT_GROUPS)
|
||||
@@ -2417,21 +2426,15 @@ export default function AlertsPage() {
|
||||
setLoadError(null)
|
||||
return
|
||||
}
|
||||
reader.current?.abort()
|
||||
const controller=new AbortController();reader.current=controller
|
||||
const requestRevision=++revision.current
|
||||
setLoadBusy(true)
|
||||
setLoadError(null)
|
||||
try {
|
||||
const data = await apiFetch<{
|
||||
telegram: {
|
||||
chatId: string
|
||||
tokenConfigured: boolean
|
||||
connected: boolean
|
||||
messageThreadId: number | null
|
||||
}
|
||||
groups?: AlertsApiGroup[]
|
||||
rules: AlertsApiRule[]
|
||||
history: AlertsApiHistoryEntry[]
|
||||
meta: AlertsMeta
|
||||
}>("/api/alerts")
|
||||
const data = await getAlerts(backendUrl,{signal:controller.signal})
|
||||
if(!pageActive.current||requestRevision!==revision.current)return
|
||||
setKnown(true);setReceivedAt(new Date().toISOString())
|
||||
setTokenConfigured(data.telegram.tokenConfigured)
|
||||
setTg({
|
||||
token: "",
|
||||
@@ -2445,7 +2448,7 @@ export default function AlertsPage() {
|
||||
setHistory(mapApiHistory(data.history))
|
||||
let mergedMeta: AlertsMeta = data.meta
|
||||
try {
|
||||
const bgpRows = await apiFetch<BgpSessionListRow[]>("/api/bgp/sessions")
|
||||
const bgpRows = await apiFetch<BgpSessionListRow[]>("/api/bgp/sessions",{signal:controller.signal})
|
||||
const bgpPeers = uniqStrings(
|
||||
bgpRows.map((s) => `${String(s.serverName ?? "").trim()}: ${String(s.name ?? "").trim()}`.trim()),
|
||||
)
|
||||
@@ -2457,7 +2460,7 @@ export default function AlertsPage() {
|
||||
const [srvList, greRes] = await Promise.all([
|
||||
apiFetch<
|
||||
Array<{ id: number; name: string; site?: string; country?: string; host?: string }>
|
||||
>("/api/servers"),
|
||||
>("/api/servers",{signal:controller.signal}),
|
||||
apiFetch<{
|
||||
tunnels: Array<{
|
||||
id?: string
|
||||
@@ -2467,7 +2470,7 @@ export default function AlertsPage() {
|
||||
status?: string
|
||||
comment?: string
|
||||
}>
|
||||
}>("/api/filters/gre-tunnels"),
|
||||
}>("/api/filters/gre-tunnels",{signal:controller.signal}),
|
||||
])
|
||||
const sidToSrv = new Map(
|
||||
srvList.map((s) => [
|
||||
@@ -2504,18 +2507,20 @@ export default function AlertsPage() {
|
||||
} catch {
|
||||
mergedMeta = { ...mergedMeta, greTunnelTargets: [], greTunnelDetail: [] }
|
||||
}
|
||||
setMeta(mergedMeta)
|
||||
if(pageActive.current&&requestRevision===revision.current)setMeta(mergedMeta)
|
||||
} catch (e) {
|
||||
setLoadError(e instanceof Error ? e.message : "Ошибка загрузки")
|
||||
if(pageActive.current&&requestRevision===revision.current)setLoadError(e instanceof Error ? e.message : "Ошибка загрузки")
|
||||
} finally {
|
||||
setLoadBusy(false)
|
||||
if(pageActive.current&&requestRevision===revision.current)setLoadBusy(false)
|
||||
}
|
||||
}, [apiFetch, isLive])
|
||||
}, [apiFetch, isLive, backendUrl])
|
||||
|
||||
useEffect(() => {
|
||||
let current=true
|
||||
queueMicrotask(() => {
|
||||
void load()
|
||||
if(current)void load()
|
||||
})
|
||||
return()=>{current=false}
|
||||
}, [load])
|
||||
|
||||
const flushAlertsConfig = useCallback(async (nextRules: AlertRule[], nextGroups: AlertGroup[]) => {
|
||||
@@ -2637,7 +2642,7 @@ export default function AlertsPage() {
|
||||
)
|
||||
|
||||
const serverNamesForPresets = useMemo(() => {
|
||||
if (isLive && meta?.servers?.length) return meta.servers
|
||||
if (isLive) return meta?.servers??[]
|
||||
return TYPE_TARGETS.server
|
||||
}, [isLive, meta])
|
||||
|
||||
@@ -2811,7 +2816,7 @@ export default function AlertsPage() {
|
||||
{loadBusy ? <LoaderCircleIcon className="size-4 animate-spin" /> : <RefreshCwIcon className="size-4" />}
|
||||
Обновить
|
||||
</Button>
|
||||
<Button size="sm" onClick={openRuleSheetAdd} disabled={isLive && backendStatus === false}>
|
||||
<Button size="sm" onClick={openRuleSheetAdd} disabled={!known || (isLive && backendStatus !== true)}>
|
||||
<PlusIcon className="size-4" />Добавить правило
|
||||
</Button>
|
||||
</>
|
||||
@@ -2839,6 +2844,8 @@ export default function AlertsPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isLive?<p role="status" className="text-xs text-muted-foreground">{known?"Последние данные получены: "+new Date(receivedAt!).toLocaleString("ru-RU"):loadBusy?"Загрузка оповещений…":"Данные оповещений ещё не получены."}</p>:null}
|
||||
{known?<>
|
||||
<Collapsible open={hintsExpanded} onOpenChange={setHintsExpanded} className="rounded-lg border border-dashed border-border bg-muted/15">
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs font-medium text-foreground hover:bg-muted/40 rounded-lg">
|
||||
<ChevronDownIcon className={cn("size-4 shrink-0 transition-transform", hintsExpanded && "rotate-180")} />
|
||||
@@ -3147,6 +3154,7 @@ export default function AlertsPage() {
|
||||
<HistoryCard entries={history} />
|
||||
</div>
|
||||
</div>
|
||||
</>:null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+3
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-03
|
||||
|
||||
Этап 11Б в работе; последний проверенный блок 11Б.4.2.2.1.2; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.3.1: Alerts UI source lifetime/strict readers/known/error/time; затем drafts/locks/rights/unknown/adaptive, GET consistency/outbox audit. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12.
|
||||
Этап 11Б в работе; последний проверенный блок 11Б.4.3.1.1; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.3.1.2: независимые Alerts BGP/GRE readers/last good metadata/source ABA; затем Telegram/rules drafts/locks/rights/unknown/adaptive и GET/revision/outbox audit. Затем Alerts/Releases/probe scheduling, приёмка 11Б и этап 12.
|
||||
|
||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||
|
||||
@@ -314,3 +314,5 @@ REST запрос → ожидание → подтверждённый output /
|
||||
2026-10-03: 11Б.4.2.2.1.1 — общая нормализация rules/groups и 400 до удаления; 8 unit и backend profile/builds/typecheck/lint прошли. Далее transaction receipt 11Б.4.2.2.1.2.
|
||||
|
||||
2026-10-03: 11Б.4.2.2.1.2 — transaction rules receipt/readback/lock, DI/API/JWT/build/typecheck/lint прошли. Реальный PG rollback/overlap и optimistic revision не приняты. Далее Alerts UI 11Б.4.3.1.
|
||||
|
||||
2026-10-03: 11Б.4.3.1.1 — live Alerts без первоначальных demo/0, strict primary reader/known/time, 3 E2E/lint/typecheck/build прошли. Далее independent secondary readers/source 11Б.4.3.1.2.
|
||||
@@ -28,7 +28,7 @@
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.2.2.1.2; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.3.1.1; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## Блок 11Б.4.3.1.1 — первоначальное состояние Alerts и strict reader (2026-10-03)
|
||||
|
||||
- Live root больше не инициализируется INIT_RULES/INIT_GROUPS/INIT_HISTORY/INIT_TG. До подтверждённого GET нет demo rows, ложного «Всего: 0» и якобы полученной Telegram конфигурации. Known empty отличается от unknown; основной контент раскрывается только после strict getAlerts. Browser receivedAt показан явно; failed background GET сохраняет последний снимок.
|
||||
- Source-keyed root, active lifetime/AbortController/revision для GET и secondary requests; late primary/final metadata не публикуются в другой lifetime. Load после unmount не начинает новый reader. Браузерные source/ABA проверки этого раздела ещё впереди; мутации пока старые и не приняты.
|
||||
- Убран прямой demo fallback для live serverNamesForPresets. Secondary BGP/GRE всё ещё последовательные, часть errors маскируется [] и load busy ждёт их; refresh всё ещё заменяет Telegram drafts. Это промежуточная миграция reader, не приёмка Alerts.
|
||||
- 3 browser reader сценария (malformed без demo/0/Telegram и известный пустой snapshot с failed refresh), lint, typecheck без incremental и production frontend/contracts build прошли. Первый typecheck обнаружил collision active ref/active count и transient tsbuildinfo write error; ref переименован, nonincremental проверка и build прошли.
|
||||
- Следующий точный шаг 11Б.4.3.1.2: независимые BGP/GRE readers с known/error/time, сохранение last good metadata и source/ABA E2E. Затем Telegram dirty/selective save/test/unknown, rules confirmation/lock/rights/draft preservation/adaptive. GET consistency, optimistic revision и outbox unknown policy ещё требуют аудита; этап 12 не начат.
|
||||
|
||||
## Блок 11Б.4.2.2.1.2 — transactional receipt правил (2026-10-03)
|
||||
|
||||
- Replace теперь возвращает strict rules/groups receipt из самой transaction. Omitted groups читает внутри той же transaction после nonblocking pg_try_advisory_xact_lock; lock false даёт 409 без очереди/удаления, unknown lock не подтверждает действие. Targets/conditions/groups/rules/last-fired readback использует tx context вместо отдельных pool запросов после commit.
|
||||
|
||||
@@ -244,3 +244,5 @@ ReUI PRO/base-nova; существующие Frame, Data Grid v9, KpiStatGrid, S
|
||||
Завершён 11Б.4.2.2.1.1: shared rules/groups normalization до delete, 8 unit/HTTP/JWT/builds/typecheck/lint прошли. Router readback остаётся после commit; omitted groups read вне transaction. Далее 11Б.4.2.2.1.2 transactional receipt/context/DI/concurrency; затем UI/outbox.
|
||||
|
||||
Завершён 11Б.4.2.2.1.2: transactional rules/groups receipt/readback и nonblocking replace lock/409; 4 DI + profile/build/typecheck/lint прошли. Real PG и optimistic revision не проверены. Далее 11Б.4.3.1 — Alerts UI source/strict readers/known/error/time; затем drafts/locks/rights/adaptive и GET/outbox audit.
|
||||
|
||||
Завершён 11Б.4.3.1.1: primary Alerts strict reader/unknown/known empty/time/last snapshot без live initial mocks; 3 E2E/lint/typecheck/build прошли. Secondary readers/dirty/forms пока прежние. Далее 11Б.4.3.1.2 — независимые BGP/GRE/last good/source ABA; затем forms/rights/adaptive/GET/revision/outbox audit.
|
||||
@@ -0,0 +1,5 @@
|
||||
import {test,expect,type Page} from "@playwright/test"
|
||||
const empty={telegram:{chatId:"",tokenConfigured:false,connected:false,messageThreadId:null},groups:[],rules:[],history:[],meta:{servers:[],greClients:[],probeTargets:[],rttLossTargets:[],trafficServers:[],serversDetail:[]}}
|
||||
async function setup(page:Page,reply:()=>{status?:number;json: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(reply());if(path==="/api/bgp/sessions"||path==="/api/servers")return r.fulfill({json:[]});if(path==="/api/filters/gre-tunnels")return r.fulfill({json:{tunnels:[]}});return r.fulfill({status:503,json:{error:"outside alerts fixture"}})});await page.goto("/alerts")}
|
||||
for(const malformed of [{},{...empty,rules:null}])test("Alerts reader: malformed live response does not show demo rules or configured Telegram/"+JSON.stringify(malformed),async({page})=>{await setup(page,()=>({json:malformed}));await expect(page.getByText("Данные оповещений не подтверждены сервером",{exact:true})).toBeVisible();await expect(page.getByText("Данные оповещений ещё не получены.",{exact:true})).toBeVisible();await expect(page.getByRole("button",{name:"Добавить правило",exact:true})).toBeDisabled();await expect(page.getByText("Всего: 0",{exact:true})).toHaveCount(0);await expect(page.getByText("Bot Token",{exact:true})).toHaveCount(0)})
|
||||
test("Alerts reader: confirmed empty is distinct from unknown and background failure retains snapshot",async({page})=>{let fail=false;await setup(page,()=>fail?{status:503,json:{error:"Refresh unavailable"}}:{json:empty});await expect(page.getByText("Всего: 0",{exact:true})).toBeVisible();await expect(page.getByText(/Последние данные получены:/)).toBeVisible();fail=true;await page.getByRole("button",{name:"Обновить",exact:true}).click();await expect(page.getByText("Refresh unavailable",{exact:true})).toBeVisible();await expect(page.getByText("Всего: 0",{exact:true})).toBeVisible();await expect(page.getByText(/Последние данные получены:/)).toBeVisible()})
|
||||
Reference in new issue
Block a user