From 16012543ea7614eaa90006ae9e1fbae034971208 Mon Sep 17 00:00:00 2001 From: "peter.fong" Date: Thu, 4 Dec 2025 21:22:03 +0100 Subject: [PATCH] templates. done, --- bookscraper/app.py | 159 ++++++++++-- bookscraper/scraper/state.py | 7 + bookscraper/static/css/dashboard.css | 241 ++++++++++++++++++ bookscraper/static/css/style.css | 160 ++++++++++++ bookscraper/static/js/app.js | 33 +++ bookscraper/static/js/dashboard.js | 150 +++++++++++ bookscraper/static/js/helpers.js | 13 + bookscraper/static/js/log_view.js | 87 +++++++ bookscraper/static/js/progress.js | 72 ++++++ bookscraper/templates/base.html | 26 ++ .../templates/components/book_card.html | 0 .../templates/components/book_list_item.html | 61 +++++ .../templates/components/log_view.html | 44 ++++ bookscraper/templates/components/nav.html | 28 ++ .../templates/components/progress_box.html | 61 +++++ .../templates/components/url_input.html | 21 ++ .../templates/dashboard/book_detail.html | 44 ++++ .../templates/dashboard/dashboard.html | 57 +++++ bookscraper/templates/home.html | 23 ++ bookscraper/templates/layout.html | 33 +++ 20 files changed, 1303 insertions(+), 17 deletions(-) create mode 100644 bookscraper/scraper/state.py create mode 100644 bookscraper/static/css/dashboard.css create mode 100644 bookscraper/static/css/style.css create mode 100644 bookscraper/static/js/app.js create mode 100644 bookscraper/static/js/dashboard.js create mode 100644 bookscraper/static/js/helpers.js create mode 100644 bookscraper/static/js/log_view.js create mode 100644 bookscraper/static/js/progress.js create mode 100644 bookscraper/templates/base.html create mode 100644 bookscraper/templates/components/book_card.html create mode 100644 bookscraper/templates/components/book_list_item.html create mode 100644 bookscraper/templates/components/log_view.html create mode 100644 bookscraper/templates/components/nav.html create mode 100644 bookscraper/templates/components/progress_box.html create mode 100644 bookscraper/templates/components/url_input.html create mode 100644 bookscraper/templates/dashboard/book_detail.html create mode 100644 bookscraper/templates/dashboard/dashboard.html create mode 100644 bookscraper/templates/home.html create mode 100644 bookscraper/templates/layout.html diff --git a/bookscraper/app.py b/bookscraper/app.py index bf758c8..2c53ea1 100644 --- a/bookscraper/app.py +++ b/bookscraper/app.py @@ -17,26 +17,28 @@ from scraper.abort import set_abort from scraper.progress import get_progress # UI LOGS (GLOBAL — no book_id) -from scraper.ui_log import get_ui_logs, reset_ui_logs # <-- ADDED +from scraper.ui_log import get_ui_logs, reset_ui_logs from celery.result import AsyncResult +from scraper.state import state as r -# ⬇⬇⬇ TOEGEVOEGD voor cover-serving +# Cover serving from flask import send_from_directory import os -app = Flask(__name__) +import redis +# Flask +app = Flask(__name__) # ===================================================== -# STATIC FILE SERVING FOR OUTPUT ← TOEGEVOEGD +# STATIC FILE SERVING FOR OUTPUT # ===================================================== OUTPUT_ROOT = os.getenv("BOOKSCRAPER_OUTPUT_DIR", "output") @app.route("/output/") def serve_output(filename): - """Serve output files such as cover.jpg and volumes.""" return send_from_directory(OUTPUT_ROOT, filename, as_attachment=False) @@ -56,13 +58,15 @@ def start_scraping(): url = request.form.get("url", "").strip() if not url: - return render_template("result.html", error="Geen URL opgegeven.") + # ★ FIX: dashboard moet altijd books + logs meekrijgen + return render_template( + "dashboard/dashboard.html", + error="Geen URL opgegeven.", + books=list_active_books(), + logs=get_ui_logs(), + ) - # --------------------------------------------------------- - # NEW: Clear UI log buffer when starting a new scrape - # --------------------------------------------------------- reset_ui_logs() - log_debug(f"[WEB] Scraping via Celery: {url}") async_result = celery_app.send_task( @@ -71,16 +75,17 @@ def start_scraping(): queue="scraping", ) + # ★ FIX: direct dashboard tonen met actuele data return render_template( - "result.html", - message="Scraping gestart.", + "dashboard/dashboard.html", scraping_task_id=async_result.id, - book_title=None, + books=list_active_books(), + logs=get_ui_logs(), ) # ===================================================== -# CLEAR UI LOGS MANUALLY (NEW) +# CLEAR UI LOGS # ===================================================== @app.route("/clear-logs", methods=["POST"]) def clear_logs(): @@ -115,7 +120,7 @@ def logs(): # ===================================================== -# CELERY RESULT → return book_id when scraping finishes +# CELERY RESULT → return book_id # ===================================================== @app.route("/celery-result/", methods=["GET"]) def celery_result(task_id): @@ -123,13 +128,133 @@ def celery_result(task_id): if result.successful(): return jsonify({"ready": True, "result": result.get()}) - if result.failed(): return jsonify({"ready": True, "error": "failed"}) - return jsonify({"ready": False}) +# ===================================================== +# REDIS BACKEND — BOOK STATE MODEL +# ===================================================== +REDIS_URL = os.getenv("REDIS_BROKER", "redis://redis:6379/0") +r = redis.Redis.from_url(REDIS_URL, decode_responses=True) + + +def list_active_books(): + """Return list of active books from Redis Book State Model.""" + keys = r.keys("book:*:status") + books = [] + + for key in keys: + book_id = key.split(":")[1] + status = r.get(f"book:{book_id}:status") or "unknown" + title = r.get(f"book:{book_id}:title") or book_id + + dl_done = int(r.get(f"book:{book_id}:download:done") or 0) + dl_total = int(r.get(f"book:{book_id}:download:total") or 0) + au_done = int(r.get(f"book:{book_id}:audio:done") or 0) + au_total = dl_total + + books.append( + { + "book_id": book_id, + "title": title, + "status": status, + "download_done": dl_done, + "download_total": dl_total, + "audio_done": au_done, + "audio_total": au_total, + } + ) + + return books + + +# ===================================================== +# API: list all active books +# ===================================================== +@app.route("/api/books") +def api_books(): + return jsonify(list_active_books()) + + +# ===================================================== +# API: book status +# ===================================================== +@app.route("/api/book//status") +def api_book_status(book_id): + status = r.get(f"book:{book_id}:status") or "unknown" + dl_done = int(r.get(f"book:{book_id}:download:done") or 0) + dl_total = int(r.get(f"book:{book_id}:download:total") or 0) + au_done = int(r.get(f"book:{book_id}:audio:done") or 0) + au_total = dl_total + + return jsonify( + { + "book_id": book_id, + "status": status, + "download_done": dl_done, + "download_total": dl_total, + "audio_done": au_done, + "audio_total": au_total, + } + ) + + +# ===================================================== +# API: book logs +# ===================================================== +@app.route("/api/book//logs") +def api_book_logs(book_id): + logs = r.lrange(f"logs:{book_id}", 0, -1) or [] + return jsonify(logs) + + +# ===================================================== +# VIEW: DASHBOARD +# ===================================================== +@app.route("/dashboard") +def dashboard(): + # ★ FIX: dashboard moet altijd books + logs krijgen + return render_template( + "dashboard/dashboard.html", + books=list_active_books(), + logs=get_ui_logs(), + ) + + +# ===================================================== +# VIEW: BOOK DETAIL PAGE +# ===================================================== +@app.route("/book/") +def book_detail(book_id): + title = r.get(f"book:{book_id}:title") or book_id + return render_template( + "dashboard/book_detail.html", + book_id=book_id, + title=title, + logs=get_ui_logs(), + ) + + +@app.route("/debug/redis-keys") +def debug_redis_keys(): + cursor = 0 + results = {} + + while True: + cursor, keys = r.scan(cursor, match="*", count=200) + for k in keys: + try: + results[k] = r.get(k) + except: + results[k] = "" + if cursor == 0: + break + + return jsonify(results) + + # ===================================================== # RUN FLASK # ===================================================== diff --git a/bookscraper/scraper/state.py b/bookscraper/scraper/state.py new file mode 100644 index 0000000..dc03b7a --- /dev/null +++ b/bookscraper/scraper/state.py @@ -0,0 +1,7 @@ +# scraper/state.py +import os +import redis + +REDIS_STATE_URL = os.getenv("REDIS_STATE", "redis://redis:6379/2") + +state = redis.Redis.from_url(REDIS_STATE_URL, decode_responses=True) diff --git a/bookscraper/static/css/dashboard.css b/bookscraper/static/css/dashboard.css new file mode 100644 index 0000000..1868e87 --- /dev/null +++ b/bookscraper/static/css/dashboard.css @@ -0,0 +1,241 @@ +/* ======================================================================= + File: static/css/dashboard.css + Purpose: + Clean full-width vertical dashboard layout with large log viewer. + ======================================================================= */ + +/* ------------------------------ + GENERAL PAGE LAYOUT + ------------------------------ */ + +/* Dashboard content should use full width */ +.dashboard-container { + display: flex; + flex-direction: column; + width: 100%; + max-width: 1200px; /* voorkomt overflow rechts */ + margin: 20px auto; + padding: 0 20px; + gap: 18px; /* kleiner dan 30px */ +} + +/* ------------------------------ + SECTIONS (input, progress, logs) + ------------------------------ */ + +.dashboard-section { + background: #ffffff; + padding: 16px; /* kleiner */ + border-radius: 6px; + border: 1px solid #ddd; + margin: 0; /* weg extra witruimte */ +} + +.page-title { + font-size: 22px; + margin-bottom: 15px; +} + +/* ------------------------------ + BOOK LIST (optional) + ------------------------------ */ + +.book-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.book-list-empty { + padding: 18px; + text-align: center; + color: #777; +} + +/* List item */ +.book-list-item { + padding: 12px 16px; + background: #f7f7f7; + border-radius: 6px; + border: 1px solid #ccc; + cursor: pointer; + + display: flex; + flex-direction: column; + gap: 6px; + + transition: background 0.2s, border-color 0.2s; +} + +.book-list-item:hover, +.book-list-item.active { + background: #eaf3ff; + border-color: #1e88e5; +} + +/* Title + metadata */ +.book-title { + font-size: 16px; + font-weight: 600; +} + +.book-meta { + font-size: 12px; + color: #555; +} + +.meta-label { + font-weight: 600; +} + +/* ------------------------------ + PROGRESS BOX + ------------------------------ */ + +.progress-box { + background: #fafafa; + border: 1px solid #ddd; + padding: 18px; + border-radius: 6px; + width: 100%; +} + +.progress-header h2 { + margin-bottom: 5px; +} + +.progress-subtitle { + font-size: 14px; + color: #333; + font-weight: 600; +} + +.progress-bookid { + font-size: 12px; + color: #777; + margin-bottom: 15px; +} + +.progress-bar { + height: 14px; + background: #ddd; + border-radius: 6px; + overflow: hidden; + margin-bottom: 6px; +} + +.progress-bar-fill { + height: 100%; + background: #1e88e5; +} + +.progress-bar-fill.audio-fill { + background: #e65100; +} + +.progress-stats { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #444; + margin-top: 4px; +} + +/* ------------------------------ + LOG VIEWER — LARGE FULL-WIDTH + ------------------------------ */ + +.log-viewer { + width: 100%; + max-width: 100%; + overflow: hidden; /* voorkom horizontaal uitsteken */ +} + +.log-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.log-filters { + display: flex; + align-items: center; + gap: 8px; +} + +.log-output { + flex: 1; + width: 100%; + max-width: 100%; + min-height: 60vh; + max-height: 75vh; + + overflow-y: auto; + overflow-x: hidden; /* voorkom dat de log naar rechts uitsteekt */ + + background: #000000; /* Pure terminal black */ + color: #00ff66; /* Matrix / retro green */ + border: 1px solid #0f0; /* neon green frame */ + border-radius: 6px; + padding: 12px; + + font-family: "SF Mono", "Consolas", "Courier New", monospace; + font-size: 13px; + line-height: 1.35; + + white-space: pre-wrap; /* wraps text */ + word-break: break-word; /* lange links breken */ +} + +.log-line { + white-space: pre-wrap; + padding: 2px 0; +} + +.log-empty { + color: #888; + font-style: italic; +} +.log-line { + color: #00ff66; +} + +.log-line:has([DL]), +.log-line:has([DOWNLOAD]) { + color: #00ccff; /* cyan */ +} + +.log-line:has([PARSE]) { + color: #ffaa00; +} /* orange */ +.log-line:has([SAVE]) { + color: #ffdd33; +} +.log-line:has([AUDIO]) { + color: #ff66ff; +} /* purple */ +.log-line:has([CTRL]) { + color: #66aaff; +} +.log-line:has([ERROR]) { + color: #ff3333; +} /* red for errors */ + +/* ------------------------------ + PLACEHOLDER + ------------------------------ */ + +.dashboard-placeholder { + font-size: 15px; + padding: 20px; + text-align: center; + color: #777; +} +.footer { + text-align: center; + padding: 12px; + color: #666; + margin-top: 25px; + font-size: 12px; + border-top: 1px solid #ddd; +} diff --git a/bookscraper/static/css/style.css b/bookscraper/static/css/style.css new file mode 100644 index 0000000..7df8584 --- /dev/null +++ b/bookscraper/static/css/style.css @@ -0,0 +1,160 @@ +/* ======================================================================= + File: static/css/style.css + Purpose: + Global base styling for all pages. + Includes typography, buttons, forms, layout primitives. + ======================================================================= */ + +/* ------------------------------ + RESET / BASE + ------------------------------ */ + +html, +body { + margin: 0; + padding: 0; + font-family: Arial, Helvetica, sans-serif; + background: #f5f6fa; + color: #222; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 20px; +} + +h1, +h2, +h3 { + margin: 0 0 15px 0; + font-weight: 600; +} + +a { + color: #1e88e5; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* ------------------------------ + BUTTONS + ------------------------------ */ + +.btn-primary { + background: #1e88e5; + color: #fff; + padding: 10px 18px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 15px; + transition: background 0.2s ease; +} + +.btn-primary:hover { + background: #1669b9; +} + +.btn-small { + padding: 5px 10px; + background: #ccc; + border-radius: 4px; + border: none; + font-size: 13px; +} + +.btn-small:hover { + background: #bbb; +} + +/* ------------------------------ + FORM ELEMENTS + ------------------------------ */ + +.url-form { + display: flex; + gap: 10px; + flex-direction: column; + max-width: 550px; +} + +.url-label { + font-weight: 600; +} + +.url-input { + padding: 10px; + font-size: 15px; + border: 1px solid #bbb; + border-radius: 4px; +} + +.url-submit { + align-self: flex-start; +} + +/* ------------------------------ + NAVBAR + ------------------------------ */ + +.navbar { + background: #ffffff; + border-bottom: 1px solid #ddd; + padding: 12px 20px; +} + +.nav-inner { + max-width: 1200px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +.nav-brand a { + font-size: 20px; + font-weight: bold; + color: #1e88e5; +} + +.nav-links { + list-style: none; + display: flex; + gap: 25px; + margin: 0; + padding: 0; +} + +.nav-item { + font-size: 15px; + color: #333; +} + +.nav-item:hover { + color: #1e88e5; +} + +/* ------------------------------ + LANDING PAGE + ------------------------------ */ + +.landing-container { + max-width: 600px; + margin: 40px auto; + background: #fff; + padding: 25px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.landing-title { + margin-bottom: 20px; +} + +.landing-links { + margin-top: 20px; +} diff --git a/bookscraper/static/js/app.js b/bookscraper/static/js/app.js new file mode 100644 index 0000000..7e2f98d --- /dev/null +++ b/bookscraper/static/js/app.js @@ -0,0 +1,33 @@ +/* ======================================================================= + File: static/js/app.js + Purpose: + Global utility functions shared across all scripts. + No page-specific logic here. + ======================================================================= */ + +// Shortcuts +const $ = (sel, parent = document) => parent.querySelector(sel); +const $$ = (sel, parent = document) => parent.querySelectorAll(sel); + +// Safe log +function dbg(...args) { + console.log("[APP]", ...args); +} + +// AJAX helper +async function apiGet(url) { + try { + const res = await fetch(url, { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return await res.json(); + } catch (err) { + console.error("API GET Error:", url, err); + return null; + } +} + +// Auto-scroll utility +function autoScroll(el) { + if (!el) return; + el.scrollTop = el.scrollHeight; +} diff --git a/bookscraper/static/js/dashboard.js b/bookscraper/static/js/dashboard.js new file mode 100644 index 0000000..bad2ac9 --- /dev/null +++ b/bookscraper/static/js/dashboard.js @@ -0,0 +1,150 @@ +/* ======================================================================= + File: static/js/dashboard.js + Purpose: + Dashboard interactions: + - select book + - refresh logs + - refresh progress + NOTE: + $ / $$ / autoScroll komen uit helpers.js + ======================================================================= */ + +/* --------------------------------------------------------- + Simple 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 → setup + --------------------------------------------------------- */ +document.addEventListener("DOMContentLoaded", () => { + console.log(">>> dashboard.js DOMContentLoaded"); + // ===================================================== + // GLOBAL FALLBACK POLLING — ALWAYS FETCH LOGS + // Runs when no books exist or no selection has been made + // ===================================================== + console.log(">>> dashboard.js: enabling global fallback polling"); + + setInterval(() => { + // if no active book → fetch global logs + if (!ACTIVE_BOOK) { + refreshBook(null); // triggers /logs + } + }, 2000); + + const items = $$(".book-list-item"); + console.log(">>> dashboard.js found book-list items:", items.length); + + // Geen boeken → geen polling starten + // if (!items || items.length === 0) { + // console.log(">>> dashboard.js: geen boeken aanwezig, polling uit."); + // return; + // } + + // Book selection listener + items.forEach((item) => { + item.addEventListener("click", () => { + console.log(">>> dashboard.js: user clicked book:", item.dataset.bookId); + selectBook(item.dataset.bookId); + }); + }); + + // Auto-select first book + if (!ACTIVE_BOOK && items[0]) { + console.log( + ">>> dashboard.js: auto-select first book:", + items[0].dataset.bookId + ); + selectBook(items[0].dataset.bookId); + } +}); + +/* --------------------------------------------------------- + Select a book (updates UI + starts polling) + --------------------------------------------------------- */ +function selectBook(bookId) { + console.log(">>> selectBook(", bookId, ")"); + + ACTIVE_BOOK = bookId; + + // Highlight + $$(".book-list-item").forEach((el) => { + el.classList.toggle("active", el.dataset.bookId === bookId); + }); + + // Reset previous polling + if (REFRESH_INTERVAL) { + console.log(">>> dashboard.js: clearing previous polling interval"); + clearInterval(REFRESH_INTERVAL); + } + + // Start new polling + console.log(">>> dashboard.js: starting polling for bookId =", bookId); + REFRESH_INTERVAL = setInterval(() => { + refreshBook(ACTIVE_BOOK); + }, 2000); + + // Immediate refresh + refreshBook(ACTIVE_BOOK); +} + +/* --------------------------------------------------------- + Fetch logs + progress from API + --------------------------------------------------------- */ +async function refreshBook(bookId) { + console.log(">>> refreshBook(", bookId, ")"); + + // 1) Als er GEEN bookId is → haal alleen globale logs op + if (!bookId) { + console.log(">>> refreshBook: no active book → fetch /logs"); + + const data = await apiGet("/logs"); + if (data && data.logs) updateLogs(data.logs); + + return; // klaar + } + + // 2) Als er WEL een boek is → haal book status + logs op + const state = await apiGet(`/api/book/${bookId}/status`); + const logs = await apiGet(`/api/book/${bookId}/logs`); + + console.log(">>> refreshBook state =", state); + console.log(">>> refreshBook logs =", logs); + + if (state) updateProgressBars(state); + if (logs) updateLogs(logs); +} + +/* --------------------------------------------------------- + Update LOG VIEW panel + --------------------------------------------------------- */ +function updateLogs(logList) { + const output = $("#log-output"); + if (!output) { + console.warn(">>> updateLogs: no #log-output element found"); + return; + } + + output.innerHTML = ""; + + logList.forEach((line) => logAppend(line)); + + autoScroll(output); +} diff --git a/bookscraper/static/js/helpers.js b/bookscraper/static/js/helpers.js new file mode 100644 index 0000000..a24b496 --- /dev/null +++ b/bookscraper/static/js/helpers.js @@ -0,0 +1,13 @@ +/* ======================================================================= + File: static/js/helpers.js + Purpose: + Shared DOM helpers for all JS files. + ======================================================================= */ + +window.$ = (sel) => document.querySelector(sel); +window.$$ = (sel) => document.querySelectorAll(sel); + +window.autoScroll = function (el) { + if (!el) return; + el.scrollTop = el.scrollHeight; +}; diff --git a/bookscraper/static/js/log_view.js b/bookscraper/static/js/log_view.js new file mode 100644 index 0000000..85507ef --- /dev/null +++ b/bookscraper/static/js/log_view.js @@ -0,0 +1,87 @@ +/* ======================================================================= + File: static/js/log_view.js + Purpose: + Log viewer functionality: + - filtering + - clearing + - auto-scroll + - refresh support for dashboard polling + ======================================================================= */ + +console.log(">>> log_view.js LOADING…"); + +/* --------------------------------------------------------- + Log filtering + --------------------------------------------------------- */ +let LOG_FILTER = "ALL"; + +function applyLogFilter() { + console.log(">>> log_view.js applyLogFilter(), filter =", LOG_FILTER); + + const lines = $$(".log-line"); + console.log(">>> log_view.js number of log-line elements:", lines.length); + + lines.forEach((line) => { + const text = line.innerText; + line.style.display = + LOG_FILTER === "ALL" || text.includes(LOG_FILTER) ? "block" : "none"; + }); +} + +/* --------------------------------------------------------- + UI bindings + --------------------------------------------------------- */ +document.addEventListener("DOMContentLoaded", () => { + console.log(">>> log_view.js DOMContentLoaded"); + + const filterSel = $("#log-filter"); + const clearBtn = $("#log-clear"); + const output = $("#log-output"); + + if (!filterSel) { + console.log(">>> log_view.js: No log viewer found on this page."); + return; + } + + console.log(">>> log_view.js: log viewer detected."); + + // Filter dropdown + filterSel.addEventListener("change", () => { + LOG_FILTER = filterSel.value; + console.log(">>> log_view.js filter changed to:", LOG_FILTER); + applyLogFilter(); + }); + + // Clear log window + if (clearBtn) { + clearBtn.addEventListener("click", () => { + console.log(">>> log_view.js log-clear clicked → clearing output"); + if (output) output.innerHTML = ""; + }); + } +}); + +/* --------------------------------------------------------- + Append a line to the log output + --------------------------------------------------------- */ +function logAppend(lineText) { + const output = $("#log-output"); + + if (!output) { + console.log(">>> log_view.js logAppend() SKIPPED — no #log-output"); + return; + } + + console.log(">>> log_view.js logAppend():", lineText); + + const div = document.createElement("div"); + div.className = "log-line"; + div.innerText = lineText; + + output.appendChild(div); + + applyLogFilter(); + autoScroll(output); +} + +console.log(">>> log_view.js LOADED"); diff --git a/bookscraper/static/js/progress.js b/bookscraper/static/js/progress.js new file mode 100644 index 0000000..a462eea --- /dev/null +++ b/bookscraper/static/js/progress.js @@ -0,0 +1,72 @@ +/* ======================================================================= + File: static/js/progress.js + Purpose: + Update progress bars dynamically for the current book. + Expects data from API endpoints via dashboard.js or start.js. + ======================================================================= */ + +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; + } + + // Data format expected: + // { + // download_done, + // download_total, + // audio_done, + // audio_total + // } + + const barDL = $(".progress-bar-fill"); + const barAU = $(".progress-bar-fill.audio-fill"); + + console.log(">>> progress.js barDL =", barDL); + console.log(">>> progress.js barAU =", barAU); + + 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 updated DL bar to", pctDL.toFixed(1) + "%"); + } else { + console.warn(">>> progress.js: barDL NOT FOUND"); + } + + if (barAU) { + barAU.style.width = pctAU.toFixed(1) + "%"; + console.log(">>> progress.js updated AU bar to", pctAU.toFixed(1) + "%"); + } else { + console.warn(">>> progress.js: barAU NOT FOUND"); + } + + // Update textual stats + const stats = $$(".progress-stats span"); + console.log(">>> progress.js stats elements found:", stats.length); + + // Expected structure: [DL "x/y", DL "pct", AU "x/y", AU "pct"] + 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, found", + stats.length + ); + } +} diff --git a/bookscraper/templates/base.html b/bookscraper/templates/base.html new file mode 100644 index 0000000..57961e4 --- /dev/null +++ b/bookscraper/templates/base.html @@ -0,0 +1,26 @@ + + + + + + BookScraper + + + + + + + + + {% include "components/nav.html" %} + + +
{% block content %}{% endblock %}
+ + + + + + + + diff --git a/bookscraper/templates/components/book_card.html b/bookscraper/templates/components/book_card.html new file mode 100644 index 0000000..e69de29 diff --git a/bookscraper/templates/components/book_list_item.html b/bookscraper/templates/components/book_list_item.html new file mode 100644 index 0000000..69dcaf2 --- /dev/null +++ b/bookscraper/templates/components/book_list_item.html @@ -0,0 +1,61 @@ + + +
+ +
+
{{ book.title }}
+ +
+ ID: {{ book.book_id }} {% if + book.last_update %} + + Updated: {{ book.last_update }} {% endif + %} +
+
+ + +
+ + {{ book.status|capitalize }} + +
+ + +
+ +
+ DL: + + {% set pct_dl = 0 %} {% if book.download_total > 0 %} {% set pct_dl = (100 + * book.download_done / book.download_total) | round(0) %} {% endif %} + +
+
+
+ {{ pct_dl }}% +
+ + +
+ AU: + + {% set pct_au = 0 %} {% if book.audio_total > 0 %} {% set pct_au = (100 * + book.audio_done / book.audio_total) | round(0) %} {% endif %} + +
+
+
+ {{ pct_au }}% +
+
+
diff --git a/bookscraper/templates/components/log_view.html b/bookscraper/templates/components/log_view.html new file mode 100644 index 0000000..87cb611 --- /dev/null +++ b/bookscraper/templates/components/log_view.html @@ -0,0 +1,44 @@ + + +
+ +
+

