Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

keeps: per-stage elapsed telemetry in the rekeep flow

Long oven bakes (thumbnail stage can run up to 150s) looked identical to
a hang — the same 'Baking thumbnail...' text sat on screen with no sense
of motion and no way to tell if anything was happening. Reported while
regenerating $tam when it took a while and the user felt stuck without
clear feedback.

Adds three overlapping signals during pollJobStatus:

1. Live elapsed seconds appended to the progress overlay detail after
5s, formatted as '· 12s' / '· 1m 4s'. Updates every poll tick.

2. Track-log entry on entering any long-running stage (thumbnail, bundle,
ipfs, metadata) so there's a timestamped record of when it began.

3. Track-log heartbeat at 20s from stage start, then every 30s while the
stage is still active. Prevents the 'is this frozen?' feeling during
the oven's 150s ceiling.

On stage completion a 'took Ns' entry lands in the track log for the
same long-running stages, and the timeline detail gets the total time
appended so it survives after the flow moves on.

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

+57 -3
+57 -3
system/public/kidlisp.com/keeps.html
··· 6227 6227 let lastStage = null; 6228 6228 let thumbnailFallbackWarned = false; // only raise the fallback warning once per job 6229 6229 const STAGE_STUCK_THRESHOLD_MS = 5 * 60 * 1000; // 5 min with no update = stale 6230 + // Per-stage elapsed telemetry. Long oven bakes (up to 150s) look 6231 + // identical to a hang without this — user was left staring at 6232 + // "Baking thumbnail..." with no sense of motion. We track when each 6233 + // stage first went active, append live elapsed seconds to the stage 6234 + // detail, log it on stage transitions, and drop a heartbeat into the 6235 + // track log every 30s of inactivity so things feel alive. 6236 + const stageStartedAt = {}; 6237 + const stageHeartbeatAt = {}; 6238 + const STAGE_ELAPSED_SHOW_MS = 5 * 1000; // start appending "· 5s" after 5s 6239 + const STAGE_HEARTBEAT_MS = 30 * 1000; // track-log "still working" every 30s 6240 + const STAGE_FIRST_HEARTBEAT_MS = 20 * 1000; // first one at 20s since stage start 6241 + const TRACK_LOG_ON_STAGE_ENTER = new Set(['thumbnail', 'bundle', 'ipfs', 'metadata']); 6242 + const prettyStage = (id) => (MINT_STEPS.find(s => s.id === id)?.label || id); 6243 + function formatElapsed(ms) { 6244 + const s = Math.round(ms / 1000); 6245 + if (s < 60) return `${s}s`; 6246 + return `${Math.floor(s / 60)}m ${s % 60}s`; 6247 + } 6230 6248 6231 6249 function stopPolling() { 6232 6250 if (mintPollTimer) { clearInterval(mintPollTimer); mintPollTimer = null; } ··· 6278 6296 6279 6297 // Update timeline from job stage 6280 6298 if (job.stage && job.stage !== 'ready') { 6281 - setMintStep(job.stage, 'active', job.stageMessage || job.stage); 6282 - // Mark previous stages done 6299 + const now = Date.now(); 6300 + const isNewStage = !stageStartedAt[job.stage]; 6301 + if (isNewStage) { 6302 + stageStartedAt[job.stage] = now; 6303 + stageHeartbeatAt[job.stage] = now; 6304 + if (TRACK_LOG_ON_STAGE_ENTER.has(job.stage)) { 6305 + addTrackEntry(`${prettyStage(job.stage)}: ${job.stageMessage || 'working...'}`, 'active'); 6306 + } 6307 + } 6308 + const stageElapsed = now - stageStartedAt[job.stage]; 6309 + const baseMsg = job.stageMessage || job.stage; 6310 + // Append live elapsed seconds after 5s so long oven bakes 6311 + // (thumbnail can take ~150s) visibly tick forward. 6312 + const detail = stageElapsed >= STAGE_ELAPSED_SHOW_MS 6313 + ? `${baseMsg} · ${formatElapsed(stageElapsed)}` 6314 + : baseMsg; 6315 + setMintStep(job.stage, 'active', detail); 6316 + // Track-log heartbeat so the user sees motion even when the 6317 + // stageMessage itself isn't changing. First at 20s from stage 6318 + // start, then every 30s. 6319 + const lastBeatAt = stageHeartbeatAt[job.stage]; 6320 + const nextBeatAt = (lastBeatAt === stageStartedAt[job.stage]) 6321 + ? stageStartedAt[job.stage] + STAGE_FIRST_HEARTBEAT_MS 6322 + : lastBeatAt + STAGE_HEARTBEAT_MS; 6323 + if (now >= nextBeatAt && !isNewStage) { 6324 + stageHeartbeatAt[job.stage] = now; 6325 + addTrackEntry(`${prettyStage(job.stage)}: still working — ${formatElapsed(stageElapsed)} elapsed`, 'active'); 6326 + } 6327 + // Mark previous stages done (log their total elapsed once). 6283 6328 const stageIndex = MINT_STEPS.findIndex(s => s.id === job.stage); 6284 6329 for (let i = 0; i < stageIndex; i++) { 6285 6330 const prevId = MINT_STEPS[i].id; 6286 6331 if (prevId !== 'wallet' && mintStepStates[prevId]?.status !== 'done') { 6287 - setMintStep(prevId, 'done', mintStepStates[prevId]?.detail || ''); 6332 + const prevStart = stageStartedAt[prevId]; 6333 + const prevElapsed = prevStart ? (now - prevStart) : 0; 6334 + const prevDetail = mintStepStates[prevId]?.detail || ''; 6335 + const detailWithTime = (prevElapsed && TRACK_LOG_ON_STAGE_ENTER.has(prevId)) 6336 + ? `${prevDetail ? prevDetail + ' · ' : ''}took ${formatElapsed(prevElapsed)}`.trim() 6337 + : prevDetail; 6338 + setMintStep(prevId, 'done', detailWithTime); 6339 + if (prevStart && TRACK_LOG_ON_STAGE_ENTER.has(prevId)) { 6340 + addTrackEntry(`${prettyStage(prevId)}: done in ${formatElapsed(prevElapsed)}`, 'done'); 6341 + } 6288 6342 } 6289 6343 } 6290 6344 }