refactor admin navigation; update links to ingest pages and improve loading of data in parallel
This commit is contained in:
+98
-7
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user