Live Log

+ +
+ + + + + +
+
+ + +
+ {% if logs and logs|length > 0 %} {% for line in logs %} +
{{ line }}
+ {% endfor %} {% else %} +
No logs yet…
+ {% endif %} +
+
+ + diff --git a/bookscraper/templates/components/nav.html b/bookscraper/templates/components/nav.html new file mode 100644 index 0000000..782a379 --- /dev/null +++ b/bookscraper/templates/components/nav.html @@ -0,0 +1,28 @@ + + + diff --git a/bookscraper/templates/components/progress_box.html b/bookscraper/templates/components/progress_box.html new file mode 100644 index 0000000..11a91e1 --- /dev/null +++ b/bookscraper/templates/components/progress_box.html @@ -0,0 +1,61 @@ + + +
+ +
+

Progress

+ {% if title %} +
{{ title }}
+ {% endif %} {% if book_id %} +
Book ID: {{ book_id }}
+ {% endif %} +
+ + +
+

Download Progress

+ +
+ {% set pct = 0 %} {% if download_total > 0 %} {% set pct = (100 * + download_done / download_total) | round(1) %} {% endif %} +
+
+ +
+ {{ download_done }} / {{ download_total }} + {{ pct }}% +
+
+ + +
+

Audio Progress

+ +
+ {% set pct2 = 0 %} {% if audio_total > 0 %} {% set pct2 = (100 * + audio_done / audio_total) | round(1) %} {% endif %} +
+
+ +
+ {{ audio_done }} / {{ audio_total }} + {{ pct2 }}% +
+
+ +
diff --git a/bookscraper/templates/components/url_input.html b/bookscraper/templates/components/url_input.html new file mode 100644 index 0000000..634e2f2 --- /dev/null +++ b/bookscraper/templates/components/url_input.html @@ -0,0 +1,21 @@ + + +
+ + + + + +
diff --git a/bookscraper/templates/dashboard/book_detail.html b/bookscraper/templates/dashboard/book_detail.html new file mode 100644 index 0000000..342be63 --- /dev/null +++ b/bookscraper/templates/dashboard/book_detail.html @@ -0,0 +1,44 @@ + + +{% extends "layout.html" %} {% block content %} + +
+

