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 || "-"}
))
)}
);
}