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 {