Phase 2: Docker and OIDC auth
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { useCounts, useLibraries, useNowPlaying } from "../hooks/useDashboard";
|
||||
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 { NowPlaying } from "../components/NowPlaying";
|
||||
import { MetricCard } from "../components/MetricCard";
|
||||
@@ -20,96 +23,223 @@ 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),
|
||||
};
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { data: counts } = useCounts();
|
||||
const { data: libraries } = useLibraries();
|
||||
const { data: nowPlaying } = useNowPlaying();
|
||||
const { data: activity } = useActivity();
|
||||
const { data: metrics } = useMonitoringMetrics();
|
||||
const { data: disk } = useDiskSpace();
|
||||
|
||||
const latest = metrics?.samples?.at(-1);
|
||||
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),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Now Playing */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">Now playing</h2>
|
||||
{nowPlaying && <NowPlaying sessions={nowPlaying} />}
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Server Overview */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">Server overview</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-3">
|
||||
<MetricCard
|
||||
label="CPU"
|
||||
value={latest ? `${latest.cpu_pct.toFixed(1)}%` : "-"}
|
||||
<Stack spacing={3}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Activity
|
||||
</Typography>
|
||||
{activity && (
|
||||
<NowPlaying
|
||||
sessions={activity}
|
||||
onSelectSession={(session) =>
|
||||
navigate(`/users?user=${encodeURIComponent(session.user)}`)
|
||||
}
|
||||
/>
|
||||
<MetricCard
|
||||
label="IO Wait"
|
||||
value={latest ? `${(latest.iowait_pct ?? 0).toFixed(1)}%` : "-"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="RAM"
|
||||
value={latest ? `${latest.mem_pct.toFixed(1)}%` : "-"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Net down"
|
||||
value={latest ? formatRate(latest.net_rx_bytes_per_sec) : "-"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Net up"
|
||||
value={latest ? formatRate(latest.net_tx_bytes_per_sec) : "-"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Disk read"
|
||||
value={latest ? formatRate(latest.disk_read_bps) : "-"}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Disk write"
|
||||
value={latest ? formatRate(latest.disk_write_bps) : "-"}
|
||||
/>
|
||||
</div>
|
||||
{disk && (
|
||||
<div className="mt-3 grid grid-cols-4 gap-3">
|
||||
<MetricCard label="Disk used" value={formatBytes(disk.used)} />
|
||||
<MetricCard
|
||||
label="Disk available"
|
||||
value={formatBytes(disk.available)}
|
||||
/>
|
||||
<MetricCard label="Disk total" value={formatBytes(disk.size)} />
|
||||
<MetricCard label="Used %" value={disk.used_pct} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Box>
|
||||
|
||||
<hr />
|
||||
<Divider />
|
||||
|
||||
{/* Media Library Overview */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-3">Media library overview</h2>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Monitoring Overview
|
||||
</Typography>
|
||||
<Grid container spacing={1.5}>
|
||||
<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 && (
|
||||
<Grid container spacing={1.5} sx={{ mt: 0.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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 1.5 }}>
|
||||
Library Stats
|
||||
</Typography>
|
||||
{counts && (
|
||||
<div className="grid grid-cols-4 gap-3 mb-4">
|
||||
<MetricCard
|
||||
label="Total"
|
||||
value={(
|
||||
counts.movies +
|
||||
counts.series +
|
||||
counts.episodes
|
||||
).toLocaleString()}
|
||||
/>
|
||||
<MetricCard label="Movies" value={counts.movies.toLocaleString()} />
|
||||
<MetricCard label="Series" value={counts.series.toLocaleString()} />
|
||||
<MetricCard
|
||||
label="Episodes"
|
||||
value={counts.episodes.toLocaleString()}
|
||||
/>
|
||||
</div>
|
||||
<Grid container spacing={1.5} sx={{ mb: 2 }}>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Total"
|
||||
value={(
|
||||
counts.movies +
|
||||
counts.series +
|
||||
counts.episodes
|
||||
).toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Movies"
|
||||
value={counts.movies.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Series"
|
||||
value={counts.series.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
label="Episodes"
|
||||
value={counts.episodes.toLocaleString()}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{libraries && <LibraryOverview libraries={libraries} />}
|
||||
</section>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user