add event_date column to event_knowledge and event_predictions tables; update related logic in admin panel and augorWorker
This commit is contained in:
parent
c31f8b0b16
commit
8aa10d9bc2
4 changed files with 365 additions and 89 deletions
|
|
@ -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
|
||||
|
||||
When making any changes to the database schema or data, a strictly no data loss policy must be followed. This means:
|
||||
|
|
|
|||
283
admin.html
283
admin.html
|
|
@ -478,6 +478,15 @@
|
|||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -689,6 +698,7 @@
|
|||
<div id="intel-graph-svg-wrap">
|
||||
<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>
|
||||
<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 id="graph-sidebar">
|
||||
<div id="graph-sidebar-title"></div>
|
||||
|
|
@ -697,9 +707,15 @@
|
|||
</div>
|
||||
|
||||
<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:#1e293b; 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>
|
||||
<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:#0f172a; border:2px solid #475569"></span>Untracked entity</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>
|
||||
|
||||
|
|
@ -1215,6 +1231,32 @@ document.getElementById('i-view').onchange = () => { intelOffset = 0; loadIntell
|
|||
|
||||
// ── 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) {
|
||||
document.getElementById('intel-graph-wrap').style.display = visible ? 'block' : 'none';
|
||||
document.getElementById('intel-table-wrap').style.display = visible ? 'none' : '';
|
||||
|
|
@ -1222,6 +1264,9 @@ function showGraphView(visible) {
|
|||
}
|
||||
|
||||
|
||||
let graphAllNodes = [];
|
||||
let graphAllLinks = [];
|
||||
|
||||
async function loadIntelGraph() {
|
||||
showGraphView(true);
|
||||
|
||||
|
|
@ -1237,22 +1282,20 @@ async function loadIntelGraph() {
|
|||
|
||||
document.getElementById('graph-empty').style.display = 'none';
|
||||
|
||||
// build keyed nodes
|
||||
const nodeMap = new Map();
|
||||
for (const n of data.nodes) {
|
||||
const key = n.tracked ? `c_${n.id}` : `u_${n.name}`;
|
||||
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) {
|
||||
const src = `c_${e.from_company_id}`;
|
||||
const tgt = e.to_company_id ? `c_${e.to_company_id}` : `u_${e.to_entity}`;
|
||||
|
||||
if (nodeMap.has(src) && nodeMap.has(tgt)) {
|
||||
links.push({
|
||||
graphAllLinks.push({
|
||||
source: src,
|
||||
target: tgt,
|
||||
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');
|
||||
svgEl.innerHTML = '';
|
||||
|
||||
const width = svgEl.clientWidth || 800;
|
||||
const width = svgEl.clientWidth || 900;
|
||||
const height = svgEl.clientHeight || 600;
|
||||
|
||||
const svg = d3.select(svgEl);
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
// arrow marker
|
||||
svg.append('defs').append('marker')
|
||||
// svg defs — arrow marker + glow filter
|
||||
const defs = svg.append('defs');
|
||||
|
||||
defs.append('marker')
|
||||
.attr('id', 'ig-arrow')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 22)
|
||||
.attr('refX', 28)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 5)
|
||||
.attr('markerHeight', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.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);
|
||||
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.key).distance(150))
|
||||
.force('charge', d3.forceManyBody().strength(-500))
|
||||
const simulation = d3.forceSimulation(nodesCopy)
|
||||
.force('link', d3.forceLink(linksCopy).id(d => d.key).distance(d => {
|
||||
// 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('collide', d3.forceCollide(40));
|
||||
.force('collide', d3.forceCollide(d => d.tracked ? 52 : 38));
|
||||
|
||||
|
||||
const linkLines = g.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
// links
|
||||
const linkG = g.append('g');
|
||||
|
||||
const linkLines = linkG.selectAll('line')
|
||||
.data(linksCopy)
|
||||
.join('line')
|
||||
.attr('stroke', '#334155')
|
||||
.attr('stroke-width', d => 1 + (d.count / maxCount) * 3)
|
||||
.attr('stroke-opacity', d => 0.3 + (d.count / maxCount) * 0.6)
|
||||
.attr('stroke-width', d => {
|
||||
// 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)');
|
||||
|
||||
// relationship type on hover only via title
|
||||
linkLines.append('title').text(d => `${d.type} (×${d.count})`);
|
||||
// edge labels — size and opacity proportional to 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')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.data(nodesCopy)
|
||||
.join('g')
|
||||
.attr('cursor', d => d.tracked ? 'pointer' : 'default')
|
||||
.call(
|
||||
|
|
@ -1336,30 +1449,67 @@ function renderIntelGraph(nodes, links) {
|
|||
if (d.tracked) openGraphSidebar(d);
|
||||
});
|
||||
|
||||
nodeGroups.append('circle')
|
||||
.attr('r', d => d.tracked ? 16 : 11)
|
||||
.attr('fill', d => d.tracked ? '#1e3a5f' : '#0f172a')
|
||||
.attr('stroke', d => d.tracked ? '#3b82f6' : '#475569')
|
||||
.attr('stroke-width', d => d.tracked ? 2 : 1.5);
|
||||
|
||||
// ticker label inside tracked nodes; short name below untracked nodes
|
||||
nodeGroups.filter(d => d.tracked).append('text')
|
||||
.text(d => d.ticker || d.name.slice(0, 5))
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', 9)
|
||||
.attr('font-weight', '600')
|
||||
.attr('fill', '#93c5fd')
|
||||
// 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.filter(d => !d.tracked).append('text')
|
||||
.text(d => d.name.length > 12 ? d.name.slice(0, 11) + '…' : d.name)
|
||||
nodeGroups.append('circle')
|
||||
.attr('r', d => d.tracked ? 20 : 12)
|
||||
.attr('fill', d => d.tracked ? '#1a3152' : '#0b1524')
|
||||
.attr('stroke', d => d.tracked ? '#3b82f6' : '#334155')
|
||||
.attr('stroke-width', d => d.tracked ? 2 : 1.5);
|
||||
|
||||
// short ticker label inside circle for tracked nodes
|
||||
nodeGroups.filter(d => d.tracked).append('text')
|
||||
.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('dominant-baseline', 'middle')
|
||||
.attr('font-size', 8)
|
||||
.attr('font-weight', '700')
|
||||
.attr('fill', '#93c5fd')
|
||||
.attr('letter-spacing', '0.04em')
|
||||
.attr('pointer-events', 'none');
|
||||
|
||||
// full company name below tracked 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', 10)
|
||||
.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('y', 20)
|
||||
.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);
|
||||
|
||||
|
||||
|
|
@ -1370,15 +1520,18 @@ function renderIntelGraph(nodes, links) {
|
|||
.attr('x2', d => d.target.x)
|
||||
.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})`);
|
||||
});
|
||||
|
||||
// fit everything into view once the simulation has cooled enough
|
||||
simulation.on('end', () => {
|
||||
const bounds = g.node().getBBox();
|
||||
if (!bounds.width || !bounds.height) return;
|
||||
|
||||
const pad = 40;
|
||||
const pad = 60;
|
||||
const scaleX = width / (bounds.width + pad * 2);
|
||||
const scaleY = height / (bounds.height + pad * 2);
|
||||
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 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));
|
||||
});
|
||||
}
|
||||
|
|
@ -1490,24 +1643,32 @@ async function runSql() {
|
|||
|
||||
elapsedEl.textContent = `${data.elapsed}ms`;
|
||||
|
||||
if (data.rows && data.rows.length > 0) {
|
||||
const cols = Object.keys(data.rows[0]);
|
||||
resultsEl.innerHTML = `
|
||||
<div class="table-wrap" style="margin-top:4px">
|
||||
const blocks = [];
|
||||
for (const r of (data.results || [])) {
|
||||
if (r.error) {
|
||||
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>`);
|
||||
} else if (r.rows && r.rows.length > 0) {
|
||||
const cols = Object.keys(r.rows[0]);
|
||||
blocks.push(`
|
||||
<div style="margin-bottom:16px">
|
||||
<div class="table-wrap">
|
||||
<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:var(--muted-dark)">NULL</span>'}</span></td>`).join('')}</tr>`
|
||||
<tbody>${r.rows.map(row =>
|
||||
`<tr>${cols.map(c => `<td><span class="truncate" style="max-width:300px" title="${String(row[c] ?? '').replace(/"/g,'"')}">${row[c] ?? '<span style="color:var(--muted-dark)">NULL</span>'}</span></td>`).join('')}</tr>`
|
||||
).join('')}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="color:var(--muted-dark); 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:var(--muted); font-size:13px; padding-top:8px">No rows returned.</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 {
|
||||
resultsEl.innerHTML = `<div style="color:#86efac; font-size:13px; padding-top:8px">${data.changes} row${data.changes !== 1 ? 's' : ''} affected.</div>`;
|
||||
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) {
|
||||
errEl.textContent = e.message;
|
||||
errEl.style.display = '';
|
||||
|
|
|
|||
|
|
@ -113,11 +113,8 @@ function runColumnMigrations(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(
|
||||
"INSERT INTO tracked_companies (name, ticker, aliases) VALUES (?, ?, ?)"
|
||||
"INSERT OR IGNORE INTO tracked_companies (name, ticker, aliases) VALUES (?, ?, ?)"
|
||||
);
|
||||
|
||||
const companies = [
|
||||
|
|
@ -168,6 +165,122 @@ function seedCompanies(db) {
|
|||
{ name: "Western Digital", ticker: "WDC", aliases: ["WD", "Western Digital Corporation"] },
|
||||
{ name: "Seagate", ticker: "STX", aliases: ["Seagate Technology"] },
|
||||
{ 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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
if (!checkAuth(request, reply)) return;
|
||||
|
||||
|
|
@ -368,29 +368,27 @@ async function adminRoutes(fastify) {
|
|||
const target = database === 'intelligence' ? getIntelligenceDb() : db;
|
||||
if (!target) { reply.code(400); return { error: 'database not available' }; }
|
||||
|
||||
try {
|
||||
const stmt = target.prepare(sql);
|
||||
// split on semicolons, drop empty statements
|
||||
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
||||
|
||||
const results = [];
|
||||
const start = Date.now();
|
||||
|
||||
let rows, changes, lastInsertRowid;
|
||||
for (const s of statements) {
|
||||
try {
|
||||
const stmt = target.prepare(s);
|
||||
if (stmt.reader) {
|
||||
rows = stmt.all();
|
||||
results.push({ sql: s, rows: stmt.all() });
|
||||
} else {
|
||||
const info = stmt.run();
|
||||
changes = info.changes;
|
||||
lastInsertRowid = info.lastInsertRowid;
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue