Netpulse_SasS/web/src/hooks/useAlerts.ts
byrsapty 36f29e23ec Повноцінний веб: навігація і вісім сторінок
До цього «веб» був одним екраном: логін, мапа й панель алертів. Усе,
що вміло API, доводилось викликати curl-ом.

Каркас:
- бічна навігація з мобільною шухлядою, індикатор зв'язку в шапці,
  живий лічильник алертів
- пункт, на який немає права, не показується взагалі; пряме посилання
  лишається робочим і дає пояснення з назвою потрібного права
- стартова сторінка залежить від ролі: глядача без прав на мапи вітати
  відмовою — поганий перший екран

Сторінки: мапа, пристрої з пошуком і картками алертів, алерти з
фільтрами, правила зі створенням через форму, канали з кнопкою
перевірки, зонди, команда з керуванням ролями, профіль зі зміною
пароля.

DataTable на телефоні перестає бути таблицею: горизонтальний скрол на
375 px робить дані формально присутніми й фактично нечитабельними.

Знайдено роботою з живим UI:
- WebSocket жив усередині мапи, тому на решті сторінок живих оновлень
  не було взагалі — лічильник алертів замерзав, щойно людина йшла з
  мапи. З'єднання винесено в модуль-одинак, яким володіє оболонка
- сторінка пристроїв перечитувала все на кожну подію алерту; зі
  злиттям сплеску й спільними алертами той самий сплеск коштує
  2 запити замість дванадцяти
- канал із секретом показувався як «секрету немає»: прапорець ставився
  лише в гілці розшифровки, а перелік для UI викликається без ключа

Перевірено в браузері проти повного стека: усі вісім сторінок із
живими даними, створення правила й користувача через форми, перевірка
каналу, обмеження глядача, мобільний вигляд на 375 px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 01:17:57 +03:00

92 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useState } from 'react'
import { api } from '../api/client'
import { session } from '../api/session'
import { isAlertEvent, useLiveRefresh } from './useLiveRefresh'
import type { Alert, AlertCounts } from '../types'
const emptyCounts: AlertCounts = {
firing: 0,
acknowledged: 0,
suppressed: 0,
disaster: 0,
high: 0,
}
export interface LiveAlerts {
alerts: Alert[]
counts: AlertCounts
loading: boolean
error: string | null
reload: () => Promise<void>
ack: (id: string) => Promise<void>
close: (id: string) => Promise<void>
mute: (deviceID: string, minutes: number) => Promise<void>
}
/**
* Тримає список активних алертів.
*
* Подія по WebSocket не несе всього алерту — лише те, що щось
* змінилося. Список перечитується цілком: він короткий (десятки рядків),
* а склеювати його з дельт означало б розходитись зі станом сервера
* рівно в момент аварії, коли подій найбільше.
*/
export function useAlerts(): LiveAlerts {
const [alerts, setAlerts] = useState<Alert[]>([])
const [counts, setCounts] = useState<AlertCounts>(emptyCounts)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const canRead = session.can('alerts:read')
const reload = useCallback(async () => {
if (!canRead) {
setLoading(false)
return
}
try {
const r = await api.listAlerts()
setAlerts(r.alerts)
setCounts(r.counts)
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setLoading(false)
}
}, [canRead])
useEffect(() => {
void reload()
}, [reload])
useLiveRefresh(isAlertEvent, () => {
if (canRead) void reload()
})
const ack = useCallback(
async (id: string) => {
await api.ackAlert(id)
await reload()
},
[reload],
)
const close = useCallback(
async (id: string) => {
await api.closeAlert(id)
await reload()
},
[reload],
)
const mute = useCallback(
async (deviceID: string, minutes: number) => {
await api.muteDevice(deviceID, minutes)
await reload()
},
[reload],
)
return { alerts, counts, loading, error, reload, ack, close, mute }
}