Extract 8 components from the 1144-line jobs/[id]/page.tsx: - JobSummaryBanner, JobOverviewCard, JobTimelineCard - JobProgressCard, IndexStatsCard, ThumbnailStatsCard - MetadataReportCards, ReadingStatusReportCards - DownloadDetectionCards, JobErrorsCard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import { Card, CardHeader, CardTitle, CardDescription, CardContent, StatBox, ProgressBar } from "@/app/components/ui";
|
|
import type { TranslateFunction } from "@/lib/i18n/dictionaries";
|
|
|
|
interface JobProgressCardProps {
|
|
job: {
|
|
type: string;
|
|
status: string;
|
|
started_at: string | null;
|
|
finished_at: string | null;
|
|
total_files: number | null;
|
|
processed_files: number | null;
|
|
progress_percent: number | null;
|
|
current_file: string | null;
|
|
stats_json: {
|
|
scanned_files: number;
|
|
indexed_files: number;
|
|
removed_files: number;
|
|
errors: number;
|
|
warnings: number;
|
|
} | null;
|
|
};
|
|
isThumbnailOnly: boolean;
|
|
progressTitle: string;
|
|
progressDescription: string | undefined;
|
|
t: TranslateFunction;
|
|
formatDuration: (start: string, end: string | null) => string;
|
|
formatSpeed: (count: number, durationMs: number) => string;
|
|
}
|
|
|
|
export function JobProgressCard({ job, isThumbnailOnly, progressTitle, progressDescription, t }: JobProgressCardProps) {
|
|
const isCompleted = job.status === "success";
|
|
const isPhase2 = job.status === "extracting_pages" || job.status === "generating_thumbnails";
|
|
|
|
const showProgressCard =
|
|
(isCompleted || job.status === "failed" || job.status === "running" || isPhase2) &&
|
|
(job.total_files != null || !!job.current_file);
|
|
|
|
if (!showProgressCard) return null;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{progressTitle}</CardTitle>
|
|
{progressDescription && <CardDescription>{progressDescription}</CardDescription>}
|
|
</CardHeader>
|
|
<CardContent>
|
|
{job.total_files != null && job.total_files > 0 && (
|
|
<>
|
|
<ProgressBar value={job.progress_percent || 0} showLabel size="lg" className="mb-4" />
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<StatBox
|
|
value={job.processed_files ?? 0}
|
|
label={isThumbnailOnly || isPhase2 ? t("jobDetail.generated") : t("jobDetail.processed")}
|
|
variant="primary"
|
|
/>
|
|
<StatBox value={job.total_files} label={t("jobDetail.total")} />
|
|
<StatBox
|
|
value={Math.max(0, job.total_files - (job.processed_files ?? 0))}
|
|
label={t("jobDetail.remaining")}
|
|
variant={isCompleted ? "default" : "warning"}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
{job.current_file && (
|
|
<div className="mt-4 p-3 bg-muted/50 rounded-lg">
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wide">{t("jobDetail.currentFile")}</span>
|
|
<code className="block mt-1 text-xs font-mono text-foreground break-all">{job.current_file}</code>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function IndexStatsCard({ job, t, formatDuration, formatSpeed, durationMs }: {
|
|
job: JobProgressCardProps["job"];
|
|
t: TranslateFunction;
|
|
formatDuration: (start: string, end: string | null) => string;
|
|
formatSpeed: (count: number, durationMs: number) => string;
|
|
durationMs: number;
|
|
}) {
|
|
if (!job.stats_json) return null;
|
|
|
|
const speedCount = job.stats_json.scanned_files;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t("jobDetail.indexStats")}</CardTitle>
|
|
{job.started_at && (
|
|
<CardDescription>
|
|
{formatDuration(job.started_at, job.finished_at)}
|
|
{speedCount > 0 && ` · ${formatSpeed(speedCount, durationMs)} scan rate`}
|
|
</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
|
|
<StatBox value={job.stats_json.scanned_files} label={t("jobDetail.scanned")} variant="success" />
|
|
<StatBox value={job.stats_json.indexed_files} label={t("jobDetail.indexed")} variant="primary" />
|
|
<StatBox value={job.stats_json.removed_files} label={t("jobDetail.removed")} variant="warning" />
|
|
<StatBox value={job.stats_json.warnings ?? 0} label={t("jobDetail.warnings")} variant={(job.stats_json.warnings ?? 0) > 0 ? "warning" : "default"} />
|
|
<StatBox value={job.stats_json.errors} label={t("jobDetail.errors")} variant={job.stats_json.errors > 0 ? "error" : "default"} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function ThumbnailStatsCard({ job, t, formatDuration, formatSpeed, durationMs }: {
|
|
job: JobProgressCardProps["job"];
|
|
t: TranslateFunction;
|
|
formatDuration: (start: string, end: string | null) => string;
|
|
formatSpeed: (count: number, durationMs: number) => string;
|
|
durationMs: number;
|
|
}) {
|
|
if (job.total_files == null) return null;
|
|
|
|
const speedCount = job.processed_files ?? 0;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t("jobDetail.thumbnailStats")}</CardTitle>
|
|
{job.started_at && (
|
|
<CardDescription>
|
|
{formatDuration(job.started_at, job.finished_at)}
|
|
{speedCount > 0 && ` · ${formatSpeed(speedCount, durationMs)} thumbnails/s`}
|
|
</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<StatBox value={job.processed_files ?? job.total_files} label={t("jobDetail.generated")} variant="success" />
|
|
<StatBox value={job.total_files} label={t("jobDetail.total")} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|