From 3f7d4d078877eaf1c36bdfe1456ddf1c393990cb Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:26:14 +0530 Subject: [PATCH 1/4] fix: make dashboard charts and animations light-theme aware Canvas charts now read the theme signal and use appropriate colors for grid lines, axis labels, and gradients in both dark and light mode. Shimmer skeleton animation has separate light-mode gradient using slate colors instead of dark surface variables. --- dashboard/src/charts/throughput-chart.tsx | 20 ++++++++++++++------ dashboard/src/charts/timeseries-chart.tsx | 18 ++++++++++++------ dashboard/src/index.css | 8 +++++++- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/dashboard/src/charts/throughput-chart.tsx b/dashboard/src/charts/throughput-chart.tsx index ea9ec3f..afc42da 100644 --- a/dashboard/src/charts/throughput-chart.tsx +++ b/dashboard/src/charts/throughput-chart.tsx @@ -1,5 +1,6 @@ import { TrendingUp } from "lucide-preact"; import { useEffect, useRef } from "preact/hooks"; +import { theme } from "../hooks"; interface ThroughputChartProps { data: number[]; @@ -15,6 +16,13 @@ export function ThroughputChart({ data }: ThroughputChartProps) { const ctx = canvas.getContext("2d"); if (!ctx) return; + const isDark = theme.value === "dark"; + const gridColor = isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.06)"; + const textColor = isDark ? "rgba(139,149,165,0.5)" : "rgba(100,116,139,0.7)"; + const placeholderColor = isDark ? "rgba(139,149,165,0.4)" : "rgba(100,116,139,0.5)"; + const areaTop = isDark ? "rgba(34,197,94,0.2)" : "rgba(34,197,94,0.12)"; + const areaBottom = isDark ? "rgba(34,197,94,0.01)" : "rgba(34,197,94,0.02)"; + const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; @@ -26,7 +34,7 @@ export function ThroughputChart({ data }: ThroughputChartProps) { ctx.clearRect(0, 0, w, h); if (data.length < 2) { - ctx.fillStyle = "rgba(139,149,165,0.4)"; + ctx.fillStyle = placeholderColor; ctx.font = "12px -apple-system, sans-serif"; ctx.textAlign = "center"; ctx.fillText("Collecting data\u2026", w / 2, h / 2); @@ -41,13 +49,13 @@ export function ThroughputChart({ data }: ThroughputChartProps) { // Grid lines for (let i = 0; i <= 4; i++) { const y = pad.top + ch * (1 - i / 4); - ctx.strokeStyle = "rgba(255,255,255,0.04)"; + ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke(); - ctx.fillStyle = "rgba(139,149,165,0.5)"; + ctx.fillStyle = textColor; ctx.font = "10px -apple-system, sans-serif"; ctx.textAlign = "right"; ctx.fillText(((max * i) / 4).toFixed(1), pad.left - 6, y + 3); @@ -55,8 +63,8 @@ export function ThroughputChart({ data }: ThroughputChartProps) { // Gradient fill const gradient = ctx.createLinearGradient(0, pad.top, 0, pad.top + ch); - gradient.addColorStop(0, "rgba(34,197,94,0.2)"); - gradient.addColorStop(1, "rgba(34,197,94,0.01)"); + gradient.addColorStop(0, areaTop); + gradient.addColorStop(1, areaBottom); ctx.beginPath(); ctx.moveTo(pad.left, pad.top + ch); @@ -96,7 +104,7 @@ export function ThroughputChart({ data }: ThroughputChartProps) { ctx.lineWidth = 2; ctx.stroke(); } - }, [data]); + }, [data, theme.value]); const current = data.length > 0 ? data[data.length - 1] : 0; diff --git a/dashboard/src/charts/timeseries-chart.tsx b/dashboard/src/charts/timeseries-chart.tsx index f1a8ef6..18d7752 100644 --- a/dashboard/src/charts/timeseries-chart.tsx +++ b/dashboard/src/charts/timeseries-chart.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "preact/hooks"; -import type { TimeseriesBucket } from "../api/types"; +import type { TimeseriesBucket } from "../api"; +import { theme } from "../hooks"; interface TimeseriesChartProps { data: TimeseriesBucket[]; @@ -23,10 +24,15 @@ export function TimeseriesChart({ data }: TimeseriesChartProps) { const w = rect.width; const h = rect.height; + const isDark = theme.value === "dark"; + const gridColor = isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.06)"; + const textColor = isDark ? "rgba(139,149,165,0.5)" : "rgba(100,116,139,0.7)"; + const placeholderColor = isDark ? "rgba(139,149,165,0.4)" : "rgba(100,116,139,0.5)"; + ctx.clearRect(0, 0, w, h); if (!data.length) { - ctx.fillStyle = "rgba(139,149,165,0.4)"; + ctx.fillStyle = placeholderColor; ctx.font = "12px -apple-system, sans-serif"; ctx.textAlign = "center"; ctx.fillText("No timeseries data", w / 2, h / 2); @@ -44,13 +50,13 @@ export function TimeseriesChart({ data }: TimeseriesChartProps) { // Y-axis grid for (let i = 0; i <= 4; i++) { const y = pad.top + ch * (1 - i / 4); - ctx.strokeStyle = "rgba(255,255,255,0.04)"; + ctx.strokeStyle = gridColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke(); - ctx.fillStyle = "rgba(139,149,165,0.5)"; + ctx.fillStyle = textColor; ctx.font = "10px -apple-system, sans-serif"; ctx.textAlign = "right"; ctx.fillText(Math.round((maxCount * i) / 4).toString(), pad.left - 6, y + 3); @@ -79,7 +85,7 @@ export function TimeseriesChart({ data }: TimeseriesChartProps) { }); // X-axis timestamps - ctx.fillStyle = "rgba(139,149,165,0.5)"; + ctx.fillStyle = textColor; ctx.font = "10px -apple-system, sans-serif"; ctx.textAlign = "center"; const labelCount = Math.min(6, data.length); @@ -90,7 +96,7 @@ export function TimeseriesChart({ data }: TimeseriesChartProps) { const x = pad.left + idx * (barW + gap) + barW / 2; ctx.fillText(label, x, h - 10); } - }, [data]); + }, [data, theme.value]); return (
diff --git a/dashboard/src/index.css b/dashboard/src/index.css index 7469947..3954449 100644 --- a/dashboard/src/index.css +++ b/dashboard/src/index.css @@ -109,7 +109,7 @@ html:not(.dark) body { animation: fadeIn 0.2s ease-out; } -.animate-shimmer { +.dark .animate-shimmer { background: linear-gradient( 90deg, var(--color-surface-3) 25%, @@ -120,6 +120,12 @@ html:not(.dark) body { animation: shimmer 1.5s infinite; } +html:not(.dark) .animate-shimmer { + background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + /* Monospace */ .font-mono { font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", monospace; From bb824e3d10e8f493f300cf472f8f9ff3d8e011de Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:26:24 +0530 Subject: [PATCH 2/4] feat: add error state component and display API errors on all pages New ErrorState component with WifiOff icon, error message, and Retry button. All 11 page components now check the error return from useApi and show ErrorState when the backend is unreachable. Previously, failed API calls resulted in blank pages with no feedback. --- dashboard/src/components/ui/error-state.tsx | 25 ++++++++++++ dashboard/src/pages/circuit-breakers.tsx | 15 +++----- dashboard/src/pages/dead-letters.tsx | 27 +++++++------ dashboard/src/pages/job-detail.tsx | 37 +++++++++++------- dashboard/src/pages/jobs.tsx | 42 ++++++++++++--------- dashboard/src/pages/logs.tsx | 26 ++++++------- dashboard/src/pages/metrics.tsx | 23 ++++++----- dashboard/src/pages/overview.tsx | 32 ++++++++++------ dashboard/src/pages/queues.tsx | 25 ++++++------ dashboard/src/pages/resources.tsx | 14 +++---- dashboard/src/pages/system.tsx | 33 ++++++++++------ dashboard/src/pages/workers.tsx | 13 +++---- 12 files changed, 188 insertions(+), 124 deletions(-) create mode 100644 dashboard/src/components/ui/error-state.tsx diff --git a/dashboard/src/components/ui/error-state.tsx b/dashboard/src/components/ui/error-state.tsx new file mode 100644 index 0000000..a6b44a1 --- /dev/null +++ b/dashboard/src/components/ui/error-state.tsx @@ -0,0 +1,25 @@ +import { RefreshCw, WifiOff } from "lucide-preact"; +import { Button } from "./button"; + +interface ErrorStateProps { + message: string; + onRetry?: () => void; +} + +export function ErrorState({ message, onRetry }: ErrorStateProps) { + return ( +
+
+ +
+

Unable to load data

+

{message}

+ {onRetry && ( + + )} +
+ ); +} diff --git a/dashboard/src/pages/circuit-breakers.tsx b/dashboard/src/pages/circuit-breakers.tsx index a02b20f..78161be 100644 --- a/dashboard/src/pages/circuit-breakers.tsx +++ b/dashboard/src/pages/circuit-breakers.tsx @@ -1,12 +1,8 @@ import { ShieldAlert } from "lucide-preact"; -import type { CircuitBreaker as CBType } from "../api/types"; -import { Badge } from "../components/ui/badge"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import { fmtTime } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import type { CircuitBreaker as CBType } from "../api"; +import { Badge, type Column, DataTable, EmptyState, ErrorState, Loading } from "../components/ui"; +import { useApi } from "../hooks"; +import { fmtTime, type RoutableProps } from "../lib"; const CB_COLUMNS: Column[] = [ { header: "Task", accessor: (b) => {b.task_name} }, @@ -29,8 +25,9 @@ const CB_COLUMNS: Column[] = [ ]; export function CircuitBreakers(_props: RoutableProps) { - const { data: breakers, loading } = useApi("/api/circuit-breakers"); + const { data: breakers, loading, error, refetch } = useApi("/api/circuit-breakers"); + if (error && !breakers) return ; if (loading && !breakers) return ; return ( diff --git a/dashboard/src/pages/dead-letters.tsx b/dashboard/src/pages/dead-letters.tsx index 1da06ea..640e6cc 100644 --- a/dashboard/src/pages/dead-letters.tsx +++ b/dashboard/src/pages/dead-letters.tsx @@ -1,17 +1,18 @@ import { ChevronDown, ChevronRight, Group, List, RotateCcw, Skull, Trash2 } from "lucide-preact"; import { useState } from "preact/hooks"; -import { apiPost } from "../api/client"; -import type { DeadLetter } from "../api/types"; -import { Button } from "../components/ui/button"; -import { ConfirmDialog } from "../components/ui/confirm-dialog"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { Pagination } from "../components/ui/pagination"; -import { useApi } from "../hooks/use-api"; -import { addToast } from "../hooks/use-toast"; -import { fmtTime, truncateId } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import { apiPost, type DeadLetter } from "../api"; +import { + Button, + type Column, + ConfirmDialog, + DataTable, + EmptyState, + ErrorState, + Loading, + Pagination, +} from "../components/ui"; +import { addToast, useApi } from "../hooks"; +import { fmtTime, type RoutableProps, truncateId } from "../lib"; const PAGE_SIZE = 20; @@ -42,6 +43,7 @@ export function DeadLetters(_props: RoutableProps) { const { data: items, loading, + error, refetch, } = useApi( `/api/dead-letters?limit=${grouped ? 200 : PAGE_SIZE}&offset=${grouped ? 0 : page * PAGE_SIZE}`, @@ -139,6 +141,7 @@ export function DeadLetters(_props: RoutableProps) { }, ]; + if (error && !items) return ; if (loading && !items) return ; const groups = grouped && items ? groupByError(items) : []; diff --git a/dashboard/src/pages/job-detail.tsx b/dashboard/src/pages/job-detail.tsx index 0651fb3..8810fa7 100644 --- a/dashboard/src/pages/job-detail.tsx +++ b/dashboard/src/pages/job-detail.tsx @@ -1,18 +1,26 @@ import { FileText, RotateCcw } from "lucide-preact"; import { route } from "preact-router"; -import { apiPost } from "../api/client"; -import type { DagData, Job, JobError, ReplayEntry, TaskLog } from "../api/types"; -import { DagViewer } from "../charts/dag-viewer"; -import { Badge } from "../components/ui/badge"; -import { Button } from "../components/ui/button"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { ProgressBar } from "../components/ui/progress-bar"; -import { useApi } from "../hooks/use-api"; -import { addToast } from "../hooks/use-toast"; -import { fmtTime, truncateId } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import { + apiPost, + type DagData, + type Job, + type JobError, + type ReplayEntry, + type TaskLog, +} from "../api"; +import { DagViewer } from "../charts"; +import { + Badge, + Button, + type Column, + DataTable, + EmptyState, + ErrorState, + Loading, + ProgressBar, +} from "../components/ui"; +import { addToast, useApi } from "../hooks"; +import { fmtTime, type RoutableProps, truncateId } from "../lib"; interface JobDetailProps extends RoutableProps { id?: string; @@ -67,12 +75,13 @@ const REPLAY_COLUMNS: Column[] = [ ]; export function JobDetail({ id }: JobDetailProps) { - const { data: job, loading, refetch } = useApi(`/api/jobs/${id}`); + const { data: job, loading, error, refetch } = useApi(`/api/jobs/${id}`); const { data: errors } = useApi(`/api/jobs/${id}/errors`); const { data: logs } = useApi(`/api/jobs/${id}/logs`); const { data: replayHistory } = useApi(`/api/jobs/${id}/replay-history`); const { data: dag } = useApi(`/api/jobs/${id}/dag`); + if (error && !job) return ; if (loading && !job) return ; if (!job) return ; diff --git a/dashboard/src/pages/jobs.tsx b/dashboard/src/pages/jobs.tsx index e9c6f76..12ec6a2 100644 --- a/dashboard/src/pages/jobs.tsx +++ b/dashboard/src/pages/jobs.tsx @@ -1,21 +1,22 @@ import { Ban, ListTodo, RotateCcw, Search, X } from "lucide-preact"; import { useState } from "preact/hooks"; import { route } from "preact-router"; -import { apiPost } from "../api/client"; -import type { Job, QueueStats } from "../api/types"; -import { Badge } from "../components/ui/badge"; -import { Button } from "../components/ui/button"; -import { ConfirmDialog } from "../components/ui/confirm-dialog"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { Pagination } from "../components/ui/pagination"; -import { ProgressBar } from "../components/ui/progress-bar"; -import { StatsGrid } from "../components/ui/stats-grid"; -import { useApi } from "../hooks/use-api"; -import { addToast } from "../hooks/use-toast"; -import { fmtTime, truncateId } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import { apiPost, type Job, type QueueStats } from "../api"; +import { + Badge, + Button, + type Column, + ConfirmDialog, + DataTable, + EmptyState, + ErrorState, + Loading, + Pagination, + ProgressBar, + StatsGrid, +} from "../components/ui"; +import { addToast, useApi } from "../hooks"; +import { fmtTime, type RoutableProps, truncateId } from "../lib"; interface Filters { status: string; @@ -83,10 +84,15 @@ export function Jobs(_props: RoutableProps) { const [selected, setSelected] = useState>(new Set()); const [showBulkCancel, setShowBulkCancel] = useState(false); - const { data: stats } = useApi("/api/stats"); + const { + data: stats, + error: statsError, + refetch: refetchStats, + } = useApi("/api/stats"); const { data: jobs, loading, + error: jobsError, refetch, } = useApi(buildUrl(filters, page), [ filters.status, @@ -239,7 +245,9 @@ export function Jobs(_props: RoutableProps) {
)} - {loading && !jobs ? ( + {jobsError && !jobs ? ( + + ) : loading && !jobs ? ( ) : !jobs?.length ? ( diff --git a/dashboard/src/pages/logs.tsx b/dashboard/src/pages/logs.tsx index a93b1a7..7d030d5 100644 --- a/dashboard/src/pages/logs.tsx +++ b/dashboard/src/pages/logs.tsx @@ -1,13 +1,9 @@ import { ScrollText } from "lucide-preact"; import { useState } from "preact/hooks"; -import type { TaskLog } from "../api/types"; -import { Badge } from "../components/ui/badge"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import { fmtTime, truncateId } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import type { TaskLog } from "../api"; +import { Badge, type Column, DataTable, EmptyState, ErrorState, Loading } from "../components/ui"; +import { useApi } from "../hooks"; +import { fmtTime, type RoutableProps, truncateId } from "../lib"; const LOG_COLUMNS: Column[] = [ { header: "Time", accessor: (l) => {fmtTime(l.logged_at)} }, @@ -40,10 +36,12 @@ export function Logs(_props: RoutableProps) { if (taskFilter) params.set("task", taskFilter); if (levelFilter) params.set("level", levelFilter); - const { data: logs, loading } = useApi(`/api/logs?${params}`, [ - taskFilter, - levelFilter, - ]); + const { + data: logs, + loading, + error, + refetch, + } = useApi(`/api/logs?${params}`, [taskFilter, levelFilter]); const inputClass = "dark:bg-surface-3 bg-white dark:text-gray-200 text-slate-700 border dark:border-white/[0.06] border-slate-200 rounded-lg px-3 py-2 text-[13px] placeholder:text-muted/50 focus:border-accent/50 transition-colors"; @@ -80,7 +78,9 @@ export function Logs(_props: RoutableProps) { - {loading && !logs ? ( + {error && !logs ? ( + + ) : loading && !logs ? ( ) : !logs?.length ? ( diff --git a/dashboard/src/pages/metrics.tsx b/dashboard/src/pages/metrics.tsx index 9b0b84a..58ed4cc 100644 --- a/dashboard/src/pages/metrics.tsx +++ b/dashboard/src/pages/metrics.tsx @@ -1,12 +1,10 @@ import { BarChart3 } from "lucide-preact"; import { useState } from "preact/hooks"; -import type { MetricsResponse, TaskMetrics, TimeseriesBucket } from "../api/types"; -import { TimeseriesChart } from "../charts/timeseries-chart"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import type { RoutableProps } from "../lib/routes"; +import type { MetricsResponse, TaskMetrics, TimeseriesBucket } from "../api"; +import { TimeseriesChart } from "../charts"; +import { type Column, DataTable, EmptyState, ErrorState, Loading } from "../components/ui"; +import { useApi } from "../hooks"; +import type { RoutableProps } from "../lib"; interface MetricsRow extends TaskMetrics { task_name: string; @@ -77,9 +75,12 @@ const TIME_RANGES = [ export function Metrics(_props: RoutableProps) { const [since, setSince] = useState(3600); - const { data: metrics, loading } = useApi(`/api/metrics?since=${since}`, [ - since, - ]); + const { + data: metrics, + loading, + error, + refetch, + } = useApi(`/api/metrics?since=${since}`, [since]); const { data: timeseries } = useApi( `/api/metrics/timeseries?since=${since}&bucket=${since <= 3600 ? 60 : since <= 21600 ? 300 : 900}`, [since], @@ -89,6 +90,8 @@ export function Metrics(_props: RoutableProps) { ? Object.entries(metrics).map(([task_name, m]) => ({ task_name, ...m })) : []; + if (error && !metrics) return ; + return (
diff --git a/dashboard/src/pages/overview.tsx b/dashboard/src/pages/overview.tsx index 8407996..0d62f54 100644 --- a/dashboard/src/pages/overview.tsx +++ b/dashboard/src/pages/overview.tsx @@ -1,17 +1,19 @@ import { LayoutDashboard } from "lucide-preact"; import { useRef } from "preact/hooks"; import { route } from "preact-router"; -import type { Job, QueueStats } from "../api/types"; -import { ThroughputChart } from "../charts/throughput-chart"; -import { Badge } from "../components/ui/badge"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { Loading } from "../components/ui/loading"; -import { ProgressBar } from "../components/ui/progress-bar"; -import { StatsGrid } from "../components/ui/stats-grid"; -import { useApi } from "../hooks/use-api"; -import { refreshInterval } from "../hooks/use-auto-refresh"; -import { fmtTime, truncateId } from "../lib/format"; -import type { RoutableProps } from "../lib/routes"; +import type { Job, QueueStats } from "../api"; +import { ThroughputChart } from "../charts"; +import { + Badge, + type Column, + DataTable, + ErrorState, + Loading, + ProgressBar, + StatsGrid, +} from "../components/ui"; +import { refreshInterval, useApi } from "../hooks"; +import { fmtTime, type RoutableProps, truncateId } from "../lib"; const JOB_COLUMNS: Column[] = [ { @@ -26,7 +28,12 @@ const JOB_COLUMNS: Column[] = [ ]; export function Overview(_props: RoutableProps) { - const { data: stats, loading: statsLoading } = useApi("/api/stats"); + const { + data: stats, + loading: statsLoading, + error: statsError, + refetch: refetchStats, + } = useApi("/api/stats"); const { data: jobs } = useApi("/api/jobs?limit=10"); const prevCompleted = useRef(0); @@ -43,6 +50,7 @@ export function Overview(_props: RoutableProps) { history.current = [...history.current.slice(-59), throughput]; } + if (statsError && !stats) return ; if (statsLoading && !stats) return ; return ( diff --git a/dashboard/src/pages/queues.tsx b/dashboard/src/pages/queues.tsx index 7c84ce5..3a7136b 100644 --- a/dashboard/src/pages/queues.tsx +++ b/dashboard/src/pages/queues.tsx @@ -1,14 +1,16 @@ import { Layers, Pause, Play } from "lucide-preact"; -import { apiPost } from "../api/client"; -import type { QueueStatsMap } from "../api/types"; -import { Badge } from "../components/ui/badge"; -import { Button } from "../components/ui/button"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import { addToast } from "../hooks/use-toast"; -import type { RoutableProps } from "../lib/routes"; +import { apiPost, type QueueStatsMap } from "../api"; +import { + Badge, + Button, + type Column, + DataTable, + EmptyState, + ErrorState, + Loading, +} from "../components/ui"; +import { addToast, useApi } from "../hooks"; +import type { RoutableProps } from "../lib"; interface QueueRow { name: string; @@ -18,7 +20,7 @@ interface QueueRow { } export function Queues(_props: RoutableProps) { - const { data: queueStats, loading, refetch } = useApi("/api/stats/queues"); + const { data: queueStats, loading, error, refetch } = useApi("/api/stats/queues"); const { data: pausedQueues, refetch: refetchPaused } = useApi("/api/queues/paused"); const pausedSet = new Set(pausedQueues ?? []); @@ -82,6 +84,7 @@ export function Queues(_props: RoutableProps) { }, ]; + if (error && !queueStats) return ; if (loading && !queueStats) return ; return ( diff --git a/dashboard/src/pages/resources.tsx b/dashboard/src/pages/resources.tsx index 6a0a6d2..0412a85 100644 --- a/dashboard/src/pages/resources.tsx +++ b/dashboard/src/pages/resources.tsx @@ -1,11 +1,8 @@ import { Box } from "lucide-preact"; -import type { ResourceStatus } from "../api/types"; -import { Badge } from "../components/ui/badge"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import type { RoutableProps } from "../lib/routes"; +import type { ResourceStatus } from "../api"; +import { Badge, type Column, DataTable, EmptyState, ErrorState, Loading } from "../components/ui"; +import { useApi } from "../hooks"; +import type { RoutableProps } from "../lib"; const RESOURCE_COLUMNS: Column[] = [ { header: "Name", accessor: (r) => {r.name} }, @@ -47,8 +44,9 @@ const RESOURCE_COLUMNS: Column[] = [ ]; export function Resources(_props: RoutableProps) { - const { data: resources, loading } = useApi("/api/resources"); + const { data: resources, loading, error, refetch } = useApi("/api/resources"); + if (error && !resources) return ; if (loading && !resources) return ; return ( diff --git a/dashboard/src/pages/system.tsx b/dashboard/src/pages/system.tsx index 1eb7c7b..8ae12b0 100644 --- a/dashboard/src/pages/system.tsx +++ b/dashboard/src/pages/system.tsx @@ -1,10 +1,8 @@ import { Cog } from "lucide-preact"; -import type { InterceptionStats, ProxyStats } from "../api/types"; -import { type Column, DataTable } from "../components/ui/data-table"; -import { EmptyState } from "../components/ui/empty-state"; -import { Loading } from "../components/ui/loading"; -import { useApi } from "../hooks/use-api"; -import type { RoutableProps } from "../lib/routes"; +import type { InterceptionStats, ProxyStats } from "../api"; +import { type Column, DataTable, EmptyState, ErrorState, Loading } from "../components/ui"; +import { useApi } from "../hooks"; +import type { RoutableProps } from "../lib"; interface ProxyRow { handler: string; @@ -52,9 +50,18 @@ const INTERCEPTION_COLUMNS: Column[] = [ ]; export function System(_props: RoutableProps) { - const { data: proxyStats, loading: proxyLoading } = useApi("/api/proxy-stats"); - const { data: interceptionStats, loading: interceptLoading } = - useApi("/api/interception-stats"); + const { + data: proxyStats, + loading: proxyLoading, + error: proxyError, + refetch: refetchProxy, + } = useApi("/api/proxy-stats"); + const { + data: interceptionStats, + loading: interceptLoading, + error: interceptError, + refetch: refetchIntercept, + } = useApi("/api/interception-stats"); const proxyRows: ProxyRow[] = proxyStats ? Object.entries(proxyStats).map(([handler, s]) => ({ handler, ...s })) @@ -80,7 +87,9 @@ export function System(_props: RoutableProps) {

Proxy Reconstruction

- {proxyLoading && !proxyStats ? ( + {proxyError && !proxyStats ? ( + + ) : proxyLoading && !proxyStats ? ( ) : !proxyRows.length ? (

Interception

- {interceptLoading && !interceptionStats ? ( + {interceptError && !interceptionStats ? ( + + ) : interceptLoading && !interceptionStats ? ( ) : !interceptRows.length ? ( ("/api/workers"); + const { data: workers, loading, error, refetch } = useApi("/api/workers"); const { data: stats } = useApi("/api/stats"); + if (error && !workers) return ; if (loading && !workers) return ; return ( From a0f21964051aea1ecba6b72c71210d12b208e42c Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:26:35 +0530 Subject: [PATCH 3/4] refactor: add barrel exports and consolidate dashboard imports Add index.ts barrel files for api/, hooks/, components/ui/, components/layout/, charts/, and lib/ directories. Update all imports across pages, charts, and components to use barrel exports instead of direct file paths. Reduces import clutter from 6-10 lines to 3-4 per file. --- dashboard/src/api/index.ts | 22 +++++ dashboard/src/app.tsx | 4 +- dashboard/src/charts/dag-viewer.tsx | 2 +- dashboard/src/charts/index.ts | 3 + dashboard/src/components/layout/header.tsx | 9 +- dashboard/src/components/layout/index.ts | 3 + dashboard/src/components/ui/badge.tsx | 2 +- dashboard/src/components/ui/index.ts | 12 +++ dashboard/src/components/ui/stat-card.tsx | 2 +- dashboard/src/components/ui/stats-grid.tsx | 2 +- dashboard/src/components/ui/toast.tsx | 2 +- dashboard/src/hooks/index.ts | 9 ++ dashboard/src/hooks/use-api.ts | 2 +- dashboard/src/lib/index.ts | 2 + py_src/taskito/templates/dashboard.html | 95 ++++++++++++---------- 15 files changed, 116 insertions(+), 55 deletions(-) create mode 100644 dashboard/src/api/index.ts create mode 100644 dashboard/src/charts/index.ts create mode 100644 dashboard/src/components/layout/index.ts create mode 100644 dashboard/src/components/ui/index.ts create mode 100644 dashboard/src/hooks/index.ts create mode 100644 dashboard/src/lib/index.ts diff --git a/dashboard/src/api/index.ts b/dashboard/src/api/index.ts new file mode 100644 index 0000000..f11dda3 --- /dev/null +++ b/dashboard/src/api/index.ts @@ -0,0 +1,22 @@ +export { ApiError, api, apiPost } from "./client"; +export type { + CircuitBreaker, + DagData, + DagEdge, + DagNode, + DeadLetter, + InterceptionStats, + Job, + JobError, + JobStatus, + MetricsResponse, + ProxyStats, + QueueStats, + QueueStatsMap, + ReplayEntry, + ResourceStatus, + TaskLog, + TaskMetrics, + TimeseriesBucket, + Worker, +} from "./types"; diff --git a/dashboard/src/app.tsx b/dashboard/src/app.tsx index cbe3ae6..5d65e7d 100644 --- a/dashboard/src/app.tsx +++ b/dashboard/src/app.tsx @@ -1,6 +1,6 @@ import Router from "preact-router"; -import { Shell } from "./components/layout/shell"; -import { ToastContainer } from "./components/ui/toast"; +import { Shell } from "./components/layout"; +import { ToastContainer } from "./components/ui"; import { CircuitBreakers } from "./pages/circuit-breakers"; import { DeadLetters } from "./pages/dead-letters"; import { JobDetail } from "./pages/job-detail"; diff --git a/dashboard/src/charts/dag-viewer.tsx b/dashboard/src/charts/dag-viewer.tsx index 7e2c95f..b699fde 100644 --- a/dashboard/src/charts/dag-viewer.tsx +++ b/dashboard/src/charts/dag-viewer.tsx @@ -1,5 +1,5 @@ import { route } from "preact-router"; -import type { DagData, DagNode, JobStatus } from "../api/types"; +import type { DagData, DagNode, JobStatus } from "../api"; interface DagViewerProps { dag: DagData; diff --git a/dashboard/src/charts/index.ts b/dashboard/src/charts/index.ts new file mode 100644 index 0000000..bd85f55 --- /dev/null +++ b/dashboard/src/charts/index.ts @@ -0,0 +1,3 @@ +export { DagViewer } from "./dag-viewer"; +export { ThroughputChart } from "./throughput-chart"; +export { TimeseriesChart } from "./timeseries-chart"; diff --git a/dashboard/src/components/layout/header.tsx b/dashboard/src/components/layout/header.tsx index 62b5d8c..9371c2c 100644 --- a/dashboard/src/components/layout/header.tsx +++ b/dashboard/src/components/layout/header.tsx @@ -1,8 +1,13 @@ import { Moon, RefreshCw, Search, Sun, Zap } from "lucide-preact"; import { useEffect, useState } from "preact/hooks"; import { route } from "preact-router"; -import { lastRefreshAt, refreshInterval, setRefreshInterval } from "../../hooks/use-auto-refresh"; -import { theme, toggleTheme } from "../../hooks/use-theme"; +import { + lastRefreshAt, + refreshInterval, + setRefreshInterval, + theme, + toggleTheme, +} from "../../hooks"; function RelativeTime() { const [ago, setAgo] = useState(""); diff --git a/dashboard/src/components/layout/index.ts b/dashboard/src/components/layout/index.ts new file mode 100644 index 0000000..70b058c --- /dev/null +++ b/dashboard/src/components/layout/index.ts @@ -0,0 +1,3 @@ +export { Header } from "./header"; +export { Shell } from "./shell"; +export { Sidebar } from "./sidebar"; diff --git a/dashboard/src/components/ui/badge.tsx b/dashboard/src/components/ui/badge.tsx index 41f6361..b0cd90a 100644 --- a/dashboard/src/components/ui/badge.tsx +++ b/dashboard/src/components/ui/badge.tsx @@ -1,4 +1,4 @@ -import type { JobStatus } from "../../api/types"; +import type { JobStatus } from "../../api"; const STATUS_STYLES: Record = { pending: "bg-warning/10 text-warning border-warning/20", diff --git a/dashboard/src/components/ui/index.ts b/dashboard/src/components/ui/index.ts new file mode 100644 index 0000000..3f81d01 --- /dev/null +++ b/dashboard/src/components/ui/index.ts @@ -0,0 +1,12 @@ +export { Badge } from "./badge"; +export { Button } from "./button"; +export { ConfirmDialog } from "./confirm-dialog"; +export { type Column, DataTable } from "./data-table"; +export { EmptyState } from "./empty-state"; +export { ErrorState } from "./error-state"; +export { CardSkeleton, Loading, TableSkeleton } from "./loading"; +export { Pagination } from "./pagination"; +export { ProgressBar } from "./progress-bar"; +export { StatCard } from "./stat-card"; +export { StatsGrid } from "./stats-grid"; +export { ToastContainer } from "./toast"; diff --git a/dashboard/src/components/ui/stat-card.tsx b/dashboard/src/components/ui/stat-card.tsx index b19241b..10a168d 100644 --- a/dashboard/src/components/ui/stat-card.tsx +++ b/dashboard/src/components/ui/stat-card.tsx @@ -1,6 +1,6 @@ import type { LucideIcon } from "lucide-preact"; import { Ban, CheckCircle2, Clock, Play, Skull, XCircle } from "lucide-preact"; -import { fmtNumber } from "../../lib/format"; +import { fmtNumber } from "../../lib"; interface StatCardProps { label: string; diff --git a/dashboard/src/components/ui/stats-grid.tsx b/dashboard/src/components/ui/stats-grid.tsx index 6f1e3c0..36cc2bf 100644 --- a/dashboard/src/components/ui/stats-grid.tsx +++ b/dashboard/src/components/ui/stats-grid.tsx @@ -1,4 +1,4 @@ -import type { QueueStats } from "../../api/types"; +import type { QueueStats } from "../../api"; import { StatCard } from "./stat-card"; interface StatsGridProps { diff --git a/dashboard/src/components/ui/toast.tsx b/dashboard/src/components/ui/toast.tsx index 35f65cd..913356a 100644 --- a/dashboard/src/components/ui/toast.tsx +++ b/dashboard/src/components/ui/toast.tsx @@ -1,5 +1,5 @@ import { CheckCircle2, Info, X, XCircle } from "lucide-preact"; -import { dismissToast, type Toast, toasts } from "../../hooks/use-toast"; +import { dismissToast, type Toast, toasts } from "../../hooks"; const TYPE_CONFIG: Record< Toast["type"], diff --git a/dashboard/src/hooks/index.ts b/dashboard/src/hooks/index.ts new file mode 100644 index 0000000..cd86387 --- /dev/null +++ b/dashboard/src/hooks/index.ts @@ -0,0 +1,9 @@ +export { useApi } from "./use-api"; +export { + lastRefreshAt, + markRefreshed, + refreshInterval, + setRefreshInterval, +} from "./use-auto-refresh"; +export { theme, toggleTheme } from "./use-theme"; +export { addToast, dismissToast, type Toast, toasts } from "./use-toast"; diff --git a/dashboard/src/hooks/use-api.ts b/dashboard/src/hooks/use-api.ts index bae158e..7252770 100644 --- a/dashboard/src/hooks/use-api.ts +++ b/dashboard/src/hooks/use-api.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import { api } from "../api/client"; +import { api } from "../api"; import { markRefreshed, refreshInterval } from "./use-auto-refresh"; interface UseApiResult { diff --git a/dashboard/src/lib/index.ts b/dashboard/src/lib/index.ts new file mode 100644 index 0000000..e0f1f84 --- /dev/null +++ b/dashboard/src/lib/index.ts @@ -0,0 +1,2 @@ +export { fmtDuration, fmtNumber, fmtTime, truncateId } from "./format"; +export { ROUTES, type RoutableProps } from "./routes"; diff --git a/py_src/taskito/templates/dashboard.html b/py_src/taskito/templates/dashboard.html index 1e530ca..783609e 100644 --- a/py_src/taskito/templates/dashboard.html +++ b/py_src/taskito/templates/dashboard.html @@ -4,223 +4,228 @@ taskito dashboard - - + */const Yt=k("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);/** + * @license lucide-preact v0.577.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Xr=k("zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);class Zt extends Error{constructor(t,n){super(n),this.status=t}}async function Kr(e,t){const n=await fetch(e,{signal:t});if(!n.ok)throw new Zt(n.status,`${n.status} ${n.statusText}`);return n.json()}async function H(e,t){const n=await fetch(e,{method:"POST",signal:t});if(!n.ok)throw new Zt(n.status,`${n.status} ${n.statusText}`);return n.json()}var en=Symbol.for("preact-signals");function Xe(){if(Y>1)Y--;else{var e,t=!1;for((function(){var o=Re;for(Re=void 0;o!==void 0;)o.S.v===o.v&&(o.S.i=o.i),o=o.o})();ue!==void 0;){var n=ue;for(ue=void 0,je++;n!==void 0;){var s=n.u;if(n.u=void 0,n.f&=-3,!(8&n.f)&&er(n))try{n.c()}catch(o){t||(e=o,t=!0)}n=s}}if(je=0,Y--,t)throw e}}var w=void 0;function Xt(e){var t=w;w=void 0;try{return e()}finally{w=t}}var ue=void 0,Y=0,je=0,gt=0,Re=void 0,Pe=0;function Kt(e){if(w!==void 0){var t=e.n;if(t===void 0||t.t!==w)return t={i:0,S:e,p:w.s,n:void 0,t:w,e:void 0,x:void 0,r:t},w.s!==void 0&&(w.s.n=t),w.s=t,e.n=t,32&w.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=w.s,t.n=void 0,w.s.n=t,w.s=t),t}}function R(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.l=0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}R.prototype.brand=en;R.prototype.h=function(){return!0};R.prototype.S=function(e){var t=this,n=this.t;n!==e&&e.e===void 0&&(e.x=n,this.t=e,n!==void 0?n.e=e:Xt(function(){var s;(s=t.W)==null||s.call(t)}))};R.prototype.U=function(e){var t=this;if(this.t!==void 0){var n=e.e,s=e.x;n!==void 0&&(n.x=s,e.e=void 0),s!==void 0&&(s.e=n,e.x=void 0),e===this.t&&(this.t=s,s===void 0&&Xt(function(){var o;(o=t.Z)==null||o.call(t)}))}};R.prototype.subscribe=function(e){var t=this;return Ee(function(){var n=t.value,s=w;w=void 0;try{e(n)}finally{w=s}},{name:"sub"})};R.prototype.valueOf=function(){return this.value};R.prototype.toString=function(){return this.value+""};R.prototype.toJSON=function(){return this.value};R.prototype.peek=function(){var e=w;w=void 0;try{return this.value}finally{w=e}};Object.defineProperty(R.prototype,"value",{get:function(){var e=Kt(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(je>100)throw new Error("Cycle detected");(function(n){Y!==0&&je===0&&n.l!==gt&&(n.l=gt,Re={S:n,v:n.v,i:n.i,o:Re})})(this),this.v=e,this.i++,Pe++,Y++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{Xe()}}}});function oe(e,t){return new R(e,t)}function er(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function tr(e){for(var t=e.s;t!==void 0;t=t.n){var n=t.S.n;if(n!==void 0&&(t.r=n),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function rr(e){for(var t=e.s,n=void 0;t!==void 0;){var s=t.p;t.i===-1?(t.S.U(t),s!==void 0&&(s.n=t.n),t.n!==void 0&&(t.n.p=s)):n=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=s}e.s=n}function K(e,t){R.call(this,void 0),this.x=e,this.s=void 0,this.g=Pe-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}K.prototype=new R;K.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===Pe))return!0;if(this.g=Pe,this.f|=1,this.i>0&&!er(this))return this.f&=-2,!0;var e=w;try{tr(this),w=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(n){this.v=n,this.f|=16,this.i++}return w=e,rr(this),this.f&=-2,!0};K.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}R.prototype.S.call(this,e)};K.prototype.U=function(e){if(this.t!==void 0&&(R.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};K.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(K.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=Kt(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function tn(e,t){return new K(e,t)}function nr(e){var t=e.m;if(e.m=void 0,typeof t=="function"){Y++;var n=w;w=void 0;try{t()}catch(s){throw e.f&=-2,e.f|=8,Ke(e),s}finally{w=n,Xe()}}}function Ke(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,nr(e)}function rn(e){if(w!==this)throw new Error("Out-of-order effect");rr(this),w=e,this.f&=-2,8&this.f&&Ke(this),Xe()}function ie(e,t){this.x=e,this.m=void 0,this.s=void 0,this.u=void 0,this.f=32,this.name=t==null?void 0:t.name}ie.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.m=t)}finally{e()}};ie.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,nr(this),tr(this),Y++;var e=w;return w=this,rn.bind(this,e)};ie.prototype.N=function(){2&this.f||(this.f|=2,this.u=ue,ue=this)};ie.prototype.d=function(){this.f|=8,1&this.f||Ke(this)};ie.prototype.dispose=function(){this.d()};function Ee(e,t){var n=new ie(e,t);try{n.c()}catch(o){throw n.d(),o}var s=n.d.bind(n);return s[Symbol.dispose]=s,s}var ve;function ce(e,t){$[e]=t.bind(null,$[e]||function(){})}function Ae(e){if(ve){var t=ve;ve=void 0,t()}ve=e&&e.S()}function sr(e){var t=this,n=e.data,s=sn(n);s.value=n;var o=Le(function(){for(var a=t.__v;a=a.__;)if(a.__c){a.__c.__$f|=4;break}return t.__$u.c=function(){var i,d=t.__$u.S(),h=o.value;d(),xt(h)||((i=t.base)==null?void 0:i.nodeType)!==3?(t.__$f|=1,t.setState({})):t.base.data=h},tn(function(){var i=s.value.value;return i===0?0:i===!0?"":i||""})},[]);return o.value}sr.displayName="_st";Object.defineProperties(R.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:sr},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});ce("__b",function(e,t){if(typeof t.type=="string"){var n,s=t.props;for(var o in s)if(o!=="children"){var a=s[o];a instanceof R&&(n||(t.__np=n={}),n[o]=a,s[o]=a.peek())}}e(t)});ce("__r",function(e,t){e(t),Ae();var n,s=t.__c;s&&(s.__$f&=-2,(n=s.__$u)===void 0&&(s.__$u=n=(function(o){var a;return Ee(function(){a=this}),a.c=function(){s.__$f|=1,s.setState({})},a})())),Ae(n)});ce("__e",function(e,t,n,s){Ae(),e(t,n,s)});ce("diffed",function(e,t){Ae();var n;if(typeof t.type=="string"&&(n=t.__e)){var s=t.__np,o=t.props;if(s){var a=n.U;if(a)for(var i in a){var d=a[i];d!==void 0&&!(i in s)&&(d.d(),a[i]=void 0)}else n.U=a={};for(var h in s){var l=a[h],m=s[h];l===void 0?(l=nn(n,h,m,o),a[h]=l):l.o(m,o)}}}e(t)});function nn(e,t,n,s){var o=t in e&&e.ownerSVGElement===void 0,a=oe(n);return{o:function(i,d){a.value=i,s=d},d:Ee(function(){var i=a.value.value;s[t]!==i&&(s[t]=i,o?e[t]=i:i?e.setAttribute(t,i):e.removeAttribute(t))})}}ce("unmount",function(e,t){if(typeof t.type=="string"){var n=t.__e;if(n){var s=n.U;if(s){n.U=void 0;for(var o in s){var a=s[o];a&&a.d()}}}}else{var i=t.__c;if(i){var d=i.__$u;d&&(i.__$u=void 0,d.d())}}e(t)});ce("__h",function(e,t,n,s){(s<3||s===9)&&(t.__$f|=2),e(t,n,s)});te.prototype.shouldComponentUpdate=function(e,t){if(this.__R)return!0;var n=this.__$u,s=n&&n.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){if(!(s||2&this.__$f||4&this.__$f)||1&this.__$f)return!0}else if(!(s||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var i in this.props)if(!(i in e))return!0;return!1};function sn(e){return Le(function(){return oe(e)},[])}const Z=oe(5e3),He=oe(Date.now());function an(e){Z.value=e}function on(){He.value=Date.now()}function M(e,t=[]){const[n,s]=j(null),[o,a]=j(!0),[i,d]=j(null),h=se(null),l=se(!0),m=br(()=>{var u;if(!e){s(null),a(!1);return}(u=h.current)==null||u.abort();const c=new AbortController;h.current=c,a(p=>n===null?!0:p),Kr(e,c.signal).then(p=>{l.current&&!c.signal.aborted&&(s(p),d(null),a(!1),on())}).catch(p=>{l.current&&!c.signal.aborted&&(d(p.message??"Failed to fetch"),a(!1))})},[e,...t]);return ne(()=>(l.current=!0,m(),()=>{var c;l.current=!1,(c=h.current)==null||c.abort()}),[m]),ne(()=>{const c=Z.value;if(c<=0||!e)return;const u=setInterval(m,c);return()=>clearInterval(u)},[m,e,Z.value]),{data:n,loading:o,error:i,refetch:m}}const cn=localStorage.getItem("taskito-theme"),z=oe(cn??"dark");Ee(()=>{const e=document.documentElement;z.value==="dark"?(e.classList.add("dark"),e.classList.remove("light")):(e.classList.remove("dark"),e.classList.add("light")),localStorage.setItem("taskito-theme",z.value)});function ln(){z.value=z.value==="dark"?"light":"dark"}const V=oe([]);let dn=0;function A(e,t="info",n=3e3){const s=String(++dn);V.value=[...V.value,{id:s,message:e,type:t}],setTimeout(()=>{V.value=V.value.filter(o=>o.id!==s)},n)}function un(e){V.value=V.value.filter(t=>t.id!==e)}function hn(){const[e,t]=j("");return ne(()=>{const n=()=>{const o=Math.round((Date.now()-He.value)/1e3);t(o<2?"just now":`${o}s ago`)};n();const s=setInterval(n,1e3);return()=>clearInterval(s)},[He.value]),r("span",{class:"text-[11px] text-muted tabular-nums",children:e})}function pn(){const[e,t]=j("");return r("header",{class:"h-14 flex items-center justify-between px-6 dark:bg-surface-2/80 bg-white/80 backdrop-blur-md border-b dark:border-white/[0.06] border-slate-200 sticky top-0 z-40",children:[r("a",{href:"/",class:"flex items-center gap-2 no-underline group",children:[r("div",{class:"w-7 h-7 rounded-lg bg-gradient-to-br from-accent to-accent-light flex items-center justify-center shadow-md shadow-accent/20",children:r(Xr,{class:"w-4 h-4 text-white",strokeWidth:2.5})}),r("span",{class:"text-[15px] font-semibold dark:text-white text-slate-900 tracking-tight",children:"taskito"}),r("span",{class:"text-xs text-muted font-normal hidden sm:inline",children:"dashboard"})]}),r("form",{onSubmit:s=>{s.preventDefault();const o=e.trim();o&&(ae(`/jobs/${o}`),t(""))},class:"flex items-center gap-2 dark:bg-surface-3 bg-slate-100 rounded-lg px-3 py-1.5 border dark:border-white/[0.06] border-slate-200 w-64",children:[r(Vt,{class:"w-3.5 h-3.5 text-muted shrink-0"}),r("input",{type:"text",placeholder:"Jump to job ID\\u2026",value:e,onInput:s=>t(s.target.value),class:"bg-transparent border-none outline-none text-[13px] dark:text-gray-200 text-slate-700 placeholder:text-muted/50 w-full"})]}),r("div",{class:"flex items-center gap-3",children:[r("div",{class:"flex items-center gap-2 text-xs",children:[r(Jt,{class:`w-3.5 h-3.5 ${Z.value>0?"text-accent":"text-muted"}`,strokeWidth:Z.value>0?2:1.5}),r("select",{class:"dark:bg-surface-3 bg-slate-100 dark:text-gray-300 text-slate-600 border dark:border-white/[0.06] border-slate-200 rounded-md px-2 py-1 text-xs cursor-pointer hover:dark:border-white/10 hover:border-slate-300 transition-colors",value:Z.value,onChange:s=>an(Number(s.target.value)),children:[r("option",{value:2e3,children:"2s"}),r("option",{value:5e3,children:"5s"}),r("option",{value:1e4,children:"10s"}),r("option",{value:0,children:"Off"})]}),r(hn,{})]}),r("div",{class:"w-px h-5 dark:bg-white/[0.06] bg-slate-200"}),r("button",{type:"button",onClick:ln,class:"p-2 rounded-lg dark:text-gray-400 text-slate-500 hover:dark:text-white hover:text-slate-900 hover:dark:bg-surface-3 hover:bg-slate-100 transition-all duration-150 border-none cursor-pointer bg-transparent",title:`Switch to ${z.value==="dark"?"light":"dark"} mode`,children:z.value==="dark"?r(Jr,{class:"w-4 h-4"}):r(Hr,{class:"w-4 h-4"})})]})]})}const fn=[{title:"Monitoring",items:[{path:"/",label:"Overview",icon:Ft},{path:"/jobs",label:"Jobs",icon:Ht},{path:"/metrics",label:"Metrics",icon:It},{path:"/logs",label:"Logs",icon:Gt}]},{title:"Infrastructure",items:[{path:"/workers",label:"Workers",icon:ze},{path:"/queues",label:"Queues",icon:zt},{path:"/resources",label:"Resources",icon:Ut},{path:"/circuit-breakers",label:"Circuit Breakers",icon:Qt}]},{title:"Advanced",items:[{path:"/dead-letters",label:"Dead Letters",icon:Ze},{path:"/system",label:"System",icon:Dt}]}];function mn(e,t){return t==="/"?e==="/":e===t||e.startsWith(`${t}/`)}function gn(){const[e,t]=j(me());return ne(()=>{const n=()=>t(me());return addEventListener("popstate",n),addEventListener("pushstate",n),()=>{removeEventListener("popstate",n),removeEventListener("pushstate",n)}},[]),r("aside",{class:"w-60 shrink-0 border-r dark:border-white/[0.06] border-slate-200 dark:bg-surface-2/50 bg-slate-50/50 overflow-y-auto h-[calc(100vh-56px)]",children:r("nav",{class:"p-3 space-y-5 pt-4",children:fn.map(n=>r("div",{children:[r("div",{class:"px-3 pb-2 text-[10px] font-bold uppercase tracking-[0.1em] text-muted/60",children:n.title}),r("div",{class:"space-y-0.5",children:n.items.map(s=>{const o=mn(e,s.path),a=s.icon;return r("a",{href:s.path,class:`flex items-center gap-2.5 px-3 py-2 text-[13px] rounded-lg transition-all duration-150 no-underline relative ${o?"dark:bg-accent/10 bg-accent/5 dark:text-white text-slate-900 font-medium":"dark:text-gray-400 text-slate-500 hover:dark:text-gray-200 hover:text-slate-700 hover:dark:bg-white/[0.03] hover:bg-slate-100"}`,children:[o&&r("div",{class:"absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 rounded-r-full bg-accent"}),r(a,{class:`w-4 h-4 shrink-0 ${o?"text-accent":""}`,strokeWidth:o?2.2:1.8}),s.label]},s.path)})})]},n.title))})})}function _n({children:e}){return r("div",{class:"min-h-screen",children:[r(pn,{}),r("div",{class:"flex",children:[r(gn,{}),r("main",{class:"flex-1 p-8 overflow-auto h-[calc(100vh-56px)]",children:r("div",{class:"max-w-[1280px] mx-auto",children:e})})]})]})}const xn={pending:"bg-warning/10 text-warning border-warning/20",running:"bg-info/10 text-info border-info/20",complete:"bg-success/10 text-success border-success/20",failed:"bg-danger/10 text-danger border-danger/20",dead:"bg-danger/15 text-danger border-danger/25",cancelled:"bg-muted/10 text-muted border-muted/20",closed:"bg-success/10 text-success border-success/20",open:"bg-danger/10 text-danger border-danger/20",half_open:"bg-warning/10 text-warning border-warning/20",healthy:"bg-success/10 text-success border-success/20",unhealthy:"bg-danger/10 text-danger border-danger/20",degraded:"bg-warning/10 text-warning border-warning/20",active:"bg-success/10 text-success border-success/20",paused:"bg-warning/10 text-warning border-warning/20"},bn={pending:"bg-warning",running:"bg-info",complete:"bg-success",failed:"bg-danger",dead:"bg-danger",cancelled:"bg-muted",closed:"bg-success",open:"bg-danger",half_open:"bg-warning",healthy:"bg-success",unhealthy:"bg-danger",degraded:"bg-warning",active:"bg-success",paused:"bg-warning"};function B({status:e}){const t=xn[e]??"bg-muted/10 text-muted border-muted/20",n=bn[e]??"bg-muted";return r("span",{class:`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[11px] font-semibold uppercase tracking-wide border ${t}`,children:[r("span",{class:`w-1.5 h-1.5 rounded-full ${n}`}),e]})}const vn={primary:"bg-accent text-white shadow-sm shadow-accent/20 hover:bg-accent/90 hover:shadow-md hover:shadow-accent/25 active:scale-[0.98]",danger:"bg-danger text-white shadow-sm shadow-danger/20 hover:bg-danger/90 hover:shadow-md hover:shadow-danger/25 active:scale-[0.98]",ghost:"dark:text-gray-400 text-slate-500 hover:dark:bg-surface-3 hover:bg-slate-100 hover:dark:text-gray-200 hover:text-slate-700 active:scale-[0.98]"};function U({onClick:e,variant:t="primary",disabled:n,children:s,class:o=""}){return r("button",{type:"button",onClick:e,disabled:n,class:`inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-[13px] font-medium cursor-pointer transition-all duration-150 disabled:opacity-40 disabled:cursor-default disabled:shadow-none border-none ${vn[t]} ${o}`,children:s})}function ar({message:e,onConfirm:t,onCancel:n}){return r("div",{class:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm animate-fade-in",onClick:n,children:r("div",{class:"dark:bg-surface-2 bg-white rounded-xl shadow-2xl dark:shadow-black/50 p-6 max-w-sm mx-4 border dark:border-white/[0.08] border-slate-200 animate-slide-in",onClick:s=>s.stopPropagation(),children:[r("div",{class:"flex items-start gap-3 mb-5",children:[r("div",{class:"p-2 rounded-lg bg-danger-dim shrink-0",children:r(Yr,{class:"w-5 h-5 text-danger",strokeWidth:2})}),r("p",{class:"text-sm dark:text-gray-200 text-slate-700 pt-1",children:e})]}),r("div",{class:"flex justify-end gap-2.5",children:[r(U,{variant:"ghost",onClick:n,children:"Cancel"}),r(U,{variant:"danger",onClick:t,children:"Confirm"})]})]})})}function E({columns:e,data:t,onRowClick:n,children:s,selectable:o,selectedKeys:a,rowKey:i,onSelectionChange:d}){const h=o&&i?t.map(i):[],l=o&&h.length>0&&h.every(u=>a==null?void 0:a.has(u)),m=()=>{d&&d(l?new Set:new Set(h))},c=u=>{if(!d||!a)return;const p=new Set(a);p.has(u)?p.delete(u):p.add(u),d(p)};return r("div",{class:"dark:bg-surface-2 bg-white rounded-xl shadow-sm dark:shadow-black/20 overflow-hidden border dark:border-white/[0.06] border-slate-200",children:[r("div",{class:"overflow-x-auto",children:r("table",{class:"w-full border-collapse text-[13px]",children:[r("thead",{children:r("tr",{children:[o&&r("th",{class:"w-10 text-center px-3 py-2.5 dark:bg-surface-3/50 bg-slate-50 border-b dark:border-white/[0.04] border-slate-100",children:r("input",{type:"checkbox",checked:l,onChange:m,class:"accent-accent cursor-pointer"})}),e.map((u,p)=>r("th",{class:`text-left px-4 py-2.5 dark:bg-surface-3/50 bg-slate-50 text-muted font-semibold text-[11px] uppercase tracking-[0.05em] whitespace-nowrap border-b dark:border-white/[0.04] border-slate-100 ${u.className??""}`,children:u.header},p))]})}),r("tbody",{children:t.map((u,p)=>{const f=o&&i?i(u):String(p),v=a==null?void 0:a.has(f);return r("tr",{onClick:n?()=>n(u):void 0,class:`border-b dark:border-white/[0.03] border-slate-50 last:border-0 transition-colors duration-100 ${p%2===1?"dark:bg-white/[0.01] bg-slate-50/30":""} ${v?"dark:bg-accent/[0.08] bg-accent/[0.04]":""} ${n?"cursor-pointer hover:dark:bg-accent/[0.04] hover:bg-accent/[0.02]":""}`,children:[o&&r("td",{class:"w-10 text-center px-3 py-3",children:r("input",{type:"checkbox",checked:v,onChange:()=>c(f),onClick:_=>_.stopPropagation(),class:"accent-accent cursor-pointer"})}),e.map((_,b)=>r("td",{class:`px-4 py-3 whitespace-nowrap ${_.className??""}`,children:typeof _.accessor=="function"?_.accessor(u):u[_.accessor]},b))]},f)})})]})}),s]})}function q({message:e,subtitle:t}){return r("div",{class:"flex flex-col items-center justify-center py-16 text-center",children:[r("div",{class:"w-12 h-12 rounded-xl dark:bg-surface-3 bg-slate-100 flex items-center justify-center mb-4",children:r(Dr,{class:"w-6 h-6 text-muted",strokeWidth:1.5})}),r("p",{class:"text-sm font-medium dark:text-gray-300 text-slate-600",children:e}),t&&r("p",{class:"text-xs text-muted mt-1",children:t})]})}function O({message:e,onRetry:t}){return r("div",{class:"flex flex-col items-center justify-center py-20 text-center",children:[r("div",{class:"w-14 h-14 rounded-2xl dark:bg-danger/10 bg-danger/5 flex items-center justify-center mb-5",children:r(Zr,{class:"w-7 h-7 text-danger",strokeWidth:1.5})}),r("p",{class:"text-sm font-medium dark:text-gray-200 text-slate-700 mb-1",children:"Unable to load data"}),r("p",{class:"text-xs text-muted max-w-xs mb-5",children:e}),t&&r(U,{variant:"ghost",onClick:t,children:[r(Jt,{class:"w-3.5 h-3.5"}),"Retry"]})]})}function W(){return r("div",{class:"flex items-center justify-center py-20",children:r("div",{class:"flex items-center gap-3 text-muted text-sm",children:[r("div",{class:"w-5 h-5 border-2 border-accent/30 border-t-accent rounded-full animate-spin"}),r("span",{children:"Loading\\u2026"})]})})}function or({page:e,pageSize:t,itemCount:n,onPageChange:s}){return r("div",{class:"flex items-center justify-between px-4 py-3 text-[13px] text-muted border-t dark:border-white/[0.04] border-slate-100",children:[r("span",{children:["Showing ",e*t+1,"\\u2013",e*t+n," items"]}),r("div",{class:"flex gap-1.5",children:[r("button",{type:"button",onClick:()=>s(e-1),disabled:e===0,class:"inline-flex items-center gap-1 px-3 py-1.5 rounded-lg dark:bg-surface-3 bg-slate-100 dark:text-gray-300 text-slate-600 border dark:border-white/[0.06] border-slate-200 disabled:opacity-30 cursor-pointer disabled:cursor-default hover:enabled:dark:bg-surface-4 hover:enabled:bg-slate-200 transition-all duration-150 text-[13px]",children:[r(Or,{class:"w-3.5 h-3.5"}),"Prev"]}),r("button",{type:"button",onClick:()=>s(e+1),disabled:nt?e.slice(0,t):e}const kn={pending:{color:"text-warning",bg:"bg-warning-dim",border:"border-l-warning",icon:Me},running:{color:"text-info",bg:"bg-info-dim",border:"border-l-info",icon:Bt},completed:{color:"text-success",bg:"bg-success-dim",border:"border-l-success",icon:Wt},failed:{color:"text-danger",bg:"bg-danger-dim",border:"border-l-danger",icon:qt},dead:{color:"text-danger",bg:"bg-danger-dim",border:"border-l-danger",icon:Ze},cancelled:{color:"text-muted",bg:"bg-muted/10",border:"border-l-muted/40",icon:Nt}};function wn({label:e,value:t,color:n}){const s=kn[e],o=n??(s==null?void 0:s.color)??"text-accent-light",a=(s==null?void 0:s.bg)??"bg-accent-dim",i=(s==null?void 0:s.border)??"border-l-accent",d=(s==null?void 0:s.icon)??Me;return r("div",{class:`dark:bg-surface-2 bg-white rounded-xl p-5 shadow-sm dark:shadow-black/20 border-l-[3px] ${i} dark:border-t dark:border-r dark:border-b dark:border-white/[0.04] border border-slate-100 transition-all duration-150 hover:shadow-md hover:dark:shadow-black/30`,children:r("div",{class:"flex items-start justify-between",children:[r("div",{children:[r("div",{class:`text-3xl font-bold tabular-nums tracking-tight ${o}`,children:yn(t)}),r("div",{class:"text-xs text-muted uppercase mt-1.5 tracking-wider font-medium",children:e})]}),r("div",{class:`p-2 rounded-lg ${a}`,children:r(d,{class:`w-5 h-5 ${o}`,strokeWidth:1.8})})]})})}const $n=["pending","running","completed","failed","dead","cancelled"];function ir({stats:e}){return r("div",{class:"grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-4 mb-8",children:$n.map(t=>r(wn,{label:t,value:e[t]??0},t))})}const Cn={success:{border:"border-l-success",icon:Wt,iconColor:"text-success"},error:{border:"border-l-danger",icon:qt,iconColor:"text-danger"},info:{border:"border-l-info",icon:Fr,iconColor:"text-info"}};function Sn(){const e=V.value;return e.length?r("div",{class:"fixed bottom-5 right-5 z-50 flex flex-col gap-2.5 max-w-sm",children:e.map(t=>{const n=Cn[t.type],s=n.icon;return r("div",{class:`flex items-start gap-3 border-l-[3px] ${n.border} rounded-lg px-4 py-3.5 text-[13px] dark:bg-surface-2 bg-white shadow-xl dark:shadow-black/40 dark:text-gray-200 text-slate-700 animate-slide-in border dark:border-white/[0.06] border-slate-200`,role:"alert",children:[r(s,{class:`w-4.5 h-4.5 ${n.iconColor} shrink-0 mt-0.5`,strokeWidth:2}),r("span",{class:"flex-1",children:t.message}),r("button",{type:"button",onClick:()=>un(t.id),class:"text-muted hover:dark:text-white hover:text-slate-900 transition-colors border-none bg-transparent cursor-pointer p-0.5",children:r(Yt,{class:"w-3.5 h-3.5"})})]},t.id)})}):null}const Mn=[{header:"Task",accessor:e=>r("span",{class:"font-medium",children:e.task_name})},{header:"State",accessor:e=>r(B,{status:e.state})},{header:"Failures",accessor:e=>r("span",{class:e.failure_count>0?"text-danger tabular-nums":"tabular-nums",children:e.failure_count})},{header:"Threshold",accessor:e=>r("span",{class:"tabular-nums",children:e.threshold})},{header:"Window",accessor:e=>`${(e.window_ms/1e3).toFixed(0)}s`},{header:"Cooldown",accessor:e=>`${(e.cooldown_ms/1e3).toFixed(0)}s`},{header:"Last Failure",accessor:e=>r("span",{class:"text-muted",children:L(e.last_failure_at)})}];function Tn(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/circuit-breakers");return s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Qt,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Circuit Breakers"}),r("p",{class:"text-xs text-muted",children:"Automatic failure protection status"})]})]}),t!=null&&t.length?r(E,{columns:Mn,data:t}):r(q,{message:"No circuit breakers configured",subtitle:"Circuit breakers activate when tasks fail repeatedly"})]})}const Ue=20;function jn(e){const t=new Map;for(const n of e){const s=n.error??"(no error message)",o=t.get(s);o?o.push(n):t.set(s,[n])}return Array.from(t.entries()).map(([n,s])=>({error:n,items:s})).sort((n,s)=>s.items.length-n.items.length)}function Rn(e){const[t,n]=j(0),[s,o]=j(!1),[a,i]=j(!1),[d,h]=j(new Set),{data:l,loading:m,error:c,refetch:u}=M(`/api/dead-letters?limit=${a?200:Ue}&offset=${a?0:t*Ue}`,[t,a]),p=async g=>{try{await H(`/api/dead-letters/${g}/retry`),A("Dead letter retried","success"),u()}catch{A("Failed to retry dead letter","error")}},f=async g=>{let y=0;for(const C of g.items)try{await H(`/api/dead-letters/${C.id}/retry`),y++}catch{}A(`Retried ${y} of ${g.items.length} dead letters`,y>0?"success":"error"),u()},v=async()=>{o(!1);try{const g=await H("/api/dead-letters/purge");A(`Purged ${g.purged} dead letters`,"success"),u()}catch{A("Failed to purge dead letters","error")}},_=g=>{const y=new Set(d);y.has(g)?y.delete(g):y.add(g),h(y)},b=[{header:"ID",accessor:g=>r("span",{class:"font-mono text-xs text-accent-light",children:X(g.id)})},{header:"Original Job",accessor:g=>r("a",{href:`/jobs/${g.original_job_id}`,class:"font-mono text-xs text-accent-light hover:underline",children:X(g.original_job_id)})},{header:"Task",accessor:g=>r("span",{class:"font-medium",children:g.task_name})},{header:"Queue",accessor:"queue"},{header:"Error",accessor:g=>r("span",{class:"text-danger text-xs",title:g.error??"",children:g.error?g.error.length>50?`${g.error.slice(0,50)}…`:g.error:"—"}),className:"max-w-[250px]"},{header:"Retries",accessor:g=>r("span",{class:"text-warning tabular-nums",children:g.retry_count})},{header:"Failed At",accessor:g=>r("span",{class:"text-muted",children:L(g.failed_at)})},{header:"Actions",accessor:g=>r(U,{onClick:()=>p(g.id),children:[r(Te,{class:"w-3.5 h-3.5"}),"Retry"]})}];if(c&&!l)return r(O,{message:c,onRetry:u});if(m&&!l)return r(W,{});const x=a&&l?jn(l):[];return r("div",{children:[r("div",{class:"flex items-center justify-between mb-6",children:[r("div",{class:"flex items-center gap-3",children:[r("div",{class:"p-2 rounded-lg bg-danger-dim",children:r(Ze,{class:"w-5 h-5 text-danger",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Dead Letters"}),r("p",{class:"text-xs text-muted",children:"Failed jobs that exhausted all retries"})]})]}),r("div",{class:"flex items-center gap-2",children:l&&l.length>0&&r(G,{children:[r("div",{class:"flex dark:bg-surface-3 bg-slate-100 rounded-lg p-1",children:[r("button",{type:"button",onClick:()=>{i(!1),n(0)},class:`inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md border-none cursor-pointer transition-all duration-150 ${a?"bg-transparent dark:text-gray-400 text-slate-500 hover:dark:text-white":"bg-accent text-white shadow-sm shadow-accent/20"}`,children:[r(zr,{class:"w-3.5 h-3.5"}),"List"]}),r("button",{type:"button",onClick:()=>i(!0),class:`inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md border-none cursor-pointer transition-all duration-150 ${a?"bg-accent text-white shadow-sm shadow-accent/20":"bg-transparent dark:text-gray-400 text-slate-500 hover:dark:text-white"}`,children:[r(qr,{class:"w-3.5 h-3.5"}),"Group"]})]}),r(U,{variant:"danger",onClick:()=>o(!0),children:[r(Vr,{class:"w-3.5 h-3.5"}),"Purge All"]})]})})]}),l!=null&&l.length?a?r("div",{class:"space-y-3",children:x.map(g=>{const y=d.has(g.error);return r("div",{class:"dark:bg-surface-2 bg-white rounded-xl shadow-sm dark:shadow-black/20 border dark:border-white/[0.06] border-slate-200 overflow-hidden",children:[r("div",{class:"flex items-center gap-3 px-5 py-4 cursor-pointer hover:dark:bg-white/[0.02] hover:bg-slate-50 transition-colors",onClick:()=>_(g.error),children:[y?r(Ir,{class:"w-4 h-4 text-muted shrink-0"}):r(Ot,{class:"w-4 h-4 text-muted shrink-0"}),r("div",{class:"flex-1 min-w-0",children:r("span",{class:"text-danger text-sm font-mono truncate block",children:g.error})}),r("span",{class:"shrink-0 px-2.5 py-0.5 rounded-full text-xs font-semibold tabular-nums bg-danger/10 text-danger border border-danger/20",children:g.items.length}),r(U,{onClick:()=>{f(g)},children:[r(Te,{class:"w-3.5 h-3.5"}),"Retry All"]})]}),y&&r("div",{class:"border-t dark:border-white/[0.04] border-slate-100",children:r(E,{columns:b,data:g.items})})]},g.error)})}):r(E,{columns:b,data:l,children:r(or,{page:t,pageSize:Ue,itemCount:l.length,onPageChange:n})}):r(q,{message:"No dead letters",subtitle:"All jobs are processing normally"}),s&&r(ar,{message:"Purge all dead letters? This cannot be undone.",onConfirm:v,onCancel:()=>o(!1)})]})}const Pn={pending:"#ffa726",running:"#42a5f5",complete:"#66bb6a",failed:"#ef5350",dead:"#ef5350",cancelled:"#a0a0b0"};function An({dag:e}){if(!e.nodes||e.nodes.length<=1)return null;const t=160,n=36,s=40,o=20,a={},i={};e.nodes.forEach(f=>{a[f.id]=[],i[f.id]=0}),e.edges.forEach(f=>{a[f.from]||(a[f.from]=[]),a[f.from].push(f.to),i[f.to]=(i[f.to]||0)+1});const d=[],h=new Set;let l=e.nodes.filter(f=>(i[f.id]||0)===0).map(f=>f.id);for(;l.length;){d.push([...l]);for(const v of l)h.add(v);const f=[];l.forEach(v=>{(a[v]||[]).forEach(_=>{!h.has(_)&&!f.includes(_)&&f.push(_)})}),l=f}e.nodes.forEach(f=>{h.has(f.id)||(d.push([f.id]),h.add(f.id))});const m={};for(const f of e.nodes)m[f.id]=f;const c={};let u=0,p=0;return d.forEach((f,v)=>{f.forEach((_,b)=>{const x=20+v*(t+s),g=20+b*(n+o);c[_]={x,y:g},u=Math.max(u,x+t+20),p=Math.max(p,g+n+20)})}),r("div",{class:"mt-4",children:[r("h3",{class:"text-sm text-muted mb-2",children:"Dependency Graph"}),r("div",{class:"dark:bg-surface-2 bg-white rounded-lg shadow-sm dark:shadow-black/30 p-4 overflow-x-auto border border-transparent dark:border-white/5",children:r("svg",{width:u,height:p,role:"img","aria-label":"Job dependency graph",children:[r("title",{children:"Job dependency graph"}),r("defs",{children:r("marker",{id:"arrow",viewBox:"0 0 10 10",refX:"10",refY:"5",markerWidth:"8",markerHeight:"8",orient:"auto",children:r("path",{d:"M0,0 L10,5 L0,10 z",fill:"#a0a0b0"})})}),e.edges.map((f,v)=>{const _=c[f.from],b=c[f.to];return!_||!b?null:r("line",{x1:_.x+t,y1:_.y+n/2,x2:b.x,y2:b.y+n/2,stroke:"#a0a0b0","stroke-width":"1.5",fill:"none","marker-end":"url(#arrow)"},v)}),e.nodes.map(f=>{const v=c[f.id];if(!v)return null;const _=Pn[f.status]||"#a0a0b0";return r("g",{class:"cursor-pointer",onClick:()=>ae(`/jobs/${f.id}`),children:[r("rect",{x:v.x,y:v.y,width:t,height:n,fill:`${_}22`,stroke:_,"stroke-width":"1.5",rx:"6",ry:"6"}),r("text",{x:v.x+8,y:v.y+14,fill:_,"font-size":"10","font-weight":"600",children:f.status.toUpperCase()}),r("text",{x:v.x+8,y:v.y+28,"font-size":"10",fill:"#a0a0b0",children:f.task_name.length>18?f.task_name.slice(-18):f.task_name})]},f.id)})]})})]})}function Ln({data:e}){const t=se(null);ne(()=>{const s=t.current;if(!s)return;const o=s.getContext("2d");if(!o)return;const a=z.value==="dark",i=a?"rgba(255,255,255,0.04)":"rgba(0,0,0,0.06)",d=a?"rgba(139,149,165,0.5)":"rgba(100,116,139,0.7)",h=a?"rgba(139,149,165,0.4)":"rgba(100,116,139,0.5)",l=a?"rgba(34,197,94,0.2)":"rgba(34,197,94,0.12)",m=a?"rgba(34,197,94,0.01)":"rgba(34,197,94,0.02)",c=window.devicePixelRatio||1,u=s.getBoundingClientRect();s.width=u.width*c,s.height=u.height*c,o.scale(c,c);const p=u.width,f=u.height;if(o.clearRect(0,0,p,f),e.length<2){o.fillStyle=h,o.font="12px -apple-system, sans-serif",o.textAlign="center",o.fillText("Collecting data…",p/2,f/2);return}const v=Math.max(...e,1),_={top:12,right:12,bottom:24,left:44},b=p-_.left-_.right,x=f-_.top-_.bottom;for(let y=0;y<=4;y++){const C=_.top+x*(1-y/4);o.strokeStyle=i,o.lineWidth=1,o.beginPath(),o.moveTo(_.left,C),o.lineTo(p-_.right,C),o.stroke(),o.fillStyle=d,o.font="10px -apple-system, sans-serif",o.textAlign="right",o.fillText((v*y/4).toFixed(1),_.left-6,C+3)}const g=o.createLinearGradient(0,_.top,0,_.top+x);if(g.addColorStop(0,l),g.addColorStop(1,m),o.beginPath(),o.moveTo(_.left,_.top+x),e.forEach((y,C)=>{const I=_.left+C/(e.length-1)*b,P=_.top+x*(1-y/v);o.lineTo(I,P)}),o.lineTo(_.left+b,_.top+x),o.closePath(),o.fillStyle=g,o.fill(),o.beginPath(),e.forEach((y,C)=>{const I=_.left+C/(e.length-1)*b,P=_.top+x*(1-y/v);C===0?o.moveTo(I,P):o.lineTo(I,P)}),o.strokeStyle="#22c55e",o.lineWidth=2,o.lineJoin="round",o.stroke(),e.length>0){const y=_.left+b,C=_.top+x*(1-e[e.length-1]/v);o.beginPath(),o.arc(y,C,3,0,Math.PI*2),o.fillStyle="#22c55e",o.fill(),o.beginPath(),o.arc(y,C,5,0,Math.PI*2),o.strokeStyle="rgba(34,197,94,0.3)",o.lineWidth=2,o.stroke()}},[e,z.value]);const n=e.length>0?e[e.length-1]:0;return r("div",{class:"dark:bg-surface-2 bg-white rounded-xl shadow-sm dark:shadow-black/20 p-5 mb-6 border dark:border-white/[0.06] border-slate-200",children:[r("div",{class:"flex items-center justify-between mb-4",children:[r("div",{class:"flex items-center gap-2",children:[r(Qr,{class:"w-4 h-4 text-success",strokeWidth:2}),r("h3",{class:"text-sm font-medium dark:text-gray-300 text-slate-600",children:"Throughput"})]}),r("span",{class:"text-xl font-bold tabular-nums text-success",children:[n.toFixed(1)," ",r("span",{class:"text-xs font-normal text-muted",children:"jobs/s"})]})]}),r("canvas",{ref:t,class:"w-full",style:{height:"180px"}})]})}function En({data:e}){const t=se(null);return ne(()=>{const n=t.current;if(!n)return;const s=n.getContext("2d");if(!s)return;const o=window.devicePixelRatio||1,a=n.getBoundingClientRect();n.width=a.width*o,n.height=a.height*o,s.scale(o,o);const i=a.width,d=a.height,h=z.value==="dark",l=h?"rgba(255,255,255,0.04)":"rgba(0,0,0,0.06)",m=h?"rgba(139,149,165,0.5)":"rgba(100,116,139,0.7)",c=h?"rgba(139,149,165,0.4)":"rgba(100,116,139,0.5)";if(s.clearRect(0,0,i,d),!e.length){s.fillStyle=c,s.font="12px -apple-system, sans-serif",s.textAlign="center",s.fillText("No timeseries data",i/2,d/2);return}const u={top:12,right:12,bottom:32,left:48},p=i-u.left-u.right,f=d-u.top-u.bottom,v=Math.max(...e.map(g=>g.success+g.failure),1),_=Math.max(3,p/e.length-2),b=Math.max(1,(p-_*e.length)/e.length);for(let g=0;g<=4;g++){const y=u.top+f*(1-g/4);s.strokeStyle=l,s.lineWidth=1,s.beginPath(),s.moveTo(u.left,y),s.lineTo(i-u.right,y),s.stroke(),s.fillStyle=m,s.font="10px -apple-system, sans-serif",s.textAlign="right",s.fillText(Math.round(v*g/4).toString(),u.left-6,y+3)}e.forEach((g,y)=>{const C=u.left+y*(_+b),I=g.success/v*f,P=g.failure/v*f;s.fillStyle="rgba(34,197,94,0.65)",s.beginPath();const le=u.top+f-I-P;s.roundRect(C,le,_,I,[2,2,0,0]),s.fill(),P>0&&(s.fillStyle="rgba(239,68,68,0.65)",s.beginPath(),s.roundRect(C,u.top+f-P,_,P,[0,0,2,2]),s.fill())}),s.fillStyle=m,s.font="10px -apple-system, sans-serif",s.textAlign="center";const x=Math.min(6,e.length);for(let g=0;gr("span",{class:"text-muted",children:L(e.failed_at)})}],Un=[{header:"Time",accessor:e=>r("span",{class:"text-muted",children:L(e.logged_at)})},{header:"Level",accessor:e=>r(B,{status:e.level==="error"?"failed":e.level==="warning"?"pending":"complete"})},{header:"Message",accessor:"message"},{header:"Extra",accessor:e=>e.extra??"—",className:"max-w-[200px] truncate"}],In=[{header:"Replay Job",accessor:e=>r("a",{href:`/jobs/${e.replay_job_id}`,class:"font-mono text-xs text-accent-light hover:underline",children:X(e.replay_job_id)})},{header:"Replayed At",accessor:e=>r("span",{class:"text-muted",children:L(e.replayed_at)})},{header:"Original Error",accessor:e=>e.original_error??"—",className:"max-w-[200px] truncate"},{header:"Replay Error",accessor:e=>e.replay_error??"—",className:"max-w-[200px] truncate"}];function On({id:e}){const{data:t,loading:n,error:s,refetch:o}=M(`/api/jobs/${e}`),{data:a}=M(`/api/jobs/${e}/errors`),{data:i}=M(`/api/jobs/${e}/logs`),{data:d}=M(`/api/jobs/${e}/replay-history`),{data:h}=M(`/api/jobs/${e}/dag`);if(s&&!t)return r(O,{message:s,onRetry:o});if(n&&!t)return r(W,{});if(!t)return r(q,{message:`Job not found: ${e}`});const l=async()=>{try{const u=await H(`/api/jobs/${e}/cancel`);A(u.cancelled?"Job cancelled":"Failed to cancel job",u.cancelled?"success":"error"),o()}catch{A("Failed to cancel job","error")}},m=async()=>{try{const u=await H(`/api/jobs/${e}/replay`);A("Job replayed","success"),ae(`/jobs/${u.replay_job_id}`)}catch{A("Failed to replay job","error")}},c={pending:"border-t-warning",running:"border-t-info",complete:"border-t-success",failed:"border-t-danger",dead:"border-t-danger",cancelled:"border-t-muted"};return r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Wr,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:["Job ",r("span",{class:"font-mono text-accent-light",children:X(t.id)})]}),r("p",{class:"text-xs text-muted",children:t.task_name})]})]}),r("div",{class:`dark:bg-surface-2 bg-white rounded-xl shadow-sm dark:shadow-black/20 p-6 border dark:border-white/[0.06] border-slate-200 border-t-[3px] ${c[t.status]??"border-t-muted"}`,children:[r("div",{class:"grid grid-cols-[140px_1fr] gap-x-6 gap-y-3 text-[13px]",children:[r("span",{class:"text-muted font-medium",children:"ID"}),r("span",{class:"font-mono text-xs break-all dark:text-gray-300 text-slate-600",children:t.id}),r("span",{class:"text-muted font-medium",children:"Status"}),r("span",{children:r(B,{status:t.status})}),r("span",{class:"text-muted font-medium",children:"Task"}),r("span",{class:"font-medium",children:t.task_name}),r("span",{class:"text-muted font-medium",children:"Queue"}),r("span",{children:t.queue}),r("span",{class:"text-muted font-medium",children:"Priority"}),r("span",{children:t.priority}),r("span",{class:"text-muted font-medium",children:"Progress"}),r("span",{children:r(et,{progress:t.progress})}),r("span",{class:"text-muted font-medium",children:"Retries"}),r("span",{class:t.retry_count>0?"text-warning":"",children:[t.retry_count," / ",t.max_retries]}),r("span",{class:"text-muted font-medium",children:"Created"}),r("span",{class:"text-muted",children:L(t.created_at)}),r("span",{class:"text-muted font-medium",children:"Scheduled"}),r("span",{class:"text-muted",children:L(t.scheduled_at)}),r("span",{class:"text-muted font-medium",children:"Started"}),r("span",{class:"text-muted",children:t.started_at?L(t.started_at):"—"}),r("span",{class:"text-muted font-medium",children:"Completed"}),r("span",{class:"text-muted",children:t.completed_at?L(t.completed_at):"—"}),r("span",{class:"text-muted font-medium",children:"Timeout"}),r("span",{children:[(t.timeout_ms/1e3).toFixed(0),"s"]}),t.error&&r(G,{children:[r("span",{class:"text-muted font-medium",children:"Error"}),r("span",{class:"text-danger text-xs font-mono bg-danger-dim rounded px-2 py-1",children:t.error})]}),t.unique_key&&r(G,{children:[r("span",{class:"text-muted font-medium",children:"Unique Key"}),r("span",{class:"font-mono text-xs",children:t.unique_key})]}),t.metadata&&r(G,{children:[r("span",{class:"text-muted font-medium",children:"Metadata"}),r("span",{class:"font-mono text-xs",children:t.metadata})]})]}),r("div",{class:"flex gap-2.5 mt-5 pt-5 border-t dark:border-white/[0.06] border-slate-100",children:[t.status==="pending"&&r(U,{variant:"danger",onClick:l,children:"Cancel Job"}),r(U,{onClick:m,children:[r(Te,{class:"w-3.5 h-3.5"}),"Replay"]})]})]}),a&&a.length>0&&r("div",{class:"mt-6",children:[r("h3",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700 mb-3",children:["Error History ",r("span",{class:"text-muted font-normal",children:["(",a.length,")"]})]}),r(E,{columns:Nn,data:a})]}),i&&i.length>0&&r("div",{class:"mt-6",children:[r("h3",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700 mb-3",children:["Task Logs ",r("span",{class:"text-muted font-normal",children:["(",i.length,")"]})]}),r(E,{columns:Un,data:i})]}),d&&d.length>0&&r("div",{class:"mt-6",children:[r("h3",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700 mb-3",children:["Replay History ",r("span",{class:"text-muted font-normal",children:["(",d.length,")"]})]}),r(E,{columns:In,data:d})]}),h&&r(An,{dag:h}),r("div",{class:"mt-6",children:r("a",{href:"/jobs",class:"text-accent-light text-[13px] hover:underline",children:["←"," Back to jobs"]})})]})}const Be=20,Wn=[{header:"ID",accessor:e=>r("span",{class:"font-mono text-xs text-accent-light",children:X(e.id)})},{header:"Task",accessor:"task_name"},{header:"Queue",accessor:"queue"},{header:"Status",accessor:e=>r(B,{status:e.status})},{header:"Priority",accessor:"priority"},{header:"Progress",accessor:e=>r(et,{progress:e.progress})},{header:"Retries",accessor:e=>r("span",{class:e.retry_count>0?"text-warning":"text-muted",children:[e.retry_count,"/",e.max_retries]})},{header:"Created",accessor:e=>r("span",{class:"text-muted",children:L(e.created_at)})}];function qn(e,t){const n=new URLSearchParams;return n.set("limit",String(Be)),n.set("offset",String(t*Be)),e.status&&n.set("status",e.status),e.queue&&n.set("queue",e.queue),e.task&&n.set("task",e.task),e.metadata&&n.set("metadata",e.metadata),e.error&&n.set("error",e.error),e.created_after&&n.set("created_after",String(new Date(e.created_after).getTime())),e.created_before&&n.set("created_before",String(new Date(e.created_before).getTime())),`/api/jobs?${n}`}function Dn(e){const[t,n]=j({status:"",queue:"",task:"",metadata:"",error:"",created_after:"",created_before:""}),[s,o]=j(0),[a,i]=j(new Set),[d,h]=j(!1),{data:l}=M("/api/stats"),{data:m,loading:c,error:u,refetch:p}=M(qn(t,s),[t.status,t.queue,t.task,t.metadata,t.error,t.created_after,t.created_before,s]),f=(x,g)=>{n(y=>({...y,[x]:g})),o(0),i(new Set)},v=async()=>{h(!1);let x=0;for(const g of a)try{(await H(`/api/jobs/${g}/cancel`)).cancelled&&x++}catch{}A(`Cancelled ${x} of ${a.size} jobs`,x>0?"success":"error"),i(new Set),p()},_=async()=>{let x=0;for(const g of a)try{await H(`/api/jobs/${g}/replay`),x++}catch{}A(`Replayed ${x} of ${a.size} jobs`,x>0?"success":"error"),i(new Set),p()},b="dark:bg-surface-3 bg-white dark:text-gray-200 text-slate-700 border dark:border-white/[0.06] border-slate-200 rounded-lg px-3 py-2 text-[13px] placeholder:text-muted/50 focus:border-accent/50 transition-colors";return r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Ht,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Jobs"}),r("p",{class:"text-xs text-muted",children:"Browse and filter task queue jobs"})]})]}),l&&r(ir,{stats:l}),r("div",{class:"dark:bg-surface-2 bg-white rounded-xl p-4 mb-4 border dark:border-white/[0.06] border-slate-200",children:[r("div",{class:"flex items-center gap-2 mb-3 text-xs text-muted font-medium uppercase tracking-wider",children:[r(Vt,{class:"w-3.5 h-3.5"}),"Filters"]}),r("div",{class:"grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-2.5",children:[r("select",{class:b,value:t.status,onChange:x=>f("status",x.target.value),children:[r("option",{value:"",children:"All statuses"}),r("option",{value:"pending",children:"Pending"}),r("option",{value:"running",children:"Running"}),r("option",{value:"complete",children:"Complete"}),r("option",{value:"failed",children:"Failed"}),r("option",{value:"dead",children:"Dead"}),r("option",{value:"cancelled",children:"Cancelled"})]}),r("input",{class:b,placeholder:"Queue\\u2026",value:t.queue,onInput:x=>f("queue",x.target.value)}),r("input",{class:b,placeholder:"Task\\u2026",value:t.task,onInput:x=>f("task",x.target.value)}),r("input",{class:b,placeholder:"Metadata\\u2026",value:t.metadata,onInput:x=>f("metadata",x.target.value)}),r("input",{class:b,placeholder:"Error text\\u2026",value:t.error,onInput:x=>f("error",x.target.value)}),r("input",{class:b,type:"date",title:"Created after",value:t.created_after,onInput:x=>f("created_after",x.target.value)}),r("input",{class:b,type:"date",title:"Created before",value:t.created_before,onInput:x=>f("created_before",x.target.value)})]})]}),a.size>0&&r("div",{class:"flex items-center gap-3 mb-4 px-4 py-3 rounded-xl dark:bg-accent/[0.08] bg-accent/[0.04] border dark:border-accent/20 border-accent/10",children:[r("span",{class:"text-sm font-medium dark:text-gray-200 text-slate-700",children:[a.size," job",a.size>1?"s":""," selected"]}),r("div",{class:"flex gap-2 ml-auto",children:[r(U,{variant:"danger",onClick:()=>h(!0),children:[r(Nt,{class:"w-3.5 h-3.5"}),"Cancel Selected"]}),r(U,{onClick:_,children:[r(Te,{class:"w-3.5 h-3.5"}),"Replay Selected"]}),r(U,{variant:"ghost",onClick:()=>i(new Set),children:[r(Yt,{class:"w-3.5 h-3.5"}),"Clear"]})]})]}),u&&!m?r(O,{message:u,onRetry:p}):c&&!m?r(W,{}):m!=null&&m.length?r(E,{columns:Wn,data:m,onRowClick:x=>ae(`/jobs/${x.id}`),selectable:!0,selectedKeys:a,rowKey:x=>x.id,onSelectionChange:i,children:r(or,{page:s,pageSize:Be,itemCount:m.length,onPageChange:o})}):r(q,{message:"No jobs found",subtitle:"Try adjusting your filters"}),d&&r(ar,{message:`Cancel ${a.size} selected job${a.size>1?"s":""}? Only pending jobs can be cancelled.`,onConfirm:v,onCancel:()=>h(!1)})]})}const Fn=[{header:"Time",accessor:e=>r("span",{class:"text-muted",children:L(e.logged_at)})},{header:"Level",accessor:e=>r(B,{status:e.level==="error"?"failed":e.level==="warning"?"pending":"complete"})},{header:"Task",accessor:e=>r("span",{class:"font-medium",children:e.task_name})},{header:"Job",accessor:e=>r("a",{href:`/jobs/${e.job_id}`,class:"font-mono text-xs text-accent-light hover:underline",children:X(e.job_id)})},{header:"Message",accessor:"message"},{header:"Extra",accessor:e=>e.extra??"—",className:"max-w-[200px] truncate"}];function zn(e){const[t,n]=j(""),[s,o]=j(""),a=new URLSearchParams({limit:"100"});t&&a.set("task",t),s&&a.set("level",s);const{data:i,loading:d,error:h,refetch:l}=M(`/api/logs?${a}`,[t,s]),m="dark:bg-surface-3 bg-white dark:text-gray-200 text-slate-700 border dark:border-white/[0.06] border-slate-200 rounded-lg px-3 py-2 text-[13px] placeholder:text-muted/50 focus:border-accent/50 transition-colors";return r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Gt,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Logs"}),r("p",{class:"text-xs text-muted",children:"Structured task execution logs"})]})]}),r("div",{class:"flex gap-2.5 mb-5",children:[r("input",{class:`${m} w-44`,placeholder:"Filter by task\\u2026",value:t,onInput:c=>n(c.target.value)}),r("select",{class:m,value:s,onChange:c=>o(c.target.value),children:[r("option",{value:"",children:"All levels"}),r("option",{value:"error",children:"Error"}),r("option",{value:"warning",children:"Warning"}),r("option",{value:"info",children:"Info"}),r("option",{value:"debug",children:"Debug"})]})]}),h&&!i?r(O,{message:h,onRetry:l}):d&&!i?r(W,{}):i!=null&&i.length?r(E,{columns:Fn,data:i}):r(q,{message:"No logs yet",subtitle:"Logs appear when tasks execute"})]})}function ye(e,t){return e<=t.good?"text-success":e<=t.warn?"text-warning":"text-danger"}const Hn=[{header:"Task",accessor:e=>r("span",{class:"font-medium",children:e.task_name})},{header:"Total",accessor:e=>r("span",{class:"tabular-nums",children:e.count})},{header:"Success",accessor:e=>r("span",{class:"text-success tabular-nums",children:e.success_count})},{header:"Failures",accessor:e=>r("span",{class:e.failure_count>0?"text-danger tabular-nums":"text-muted tabular-nums",children:e.failure_count})},{header:"Avg",accessor:e=>r("span",{class:`tabular-nums ${ye(e.avg_ms,{good:100,warn:500})}`,children:[e.avg_ms,"ms"]})},{header:"P50",accessor:e=>r("span",{class:"tabular-nums text-muted",children:[e.p50_ms,"ms"]})},{header:"P95",accessor:e=>r("span",{class:`tabular-nums ${ye(e.p95_ms,{good:200,warn:1e3})}`,children:[e.p95_ms,"ms"]})},{header:"P99",accessor:e=>r("span",{class:`tabular-nums ${ye(e.p99_ms,{good:500,warn:2e3})}`,children:[e.p99_ms,"ms"]})},{header:"Min",accessor:e=>r("span",{class:"tabular-nums text-muted",children:[e.min_ms,"ms"]})},{header:"Max",accessor:e=>r("span",{class:`tabular-nums ${ye(e.max_ms,{good:1e3,warn:5e3})}`,children:[e.max_ms,"ms"]})}],Bn=[{label:"1h",seconds:3600},{label:"6h",seconds:21600},{label:"24h",seconds:86400}];function Jn(e){const[t,n]=j(3600),{data:s,loading:o,error:a,refetch:i}=M(`/api/metrics?since=${t}`,[t]),{data:d}=M(`/api/metrics/timeseries?since=${t}&bucket=${t<=3600?60:t<=21600?300:900}`,[t]),h=s?Object.entries(s).map(([l,m])=>({task_name:l,...m})):[];return a&&!s?r(O,{message:a,onRetry:i}):r("div",{children:[r("div",{class:"flex items-center justify-between mb-6",children:[r("div",{class:"flex items-center gap-3",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(It,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Metrics"}),r("p",{class:"text-xs text-muted",children:"Task performance and throughput"})]})]}),r("div",{class:"flex gap-1 dark:bg-surface-3 bg-slate-100 rounded-lg p-1",children:Bn.map(l=>r("button",{type:"button",onClick:()=>n(l.seconds),class:`px-3 py-1.5 text-xs font-medium rounded-md border-none cursor-pointer transition-all duration-150 ${t===l.seconds?"bg-accent text-white shadow-sm shadow-accent/20":"bg-transparent dark:text-gray-400 text-slate-500 hover:dark:text-white hover:text-slate-900"}`,children:l.label},l.label))})]}),d&&d.length>0&&r(En,{data:d}),o&&!s?r(W,{}):h.length?r(E,{columns:Hn,data:h}):r(q,{message:"No metrics yet",subtitle:"Run some tasks to see performance data"})]})}const Gn=[{header:"ID",accessor:e=>r("span",{class:"font-mono text-xs text-accent-light",children:X(e.id)})},{header:"Task",accessor:"task_name"},{header:"Queue",accessor:"queue"},{header:"Status",accessor:e=>r(B,{status:e.status})},{header:"Progress",accessor:e=>r(et,{progress:e.progress})},{header:"Created",accessor:e=>r("span",{class:"text-muted",children:L(e.created_at)})}];function Vn(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/stats"),{data:a}=M("/api/jobs?limit=10"),i=se(0),d=se([]);if(t){const h=t.completed||0,l=Z.value||5e3;let m=0;i.current>0&&(m=parseFloat(((h-i.current)/(l/1e3)).toFixed(1))),i.current=h,d.current=[...d.current.slice(-59),m]}return s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Ft,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Overview"}),r("p",{class:"text-xs text-muted",children:"Real-time queue status"})]})]}),t&&r(ir,{stats:t}),r(Ln,{data:d.current}),r("div",{class:"flex items-center gap-2 mb-4 mt-8",children:[r("h2",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700",children:"Recent Jobs"}),r("span",{class:"text-xs text-muted",children:"(latest 10)"})]}),a!=null&&a.length?r(E,{columns:Gn,data:a,onRowClick:h=>ae(`/jobs/${h.id}`)}):null]})}function Qn(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/stats/queues"),{data:a,refetch:i}=M("/api/queues/paused"),d=new Set(a??[]),h=t?Object.entries(t).map(([u,p])=>({name:u,pending:p.pending??0,running:p.running??0,paused:d.has(u)})):[],l=async u=>{try{await H(`/api/queues/${encodeURIComponent(u)}/pause`),A(`Queue "${u}" paused`,"success"),o(),i()}catch{A(`Failed to pause queue "${u}"`,"error")}},m=async u=>{try{await H(`/api/queues/${encodeURIComponent(u)}/resume`),A(`Queue "${u}" resumed`,"success"),o(),i()}catch{A(`Failed to resume queue "${u}"`,"error")}},c=[{header:"Queue",accessor:u=>r("span",{class:"font-medium",children:u.name})},{header:"Pending",accessor:u=>r("span",{class:"text-warning tabular-nums font-medium",children:u.pending})},{header:"Running",accessor:u=>r("span",{class:"text-info tabular-nums font-medium",children:u.running})},{header:"Status",accessor:u=>r(B,{status:u.paused?"paused":"active"})},{header:"Actions",accessor:u=>u.paused?r(U,{onClick:()=>m(u.name),children:[r(Bt,{class:"w-3.5 h-3.5"}),"Resume"]}):r(U,{variant:"ghost",onClick:()=>l(u.name),children:[r(Br,{class:"w-3.5 h-3.5"}),"Pause"]})}];return s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(zt,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Queue Management"}),r("p",{class:"text-xs text-muted",children:"Monitor and control individual queues"})]})]}),h.length?r(E,{columns:c,data:h}):r(q,{message:"No queues found",subtitle:"Queues appear when tasks are enqueued"})]})}const Yn=[{header:"Name",accessor:e=>r("span",{class:"font-medium",children:e.name})},{header:"Scope",accessor:e=>r(B,{status:e.scope})},{header:"Health",accessor:e=>r(B,{status:e.health})},{header:"Init (ms)",accessor:e=>r("span",{class:"tabular-nums text-muted",children:[e.init_duration_ms.toFixed(1),"ms"]})},{header:"Recreations",accessor:e=>r("span",{class:`tabular-nums ${e.recreations>0?"text-warning":"text-muted"}`,children:e.recreations})},{header:"Dependencies",accessor:e=>e.depends_on.length?r("span",{class:"text-xs",children:e.depends_on.join(", ")}):r("span",{class:"text-muted",children:"—"})},{header:"Pool",accessor:e=>e.pool?r("span",{class:"text-xs tabular-nums",children:[r("span",{class:"text-info",children:e.pool.active}),"/",e.pool.size," active,"," ",r("span",{class:"text-muted",children:[e.pool.idle," idle"]})]}):r("span",{class:"text-muted",children:"—"})}];function Zn(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/resources");return s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Ut,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Resources"}),r("p",{class:"text-xs text-muted",children:"Worker dependency injection runtime"})]})]}),t!=null&&t.length?r(E,{columns:Yn,data:t}):r(q,{message:"No resources registered",subtitle:"Resources appear when workers start with DI configuration"})]})}const Xn=[{header:"Handler",accessor:e=>r("span",{class:"font-medium",children:e.handler})},{header:"Reconstructions",accessor:e=>r("span",{class:"tabular-nums",children:e.reconstructions})},{header:"Avg (ms)",accessor:e=>r("span",{class:"tabular-nums text-muted",children:[e.avg_ms.toFixed(1),"ms"]})},{header:"Errors",accessor:e=>r("span",{class:`tabular-nums ${e.errors>0?"text-danger font-medium":"text-muted"}`,children:e.errors})}],Kn=[{header:"Strategy",accessor:e=>r("span",{class:"font-medium uppercase text-xs tracking-wide",children:e.strategy})},{header:"Count",accessor:e=>r("span",{class:"tabular-nums",children:e.count})},{header:"Avg (ms)",accessor:e=>r("span",{class:"tabular-nums text-muted",children:[e.avg_ms.toFixed(1),"ms"]})}];function es(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/proxy-stats"),{data:a,loading:i,error:d,refetch:h}=M("/api/interception-stats"),l=t?Object.entries(t).map(([c,u])=>({handler:c,...u})):[],m=a?Object.entries(a).map(([c,u])=>({strategy:c,...u})):[];return r("div",{children:[r("div",{class:"flex items-center gap-3 mb-8",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(Dt,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"System Internals"}),r("p",{class:"text-xs text-muted",children:"Proxy reconstruction and interception metrics"})]})]}),r("div",{class:"mb-8",children:[r("h2",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700 mb-3",children:"Proxy Reconstruction"}),s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):l.length?r(E,{columns:Xn,data:l}):r(q,{message:"No proxy stats available",subtitle:"Stats appear when proxy handlers are used"})]}),r("div",{children:[r("h2",{class:"text-sm font-semibold dark:text-gray-200 text-slate-700 mb-3",children:"Interception"}),d&&!a?r(O,{message:d,onRetry:h}):i&&!a?r(W,{}):m.length?r(E,{columns:Kn,data:m}):r(q,{message:"No interception stats available",subtitle:"Stats appear when argument interception is enabled"})]})]})}function ts(e){const{data:t,loading:n,error:s,refetch:o}=M("/api/workers"),{data:a}=M("/api/stats");return s&&!t?r(O,{message:s,onRetry:o}):n&&!t?r(W,{}):r("div",{children:[r("div",{class:"flex items-center gap-3 mb-6",children:[r("div",{class:"p-2 rounded-lg dark:bg-surface-3 bg-slate-100",children:r(ze,{class:"w-5 h-5 text-accent",strokeWidth:1.8})}),r("div",{children:[r("h1",{class:"text-lg font-semibold dark:text-white text-slate-900",children:"Workers"}),r("p",{class:"text-xs text-muted",children:[(t==null?void 0:t.length)??0," active ","·"," ",(a==null?void 0:a.running)??0," running jobs"]})]})]}),t!=null&&t.length?r("div",{class:"grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-4",children:t.map(i=>r("div",{class:"dark:bg-surface-2 bg-white rounded-xl shadow-sm dark:shadow-black/20 p-5 border dark:border-white/[0.06] border-slate-200 transition-all duration-150 hover:shadow-md hover:dark:shadow-black/30",children:[r("div",{class:"flex items-center gap-2 mb-3",children:[r("span",{class:"w-2 h-2 rounded-full bg-success shadow-sm shadow-success/40"}),r("span",{class:"font-mono text-xs text-accent-light font-medium",children:i.worker_id})]}),r("div",{class:"space-y-2 text-[13px]",children:[r("div",{class:"flex items-center gap-2 text-muted",children:[r(ze,{class:"w-3.5 h-3.5"}),"Queues: ",r("span",{class:"dark:text-gray-200 text-slate-700",children:i.queues})]}),r("div",{class:"flex items-center gap-2 text-muted",children:[r(Me,{class:"w-3.5 h-3.5"}),"Last heartbeat:"," ",r("span",{class:"dark:text-gray-200 text-slate-700",children:L(i.last_heartbeat)})]}),r("div",{class:"flex items-center gap-2 text-muted",children:[r(Me,{class:"w-3.5 h-3.5"}),"Registered:"," ",r("span",{class:"dark:text-gray-200 text-slate-700",children:L(i.registered_at)})]}),i.tags&&r("div",{class:"flex items-center gap-2 text-muted",children:[r(Gr,{class:"w-3.5 h-3.5"}),"Tags: ",r("span",{class:"dark:text-gray-200 text-slate-700",children:i.tags})]})]})]},i.worker_id))}):r(q,{message:"No active workers",subtitle:"Workers will appear when they connect"})]})}function rs(){return r(_n,{children:[r(Et,{children:[r(Vn,{path:"/"}),r(Dn,{path:"/jobs"}),r(On,{path:"/jobs/:id"}),r(Jn,{path:"/metrics"}),r(zn,{path:"/logs"}),r(ts,{path:"/workers"}),r(Tn,{path:"/circuit-breakers"}),r(Rn,{path:"/dead-letters"}),r(Zn,{path:"/resources"}),r(Qn,{path:"/queues"}),r(es,{path:"/system"})]}),r(Sn,{})]})}fr(r(rs,{}),document.getElementById("app")); +
From aa849c35532a94433b64ddae0ff8ef495cd200a7 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:39:16 +0530 Subject: [PATCH 4/4] fix: make sidebar fully opaque in light mode Replace semi-transparent bg-slate-50/50 with solid bg-white so the sidebar is readable in light mode. Dark mode uses solid bg-surface-2 instead of 50% opacity. --- dashboard/src/components/layout/sidebar.tsx | 2 +- py_src/taskito/templates/dashboard.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/src/components/layout/sidebar.tsx b/dashboard/src/components/layout/sidebar.tsx index 0064754..e913590 100644 --- a/dashboard/src/components/layout/sidebar.tsx +++ b/dashboard/src/components/layout/sidebar.tsx @@ -72,7 +72,7 @@ export function Sidebar() { }, []); return ( -