// Экран «Состояние» — серверы и боты по каждому ВПН-сервису. // Данные приходят из /api/health; источник у каждого сервиса свой (см. // app/health.py). Пока источник — мок, это помечено баннером и бейджами. const { useState: useStateSv, useEffect: useEffectSv } = React; const HEALTH_STATUS = { ok: { dot: "bg-[#22c55e]", label: "В сети", color: "text-[#22c55e]", border: "border-[#22c55e]/20" }, high: { dot: "bg-[#eab308]", label: "Высокая нагрузка", color: "text-[#eab308]", border: "border-[#eab308]/20" }, down: { dot: "bg-[#ef4444]", label: "Недоступен", color: "text-[#ef4444]", border: "border-[#ef4444]/20" }, unknown: { dot: "bg-zinc-500", label: "Неизвестно", color: "text-zinc-400", border: "border-[#2a2a3a]" }, }; function MockBadge({ show }) { if (!show) return null; return ( MOCK ); } function Metric({ label, value }) { return (
{label}
{value ?? "—"}
); } function HealthCardShell({ item, children }) { const cfg = HEALTH_STATUS[item.status] || HEALTH_STATUS.unknown; return (
{item.name}
{cfg.label}
{children} {item.message && (
{item.message}
)}
источник: {item.source}
); } function ServerCard({ server }) { const m = server.metrics || {}; return (
{server.location &&
📍 {server.location}
}
); } function BotCard({ bot }) { const m = bot.metrics || {}; const lastUpdate = m.lastUpdate ? new Date(m.lastUpdate).toLocaleTimeString("ru", { hour: "2-digit", minute: "2-digit" }) : null; return (
{m.role &&
🤖 {m.role}
}
); } function HealthSummary({ items }) { const count = (st) => items.filter((i) => i.status === st).length; const cells = [ { label: "В сети", value: count("ok"), color: "#22c55e" }, { label: "Высокая нагрузка", value: count("high"), color: "#eab308" }, { label: "Недоступно", value: count("down"), color: "#ef4444" }, ]; return (
{cells.map((c) => (
{/* Иконка съедает ширину на телефоне — там достаточно цифры и подписи */}
{c.value}
{c.label}
))}
); } function ServiceHealthSection({ snapshot, showServiceTitle }) { const servers = snapshot.servers || []; const bots = snapshot.bots || []; return (
{showServiceTitle && (

{snapshot.serviceName}

)}
Серверы
{servers.length === 0 ? (
Нет данных
) : (
{servers.map((s) => )}
)}
Боты
{bots.length === 0 ? (
Нет данных
) : (
{bots.map((b) => )}
)}
); } // Админский блок: какой источник данных опрашивается у сервиса. Список // провайдеров приходит с бэкенда из реестра app.health — свой провайдер // появляется здесь сам, без правки фронта. function SourcePicker({ serviceId, onChanged }) { const [cfg, setCfg] = useStateSv(null); const [saving, setSaving] = useStateSv(false); // Какой из блоков (servers | bots) сейчас раскрыт под редактирование // конфига, и текст в его текстарее — раздельно, чтобы правка одного не // затирала несохранённый черновик другого. const [openKind, setOpenKind] = useStateSv(null); const [configText, setConfigText] = useStateSv("{}"); const [configErr, setConfigErr] = useStateSv(null); useEffectSv(() => { if (serviceId === null) { setCfg(null); setOpenKind(null); return; } window.apiFetch("GET", `/api/settings/monitoring?service_id=${serviceId}`) .then(setCfg).catch(() => setCfg(null)); }, [serviceId]); async function save(next) { setSaving(true); try { await window.apiFetch("PUT", `/api/settings/monitoring?service_id=${serviceId}`, next); onChanged && onChanged(); } catch { /* сообщение покажет общий тост */ } setSaving(false); } function pick(kind, provider) { const next = { interval: cfg.interval, servers: cfg.servers, bots: cfg.bots }; next[kind] = { provider, config: next[kind].config || {} }; setCfg((c) => ({ ...c, ...next })); save(next); } function toggleConfig(kind) { if (openKind === kind) { setOpenKind(null); return; } setOpenKind(kind); setConfigText(JSON.stringify(cfg[kind]?.config || {}, null, 2)); setConfigErr(null); } function saveConfig(kind) { let parsed; try { parsed = JSON.parse(configText || "{}"); } catch (e) { setConfigErr("Это не похоже на JSON: " + e.message); return; } setConfigErr(null); const next = { interval: cfg.interval, servers: cfg.servers, bots: cfg.bots }; next[kind] = { provider: cfg[kind]?.provider, config: parsed }; setCfg((c) => ({ ...c, ...next })); save(next); } if (!cfg) return null; const rows = [["servers", "Серверы"], ["bots", "Боты"]]; return (
Источник данных · {cfg.serviceName}
{rows.map(([kind, label]) => (
{label} {(cfg.available?.[kind] || []).map((p) => { const on = cfg[kind]?.provider === p.name; return ( ); })}
{openKind === kind && (