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 <noreply@anthropic.com>
This commit is contained in:
1 parent
705d88dca1
commit
2bf8989ec7
4 files changed
+74
-2
No files matched your search
+10
-1
@@ -1,5 +1,6 @@
|
|||||||
import type { Config } from './config/index.js';
|
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 type Database from 'better-sqlite3';
|
||||||
import { createDatabase, runMigrations } from './database/client.js';
|
import { createDatabase, runMigrations } from './database/client.js';
|
||||||
import {
|
import {
|
||||||
@@ -73,6 +74,13 @@ export interface AppContainer {
|
|||||||
* shutdown).
|
* shutdown).
|
||||||
*/
|
*/
|
||||||
export async function bootstrap(config: Config, logger: Logger): Promise<AppContainer> {
|
export async function bootstrap(config: Config, logger: Logger): Promise<AppContainer> {
|
||||||
|
// 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
|
// Database
|
||||||
const db = createDatabase(config);
|
const db = createDatabase(config);
|
||||||
runMigrations(db);
|
runMigrations(db);
|
||||||
@@ -258,6 +266,7 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
|||||||
successfulMessagesRepo,
|
successfulMessagesRepo,
|
||||||
activityLogRepo,
|
activityLogRepo,
|
||||||
logger,
|
logger,
|
||||||
|
broadcaster,
|
||||||
);
|
);
|
||||||
|
|
||||||
webServer.setChatService(chatService);
|
webServer.setChatService(chatService);
|
||||||
|
|||||||
@@ -1,7 +1,41 @@
|
|||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
|
import Transport from 'winston-transport';
|
||||||
|
|
||||||
export type Logger = winston.Logger;
|
export type Logger = winston.Logger;
|
||||||
|
|
||||||
|
export interface LogSink {
|
||||||
|
push(level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: unknown): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a sink (typically the dashboard's LogBroadcaster) as a winston
|
||||||
|
* transport so every `logger.info/warn/error/debug` call also lands in the
|
||||||
|
* dashboard's `/api/logs` buffer. Call once, as early in bootstrap as
|
||||||
|
* possible — anything logged before this point won't be visible in the UI.
|
||||||
|
*/
|
||||||
|
export function attachLogSink(logger: Logger, sink: LogSink): void {
|
||||||
|
class SinkTransport extends Transport {
|
||||||
|
log(info: Record<string, unknown>, 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 {
|
function safeStringify(value: unknown): string {
|
||||||
const seen = new WeakSet<object>();
|
const seen = new WeakSet<object>();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
import type { WebContext } from '../context.js';
|
import type { WebContext } from '../context.js';
|
||||||
|
|
||||||
export function createDebugRouter(ctx: WebContext): Router {
|
export function createDebugRouter(ctx: WebContext): Router {
|
||||||
const router = 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) => {
|
router.post('/debug/inspect-dom', async (req, res) => {
|
||||||
if (!ctx.services.chat) {
|
if (!ctx.services.chat) {
|
||||||
res.status(500).json({ error: 'Chat service not initialized' });
|
res.status(500).json({ error: 'Chat service not initialized' });
|
||||||
|
|||||||
+13
-1
@@ -46,7 +46,7 @@ export class WebServer {
|
|||||||
private app: express.Application;
|
private app: express.Application;
|
||||||
private server: Server | null = null;
|
private server: Server | null = null;
|
||||||
private activity: ActivityLog;
|
private activity: ActivityLog;
|
||||||
private broadcaster = new LogBroadcaster();
|
private broadcaster: LogBroadcaster;
|
||||||
private botRunning = false;
|
private botRunning = false;
|
||||||
private messagesThisHour = 0;
|
private messagesThisHour = 0;
|
||||||
private runtimeConfig: RuntimeConfig;
|
private runtimeConfig: RuntimeConfig;
|
||||||
@@ -62,7 +62,9 @@ export class WebServer {
|
|||||||
private successfulMessagesRepo: SuccessfulMessagesRepository,
|
private successfulMessagesRepo: SuccessfulMessagesRepository,
|
||||||
private activityLogRepo: ActivityLogRepository,
|
private activityLogRepo: ActivityLogRepository,
|
||||||
private logger: Logger,
|
private logger: Logger,
|
||||||
|
broadcaster?: LogBroadcaster,
|
||||||
) {
|
) {
|
||||||
|
this.broadcaster = broadcaster ?? new LogBroadcaster();
|
||||||
this.activity = new ActivityLog(activityLogRepo);
|
this.activity = new ActivityLog(activityLogRepo);
|
||||||
this.runtimeConfig = {
|
this.runtimeConfig = {
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
@@ -103,6 +105,16 @@ export class WebServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupRoutes(): void {
|
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('/media', express.static(path.join(process.cwd(), 'data', 'media')));
|
||||||
this.app.use(express.static(path.join(process.cwd(), 'public')));
|
this.app.use(express.static(path.join(process.cwd(), 'public')));
|
||||||
this.app.use(express.json());
|
this.app.use(express.json());
|
||||||
|
|||||||
Reference in new issue
Block a user