You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kmftools/bookscraper/static/js/dashboard.js

260 lines
7.1 KiB

/* =======================================================================
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 = `
<div class="book-title">${b.title}</div>
<div class="book-status">${b.status}</div>
<div class="book-progress">
${b.download_done}/${b.download_total} downloaded,
${b.audio_done}/${b.audio_total} audio
</div>
`;
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
}