---
phase: 05.1-post-audit-polish
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/sidecar/http_server.py
  - python/opendeviationbar/sidecar/__init__.py
  - tests/test_sync_flush_atomic_replace.py
  - tests/test_health_ws_mode.py
autonomous: true
requirements: []
gap_closure: true

must_haves:
  truths:
    - "/health endpoint includes ws_mode field and expected_ws_connections count"
    - "test helper _make_bar() uses open_time_ms/close_time_ms keys matching production Rust output"
    - "cross-midnight filter test exercises the actual key name that production code reads"
  artifacts:
    - path: "python/opendeviationbar/sidecar/http_server.py"
      provides: "/health response with ws_mode and expected_ws_connections"
      contains: "ws_mode"
    - path: "tests/test_sync_flush_atomic_replace.py"
      provides: "corrected _make_bar helper with open_time_ms/close_time_ms keys"
      contains: "open_time_ms"
    - path: "tests/test_health_ws_mode.py"
      provides: "dedicated test for /health ws_mode field"
      contains: "ws_mode"
  key_links:
    - from: "python/opendeviationbar/sidecar/__init__.py"
      to: "python/opendeviationbar/sidecar/http_server.py"
      via: "_health_state['ws_mode'] set at startup, read by _handle_health"
      pattern: "_health_state.*ws_mode"
    - from: "tests/test_sync_flush_atomic_replace.py"
      to: "python/opendeviationbar/sidecar/midnight.py"
      via: "_make_bar dict keys match bar.get('open_time_ms') in L1 filter"
      pattern: "open_time_ms"
---

<objective>
Close two tech debt items from v3.0 milestone audit: (1) MISS-01 -- /health endpoint missing ws_mode and active WS connection count, (2) test helper key mismatch where _make_bar() uses open_time_us/close_time_us but production Rust emits open_time_ms/close_time_ms.

