/* ======================================================================= File: static/js/dashboard.js Purpose: Dashboard interactions: - Select active book_idx - Live logs & progress - Bookcard AJAX start/abort NOTE: updateLogs() is provided by log_view.js ======================================================================= */ /* --------------------------------------------------------- Utility: Safe fetch wrapper --------------------------------------------------------- */ async function apiGet(url) { try { const r = await fetch(url); if (!r.ok) return null; return await r.json(); } catch (e) { console.error("API GET failed:", url, e); return null; } } /* --------------------------------------------------------- Dashboard state --------------------------------------------------------- */ let ACTIVE_BOOK_IDX = null; let REFRESH_INTERVAL = null; console.log(">>> dashboard.js LOADED"); /* --------------------------------------------------------- DOM READY --------------------------------------------------------- */ document.addEventListener("DOMContentLoaded", () => { console.log(">>> dashboard.js DOMContentLoaded"); // Fallback: global logs when no active book_idx setInterval(() => { if (!ACTIVE_BOOK_IDX) refreshBook(null); }, 2000); // Sidebar items const items = $$(".book-list-item"); items.forEach((item) => { item.addEventListener("click", () => { selectBook(item.dataset.bookIdx); }); }); // Auto-select first book if (!ACTIVE_BOOK_IDX && items[0]) { selectBook(items[0].dataset.bookIdx); } // Bind start/abort buttons inside cards bindBookCardButtons(); // Refresh sidebar every few seconds setInterval(refreshActiveBooks, 2800); }); /* --------------------------------------------------------- Select a book_idx --------------------------------------------------------- */ function selectBook(bookIdx) { ACTIVE_BOOK_IDX = bookIdx; console.log(">>> Selecting book_idx", bookIdx); // Highlight sidebar $$(".book-list-item").forEach((el) => { el.classList.toggle("active", el.dataset.bookIdx === bookIdx); }); // Reset polling if (REFRESH_INTERVAL) clearInterval(REFRESH_INTERVAL); REFRESH_INTERVAL = setInterval(() => { refreshBook(ACTIVE_BOOK_IDX); }, 2000); refreshBook(ACTIVE_BOOK_IDX); } /* --------------------------------------------------------- Refresh sidebar list --------------------------------------------------------- */ async function refreshActiveBooks() { const books = await apiGet("/api/books"); if (!books) return; const container = $("#book-list"); if (!container) return; container.innerHTML = ""; books.forEach((b) => { const div = document.createElement("div"); div.className = "book-list-item"; div.dataset.bookIdx = b.book_idx; div.innerHTML = `
${b.title}
${b.status}
${b.download_done}/${b.download_total} downloaded, ${b.audio_done}/${b.audio_total} audio
`; div.addEventListener("click", () => selectBook(b.book_idx)); container.appendChild(div); }); if (!ACTIVE_BOOK_IDX && books.length > 0) { selectBook(books[0].book_idx); } } /* --------------------------------------------------------- Fetch logs + progress --------------------------------------------------------- */ async function refreshBook(bookIdx) { if (!bookIdx) { const data = await apiGet("/logs"); if (data) updateLogs(data); return; } const state = await apiGet(`/api/book/${bookIdx}/status`); const logs = await apiGet(`/api/book/${bookIdx}/logs`); if (state) { updateProgressBars(state); refreshBookCards(); } if (logs) updateLogs(logs); } /* --------------------------------------------------------- BOOKCARD BUTTON BINDING (idempotent) --------------------------------------------------------- */ function bindBookCardButtons() { console.log(">>> bindBookCardButtons() scanning…"); // START BUTTONS document.querySelectorAll(".book-card .icon-start").forEach((btn) => { if (btn.dataset.bound === "1") return; btn.dataset.bound = "1"; btn.addEventListener("click", (ev) => { ev.preventDefault(); if (btn.disabled) return; const card = btn.closest(".book-card"); const bookIdx = card?.dataset.bookIdx; console.log(">>> START clicked:", bookIdx); if (!bookIdx) { console.error(">>> ERROR: bookIdx missing on .book-card dataset"); return; } startBook(bookIdx); }); }); // ABORT BUTTONS document.querySelectorAll(".book-card .icon-abort").forEach((btn) => { if (btn.dataset.bound === "1") return; btn.dataset.bound = "1"; btn.addEventListener("click", (ev) => { ev.preventDefault(); if (btn.disabled) return; const card = btn.closest(".book-card"); const bookIdx = card?.dataset.bookIdx; console.log(">>> ABORT clicked:", bookIdx); if (!bookIdx) { console.error(">>> ERROR: bookIdx missing on .book-card dataset"); return; } abortBookAjax(bookIdx); }); }); } /* --------------------------------------------------------- AJAX START --------------------------------------------------------- */ function startBook(bookIdx) { console.log(">>> startBook():", bookIdx); fetch("/start", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `book_idx=${bookIdx}`, // backend expects field name book_idx }) .then(async (r) => { console.log(">>> /start status:", r.status); let data = null; try { data = await r.json(); } catch (e) {} console.log(">>> /start response:", data); refreshBookCards(); refreshBook(bookIdx); }) .catch((err) => console.error("Start failed:", err)); } /* --------------------------------------------------------- AJAX ABORT --------------------------------------------------------- */ function abortBookAjax(bookIdx) { if (!confirm(`Abort tasks for book ${bookIdx}?`)) return; console.log(">>> abortBookAjax():", bookIdx); fetch(`/abort/${bookIdx}`, { method: "POST" }) .then(async (r) => { let data = null; try { data = await r.json(); } catch (e) {} console.log(">>> /abort response:", data); refreshBookCards(); refreshBook(bookIdx); }) .catch((err) => console.error("Abort failed:", err)); } /* --------------------------------------------------------- Refresh all book-cards (status, classes, buttons) --------------------------------------------------------- */ async function refreshBookCards() { const books = await apiGet("/api/books"); if (!books) return; document.querySelectorAll(".book-card").forEach((card) => { const idx = card.dataset.bookIdx; const info = books.find((b) => b.book_idx === idx); if (!info) return; // Status CSS card.className = `book-card ${info.status}`; // Button states const startBtn = card.querySelector(".icon-start"); const abortBtn = card.querySelector(".icon-abort"); if (startBtn) startBtn.disabled = info.status !== "registered"; if (abortBtn) abortBtn.disabled = ![ "processing", "downloading", "parsing", "audio", ].includes(info.status); }); bindBookCardButtons(); }