refactor admin navigation; update links to ingest pages and improve loading of data in parallel

This commit is contained in:
ImBenji
2026-04-24 00:08:52 +01:00
parent 16cd61fdf5
commit fc7ea464b3
17 changed files with 292 additions and 67 deletions
+98 -7
View File
@@ -54,8 +54,8 @@ 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/articles': path.join(pagesDir, 'articles.html'),
'/admin/events': path.join(pagesDir, 'events.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'),
'/admin/sql': path.join(pagesDir, 'sql.html'),
'/admin/intelligence/knowledge': path.join(pagesDir, 'intelligence', 'knowledge.html'),
@@ -78,19 +78,25 @@ async function adminRoutes(fastify) {
});
// static assets (css + js) under /admin/assets/*
// use must-revalidate so the browser always checks for a newer copy;
// prevents the admin getting stuck on old js after a deploy.
// 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.
fastify.register(fastifyStatic, {
root: assetsDir,
prefix: '/admin/assets/',
decorateReply: false,
cacheControl: true,
maxAge: 0,
maxAge: 3600 * 1000, // 1h, in ms (fastify-static forwards to send())
});
// top-level entry — redirect to articles
// top-level entry — redirect into ingest/articles
fastify.get('/admin', async (request, reply) => {
reply.redirect('/admin/articles');
reply.redirect('/admin/ingest/articles');
});
// ingest root — redirect to the articles subsection
fastify.get('/admin/ingest', async (request, reply) => {
reply.redirect('/admin/ingest/articles');
});
// intelligence root — redirect to the knowledge subsection
@@ -98,6 +104,10 @@ async function adminRoutes(fastify) {
reply.redirect('/admin/intelligence/knowledge');
});
// backward-compat redirects from the pre-merge urls
fastify.get('/admin/articles', async (request, reply) => reply.redirect('/admin/ingest/articles'));
fastify.get('/admin/events', async (request, reply) => reply.redirect('/admin/ingest/events'));
// wire up each pretty page path
for (const [route, filePath] of Object.entries(pageMap)) {
fastify.get(route, async (request, reply) => sendPage(reply, filePath));
@@ -478,6 +488,23 @@ async function adminRoutes(fastify) {
return [];
}
// pick the most recent event_date out of the predictions that fed each signal
const latestEventDateStmt = db.prepare(`
SELECT MAX(event_date) as latest
FROM event_predictions
WHERE id IN (SELECT value FROM json_each(?))
AND event_date IS NOT NULL
`);
for (const row of rows) {
let latest = null;
try {
const res = latestEventDateStmt.get(row.supporting_prediction_ids || "[]");
latest = res?.latest || null;
} catch (_) {}
row.latest_event_date = latest;
}
return rows;
});
@@ -491,6 +518,70 @@ async function adminRoutes(fastify) {
return { ok: true };
});
// references behind a signal — walks predictions → events → articles
// so the frontend can show the actual sources that fed the signal
fastify.get('/admin/api/intelligence/signals/:company_id/references', async (request, reply) => {
if (!checkAuth(request, reply)) return;
const idb = getIntelligenceDb();
if (!idb) return { events: [] };
const companyId = parseInt(request.params.company_id, 10);
const signal = idb.prepare(`
SELECT supporting_prediction_ids FROM trade_signals WHERE company_id = ?
`).get(companyId);
if (!signal) return { events: [] };
let predIds = [];
try { predIds = JSON.parse(signal.supporting_prediction_ids || "[]"); } catch (_) {}
if (!predIds.length) return { events: [] };
const placeholders = predIds.map(() => '?').join(',');
// pull the event_ids (and keep the newest event_date per event) from the
// predictions that fed the signal
const predRows = idb.prepare(`
SELECT event_id, MAX(event_date) as event_date
FROM event_predictions
WHERE id IN (${placeholders})
GROUP BY event_id
`).all(...predIds);
const eventMeta = new Map();
for (const p of predRows) {
if (p.event_id != null) eventMeta.set(p.event_id, p.event_date || null);
}
if (eventMeta.size === 0) return { events: [] };
const eventIds = [...eventMeta.keys()];
const eventPh = eventIds.map(() => '?').join(',');
const eventRows = db.prepare(`
SELECT id, title, created_at FROM events
WHERE id IN (${eventPh})
ORDER BY created_at DESC
`).all(...eventIds);
const artStmt = db.prepare(`
SELECT id, title, source, pub_date, url
FROM articles
WHERE event_id = ?
ORDER BY COALESCE(pub_date, ingested_at) DESC
LIMIT 5
`);
const events = eventRows.map(ev => ({
...ev,
event_date: eventMeta.get(ev.id) || null,
articles: artStmt.all(ev.id),
}));
return { events };
});
// per-company facts for the graph sidebar
fastify.get('/admin/api/intelligence/facts/:company_id', async (request, reply) => {
if (!checkAuth(request, reply)) return;