feat: Refactor Settings and Viewer routes to extend BaseRouteHandler for improved error handling

- Introduced BaseRouteHandler class to centralize error handling and response management.
- Updated SettingsRoutes to use wrapHandler for automatic error logging and response.
- Refactored ViewerRoutes to extend BaseRouteHandler and utilize wrapHandler for health check and UI serving.
- Enhanced error handling in SettingsRoutes and ViewerRoutes for better maintainability and readability.
This commit is contained in:
Alex Newman
2025-12-07 22:08:06 -05:00
parent 922f04e66a
commit 9cb4b9d02a
8 changed files with 870 additions and 998 deletions
+114 -167
View File
@@ -11,14 +11,14 @@ import { readFileSync, statSync, existsSync } from 'fs';
import { homedir } from 'os';
import { getPackageRoot } from '../../../../shared/paths.js';
import { getWorkerPort } from '../../../../shared/worker-utils.js';
import { logger } from '../../../../utils/logger.js';
import { PaginationHelper } from '../../PaginationHelper.js';
import { DatabaseManager } from '../../DatabaseManager.js';
import { SessionManager } from '../../SessionManager.js';
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
import type { WorkerService } from '../../../worker-service.js';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
export class DataRoutes {
export class DataRoutes extends BaseRouteHandler {
constructor(
private paginationHelper: PaginationHelper,
private dbManager: DatabaseManager,
@@ -26,7 +26,9 @@ export class DataRoutes {
private sseBroadcaster: SSEBroadcaster,
private workerService: WorkerService,
private startTime: number
) {}
) {
super();
}
setupRoutes(app: express.Application): void {
// Pagination endpoints
@@ -51,233 +53,178 @@ export class DataRoutes {
/**
* Get paginated observations
*/
private handleGetObservations(req: Request, res: Response): void {
try {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getObservations(offset, limit, project);
res.json(result);
} catch (error) {
logger.failure('WORKER', 'Get observations failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
private handleGetObservations = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getObservations(offset, limit, project);
res.json(result);
});
/**
* Get paginated summaries
*/
private handleGetSummaries(req: Request, res: Response): void {
try {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getSummaries(offset, limit, project);
res.json(result);
} catch (error) {
logger.failure('WORKER', 'Get summaries failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
private handleGetSummaries = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getSummaries(offset, limit, project);
res.json(result);
});
/**
* Get paginated user prompts
*/
private handleGetPrompts(req: Request, res: Response): void {
try {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getPrompts(offset, limit, project);
res.json(result);
} catch (error) {
logger.failure('WORKER', 'Get prompts failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
private handleGetPrompts = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getPrompts(offset, limit, project);
res.json(result);
});
/**
* Get observation by ID
* GET /api/observation/:id
*/
private handleGetObservationById(req: Request, res: Response): void {
try {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: 'Invalid observation ID' });
return;
}
private handleGetObservationById = this.wrapHandler((req: Request, res: Response): void => {
const id = this.parseIntParam(req, res, 'id');
if (id === null) return;
const store = this.dbManager.getSessionStore();
const observation = store.getObservationById(id);
const store = this.dbManager.getSessionStore();
const observation = store.getObservationById(id);
if (!observation) {
res.status(404).json({ error: `Observation #${id} not found` });
return;
}
res.json(observation);
} catch (error) {
logger.failure('WORKER', 'Get observation by ID failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
if (!observation) {
this.notFound(res, `Observation #${id} not found`);
return;
}
}
res.json(observation);
});
/**
* Get session by ID
* GET /api/session/:id
*/
private handleGetSessionById(req: Request, res: Response): void {
try {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: 'Invalid session ID' });
return;
}
private handleGetSessionById = this.wrapHandler((req: Request, res: Response): void => {
const id = this.parseIntParam(req, res, 'id');
if (id === null) return;
const store = this.dbManager.getSessionStore();
const sessions = store.getSessionSummariesByIds([id]);
const store = this.dbManager.getSessionStore();
const sessions = store.getSessionSummariesByIds([id]);
if (sessions.length === 0) {
res.status(404).json({ error: `Session #${id} not found` });
return;
}
res.json(sessions[0]);
} catch (error) {
logger.failure('WORKER', 'Get session by ID failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
if (sessions.length === 0) {
this.notFound(res, `Session #${id} not found`);
return;
}
}
res.json(sessions[0]);
});
/**
* Get user prompt by ID
* GET /api/prompt/:id
*/
private handleGetPromptById(req: Request, res: Response): void {
try {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ error: 'Invalid prompt ID' });
return;
}
private handleGetPromptById = this.wrapHandler((req: Request, res: Response): void => {
const id = this.parseIntParam(req, res, 'id');
if (id === null) return;
const store = this.dbManager.getSessionStore();
const prompts = store.getUserPromptsByIds([id]);
const store = this.dbManager.getSessionStore();
const prompts = store.getUserPromptsByIds([id]);
if (prompts.length === 0) {
res.status(404).json({ error: `Prompt #${id} not found` });
return;
}
res.json(prompts[0]);
} catch (error) {
logger.failure('WORKER', 'Get prompt by ID failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
if (prompts.length === 0) {
this.notFound(res, `Prompt #${id} not found`);
return;
}
}
res.json(prompts[0]);
});
/**
* Get database statistics (with worker metadata)
*/
private handleGetStats(req: Request, res: Response): void {
try {
const db = this.dbManager.getSessionStore().db;
private handleGetStats = this.wrapHandler((req: Request, res: Response): void => {
const db = this.dbManager.getSessionStore().db;
// Read version from package.json
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version;
// Read version from package.json
const packageRoot = getPackageRoot();
const packageJsonPath = path.join(packageRoot, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version;
// Get database stats
const totalObservations = db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
const totalSessions = db.prepare('SELECT COUNT(*) as count FROM sdk_sessions').get() as { count: number };
const totalSummaries = db.prepare('SELECT COUNT(*) as count FROM session_summaries').get() as { count: number };
// Get database stats
const totalObservations = db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
const totalSessions = db.prepare('SELECT COUNT(*) as count FROM sdk_sessions').get() as { count: number };
const totalSummaries = db.prepare('SELECT COUNT(*) as count FROM session_summaries').get() as { count: number };
// Get database file size and path
const dbPath = path.join(homedir(), '.claude-mem', 'claude-mem.db');
let dbSize = 0;
if (existsSync(dbPath)) {
dbSize = statSync(dbPath).size;
}
// Worker metadata
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
const activeSessions = this.sessionManager.getActiveSessionCount();
const sseClients = this.sseBroadcaster.getClientCount();
res.json({
worker: {
version,
uptime,
activeSessions,
sseClients,
port: getWorkerPort()
},
database: {
path: dbPath,
size: dbSize,
observations: totalObservations.count,
sessions: totalSessions.count,
summaries: totalSummaries.count
}
});
} catch (error) {
logger.failure('WORKER', 'Get stats failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
// Get database file size and path
const dbPath = path.join(homedir(), '.claude-mem', 'claude-mem.db');
let dbSize = 0;
if (existsSync(dbPath)) {
dbSize = statSync(dbPath).size;
}
}
// Worker metadata
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
const activeSessions = this.sessionManager.getActiveSessionCount();
const sseClients = this.sseBroadcaster.getClientCount();
res.json({
worker: {
version,
uptime,
activeSessions,
sseClients,
port: getWorkerPort()
},
database: {
path: dbPath,
size: dbSize,
observations: totalObservations.count,
sessions: totalSessions.count,
summaries: totalSummaries.count
}
});
});
/**
* Get list of distinct projects from observations
* GET /api/projects
*/
private handleGetProjects(req: Request, res: Response): void {
try {
const db = this.dbManager.getSessionStore().db;
private handleGetProjects = this.wrapHandler((req: Request, res: Response): void => {
const db = this.dbManager.getSessionStore().db;
const rows = db.prepare(`
SELECT DISTINCT project
FROM observations
WHERE project IS NOT NULL
GROUP BY project
ORDER BY MAX(created_at_epoch) DESC
`).all() as Array<{ project: string }>;
const rows = db.prepare(`
SELECT DISTINCT project
FROM observations
WHERE project IS NOT NULL
GROUP BY project
ORDER BY MAX(created_at_epoch) DESC
`).all() as Array<{ project: string }>;
const projects = rows.map(row => row.project);
const projects = rows.map(row => row.project);
res.json({ projects });
} catch (error) {
logger.failure('WORKER', 'Get projects failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
res.json({ projects });
});
/**
* Get current processing status
* GET /api/processing-status
*/
private handleGetProcessingStatus(req: Request, res: Response): void {
private handleGetProcessingStatus = this.wrapHandler((req: Request, res: Response): void => {
const isProcessing = this.sessionManager.isAnySessionProcessing();
const queueDepth = this.sessionManager.getTotalActiveWork(); // Includes queued + actively processing
res.json({ isProcessing, queueDepth });
}
});
/**
* Set processing status (called by hooks)
* NOTE: This now broadcasts computed status based on active processing (ignores input)
*/
private handleSetProcessing(req: Request, res: Response): void {
try {
// Broadcast current computed status (ignores manual input)
this.workerService.broadcastProcessingStatus();
private handleSetProcessing = this.wrapHandler((req: Request, res: Response): void => {
// Broadcast current computed status (ignores manual input)
this.workerService.broadcastProcessingStatus();
const isProcessing = this.sessionManager.isAnySessionProcessing();
const queueDepth = this.sessionManager.getTotalQueueDepth();
const activeSessions = this.sessionManager.getActiveSessionCount();
logger.debug('WORKER', 'Processing status broadcast', { isProcessing, queueDepth, activeSessions });
const isProcessing = this.sessionManager.isAnySessionProcessing();
const queueDepth = this.sessionManager.getTotalQueueDepth();
const activeSessions = this.sessionManager.getActiveSessionCount();
res.json({ status: 'ok', isProcessing });
} catch (error) {
logger.failure('WORKER', 'Failed to broadcast processing status', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
res.json({ status: 'ok', isProcessing });
});
/**
* Parse pagination parameters from request query