diff --git a/TODO.md b/TODO.md index 6e90576..f20f6ec 100644 --- a/TODO.md +++ b/TODO.md @@ -255,6 +255,21 @@ - backend debug broadcasts ## Recent Change Log +- Date: `2026-03-16` +- What changed: + - Rewired the result modal to consume backend media metadata instead of hard-coded source branches. + - Google Video now uses embed-first modal rendering again, with iframe timeout fallback to thumbnail/panel mode. + - Search cards now suppress low-value favicon/logo thumbnails and show a neutral “preview unavailable” media state instead of tiny site icons or placeholder-like junk. + - Bumped frontend asset version so browsers pick up the new modal logic. +- Why it changed: + - The UI was still rendering Google Video as a static image panel, and Envato/Artgrid cards were surfacing unusable thumbnails that made the results look broken even when metadata existed. +- How it was verified: + - `go test ./...` + - `bash scripts/selftest.sh` +- What is still risky or incomplete: + - Browser-level validation was still not fully reproducible here, so iframe timeout behavior and provider-specific rendering quirks still need live confirmation in the deployed UI. + - `node` is not installed in this environment, so frontend syntax/build verification is still limited to static inspection plus app boot smoke testing. + - Date: `2026-03-16` - What changed: - Hardened search result enrichment and recommendation metadata for preview recovery work. diff --git a/frontend/app.js b/frontend/app.js index e4579ca..897416f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -49,6 +49,7 @@ const resultModalThumbnail = document.getElementById("resultModalThumbnail"); const resultModalGooglePanel = document.getElementById("resultModalGooglePanel"); const resultModalGoogleImage = document.getElementById("resultModalGoogleImage"); const resultModalGoogleText = document.getElementById("resultModalGoogleText"); +const resultModalFallbackLabel = document.getElementById("resultModalFallbackLabel"); const resultModalOpenExternal = document.getElementById("resultModalOpenExternal"); const resultModalDownload = document.getElementById("resultModalDownload"); const closeResultModal = document.getElementById("closeResultModal"); @@ -65,6 +66,7 @@ const resultModalReady = Boolean( resultModalGooglePanel && resultModalGoogleImage && resultModalGoogleText && + resultModalFallbackLabel && resultModalOpenExternal && resultModalDownload && closeResultModal, @@ -88,6 +90,41 @@ function proxiedPreviewURL(src) { return `/api/preview/stream?url=${encodeURIComponent(src)}`; } +function isLowValueThumbnailURL(src) { + const lower = String(src || "").toLowerCase(); + if (!lower) { + return true; + } + return [ + "favicon", + "apple-touch-icon", + "/logo", + "/icon", + "icon.", + "logo.", + "placehold.co", + ].some((token) => lower.includes(token)) || + ((lower.includes("googleusercontent.com") || lower.includes("gstatic.com") || lower.includes("bing.com") || lower.includes("duckduckgo.com")) && !lower.includes("ytimg.com")); +} + +function hasUsableThumbnail(src) { + return Boolean(src) && !isLowValueThumbnailURL(src); +} + +function summarizeReason(reason) { + const text = String(reason || "").trim(); + if (!text) { + return ""; + } + if (text === "Ranked candidate pending stronger visual evidence.") { + return "Preview evidence pending"; + } + if (text === "Fallback due to missing provider preview.") { + return "Provider preview missing"; + } + return text; +} + function setStatus(label, progress) { statusLabel.textContent = label; statusBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; @@ -393,13 +430,19 @@ function hideModal(element) { } function buildResultModalEmbedURL(item) { + if (item?.embedUrl) { + if (item.source === "Google Video" && item.embedUrl.includes("youtube-nocookie.com/embed/")) { + return `${item.embedUrl}&origin=${encodeURIComponent(window.location.origin)}`; + } + return item.embedUrl; + } if (!item?.link) { return "about:blank"; } if (item.source === "Google Video") { const videoId = extractYouTubeID(item.link); if (videoId) { - return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&origin=${encodeURIComponent(window.location.origin)}`; + return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0&playsinline=1&modestbranding=1&enablejsapi=1&origin=${encodeURIComponent(window.location.origin)}`; } } return item.link; @@ -409,12 +452,14 @@ function resetResultModalMedia() { if (!resultModalReady) { return; } + resultModalFrame.onload = null; resultModalFrame.src = "about:blank"; resultModalVideo.pause(); detachVideoSource(resultModalVideo); resultModalThumbnail.removeAttribute("src"); resultModalGoogleImage.removeAttribute("src"); resultModalGoogleText.textContent = ""; + resultModalFallbackLabel.textContent = "Preview Fallback"; resultModalMediaFrame.style.aspectRatio = ""; setHidden(resultModalFrame, true, ""); setHidden(resultModalVideo, true, ""); @@ -444,13 +489,27 @@ function showResultModalThumbnail(src, alt) { setHidden(resultModalThumbnail, false, ""); } -function showResultModalGooglePanel(item) { - resultModalGoogleImage.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER; +function showResultModalGooglePanel(item, message = "") { + resultModalFallbackLabel.textContent = item.source || "Preview Fallback"; + resultModalGoogleImage.src = hasUsableThumbnail(item.thumbnailUrl) ? item.thumbnailUrl : PREVIEW_PLACEHOLDER; resultModalGoogleImage.alt = item.title || ""; - resultModalGoogleText.textContent = item.snippet || item.reason || "YouTube 페이지 열기 또는 Direct Download를 사용할 수 있습니다."; + resultModalGoogleText.textContent = message || item.previewBlockedReason || item.snippet || item.reason || "Preview fallback is being shown."; setHidden(resultModalGooglePanel, false, "flex"); } +function fallbackResultModalMedia(item, reason) { + logEvent("result:modal:fallback", { title: item.title, source: item.source, reason, mediaMode: item.mediaMode }); + if (item.previewVideoUrl) { + showResultModalVideo(item.previewVideoUrl); + return; + } + if (hasUsableThumbnail(item.thumbnailUrl)) { + showResultModalThumbnail(item.thumbnailUrl, item.title || ""); + return; + } + showResultModalGooglePanel(item, reason || "Preview fallback is being shown."); +} + function renderResults(results) { searchResults.innerHTML = ""; if (!results.length) { @@ -461,15 +520,28 @@ function renderResults(results) { const node = cardTemplate.content.firstElementChild.cloneNode(true); const image = node.querySelector("img"); const previewVideo = node.querySelector(".preview-hover"); + const mediaFallback = node.querySelector(".media-fallback"); const overlays = node.querySelectorAll(".preview-overlay"); - image.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER; - image.alt = item.title; + const usableThumbnail = hasUsableThumbnail(item.thumbnailUrl); + if (usableThumbnail) { + image.src = item.thumbnailUrl; + image.alt = item.title; + image.classList.remove("hidden"); + mediaFallback.classList.add("hidden"); + } else { + image.removeAttribute("src"); + image.alt = ""; + image.classList.add("hidden"); + mediaFallback.classList.remove("hidden"); + mediaFallback.classList.add("flex"); + mediaFallback.textContent = item.source === "Envato" || item.source === "Artgrid" ? `${item.source} preview unavailable` : "Preview unavailable"; + } node.querySelector("h3").textContent = item.title; - node.querySelector(".result-snippet").textContent = item.reason || item.snippet || item.source || ""; - node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : ""; + node.querySelector(".result-snippet").textContent = summarizeReason(item.reason) || item.snippet || item.source || ""; + node.querySelector(".result-reason").textContent = item.snippet ? `Source: ${item.snippet}` : (item.previewBlockedReason || ""); node.querySelector(".source-badge").textContent = item.source; node.addEventListener("click", () => openResultModal(item)); - previewVideo.poster = item.thumbnailUrl || ""; + previewVideo.poster = usableThumbnail ? item.thumbnailUrl : ""; if (item.previewVideoUrl) { const mediaArea = node.querySelector(".relative"); mediaArea.addEventListener("mouseenter", () => { @@ -521,16 +593,32 @@ function openResultModal(item) { activeResultItem = item; resultModalTitle.textContent = item.title || "Untitled"; resultModalSource.textContent = item.source || ""; - resultModalReason.textContent = item.reason || "AI 노트가 없습니다."; + resultModalReason.textContent = summarizeReason(item.reason) || "AI 노트가 없습니다."; resultModalSnippet.textContent = item.snippet || "원본 페이지에서 사용할 수 있는 설명이 없습니다."; resultModalOpenExternal.href = item.link || "#"; const canDirectDownload = item.source === "Google Video" && item.link; resultModalDownload.classList.toggle("hidden", !canDirectDownload); resetResultModalMedia(); - if (item.source === "Google Video") { - showResultModalGooglePanel(item); + const embedURL = buildResultModalEmbedURL(item); + const fallbackReason = item.previewBlockedReason || "Embedded view was unavailable, switched to fallback preview."; + if (item.mediaMode === "embed" && embedURL && embedURL !== "about:blank") { + showResultModalFrame(embedURL); + const timeout = window.setTimeout(() => { + logEvent("result:modal:iframe_timeout", { title: item.title, source: item.source, embedURL }); + if (activeResultItem?.link === item.link) { + resetResultModalMedia(); + fallbackResultModalMedia(item, fallbackReason); + } + }, item.source === "Google Video" ? 5000 : 2000); + resultModalFrame.onload = () => { + window.clearTimeout(timeout); + }; + } else if (item.mediaMode === "preview_video" && item.previewVideoUrl) { + showResultModalVideo(item.previewVideoUrl); + } else if (item.mediaMode === "thumbnail" && hasUsableThumbnail(item.thumbnailUrl)) { + showResultModalThumbnail(item.thumbnailUrl, item.title || ""); } else { - showResultModalFrame(item.link || "about:blank"); + fallbackResultModalMedia(item, fallbackReason); } showModal(resultModal); logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link }); diff --git a/frontend/index.html b/frontend/index.html index 509c9e0..3a10d86 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -172,7 +172,7 @@
-

Google Video

+

Preview Fallback

@@ -202,6 +202,9 @@
+
AI Recommended
@@ -213,6 +216,6 @@ - +