Compare commits
2 Commits
3a62dfae79
...
516bca6de5
| Author | SHA1 | Date |
|---|---|---|
|
|
516bca6de5 | 4 days ago |
|
|
fa2f212e03 | 4 days ago |
@ -0,0 +1,129 @@
|
|||||||
|
/* ============================================================
|
||||||
|
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 downloadDone = Number(s.downloaded || 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
|
File: templates/components/progress_box.html
|
||||||
Purpose: Reusable progress overview (download + audio) for any book.
|
Purpose:
|
||||||
Notes:
|
Dumb progress UI for a book card.
|
||||||
- Expects the following variables from Flask:
|
Initial values via Jinja, live updates via state_updater.js
|
||||||
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.
|
|
||||||
======================================================================= -->
|
|
||||||
|
|
||||||
<div class="progress-box">
|
<div class="progress-box">
|
||||||
<!-- Header -->
|
<!-- DOWNLOAD -->
|
||||||
<div class="progress-header">
|
<div class="progress-row">
|
||||||
<h2>Progress</h2>
|
<div class="progress-label">Download</div>
|
||||||
{% if title %}
|
<div class="progressbar">
|
||||||
<div class="progress-subtitle">{{ title }}</div>
|
<div
|
||||||
{% endif %} {% if book_idx %}
|
class="progressbar-fill download"
|
||||||
<div class="progress-bookid">Book IDX: <span>{{ book_idx }}</span></div>
|
data-field="download_pct"
|
||||||
{% endif %}
|
style="width: 0%"
|
||||||
</div>
|
></div>
|
||||||
|
<div class="progressbar-text" data-field="download_text">0 / 0</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AUDIO SECTION -->
|
<!-- AUDIO -->
|
||||||
<div class="progress-section">
|
<div class="progress-row">
|
||||||
<h3>Audio Progress</h3>
|
<div class="progress-label">Audio</div>
|
||||||
|
<div class="progressbar">
|
||||||
<div class="progress-bar audio">
|
|
||||||
{% set pct2 = 0 %} {% if audio_total > 0 %} {% set pct2 = (100 *
|
|
||||||
audio_done / audio_total) | round(1) %} {% endif %}
|
|
||||||
<div
|
<div
|
||||||
class="progress-bar-fill audio-fill"
|
class="progressbar-fill audio"
|
||||||
style="width: {{ pct2 }}%;"
|
data-field="audio_pct"
|
||||||
|
style="width: 0%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
<div class="progressbar-text" data-field="audio_text">0 / 0</div>
|
||||||
|
|
||||||
<div class="progress-stats">
|
|
||||||
<span>{{ audio_done }} / {{ audio_total }}</span>
|
|
||||||
<span>{{ pct2 }}%</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/progress.js"></script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in new issue