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";
// ─── Helpers ──────────────────────────────────────────────────────────────────
const JOB_LABELS = {
"local-backup": "Local Backup (8 PM)",
"usb-backup": "USB Backup (9 PM)",
};
function jobLabel(name) {
return JOB_LABELS[name] ?? name;
}
function StatusBadge({ status }) {
if (status === "success")
return (
Success
);
if (status === "failed")
return (
Failed
);
if (status === "skipped")
return (
Skipped
);
return (
Running
);
}
function formatDuration(ms) {
if (ms === null)
return "—";
if (ms < 1000)
return `${ms}ms`;
return `${(ms / 1000).toFixed(1)}s`;
}
function formatDate(iso) {
if (!iso)
return "—";
return format(new Date(iso), "MMM d, yyyy HH:mm:ss");
}
function timeAgo(iso) {
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.
) : (
| Job |
Started |
Duration |
Status |
{summary.recentLogs.map((log) => (
|
{jobLabel(log.jobName)}
|
{formatDate(log.startedAt)}
|
{formatDuration(log.durationMs)}
|
|
))}
)}
);
}