a0dd516cd5
Systematic cleanup of every error handling anti-pattern detected by the automated scanner. 289 issues fixed via code changes, 12 approved with specific technical justifications. Changes across 90 files: - GENERIC_CATCH (141): Added instanceof Error type discrimination - LARGE_TRY_BLOCK (82): Extracted helper methods to narrow try scope to ≤10 lines - NO_LOGGING_IN_CATCH (65): Added logger/console calls for error visibility - CATCH_AND_CONTINUE_CRITICAL_PATH (10): Added throw/return or approved overrides - ERROR_STRING_MATCHING (2): Approved with rationale (no typed error classes) - ERROR_MESSAGE_GUESSING (1): Replaced chained .includes() with documented pattern array - PROMISE_CATCH_NO_LOGGING (1): Added logging to .catch() handler Also fixes a detector bug where nested try/catch inside a catch block corrupted brace-depth tracking, causing false positives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
/**
|
|
* Viewer Routes
|
|
*
|
|
* Handles health check, viewer UI, and SSE stream endpoints.
|
|
* These are used by the web viewer UI at http://localhost:37777
|
|
*/
|
|
|
|
import express, { Request, Response } from 'express';
|
|
import path from 'path';
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { logger } from '../../../../utils/logger.js';
|
|
import { getPackageRoot } from '../../../../shared/paths.js';
|
|
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
|
import { DatabaseManager } from '../../DatabaseManager.js';
|
|
import { SessionManager } from '../../SessionManager.js';
|
|
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
|
|
|
export class ViewerRoutes extends BaseRouteHandler {
|
|
constructor(
|
|
private sseBroadcaster: SSEBroadcaster,
|
|
private dbManager: DatabaseManager,
|
|
private sessionManager: SessionManager
|
|
) {
|
|
super();
|
|
}
|
|
|
|
setupRoutes(app: express.Application): void {
|
|
// Serve static UI assets (JS, CSS, fonts, etc.)
|
|
const packageRoot = getPackageRoot();
|
|
app.use(express.static(path.join(packageRoot, 'ui')));
|
|
|
|
app.get('/health', this.handleHealth.bind(this));
|
|
app.get('/', this.handleViewerUI.bind(this));
|
|
app.get('/stream', this.handleSSEStream.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Health check endpoint
|
|
*/
|
|
private handleHealth = this.wrapHandler((req: Request, res: Response): void => {
|
|
res.json({ status: 'ok', timestamp: Date.now() });
|
|
});
|
|
|
|
/**
|
|
* Serve viewer UI
|
|
*/
|
|
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
|
|
const packageRoot = getPackageRoot();
|
|
|
|
// Try cache structure first (ui/viewer.html), then marketplace structure (plugin/ui/viewer.html)
|
|
const viewerPaths = [
|
|
path.join(packageRoot, 'ui', 'viewer.html'),
|
|
path.join(packageRoot, 'plugin', 'ui', 'viewer.html')
|
|
];
|
|
|
|
const viewerPath = viewerPaths.find(p => existsSync(p));
|
|
|
|
if (!viewerPath) {
|
|
throw new Error('Viewer UI not found at any expected location');
|
|
}
|
|
|
|
const html = readFileSync(viewerPath, 'utf-8');
|
|
res.setHeader('Content-Type', 'text/html');
|
|
res.send(html);
|
|
});
|
|
|
|
/**
|
|
* SSE stream endpoint
|
|
*/
|
|
private handleSSEStream = this.wrapHandler((req: Request, res: Response): void => {
|
|
// Guard: if DB is not yet initialized, return 503 before registering client
|
|
try {
|
|
this.dbManager.getSessionStore();
|
|
} catch (initError: unknown) {
|
|
if (initError instanceof Error) {
|
|
logger.warn('HTTP', 'SSE stream requested before DB initialization', {}, initError);
|
|
}
|
|
res.status(503).json({ error: 'Service initializing' });
|
|
return;
|
|
}
|
|
|
|
// Setup SSE headers
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
|
|
// Add client to broadcaster
|
|
this.sseBroadcaster.addClient(res);
|
|
|
|
// Send initial_load event with project/source catalog
|
|
const projectCatalog = this.dbManager.getSessionStore().getProjectCatalog();
|
|
this.sseBroadcaster.broadcast({
|
|
type: 'initial_load',
|
|
projects: projectCatalog.projects,
|
|
sources: projectCatalog.sources,
|
|
projectsBySource: projectCatalog.projectsBySource,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Send initial processing status (based on queue depth + active generators)
|
|
const isProcessing = this.sessionManager.isAnySessionProcessing();
|
|
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
|
|
this.sseBroadcaster.broadcast({
|
|
type: 'processing_status',
|
|
isProcessing,
|
|
queueDepth
|
|
});
|
|
});
|
|
}
|