Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# PRD: FU-P11-T2-1 — Push Session Data via WebSocket for Real-Time Timeline Updates

**Task ID:** FU-P11-T2-1
**Phase:** Phase 11 — Dashboard Enhancements
**Priority:** P3
**Date:** 2026-02-16
**Branch:** `feature/FU-P11-T2-1-session-websocket-push`
**Dependencies:** P11-T2 ✅

---

## Background

P11-T2 added the session timeline view to the dashboard with a `GET /api/sessions` endpoint and a `renderTimeline()` frontend function. The original acceptance criteria included "real-time via WebSocket," but the implementation used a 15-second `setInterval` poll instead. This task completes that acceptance criterion by pushing session data through the existing WebSocket `metrics_update` message.

---

## Problem Statement

The session timeline currently refreshes on a 15-second interval (`setInterval(loadSessions, 15000)`). This is inconsistent with the rest of the dashboard (KPIs, charts, latency table) that update in real-time via WebSocket push. Users see stale session data for up to 15 seconds after a new tool call.

---

## Solution

1. **Backend (`server.py`)**: Include `sessions` in the WebSocket `metrics_update` payload by calling `detect_sessions()` on each broadcast.
2. **Frontend (`dashboard.js`)**: In `handleMetricsUpdate`, if `data.sessions` is present, call `renderTimeline(data.sessions)`. Remove the `setInterval(loadSessions, 15000)` poll. Optionally, include sessions in the HTTP fallback `startPolling()` for when WebSocket is disconnected.

---

## Deliverables

### Backend: `src/mcpbridge_wrapper/webui/server.py`

In `ws_metrics`, extend the sent JSON:

```python
# Before (current)
await websocket.send_json({
"type": "metrics_update",
"summary": summary,
"timeseries": timeseries,
})

# After
entries = audit.get_entries(limit=10000)
sessions = detect_sessions(entries, gap_seconds=float(config.session_gap_seconds))
await websocket.send_json({
"type": "metrics_update",
"summary": summary,
"timeseries": timeseries,
"sessions": sessions,
})
```

The `ws_metrics` handler already has access to `audit` and `config` through closure. `detect_sessions` is already imported at the top of `server.py`.

### Frontend: `src/mcpbridge_wrapper/webui/static/dashboard.js`

1. **`handleMetricsUpdate`**: Add `renderTimeline(data.sessions)` when `data.sessions` is present:
```js
function handleMetricsUpdate(data) {
updateKPIs(data.summary);
updateToolCharts(data.summary.tool_counts);
updateErrorBreakdownChart(data.summary.error_counts_by_code || {});
updateLatencyTable(data.summary.tool_latency);
updateTimeline(data.timeseries);
updateLatencyChart(data.timeseries);
if (data.sessions !== undefined) {
renderTimeline(data.sessions);
}
}
```

2. **Remove 15s poll**: Remove `setInterval(loadSessions, 15000)` from `init()`.

3. **Fallback polling**: In `startPolling()`, add sessions to the HTTP fallback (only fires when WS is closed). Include `GET /api/sessions` in the polling Promise.all and call `renderTimeline()` from the result.

### Tests: `tests/unit/webui/test_server.py`

Add a test asserting that the WebSocket `metrics_update` message includes a `sessions` key.

---

## Acceptance Criteria

- [ ] WebSocket `metrics_update` message includes `sessions` key (list of session objects)
- [ ] `handleMetricsUpdate` calls `renderTimeline(data.sessions)` when sessions are present
- [ ] `setInterval(loadSessions, 15000)` removed from `init()`
- [ ] HTTP fallback polling includes sessions (for WS-disconnected scenario)
- [ ] New test: WS message contains `sessions` key
- [ ] All existing tests pass; coverage ≥ 90%

---

## Non-Goals

- No changes to the session detection algorithm (`sessions.py`)
- No changes to the `GET /api/sessions` endpoint (kept for manual refresh button)
- No UI/CSS changes
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Validation Report: FU-P11-T2-1 — Push Session Data via WebSocket

**Date:** 2026-02-16
**Branch:** `feature/FU-P11-T2-1-session-websocket-push`
**Verdict:** PASS

---

## Acceptance Criteria

