fix: dashboard research-DB connection self-heals — the 'TradingView failing' root cause

The charts weren't broken — the data pipe was empty. dashboard_api opened the
research connection ONCE at boot; if the DB was unreachable then (container
started before Postgres, or a blip) it stayed None forever, so /api/chart and
/api/bars returned 'no research db attached' and every candlestick + sparkline
rendered blank. Live-verified: GET /api/chart returned bars:[] reason:'no
research db attached' on the deployed dashboard. Now DashboardState._research()
opens the connection LAZILY and reopens if None — same self-heal as the
event-store emit fix. 2 tests; chart/backtest endpoint tests still green.
No library change: Lightweight Charts v5.2 is fine, it just had nothing to draw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-20 12:04:43 -05:00
1 parent 5f04d5584a
commit a00f967bdf
2 files changed
+75 -6

No files matched your search

+34 -6
View File
@@ -63,6 +63,12 @@ from storage.projections import (
query_watch_states_snapshot,
)
import logging
from research.multi_schema import open_research_db
logger = logging.getLogger(__name__)
STATIC_DIR = Path(__file__).parent / "static"
@@ -250,6 +256,26 @@ class DashboardState:
self.research_con = research_con
self.watch_inject = watch_inject
self._lock = threading.Lock()
self._research_lock = threading.Lock()
def _research(self):
"""The research DB connection, opened LAZILY and self-healing. It used
to be opened once at boot; if the DB was unreachable then (container
started before Postgres, or a blip), it stayed None forever and every
chart/sparkline/backtest returned 'no research db attached' — which
read as broken TradingView charts when the real cause was an empty data
pipe. Now a None connection is (re)opened on demand; the facade itself
auto-reconnects a dropped-but-open one."""
if self.research_con is not None:
return self.research_con
with self._research_lock:
if self.research_con is None:
try:
self.research_con = open_research_db("market")
except Exception as exc: # noqa: BLE001 — degrade to None, retry next call
logger.warning("research DB open failed (charts empty): %s", exc)
self.research_con = None
return self.research_con
# lazily-built, process-wide SSE change signal (shared by all clients)
self._change_signal: _ChangeSignal | None = None
# backtest replay cache (plan 015) — a DEDICATED lock, not self._lock,
@@ -355,22 +381,23 @@ class DashboardState:
pipeline). If today has no bars yet (pre-market, weekend, or replay of
an older session), falls back to the most recent session that does.
"""
if self.research_con is None:
con = self._research()
if con is None:
return {"symbol": symbol, "bars": [], "reason": "no research db attached"}
from research.query import query_minute_bars
with self._lock:
try:
session_date = _eastern_session_date()
df = query_minute_bars(self.research_con, symbol, session_date)
df = query_minute_bars(con, symbol, session_date)
if df is None or df.empty:
# fall back to the latest session with bars for this symbol
row = self.research_con.execute(
row = con.execute(
"SELECT MAX(session_date) FROM minute_bars WHERE symbol = ?",
[symbol],
).fetchone()
if row and row[0] is not None:
df = query_minute_bars(self.research_con, symbol, row[0])
df = query_minute_bars(con, symbol, row[0])
except Exception as exc: # noqa: BLE001
return {"symbol": symbol, "bars": [], "reason": str(exc)}
if df is None or df.empty:
@@ -403,7 +430,8 @@ class DashboardState:
arbitrary past sessions the plain ``hours``-back-from-now window can
never reach (it's capped at 72h).
"""
if self.research_con is None:
con = self._research()
if con is None:
return {"symbol": symbol, "bars": [], "reason": "no research db attached"}
from zoneinfo import ZoneInfo
@@ -421,7 +449,7 @@ class DashboardState:
with self._lock:
try:
rows = self.research_con.execute(
rows = con.execute(
"SELECT timestamp, open, high, low, close, volume "
"FROM minute_bars WHERE symbol = ? AND timestamp >= ? "
"ORDER BY timestamp",
@@ -0,0 +1,41 @@
"""DashboardState research-DB connection must self-heal (2026-07-20).
The dashboard opened its research connection ONCE at boot; if the DB was
unreachable then (container started before Postgres, or a blip), it stayed
None forever and every chart/sparkline/backtest returned 'no research db
attached' — which read as 'the TradingView charts are broken' when the real
cause was an empty data pipe. _research() reopens lazily on demand."""
import api.main as m
def test_research_con_reopens_when_none(monkeypatch):
opened = {"n": 0}
class FakeCon:
def execute(self, *a, **k):
class R:
def fetchall(self_): # noqa: N805
return []
def df(self_): # noqa: N805
import pandas as pd
return pd.DataFrame()
return R()
monkeypatch.setattr(m, "open_research_db", lambda name: (opened.__setitem__("n", opened["n"] + 1) or FakeCon()))
st = m.DashboardState("momentum", research_con=None) # boot-time open FAILED -> None
c1 = st._research()
assert c1 is not None and opened["n"] == 1 # lazily opened
c2 = st._research()
assert c2 is c1 and opened["n"] == 1 # cached, not reopened each call
def test_research_none_when_open_keeps_failing(monkeypatch):
def boom(name):
raise RuntimeError("db down")
monkeypatch.setattr(m, "open_research_db", boom)
st = m.DashboardState("momentum", research_con=None)
assert st._research() is None # degrades cleanly, no raise
# and bars() still returns the graceful shape, not an exception
out = st.bars("AAPL")
assert out["bars"] == [] and "reason" in out