fix(web): scan progress — correct percentages, no layout shift, refresh-survivable
Bug: the admin 'Scanning Libraries' widget fed the server's 0-1 progress straight into percent strings and bar widths, so the overall and per-library percentages capped at '1%' and the bars never filled. Both now scale by 100 (matching the setup page, which was already correct). Bug: scan state only reached clients via websocket events, so a page opened (or refreshed) mid-scan showed nothing until the next tick — and the admin widget never re-attached at all. The worker now exposes active jobs (with their library_id so rows keep their names), served as GET /api/scanner/active (admin, re-attaches the widget + polling) and GET /api/scanner/active-summary (any user, restores the sidebar indicator on load). Idle hides both, exactly as before. Annoyance: the sidebar's 'Scanning… N%' was a block element between the logo and the nav, shoving every menu entry down while a scan ran. The percent now lives inside the fixed-height logo row next to the spinner (spinner last, so growing digits extend leftward); nothing below it moves.
This commit is contained in:
1 parent
9bc149f3d9
commit
fd6f0e3353
11 files changed
+245
-38
No files matched your search
+59
-3
@@ -138,7 +138,8 @@ function pollScanProgress(
|
||||
}
|
||||
}
|
||||
|
||||
const overallProgress = Math.round(totalProgress / jobIds.length);
|
||||
// status.progress arrives 0-1 from the server; render as percent.
|
||||
const overallProgress = Math.round((totalProgress / jobIds.length) * 100);
|
||||
const progressBar = document.getElementById(
|
||||
"scan-progress-bar",
|
||||
) as HTMLElement;
|
||||
@@ -172,14 +173,18 @@ function updateLibraryProgress(jobId: string, status: any): void {
|
||||
const bar = document.getElementById(`bar-${jobId}`) as HTMLElement;
|
||||
const statusText = document.getElementById(`status-${jobId}`) as HTMLElement;
|
||||
|
||||
// status.progress arrives 0-1 from the server; render as percent,
|
||||
// otherwise the bar sits at sub-1% forever and never fills.
|
||||
const percent = Math.round((status.progress || 0) * 100);
|
||||
|
||||
if (bar) {
|
||||
bar.style.width = (status.progress || 0) + "%";
|
||||
bar.style.width = percent + "%";
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
const statusMessages: Record<string, string> = {
|
||||
pending: "Pending...",
|
||||
running: `Scanning... ${status.progress || 0}%`,
|
||||
running: `Scanning... ${percent}%`,
|
||||
completed: `✓ Complete (${status.new_items || 0} items)`,
|
||||
failed: `✗ Failed`,
|
||||
};
|
||||
@@ -187,6 +192,55 @@ function updateLibraryProgress(jobId: string, status: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
// On page load: if a scan is already running (started before this page
|
||||
// was opened, or the page was refreshed mid-scan), surface the progress
|
||||
// widget and re-attach polling. Websocket start-events only reach pages
|
||||
// that were already open when the scan began.
|
||||
async function checkActiveScans(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
try {
|
||||
const resp = await fetch("/api/scanner/active", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const jobs = (data.jobs || []) as Array<{
|
||||
job_id: string;
|
||||
library_id?: string;
|
||||
}>;
|
||||
if (jobs.length === 0) return;
|
||||
|
||||
// Label rows by library name where we can resolve one.
|
||||
const libraryNames: Record<string, string> = {};
|
||||
try {
|
||||
const libsResp = await fetch("/api/libraries", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (libsResp.ok) {
|
||||
const libsData = await libsResp.json();
|
||||
for (const lib of libsData.data || []) {
|
||||
libraryNames[lib.id] = lib.name;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Names fall back to ids below.
|
||||
}
|
||||
|
||||
const jobIds = jobs.map((job) => job.job_id);
|
||||
const names: Record<string, string> = {};
|
||||
for (const job of jobs) {
|
||||
names[job.job_id] =
|
||||
(job.library_id && libraryNames[job.library_id]) ||
|
||||
`Library ${(job.library_id || job.job_id).slice(0, 8)}`;
|
||||
}
|
||||
showScanProgress(jobIds, names);
|
||||
pollScanProgress(jobIds, names);
|
||||
} catch (error) {
|
||||
console.error("Failed to check for active scans:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showScanResults(
|
||||
libCount: number,
|
||||
files: number,
|
||||
@@ -389,6 +443,7 @@ document.querySelectorAll<HTMLElement>("[data-merge-source]").forEach((el) => {
|
||||
});
|
||||
|
||||
export {
|
||||
checkActiveScans,
|
||||
hideScanProgress,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
@@ -396,6 +451,7 @@ export {
|
||||
};
|
||||
|
||||
Alpine.data("admin", () => ({
|
||||
checkActiveScans,
|
||||
hideScanProgress,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
|
||||
@@ -41,6 +41,29 @@ const restoreLibrarySelection = (): void => {
|
||||
};
|
||||
|
||||
const initializeScanListener = function (this: any) {
|
||||
// Restore state on page load: a scan that started before this page
|
||||
// was opened (or a refresh mid-scan) emits no websocket event until
|
||||
// its next progress tick, so ask the server directly first.
|
||||
const restoreActiveScan = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
const resp = await fetch("/api/scanner/active-summary", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
if (data.scanning) {
|
||||
this.scanning = true;
|
||||
this.scanProgress = Math.round((data.progress || 0) * 100);
|
||||
}
|
||||
} catch {
|
||||
// Offline or endpoint unavailable — websocket events still drive
|
||||
// the state once they arrive.
|
||||
}
|
||||
};
|
||||
restoreActiveScan();
|
||||
|
||||
const handler = (message: any) => {
|
||||
switch (message.type) {
|
||||
case "scan_progress":
|
||||
|
||||
Reference in new issue
Block a user