import { useState, useEffect, useCallback } from 'react'; import api from './lib/api.js'; import { IconServer, IconRefresh, IconChartBar, IconAlertCircle, } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; const REQUEST_TIMEOUT_MS = 15000; function formatBytes(bytes) { if (bytes == null || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } /** Цвет прогресс-бара по проценту: зелёный / жёлтый / красный */ function progressVariant(percent) { if (percent == null) return 'secondary'; if (percent >= 90) return 'danger'; if (percent >= 70) return 'warning'; return 'success'; } /** Один KPI-блок: заголовок, значение, прогресс-бар (как на референсной карточке) */ function KpiBlock({ label, value, percent, variant }) { const barVariant = variant || progressVariant(percent); return (
{label}
{value} {percent != null && ( {percent}% )}
{percent != null && (
)}
); } /** Карточка одного роутера: иконка перед названием, сводка, KPI (RAM, CPU, HDD) */ function RouterResourceCard({ router }) { const { name, host, error, resource } = router; const hasResource = resource && !error; return (

{name}

{host} {resource?.version && ( · RouterOS {resource.version} )}

{error && (
{error}
)} {hasResource && (
{/* RAM */}
{/* CPU */}
{/* HDD */}
)} {hasResource && (resource.boardName || resource.uptime) && (
{resource.boardName && {resource.boardName}} {resource.uptime && ( · Uptime: {resource.uptime} )}
)}
); } export default function ResourceStatsPage() { const [routers, setRouters] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchStats = useCallback(async (abortSignal) => { setLoading(true); setError(null); const opts = { timeout: REQUEST_TIMEOUT_MS, noRetry: true, ...(abortSignal ? { signal: abortSignal } : {}) }; try { const res = await api.get('/resources/stats', opts); const data = res?.data; setRouters(Array.isArray(data?.routers) ? data.routers : []); } catch (e) { if (e?.name === 'CanceledError' || e?.name === 'AbortError' || e?.code === 'ERR_CANCELED') return; setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику'); // Не очищаем список — пользователь видит старые данные и сообщение об ошибке } finally { setLoading(false); } }, []); useEffect(() => { const controller = new AbortController(); fetchStats(controller.signal); return () => controller.abort(); }, [fetchStats]); return (
} meta="Потребление RAM, CPU и HDD по каждому роутеру (MikroTik)" actions={ } /> {error && (
{error}
)} {loading && routers.length === 0 ? (
{[1, 2, 3].map((i) => (
))}
) : routers.length === 0 ? (

Нет роутеров с настроенным MikroTik API (jumphost/home).

) : (
{routers.map((router) => (
))}
)}
); }