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 @@