Netpulse_SasS/web/src/pages/AgentsPage.tsx
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

101 lines
3.9 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 { useEffect, useState } from 'react'
import { api } from '../api/client'
import { Card, ErrorNote, PageHeader, Spinner, StatusBadge, ago } from '../components/ui'
import type { AgentSummary } from '../types'
export function AgentsPage() {
const [agents, setAgents] = useState<AgentSummary[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
api
.listAgents()
.then(setAgents)
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
.finally(() => setLoading(false))
}, [])
const online = agents.filter((a) => a.status === 'online').length
return (
<>
<PageHeader
title="Зонди"
subtitle={loading ? undefined : `${online} з ${agents.length} на зв’язку`}
/>
<div className="min-h-0 flex-1 overflow-y-auto p-3 md:p-4">
{error && <ErrorNote>{error}</ErrorNote>}
{loading ? (
<Spinner />
) : agents.length === 0 ? (
<p className="px-4 py-16 text-center text-sm text-slate-600">
Зондів ще немає. Зонд сам підключається до сервера відкривати порти в мережі
клієнта не потрібно.
</p>
) : (
<ul className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{agents.map((a) => (
<li key={a.id}>
<Card className="p-3">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-slate-100">{a.name}</span>
<span className="ml-auto">
<StatusBadge
status={a.status}
label={a.status === 'online' ? 'на зв’язку' : 'офлайн'}
/>
</span>
</div>
<dl className="mt-2.5 space-y-1 text-xs">
<Pair k="Платформа" v={`${a.os ?? '?'}/${a.arch ?? '?'}`} />
<Pair k="Версія" v={a.version ?? '—'} />
<Pair k="Пристроїв" v={String(a.device_count)} />
<Pair k="Останній пульс" v={ago(a.last_heartbeat_at)} />
{typeof a.health?.rss_bytes === 'number' && (
<Pair
k="Памʼять"
v={`${((a.health.rss_bytes as number) / 1048576).toFixed(1)} МБ`}
/>
)}
</dl>
<div className="mt-2.5 flex flex-wrap gap-1">
{a.enabled_modules.map((m) => (
<span
key={m}
className="rounded border border-slate-700 bg-slate-800 px-1.5 py-0.5 text-[11px] text-slate-400"
>
{m}
</span>
))}
{a.enabled_modules.length === 0 && (
<span className="text-[11px] text-slate-600">модулі не активовані</span>
)}
</div>
</Card>
</li>
))}
</ul>
)}
<p className="mt-4 text-xs leading-relaxed text-slate-600">
Керування набором модулів із інтерфейсу ще не реалізоване транспорт для цього
готовий, але сторінка налаштувань зонда попереду.
</p>
</div>
</>
)
}
function Pair({ k, v }: { k: string; v: string }) {
return (
<div className="flex justify-between gap-3">
<dt className="text-slate-500">{k}</dt>
<dd className="tabular-nums text-slate-300">{v}</dd>
</div>
)
}