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