// Copyright (c) 2026 Gordon Bolton. MIT License. const express = require('express'); const router = express.Router(); const logStreamAuth = require('../middleware/logStreamAuth'); const { logEmitter, logBuffer, clientLogBuffer, ingestClientLogs } = require('../utils/logCapture'); // Public status check (no auth, no 403 block, returns standard config state) router.get('/status', (req, res) => { res.json({ enabled: process.env.ENABLE_LOG_STREAM === 'true' }); }); // Global toggle check router.use((req, res, next) => { if (process.env.ENABLE_LOG_STREAM !== 'true') { return res.status(403).json({ error: 'Log streaming feature is disabled' }); } next(); }); // Enforce subnet and authentication validations on all debug routes router.use(logStreamAuth); /** * GET /api/debug/server-logs * Exposes a real-time SSE stream of intercepted server stdout/stderr logs. */ router.get('/server-logs', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); // Send historical server logs buffer first for (const line of logBuffer) { res.write(`data: ${line}\n\n`); } // Gracefully close for integration testing if (req.query.testClose === 'true') { res.end(); return; } const sendLog = (line) => { try { res.write(`data: ${line}\n\n`); } catch (err) { console.error('[debugRoutes] Error sending server log line:', err.message); } }; logEmitter.on('server-log', sendLog); // 25s heartbeat comment to prevent proxy timeouts const heartbeat = setInterval(() => { try { res.write(': heartbeat\n\n'); } catch { // Ignore } }, 25000); req.on('close', () => { clearInterval(heartbeat); logEmitter.off('server-log', sendLog); }); }); /** * GET /api/debug/client-logs * Exposes a real-time SSE stream of ingested client-side console logs. */ router.get('/client-logs', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' }); // Send historical client logs buffer first for (const line of clientLogBuffer) { res.write(`data: ${line}\n\n`); } // Gracefully close for integration testing if (req.query.testClose === 'true') { res.end(); return; } const sendClientLog = (line) => { try { res.write(`data: ${line}\n\n`); } catch (err) { console.error('[debugRoutes] Error sending client log line:', err.message); } }; logEmitter.on('client-log', sendClientLog); // 25s heartbeat comment to prevent proxy timeouts const heartbeat = setInterval(() => { try { res.write(': heartbeat\n\n'); } catch { // Ignore } }, 25000); req.on('close', () => { clearInterval(heartbeat); logEmitter.off('client-log', sendClientLog); }); }); /** * POST /api/debug/client-logs * Receives batches of frontend console logs to store in buffer and emit. */ router.post('/client-logs', (req, res) => { const logs = req.body; if (!Array.isArray(logs)) { return res.status(400).json({ error: 'Body must be a JSON array of log entries' }); } try { ingestClientLogs(logs); return res.status(200).json({ success: true, count: logs.length }); } catch (err) { console.error('[debugRoutes] Ingestion failed:', err.message); return res.status(500).json({ error: 'Internal server error during ingestion' }); } }); module.exports = router;