From 69206260d30a5adbf74b816995f2efc4682a68b1 Mon Sep 17 00:00:00 2001 From: ImBenji Date: Fri, 24 Apr 2026 00:21:52 +0100 Subject: [PATCH] refactor admin navigation; update links to ingest pages and improve loading of data in parallel --- public/admin/assets/css/intel.css | 218 +++++++++++++++---- public/admin/assets/js/intel-signals.js | 160 ++++++++------ public/admin/pages/intelligence/graph.html | 2 +- public/admin/pages/intelligence/signals.html | 56 +++++ 4 files changed, 330 insertions(+), 106 deletions(-) diff --git a/public/admin/assets/css/intel.css b/public/admin/assets/css/intel.css index 4c6d97d..18b8175 100644 --- a/public/admin/assets/css/intel.css +++ b/public/admin/assets/css/intel.css @@ -75,10 +75,45 @@ /* ── 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 { display: flex; - gap: 14px; + gap: 12px; } #intel-graph-svg-wrap { @@ -88,7 +123,7 @@ border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; - height: 600px; + height: 600px; /* fallback for any other page that might reuse this */ } #intel-graph-svg { @@ -498,90 +533,187 @@ 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 { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-dark); font-weight: 600; - margin-bottom: 5px; + margin-bottom: 6px; } -.signal-detail-section ul { - padding-left: 16px; - margin: 0; - font-size: 12px; - color: var(--muted); - line-height: 1.7; + +/* ── signal detail modal ─────────────────────────────────────────────────── */ +/* wider than the default modal (920px) and split into two columns so the + summary/drivers/risks live alongside the reference event list. + fixed height gives it a predictable aspect ratio and lets each column + 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; 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; margin: 0; } -.signal-regen-btn { - margin-top: 10px; - font-size: 12px; - padding: 5px 12px; - background: transparent; - border-color: var(--border); +.signal-modal-section ul { + padding-left: 18px; + margin: 0; + font-size: 13px; 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 { border-left: 2px solid var(--border); - padding: 2px 0 6px 8px; - margin-bottom: 8px; + padding: 4px 0 10px 10px; + margin-bottom: 12px; } +.signal-ref-event:last-child { margin-bottom: 0; } + .signal-ref-event-title { - font-size: 12px; + font-size: 13px; color: var(--foreground); - font-weight: 500; + font-weight: 600; display: flex; justify-content: space-between; gap: 8px; align-items: baseline; + line-height: 1.3; } .signal-ref-articles { - padding-left: 14px; - margin: 4px 0 0; - font-size: 11.5px; - color: var(--muted); - line-height: 1.55; + list-style: none; + padding-left: 0; + margin: 6px 0 0; + font-size: 12px; + line-height: 1.5; } .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 { - color: var(--muted); + color: var(--accent); 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 { margin-left: 6px; diff --git a/public/admin/assets/js/intel-signals.js b/public/admin/assets/js/intel-signals.js index c326dd3..3770685 100644 --- a/public/admin/assets/js/intel-signals.js +++ b/public/admin/assets/js/intel-signals.js @@ -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 +// 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() { let data; try { @@ -9,6 +15,9 @@ async function loadSignals() { data = []; } + signalsById.clear(); + (data || []).forEach(s => signalsById.set(s.company_id, s)); + const grid = document.getElementById("intel-signals-grid"); const empty = document.getElementById("intel-signals-empty"); @@ -22,21 +31,11 @@ async function loadSignals() { grid.innerHTML = data.map(s => { const firstSentence = (s.summary || "").split(/\.\s+/)[0].replace(/\.$/, "") + "."; - const eventTs = s.latest_event_date ? s.latest_event_date.slice(0, 10) : null; - - 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 => `
  • ${escapeHtml(d)}
  • `).join(""); - const riskItems = risks.map(r => `
  • ${escapeHtml(r)}
  • `).join(""); + const ts = s.generated_at ? s.generated_at.slice(0, 16).replace("T", " ") : "—"; return ` -
    -
    +
    +
    ${escapeHtml(s.company_name)}
    @@ -50,24 +49,7 @@ async function loadSignals() { ${escapeHtml(s.timeframe)}
    ${escapeHtml(firstSentence)}
    -
    ${eventTs ? `Latest event ${eventTs}` : "No dated event"}
    -
    -
    -
    -
    Summary
    -

    ${escapeHtml(s.summary || "—")}

    -
    - ${driverItems ? `
    Key drivers
      ${driverItems}
    ` : ""} - ${riskItems ? `
    Risk factors
      ${riskItems}
    ` : ""} -
    -
    Data used
    -

    ${predIds.length} predictions  ·  window: 90 days

    -
    -
    -
    References
    -

    Loading…

    -
    - +
    Generated ${ts}
    `; @@ -75,52 +57,103 @@ async function loadSignals() { } -function toggleSignalCard(companyId) { - const card = document.getElementById(`signal-card-${companyId}`); - if (!card) return; +let currentSignal = null; - card.classList.toggle("expanded"); +function openSignalModal(companyId) { + const s = signalsById.get(companyId); + if (!s) return; - if (card.classList.contains("expanded")) { - loadSignalReferences(companyId); + 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 = [ + `conf: ${escapeHtml(s.confidence)}`, + `risk: ${escapeHtml(s.risk_level)}`, + `${escapeHtml(s.timeframe)}`, + ].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 => `
  • ${escapeHtml(d)}
  • `).join(""); + } else { + driversWrap.style.display = "none"; } + + if (risks.length) { + risksWrap.style.display = ""; + document.getElementById("signal-modal-risks").innerHTML = + risks.map(r => `
  • ${escapeHtml(r)}
  • `).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 = + `

    Loading…

    `; + + document.getElementById("signalOverlay").classList.add("open"); + + loadSignalReferences(companyId); +} + + +function closeSignalModal() { + document.getElementById("signalOverlay").classList.remove("open"); + currentSignal = null; } async function loadSignalReferences(companyId) { - const container = document.getElementById(`signal-refs-${companyId}`); - if (!container || container.dataset.loaded === "1") return; - - container.dataset.loaded = "1"; + const container = document.getElementById("signal-modal-refs"); let data; try { data = await api(`/admin/api/intelligence/signals/${companyId}/references`); } catch (_) { - container.innerHTML = ` -
    References
    -

    Failed to load references

    - `; - container.dataset.loaded = "0"; + container.innerHTML = `

    Failed to load references

    `; return; } + // guard against stale responses if the user closed/switched modals mid-request + if (!currentSignal || currentSignal.company_id !== companyId) return; + const events = data?.events || []; if (!events.length) { - container.innerHTML = ` -
    References
    -

    No linked events

    - `; + container.innerHTML = `

    No linked events

    `; return; } - const blocks = events.map(ev => { + container.innerHTML = events.map(ev => { const dateStr = (ev.event_date || ev.created_at || "").slice(0, 10); const articles = (ev.articles || []).map(a => { - const src = a.source ? `${escapeHtml(a.source)}` : ""; - const pd = a.pub_date ? `${escapeHtml(a.pub_date.slice(0, 10))}` : ""; + const src = a.source ? `${escapeHtml(a.source)}` : ""; + const pd = a.pub_date ? `${escapeHtml(a.pub_date.slice(0, 10))}` : ""; return `
  • ${escapeHtml(a.title || a.url)} @@ -139,27 +172,27 @@ async function loadSignalReferences(companyId) {
  • `; }).join(""); - - container.innerHTML = ` -
    References
    - ${blocks} - `; } -async function regenerateSignal(ev, companyId) { - ev.stopPropagation(); +async function regenerateSignal() { + if (!currentSignal) return; + const companyId = currentSignal.company_id; try { await api(`/admin/api/intelligence/signals/${companyId}`, { method: "DELETE" }); 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(); const grid = document.getElementById("intel-signals-grid"); if (!grid.children.length) { document.getElementById("intel-signals-empty").style.display = ""; } + + closeSignalModal(); } catch (_) { toast("Failed to clear signal", true); } @@ -167,6 +200,9 @@ async function regenerateSignal(ev, companyId) { document.addEventListener("DOMContentLoaded", () => { + document.getElementById("signalCloseBtn").onclick = closeSignalModal; + document.getElementById("signalRegenBtn").onclick = regenerateSignal; + // fire both in parallel — signals api returns [] when intelligence // db is unavailable, so no need to gate on the stats call Promise.all([loadIntelStatsRow(), loadSignals()]); diff --git a/public/admin/pages/intelligence/graph.html b/public/admin/pages/intelligence/graph.html index 9ec6b9f..a6ad99b 100644 --- a/public/admin/pages/intelligence/graph.html +++ b/public/admin/pages/intelligence/graph.html @@ -9,7 +9,7 @@ - +

    Duriin Admin

    diff --git a/public/admin/pages/intelligence/signals.html b/public/admin/pages/intelligence/signals.html index 2ba60f2..25bff82 100644 --- a/public/admin/pages/intelligence/signals.html +++ b/public/admin/pages/intelligence/signals.html @@ -43,6 +43,62 @@ + + +
    + +
    + +