Files
stripstream-librarian/apps/backoffice/app/components/JobsList.tsx
Froidefond Julien e64848a216 feat: implement thumbnail generation and management
- Remove unused image dependencies from Cargo.lock.
- Update API to handle thumbnail generation and checkup processes.
- Introduce new routes for rebuilding and regenerating thumbnails.
- Enhance job tracking with progress indicators for thumbnail jobs.
- Update front-end components to display thumbnail job status and progress.
- Add backend logic for managing thumbnail jobs and integrating with the API.
- Refactor existing code to accommodate new thumbnail functionalities.
2026-03-08 20:55:12 +01:00

138 lines
4.5 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { JobRow } from "./JobRow";
interface Job {
id: string;
library_id: string | null;
type: string;
status: string;
created_at: string;
started_at: string | null;
finished_at: string | null;
error_opt: string | null;
stats_json: {
scanned_files: number;
indexed_files: number;
removed_files: number;
errors: number;
} | null;
progress_percent: number | null;
processed_files: number | null;
total_files: number | null;
}
interface JobsListProps {
initialJobs: Job[];
libraries: Map<string, string>;
highlightJobId?: string;
}
function formatDuration(start: string, end: string | null): string {
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const diff = endDate.getTime() - startDate.getTime();
if (diff < 60000) return `${Math.floor(diff / 1000)}s`;
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ${Math.floor((diff % 60000) / 1000)}s`;
return `${Math.floor(diff / 3600000)}h ${Math.floor((diff % 3600000) / 60000)}m`;
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
if (mins < 1) return "Just now";
return `${mins}m ago`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
return date.toLocaleDateString();
}
export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListProps) {
const [jobs, setJobs] = useState(initialJobs);
// Refresh jobs list via SSE
useEffect(() => {
const eventSource = new EventSource("/api/jobs/stream");
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (Array.isArray(data)) {
setJobs(data);
}
} catch (error) {
console.error("Failed to parse SSE data:", error);
}
};
eventSource.onerror = (err) => {
console.error("SSE error:", err);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
const handleCancel = async (id: string) => {
try {
const response = await fetch(`/api/jobs/${id}/cancel`, {
method: "POST",
});
if (response.ok) {
setJobs(jobs.map(job =>
job.id === id ? { ...job, status: "cancelled" } : job
));
}
} catch (error) {
console.error("Failed to cancel job:", error);
}
};
return (
<div className="bg-card rounded-xl shadow-sm border border-border/60 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border/60 bg-muted/50">
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Library</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Type</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Files</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Thumbnails</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Duration</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Created</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-border/60">
{jobs.map((job) => (
<JobRow
key={job.id}
job={job}
libraryName={job.library_id ? libraries.get(job.library_id) : undefined}
highlighted={job.id === highlightJobId}
onCancel={handleCancel}
formatDate={formatDate}
formatDuration={formatDuration}
/>
))}
</tbody>
</table>
</div>
</div>
);
}