now supports multi machine monitoring

This commit is contained in:
2026-05-06 15:09:15 +02:00
parent 8f0a9650b0
commit d8d80867ce
20 changed files with 757 additions and 426 deletions
+9 -158
View File
@@ -1,83 +1,22 @@
import { useMemo } from "react";
import { Box, Divider, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useCounts, useLibraries, useActivity } from "../hooks/useDashboard";
import { useMonitoringMetrics, useDiskSpace } from "../hooks/useMonitoring";
import {
useCounts,
useLibraries,
useActivity,
useMonitoringOverview,
} from "../hooks/useDashboard";
import { NowPlaying } from "../components/NowPlaying";
import { MetricCard } from "../components/MetricCard";
import { DiskSpaceCard } from "../components/DiskSpaceCard";
import { LibraryOverview } from "../components/LibraryOverview";
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 formatPct(value: number): string {
return `${value.toFixed(1)}%`;
}
function summarize(values: number[]) {
if (values.length === 0) return null;
const total = values.reduce((sum, value) => sum + value, 0);
return {
avg: total / values.length,
min: Math.min(...values),
max: Math.max(...values),
};
}
import { MonitoringOverviewTable } from "../components/MonitoringOverviewTable";
export function Dashboard() {
const navigate = useNavigate();
const { data: counts } = useCounts();
const { data: libraries } = useLibraries();
const { data: activity } = useActivity();
const { data: metrics } = useMonitoringMetrics();
const { data: disk } = useDiskSpace();
const monitoringWindow = useMemo(() => {
const samples = metrics?.samples ?? [];
if (samples.length === 0) return [];
const latestTs = samples.at(-1)?.ts ?? 0;
const windowStart = latestTs - 10 * 60;
const windowed = samples.filter((sample) => sample.ts >= windowStart);
return windowed.length > 0 ? windowed : samples;
}, [metrics?.samples]);
const cpuSummary = summarize(
monitoringWindow.map((sample) => sample.cpu_pct),
);
const iowaitSummary = summarize(
monitoringWindow
.map((sample) => sample.iowait_pct)
.filter((value): value is number => value !== undefined),
);
const memSummary = summarize(
monitoringWindow.map((sample) => sample.mem_pct),
);
const netRxSummary = summarize(
monitoringWindow.map((sample) => sample.net_rx_bytes_per_sec),
);
const netTxSummary = summarize(
monitoringWindow.map((sample) => sample.net_tx_bytes_per_sec),
);
const diskReadSummary = summarize(
monitoringWindow.map((sample) => sample.disk_read_bps),
);
const diskWriteSummary = summarize(
monitoringWindow.map((sample) => sample.disk_write_bps),
);
const { data: monitoringOverview } = useMonitoringOverview();
return (
<Stack spacing={3}>
@@ -101,95 +40,7 @@ export function Dashboard() {
<Typography variant="h5" sx={{ mb: 1.5 }}>
Monitoring Overview
</Typography>
<Grid container spacing={2} sx={{ alignItems: "stretch" }}>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="CPU (10m avg)"
value={cpuSummary ? formatPct(cpuSummary.avg) : "-"}
subtext={
cpuSummary
? `High: ${formatPct(cpuSummary.max)}\nLow: ${formatPct(cpuSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="IO Wait (10m avg)"
value={iowaitSummary ? formatPct(iowaitSummary.avg) : "-"}
subtext={
iowaitSummary
? `High: ${formatPct(iowaitSummary.max)}\nLow: ${formatPct(iowaitSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="RAM (10m avg)"
value={memSummary ? formatPct(memSummary.avg) : "-"}
subtext={
memSummary
? `High: ${formatPct(memSummary.max)}\nLow: ${formatPct(memSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Net down (10m avg)"
value={netRxSummary ? formatRate(netRxSummary.avg) : "-"}
subtext={
netRxSummary
? `High: ${formatRate(netRxSummary.max)}\nLow: ${formatRate(netRxSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Net up (10m avg)"
value={netTxSummary ? formatRate(netTxSummary.avg) : "-"}
subtext={
netTxSummary
? `High: ${formatRate(netTxSummary.max)}\nLow: ${formatRate(netTxSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Disk read (10m avg)"
value={diskReadSummary ? formatRate(diskReadSummary.avg) : "-"}
subtext={
diskReadSummary
? `High: ${formatRate(diskReadSummary.max)}\nLow: ${formatRate(diskReadSummary.min)}`
: undefined
}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Disk write (10m avg)"
value={diskWriteSummary ? formatRate(diskWriteSummary.avg) : "-"}
subtext={
diskWriteSummary
? `High: ${formatRate(diskWriteSummary.max)}\nLow: ${formatRate(diskWriteSummary.min)}`
: undefined
}
/>
</Grid>
</Grid>
{disk && (
<Box sx={{ mt: 0.5 }}>
<DiskSpaceCard
used={disk.used}
available={disk.available}
size={disk.size}
usedPct={disk.used_pct}
/>
</Box>
)}
<MonitoringOverviewTable overview={monitoringOverview} />
</Box>
<Divider />
+65 -158
View File
@@ -1,173 +1,80 @@
import { Alert, Button, Chip, Stack, Typography } from "@mui/material";
import {
Box,
Button,
Chip,
Divider,
Grid,
Stack,
Typography,
} from "@mui/material";
import {
useMonitoringStatus,
useMonitoringMetrics,
useDiskSpace,
useCollectorControls,
useMonitoringMachines,
useMonitoringPoller,
} from "../hooks/useMonitoring";
import { MetricCard } from "../components/MetricCard";
import { MonitoringCharts } from "../components/MonitoringCharts";
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`;
}
import { MachineMonitoringSection } from "../components/MachineMonitoringSection";
import { useNavigate } from "react-router-dom";
export function Monitoring() {
const { data: status } = useMonitoringStatus();
const { data: metrics } = useMonitoringMetrics();
const { data: disk } = useDiskSpace();
const { start, stop, restart } = useCollectorControls();
const samples = metrics?.samples ?? [];
const latest = samples.at(-1);
const avg = (arr: number[]) =>
arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
const max = (arr: number[]) => (arr.length ? Math.max(...arr) : 0);
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 { data: machines, isLoading, error } = useMonitoringMachines();
const { data: poller } = useMonitoringPoller();
const navigate = useNavigate();
return (
<Stack spacing={3}>
<Stack
direction="row"
spacing={1.5}
sx={{ alignItems: "center", flexWrap: "wrap" }}
>
<Typography variant="h5">Monitoring</Typography>
<Chip
label={status?.status ?? "unknown"}
color="primary"
variant="outlined"
/>
<Button
size="small"
variant="outlined"
onClick={() => start.mutate()}
disabled={start.isPending}
<Stack spacing={0.5}>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: "center", flexWrap: "wrap" }}
>
Start
</Button>
<Button
size="small"
variant="outlined"
onClick={() => restart.mutate()}
disabled={restart.isPending}
>
Restart
</Button>
<Button
size="small"
variant="outlined"
onClick={() => stop.mutate()}
disabled={stop.isPending}
>
Stop
</Button>
<Typography variant="h5">Monitoring</Typography>
{poller && (
<Chip
size="small"
variant="outlined"
color={poller.worker_running ? "success" : "default"}
label={
poller.worker_running
? `Poller running · ${poller.interval_seconds}s`
: "Poller stopped"
}
/>
)}
</Stack>
<Typography variant="body2" color="text.secondary">
Each machine below is monitored with its own settings and collector
state, and recent activity is gathered automatically by the backend.
</Typography>
</Stack>
<Grid container spacing={1.5}>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="CPU now"
value={latest ? `${latest.cpu_pct.toFixed(1)}%` : "-"}
subtext={`avg ${avg(cpuArr).toFixed(1)}%\npeak ${max(cpuArr).toFixed(1)}%`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="IO Wait"
value={latest ? `${(latest.iowait_pct ?? 0).toFixed(1)}%` : "-"}
subtext={`avg ${avg(iowArr).toFixed(1)}%\npeak ${max(iowArr).toFixed(1)}%`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="RAM now"
value={latest ? `${latest.mem_pct.toFixed(1)}%` : "-"}
subtext={`avg ${avg(memArr).toFixed(1)}%\npeak ${max(memArr).toFixed(1)}%`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Net down"
value={latest ? formatRate(latest.net_rx_bytes_per_sec) : "-"}
subtext={`avg ${formatRate(avg(netDownArr))}\npeak ${formatRate(max(netDownArr))}`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Net up"
value={latest ? formatRate(latest.net_tx_bytes_per_sec) : "-"}
subtext={`avg ${formatRate(avg(netUpArr))}\npeak ${formatRate(max(netUpArr))}`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Disk read"
value={latest ? formatRate(latest.disk_read_bps) : "-"}
subtext={`avg ${formatRate(avg(diskReadArr))}\npeak ${formatRate(max(diskReadArr))}`}
/>
</Grid>
<Grid size={{ xs: 6, md: 3, lg: 12 / 7 }}>
<MetricCard
label="Disk write"
value={latest ? formatRate(latest.disk_write_bps) : "-"}
subtext={`avg ${formatRate(avg(diskWriteArr))}\npeak ${formatRate(max(diskWriteArr))}`}
/>
</Grid>
</Grid>
{disk && (
<Grid container spacing={1.5}>
<Grid size={{ xs: 6, md: 3 }}>
<MetricCard label="Disk used" value={formatBytes(disk.used)} />
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<MetricCard
label="Disk available"
value={formatBytes(disk.available)}
/>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<MetricCard label="Disk total" value={formatBytes(disk.size)} />
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<MetricCard label="Used %" value={disk.used_pct} />
</Grid>
</Grid>
{error && <Alert severity="error">{String(error)}</Alert>}
{isLoading && (
<Alert severity="info">Loading monitoring machines...</Alert>
)}
{!isLoading && (machines?.length ?? 0) === 0 && (
<Alert
severity="warning"
action={
<Button
color="inherit"
size="small"
onClick={() => navigate("/settings")}
>
Open Settings
</Button>
}
>
No monitoring machines are configured yet.
</Alert>
)}
<Divider />
<Box>
<MonitoringCharts samples={samples} />
</Box>
{machines?.map((machine) => (
<Stack
key={machine.id}
spacing={2}
sx={{
p: 2,
border: 1,
borderColor: "divider",
borderRadius: 2,
bgcolor: "background.paper",
}}
>
<MachineMonitoringSection machine={machine} />
</Stack>
))}
</Stack>
);
}