feat: one-click source links on catalyst feed headlines

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 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-31 18:40:49 -05:00
1 parent 81bf1540ef
commit 0c28cf921f
2 files changed
+15 -3

No files matched your search

+7 -1
View File
@@ -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) {
+ '<div class="cf-nums" title="sentiment ' + sentTxt + ' · conviction ' + conv.toFixed(2) + '">'
+ '<span class="cf-conv">' + conv.toFixed(2) + '</span></div>'
+ '<div class="cf-text">'
+ '<div class="cf-head">' + esc(r.headline || '') + '</div>'
+ '<div class="cf-head">' + (r.url
? '<a class="cf-link" href="' + esc(r.url) + '" target="_blank" rel="noopener">' + esc(r.headline || '') + ' ↗</a>'
: esc(r.headline || '')) + '</div>'
+ (r.rationale ? '<div class="cf-why">“' + esc(r.rationale) + '”</div>' : '')
+ '</div>'
+ '</div>';
+8 -2
View File
@@ -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,