/* Monitoring.jsx — моніторинг у реальному часі (live API). */ const { useState: useStateM, useEffect: useEffectM } = React; function Monitoring({ onNav }) { const [data, setData] = useStateM(null); const [loading, setLoading] = useStateM(true); const [error, setError] = useStateM(null); const [filter, setFilter] = useStateM("all"); const [acked, setAcked] = useStateM(new Set()); const [lastUpdate, setLastUpdate] = useStateM(new Date()); const reload = async () => { try { const res = await window.passengerApi.monitoring.snapshot(); setData(res); setLastUpdate(new Date()); setError(null); } catch (e) { setError(e); window.showToast && window.showToast("Не вдалося завантажити Моніторинг: " + (e.message || ""), "bad"); } finally { setLoading(false); } }; useEffectM(() => { reload(); }, []); window.passengerApi.usePolling(reload, 30000, []); const kpi = (data && data.kpi) || {}; const alerts = (data && data.alerts) || []; const routes = (data && data.routes) || []; const handleAck = async (alert) => { try { await window.passengerApi.monitoring.ack({ central_id: alert.central_id, code: alert.code, }); setAcked(prev => new Set([...prev, alert.id])); window.showToast && window.showToast(`${alert.central_id} підтверджено — алерт закрито`, "ok"); await reload(); } catch (e) { window.showToast && window.showToast("Помилка ack: " + (e.message || ""), "bad"); } }; const handleAckAll = async () => { const targets = alerts.filter(a => !acked.has(a.id) && a.severity === "bad"); if (!targets.length) return; try { const results = await Promise.allSettled( targets.map(a => window.passengerApi.monitoring.ack({ central_id: a.central_id, code: a.code })) ); const ok = results.filter(r => r.status === "fulfilled").length; const failed = results.length - ok; setAcked(prev => { const next = new Set(prev); targets.forEach(a => next.add(a.id)); return next; }); window.showToast && window.showToast(`Підтверджено ${ok}${failed ? `, помилок ${failed}` : ""}`, ok ? "ok" : "warn"); await reload(); } catch (e) { window.showToast && window.showToast("Помилка bulk-ack: " + (e.message || ""), "bad"); } }; const filteredAlerts = alerts.filter(a => { if (filter === "problems") return a.severity === "bad" && !acked.has(a.id); if (filter === "ok") return a.severity !== "bad" || acked.has(a.id); return true; }); const fmtTime = (d) => d.toLocaleTimeString("uk-UA", { hour: "2-digit", minute: "2-digit" }); return ( <> {kpi.problems > 0 && ( )} } />
{error && (
Помилка завантаження.
)} {/* KPI row */}
{[ { label: "Проблемні узли", value: kpi.problems != null ? kpi.problems : "—", color: (kpi.problems || 0) > 0 ? "var(--bad)" : "var(--ok)", bg: (kpi.problems || 0) > 0 ? "var(--bad-bg)" : "var(--ok-bg)", border: (kpi.problems || 0) > 0 ? "var(--bad-border)" : "var(--ok-border)" }, { label: "ТЗ онлайн", value: kpi.vehicles_label || "—", color: "var(--text-1)", bg: "var(--surface)", border: "var(--border)" }, { label: "Середня доступність", value: kpi.avg_availability_pct != null ? `${kpi.avg_availability_pct}%` : "—", color: (kpi.avg_availability_pct || 0) >= 95 ? "var(--ok)" : "var(--warn)", bg: (kpi.avg_availability_pct || 0) >= 95 ? "var(--ok-bg)" : "var(--warn-bg)", border: (kpi.avg_availability_pct || 0) >= 95 ? "var(--ok-border)" : "var(--warn-border)" }, { label: "Інциденти", value: kpi.incidents_open != null ? kpi.incidents_open : "—", color: (kpi.incidents_open || 0) > 0 ? "var(--warn)" : "var(--text-1)", bg: (kpi.incidents_open || 0) > 0 ? "var(--warn-bg)" : "var(--surface)", border: (kpi.incidents_open || 0) > 0 ? "var(--warn-border)" : "var(--border)" }, ].map((k, i) => (
{k.label}
{k.value}
))}
{/* Alerts list (admin/fleet alerts) */}
Авто-оновлення кожні 30 с
{filteredAlerts.length === 0 ? (
{kpi.problems ? "🔍" : "✅"}
{kpi.problems ? `Усі ${kpi.problems} критичних оброблено в цій вкладці` : "Алертів немає"}
) : ( {filteredAlerts.map(a => { const isAcked = acked.has(a.id) || a.acked_at; return ( ); })}
Узел / код Серйозність Повідомлення Вік
{a.central_id || "—"}
{a.code}
{isAcked ? ✓ Підтверджено : a.severity === "bad" ? ⚠ Проблема : a.severity === "warn" ? ⚡ Попередження : ✓ Норма} {a.message || "—"} {a.age_label || ""} {!isAcked && ( )}
)}
{/* Routes list (passenger_routes) */} {routes.length > 0 && (
Маршрути ({routes.length})
{routes.map(r => ( ))}
Маршрут Статус Призупинено
{r.code}
{r.name}
{r.paused ? ⏸ Призупинено : r.status === "published" ? ● Активний : ○ Чернетка} {r.paused_reason || (r.paused ? "—" : "")}
)}
); } Object.assign(window, { Monitoring });