From 0c28cf921f0120616cf95d0a74402ae99b35446e Mon Sep 17 00:00:00 2001 From: momentum dev Date: Fri, 31 Jul 2026 18:40:49 -0500 Subject: [PATCH] feat: one-click source links on catalyst feed headlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every LLM catalyst read's headline now links to the raw article (raw_news_items.raw_url joined by headline), styled as a distinct dotted underline per NN/g citation guidance — any AI verdict traces to its source in one click. Rows without a matching raw item degrade to plain text. Join verified against prod: 0.29s for the full feed query. Co-Authored-By: Claude Fable 5 --- api/static/dashboard.html | 8 +++++++- storage/projections.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/api/static/dashboard.html b/api/static/dashboard.html index f4d2b96..ccaf1f3 100644 --- a/api/static/dashboard.html +++ b/api/static/dashboard.html @@ -322,6 +322,10 @@ .cf-nums { width: 40px; text-align: right; flex: none; color: var(--muted); font: 500 10px var(--mono); } .cf-text { flex: 1; min-width: 0; } .cf-head { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + /* one-click trace-back to the source article (NN/g: citations visible and + styled distinctly, next to the claim) */ + .cf-link { color: inherit; text-decoration: underline dotted; text-decoration-color: var(--accent); text-underline-offset: 3px; } + .cf-link:hover { color: var(--accent); } .cf-why { color: var(--muted); font-style: italic; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* clickable engine chip + its filter indicator */ @@ -1281,7 +1285,9 @@ function renderCatalystFeed(feed) { + '
' + '' + conv.toFixed(2) + '
' + '
' - + '
' + esc(r.headline || '') + '
' + + '
' + (r.url + ? '' + esc(r.headline || '') + ' ↗' + : esc(r.headline || '')) + '
' + (r.rationale ? '
“' + esc(r.rationale) + '”
' : '') + '
' + ''; diff --git a/storage/projections.py b/storage/projections.py index ac64e4d..31be1dd 100644 --- a/storage/projections.py +++ b/storage/projections.py @@ -984,7 +984,12 @@ def query_catalyst_feed(store, limit: int = 40, lookback_hours: int = 24) -> lis try: rows = store.con.execute( "SELECT symbol, catalyst_type, sentiment, conviction, is_dilutive, " - "rationale, headline, source, model, enriched_at " + "rationale, headline, source, model, enriched_at, " + # source URL for one-click trace-back (cache stores no URL; the raw + # item that produced the headline does) + "(SELECT r.raw_url FROM raw_news_items r " + " WHERE r.raw_title = news_catalyst_cache.headline " + " AND r.raw_url IS NOT NULL LIMIT 1) AS url " "FROM news_catalyst_cache WHERE enriched_at >= ? " "ORDER BY enriched_at DESC LIMIT ?", [cutoff, int(limit)], @@ -992,10 +997,11 @@ def query_catalyst_feed(store, limit: int = 40, lookback_hours: int = 24) -> lis except Exception: # noqa: BLE001 — table may not exist / Ollama never ran return [] out: list[dict] = [] - for sym, ctype, sentiment, conv, is_dil, rationale, headline, source, model, ts in rows: + for sym, ctype, sentiment, conv, is_dil, rationale, headline, source, model, ts, url in rows: conv_f = float(conv) if conv is not None else None is_dil_b = bool(is_dil) out.append({ + "url": url, "symbol": (sym or "").upper(), "catalyst_type": ctype, "sentiment": round(float(sentiment), 3) if sentiment is not None else None,