- Moved hydration module back from _deprecated (needed for product_refresh) - Restored product_refresh handler for processing stored payloads - Restored geolocation service for findadispo/findagram - Stubbed system routes that depend on deprecated SyncOrchestrator - Removed crawler-sandbox route (deprecated) - Fixed all TypeScript compilation errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
585 lines
18 KiB
TypeScript
585 lines
18 KiB
TypeScript
/**
|
|
* System API Routes
|
|
*
|
|
* Provides REST API endpoints for system monitoring and control:
|
|
* - /api/system/sync/* - Sync orchestrator
|
|
* - /api/system/dlq/* - Dead-letter queue
|
|
* - /api/system/integrity/* - Integrity checks
|
|
* - /api/system/fix/* - Auto-fix routines
|
|
* - /api/system/alerts/* - System alerts
|
|
* - /metrics - Prometheus metrics
|
|
*
|
|
* Phase 5: Full Production Sync + Monitoring
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { Pool } from 'pg';
|
|
import {
|
|
SyncOrchestrator,
|
|
MetricsService,
|
|
DLQService,
|
|
AlertService,
|
|
IntegrityService,
|
|
AutoFixService,
|
|
} from '../services';
|
|
|
|
export function createSystemRouter(pool: Pool): Router {
|
|
const router = Router();
|
|
|
|
// Initialize services
|
|
const metrics = new MetricsService(pool);
|
|
const dlq = new DLQService(pool);
|
|
const alerts = new AlertService(pool);
|
|
const integrity = new IntegrityService(pool, alerts);
|
|
const autoFix = new AutoFixService(pool, alerts);
|
|
const orchestrator = new SyncOrchestrator(pool, metrics, dlq, alerts);
|
|
|
|
// ============================================================
|
|
// SYNC ORCHESTRATOR ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /api/system/sync/status
|
|
* Get current sync status
|
|
*/
|
|
router.get('/sync/status', async (_req: Request, res: Response) => {
|
|
try {
|
|
const status = await orchestrator.getStatus();
|
|
res.json(status);
|
|
} catch (error) {
|
|
console.error('[System] Sync status error:', error);
|
|
res.status(500).json({ error: 'Failed to get sync status' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/sync/run
|
|
* Trigger a sync run
|
|
*/
|
|
router.post('/sync/run', async (req: Request, res: Response) => {
|
|
try {
|
|
const triggeredBy = req.body.triggeredBy || 'api';
|
|
const result = await orchestrator.runSync();
|
|
res.json({
|
|
success: true,
|
|
triggeredBy,
|
|
metrics: result,
|
|
});
|
|
} catch (error) {
|
|
console.error('[System] Sync run error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Sync run failed',
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/sync/queue-depth
|
|
* Get queue depth information
|
|
*/
|
|
router.get('/sync/queue-depth', async (_req: Request, res: Response) => {
|
|
try {
|
|
const depth = await orchestrator.getQueueDepth();
|
|
res.json(depth);
|
|
} catch (error) {
|
|
console.error('[System] Queue depth error:', error);
|
|
res.status(500).json({ error: 'Failed to get queue depth' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/sync/health
|
|
* Get sync health status
|
|
*/
|
|
router.get('/sync/health', async (_req: Request, res: Response) => {
|
|
try {
|
|
const health = await orchestrator.getHealth();
|
|
res.status(health.healthy ? 200 : 503).json(health);
|
|
} catch (error) {
|
|
console.error('[System] Health check error:', error);
|
|
res.status(500).json({ healthy: false, error: 'Health check failed' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/sync/pause
|
|
* Pause the orchestrator
|
|
*/
|
|
router.post('/sync/pause', async (req: Request, res: Response) => {
|
|
try {
|
|
const reason = req.body.reason || 'Manual pause';
|
|
await orchestrator.pause(reason);
|
|
res.json({ success: true, message: 'Orchestrator paused' });
|
|
} catch (error) {
|
|
console.error('[System] Pause error:', error);
|
|
res.status(500).json({ error: 'Failed to pause orchestrator' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/sync/resume
|
|
* Resume the orchestrator
|
|
*/
|
|
router.post('/sync/resume', async (_req: Request, res: Response) => {
|
|
try {
|
|
await orchestrator.resume();
|
|
res.json({ success: true, message: 'Orchestrator resumed' });
|
|
} catch (error) {
|
|
console.error('[System] Resume error:', error);
|
|
res.status(500).json({ error: 'Failed to resume orchestrator' });
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// DLQ ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /api/system/dlq
|
|
* List DLQ payloads
|
|
*/
|
|
router.get('/dlq', async (req: Request, res: Response) => {
|
|
try {
|
|
const options = {
|
|
status: req.query.status as string,
|
|
errorType: req.query.errorType as string,
|
|
dispensaryId: req.query.dispensaryId ? parseInt(req.query.dispensaryId as string) : undefined,
|
|
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
|
|
offset: req.query.offset ? parseInt(req.query.offset as string) : 0,
|
|
};
|
|
|
|
const result = await dlq.listPayloads(options);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[System] DLQ list error:', error);
|
|
res.status(500).json({ error: 'Failed to list DLQ payloads' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/dlq/stats
|
|
* Get DLQ statistics
|
|
*/
|
|
router.get('/dlq/stats', async (_req: Request, res: Response) => {
|
|
try {
|
|
const stats = await dlq.getStats();
|
|
res.json(stats);
|
|
} catch (error) {
|
|
console.error('[System] DLQ stats error:', error);
|
|
res.status(500).json({ error: 'Failed to get DLQ stats' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/dlq/summary
|
|
* Get DLQ summary by error type
|
|
*/
|
|
router.get('/dlq/summary', async (_req: Request, res: Response) => {
|
|
try {
|
|
const summary = await dlq.getSummary();
|
|
res.json(summary);
|
|
} catch (error) {
|
|
console.error('[System] DLQ summary error:', error);
|
|
res.status(500).json({ error: 'Failed to get DLQ summary' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/dlq/:id
|
|
* Get a specific DLQ payload
|
|
*/
|
|
router.get('/dlq/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const payload = await dlq.getPayload(req.params.id);
|
|
if (!payload) {
|
|
return res.status(404).json({ error: 'Payload not found' });
|
|
}
|
|
res.json(payload);
|
|
} catch (error) {
|
|
console.error('[System] DLQ get error:', error);
|
|
res.status(500).json({ error: 'Failed to get DLQ payload' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/dlq/:id/retry
|
|
* Retry a DLQ payload
|
|
*/
|
|
router.post('/dlq/:id/retry', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await dlq.retryPayload(req.params.id);
|
|
if (result.success) {
|
|
res.json(result);
|
|
} else {
|
|
res.status(400).json(result);
|
|
}
|
|
} catch (error) {
|
|
console.error('[System] DLQ retry error:', error);
|
|
res.status(500).json({ error: 'Failed to retry payload' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/dlq/:id/abandon
|
|
* Abandon a DLQ payload
|
|
*/
|
|
router.post('/dlq/:id/abandon', async (req: Request, res: Response) => {
|
|
try {
|
|
const reason = req.body.reason || 'Manually abandoned';
|
|
const abandonedBy = req.body.abandonedBy || 'api';
|
|
const success = await dlq.abandonPayload(req.params.id, reason, abandonedBy);
|
|
res.json({ success });
|
|
} catch (error) {
|
|
console.error('[System] DLQ abandon error:', error);
|
|
res.status(500).json({ error: 'Failed to abandon payload' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/dlq/bulk-retry
|
|
* Bulk retry payloads by error type
|
|
*/
|
|
router.post('/dlq/bulk-retry', async (req: Request, res: Response) => {
|
|
try {
|
|
const { errorType } = req.body;
|
|
if (!errorType) {
|
|
return res.status(400).json({ error: 'errorType is required' });
|
|
}
|
|
const result = await dlq.bulkRetryByErrorType(errorType);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[System] DLQ bulk retry error:', error);
|
|
res.status(500).json({ error: 'Failed to bulk retry' });
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// INTEGRITY CHECK ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* POST /api/system/integrity/run
|
|
* Run all integrity checks
|
|
*/
|
|
router.post('/integrity/run', async (req: Request, res: Response) => {
|
|
try {
|
|
const triggeredBy = req.body.triggeredBy || 'api';
|
|
const result = await integrity.runAllChecks(triggeredBy);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[System] Integrity run error:', error);
|
|
res.status(500).json({ error: 'Failed to run integrity checks' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/integrity/runs
|
|
* Get recent integrity check runs
|
|
*/
|
|
router.get('/integrity/runs', async (req: Request, res: Response) => {
|
|
try {
|
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
|
|
const runs = await integrity.getRecentRuns(limit);
|
|
res.json(runs);
|
|
} catch (error) {
|
|
console.error('[System] Integrity runs error:', error);
|
|
res.status(500).json({ error: 'Failed to get integrity runs' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/integrity/runs/:runId
|
|
* Get results for a specific integrity run
|
|
*/
|
|
router.get('/integrity/runs/:runId', async (req: Request, res: Response) => {
|
|
try {
|
|
const results = await integrity.getRunResults(req.params.runId);
|
|
res.json(results);
|
|
} catch (error) {
|
|
console.error('[System] Integrity run results error:', error);
|
|
res.status(500).json({ error: 'Failed to get run results' });
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// AUTO-FIX ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /api/system/fix/routines
|
|
* Get available fix routines
|
|
*/
|
|
router.get('/fix/routines', (_req: Request, res: Response) => {
|
|
try {
|
|
const routines = autoFix.getAvailableRoutines();
|
|
res.json(routines);
|
|
} catch (error) {
|
|
console.error('[System] Get routines error:', error);
|
|
res.status(500).json({ error: 'Failed to get routines' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/fix/:routine
|
|
* Run a fix routine
|
|
*/
|
|
router.post('/fix/:routine', async (req: Request, res: Response) => {
|
|
try {
|
|
const routineName = req.params.routine;
|
|
const dryRun = req.body.dryRun === true;
|
|
const triggeredBy = req.body.triggeredBy || 'api';
|
|
|
|
const result = await autoFix.runRoutine(routineName as any, triggeredBy, { dryRun });
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[System] Fix routine error:', error);
|
|
res.status(500).json({ error: 'Failed to run fix routine' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/fix/runs
|
|
* Get recent fix runs
|
|
*/
|
|
router.get('/fix/runs', async (req: Request, res: Response) => {
|
|
try {
|
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20;
|
|
const runs = await autoFix.getRecentRuns(limit);
|
|
res.json(runs);
|
|
} catch (error) {
|
|
console.error('[System] Fix runs error:', error);
|
|
res.status(500).json({ error: 'Failed to get fix runs' });
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// ALERTS ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /api/system/alerts
|
|
* List alerts
|
|
*/
|
|
router.get('/alerts', async (req: Request, res: Response) => {
|
|
try {
|
|
const options = {
|
|
status: req.query.status as any,
|
|
severity: req.query.severity as any,
|
|
type: req.query.type as string,
|
|
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
|
|
offset: req.query.offset ? parseInt(req.query.offset as string) : 0,
|
|
};
|
|
|
|
const result = await alerts.listAlerts(options);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('[System] Alerts list error:', error);
|
|
res.status(500).json({ error: 'Failed to list alerts' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/alerts/active
|
|
* Get active alerts
|
|
*/
|
|
router.get('/alerts/active', async (_req: Request, res: Response) => {
|
|
try {
|
|
const activeAlerts = await alerts.getActiveAlerts();
|
|
res.json(activeAlerts);
|
|
} catch (error) {
|
|
console.error('[System] Active alerts error:', error);
|
|
res.status(500).json({ error: 'Failed to get active alerts' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/alerts/summary
|
|
* Get alert summary
|
|
*/
|
|
router.get('/alerts/summary', async (_req: Request, res: Response) => {
|
|
try {
|
|
const summary = await alerts.getSummary();
|
|
res.json(summary);
|
|
} catch (error) {
|
|
console.error('[System] Alerts summary error:', error);
|
|
res.status(500).json({ error: 'Failed to get alerts summary' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/alerts/:id/acknowledge
|
|
* Acknowledge an alert
|
|
*/
|
|
router.post('/alerts/:id/acknowledge', async (req: Request, res: Response) => {
|
|
try {
|
|
const alertId = parseInt(req.params.id);
|
|
const acknowledgedBy = req.body.acknowledgedBy || 'api';
|
|
const success = await alerts.acknowledgeAlert(alertId, acknowledgedBy);
|
|
res.json({ success });
|
|
} catch (error) {
|
|
console.error('[System] Acknowledge alert error:', error);
|
|
res.status(500).json({ error: 'Failed to acknowledge alert' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/alerts/:id/resolve
|
|
* Resolve an alert
|
|
*/
|
|
router.post('/alerts/:id/resolve', async (req: Request, res: Response) => {
|
|
try {
|
|
const alertId = parseInt(req.params.id);
|
|
const resolvedBy = req.body.resolvedBy || 'api';
|
|
const success = await alerts.resolveAlert(alertId, resolvedBy);
|
|
res.json({ success });
|
|
} catch (error) {
|
|
console.error('[System] Resolve alert error:', error);
|
|
res.status(500).json({ error: 'Failed to resolve alert' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/alerts/bulk-acknowledge
|
|
* Bulk acknowledge alerts
|
|
*/
|
|
router.post('/alerts/bulk-acknowledge', async (req: Request, res: Response) => {
|
|
try {
|
|
const { ids, acknowledgedBy } = req.body;
|
|
if (!ids || !Array.isArray(ids)) {
|
|
return res.status(400).json({ error: 'ids array is required' });
|
|
}
|
|
const count = await alerts.bulkAcknowledge(ids, acknowledgedBy || 'api');
|
|
res.json({ acknowledged: count });
|
|
} catch (error) {
|
|
console.error('[System] Bulk acknowledge error:', error);
|
|
res.status(500).json({ error: 'Failed to bulk acknowledge' });
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// METRICS ENDPOINTS
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /api/system/metrics
|
|
* Get all current metrics
|
|
*/
|
|
router.get('/metrics', async (_req: Request, res: Response) => {
|
|
try {
|
|
const allMetrics = await metrics.getAllMetrics();
|
|
res.json(allMetrics);
|
|
} catch (error) {
|
|
console.error('[System] Metrics error:', error);
|
|
res.status(500).json({ error: 'Failed to get metrics' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/metrics/:name
|
|
* Get a specific metric
|
|
*/
|
|
router.get('/metrics/:name', async (req: Request, res: Response) => {
|
|
try {
|
|
const metric = await metrics.getMetric(req.params.name);
|
|
if (!metric) {
|
|
return res.status(404).json({ error: 'Metric not found' });
|
|
}
|
|
res.json(metric);
|
|
} catch (error) {
|
|
console.error('[System] Metric error:', error);
|
|
res.status(500).json({ error: 'Failed to get metric' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/metrics/:name/history
|
|
* Get metric time series
|
|
*/
|
|
router.get('/metrics/:name/history', async (req: Request, res: Response) => {
|
|
try {
|
|
const hours = req.query.hours ? parseInt(req.query.hours as string) : 24;
|
|
const history = await metrics.getMetricHistory(req.params.name, hours);
|
|
res.json(history);
|
|
} catch (error) {
|
|
console.error('[System] Metric history error:', error);
|
|
res.status(500).json({ error: 'Failed to get metric history' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/errors
|
|
* Get error summary
|
|
*/
|
|
router.get('/errors', async (_req: Request, res: Response) => {
|
|
try {
|
|
const summary = await metrics.getErrorSummary();
|
|
res.json(summary);
|
|
} catch (error) {
|
|
console.error('[System] Error summary error:', error);
|
|
res.status(500).json({ error: 'Failed to get error summary' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/system/errors/recent
|
|
* Get recent errors
|
|
*/
|
|
router.get('/errors/recent', async (req: Request, res: Response) => {
|
|
try {
|
|
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
|
|
const errorType = req.query.type as string;
|
|
const errors = await metrics.getRecentErrors(limit, errorType);
|
|
res.json(errors);
|
|
} catch (error) {
|
|
console.error('[System] Recent errors error:', error);
|
|
res.status(500).json({ error: 'Failed to get recent errors' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/system/errors/acknowledge
|
|
* Acknowledge errors
|
|
*/
|
|
router.post('/errors/acknowledge', async (req: Request, res: Response) => {
|
|
try {
|
|
const { ids, acknowledgedBy } = req.body;
|
|
if (!ids || !Array.isArray(ids)) {
|
|
return res.status(400).json({ error: 'ids array is required' });
|
|
}
|
|
const count = await metrics.acknowledgeErrors(ids, acknowledgedBy || 'api');
|
|
res.json({ acknowledged: count });
|
|
} catch (error) {
|
|
console.error('[System] Acknowledge errors error:', error);
|
|
res.status(500).json({ error: 'Failed to acknowledge errors' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
/**
|
|
* Create Prometheus metrics endpoint (standalone)
|
|
*/
|
|
export function createPrometheusRouter(pool: Pool): Router {
|
|
const router = Router();
|
|
const metrics = new MetricsService(pool);
|
|
|
|
/**
|
|
* GET /metrics
|
|
* Prometheus-compatible metrics endpoint
|
|
*/
|
|
router.get('/', async (_req: Request, res: Response) => {
|
|
try {
|
|
const prometheusOutput = await metrics.getPrometheusMetrics();
|
|
res.set('Content-Type', 'text/plain; version=0.0.4');
|
|
res.send(prometheusOutput);
|
|
} catch (error) {
|
|
console.error('[Prometheus] Metrics error:', error);
|
|
res.status(500).send('# Error generating metrics');
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|