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.
272 lines
7.4 KiB
272 lines
7.4 KiB
/* =======================================================================
|
|
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 = `
|
|
<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_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();
|
|
}
|