| Criterion | Status |
|-----------|--------|
| WebSocket `metrics_update` message includes `sessions` key | ✅ PASS |
| `handleMetricsUpdate` calls `renderTimeline(data.sessions)` when sessions present | ✅ PASS |
| `setInterval(loadSessions, 15000)` removed from `init()` | ✅ PASS |
| HTTP fallback polling includes sessions (WS-disconnected scenario) | ✅ PASS |
| New test: WS message contains `sessions` key | ✅ PASS |
| All existing tests pass; coverage ≥ 90% | ✅ PASS |

---

## Changes Made

### `src/mcpbridge_wrapper/webui/server.py`
- `ws_metrics`: Added `audit.get_entries(limit=10000)` + `detect_sessions(...)` call on each WebSocket broadcast tick
- Added `"sessions": sessions` to the `metrics_update` JSON payload

### `src/mcpbridge_wrapper/webui/static/dashboard.js`
- `handleMetricsUpdate`: Added `if (data.sessions !== undefined) { renderTimeline(data.sessions); }` — session timeline now updates on every WebSocket push
- `startPolling` (HTTP fallback): Added `GET /api/sessions` to the `Promise.all` — sessions also refresh during WS-disconnected fallback polling
- `init()`: Removed `setInterval(loadSessions, 15000)` — 15s poll eliminated; WS push is now the primary update path

### `tests/unit/webui/test_server.py`
- Added `test_websocket_metrics_update_includes_sessions` in `TestCreateApp`: asserts `message["sessions"]` exists and is a list

---

## Quality Gates

| Gate | Result |
|------|--------|
| `pytest` | 458 passed, 5 skipped |
| `ruff check src/` | All checks passed |
| `pytest --cov` | 95.95% total (≥ 90% required) |
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## REVIEW REPORT — FU-P11-T2-1 Session WebSocket Push

**Scope:** main..HEAD
**Files:** 3 source/test files changed (+28 lines net in src+tests)
**Date:** 2026-02-16

---

### Summary Verdict

- [ ] Approve
- [x] Approve with comments
- [ ] Request changes
- [ ] Block

**Overall:** The implementation is minimal, correct, and well-tested. One low-severity performance note below; no blockers.

---

### Critical Issues

_None._

---

### Secondary Issues

**[Low] `audit.get_entries(limit=10000)` + `detect_sessions()` called on every WebSocket tick**

- On the default 1-second refresh interval, `detect_sessions` scans up to 10,000 entries once per second. `detect_sessions` is O(n) and lightweight for typical audit log sizes, but under very high tool-call volume (approaching 10k in-memory entries) this adds ~1–2ms CPU per tick.
- **Suggestion:** Not worth addressing now. If profiling identifies this as a hotspot, cache the sessions result and invalidate on new `log()` calls via a dirty flag in `AuditLogger`. Log as a future optimisation if observed.

**[Nit] `float(config.session_gap_seconds)` cast is unnecessary**

- `config.session_gap_seconds` returns `int`; Python auto-promotes `int` to `float` in arithmetic. The explicit cast adds noise with no functional benefit.
- **Suggestion:** Remove the cast: `gap_seconds=config.session_gap_seconds`. Low priority, cosmetic only.

---

### Architectural Notes

