add signal generation feature; implement signals view in admin panel
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user