Purpose: MISS-01 blocks SC-4 observability (monit/heartbeat can't verify WS mode). The test helper mismatch means TestSyncFlushCrossMidnightFilter's cross-midnight bar always passes the filter (open_time_ms gets 0 via .get default), making the test pass vacuously.
Output: Patched /health endpoint, corrected test helper, new test for ws_mode field.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@python/opendeviationbar/sidecar/http_server.py
@python/opendeviationbar/sidecar/__init__.py
@tests/test_sync_flush_atomic_replace.py

<interfaces>
<!-- Key types and contracts the executor needs. -->

From python/opendeviationbar/sidecar/http_server.py:

```python
# Module-level mutable state dict shared between sidecar main loop and HTTP handler
_health_state: dict[str, object] = {
    "metrics": {},
    "stream_signals": { ... },
}

def _update_health_metrics(metrics: dict[str, int]) -> None: ...
def _start_health_server(port: int, sse_enabled: bool = True) -> HTTPServer | None: ...
```

From python/opendeviationbar/sidecar/**init**.py (line 219, 509, 719):

```python
# ws_mode is available from config at startup:
engine = StreamManager(..., ws_mode=config.ws_mode)
_health_state["start_monotonic"] = sidecar_start_time
_health_state["engine"] = engine
```

From python/opendeviationbar/config/sidecar.py (line 201):

```python
ws_mode: Literal["combined", "per_symbol"] = "combined"
```

From src/helpers.rs (line 103-104) -- production bar dict keys:

```rust
dict.set_item(ikey(py, "open_time_ms"), bar.open_time)?;
dict.set_item(ikey(py, "close_time_ms"), bar.close_time)?;
```

From python/opendeviationbar/sidecar/midnight.py (line 709) -- L1 cross-midnight filter:

```python
open_us = bar.get("open_time_ms", 0)
close_us = bar.get("close_time_ms", 0)
```

</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add ws_mode and expected_ws_connections to /health endpoint (MISS-01)</name>
  <files>python/opendeviationbar/sidecar/__init__.py, python/opendeviationbar/sidecar/http_server.py, tests/test_health_ws_mode.py</files>
  <read_first>python/opendeviationbar/sidecar/http_server.py, python/opendeviationbar/sidecar/__init__.py (lines 500-525, 215-225)</read_first>
  <behavior>
    - Test 1: /health response JSON contains "ws_mode" field with value "combined" when _health_state["ws_mode"] = "combined"
    - Test 2: /health response JSON contains "expected_ws_connections" field with value 2 when ws_mode is "combined"
    - Test 3: /health response JSON contains "expected_ws_connections" field equal to len(symbols) when ws_mode is "per_symbol"
  </behavior>
  <action>
    1. In `python/opendeviationbar/sidecar/__init__.py`, after line 509 (`_health_state["start_monotonic"] = sidecar_start_time`), add:
       ```python
       _health_state["ws_mode"] = config.ws_mode
       _health_state["ws_symbols_count"] = len(config.symbols)
       ```

    2. In `python/opendeviationbar/sidecar/http_server.py`, in `_handle_health()`, add two fields to the `resp` dict (after "stream_signals"):
       ```python
       "ws_mode": _health_state.get("ws_mode", "unknown"),
       "expected_ws_connections": (
           2 if _health_state.get("ws_mode") == "combined"
           else _health_state.get("ws_symbols_count", 0)
       ),
       ```

    3. Create `tests/test_health_ws_mode.py` with 3 tests:
       - Test combined mode: set `_health_state["ws_mode"] = "combined"`, start health server on free port, GET /health, assert `ws_mode == "combined"` and `expected_ws_connections == 2`.
       - Test per_symbol mode: set `_health_state["ws_mode"] = "per_symbol"` and `_health_state["ws_symbols_count"] = 18`, assert `expected_ws_connections == 18`.
       - Test default (no ws_mode set): assert `ws_mode == "unknown"`.
       Use same pattern as TestJunctionTelemetryInStatus in test_sync_flush_atomic_replace.py (socket free port, real HTTP server, cleanup in finally).

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_health_ws_mode.py -xvs 2>&1 | tail -30</automated>
  </verify>
  <done>
    - /health JSON response includes "ws_mode" (string) and "expected_ws_connections" (int)
    - combined mode shows 2 connections, per_symbol mode shows symbol count
    - All 3 tests pass
  </done>
</task>

<task type="auto">
  <name>Task 2: Fix _make_bar() key mismatch and verify cross-midnight filter test</name>
  <files>tests/test_sync_flush_atomic_replace.py</files>
  <read_first>tests/test_sync_flush_atomic_replace.py (lines 16-51)</read_first>
  <action>
    1. In `_make_bar()` (line 26-30), change the dict keys from `open_time_us` / `close_time_us` to `open_time_ms` / `close_time_ms` to match production Rust output (src/helpers.rs:103-104). The parameter names are already correct -- only the dict keys in the return value are wrong:
       ```python
       return {
           "_symbol": symbol,
           "_threshold": threshold,
           "open_time_ms": open_time_ms,    # was "open_time_us"
           "close_time_ms": close_time_ms,  # was "close_time_us"
           ...
       }
       ```

    2. In `_make_cross_midnight_bar()` (line 40-51), the local variables use `_us` suffix but the values are actually microseconds (dividing by `day_us = 86_400_000_000`). This is correct since the column is named `_ms` but stores microsecond values (legacy naming from the ms-to-us migration). The function already passes values through `_make_bar(open_time_ms=..., close_time_ms=...)` which now maps to the correct dict key. No changes needed in this function.

    3. Run TestSyncFlushCrossMidnightFilter specifically to verify it still passes AND actually exercises the filter (previously it passed vacuously because `bar.get("open_time_ms", 0)` returned 0 since the key was `open_time_us`).

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_sync_flush_atomic_replace.py -xvs 2>&1 | tail -40</automated>
  </verify>
  <done>
    - _make_bar() returns dict with "open_time_ms" and "close_time_ms" keys
    - All 9 existing tests in test_sync_flush_atomic_replace.py pass
    - TestSyncFlushCrossMidnightFilter now genuinely tests the L1 filter (cross-midnight bar has non-zero open_time_ms that crosses day boundary)
  </done>
</task>

</tasks>

<verification>
1. `uv run --python 3.13 pytest tests/test_health_ws_mode.py tests/test_sync_flush_atomic_replace.py -xvs` -- all tests pass
2. `uv run --python 3.13 pytest tests/ -x --timeout=60` -- no regressions in full test suite
3. `mise run check-full` -- lint + fmt + test all pass
</verification>

<success_criteria>

- /health endpoint JSON includes ws_mode and expected_ws_connections fields (MISS-01 closed)
- test_cross_midnight_bars_filtered exercises the real open_time_ms key path
- All existing tests pass with no regressions
  </success_criteria>

<output>
After completion, create `.planning/phases/05.1-post-audit-polish/05.1-01-SUMMARY.md`
</output>
