Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416#!/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 { 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 2048px 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);});
// ── 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'));