# Cockpit Surfacing + Mobile Redesign Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Surface the pipeline's B2a allocation, B3 funding health, and C1/C2 executed-vs-modeled reconciliation in the existing server-rendered cockpit, fully integrated into the Overview + Detail pages, and redesign navigation + layout to be mobile-first — read-only, no pipeline logic, browser-verified locally.

**Architecture:** Read-model methods (`ForwardNavRepo` reads + `DashboardService`) join the already-written `strategy_allocation` / `strategy_funding` tables and pair each modeled track with its executed twin (reverse of C1/C2's `_REF_ALIAS`); the DTOs (`FleetRow`/`StrategyDetail`/`OverviewModel`) gain the new fields; the Jinja2 templates (`base.html` nav+CSS, `cockpit.html`, `strategy.html`) render them. Every new read degrades to `—`/hidden, never a 500.

**Tech Stack:** Python 3.13, FastAPI + Jinja2 + HTMX, SQLAlchemy 2.0 (SQLite tests / Postgres prod), inline CSS, pytest, Playwright (browser verification), ruff.

## Global Constraints

- Read-only display of already-written tables — NO new pipeline logic, NO capital moved, NO new nightly assets, NO new top-level routes (integrate into the existing `/` and `/strategy/{sid}`).
- Locked design (from the approved mockup): bottom-tab nav on mobile (`≤640px`) + top bar on desktop; capital meter (`deployed $X / $35,000 ceiling`); funding badge `funded`(green `#3fb950`)/`decaying N/10`(amber `#d29922`, NEW token)/`de-funded`(red `#f85149`); the executed twin nested UNDER its modeled edge (never a separate top-level row).
- Honor the existing GitHub-dark terminal aesthetic (monospace, `tabular-nums`, existing badge classes) — refine, don't re-skin. Dark-committed (single theme is deliberate for a trading console).
- Graceful degradation is load-bearing: prod's schema/data lags until deploy, so a missing table/row/column → `—`/hidden, never 500 (mirror the existing `except Exception: return []`).
- Exec pairing key = REVERSE of `CockpitBacktestRefProvider._REF_ALIAS` → `{"multistrat":"multistrat_exec","bybit_4edge":"bybit_4edge_exec"}`. Executed twins are `tier=research` — folded into their modeled parent on the Overview, not rendered as their own top-level rows.
- `ceiling` = `ALLOCATION_POLICY.capital_ceiling` (B2a); `gross_deployed` = Σ target_dollars.
- The DEPLOY (push + argo + migrate) is OUT OF SCOPE — a separate follow-up gated on this being browser-verified locally.

---

### Task 1: Repo reads — `all_allocations()` + `all_funding()`

**Files:**
- Modify: `src/fxhnt/adapters/persistence/forward_nav.py`
- Test: `tests/integration/test_dashboard_reads.py`

**Interfaces:**
- Produces: `ForwardNavRepo.all_allocations() -> list[StrategyAllocationRow]`, `ForwardNavRepo.all_funding() -> list[StrategyFundingRow]` (full rows; `[]` when the table is empty).

- [ ] **Step 1: Write the failing test**

Create `tests/integration/test_dashboard_reads.py`:

```python
import datetime as dt

from fxhnt.adapters.persistence.cockpit_models import StrategyAllocationRow, StrategyFundingRow
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo


def _repo(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    return repo


def test_all_allocations_and_funding_roundtrip(tmp_path):
    repo = _repo(tmp_path)
    at = dt.datetime(2026, 7, 10)
    repo.replace_allocations([StrategyAllocationRow(strategy_id="bybit_4edge", target_weight=0.54,
        target_dollars=18900.0, policy_version=1, policy_hash="h", as_of="2026-07-10", computed_at=at)], at)
    repo.replace_funding([StrategyFundingRow(strategy_id="bybit_4edge", state="funded", label="funded",
        decay_run=0, recovery_run=0, first_pass_date=None, policy_version=2, policy_hash="h",
        as_of="2026-07-10", computed_at=at)], at)
    allocs = {a.strategy_id: a for a in repo.all_allocations()}
    funds = {f.strategy_id: f for f in repo.all_funding()}
    assert allocs["bybit_4edge"].target_dollars == 18900.0
    assert allocs["bybit_4edge"].target_weight == 0.54
    assert funds["bybit_4edge"].label == "funded"


def test_reads_empty_when_absent(tmp_path):
    repo = _repo(tmp_path)
    assert repo.all_allocations() == []
    assert repo.all_funding() == []
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_dashboard_reads.py -q`
Expected: FAIL — `AttributeError: 'ForwardNavRepo' object has no attribute 'all_allocations'`.

- [ ] **Step 3: Add the reads**

In `forward_nav.py`, ensure `StrategyFundingRow` is imported in the `cockpit_models` import block (beside `StrategyAllocationRow`), then add after `current_funding` (near line 286):

```python
    def all_allocations(self) -> list[StrategyAllocationRow]:
        with Session(self._engine) as s:
            return list(s.scalars(select(StrategyAllocationRow)))

    def all_funding(self) -> list[StrategyFundingRow]:
        with Session(self._engine) as s:
            return list(s.scalars(select(StrategyFundingRow)))
```

- [ ] **Step 4: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/integration/test_dashboard_reads.py -q` → PASS (2).
Run: `ruff check src/fxhnt/adapters/persistence/forward_nav.py tests/integration/test_dashboard_reads.py`
```bash
git add src/fxhnt/adapters/persistence/forward_nav.py tests/integration/test_dashboard_reads.py
git commit -m "feat(cockpit): repo reads for allocation + funding rows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 2: Read-model — DTO fields + `DashboardService` allocation/funding/exec pairing

**Files:**
- Modify: `src/fxhnt/ports/dashboard.py` (extend DTOs)
- Modify: `src/fxhnt/application/dashboard_service.py`
- Test: `tests/integration/test_dashboard_surfacing.py`

**Interfaces:**
- Consumes: `all_allocations()`/`all_funding()` (Task 1), `all_summaries()` (executed twins' forward summaries), `ALLOCATION_POLICY.capital_ceiling`.
- Produces: `FleetRow`/`StrategyDetail` gain `alloc_dollars`/`alloc_weight`/`alloc_policy_version`/`funding_state`/`funding_label`/`funding_decay_run`/`funding_recovery_run`/`funding_first_pass_date`/`funding_policy_version`/`exec_twin_sid`/`exec_return_pct`/`exec_divergence_pct` (all default None/""/0). `OverviewModel` gains `gross_deployed`/`ceiling`/`n_funded`/`n_defunded`. `DashboardService.exec_twin_sids() -> set[str]` (the twin sids to hide from top-level).

- [ ] **Step 1: Write the failing test**

Create `tests/integration/test_dashboard_surfacing.py`:

```python
import datetime as dt

from fxhnt.adapters.persistence.cockpit_models import (
    ForwardSummaryRow, StrategyAllocationRow, StrategyFundingRow,
)
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.dashboard_service import DashboardService


def _seed(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    at = dt.datetime(2026, 7, 10)
    def _sum(sid, tr):
        return ForwardSummaryRow(strategy_id=sid, as_of="2026-07-10", days=5, nav=1.0 + tr, total_return=tr,
                                 sharpe=1.0, maxdd=-0.05, gate_status="WAIT", gate_reason="", src_updated_at=at)
    from sqlalchemy.orm import Session
    with Session(repo._engine) as s:
        s.add_all([_sum("bybit_4edge", 0.0031), _sum("bybit_4edge_exec", 0.0019)])
        s.commit()
    repo.replace_allocations([StrategyAllocationRow(strategy_id="bybit_4edge", target_weight=0.54,
        target_dollars=18900.0, policy_version=1, policy_hash="h", as_of="2026-07-10", computed_at=at)], at)
    repo.replace_funding([
        StrategyFundingRow(strategy_id="bybit_4edge", state="funded", label="funded", decay_run=0,
            recovery_run=0, first_pass_date=None, policy_version=2, policy_hash="h", as_of="2026-07-10", computed_at=at),
        StrategyFundingRow(strategy_id="multistrat", state="de-funded", label="de-funded", decay_run=0,
            recovery_run=0, first_pass_date="2026-06-20", policy_version=2, policy_hash="h", as_of="2026-07-10", computed_at=at),
    ], at)
    return repo


def test_fleet_carries_allocation_funding_and_exec_pairing(tmp_path):
    svc = DashboardService(_seed(tmp_path))
    rows = {f.strategy_id: f for f in svc.fleet()}
    b = rows["bybit_4edge"]
    assert b.alloc_dollars == 18900.0 and abs(b.alloc_weight - 0.54) < 1e-9
    assert b.funding_label == "funded"
    # exec pairing: bybit_4edge modeled paired with bybit_4edge_exec (return 0.19% vs 0.31% -> Δ -0.12%)
    assert b.exec_twin_sid == "bybit_4edge_exec"
    assert abs(b.exec_return_pct - 0.19) < 1e-6
    assert abs(b.exec_divergence_pct - (-0.12)) < 1e-6
    # a de-funded edge
    assert rows["multistrat"].funding_label == "de-funded"
    # the executed twin is flagged for hiding at top level
    assert "bybit_4edge_exec" in svc.exec_twin_sids()


def test_overview_capital_meter(tmp_path):
    ov = DashboardService(_seed(tmp_path)).overview()
    assert ov.gross_deployed == 18900.0
    assert ov.ceiling == 35000.0
    assert ov.n_funded == 1 and ov.n_defunded == 1


def test_graceful_when_no_alloc_funding(tmp_path):
    # a repo with summaries but NO allocation/funding rows -> fields default, no crash.
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'e.db'}")
    repo.migrate()
    rows = {f.strategy_id: f for f in DashboardService(repo).fleet()}
    any_row = next(iter(rows.values()))
    assert any_row.alloc_dollars is None and any_row.funding_label == ""
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_dashboard_surfacing.py -q`
Expected: FAIL — `TypeError`/`AttributeError` on the new FleetRow fields / `exec_twin_sids`.

- [ ] **Step 3: Extend the DTOs**

In `src/fxhnt/ports/dashboard.py`, append these fields to `FleetRow` (after `reference_only`):

```python
    # B2a allocation (None when no strategy_allocation row).
    alloc_dollars: float | None = None
    alloc_weight: float | None = None
    alloc_policy_version: int | None = None
    # B3 funding health ("" when no strategy_funding row).
    funding_state: str = ""          # "" | "funded" | "de-funded"
    funding_label: str = ""          # "" | "funded" | "decaying" | "de-funded"
    funding_decay_run: int = 0
    funding_recovery_run: int = 0
    funding_first_pass_date: str | None = None
    funding_policy_version: int | None = None
    # C1/C2 executed twin (set only on a MODELED track that has an executed twin with forward data).
    exec_twin_sid: str = ""
    exec_return_pct: float | None = None
    exec_divergence_pct: float | None = None
```

Append the SAME 12 fields to `StrategyDetail` (after `note`). Add to `OverviewModel` (after `edges`):

```python
    gross_deployed: float = 0.0
    ceiling: float = 0.0
    n_funded: int = 0
    n_defunded: int = 0
```

Add to the `DashboardReadModel` Protocol:
```python
    def exec_twin_sids(self) -> set[str]: ...
```

- [ ] **Step 4: Populate them in `DashboardService`**

In `dashboard_service.py`, add the import and a reverse-alias constant near the top:

```python
from fxhnt.application.allocation_policy import ALLOCATION_POLICY
from fxhnt.application.forward_ingest import CockpitBacktestRefProvider

# modeled sid -> its executed twin sid (reverse of C1/C2's exec->modeled reconciliation alias).
_EXEC_TWIN = {modeled: exec_sid for exec_sid, modeled in CockpitBacktestRefProvider._REF_ALIAS.items()}
```

Add a helper method:
```python
    def exec_twin_sids(self) -> set[str]:
        """The executed-twin sids (research-tier observed tracks) folded into their modeled parent on the
        Overview — so they are NOT rendered as their own top-level rows."""
        return set(_EXEC_TWIN.values())
```

In `fleet()`, after building `summaries`, load allocation + funding (graceful):
```python
        try:
            allocs = {a.strategy_id: a for a in self._repo.all_allocations()}
        except Exception:  # noqa: BLE001 — a missing/absent table must never break the fleet
            allocs = {}
        try:
            funds = {f.strategy_id: f for f in self._repo.all_funding()}
        except Exception:  # noqa: BLE001
            funds = {}
```
Then, when constructing each `FleetRow`, add the new kwargs:
```python
                alloc_dollars=(allocs[reg.strategy_id].target_dollars if reg.strategy_id in allocs else None),
                alloc_weight=(allocs[reg.strategy_id].target_weight if reg.strategy_id in allocs else None),
                alloc_policy_version=(allocs[reg.strategy_id].policy_version if reg.strategy_id in allocs else None),
                funding_state=(funds[reg.strategy_id].state if reg.strategy_id in funds else ""),
                funding_label=(funds[reg.strategy_id].label if reg.strategy_id in funds else ""),
                funding_decay_run=(funds[reg.strategy_id].decay_run if reg.strategy_id in funds else 0),
                funding_recovery_run=(funds[reg.strategy_id].recovery_run if reg.strategy_id in funds else 0),
                funding_first_pass_date=(funds[reg.strategy_id].first_pass_date if reg.strategy_id in funds else None),
                funding_policy_version=(funds[reg.strategy_id].policy_version if reg.strategy_id in funds else None),
                **self._exec_pair(reg.strategy_id, summaries),
```
Add the pairing helper (pure):
```python
    @staticmethod
    def _exec_pair(sid, summaries):
        """exec_twin_sid / exec_return_pct / exec_divergence_pct for a MODELED sid whose executed twin has a
        forward summary; empty dict-of-defaults otherwise (so **-splat is a no-op for non-paired tracks)."""
        twin = _EXEC_TWIN.get(sid)
        if twin is None or twin not in summaries or sid not in summaries:
            return {"exec_twin_sid": "", "exec_return_pct": None, "exec_divergence_pct": None}
        exec_r = 100.0 * summaries[twin].total_return
        modeled_r = 100.0 * summaries[sid].total_return
        return {"exec_twin_sid": twin, "exec_return_pct": exec_r,
                "exec_divergence_pct": exec_r - modeled_r}
```

In `overview()`, compute the meter before the `return` (graceful):
```python
        try:
            allocs = list(self._repo.all_allocations())
        except Exception:  # noqa: BLE001
            allocs = []
        try:
            funds = list(self._repo.all_funding())
        except Exception:  # noqa: BLE001
            funds = []
        gross = sum(abs(a.target_dollars) for a in allocs)
        n_funded = sum(1 for f in funds if f.state == "funded")
        n_defunded = sum(1 for f in funds if f.state == "de-funded")
```
and add to the `OverviewModel(...)` call: `gross_deployed=gross, ceiling=ALLOCATION_POLICY.capital_ceiling, n_funded=n_funded, n_defunded=n_defunded`.

In `detail()`, populate the same alloc/funding/exec fields on `StrategyDetail` (reuse the same `allocs`/`funds` reads + `_exec_pair(strategy_id, summaries)` where `summaries = {r.strategy_id: r for r in self._repo.all_summaries()}`).

- [ ] **Step 5: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/integration/test_dashboard_surfacing.py -q` → PASS (3).
Run: `ruff check src/fxhnt/ports/dashboard.py src/fxhnt/application/dashboard_service.py tests/integration/test_dashboard_surfacing.py`
```bash
git add src/fxhnt/ports/dashboard.py src/fxhnt/application/dashboard_service.py tests/integration/test_dashboard_surfacing.py
git commit -m "feat(cockpit): read-model surfaces allocation, funding, exec-vs-modeled pairing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 3: `base.html` — mobile bottom-tab nav + CSS tokens

**Files:**
- Modify: `src/fxhnt/adapters/web/templates/base.html`
- Test: `tests/integration/test_cockpit_render.py`

**Interfaces:**
- Produces: a bottom-tab `<nav class="tabs">` shown `≤640px`, top `nav.bar` shown desktop; CSS tokens `--warn`/`--warn-bg`, `.b.funded`/`.b.decaying`/`.b.defunded`, `.meter`/`.bar`, `.exec`/`.exec-tag`, `.card` etc. (from the approved mockup).

- [ ] **Step 1: Write the failing test**

Create `tests/integration/test_cockpit_render.py`:

```python
from fastapi.testclient import TestClient

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.web.app import create_app


def _client(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    return TestClient(create_app(repo))


def test_overview_renders_bottom_tab_nav(tmp_path):
    r = _client(tmp_path).get("/")
    assert r.status_code == 200
    assert 'class="tabs"' in r.text            # the mobile bottom-tab nav is present
    assert ">Overview<" in r.text
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_cockpit_render.py -q`
Expected: FAIL — `assert 'class="tabs"' in r.text` (no bottom-tab nav yet).

- [ ] **Step 3: Add the bottom-tab nav**

In `base.html`, immediately AFTER the closing `</header>` line (currently `...{{ now }}</span></header>`), add the bottom tab bar (a sibling of `<main>`):

```html
  {# mobile bottom-tab nav: thumb-reachable, shown only <=640px (see CSS). Mirrors the desktop nav.bar. #}
  <nav class="tabs" hx-boost="true" aria-label="Primary">
    <a href="/" class="tab {{ 'on' if path == '/' }}"><span class="ic" aria-hidden="true">▚</span>Overview</a>
    <a href="/paper" class="tab {{ 'on' if path == '/paper' }}"><span class="ic" aria-hidden="true">▤</span>Paper</a>
    <a href="/paper/replay" class="tab {{ 'on' if path.startswith('/paper/replay') }}"><span class="ic" aria-hidden="true">▶</span>Replay</a>
    <a href="/paper/sim" class="tab {{ 'on' if path.startswith('/paper/sim') }}"><span class="ic" aria-hidden="true">◷</span>Backtest</a>
  </nav>
```

- [ ] **Step 4: Add the CSS tokens + classes**

In the `<style>` block, add the amber semantic token and the new component classes (append inside `<style>`, before `@media`):

```css
    /* B3 funding: a semantic amber for "decaying", distinct from the neutral-grey WAIT. */
    .b { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:20px;
         font-size:11px; font-weight:600; line-height:1.4; white-space:nowrap; }
    .b.small { font-size:10px; padding:1px 6px; }
    .b .dot { width:6px; height:6px; border-radius:50%; background:currentColor; }
    .b.funded { background:#193b1e; color:#3fb950; }
    .b.decaying { background:#3a2d10; color:#d29922; }
    .b.defunded { background:#3b1919; color:#f85149; }
    /* capital meter (allocation vs ceiling) */
    .meter { margin-top:12px; padding-top:12px; border-top:1px dashed #30363d; }
    .meter .mrow { display:flex; justify-content:space-between; font-size:12px; color:#8b949e; margin-bottom:6px; }
    .meter .mrow b { color:#c9d1d9; font-weight:600; }
    .capbar { height:8px; border-radius:6px; background:#161b22; overflow:hidden; border:1px solid #21262d; }
    .capbar > i { display:block; height:100%; background:linear-gradient(90deg,#238636,#3fb950); border-radius:6px; }
    .meter .mcap { display:flex; justify-content:space-between; font-size:10.5px; color:#6e7681; margin-top:5px; }
    /* executed twin: a nested sub-line/sub-row under a modeled edge */
    .exec-tag { font-size:9.5px; letter-spacing:.05em; text-transform:uppercase; color:#6e7681;
                border:1px solid #30363d; border-radius:5px; padding:1px 5px; }
    tr.exrow td { padding-top:2px; }
    tr.exrow td:first-child { padding-left:22px; color:#6e7681; font-size:11.5px; }
    /* detail lifecycle blocks */
    .lc { border:1px solid #21262d; border-radius:10px; margin:11px 0; overflow:hidden; }
    .lc > .lct { display:flex; align-items:center; gap:8px; padding:9px 12px; background:#161b22;
                 font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:#8b949e;
                 border-bottom:1px solid #21262d; }
    .lc > .lct .r { margin-left:auto; text-transform:none; letter-spacing:0; }
    .lc .lcb { padding:11px 12px; }
    .kv { display:flex; justify-content:space-between; gap:12px; padding:4px 0; font-size:12.5px;
          border-bottom:1px solid #21262d; } .kv:last-child { border-bottom:0; }
    .kv .k { color:#8b949e; } .kv .v { font-weight:600; text-align:right; }
    .why { font-size:11.5px; color:#6e7681; margin-top:7px; line-height:1.5; }
    /* bottom tabs: hidden on desktop, shown on mobile */
    nav.tabs { display:none; }
```

Then INSIDE the existing `@media (max-width: 640px) { ... }` block, add:

```css
      nav.bar { display:none; }               /* hide the top nav row on mobile */
      main { padding-bottom:64px; }           /* clear the fixed bottom bar */
      nav.tabs { position:fixed; bottom:0; left:0; right:0; z-index:20; display:grid;
                 grid-template-columns:repeat(4,1fr); background:rgba(13,17,23,.94);
                 backdrop-filter:blur(10px); border-top:1px solid #21262d; }
      nav.tabs .tab { padding:9px 0 11px; text-align:center; color:#6e7681; font-size:10.5px;
                      letter-spacing:.02em; }
      nav.tabs .tab .ic { display:block; font-size:16px; line-height:1; margin:0 auto 4px; }
      nav.tabs .tab.on { color:#58a6ff; }
      nav.tabs .tab:focus-visible { outline:2px solid #58a6ff; outline-offset:-2px; }
```

- [ ] **Step 5: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/integration/test_cockpit_render.py -q` → PASS.
(Templates aren't ruff-linted; run `ruff check tests/integration/test_cockpit_render.py`.)
```bash
git add src/fxhnt/adapters/web/templates/base.html tests/integration/test_cockpit_render.py
git commit -m "feat(cockpit): mobile bottom-tab nav + funding/meter/exec CSS tokens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 4: `cockpit.html` + overview route — capital meter, allocation/funding columns, nested exec twin

**Files:**
- Modify: `src/fxhnt/adapters/web/templates/cockpit.html`
- Modify: `src/fxhnt/adapters/web/app.py` (the `cockpit` route context)
- Test: `tests/integration/test_cockpit_render.py` (extend)

**Interfaces:**
- Consumes: `FleetRow.alloc_*`/`funding_*`/`exec_*` (Task 2), `OverviewModel.gross_deployed`/`ceiling`/`n_funded`/`n_defunded`, `svc.exec_twin_sids()`.

- [ ] **Step 1: Write the failing test (extend)**

Append to `tests/integration/test_cockpit_render.py`:

```python
import datetime as dt

from fxhnt.adapters.persistence.cockpit_models import StrategyAllocationRow, StrategyFundingRow


def test_overview_shows_allocation_and_funding(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    at = dt.datetime(2026, 7, 10)
    repo.replace_allocations([StrategyAllocationRow(strategy_id="bybit_4edge", target_weight=0.54,
        target_dollars=18900.0, policy_version=1, policy_hash="h", as_of="2026-07-10", computed_at=at)], at)
    repo.replace_funding([StrategyFundingRow(strategy_id="bybit_4edge", state="funded", label="funded",
        decay_run=0, recovery_run=0, first_pass_date=None, policy_version=2, policy_hash="h",
        as_of="2026-07-10", computed_at=at)], at)
    r = TestClient(create_app(repo)).get("/")
    assert r.status_code == 200
    assert "18,900" in r.text or "18900" in r.text     # allocation dollars surfaced
    assert "funded" in r.text                            # funding badge surfaced
    assert "/ $35,000" in r.text or "35,000" in r.text   # capital meter ceiling
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_cockpit_render.py::test_overview_shows_allocation_and_funding -q`
Expected: FAIL (allocation/funding/meter not rendered yet).

- [ ] **Step 3: Wire the overview route**

In `app.py` `cockpit(...)`, replace `fleet = svc.fleet()` with:
```python
        fleet = svc.fleet()
        overview = svc.overview()
        twin_sids = svc.exec_twin_sids()
        # the executed twins are research-tier; they nest under their modeled parent, so exclude them from BOTH
        # the top-level "all forward tracks" list AND the research grouping (never a separate row).
        fleet = [f for f in fleet if f.strategy_id not in twin_sids]
        fleet_top = fleet
```
`fleet` is now already twin-free, so the existing `deploy_rows` and `research_groups` derivations below it
automatically exclude the twins (no separate change needed). Add `"overview": overview, "fleet_top": fleet_top`
to the `TemplateResponse` context dict. (Keep passing `fleet` too — it is now the twin-free list.)

- [ ] **Step 4: Add the capital meter + the columns to `cockpit.html`**

After `{{ headline_card(headline, headline_curve) }}` (line 10), add the capital meter:
```html
  {% if overview.gross_deployed or overview.ceiling %}
  <div class="meter">
    <div class="mrow"><span>Capital deployed</span><span><b>${{ overview.gross_deployed|money }}</b> / ${{ overview.ceiling|money }}</span></div>
    <div class="capbar"><i style="width:{{ ((overview.gross_deployed / overview.ceiling * 100) if overview.ceiling else 0)|round(0, 'floor') }}%"></i></div>
    <div class="mcap"><span>{{ overview.n_funded }} funded · {{ overview.n_defunded }} de-funded</span>
      <span>{{ ((overview.gross_deployed / overview.ceiling * 100) if overview.ceiling else 0)|round(0, 'floor')|int }}% of ceiling</span></div>
  </div>
  {% endif %}
```

Change the "All forward tracks" table header (line 41) to:
```html
    <thead><tr><th>track</th><th>tier</th><th>allocated</th><th>funding</th><th>backtest</th><th>forward gate</th></tr></thead>
```
Change its body loop (lines 43-55) to iterate `fleet_top` and add the two cells + the nested exec sub-row:
```html
    {% for f in fleet_top|sort(attribute='tier') %}
      <tr>
        <td data-label="track"><a href="/strategy/{{ f.strategy_id }}">{{ f.display_name }}</a>
          <span class="muted" style="font-size:11px"> · {{ f.venue }}</span> {{ exec_badge(f.execution) }}
          {% if f.reference_only %}<span class="muted" style="font-size:9px;border:1px solid currentColor;border-radius:3px;padding:0 4px;opacity:.55" title="reference-only">ref</span>{% endif %}
          {% if f.status %}<span class="badge WAIT" style="font-size:10px" title="{{ f.note }}">{{ f.status }}</span>{% endif %}</td>
        <td data-label="tier" class="muted">{{ f.tier }}</td>
        <td data-label="allocated">
          {%- if f.alloc_dollars is not none and f.alloc_dollars > 0 -%}
            <b>${{ f.alloc_dollars|money }}</b>{% if f.alloc_weight is not none %} <span class="muted">· {{ (f.alloc_weight*100)|round|int }}%</span>{% endif %}
          {%- elif f.funding_label == 'de-funded' -%}<span class="muted">$0</span>
          {%- else -%}<span class="muted">—</span>{%- endif -%}</td>
        <td data-label="funding">
          {%- if f.funding_label == 'funded' -%}<span class="b funded small"><span class="dot"></span>funded</span>
          {%- elif f.funding_label == 'decaying' -%}<span class="b decaying small"><span class="dot"></span>decaying {{ f.funding_decay_run }}/10</span>
          {%- elif f.funding_label == 'de-funded' -%}<span class="b defunded small"><span class="dot"></span>de-funded</span>
          {%- else -%}<span class="muted">—</span>{%- endif -%}</td>
        <td data-label="backtest">
          {% if f.bt_sharpe is not none %}Sh {{ '%.2f'|format(f.bt_sharpe) }}{% if f.bt_cagr is not none %} / {{ '%+.0f'|format(f.bt_cagr * 100) }}%{% endif %}
          {% else %}<span class="muted">—</span>{% endif %}</td>
        <td data-label="forward gate">{{ f.days }}/{{ f.gate_min_days }} · <span class="badge {{ f.gate_status }}">{{ f.gate_status }}</span></td>
      </tr>
      {% if f.exec_twin_sid and f.exec_return_pct is not none %}
      <tr class="exrow">
        <td data-label="track" colspan="2">↳ <a href="/strategy/{{ f.exec_twin_sid }}">executed</a> <span class="exec-tag">{{ f.venue }}</span></td>
        <td data-label="allocated" class="muted">envelope</td>
        <td data-label="funding" class="muted">net of fees</td>
        <td data-label="backtest" class="muted">exec {{ '%+.2f'|format(f.exec_return_pct) }}%</td>
        <td data-label="forward gate">Δ <span class="{{ 'pos' if f.exec_divergence_pct >= 0 else 'neg' }}">{{ '%+.2f'|format(f.exec_divergence_pct) }}%</span></td>
      </tr>
      {% endif %}
    {% endfor %}
```

- [ ] **Step 5: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/integration/test_cockpit_render.py -q` → PASS (all).
Run: `ruff check src/fxhnt/adapters/web/app.py`  (only your changed lines must be clean; app.py may carry pre-existing violations — do not touch those).
```bash
git add src/fxhnt/adapters/web/templates/cockpit.html src/fxhnt/adapters/web/app.py tests/integration/test_cockpit_render.py
git commit -m "feat(cockpit): overview capital meter + allocation/funding columns + nested exec twin

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 5: `strategy.html` + detail route — funding / allocation / executed-reality blocks

**Files:**
- Modify: `src/fxhnt/adapters/web/templates/strategy.html`
- Modify: `src/fxhnt/adapters/web/app.py` (the `strategy` route — optional crypto slippage read)
- Test: `tests/integration/test_cockpit_render.py` (extend)

**Interfaces:**
- Consumes: `StrategyDetail.alloc_*`/`funding_*`/`exec_*` (Task 2). Optional: `bybit_testnet_nav` mean slippage for the crypto exec twin.

- [ ] **Step 1: Write the failing test (extend)**

Append to `tests/integration/test_cockpit_render.py`:

```python
def test_detail_shows_lifecycle_blocks(tmp_path):
    repo = ForwardNavRepo(f"sqlite:///{tmp_path/'c.db'}")
    repo.migrate()
    at = dt.datetime(2026, 7, 10)
    repo.replace_funding([StrategyFundingRow(strategy_id="bybit_4edge", state="funded", label="funded",
        decay_run=0, recovery_run=0, first_pass_date=None, policy_version=2, policy_hash="h",
        as_of="2026-07-10", computed_at=at)], at)
    repo.replace_allocations([StrategyAllocationRow(strategy_id="bybit_4edge", target_weight=0.54,
        target_dollars=18900.0, policy_version=1, policy_hash="h", as_of="2026-07-10", computed_at=at)], at)
    r = TestClient(create_app(repo)).get("/strategy/bybit_4edge")
    assert r.status_code == 200
    assert "Funding health" in r.text
    assert "Allocation" in r.text
    assert "18,900" in r.text or "18900" in r.text
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest tests/integration/test_cockpit_render.py::test_detail_shows_lifecycle_blocks -q`
Expected: FAIL (no lifecycle blocks yet).

- [ ] **Step 3: Add the lifecycle blocks to `strategy.html`**

Read `strategy.html` first. After the status/forward header block and BEFORE the "backtest verdict" section, insert:

```html
{% if d.funding_label %}
<div class="lc">
  <div class="lct">Funding health <span class="r">
    {%- if d.funding_label == 'funded' -%}<span class="b funded small"><span class="dot"></span>funded</span>
    {%- elif d.funding_label == 'decaying' -%}<span class="b decaying small"><span class="dot"></span>decaying {{ d.funding_decay_run }}/10</span>
    {%- else -%}<span class="b defunded small"><span class="dot"></span>de-funded</span>{%- endif -%}</span></div>
  <div class="lcb">
    <div class="kv"><span class="k">State</span><span class="v">{{ d.funding_label }}</span></div>
    <div class="kv"><span class="k">First passed</span><span class="v">{{ d.funding_first_pass_date or '— (deploy-tier by config)' }}</span></div>
    <div class="kv"><span class="k">Decay / recovery run</span><span class="v">{{ d.funding_decay_run }} / {{ d.funding_recovery_run }}</span></div>
    <div class="why">De-funds after 10 consecutive divergence days from the backtest; re-funds after 15 clean days.
      {% if d.funding_policy_version %}<span class="muted">gate-policy v{{ d.funding_policy_version }}</span>{% endif %}</div>
  </div>
</div>
{% endif %}
{% if d.alloc_dollars is not none %}
<div class="lc">
  <div class="lct">Allocation <span class="r">${{ d.alloc_dollars|money }}</span></div>
  <div class="lcb">
    <div class="kv"><span class="k">Target capital</span><span class="v">${{ d.alloc_dollars|money }}</span></div>
    {% if d.alloc_weight is not none %}<div class="kv"><span class="k">Book weight</span><span class="v">{{ (d.alloc_weight*100)|round|int }}% of gross</span></div>{% endif %}
    <div class="why">Ex-ante vol-target sizing, marginal-Sharpe survivor, bounded by the $35k fund ceiling.
      {% if d.alloc_policy_version %}<span class="muted">alloc-policy v{{ d.alloc_policy_version }}</span>{% endif %}</div>
  </div>
</div>
{% elif d.funding_label == 'de-funded' %}
<div class="lc"><div class="lct">Allocation <span class="r">$0</span></div>
  <div class="lcb"><div class="why">$0 — de-funded / killswitched, held as cash.</div></div></div>
{% endif %}
{% if d.exec_twin_sid and d.exec_return_pct is not none %}
<div class="lc">
  <div class="lct">Executed reality <span class="r"><span class="exec-tag">{{ d.venue }}</span></span></div>
  <div class="lcb">
    <div class="kv"><span class="k">Executed forward</span><span class="v">{{ '%+.2f'|format(d.exec_return_pct) }}%</span></div>
    <div class="kv"><span class="k">Modeled forward</span><span class="v">{{ '%+.2f'|format(d.total_return_pct) }}%</span></div>
    <div class="kv"><span class="k">Divergence</span><span class="v {{ 'pos' if d.exec_divergence_pct >= 0 else 'neg' }}">{{ '%+.2f'|format(d.exec_divergence_pct) }}%</span></div>
    {% if exec_slippage_bps is not none %}<div class="kv"><span class="k">Mean slippage</span><span class="v">{{ '%.1f'|format(exec_slippage_bps) }} bp</span></div>{% endif %}
    <div class="why">Envelope-basis, funding &amp; fee net — execution decay shows as divergence.
      <a href="/strategy/{{ d.exec_twin_sid }}">{{ d.exec_twin_sid }} →</a></div>
  </div>
</div>
{% endif %}
```

- [ ] **Step 4: Wire the optional crypto slippage in the detail route**

In `app.py` `strategy(...)`, add before the `TemplateResponse` (graceful, crypto-only):
```python
        exec_slippage_bps = None
        if d.exec_twin_sid == "bybit_4edge_exec":
            try:
                from fxhnt.adapters.persistence.bybit_testnet_repo import BybitTestnetRepo
                nav = BybitTestnetRepo(get_settings().operational_dsn).latest_testnet_nav()
                exec_slippage_bps = nav.mean_exec_slippage_bps if nav else None
            except Exception:  # noqa: BLE001 — slippage is a nice-to-have; never break the detail page
                exec_slippage_bps = None
```
Add `"exec_slippage_bps": exec_slippage_bps` to the context dict. If `BybitTestnetRepo` has no `latest_testnet_nav()` reader, add a minimal one (`select ... order by run_date desc limit 1`); if that is more than a trivial add, pass `exec_slippage_bps=None` and note the slippage read is deferred (the block already hides when None).

- [ ] **Step 5: Run to verify pass + ruff + commit**

Run: `python -m pytest tests/integration/test_cockpit_render.py -q` → PASS (all).
Run: `ruff check src/fxhnt/adapters/web/app.py`  (changed lines clean).
```bash
git add src/fxhnt/adapters/web/templates/strategy.html src/fxhnt/adapters/web/app.py tests/integration/test_cockpit_render.py
git commit -m "feat(cockpit): detail page funding / allocation / executed-reality blocks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 6: Seed script + local browser verification (the acceptance)

**Files:**
- Create: `scripts/seed_cockpit_demo.py`
- (No unit test — this task's deliverable is the browser verification + screenshots.)

**Interfaces:**
- Produces: a runnable seed that fills a LOCAL sqlite/postgres with representative forward + allocation + funding + exec rows, so the cockpit renders the full surfacing un-confounded by prod's stale schema.

- [ ] **Step 1: Write the seed script**

Create `scripts/seed_cockpit_demo.py` — seed a local DB (DSN from `FXHNT_OPERATIONAL_DSN`, default `sqlite:///./cockpit-demo.db`) with: a `bybit_4edge` forward_summary + backtest_summary + nav history, a `bybit_4edge_exec` forward_summary (executed), a `multistrat` (de-funded) forward_summary, `strategy_allocation` rows (bybit_4edge $18,900/0.54; positioning $9,500/0.27), and `strategy_funding` rows (bybit_4edge funded; positioning decaying 4/10; multistrat de-funded). Use `repo.migrate()` (creates the CURRENT schema) then `replace_allocations`/`replace_funding` + a `Session` insert for the summaries/nav. Mirror the seed values from `tests/integration/test_dashboard_surfacing.py` so the screenshots match the tests. Print the DSN + a hint to run the cockpit against it.

- [ ] **Step 2: Seed + serve the cockpit locally**

```bash
cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate
export FXHNT_OPERATIONAL_DSN="sqlite:///$(pwd)/cockpit-demo.db"
python scripts/seed_cockpit_demo.py
FXHNT_DEV_PORT=8801 python scripts/dev_cockpit_local.py    # dev helper serves against FXHNT_OPERATIONAL_DSN, no prod port-forward
```
Poll `curl -sf http://127.0.0.1:8801/healthz` until it responds. (If `dev_cockpit_local.py` hardcodes the prod DSN, run the app via `uvicorn` on `create_app(ForwardNavRepo(os.environ["FXHNT_OPERATIONAL_DSN"]))` instead — no `.migrate()` needed, the seed already migrated.)

- [ ] **Step 3: Playwright-verify desktop + mobile — READ THE NUMBERS**

Drive Playwright (the controller does this, not a subagent):
- Desktop (default viewport): screenshot `/` — confirm the **capital meter** (`$28,400 / $35,000`, fill bar), the **allocated** + **funding** columns (funded green / decaying amber / de-funded red), and the **nested exec sub-row** under bybit_4edge. Screenshot `/strategy/bybit_4edge` — confirm the **Funding health**, **Allocation**, **Executed reality** blocks.
- Mobile: resize to 390×844, reload `/` — confirm the **bottom-tab nav** is fixed at the bottom, the fleet is **cards**, and the funding badges + allocation read cleanly.
- Read the actual rendered numbers against the seed (not "looks fine"): the meter %, the allocation dollars, the funding labels, the divergence.

- [ ] **Step 4: Commit the seed script**

```bash
git add scripts/seed_cockpit_demo.py
git commit -m "chore(cockpit): local demo seed for browser verification of the surfacing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

## Final verification (after all tasks)

- [ ] Full suite: `cd /home/jgrusewski/Work/fxhnt && source .venv/bin/activate && python -m pytest -q -p no:cacheprovider` — all green (prior baseline 1952 + the new cockpit tests).
- [ ] `ruff check src/fxhnt/application/dashboard_service.py src/fxhnt/ports/dashboard.py src/fxhnt/adapters/persistence/forward_nav.py` — no new violations.
- [ ] The Task 6 browser screenshots (desktop + mobile, overview + detail) reviewed — the numbers read correctly. This is the acceptance gate before the (separate) deploy.
