docs: enterprise refactor plan with ralph specs
This commit is contained in:
93
dist/server/routes/anomalies.js
vendored
Normal file
93
dist/server/routes/anomalies.js
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAnomalyRouter = createAnomalyRouter;
|
||||
const express_1 = require("express");
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
function createAnomalyRouter(store, enrichmentService) {
|
||||
const router = (0, express_1.Router)();
|
||||
// GET /api/anomalies — list all anomalies (optionally filtered)
|
||||
router.get('/', (req, res) => {
|
||||
const sessionId = req.query['sessionId'];
|
||||
const severity = req.query['severity'];
|
||||
const anomalies = store.getAllAnomalies(sessionId, severity);
|
||||
const mapped = anomalies.map((a) => ({
|
||||
id: a.id,
|
||||
sessionId: store.findSessionForAnomaly(a.id),
|
||||
type: a.type,
|
||||
severity: a.severity,
|
||||
description: a.description,
|
||||
timestamp: a.timestamp,
|
||||
screenshotUrl: a.evidence.screenshotPath
|
||||
? `/api/anomalies/${a.id}/screenshot`
|
||||
: undefined,
|
||||
}));
|
||||
res.json(mapped);
|
||||
});
|
||||
// GET /api/anomalies/:anomalyId — full anomaly detail
|
||||
router.get('/:anomalyId', (req, res) => {
|
||||
const anomalyId = req.params['anomalyId'];
|
||||
const anomaly = store.getAnomaly(anomalyId);
|
||||
if (!anomaly) {
|
||||
res.status(404).json({ error: 'Anomaly not found' });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
...anomaly,
|
||||
sessionId: store.findSessionForAnomaly(anomaly.id),
|
||||
screenshotUrl: anomaly.evidence.screenshotPath
|
||||
? `/api/anomalies/${anomaly.id}/screenshot`
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
// GET /api/anomalies/:anomalyId/screenshot — serve PNG
|
||||
router.get('/:anomalyId/screenshot', (req, res) => {
|
||||
const anomalyId = req.params['anomalyId'];
|
||||
const filePath = store.screenshotPath(anomalyId);
|
||||
if (!filePath || !fs_1.default.existsSync(filePath)) {
|
||||
res.status(404).json({ error: 'Screenshot not found' });
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
fs_1.default.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
// POST /api/anomalies/:anomalyId/replay — trigger replay
|
||||
router.post('/:anomalyId/replay', async (req, res) => {
|
||||
const anomalyId = req.params['anomalyId'];
|
||||
try {
|
||||
const replayId = await store.replayAnomaly(anomalyId);
|
||||
res.json({ replayId, status: 'running' });
|
||||
}
|
||||
catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
res.status(404).json({ error: msg });
|
||||
}
|
||||
});
|
||||
// POST /api/anomalies/:anomalyId/enrich — AI enrichment
|
||||
router.post('/:anomalyId/enrich', async (req, res) => {
|
||||
const anomalyId = req.params['anomalyId'];
|
||||
const anomaly = store.getAnomaly(anomalyId);
|
||||
if (!anomaly) {
|
||||
res.status(404).json({ error: 'Anomaly not found' });
|
||||
return;
|
||||
}
|
||||
if (!enrichmentService?.hasProvider()) {
|
||||
res.status(503).json({ error: 'No AI provider configured (set ABE_AI_PROVIDER)' });
|
||||
return;
|
||||
}
|
||||
const context = {
|
||||
domSnapshot: '',
|
||||
httpLog: anomaly.evidence.httpLog ?? [],
|
||||
consoleErrors: anomaly.evidence.rawErrors ?? [],
|
||||
actionTrace: anomaly.actionTrace,
|
||||
pageTitle: '',
|
||||
url: anomaly.actionTrace[anomaly.actionTrace.length - 1]?.url ?? '',
|
||||
};
|
||||
// Run async — emit WS event when done
|
||||
void enrichmentService.enrich(anomaly, context);
|
||||
res.json({ status: 'enriching', anomalyId });
|
||||
});
|
||||
return router;
|
||||
}
|
||||
48
dist/server/routes/config.js
vendored
Normal file
48
dist/server/routes/config.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Config routes — GET /api/config and PATCH /api/config
|
||||
* Manages server-side configuration for notifications and defaults.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getServerConfig = getServerConfig;
|
||||
exports.createConfigRouter = createConfigRouter;
|
||||
const express_1 = require("express");
|
||||
const defaultConfig = {
|
||||
slackWebhookUrl: process.env['ABE_SLACK_WEBHOOK_URL'] ?? null,
|
||||
notifyMinSeverity: process.env['ABE_NOTIFY_MIN_SEVERITY'] ?? 'high',
|
||||
defaultMaxStates: 50,
|
||||
defaultMaxDepth: 5,
|
||||
defaultActionDelayMs: 500,
|
||||
defaultExcludedPaths: [],
|
||||
};
|
||||
let serverConfig = { ...defaultConfig };
|
||||
function getServerConfig() {
|
||||
return { ...serverConfig };
|
||||
}
|
||||
function createConfigRouter() {
|
||||
const router = (0, express_1.Router)();
|
||||
// GET /api/config — returns current config (without API key)
|
||||
router.get('/', (_req, res) => {
|
||||
res.json(serverConfig);
|
||||
});
|
||||
// PATCH /api/config — updates config fields
|
||||
router.patch('/', (req, res) => {
|
||||
const body = req.body;
|
||||
const validKeys = [
|
||||
'slackWebhookUrl',
|
||||
'notifyMinSeverity',
|
||||
'defaultMaxStates',
|
||||
'defaultMaxDepth',
|
||||
'defaultActionDelayMs',
|
||||
'defaultExcludedPaths',
|
||||
];
|
||||
for (const key of validKeys) {
|
||||
if (key in body) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serverConfig[key] = body[key];
|
||||
}
|
||||
}
|
||||
res.json(serverConfig);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
122
dist/server/routes/schedules.js
vendored
Normal file
122
dist/server/routes/schedules.js
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Schedules routes — CRUD for /api/schedules
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createScheduleRouter = createScheduleRouter;
|
||||
const express_1 = require("express");
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const cron = __importStar(require("node-cron"));
|
||||
const SchedulerService_1 = require("../scheduler/SchedulerService");
|
||||
function createScheduleRouter(scheduleRepo, scheduler) {
|
||||
const router = (0, express_1.Router)();
|
||||
// GET /api/schedules
|
||||
router.get('/', (_req, res) => {
|
||||
const schedules = scheduleRepo.findAll();
|
||||
res.json(schedules);
|
||||
});
|
||||
// POST /api/schedules
|
||||
router.post('/', (req, res) => {
|
||||
const { name, url, config, cronExpression, enabled } = req.body;
|
||||
if (!name || !url || !cronExpression) {
|
||||
res.status(400).json({ error: 'name, url, and cronExpression are required' });
|
||||
return;
|
||||
}
|
||||
if (!cron.validate(cronExpression)) {
|
||||
res.status(400).json({ error: 'Invalid cron expression' });
|
||||
return;
|
||||
}
|
||||
const id = crypto.randomUUID();
|
||||
const nextRunAt = SchedulerService_1.SchedulerService.computeNextRunAt(cronExpression);
|
||||
scheduleRepo.create({
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
configJson: JSON.stringify(config ?? {}),
|
||||
cronExpression,
|
||||
enabled: enabled !== false,
|
||||
nextRunAt: nextRunAt ?? undefined,
|
||||
});
|
||||
const record = scheduleRepo.findById(id);
|
||||
if (record.enabled) {
|
||||
scheduler.register(record);
|
||||
}
|
||||
res.status(201).json(record);
|
||||
});
|
||||
// PATCH /api/schedules/:id
|
||||
router.patch('/:id', (req, res) => {
|
||||
const id = String(req.params['id']);
|
||||
const existing = scheduleRepo.findById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: 'Schedule not found' });
|
||||
return;
|
||||
}
|
||||
const { name, url, config, cronExpression, enabled } = req.body;
|
||||
if (cronExpression !== undefined && !cron.validate(cronExpression)) {
|
||||
res.status(400).json({ error: 'Invalid cron expression' });
|
||||
return;
|
||||
}
|
||||
scheduleRepo.update(id, {
|
||||
name,
|
||||
url,
|
||||
configJson: config !== undefined ? JSON.stringify(config) : undefined,
|
||||
cronExpression,
|
||||
enabled,
|
||||
});
|
||||
const updated = scheduleRepo.findById(id);
|
||||
// Re-register/unregister cron job
|
||||
if (updated.enabled) {
|
||||
scheduler.register(updated);
|
||||
}
|
||||
else {
|
||||
scheduler.unregister(id);
|
||||
}
|
||||
res.json(updated);
|
||||
});
|
||||
// DELETE /api/schedules/:id
|
||||
router.delete('/:id', (req, res) => {
|
||||
const id = String(req.params['id']);
|
||||
const existing = scheduleRepo.findById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: 'Schedule not found' });
|
||||
return;
|
||||
}
|
||||
scheduler.unregister(String(id));
|
||||
scheduleRepo.delete(String(id));
|
||||
res.status(204).send();
|
||||
});
|
||||
return router;
|
||||
}
|
||||
104
dist/server/routes/sessions.js
vendored
Normal file
104
dist/server/routes/sessions.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createSessionRouter = createSessionRouter;
|
||||
const express_1 = require("express");
|
||||
const ExplorationConfig_1 = require("../../core/ExplorationConfig");
|
||||
function createSessionRouter(store) {
|
||||
const router = (0, express_1.Router)();
|
||||
// POST /api/sessions — start a new exploration
|
||||
router.post('/', async (req, res) => {
|
||||
const body = req.body;
|
||||
const { url, seed = 42 } = body;
|
||||
if (!url || typeof url !== 'string') {
|
||||
res.status(400).json({ error: 'url is required' });
|
||||
return;
|
||||
}
|
||||
// Enforce concurrent session limit
|
||||
const stats = store.getStats();
|
||||
const limit = store.getMaxConcurrent();
|
||||
if (stats.runningSessions >= limit) {
|
||||
res.status(429).json({
|
||||
error: 'Max concurrent sessions reached',
|
||||
active: stats.runningSessions,
|
||||
limit,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const config = {
|
||||
...ExplorationConfig_1.DEFAULT_EXPLORATION_CONFIG,
|
||||
...(body.config ?? {}),
|
||||
};
|
||||
// If allowedDomains not specified, derive from the target URL
|
||||
if (config.allowedDomains.length === 0) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
config.allowedDomains = [hostname];
|
||||
}
|
||||
catch {
|
||||
// leave empty
|
||||
}
|
||||
}
|
||||
const record = await store.startSession({
|
||||
url,
|
||||
seed,
|
||||
maxStates: config.maxStates,
|
||||
explorationConfig: config,
|
||||
});
|
||||
res.status(201).json({
|
||||
sessionId: record.sessionId,
|
||||
status: record.status,
|
||||
startedAt: record.startedAt,
|
||||
});
|
||||
});
|
||||
// GET /api/sessions — list all sessions
|
||||
router.get('/', (_req, res) => {
|
||||
const sessions = store.getAllSessions().map((s) => ({
|
||||
sessionId: s.sessionId,
|
||||
url: s.url,
|
||||
status: s.status,
|
||||
startedAt: s.startedAt,
|
||||
anomaliesFound: s.anomaliesFound,
|
||||
statesVisited: s.statesVisited,
|
||||
}));
|
||||
res.json(sessions);
|
||||
});
|
||||
// GET /api/sessions/:sessionId — session detail
|
||||
router.get('/:sessionId', (req, res) => {
|
||||
const record = store.getSession(req.params['sessionId']);
|
||||
if (!record) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
sessionId: record.sessionId,
|
||||
url: record.url,
|
||||
status: record.status,
|
||||
startedAt: record.startedAt,
|
||||
finishedAt: record.finishedAt,
|
||||
statesVisited: record.statesVisited,
|
||||
anomaliesFound: record.anomaliesFound,
|
||||
seed: record.seed,
|
||||
});
|
||||
});
|
||||
// DELETE /api/sessions/:sessionId — stop an active session
|
||||
router.delete('/:sessionId', (req, res) => {
|
||||
const stopped = store.stopSession(req.params['sessionId']);
|
||||
if (!stopped) {
|
||||
res.status(404).json({ error: 'Session not found or not running' });
|
||||
return;
|
||||
}
|
||||
res.json({ stopped: true });
|
||||
});
|
||||
// GET /api/sessions/:sessionId/performance — performance metrics for session
|
||||
router.get('/:sessionId/performance', (req, res) => {
|
||||
const sessionId = req.params['sessionId'];
|
||||
const record = store.getSession(sessionId);
|
||||
if (!record) {
|
||||
res.status(404).json({ error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
const metrics = store.getPerformanceMetrics(sessionId);
|
||||
res.json(metrics);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
52
dist/server/routes/visual.js
vendored
Normal file
52
dist/server/routes/visual.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Visual regression routes — /api/visual
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createVisualRouter = createVisualRouter;
|
||||
const express_1 = require("express");
|
||||
function createVisualRouter(repo) {
|
||||
const router = (0, express_1.Router)();
|
||||
// GET /api/visual/comparisons
|
||||
router.get('/comparisons', (req, res) => {
|
||||
const sessionId = req.query['sessionId'];
|
||||
const status = req.query['status'];
|
||||
const comparisons = repo.findComparisons({ sessionId, status });
|
||||
res.json(comparisons);
|
||||
});
|
||||
// POST /api/visual/baselines/:comparisonId/approve
|
||||
router.post('/baselines/:comparisonId/approve', (req, res) => {
|
||||
const comparisonId = String(req.params['comparisonId']);
|
||||
const comparison = repo.findComparisonById(comparisonId);
|
||||
if (!comparison) {
|
||||
res.status(404).json({ error: 'Comparison not found' });
|
||||
return;
|
||||
}
|
||||
const baselineId = repo.promoteToBaseline(comparisonId);
|
||||
res.json({ baselineId, status: 'approved' });
|
||||
});
|
||||
// POST /api/visual/baselines/:comparisonId/reject
|
||||
router.post('/baselines/:comparisonId/reject', (req, res) => {
|
||||
const comparisonId = String(req.params['comparisonId']);
|
||||
const comparison = repo.findComparisonById(comparisonId);
|
||||
if (!comparison) {
|
||||
res.status(404).json({ error: 'Comparison not found' });
|
||||
return;
|
||||
}
|
||||
repo.updateComparisonStatus(comparisonId, 'failed');
|
||||
res.json({ status: 'rejected' });
|
||||
});
|
||||
// POST /api/visual/baselines/approve-all
|
||||
router.post('/baselines/approve-all', (req, res) => {
|
||||
const { sessionId } = req.body;
|
||||
const pending = repo.findComparisons({ sessionId, status: 'new_state' });
|
||||
const approved = [];
|
||||
for (const comp of pending) {
|
||||
const id = repo.promoteToBaseline(comp.id);
|
||||
if (id)
|
||||
approved.push(id);
|
||||
}
|
||||
res.json({ approved: approved.length });
|
||||
});
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user