parent
3a62dfae79
commit
fa2f212e03
@ -0,0 +1,128 @@
|
||||
/* ============================================================
|
||||
File: static/js/bookcard_controller.js
|
||||
Purpose:
|
||||
Single owner for updating book-card DOM from merged state
|
||||
(would_merge_to)
|
||||
============================================================ */
|
||||
|
||||
console.log("[BOOKCARD] controller loaded");
|
||||
|
||||
/* ============================================================
|
||||
ENTRY POINT (called by state_updater.js)
|
||||
============================================================ */
|
||||
|
||||
function updateBookCardsFromState(stateList) {
|
||||
console.log("[BOOKCARD] updateBookCardsFromState called");
|
||||
|
||||
if (!Array.isArray(stateList)) {
|
||||
console.warn("[BOOKCARD] Invalid stateList", stateList);
|
||||
return;
|
||||
}
|
||||
|
||||
const stateById = {};
|
||||
|
||||
stateList.forEach((entry) => {
|
||||
const merged = entry.would_merge_to;
|
||||
if (!merged || merged.book_idx == null) {
|
||||
console.warn("[BOOKCARD] entry without merged/book_idx", entry);
|
||||
return;
|
||||
}
|
||||
stateById[String(merged.book_idx)] = merged;
|
||||
});
|
||||
|
||||
document.querySelectorAll(".book-card").forEach((card) => {
|
||||
const bookIdx = card.dataset.bookIdx;
|
||||
const state = stateById[bookIdx];
|
||||
|
||||
if (!state) {
|
||||
console.debug("[BOOKCARD] No state for book_idx", bookIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[BOOKCARD] Updating card", bookIdx, state.status);
|
||||
updateSingleBookCard(card, state);
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SINGLE CARD UPDATE
|
||||
============================================================ */
|
||||
|
||||
function updateSingleBookCard(card, state) {
|
||||
console.log("[BOOKCARD] updateSingleBookCard", state.book_idx);
|
||||
|
||||
updateStatus(card, state);
|
||||
updateButtons(card, state);
|
||||
updateProgress(card, state);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
STATUS
|
||||
============================================================ */
|
||||
|
||||
function updateStatus(card, state) {
|
||||
console.log("[BOOKCARD][STATUS]", state.book_idx, "→", state.status);
|
||||
card.className = `book-card ${state.status || ""}`;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BUTTONS
|
||||
============================================================ */
|
||||
|
||||
function updateButtons(card, state) {
|
||||
const startBtn = card.querySelector(".icon-start");
|
||||
const abortBtn = card.querySelector(".icon-abort");
|
||||
|
||||
const busy = ["starting", "downloading", "parsing", "audio"];
|
||||
|
||||
console.log("[BOOKCARD][BUTTONS]", state.book_idx, "status:", state.status);
|
||||
|
||||
if (startBtn) {
|
||||
// startBtn.disabled = busy.includes(state.status);
|
||||
}
|
||||
|
||||
if (abortBtn) {
|
||||
abortBtn.disabled = !busy.includes(state.status);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PROGRESS (DOWNLOAD + AUDIO)
|
||||
============================================================ */
|
||||
|
||||
function updateProgress(card, s) {
|
||||
const total = Number(s.chapters_total || 0);
|
||||
|
||||
const downloadDone =
|
||||
Number(s.chapters_download_done || 0) +
|
||||
Number(s.chapters_download_skipped || 0);
|
||||
|
||||
const audioDone = Number(s.audio_done || 0) + Number(s.audio_skipped || 0);
|
||||
|
||||
const downloadPct =
|
||||
total > 0 ? Math.min((downloadDone / total) * 100, 100) : 0;
|
||||
|
||||
const audioPct = total > 0 ? Math.min((audioDone / total) * 100, 100) : 0;
|
||||
|
||||
console.log("[BOOKCARD][PROGRESS]", s.book_idx, {
|
||||
total,
|
||||
downloadDone,
|
||||
audioDone,
|
||||
downloadPct,
|
||||
audioPct,
|
||||
});
|
||||
|
||||
/* ---- DOWNLOAD ---- */
|
||||
const dlBar = card.querySelector('[data-field="download_pct"]');
|
||||
const dlText = card.querySelector('[data-field="download_text"]');
|
||||
|
||||
if (dlBar) dlBar.style.width = `${downloadPct}%`;
|
||||
if (dlText) dlText.textContent = `${downloadDone} / ${total}`;
|
||||
|
||||
/* ---- AUDIO ---- */
|
||||
const auBar = card.querySelector('[data-field="audio_pct"]');
|
||||
const auText = card.querySelector('[data-field="audio_text"]');
|
||||
|
||||
if (auBar) auBar.style.width = `${audioPct}%`;
|
||||
if (auText) auText.textContent = `${audioDone} / ${total}`;
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
/* ============================================================
|
||||
File: static/js/inspect_state.js
|
||||
Purpose:
|
||||
- Receive merged state via state_updater.js
|
||||
- Update ONLY the right-side state tables
|
||||
- NO polling, NO fetch
|
||||
============================================================ */
|
||||
|
||||
console.log("[inspect_state] JS loaded (subscriber mode)");
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
State subscription
|
||||
------------------------------------------------------------ */
|
||||
|
||||
window.addEventListener("state:update", (e) => {
|
||||
const entries = e.detail;
|
||||
|
||||
if (!Array.isArray(entries)) {
|
||||
console.warn("[inspect_state] state:update payload is not array", entries);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[inspect_state] state:update received entries:", entries.length);
|
||||
updateInspectTables(entries);
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Update tables
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function updateInspectTables(entries) {
|
||||
console.log("[inspect_state] updating tables");
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const bookIdx = entry.book_idx;
|
||||
if (bookIdx == null) {
|
||||
console.warn("[inspect_state] entry without book_idx", entry);
|
||||
return;
|
||||
}
|
||||
|
||||
const block = document.querySelector(
|
||||
`.state-block[data-book-idx="${bookIdx}"]`
|
||||
);
|
||||
if (!block) {
|
||||
console.warn("[inspect_state] no state-block for book_idx", bookIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
const table = block.querySelector(".state-table");
|
||||
if (!table) {
|
||||
console.warn("[inspect_state] no state-table for book_idx", bookIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[inspect_state] updating table for book_idx", bookIdx);
|
||||
|
||||
const sql = entry.sqlite || {};
|
||||
const redis = entry.redis || {};
|
||||
const merged = entry.would_merge_to || {};
|
||||
|
||||
table.innerHTML = `
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>SQLite</th>
|
||||
<th>Redis</th>
|
||||
<th>Merged</th>
|
||||
</tr>
|
||||
${row("status", sql, redis, merged)}
|
||||
${row("chapters_total", sql, redis, merged)}
|
||||
${row("downloaded", sql, redis, merged)}
|
||||
${row("chapters_download_done", sql, redis, merged)}
|
||||
${row("chapters_download_skipped", sql, redis, merged)}
|
||||
${row("parsed", sql, redis, merged)}
|
||||
${row("chapters_parsed_done", sql, redis, merged)}
|
||||
${row("audio_done", sql, redis, merged)}
|
||||
${row("audio_skipped", sql, redis, merged)}
|
||||
${row("last_update", sql, redis, merged)}
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Row helper
|
||||
------------------------------------------------------------ */
|
||||
|
||||
function row(field, sql, redis, merged) {
|
||||
const s = sql[field] ?? "";
|
||||
const r = redis[field] ?? "";
|
||||
const m = merged[field] ?? "";
|
||||
|
||||
const cls = String(s) === String(r) ? "same" : "diff";
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<th>${field}</th>
|
||||
<td class="${cls}">${s}</td>
|
||||
<td class="${cls}">${r}</td>
|
||||
<td>${m}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
/* =======================================================================
|
||||
File: static/js/progress.js
|
||||
Purpose:
|
||||
Update progress bars dynamically for the current book.
|
||||
Only updates the main progress box (book_detail page).
|
||||
======================================================================= */
|
||||
|
||||
console.log(">>> progress.js LOADED");
|
||||
|
||||
function updateProgressBars(data) {
|
||||
console.log(">>> progress.js updateProgressBars() CALLED with:", data);
|
||||
|
||||
if (!data) {
|
||||
console.warn(">>> progress.js: NO DATA RECEIVED");
|
||||
return;
|
||||
}
|
||||
|
||||
// We always update inside the main progress box:
|
||||
const container = document.querySelector("#progressSection");
|
||||
if (!container) {
|
||||
console.warn(">>> progress.js: #progressSection NOT FOUND");
|
||||
return;
|
||||
}
|
||||
|
||||
// Select bars ONLY inside the correct section
|
||||
const barDL = container.querySelector(
|
||||
".progress-bar:not(.audio) .progress-bar-fill"
|
||||
);
|
||||
const barAU = container.querySelector(
|
||||
".progress-bar.audio .progress-bar-fill"
|
||||
);
|
||||
|
||||
const pctDL =
|
||||
data.download_total > 0
|
||||
? (100 * data.download_done) / data.download_total
|
||||
: 0;
|
||||
|
||||
const pctAU =
|
||||
data.audio_total > 0 ? (100 * data.audio_done) / data.audio_total : 0;
|
||||
|
||||
if (barDL) {
|
||||
barDL.style.width = pctDL.toFixed(1) + "%";
|
||||
console.log(">>> progress.js DL bar =", pctDL.toFixed(1) + "%");
|
||||
} else {
|
||||
console.warn(">>> progress.js: barDL NOT FOUND INSIDE #progressSection");
|
||||
}
|
||||
|
||||
if (barAU) {
|
||||
barAU.style.width = pctAU.toFixed(1) + "%";
|
||||
console.log(">>> progress.js AU bar =", pctAU.toFixed(1) + "%");
|
||||
} else {
|
||||
console.warn(">>> progress.js: barAU NOT FOUND INSIDE #progressSection");
|
||||
}
|
||||
|
||||
// Textual stats — only update inside progress box
|
||||
const stats = container.querySelectorAll(".progress-stats span");
|
||||
|
||||
// Expected: [DL x/y, DL %, AU x/y, AU %]
|
||||
if (stats.length >= 4) {
|
||||
stats[0].innerText = `${data.download_done} / ${data.download_total}`;
|
||||
stats[1].innerText = pctDL.toFixed(1) + "%";
|
||||
stats[2].innerText = `${data.audio_done} / ${data.audio_total}`;
|
||||
stats[3].innerText = pctAU.toFixed(1) + "%";
|
||||
|
||||
console.log(">>> progress.js stats updated");
|
||||
} else {
|
||||
console.warn(
|
||||
">>> progress.js: not enough stats spans in the container, found",
|
||||
stats.length
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
/* ========================================================
|
||||
File: static/js/state_updater.js
|
||||
Purpose:
|
||||
- Poll /api/state/all
|
||||
- Dispatch merged state to subscribers
|
||||
(bookcard_controller, inspect_state, others)
|
||||
- Pause polling when tab inactive
|
||||
======================================================== */
|
||||
|
||||
console.log("[STATE-UPDATER] loaded");
|
||||
|
||||
const STATE_POLL_INTERVAL_MS = 2500;
|
||||
const STATE_ENDPOINT = "/api/state/all";
|
||||
|
||||
let STATE_TIMER = null;
|
||||
|
||||
/* ========================================================
|
||||
INIT
|
||||
======================================================== */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initStateUpdater();
|
||||
});
|
||||
|
||||
function initStateUpdater() {
|
||||
const cards = document.querySelectorAll(".book-card");
|
||||
|
||||
if (cards.length === 0) {
|
||||
console.log("[STATE-UPDATER] No bookcards found — skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[STATE-UPDATER] Starting updater for ${cards.length} bookcards`);
|
||||
|
||||
startPolling(true);
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
document.hidden ? stopPolling() : startPolling(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* ========================================================
|
||||
DISPATCH
|
||||
======================================================== */
|
||||
|
||||
function dispatchState(entries) {
|
||||
console.debug("[STATE] dispatch", entries.length);
|
||||
|
||||
// 1. Bookcards
|
||||
if (typeof window.updateBookCardsFromState === "function") {
|
||||
window.updateBookCardsFromState(entries);
|
||||
}
|
||||
|
||||
// 2. Inspect state tables / other subscribers
|
||||
window.dispatchEvent(new CustomEvent("state:update", { detail: entries }));
|
||||
}
|
||||
|
||||
/* ========================================================
|
||||
POLLING CONTROL
|
||||
======================================================== */
|
||||
|
||||
function startPolling(immediate = false) {
|
||||
if (STATE_TIMER) return;
|
||||
|
||||
console.log("[STATE-UPDATER] Start polling");
|
||||
|
||||
if (immediate) pollState();
|
||||
|
||||
STATE_TIMER = setInterval(pollState, STATE_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (!STATE_TIMER) return;
|
||||
|
||||
console.log("[STATE-UPDATER] Stop polling (tab inactive)");
|
||||
clearInterval(STATE_TIMER);
|
||||
STATE_TIMER = null;
|
||||
}
|
||||
|
||||
/* ========================================================
|
||||
POLL API
|
||||
======================================================== */
|
||||
|
||||
async function pollState() {
|
||||
if (document.hidden) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(STATE_ENDPOINT, { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
|
||||
const entries = await resp.json();
|
||||
if (!Array.isArray(entries)) return;
|
||||
|
||||
dispatchState(entries);
|
||||
} catch (e) {
|
||||
console.error("[STATE-UPDATER] poll error", e);
|
||||
}
|
||||
}
|
||||
@ -1,62 +1,34 @@
|
||||
<!-- =======================================================================
|
||||
File: templates/components/progress_box.html
|
||||
Purpose: Reusable progress overview (download + audio) for any book.
|
||||
Notes:
|
||||
- Expects the following variables from Flask:
|
||||
book_idx: str
|
||||
title: str
|
||||
download_total: int
|
||||
download_done: int
|
||||
audio_total: int
|
||||
audio_done: int
|
||||
- Pure HTML; JS for live updates will be added later.
|
||||
======================================================================= -->
|
||||
Purpose:
|
||||
Dumb progress UI for a book card.
|
||||
Initial values via Jinja, live updates via state_updater.js
|
||||
======================================================================= -->
|
||||
|
||||
<div class="progress-box">
|
||||
<!-- Header -->
|
||||
<div class="progress-header">
|
||||
<h2>Progress</h2>
|
||||
{% if title %}
|
||||
<div class="progress-subtitle">{{ title }}</div>
|
||||
{% endif %} {% if book_idx %}
|
||||
<div class="progress-bookid">Book IDX: <span>{{ book_idx }}</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- DOWNLOAD SECTION -->
|
||||
<div class="progress-section">
|
||||
<h3>Download Progress</h3>
|
||||
|
||||
<div class="progress-bar">
|
||||
{% set pct = 0 %} {% if download_total > 0 %} {% set pct = (100 *
|
||||
download_done / download_total) | round(1) %} {% endif %}
|
||||
<div class="progress-bar-fill" style="width: {{ pct }}%;"></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-stats">
|
||||
<span>{{ download_done }} / {{ download_total }}</span>
|
||||
<span>{{ pct }}%</span>
|
||||
<!-- DOWNLOAD -->
|
||||
<div class="progress-row">
|
||||
<div class="progress-label">Download</div>
|
||||
<div class="progressbar">
|
||||
<div
|
||||
class="progressbar-fill download"
|
||||
data-field="download_pct"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
<div class="progressbar-text" data-field="download_text">0 / 0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AUDIO SECTION -->
|
||||
<div class="progress-section">
|
||||
<h3>Audio Progress</h3>
|
||||
|
||||
<div class="progress-bar audio">
|
||||
{% set pct2 = 0 %} {% if audio_total > 0 %} {% set pct2 = (100 *
|
||||
audio_done / audio_total) | round(1) %} {% endif %}
|
||||
<!-- AUDIO -->
|
||||
<div class="progress-row">
|
||||
<div class="progress-label">Audio</div>
|
||||
<div class="progressbar">
|
||||
<div
|
||||
class="progress-bar-fill audio-fill"
|
||||
style="width: {{ pct2 }}%;"
|
||||
class="progressbar-fill audio"
|
||||
data-field="audio_pct"
|
||||
style="width: 0%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="progress-stats">
|
||||
<span>{{ audio_done }} / {{ audio_total }}</span>
|
||||
<span>{{ pct2 }}%</span>
|
||||
<div class="progressbar-text" data-field="audio_text">0 / 0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/progress.js"></script>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in new issue