Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

keeps: fix handle resolution + livelier rekeep telemetry

store-kidlisp: ?recent=true was running $lookup on the full kidlisp
collection before $sort/$limit, silently emptying handleInfo on the
big scan and returning handle=null for every entry. Reorder so
$sort+$limit run first, push handle-filter $match after $lookup when
needed, and pass allowDiskUse: true. Mirrors the working pattern in
tv.mjs/fetchKidlisp.

keeps.html: push the per-user filter to the server (was hauling ~17K
rows over the wire to client-filter down to a few hundred), drop the
limit accordingly, and wire the existing oven /grab-status poll to
update the timeline detail directly during thumbnail/bundle stages
so users don't wait 2-4s for the MongoDB → keep-status round-trip.

keep-prepare-background: tighten the server-side oven poller from
2s to 1s so clients reading via keep-status (rather than directly
hitting oven) also see fresher stages.

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

+54 -30
+1 -1
system/netlify/functions/keep-prepare-background.mjs
··· 453 453 await updateJobStage(jobId, stage, `${detail}${frame}${pct}`); 454 454 } 455 455 } catch {} 456 - }, 2000); 456 + }, 1000); 457 457 } 458 458 function stopOvenProgressPoller() { 459 459 if (ovenProgressTimer) { clearInterval(ovenProgressTimer); ovenProgressTimer = null; }
+25 -24
system/netlify/functions/store-kidlisp.mjs
··· 669 669 const since = event.queryStringParameters?.since; // ISO timestamp — only return entries newer than this 670 670 671 671 console.log(`📊 Codes request: limit=${actualLimit}, sort=${sortBy}, handle=${filterHandle || 'all'}${since ? `, since=${since}` : ''}`); 672 - 673 - // Aggregate pipeline to join with handles collection (like moods.mjs) 674 - const pipeline = [ 672 + 673 + const handleLookupStages = [ 675 674 { 676 675 $lookup: { 677 676 from: "@handles", ··· 680 679 as: "handleInfo" 681 680 } 682 681 }, 683 - // Add computed handle field for filtering 684 682 { 685 683 $addFields: { 686 - handle: { 684 + handle: { 687 685 $cond: { 688 686 if: { $gt: [{ $size: "$handleInfo" }, 0] }, 689 687 then: { $concat: ["@", { $arrayElemAt: ["$handleInfo.handle", 0] }] }, ··· 691 689 } 692 690 } 693 691 } 694 - }, 692 + } 695 693 ]; 696 - 697 - // Filter by timestamp if `since` provided (for live updates) 694 + 695 + // Match/sort/limit before $lookup so the join runs on a small working 696 + // set — the previous order silently dropped handleInfo on large scans. 697 + const pipeline = []; 698 + 698 699 if (since) { 699 700 const sinceDate = new Date(since); 700 701 if (!isNaN(sinceDate.getTime())) { ··· 702 703 } 703 704 } 704 705 705 - // Add handle filter if specified 706 + pipeline.push({ 707 + $sort: sortBy === 'hits' ? { hits: -1, when: -1 } : { when: -1 } 708 + }); 709 + 706 710 if (filterHandle) { 711 + // Filter is on the computed handle, so the lookup has to precede $limit. 712 + pipeline.push(...handleLookupStages); 707 713 pipeline.push({ 708 714 $match: { handle: filterHandle.startsWith('@') ? filterHandle : `@${filterHandle}` } 709 715 }); 716 + pipeline.push({ $limit: actualLimit }); 717 + } else { 718 + pipeline.push({ $limit: actualLimit }); 719 + pipeline.push(...handleLookupStages); 710 720 } 711 - 712 - // Sort by hits or recent 713 - pipeline.push({ 714 - $sort: sortBy === 'hits' ? { hits: -1, when: -1 } : { when: -1 } 715 - }); 716 - 717 - pipeline.push({ 718 - $limit: actualLimit 719 - }); 720 - 721 + 721 722 pipeline.push({ 722 723 $project: { 723 724 _id: 0, ··· 726 727 when: 1, 727 728 hits: 1, 728 729 user: 1, 729 - kept: 1, // Include kept status 730 - tezos: 1, // Include legacy tezos field 730 + kept: 1, 731 + tezos: 1, 731 732 pendingRebake: 1, 732 - handle: 1 // Already computed 733 + handle: 1 733 734 } 734 735 }); 735 - 736 - const docs = await collection.aggregate(pipeline).toArray(); 736 + 737 + const docs = await collection.aggregate(pipeline, { allowDiskUse: true }).toArray(); 737 738 738 739 // Create preview versions of source code (truncate long sources) 739 740 const recentCodes = docs.map(doc => {
+28 -5
system/public/kidlisp.com/keeps.html
··· 3681 3681 const contractHint = encodeURIComponent(getKeepsContractAddress() || ''); 3682 3682 const contractVersion = encodeURIComponent(getKeepsContractVersion() || ''); 3683 3683 const contractProfile = encodeURIComponent(getKeepsContractProfile() || ''); 3684 - return `${AC_BASE}/api/store-kidlisp?recent=true&contract=${contractHint}&contractVersion=${contractVersion}&contractProfile=${contractProfile}${extra}&ts=${Date.now()}`; 3684 + // Push the per-user filter to the server. Without this we'd haul the 3685 + // whole kidlisp collection (~17K rows) over the wire just to throw out 3686 + // everything but the current user's pieces in scopeIndexEntries. 3687 + const handleParam = acHandle ? `&handle=${encodeURIComponent(acHandle)}` : ''; 3688 + return `${AC_BASE}/api/store-kidlisp?recent=true&contract=${contractHint}&contractVersion=${contractVersion}&contractProfile=${contractProfile}${handleParam}${extra}&ts=${Date.now()}`; 3685 3689 } 3686 3690 3687 3691 async function fetchAllCodes(sort = 'recent') { 3688 3692 try { 3689 - const url = buildStoreUrl(`&limit=100000&sort=${sort}`); 3693 + // 2000 is well above any plausible per-user piece count; the server 3694 + // already filtered to acHandle so this is just a safety ceiling. 3695 + const url = buildStoreUrl(`&limit=2000&sort=${sort}`); 3690 3696 const res = await fetch(url, { cache: 'no-store' }); 3691 3697 if (!res.ok) throw new Error(`HTTP ${res.status}`); 3692 3698 const data = await res.json(); ··· 6182 6188 return; 6183 6189 } 6184 6190 6185 - // Show live oven preview — poll oven /grab-status directly (not via MongoDB) 6191 + // Live oven telemetry — poll oven /grab-status directly so stage 6192 + // text + preview frame update without waiting for the MongoDB 6193 + // → keep-status round-trip (which adds ~2-4s of perceived lag). 6186 6194 if ((job.stage === 'thumbnail' || job.stage === 'bundle') && !mintOvenPreviewTimer) { 6187 6195 mintOvenPreviewTimer = setInterval(async () => { 6188 6196 try { ··· 6190 6198 if (!res.ok) return; 6191 6199 const status = await res.json(); 6192 6200 const progress = status.progress; 6193 - if (!progress?.previewFrame || !progress.piece?.includes(piece)) return; 6201 + if (!progress?.piece?.includes(piece)) return; 6202 + 6203 + // Push stage detail into the timeline immediately. Mirrors 6204 + // the formatting the background worker writes to MongoDB 6205 + // (keep-prepare-background.mjs::startOvenProgressPoller). 6206 + const detail = progress.stageDetail || progress.stage || ''; 6207 + const frame = progress.currentFrame != null && progress.totalFrames 6208 + ? ` (${progress.currentFrame}/${progress.totalFrames})` 6209 + : ''; 6210 + const pct = progress.percent != null ? ` ${progress.percent}%` : ''; 6211 + const msg = `${detail}${frame}${pct}`.trim(); 6212 + if (msg && (lastStage === 'thumbnail' || lastStage === 'bundle')) { 6213 + setMintStep(lastStage, 'active', msg); 6214 + } 6215 + 6216 + if (!progress.previewFrame) return; 6194 6217 const pf = document.getElementById('mint-preview-frame'); 6195 6218 if (!pf || pf.querySelector('.job-thumbnail')) return; 6196 6219 const overlay = pf.querySelector('.mint-progress-overlay'); ··· 6206 6229 } 6207 6230 img.src = 'data:image/jpeg;base64,' + progress.previewFrame; 6208 6231 } catch {} 6209 - }, 1500); 6232 + }, 1000); 6210 6233 } 6211 6234 // Stop oven preview polling when bake stage is done 6212 6235 if (job.stage !== 'thumbnail' && job.stage !== 'bundle' && mintOvenPreviewTimer) {