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 (
{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([]); 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 ? : }
); }