Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517#!/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, 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 { 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>
<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.// =============================================================================
// Fast redirect to CDN-cached 1024px animated webpapp.get('/kidlisp-backdrop.webp', async (req, res) => { try { // Get cached URL without triggering generation (fast!) const url = await getLatestBackdropUrl(); if (url) { res.setHeader('Cache-Control', 'public, max-age=3600'); res.setHeader('X-Cache', 'CDN'); return res.redirect(301, url); } // No cached backdrop - generate synchronously (first request will be slow) addServerLog('warn', '⚠️', 'Backdrop cache miss, generating...'); const result = await generateKidlispBackdrop(false); if (result.url) { res.setHeader('Cache-Control', 'public, max-age=3600'); 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 }); }});
// 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);});
// ── 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 // 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'));