diff --git a/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Push_Session_Data_via_WebSocket.md b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Push_Session_Data_via_WebSocket.md new file mode 100644 index 00000000..157c81d4 --- /dev/null +++ b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Push_Session_Data_via_WebSocket.md @@ -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 diff --git a/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Validation_Report.md b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Validation_Report.md new file mode 100644 index 00000000..d851fce9 --- /dev/null +++ b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/FU-P11-T2-1_Validation_Report.md @@ -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) | diff --git a/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/REVIEW_FU-P11-T2-1_session_websocket_push.md b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/REVIEW_FU-P11-T2-1_session_websocket_push.md new file mode 100644 index 00000000..b1428d4e --- /dev/null +++ b/SPECS/ARCHIVE/FU-P11-T2-1_Push_Session_Data_via_WebSocket/REVIEW_FU-P11-T2-1_session_websocket_push.md @@ -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. diff --git a/SPECS/ARCHIVE/INDEX.md b/SPECS/ARCHIVE/INDEX.md index 9bc9e90b..a60822d8 100644 --- a/SPECS/ARCHIVE/INDEX.md +++ b/SPECS/ARCHIVE/INDEX.md @@ -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 @@ -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 @@ -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 | diff --git a/SPECS/INPROGRESS/next.md b/SPECS/INPROGRESS/next.md index 6cab6905..c02673b0 100644 --- a/SPECS/INPROGRESS/next.md +++ b/SPECS/INPROGRESS/next.md @@ -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) diff --git a/SPECS/Workplan.md b/SPECS/Workplan.md index 2eb9a843..3038cb89 100644 --- a/SPECS/Workplan.md +++ b/SPECS/Workplan.md @@ -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 @@ -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 @@ -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 diff --git a/src/mcpbridge_wrapper/webui/server.py b/src/mcpbridge_wrapper/webui/server.py index a16d8b6b..9367b57d 100644 --- a/src/mcpbridge_wrapper/webui/server.py +++ b/src/mcpbridge_wrapper/webui/server.py @@ -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) diff --git a/src/mcpbridge_wrapper/webui/static/dashboard.js b/src/mcpbridge_wrapper/webui/static/dashboard.js index e5768b42..b8328439 100644 --- a/src/mcpbridge_wrapper/webui/static/dashboard.js +++ b/src/mcpbridge_wrapper/webui/static/dashboard.js @@ -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 --- @@ -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); @@ -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) { diff --git a/tests/unit/webui/test_server.py b/tests/unit/webui/test_server.py index 7ec0e34b..baa1e4a5 100644 --- a/tests/unit/webui/test_server.py +++ b/tests/unit/webui/test_server.py @@ -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."""