add event_date column to event_knowledge and event_predictions tables; update related logic in admin panel and augorWorker

This commit is contained in:
ImBenji 2026-04-23 16:21:30 +01:00
parent c31f8b0b16
commit 8aa10d9bc2
4 changed files with 365 additions and 89 deletions

View file

@ -1,3 +1,7 @@
# Production Database
Never run direct database operations (inserts, updates, deletes, schema changes) against the local SQLite files (e.g. intelligence.sqlite, archive.sqlite). These are local copies and do not affect the production database. All data changes must go through code that runs in production — seed functions, migration scripts, workers, etc.
# Database Policy # Database Policy
When making any changes to the database schema or data, a strictly no data loss policy must be followed. This means: When making any changes to the database schema or data, a strictly no data loss policy must be followed. This means:

View file

@ -478,6 +478,15 @@
height: 600px; height: 600px;
} }
#intel-graph-svg-wrap.graph-expanded {
position: fixed;
inset: 0;
z-index: 500;
border-radius: 0;
border: none;
height: 100dvh;
}
#intel-graph-svg { #intel-graph-svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -689,6 +698,7 @@
<div id="intel-graph-svg-wrap"> <div id="intel-graph-svg-wrap">
<svg id="intel-graph-svg"></svg> <svg id="intel-graph-svg"></svg>
<div id="graph-empty" style="display:none; position:absolute; inset:0; color:var(--muted); font-size:13px; text-align:center; padding-top:120px">No relationship data yet</div> <div id="graph-empty" style="display:none; position:absolute; inset:0; color:var(--muted); font-size:13px; text-align:center; padding-top:120px">No relationship data yet</div>
<button id="graph-expand-btn" onclick="toggleGraphExpand()" style="position:absolute; top:10px; right:10px; padding:5px 10px; font-size:12px; background:var(--bg-card); border:1px solid var(--border); color:var(--muted); border-radius:var(--radius); cursor:pointer; z-index:10" title="Expand graph">⤢ Expand</button>
</div> </div>
<div id="graph-sidebar"> <div id="graph-sidebar">
<div id="graph-sidebar-title"></div> <div id="graph-sidebar-title"></div>
@ -697,9 +707,15 @@
</div> </div>
<div id="graph-legend"> <div id="graph-legend">
<span><span class="graph-legend-dot" style="background:#3b82f6; border:2px solid #1d4ed8"></span>Tracked company</span> <span><span class="graph-legend-dot" style="background:#1e3a5f; border:2px solid #3b82f6"></span>Tracked company</span>
<span><span class="graph-legend-dot" style="background:#1e293b; border:2px solid #475569"></span>Untracked entity</span> <span><span class="graph-legend-dot" style="background:#0f172a; border:2px solid #475569"></span>Untracked entity</span>
<span style="margin-left:8px; color:var(--muted-dark)">Scroll to zoom · drag nodes · click to see facts</span>
<label style="display:flex; align-items:center; gap:6px; margin-left:16px; cursor:pointer; user-select:none">
<input type="checkbox" id="graph-show-untracked" style="accent-color:var(--accent); width:13px; height:13px" />
<span>Show untracked entities</span>
</label>
<span style="margin-left:auto; color:var(--muted-dark)">Scroll to zoom · drag nodes · click for facts</span>
</div> </div>
</div> </div>
@ -1215,6 +1231,32 @@ document.getElementById('i-view').onchange = () => { intelOffset = 0; loadIntell
// ── intelligence graph ───────────────────────────────────────────────────── // ── intelligence graph ─────────────────────────────────────────────────────
// tickers that dont make a readable label inside a node
const TICKER_DISPLAY = {
'005930.KS': 'SMSNG',
'000660.KS': 'HYNIX',
'OPENAI': 'OAI',
'ANTHROPIC': 'ANTH',
};
// split a name into up to two lines at a natural word boundary
function splitLabel(name, maxLine) {
if (name.length <= maxLine) return [name];
const mid = Math.floor(name.length / 2);
// find a space near the middle to break on
let best = -1;
let bestDist = Infinity;
for (let i = 0; i < name.length; i++) {
if (name[i] === ' ') {
const dist = Math.abs(i - mid);
if (dist < bestDist) { bestDist = dist; best = i; }
}
}
if (best === -1) return [name]; // no spaces, just show as-is
return [name.slice(0, best), name.slice(best + 1)];
}
function showGraphView(visible) { function showGraphView(visible) {
document.getElementById('intel-graph-wrap').style.display = visible ? 'block' : 'none'; document.getElementById('intel-graph-wrap').style.display = visible ? 'block' : 'none';
document.getElementById('intel-table-wrap').style.display = visible ? 'none' : ''; document.getElementById('intel-table-wrap').style.display = visible ? 'none' : '';
@ -1222,6 +1264,9 @@ function showGraphView(visible) {
} }
let graphAllNodes = [];
let graphAllLinks = [];
async function loadIntelGraph() { async function loadIntelGraph() {
showGraphView(true); showGraphView(true);
@ -1237,22 +1282,20 @@ async function loadIntelGraph() {
document.getElementById('graph-empty').style.display = 'none'; document.getElementById('graph-empty').style.display = 'none';
// build keyed nodes
const nodeMap = new Map(); const nodeMap = new Map();
for (const n of data.nodes) { for (const n of data.nodes) {
const key = n.tracked ? `c_${n.id}` : `u_${n.name}`; const key = n.tracked ? `c_${n.id}` : `u_${n.name}`;
nodeMap.set(key, { ...n, key }); nodeMap.set(key, { ...n, key });
} }
const nodes = Array.from(nodeMap.values()); graphAllNodes = Array.from(nodeMap.values());
const links = []; graphAllLinks = [];
for (const e of data.edges) { for (const e of data.edges) {
const src = `c_${e.from_company_id}`; const src = `c_${e.from_company_id}`;
const tgt = e.to_company_id ? `c_${e.to_company_id}` : `u_${e.to_entity}`; const tgt = e.to_company_id ? `c_${e.to_company_id}` : `u_${e.to_entity}`;
if (nodeMap.has(src) && nodeMap.has(tgt)) { if (nodeMap.has(src) && nodeMap.has(tgt)) {
links.push({ graphAllLinks.push({
source: src, source: src,
target: tgt, target: tgt,
type: e.relationship_type, type: e.relationship_type,
@ -1262,62 +1305,132 @@ async function loadIntelGraph() {
} }
} }
renderIntelGraph(nodes, links); renderIntelGraph();
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
const wrap = document.getElementById('intel-graph-svg-wrap');
if (wrap.classList.contains('graph-expanded')) toggleGraphExpand();
}
});
document.getElementById('graph-show-untracked').addEventListener('change', () => {
if (graphAllNodes.length) renderIntelGraph();
});
function toggleGraphExpand() {
const wrap = document.getElementById('intel-graph-svg-wrap');
const btn = document.getElementById('graph-expand-btn');
const expanded = wrap.classList.toggle('graph-expanded');
btn.textContent = expanded ? '⤡ Collapse' : '⤢ Expand';
// re-render so the simulation uses the new dimensions
if (graphAllNodes.length) renderIntelGraph();
} }
function renderIntelGraph(nodes, links) { function renderIntelGraph() {
const showUntracked = document.getElementById('graph-show-untracked').checked;
const nodes = showUntracked
? graphAllNodes
: graphAllNodes.filter(n => n.tracked);
const visibleKeys = new Set(nodes.map(n => n.key));
const links = graphAllLinks.filter(l => visibleKeys.has(
typeof l.source === 'object' ? l.source.key : l.source
) && visibleKeys.has(
typeof l.target === 'object' ? l.target.key : l.target
));
// deep-copy so d3 doesnt mutate our stored arrays on re-render
const nodesCopy = nodes.map(n => ({ ...n }));
const linksCopy = links.map(l => ({
...l,
source: typeof l.source === 'object' ? l.source.key : l.source,
target: typeof l.target === 'object' ? l.target.key : l.target,
}));
const svgEl = document.getElementById('intel-graph-svg'); const svgEl = document.getElementById('intel-graph-svg');
svgEl.innerHTML = ''; svgEl.innerHTML = '';
const width = svgEl.clientWidth || 800; const width = svgEl.clientWidth || 900;
const height = svgEl.clientHeight || 600; const height = svgEl.clientHeight || 600;
const svg = d3.select(svgEl); const svg = d3.select(svgEl);
const g = svg.append('g'); const g = svg.append('g');
// arrow marker // svg defs — arrow marker + glow filter
svg.append('defs').append('marker') const defs = svg.append('defs');
defs.append('marker')
.attr('id', 'ig-arrow') .attr('id', 'ig-arrow')
.attr('viewBox', '0 -5 10 10') .attr('viewBox', '0 -5 10 10')
.attr('refX', 22) .attr('refX', 28)
.attr('refY', 0) .attr('refY', 0)
.attr('markerWidth', 5) .attr('markerWidth', 5)
.attr('markerHeight', 5) .attr('markerHeight', 5)
.attr('orient', 'auto') .attr('orient', 'auto')
.append('path') .append('path')
.attr('d', 'M0,-5L10,0L0,5') .attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#334155'); .attr('fill', '#475569');
const maxCount = Math.max(...links.map(l => l.count), 1); const glow = defs.append('filter')
.attr('id', 'ig-glow')
.attr('x', '-50%').attr('y', '-50%')
.attr('width', '200%').attr('height', '200%');
glow.append('feGaussianBlur').attr('stdDeviation', '4').attr('result', 'blur');
const merge = glow.append('feMerge');
merge.append('feMergeNode').attr('in', 'blur');
merge.append('feMergeNode').attr('in', 'SourceGraphic');
const zoomBehavior = d3.zoom().scaleExtent([0.1, 5]).on('zoom', ev => g.attr('transform', ev.transform)); const maxCount = Math.max(...linksCopy.map(l => l.count), 1);
const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', ev => g.attr('transform', ev.transform));
svg.call(zoomBehavior); svg.call(zoomBehavior);
const simulation = d3.forceSimulation(nodes) const simulation = d3.forceSimulation(nodesCopy)
.force('link', d3.forceLink(links).id(d => d.key).distance(150)) .force('link', d3.forceLink(linksCopy).id(d => d.key).distance(d => {
.force('charge', d3.forceManyBody().strength(-500)) // tracked-tracked links spread wider
const bothTracked = typeof d.source === 'object' ? d.source.tracked && d.target.tracked : false;
return bothTracked ? 220 : 160;
}))
.force('charge', d3.forceManyBody().strength(-900))
.force('center', d3.forceCenter(width / 2, height / 2)) .force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(40)); .force('collide', d3.forceCollide(d => d.tracked ? 52 : 38));
const linkLines = g.append('g') // links
.selectAll('line') const linkG = g.append('g');
.data(links)
const linkLines = linkG.selectAll('line')
.data(linksCopy)
.join('line') .join('line')
.attr('stroke', '#334155') .attr('stroke', '#334155')
.attr('stroke-width', d => 1 + (d.count / maxCount) * 3) .attr('stroke-width', d => {
.attr('stroke-opacity', d => 0.3 + (d.count / maxCount) * 0.6) // more dramatic: 1px at count=1, up to 7px at maxCount
return 1 + Math.pow(d.count / maxCount, 0.6) * 6;
})
.attr('stroke-opacity', d => 0.25 + (d.count / maxCount) * 0.65)
.attr('marker-end', 'url(#ig-arrow)'); .attr('marker-end', 'url(#ig-arrow)');
// relationship type on hover only via title // edge labels — size and opacity proportional to count
linkLines.append('title').text(d => `${d.type} (×${d.count})`); const linkLabels = linkG.selectAll('text')
.data(linksCopy)
.join('text')
.text(d => d.type)
.attr('text-anchor', 'middle')
.attr('font-size', d => 7 + Math.round((d.count / maxCount) * 4))
.attr('fill', '#475569')
.attr('opacity', d => 0.4 + (d.count / maxCount) * 0.5)
.attr('pointer-events', 'none');
// nodes
const nodeGroups = g.append('g') const nodeGroups = g.append('g')
.selectAll('g') .selectAll('g')
.data(nodes) .data(nodesCopy)
.join('g') .join('g')
.attr('cursor', d => d.tracked ? 'pointer' : 'default') .attr('cursor', d => d.tracked ? 'pointer' : 'default')
.call( .call(
@ -1336,29 +1449,66 @@ function renderIntelGraph(nodes, links) {
if (d.tracked) openGraphSidebar(d); if (d.tracked) openGraphSidebar(d);
}); });
// tracked node: glow circle + main circle
nodeGroups.filter(d => d.tracked).append('circle')
.attr('r', 24)
.attr('fill', 'rgba(59,130,246,0.08)')
.attr('stroke', 'rgba(59,130,246,0.25)')
.attr('stroke-width', 1)
.attr('filter', 'url(#ig-glow)')
.attr('pointer-events', 'none');
nodeGroups.append('circle') nodeGroups.append('circle')
.attr('r', d => d.tracked ? 16 : 11) .attr('r', d => d.tracked ? 20 : 12)
.attr('fill', d => d.tracked ? '#1e3a5f' : '#0f172a') .attr('fill', d => d.tracked ? '#1a3152' : '#0b1524')
.attr('stroke', d => d.tracked ? '#3b82f6' : '#475569') .attr('stroke', d => d.tracked ? '#3b82f6' : '#334155')
.attr('stroke-width', d => d.tracked ? 2 : 1.5); .attr('stroke-width', d => d.tracked ? 2 : 1.5);
// ticker label inside tracked nodes; short name below untracked nodes // short ticker label inside circle for tracked nodes
nodeGroups.filter(d => d.tracked).append('text') nodeGroups.filter(d => d.tracked).append('text')
.text(d => d.ticker || d.name.slice(0, 5)) .text(d => TICKER_DISPLAY[d.ticker] || (d.ticker && d.ticker.length <= 5 ? d.ticker : d.name.slice(0, 4).toUpperCase()))
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle') .attr('dominant-baseline', 'middle')
.attr('font-size', 9) .attr('font-size', 8)
.attr('font-weight', '600') .attr('font-weight', '700')
.attr('fill', '#93c5fd') .attr('fill', '#93c5fd')
.attr('letter-spacing', '0.04em')
.attr('pointer-events', 'none'); .attr('pointer-events', 'none');
nodeGroups.filter(d => !d.tracked).append('text') // full company name below tracked nodes — wrapped
.text(d => d.name.length > 12 ? d.name.slice(0, 11) + '…' : d.name) nodeGroups.filter(d => d.tracked).each(function(d) {
.attr('text-anchor', 'middle') const lines = splitLabel(d.name, 14);
.attr('y', 20) const el = d3.select(this).append('text')
.attr('font-size', 9) .attr('text-anchor', 'middle')
.attr('fill', '#475569') .attr('font-size', 10)
.attr('pointer-events', 'none'); .attr('font-weight', '500')
.attr('fill', '#cbd5e1')
.attr('pointer-events', 'none');
lines.forEach((line, i) => {
el.append('tspan')
.attr('x', 0)
.attr('dy', i === 0 ? 32 : 13)
.text(line);
});
});
// full entity name below untracked nodes — wrapped
nodeGroups.filter(d => !d.tracked).each(function(d) {
const lines = splitLabel(d.name, 14);
const el = d3.select(this).append('text')
.attr('text-anchor', 'middle')
.attr('font-size', 9)
.attr('fill', '#475569')
.attr('pointer-events', 'none');
lines.forEach((line, i) => {
el.append('tspan')
.attr('x', 0)
.attr('dy', i === 0 ? 20 : 12)
.text(line);
});
});
nodeGroups.append('title').text(d => d.name); nodeGroups.append('title').text(d => d.name);
@ -1370,15 +1520,18 @@ function renderIntelGraph(nodes, links) {
.attr('x2', d => d.target.x) .attr('x2', d => d.target.x)
.attr('y2', d => d.target.y); .attr('y2', d => d.target.y);
linkLabels
.attr('x', d => (d.source.x + d.target.x) / 2)
.attr('y', d => (d.source.y + d.target.y) / 2 - 3);
nodeGroups.attr('transform', d => `translate(${d.x},${d.y})`); nodeGroups.attr('transform', d => `translate(${d.x},${d.y})`);
}); });
// fit everything into view once the simulation has cooled enough
simulation.on('end', () => { simulation.on('end', () => {
const bounds = g.node().getBBox(); const bounds = g.node().getBBox();
if (!bounds.width || !bounds.height) return; if (!bounds.width || !bounds.height) return;
const pad = 40; const pad = 60;
const scaleX = width / (bounds.width + pad * 2); const scaleX = width / (bounds.width + pad * 2);
const scaleY = height / (bounds.height + pad * 2); const scaleY = height / (bounds.height + pad * 2);
const scale = Math.min(scaleX, scaleY, 1); const scale = Math.min(scaleX, scaleY, 1);
@ -1386,7 +1539,7 @@ function renderIntelGraph(nodes, links) {
const tx = width / 2 - scale * (bounds.x + bounds.width / 2); const tx = width / 2 - scale * (bounds.x + bounds.width / 2);
const ty = height / 2 - scale * (bounds.y + bounds.height / 2); const ty = height / 2 - scale * (bounds.y + bounds.height / 2);
svg.transition().duration(600) svg.transition().duration(700)
.call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(scale)); .call(zoomBehavior.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
}); });
} }
@ -1490,24 +1643,32 @@ async function runSql() {
elapsedEl.textContent = `${data.elapsed}ms`; elapsedEl.textContent = `${data.elapsed}ms`;
if (data.rows && data.rows.length > 0) { const blocks = [];
const cols = Object.keys(data.rows[0]); for (const r of (data.results || [])) {
resultsEl.innerHTML = ` if (r.error) {
<div class="table-wrap" style="margin-top:4px"> blocks.push(`<div style="color:#fca5a5; font-size:13px; margin-bottom:12px"><span style="color:var(--muted-dark); font-size:11px; display:block; margin-bottom:4px; font-family:'SF Mono','Fira Code',monospace">${r.sql.slice(0, 120)}</span>${r.error}</div>`);
<table> } else if (r.rows && r.rows.length > 0) {
<thead><tr>${cols.map(c => `<th>${c}</th>`).join('')}</tr></thead> const cols = Object.keys(r.rows[0]);
<tbody>${data.rows.map(r => blocks.push(`
`<tr>${cols.map(c => `<td><span class="truncate" style="max-width:300px" title="${String(r[c] ?? '').replace(/"/g,'&quot;')}">${r[c] ?? '<span style="color:var(--muted-dark)">NULL</span>'}</span></td>`).join('')}</tr>` <div style="margin-bottom:16px">
).join('')}</tbody> <div class="table-wrap">
</table> <table>
</div> <thead><tr>${cols.map(c => `<th>${c}</th>`).join('')}</tr></thead>
<div style="color:var(--muted-dark); font-size:12px; margin-top:8px">${data.rows.length} row${data.rows.length !== 1 ? 's' : ''}</div> <tbody>${r.rows.map(row =>
`; `<tr>${cols.map(c => `<td><span class="truncate" style="max-width:300px" title="${String(row[c] ?? '').replace(/"/g,'&quot;')}">${row[c] ?? '<span style="color:var(--muted-dark)">NULL</span>'}</span></td>`).join('')}</tr>`
} else if (data.rows) { ).join('')}</tbody>
resultsEl.innerHTML = '<div style="color:var(--muted); font-size:13px; padding-top:8px">No rows returned.</div>'; </table>
} else { </div>
resultsEl.innerHTML = `<div style="color:#86efac; font-size:13px; padding-top:8px">${data.changes} row${data.changes !== 1 ? 's' : ''} affected.</div>`; <div style="color:var(--muted-dark); font-size:12px; margin-top:6px">${r.rows.length} row${r.rows.length !== 1 ? 's' : ''}</div>
</div>
`);
} else if (r.rows) {
blocks.push(`<div style="color:var(--muted); font-size:13px; margin-bottom:12px">No rows returned.</div>`);
} else {
blocks.push(`<div style="color:#86efac; font-size:13px; margin-bottom:12px">${r.changes} row${r.changes !== 1 ? 's' : ''} affected.</div>`);
}
} }
resultsEl.innerHTML = blocks.join('');
} catch (e) { } catch (e) {
errEl.textContent = e.message; errEl.textContent = e.message;
errEl.style.display = ''; errEl.style.display = '';

View file

@ -113,11 +113,8 @@ function runColumnMigrations(db) {
} }
function seedCompanies(db) { function seedCompanies(db) {
const count = db.prepare("SELECT COUNT(*) as c FROM tracked_companies").get().c;
if (count > 0) return;
const insert = db.prepare( const insert = db.prepare(
"INSERT INTO tracked_companies (name, ticker, aliases) VALUES (?, ?, ?)" "INSERT OR IGNORE INTO tracked_companies (name, ticker, aliases) VALUES (?, ?, ?)"
); );
const companies = [ const companies = [
@ -168,6 +165,122 @@ function seedCompanies(db) {
{ name: "Western Digital", ticker: "WDC", aliases: ["WD", "Western Digital Corporation"] }, { name: "Western Digital", ticker: "WDC", aliases: ["WD", "Western Digital Corporation"] },
{ name: "Seagate", ticker: "STX", aliases: ["Seagate Technology"] }, { name: "Seagate", ticker: "STX", aliases: ["Seagate Technology"] },
{ name: "Pure Storage", ticker: "PSTG", aliases: [] }, { name: "Pure Storage", ticker: "PSTG", aliases: [] },
// more AI labs
{ name: "Mistral AI", ticker: "MISTRAL", aliases: ["Mistral"] },
{ name: "Cohere", ticker: "COHERE", aliases: [] },
{ name: "DeepSeek", ticker: "DEEPSEEK", aliases: ["DeepSeek AI"] },
{ name: "Stability AI", ticker: "STABILITY", aliases: [] },
{ name: "Inflection AI", ticker: "INFLECTION", aliases: [] },
{ name: "Scale AI", ticker: "SCALEAI", aliases: ["Scale"] },
{ name: "Hugging Face", ticker: "HF", aliases: [] },
{ name: "Cerebras Systems", ticker: "CBRS", aliases: ["Cerebras"] },
{ name: "Groq", ticker: "GROQ", aliases: [] },
// chinese tech
{ name: "Alibaba", ticker: "BABA", aliases: ["Alibaba Group", "Taobao", "Alipay"] },
{ name: "Tencent", ticker: "0700.HK", aliases: ["Tencent Holdings", "WeChat"] },
{ name: "Baidu", ticker: "BIDU", aliases: ["Baidu Inc"] },
{ name: "Huawei", ticker: "HUAWEI", aliases: ["Huawei Technologies"] },
{ name: "ByteDance", ticker: "BYTEDANCE", aliases: ["TikTok", "Douyin"] },
{ name: "Xiaomi", ticker: "1810.HK", aliases: [] },
{ name: "SMIC", ticker: "688981.SS", aliases: ["Semiconductor Manufacturing International Corporation"] },
{ name: "DJI", ticker: "DJI", aliases: ["Da-Jiang Innovations"] },
// defense / aerospace
{ name: "Lockheed Martin", ticker: "LMT", aliases: ["Lockheed"] },
{ name: "Raytheon Technologies", ticker: "RTX", aliases: ["RTX", "Raytheon"] },
{ name: "Northrop Grumman", ticker: "NOC", aliases: ["Northrop"] },
{ name: "Boeing", ticker: "BA", aliases: ["Boeing Company"] },
{ name: "General Dynamics", ticker: "GD", aliases: [] },
{ name: "BAE Systems", ticker: "BA.L", aliases: ["BAE"] },
{ name: "L3Harris Technologies", ticker: "LHX", aliases: ["L3Harris"] },
{ name: "Leidos", ticker: "LDOS", aliases: [] },
{ name: "SAIC", ticker: "SAIC", aliases: ["Science Applications International Corporation"] },
{ name: "Booz Allen Hamilton", ticker: "BAH", aliases: ["Booz Allen"] },
{ name: "Thales Group", ticker: "HO.PA", aliases: ["Thales"] },
{ name: "Airbus", ticker: "AIR.PA", aliases: ["Airbus SE"] },
{ name: "Leonardo", ticker: "LDO.MI", aliases: ["Leonardo SpA"] },
{ name: "Rheinmetall", ticker: "RHM.DE", aliases: [] },
// telecom
{ name: "Ericsson", ticker: "ERIC", aliases: ["Telefonaktiebolaget LM Ericsson"] },
{ name: "Nokia", ticker: "NOK", aliases: ["Nokia Corporation"] },
{ name: "AT&T", ticker: "T", aliases: [] },
{ name: "Verizon", ticker: "VZ", aliases: ["Verizon Communications"] },
{ name: "T-Mobile", ticker: "TMUS", aliases: ["T-Mobile US"] },
{ name: "Deutsche Telekom", ticker: "DTE.DE", aliases: [] },
{ name: "SoftBank", ticker: "9984.T", aliases: ["SoftBank Group"] },
// finance / banking
{ name: "JPMorgan Chase", ticker: "JPM", aliases: ["JPMorgan", "JP Morgan"] },
{ name: "Goldman Sachs", ticker: "GS", aliases: ["Goldman"] },
{ name: "BlackRock", ticker: "BLK", aliases: [] },
{ name: "Visa", ticker: "V", aliases: [] },
{ name: "Mastercard", ticker: "MA", aliases: [] },
{ name: "Morgan Stanley", ticker: "MS", aliases: [] },
{ name: "Citigroup", ticker: "C", aliases: ["Citi"] },
{ name: "Bank of America", ticker: "BAC", aliases: ["BofA"] },
{ name: "HSBC", ticker: "HSBA.L", aliases: ["HSBC Holdings"] },
{ name: "UBS", ticker: "UBS", aliases: [] },
// energy / resources
{ name: "ExxonMobil", ticker: "XOM", aliases: ["Exxon"] },
{ name: "Chevron", ticker: "CVX", aliases: [] },
{ name: "Shell", ticker: "SHEL", aliases: ["Shell plc", "Royal Dutch Shell"] },
{ name: "BP", ticker: "BP", aliases: ["British Petroleum"] },
{ name: "TotalEnergies", ticker: "TTE.PA", aliases: ["Total"] },
{ name: "Saudi Aramco", ticker: "2222.SR", aliases: ["Aramco"] },
{ name: "NextEra Energy", ticker: "NEE", aliases: [] },
// pharma / biotech
{ name: "Pfizer", ticker: "PFE", aliases: [] },
{ name: "Johnson & Johnson", ticker: "JNJ", aliases: ["J&J"] },
{ name: "Moderna", ticker: "MRNA", aliases: [] },
{ name: "AstraZeneca", ticker: "AZN", aliases: [] },
{ name: "Novartis", ticker: "NVS", aliases: [] },
{ name: "Roche", ticker: "ROG.SW", aliases: ["Roche Holding"] },
{ name: "Eli Lilly", ticker: "LLY", aliases: [] },
{ name: "CRISPR Therapeutics", ticker: "CRSP", aliases: [] },
// automotive
{ name: "Toyota", ticker: "TM", aliases: ["Toyota Motor"] },
{ name: "Volkswagen", ticker: "VOW.DE", aliases: ["VW"] },
{ name: "Ford", ticker: "F", aliases: ["Ford Motor"] },
{ name: "General Motors", ticker: "GM", aliases: ["GM"] },
{ name: "BYD", ticker: "002594.SZ", aliases: ["BYD Company"] },
{ name: "Rivian", ticker: "RIVN", aliases: [] },
// media / entertainment
{ name: "Netflix", ticker: "NFLX", aliases: [] },
{ name: "Disney", ticker: "DIS", aliases: ["The Walt Disney Company"] },
{ name: "Comcast", ticker: "CMCSA", aliases: ["NBCUniversal"] },
{ name: "Spotify", ticker: "SPOT", aliases: [] },
{ name: "Warner Bros Discovery", ticker: "WBD", aliases: ["Warner Bros", "HBO"] },
{ name: "News Corp", ticker: "NWSA", aliases: ["Rupert Murdoch"] },
// retail / e-commerce
{ name: "Walmart", ticker: "WMT", aliases: [] },
{ name: "Shopify", ticker: "SHOP", aliases: [] },
{ name: "eBay", ticker: "EBAY", aliases: [] },
// cybersecurity
{ name: "CrowdStrike", ticker: "CRWD", aliases: [] },
{ name: "Palo Alto Networks", ticker: "PANW", aliases: [] },
{ name: "SentinelOne", ticker: "S", aliases: [] },
{ name: "Fortinet", ticker: "FTNT", aliases: [] },
{ name: "Cloudflare", ticker: "NET", aliases: [] },
{ name: "Recorded Future", ticker: "RF", aliases: [] },
// space
{ name: "SpaceX", ticker: "SPACEX", aliases: ["Space Exploration Technologies"] },
{ name: "Blue Origin", ticker: "BLUEORIGIN", aliases: [] },
{ name: "Planet Labs", ticker: "PL", aliases: [] },
// consulting / professional services
{ name: "McKinsey", ticker: "MCKINSEY", aliases: ["McKinsey & Company"] },
{ name: "Accenture", ticker: "ACN", aliases: [] },
{ name: "Deloitte", ticker: "DELOITTE", aliases: [] },
]; ];
for (const c of companies) { for (const c of companies) {

View file

@ -358,7 +358,7 @@ async function adminRoutes(fastify) {
}); });
// raw sql console // raw sql console — supports multiple statements separated by ;
fastify.post('/admin/api/sql', async (request, reply) => { fastify.post('/admin/api/sql', async (request, reply) => {
if (!checkAuth(request, reply)) return; if (!checkAuth(request, reply)) return;
@ -368,29 +368,27 @@ async function adminRoutes(fastify) {
const target = database === 'intelligence' ? getIntelligenceDb() : db; const target = database === 'intelligence' ? getIntelligenceDb() : db;
if (!target) { reply.code(400); return { error: 'database not available' }; } if (!target) { reply.code(400); return { error: 'database not available' }; }
try { // split on semicolons, drop empty statements
const stmt = target.prepare(sql); const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
const start = Date.now();
let rows, changes, lastInsertRowid; const results = [];
if (stmt.reader) { const start = Date.now();
rows = stmt.all();
} else { for (const s of statements) {
const info = stmt.run(); try {
changes = info.changes; const stmt = target.prepare(s);
lastInsertRowid = info.lastInsertRowid; if (stmt.reader) {
results.push({ sql: s, rows: stmt.all() });
} else {
const info = stmt.run();
results.push({ sql: s, changes: info.changes, lastInsertRowid: info.lastInsertRowid });
}
} catch (err) {
results.push({ sql: s, error: err.message });
} }
return {
rows: rows || null,
changes: changes ?? null,
lastInsertRowid: lastInsertRowid ?? null,
elapsed: Date.now() - start,
};
} catch (err) {
reply.code(400);
return { error: err.message };
} }
return { results, elapsed: Date.now() - start };
}); });
// stats for dashboard header // stats for dashboard header