Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597#!/usr/bin/env node// Oven Server// Main Express server for the unified bake processing service
import 'dotenv/config';import express from 'express';import https from 'https';import http from 'http';import fs from 'fs';import { execSync } from 'child_process';import { gunzipSync, gzipSync } from 'node:zlib';import { WebSocketServer } from 'ws';import { healthHandler, bakeHandler, statusHandler, bakeCompleteHandler, bakeStatusHandler, getActiveBakes, getIncomingBakes, getRecentBakes, subscribeToUpdates, cleanupStaleBakes } from './baker.mjs';import { grabHandler, grabGetHandler, grabIPFSHandler, grabPiece, getCachedOrGenerate, getActiveGrabs, getRecentGrabs, getLatestKeepThumbnail, ensureLatestKeepThumbnail, getLatestIPFSUpload, getAllLatestIPFSUploads, setNotifyCallback, setLogCallback, cleanupStaleGrabs, clearAllActiveGrabs, getQueueStatus, getCurrentProgress, getAllProgress, getConcurrencyStatus, IPFS_GATEWAY, generateKidlispOGImage, getOGImageCacheStatus, getFrozenPieces, clearFrozenPiece, getLatestOGImageUrl, regenerateOGImagesBackground, generateKidlispBackdrop, getLatestBackdropUrl, listBackdropPool, pickBackdropUrl, warmBackdropPool, getBackdropPoolStatus, APP_SCREENSHOT_PRESETS, generateNotepatOGImage, getLatestNotepatOGUrl, prewarmGrabBrowser, generateNewsOGImage } from './grabber.mjs';import archiver from 'archiver';import sharp from 'sharp';import { createBundle, createJSPieceBundle, createJSLibraryBundle, createM4DBundle, generateDeviceHTML, prewarmCache, getCacheStatus, setSkipMinification } from './bundler.mjs';import { bundleMini, fetchPieceSource } from './kidlisp-mini/bundle.mjs';import { streamOSImage, getOSBuildStatus, invalidateManifest, purgeOSBuildCache, clearOSBuildLocalCache } from './os-builder.mjs';import { startOSBaseBuild, getOSBaseBuild, getOSBaseBuildsSummary, cancelOSBaseBuild } from './os-base-build.mjs';import { startNativeBuild, getNativeBuild, getNativeBuildsSummary, cancelNativeBuild, onNativeBuildProgress } from './native-builder.mjs';import { startPoller as startNativeGitPoller, getPollerStatus as getNativePollerStatus } from './native-git-poller.mjs';import { startPapersBuild, getPapersBuild, getPapersBuildsSummary, cancelPapersBuild } from './papers-builder.mjs';import { startPoller as startPapersGitPoller, getPollerStatus as getPapersPollerStatus } from './papers-git-poller.mjs';import { startPaperCrunch, getPaperCrunch, getPaperCrunchesSummary, getPaperCrunchPdf, cancelPaperCrunch, CRUNCH_LIMITS } from './paper-crunch.mjs';import { startRecapBuild, getRecapBuild, getRecapBuildsSummary, cancelRecapBuild, getRecapMp4Path } from './recap-builder.mjs';import { startOskiewarReel, getOskiewarReel, getOskiewarReels, getOskiewarReelFile, cancelOskiewarReel } from './oskiewar-reel-builder.mjs';import { startPoller as startRecapGitPoller, getPollerStatus as getRecapPollerStatus } from './recap-git-poller.mjs';import { join, dirname, basename } from 'path';import { fileURLToPath } from 'url';import { MongoClient } from 'mongodb';
const app = express();const PORT = process.env.PORT || 3002;const dev = process.env.NODE_ENV === 'development';
// Track server start time for uptime displayconst SERVER_START_TIME = Date.now();
// Get git version at startup (from env var set during deploy, or try git)let GIT_VERSION = process.env.OVEN_VERSION || 'unknown';if (GIT_VERSION === 'unknown') { try { GIT_VERSION = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); } catch (e) { // Not a git repo, that's fine }}console.log(`📦 Oven version: ${GIT_VERSION}`);console.log(`🕐 Server started at: ${new Date(SERVER_START_TIME).toISOString()}`);
// Activity log buffer for streaming to clientsconst activityLogBuffer = [];const MAX_ACTIVITY_LOG = 100;let wss = null; // Will be set after server starts
const NATIVE_BUILD_COLLECTION = process.env.NATIVE_BUILD_COLLECTION || 'oven-native-builds';const NATIVE_BUILD_STATUS_CACHE_MS = 30000;let nativeBuildStatusMongoClient = null;let nativeBuildStatusMongoDb = null;let nativeBuildStatusCacheAt = 0;let nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] };
function addServerLog(type, icon, msg) { const entry = { time: new Date().toISOString(), type, icon, msg }; activityLogBuffer.unshift(entry); if (activityLogBuffer.length > MAX_ACTIVITY_LOG) { activityLogBuffer.pop(); } // Broadcast to connected clients if wss exists and has clients if (wss && wss.clients) { const logMsg = JSON.stringify({ logEntry: entry }); wss.clients.forEach(client => { if (client.readyState === 1) client.send(logMsg); }); }}
function toIsoOrNull(value) { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d.toISOString();}
async function getNativeBuildStatusMongoDb() { if (nativeBuildStatusMongoDb) return nativeBuildStatusMongoDb; const uri = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (!uri || !dbName) return null; try { nativeBuildStatusMongoClient = await MongoClient.connect(uri); nativeBuildStatusMongoDb = nativeBuildStatusMongoClient.db(dbName); return nativeBuildStatusMongoDb; } catch (err) { console.error('[os-releases] MongoDB connect failed:', err.message); return null; }}
async function getNativeBuildStatusData() { const now = Date.now(); if (now - nativeBuildStatusCacheAt < NATIVE_BUILD_STATUS_CACHE_MS) { return nativeBuildStatusCache; }
const db = await getNativeBuildStatusMongoDb(); if (!db) { nativeBuildStatusCacheAt = now; nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] }; return nativeBuildStatusCache; }
try { const docs = await db .collection(NATIVE_BUILD_COLLECTION) .find({}) .sort({ when: -1 }) .limit(250) .toArray();
const byName = new Map(); const failedAttempts = []; for (const doc of docs) { const name = String(doc.buildName || doc.name || '').trim(); if (!name) continue; const status = String(doc.status || 'unknown').toLowerCase(); const ref = String(doc.ref || doc.gitHash || '').trim(); const item = { name, status, ref: ref || null, error: doc.error || null, commitMsg: doc.commitMsg || null, buildTs: toIsoOrNull(doc.finishedAt) || toIsoOrNull(doc.startedAt) || toIsoOrNull(doc.createdAt) || toIsoOrNull(doc.when) || null, }; if (!byName.has(name)) byName.set(name, item); if ((status === 'failed' || status === 'cancelled') && failedAttempts.length < 8) { failedAttempts.push(item); } }
nativeBuildStatusCacheAt = now; nativeBuildStatusCache = { byName, failedAttempts }; return nativeBuildStatusCache; } catch (err) { console.error('[os-releases] Failed to load native build statuses:', err.message); nativeBuildStatusCacheAt = now; nativeBuildStatusCache = { byName: new Map(), failedAttempts: [] }; return nativeBuildStatusCache; }}
// Export for use in other modulesexport { addServerLog };
// Log server startupaddServerLog('info', '🔥', 'Oven server starting...');
// OS base-build admin key auth// Accepts either OS_BUILD_ADMIN_KEY directly in env, or OS_BUILD_ADMIN_KEY_FILE.let cachedOSBuildAdminKey = null;let cachedOSBuildAdminMtimeMs = null;
function getConfiguredOSBuildAdminKey() { const envKey = (process.env.OS_BUILD_ADMIN_KEY || '').trim(); if (envKey) return envKey;
const keyFile = (process.env.OS_BUILD_ADMIN_KEY_FILE || '').trim(); if (!keyFile) return '';
try { const stat = fs.statSync(keyFile); if (cachedOSBuildAdminKey && cachedOSBuildAdminMtimeMs === stat.mtimeMs) { return cachedOSBuildAdminKey; } const nextKey = fs.readFileSync(keyFile, 'utf8').trim(); cachedOSBuildAdminKey = nextKey; cachedOSBuildAdminMtimeMs = stat.mtimeMs; return nextKey; } catch { return ''; }}
function getOSBuildRequestKey(req) { const headerKey = (req.get('x-oven-os-key') || '').trim(); if (headerKey) return headerKey; const auth = (req.get('authorization') || '').trim(); if (auth.startsWith('Bearer ')) return auth.slice(7).trim(); return '';}
function requireOSBuildAdmin(req, res, next) { const expectedKey = getConfiguredOSBuildAdminKey(); if (!expectedKey) { return res.status(503).json({ error: 'OS build admin key not configured. Set OS_BUILD_ADMIN_KEY or OS_BUILD_ADMIN_KEY_FILE.', }); }
const providedKey = getOSBuildRequestKey(req); if (!providedKey || providedKey !== expectedKey) { return res.status(401).json({ error: 'Unauthorized' }); }
return next();}
// ===== SHARED PROGRESS UI COMPONENTS =====// Shared CSS for progress indicators across all oven dashboardsconst PROGRESS_UI_CSS = ` /* Oven Progress UI - shared across all dashboards */ .oven-loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.85); color: #888; text-align: center; padding: 10px; z-index: 10; } .oven-loading .preview-img { width: 80px; height: 80px; image-rendering: pixelated; border: 1px solid #333; margin-bottom: 8px; display: none; background: #111; } .oven-loading .loading-text { font-size: 12px; color: #fff; } .oven-loading .progress-text { font-size: 11px; margin-top: 8px; color: #88ff88; font-family: monospace; max-width: 150px; word-break: break-word; } .oven-loading .progress-bar { width: 80%; max-width: 150px; height: 4px; background: #333; border-radius: 2px; margin: 8px auto 0; overflow: hidden; } .oven-loading .progress-bar-fill { height: 100%; background: #88ff88; width: 0%; transition: width 0.3s ease; } .oven-loading.error { color: #f44; } .oven-loading.success { color: #4f4; }`;
// Shared JavaScript for progress polling and UI updatesconst PROGRESS_UI_JS = ` // Shared progress state let progressPollInterval = null; // Update any loading indicator with progress data function updateOvenLoadingUI(container, data, queueInfo) { if (!container) return; const loadingText = container.querySelector('.loading-text'); const progressText = container.querySelector('.progress-text'); const progressBar = container.querySelector('.progress-bar-fill'); const previewImg = container.querySelector('.preview-img'); // Check if item is in queue and get position let queuePosition = null; if (queueInfo && queueInfo.length > 0 && data.piece) { const queueItem = queueInfo.find(q => q.piece === data.piece); if (queueItem) { queuePosition = queueItem.position; } } // Map stage to friendly text const stageText = { 'loading': '🚀 Loading piece...', 'waiting-content': '⏳ Waiting for render...', 'settling': '⏸️ Settling...', 'capturing': '📸 Capturing...', 'encoding': '🔄 Processing...', 'uploading': '☁️ Uploading...', 'queued': queuePosition ? '⏳ In queue (#' + queuePosition + ')...' : '⏳ In queue...', }; if (loadingText && data.stage) { loadingText.textContent = stageText[data.stage] || data.stage; } if (progressText && data.stageDetail) { progressText.textContent = data.stageDetail; } if (progressBar && data.percent != null) { progressBar.style.width = data.percent + '%'; } // Show streaming preview if (previewImg && data.previewFrame) { previewImg.src = 'data:image/jpeg;base64,' + data.previewFrame; previewImg.style.display = 'block'; } } // Create loading HTML structure function createOvenLoadingHTML(initialText = '🔥 Loading...') { return '<img class="preview-img" alt="preview">' + '<span class="loading-text">' + initialText + '</span>' + '<div class="progress-text"></div>' + '<div class="progress-bar"><div class="progress-bar-fill"></div></div>'; } // Start polling /grab-status for progress updates function startProgressPolling(callback, intervalMs = 150) { stopProgressPolling(); progressPollInterval = setInterval(async () => { try { const res = await fetch('/grab-status'); const data = await res.json(); if (callback && data.progress) { callback(data); } } catch (err) { // Ignore polling errors } }, intervalMs); } function stopProgressPolling() { if (progressPollInterval) { clearInterval(progressPollInterval); progressPollInterval = null; } }`;
// Parse JSON bodiesapp.use(express.json());
// CORS headers for cross-origin image loading (needed for canvas pixel validation)app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader( 'Access-Control-Expose-Headers', 'Content-Length, Content-Disposition, X-AC-OS-Requested-Layout, X-AC-OS-Layout, X-AC-OS-Fallback, X-AC-OS-Fallback-Reason, X-Build, X-Patch', ); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next();});
// Serve font glyph JSONs locally for Puppeteer captures.// Font_1 glyph XHR requests from the disk.mjs worker are redirected here// by the request interceptor to avoid Puppeteer's broken concurrent XHR handling.const __serverDirname = dirname(fileURLToPath(import.meta.url));app.get('/local-glyph/*', (req, res) => { const glyphPath = req.params[0]; // Express auto-decodes URI params // Sanitize: only allow paths within ac-source/disks/drawings if (glyphPath.includes('..') || glyphPath.includes('\0')) { return res.status(400).send('Invalid path'); } const filePath = join(__serverDirname, 'ac-source', 'disks', 'drawings', glyphPath); res.sendFile(filePath, (err) => { if (err) res.status(404).json({ error: 'glyph not found' }); });});
// Oven TV dashboard — live-updating visual bake monitorapp.get('/', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(OVEN_TV_HTML);});
const OVEN_TV_HTML = `<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>oven</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png"> <style> :root { --bg: #f7f7f7; --bg-deep: #ececec; --bg-card: #fff; --bg-hover: #f0f0f0; --text: #111; --text-secondary: #555; --text-muted: #888; --text-dim: #aaa; --border: #ddd; --border-subtle: #e8e8e8; --accent: rgb(205, 92, 155); --accent-hover: rgb(220, 110, 170); --success: #2a9a2a; --error: #c44; --preview-bg: #e0e0e0; --overlay-bg: rgba(255,255,255,0.92); --scrollbar: transparent; } @media (prefers-color-scheme: dark) { :root { --bg: #1e1e1e; --bg-deep: #161616; --bg-card: #252526; --bg-hover: #2a2a2a; --text: #d4d4d4; --text-secondary: #888; --text-muted: #666; --text-dim: #444; --border: #3e3e42; --border-subtle: #2e2e32; --accent: rgb(205, 92, 155); --accent-hover: rgb(225, 115, 175); --success: #4caf50; --error: #f44; --preview-bg: #111; --overlay-bg: rgba(0,0,0,0.88); } }
* { box-sizing: border-box; margin: 0; padding: 0; } ::-webkit-scrollbar { display: none; }
body { font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 15px; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.status-bar { background: var(--bg-deep); border-bottom: 2px solid var(--accent); padding: 5px 12px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; gap: 8px; } .status-bar .title { color: var(--accent); font-weight: bold; font-size: 1.1em; } .status-bar .stats { display: flex; gap: 12px; color: var(--text-muted); font-size: 0.95em; } .status-bar .stats span { white-space: nowrap; } .status-bar .stats .active { color: var(--success); } .status-bar .stats .queued { color: var(--accent); } .sb-btn { background: var(--bg-card); color: var(--text-secondary); border: 1px solid var(--border); padding: 3px 8px; cursor: pointer; font-family: inherit; font-size: 0.85em; border-radius: 3px; text-decoration: none; display: inline-block; } .sb-btn:hover { color: var(--text); border-color: var(--accent); }
.hero { flex: 1; display: flex; align-items: center; justify-content: center; gap: 10px; padding: 10px; min-height: 0; overflow: hidden; } .hero.idle { color: var(--text-dim); font-size: 1.2em; } .hero-card { background: var(--bg-card); border: 2px solid var(--border); border-radius: 6px; display: flex; flex-direction: row; align-items: stretch; height: 90px; min-width: 200px; max-width: 320px; flex-shrink: 0; overflow: hidden; } .hero-card.capturing { border-color: var(--accent); } .hero-card .preview { width: 86px; min-width: 86px; background: var(--preview-bg); overflow: hidden; display: flex; align-items: center; justify-content: center; } .hero-card .preview img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; } .hero-card .preview .placeholder { color: var(--text-dim); font-size: 1.2em; } .hero-card .info { flex: 1; padding: 4px 6px; display: flex; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; overflow: hidden; } .hero-card .piece-name { color: var(--accent); font-weight: bold; font-size: 0.95em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .hero-card .meta { color: var(--text-dim); font-size: 0.75em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; } .hero-card .meta .author { color: var(--text-secondary); } .hero-card .stage { color: var(--text-muted); font-size: 0.8em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; } .hero-card .progress-bar { width: 100%; height: 3px; background: var(--border); flex-shrink: 0; margin-top: auto; } .hero-card .progress-bar .fill { height: 100%; background: var(--accent); transition: width 0.3s ease; }
.strip { background: var(--bg-deep); border-top: 1px solid var(--border); padding: 5px 12px; flex-shrink: 0; overflow: hidden; } .strip-label { color: var(--text-muted); font-size: 0.75em; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; } .strip-items { display: flex; gap: 6px; padding-bottom: 2px; white-space: nowrap; } .strip-items.train { animation: train-scroll 30s linear infinite; width: max-content; } @keyframes train-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } .strip-item { background: var(--bg-card); border: 1px solid var(--border); border-radius: 3px; padding: 2px 8px; white-space: nowrap; font-size: 0.9em; flex-shrink: 0; } .strip-item.queue { color: var(--accent); } .strip-empty { color: var(--text-dim); font-size: 0.9em; padding: 2px 0; }
.history { background: var(--bg-deep); border-top: 1px solid var(--border); flex: 1; min-height: 0; overflow-y: auto; padding: 0; } .history .strip-label { padding: 5px 12px 3px; } .history-row { display: flex; align-items: center; gap: 10px; padding: 5px 12px; border-bottom: 1px solid var(--border-subtle); } .history-row:hover { background: var(--bg-hover); } .history-row .h-thumb { width: 44px; height: 44px; min-width: 44px; border-radius: 3px; background: var(--bg-card); flex-shrink: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; } .history-row .h-thumb img { width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; } .history-row .h-thumb .h-none { color: var(--text-dim); font-size: 1em; } .history-row .h-main { flex: 1; min-width: 0; } .history-row .h-piece { font-weight: bold; font-size: 0.9em; } .history-row .h-piece a { color: inherit; text-decoration: none; } .history-row .h-piece a:hover { text-decoration: underline; } .history-row .h-meta { color: var(--text-muted); font-size: 0.78em; margin-top: 2px; display: flex; gap: 10px; flex-wrap: wrap; } .history-row .h-meta span { white-space: nowrap; } .history-row .h-error { color: var(--error); font-size: 0.78em; margin-top: 2px; opacity: 0.8; } .history-row .h-links { margin-top: 2px; display: flex; gap: 8px; } .history-row .h-links a { color: var(--accent); font-size: 0.78em; text-decoration: none; } .history-row .h-links a:hover { text-decoration: underline; } .history-row .h-right { flex-shrink: 0; text-align: right; font-size: 0.8em; } .history-row .h-status-done { color: var(--success); } .history-row .h-status-failed { color: var(--error); } .history-row .h-status-other { color: var(--text-muted); } .history-row .h-ago { color: var(--text-dim); font-size: 0.85em; }
@keyframes card-enter { from { opacity: 0; transform: translateX(-120px) scale(0.9); } to { opacity: 1; transform: translateX(0) scale(1); } } @keyframes card-exit { from { opacity: 1; transform: translateX(0) scale(1); } to { opacity: 0; transform: translateX(120px) scale(0.9); } } .hero-card { animation: card-enter 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; } .hero-card.exiting { animation: card-exit 0.5s cubic-bezier(0.55, 0, 1, 0.45) forwards; pointer-events: none; }
.capture-bar { display: none; }
.log-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--overlay-bg); z-index: 200; padding: 32px 16px 16px; overflow-y: auto; } .log-overlay.open { display: block; } .log-overlay .close { position: fixed; top: 6px; right: 12px; background: none; border: none; color: var(--accent); font-size: 1.3em; cursor: pointer; font-family: inherit; } .log-entry { padding: 1px 0; font-size: 0.82em; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .log-entry .time { color: var(--text-muted); } .log-entry.error { color: var(--error); } .log-entry.success { color: var(--success); } </style></head><body>
<div class="status-bar"> <span class="title">oven</span> <div class="stats"> <span class="active" id="stat-active">0/6 active</span> <span class="queued" id="stat-queued">0 queued</span> <span id="stat-uptime">--</span> <span id="stat-version">--</span> </div> <div style="display:flex;gap:4px"> <a href="/tools" class="sb-btn">Tools</a> <button class="sb-btn" id="log-btn" onclick="document.getElementById('log-overlay').classList.toggle('open')">Log</button> </div> </div>
<div class="hero idle" id="hero">Waiting for grabs...</div>
<div class="strip" id="queue-strip"> <div class="strip-label">Up Next</div> <div class="strip-items" id="queue-items"> <span class="strip-empty">No items queued</span> </div> </div>
<div class="history" id="history"> <div class="strip-label">Recent</div> <div id="history-items"> <span class="strip-empty">No recent grabs</span> </div> </div>
<div class="log-overlay" id="log-overlay"> <button class="close" onclick="this.parentElement.classList.remove('open')">x</button> <div id="log-entries"></div> </div>
<script> let serverVersion = null; let ws = null; let reconnectTimer = null;
function connect() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(proto + '//' + location.host + '/ws');
ws.onopen = () => { document.getElementById('stat-version').textContent = 'connected'; document.getElementById('stat-version').style.color = 'var(--success)'; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } };
ws.onclose = () => { document.getElementById('stat-version').textContent = 'disconnected'; document.getElementById('stat-version').style.color = 'var(--error)'; reconnectTimer = setTimeout(connect, 2000); };
ws.onmessage = (event) => { const data = JSON.parse(event.data);
if (data.logEntry) { addLog(data.logEntry); return; } if (serverVersion && data.version && data.version !== serverVersion) { location.reload(); return; } serverVersion = data.version;
if (data.recentLogs) { data.recentLogs.forEach(addLog); }
updateStatusBar(data); renderHero(data.grabProgress || {}); renderQueue(data.grabs?.queue || []); renderHistory(data.grabs?.recent || []); }; }
function updateStatusBar(data) { const c = data.concurrency || {}; document.getElementById('stat-active').textContent = (c.active || 0) + '/' + (c.max || 6) + ' active'; document.getElementById('stat-queued').textContent = (c.queueDepth || 0) + ' queued';
if (data.uptime) { const s = Math.floor(data.uptime / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); const d = Math.floor(h / 24); let upStr; if (d > 0) upStr = d + 'd ' + (h % 24) + 'h'; else if (h > 0) upStr = h + 'h ' + (m % 60) + 'm'; else upStr = m + 'm ' + (s % 60) + 's'; document.getElementById('stat-uptime').textContent = 'up ' + upStr; } if (data.version) { document.getElementById('stat-version').textContent = data.version; document.getElementById('stat-version').style.color = 'var(--text-muted)'; } }
let heroCards = {}; // grabId → DOM element let exitingCards = new Set(); // grabIds currently animating out
function ago(ms) { const s = Math.floor(ms / 1000); if (s < 60) return s + 's ago'; const m = Math.floor(s / 60); return m + 'm ' + (s % 60) + 's ago'; }
function shortDate(iso) { if (!iso) return ''; const d = new Date(iso); if (isNaN(d)) return ''; const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return months[d.getMonth()] + ' ' + d.getDate(); }
function renderHero(grabProgress) { const hero = document.getElementById('hero'); const entries = Object.entries(grabProgress).filter(([, p]) => p.stage);
// Animate out cards no longer in progress const activeIds = new Set(entries.map(([id]) => id)); for (const id of Object.keys(heroCards)) { if (!activeIds.has(id) && !exitingCards.has(id)) { exitingCards.add(id); const card = heroCards[id]; card.classList.add('exiting'); card.addEventListener('animationend', () => { card.remove(); delete heroCards[id]; exitingCards.delete(id); // Check if hero should go idle after last card exits if (Object.keys(heroCards).length === 0 && exitingCards.size === 0) { hero.className = 'hero idle'; hero.textContent = 'Waiting for grabs\\u2026'; } }, { once: true }); } }
if (entries.length === 0 && Object.keys(heroCards).length === 0 && exitingCards.size === 0) { if (!hero.classList.contains('idle')) { hero.className = 'hero idle'; hero.textContent = 'Waiting for grabs\\u2026'; } return; }
// Clear idle text when transitioning to active if (hero.classList.contains('idle')) { hero.innerHTML = ''; } hero.className = 'hero';
// Update or create cards entries.forEach(([grabId, p]) => { const previewSrc = p.previewFrame ? 'data:image/jpeg;base64,' + p.previewFrame : ''; const stageLabel = p.stage ? (p.stage.charAt(0).toUpperCase() + p.stage.slice(1)) : ''; const detail = p.stageDetail || ''; const pct = p.percent || 0; const now = Date.now(); const reqAgo = p.requestedAt ? ago(now - p.requestedAt) : ''; const authorStr = p.author || ''; const createdStr = shortDate(p.pieceCreatedAt); const sourceStr = p.source || ''; const originStr = p.requestOrigin ? p.requestOrigin.replace(/^https?:\\/\\//, '').split('/')[0] : '';
let metaParts = []; if (authorStr) metaParts.push('<span class="author">' + esc(authorStr) + '</span>'); if (sourceStr) metaParts.push(sourceStr); if (originStr) metaParts.push(originStr); if (createdStr) metaParts.push('created ' + createdStr); if (reqAgo) metaParts.push(reqAgo); const metaHtml = metaParts.join(' · ');
let card = heroCards[grabId]; if (!card) { card = document.createElement('div'); card.className = 'hero-card' + (p.stage === 'capturing' ? ' capturing' : ''); card.innerHTML = '<div class="preview">' + (previewSrc ? '<img src="' + previewSrc + '" alt="">' : '<span class="placeholder">···</span>') + '</div>' + '<div class="info">' + '<div class="piece-name">' + esc(p.piece || grabId) + '</div>' + '<div class="meta">' + metaHtml + '</div>' + '<div class="stage">' + esc(stageLabel + (detail ? ' — ' + detail : '')) + '</div>' + '<div class="progress-bar"><div class="fill" style="width:' + pct + '%"></div></div>' + '</div>'; hero.appendChild(card); heroCards[grabId] = card; } else { card.className = 'hero-card' + (p.stage === 'capturing' ? ' capturing' : ''); const img = card.querySelector('.preview img'); if (previewSrc && img) { if (img.src !== previewSrc) img.src = previewSrc; } else if (previewSrc && !img) { card.querySelector('.preview').innerHTML = '<img src="' + previewSrc + '" alt="">'; } card.querySelector('.piece-name').textContent = p.piece || grabId; card.querySelector('.meta').innerHTML = metaHtml; card.querySelector('.stage').textContent = stageLabel + (detail ? ' — ' + detail : ''); card.querySelector('.fill').style.width = pct + '%'; } }); }
let lastQueueKey = ''; function renderQueue(queue) { const el = document.getElementById('queue-items'); if (!queue || queue.length === 0) { if (lastQueueKey !== 'empty') { el.innerHTML = '<span class="strip-empty">No items queued</span>'; el.classList.remove('train'); lastQueueKey = 'empty'; } return; } // Only re-render if queue contents changed (avoids restarting CSS animation) const queueKey = queue.map(q => q.piece).join(','); if (queueKey === lastQueueKey) return; lastQueueKey = queueKey;
const items = queue.map((item, i) => '<div class="strip-item queue">' + '#' + (i + 1) + ' ' + esc(item.piece || '?') + ' <span style="color:var(--text-muted)">(' + esc(item.format || '?') + ')</span>' + (item.estimatedWait ? ' <span style="color:var(--text-dim)">~' + Math.ceil(item.estimatedWait / 1000) + 's</span>' : '') + '</div>' ).join(''); // Duplicate items for seamless looping train effect if (queue.length > 4) { el.innerHTML = items + items; el.classList.add('train'); el.style.animationDuration = Math.max(10, queue.length * 2) + 's'; } else { el.innerHTML = items; el.classList.remove('train'); } }
let lastHistoryKey = ''; function renderHistory(recent) { const el = document.getElementById('history-items'); if (!recent || recent.length === 0) { if (lastHistoryKey !== 'empty') { el.innerHTML = '<span class="strip-empty" style="padding:5px 12px">No recent grabs</span>'; lastHistoryKey = 'empty'; } return; } const historyKey = recent.slice(0, 30).map(g => (g.id || g.piece) + ':' + g.status).join(','); if (historyKey === lastHistoryKey) return; lastHistoryKey = historyKey; el.innerHTML = recent.slice(0, 30).map(grab => { const thumbImg = grab.cdnUrl ? '<img src="' + esc(grab.cdnUrl) + '" alt="">' : '<span class="h-none">--</span>';
const pieceClass = grab.status === 'failed' ? 'h-status-failed' : '';
const dur = grab.duration ? Math.round(grab.duration / 1000) + 's' : ''; const dim = grab.dimensions ? grab.dimensions.width + 'x' + grab.dimensions.height : ''; const fmt = (grab.format || '').toUpperCase(); const size = grab.size ? (grab.size > 1024*1024 ? (grab.size/1024/1024).toFixed(1)+'MB' : Math.round(grab.size/1024)+'KB') : '';
const metaParts = [fmt, dim, dur, size].filter(Boolean); const metaHTML = metaParts.map(m => '<span>' + esc(m) + '</span>').join('');
const errorHTML = grab.error ? '<div class="h-error">' + esc(grab.error) + '</div>' : '';
let linksHTML = ''; if (grab.cdnUrl) { linksHTML = '<div class="h-links">' + '<a href="' + esc(grab.cdnUrl) + '" target="_blank">Open</a>' + '<a href="' + esc(grab.cdnUrl) + '" download>Download</a>' + '</div>'; }
const statusClass = grab.status === 'complete' ? 'h-status-done' : grab.status === 'failed' ? 'h-status-failed' : 'h-status-other'; const statusLabel = grab.status === 'complete' ? 'done' : grab.status === 'failed' ? 'failed' : esc(grab.status || '?');
const ago = grab.completedAt ? timeAgo(grab.completedAt) : '';
const pieceName = esc(grab.piece || grab.id || '?'); const pieceLink = grab.cdnUrl ? '<a href="' + esc(grab.cdnUrl) + '" target="_blank">' + pieceName + '</a>' : pieceName;
return '<div class="history-row">' + '<div class="h-thumb">' + thumbImg + '</div>' + '<div class="h-main">' + '<div class="h-piece ' + pieceClass + '">' + pieceLink + '</div>' + '<div class="h-meta">' + metaHTML + '</div>' + errorHTML + linksHTML + '</div>' + '<div class="h-right">' + '<div class="' + statusClass + '">' + statusLabel + '</div>' + '<div class="h-ago">' + esc(ago) + '</div>' + '</div>' + '</div>'; }).join(''); }
function timeAgo(ts) { const s = Math.floor((Date.now() - ts) / 1000); if (s < 60) return s + 's ago'; const m = Math.floor(s / 60); if (m < 60) return m + 'm ago'; const h = Math.floor(m / 60); if (h < 24) return h + 'h ago'; return Math.floor(h / 24) + 'd ago'; }
function addLog(entry) { if (!entry) return; const el = document.getElementById('log-entries'); const div = document.createElement('div'); div.className = 'log-entry' + (entry.type === 'error' ? ' error' : entry.type === 'success' ? ' success' : ''); const time = entry.time ? new Date(entry.time).toLocaleTimeString() : ''; div.innerHTML = '<span class="time">' + time + '</span> ' + esc((entry.icon || '') + ' ' + (entry.msg || '')); el.prepend(div); while (el.children.length > 200) el.removeChild(el.lastChild); }
function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
connect(); </script></body></html>`;
// Tools submenu — links to OG images, app screenshots, bundles, status pagesapp.get('/tools', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(`<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>oven / tools</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root { --bg: #f7f7f7; --text: #111; --text-muted: #888; --text-dim: #aaa; --accent: rgb(205, 92, 155); --border: #ddd; } @media (prefers-color-scheme: dark) { :root { --bg: #1e1e1e; --text: #d4d4d4; --text-muted: #666; --text-dim: #444; --accent: rgb(205, 92, 155); --border: #3e3e42; } } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 12px; background: var(--bg); color: var(--text); padding: 20px; } a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } h1 { color: var(--accent); margin-bottom: 20px; font-size: 1.1em; } h1 a { color: var(--text-muted); } h2 { color: var(--text-muted); margin: 16px 0 6px; font-size: 0.9em; text-transform: uppercase; letter-spacing: 1px; } .links { display: flex; flex-direction: column; gap: 4px; margin-left: 10px; } .links a { padding: 2px 0; } .desc { color: var(--text-dim); font-size: 0.85em; margin-left: 8px; } .panel { margin: 8px 0 0 10px; padding: 8px; border: 1px solid var(--border); max-width: 920px; border-radius: 4px; background: rgba(0,0,0,0.02); } .row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 6px; } input, button { font: inherit; font-size: 0.9em; border: 1px solid var(--border); background: transparent; color: var(--text); padding: 4px 6px; border-radius: 3px; } input { min-width: 160px; } button { cursor: pointer; } button:hover { border-color: var(--accent); color: var(--accent); } pre { border: 1px solid var(--border); padding: 8px; overflow: auto; max-height: 240px; white-space: pre-wrap; word-break: break-word; } hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; } </style></head><body> <h1><a href="/">oven</a> / tools</h1>
<h2>OG Images</h2> <div class="links"> <div><a href="/kidlisp-og.png">/kidlisp-og.png</a><span class="desc">KidLisp OG image</span></div> <div><a href="/kidlisp-og">/kidlisp-og</a><span class="desc">KidLisp OG HTML page</span></div> <div><a href="/kidlisp-og/status">/kidlisp-og/status</a><span class="desc">OG cache status</span></div> <div><a href="/kidlisp-og/preview">/kidlisp-og/preview</a><span class="desc">OG preview</span></div> <div><a href="/og-preview">/og-preview</a><span class="desc">OG preview (alt)</span></div> <div><a href="/notepat-og.png">/notepat-og.png</a><span class="desc">Notepat OG image</span></div> <div><a href="/news-og/ncd2.png">/news-og/:code.png</a><span class="desc">News article OG image</span></div> <div><a href="/kidlisp-backdrop.webp">/kidlisp-backdrop.webp</a><span class="desc">KidLisp backdrop animation</span></div> <div><a href="/kidlisp-backdrop">/kidlisp-backdrop</a><span class="desc">KidLisp backdrop page</span></div> <div><a href="/kidlisp-backdrop/pool">/kidlisp-backdrop/pool</a><span class="desc">Backdrop pool status (POST /pool/warm to render)</span></div> </div>
<h2>App Screenshots</h2> <div class="links"> <div><a href="/app-screenshots">/app-screenshots</a><span class="desc">Screenshot dashboard</span></div> </div>
<h2>Bundles</h2> <div class="links"> <div><a href="/bundle-status">/bundle-status</a><span class="desc">Bundle cache status</span></div> <div><a href="/bundle-html?piece=prompt">/bundle-html?piece=...</a><span class="desc">Generate HTML bundle (SSE)</span></div> </div>
<h2>Grabs</h2> <div class="links"> <div><a href="/grab-status">/grab-status</a><span class="desc">Active grabs + queue (JSON)</span></div> <div><a href="/api/frozen">/api/frozen</a><span class="desc">Frozen pieces list</span></div> <div><a href="/keeps/all">/keeps/all</a><span class="desc">All latest IPFS uploads</span></div> <div><a href="/keeps/latest">/keeps/latest</a><span class="desc">Latest keep thumbnail</span></div> </div>
<h2>Status</h2> <div class="links"> <div><a href="/health">/health</a><span class="desc">Health check</span></div> <div><a href="/status">/status</a><span class="desc">Server status + recent bakes</span></div> </div>
<h2>OS Base Builds</h2> <div class="links"> <div><a href="/os-base-build">/os-base-build</a><span class="desc">Background FedOS base-image jobs</span></div> </div> <div class="panel"> <div class="row"> <input id="os-admin-key" type="password" placeholder="admin key (x-oven-os-key)"> <select id="os-flavor" style="width:100px"><option value="alpine" selected>Alpine</option><option value="fedora">Fedora</option><option value="native">Native</option></select> <input id="os-image-size" type="number" min="1" max="32" value="1" style="width:90px"> <button id="os-start-btn" type="button">Start Base Build</button> <button id="os-refresh-btn" type="button">Refresh</button> </div> <div id="os-job-meta" class="desc">Loading base-build status...</div> <pre id="os-job-log">No active base-image job</pre> </div>
<script> const osMetaEl = document.getElementById('os-job-meta'); const osLogEl = document.getElementById('os-job-log'); const osKeyEl = document.getElementById('os-admin-key'); const osSizeEl = document.getElementById('os-image-size'); const osFlavorEl = document.getElementById('os-flavor'); const osStartBtn = document.getElementById('os-start-btn'); const osRefreshBtn = document.getElementById('os-refresh-btn'); osFlavorEl.addEventListener('change', function() { osSizeEl.value = osFlavorEl.value === 'alpine' ? '1' : '4'; }); let osPollTimer = null;
function esc(str) { return String(str || '').replace(/[&<>"']/g, function (ch) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]; }); }
async function fetchJSON(url, opts) { const res = await fetch(url, opts || {}); const text = await res.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; } if (!res.ok) { throw new Error((data && data.error) || ('HTTP ' + res.status)); } return data; }
function jobLabel(job) { if (!job) return 'none'; const pct = Number.isFinite(job.percent) ? (' ' + job.percent + '%') : ''; return job.id + ' | ' + job.status + pct + ' | ' + (job.stage || '--'); }
async function refreshOSBaseBuild() { try { const summary = await fetchJSON('/os-base-build'); const active = summary.active || null; const recent = Array.isArray(summary.recent) ? summary.recent : []; if (active) { osMetaEl.innerHTML = 'Active: <strong>' + esc(jobLabel(active)) + '</strong>'; const detail = await fetchJSON('/os-base-build/' + encodeURIComponent(active.id) + '?logs=1&tail=100'); const lines = Array.isArray(detail.logs) ? detail.logs : []; osLogEl.textContent = lines.map(function (l) { return '[' + (l.ts || '') + '][' + (l.stream || 'out') + '] ' + (l.line || ''); }).join('\n') || 'No logs yet'; return; }
const latest = recent.length > 0 ? recent[0] : null; osMetaEl.innerHTML = 'Active: none' + (latest ? ' | Latest: <strong>' + esc(jobLabel(latest)) + '</strong>' : ''); osLogEl.textContent = latest ? ((latest.message || '(no message)') + '\n\nUse /os-base-build/' + latest.id + '?logs=1&tail=200 for full logs.') : 'No base-image jobs yet'; } catch (error) { osMetaEl.textContent = 'Status error: ' + error.message; } }
async function startOSBaseBuild() { const key = osKeyEl.value.trim(); const flavor = osFlavorEl.value || 'alpine'; const defaultSize = flavor === 'alpine' ? 1 : 4; const imageSizeGB = Math.max(1, parseInt(osSizeEl.value || String(defaultSize), 10) || defaultSize); osStartBtn.disabled = true; try { const data = await fetchJSON('/os-base-build', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-oven-os-key': key, }, body: JSON.stringify({ imageSizeGB, publish: true, flavor }), }); osMetaEl.textContent = 'Started ' + flavor + ' base build job ' + data.id; } catch (error) { osMetaEl.textContent = 'Start failed: ' + error.message; } finally { osStartBtn.disabled = false; refreshOSBaseBuild(); } }
osStartBtn.addEventListener('click', startOSBaseBuild); osRefreshBtn.addEventListener('click', refreshOSBaseBuild); refreshOSBaseBuild(); osPollTimer = setInterval(refreshOSBaseBuild, 3000); window.addEventListener('beforeunload', function () { if (osPollTimer) clearInterval(osPollTimer); }); </script></body></html>`);});
// API endpointsapp.get('/health', healthHandler);
// Override status to include grabsapp.get('/status', async (req, res) => { await cleanupStaleBakes(); res.json({ version: GIT_VERSION, serverStartTime: SERVER_START_TIME, uptime: Date.now() - SERVER_START_TIME, incoming: Array.from(getIncomingBakes().values()), active: Array.from(getActiveBakes().values()), recent: getRecentBakes(), grabs: { active: getActiveGrabs(), recent: getRecentGrabs(), ipfsThumbs: getAllLatestIPFSUploads() }, osBaseBuilds: getOSBaseBuildsSummary(), });});
app.post('/bake', bakeHandler);app.post('/bake-complete', bakeCompleteHandler);app.post('/bake-status', bakeStatusHandler);
// ─────────────────────────────────────────────────────────────────// Firmware splash renderer (for install-firmware.sh bootsplash swap)//// coreboot's CBFS bootsplash.bmp decoder is extremely narrow: it only// accepts uncompressed 24-bit BGR BMPs with the 54-byte classic header// (BITMAPFILEHEADER + BITMAPINFOHEADER). sharp does not emit BMP, so we// render to raw RGB, swap to BGR, pad rows to 4-byte alignment, and// prepend the header manually.//// Default resolution 1366×768 matches the Chromebook 14 panel coreboot// initializes on MrChromebox. Caller can pass ?w=&h= for other panels,// capped at 1920×1200 so an accidental query doesn't burn the process.// ─────────────────────────────────────────────────────────────────app.get('/firmware/splash.bmp', async (req, res) => { try { const w = Math.max(320, Math.min(1920, parseInt(req.query.w) || 1366)); const h = Math.max(240, Math.min(1200, parseInt(req.query.h) || 768));
// SVG splash — simple black background with centered aesthetic.computer // wordmark. Keep the markup self-contained (no external fonts/assets) // so the request stays fast and cacheable at the CDN layer. const svg = `<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"> <rect width="${w}" height="${h}" fill="#000"/> <g font-family="Courier New, ui-monospace, monospace" text-anchor="middle"> <text x="${w/2}" y="${h/2 - 10}" fill="#ffffff" font-size="${Math.floor(h/14)}" letter-spacing="4">aesthetic.computer</text> <text x="${w/2}" y="${h/2 + Math.floor(h/14) + 12}" fill="#666" font-size="${Math.floor(h/36)}" letter-spacing="2">native</text> </g></svg>`;
const { data } = await sharp(Buffer.from(svg)) .resize(w, h) .raw() .toBuffer({ resolveWithObject: true });
// RGB (sharp default) → BGR, rows bottom-up, 4-byte aligned. const rowBytes = w * 3; const rowPad = (4 - (rowBytes % 4)) % 4; const paddedRow = rowBytes + rowPad; const pixelBytes = paddedRow * h; const fileSize = 54 + pixelBytes;
const header = Buffer.alloc(54); // BITMAPFILEHEADER (14) header.write('BM', 0); header.writeUInt32LE(fileSize, 2); header.writeUInt32LE(0, 6); // reserved header.writeUInt32LE(54, 10); // pixel data offset // BITMAPINFOHEADER (40) header.writeUInt32LE(40, 14); // header size header.writeInt32LE(w, 18); header.writeInt32LE(h, 22); // positive → bottom-up header.writeUInt16LE(1, 26); // planes header.writeUInt16LE(24, 28); // bpp header.writeUInt32LE(0, 30); // BI_RGB (uncompressed) header.writeUInt32LE(pixelBytes, 34); // image size header.writeInt32LE(2835, 38); // X res (72 DPI in px/m) header.writeInt32LE(2835, 42); // Y res header.writeUInt32LE(0, 46); // palette colors header.writeUInt32LE(0, 50); // important colors
const pixels = Buffer.alloc(pixelBytes); for (let y = 0; y < h; y++) { const srcY = (h - 1 - y) * rowBytes; // flip vertically const dstY = y * paddedRow; for (let x = 0; x < w; x++) { const si = srcY + x * 3; const di = dstY + x * 3; pixels[di] = data[si + 2]; // B pixels[di + 1] = data[si + 1]; // G pixels[di + 2] = data[si]; // R } // row padding bytes left as zero }
res.set({ 'Content-Type': 'image/bmp', 'Content-Length': fileSize, 'Cache-Control': 'public, max-age=3600', }); res.end(Buffer.concat([header, pixels])); } catch (err) { console.error('[firmware/splash.bmp] render failed:', err); res.status(500).send('render failed'); }});
// Icon endpoint - small square thumbnails (compatible with grab.aesthetic.computer)// GET /icon/{width}x{height}/{piece}.png// Uses 24h Spaces cache to avoid regenerating on every requestapp.get('/icon/:size/:piece.png', async (req, res) => { const { size, piece } = req.params; const [width, height] = size.split('x').map(n => parseInt(n) || 128); const w = Math.min(width, 512); const h = Math.min(height, 512); try { const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate('icons', piece, w, h, async () => { const result = await grabPiece(piece, { format: 'png', width: w, height: h, density: 1, }); if (!result.success) throw new Error(result.error); // Handle case where grabPiece returns from its own cache (cdnUrl but no buffer) if (result.cached && result.cdnUrl && !result.buffer) { // Fetch the buffer from the CDN URL const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch cached icon: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } return result.buffer; }); if (fromCache && cdnUrl) { res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=86400'); return res.redirect(302, cdnUrl); } res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('X-Cache', 'MISS'); res.send(buffer); } catch (error) { console.error('Icon handler error:', error); res.status(500).json({ error: error.message }); }});
// Animated WebP Icon endpoint - small animated square favicons// GET /icon/{width}x{height}/{piece}.webp// Uses 7-day Spaces cache since animated icons are expensive to generateapp.get('/icon/:size/:piece.webp', async (req, res) => { const { size, piece } = req.params; const [width, height] = size.split('x').map(n => parseInt(n) || 128); // Keep animated icons small for performance (max 128x128) const w = Math.min(width, 128); const h = Math.min(height, 128); // Query params for customization const frames = Math.min(parseInt(req.query.frames) || 30, 60); // Default 30 frames, max 60 const fps = Math.min(parseInt(req.query.fps) || 15, 30); // Default 15 fps, max 30 try { const cacheKey = `${piece}-${w}x${h}-f${frames}-fps${fps}`; const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate('animated-icons', cacheKey, w, h, async () => { const result = await grabPiece(piece, { format: 'webp', width: w, height: h, density: 1, frames: frames, fps: fps, }); if (!result.success) throw new Error(result.error); // Handle case where grabPiece returns from its own cache (cdnUrl but no buffer) if (result.cached && result.cdnUrl && !result.buffer) { const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch cached icon: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } return result.buffer; }, 'webp'); if (fromCache && cdnUrl) { res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days return res.redirect(302, cdnUrl); } res.setHeader('Content-Type', 'image/webp'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day for fresh res.setHeader('X-Cache', 'MISS'); res.send(buffer); } catch (error) { console.error('Animated icon handler error:', error); res.status(500).json({ error: error.message }); }});
// Preview endpoint - larger social media images (compatible with grab.aesthetic.computer)// GET /preview/{width}x{height}/{piece}.png// Uses 24h Spaces cache to avoid regenerating on every requestapp.get('/preview/:size/:piece.png', async (req, res) => { const { size, piece } = req.params; const [width, height] = size.split('x').map(n => parseInt(n) || 1200); const w = Math.min(width, 1920); const h = Math.min(height, 1080); try { const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate('previews', piece, w, h, async () => { const result = await grabPiece(piece, { format: 'png', width: w, height: h, density: 4, viewportScale: 1, }); if (!result.success) throw new Error(result.error); // Handle case where grabPiece returns from its own cache (cdnUrl but no buffer) if (result.cached && result.cdnUrl && !result.buffer) { const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch cached preview: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } return result.buffer; }); if (fromCache && cdnUrl) { res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=86400'); return res.redirect(302, cdnUrl); } res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('X-Cache', 'MISS'); res.send(buffer); } catch (error) { console.error('Preview handler error:', error); res.status(500).json({ error: error.message }); }});
// Product images — static assets for AC hardware/products// GET /product/{name}.png — redirects to Spaces CDN: products/{name}.pngconst PRODUCT_CDN = process.env.ART_CDN_BASE || 'https://art.aesthetic.computer';app.get('/product/:name.png', (req, res) => { const { name } = req.params; if (!/^[a-z0-9-]+$/.test(name)) return res.status(400).json({ error: 'Invalid product name' }); res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days res.redirect(302, `${PRODUCT_CDN}/products/${name}.png`);});
// Grab endpoint - capture screenshots/GIFs from KidLisp piecesapp.post('/grab', grabHandler);app.get('/grab/:format/:width/:height/:piece', grabGetHandler);app.post('/grab-ipfs', grabIPFSHandler);
// Grab status endpointapp.get('/grab-status', (req, res) => { res.json({ active: getActiveGrabs(), recent: getRecentGrabs(), queue: getQueueStatus(), progress: getCurrentProgress(), grabProgress: getAllProgress(), concurrency: getConcurrencyStatus(), osBaseBuilds: getOSBaseBuildsSummary(), });});
// Cleanup stale grabs (grabs stuck for > 5 minutes)app.post('/grab-cleanup', (req, res) => { const result = cleanupStaleGrabs(); addServerLog('cleanup', '🧹', `Manual cleanup: ${result.cleaned} stale grabs removed`); res.json({ success: true, ...result });});
// Emergency clear all active grabs (admin only)app.post('/grab-clear', (req, res) => { const result = clearAllActiveGrabs(); addServerLog('cleanup', '🗑️', `Emergency clear: ${result.cleared} grabs force-cleared`); res.json({ success: true, ...result });});
// Frozen pieces API - get list of frozen piecesapp.get('/api/frozen', (req, res) => { res.json({ frozen: getFrozenPieces() });});
// Clear a piece from the frozen listapp.delete('/api/frozen/:piece', async (req, res) => { const piece = decodeURIComponent(req.params.piece); const result = await clearFrozenPiece(piece); addServerLog('cleanup', '✅', `Cleared frozen piece: ${piece}`); res.json(result);});
// Live collection thumbnail endpoint - redirects to most recent kept WebP// Use this as the collection imageUri for a dynamic thumbnailapp.get('/keeps/latest', async (req, res) => { let latest = getLatestKeepThumbnail(); if (!latest) { latest = await ensureLatestKeepThumbnail(); } if (!latest) { return res.status(404).json({ error: 'No keeps have been captured yet', hint: 'No minted keep thumbnail found in oven or kidlisp records yet' }); } // Redirect to IPFS gateway const gatewayUrl = `${IPFS_GATEWAY}/ipfs/${latest.ipfsCid}`; res.redirect(302, gatewayUrl);});
// Get latest thumbnail for a specific pieceapp.get('/keeps/latest/:piece', (req, res) => { const latest = getLatestIPFSUpload(req.params.piece); if (!latest) { return res.status(404).json({ error: `No keeps captured for piece: ${req.params.piece}`, hint: `Mint ${req.params.piece} with --thumbnail flag to populate this endpoint` }); } const gatewayUrl = `${IPFS_GATEWAY}/ipfs/${latest.ipfsCid}`; res.redirect(302, gatewayUrl);});
// Get all latest thumbnails as JSON (for debugging/monitoring)app.get('/keeps/all', (req, res) => { res.json({ latest: getLatestKeepThumbnail(), byPiece: getAllLatestIPFSUploads() });});
// =============================================================================// KidLisp.com OG Preview Image Endpoint// =============================================================================
// Fast static PNG endpoint - redirects instantly to CDN (for social media crawlers)// Use this URL in og:image and twitter:image meta tagsapp.get('/kidlisp-og.png', async (req, res) => { try { const layout = req.query.layout || 'mosaic'; // Get cached URL without triggering generation (fast!) const url = await getLatestOGImageUrl(layout); if (url) { // Redirect to CDN - instant response (302 so browsers don't permanently cache stale redirects) res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('X-Cache', 'CDN'); return res.redirect(302, url); } // No cached image yet - trigger background regeneration and serve a recent fallback addServerLog('warn', '⚠️', `OG cache miss for ${layout}, triggering regen`); // Trigger async regeneration (don't await) regenerateOGImagesBackground().catch(err => { addServerLog('error', '❌', `Async OG regen failed: ${err.message}`); }); // Use yesterday's image as fallback (likely exists) const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; const fallbackUrl = `https://art.aesthetic.computer/og/kidlisp/${yesterday}-${layout}.png`; res.setHeader('Cache-Control', 'public, max-age=300'); // Short cache for fallback return res.redirect(302, fallbackUrl); } catch (error) { console.error('KidLisp OG PNG error:', error); // Ultimate fallback - yesterday's mosaic const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; return res.redirect(302, `https://art.aesthetic.computer/og/kidlisp/${yesterday}-mosaic.png`); }});
// ─── TzKT dapp images ───────────────────────────────────────────────────────app.get('/kidlisp-og/tzkt-cover.jpg', async (req, res) => { try { addServerLog('info', '🖼️', 'TzKT cover (640x360)'); const result = await generateKidlispOGImage('mosaic', true, { noDotCom: true }); const jpg = await sharp(result.buffer).resize(640, 360, { fit: 'cover' }).jpeg({ quality: 90 }).toBuffer(); res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Content-Length', jpg.length); res.setHeader('Cache-Control', 'public, max-age=3600'); res.send(jpg); } catch (error) { console.error('TzKT cover error:', error); res.status(500).json({ error: error.message }); }});
app.get('/kidlisp-og/tzkt-logo.jpg', async (req, res) => { try { addServerLog('info', '🖼️', 'TzKT logo (200x200)'); // Render $ in Comic Relief via Puppeteer (lightweight, ~3s) const puppeteer = await import('puppeteer'); const browser = await puppeteer.default.launch({ headless: 'new', args: ['--no-sandbox', '--disable-dev-shm-usage'] }); const page = await browser.newPage(); try { await page.setViewport({ width: 200, height: 200, deviceScaleFactor: 2 }); await page.setContent(`<!DOCTYPE html><html><head> <link href="https://fonts.googleapis.com/css2?family=Comic+Relief:wght@700&display=swap" rel="stylesheet"> <style> *{margin:0;padding:0} body{width:200px;height:200px;display:flex;align-items:center;justify-content:center;background:#9370DB} .d{font-family:'Comic Relief',cursive;font-size:170px;font-weight:700;color:limegreen;text-shadow:6px 6px 0 rgba(0,0,0,0.5);margin-top:-10px} </style></head> <body><div class="d">$</div></body></html>`, { waitUntil: 'networkidle0' }); await page.evaluate(() => document.fonts.ready); await new Promise(r => setTimeout(r, 300)); const png = await page.screenshot({ type: 'png' }); const jpg = await sharp(png).resize(200, 200).jpeg({ quality: 90 }).toBuffer(); res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Content-Length', jpg.length); res.setHeader('Cache-Control', 'public, max-age=86400'); res.send(jpg); } finally { await page.close(); await browser.close(); } } catch (error) { console.error('TzKT logo error:', error); res.status(500).json({ error: error.message }); }});
// ─── Site-specific OG images ─────────────────────────────────────────────────app.get('/kidlisp-og/site/:site.png', async (req, res) => { const site = req.params.site; if (!['keeps', 'buy'].includes(site)) return res.status(400).json({ error: 'Invalid site', valid: ['keeps', 'buy'] }); try { addServerLog('info', '🖼️', `Site OG: ${site}.kidlisp.com`); // Use raw mosaic (no branding text) as background const rawUrl = await getLatestOGImageUrl('mosaic-raw'); let mosaicBuffer; if (rawUrl) { const resp = await fetch(rawUrl); if (!resp.ok) throw new Error(`Failed to fetch raw mosaic: ${resp.status}`); mosaicBuffer = Buffer.from(await resp.arrayBuffer()); } else { // Fallback: generate mosaic and use its buffer (will have branding but better than nothing) const result = await generateKidlispOGImage('mosaic', false); mosaicBuffer = result.buffer; if (!mosaicBuffer && result.url) { const resp = await fetch(result.url); if (!resp.ok) throw new Error(`Failed to fetch mosaic: ${resp.status}`); mosaicBuffer = Buffer.from(await resp.arrayBuffer()); } } const bg = await sharp(mosaicBuffer).blur(6).toBuffer(); const darkOverlay = Buffer.from(`<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="rgba(0,0,0,0.4)"/></svg>`); const prefixLetters = site === 'keeps' ? 'keeps'.split('').map(c => `<tspan fill="#00ff41">${c}</tspan>`).join('') : [['b','#FF6B6B'],['u','#4ECDC4'],['y','#FFE66D']].map(([c,col]) => `<tspan fill="${col}">${c}</tspan>`).join(''); const kidlispLetters = [['K','#FF6B6B'],['i','#4ECDC4'],['d','#FFE66D'],['L','#A8E6CF'],['i','#FF8B94'],['s','#F7DC6F'],['p','#BB8FCE']].map(([c,col]) => `<tspan fill="${col}">${c}</tspan>`).join(''); const tspans = `${prefixLetters}<tspan fill="#70D6FF">.</tspan>${kidlispLetters}<tspan fill="#70D6FF">.com</tspan>`; const brandingSvg = Buffer.from(`<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg"><text x="600" y="340" font-family="Comic Sans MS, cursive" font-size="90" font-weight="700" text-anchor="middle" style="paint-order: stroke; stroke: black; stroke-width: 6px;">${tspans}</text></svg>`); const composited = await sharp(bg).composite([{ input: darkOverlay }, { input: brandingSvg }]).png().toBuffer(); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', composited.length); res.setHeader('Cache-Control', 'public, max-age=3600'); res.send(composited); } catch (error) { console.error(`Site OG error (${site}):`, error); res.status(500).json({ error: error.message }); }});
// Dynamic OG image for kidlisp.com - rotates daily based on top hits// Supports multiple layout options: featured, mosaic, filmstrip, code-splitapp.get('/kidlisp-og', async (req, res) => { try { const layout = req.query.layout || 'featured'; const force = req.query.force === 'true'; // Validate layout const validLayouts = ['featured', 'mosaic', 'filmstrip', 'code-split']; if (!validLayouts.includes(layout)) { return res.status(400).json({ error: 'Invalid layout', valid: validLayouts, }); } addServerLog('info', '🖼️', `KidLisp OG request: ${layout}${force ? ' (force)' : ''}`); const result = await generateKidlispOGImage(layout, force); if (result.cached && result.url) { // Redirect to CDN URL for cached images addServerLog('success', '📦', `OG cache hit → ${result.url.split('/').pop()}`); res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=3600'); return res.redirect(302, result.url); } // Fresh generation - return the buffer directly addServerLog('success', '🎨', `OG generated: ${layout} (${result.featuredPiece?.code || 'mosaic'})`); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', result.buffer.length); res.setHeader('Cache-Control', 'public, max-age=86400'); // 24hr cache res.setHeader('X-Cache', 'MISS'); res.setHeader('X-OG-Layout', layout); res.setHeader('X-OG-Generated', result.generatedAt); if (result.featuredPiece) { res.setHeader('X-OG-Featured', result.featuredPiece.code); } res.send(result.buffer); } catch (error) { console.error('KidLisp OG error:', error); addServerLog('error', '❌', `OG error: ${error.message}`); res.status(500).json({ error: 'Failed to generate OG image', message: error.message }); }});
// OG image cache status endpointapp.get('/kidlisp-og/status', (req, res) => { res.json({ ...getOGImageCacheStatus(), availableLayouts: ['featured', 'mosaic', 'filmstrip', 'code-split'], usage: { recommended: '/kidlisp-og.png (instant, for og:image tags)', withLayout: '/kidlisp-og.png?layout=mosaic', dynamic: '/kidlisp-og (may regenerate on-demand)', forceRegenerate: '/kidlisp-og?force=true', }, note: 'Use /kidlisp-og.png for social media meta tags - it redirects instantly to cached CDN images' });});
// Preview all OG images (generalized for kidlisp, notepat, etc)app.get('/og-preview', (req, res) => { const baseUrl = req.protocol + '://' + req.get('host');
const ogImages = [ { name: 'KidLisp', slug: 'kidlisp-og', prodUrls: [ 'https://kidlisp.com', 'https://aesthetic.computer/kidlisp' ], layouts: ['featured', 'mosaic', 'filmstrip', 'code-split'], description: 'Dynamic layouts featuring recent KidLisp pieces' }, { name: 'Notepat', slug: 'notepat-og', prodUrls: [ 'https://notepat.com', 'https://aesthetic.computer/notepat' ], layouts: null, // Single layout description: 'Split-layout chromatic piano interface' } ];
res.setHeader('Content-Type', 'text/html'); res.send(`<!DOCTYPE html><html><head> <title>OG Image Preview</title> <style> body { font-family: monospace; background: #1a1a2e; color: white; padding: 20px; max-width: 1400px; margin: 0 auto; } h1 { color: #88ff88; } h2 { color: #ffaa00; margin-top: 40px; } h3 { color: #ff88aa; margin-top: 20px; } .note { background: #2a2a4e; padding: 16px; border-radius: 8px; margin: 20px 0; line-height: 1.6; } .note code { background: #3a3a5e; padding: 2px 6px; border-radius: 4px; color: #88ffaa; } .og-section { border: 2px solid #333; padding: 20px; border-radius: 8px; margin: 30px 0; background: #16162e; } .prod-urls { margin: 15px 0; } .prod-urls a { display: inline-block; color: #88ccff; text-decoration: none; background: #2a2a4e; padding: 6px 12px; border-radius: 4px; margin: 4px 4px 4px 0; } .prod-urls a:hover { background: #3a3a5e; } .layout { margin: 20px 0; padding: 15px; background: #0a0a1e; border-radius: 6px; } .layout h4 { color: #ffcc66; margin: 0 0 10px 0; } .layout img { max-width: 100%; border: 2px solid #444; border-radius: 4px; } .layout .actions { margin: 10px 0; } .layout .actions a { color: #88ccff; margin-right: 15px; text-decoration: none; } .layout .actions a:hover { text-decoration: underline; } .single-image { margin: 20px 0; } .single-image img { max-width: 100%; border: 2px solid #444; border-radius: 4px; } .back-link { display: inline-block; margin-top: 40px; color: #888; text-decoration: none; } .back-link:hover { color: #aaa; } </style></head><body> <h1>🖼️ OG Image Preview Dashboard</h1> <div class="note"> <strong>About:</strong> This page shows all Open Graph (OG) images used for social media previews.<br> <strong>Usage:</strong> Use the <code>.png</code> endpoints in meta tags for instant CDN redirects (no timeouts).<br> <strong>Testing:</strong> Click production URLs below to verify OG tags are working correctly. </div>
${ogImages.map(og => ` <div class="og-section"> <h2>${og.name}</h2> <p style="color: #aaa; margin: 10px 0;">${og.description}</p>
<div class="prod-urls"> <strong style="color: #88ff88;">Production URLs:</strong><br> ${og.prodUrls.map(url => `<a href="${url}" target="_blank">${url} →</a>`).join(' ')} </div>
<div class="note"> <strong>OG Endpoint:</strong> <code>${baseUrl}/${og.slug}.png</code> </div>
${og.layouts ? ` <h3>Layouts:</h3> ${og.layouts.map(layout => ` <div class="layout"> <h4>${layout.charAt(0).toUpperCase() + layout.slice(1)}</h4> <div class="actions"> <a href="${baseUrl}/${og.slug}?layout=${layout}&force=true">Force Regenerate</a> <a href="${baseUrl}/${og.slug}/status">Cache Status</a> </div> <img src="${baseUrl}/${og.slug}?layout=${layout}" alt="${layout} layout" loading="lazy"> </div> `).join('')} ` : ` <div class="single-image"> <div class="actions"> <a href="${baseUrl}/${og.slug}.png?force=true">Force Regenerate</a> </div> <img src="${baseUrl}/${og.slug}.png" alt="${og.name} OG image" loading="lazy"> </div> `} </div> `).join('')}
<a href="/" class="back-link">← Back to Oven Dashboard</a></body></html>`);});
// Legacy redirect for old kidlisp preview URLapp.get('/kidlisp-og/preview', (req, res) => { res.redirect(302, '/og-preview');});
// Notepat branded OG image for notepat.comapp.get('/notepat-og.png', async (req, res) => { try { const force = req.query.force === 'true'; addServerLog('info', '🎹', `Notepat OG request${force ? ' (force)' : ''}`); const result = await generateNotepatOGImage(force); if (result.cached && result.url) { // Proxy the image back instead of redirecting (iOS crawlers won't follow 301s on og:image) addServerLog('success', '📦', `Notepat OG cache hit → proxying`); try { const cdnResponse = await fetch(result.url); if (!cdnResponse.ok) throw new Error(`CDN fetch failed: ${cdnResponse.status}`); const buffer = Buffer.from(await cdnResponse.arrayBuffer()); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', 'public, max-age=604800'); // 7-day cache res.setHeader('X-Cache', 'HIT'); return res.send(buffer); } catch (fetchErr) { // Fall back to redirect if proxy fails addServerLog('warn', '⚠️', `Notepat OG proxy failed, falling back to redirect: ${fetchErr.message}`); return res.redirect(301, result.url); } }
// Fresh generation - return the buffer directly addServerLog('success', '🎨', `Notepat OG generated`); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', result.buffer.length); res.setHeader('Cache-Control', 'public, max-age=604800'); // 7-day cache res.setHeader('X-Cache', 'MISS'); res.send(result.buffer); } catch (error) { console.error('Notepat OG error:', error); addServerLog('error', '❌', `Notepat OG error: ${error.message}`); res.status(500).json({ error: 'Failed to generate Notepat OG image', message: error.message }); }});
// =============================================================================// News OG Image - Dynamic social cards for news.aesthetic.computer articles// =============================================================================
app.get('/news-og/:code.png', async (req, res) => { try { const code = req.params.code; const force = req.query.force === 'true';
addServerLog('info', '📰', `News OG request: ${code}${force ? ' (force)' : ''}`);
// Fetch post from MongoDB. const mongoUri = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (!mongoUri || !dbName) { return res.status(500).json({ error: 'MongoDB not configured' }); }
const client = new MongoClient(mongoUri); await client.connect(); const db = client.db(dbName); const post = await db.collection('news-posts').findOne({ code, status: { $ne: 'dead' } });
if (!post) { await client.close(); return res.status(404).json({ error: 'Post not found' }); }
// Hydrate handle. if (post.user) { const handleDoc = await db.collection('@handles').findOne({ _id: post.user }); post.handle = handleDoc ? `@${handleDoc.handle}` : '@anon'; } else { post.handle = '@anon'; } await client.close();
const result = await generateNewsOGImage(post, force);
if (result.cached && result.url) { addServerLog('success', '📦', `News OG cache hit: ${code} → proxying`); try { const cdnResponse = await fetch(result.url); if (!cdnResponse.ok) throw new Error(`CDN fetch failed: ${cdnResponse.status}`); const buffer = Buffer.from(await cdnResponse.arrayBuffer()); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', 'public, max-age=604800'); res.setHeader('X-Cache', 'HIT'); return res.send(buffer); } catch (fetchErr) { addServerLog('warn', '⚠️', `News OG proxy failed: ${fetchErr.message}`); return res.redirect(301, result.url); } }
addServerLog('success', '🎨', `News OG generated: ${code}`); res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', result.buffer.length); res.setHeader('Cache-Control', 'public, max-age=604800'); res.setHeader('X-Cache', 'MISS'); res.send(result.buffer); } catch (error) { console.error('News OG error:', error); addServerLog('error', '❌', `News OG error: ${error.message}`); res.status(500).json({ error: 'Failed to generate News OG image', message: error.message }); }});
// =============================================================================// KidLisp Backdrop - Animated WebP for login screens, Auth0, etc.// =============================================================================
// A random pool clip on every hit (302 + no-store so nothing pins one).// ?piece=$code a specific pool piece// ?not=$code skip this one (client-side "next")// Falls back to the daily backdrop while the pool is empty.app.get('/kidlisp-backdrop.webp', async (req, res) => { try { const pick = await pickBackdropUrl({ code: typeof req.query.piece === 'string' ? req.query.piece : null, exclude: typeof req.query.not === 'string' ? req.query.not.replace(/^\$/, '') : null, }); if (pick?.url) { res.setHeader('Cache-Control', 'no-store'); res.setHeader('X-Cache', pick.pool ? 'POOL' : 'CDN'); res.setHeader('X-Backdrop-Piece', pick.piece || 'unknown'); res.setHeader('X-Backdrop-Pool', String(pick.pool || 0)); if (!pick.pool) warmBackdropPool().catch(() => {}); return res.redirect(302, pick.url); }
// Nothing anywhere yet - render today's daily backdrop synchronously. addServerLog('warn', '⚠️', 'Backdrop cache miss, generating...'); const result = await generateKidlispBackdrop(false); warmBackdropPool().catch(() => {}); if (result.url) { res.setHeader('Cache-Control', 'no-store'); res.setHeader('X-Cache', 'MISS'); return res.redirect(302, result.url); } res.status(503).json({ error: 'Backdrop generation in progress, try again shortly' }); } catch (error) { console.error('Backdrop error:', error); res.status(500).json({ error: 'Failed to get backdrop', message: error.message }); }});
// Pool status + manual warmapp.get('/kidlisp-backdrop/pool', async (req, res) => { try { await listBackdropPool(req.query.refresh === 'true'); res.setHeader('Cache-Control', 'no-store'); res.json(getBackdropPoolStatus()); } catch (error) { res.status(500).json({ error: error.message }); }});app.post('/kidlisp-backdrop/pool/warm', requireOSBuildAdmin, async (req, res) => { const force = req.query.force === 'true'; addServerLog('info', '🖼️', `Backdrop pool warm requested${force ? ' (force)' : ''}`); warmBackdropPool({ force }) .then((s) => addServerLog('success', '🎨', `Backdrop pool warm: ${s.rendered?.length ?? 0} rendered, ${s.fresh?.length ?? 0} fresh, ${s.failed?.length ?? 0} failed`)) .catch((err) => addServerLog('error', '❌', `Backdrop pool warm failed: ${err.message}`)); res.json({ started: true, force });});
// Dynamic backdrop generation (may regenerate on-demand)app.get('/kidlisp-backdrop', async (req, res) => { try { const force = req.query.force === 'true'; addServerLog('info', '🖼️', `Backdrop request${force ? ' (force)' : ''}`); const result = await generateKidlispBackdrop(force); if (result.url) { addServerLog('success', '🎨', `Backdrop: ${result.piece} → ${result.cached ? 'cached' : 'generated'}`); res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('X-Cache', result.cached ? 'HIT' : 'MISS'); res.setHeader('X-Backdrop-Piece', result.piece || 'unknown'); return res.redirect(302, result.url); } res.status(500).json({ error: 'Failed to generate backdrop' }); } catch (error) { console.error('Backdrop error:', error); addServerLog('error', '❌', `Backdrop error: ${error.message}`); res.status(500).json({ error: 'Failed to generate backdrop', message: error.message }); }});
// =============================================================================// App Store Screenshots - Generate screenshots for Google Play / App Store// =============================================================================
// App screenshots dashboardapp.get('/app-screenshots', (req, res) => { const piece = req.query.piece || 'prompt'; const presets = Object.entries(APP_SCREENSHOT_PRESETS); res.setHeader('Content-Type', 'text/html'); res.send(`<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>📱 App Store Screenshots - Oven</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: monospace; font-size: 14px; background: #0a0a12; color: #fff; min-height: 100vh; padding: 20px; } header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1em; padding-bottom: 20px; border-bottom: 2px solid #333; margin-bottom: 20px; } h1 { color: #88ff88; font-size: 1.5em; } .controls { display: flex; gap: 1em; align-items: center; flex-wrap: wrap; } input, select, button { font-family: monospace; font-size: 14px; padding: 8px 12px; border: 1px solid #444; background: #1a1a2e; color: #fff; border-radius: 4px; } button { cursor: pointer; background: #2a2a4e; } button:hover { background: #3a3a5e; border-color: #88ff88; } button:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: #226622; border-color: #88ff88; } .btn-primary:hover { background: #338833; } .requirements { background: #1a1a2e; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; } .requirements h3 { color: #ffaa00; margin-bottom: 10px; } .requirements ul { list-style: none; } .requirements li { margin: 5px 0; padding-left: 20px; position: relative; } .requirements li::before { content: '✓'; position: absolute; left: 0; color: #88ff88; } .category { margin-bottom: 30px; } .category h2 { color: #ffaa00; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #333; } .screenshots { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } .screenshot { background: #1a1a2e; border: 1px solid #333; border-radius: 8px; overflow: hidden; } .screenshot:hover { border-color: #88ff88; } .screenshot-preview { background: #000; display: flex; align-items: center; justify-content: center; min-height: 200px; position: relative; } .screenshot-preview img { max-width: 100%; max-height: 300px; object-fit: contain; } .screenshot-preview .loading { position: absolute; color: #888; text-align: center; padding: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; } .screenshot-preview .loading .preview-img { width: 80px; height: 80px; image-rendering: pixelated; border: 1px solid #333; margin-bottom: 8px; display: none; } .screenshot-preview .loading .progress-text { font-size: 11px; margin-top: 8px; color: #88ff88; font-family: monospace; max-width: 150px; word-break: break-word; } .screenshot-preview .loading .progress-bar { width: 80%; max-width: 150px; height: 4px; background: #333; border-radius: 2px; margin: 8px auto 0; overflow: hidden; } .screenshot-preview .loading .progress-bar-fill { height: 100%; background: #88ff88; width: 0%; transition: width 0.3s ease; } .screenshot-preview .error { color: #ff4444; padding: 20px; text-align: center; } .screenshot-info { padding: 15px; } .screenshot-info h4 { margin-bottom: 8px; } .screenshot-info .dims { color: #888; font-size: 12px; margin-bottom: 10px; } .screenshot-info .actions { display: flex; gap: 8px; flex-wrap: wrap; } .screenshot-info .actions a, .screenshot-info .actions button { font-size: 12px; padding: 6px 10px; text-decoration: none; color: #88ccff; } .status { position: fixed; bottom: 20px; right: 20px; background: #2a2a4e; padding: 15px 20px; border-radius: 8px; border: 1px solid #444; display: none; } .status.show { display: block; } .status.success { border-color: #88ff88; } .status.error { border-color: #ff4444; } .back-link { color: #88ccff; text-decoration: none; margin-bottom: 20px; display: inline-block; } .back-link:hover { text-decoration: underline; } </style></head><body> <a href="/" class="back-link">← Back to Oven Dashboard</a> <header> <h1>📱 App Store Screenshots</h1> <div class="controls"> <label>Piece: <input type="text" id="piece-input" value="${piece}" placeholder="prompt"></label> <button onclick="changePiece()">Load</button> <button onclick="regenerateAll()" class="btn-primary">🔄 Regenerate All</button> <button onclick="downloadZip()" class="btn-primary">📦 Download ZIP</button> </div> </header> <div class="requirements"> <h3>📋 Google Play Requirements</h3> <ul> <li>PNG or JPEG, max 8MB each</li> <li>16:9 or 9:16 aspect ratio</li> <li>Phone: 320-3840px per side, 1080px min for promotion</li> <li>7" Tablet: 320-3840px per side</li> <li>10" Tablet: 1080-7680px per side</li> <li>2-8 screenshots per category required</li> </ul> </div> <div class="category"> <h2>📱 Phone Screenshots</h2> <div class="screenshots"> ${presets.filter(([k, v]) => v.category === 'phone').map(([key, preset]) => ` <div class="screenshot" data-preset="${key}"> <div class="screenshot-preview"> <span class="loading" data-loading="${key}"> <img class="preview-img" alt="preview"> <span class="loading-text">🔥 Loading...</span> <div class="progress-text" data-progress-text="${key}"></div> <div class="progress-bar"><div class="progress-bar-fill" data-progress-bar="${key}"></div></div> </span> <img src="/app-screenshots/${key}/${piece}.png" alt="${preset.label}" data-img="${key}" onload="this.previousElementSibling.style.display='none'" onerror="this.style.display='none'; this.previousElementSibling.innerHTML='❌ Failed to load'"> </div> <div class="screenshot-info"> <h4>${preset.label}</h4> <div class="dims">${preset.width} × ${preset.height}px</div> <div class="actions"> <a href="/app-screenshots/${key}/${piece}.png" download="${piece}-${key}.png">⬇️ Download</a> <button onclick="regenerate('${key}')">🔄 Regenerate</button> </div> </div> </div> `).join('')} </div> </div> <div class="category"> <h2>📱 7-inch Tablet Screenshots</h2> <div class="screenshots"> ${presets.filter(([k, v]) => v.category === 'tablet7').map(([key, preset]) => ` <div class="screenshot" data-preset="${key}"> <div class="screenshot-preview"> <span class="loading" data-loading="${key}"> <img class="preview-img" alt="preview"> <span class="loading-text">🔥 Loading...</span> <div class="progress-text" data-progress-text="${key}"></div> <div class="progress-bar"><div class="progress-bar-fill" data-progress-bar="${key}"></div></div> </span> <img src="/app-screenshots/${key}/${piece}.png" alt="${preset.label}" data-img="${key}" onload="this.previousElementSibling.style.display='none'" onerror="this.style.display='none'; this.previousElementSibling.innerHTML='❌ Failed to load'"> </div> <div class="screenshot-info"> <h4>${preset.label}</h4> <div class="dims">${preset.width} × ${preset.height}px</div> <div class="actions"> <a href="/app-screenshots/${key}/${piece}.png" download="${piece}-${key}.png">⬇️ Download</a> <button onclick="regenerate('${key}')">🔄 Regenerate</button> </div> </div> </div> `).join('')} </div> </div> <div class="category"> <h2>📱 10-inch Tablet Screenshots</h2> <div class="screenshots"> ${presets.filter(([k, v]) => v.category === 'tablet10').map(([key, preset]) => ` <div class="screenshot" data-preset="${key}"> <div class="screenshot-preview"> <span class="loading" data-loading="${key}"> <img class="preview-img" alt="preview"> <span class="loading-text">🔥 Loading...</span> <div class="progress-text" data-progress-text="${key}"></div> <div class="progress-bar"><div class="progress-bar-fill" data-progress-bar="${key}"></div></div> </span> <img src="/app-screenshots/${key}/${piece}.png" alt="${preset.label}" data-img="${key}" onload="this.previousElementSibling.style.display='none'" onerror="this.style.display='none'; this.previousElementSibling.innerHTML='❌ Failed to load'"> </div> <div class="screenshot-info"> <h4>${preset.label}</h4> <div class="dims">${preset.width} × ${preset.height}px</div> <div class="actions"> <a href="/app-screenshots/${key}/${piece}.png" download="${piece}-${key}.png">⬇️ Download</a> <button onclick="regenerate('${key}')">🔄 Regenerate</button> </div> </div> </div> `).join('')} </div> </div> <div id="status" class="status"></div> <script> const currentPiece = '${piece}'; function showStatus(msg, type = 'info') { const el = document.getElementById('status'); el.textContent = msg; el.className = 'status show ' + type; setTimeout(() => el.className = 'status', 3000); } function changePiece() { const piece = document.getElementById('piece-input').value.trim() || 'prompt'; window.location.href = '/app-screenshots?piece=' + encodeURIComponent(piece); } document.getElementById('piece-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') changePiece(); }); async function regenerate(preset) { showStatus('Regenerating ' + preset + '... (this takes ~30s)'); console.log('🔄 Starting regeneration for:', preset); // Show loading indicator and hide current image const card = document.querySelector('[data-preset="' + preset + '"]'); const img = card.querySelector('[data-img]'); const loading = card.querySelector('.loading'); img.style.display = 'none'; loading.style.display = 'flex'; loading.innerHTML = '<img class="preview-img" alt="preview"><span class="loading-text">🔄 Regenerating...</span><div class="progress-text"></div><div class="progress-bar"><div class="progress-bar-fill" style="width:0%"></div></div>'; const startTime = Date.now(); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 120000); // 2 min timeout console.log('📡 Fetching with force=true...'); const res = await fetch('/app-screenshots/' + preset + '/' + currentPiece + '.png?force=true&t=' + Date.now(), { signal: controller.signal, cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }); clearTimeout(timeoutId); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log('📡 Response received after ' + elapsed + 's, status:', res.status); if (res.ok) { // Force reload the image with cache-busting const newSrc = '/app-screenshots/' + preset + '/' + currentPiece + '.png?t=' + Date.now(); console.log('🖼️ Setting new image src:', newSrc); img.src = newSrc; img.style.display = 'block'; loading.style.display = 'none'; showStatus('✅ ' + preset + ' regenerated in ' + elapsed + 's!', 'success'); } else { const error = await res.text(); console.error('❌ Regeneration failed:', res.status, error); loading.innerHTML = '❌ Failed: ' + (error || res.status); showStatus('❌ Failed to regenerate: ' + res.status, 'error'); } } catch (err) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.error('❌ Regeneration error after ' + elapsed + 's:', err); if (err.name === 'AbortError') { loading.innerHTML = '⏱️ Timeout - still processing?'; showStatus('⏱️ Request timed out - try refreshing', 'error'); } else { loading.innerHTML = '❌ ' + err.message; showStatus('❌ ' + err.message, 'error'); } } } async function regenerateAll() { const presets = ${JSON.stringify(Object.keys(APP_SCREENSHOT_PRESETS))}; showStatus('Regenerating all screenshots...'); for (const preset of presets) { showStatus('Regenerating ' + preset + '...'); try { await fetch('/app-screenshots/' + preset + '/' + currentPiece + '.png?force=true'); } catch (err) { console.error('Failed:', preset, err); } } showStatus('✅ All screenshots regenerated! Reloading...', 'success'); setTimeout(() => window.location.reload(), 1000); } function downloadZip() { showStatus('Preparing ZIP download...'); window.location.href = '/app-screenshots/download/' + currentPiece; } // WebSocket for real-time progress updates const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; let ws = null; let reconnectAttempts = 0; function connectWebSocket() { ws = new WebSocket(protocol + '//' + location.host + '/ws'); ws.onopen = () => { console.log('📡 WebSocket connected'); reconnectAttempts = 0; }; ws.onclose = () => { console.log('📡 WebSocket disconnected, reconnecting...'); const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000); reconnectAttempts++; setTimeout(connectWebSocket, delay); }; ws.onerror = () => ws.close(); ws.onmessage = (event) => { try { const data = JSON.parse(event.data); // Check if there's active grab progress for our piece if (data.grabs && data.grabs.active) { const activeGrab = data.grabs.active.find(g => g.piece === currentPiece || g.piece === '$' + currentPiece ); if (activeGrab) { // Find which preset this matches (by dimensions) for (const [preset, config] of Object.entries(${JSON.stringify(APP_SCREENSHOT_PRESETS)})) { if (activeGrab.dimensions && activeGrab.dimensions.width === config.width && activeGrab.dimensions.height === config.height) { updateProgressUI(preset, activeGrab.status, null); } } } } } catch (err) { console.error('WebSocket parse error:', err); } }; } // Poll for detailed progress since grabs report to /grab-status async function pollProgress() { try { const res = await fetch('/grab-status'); const data = await res.json(); if (data.progress && data.progress.piece) { const piece = data.progress.piece; if (piece === currentPiece || piece === '$' + currentPiece) { // Check queue position for this piece let queuePosition = null; if (data.queue && data.queue.length > 0) { const queueItem = data.queue.find(q => q.piece === piece); if (queueItem) queuePosition = queueItem.position; } // Find matching preset by checking dimensions in active grabs if (data.active && data.active.length > 0) { const activeGrab = data.active.find(g => g.piece === currentPiece || g.piece === '$' + currentPiece ); if (activeGrab && activeGrab.dimensions) { for (const [preset, config] of Object.entries(${JSON.stringify(APP_SCREENSHOT_PRESETS)})) { if (activeGrab.dimensions.width === config.width && activeGrab.dimensions.height === config.height) { updateProgressUI(preset, data.progress.stage, data.progress.percent, data.progress.stageDetail, null, queuePosition); break; } } } } // Fallback: update all visible loading indicators with generic progress document.querySelectorAll('.loading[data-loading]').forEach(el => { if (el.style.display !== 'none') { const progressText = el.querySelector('.progress-text'); const progressBar = el.querySelector('.progress-bar-fill'); const previewImg = el.querySelector('.preview-img'); if (progressText && data.progress.stageDetail) { progressText.textContent = data.progress.stageDetail; } if (progressBar && data.progress.percent) { progressBar.style.width = data.progress.percent + '%'; } // Display streaming preview if available if (previewImg && data.progress.previewFrame) { previewImg.src = 'data:image/jpeg;base64,' + data.progress.previewFrame; previewImg.style.display = 'block'; } } }); } } } catch (err) { // Ignore polling errors } } function updateProgressUI(preset, stage, percent, detail, previewFrame, queuePosition) { const loading = document.querySelector('[data-loading="' + preset + '"]'); if (!loading || loading.style.display === 'none') return; const progressText = loading.querySelector('.progress-text'); const progressBar = loading.querySelector('.progress-bar-fill'); const previewImg = loading.querySelector('.preview-img'); // Map stage to friendly text const stageText = { 'loading': '🚀 Loading piece...', 'waiting-content': '⏳ Waiting for render...', 'settling': '⏸️ Settling...', 'capturing': '📸 Capturing...', 'encoding': '🔄 Processing...', 'uploading': '☁️ Uploading...', 'queued': queuePosition ? '⏳ In queue (#' + queuePosition + ')...' : '⏳ In queue...', }; if (progressText) { progressText.textContent = detail || stageText[stage] || stage || ''; } if (progressBar && percent != null) { progressBar.style.width = percent + '%'; } // Show streaming preview if (previewImg && previewFrame) { previewImg.src = 'data:image/jpeg;base64,' + previewFrame; previewImg.style.display = 'block'; } } // Start WebSocket and polling connectWebSocket(); const pollInterval = setInterval(pollProgress, 150); // Poll fast for smooth previews // Cleanup on page unload window.addEventListener('beforeunload', () => { clearInterval(pollInterval); if (ws) ws.close(); }); </script></body></html>`);});
// Individual app screenshot endpointapp.get('/app-screenshots/:preset/:piece.png', async (req, res) => { const { preset, piece } = req.params; const force = req.query.force === 'true'; const presetConfig = APP_SCREENSHOT_PRESETS[preset]; if (!presetConfig) { return res.status(400).json({ error: 'Invalid preset', valid: Object.keys(APP_SCREENSHOT_PRESETS) }); } const { width, height } = presetConfig; try { addServerLog('capture', '📱', `App screenshot: ${piece} (${preset} ${width}×${height}${force ? ' FORCE' : ''})`); const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate( 'app-screenshots', `${piece}-${preset}`, width, height, async () => { const result = await grabPiece(piece, { format: 'png', width, height, density: 4, // Pixel art - larger art pixels (4x) viewportScale: 1, // Capture at exact output size skipCache: force, }); if (!result.success) throw new Error(result.error); // Handle cached result (cdnUrl but no buffer) if (result.cached && result.cdnUrl && !result.buffer) { const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch cached screenshot: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } return result.buffer; }, 'png', // ext force // skipCache - pass force flag to skip CDN cache ); if (fromCache && cdnUrl && !force) { res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days return res.redirect(302, cdnUrl); } res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); // When force=true, prevent caching res.setHeader('Cache-Control', force ? 'no-store, no-cache, must-revalidate' : 'public, max-age=86400'); res.setHeader('X-Cache', force ? 'REGENERATED' : 'MISS'); res.setHeader('X-Screenshot-Preset', preset); res.setHeader('X-Screenshot-Dimensions', `${width}x${height}`); res.send(buffer); } catch (error) { console.error('App screenshot error:', error); addServerLog('error', '❌', `App screenshot failed: ${piece} ${preset} - ${error.message}`); res.status(500).json({ error: error.message }); }});
// ─── News screenshot endpoint ──────────────────────────────────────────────// Captures a piece at 16:9 (1200×675) for embedding in news.aesthetic.computer// posts. Returns JSON with the CDN URL so the CLI can emit markdown.// Usage: GET /news-screenshot/notepat.png// GET /news-screenshot/notepat.png?force=trueapp.get('/news-screenshot/:piece.png', async (req, res) => { const { piece } = req.params; const force = req.query.force === 'true'; const width = 1200, height = 675; // 16:9
try { addServerLog('capture', '📰', `News screenshot: ${piece} (${width}×${height}${force ? ' FORCE' : ''})`);
const { cdnUrl, fromCache, buffer } = await getCachedOrGenerate( 'news-screenshots', piece, width, height, async () => { const result = await grabPiece(piece, { format: 'png', width, height, density: 2, viewportScale: 1, skipCache: force, source: 'news', });
if (!result.success) throw new Error(result.error);
if (result.cached && result.cdnUrl && !result.buffer) { const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch cached screenshot: ${response.status}`); return Buffer.from(await response.arrayBuffer()); }
return result.buffer; }, 'png', force, );
// Return JSON only when explicitly requested; default to serving the image. if (req.query.json === 'true') { return res.json({ piece, url: cdnUrl || `https://oven.aesthetic.computer/news-screenshot/${piece}.png`, cached: fromCache, width, height, }); }
if (fromCache && cdnUrl && !force) { res.setHeader('X-Cache', 'HIT'); res.setHeader('Cache-Control', 'public, max-age=604800'); return res.redirect(302, cdnUrl); }
res.setHeader('Content-Type', 'image/png'); res.setHeader('Content-Length', buffer.length); res.setHeader('Cache-Control', force ? 'no-store' : 'public, max-age=86400'); res.setHeader('X-Cache', force ? 'REGENERATED' : 'MISS'); res.send(buffer);
} catch (error) { console.error('News screenshot error:', error); addServerLog('error', '❌', `News screenshot failed: ${piece} - ${error.message}`); res.status(500).json({ error: error.message }); }});
// Bulk ZIP download endpointapp.get('/app-screenshots/download/:piece', async (req, res) => { const { piece } = req.params; const presets = Object.entries(APP_SCREENSHOT_PRESETS); addServerLog('info', '📦', `Generating ZIP for ${piece} (${presets.length} screenshots)`); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${piece}-app-screenshots.zip"`); const archive = archiver('zip', { zlib: { level: 9 } }); archive.pipe(res); for (const [presetKey, preset] of presets) { try { const { cdnUrl, buffer } = await getCachedOrGenerate( 'app-screenshots', `${piece}-${presetKey}`, preset.width, preset.height, async () => { const result = await grabPiece(piece, { format: 'png', width: preset.width, height: preset.height, density: 4, // Pixel art - larger art pixels (4x) viewportScale: 1, // Capture at exact output size }); if (!result.success) throw new Error(result.error); if (result.cached && result.cdnUrl && !result.buffer) { const response = await fetch(result.cdnUrl); if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); return Buffer.from(await response.arrayBuffer()); } return result.buffer; } ); // Get buffer from CDN if we only have URL let imageBuffer = buffer; if (!imageBuffer && cdnUrl) { const response = await fetch(cdnUrl); if (response.ok) { imageBuffer = Buffer.from(await response.arrayBuffer()); } } if (imageBuffer) { const filename = `${preset.category}/${piece}-${presetKey}.png`; archive.append(imageBuffer, { name: filename }); addServerLog('success', '✅', `Added to ZIP: ${filename}`); } } catch (err) { console.error(`Failed to add ${presetKey} to ZIP:`, err); addServerLog('error', '❌', `ZIP: Failed ${presetKey} - ${err.message}`); } } archive.finalize();});
// JSON API for app screenshots statusapp.get('/api/app-screenshots/:piece', async (req, res) => { const { piece } = req.params; const screenshots = {}; for (const [key, preset] of Object.entries(APP_SCREENSHOT_PRESETS)) { screenshots[key] = { ...preset, url: `/app-screenshots/${key}/${piece}.png`, downloadUrl: `/app-screenshots/${key}/${piece}.png?download=true`, }; } res.json({ piece, presets: screenshots, zipUrl: `/app-screenshots/download/${piece}`, dashboardUrl: `/app-screenshots?piece=${piece}`, });});
// ─── Pack HTML endpoint (alias: /bundle-html) ──────────────────────
app.get(['/pack-html', '/bundle-html'], async (req, res) => { const code = req.query.code; const piece = req.query.piece; const format = req.query.format || 'html'; const nocache = req.query.nocache === '1' || req.query.nocache === 'true'; const nocompress = req.query.nocompress === '1' || req.query.nocompress === 'true'; const nominify = req.query.nominify === '1' || req.query.nominify === 'true'; const brotli = req.query.brotli === '1' || req.query.brotli === 'true'; const inline = req.query.inline === '1' || req.query.inline === 'true'; const noboxart = req.query.noboxart === '1' || req.query.noboxart === 'true'; const keeplabel = req.query.keeplabel === '1' || req.query.keeplabel === 'true'; const density = parseInt(req.query.density) || null; const mode = req.query.mode;
// Device mode: simple iframe wrapper (fast path) if (mode === 'device') { const pieceCode = code || piece; if (!pieceCode) return res.status(400).send('Missing code or piece parameter'); return res.set({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=60' }).send(generateDeviceHTML(pieceCode, density)); }
setSkipMinification(nominify);
const isJSPiece = !!piece; const bundleTarget = piece || code; if (!bundleTarget) { return res.status(400).json({ error: "Missing 'code' or 'piece' parameter.", usage: { kidlisp: "/pack-html?code=39j", javascript: "/pack-html?piece=notepat" } }); }
// M4D mode: .amxd binary if (format === 'm4d') { try { const onProgress = (p) => console.log(`[bundler] m4d ${p.stage}: ${p.message}`); const { binary, filename } = await createM4DBundle(bundleTarget, isJSPiece, onProgress, density); res.set({ 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${filename}"`, 'Cache-Control': 'no-cache' }); return res.send(binary); } catch (error) { console.error('M4D bundle failed:', error); return res.status(500).json({ error: error.message }); } }
// JS library mode: emit aesthetic.computer.js (a frozen runtime *library*, // not an HTML doc). Supports SSE streaming (format=js-stream) and a plain // file download / json (format=js). if (format === 'js' || format === 'js-stream') { if (format === 'js-stream') { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); res.flushHeaders(); const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); }; try { const onProgress = (p) => sendEvent('progress', p); const { js, filename, sizeKB } = await createJSLibraryBundle(bundleTarget, isJSPiece, onProgress, density, nocache); sendEvent('complete', { filename, content: Buffer.from(js).toString('base64'), sizeKB }); } catch (error) { console.error('JS library bundle failed:', error); sendEvent('error', { error: error.message }); } return res.end(); } try { const onProgress = (p) => console.log(`[bundler] js ${p.stage}: ${p.message}`); const { js, filename, sizeKB } = await createJSLibraryBundle(bundleTarget, isJSPiece, onProgress, density, nocache); if (req.query.json === '1') { return res.json({ filename, content: Buffer.from(js).toString('base64'), sizeKB }); } const headers = { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'public, max-age=3600' }; if (!inline) headers['Content-Disposition'] = `attachment; filename="${filename}"`; return res.set(headers).send(js); } catch (error) { console.error('JS library bundle failed:', error); return res.status(500).json({ error: error.message }); } }
// SSE streaming mode if (format === 'stream') { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); res.flushHeaders();
const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
try { const onProgress = (p) => sendEvent('progress', p); const { html, filename, sizeKB } = isJSPiece ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, false, nocache) : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, nocache); sendEvent('complete', { filename, content: Buffer.from(html).toString('base64'), sizeKB }); } catch (error) { console.error('Bundle failed:', error); sendEvent('error', { error: error.message }); } return res.end(); }
// Non-streaming modes (json, html download, inline) try { const progressLog = []; const onProgress = (p) => { progressLog.push(p.message); console.log(`[bundler] ${p.stage}: ${p.message}`); }; const result = isJSPiece ? await createJSPieceBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, false, nocache) : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel, nocache); const { html, filename, sizeKB, mainSource, authorHandle, userCode, packDate, depCount } = result;
if (format === 'json' || format === 'base64') { return res.json({ filename, content: Buffer.from(html).toString('base64'), sizeKB, progress: progressLog, sourceCode: mainSource, authorHandle, userCode, packDate, depCount }); }
const headers = { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=3600' }; if (!inline) headers['Content-Disposition'] = `attachment; filename="${filename}"`; return res.set(headers).send(html); } catch (error) { console.error('Bundle failed:', error); return res.status(500).json({ error: error.message }); }});
// JS library packer — convenience alias for /pack-html?...&format=js// Emits aesthetic.computer.js, a frozen single-file AC runtime *library*.app.get('/packjs', async (req, res) => { const code = req.query.code; const piece = req.query.piece; const density = parseInt(req.query.density) || null; const nocache = req.query.nocache === '1' || req.query.nocache === 'true'; const inline = req.query.inline === '1' || req.query.inline === 'true'; const stream = req.query.format === 'stream';
const isJSPiece = !!piece; const bundleTarget = piece || code; if (!bundleTarget) { return res.status(400).json({ error: "Missing 'code' or 'piece' parameter.", usage: { kidlisp: "/packjs?code=39j", javascript: "/packjs?piece=notepat" } }); }
if (stream) { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); res.flushHeaders(); const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); }; try { const onProgress = (p) => sendEvent('progress', p); const { js, filename, sizeKB } = await createJSLibraryBundle(bundleTarget, isJSPiece, onProgress, density, nocache); sendEvent('complete', { filename, content: Buffer.from(js).toString('base64'), sizeKB }); } catch (error) { console.error('JS library bundle failed:', error); sendEvent('error', { error: error.message }); } return res.end(); }
try { const onProgress = (p) => console.log(`[bundler] packjs ${p.stage}: ${p.message}`); const { js, filename, sizeKB } = await createJSLibraryBundle(bundleTarget, isJSPiece, onProgress, density, nocache); const headers = { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'public, max-age=3600', 'X-Bundle-Size-KB': String(sizeKB) }; if (!inline) headers['Content-Disposition'] = `attachment; filename="${filename}"`; return res.set(headers).send(js); } catch (error) { console.error('JS library bundle failed:', error); return res.status(500).json({ error: error.message }); }});
// KidLisp Mini Bundler (RBP-26 profile, ~20 KB target)app.get('/bundle-mini', async (req, res) => { try { const code = req.query.code; const piece = req.query.piece; const seed = req.query.seed ? parseInt(req.query.seed) : null; const download = req.query.dl === '1' || req.query.dl === 'true';
let source = code;
// If piece name is provided, fetch from MongoDB if (piece && !code) { try { source = await fetchPieceSource(piece); } catch (error) { return res.status(404).json({ error: error.message }); } }
if (!source) { return res.status(400).json({ error: "Missing 'code' or 'piece' parameter", usage: "/bundle-mini?code=<source> or /bundle-mini?piece=<name>" }); }
const html = await bundleMini(source, seed); const sizeKB = Math.ceil(Buffer.byteLength(html, 'utf8') / 1024);
const headers = { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=3600', 'X-Bundle-Size-KB': sizeKB.toString() };
if (download) { const pieceName = piece ? piece.replace(/^\$/, '') : 'bundle'; headers['Content-Disposition'] = `attachment; filename="${pieceName}-mini.html"`; }
res.set(headers).send(html); } catch (error) { console.error('[bundle-mini] Error:', error); res.status(500).json({ error: error.message }); }});
// Prewarm the core bundle cache (called by deploy.sh after restart)app.post(['/pack-prewarm', '/bundle-prewarm'], async (req, res) => { try { addServerLog('info', '📦', 'Bundle prewarm started...'); const result = await prewarmCache(); addServerLog('success', '📦', `Bundle cache ready: ${result.fileCount} files in ${result.elapsed}ms (${result.commit})`); res.json(result); } catch (error) { addServerLog('error', '❌', `Bundle prewarm failed: ${error.message}`); res.status(500).json({ error: error.message }); }});
// Cache statusapp.get(['/pack-status', '/bundle-status'], (req, res) => { res.json(getCacheStatus());});
// ===== OS IMAGE BUILDER =====// Assembles bootable FedAC OS artifacts with a piece injected into the FEDAC-PIECE partition.// Requires: pre-baked base image on CDN + e2fsprogs (debugfs) on server.
app.get('/os', async (req, res) => { const code = req.query.code; const piece = req.query.piece; const format = req.query.format || 'download'; const density = parseInt(req.query.density) || 8; const flavor = (req.query.flavor || 'alpine').toLowerCase(); const nocache = req.query.nocache === '1' || req.query.nocache === 'true';
if (!['alpine', 'fedora', 'native'].includes(flavor)) { return res.status(400).json({ error: "Invalid flavor. Use 'alpine', 'fedora', or 'native'." }); }
// Native flavor: pre-built bare-metal kernel images on CDN (no dynamic assembly) if (flavor === 'native') { const nativePiece = piece || code || 'notepat'; const cdnUrl = `https://releases.aesthetic.computer/os/native-${nativePiece}-latest.img.gz`; const filename = `${nativePiece}-native.img.gz`; addServerLog('info', '💿', `OS native redirect: ${nativePiece}`);
if (format === 'stream') { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); res.flushHeaders(); const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); }; sendEvent('progress', { stage: 'native', message: 'Native image ready on CDN', percent: 100 }); sendEvent('complete', { message: 'Native OS image ready', downloadUrl: cdnUrl, filename, cached: true, flavor: 'native', elapsed: 0 }); return res.end(); } return res.redirect(cdnUrl); }
const isJSPiece = !!piece; const target = piece || code; if (!target) { return res.status(400).json({ error: "Missing 'code' or 'piece' parameter.", usage: { kidlisp: "/os?code=39j", javascript: "/os?piece=notepat" }, }); }
addServerLog('info', '💿', `OS ISO build started: ${target} (${flavor})${nocache ? ' [nocache]' : ''}`);
// SSE streaming progress mode (for UI) if (format === 'stream') { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
try { const result = await streamOSImage(null, target, isJSPiece, density, (p) => sendEvent('progress', p), flavor, { nocache }); const downloadParam = isJSPiece ? `piece=${encodeURIComponent(target)}` : `code=${encodeURIComponent(target)}`; // Prefer CDN URL for fast download; fall back to oven direct. const downloadUrl = result.cdnUrl || `/os?${downloadParam}&density=${density}&flavor=${flavor}`; sendEvent('complete', { message: result.cached ? 'OS ISO ready (CDN cached)' : 'OS ISO ready', downloadUrl, elapsed: result.elapsed, filename: result.filename, timings: result.timings, cached: result.cached || false, flavor, }); } catch (err) { console.error('[os] SSE build failed:', err); sendEvent('error', { error: err.message }); } return res.end(); }
// Direct download mode try { const result = await streamOSImage(res, target, isJSPiece, density, (p) => { console.log(`[os] ${p.stage}: ${p.message}`); }, flavor, { nocache }); addServerLog('success', '💿', `OS ISO build complete: ${target}/${flavor} (${Math.round(result.elapsed / 1000)}s)`); } catch (err) { console.error('[os] Build failed:', err); addServerLog('error', '❌', `OS build failed: ${err.message}`); if (!res.headersSent) { res.status(500).json({ error: err.message }); } }});
app.get('/os-status', (req, res) => { res.json(getOSBuildStatus());});
// Proxy releases.json with CORS for the web os.mjs piece.app.get('/os-releases', async (req, res) => { try { const r = await fetch(`${RELEASES_BASE}/releases.json`); if (!r.ok) return res.status(r.status).json({ error: 'Failed to fetch releases' }); const data = await r.json(); const releases = Array.isArray(data?.releases) ? data.releases : [];
const { byName, failedAttempts } = await getNativeBuildStatusData(); const releaseNames = new Set(); for (const rel of releases) { const name = String(rel?.name || '').trim(); if (!name) continue; releaseNames.add(name); const statusMeta = byName.get(name); if (statusMeta?.status) rel.status = statusMeta.status; else if (!rel.status) rel.status = 'success'; if (statusMeta?.error && !rel.error) rel.error = statusMeta.error; if (statusMeta?.buildTs && !rel.last_attempt_ts) rel.last_attempt_ts = statusMeta.buildTs; }
const failedRows = failedAttempts .filter((it) => !releaseNames.has(it.name)) .map((it) => ({ name: it.name, git_hash: it.ref ? it.ref.slice(0, 40) : null, build_ts: it.buildTs || new Date().toISOString(), commit_msg: it.commitMsg || it.error || `build ${it.status}`, status: it.status, error: it.error || null, deprecated: true, attempt_only: true, size: 0, }));
data.releases = releases.concat(failedRows); res.json(data); } catch (err) { res.status(502).json({ error: err.message }); }});
// Flush the cached OS template so the next download gets the fresh one.app.post('/os-cache-flush', (req, res) => { templateCache = null; templateCacheTime = 0; console.log('[os-image] Template cache flushed'); res.json({ flushed: true });});
// Personalized FedAC OS .img download for authenticated AC users.// Downloads the template .img from DO Spaces, patches config.json in-place,// and streams back. Compatible with Fedora Media Writer, Balena Etcher, dd.const RELEASES_BASE = 'https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os';const TEMPLATE_IMG_URL = `${RELEASES_BASE}/native-notepat-latest.img`;const TEMPLATE_GZ_URL = `${RELEASES_BASE}/native-notepat-latest.img.gz`; // legacy fallbackconst TEMPLATE_VMLINUZ_URL = `${RELEASES_BASE}/native-notepat-latest.vmlinuz`;const TEMPLATE_CL_VMLINUZ_URL = `${RELEASES_BASE}/cl-native-notepat-latest.vmlinuz`;const CONFIG_MARKER_LEGACY = '{"handle":"","piece":"notepat","sub":"","email":""}';const CONFIG_PAD_SIZE_LEGACY = 4096;const IDENTITY_MARKER = 'AC_IDENTITY_BLOCK_V1';const IDENTITY_BLOCK_SIZE = 32768;
// Cache the decompressed template in memorylet templateCache = null;let templateCacheTime = 0;const TEMPLATE_CACHE_TTL = 60 * 60 * 1000; // 1 hour
async function getTemplate() { if (templateCache && Date.now() - templateCacheTime < TEMPLATE_CACHE_TTL) { return templateCache; } // Try the raw .img first, fall back to the older compressed image if needed. let raw; const imgRes = await fetch(TEMPLATE_IMG_URL); if (imgRes.ok) { console.log('[os-image] Downloading template .img...'); raw = Buffer.from(await imgRes.arrayBuffer()); } else { console.log('[os-image] No .img found, trying legacy .img.gz fallback...'); const gzRes = await fetch(TEMPLATE_GZ_URL); if (gzRes.ok) { const compressed = Buffer.from(await gzRes.arrayBuffer()); console.log(`[os-image] Decompressing ${(compressed.length / 1048576).toFixed(1)}MB...`); raw = gunzipSync(compressed); } else { throw new Error(`Template download failed (no .img or .img.gz available)`); } } templateCache = raw; templateCacheTime = Date.now(); console.log(`[os-image] Template cached: ${(templateCache.length / 1048576).toFixed(1)}MB`); return templateCache;}
function kernelUrlForVariant(variant) { return variant === 'cl' ? TEMPLATE_CL_VMLINUZ_URL : TEMPLATE_VMLINUZ_URL;}
async function buildPersonalizedEfiImage({ kernelUrl, configJson }) { const id = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const tmpBase = `/tmp/os-image-${id}`; const kernelPath = `${tmpBase}-BOOTX64.EFI`; const configPath = `${tmpBase}-config.json`; const imagePath = `${tmpBase}.img`; const efiOffsetSectors = 2048; const efiOffsetBytes = efiOffsetSectors * 512; let kernelData = null;
try { const kRes = await fetch(kernelUrl); if (!kRes.ok) { throw new Error(`Kernel download failed (${kRes.status})`); } kernelData = Buffer.from(await kRes.arrayBuffer()); await fs.promises.writeFile(kernelPath, kernelData); await fs.promises.writeFile(configPath, configJson);
// Keep headroom for FAT metadata and future kernel size growth. const minBytes = kernelData.length + Buffer.byteLength(configJson) + (32 * 1024 * 1024); const imageSizeMiB = Math.max(384, Math.ceil(minBytes / 1048576) + 32);
execSync(`dd if=/dev/zero of="${imagePath}" bs=1M count=${imageSizeMiB} status=none`); execSync( `printf 'label: gpt\nstart=${efiOffsetSectors}, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\n' | ` + `sfdisk --force --no-reread "${imagePath}" >/dev/null`, ); execSync(`mkfs.vfat -F 32 --offset=${efiOffsetSectors} "${imagePath}" >/dev/null`); execSync(`mmd -i "${imagePath}@@${efiOffsetBytes}" ::EFI ::EFI/BOOT`); execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${kernelPath}" ::EFI/BOOT/BOOTX64.EFI`); execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${configPath}" ::config.json`);
return await fs.promises.readFile(imagePath); } finally { await Promise.allSettled([ fs.promises.unlink(kernelPath), fs.promises.unlink(configPath), fs.promises.unlink(imagePath), ]); }}
// User config endpoint for edge worker image patchingapp.get('/api/user-config', async (req, res) => { const authHeader = req.headers.authorization || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; if (!token) return res.status(401).json({ error: 'Authorization required' });
let userInfo; try { const uiRes = await fetch('https://hi.aesthetic.computer/userinfo', { headers: { Authorization: `Bearer ${token}` }, }); if (!uiRes.ok) throw new Error(`Auth0 ${uiRes.status}`); userInfo = await uiRes.json(); } catch (err) { return res.status(401).json({ error: `Authentication failed: ${err.message}` }); }
const sub = userInfo.sub || ''; let handle = ''; try { const handleRes = await fetch(`https://aesthetic.computer/handle?for=${encodeURIComponent(sub)}`); if (handleRes.ok) { const data = await handleRes.json(); handle = data.handle || ''; } } catch (_) {}
if (!handle) return res.status(403).json({ error: 'No handle found' });
let claudeToken = '', githubPat = ''; try { const mongoUri = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (mongoUri) { const { MongoClient } = await import('mongodb'); const client = new MongoClient(mongoUri); await client.connect(); const doc = await client.db(dbName).collection('@handles').findOne({ _id: sub }); if (doc) { claudeToken = doc.claudeCodeToken || ''; githubPat = doc.githubPat || ''; } await client.close(); } } catch (err) { console.warn(`[user-config] Token lookup failed: ${err.message}`); }
const reqPiece = req.query.piece || 'notepat'; const ALLOWED_PIECES = ['notepat', 'prompt', 'chat', 'laer-klokken']; const bootPiece = ALLOWED_PIECES.includes(reqPiece) ? reqPiece : 'notepat'; const wifiParam = req.query.wifi; const wifiEnabled = wifiParam !== '0' && wifiParam !== 'false';
const config = { handle, piece: bootPiece, sub, email: userInfo.email || '', token }; if (claudeToken) config.claudeToken = claudeToken; if (githubPat) config.githubPat = githubPat; if (!wifiEnabled) config.wifi = false;
res.json(config);});
app.get('/os-image', async (req, res) => { const authHeader = req.headers.authorization || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; if (!token) { return res.status(401).json({ error: 'Authorization required. Log in at aesthetic.computer first.' }); }
let userInfo; try { const uiRes = await fetch('https://hi.aesthetic.computer/userinfo', { headers: { Authorization: `Bearer ${token}` }, }); if (!uiRes.ok) throw new Error(`Auth0 ${uiRes.status}`); userInfo = await uiRes.json(); } catch (err) { return res.status(401).json({ error: `Authentication failed: ${err.message}` }); }
let handle = ''; const sub = userInfo.sub || ''; try { const handleRes = await fetch( `https://aesthetic.computer/handle?for=${encodeURIComponent(sub)}` ); if (handleRes.ok) { const data = await handleRes.json(); handle = data.handle || ''; } } catch (_) {}
if (!handle) { return res.status(403).json({ error: 'You need a handle first. Visit aesthetic.computer/handle to claim one.' }); }
const ALLOWED_PIECES = ['notepat', 'prompt', 'chat', 'laer-klokken']; const reqPiece = req.query.piece || 'notepat'; const bootPiece = ALLOWED_PIECES.includes(reqPiece) ? reqPiece : 'notepat'; const wifiParam = req.query.wifi; const wifiEnabled = wifiParam !== '0' && wifiParam !== 'false'; const requestedLayout = String(req.query.layout || 'img').toLowerCase(); const variant = String(req.query.variant || '').toLowerCase() === 'cl' ? 'cl' : 'c';
let claudeToken = '', githubPat = ''; try { const mongoUri = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (mongoUri) { const { MongoClient } = await import('mongodb'); const client = new MongoClient(mongoUri); await client.connect(); const doc = await client.db(dbName).collection('@handles').findOne({ _id: sub }); if (doc) { claudeToken = doc.claudeCodeToken || ''; githubPat = doc.githubPat || ''; } await client.close(); } } catch (err) { console.warn(`[os-image] Token lookup failed: ${err.message}`); }
console.log(`[os-image] Building personalized image for @${handle} (boot: ${bootPiece}, wifi: ${wifiEnabled}, variant: ${variant}, claude: ${!!claudeToken}, git: ${!!githubPat})`);
const configObj = { handle, piece: bootPiece, sub: userInfo.sub || '', email: userInfo.email || '', token: token, }; if (claudeToken) configObj.claudeToken = claudeToken; if (githubPat) configObj.githubPat = githubPat; if (!wifiEnabled) configObj.wifi = false; const configJson = JSON.stringify(configObj);
let imgData; let fallbackImage = false; let fallbackReason = ''; const buildKernelFallback = async (reason) => { fallbackReason = reason; console.warn( `[os-image] ${reason}; generating ${variant} EFI image for @${handle}`, ); imgData = await buildPersonalizedEfiImage({ kernelUrl: kernelUrlForVariant(variant), configJson, }); fallbackImage = true; }; try { const template = await getTemplate(); imgData = Buffer.from(template); } catch (err) { try { await buildKernelFallback('template-unavailable'); } catch (fallbackErr) { return res.status(503).json({ error: `Template not available (${err.message}) and fallback image build failed: ${fallbackErr.message}`, }); } }
let identityPatchCount = 0; let configPatchCount = 0;
if (!fallbackImage) { const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); let idx = imgData.indexOf(identityMarkerBuf); while (idx !== -1) { const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); header.copy(block); block.copy(imgData, idx); identityPatchCount++; idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); }
const padded = configJson.length >= CONFIG_PAD_SIZE_LEGACY ? configJson.slice(0, CONFIG_PAD_SIZE_LEGACY) : configJson + ' '.repeat(CONFIG_PAD_SIZE_LEGACY - configJson.length); const configBytes = Buffer.from(padded); const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); idx = imgData.indexOf(legacyMarkerBuf); while (idx !== -1) { configBytes.copy(imgData, idx); configPatchCount++; idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); }
if (identityPatchCount === 0 && configPatchCount === 0) { try { await buildKernelFallback('template-missing-config-placeholder'); } catch (err) { return res.status(500).json({ error: `Template image missing config placeholder and fallback image build failed: ${err.message}`, }); } } }
if (!fallbackImage) { console.log( `[os-image] Patched ${identityPatchCount} identity block(s) and ${configPatchCount} config location(s) for @${handle}`, ); }
addServerLog('success', '💿', `OS image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); res.setHeader('X-AC-OS-Requested-Layout', requestedLayout || 'img'); res.setHeader('X-AC-OS-Layout', 'img'); if (fallbackImage) { res.setHeader('X-AC-OS-Fallback', 'kernel-efi-image'); res.setHeader('X-AC-OS-Fallback-Reason', fallbackReason); }
let releaseName = 'native'; try { const relRes = await fetch(`${RELEASES_BASE}/releases.json`); if (relRes.ok) { const relData = await relRes.json(); releaseName = relData?.releases?.[0]?.name || releaseName; } } catch (_) {} const coreName = 'AC-' + releaseName; const d = new Date(); const p = (n) => String(n).padStart(2, '0'); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.img"`); res.setHeader('Content-Length', imgData.length); res.end(imgData);});
// Background base image jobs (build + upload) for FedOS pipeline.app.get('/os-base-build', (req, res) => { res.json(getOSBaseBuildsSummary());});
app.get('/os-base-build/:jobId', (req, res) => { const tail = Math.max(0, Math.min(2000, parseInt(req.query.tail, 10) || 200)); const includeLogs = req.query.logs === '1' || req.query.logs === 'true'; const job = getOSBaseBuild(req.params.jobId, { includeLogs, tail }); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/os-base-build/:jobId/stream', (req, res) => { const jobId = req.params.jobId; const initial = getOSBaseBuild(jobId, { includeLogs: true, tail: 500 }); if (!initial) return res.status(404).json({ error: 'Job not found' });
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
let sentLogs = 0; const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
const sendSnapshot = () => { const job = getOSBaseBuild(jobId, { includeLogs: true, tail: 2000 }); if (!job) { sendEvent('error', { error: 'Job not found' }); return false; }
const logs = Array.isArray(job.logs) ? job.logs : []; if (logs.length > sentLogs) { sendEvent('logs', { logs: logs.slice(sentLogs) }); sentLogs = logs.length; } sendEvent('status', { id: job.id, status: job.status, stage: job.stage, message: job.message, percent: job.percent, updatedAt: job.updatedAt, finishedAt: job.finishedAt, error: job.error, upload: job.upload, }); if (job.status === 'success' || job.status === 'failed' || job.status === 'cancelled') { sendEvent('complete', { status: job.status, error: job.error, upload: job.upload, }); return false; } return true; };
sendEvent('status', { id: initial.id, status: initial.status, stage: initial.stage, message: initial.message, percent: initial.percent, updatedAt: initial.updatedAt, }); if (Array.isArray(initial.logs) && initial.logs.length > 0) { sendEvent('logs', { logs: initial.logs }); sentLogs = initial.logs.length; }
const timer = setInterval(() => { if (!sendSnapshot()) { clearInterval(timer); res.end(); } }, 1000);
req.on('close', () => { clearInterval(timer); });});
app.post('/os-base-build', requireOSBuildAdmin, async (req, res) => { const flavor = (req.body?.flavor || 'alpine').toLowerCase(); if (!['alpine', 'fedora'].includes(flavor)) { return res.status(400).json({ error: "Invalid flavor. Use 'alpine' or 'fedora'." }); } const defaultSize = flavor === 'alpine' ? 1 : 4; const imageSizeGB = Math.max(1, Math.min(32, parseInt(req.body?.imageSizeGB, 10) || defaultSize)); const publish = req.body?.publish !== false; const requestedWorkBase = typeof req.body?.workBase === 'string' ? req.body.workBase.trim() : ''; const workBase = requestedWorkBase || undefined;
try { const job = await startOSBaseBuild( { imageSizeGB, publish, flavor, workBase }, { onStart: (j) => addServerLog('info', '💿', `OS base build started: ${j.id} (${flavor}, ${imageSizeGB}GiB${workBase ? `, workBase=${workBase}` : ''})`), onUploadComplete: async (j) => { addServerLog('success', '☁️', `OS base upload complete: ${j.upload.imageKey}`); invalidateManifest(flavor); addServerLog('info', '💿', `OS manifest cache invalidated (${flavor}) after base upload`); // Purge all cached per-piece builds for this flavor — the new base image // changes the image layout, so old cached builds are stale. const purgeResult = await purgeOSBuildCache(flavor); addServerLog('info', '🗑️', `Purged ${purgeResult.deleted} cached ${flavor} build(s) from CDN`); }, onSuccess: (j) => addServerLog('success', '💿', `OS base build complete: ${j.id} (${flavor})`), onError: (j) => addServerLog('error', '❌', `OS base build failed: ${j.id} (${j.error})`), }, ); return res.status(202).json(job); } catch (error) { if (error.code === 'OS_BASE_BUSY') { return res.status(409).json({ error: error.message, activeJobId: error.activeJobId }); } return res.status(500).json({ error: error.message }); }});
app.post('/os-base-build/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelOSBaseBuild(req.params.jobId); if (!result.ok) { return res.status(400).json(result); } addServerLog('info', '🛑', `OS base build cancel requested: ${req.params.jobId}`); return res.json(result);});
// ── Native OTA Build ──────────────────────────────────────────────────────// Builds fedac/native kernel + uploads vmlinuz to DO Spaces CDN.// Auth: same OS_BUILD_ADMIN_KEY used for /os-base-build.// Auto-triggered by native-git-poller.mjs (polls origin/main every 60s).// Can also be triggered manually via POST with admin key.
app.get('/native-build', (req, res) => { res.json({ ...getNativeBuildsSummary(), poller: getNativePollerStatus() });});
app.get('/native-build/:jobId', (req, res) => { const tail = Math.max(0, Math.min(2000, parseInt(req.query.tail, 10) || 200)); const includeLogs = req.query.logs === '1' || req.query.logs === 'true'; const job = getNativeBuild(req.params.jobId, { includeLogs, tail }); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/native-build/:jobId/stream', (req, res) => { const jobId = req.params.jobId; const initial = getNativeBuild(jobId, { includeLogs: true, tail: 500 }); if (!initial) return res.status(404).json({ error: 'Job not found' });
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
let sentLogs = 0; const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
if (Array.isArray(initial.logs) && initial.logs.length > 0) { sendEvent('logs', { logs: initial.logs }); sentLogs = initial.logs.length; } sendEvent('status', { id: initial.id, status: initial.status, stage: initial.stage, percent: initial.percent });
const timer = setInterval(() => { const job = getNativeBuild(jobId, { includeLogs: true, tail: 2000 }); if (!job) { clearInterval(timer); res.end(); return; } const logs = Array.isArray(job.logs) ? job.logs : []; if (logs.length > sentLogs) { sendEvent('logs', { logs: logs.slice(sentLogs) }); sentLogs = logs.length; } sendEvent('status', { id: job.id, status: job.status, stage: job.stage, percent: job.percent, error: job.error }); if (job.status === 'success' || job.status === 'failed' || job.status === 'cancelled') { sendEvent('complete', { status: job.status, error: job.error }); clearInterval(timer); res.end(); } }, 1000);
req.on('close', () => clearInterval(timer));});
app.post('/native-build', requireOSBuildAdmin, async (req, res) => { try { const job = await startNativeBuild({ ref: req.body?.ref || 'unknown', changed_paths: req.body?.changed_paths || '', variant: req.body?.variant || 'c', // "c", "cl", "nix", "both", or "all" }); addServerLog('info', '🔨', `Native OTA build started: ${job.id} (ref=${job.ref}, flags=${job.flags.join(' ') || 'none'})`); return res.status(202).json(job); } catch (err) { if (err.code === 'NATIVE_BUILD_BUSY') { return res.status(409).json({ error: err.message, activeJobId: err.activeJobId }); } return res.status(500).json({ error: err.message }); }});
app.post('/native-build/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelNativeBuild(req.params.jobId); if (!result.ok) return res.status(400).json(result); addServerLog('info', '🛑', `Native build cancel requested: ${req.params.jobId}`); return res.json(result);});
// ── Papers PDF Build ──────────────────────────────────────────────────────// Builds all AC paper PDFs from LaTeX sources using xelatex.// Auth: same OS_BUILD_ADMIN_KEY used for /native-build.// Auto-triggered by papers-git-poller.mjs (polls origin/main every 60s).// Can also be triggered manually via POST with admin key.
app.get('/papers-build', (req, res) => { res.json({ ...getPapersBuildsSummary(), poller: getPapersPollerStatus() });});
app.get('/papers-build/:jobId', (req, res) => { const tail = Math.max(0, Math.min(2000, parseInt(req.query.tail, 10) || 200)); const includeLogs = req.query.logs === '1' || req.query.logs === 'true'; const job = getPapersBuild(req.params.jobId, { includeLogs, tail }); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/papers-build/:jobId/stream', (req, res) => { const jobId = req.params.jobId; const initial = getPapersBuild(jobId, { includeLogs: true, tail: 500 }); if (!initial) return res.status(404).json({ error: 'Job not found' });
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
let sentLogs = 0; const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
if (Array.isArray(initial.logs) && initial.logs.length > 0) { sendEvent('logs', { logs: initial.logs }); sentLogs = initial.logs.length; } sendEvent('status', { id: initial.id, status: initial.status, stage: initial.stage, percent: initial.percent });
const timer = setInterval(() => { const job = getPapersBuild(jobId, { includeLogs: true, tail: 2000 }); if (!job) { clearInterval(timer); res.end(); return; } const logs = Array.isArray(job.logs) ? job.logs : []; if (logs.length > sentLogs) { sendEvent('logs', { logs: logs.slice(sentLogs) }); sentLogs = logs.length; } sendEvent('status', { id: job.id, status: job.status, stage: job.stage, percent: job.percent, error: job.error }); if (job.status === 'success' || job.status === 'failed' || job.status === 'cancelled') { sendEvent('complete', { status: job.status, error: job.error }); clearInterval(timer); res.end(); } }, 1000);
req.on('close', () => clearInterval(timer));});
app.post('/papers-build', requireOSBuildAdmin, async (req, res) => { try { const job = await startPapersBuild({ ref: req.body?.ref || 'unknown', changed_paths: req.body?.changed_paths || '', }); addServerLog('info', '📄', `Papers PDF build started: ${job.id} (ref=${job.ref})`); return res.status(202).json(job); } catch (err) { if (err.code === 'PAPERS_BUILD_BUSY') { return res.status(409).json({ error: err.message, activeJobId: err.activeJobId }); } return res.status(500).json({ error: err.message }); }});
app.post('/papers-build/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelPapersBuild(req.params.jobId); if (!result.ok) return res.status(400).json(result); addServerLog('info', '🛑', `Papers build cancel requested: ${req.params.jobId}`); return res.json(result);});
// ── Paper Crunch ───────────────────────────────────────────────────────────// On-demand xelatex for a private document that is NOT in the repo: POST a// .tar.gz of the paper directory, stream the log, download the PDF. Nothing is// committed and nothing reaches papers.aesthetic.computer — that is the whole// point (see paper-crunch.mjs). Client: `node papers/bin/crunch.mjs <dir>`.//// Every route takes the admin key, including the reads — the documents are// private, so a 10-char job id is not the thing standing between them and the// world. Auth: same OS_BUILD_ADMIN_KEY used for /papers-build.
app.post('/paper-crunch', requireOSBuildAdmin, express.raw({ type: '*/*', limit: CRUNCH_LIMITS.MAX_BUNDLE_BYTES }), async (req, res) => { try { const job = await startPaperCrunch(req.body, { name: req.get('x-crunch-name'), tex: req.get('x-crunch-tex'), }); addServerLog('info', '📐', `Paper crunch started: ${job.id} (${job.name})`); return res.status(202).json({ ...job, jobId: job.id }); } catch (err) { if (err.code === 'CRUNCH_BUSY') { return res.status(409).json({ error: err.message, activeJobId: err.activeJobId }); } if (err.code === 'CRUNCH_BAD_BUNDLE') { return res.status(400).json({ error: err.message }); } return res.status(500).json({ error: err.message }); }});
app.get('/paper-crunch', requireOSBuildAdmin, (req, res) => { res.json({ ...getPaperCrunchesSummary(), limits: CRUNCH_LIMITS });});
app.get('/paper-crunch/:jobId', requireOSBuildAdmin, (req, res) => { const tail = Math.max(0, Math.min(2000, parseInt(req.query.tail, 10) || 200)); const includeLogs = req.query.logs === '1' || req.query.logs === 'true'; const job = getPaperCrunch(req.params.jobId, { includeLogs, tail }); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/paper-crunch/:jobId/stream', requireOSBuildAdmin, (req, res) => { const jobId = req.params.jobId; const initial = getPaperCrunch(jobId, { includeLogs: true, tail: 500 }); if (!initial) return res.status(404).json({ error: 'Job not found' });
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
let sentLogs = 0; const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
if (Array.isArray(initial.logs) && initial.logs.length > 0) { sendEvent('logs', { logs: initial.logs }); sentLogs = initial.logs.length; } sendEvent('status', { id: initial.id, status: initial.status, stage: initial.stage, percent: initial.percent });
const timer = setInterval(() => { const job = getPaperCrunch(jobId, { includeLogs: true, tail: 2000 }); if (!job) { clearInterval(timer); res.end(); return; } const logs = Array.isArray(job.logs) ? job.logs : []; if (logs.length > sentLogs) { sendEvent('logs', { logs: logs.slice(sentLogs) }); sentLogs = logs.length; } sendEvent('status', { id: job.id, status: job.status, stage: job.stage, percent: job.percent, error: job.error }); if (job.status === 'success' || job.status === 'failed' || job.status === 'cancelled') { sendEvent('complete', { status: job.status, error: job.error, pages: job.pages, pdfBytes: job.pdfBytes }); clearInterval(timer); res.end(); } }, 1000);
req.on('close', () => clearInterval(timer));});
app.get('/paper-crunch/:jobId/pdf', requireOSBuildAdmin, (req, res) => { const pdf = getPaperCrunchPdf(req.params.jobId); if (!pdf) return res.status(404).json({ error: 'No PDF for that job (unfinished, failed, or reaped)' }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="${pdf.name}"`); res.sendFile(pdf.path);});
app.post('/paper-crunch/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelPaperCrunch(req.params.jobId); if (!result.ok) return res.status(400).json(result); addServerLog('info', '🛑', `Paper crunch cancel requested: ${req.params.jobId}`); return res.json(result);});
// ── Recap Builds ───────────────────────────────────────────────────────────// Builds a recap mp4 from a single audience config (e.g. jeffrey-73h-2026-05-02).// Auth: same OS_BUILD_ADMIN_KEY used for /native-build / /papers-build.// Auto-triggered by recap-git-poller.mjs when recap/audience/*.mjs changes// land on origin/main. Manual trigger: POST /recap-build with { audience }.
app.get('/recap-build', (req, res) => { res.json({ ...getRecapBuildsSummary(), poller: getRecapPollerStatus() });});
app.get('/recap-build/:jobId', (req, res) => { const tail = Math.max(0, Math.min(2000, parseInt(req.query.tail, 10) || 200)); const includeLogs = req.query.logs === '1' || req.query.logs === 'true'; const job = getRecapBuild(req.params.jobId, { includeLogs, tail }); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/recap-build/:jobId/stream', (req, res) => { const jobId = req.params.jobId; const initial = getRecapBuild(jobId, { includeLogs: true, tail: 500 }); if (!initial) return res.status(404).json({ error: 'Job not found' });
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.flushHeaders();
let sentLogs = 0; const sendEvent = (type, data) => { res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`); if (typeof res.flush === 'function') res.flush(); };
if (Array.isArray(initial.logs) && initial.logs.length > 0) { sendEvent('logs', { logs: initial.logs }); sentLogs = initial.logs.length; } sendEvent('status', { id: initial.id, status: initial.status, stage: initial.stage, percent: initial.percent });
const timer = setInterval(() => { const job = getRecapBuild(jobId, { includeLogs: true, tail: 2000 }); if (!job) { clearInterval(timer); res.end(); return; } const logs = Array.isArray(job.logs) ? job.logs : []; if (logs.length > sentLogs) { sendEvent('logs', { logs: logs.slice(sentLogs) }); sentLogs = logs.length; } sendEvent('status', { id: job.id, status: job.status, stage: job.stage, percent: job.percent, error: job.error }); if (job.status === 'success' || job.status === 'failed' || job.status === 'cancelled') { sendEvent('complete', { status: job.status, error: job.error, mp4Path: job.mp4Path, mp4Bytes: job.mp4Bytes }); clearInterval(timer); res.end(); } }, 1000);
req.on('close', () => clearInterval(timer));});
// Stream the resulting mp4 for a successful job. Public — anyone with// the (random) jobId can download. Recap mp4s aren't committed to git.app.get('/recap-build/:jobId/mp4', async (req, res) => { const mp4Path = getRecapMp4Path(req.params.jobId); if (!mp4Path) return res.status(404).json({ error: 'mp4 not available (job not finished or not found)' }); res.setHeader('Content-Type', 'video/mp4'); res.setHeader('Content-Disposition', `inline; filename="${basename(mp4Path)}"`); const { createReadStream, statSync } = await import('fs'); try { const stat = statSync(mp4Path); res.setHeader('Content-Length', stat.size); createReadStream(mp4Path).pipe(res); } catch (err) { res.status(500).json({ error: err.message }); }});
app.post('/recap-build', requireOSBuildAdmin, async (req, res) => { try { const audience = req.body?.audience; if (!audience) return res.status(400).json({ error: 'Missing `audience` in body' }); const job = await startRecapBuild({ audience, ref: req.body?.ref || 'unknown', }); addServerLog('info', '🎬', `Recap build started: ${job.id} (audience=${audience})`); return res.status(202).json(job); } catch (err) { if (err.code === 'RECAP_BUILD_BUSY') { return res.status(409).json({ error: err.message, activeJobId: err.activeJobId }); } if (err.code === 'RECAP_BUILD_BAD_AUDIENCE') { return res.status(400).json({ error: err.message }); } return res.status(500).json({ error: err.message }); }});
app.post('/recap-build/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelRecapBuild(req.params.jobId); if (!result.ok) return res.status(400).json(result); addServerLog('info', '🛑', `Recap build cancel requested: ${req.params.jobId}`); return res.json(result);});
// ── Oskiewar Replay Oven ──────────────────────────────────────────────────// Remote-only rendering. Oven receives a date/slot/ref, checks out that exact// commit, runs a latest-build bot fight, and returns gated review artifacts.// Instagram credentials and publication remain on the operator machine.app.get('/oskiewar-reel', requireOSBuildAdmin, (req, res) => res.json(getOskiewarReels()));
app.get('/oskiewar-reel/:jobId', requireOSBuildAdmin, (req, res) => { const job = getOskiewarReel(req.params.jobId, req.query.logs === '1' || req.query.logs === 'true'); if (!job) return res.status(404).json({ error: 'Job not found' }); return res.json(job);});
app.get('/oskiewar-reel/:jobId/:artifact(reel|cover|thumbnail|sidecar)', requireOSBuildAdmin, (req, res) => { const file = getOskiewarReelFile(req.params.jobId, req.params.artifact); if (!file) return res.status(404).json({ error: 'Artifact not available' }); const type = req.params.artifact === 'reel' ? 'video/mp4' : req.params.artifact === 'sidecar' ? 'application/json' : 'image/jpeg'; res.type(type).sendFile(file); });
app.post('/oskiewar-reel', requireOSBuildAdmin, (req, res) => { try { const job = startOskiewarReel(req.body || {}); addServerLog('info', '🥊', `Oskiewar Reel job ${job.id} · ${job.day} #${job.index}`); return res.status(202).json(job); } catch (error) { if (error.code === 'REEL_JOB_BUSY') return res.status(409).json({ error: error.message, activeJobId: error.activeJobId }); if (error.code === 'BAD_REEL_JOB') return res.status(400).json({ error: error.message }); return res.status(500).json({ error: error.message }); }});
app.post('/oskiewar-reel/:jobId/cancel', requireOSBuildAdmin, (req, res) => { const result = cancelOskiewarReel(req.params.jobId); if (!result.ok) return res.status(400).json(result); return res.json(result);});
// ── OS Release Upload ──────────────────────────────────────────────────────// Accepts a vmlinuz binary + metadata, uploads to DO Spaces as OTA release.// Auth: AC token (Bearer) verified against Auth0 userinfo.// Usage: curl -X POST /os-release-upload \// -H "Authorization: Bearer <ac_token>" \// -H "X-Build-Name: swift-egret" \// -H "X-Git-Hash: abc1234" \// -H "X-Build-Ts: 2026-03-11T12:00" \// --data-binary @build/vmlinuzapp.post('/os-release-upload', async (req, res) => { // Auth: verify AC token const authHeader = req.headers.authorization || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; if (!token) { return res.status(401).json({ error: 'Missing Authorization: Bearer <ac_token>' }); }
// Verify token against Auth0 const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || 'hi.aesthetic.computer'; let user; try { const uiRes = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, { headers: { Authorization: `Bearer ${token}` }, }); if (!uiRes.ok) throw new Error(`Auth0 returned ${uiRes.status}`); user = await uiRes.json(); } catch (err) { addServerLog('error', '🔒', `OS release upload auth failed: ${err.message}`); return res.status(401).json({ error: 'Invalid or expired token. Run: ac-login' }); }
const userSub = user.sub || 'unknown'; const userName = user.name || user.nickname || user.email || userSub; addServerLog('info', '📦', `OS release upload from ${userName} (${userSub})`);
// Collect binary body const chunks = []; for await (const chunk of req) chunks.push(chunk); const vmlinuz = Buffer.concat(chunks);
if (vmlinuz.length < 1_000_000) { return res.status(400).json({ error: `File too small (${vmlinuz.length} bytes). Expected vmlinuz ~35-45MB.` }); }
// Metadata from headers const buildName = req.headers['x-build-name'] || `upload-${Date.now()}`; const gitHash = req.headers['x-git-hash'] || 'unknown'; const buildTs = req.headers['x-build-ts'] || new Date().toISOString().slice(0, 16); const commitMsg = req.headers['x-commit-msg'] || ''; const version = `${buildName} ${gitHash}-${buildTs}`;
// SHA256 const crypto = await import('crypto'); const sha256 = crypto.createHash('sha256').update(vmlinuz).digest('hex');
// Upload to DO Spaces const { S3Client, PutObjectCommand, GetObjectCommand } = await import('@aws-sdk/client-s3'); const accessKeyId = process.env.OS_SPACES_KEY || process.env.ART_SPACES_KEY; const secretAccessKey = process.env.OS_SPACES_SECRET || process.env.ART_SPACES_SECRET; if (!accessKeyId || !secretAccessKey) { return res.status(503).json({ error: 'OS Spaces credentials not configured on server.' }); }
const spacesEndpoint = process.env.OS_SPACES_ENDPOINT || 'https://sfo3.digitaloceanspaces.com'; const spacesBucket = process.env.OS_SPACES_BUCKET || 'releases-aesthetic-computer'; const cdnBase = process.env.OS_SPACES_CDN_BASE || `https://${spacesBucket}.sfo3.digitaloceanspaces.com`;
const s3 = new S3Client({ region: process.env.OS_SPACES_REGION || 'us-east-1', endpoint: spacesEndpoint, credentials: { accessKeyId, secretAccessKey }, });
const upload = async (key, body, contentType) => { await s3.send(new PutObjectCommand({ Bucket: spacesBucket, Key: key, Body: body, ContentType: contentType, ACL: 'public-read', })); };
try { addServerLog('info', '☁️', `Uploading ${buildName}: ${(vmlinuz.length / 1048576).toFixed(1)}MB, sha=${sha256.slice(0, 12)}...`);
// Upload version + sha256 first (canary), then vmlinuz // Version file: line 1 = version string, line 2 = kernel size in bytes const versionWithSize = `${version}\n${vmlinuz.length}`; await upload('os/native-notepat-latest.version', versionWithSize, 'text/plain'); await upload('os/native-notepat-latest.sha256', sha256, 'text/plain'); await upload('os/native-notepat-latest.vmlinuz', vmlinuz, 'application/octet-stream');
// Update releases.json let releases = { releases: [] }; try { const existing = await s3.send(new GetObjectCommand({ Bucket: spacesBucket, Key: 'os/releases.json', })); const body = await existing.Body.transformToString(); releases = JSON.parse(body); } catch { /* first release or missing */ }
releases.releases = releases.releases || []; const userHandle = req.headers['x-handle'] || user.nickname || user.name || userName; // Mark all existing builds as deprecated for (const r of releases.releases) r.deprecated = true;
releases.releases.unshift({ version, name: buildName, sha256, size: vmlinuz.length, git_hash: gitHash, build_ts: buildTs, commit_msg: commitMsg, user: userSub, handle: userHandle, url: `${cdnBase}/os/native-notepat-latest.vmlinuz`, }); releases.releases = releases.releases.slice(0, 50); releases.latest = version; releases.latest_name = buildName;
await upload('os/releases.json', JSON.stringify(releases, null, 2), 'application/json');
// Broadcast new build to all connected WebSocket clients (os.mjs pieces) if (wss && wss.clients) { const buildMsg = JSON.stringify({ type: 'os:new-build', releases }); wss.clients.forEach(client => { if (client.readyState === 1) client.send(buildMsg); }); }
addServerLog('success', '🚀', `OS release published: ${buildName} (${gitHash}) by ${userName}`); return res.json({ ok: true, name: buildName, version, sha256, size: vmlinuz.length, url: `${cdnBase}/os/native-notepat-latest.vmlinuz`, user: userSub, }); } catch (err) { addServerLog('error', '❌', `OS release upload failed: ${err.message}`); return res.status(500).json({ error: `Upload failed: ${err.message}` }); }});
app.post('/os-invalidate', async (req, res) => { const purge = req.body?.purge === true; const clearLocal = req.body?.local === true || req.body?.clearLocal === true; const flavor = req.body?.flavor; invalidateManifest(flavor); addServerLog('info', '💿', `OS base image manifest cache invalidated${flavor ? ` (${flavor})` : ''}`);
let localResult = null; if (clearLocal) { localResult = await clearOSBuildLocalCache(flavor); addServerLog('info', '🧹', `Cleared ${localResult.deleted} local base-image cache file(s)${flavor ? ` (${flavor})` : ''}`); }
if (purge) { const purgeResult = await purgeOSBuildCache(flavor); addServerLog('info', '🗑️', `Purged ${purgeResult.deleted} cached build(s) from CDN${flavor ? ` (${flavor})` : ''}`); return res.json({ ok: true, message: clearLocal ? 'Manifest invalidated, local base cache cleared, and CDN build cache purged.' : 'Manifest + CDN build cache purged.', purged: purgeResult.deleted, localCleared: localResult?.deleted || 0, localDirs: localResult?.dirs || [], }); }
if (clearLocal) { return res.json({ ok: true, message: 'Manifest invalidated and local base-image cache cleared.', localCleared: localResult.deleted, localDirs: localResult.dirs, }); }
res.json({ ok: true, message: 'Manifest cache invalidated — next build will re-fetch.' });});
// 404 handlerapp.use((req, res) => { res.status(404).json({ error: 'Not found' });});
// Error handlerapp.use((err, req, res, next) => { console.error('❌ Server error:', err); res.status(500).json({ error: 'Internal server error', message: err.message });});
// Create server and WebSocketlet server;if (dev) { // Load local SSL certs in development mode const httpsOptions = { key: fs.readFileSync('../ssl-dev/localhost-key.pem'), cert: fs.readFileSync('../ssl-dev/localhost.pem'), }; server = https.createServer(httpsOptions, app); server.listen(PORT, () => { console.log(`🔥 Oven server running on https://localhost:${PORT} (dev mode)`); });} else { // Production - plain HTTP (Caddy handles SSL) server = http.createServer(app); server.listen(PORT, () => { console.log(`🔥 Oven server running on http://localhost:${PORT}`); addServerLog('success', '🔥', `Oven server ready (v${GIT_VERSION.slice(0,8)})`); // Pre-warm Puppeteer browser so first keep thumbnail bake is fast setTimeout(() => { addServerLog('info', '🌐', 'Pre-warming grab browser...'); prewarmGrabBrowser().then(() => { addServerLog('success', '🌐', 'Browser pre-warm complete'); }).catch(err => { addServerLog('error', '⚠️', `Browser pre-warm failed: ${err.message}`); }); }, 5000); // Give server 5s to settle first
// Start background OG image regeneration after a short delay setTimeout(() => { addServerLog('info', '🖼️', 'Starting background OG regeneration...'); regenerateOGImagesBackground().then(() => { addServerLog('success', '🖼️', 'OG images ready for social sharing'); }).catch(err => { addServerLog('error', '❌', `OG regen failed: ${err.message}`); }); }, 10000); // Wait 10s for server to fully initialize // Warm the KidLisp backdrop pool (one clip per top @jeffrey piece) after // the OG pass, then top it up daily so new top pieces join the rotation. const warmPool = (why) => { addServerLog('info', '🖼️', `Backdrop pool warm (${why})...`); warmBackdropPool().then((s) => { if (!s.skipped) addServerLog('success', '🎨', `Backdrop pool: ${s.rendered.length} rendered, ${s.fresh.length} fresh, ${s.failed.length} failed`); }).catch(err => addServerLog('error', '❌', `Backdrop pool warm failed: ${err.message}`)); }; setTimeout(() => warmPool('startup'), 90000); setInterval(() => warmPool('daily'), 24 * 60 * 60 * 1000);
// Schedule periodic regeneration (every 6 hours) setInterval(() => { addServerLog('info', '🖼️', 'Scheduled OG regeneration starting...'); regenerateOGImagesBackground().catch(err => { addServerLog('error', '❌', `Scheduled OG regen failed: ${err.message}`); }); }, 6 * 60 * 60 * 1000); // 6 hours
// Start native OTA git poller — watches for fedac/native/ changes startNativeGitPoller({ startNativeBuild, addServerLog });
// Start papers PDF git poller — watches for papers/ changes startPapersGitPoller({ startPapersBuild, addServerLog }); startRecapGitPoller({ startRecapBuild, addServerLog }); });}
// WebSocket serverwss = new WebSocketServer({ server, path: '/ws' });
// Wire up grabber notifications to broadcast to all WebSocket clientssetNotifyCallback(() => { wss.clients.forEach((client) => { if (client.readyState === 1) { // OPEN client.send(JSON.stringify({ version: GIT_VERSION, serverStartTime: SERVER_START_TIME, uptime: Date.now() - SERVER_START_TIME, incoming: Array.from(getIncomingBakes().values()), active: Array.from(getActiveBakes().values()), recent: getRecentBakes(), grabs: { active: getActiveGrabs(), recent: getRecentGrabs(), queue: getQueueStatus(), ipfsThumbs: getAllLatestIPFSUploads() }, grabProgress: getAllProgress(), concurrency: getConcurrencyStatus(), osBaseBuilds: getOSBaseBuildsSummary(), })); } });});
// Wire up grabber log messages to broadcast to clientssetLogCallback((type, icon, msg) => { addServerLog(type, icon, msg);});
// Wire up native build progress to broadcast to all WebSocket clientsonNativeBuildProgress((snapshot) => { if (wss && wss.clients) { const msg = JSON.stringify({ type: 'os:build-progress', build: snapshot }); wss.clients.forEach(client => { if (client.readyState === 1) client.send(msg); }); }});
wss.on('connection', async (ws) => { console.log('📡 WebSocket client connected'); addServerLog('info', '📡', 'Dashboard client connected'); // Clean up stale bakes before sending initial state await cleanupStaleBakes(); // Send initial state with recent logs ws.send(JSON.stringify({ version: GIT_VERSION, serverStartTime: SERVER_START_TIME, uptime: Date.now() - SERVER_START_TIME, incoming: Array.from(getIncomingBakes().values()), active: Array.from(getActiveBakes().values()), recent: getRecentBakes(), grabs: { active: getActiveGrabs(), recent: getRecentGrabs(), queue: getQueueStatus(), ipfsThumbs: getAllLatestIPFSUploads() }, grabProgress: getAllProgress(), concurrency: getConcurrencyStatus(), osBaseBuilds: getOSBaseBuildsSummary(), frozen: getFrozenPieces(), recentLogs: activityLogBuffer.slice(0, 50) // Send last 50 log entries })); // Subscribe to updates const unsubscribe = subscribeToUpdates((update) => { if (ws.readyState === 1) { // OPEN ws.send(JSON.stringify({ version: GIT_VERSION, serverStartTime: SERVER_START_TIME, uptime: Date.now() - SERVER_START_TIME, incoming: Array.from(getIncomingBakes().values()), active: Array.from(getActiveBakes().values()), recent: getRecentBakes(), grabs: { active: getActiveGrabs(), recent: getRecentGrabs(), queue: getQueueStatus(), ipfsThumbs: getAllLatestIPFSUploads() }, grabProgress: getAllProgress(), concurrency: getConcurrencyStatus(), osBaseBuilds: getOSBaseBuildsSummary(), frozen: getFrozenPieces() })); } }); ws.on('close', () => { console.log('📡 WebSocket client disconnected'); unsubscribe(); });});
// Graceful shutdown handlingasync function shutdown(signal) { console.log(`\n🛑 Received ${signal}, shutting down gracefully...`); // Close WebSocket connections wss.clients.forEach(ws => ws.close()); // Close HTTP server server.close(() => { console.log('✅ HTTP server closed'); }); // Close browser if open try { const { closeBrowser } = await import('./grabber.mjs'); await closeBrowser?.(); console.log('✅ Browser closed'); } catch (e) { // Browser close is optional } // Exit after a short delay setTimeout(() => { console.log('👋 Goodbye!'); process.exit(0); }, 500);}
process.on('SIGTERM', () => shutdown('SIGTERM'));process.on('SIGINT', () => shutdown('SIGINT'));