diff --git a/dashboard_publisher.py b/dashboard_publisher.py index 4f9c239..fe1844b 100644 --- a/dashboard_publisher.py +++ b/dashboard_publisher.py @@ -82,8 +82,7 @@ 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, "dashboard.css", "dashboard.js", "gauge.js", - SUMMARY_PHP_FILE] +STATIC_FILES = [PAGE_FILE, "dashboard.css", "dashboard.js", SUMMARY_PHP_FILE] # ══════════════════════════════════════════════════════════════════ @@ -323,6 +322,7 @@ class DashboardPublisher: def __init__(self, out_dir: str): self.out_dir = out_dir self.snapshots: Dict[str, dict] = {} + self._reports = {} self.news_payload: Optional[dict] = None self._dp = None self._news_engine: Optional[NewsEngine] = None @@ -359,13 +359,7 @@ class DashboardPublisher: try: report = produce_signal(XAUUSD, tf, self._provider(), CONFIRMATIONS[tf]) - # Advisory AI panel, post-hoc and display-only. Off unless - # config.json agent_panel.dashboard is true. The reuse-TTL - # cache + max_panels_per_day budget bound the spend against - # this process's own DeepSeek cap (a persisting M5 signal - # re-serves its cached verdict instead of re-running every - # bar); a fresh panel adds ≤~25 s before this TF publishes. - attach_panel(report, surface="dashboard") + self._reports[tf.value] = report self.snapshots[tf.value] = { "status": "ok", "scanned_at": datetime.now(timezone.utc).isoformat(), @@ -377,16 +371,41 @@ class DashboardPublisher: logger.info(f"{tf.value}: {report.final_signal.name} " f"q={report.signal_quality or '-'} " f"({time.time() - started:.1f}s)") - except Exception as e: + except Exception: # Keep the last good report; the page shows it with an # error badge instead of going blank. logger.exception(f"{tf.value} scan failed") + self._reports.pop(tf.value, None) entry = self.snapshots.get(tf.value) or _empty_tf_entry() entry["status"] = "error" - entry["error"] = f"{type(e).__name__}: {e}" + # The public snapshot carries a stable message; exception + # detail remains in dashboard.log for the operator. + entry["error"] = "Scan unavailable" entry["error_at"] = datetime.now(timezone.utc).isoformat() self.snapshots[tf.value] = entry + def attach_panels(self, tfs: List[Timeframe]) -> bool: + """Add advisory panels after publishing the technical snapshot. + + A panel is display-only and can take tens of seconds on a cold LLM + request. Keeping its report object locally lets the page receive the + candle-close signal first, then a second atomic snapshot only when a + panel produced a result. + """ + updated = False + for tf in tfs: + report = self._reports.get(tf.value) + if report is None: + continue + if not attach_panel(report, surface="dashboard"): + continue + entry = self.snapshots.get(tf.value) + if entry is None or entry.get("status") != "ok": + continue + entry["report"] = report_to_webapp_dict(report) + updated = True + return updated + def refresh_news(self) -> None: try: ne = self._news() @@ -413,11 +432,11 @@ class DashboardPublisher: def run_cycle(self, tfs: List[Timeframe]) -> None: for tf in tfs: # sequential — see module docstring (cache dedup) self.scan_tf(tf) - # Snapshot lands BEFORE the news refresh: when the news TTL has - # expired, refresh_news does FF + 4 RSS fetches (and possibly - # DeepSeek calls) — 20-30 s that must not keep fresh signals off - # the page. + # Technical signals land before any optional slow work. News and the + # advisory panel are display-only and may take 20-30 seconds cold. self.publish_snapshot() + if self.attach_panels(tfs): + self.publish_snapshot() self.refresh_news() self.publish_news() diff --git a/static/dashboard.css b/static/dashboard.css index d1694c6..7c6a226 100644 --- a/static/dashboard.css +++ b/static/dashboard.css @@ -96,17 +96,6 @@ html, body { color: var(--xau-signal, var(--bs-secondary)) !important; } -/* timeframe cards */ -.tf-card { - cursor: pointer; - transition: border-color 0.15s ease, box-shadow 0.15s ease; -} - -.tf-card:hover, .tf-card:focus-visible { - border-color: var(--bs-primary); - box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.15); -} - .tf-name { font-family: var(--xau-font-mono); font-weight: 700; @@ -115,6 +104,58 @@ html, body { letter-spacing: 0.05em; } +.tf-selector { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.tf-select { + min-width: 0; + padding: 0.7rem 0.8rem; + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + background: var(--bs-tertiary-bg); + color: var(--bs-body-color); + text-align: left; + transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; +} + +.tf-select:hover { + border-color: rgba(var(--bs-primary-rgb), 0.65); +} + +.tf-select[aria-pressed="true"] { + border-color: var(--xau-gold); + background: rgba(245, 179, 1, 0.09); + box-shadow: inset 0 0 0 1px rgba(245, 179, 1, 0.16); +} + +.tf-select .tf-name { + font-size: 1rem; +} + +.workspace-card { + overflow: hidden; +} + +.workspace-signal { + font-family: var(--xau-font-mono); + font-size: clamp(1.55rem, 3vw, 2.35rem); + font-weight: 700; + letter-spacing: 0.03em; + line-height: 1; +} + +.workspace-meta { + min-width: 0; +} + +.workspace-reasons > div + div { + margin-top: 0.35rem; +} + /* score bar: zero-centered diverging with midpoint tick */ .scorebar { display: flex; @@ -122,24 +163,17 @@ html, body { gap: 0.75rem; } -.scorebar .track { +.scorebar .gauge-container { position: relative; flex: 1; - height: 9px; + min-width: 60px; + height: 10px; border-radius: 5px; background: var(--bs-secondary-bg); overflow: hidden; } -/* Chart.js gauge canvas host (see gauge.js) — thin bar in the row */ -.scorebar .gauge-container { - position: relative; - flex: 1; - min-width: 60px; - height: 22px; -} - -.scorebar .track::after { +.scorebar .gauge-container::after { content: ""; position: absolute; left: 50%; @@ -155,7 +189,8 @@ html, body { top: 0; bottom: 0; border-radius: 4px; - transition: width 0.5s ease; + width: var(--score-width); + transition: width 0.25s ease; } .scorebar .fill.pos { @@ -183,13 +218,24 @@ html, body { /* rail chips in hero */ .rail-chip { - cursor: pointer; + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + background: var(--bs-tertiary-bg); + color: var(--bs-body-color); font-family: var(--xau-font-mono); font-size: 0.9rem; + line-height: 1.3; + padding: 0.3rem 0.5rem; } -.rail-chip:hover { - border-color: var(--bs-primary) !important; +.rail-chip:hover, .rail-chip[aria-current="true"] { + border-color: var(--xau-gold); +} + +.rail-chip:focus-visible, .tf-select:focus-visible, .wire-item:focus-visible, +.cal-event:focus-visible { + outline: 2px solid var(--xau-gold); + outline-offset: 2px; } /* detail dialog sections */ @@ -211,6 +257,10 @@ html, body { font-size: 0.9rem; } +.detail-table { + overflow-x: auto; +} + .d-table .ind-k { font-family: var(--xau-font-mono); font-weight: 600; @@ -234,14 +284,36 @@ html, body { /* news */ .wire-item { - cursor: pointer; + display: flex; + width: 100%; + border: 0; border-radius: var(--bs-border-radius); + background: transparent; + color: inherit; + cursor: pointer; + font: inherit; + text-align: start; } .wire-item:hover { background: rgba(var(--bs-primary-rgb), 0.08); } +.cal-event { + display: block; + width: 100%; + padding: 0; + border: 0; + background: transparent; + color: inherit; + text-align: inherit; +} + +.cal-event:hover { + color: var(--bs-primary); + text-decoration: underline; +} + .wire-score { font-family: var(--xau-font-mono); font-size: 0.85rem; @@ -263,3 +335,17 @@ html, body { .scorebar .fill { transition: none; } * { transition: none !important; } } + +@media (max-width: 575.98px) { + .tf-selector { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .workspace-card .card-body { + padding: 1rem; + } + + .rail-chip { + flex: 1 1 calc(50% - 0.5rem); + } +} diff --git a/static/dashboard.html b/static/dashboard.html index 94faeb3..6dc0a29 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -11,7 +11,6 @@ - @@ -20,12 +19,12 @@
-
XAU/USD
+

XAU/USD

—
-
+
-
…
+
…
M5 close in --:--
updated —
@@ -36,68 +35,75 @@ Market closed (weekend) — showing Friday's last state. Calendar and news stay live.
+
-
-
-
-
-
-
+
+
+
+
Market context
+
+
+
-
- - Timeframes - — select one for full detail -
-
+
+

+ + Signal workspace + — select a timeframe +

+
+
-
- Fundamentals -
-
-
-
-
-
-
Economic calendar
- times local · USD events -
- - +
+

+ Fundamentals +

+
+
+
+
+
+

Economic calendar

+ times local · USD events +
+ + +
+
+
+ + + + + + + + + + + + + +
TimeCurImpactEventFcstPrevActual
Loading calendar…
+
-
- - - - - - - - - - - - - -
TimeCurImpactEventFcstPrevActual
Loading calendar…
+
+
+
+
+
+

Newswire

+ +
+
+
+
-
-
-
-
-
-
Newswire
- -
-
-
-
-
-
-
+ +