From 8ed1e84772d4fbc12c081085962b43c8615a188a Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Fri, 13 Mar 2026 16:50:51 +0900 Subject: [PATCH] Add in-app debug log panel --- backend/handlers/api.go | 17 ++++++ frontend/app.js | 122 ++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 21 ++++++- frontend/style.css | 32 +++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) diff --git a/backend/handlers/api.go b/backend/handlers/api.go index e73f30b..e71345c 100644 --- a/backend/handlers/api.go +++ b/backend/handlers/api.go @@ -88,6 +88,10 @@ func RegisterRoutes(router *gin.Engine, app *App) { router.POST("/api/search", app.searchMedia) } +func (a *App) debug(message string, data any) { + a.Hub.Broadcast("debug", gin.H{"message": message, "data": data}) +} + func (a *App) handleWS(c *gin.Context) { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, @@ -193,23 +197,28 @@ func (a *App) previewDownload(c *gin.Context) { return } + a.debug("download preview requested", gin.H{"url": req.URL}) cmd := exec.Command("python3", a.WorkerScript, "--mode", "probe", "--url", req.URL) output, err := cmd.CombinedOutput() if err != nil { + a.debug("download preview failed", gin.H{"url": req.URL, "output": string(output), "error": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": summarizeOutput("download preview probe failed", output, err)}) return } var preview PreviewResponse if err := json.Unmarshal(output, &preview); err != nil { + a.debug("download preview invalid json", gin.H{"url": req.URL, "output": string(output)}) c.JSON(http.StatusBadGateway, gin.H{"error": summarizeOutput("download preview returned invalid JSON", output, err)}) return } + a.debug("download preview succeeded", preview) c.JSON(http.StatusOK, preview) } func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath string) { a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "queued", "progress": 0, "url": url}) + a.debug("download command started", gin.H{"url": url, "start": start, "end": end, "quality": quality, "outputPath": outputPath}) cmd := exec.Command("python3", a.WorkerScript, "--url", url, "--start", start, "--end", end, "--quality", quality, "--output", outputPath) stdout, err := cmd.StdoutPipe() if err != nil { @@ -231,6 +240,7 @@ func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath s var msg map[string]any if err := json.Unmarshal(line, &msg); err == nil { msg["type"] = "download" + a.debug("download worker event", msg) a.Hub.Broadcast("progress", msg) } } @@ -242,7 +252,9 @@ func (a *App) runDownload(recordID int64, url, start, end, quality, outputPath s if err := cmd.Wait(); err != nil { status = "failed" a.Hub.Broadcast("progress", gin.H{"type": "download", "status": "error", "progress": 100, "message": err.Error()}) + a.debug("download command failed", gin.H{"url": url, "error": err.Error()}) } + a.debug("download command completed", gin.H{"url": url, "status": status, "outputPath": outputPath}) _ = models.MarkDownloadCompleted(a.DB, recordID, status) } @@ -265,15 +277,18 @@ func (a *App) searchMedia(c *gin.Context) { if len(queryVariants) == 0 { queryVariants = []string{req.Query} } + a.debug("search query variants", gin.H{"query": req.Query, "variants": queryVariants, "platforms": req.Platforms}) enabledPlatforms := normalizePlatforms(req.Platforms) a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "searching " + selectedPlatformLabel(enabledPlatforms), "progress": 35}) results, err := a.SearchService.SearchMedia(queryVariants, enabledPlatforms) if err != nil { + a.debug("search backend failed", gin.H{"error": err.Error(), "variants": queryVariants}) a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search failed", "progress": 100, "message": err.Error()}) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } + a.debug("search backend results", gin.H{"count": len(results), "results": results}) if len(results) == 0 { warning := "SearXNG returned no renderable results." a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "no renderable search results", "progress": 100, "message": warning}) @@ -287,8 +302,10 @@ func (a *App) searchMedia(c *gin.Context) { rankQuery = strings.Join(queryVariants[:min(len(queryVariants), 3)], " ") } scored := rankSearchResults(rankQuery, results) + a.debug("search ranked results", gin.H{"count": len(scored), "results": scored}) a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75}) recommended := evaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored) + a.debug("search gemini recommendations", gin.H{"count": len(recommended), "results": recommended}) err = nil if len(recommended) == 0 { err = fmt.Errorf("gemini vision returned no recommended items across all candidate batches") diff --git a/frontend/app.js b/frontend/app.js index 3596f0d..b8f6738 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -30,6 +30,13 @@ const startLabel = document.getElementById("startLabel"); const endLabel = document.getElementById("endLabel"); const setStartFromPreview = document.getElementById("setStartFromPreview"); const setEndFromPreview = document.getElementById("setEndFromPreview"); +const debugToggle = document.getElementById("debugToggle"); +const debugPanel = document.getElementById("debugPanel"); +const closeDebugPanel = document.getElementById("closeDebugPanel"); +const clearLogs = document.getElementById("clearLogs"); +const downloadLogs = document.getElementById("downloadLogs"); +const debugLogList = document.getElementById("debugLogList"); +const debugSummary = document.getElementById("debugSummary"); let pendingDownload = null; let cropStart = 0; @@ -38,10 +45,50 @@ let cropMax = 0; let activeThumb = null; const activePlatforms = new Set(["envato", "artgrid", "google video"]); const hlsInstances = new WeakMap(); +const debugEntries = []; function setStatus(label, progress) { statusLabel.textContent = label; statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; + logEvent("status", { label, progress }); +} + +function logEvent(type, payload) { + const entry = { + ts: new Date().toISOString(), + type, + payload, + }; + debugEntries.push(entry); + if (debugEntries.length > 400) { + debugEntries.shift(); + } + renderLogs(); +} + +function safeStringify(value) { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +function renderLogs() { + debugSummary.textContent = `${debugEntries.length} events captured`; + debugLogList.innerHTML = ""; + for (const entry of debugEntries.slice().reverse()) { + const node = document.createElement("div"); + node.className = "debug-entry"; + node.innerHTML = ` +
+ ${entry.type} + ${entry.ts} +
+
${safeStringify(entry.payload)}
+ `; + debugLogList.appendChild(node); + } } function toClock(totalSeconds) { @@ -101,6 +148,7 @@ function syncPlatformButtons() { button.classList.toggle("text-zinc-300", !active); button.classList.toggle("border-white/20", !active); } + logEvent("platforms:update", { active: Array.from(activePlatforms) }); } function connectWS() { @@ -109,8 +157,10 @@ function connectWS() { socket.addEventListener("message", (event) => { const payload = JSON.parse(event.data); if (payload.event !== "progress") { + logEvent("ws:message:ignored", payload); return; } + logEvent("ws:message", payload); const data = payload.data; const label = data.status || `${data.type || "task"} in progress`; setStatus(label, Number(data.progress ?? 0)); @@ -125,13 +175,32 @@ function connectWS() { } }); socket.addEventListener("close", () => { + logEvent("ws:close", { reason: "socket closed" }); setTimeout(connectWS, 1000); }); + socket.addEventListener("open", () => { + logEvent("ws:open", { url: socket.url }); + }); + socket.addEventListener("error", () => { + logEvent("ws:error", { url: socket.url }); + }); } async function api(path, options = {}) { + logEvent("api:request", { + path, + method: options.method || "GET", + hasBody: Boolean(options.body), + bodyPreview: typeof options.body === "string" ? options.body.slice(0, 800) : "[non-string body]", + }); const response = await fetch(path, options); const data = await response.json().catch(() => ({})); + logEvent("api:response", { + path, + status: response.status, + ok: response.ok, + body: data, + }); if (!response.ok) { const error = new Error(data.error || "request failed"); error.status = response.status; @@ -144,6 +213,7 @@ async function api(path, options = {}) { function attachVideoSource(video, src) { detachVideoSource(video); if (!src) { + logEvent("preview:attach:skipped", { reason: "empty src" }); return; } if (src.endsWith(".m3u8") && window.Hls && window.Hls.isSupported()) { @@ -151,9 +221,11 @@ function attachVideoSource(video, src) { hls.loadSource(src); hls.attachMedia(video); hlsInstances.set(video, hls); + logEvent("preview:attach:hls", { src }); return; } video.src = src; + logEvent("preview:attach:file", { src }); } function detachVideoSource(video) { @@ -161,6 +233,7 @@ function detachVideoSource(video) { if (existing) { existing.destroy(); hlsInstances.delete(video); + logEvent("preview:detach:hls", { ok: true }); } video.removeAttribute("src"); video.load(); @@ -188,11 +261,13 @@ function renderResults(results) { previewVideo.poster = item.thumbnailUrl || ""; const mediaArea = node.querySelector(".relative"); mediaArea.addEventListener("mouseenter", () => { + logEvent("preview:hover:start", { title: item.title, source: item.source, previewVideoUrl: item.previewVideoUrl }); overlays.forEach((overlay) => overlay.classList.add("hidden")); previewVideo.classList.remove("hidden"); previewVideo.play().catch(() => {}); }); mediaArea.addEventListener("mouseleave", () => { + logEvent("preview:hover:end", { title: item.title, source: item.source }); previewVideo.pause(); previewVideo.currentTime = 0; previewVideo.classList.add("hidden"); @@ -219,6 +294,7 @@ searchForm.addEventListener("submit", async (event) => { searchWarning.textContent = data.warning; searchWarning.classList.remove("hidden"); } + logEvent("search:completed", { results: data.results?.length || 0, queries: data.queries || [] }); setStatus("search complete", 100); } catch (error) { searchWarning.textContent = error.message; @@ -234,12 +310,14 @@ async function uploadFile(file) { uploadResult.textContent = "uploading..."; try { await api("/api/upload", { method: "POST", body: formData }); + logEvent("upload:completed", { fileName: file.name, size: file.size }); } catch (error) { uploadResult.textContent = error.message; } } function openPreviewModal(preview) { + logEvent("preview:modal:open", preview); previewTitle.textContent = preview.title; previewThumbnail.src = preview.thumbnail; previewThumbnail.alt = preview.title; @@ -271,6 +349,7 @@ function openPreviewModal(preview) { } function closeModal() { + logEvent("preview:modal:close", { title: previewTitle.textContent }); previewVideo.pause(); detachVideoSource(previewVideo); previewMediaFrame.style.aspectRatio = ""; @@ -332,6 +411,7 @@ downloadForm.addEventListener("submit", async (event) => { downloadResult.textContent = "preview loaded"; } catch (error) { downloadResult.textContent = error.message; + logEvent("download:preview:error", { message: error.message, data: error.data || null }); } }); @@ -355,6 +435,7 @@ confirmDownload.addEventListener("click", async () => { downloadResult.textContent = data.message; } catch (error) { downloadResult.textContent = error.message; + logEvent("download:start:error", { message: error.message, data: error.data || null }); } }); @@ -399,11 +480,21 @@ for (const [thumb, name] of [[startThumb, "start"], [endThumb, "end"]]) { }); } previewVideo.addEventListener("loadedmetadata", () => { + logEvent("preview:modal:loadedmetadata", { + width: previewVideo.videoWidth, + height: previewVideo.videoHeight, + src: previewVideo.currentSrc || previewVideo.src, + }); if (previewVideo.videoWidth > 0 && previewVideo.videoHeight > 0) { previewMediaFrame.style.aspectRatio = `${previewVideo.videoWidth} / ${previewVideo.videoHeight}`; } }); previewThumbnail.addEventListener("load", () => { + logEvent("preview:thumbnail:loaded", { + width: previewThumbnail.naturalWidth, + height: previewThumbnail.naturalHeight, + src: previewThumbnail.currentSrc || previewThumbnail.src, + }); if (!previewVideo.src && previewThumbnail.naturalWidth > 0 && previewThumbnail.naturalHeight > 0) { previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`; } @@ -421,6 +512,37 @@ for (const button of platformToggles) { }); } +debugToggle.addEventListener("click", () => { + debugPanel.classList.remove("hidden"); + logEvent("debug:panel:open", {}); +}); +closeDebugPanel.addEventListener("click", () => { + debugPanel.classList.add("hidden"); +}); +clearLogs.addEventListener("click", () => { + debugEntries.length = 0; + renderLogs(); +}); +downloadLogs.addEventListener("click", () => { + const blob = new Blob( + [debugEntries.map((entry) => `[${entry.ts}] ${entry.type}\n${safeStringify(entry.payload)}\n`).join("\n")], + { type: "text/plain;charset=utf-8" }, + ); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `ai-media-hub-${new Date().toISOString().replace(/[:.]/g, "-")}.log`; + anchor.click(); + URL.revokeObjectURL(url); +}); +window.addEventListener("error", (event) => { + logEvent("window:error", { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno }); +}); +window.addEventListener("unhandledrejection", (event) => { + logEvent("window:unhandledrejection", { reason: String(event.reason) }); +}); + connectWS(); syncPlatformButtons(); setStatus(`active platforms: ${Array.from(activePlatforms).join(", ")}`, 0); +logEvent("app:ready", { activePlatforms: Array.from(activePlatforms) }); diff --git a/frontend/index.html b/frontend/index.html index 376b1f5..4eed718 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -74,6 +74,25 @@ + + + +