add intelligence and SQL tabs to admin interface with corresponding API endpoints

This commit is contained in:
ImBenji
2026-04-22 20:50:08 +01:00
parent ac7c87c6cf
commit 18d062fd2d
9 changed files with 428 additions and 228 deletions
+239
View File
@@ -247,6 +247,8 @@
<button class="tab-btn active" data-tab="articles">Articles</button>
<button class="tab-btn" data-tab="events">Events</button>
<button class="tab-btn" data-tab="stats">Stats</button>
<button class="tab-btn" data-tab="intelligence">Intelligence</button>
<button class="tab-btn" data-tab="sql">SQL</button>
</div>
</header>
@@ -320,6 +322,63 @@
</div>
</div>
<!-- Intelligence tab -->
<div id="tab-intelligence" style="display:none">
<div id="intel-unavailable" style="display:none; color:#94a3b8; padding: 24px 0">intelligence.sqlite not found — is the intelligence worker running?</div>
<div id="intel-content">
<div style="display:flex; gap:24px; flex-wrap:wrap; margin-bottom:24px" id="intel-stats-row"></div>
<div style="display:flex; gap:16px; margin-bottom:14px; flex-wrap:wrap; align-items:flex-end">
<label style="display:flex; flex-direction:column; gap:4px; font-size:12px; color:#94a3b8">Company
<select id="i-company"><option value="">All companies</option></select>
</label>
<label style="display:flex; flex-direction:column; gap:4px; font-size:12px; color:#94a3b8">View
<select id="i-view">
<option value="knowledge">Knowledge</option>
<option value="predictions">Predictions</option>
</select>
</label>
<label style="display:flex; flex-direction:column; gap:4px; font-size:12px; color:#94a3b8">Type
<select id="i-type">
<option value="">All types</option>
<option value="relationship">Relationship</option>
<option value="theme">Theme</option>
<option value="factor">Factor</option>
</select>
</label>
<button class="primary" onclick="loadIntelligence()">Filter</button>
</div>
<table>
<thead id="intel-thead"></thead>
<tbody id="intel-tbody"></tbody>
</table>
<div class="pagination">
<button id="iPrevBtn">← Prev</button>
<span id="iPageInfo"></span>
<button id="iNextBtn">Next →</button>
</div>
</div>
</div>
<!-- SQL tab -->
<div id="tab-sql" style="display:none">
<div style="display:flex; gap:10px; margin-bottom:10px; align-items:center">
<select id="sql-db" style="min-width:160px">
<option value="archive">archive.sqlite</option>
<option value="intelligence">intelligence.sqlite</option>
</select>
<button class="primary" id="sql-run-btn">Run</button>
<span id="sql-elapsed" style="color:#64748b; font-size:12px"></span>
</div>
<textarea id="sql-input" style="width:100%; min-height:120px; font-family:monospace; font-size:13px; margin-bottom:12px" placeholder="SELECT ..."></textarea>
<div id="sql-error" style="color:#fca5a5; font-size:13px; margin-bottom:10px; display:none"></div>
<div id="sql-results" style="overflow-x:auto"></div>
</div>
<!-- Stats tab -->
<div id="tab-stats" style="display:none">
<div style="display:flex; gap:32px; flex-wrap:wrap; padding-top:8px">
@@ -653,12 +712,191 @@ document.getElementById('eDeleteBtn').onclick = async () => {
} catch (e) { toast('Delete failed', true); }
};
// ── intelligence ───────────────────────────────────────────────────────────
let intelOffset = 0;
async function loadIntelligenceStats() {
const data = await api('/admin/api/intelligence/stats');
if (!data.available) {
document.getElementById('intel-unavailable').style.display = '';
document.getElementById('intel-content').style.display = 'none';
return false;
}
document.getElementById('intel-unavailable').style.display = 'none';
document.getElementById('intel-content').style.display = '';
const queueMap = {};
(data.queue || []).forEach(r => queueMap[r.status] = r.n);
document.getElementById('intel-stats-row').innerHTML = [
['Queue pending', (queueMap.pending || 0).toLocaleString()],
['Processed', (queueMap.processed || 0).toLocaleString()],
['Skipped', (queueMap.skipped || 0).toLocaleString()],
['Knowledge rows', data.knowledge.toLocaleString()],
['Predictions', data.predictions.toLocaleString()],
['Companies', `${data.embeddings}/${data.companies} embedded`],
].map(([label, value]) => `
<div class="stat">
<span class="label">${label}</span>
<span class="value" style="font-size:16px">${value}</span>
</div>
`).join('');
return true;
}
async function loadIntelligenceCompanies() {
const companies = await api('/admin/api/intelligence/companies');
const sel = document.getElementById('i-company');
sel.innerHTML = '<option value="">All companies</option>';
companies.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = `${c.name} (${c.ticker})`;
sel.appendChild(opt);
});
}
async function loadIntelligence() {
const view = document.getElementById('i-view').value;
const companyId = document.getElementById('i-company').value;
const type = document.getElementById('i-type').value;
const params = new URLSearchParams({ limit: PAGE, offset: intelOffset });
if (companyId) params.set('company_id', companyId);
if (view === 'knowledge') {
document.getElementById('i-type').parentElement.style.display = '';
if (type) params.set('type', type);
const data = await api(`/admin/api/intelligence/knowledge?${params}`);
document.getElementById('intel-thead').innerHTML = `
<tr><th>ID</th><th>Company</th><th>Event</th><th>Type</th><th>Data</th><th>Created</th></tr>`;
document.getElementById('intel-tbody').innerHTML = data.rows.map(r => {
let parsed = {};
try { parsed = JSON.parse(r.data); } catch (_) {}
const summary = Object.values(parsed).filter(v => typeof v === 'string').join(' · ').slice(0, 120);
return `<tr>
<td style="color:#64748b">${r.id}</td>
<td style="white-space:nowrap">${r.company_name}</td>
<td style="color:#64748b">${r.event_id}</td>
<td><span class="badge null">${r.type}</span></td>
<td><span class="truncate" style="max-width:360px" title="${r.data.replace(/"/g,'&quot;')}">${summary}</span></td>
<td style="color:#64748b; white-space:nowrap">${r.created_at ? r.created_at.slice(0,16) : '—'}</td>
</tr>`;
}).join('');
const total = data.total;
document.getElementById('iPageInfo').textContent =
`${intelOffset + 1}${Math.min(intelOffset + PAGE, total)} of ${total.toLocaleString()}`;
document.getElementById('iPrevBtn').disabled = intelOffset === 0;
document.getElementById('iNextBtn').disabled = intelOffset + PAGE >= total;
} else {
document.getElementById('i-type').parentElement.style.display = 'none';
const data = await api(`/admin/api/intelligence/predictions?${params}`);
document.getElementById('intel-thead').innerHTML = `
<tr><th>ID</th><th>Company</th><th>Event</th><th>Type</th><th>Direction</th><th>Magnitude</th><th>Timeframe</th><th>Rationale</th><th>Created</th></tr>`;
document.getElementById('intel-tbody').innerHTML = data.rows.map(r => `
<tr>
<td style="color:#64748b">${r.id}</td>
<td style="white-space:nowrap">${r.company_name}</td>
<td style="color:#64748b">${r.event_id}</td>
<td><span class="badge null">${r.type}</span></td>
<td>${r.direction || '—'}</td>
<td>${r.magnitude || '—'}</td>
<td>${r.timeframe || '—'}</td>
<td><span class="truncate" style="max-width:300px" title="${(r.rationale||'').replace(/"/g,'&quot;')}">${r.rationale || '—'}</span></td>
<td style="color:#64748b; white-space:nowrap">${r.created_at ? r.created_at.slice(0,16) : '—'}</td>
</tr>
`).join('');
const total = data.total;
document.getElementById('iPageInfo').textContent =
`${intelOffset + 1}${Math.min(intelOffset + PAGE, total)} of ${total.toLocaleString()}`;
document.getElementById('iPrevBtn').disabled = intelOffset === 0;
document.getElementById('iNextBtn').disabled = intelOffset + PAGE >= total;
}
}
document.getElementById('iPrevBtn').onclick = () => { intelOffset = Math.max(0, intelOffset - PAGE); loadIntelligence(); };
document.getElementById('iNextBtn').onclick = () => { intelOffset += PAGE; loadIntelligence(); };
document.getElementById('i-view').onchange = () => { intelOffset = 0; loadIntelligence(); };
// ── sql console ────────────────────────────────────────────────────────────
async function runSql() {
const sql = document.getElementById('sql-input').value.trim();
if (!sql) return;
const database = document.getElementById('sql-db').value;
const errEl = document.getElementById('sql-error');
const resultsEl = document.getElementById('sql-results');
const elapsedEl = document.getElementById('sql-elapsed');
errEl.style.display = 'none';
resultsEl.innerHTML = '';
elapsedEl.textContent = '';
try {
const data = await fetch('/admin/api/sql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sql, database }),
}).then(r => r.json());
if (data.error) {
errEl.textContent = data.error;
errEl.style.display = '';
return;
}
elapsedEl.textContent = `${data.elapsed}ms`;
if (data.rows && data.rows.length > 0) {
const cols = Object.keys(data.rows[0]);
resultsEl.innerHTML = `
<table>
<thead><tr>${cols.map(c => `<th>${c}</th>`).join('')}</tr></thead>
<tbody>${data.rows.map(r =>
`<tr>${cols.map(c => `<td><span class="truncate" style="max-width:300px" title="${String(r[c] ?? '').replace(/"/g,'&quot;')}">${r[c] ?? '<span style="color:#64748b">NULL</span>'}</span></td>`).join('')}</tr>`
).join('')}</tbody>
</table>
<div style="color:#64748b; font-size:12px; margin-top:8px">${data.rows.length} row${data.rows.length !== 1 ? 's' : ''}</div>
`;
} else if (data.rows) {
resultsEl.innerHTML = '<div style="color:#64748b; font-size:13px">No rows returned.</div>';
} else {
resultsEl.innerHTML = `<div style="color:#86efac; font-size:13px">${data.changes} row${data.changes !== 1 ? 's' : ''} affected.</div>`;
}
} catch (e) {
errEl.textContent = e.message;
errEl.style.display = '';
}
}
document.getElementById('sql-run-btn').onclick = runSql;
document.getElementById('sql-input').addEventListener('keydown', e => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) runSql();
});
// ── tabs ───────────────────────────────────────────────────────────────────
const tabContents = {
articles: document.getElementById('tab-articles'),
events: document.getElementById('tab-events'),
stats: document.getElementById('tab-stats'),
intelligence: document.getElementById('tab-intelligence'),
sql: document.getElementById('tab-sql'),
};
document.querySelectorAll('.tab-btn').forEach(btn => {
@@ -671,6 +909,7 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
});
if (tab === 'events') loadEvents();
if (tab === 'stats') loadStats();
if (tab === 'intelligence') { intelOffset = 0; loadIntelligenceStats().then(ok => { if (ok) { loadIntelligenceCompanies(); loadIntelligence(); } }); }
};
});