mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
Show disabled jobs properly in admin health dashboard
- Backup and update check jobs show as "disabled" (not "succeeded") when their respective settings are off - Disabled jobs excluded from "x of y jobs healthy" count, sorted to bottom of list, with schedule/next-run hidden and trigger button disabled - Active jobs sorted by next run time - "Never run" uses amber dot to differentiate from gray "disabled" dot - Extract RefreshButton component and add it to all three health cards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -161,22 +161,7 @@ export function SystemHealthCards() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<RefreshButton isValidating={isValidating} onRefresh={refresh} />
|
||||||
<TooltipTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
aria-label="Refresh system health"
|
|
||||||
onClick={() => refresh()}
|
|
||||||
disabled={isValidating}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isValidating ? <Spinner /> : <IconRefresh />}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Refresh</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
@@ -273,24 +258,31 @@ export function SystemHealthCards() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* ── Card 2: Background Jobs ── */}
|
{/* ── Card 2: Background Jobs ── */}
|
||||||
<BackgroundJobsCard jobs={data.jobs} onRefresh={refresh} />
|
<BackgroundJobsCard
|
||||||
|
jobs={data.jobs}
|
||||||
|
isValidating={isValidating}
|
||||||
|
onRefresh={refresh}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* ── Card 3: Storage ── */}
|
{/* ── Card 3: Storage ── */}
|
||||||
<Card className="border-l-2 border-l-primary/30">
|
<Card className="border-l-2 border-l-primary/30">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
<div className="flex items-start gap-3">
|
||||||
<IconDatabase
|
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||||
aria-hidden={true}
|
<IconDatabase
|
||||||
className="size-4 text-primary"
|
aria-hidden={true}
|
||||||
/>
|
className="size-4 text-primary"
|
||||||
</div>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<CardTitle>Storage</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle>Storage</CardTitle>
|
||||||
Image cache and backup disk usage
|
<CardDescription>
|
||||||
</CardDescription>
|
Image cache and backup disk usage
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RefreshButton isValidating={isValidating} onRefresh={refresh} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
@@ -360,12 +352,41 @@ export function SystemHealthCards() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RefreshButton({
|
||||||
|
isValidating,
|
||||||
|
onRefresh,
|
||||||
|
}: {
|
||||||
|
isValidating: boolean;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Refresh system health"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={isValidating}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isValidating ? <Spinner /> : <IconRefresh />}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Refresh</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Background Jobs card with table layout and manual trigger */
|
/** Background Jobs card with table layout and manual trigger */
|
||||||
function BackgroundJobsCard({
|
function BackgroundJobsCard({
|
||||||
jobs,
|
jobs,
|
||||||
|
isValidating,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
}: {
|
}: {
|
||||||
jobs: SystemHealthData["jobs"];
|
jobs: SystemHealthData["jobs"];
|
||||||
|
isValidating: boolean;
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [triggeringJob, setTriggeringJob] = useState<string | null>(null);
|
const [triggeringJob, setTriggeringJob] = useState<string | null>(null);
|
||||||
@@ -392,24 +413,39 @@ function BackgroundJobsCard({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const healthyCount = jobs.filter((j) => j.lastStatus === "success").length;
|
const sortedJobs = [...jobs].sort((a, b) => {
|
||||||
|
// Disabled jobs go to the bottom
|
||||||
|
if (a.disabled !== b.disabled) return a.disabled ? 1 : -1;
|
||||||
|
// Active jobs sorted by next run time (soonest first, null last)
|
||||||
|
if (!a.nextRunAt && !b.nextRunAt) return 0;
|
||||||
|
if (!a.nextRunAt) return 1;
|
||||||
|
if (!b.nextRunAt) return -1;
|
||||||
|
return new Date(a.nextRunAt).getTime() - new Date(b.nextRunAt).getTime();
|
||||||
|
});
|
||||||
|
const activeJobs = jobs.filter((j) => !j.disabled);
|
||||||
|
const healthyCount = activeJobs.filter(
|
||||||
|
(j) => j.lastStatus === "success",
|
||||||
|
).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-l-2 border-l-primary/30">
|
<Card className="border-l-2 border-l-primary/30">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
<div className="flex items-start gap-3">
|
||||||
<IconCalendarCheck
|
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||||
aria-hidden={true}
|
<IconCalendarCheck
|
||||||
className="size-4 text-primary"
|
aria-hidden={true}
|
||||||
/>
|
className="size-4 text-primary"
|
||||||
</div>
|
/>
|
||||||
<div>
|
</div>
|
||||||
<CardTitle>Background jobs</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle>Background jobs</CardTitle>
|
||||||
{healthyCount} of {jobs.length} jobs healthy
|
<CardDescription>
|
||||||
</CardDescription>
|
{healthyCount} of {activeJobs.length} jobs healthy
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RefreshButton isValidating={isValidating} onRefresh={onRefresh} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardContent className="border-t border-border/30 px-0 pt-0 pb-0">
|
<CardContent className="border-t border-border/30 px-0 pt-0 pb-0">
|
||||||
@@ -434,7 +470,7 @@ function BackgroundJobsCard({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{jobs.map((job) => {
|
{sortedJobs.map((job) => {
|
||||||
const isTriggering = triggeringJob === job.jobName;
|
const isTriggering = triggeringJob === job.jobName;
|
||||||
const isRunning = job.isCurrentlyRunning || isTriggering;
|
const isRunning = job.isCurrentlyRunning || isTriggering;
|
||||||
|
|
||||||
@@ -446,10 +482,12 @@ function BackgroundJobsCard({
|
|||||||
{/* Job name + status */}
|
{/* Job name + status */}
|
||||||
<TableCell className="pl-5">
|
<TableCell className="pl-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isRunning ? (
|
{job.disabled ? (
|
||||||
|
<StatusDot status="inactive" label="Disabled" />
|
||||||
|
) : isRunning ? (
|
||||||
<Spinner className="size-2.5" />
|
<Spinner className="size-2.5" />
|
||||||
) : job.lastStatus === null ? (
|
) : job.lastStatus === null ? (
|
||||||
<StatusDot status="inactive" label="Never run" />
|
<StatusDot status="warn" label="Never run" />
|
||||||
) : job.lastStatus === "success" ? (
|
) : job.lastStatus === "success" ? (
|
||||||
<StatusDot status="ok" label="Last run succeeded" />
|
<StatusDot status="ok" label="Last run succeeded" />
|
||||||
) : (
|
) : (
|
||||||
@@ -553,7 +591,7 @@ function BackgroundJobsCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
aria-label="Trigger job"
|
aria-label="Trigger job"
|
||||||
className="size-6"
|
className="size-6"
|
||||||
disabled={isRunning}
|
disabled={isRunning || job.disabled}
|
||||||
onClick={() => handleTrigger(job.jobName)}
|
onClick={() => handleTrigger(job.jobName)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { db } from "@/lib/db/client";
|
|||||||
import { cronRuns, episodes, titles, user } from "@/lib/db/schema";
|
import { cronRuns, episodes, titles, user } from "@/lib/db/schema";
|
||||||
import { listBackups } from "@/lib/services/backup";
|
import { listBackups } from "@/lib/services/backup";
|
||||||
import { imageCacheEnabled } from "@/lib/services/image-cache";
|
import { imageCacheEnabled } from "@/lib/services/image-cache";
|
||||||
|
import { getSetting } from "@/lib/services/settings";
|
||||||
|
import { isUpdateCheckEnabled } from "@/lib/services/update-check";
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./data";
|
const DATA_DIR = process.env.DATA_DIR || "./data";
|
||||||
const DATABASE_URL =
|
const DATABASE_URL =
|
||||||
@@ -39,6 +41,7 @@ export interface SystemHealthData {
|
|||||||
lastStatus: "running" | "success" | "error" | null;
|
lastStatus: "running" | "success" | "error" | null;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
isCurrentlyRunning: boolean;
|
isCurrentlyRunning: boolean;
|
||||||
|
disabled: boolean;
|
||||||
}[];
|
}[];
|
||||||
imageCache: {
|
imageCache: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -175,15 +178,21 @@ function getJobsHealth(): SystemHealthData["jobs"] {
|
|||||||
lastDurationMs = latest.finishedAt.getTime() - latest.startedAt.getTime();
|
lastDurationMs = latest.finishedAt.getTime() - latest.startedAt.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
(jobName === "scheduledBackup" &&
|
||||||
|
getSetting("scheduledBackups") !== "true") ||
|
||||||
|
(jobName === "updateCheck" && !isUpdateCheckEnabled());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jobName,
|
jobName,
|
||||||
cronPattern: schedule?.pattern ?? null,
|
cronPattern: disabled ? null : (schedule?.pattern ?? null),
|
||||||
nextRunAt: schedule?.nextRunAt ?? null,
|
nextRunAt: disabled ? null : (schedule?.nextRunAt ?? null),
|
||||||
lastRunAt: latest?.startedAt?.toISOString() ?? null,
|
lastRunAt: latest?.startedAt?.toISOString() ?? null,
|
||||||
lastDurationMs,
|
lastDurationMs,
|
||||||
lastStatus: (latest?.status as "running" | "success" | "error") ?? null,
|
lastStatus: (latest?.status as "running" | "success" | "error") ?? null,
|
||||||
lastError: latest?.errorMessage ?? null,
|
lastError: latest?.errorMessage ?? null,
|
||||||
isCurrentlyRunning,
|
isCurrentlyRunning,
|
||||||
|
disabled,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user