add signal generation feature; implement signals view in admin panel

This commit is contained in:
ImBenji
2026-04-23 22:00:36 +01:00
parent 53058ab94d
commit 9d89dc95e4
5 changed files with 1009 additions and 20 deletions
+114
View File
@@ -364,6 +364,37 @@ async function adminRoutes(fastify) {
});
// trade signals
fastify.get('/admin/api/intelligence/signals', async (request, reply) => {
if (!checkAuth(request, reply)) return;
const db = getIntelligenceDb();
if (!db) return [];
let rows;
try {
rows = db.prepare(`
SELECT ts.*, tc.name as company_name, tc.ticker
FROM trade_signals ts
JOIN tracked_companies tc ON tc.id = ts.company_id
ORDER BY ts.generated_at DESC
`).all();
} catch (_) {
return [];
}
return rows;
});
fastify.delete('/admin/api/intelligence/signals/:company_id', async (request, reply) => {
if (!checkAuth(request, reply)) return;
const db = getIntelligenceDb();
if (!db) { reply.code(503); return { error: 'intelligence db unavailable' }; }
const companyId = parseInt(request.params.company_id, 10);
db.prepare('DELETE FROM trade_signals WHERE company_id = ?').run(companyId);
return { ok: true };
});
// per-company facts for the graph sidebar
fastify.get('/admin/api/intelligence/facts/:company_id', async (request, reply) => {
if (!checkAuth(request, reply)) return;
@@ -381,6 +412,89 @@ async function adminRoutes(fastify) {
});
// evidence backing a single graph edge — the relationship row itself,
// the consolidated facts that produced it, and the source events+articles.
fastify.get('/admin/api/intelligence/edge-evidence', async (request, reply) => {
if (!checkAuth(request, reply)) return;
const idb = getIntelligenceDb();
if (!idb) return { edge: null, facts: [], events: [] };
const fromId = parseInt(request.query.from_id, 10);
const toId = parseInt(request.query.to_id, 10);
const type = (request.query.type || '').toLowerCase();
if (!fromId || !toId || !type) {
reply.code(400);
return { error: 'missing from_id / to_id / type' };
}
// direct edge — try the exact match first
let edge = idb.prepare(`
SELECT * FROM company_relationships
WHERE from_company_id = ? AND to_company_id = ? AND relationship_type = ?
`).get(fromId, toId, type);
// investor edges are stored as 'dependency' in the reverse direction too —
// the frontend normalizes dependency→investor, so lookup the flipped row
if (!edge && type === 'investor') {
edge = idb.prepare(`
SELECT * FROM company_relationships
WHERE from_company_id = ? AND to_company_id = ? AND relationship_type = 'dependency'
`).get(toId, fromId);
}
const toCompany = idb.prepare(`SELECT name FROM tracked_companies WHERE id = ?`).get(toId);
const toName = toCompany ? toCompany.name : null;
// backing facts — relationship-type facts on the source company that mention the target
let facts = [];
if (toName) {
facts = idb.prepare(`
SELECT id, claim, confidence, confirmation_count, supporting_event_ids, first_seen_at, last_seen_at
FROM company_facts
WHERE company_id = ? AND type = 'relationship' AND claim LIKE ?
ORDER BY confirmation_count DESC
LIMIT 20
`).all(fromId, `%${toName}%`);
}
// merge event ids from the edge row + all backing facts
const eventIds = new Set();
if (edge && edge.supporting_event_ids) {
try { JSON.parse(edge.supporting_event_ids).forEach(id => eventIds.add(id)); } catch (_) {}
}
for (const f of facts) {
try { JSON.parse(f.supporting_event_ids || '[]').forEach(id => eventIds.add(id)); } catch (_) {}
}
// resolve events + articles from archive.sqlite
const events = [];
if (eventIds.size > 0) {
const ids = [...eventIds];
const placeholders = ids.map(() => '?').join(',');
const eventRows = db.prepare(`
SELECT id, title, created_at FROM events
WHERE id IN (${placeholders})
ORDER BY created_at DESC
`).all(...ids);
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 8
`);
for (const ev of eventRows) {
events.push({ ...ev, articles: artStmt.all(ev.id) });
}
}
return { edge, facts, events };
});
// raw sql console — supports multiple statements separated by ;
fastify.post('/admin/api/sql', async (request, reply) => {
if (!checkAuth(request, reply)) return;