Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 127 additions & 96 deletions README.md

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions docs/design/DASHBOARD_TWO_PAGE_MERGE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Two-page dashboard merge design

Status: local proposal awaiting approval. Do not merge or deploy yet.
Status: implemented. The GitHub Pages workflow publishes production updates
only from `main`.

## Decision

Expand Down Expand Up @@ -80,9 +81,9 @@ The daily pipeline should:
Publishing must be atomic: a cycle should never expose one page with a newer data
contract than the other.

## Approval boundary
## Implementation status

The current local Horizon Comparison preview demonstrates the second page. After
approval, the merge work will add the page switcher and v2 arm support to the
original dashboard, generate the final `horizons.html`, update the refresh and
deployment workflows, and then request approval before pushing.
The shared page switcher, v2 arm support, partial-outcome display, collapsed
SHAP panels, final `horizons.html`, daily refresh integration, public payload
validation, and GitHub Pages packaging are implemented. The deployment job
runs only for `main`.
2 changes: 1 addition & 1 deletion site/data/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 3,
"generated_at_utc": "2026-07-29T22:29:54.012984+00:00",
"generated_at_utc": "2026-07-29T23:59:49.062830+00:00",
"latest_decision_date": "2026-07-28",
"snapshot_count": 17,
"evaluation_count": 14,
Expand Down
2 changes: 1 addition & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Live Forward Research Monitor</title>
<link rel="stylesheet" href="./assets/dashboard.css"></head><body>
<header><div class="hero-row"><div><div class="eyebrow">AI Portfolio Research</div><h1>Live Forward Monitor</h1><p>Out-of-sample model portfolios tracked against SPY. Every session uses the portfolio frozen before its evaluation window.</p><nav class="page-switcher" aria-label="Monitor pages"><a class="page-link active" href="dashboard.html" aria-current="page"><strong>Live Forward Monitor</strong><small>Default · daily evidence and stock-level view</small></a><a class="page-link" href="./horizons.html"><strong>Horizon Comparison</strong><small>5 / 10 / 20-session experiment</small></a></nav></div><div class="hero-badge"><span>Default view</span><strong>Daily evidence</strong><span>Frozen-cohort monitor</span></div></div></header>
<header><div class="hero-row"><div><div class="eyebrow">AI Portfolio Research</div><h1>Live Forward Monitor</h1><p>Out-of-sample model portfolios tracked against SPY. Every session uses the portfolio frozen before its evaluation window.</p><nav class="page-switcher" aria-label="Monitor pages"><a class="page-link active" href="./index.html" aria-current="page"><strong>Live Forward Monitor</strong><small>Default · daily evidence and stock-level view</small></a><a class="page-link" href="./horizons.html"><strong>Horizon Comparison</strong><small>5 / 10 / 20-session experiment</small></a></nav></div><div class="hero-badge"><span>Default view</span><strong>Daily evidence</strong><span>Frozen-cohort monitor</span></div></div></header>
<main>
<div class="toolbar panel"><div class="toolbar-group"><label>Start date<input id="startDate" type="date"></label><label>End date<input id="endDate" type="date"></label><div class="segmented"><button data-range="latest">Latest session</button><button data-range="5">5 sessions</button><button data-range="all" class="active">All available</button></div></div><div class="toolbar-group"><button id="refreshData" title="Reload deployed dashboard data">Refresh data</button><div class="status" id="dataStatus">Frozen evidence loaded</div></div></div>
<section class="pending-panel panel" id="nextSession"></section>
Expand Down
13 changes: 10 additions & 3 deletions src/experiments/live_dashboard_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,16 @@ def publish_live_dashboard(

latest_decision = max(item["decision_date"] for item in snapshots)
assets = dashboard_site_assets()
assets["index.html"] = assets["index.html"].replace(
'href="../live_forward_horizon_matched_v2/dashboard.html"',
'href="./horizons.html"',
assets["index.html"] = (
assets["index.html"]
.replace(
'class="page-link active" href="dashboard.html"',
'class="page-link active" href="./index.html"',
)
.replace(
'href="../live_forward_horizon_matched_v2/dashboard.html"',
'href="./horizons.html"',
)
)
for relative, contents in assets.items():
target = destination / relative
Expand Down
5 changes: 5 additions & 0 deletions src/experiments/public_dashboard_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def validate_public_dashboard_bundle(output_dir: str | Path) -> dict[str, Any]:
exposed = [token for token in forbidden if token in public_text]
if exposed:
raise ValueError(f"Public dashboard contains forbidden private fields: {exposed}")
index_html = (root / "index.html").read_text(encoding="utf-8")
if 'href="dashboard.html"' in index_html:
raise ValueError(
"Public dashboard must link its default page through index.html"
)

payload = json.loads(dashboard_text)
snapshots = payload.get("snapshots", []) if isinstance(payload, dict) else payload
Expand Down
2 changes: 2 additions & 0 deletions tests/test_live_dashboard_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ def test_publish_site_splits_assets_and_passes_validation(
horizons = (site / "horizons.html").read_text(encoding="utf-8")
assert '<link rel="stylesheet" href="./assets/dashboard.css">' in index
assert '<script src="./assets/loader.js" defer></script>' in index
assert 'href="./index.html" aria-current="page"' in index
assert 'href="dashboard.html"' not in index
assert 'href="./horizons.html"' in index
assert 'href="./index.html"' in horizons
assert "Horizon Comparison" in horizons
Expand Down