setHoverIdx(null)}
>
@@ -133,20 +172,21 @@ function Sparkline({ buckets }: { buckets: TimeseriesBucket[] }) {
aria-hidden
/>
{formatRelative(hovered.timestamp)}
- {formatCount(hovered.count)} runs · {formatCount(hovered.failure)} failed
+ {formatCount(hovered.count)} runs ·{" "}
+ {formatCount(hovered.failure)} failed
>
) : null}
-
+
{startLabel}
{midLabel}
- {endLabel}
+ now
);
diff --git a/dashboard/src/features/overview/components/workers-card.tsx b/dashboard/src/features/overview/components/workers-card.tsx
new file mode 100644
index 00000000..3a415c10
--- /dev/null
+++ b/dashboard/src/features/overview/components/workers-card.tsx
@@ -0,0 +1,71 @@
+import { Server, Skull } from "lucide-react";
+import { Badge, Card, EmptyState, Skeleton } from "@/components/ui";
+import { isWorkerStale } from "@/features/workers/utils";
+import type { Worker } from "@/lib/api-types";
+import { formatRelative } from "@/lib/time";
+
+interface WorkersCardProps {
+ workers: Worker[] | undefined;
+ loading: boolean;
+}
+
+/** A compact worker roster for the Overview — id, queues, and liveness. */
+export function WorkersCard({ workers, loading }: WorkersCardProps) {
+ if (loading && !workers) return
;
+
+ const list = workers ?? [];
+ if (list.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {list.slice(0, 6).map((w) => {
+ const stale = isWorkerStale(w);
+ const queues = w.queues
+ .split(",")
+ .map((q) => q.trim())
+ .filter(Boolean)
+ .join(" · ");
+ return (
+
+
+ {stale ? : }
+
+
+
+ {w.worker_id}
+
+
+ {queues || "no queues"}
+
+
+ {stale ? (
+
+ Stale
+
+ ) : (
+
+ {formatRelative(w.last_heartbeat)}
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/dashboard/src/features/workers/index.ts b/dashboard/src/features/workers/index.ts
index a234113b..fc82a009 100644
--- a/dashboard/src/features/workers/index.ts
+++ b/dashboard/src/features/workers/index.ts
@@ -1,2 +1,3 @@
export * from "./components";
export * from "./hooks";
+export * from "./utils";
diff --git a/dashboard/src/features/workers/utils.ts b/dashboard/src/features/workers/utils.ts
new file mode 100644
index 00000000..78260cb9
--- /dev/null
+++ b/dashboard/src/features/workers/utils.ts
@@ -0,0 +1,13 @@
+import type { Worker } from "@/lib/api-types";
+
+/** A worker is "stale" once its last heartbeat is older than this. */
+export const WORKER_STALE_AFTER_MS = 30_000;
+
+/**
+ * Whether a worker has missed its heartbeat window. Computed against a
+ * caller-supplied `now` so callers that re-render on a clock tick keep the
+ * status fresh (the heartbeat ages relative to wall-clock, not a frozen ref).
+ */
+export function isWorkerStale(worker: Worker, now: number = Date.now()): boolean {
+ return now - worker.last_heartbeat > WORKER_STALE_AFTER_MS;
+}
diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx
index 54bd3686..c60e468d 100644
--- a/dashboard/src/routes/index.tsx
+++ b/dashboard/src/routes/index.tsx
@@ -1,9 +1,11 @@
-import { createFileRoute } from "@tanstack/react-router";
-import { PageHeader } from "@/components/layout";
-import { Separator } from "@/components/ui";
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { ChevronRight, RefreshCw } from "lucide-react";
+import { PageHeader, SectionHeading } from "@/components/layout";
+import { Badge, Button } from "@/components/ui";
import {
+ BusiestQueues,
+ Pulse,
pausedQueuesQuery,
- QueueBreakdown,
queueStatsQuery,
RecentJobs,
recentJobsQuery,
@@ -16,7 +18,10 @@ import {
useRecentJobs,
useStats,
useThroughput,
+ WorkersCard,
} from "@/features/overview";
+import { useWorkers, workersQuery } from "@/features/workers";
+import { isWorkerStale } from "@/features/workers/utils";
export const Route = createFileRoute("/")({
loader: ({ context: { queryClient } }) =>
@@ -26,6 +31,7 @@ export const Route = createFileRoute("/")({
queryClient.ensureQueryData(pausedQueuesQuery()),
queryClient.ensureQueryData(recentJobsQuery(10)),
queryClient.ensureQueryData(throughputQuery(60, 3600)),
+ queryClient.ensureQueryData(workersQuery()),
]),
component: OverviewPage,
});
@@ -36,61 +42,96 @@ function OverviewPage() {
const paused = usePausedQueues();
const jobs = useRecentJobs(10);
const throughput = useThroughput(60, 3600);
+ const workers = useWorkers();
+
+ const refreshAll = () => {
+ stats.refetch();
+ queueStats.refetch();
+ paused.refetch();
+ jobs.refetch();
+ throughput.refetch();
+ workers.refetch();
+ };
+
+ const onlineWorkers = (workers.data ?? []).filter((w) => !isWorkerStale(w)).length;
return (
<>
+
+ Refresh
+
+ }
/>
-
-
- stats.refetch()}
- />
-
+
+
-
- throughput.refetch()}
- />
-
+
stats.refetch()}
+ />
-
+ throughput.refetch()}
+ />
-
-
-
Queues
-
- queueStats.refetch()}
- />
-
+
+
+
+
+ View all
+
+
+
+ }
+ />
+
+
+
+
+ {onlineWorkers} online
+
+ }
+ />
+
+
+
-
-
-
Recent jobs
-
+
+
+
+ Open Jobs
+
+
+
+ }
+ />
Date: Sun, 31 May 2026 13:29:37 +0530
Subject: [PATCH 04/17] feat(dashboard): restyle Jobs page
---
dashboard/src/features/jobs/components/job-filters.tsx | 2 +-
dashboard/src/features/jobs/components/job-table.tsx | 9 ++-------
2 files changed, 3 insertions(+), 8 deletions(-)
diff --git a/dashboard/src/features/jobs/components/job-filters.tsx b/dashboard/src/features/jobs/components/job-filters.tsx
index b8820362..167d7905 100644
--- a/dashboard/src/features/jobs/components/job-filters.tsx
+++ b/dashboard/src/features/jobs/components/job-filters.tsx
@@ -96,7 +96,7 @@ export function JobFiltersBar({ filters, onChange, className }: JobFiltersBarPro
return (
diff --git a/dashboard/src/features/jobs/components/job-table.tsx b/dashboard/src/features/jobs/components/job-table.tsx
index 37274dc9..8e2eecf7 100644
--- a/dashboard/src/features/jobs/components/job-table.tsx
+++ b/dashboard/src/features/jobs/components/job-table.tsx
@@ -2,17 +2,16 @@ import { useNavigate } from "@tanstack/react-router";
import type { ColumnDef } from "@tanstack/react-table";
import { useCallback, useMemo } from "react";
import {
- Badge,
DataTable,
EmptyState,
ErrorState,
+ StatusBadge,
TableSkeleton,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui";
import type { Job } from "@/lib/api-types";
-import { JOB_STATUS_LABEL, JOB_STATUS_TONE } from "@/lib/status";
import { formatAbsolute, formatRelative } from "@/lib/time";
interface JobTableProps {
@@ -56,11 +55,7 @@ export function JobTable({ jobs, loading, error, onRetry }: JobTableProps) {
{
accessorKey: "status",
header: "Status",
- cell: ({ row }) => (
-
- {JOB_STATUS_LABEL[row.original.status]}
-
- ),
+ cell: ({ row }) =>
,
},
{
accessorKey: "retry_count",
From 207b1e0d8356e1f84394a95993a0d47eced56bf2 Mon Sep 17 00:00:00 2001
From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com>
Date: Sun, 31 May 2026 13:29:47 +0530
Subject: [PATCH 05/17] feat(dashboard): restyle Queues and Workers pages
---
.../queues/components/queues-table.tsx | 54 +++++++++++----
.../workers/components/workers-table.tsx | 69 ++++++++++---------
dashboard/src/routes/queues.tsx | 18 ++---
dashboard/src/routes/workers.tsx | 41 +++++------
4 files changed, 106 insertions(+), 76 deletions(-)
diff --git a/dashboard/src/features/queues/components/queues-table.tsx b/dashboard/src/features/queues/components/queues-table.tsx
index a2a4f21e..96eaede1 100644
--- a/dashboard/src/features/queues/components/queues-table.tsx
+++ b/dashboard/src/features/queues/components/queues-table.tsx
@@ -1,7 +1,15 @@
import type { ColumnDef } from "@tanstack/react-table";
import { Box, Pause, Play } from "lucide-react";
import { useMemo } from "react";
-import { Badge, Button, DataTable, EmptyState, ErrorState, TableSkeleton } from "@/components/ui";
+import {
+ Badge,
+ Button,
+ DataTable,
+ EmptyState,
+ ErrorState,
+ QueueBar,
+ TableSkeleton,
+} from "@/components/ui";
import type { QueueStatsMap } from "@/lib/api-types";
import { formatCount } from "@/lib/number";
import { usePauseQueue, useResumeQueue } from "../hooks";
@@ -25,6 +33,8 @@ interface QueuesTableProps {
onRetry: () => void;
}
+const NUM_CELL = "block w-full text-right font-mono text-[0.82rem] tabular-nums";
+
export function QueuesTable({ stats, paused, loading, error, onRetry }: QueuesTableProps) {
const rows = useMemo
(() => {
if (!stats) return [];
@@ -55,29 +65,43 @@ export function QueuesTable({ stats, paused, loading, error, onRetry }: QueuesTa
cell: ({ row }) => (
{row.original.name}
- {row.original.paused ? Paused : null}
+ {row.original.paused ? (
+
+ Paused
+
+ ) : null}
),
},
+ {
+ id: "mix",
+ header: "Mix",
+ cell: ({ row }) => (
+
+ ),
+ },
{
accessorKey: "pending",
header: "Pending",
- cell: ({ getValue }) => (
- {formatCount(getValue())}
- ),
+ cell: ({ getValue }) => {formatCount(getValue())},
},
{
accessorKey: "running",
header: "Running",
cell: ({ getValue }) => (
- {formatCount(getValue())}
+ {formatCount(getValue())}
),
},
{
accessorKey: "completed",
header: "Completed",
cell: ({ getValue }) => (
-
+
{formatCount(getValue())}
),
@@ -88,9 +112,7 @@ export function QueuesTable({ stats, paused, loading, error, onRetry }: QueuesTa
cell: ({ getValue }) => {
const total = getValue();
return (
- 0 ? "text-danger" : "text-[var(--fg-muted)]"}`}
- >
+ 0 ? "text-danger" : "text-[var(--fg-muted)]"}`}>
{formatCount(total)}
);
@@ -100,7 +122,9 @@ export function QueuesTable({ stats, paused, loading, error, onRetry }: QueuesTa
id: "actions",
header: "",
cell: ({ row }) => (
-
+
),
},
],
@@ -114,7 +138,9 @@ export function QueuesTable({ stats, paused, loading, error, onRetry }: QueuesTa
}
if (loading && rows.length === 0) {
- return ;
+ return (
+
+ );
}
if (rows.length === 0) {
@@ -141,7 +167,7 @@ function PauseResumeCell({ name, paused }: { name: string; paused: boolean }) {
if (paused) {
return (