Files
silo-server/web/src/lib/scanRuns.ts
T
84f8495e4f feat(web): collapse per-scan rows on the admin Libraries page (#436)
* feat(web): collapse per-scan rows on the Libraries page

Bulk single-file scans (e.g. autoscan picking up a season drop) rendered
one full-width row per scan under the library, each with its own stop
button that actually cancelled every scan for the library. With 70+
queued files the table became an unusable wall of paths.

Each library's scans now collapse into one summary line (N running ·
N queued) with a single cancel control, compact rows for the first few
scans (file basename, full path on hover, progress for running ones),
and a Show all expander capped to a scrollable height. The Scan Queue
popover gets the same running-first collapse per library group, shows
basenames instead of full paths, and caps its staggered fade-in delay
so deep rows don't appear seconds late.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(web): address scan-collapse review feedback

- Keep the 'Entire library' target visible on pathless (full-library)
  scan rows in the inline active-work list.
- Drop the redundant '+ N more' button in LibraryScanTasks; the header
  'Show all N' toggle is the single expand control there.
- Extract useCollapsedScans so the table rows and Scan Queue popover
  share one collapse implementation, and always derive the popover
  target from formatActiveScanTarget instead of a hardcoded fallback.
- Update the active-scan progress test for the split mode/target markup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:26:35 -04:00

97 lines
2.6 KiB
TypeScript

import type { ScanRun } from "@/api/types";
export function compareActiveScans(left: ScanRun, right: ScanRun) {
const leftRank = left.status === "running" ? 0 : 1;
const rightRank = right.status === "running" ? 0 : 1;
if (leftRank !== rightRank) {
return leftRank - rightRank;
}
const modeCompare = formatActiveScanMode(left).localeCompare(formatActiveScanMode(right));
if (modeCompare !== 0) {
return modeCompare;
}
return (left.path ?? "").localeCompare(right.path ?? "");
}
export function formatActiveScanMode(scan: Pick<ScanRun, "mode">) {
switch (scan.mode) {
case "library":
return "Full library scan";
case "subtree":
return "Subtree scan";
case "file":
return "Single file scan";
default:
return scan.mode;
}
}
export function formatActiveScanTarget(scan: Pick<ScanRun, "path">) {
if (!scan.path) {
return "Entire library";
}
const segments = scan.path.replace(/\/+$/, "").split("/");
return segments[segments.length - 1] || scan.path;
}
export function formatActiveScanTrigger(trigger: string) {
switch (trigger) {
case "autoscan":
return "Autoscan";
case "library_created":
return "Created";
case "library_paths_changed":
case "library_updated":
return "Updated";
case "library_id":
case "manual":
return "Manual";
case "task:scan_libraries":
case "task_scan_libraries":
return "Scheduled";
default:
return trigger.replace(/_/g, " ");
}
}
export function formatActiveScanTime(iso: string | undefined, prefix: string) {
if (!iso) {
return prefix;
}
const date = new Date(iso);
if (Number.isNaN(date.getTime())) {
return prefix;
}
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
if (seconds < 60) {
return `${prefix} just now`;
}
if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
return `${prefix} ${minutes}m ago`;
}
if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
return `${prefix} ${hours}h ago`;
}
const days = Math.floor(seconds / 86400);
return `${prefix} ${days}d ago`;
}
export function formatActiveScanProgress(scan: ScanRun) {
const result = scan.result;
if (!result) {
return "";
}
if (result.total_files && result.files_processed) {
const percent = Math.max(
0,
Math.min(100, Math.round((result.files_processed / result.total_files) * 100)),
);
return `${result.message ?? "Processing files"} · ${result.files_processed.toLocaleString()} / ${result.total_files.toLocaleString()} (${percent}%)`;
}
return result.message ?? "";
}