feat: overhaul Duriin autonomy console
This commit is contained in:
+97
-8
@@ -54,6 +54,7 @@ const pagesDir = path.join(publicDir, 'pages');
|
||||
// map pretty url → page html file. keep these close to the routes so its
|
||||
// obvious when a page gets added or renamed.
|
||||
const pageMap = {
|
||||
'/admin/autonomy': path.join(pagesDir, 'autonomy.html'),
|
||||
'/admin/ingest/articles': path.join(pagesDir, 'ingest', 'articles.html'),
|
||||
'/admin/ingest/events': path.join(pagesDir, 'ingest', 'events.html'),
|
||||
'/admin/stats': path.join(pagesDir, 'stats.html'),
|
||||
@@ -77,21 +78,19 @@ async function adminRoutes(fastify) {
|
||||
if (!checkAuth(request, reply)) return reply;
|
||||
});
|
||||
|
||||
// static assets (css + js) under /admin/assets/*
|
||||
// cache for an hour — avoids per-navigation revalidation round-trips
|
||||
// for the admin panel. during dev use a hard-reload (cmd-shift-r)
|
||||
// or bump the script src query string to bust it.
|
||||
// Static assets are revalidated so an operator never runs stale UI code
|
||||
// against a newly deployed autonomy API.
|
||||
fastify.register(fastifyStatic, {
|
||||
root: assetsDir,
|
||||
prefix: '/admin/assets/',
|
||||
decorateReply: false,
|
||||
cacheControl: true,
|
||||
maxAge: 3600 * 1000, // 1h, in ms (fastify-static forwards to send())
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
// top-level entry — redirect into ingest/articles
|
||||
// top-level entry — the autonomy control room is the primary product surface
|
||||
fastify.get('/admin', async (request, reply) => {
|
||||
reply.redirect('/admin/ingest/articles');
|
||||
reply.redirect('/admin/autonomy');
|
||||
});
|
||||
|
||||
// ingest root — redirect to the articles subsection
|
||||
@@ -113,6 +112,96 @@ async function adminRoutes(fastify) {
|
||||
fastify.get(route, async (request, reply) => sendPage(reply, filePath));
|
||||
}
|
||||
|
||||
// Autonomy control-room data. Keep this behind admin auth: it exposes model
|
||||
// output, broker state and operational queue details that do not belong on the
|
||||
// public status endpoint.
|
||||
fastify.get('/admin/api/autonomy/overview', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
const intelligenceDb = getIntelligenceDb();
|
||||
if (!intelligenceDb) return { enabled: false, reason: 'intelligence database unavailable' };
|
||||
const hasSchema = intelligenceDb.prepare(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='autonomy_jobs'"
|
||||
).get();
|
||||
if (!hasSchema) return { enabled: false, reason: 'autonomy schema is not initialized' };
|
||||
|
||||
const jobs = intelligenceDb.prepare(`
|
||||
SELECT lane, status, COUNT(*) AS count
|
||||
FROM autonomy_jobs GROUP BY lane, status ORDER BY lane, status
|
||||
`).all();
|
||||
const predictionCounts = intelligenceDb.prepare(`
|
||||
SELECT status, COUNT(*) AS count FROM autonomy_predictions GROUP BY status
|
||||
`).all();
|
||||
const decisionCounts = intelligenceDb.prepare(`
|
||||
SELECT action, COUNT(*) AS count FROM autonomy_decisions GROUP BY action
|
||||
`).all();
|
||||
const proposalCounts = intelligenceDb.prepare(`
|
||||
SELECT status, COUNT(*) AS count FROM autonomy_proposals GROUP BY status
|
||||
`).all();
|
||||
const outcomeSummary = intelligenceDb.prepare(`
|
||||
SELECT COUNT(*) AS total, SUM(direction_correct) AS correct,
|
||||
AVG(excess_return) AS average_excess_return
|
||||
FROM autonomy_outcomes
|
||||
`).get();
|
||||
const instruments = intelligenceDb.prepare(`
|
||||
SELECT COUNT(*) AS count FROM autonomy_instruments WHERE active=1 AND tradable=1
|
||||
`).get();
|
||||
const latestPredictions = intelligenceDb.prepare(`
|
||||
SELECT p.id, p.instrument, p.direction, p.event_type, p.causal_channel,
|
||||
p.horizon_days, p.information_cutoff, p.evidence_article_ids,
|
||||
p.invalidation_condition, p.learning_eligible, p.status, p.created_at,
|
||||
d.action, d.calibrated_probability, d.expected_excess_return, d.rationale,
|
||||
o.excess_return, o.direction_correct
|
||||
FROM autonomy_predictions p
|
||||
LEFT JOIN autonomy_decisions d ON d.id = (
|
||||
SELECT MAX(d2.id) FROM autonomy_decisions d2 WHERE d2.prediction_id = p.id
|
||||
)
|
||||
LEFT JOIN autonomy_outcomes o ON o.prediction_id = p.id
|
||||
ORDER BY p.id DESC LIMIT 12
|
||||
`).all().map((row) => {
|
||||
let evidenceCount = 0;
|
||||
try { evidenceCount = JSON.parse(row.evidence_article_ids || '[]').length; } catch (_) {}
|
||||
const { evidence_article_ids: ignored, ...safeRow } = row;
|
||||
return { ...safeRow, evidence_count: evidenceCount };
|
||||
});
|
||||
const latestOrders = intelligenceDb.prepare(`
|
||||
SELECT oi.id, oi.client_order_id, oi.instrument, oi.side, oi.notional,
|
||||
oi.status, oi.broker_order_id, oi.attempts, oi.last_error,
|
||||
oi.created_at, oi.updated_at, d.action
|
||||
FROM autonomy_order_intents oi
|
||||
JOIN autonomy_decisions d ON d.id = oi.decision_id
|
||||
ORDER BY oi.id DESC LIMIT 12
|
||||
`).all();
|
||||
const account = intelligenceDb.prepare(`
|
||||
SELECT broker, equity, cash, buying_power, captured_at
|
||||
FROM autonomy_account_snapshots ORDER BY id DESC LIMIT 1
|
||||
`).get() || null;
|
||||
const calibration = intelligenceDb.prepare(`
|
||||
SELECT cohort_key, sample_size, effective_sample_size, directional_probability,
|
||||
expected_excess_return, lower_return, upper_return, created_at
|
||||
FROM autonomy_calibration_snapshots ORDER BY id DESC LIMIT 8
|
||||
`).all();
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
mode: process.env.AUTONOMY_EXECUTION_MODE || 'shadow',
|
||||
broker: {
|
||||
name: 'Alpaca Paper',
|
||||
configured: Boolean(process.env.ALPACA_PAPER_KEY_ID && process.env.ALPACA_PAPER_SECRET_KEY),
|
||||
},
|
||||
jobs,
|
||||
predictionCounts,
|
||||
decisionCounts,
|
||||
proposalCounts,
|
||||
outcomes: outcomeSummary,
|
||||
allowlistedInstruments: instruments.count,
|
||||
latestPredictions,
|
||||
latestOrders,
|
||||
account,
|
||||
calibration,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
// list articles — all of them, not just the ones with embeddings
|
||||
fastify.get('/admin/api/articles', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
@@ -779,4 +868,4 @@ async function adminRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = adminRoutes;
|
||||
module.exports = adminRoutes;
|
||||
|
||||
Reference in New Issue
Block a user