add intelligence and SQL tabs to admin interface with corresponding API endpoints
This commit is contained in:
@@ -2,6 +2,24 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('../db');
|
||||
const config = require('../config');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
let idb = null;
|
||||
|
||||
function getIntelligenceDb() {
|
||||
if (idb) return idb;
|
||||
|
||||
const configDir = path.resolve(__dirname, '..', '..');
|
||||
const rawPath = process.env.INTELLIGENCE_DB
|
||||
|| (config.intelligence_db
|
||||
? (path.isAbsolute(config.intelligence_db) ? config.intelligence_db : path.resolve(configDir, config.intelligence_db))
|
||||
: path.resolve(configDir, 'intelligence.sqlite'));
|
||||
|
||||
if (!fs.existsSync(rawPath)) return null;
|
||||
|
||||
idb = new Database(rawPath, { readonly: true });
|
||||
return idb;
|
||||
}
|
||||
|
||||
const adminUser = (config.admin && config.admin.username) || 'admin';
|
||||
const adminPass = (config.admin && config.admin.password) || 'changeme';
|
||||
@@ -204,6 +222,122 @@ async function adminRoutes(fastify) {
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// intelligence endpoints
|
||||
fastify.get('/admin/api/intelligence/stats', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
const db = getIntelligenceDb();
|
||||
if (!db) return { available: false };
|
||||
|
||||
const queue = db.prepare(`SELECT status, COUNT(*) as n FROM article_queue GROUP BY status`).all();
|
||||
const knowledge = db.prepare(`SELECT COUNT(*) as n FROM event_knowledge`).get().n;
|
||||
const predictions = db.prepare(`SELECT COUNT(*) as n FROM event_predictions`).get().n;
|
||||
const companies = db.prepare(`SELECT COUNT(*) as n FROM tracked_companies`).get().n;
|
||||
const embeddings = db.prepare(`SELECT COUNT(*) as n FROM company_embeddings`).get().n;
|
||||
|
||||
return { available: true, queue, knowledge, predictions, companies, embeddings };
|
||||
});
|
||||
|
||||
fastify.get('/admin/api/intelligence/companies', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
const db = getIntelligenceDb();
|
||||
if (!db) return [];
|
||||
return db.prepare(`SELECT * FROM tracked_companies ORDER BY name`).all();
|
||||
});
|
||||
|
||||
fastify.get('/admin/api/intelligence/knowledge', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
const db = getIntelligenceDb();
|
||||
if (!db) return { total: 0, rows: [] };
|
||||
|
||||
const q = request.query || {};
|
||||
const limit = Math.min(parseInt(q.limit, 10) || 50, 200);
|
||||
const offset = parseInt(q.offset, 10) || 0;
|
||||
const companyId = q.company_id ? parseInt(q.company_id, 10) : null;
|
||||
const type = q.type || null;
|
||||
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
if (companyId) { conditions.push('ek.company_id = ?'); params.push(companyId); }
|
||||
if (type) { conditions.push('ek.type = ?'); params.push(type); }
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const total = db.prepare(`SELECT COUNT(*) as n FROM event_knowledge ek ${where}`).get(...params).n;
|
||||
const rows = db.prepare(`
|
||||
SELECT ek.id, ek.event_id, ek.type, ek.data, ek.created_at,
|
||||
tc.name as company_name
|
||||
FROM event_knowledge ek
|
||||
JOIN tracked_companies tc ON tc.id = ek.company_id
|
||||
${where}
|
||||
ORDER BY ek.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
|
||||
return { total, rows };
|
||||
});
|
||||
|
||||
fastify.get('/admin/api/intelligence/predictions', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
const db = getIntelligenceDb();
|
||||
if (!db) return { total: 0, rows: [] };
|
||||
|
||||
const q = request.query || {};
|
||||
const limit = Math.min(parseInt(q.limit, 10) || 50, 200);
|
||||
const offset = parseInt(q.offset, 10) || 0;
|
||||
const companyId = q.company_id ? parseInt(q.company_id, 10) : null;
|
||||
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
if (companyId) { conditions.push('ep.company_id = ?'); params.push(companyId); }
|
||||
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const total = db.prepare(`SELECT COUNT(*) as n FROM event_predictions ep ${where}`).get(...params).n;
|
||||
const rows = db.prepare(`
|
||||
SELECT ep.*, tc.name as company_name
|
||||
FROM event_predictions ep
|
||||
JOIN tracked_companies tc ON tc.id = ep.company_id
|
||||
${where}
|
||||
ORDER BY ep.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params, limit, offset);
|
||||
|
||||
return { total, rows };
|
||||
});
|
||||
|
||||
// raw sql console
|
||||
fastify.post('/admin/api/sql', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
|
||||
const { sql, database } = request.body || {};
|
||||
if (!sql || !sql.trim()) { reply.code(400); return { error: 'no sql provided' }; }
|
||||
|
||||
const target = database === 'intelligence' ? getIntelligenceDb() : db;
|
||||
if (!target) { reply.code(400); return { error: 'database not available' }; }
|
||||
|
||||
try {
|
||||
const stmt = target.prepare(sql);
|
||||
const start = Date.now();
|
||||
|
||||
let rows, changes, lastInsertRowid;
|
||||
if (stmt.reader) {
|
||||
rows = stmt.all();
|
||||
} else {
|
||||
const info = stmt.run();
|
||||
changes = info.changes;
|
||||
lastInsertRowid = info.lastInsertRowid;
|
||||
}
|
||||
|
||||
return {
|
||||
rows: rows || null,
|
||||
changes: changes ?? null,
|
||||
lastInsertRowid: lastInsertRowid ?? null,
|
||||
elapsed: Date.now() - start,
|
||||
};
|
||||
} catch (err) {
|
||||
reply.code(400);
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// stats for dashboard header
|
||||
fastify.get('/admin/api/stats', async (request, reply) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
|
||||
Reference in New Issue
Block a user