Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

keeps: surface silent thumbnail-bake fallback to the client

Previously, when the oven failed to produce a fresh thumbnail during
regenerate, keep-prepare-background silently fell back to the previously
cached thumbnailUri from piece.ipfsMedia and continued as if nothing
happened. Users saw "Media regenerated successfully" but the on-chain
thumbnail stayed stale (and objkt's thumb288 re-rendered from the same
old source, keeping the JPEG flattening). Discovered while debugging
$pie not updating its thumbnail despite regenerating.

Changes:
- keep-prepare-background: set job.thumbnailFallback = { kind, reason }
whenever we reuse a previous or artifact URI, and surface via
updateJobStage with a ⚠️ prefix so the stage message itself shows it.
- keep-job.formatJobForClient: expose thumbnailFallback to clients.
- keeps.html pollJobStatus: when job.thumbnailFallback first appears,
emit an error-level track entry explaining what happened, and mark
the thumbnail step as error. Fires once per job (thumbnailFallbackWarned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+35 -8
+1
system/backend/keep-job.mjs
··· 214 214 artifactUri: job.artifactUri, 215 215 thumbnailUri: job.thumbnailUri, 216 216 metadataUri: job.metadataUri, 217 + thumbnailFallback: job.thumbnailFallback || null, 217 218 // Ready state 218 219 preparedData: job.preparedData, 219 220 // Error state
+15 -8
system/netlify/functions/keep-prepare-background.mjs
··· 674 674 stopOvenProgressPoller(); 675 675 if (thumbResult?.ipfsUri) { 676 676 thumbnailUri = thumbResult.ipfsUri; 677 - await setJobResult(jobId, { thumbnailUri }); 677 + await setJobResult(jobId, { thumbnailUri, thumbnailFallback: null }); 678 678 await updateJobStage(jobId, "thumbnail", "Thumbnail baked"); 679 679 log("thumbnail", `Done: ${thumbnailUri}`); 680 680 } else { 681 + const errMsg = thumbResult?.error || "oven returned no ipfsUri"; 681 682 const preexisting = piece.ipfsMedia?.thumbnailUri; 682 683 if (preexisting) { 683 684 thumbnailUri = preexisting; 684 - await setJobResult(jobId, { thumbnailUri }); 685 - log("thumbnail", `Reusing previous: ${thumbnailUri}`); 685 + // Surface the silent fallback so the track log + regenerated-artifact 686 + // panel can show the user that the new thumbnail DIDN'T land and they 687 + // are still looking at the previous bake. Without this the bake 688 + // failure was invisible — new artifactUri on-chain but stale thumb. 689 + await setJobResult(jobId, { thumbnailUri, thumbnailFallback: { kind: "preexisting", reason: errMsg } }); 690 + await updateJobStage(jobId, "thumbnail", `⚠️ Bake failed (${errMsg}); reused previous thumbnail`); 691 + log("thumbnail", `⚠️ Reusing previous ${thumbnailUri} (reason: ${errMsg})`); 686 692 } else { 687 - // Fallback: use artifact as thumbnail so the mint can proceed 688 - log("thumbnail", `Oven failed (${thumbResult?.error || "unknown"}), using artifact as fallback`); 693 + // Fallback to artifact handled below once artifactUri resolves. 694 + await updateJobStage(jobId, "thumbnail", `⚠️ Bake failed (${errMsg}); will reuse artifact`); 695 + log("thumbnail", `Oven failed (${errMsg}), artifact fallback pending`); 689 696 } 690 697 } 691 698 } ··· 697 704 await updateJobStage(jobId, "ipfs", "Pinned to IPFS"); 698 705 log("ipfs", `Artifact: ${artifactUri}`); 699 706 700 - // If thumbnail failed, fall back to artifact URI 707 + // If thumbnail failed AND no previous thumbnail exists, fall back to artifact URI 701 708 if (!thumbnailUri) { 702 709 thumbnailUri = artifactUri; 703 - await setJobResult(jobId, { thumbnailUri }); 704 - log("thumbnail", `Fallback to artifact: ${thumbnailUri}`); 710 + await setJobResult(jobId, { thumbnailUri, thumbnailFallback: { kind: "artifact", reason: "no previous thumbnail to reuse" } }); 711 + log("thumbnail", `⚠️ Fallback to artifact: ${thumbnailUri}`); 705 712 } 706 713 } 707 714
+19
system/public/kidlisp.com/keeps.html
··· 6162 6162 let pollCount = 0; 6163 6163 const MAX_POLLS = 450; // 15 min at 2s intervals 6164 6164 let lastStage = null; 6165 + let thumbnailFallbackWarned = false; // only raise the fallback warning once per job 6165 6166 const STAGE_STUCK_THRESHOLD_MS = 5 * 60 * 1000; // 5 min with no update = stale 6166 6167 6167 6168 function stopPolling() { ··· 6284 6285 if (job.stage !== 'thumbnail' && job.stage !== 'bundle' && mintOvenPreviewTimer) { 6285 6286 clearInterval(mintOvenPreviewTimer); 6286 6287 mintOvenPreviewTimer = null; 6288 + } 6289 + 6290 + // Surface silent thumbnail-bake fallback (oven failed, server 6291 + // reused previous thumbnail or fell back to artifact). Without 6292 + // this the user would see a "success" but their thumbnail stayed 6293 + // stale — previously only logged server-side. 6294 + if (job.thumbnailFallback && !thumbnailFallbackWarned) { 6295 + thumbnailFallbackWarned = true; 6296 + const fb = job.thumbnailFallback; 6297 + const detail = fb.kind === 'preexisting' 6298 + ? `⚠️ Thumbnail bake failed (${fb.reason || 'unknown'}) — reusing previous thumbnail. Sync will not update the image on objkt.` 6299 + : fb.kind === 'artifact' 6300 + ? `⚠️ Thumbnail bake failed (${fb.reason || 'unknown'}) — using artifact as thumbnail.` 6301 + : `⚠️ Thumbnail bake fallback: ${JSON.stringify(fb)}`; 6302 + addTrackEntry(detail, 'error'); 6303 + if (mintStepStates['thumbnail']) { 6304 + setMintStep('thumbnail', 'error', 'Bake failed — fallback used'); 6305 + } 6287 6306 } 6288 6307 6289 6308 // Show final IPFS thumbnail when available