- The fallback polling path (`startPolling`) now makes 3 concurrent HTTP fetches instead of 2. This is intentional and correct — sessions must also update when the WebSocket is disconnected.
- `loadSessions()` is still called once at `init()` for the initial render and remains wired to the manual "Refresh" button. This is correct — those paths are independent of the WS push.
- The `data.sessions !== undefined` guard in `handleMetricsUpdate` is safe: it means old WS messages (e.g. from a server that hasn't been updated yet) won't break the frontend.

---

### Tests

- New test `test_websocket_metrics_update_includes_sessions` asserts `message["sessions"]` is a list. ✅
- All 458 tests pass; coverage 95.95% (≥90% required). ✅
- No missing coverage from this change.

---

### Next Steps

- No blocking follow-up tasks required.
- The `float()` cast nit is cosmetic only; not worth a dedicated task.
5 changes: 4 additions & 1 deletion SPECS/ARCHIVE/INDEX.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# mcpbridge-wrapper Tasks Archive

**Last Updated:** 2026-02-16 (P12-T2)
**Last Updated:** 2026-02-16 (FU-P11-T2-1)

## Archived Tasks

Expand Down Expand Up @@ -98,6 +98,7 @@
| P12-T3 | [P12-T3_Add_Error_Classification_and_Categorization/](P12-T3_Add_Error_Classification_and_Categorization/) | 2026-02-15 | PASS |
| P12-T4 | [P12-T4_Add_documentation_about_data_storage/](P12-T4_Add_documentation_about_data_storage/) | 2026-02-15 | PASS |
| P12-T2 | [P12-T2_Add_Tool_Parameter_Frequency_Analysis/](P12-T2_Add_Tool_Parameter_Frequency_Analysis/) | 2026-02-16 | PASS |
| FU-P11-T2-1 | [FU-P11-T2-1_Push_Session_Data_via_WebSocket/](FU-P11-T2-1_Push_Session_Data_via_WebSocket/) | 2026-02-16 | PASS |

## Historical Artifacts

Expand Down Expand Up @@ -273,3 +274,5 @@
| 2026-02-15 | P12-T4 | Archived REVIEW_P12-T4_data_storage_documentation report |
| 2026-02-16 | P12-T2 | Archived Add_Tool_Parameter_Frequency_Analysis (PASS) |
| 2026-02-16 | P12-T2 | Archived REVIEW_P12-T2_param_frequency_analysis report |
| 2026-02-16 | FU-P11-T2-1 | Archived Push_Session_Data_via_WebSocket (PASS) |
| 2026-02-16 | FU-P11-T2-1 | Archived REVIEW_FU-P11-T2-1_session_websocket_push report |
11 changes: 4 additions & 7 deletions SPECS/INPROGRESS/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ The previously selected task has been archived.

## Recently Archived

- 2026-02-16 — FU-P11-T2-1: Push session data via WebSocket (PASS)
- 2026-02-16 — P12-T2: Add Tool Parameter Frequency Analysis (PASS)
- 2026-02-15 — P12-T4: Add documentation about data storage (PASS)
- 2026-02-15 — P12-T3: Add Error Classification & Categorization (PASS)
- 2026-02-15 — P12-T1: Add MCP Client Identification (PASS)
- 2026-02-15 — P11-T4: Add Keyboard Shortcuts & Command Palette (PASS)
- 2026-02-15 — P11-T1: Add Tool Call Detail Inspector (PASS)

## Suggested Next Tasks

- P11-T1: Add Tool Call Detail Inspector (P2, depends on P10-T1 ✅)
- FU-P11-T2-2: Add `limit` query param to `GET /api/sessions` (P3, depends on P11-T2 ✅)
- FU-P11-T1-1: Refactor `_FakeWebUIConfig` test stub to use `MagicMock(spec=WebUIConfig)` (P3, depends on P11-T1 ✅)
- FU-P12-T2-1: Fix stacking click event listeners in `updateLatencyTable` (P3, depends on P12-T2 ✅)
- FU-P12-T1-1: Remove or document `MCPInitializeParams` in schemas (P3, depends on P12-T1 ✅)
- FU-P12-T1-2: Add code comment clarifying stdin-only client capture in `on_request` (P3, depends on P12-T1 ✅)
- FU-P12-T3-1: Document unused `error_message` parameter in `MetricsCollector.record_response` (P3, depends on P12-T3 ✅)
- FU-P12-T3-2: Add `error_code` column to audit CSV export (P3, depends on P12-T3 ✅)
- P11-T2: Add Session Timeline View (P3, depends on P11-T1)
8 changes: 4 additions & 4 deletions SPECS/Workplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1551,7 +1551,7 @@ Phase 9 Follow-up Backlog

**Intent:** Enhance the dashboard with better debugging tools, session awareness, theming, and keyboard-driven workflows.

#### P11-T1: Add Tool Call Detail Inspector (Request/Response Viewer)
#### P11-T1: Add Tool Call Detail Inspector (Request/Response Viewer)
- **Description:** Add a clickable row expansion or slide-out panel in the audit table that displays the full JSON-RPC request and response payloads. Payloads are syntax-highlighted and collapsible. Backend stores truncated payloads in a bounded ring buffer (last 500, max 64KB each) behind an optional `capture_payload` config flag (default off for privacy). New API: `GET /api/audit/{request_id}/detail` returns `{request: {...}, response: {...}}`. Frontend: click audit row to expand inline or open side panel with pretty-printed JSON.
- **Priority:** P1
- **Dependencies:** P10-T1
Expand Down Expand Up @@ -1600,10 +1600,10 @@ Phase 9 Follow-up Backlog

---

#### FU-P11-T2-1: Push session data via WebSocket for real-time timeline updates
#### FU-P11-T2-1: Push session data via WebSocket for real-time timeline updates
- **Description:** Extend the WebSocket `metrics_update` message in `server.py` to include current session data from `detect_sessions()`. Update `dashboard.js` to refresh the timeline on every WebSocket message instead of using the 15s poll. This fulfills the original P11-T2 acceptance criterion of "real-time via WebSocket."
- **Priority:** P3
- **Dependencies:** P11-T2
- **Dependencies:** P11-T2
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- Updated `src/mcpbridge_wrapper/webui/server.py` — include sessions in WebSocket payload
Expand All @@ -1618,7 +1618,7 @@ Phase 9 Follow-up Backlog
#### FU-P11-T2-2: Add `limit` query param to `GET /api/sessions`
- **Description:** Add an optional `limit` query parameter (default: all, max: 10000) to `GET /api/sessions` that caps the number of audit entries fed to `detect_sessions()`. This prevents slow responses for large audit logs.
- **Priority:** P3
- **Dependencies:** P11-T2
- **Dependencies:** P11-T2
- **Parallelizable:** yes
- **Outputs/Artifacts:**
- Updated `src/mcpbridge_wrapper/webui/server.py` — `limit` query param on sessions endpoint
Expand Down
3 changes: 3 additions & 0 deletions src/mcpbridge_wrapper/webui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,14 @@ async def ws_metrics(websocket: WebSocket) -> None:
# Send metrics every refresh interval
summary = metrics.get_summary()
timeseries = metrics.get_timeseries(config.chart_history_seconds)
entries = audit.get_entries(limit=10000)
sessions = detect_sessions(entries, gap_seconds=float(config.session_gap_seconds))
await websocket.send_json(
{
"type": "metrics_update",
"summary": summary,
"timeseries": timeseries,
"sessions": sessions,
}
)
await asyncio.sleep(config.dashboard_refresh_interval_ms / 1000.0)
Expand Down
15 changes: 12 additions & 3 deletions src/mcpbridge_wrapper/webui/static/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@
updateLatencyTable(data.summary.tool_latency);
updateTimeline(data.timeseries);
updateLatencyChart(data.timeseries);
if (data.sessions !== undefined) {
renderTimeline(data.sessions);
}
}

