dashboard: clickable news — detail dialogs + on-demand AI summary endpoint
- news_engine: RSS parser captures link/description/publisher (three new optional NewsSentiment fields); _clean_html_text/_safe_link helpers - news.json: headlines carry id = sha1(headline) + enrichment fields - static/news_summary.php (NEW): on-demand DeepSeek gold-impact summary, whitelist-gated against news.json ids, cache-forever in ai/, fail-closed file-based daily cap (150/day), key at /etc/gold-dashboard/deepseek.key; installed to the docroot by install_static - dashboard.html: newswire + calendar detail dialogs (score tiles, source description, article link, AI summary with loading/error states); delegated on stable containers, dialog keyed by id across polls - tests: +6 (357 total); docs: README-dashboard §6 one-time setup (key file, apache-owned ai/, SELinux httpd_can_network_connect), CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aq1jZ6jBsBdEC2xT3u46SA
This commit is contained in:
+27
-10
@@ -37,6 +37,7 @@ Out dir precedence: --out → DASHBOARD_OUT_DIR env → /var/www/trade
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import math
|
||||
import numbers
|
||||
@@ -76,6 +77,11 @@ SCAN_BUFFER_SECONDS = 10
|
||||
SNAPSHOT_FILE = "snapshot.json"
|
||||
NEWS_FILE = "news.json"
|
||||
PAGE_FILE = "dashboard.html"
|
||||
SUMMARY_PHP_FILE = "news_summary.php"
|
||||
# Repo files copied into the out dir at daemon start (git pull + restart
|
||||
# updates them). The PHP endpoint reads news.json in place, so it lives
|
||||
# next to the page.
|
||||
STATIC_FILES = [PAGE_FILE, SUMMARY_PHP_FILE]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
@@ -180,9 +186,15 @@ def event_to_dict(ev) -> dict:
|
||||
|
||||
|
||||
def headline_to_dict(h) -> dict:
|
||||
# id uses the same sha1(headline) scheme as NewsEngine._llm_sent_cache;
|
||||
# it is the join key the PHP summary endpoint whitelists against.
|
||||
return {
|
||||
"id": hashlib.sha1(h.headline.encode("utf-8")).hexdigest(),
|
||||
"headline": h.headline,
|
||||
"source": h.source,
|
||||
"link": h.link,
|
||||
"description": h.description,
|
||||
"publisher": h.publisher,
|
||||
"timestamp": h.timestamp.astimezone(timezone.utc).isoformat()
|
||||
if h.timestamp else None,
|
||||
"currencies": list(h.currencies_mentioned),
|
||||
@@ -324,16 +336,21 @@ class DashboardPublisher:
|
||||
return self._news_engine
|
||||
|
||||
def install_static(self) -> None:
|
||||
"""Copy the repo's dashboard.html into the out dir (atomic), so
|
||||
git pull + restart also updates the page."""
|
||||
src = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"static", PAGE_FILE)
|
||||
if not os.path.exists(src):
|
||||
logger.warning(f"{src} missing — page not installed")
|
||||
return
|
||||
dst = os.path.join(self.out_dir, PAGE_FILE)
|
||||
shutil.copyfile(src, dst + ".tmp")
|
||||
os.replace(dst + ".tmp", dst)
|
||||
"""Copy the repo's static files (page + PHP summary endpoint)
|
||||
into the out dir (atomic), so git pull + restart also updates
|
||||
them. The ai/ cache dir is NOT created here — the daemon runs as
|
||||
a different user than Apache; that dir is a one-time deploy step
|
||||
(deploy/README-dashboard.md)."""
|
||||
static_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "static")
|
||||
for name in STATIC_FILES:
|
||||
src = os.path.join(static_dir, name)
|
||||
if not os.path.exists(src):
|
||||
logger.warning(f"{src} missing — not installed")
|
||||
continue
|
||||
dst = os.path.join(self.out_dir, name)
|
||||
shutil.copyfile(src, dst + ".tmp")
|
||||
os.replace(dst + ".tmp", dst)
|
||||
|
||||
def scan_tf(self, tf: Timeframe) -> None:
|
||||
started = time.time()
|
||||
|
||||
Reference in New Issue
Block a user