From 603884c5a2df677281f345c8785fbcea7b088bbb Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 23 Sep 2026 17:31:13 +0000 Subject: [PATCH] Live log view over WebSocket, full log, scroll pinned to bottom --- frontend/src/api.ts | 11 ----- frontend/src/components/Header.vue | 67 +++++++++++++++++------------- mediahive/server.py | 38 ++++++++++++----- 3 files changed, 64 insertions(+), 52 deletions(-) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 15c4160..a07f23e 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -436,14 +436,3 @@ export async function pickFolderAndAddRoot(): Promise { const folder: string | null = await api.pick_folder() return folder } - -/** - * Fetch the tail of the application log. - */ -export async function getLog(): Promise { - const response = await fetch("/api/log") - if (!response.ok) { - throw new Error(`Failed to load log: ${response.statusText}`) - } - return response.text() -} diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue index 9b1cf2e..90eebff 100644 --- a/frontend/src/components/Header.vue +++ b/frontend/src/components/Header.vue @@ -306,9 +306,8 @@
Application log -
-
{{ appLog }}
+
{{ appLog }}
@@ -317,11 +316,11 @@ @@ -912,21 +934,6 @@ onUnmounted(() => { margin-bottom: 6px; } -.diag-refresh-btn { - padding: 4px 12px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.8rem; - cursor: pointer; - transition: background 0.2s; -} - -.diag-refresh-btn:hover { - background: rgba(255, 255, 255, 0.15); -} - .diag-log { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.75rem; diff --git a/mediahive/server.py b/mediahive/server.py index ba13cfd..a169ca8 100644 --- a/mediahive/server.py +++ b/mediahive/server.py @@ -1089,21 +1089,37 @@ async def get_version(): return {"version": version} -@app.get("/api/log") -async def get_log(): - """Return the tail of the application log file (last ~64 KB).""" +def _read_log() -> str: + """Return the full application log file.""" path = log_dir() / "mediahive.log" if not path.exists(): - return PlainTextResponse("") + return "" try: - with path.open("rb") as f: - f.seek(0, 2) - size = f.tell() - f.seek(max(0, size - 64 * 1024)) - data = f.read() - return PlainTextResponse(data.decode("utf-8", errors="replace")) + return path.read_text(encoding="utf-8", errors="replace") except OSError: - return PlainTextResponse("") + return "" + + +@app.get("/api/log") +async def get_log(): + """Return the full application log file.""" + return PlainTextResponse(_read_log()) + + +@app.websocket("/api/log/ws") +async def ws_log(ws: WebSocket) -> None: + """Stream the application log: full log on connect and on every change.""" + await ws.accept() + last_sent: str | None = None + try: + while True: + current = _read_log() + if current != last_sent: + last_sent = current + await ws.send_text(current) + await asyncio.sleep(1.0) + except WebSocketDisconnect, OSError, RuntimeError: + pass @app.post("/api/client-log", status_code=204)