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.
129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import { Alpine } from "./alpine";
|
|
import { initializeSearch } from "./search";
|
|
import {
|
|
initializeTheme,
|
|
changeTheme,
|
|
changeWoodPaneling,
|
|
loadWoodPaneling,
|
|
updateWoodPanelingIndicators,
|
|
} from "./theme";
|
|
import { getSelectedLibrary } from "./storage";
|
|
import { addListener, removeListener } from "./websocket";
|
|
|
|
const logout = (): void => {
|
|
localStorage.removeItem("token");
|
|
localStorage.removeItem("user");
|
|
window.location.href = "/";
|
|
};
|
|
|
|
const restoreLibrarySelection = (): void => {
|
|
const savedLibrary = getSelectedLibrary();
|
|
if (!savedLibrary) return;
|
|
|
|
const dropdownSelectors = [
|
|
"#library-select",
|
|
"#selected-library",
|
|
"#library_id",
|
|
];
|
|
|
|
for (const selector of dropdownSelectors) {
|
|
const dropdown = document.querySelector(selector) as HTMLSelectElement;
|
|
if (dropdown) {
|
|
const option = dropdown.querySelector(`option[value="${savedLibrary}"]`);
|
|
if (option) {
|
|
dropdown.value = savedLibrary;
|
|
dropdown.dispatchEvent(new Event("change", { bubbles: true }));
|
|
console.log("Restored library selection:", savedLibrary);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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":
|
|
this.scanning = true;
|
|
this.scanProgress = Math.round((message.data.progress || 0) * 100);
|
|
this.scanNewItems = message.data.new_items || 0;
|
|
this.scanFilesScanned = message.data.files_scanned || 0;
|
|
break;
|
|
case "scan_complete":
|
|
this.scanning = false;
|
|
this.scanProgress = 100;
|
|
this.scanNewItems = message.data?.new_items || this.scanNewItems;
|
|
window.dispatchEvent(
|
|
new CustomEvent("bookhoard:scan-complete", {
|
|
detail: message.data,
|
|
}),
|
|
);
|
|
setTimeout(() => {
|
|
this.scanProgress = 0;
|
|
this.scanNewItems = 0;
|
|
this.scanFilesScanned = 0;
|
|
}, 3000);
|
|
break;
|
|
case "scan_error":
|
|
this.scanning = false;
|
|
this.scanProgress = 0;
|
|
break;
|
|
}
|
|
};
|
|
|
|
addListener(handler);
|
|
|
|
const headerEl = document.querySelector("[x-data='header']");
|
|
if (headerEl) {
|
|
const observer = new MutationObserver(() => {
|
|
if (!document.contains(headerEl)) {
|
|
removeListener(handler);
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
};
|
|
|
|
export { logout, restoreLibrarySelection };
|
|
|
|
Alpine.data("header", () => ({
|
|
mobileMenuOpen: false,
|
|
logout,
|
|
initializeSearch,
|
|
initializeTheme,
|
|
changeTheme,
|
|
changeWoodPaneling,
|
|
loadWoodPaneling,
|
|
updateWoodPanelingIndicators,
|
|
restoreLibrarySelection,
|
|
initializeScanListener,
|
|
scanning: false,
|
|
scanProgress: 0,
|
|
scanNewItems: 0,
|
|
scanFilesScanned: 0,
|
|
}));
|