// --- Audit Detail Panel ---
Expand Down Expand Up @@ -529,12 +532,19 @@
setInterval(function () {
if (ws && ws.readyState === WebSocket.OPEN) return;

var gapInput = document.getElementById("session-gap-input");
var gap = gapInput ? parseInt(gapInput.value, 10) || 300 : 300;
Promise.all([
fetch("/api/metrics").then(function (r) { return r.json(); }),
fetch("/api/metrics/timeseries?seconds=300").then(function (r) { return r.json(); }),
fetch("/api/sessions?gap_seconds=" + gap).then(function (r) { return r.json(); }),
])
.then(function (results) {
handleMetricsUpdate({ summary: results[0], timeseries: results[1] });
handleMetricsUpdate({
summary: results[0],
timeseries: results[1],
sessions: results[2].sessions || [],
});
})
.catch(function () {});
}, 2000);
Expand Down Expand Up @@ -751,9 +761,8 @@
startPolling();
loadAuditLogs();
loadSessions();
// Refresh audit logs and sessions periodically
// Refresh audit logs periodically; sessions are pushed via WebSocket
setInterval(loadAuditLogs, 5000);
setInterval(loadSessions, 15000);

var btnRefreshSessions = document.getElementById("btn-refresh-sessions");
if (btnRefreshSessions) {
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/webui/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ def test_dashboard_served(self, client):
assert "/static/dashboard.css" in response.text
assert "/static/dashboard.js" in response.text

def test_websocket_metrics_update_includes_sessions(self, client, audit):
"""WebSocket metrics_update message includes sessions key."""
with client.websocket_connect("/ws/metrics") as websocket:
message = websocket.receive_json()
assert message["type"] == "metrics_update"
assert "sessions" in message
assert isinstance(message["sessions"], list)


class TestAuth:
"""Test authentication."""
Expand Down