import { useMemo, useState } from "react"; import { Alert, Box, Button, Chip, FormControl, Grid, InputLabel, MenuItem, Paper, Select, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, } from "@mui/material"; import { MetricCard } from "./MetricCard"; import { MonitoringCharts } from "./MonitoringCharts"; import type { MonitoringMachine } from "../types"; import { useCollectorControls, useDiskSpace, useMachineActions, useMonitoringMetrics, useMonitoringStatus, } from "../hooks/useMonitoring"; function formatBytes(bytes: number): string { if (!bytes || bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unitIdx = 0; while (value >= 1000 && unitIdx < units.length - 1) { value /= 1000; unitIdx++; } return `${value.toFixed(1)} ${units[unitIdx]}`; } function formatRate(bytes: number): string { return `${formatBytes(bytes)}/s`; } function avg(arr: number[]) { return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0; } function max(arr: number[]) { return arr.length ? Math.max(...arr) : 0; } function formatActionTime(epochSeconds: number): string { return new Date(epochSeconds * 1000).toLocaleString(); } function ErrorBanner({ label, error }: { label: string; error: unknown }) { if (!error) return null; return ( {label}: {String(error)} ); } export function MachineMonitoringSection({ machine, }: { machine: MonitoringMachine; }) { const statusQuery = useMonitoringStatus(machine.id, machine.enabled); const metricsQuery = useMonitoringMetrics(machine.id, machine.enabled); const diskQuery = useDiskSpace(machine.id, machine.enabled); const { start, stop, restart } = useCollectorControls(machine.id); const actionsQuery = useMachineActions(machine.id, machine.enabled); const [actionFilter, setActionFilter] = useState("all"); const [resultFilter, setResultFilter] = useState("all"); const status = statusQuery.data; const metrics = metricsQuery.data; const disk = diskQuery.data; const samples = metrics?.samples ?? []; const latest = samples.at(-1); const cpuArr = samples.map((s) => s.cpu_pct); const iowArr = samples.map((s) => s.iowait_pct ?? 0); const memArr = samples.map((s) => s.mem_pct); const netDownArr = samples.map((s) => s.net_rx_bytes_per_sec); const netUpArr = samples.map((s) => s.net_tx_bytes_per_sec); const diskReadArr = samples.map((s) => s.disk_read_bps); const diskWriteArr = samples.map((s) => s.disk_write_bps); const actions = actionsQuery.data?.items ?? []; const visibleActions = useMemo( () => actions.filter((action) => { const actionMatches = actionFilter === "all" || action.action === actionFilter; const resultMatches = resultFilter === "all" || action.status === resultFilter; return actionMatches && resultMatches; }), [actions, actionFilter, resultFilter], ); const hasQueryError = statusQuery.error || metricsQuery.error || diskQuery.error || actionsQuery.error; return ( {machine.name} {machine.mode === "local" ? "Local API host" : `${machine.username || "user"}@${machine.host || "host"}:${machine.port}`} {machine.media_root ? ` ยท media root ${machine.media_root}` : ""} {hasQueryError && ( )} {disk && ( )} Recent activity Collected automatically by the backend poller. Action Result {actionsQuery.error && ( Recent activity: {String(actionsQuery.error)} )} Time Action Status Duration Message {visibleActions.length === 0 ? ( No activity matches the current filters. ) : ( visibleActions.map((action) => ( {formatActionTime(action.created_at)} {action.action} {action.duration_ms} ms {action.message || action.error || "-"} )) )}
); }