import express from 'express'; import { ScreenshotCache } from './cache.js'; import { takeScreenshot, checkWebGL, closeBrowser, initialize } from './screenshot.js'; const PORT = parseInt(process.env.PORT || '8002', 10); const APP_URL = process.env.APP_URL; const CACHE_DIR = process.env.CACHE_DIR; if (!APP_URL) { console.error('Error: APP_URL environment variable is required'); process.exit(1); } if (!CACHE_DIR) { console.error('Error: CACHE_DIR environment variable is required'); process.exit(1); } const cache = new ScreenshotCache(CACHE_DIR); const app = express(); app.get('/health', (_req, res) => { res.status(200).send('ok'); }); app.get('/debug', async (_req, res) => { try { const info = await checkWebGL(); res.json(info); } catch (err) { res.status(500).json({ error: String(err) }); } }); app.get('/screenshot', async (req, res) => { try { const qs = new URLSearchParams(); for (const key of ['lat', 'lon', 'zoom', 'tab', 'og']) { const val = req.query[key]; if (typeof val === 'string' && val) { qs.set(key, val); } } // Repeated params: filter, poi for (const key of ['filter', 'poi']) { const val = req.query[key]; if (typeof val === 'string' && val) { qs.append(key, val); } else if (Array.isArray(val)) { for (const v of val) { if (typeof v === 'string' && v) qs.append(key, v); } } } // Extract path param (used for non-root pages like /invite/CODE) const pagePath = typeof req.query.path === 'string' && req.query.path ? req.query.path : '/'; if (pagePath !== '/') qs.set('path', pagePath); const cacheKey = cache.buildKey(qs); qs.delete('path'); // Check cache first const cached = cache.get(cacheKey); if (cached) { res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=86400'); res.setHeader('X-Cache', 'HIT'); cached.pipe(res); return; } // Build the URL for the frontend in screenshot mode qs.set('screenshot', '1'); const url = `${APP_URL}${pagePath}?${qs}`; console.log(`Taking screenshot: ${url}`); const jpeg = await takeScreenshot(url); // Cache it cache.set(cacheKey, jpeg); res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=86400'); res.setHeader('X-Cache', 'MISS'); res.send(jpeg); } catch (err) { console.error('Screenshot failed:', err); res.status(500).json({ error: 'Screenshot failed' }); } }); const server = app.listen(PORT, () => { console.log(`Screenshot service listening on port ${PORT}`); console.log(` APP_URL: ${APP_URL}`); console.log(` CACHE_DIR: ${CACHE_DIR}`); // Pre-warm browser and populate network cache in background. // The health endpoint is available immediately; screenshot requests // during warm-up will still work (just slower on the first call). initialize(APP_URL).catch((err) => { console.error('Initialization failed:', err); }); }); // Graceful shutdown for (const signal of ['SIGTERM', 'SIGINT']) { process.on(signal, async () => { console.log(`Received ${signal}, shutting down...`); server.close(); await closeBrowser(); process.exit(0); }); }