From 2bf8989ec72ef0f3e6a28254c1afa276cb38ce95 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Fri, 22 May 2026 21:57:33 +0100 Subject: [PATCH] Pipe winston logs to dashboard + add Matrix sniff debug route The dashboard's /api/logs buffer only ever saw lines explicitly pushed via broadcaster.push, so winston output (including the Matrix token-extractor's timeout diagnostics) was invisible from the deployed container. Attach a winston transport that forwards every entry into the LogBroadcaster before the rest of bootstrap runs, so the matrix sniff's `finalUrl=... hosts=[...]` line shows up in /api/logs. Also serve data/matrix-debug.png (the screenshot the extractor drops on timeout) at /api/debug/matrix-debug.png so we can see what chat.reddit.com looked like in the headed browser without shell access to the Dokploy host. Co-Authored-By: Claude Opus 4.7 --- src/bootstrap.ts | 11 ++++++++++- src/utils/logger.ts | 34 ++++++++++++++++++++++++++++++++++ src/web/routes/debug.ts | 17 +++++++++++++++++ src/web/server.ts | 14 +++++++++++++- 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index c92352f..088786a 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -1,5 +1,6 @@ import type { Config } from './config/index.js'; -import type { Logger } from './utils/logger.js'; +import { attachLogSink, type Logger } from './utils/logger.js'; +import { LogBroadcaster } from './web/log-broadcaster.js'; import type Database from 'better-sqlite3'; import { createDatabase, runMigrations } from './database/client.js'; import { @@ -73,6 +74,13 @@ export interface AppContainer { * shutdown). */ export async function bootstrap(config: Config, logger: Logger): Promise { + // Wire winston → dashboard log buffer immediately so the matrix-token + // sniff and other early bootstrap diagnostics are visible in /api/logs. + const broadcaster = new LogBroadcaster(); + if (config.web.enabled) { + attachLogSink(logger, broadcaster); + } + // Database const db = createDatabase(config); runMigrations(db); @@ -258,6 +266,7 @@ export async function bootstrap(config: Config, logger: Logger): Promise, callback: () => void): void { + setImmediate(() => this.emit('logged', info)); + const level = (info.level as string) || 'info'; + const allowed = ['info', 'warn', 'error', 'debug'] as const; + const lvl = (allowed as readonly string[]).includes(level) + ? (level as 'info' | 'warn' | 'error' | 'debug') + : 'info'; + const message = String(info.message ?? ''); + const { level: _l, message: _m, timestamp: _t, ...meta } = info; + const hasMeta = Object.keys(meta).length > 0; + try { + sink.push(lvl, message, hasMeta ? meta : undefined); + } catch { + // never let the sink break logging + } + callback(); + } + } + logger.add(new SinkTransport({ level: 'debug' })); +} + function safeStringify(value: unknown): string { const seen = new WeakSet(); try { diff --git a/src/web/routes/debug.ts b/src/web/routes/debug.ts index 055157d..2fd9fcf 100644 --- a/src/web/routes/debug.ts +++ b/src/web/routes/debug.ts @@ -1,9 +1,26 @@ import { Router } from 'express'; +import path from 'path'; +import fs from 'fs'; import type { WebContext } from '../context.js'; export function createDebugRouter(ctx: WebContext): Router { const router = Router(); + // Serves the screenshot dropped by the matrix token-extractor when the + // sniff times out (data/matrix-debug.png). Lets us see what chat.reddit.com + // looked like in the headed browser without needing host shell access. + router.get('/debug/matrix-debug.png', (_req, res) => { + const p = path.resolve(process.cwd(), 'data/matrix-debug.png'); + if (!fs.existsSync(p)) { + res.status(404).json({ error: 'No matrix-debug.png yet — sniff has not failed since last clean start.' }); + return; + } + const stat = fs.statSync(p); + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.set('X-Captured-At', stat.mtime.toISOString()); + res.sendFile(p); + }); + router.post('/debug/inspect-dom', async (req, res) => { if (!ctx.services.chat) { res.status(500).json({ error: 'Chat service not initialized' }); diff --git a/src/web/server.ts b/src/web/server.ts index 2db5e96..046bd8b 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -46,7 +46,7 @@ export class WebServer { private app: express.Application; private server: Server | null = null; private activity: ActivityLog; - private broadcaster = new LogBroadcaster(); + private broadcaster: LogBroadcaster; private botRunning = false; private messagesThisHour = 0; private runtimeConfig: RuntimeConfig; @@ -62,7 +62,9 @@ export class WebServer { private successfulMessagesRepo: SuccessfulMessagesRepository, private activityLogRepo: ActivityLogRepository, private logger: Logger, + broadcaster?: LogBroadcaster, ) { + this.broadcaster = broadcaster ?? new LogBroadcaster(); this.activity = new ActivityLog(activityLogRepo); this.runtimeConfig = { rateLimit: { @@ -103,6 +105,16 @@ export class WebServer { } private setupRoutes(): void { + // index.html serves the entire inline JS — never cache it. + this.app.use((req, res, next) => { + if (req.path === '/' || req.path === '/index.html') { + const p = path.join(process.cwd(), 'public', 'index.html'); + res.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + return res.sendFile(p); + } + next(); + }); + this.app.use('/media', express.static(path.join(process.cwd(), 'data', 'media'))); this.app.use(express.static(path.join(process.cwd(), 'public'))); this.app.use(express.json());