import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Activity, AlertTriangle, CheckCircle2, Clock, RefreshCw, ServerCrash, SkipForward, Wifi, WifiOff, } from "lucide-react"; import { formatDistanceToNow, format } from "date-fns"; // ─── Types ──────────────────────────────────────────────────────────────────── interface CronJobLog { id: number; jobName: string; status: string; startedAt: string; completedAt: string | null; durationMs: number | null; errorMessage: string | null; } interface CronSummary { lastRuns: CronJobLog[]; recentLogs: CronJobLog[]; } interface SeleniumStatus { online: boolean; active_jobs: number; queued_jobs: number; status: string; } // ─── Helpers ────────────────────────────────────────────────────────────────── const JOB_LABELS: Record = { "local-backup": "Local Backup (8 PM)", "usb-backup": "USB Backup (9 PM)", }; function jobLabel(name: string) { return JOB_LABELS[name] ?? name; } function StatusBadge({ status }: { status: string }) { if (status === "success") return ( Success ); if (status === "failed") return ( Failed ); if (status === "skipped") return ( Skipped ); return ( Running ); } function formatDuration(ms: number | null) { if (ms === null) return "—"; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } function formatDate(iso: string | null) { if (!iso) return "—"; return format(new Date(iso), "MMM d, yyyy HH:mm:ss"); } function timeAgo(iso: string | null) { if (!iso) return "—"; return formatDistanceToNow(new Date(iso), { addSuffix: true }); } // ─── Page ───────────────────────────────────────────────────────────────────── export default function JobMonitorPage() { const { data: summary, isLoading: loadingSummary, refetch: refetchSummary, dataUpdatedAt: summaryUpdated, } = useQuery({ queryKey: ["/job-monitor/summary"], queryFn: async () => { const res = await apiRequest("GET", "/api/job-monitor/summary"); return res.json(); }, refetchInterval: 30_000, }); const { data: failed, isLoading: loadingFailed, refetch: refetchFailed, } = useQuery({ queryKey: ["/job-monitor/failed"], queryFn: async () => { const res = await apiRequest("GET", "/api/job-monitor/failed"); return res.json(); }, refetchInterval: 30_000, }); const { data: seleniumStatus, isLoading: loadingSelenium, refetch: refetchSelenium, } = useQuery({ queryKey: ["/job-monitor/selenium-status"], queryFn: async () => { const res = await apiRequest("GET", "/api/job-monitor/selenium-status"); return res.json(); }, refetchInterval: 10_000, }); function refreshAll() { refetchSummary(); refetchFailed(); refetchSelenium(); } const lastUpdated = summaryUpdated ? format(new Date(summaryUpdated), "HH:mm:ss") : null; return (
{/* Header */}

Job Monitor

Background job health and queue status {lastUpdated && ( · last updated {lastUpdated} )}

{/* ── Cron Jobs ── */} Scheduled Cron Jobs {loadingSummary ? (

Loading…

) : !summary?.lastRuns.length ? (

No job runs recorded yet. Jobs are scheduled at 8 PM (local backup) and 9 PM (USB backup).

) : (
{summary.lastRuns.map((log) => (

{jobLabel(log.jobName)}

Last run: {formatDate(log.startedAt)}{" "} ({timeAgo(log.startedAt)})

{log.status === "failed" && log.errorMessage && (

{log.errorMessage}

)}
{formatDuration(log.durationMs)}
))}
)}
{/* ── Selenium Queue ── */} {seleniumStatus?.online ? ( ) : ( )} Selenium Job Queue {loadingSelenium ? (

Loading…

) : (
{/* Online status */}

Service

{seleniumStatus?.online ? ( Online ) : ( Offline )}
{/* Active jobs */}

Active Jobs

0 ? "text-blue-600" : "text-gray-700" }`} > {seleniumStatus?.active_jobs ?? 0}

{/* Queued jobs */}

Queued

0 ? "text-amber-600" : "text-gray-700" }`} > {seleniumStatus?.queued_jobs ?? 0}

)}
{/* ── Failed Alerts ── */} Failed Job Alerts {!loadingFailed && failed && failed.length > 0 && ( {failed.length} )} {loadingFailed ? (

Loading…

) : !failed?.length ? (
No failed jobs — everything looks healthy.
) : (
{failed.map((log) => (
{jobLabel(log.jobName)} {formatDate(log.startedAt)}
{log.errorMessage && (

{log.errorMessage}

)}
))}
)}
{/* ── Recent History ── */} Recent Run History {loadingSummary ? (

Loading…

) : !summary?.recentLogs.length ? (

No history yet.

) : (
{summary.recentLogs.map((log) => ( ))}
Job Started Duration Status
{jobLabel(log.jobName)} {formatDate(log.startedAt)} {formatDuration(log.durationMs)}
)}
); }