#!/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, createM4DBundle, generateDeviceHTML, prewarmCache, getCacheStatus, setSkipMinification } from './bundler.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 { join, dirname } 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 display const 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 clients const 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 modules export { addServerLog }; // Log server startup addServerLog('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 dashboards const 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 updates const 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 'preview' + '' + initialText + '' + '
' + '
'; } // 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 bodies app.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 monitor app.get('/', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(OVEN_TV_HTML); }); const OVEN_TV_HTML = ` oven
oven
0/6 active 0 queued -- --
Tools
Waiting for grabs...
Up Next
No items queued
Recent
No recent grabs
`; // Tools submenu β€” links to OG images, app screenshots, bundles, status pages app.get('/tools', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(` oven / tools

oven / tools

OG Images

App Screenshots

Bundles

Grabs

Status

OS Base Builds

Loading base-build status...
No active base-image job
`); }); // API endpoints app.get('/health', healthHandler); // Override status to include grabs app.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 = ` aesthetic.computer native `; 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 request app.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 generate app.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 request app.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}.png const 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 pieces app.post('/grab', grabHandler); app.get('/grab/:format/:width/:height/:piece', grabGetHandler); app.post('/grab-ipfs', grabIPFSHandler); // Grab status endpoint app.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 pieces app.get('/api/frozen', (req, res) => { res.json({ frozen: getFrozenPieces() }); }); // Clear a piece from the frozen list app.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 thumbnail app.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 piece app.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 tags app.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(`
$
`, { 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(``); const prefixLetters = site === 'keeps' ? 'keeps'.split('').map(c => `${c}`).join('') : [['b','#FF6B6B'],['u','#4ECDC4'],['y','#FFE66D']].map(([c,col]) => `${c}`).join(''); const kidlispLetters = [['K','#FF6B6B'],['i','#4ECDC4'],['d','#FFE66D'],['L','#A8E6CF'],['i','#FF8B94'],['s','#F7DC6F'],['p','#BB8FCE']].map(([c,col]) => `${c}`).join(''); const tspans = `${prefixLetters}.${kidlispLetters}.com`; const brandingSvg = Buffer.from(`${tspans}`); 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-split app.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 endpoint app.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(` OG Image Preview

πŸ–ΌοΈ OG Image Preview Dashboard

About: This page shows all Open Graph (OG) images used for social media previews.
Usage: Use the .png endpoints in meta tags for instant CDN redirects (no timeouts).
Testing: Click production URLs below to verify OG tags are working correctly.
${ogImages.map(og => `

${og.name}

${og.description}

Production URLs:
${og.prodUrls.map(url => `${url} β†’`).join(' ')}
OG Endpoint: ${baseUrl}/${og.slug}.png
${og.layouts ? `

Layouts:

${og.layouts.map(layout => `

${layout.charAt(0).toUpperCase() + layout.slice(1)}

${layout} layout
`).join('')} ` : `
${og.name} OG image
`}
`).join('')} ← Back to Oven Dashboard `); }); // Legacy redirect for old kidlisp preview URL app.get('/kidlisp-og/preview', (req, res) => { res.redirect(302, '/og-preview'); }); // Notepat branded OG image for notepat.com app.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 webp app.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 dashboard app.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(` πŸ“± App Store Screenshots - Oven ← Back to Oven Dashboard

πŸ“± App Store Screenshots

πŸ“‹ Google Play Requirements

πŸ“± Phone Screenshots

${presets.filter(([k, v]) => v.category === 'phone').map(([key, preset]) => `
preview πŸ”₯ Loading...
${preset.label}

${preset.label}

${preset.width} Γ— ${preset.height}px
⬇️ Download
`).join('')}

πŸ“± 7-inch Tablet Screenshots

${presets.filter(([k, v]) => v.category === 'tablet7').map(([key, preset]) => `
preview πŸ”₯ Loading...
${preset.label}

${preset.label}

${preset.width} Γ— ${preset.height}px
⬇️ Download
`).join('')}

πŸ“± 10-inch Tablet Screenshots

${presets.filter(([k, v]) => v.category === 'tablet10').map(([key, preset]) => `
preview πŸ”₯ Loading...
${preset.label}

${preset.label}

${preset.width} Γ— ${preset.height}px
⬇️ Download
`).join('')}
`); }); // Individual app screenshot endpoint app.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=true app.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 endpoint app.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 status app.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 }); } } // 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) : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel); 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) : await createBundle(bundleTarget, onProgress, nocompress, density, brotli, noboxart, keeplabel); 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 }); } }); // 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 status app.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 fallback const 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 memory let 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 patching app.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); }); // ── 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 " \ // -H "X-Build-Name: swift-egret" \ // -H "X-Git-Hash: abc1234" \ // -H "X-Build-Ts: 2026-03-11T12:00" \ // --data-binary @build/vmlinuz app.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 ' }); } // 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 handler app.use((req, res) => { res.status(404).json({ error: 'Not found' }); }); // Error handler app.use((err, req, res, next) => { console.error('❌ Server error:', err); res.status(500).json({ error: 'Internal server error', message: err.message }); }); // Create server and WebSocket let 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 }); }); } // WebSocket server wss = new WebSocketServer({ server, path: '/ws' }); // Wire up grabber notifications to broadcast to all WebSocket clients setNotifyCallback(() => { 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 clients setLogCallback((type, icon, msg) => { addServerLog(type, icon, msg); }); // Wire up native build progress to broadcast to all WebSocket clients onNativeBuildProgress((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 handling async 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'));