diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 734ea35..2647b0f 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -5,12 +5,10 @@ import { toast } from "sonner"; import { api } from "../api"; import { eventTitle } from "../constants"; -import { DateTime } from "../components/DateTime"; import { EmptyState } from "../components/EmptyState"; import { StatCard } from "../components/StatCard"; -import { StatusBadge } from "../components/StatusBadge"; import { StatusIcon } from "../components/StatusIcon"; -import type { Health, Job, JobEvent, JobStats } from "../types"; +import type { Health, Job, JobEvent, JobStats, JobStatus } from "../types"; const emptyStats: JobStats = { total: 0, @@ -20,6 +18,39 @@ const emptyStats: JobStats = { oldest_queued_at: null }; +function BarMetric({ label, value, max, tone = "neutral" }: { label: string; value: number; max: number; tone?: string }) { + const width = max > 0 ? Math.max(4, Math.round((value / max) * 100)) : 0; + return ( +
+ {label} +
+ +
+ {value} +
+ ); +} + +function MiniHistogram({ data }: { data: Array<{ label: string; value: number; tone: string }> }) { + const max = Math.max(...data.map((item) => item.value), 1); + return ( +
+ {data.map((item) => { + const height = item.value > 0 ? Math.max(12, Math.round((item.value / max) * 100)) : 4; + return ( +
+
+ +
+ {item.label} + {item.value} +
+ ); + })} +
+ ); +} + export function DashboardPage() { const [jobs, setJobs] = useState([]); const [events, setEvents] = useState([]); @@ -30,7 +61,7 @@ export function DashboardPage() { const [jobsData, statsData, eventsData, healthData] = await Promise.all([ api.listJobs(), api.getStats(), - api.listEvents({ limit: 8 }), + api.listEvents({ limit: 50 }), api.health() ]); setJobs(jobsData); @@ -45,7 +76,40 @@ export function DashboardPage() { return () => window.clearInterval(id); }, [refresh]); - const recentJobs = useMemo(() => jobs.slice(0, 6), [jobs]); + const recentJobs = useMemo(() => jobs.slice(0, 20), [jobs]); + const recentJobStatusData = useMemo(() => { + const counts: Record = { queued: 0, running: 0, succeeded: 0, failed: 0 }; + recentJobs.forEach((job) => { + counts[job.status] += 1; + }); + return [ + { label: "Queued", value: counts.queued, tone: "queued" }, + { label: "Running", value: counts.running, tone: "running" }, + { label: "Succeeded", value: counts.succeeded, tone: "succeeded" }, + { label: "Failed", value: counts.failed, tone: "failed" } + ]; + }, [recentJobs]); + const pressureData = useMemo( + () => [ + { label: "Queued", value: stats.by_status.queued, tone: "queued" }, + { label: "Running", value: stats.by_status.running, tone: "running" }, + { label: "Retry backoff", value: stats.retries_pending, tone: "orange" }, + { label: "Overdue locks", value: stats.overdue_running, tone: "failed" }, + { label: "Failed", value: stats.by_status.failed, tone: "failed" } + ], + [stats] + ); + const eventData = useMemo(() => { + const counts = events.reduce>((accumulator, event) => { + accumulator[event.type] = (accumulator[event.type] ?? 0) + 1; + return accumulator; + }, {}); + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 6) + .map(([type, value]) => ({ label: eventTitle(type), value, tone: type.includes("fail") ? "failed" : type.includes("success") ? "succeeded" : "running" })); + }, [events]); + const maxPressure = Math.max(...pressureData.map((item) => item.value), 1); return (
@@ -74,16 +138,18 @@ export function DashboardPage() { View jobs
-
- {recentJobs.map((job) => ( - - - {job.type} - - - ))} - {!recentJobs.length && } -
+ {recentJobs.length ? ( + <> + +
+ {recentJobs.slice(0, 16).map((job) => ( + + ))} +
+ + ) : ( + + )}
@@ -91,22 +157,13 @@ export function DashboardPage() {

Queue Pressure

-
-
- Retries pending - {stats.retries_pending} - backoff -
-
- Oldest queued - {stats.oldest_queued_at ? "waiting" : "none"} - -
-
- Failed jobs - {stats.by_status.failed} - retryable -
+
+ {pressureData.map((item) => ( + + ))} +
+
+ {stats.oldest_queued_at ? "Oldest queued job is waiting" : "No queued backlog"}
@@ -115,16 +172,7 @@ export function DashboardPage() {

Recent Events

-
- {events.map((event) => ( - - {eventTitle(event.type)} - {event.attempt ? `attempt ${event.attempt}` : "new"} - - - ))} - {!events.length && } -
+ {eventData.length ? : } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ed14cca..d6b5868 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -611,6 +611,145 @@ label { background: var(--green); } +.histogram { + align-items: end; + display: grid; + gap: 10px; + grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); + min-height: 178px; +} + +.histogram-item { + align-items: center; + display: grid; + gap: 7px; + justify-items: center; + min-width: 0; +} + +.histogram-column { + align-items: end; + background: var(--panel-raised); + border: 1px solid var(--border); + border-radius: 6px; + display: flex; + height: 112px; + overflow: hidden; + width: 100%; +} + +.histogram-column i, +.metric-track i { + background: var(--accent); + display: block; +} + +.histogram-column i { + border-radius: 6px 6px 0 0; + width: 100%; +} + +.histogram-column i.queued, +.histogram-column i.running, +.metric-track i.queued, +.metric-track i.running { + background: var(--cyan); +} + +.histogram-column i.succeeded, +.metric-track i.succeeded { + background: var(--green); +} + +.histogram-column i.failed, +.metric-track i.failed { + background: var(--red); +} + +.histogram-column i.orange, +.metric-track i.orange { + background: var(--orange); +} + +.histogram-item span { + color: var(--muted); + font-size: 12px; + font-weight: 900; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.histogram-item strong { + font-size: 18px; +} + +.job-tick-chart { + display: grid; + gap: 5px; + grid-template-columns: repeat(16, minmax(8px, 1fr)); + margin-top: 14px; +} + +.job-tick { + background: var(--subtle); + border-radius: 5px; + display: block; + height: 32px; +} + +.job-tick.running { + background: var(--cyan); +} + +.job-tick.succeeded { + background: var(--green); +} + +.job-tick.failed { + background: var(--red); +} + +.metric-bars { + display: grid; + gap: 12px; +} + +.metric-bar-row { + align-items: center; + display: grid; + gap: 10px; + grid-template-columns: minmax(100px, 130px) minmax(120px, 1fr) 42px; +} + +.metric-bar-row span { + color: var(--muted); + font-size: 13px; + font-weight: 900; +} + +.metric-track { + background: var(--panel-raised); + border: 1px solid var(--border); + border-radius: 999px; + height: 14px; + overflow: hidden; +} + +.metric-track i { + border-radius: inherit; + height: 100%; +} + +.dashboard-footnote { + color: var(--muted); + font-size: 12px; + font-weight: 800; + margin-top: 12px; +} + .bar-list, .compact-list, .modal-form {