refactor admin navigation; update links to ingest pages and improve loading of data in parallel
This commit is contained in:
parent
fc7ea464b3
commit
69206260d3
4 changed files with 330 additions and 106 deletions
|
|
@ -75,10 +75,45 @@
|
||||||
|
|
||||||
|
|
||||||
/* ── graph view ── */
|
/* ── graph view ── */
|
||||||
|
/* on the graph page we flex the page chrome top-to-bottom so the layout
|
||||||
|
grows to fill the viewport. each ancestor in the chain needs min-height:0
|
||||||
|
so the flex child can actually shrink/stretch. */
|
||||||
|
|
||||||
|
body.page-graph {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.page-graph main.content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.page-graph #intel-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.page-graph #intel-graph-layout {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.page-graph #intel-graph-svg-wrap,
|
||||||
|
body.page-graph #graph-info {
|
||||||
|
height: auto; /* let flex sizing win */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#intel-graph-layout {
|
#intel-graph-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#intel-graph-svg-wrap {
|
#intel-graph-svg-wrap {
|
||||||
|
|
@ -88,7 +123,7 @@
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 600px;
|
height: 600px; /* fallback for any other page that might reuse this */
|
||||||
}
|
}
|
||||||
|
|
||||||
#intel-graph-svg {
|
#intel-graph-svg {
|
||||||
|
|
@ -498,90 +533,187 @@
|
||||||
color: var(--muted-dark);
|
color: var(--muted-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-card-detail {
|
|
||||||
display: none;
|
|
||||||
padding: 0 16px 16px;
|
|
||||||
border-top: 1px solid var(--border-light);
|
|
||||||
padding-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-card.expanded .signal-card-detail { display: block; }
|
|
||||||
|
|
||||||
.signal-detail-section {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signal-detail-label {
|
.signal-detail-label {
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: .06em;
|
letter-spacing: .06em;
|
||||||
color: var(--muted-dark);
|
color: var(--muted-dark);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-detail-section ul {
|
|
||||||
padding-left: 16px;
|
/* ── signal detail modal ─────────────────────────────────────────────────── */
|
||||||
margin: 0;
|
/* wider than the default modal (920px) and split into two columns so the
|
||||||
font-size: 12px;
|
summary/drivers/risks live alongside the reference event list.
|
||||||
color: var(--muted);
|
fixed height gives it a predictable aspect ratio and lets each column
|
||||||
line-height: 1.7;
|
own its own scroll. */
|
||||||
|
|
||||||
|
.signal-modal {
|
||||||
|
width: 920px;
|
||||||
|
max-width: 95vw;
|
||||||
|
height: 620px;
|
||||||
|
max-height: 92vh;
|
||||||
|
padding: 0; /* reset default modal padding — we manage it per section */
|
||||||
|
overflow: hidden; /* body is the one that scrolls */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-detail-section p {
|
.signal-modal-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 22px 28px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-company {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
color: var(--foreground);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-ticker {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-head .signal-badge {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 10px 28px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.signal-modal-body {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2fr 1fr;
|
||||||
|
min-height: 0; /* required so the columns inside can scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-col {
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-col-main {
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-col-refs {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.signal-modal-section {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-modal-section:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.signal-modal-section p,
|
||||||
|
#signal-modal-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--foreground);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-regen-btn {
|
.signal-modal-section ul {
|
||||||
margin-top: 10px;
|
padding-left: 18px;
|
||||||
font-size: 12px;
|
margin: 0;
|
||||||
padding: 5px 12px;
|
font-size: 13px;
|
||||||
background: transparent;
|
|
||||||
border-color: var(--border);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-regen-btn:hover { color: var(--foreground); background: var(--bg-subtle); }
|
.signal-modal-meta-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
color: var(--muted-dark);
|
||||||
|
font-size: 11.5px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.signal-modal .modal-footer {
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* responsive — stack the two columns on narrow screens */
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.signal-modal-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.signal-modal-col-main {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.signal-ref-event {
|
.signal-ref-event {
|
||||||
border-left: 2px solid var(--border);
|
border-left: 2px solid var(--border);
|
||||||
padding: 2px 0 6px 8px;
|
padding: 4px 0 10px 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signal-ref-event:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
.signal-ref-event-title {
|
.signal-ref-event-title {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-ref-articles {
|
.signal-ref-articles {
|
||||||
padding-left: 14px;
|
list-style: none;
|
||||||
margin: 4px 0 0;
|
padding-left: 0;
|
||||||
font-size: 11.5px;
|
margin: 6px 0 0;
|
||||||
color: var(--muted);
|
font-size: 12px;
|
||||||
line-height: 1.55;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-ref-articles li {
|
.signal-ref-articles li {
|
||||||
margin-bottom: 2px;
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signal-ref-articles li:last-child { border-bottom: none; }
|
||||||
|
|
||||||
.signal-ref-articles a {
|
.signal-ref-articles a {
|
||||||
color: var(--muted);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 1px dashed var(--border);
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signal-ref-articles a:hover { color: var(--foreground); }
|
.signal-ref-articles a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.signal-ref-source {
|
.signal-ref-source {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
// intelligence → signals (trade signal cards)
|
// intelligence → signals (trade signal cards + detail popover)
|
||||||
|
// cards are glance-only; clicking opens a modal with the full breakdown.
|
||||||
// depends on: app.js, intel-shared.js
|
// depends on: app.js, intel-shared.js
|
||||||
|
|
||||||
|
// local lookup of signal rows keyed by company_id — so the modal can
|
||||||
|
// render without re-fetching. references are still lazy-loaded per open.
|
||||||
|
const signalsById = new Map();
|
||||||
|
|
||||||
|
|
||||||
async function loadSignals() {
|
async function loadSignals() {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
|
|
@ -9,6 +15,9 @@ async function loadSignals() {
|
||||||
data = [];
|
data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signalsById.clear();
|
||||||
|
(data || []).forEach(s => signalsById.set(s.company_id, s));
|
||||||
|
|
||||||
const grid = document.getElementById("intel-signals-grid");
|
const grid = document.getElementById("intel-signals-grid");
|
||||||
const empty = document.getElementById("intel-signals-empty");
|
const empty = document.getElementById("intel-signals-empty");
|
||||||
|
|
||||||
|
|
@ -22,21 +31,11 @@ async function loadSignals() {
|
||||||
|
|
||||||
grid.innerHTML = data.map(s => {
|
grid.innerHTML = data.map(s => {
|
||||||
const firstSentence = (s.summary || "").split(/\.\s+/)[0].replace(/\.$/, "") + ".";
|
const firstSentence = (s.summary || "").split(/\.\s+/)[0].replace(/\.$/, "") + ".";
|
||||||
const eventTs = s.latest_event_date ? s.latest_event_date.slice(0, 10) : null;
|
const ts = s.generated_at ? s.generated_at.slice(0, 16).replace("T", " ") : "—";
|
||||||
|
|
||||||
let drivers = [];
|
|
||||||
let risks = [];
|
|
||||||
let predIds = [];
|
|
||||||
try { drivers = JSON.parse(s.key_drivers || "[]"); } catch (_) {}
|
|
||||||
try { risks = JSON.parse(s.risk_factors || "[]"); } catch (_) {}
|
|
||||||
try { predIds = JSON.parse(s.supporting_prediction_ids || "[]"); } catch (_) {}
|
|
||||||
|
|
||||||
const driverItems = drivers.map(d => `<li>${escapeHtml(d)}</li>`).join("");
|
|
||||||
const riskItems = risks.map(r => `<li>${escapeHtml(r)}</li>`).join("");
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="signal-card" id="signal-card-${s.company_id}">
|
<div class="signal-card" onclick="openSignalModal(${s.company_id})">
|
||||||
<div class="signal-card-glance" onclick="toggleSignalCard(${s.company_id})">
|
<div class="signal-card-glance">
|
||||||
<div class="signal-card-header">
|
<div class="signal-card-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="signal-company">${escapeHtml(s.company_name)}</div>
|
<div class="signal-company">${escapeHtml(s.company_name)}</div>
|
||||||
|
|
@ -50,24 +49,7 @@ async function loadSignals() {
|
||||||
<span class="signal-tag">${escapeHtml(s.timeframe)}</span>
|
<span class="signal-tag">${escapeHtml(s.timeframe)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="signal-summary">${escapeHtml(firstSentence)}</div>
|
<div class="signal-summary">${escapeHtml(firstSentence)}</div>
|
||||||
<div class="signal-ts">${eventTs ? `Latest event ${eventTs}` : "No dated event"}</div>
|
<div class="signal-ts">Generated ${ts}</div>
|
||||||
</div>
|
|
||||||
<div class="signal-card-detail">
|
|
||||||
<div class="signal-detail-section">
|
|
||||||
<div class="signal-detail-label">Summary</div>
|
|
||||||
<p>${escapeHtml(s.summary || "—")}</p>
|
|
||||||
</div>
|
|
||||||
${driverItems ? `<div class="signal-detail-section"><div class="signal-detail-label">Key drivers</div><ul>${driverItems}</ul></div>` : ""}
|
|
||||||
${riskItems ? `<div class="signal-detail-section"><div class="signal-detail-label">Risk factors</div><ul>${riskItems}</ul></div>` : ""}
|
|
||||||
<div class="signal-detail-section">
|
|
||||||
<div class="signal-detail-label">Data used</div>
|
|
||||||
<p style="color:var(--muted-dark)">${predIds.length} predictions · window: 90 days</p>
|
|
||||||
</div>
|
|
||||||
<div class="signal-detail-section" id="signal-refs-${s.company_id}" data-loaded="0">
|
|
||||||
<div class="signal-detail-label">References</div>
|
|
||||||
<p style="color:var(--muted-dark)">Loading…</p>
|
|
||||||
</div>
|
|
||||||
<button class="signal-regen-btn" onclick="regenerateSignal(event, ${s.company_id})">Regenerate</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -75,47 +57,98 @@ async function loadSignals() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function toggleSignalCard(companyId) {
|
let currentSignal = null;
|
||||||
const card = document.getElementById(`signal-card-${companyId}`);
|
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
card.classList.toggle("expanded");
|
function openSignalModal(companyId) {
|
||||||
|
const s = signalsById.get(companyId);
|
||||||
|
if (!s) return;
|
||||||
|
|
||||||
|
currentSignal = s;
|
||||||
|
|
||||||
|
document.getElementById("signal-modal-company").textContent = s.company_name || "";
|
||||||
|
document.getElementById("signal-modal-ticker").textContent = s.ticker || "";
|
||||||
|
|
||||||
|
const badge = document.getElementById("signal-modal-badge");
|
||||||
|
badge.className = `signal-badge ${s.signal}`;
|
||||||
|
badge.textContent = s.signal;
|
||||||
|
|
||||||
|
document.getElementById("signal-modal-tags").innerHTML = [
|
||||||
|
`<span class="signal-tag">conf: ${escapeHtml(s.confidence)}</span>`,
|
||||||
|
`<span class="signal-tag">risk: ${escapeHtml(s.risk_level)}</span>`,
|
||||||
|
`<span class="signal-tag">${escapeHtml(s.timeframe)}</span>`,
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
document.getElementById("signal-modal-summary").textContent = s.summary || "—";
|
||||||
|
|
||||||
|
let drivers = [];
|
||||||
|
let risks = [];
|
||||||
|
let predIds = [];
|
||||||
|
try { drivers = JSON.parse(s.key_drivers || "[]"); } catch (_) {}
|
||||||
|
try { risks = JSON.parse(s.risk_factors || "[]"); } catch (_) {}
|
||||||
|
try { predIds = JSON.parse(s.supporting_prediction_ids || "[]"); } catch (_) {}
|
||||||
|
|
||||||
|
const driversWrap = document.getElementById("signal-modal-drivers-wrap");
|
||||||
|
const risksWrap = document.getElementById("signal-modal-risks-wrap");
|
||||||
|
|
||||||
|
if (drivers.length) {
|
||||||
|
driversWrap.style.display = "";
|
||||||
|
document.getElementById("signal-modal-drivers").innerHTML =
|
||||||
|
drivers.map(d => `<li>${escapeHtml(d)}</li>`).join("");
|
||||||
|
} else {
|
||||||
|
driversWrap.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (risks.length) {
|
||||||
|
risksWrap.style.display = "";
|
||||||
|
document.getElementById("signal-modal-risks").innerHTML =
|
||||||
|
risks.map(r => `<li>${escapeHtml(r)}</li>`).join("");
|
||||||
|
} else {
|
||||||
|
risksWrap.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("signal-modal-datausage").textContent =
|
||||||
|
`${predIds.length} predictions · 90d window`;
|
||||||
|
document.getElementById("signal-modal-ts").textContent =
|
||||||
|
s.generated_at ? s.generated_at.slice(0, 16).replace("T", " ") : "—";
|
||||||
|
|
||||||
|
// reset references panel and lazy-load
|
||||||
|
document.getElementById("signal-modal-refs").innerHTML =
|
||||||
|
`<p style="color:var(--muted-dark)">Loading…</p>`;
|
||||||
|
|
||||||
|
document.getElementById("signalOverlay").classList.add("open");
|
||||||
|
|
||||||
if (card.classList.contains("expanded")) {
|
|
||||||
loadSignalReferences(companyId);
|
loadSignalReferences(companyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function closeSignalModal() {
|
||||||
|
document.getElementById("signalOverlay").classList.remove("open");
|
||||||
|
currentSignal = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadSignalReferences(companyId) {
|
async function loadSignalReferences(companyId) {
|
||||||
const container = document.getElementById(`signal-refs-${companyId}`);
|
const container = document.getElementById("signal-modal-refs");
|
||||||
if (!container || container.dataset.loaded === "1") return;
|
|
||||||
|
|
||||||
container.dataset.loaded = "1";
|
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await api(`/admin/api/intelligence/signals/${companyId}/references`);
|
data = await api(`/admin/api/intelligence/signals/${companyId}/references`);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `<p style="color:var(--muted-dark)">Failed to load references</p>`;
|
||||||
<div class="signal-detail-label">References</div>
|
|
||||||
<p style="color:var(--muted-dark)">Failed to load references</p>
|
|
||||||
`;
|
|
||||||
container.dataset.loaded = "0";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// guard against stale responses if the user closed/switched modals mid-request
|
||||||
|
if (!currentSignal || currentSignal.company_id !== companyId) return;
|
||||||
|
|
||||||
const events = data?.events || [];
|
const events = data?.events || [];
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `<p style="color:var(--muted-dark)">No linked events</p>`;
|
||||||
<div class="signal-detail-label">References</div>
|
|
||||||
<p style="color:var(--muted-dark)">No linked events</p>
|
|
||||||
`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = events.map(ev => {
|
container.innerHTML = events.map(ev => {
|
||||||
const dateStr = (ev.event_date || ev.created_at || "").slice(0, 10);
|
const dateStr = (ev.event_date || ev.created_at || "").slice(0, 10);
|
||||||
|
|
||||||
const articles = (ev.articles || []).map(a => {
|
const articles = (ev.articles || []).map(a => {
|
||||||
|
|
@ -139,27 +172,27 @@ async function loadSignalReferences(companyId) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="signal-detail-label">References</div>
|
|
||||||
${blocks}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function regenerateSignal(ev, companyId) {
|
async function regenerateSignal() {
|
||||||
ev.stopPropagation();
|
if (!currentSignal) return;
|
||||||
|
const companyId = currentSignal.company_id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api(`/admin/api/intelligence/signals/${companyId}`, { method: "DELETE" });
|
await api(`/admin/api/intelligence/signals/${companyId}`, { method: "DELETE" });
|
||||||
toast("Signal cleared — worker will regenerate on next cycle");
|
toast("Signal cleared — worker will regenerate on next cycle");
|
||||||
const card = document.getElementById(`signal-card-${companyId}`);
|
signalsById.delete(companyId);
|
||||||
|
|
||||||
|
const card = document.querySelector(`.signal-card[onclick*="(${companyId})"]`);
|
||||||
if (card) card.remove();
|
if (card) card.remove();
|
||||||
|
|
||||||
const grid = document.getElementById("intel-signals-grid");
|
const grid = document.getElementById("intel-signals-grid");
|
||||||
if (!grid.children.length) {
|
if (!grid.children.length) {
|
||||||
document.getElementById("intel-signals-empty").style.display = "";
|
document.getElementById("intel-signals-empty").style.display = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeSignalModal();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
toast("Failed to clear signal", true);
|
toast("Failed to clear signal", true);
|
||||||
}
|
}
|
||||||
|
|
@ -167,6 +200,9 @@ async function regenerateSignal(ev, companyId) {
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.getElementById("signalCloseBtn").onclick = closeSignalModal;
|
||||||
|
document.getElementById("signalRegenBtn").onclick = regenerateSignal;
|
||||||
|
|
||||||
// fire both in parallel — signals api returns [] when intelligence
|
// fire both in parallel — signals api returns [] when intelligence
|
||||||
// db is unavailable, so no need to gate on the stats call
|
// db is unavailable, so no need to gate on the stats call
|
||||||
Promise.all([loadIntelStatsRow(), loadSignals()]);
|
Promise.all([loadIntelStatsRow(), loadSignals()]);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<link rel="stylesheet" href="/admin/assets/css/components.css">
|
<link rel="stylesheet" href="/admin/assets/css/components.css">
|
||||||
<link rel="stylesheet" href="/admin/assets/css/intel.css">
|
<link rel="stylesheet" href="/admin/assets/css/intel.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="page-graph">
|
||||||
|
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<h1>Duriin <span>Admin</span></h1>
|
<h1>Duriin <span>Admin</span></h1>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,62 @@
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- signal detail modal — two-column layout, opened via card click -->
|
||||||
|
<div class="overlay" id="signalOverlay">
|
||||||
|
<div class="modal signal-modal">
|
||||||
|
|
||||||
|
<div class="signal-modal-head">
|
||||||
|
<div>
|
||||||
|
<div id="signal-modal-company" class="signal-modal-company"></div>
|
||||||
|
<div id="signal-modal-ticker" class="signal-modal-ticker"></div>
|
||||||
|
</div>
|
||||||
|
<span id="signal-modal-badge" class="signal-badge"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="signal-modal-tags" class="signal-modal-tags"></div>
|
||||||
|
|
||||||
|
<div class="signal-modal-body">
|
||||||
|
|
||||||
|
<div class="signal-modal-col signal-modal-col-main">
|
||||||
|
<section class="signal-modal-section">
|
||||||
|
<div class="signal-detail-label">Summary</div>
|
||||||
|
<p id="signal-modal-summary"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="signal-modal-section" id="signal-modal-drivers-wrap">
|
||||||
|
<div class="signal-detail-label">Key drivers</div>
|
||||||
|
<ul id="signal-modal-drivers"></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="signal-modal-section" id="signal-modal-risks-wrap">
|
||||||
|
<div class="signal-detail-label">Risk factors</div>
|
||||||
|
<ul id="signal-modal-risks"></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="signal-modal-section signal-modal-meta-row">
|
||||||
|
<span id="signal-modal-datausage"></span>
|
||||||
|
<span>Generated <span id="signal-modal-ts"></span></span>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signal-modal-col signal-modal-col-refs">
|
||||||
|
<div class="signal-detail-label">References</div>
|
||||||
|
<div id="signal-modal-refs"><p style="color:var(--muted-dark)">Loading…</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="signalRegenBtn" class="danger">Regenerate</button>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button id="signalCloseBtn">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="toast"><span class="toast-dot"></span><span id="toast-msg"></span></div>
|
<div id="toast"><span class="toast-dot"></span><span id="toast-msg"></span></div>
|
||||||
|
|
||||||
<script src="/admin/assets/js/app.js"></script>
|
<script src="/admin/assets/js/app.js"></script>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue