add intelligence and SQL tabs to admin interface with corresponding API endpoints
This commit is contained in:
+239
@@ -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,'"')}">${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,'"')}">${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,'"')}">${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(); } }); }
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user