import { AlertTriangle, BriefcaseBusiness, Clock3, RotateCcw } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { api } from "../api";
import { eventTitle } from "../constants";
import { EmptyState } from "../components/EmptyState";
import { StatCard } from "../components/StatCard";
import { StatusIcon } from "../components/StatusIcon";
import type { Health, Job, JobEvent, JobStats, JobStatus } from "../types";
const emptyStats: JobStats = {
total: 0,
by_status: { queued: 0, running: 0, succeeded: 0, failed: 0 },
overdue_running: 0,
retries_pending: 0,
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 (
);
}
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([]);
const [stats, setStats] = useState(emptyStats);
const [health, setHealth] = useState(null);
const refresh = useCallback(async () => {
const [jobsData, statsData, eventsData, healthData] = await Promise.all([
api.listJobs(),
api.getStats(),
api.listEvents({ limit: 50 }),
api.health()
]);
setJobs(jobsData);
setStats({ ...emptyStats, ...statsData, by_status: { ...emptyStats.by_status, ...statsData.by_status } });
setEvents(eventsData.results);
setHealth(healthData);
}, []);
useEffect(() => {
void refresh().catch((caught) => toast.error(caught instanceof Error ? caught.message : String(caught)));
const id = window.setInterval(() => void refresh().catch(() => undefined), 2000);
return () => window.clearInterval(id);
}, [refresh]);
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 (
Overview
Dashboard
DB {health?.database ?? "unknown"}
} />
} />
} />
} />
} />
} />
Recent Jobs
View jobs
{recentJobs.length ? (
<>
{recentJobs.slice(0, 16).map((job) => (
))}
>
) : (
)}
Queue Pressure
{pressureData.map((item) => (
))}
{stats.oldest_queued_at ? "Oldest queued job is waiting" : "No queued backlog"}
Recent Events
{eventData.length ? : }
);
}