diff --git a/api/static/dashboard.html b/api/static/dashboard.html index 392db5a..60a8e2c 100644 --- a/api/static/dashboard.html +++ b/api/static/dashboard.html @@ -154,6 +154,20 @@ background: linear-gradient(180deg, var(--accent), #2a6fd6); flex-shrink: 0; box-shadow: 0 0 8px rgba(91,163,255,.5); } + /* collapsible panels:

..

..
*/ + details.panel > summary { list-style: none; cursor: pointer; outline: none; } + details.panel > summary::-webkit-details-marker { display: none; } + details.panel > summary > h2 { margin: 0; } + details.panel > summary > h2::after { + content: "▾"; margin-left: 12px; color: var(--muted); font-size: 11px; + flex-shrink: 0; transition: color .15s ease; + } + details.panel:not([open]) > summary > h2::after { content: "▸"; } + details.panel:not([open]) > summary > h2 { border-bottom: none; } + details.panel > summary:hover > h2 { color: #c3cde6; } + details.panel > summary:hover > h2::after { color: var(--accent); } + details.panel[open] > summary > h2 { background: linear-gradient(180deg, var(--panel-hi), rgba(22,28,42,.5)); } + #panels-toggle { margin-left: 4px; } .count { font: 600 11px var(--mono); background: var(--border); color: var(--text); border-radius: 10px; padding: 1px 8px; } .hint { margin-left: auto; font: 500 10px var(--mono); color: var(--muted); letter-spacing: .3px; text-transform: none; } table { width: 100%; border-collapse: collapse; } @@ -407,6 +421,7 @@
+
connecting...
@@ -430,70 +445,93 @@
-
-

Armed Triggers 0live proximity to the opening-range breakout · closest to fire on top

+
+

Armed Triggers 0live proximity to the opening-range breakout · closest to fire on top

No armed gappers — waiting for the open / first bars.
-
+ -
-

Catalyst Engine 0what the local LLM read & how it ruled · 🟢 bullish · 🔻 bearish · 🔴 dilutive · hover a row for the model's reasoning

-
No catalyst reads yet — the news enrichment loop hasn't classified any headlines.
-
- -
-

Account 0

-
No account snapshot yet — waiting for broker sync.
-
- -
-

All-Time Score cumulative across every closed round-trip — bot's record to date

-
No closed trades recorded yet.
-
- -
-

Daily Performance 0per-day P&L bars + cumulative equity curve · click a day to open it

-
No trading days recorded yet.
-
- -
-

Trades 0

-
-
- -
-

Approval Queue 0A approve top · R reject top

-
-
- -
-

Ready Signals 0

-
-
- -
-

Open Positions 0

+
+

Open Positions 0

-
+ -
-

Watch States 0click a row for criteria

+
+

Approval Queue 0A approve top · R reject top

+
+
+ +
+

Ready Signals 0

+
+
+ +
+

Account 0

+
No account snapshot yet — waiting for broker sync.
+
+ +
+

Catalyst Engine 0what the local LLM read & how it ruled · 🟢 bullish · 🔻 bearish · 🔴 dilutive · hover a row for the model's reasoning

+
No catalyst reads yet — the news enrichment loop hasn't classified any headlines.
+
+ +
+

Trades 0

+
+
+ +
+

Watch States 0click a row for criteria

-
+ -
-

Activity Feed 0

+
+

Daily Performance 0per-day P&L bars + cumulative equity curve · click a day to open it

+
No trading days recorded yet.
+
+ +
+

All-Time Score cumulative across every closed round-trip — bot's record to date

+
No closed trades recorded yet.
+
+ +
+

Activity Feed 0

-
+ -
-

Order Lifecycle 0

+
+

Order Lifecycle 0

-
+
+
diff --git a/storage/db_pg.py b/storage/db_pg.py index f139a05..c33d468 100644 --- a/storage/db_pg.py +++ b/storage/db_pg.py @@ -31,6 +31,7 @@ giving each test the same isolation DuckDB ":memory:" gave it. from __future__ import annotations import os +import urllib.parse as _urlparse import uuid from pathlib import Path from typing import Any @@ -173,11 +174,31 @@ class PgConnection: return self._conn +# The LAN Postgres is reached from WSL via EITHER the Windows netsh portproxy on +# 127.0.0.1 OR a direct route to the host 192.168.1.5 — and which one works FLIPS across +# WSL/host reboots and network-mode (mirrored vs NAT) changes. So when DATABASE_URL points +# at a known LAN endpoint, try the configured host first and then the alternate, so the +# loop self-heals on boot regardless of the current tunnel state (this is the recurring +# crash-loop landmine). "connection refused" returns instantly, so the fallback is cheap. +_LAN_DB_HOSTS = ("127.0.0.1", "localhost", "192.168.1.5") +_LAN_DB_FALLBACKS = ("192.168.1.5", "127.0.0.1") + + +def _dsn_with_host(dsn: str, host: str) -> str: + p = _urlparse.urlsplit(dsn) + userinfo = "" + if p.username: + userinfo = _urlparse.quote(p.username, safe="") + if p.password: + userinfo += ":" + _urlparse.quote(p.password, safe="") + userinfo += "@" + netloc = f"{userinfo}{host}:{p.port or 5432}" + return _urlparse.urlunsplit((p.scheme, netloc, p.path, p.query, p.fragment)) + + def _connect(): dsn = os.environ.get("DATABASE_URL") - if dsn: - conn = psycopg2.connect(dsn) - else: + if not dsn: conn = psycopg2.connect( host=os.environ.get("PGHOST", "/home/philip/pgrun"), port=os.environ.get("PGPORT", "5432"), @@ -185,8 +206,22 @@ def _connect(): password=os.environ.get("PGPASSWORD", ""), dbname=os.environ.get("PGDATABASE", "momentum"), ) - conn.autocommit = True - return conn + conn.autocommit = True + return conn + + candidates = [dsn] + host = _urlparse.urlsplit(dsn).hostname + if host in _LAN_DB_HOSTS: # only swap hosts for the LAN PG + candidates += [_dsn_with_host(dsn, h) for h in _LAN_DB_FALLBACKS if h != host] + last_err: Exception | None = None + for cand in candidates: + try: + conn = psycopg2.connect(cand, connect_timeout=5) + conn.autocommit = True + return conn + except psycopg2.OperationalError as exc: + last_err = exc + raise last_err if last_err else psycopg2.OperationalError("no DATABASE_URL candidates") def _apply_schema(conn, search_path: str | None = None) -> None: