Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat(at): add news backfill and tape rebake scripts

- backfill-news-to-atproto.mjs — syncs news-posts → computer.aesthetic.news (8/8 done)
- rebake-tapes.mjs — re-triggers oven MP4 conversion for stuck tapes

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

+279
+129
at/scripts/atproto/backfill-news-to-atproto.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Backfill News to ATProto 5 + * 6 + * Syncs news-posts from MongoDB to computer.aesthetic.news on PDS. 7 + * Maps news-posts.title → news.headline 8 + * 9 + * Usage: 10 + * node backfill-news-to-atproto.mjs [options] 11 + * 12 + * Options: 13 + * --dry-run Show what would be synced without making changes 14 + * --limit N Only process N news items 15 + */ 16 + 17 + import { connect } from "../../../system/backend/database.mjs"; 18 + import { createNewsOnAtproto } from "../../../system/backend/news-atproto.mjs"; 19 + import { config } from "dotenv"; 20 + 21 + config({ path: "../../../system/.env" }); 22 + 23 + const args = process.argv.slice(2); 24 + const dryRun = args.includes("--dry-run"); 25 + const limitIndex = args.indexOf("--limit"); 26 + const limit = limitIndex !== -1 ? parseInt(args[limitIndex + 1]) : null; 27 + 28 + async function backfillNews() { 29 + console.log("📰 Backfill News to ATProto\n"); 30 + console.log(`Mode: ${dryRun ? "🔍 DRY RUN" : "✍️ LIVE"}`); 31 + if (limit) console.log(`Limit: ${limit}`); 32 + console.log(); 33 + 34 + const database = await connect(); 35 + const newsPosts = database.db.collection("news-posts"); 36 + const handles = database.db.collection("@handles"); 37 + 38 + // Find news-posts without ATProto sync 39 + const query = { 40 + $or: [ 41 + { atproto: { $exists: false } }, 42 + { "atproto.rkey": { $exists: false } }, 43 + ], 44 + }; 45 + 46 + const cursor = newsPosts.find(query).sort({ when: -1 }); 47 + if (limit) cursor.limit(limit); 48 + 49 + const allNews = await cursor.toArray(); 50 + console.log(`Found ${allNews.length} unsynced news posts\n`); 51 + 52 + if (allNews.length === 0) { 53 + console.log("✨ All news already synced!"); 54 + await database.disconnect(); 55 + return; 56 + } 57 + 58 + let synced = 0, 59 + skipped = 0, 60 + failed = 0; 61 + 62 + for (let i = 0; i < allNews.length; i++) { 63 + const post = allNews[i]; 64 + const handle = await handles.findOne({ _id: post.user }); 65 + const h = handle?.handle || "unknown"; 66 + 67 + console.log( 68 + ` [${i + 1}/${allNews.length}] @${h} — "${post.title?.slice(0, 50)}"`, 69 + ); 70 + 71 + if (dryRun) { 72 + synced++; 73 + continue; 74 + } 75 + 76 + try { 77 + // Map news-posts fields to lexicon fields 78 + const newsData = { 79 + headline: post.title, 80 + body: post.text || undefined, 81 + link: post.url || undefined, 82 + when: post.when ? new Date(post.when) : new Date(), 83 + }; 84 + 85 + const result = await createNewsOnAtproto( 86 + database, 87 + post.user, 88 + newsData, 89 + post._id.toString(), 90 + ); 91 + 92 + if (result.error) { 93 + console.log(` ⏭️ ${result.error}`); 94 + skipped++; 95 + } else { 96 + // Store rkey back in MongoDB 97 + await newsPosts.updateOne( 98 + { _id: post._id }, 99 + { 100 + $set: { 101 + "atproto.rkey": result.rkey, 102 + "atproto.uri": result.uri, 103 + "atproto.did": result.did, 104 + "atproto.syncedAt": new Date().toISOString(), 105 + }, 106 + }, 107 + ); 108 + console.log(` ✅ → ${result.rkey}`); 109 + synced++; 110 + } 111 + } catch (error) { 112 + console.log(` ❌ ${error.message}`); 113 + failed++; 114 + } 115 + } 116 + 117 + console.log("\n" + "═".repeat(50)); 118 + console.log(`✅ Synced: ${synced}`); 119 + console.log(`⏭️ Skipped: ${skipped}`); 120 + console.log(`❌ Failed: ${failed}`); 121 + console.log(`📊 Total: ${allNews.length}\n`); 122 + 123 + await database.disconnect(); 124 + } 125 + 126 + backfillNews().catch((err) => { 127 + console.error("Fatal:", err); 128 + process.exit(1); 129 + });
+150
at/scripts/atproto/rebake-tapes.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Re-bake Tapes 5 + * 6 + * Re-triggers oven MP4 conversion for tapes that were never processed. 7 + * Sends each tape to the oven /bake endpoint with the correct ZIP URL. 8 + * 9 + * Usage: 10 + * node rebake-tapes.mjs [options] 11 + * 12 + * Options: 13 + * --dry-run Show what would be sent without triggering 14 + * --limit N Only process N tapes 15 + * --code CODE Re-bake a specific tape by code 16 + * --delay MS Delay between requests (default: 2000) 17 + */ 18 + 19 + import { connect } from "../../../system/backend/database.mjs"; 20 + import { config } from "dotenv"; 21 + 22 + config({ path: "../../../system/.env" }); 23 + 24 + const args = process.argv.slice(2); 25 + const dryRun = args.includes("--dry-run"); 26 + const limitIndex = args.indexOf("--limit"); 27 + const limit = limitIndex !== -1 ? parseInt(args[limitIndex + 1]) : null; 28 + const codeIndex = args.indexOf("--code"); 29 + const targetCode = codeIndex !== -1 ? args[codeIndex + 1] : null; 30 + const delayIndex = args.indexOf("--delay"); 31 + const delay = delayIndex !== -1 ? parseInt(args[delayIndex + 1]) : 2000; 32 + 33 + const OVEN_URL = process.env.OVEN_URL || "https://oven.aesthetic.computer"; 34 + const CALLBACK_URL = process.env.URL 35 + ? `${process.env.URL}/api/oven-complete` 36 + : "https://aesthetic.computer/api/oven-complete"; 37 + const CALLBACK_SECRET = process.env.OVEN_CALLBACK_SECRET; 38 + 39 + async function sleep(ms) { 40 + return new Promise((resolve) => setTimeout(resolve, ms)); 41 + } 42 + 43 + async function rebakeTapes() { 44 + console.log("🔥 Re-bake Unprocessed Tapes\n"); 45 + console.log(`Mode: ${dryRun ? "🔍 DRY RUN" : "🔥 LIVE"}`); 46 + console.log(`Oven: ${OVEN_URL}`); 47 + console.log(`Callback: ${CALLBACK_URL}`); 48 + console.log(`Secret: ${CALLBACK_SECRET ? "✅ set" : "❌ missing"}`); 49 + if (targetCode) console.log(`Code: ${targetCode}`); 50 + console.log(); 51 + 52 + if (!CALLBACK_SECRET && !dryRun) { 53 + console.error("❌ OVEN_CALLBACK_SECRET is required. Set it in system/.env"); 54 + process.exit(1); 55 + } 56 + 57 + const database = await connect(); 58 + const tapes = database.db.collection("tapes"); 59 + const handles = database.db.collection("@handles"); 60 + 61 + const query = { 62 + $or: [{ mp4Url: { $exists: false } }, { mp4Url: null }], 63 + }; 64 + 65 + if (targetCode) { 66 + query.code = targetCode; 67 + } 68 + 69 + const cursor = tapes.find(query).sort({ when: -1 }); 70 + if (limit) cursor.limit(limit); 71 + 72 + const allTapes = await cursor.toArray(); 73 + console.log(`Found ${allTapes.length} tapes needing MP4 conversion\n`); 74 + 75 + if (allTapes.length === 0) { 76 + console.log("✨ All tapes already have MP4s!"); 77 + await database.disconnect(); 78 + return; 79 + } 80 + 81 + let sent = 0, 82 + skipped = 0, 83 + failed = 0; 84 + 85 + for (let i = 0; i < allTapes.length; i++) { 86 + const tape = allTapes[i]; 87 + const handle = await handles.findOne({ _id: tape.user }); 88 + const h = handle?.handle || (tape.user ? "unknown" : "guest"); 89 + 90 + const bucket = tape.bucket || (tape.user ? "user-aesthetic-computer" : "art-aesthetic-computer"); 91 + const key = tape.user ? `${tape.user}/${tape.slug}.zip` : `${tape.slug}.zip`; 92 + const zipUrl = `https://${bucket}.sfo3.digitaloceanspaces.com/${key}`; 93 + 94 + console.log( 95 + ` [${i + 1}/${allTapes.length}] @${h} code:${tape.code} → ${zipUrl.split("/").pop()}`, 96 + ); 97 + 98 + if (dryRun) { 99 + sent++; 100 + continue; 101 + } 102 + 103 + const payload = { 104 + mongoId: tape._id.toString(), 105 + slug: tape.slug, 106 + code: tape.code, 107 + zipUrl, 108 + callbackUrl: CALLBACK_URL, 109 + callbackSecret: CALLBACK_SECRET, 110 + }; 111 + 112 + try { 113 + const res = await fetch(`${OVEN_URL}/bake`, { 114 + method: "POST", 115 + headers: { "Content-Type": "application/json" }, 116 + body: JSON.stringify(payload), 117 + }); 118 + 119 + if (res.ok) { 120 + const data = await res.json(); 121 + console.log(` ✅ Accepted: ${data.message || "ok"}`); 122 + sent++; 123 + } else { 124 + const text = await res.text(); 125 + console.log(` ❌ ${res.status}: ${text}`); 126 + failed++; 127 + } 128 + } catch (err) { 129 + console.log(` ❌ ${err.message}`); 130 + failed++; 131 + } 132 + 133 + if (i < allTapes.length - 1) { 134 + await sleep(delay); 135 + } 136 + } 137 + 138 + console.log("\n" + "═".repeat(50)); 139 + console.log(`🔥 Sent: ${sent}`); 140 + console.log(`⏭️ Skipped: ${skipped}`); 141 + console.log(`❌ Failed: ${failed}`); 142 + console.log(`📊 Total: ${allTapes.length}\n`); 143 + 144 + await database.disconnect(); 145 + } 146 + 147 + rebakeTapes().catch((err) => { 148 + console.error("Fatal:", err); 149 + process.exit(1); 150 + });