Recover modal preview rendering and card fallbacks
build-push / docker (push) Successful in 4m34s

This commit is contained in:
AI Assistant
2026-03-16 16:41:35 +09:00
parent 2064825d29
commit 794aec1496
3 changed files with 121 additions and 15 deletions
+15
View File
@@ -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.
+100 -12
View File
@@ -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;
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 });
+5 -2
View File
@@ -172,7 +172,7 @@
<img id="resultModalGoogleImage" class="aspect-video w-full object-cover" alt="" />
</div>
<div class="flex flex-col gap-3 rounded-2xl border border-white/10 bg-white/[0.04] p-5 text-left">
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">Google Video</p>
<p id="resultModalFallbackLabel" class="text-xs uppercase tracking-[0.25em] text-zinc-500">Preview Fallback</p>
<p id="resultModalGoogleText" class="text-sm leading-7 text-zinc-200"></p>
</div>
</div>
@@ -202,6 +202,9 @@
<div class="relative aspect-video overflow-hidden bg-zinc-900">
<img class="h-full w-full object-cover transition duration-500 group-hover:scale-105" alt="" />
<video class="preview-hover absolute inset-0 hidden h-full w-full object-cover" muted loop playsinline preload="none"></video>
<div class="media-fallback absolute inset-0 hidden items-center justify-center bg-[radial-gradient(circle_at_top,#2b3342,transparent_60%),linear-gradient(180deg,#111827,#05070b)] p-5 text-center text-xs uppercase tracking-[0.24em] text-zinc-300">
Preview unavailable
</div>
<div class="preview-overlay absolute left-3 top-3 rounded-full border border-white/20 bg-black/60 px-3 py-1 text-[11px] uppercase tracking-[0.25em] text-white">AI Recommended</div>
<div class="source-badge preview-overlay absolute bottom-3 left-3 rounded-full bg-white px-3 py-1 text-[11px] font-medium uppercase tracking-[0.2em] text-black"></div>
</div>
@@ -213,6 +216,6 @@
</button>
</template>
<script src="/app.js?v=20260316g" defer></script>
<script src="/app.js?v=20260316h" defer></script>
</body>
</html>