fixes and improvements

This commit is contained in:
2026-05-07 13:48:10 +02:00
parent bba23165ab
commit efc9b247c7
8 changed files with 344 additions and 245 deletions
@@ -48,15 +48,12 @@ function formatRate(bytes: number): string {
return `${formatBytes(bytes)}/s`;
}
function formatTime(epochSeconds: number | null): string {
function formatAge(epochSeconds: number | null): string {
if (!epochSeconds) return "-";
const date = new Date(epochSeconds * 1000);
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, "0");
const dd = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const min = String(date.getMinutes()).padStart(2, "0");
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
const diff = Date.now() / 1000 - epochSeconds;
if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`;
if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
return `${Math.round(diff / 3600)}h ago`;
}
function formatReadableTime(epochSeconds: number | null): string {
@@ -89,22 +86,15 @@ function formatUpdatedDetails(epochSeconds: number | null): [string, string] {
];
}
function formatAge(epochSeconds: number | null): string {
if (!epochSeconds) return "-";
const diff = Date.now() / 1000 - epochSeconds;
if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`;
if (diff < 3600) return `${Math.round(diff / 60)}m ago`;
return `${Math.round(diff / 3600)}h ago`;
}
function formatSummary(
summary: { avg: number; min: number; max: number } | null,
formatter: (value: number) => string,
) {
if (!summary) return { value: "-", subtext: "" };
if (!summary) return { value: "-", min: "", max: "" };
return {
value: formatter(summary.avg),
subtext: `Low ${formatter(summary.min)}\nHigh ${formatter(summary.max)}`,
min: formatter(summary.min),
max: formatter(summary.max),
};
}
@@ -146,19 +136,25 @@ function metricSortValue(
}
}
function metricCell(value: string, subtext?: string) {
const [minLine, maxLine] = (subtext || "").split("\n");
const min = minLine?.replace(/^Low\s+/, "").trim();
const max = maxLine?.replace(/^High\s+/, "").trim();
function MetricCell({
value,
min,
max,
}: {
value: string;
min?: string;
max?: string;
}) {
return (
<Stack
sx={{
width: "100%",
minWidth: 110,
minWidth: 0,
height: "100%",
minHeight: 96,
minHeight: 118,
textAlign: "center",
py: 0.4,
justifyContent: "space-between",
py: 0.5,
}}
>
<Box
@@ -168,16 +164,17 @@ function metricCell(value: string, subtext?: string) {
alignItems: "center",
justifyContent: "center",
px: 1.5,
py: 1.15,
py: 1.25,
minHeight: 68,
borderRadius: 1,
}}
>
<Stack spacing={0.1} sx={{ alignItems: "center" }}>
<Stack spacing={0.25} sx={{ alignItems: "center" }}>
<Typography
variant="caption"
color="text.secondary"
sx={{
fontSize: "0.62rem",
fontSize: "0.6rem",
lineHeight: 1,
textTransform: "uppercase",
letterSpacing: "0.04em",
@@ -189,7 +186,7 @@ function metricCell(value: string, subtext?: string) {
variant="body1"
sx={{
fontWeight: 900,
fontSize: "1.14rem",
fontSize: "1.12rem",
lineHeight: 1,
textAlign: "center",
whiteSpace: "nowrap",
@@ -200,46 +197,48 @@ function metricCell(value: string, subtext?: string) {
</Typography>
</Stack>
</Box>
<Stack
spacing={0.25}
sx={{
width: "100%",
pt: 0.1,
}}
>
<Stack spacing={0.25} sx={{ width: "100%" }}>
{min ? (
<Chip
size="small"
variant="outlined"
label={`Min ${min}`}
<Box
sx={{
width: "100%",
height: 18,
"& .MuiChip-label": {
px: 0.5,
py: 0,
fontSize: "0.6rem",
lineHeight: 1,
},
border: 1,
borderColor: "divider",
borderRadius: 999,
px: 0.75,
py: 0.15,
fontSize: "0.6rem",
lineHeight: 1.2,
color: "text.secondary",
textAlign: "center",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
/>
>
Min {min}
</Box>
) : null}
{max ? (
<Chip
size="small"
variant="outlined"
label={`Max ${max}`}
<Box
sx={{
width: "100%",
height: 18,
"& .MuiChip-label": {
px: 0.5,
py: 0,
fontSize: "0.6rem",
lineHeight: 1,
},
border: 1,
borderColor: "divider",
borderRadius: 999,
px: 0.75,
py: 0.15,
fontSize: "0.6rem",
lineHeight: 1.2,
color: "text.secondary",
textAlign: "center",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
/>
>
Max {max}
</Box>
) : null}
</Stack>
</Stack>
@@ -302,8 +301,11 @@ export function MonitoringOverviewTable({
label={`Machines: ${overview?.enabled ?? 0}/${overview?.total ?? 0}`}
/>
<Typography variant="caption" color="text.secondary">
Last success: {formatTime(poller?.last_success_at ?? null)} · last
run: {formatAge(poller?.last_run_at ?? null)}
Last success:{" "}
{poller?.last_success_at
? new Date(poller.last_success_at * 1000).toLocaleString()
: "-"}{" "}
· last run: {formatAge(poller?.last_run_at ?? null)}
</Typography>
</Stack>
) : null}
@@ -326,13 +328,16 @@ export function MonitoringOverviewTable({
overflowX: "auto",
overflowY: "hidden",
}
: undefined
: {
maxWidth: "100%",
overflowX: "auto",
}
}
>
<Table
size="small"
sx={{
minWidth: 1280,
minWidth: 1500,
tableLayout: "fixed",
"& .MuiTableCell-root": {
px: 1.1,
@@ -350,6 +355,7 @@ export function MonitoringOverviewTable({
<TableHead>
<TableRow>
<TableCell
sx={{ width: 260 }}
sortDirection={sortKey === "machine" ? sortDirection : false}
>
<TableSortLabel
@@ -361,6 +367,7 @@ export function MonitoringOverviewTable({
</TableSortLabel>
</TableCell>
<TableCell
sx={{ width: 90 }}
sortDirection={sortKey === "mode" ? sortDirection : false}
>
<TableSortLabel
@@ -372,6 +379,7 @@ export function MonitoringOverviewTable({
</TableSortLabel>
</TableCell>
<TableCell
sx={{ width: 120 }}
sortDirection={sortKey === "status" ? sortDirection : false}
>
<TableSortLabel
@@ -382,103 +390,33 @@ export function MonitoringOverviewTable({
Status
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "cpu" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "cpu"}
direction={sortKey === "cpu" ? sortDirection : "asc"}
onClick={() => setSort("cpu")}
{[
["cpu", "CPU"],
["iowait", "IO wait"],
["mem", "RAM"],
["net_rx", "Net down"],
["net_tx", "Net up"],
["disk_read", "Disk read"],
["disk_write", "Disk write"],
["disk_used", "Disk used"],
].map(([key, label]) => (
<TableCell
key={key}
align="center"
sx={{ width: 150 }}
sortDirection={sortKey === key ? sortDirection : false}
>
CPU
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "iowait" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "iowait"}
direction={sortKey === "iowait" ? sortDirection : "asc"}
onClick={() => setSort("iowait")}
>
IO wait
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "mem" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "mem"}
direction={sortKey === "mem" ? sortDirection : "asc"}
onClick={() => setSort("mem")}
>
RAM
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "net_rx" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "net_rx"}
direction={sortKey === "net_rx" ? sortDirection : "asc"}
onClick={() => setSort("net_rx")}
>
Net down
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "net_tx" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "net_tx"}
direction={sortKey === "net_tx" ? sortDirection : "asc"}
onClick={() => setSort("net_tx")}
>
Net up
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "disk_read" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "disk_read"}
direction={sortKey === "disk_read" ? sortDirection : "asc"}
onClick={() => setSort("disk_read")}
>
Disk read
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "disk_write" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "disk_write"}
direction={sortKey === "disk_write" ? sortDirection : "asc"}
onClick={() => setSort("disk_write")}
>
Disk write
</TableSortLabel>
</TableCell>
<TableCell
align="right"
sortDirection={sortKey === "disk_used" ? sortDirection : false}
>
<TableSortLabel
active={sortKey === "disk_used"}
direction={sortKey === "disk_used" ? sortDirection : "asc"}
onClick={() => setSort("disk_used")}
>
Disk used
</TableSortLabel>
</TableCell>
<TableSortLabel
active={sortKey === key}
direction={sortKey === key ? sortDirection : "asc"}
onClick={() => setSort(key as SortKey)}
>
{label}
</TableSortLabel>
</TableCell>
))}
<TableCell
sx={{ width: 180 }}
sortDirection={sortKey === "updated" ? sortDirection : false}
>
<TableSortLabel
@@ -526,16 +464,39 @@ export function MonitoringOverviewTable({
row.disk_write_summary,
formatRate,
);
const disk = row.disk
? {
value: row.disk.used_pct,
min: `Used ${formatBytes(row.disk.used)}`,
max: `Avail ${formatBytes(row.disk.available)}`,
}
: null;
const updated = formatUpdatedDetails(
row.latest_sample?.ts ?? null,
);
return (
<TableRow key={machine.id}>
<TableCell>
<Stack spacing={0.25}>
<TableCell sx={{ width: 260 }}>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Stack
direction="row"
spacing={0.75}
sx={{ alignItems: "center", flexWrap: "wrap" }}
sx={{
alignItems: "center",
flexWrap: "nowrap",
minWidth: 0,
}}
>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
<Typography
variant="body2"
sx={{
fontWeight: 700,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{machine.name}
</Typography>
{!machine.enabled && (
@@ -546,15 +507,24 @@ export function MonitoringOverviewTable({
/>
)}
</Stack>
<Typography variant="caption" color="text.secondary">
<Typography
variant="caption"
color="text.secondary"
sx={{
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{machine.mode === "local"
? "Local API host"
: `${machine.username || "user"}@${machine.host || "host"}:${machine.port}`}
</Typography>
</Stack>
</TableCell>
<TableCell>{machine.mode}</TableCell>
<TableCell>
<TableCell sx={{ width: 90 }}>{machine.mode}</TableCell>
<TableCell sx={{ width: 120 }}>
<Chip
size="small"
variant="outlined"
@@ -563,39 +533,68 @@ export function MonitoringOverviewTable({
/>
</TableCell>
<TableCell align="center">
{metricCell(cpu.value, cpu.subtext)}
<MetricCell
value={cpu.value}
min={cpu.min}
max={cpu.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(iowait.value, iowait.subtext)}
<MetricCell
value={iowait.value}
min={iowait.min}
max={iowait.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(mem.value, mem.subtext)}
<MetricCell
value={mem.value}
min={mem.min}
max={mem.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(netDown.value, netDown.subtext)}
<MetricCell
value={netDown.value}
min={netDown.min}
max={netDown.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(netUp.value, netUp.subtext)}
<MetricCell
value={netUp.value}
min={netUp.min}
max={netUp.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(diskRead.value, diskRead.subtext)}
<MetricCell
value={diskRead.value}
min={diskRead.min}
max={diskRead.max}
/>
</TableCell>
<TableCell align="center">
{metricCell(diskWrite.value, diskWrite.subtext)}
<MetricCell
value={diskWrite.value}
min={diskWrite.min}
max={diskWrite.max}
/>
</TableCell>
<TableCell align="center">
{row.disk
? metricCell(
row.disk.used_pct,
`Used ${formatBytes(row.disk.used)}\nAvail ${formatBytes(row.disk.available)}`,
)
: "-"}
{disk ? (
<MetricCell
value={disk.value}
min={disk.min}
max={disk.max}
/>
) : (
"-"
)}
</TableCell>
<TableCell>
<Stack spacing={0.15} sx={{ alignItems: "center" }}>
{formatUpdatedDetails(
row.latest_sample?.ts ?? null,
).map((line) => (
<TableCell sx={{ width: 180 }}>
<Stack spacing={0.1} sx={{ alignItems: "center" }}>
{updated.map((line) => (
<Typography
key={line}
variant="caption"