/* ======================================================================= File: static/js/dashboard.js Purpose: Dashboard interactions: - Select active book - 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 = null; let REFRESH_INTERVAL = null; console.log(">>> dashboard.js LOADED"); /* --------------------------------------------------------- DOM READY --------------------------------------------------------- */ document.addEventListener("DOMContentLoaded", () => { console.log(">>> dashboard.js DOMContentLoaded"); // Fallback: fetch global logs if no active book setInterval(() => { if (!ACTIVE_BOOK) refreshBook(null); }, 2000); // Sidebar items const items = $$(".book-list-item"); items.forEach((item) => { item.addEventListener("click", () => { selectBook(item.dataset.bookId); }); }); // Auto-select if (!ACTIVE_BOOK && items[0]) { selectBook(items[0].dataset.bookId); } // Initial binding of book-card buttons bindBookCardButtons(); // Refresh sidebar every 2 seconds setInterval(refreshActiveBooks, 2800); }); /* --------------------------------------------------------- Select a book --------------------------------------------------------- */ function selectBook(bookId) { ACTIVE_BOOK = bookId; console.log(">>> Selecting book", bookId); // Highlight sidebar $$(".book-list-item").forEach((el) => { el.classList.toggle("active", el.dataset.bookId === bookId); }); // Reset polling if (REFRESH_INTERVAL) clearInterval(REFRESH_INTERVAL); REFRESH_INTERVAL = setInterval(() => { refreshBook(ACTIVE_BOOK); }, 2000); refreshBook(ACTIVE_BOOK); } /* --------------------------------------------------------- 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.bookId = b.book_id; 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_id)); container.appendChild(div); }); if (!ACTIVE_BOOK && books.length > 0) { selectBook(books[0].book_id); } } /* --------------------------------------------------------- Fetch logs + progress --------------------------------------------------------- */ async function refreshBook(bookId) { if (!bookId) { const data = await apiGet("/logs"); if (data) updateLogs(data); return; } const state = await apiGet(`/api/book/${bookId}/status`); const logs = await apiGet(`/api/book/${bookId}/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; // prevent double-binding btn.dataset.bound = "1"; btn.addEventListener("click", (ev) => { ev.preventDefault(); if (btn.disabled) return; const bookId = btn.closest(".book-card").dataset.bookId; console.log(">>> START clicked:", bookId); startBook(bookId); }); }); // 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 bookId = btn.closest(".book-card").dataset.bookId; console.log(">>> ABORT clicked:", bookId); abortBookAjax(bookId); }); }); } /* --------------------------------------------------------- AJAX START --------------------------------------------------------- */ function startBook(bookId) { console.log(">>> startBook():", bookId); fetch("/start", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `book_id=${bookId}`, }) .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(bookId); }) .catch((err) => console.error("Start failed:", err)); } /* --------------------------------------------------------- AJAX ABORT --------------------------------------------------------- */ function abortBookAjax(bookId) { if (!confirm(`Abort tasks for book ${bookId}?`)) return; console.log(">>> abortBookAjax():", bookId); fetch(`/abort/${bookId}`, { method: "POST" }) .then(async (r) => { let data = null; try { data = await r.json(); } catch (e) {} console.log(">>> /abort response:", data); refreshBookCards(); refreshBook(bookId); }) .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 id = card.dataset.bookId; const info = books.find((b) => b.book_id === id); 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(); // rebind new DOM }