"use client"; import { ActionButton } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { keepPreviousData } from "@tanstack/react-query"; import { Activity, AlertTriangle, Clock, Database, Globe, HelpCircle, Image, RefreshCw, Rss, Search, Sparkle, Video, Webhook, } from "lucide-react"; import { Button } from "../ui/button"; import { AdminCard } from "./AdminCard"; interface JobStats { queued: number; pending?: number; failed?: number; } interface JobAction { label: string; onClick: () => Promise; loading: boolean; } function JobStatusExplanation() { const { t } = useTranslation(); return ( {t("admin.background_jobs.status.title")}

{t("admin.background_jobs.status.queued.title")}

{t("admin.background_jobs.status.queued.description")}

{t("admin.background_jobs.status.unprocessed.title")}

{t("admin.background_jobs.status.unprocessed.description")}

{t("admin.background_jobs.status.failed.title")}

{t("admin.background_jobs.status.failed.description")}

); } function JobCardSkeleton() { return (
); } function JobCard({ title, icon: Icon, stats, description, actions = [], }: { title: string; icon: React.ComponentType<{ className?: string }>; stats: JobStats; description: string; actions?: JobAction[]; }) { const { t } = useTranslation(); const total = stats.queued + (stats.pending || 0) + (stats.failed || 0); const hasActivity = total > 0; return (
{title}
{hasActivity && ( {t("admin.background_jobs.active")} )}
{description}
{t("admin.background_jobs.status.queued.title")} {stats.queued}
{stats.pending !== undefined && (
{t("admin.background_jobs.status.unprocessed.title")} {stats.pending}
)} {stats.failed !== undefined && stats.failed > 0 && (
{t("admin.background_jobs.status.failed.title")} {stats.failed}
)}
{actions.length > 0 && (

{t("admin.background_jobs.available_actions")}

{actions.map((action, index) => ( ( { await action.onClick(); setDialogOpen(false); }} className="h-auto justify-start px-3 py-2.5 text-left text-sm" > {t("actions.confirm")} )} > ))}
)}
); } function useJobActions() { const { t } = useTranslation(); const { mutateAsync: recrawlLinks, isPending: isRecrawlPending } = api.admin.recrawlLinks.useMutation({ onSuccess: () => { toast({ description: "Recrawl enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); const { mutateAsync: reindexBookmarks, isPending: isReindexPending } = api.admin.reindexAllBookmarks.useMutation({ onSuccess: () => { toast({ description: "Reindex enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); const { mutateAsync: reprocessAssetsFixMode, isPending: isReprocessingPending, } = api.admin.reprocessAssetsFixMode.useMutation({ onSuccess: () => { toast({ description: "Reprocessing enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); const { mutateAsync: reRunInferenceOnAllBookmarks, isPending: isInferencePending, } = api.admin.reRunInferenceOnAllBookmarks.useMutation({ onSuccess: () => { toast({ description: "Inference jobs enqueued", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); const { mutateAsync: tidyAssets, isPending: isTidyAssetsPending } = api.admin.tidyAssets.useMutation({ onSuccess: () => { toast({ description: "Tidy assets request has been enqueued!", }); }, onError: (e) => { toast({ variant: "destructive", description: e.message, }); }, }); return { crawlActions: [ { label: t("admin.background_jobs.actions.recrawl_failed_links_only"), onClick: () => recrawlLinks({ crawlStatus: "failure", runInference: true }), variant: "secondary" as const, loading: isRecrawlPending, }, { label: t("admin.background_jobs.actions.recrawl_all_links"), onClick: () => recrawlLinks({ crawlStatus: "all", runInference: true }), loading: isRecrawlPending, }, { label: `${t("admin.background_jobs.actions.recrawl_all_links")} (${t("admin.background_jobs.actions.without_inference")})`, onClick: () => recrawlLinks({ crawlStatus: "all", runInference: false }), loading: isRecrawlPending, }, ], inferenceActions: [ { label: t( "admin.background_jobs.actions.regenerate_ai_tags_for_failed_bookmarks_only", ), onClick: () => reRunInferenceOnAllBookmarks({ type: "tag", status: "failure" }), variant: "secondary" as const, loading: isInferencePending, }, { label: t( "admin.background_jobs.actions.regenerate_ai_tags_for_all_bookmarks", ), onClick: () => reRunInferenceOnAllBookmarks({ type: "tag", status: "all" }), loading: isInferencePending, }, { label: t( "admin.background_jobs.actions.regenerate_ai_summaries_for_failed_bookmarks_only", ), onClick: () => reRunInferenceOnAllBookmarks({ type: "summarize", status: "failure", }), variant: "secondary" as const, loading: isInferencePending, }, { label: t( "admin.background_jobs.actions.regenerate_ai_summaries_for_all_bookmarks", ), onClick: () => reRunInferenceOnAllBookmarks({ type: "summarize", status: "all" }), loading: isInferencePending, }, ], indexingActions: [ { label: t("admin.background_jobs.actions.reindex_all_bookmarks"), onClick: () => reindexBookmarks(), loading: isReindexPending, }, ], assetPreprocessingActions: [ { label: t("admin.background_jobs.actions.reprocess_assets_fix_mode"), onClick: () => reprocessAssetsFixMode(), loading: isReprocessingPending, }, ], tidyAssetsActions: [ { label: t("admin.background_jobs.actions.clean_assets"), onClick: () => tidyAssets(), loading: isTidyAssetsPending, }, ], }; } export default function BackgroundJobs() { const { t } = useTranslation(); const { data: serverStats } = api.admin.backgroundJobsStats.useQuery( undefined, { refetchInterval: 1000, placeholderData: keepPreviousData, }, ); const actions = useJobActions(); if (!serverStats) { return (
{Array.from({ length: 8 }).map((_, index) => ( ))}
); } const jobs = [ { title: t("admin.background_jobs.jobs.crawler.title"), icon: Globe, stats: serverStats.crawlStats, description: t("admin.background_jobs.jobs.crawler.description"), actions: actions.crawlActions, }, { title: t("admin.background_jobs.jobs.inference.title"), icon: Sparkle, stats: serverStats.inferenceStats, description: t("admin.background_jobs.jobs.inference.description"), actions: actions.inferenceActions, }, { title: t("admin.background_jobs.jobs.indexing.title"), icon: Search, stats: { queued: serverStats.indexingStats.queued }, description: t("admin.background_jobs.jobs.indexing.description"), actions: actions.indexingActions, }, { title: t("admin.background_jobs.jobs.asset_preprocessing.title"), icon: Image, stats: { queued: serverStats.assetPreprocessingStats.queued }, description: t( "admin.background_jobs.jobs.asset_preprocessing.description", ), actions: actions.assetPreprocessingActions, }, { title: t("admin.background_jobs.jobs.tidy_assets.title"), icon: Database, stats: { queued: serverStats.tidyAssetsStats.queued }, description: t("admin.background_jobs.jobs.tidy_assets.description"), actions: actions.tidyAssetsActions, }, { title: t("admin.background_jobs.jobs.video.title"), icon: Video, stats: { queued: serverStats.videoStats.queued }, description: t("admin.background_jobs.jobs.video.description"), actions: [], }, { title: t("admin.background_jobs.jobs.webhook.title"), icon: Webhook, stats: { queued: serverStats.webhookStats.queued }, description: t("admin.background_jobs.jobs.webhook.description"), actions: [], }, { title: t("admin.background_jobs.jobs.feed.title"), icon: Rss, stats: { queued: serverStats.feedStats.queued }, description: t("admin.background_jobs.jobs.feed.description"), actions: [], }, ]; return (

{t("admin.background_jobs.background_jobs")}

{t("admin.background_jobs.monitor_and_manage")}

{jobs.map((job, index) => ( ))}
); }