feat(webui): add cached image proxy with configurable dir
Add disk-backed image proxy support to the web UI and expose it via `/api/image`. The proxy validates image URLs, fetches remote images with a timeout, stores image bytes + metadata in a local cache, and serves cached responses with proper content type and cache headers. Also add `SCRAPPR_IMAGE_CACHE` (default `.cache/webui-images`) and pass it through `cmd/outward-web` into `webui.Run`, with startup logging updated to include the cache location. This reduces repeated remote fetches and makes image delivery more reliable for the UI.feat(webui): add cached image proxy with configurable dir Add disk-backed image proxy support to the web UI and expose it via `/api/image`. The proxy validates image URLs, fetches remote images with a timeout, stores image bytes + metadata in a local cache, and serves cached responses with proper content type and cache headers. Also add `SCRAPPR_IMAGE_CACHE` (default `.cache/webui-images`) and pass it through `cmd/outward-web` into `webui.Run`, with startup logging updated to include the cache location. This reduces repeated remote fetches and makes image delivery more reliable for the UI.
This commit is contained in:
@@ -212,8 +212,9 @@ function renderRecipeCard(entry) {
|
||||
.join("");
|
||||
|
||||
const canonicalImageUrl = normalizeImageUrl(entry.item.image_url || "");
|
||||
const image = canonicalImageUrl
|
||||
? `<img src="${escapeHtml(canonicalImageUrl)}" data-fallback-src="${escapeHtml(fallbackImageUrl(canonicalImageUrl))}" alt="${escapeHtml(entry.item.name)}">`
|
||||
const proxiedImageUrl = canonicalImageUrl ? imageProxyUrl(canonicalImageUrl) : "";
|
||||
const image = proxiedImageUrl
|
||||
? `<img src="${escapeHtml(proxiedImageUrl)}" alt="${escapeHtml(entry.item.name)}">`
|
||||
: `<div class="result-fallback">${initials}</div>`;
|
||||
|
||||
const station = entry.recipe.station ? `Station: ${escapeHtml(entry.recipe.station)}` : "Station: Any";
|
||||
@@ -239,18 +240,11 @@ function renderRecipeCard(entry) {
|
||||
}
|
||||
|
||||
function wireResultImages() {
|
||||
const images = resultsRoot.querySelectorAll("img[data-fallback-src]");
|
||||
const images = resultsRoot.querySelectorAll("img");
|
||||
for (const image of images) {
|
||||
image.onerror = () => {
|
||||
const fallback = image.dataset.fallbackSrc || "";
|
||||
if (!fallback || image.dataset.failedOnce === "1") {
|
||||
image.onerror = null;
|
||||
image.replaceWith(createFallbackNode(image.alt || "?"));
|
||||
return;
|
||||
}
|
||||
|
||||
image.dataset.failedOnce = "1";
|
||||
image.src = fallback;
|
||||
image.onerror = null;
|
||||
image.replaceWith(createFallbackNode(image.alt || "?"));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -308,6 +302,10 @@ function fallbackImageUrl(raw) {
|
||||
return base.replace(/\/revision\/latest$/, "");
|
||||
}
|
||||
|
||||
function imageProxyUrl(raw) {
|
||||
return `/api/image?url=${encodeURIComponent(raw)}`;
|
||||
}
|
||||
|
||||
function collapseRequirements(ingredients) {
|
||||
const counts = new Map();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user