{{ title }}

+ + + +
+ {% include "components/progress_box.html" %} +
+ + +
+

Live Log

+ {% include "components/log_view.html" %} +
+
+ + + + + + + + + + + + + + + +{% endblock %} diff --git a/bookscraper/templates/dashboard/dashboard.html b/bookscraper/templates/dashboard/dashboard.html new file mode 100644 index 0000000..d13252c --- /dev/null +++ b/bookscraper/templates/dashboard/dashboard.html @@ -0,0 +1,57 @@ +{% extends "layout.html" %} {% block content %} + +
+ + +
+

Start nieuwe scrape

+ {% include "components/url_input.html" %} +
+ +
+ + +
+

Actieve boeken

+ + {% if books and books|length > 0 %} +
+ {% for book in books %} {% include "components/book_list_item.html" %} {% + endfor %} +
+ {% else %} +
Geen actieve boeken.
+ {% endif %} +
+ +
+ + +
+

Live log (globaal)

+ + {# log_view verwacht altijd 'logs' — garandeer list #} {% set logs = logs or + [] %} {% include "components/log_view.html" %} +
+
+ + + + +{% endblock %} diff --git a/bookscraper/templates/home.html b/bookscraper/templates/home.html new file mode 100644 index 0000000..3e6a5ac --- /dev/null +++ b/bookscraper/templates/home.html @@ -0,0 +1,23 @@ + + +{% extends "layout.html" %} {% block content %} + +
+

Start a New Book Scrape

+ + + {% include "components/url_input.html" %} + + +
+ +{% endblock %} diff --git a/bookscraper/templates/layout.html b/bookscraper/templates/layout.html new file mode 100644 index 0000000..af2c248 --- /dev/null +++ b/bookscraper/templates/layout.html @@ -0,0 +1,33 @@ + + + + + + + BookScraper + + + + + + + + + + + + {% include "components/nav.html" %} + +
{% block content %}{% endblock %}
+
+ BookScraper © 2025 — Powered by Celery + Redis +
+ + + + +