feat(portal): live WebSocket connection badge in portal header#449
feat(portal): live WebSocket connection badge in portal header#449
Conversation
|
Bosun CI signal: Bosun-created PR currently has failing checks.
|
There was a problem hiding this comment.
Pull request overview
Adds a live WebSocket connection status badge to the portal header, including styling and basic regression tests, with corresponding UI changes mirrored into site/ui/.
Changes:
- Introduces WebSocket status signals (
wsStatus,wsLastReconnectAt) and updates them during WS lifecycle events. - Renders a new
ConnectionBadgecomponent in the header showing WS state + reconnect metadata. - Adds badge CSS (colors + pulse animation) and a Vitest regression test.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/styles.css | Adds connection badge styling, status colors, and pulse keyframes. |
| ui/modules/api.js | Adds reactive WS status signals and updates them on open/close/error/disconnect. |
| ui/app.js | Imports new WS signals and renders ConnectionBadge in the header. |
| tests/ui-connection-badge.test.mjs | Adds regression tests asserting badge wiring and CSS presence. |
| site/ui/styles.css | Mirrors the connection badge styling for the site UI. |
| site/ui/app.js | Mirrors header badge rendering/imports for the site UI. |
Comments suppressed due to low confidence (1)
ui/modules/api.js:393
disconnectWebSocket()is documented as canceling reconnects, but callingws.close()will still trigger theclosehandler which unconditionally setswsStatusto "reconnecting" and schedules a reconnect timer. Add a guard (e.g., amanualClose/shouldReconnectflag set bydisconnectWebSocket) so manual disconnects don’t flip the badge to reconnecting or restart the connection.
socket.addEventListener("close", () => {
wsConnected.value = false;
wsLatency.value = null;
wsStatus.value = "reconnecting";
ws = null;
stopPing();
wsReconnectCount.value += 1;
// Auto-reconnect with exponential backoff (max 15 s)
if (reconnectTimer) clearTimeout(reconnectTimer);
startCountdown(retryMs);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectWebSocket();
}, retryMs);
retryMs = Math.min(15000, retryMs * 2);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| wsStatus, | ||
| wsLastReconnectAt, |
There was a problem hiding this comment.
site/ui/modules/api.js currently does not export wsStatus or wsLastReconnectAt, so this import will throw at module-evaluation time and break the site build. Mirror the new signals + connectWebSocket/disconnectWebSocket status updates into site/ui/modules/api.js (or stop importing these names here if the site bundle shouldn’t have the badge).
| wsStatus, | |
| wsLastReconnectAt, |
| expect(apiSource).toContain("'connected'"); | ||
| expect(apiSource).toContain("'reconnecting'"); | ||
| expect(apiSource).toContain("'offline'"); |
There was a problem hiding this comment.
These assertions won’t match the implementation: the source sets statuses with double quotes (e.g. "connected"), but the test looks for single-quoted strings ('connected', etc.), so it will fail. Update the expectations to match the actual quoting style or use a more resilient regex/assertion.
| expect(apiSource).toContain("'connected'"); | |
| expect(apiSource).toContain("'reconnecting'"); | |
| expect(apiSource).toContain("'offline'"); | |
| expect(apiSource).toContain("\"connected\""); | |
| expect(apiSource).toContain("\"reconnecting\""); | |
| expect(apiSource).toContain("\"offline\""); |
| it("tracks explicit websocket badge signals and reconnect metadata", () => { | ||
| const apiSource = readFileSync(resolve(process.cwd(), "ui/modules/api.js"), "utf8"); | ||
|
|
||
| expect(apiSource).toContain("wsStatus = signal"); | ||
| expect(apiSource).toContain("wsLastReconnectAt = signal"); |
There was a problem hiding this comment.
This test only inspects ui/modules/api.js, but this PR also updates site/ui/app.js to use the new signals. Add coverage that site/ui/modules/api.js exports/updates the same badge signals so regressions like missing exports in the site bundle are caught.
| 50% { transform: scale(1.35); opacity: 0.55; } | ||
| 100% { transform: scale(1); opacity: 1; } | ||
| } | ||
|
|
There was a problem hiding this comment.
The new pulsing animation should respect prefers-reduced-motion. There’s already a reduced-motion section in ui/styles/layout.css disabling other connection pulses; add a similar rule for .connection-badge-pulse to avoid continuous animation for users who prefer reduced motion.
| @media (prefers-reduced-motion: reduce) { | |
| .connection-badge-pulse { | |
| animation: none; | |
| } | |
| } |
| 50% { transform: scale(1.35); opacity: 0.55; } | ||
| 100% { transform: scale(1); opacity: 1; } | ||
| } | ||
|
|
There was a problem hiding this comment.
The new pulsing animation should respect prefers-reduced-motion. There’s already a reduced-motion section in site/ui/styles/layout.css disabling other connection pulses; add a similar rule for .connection-badge-pulse to avoid continuous animation for users who prefer reduced motion.
| @media (prefers-reduced-motion: reduce) { | |
| .connection-badge-pulse { | |
| animation: none; | |
| } | |
| } |
| export const wsReconnectCount = signal(0); | ||
| /** Reactive signal: WebSocket badge status for portal header */ | ||
| export const wsStatus = signal("offline"); | ||
| /** Reactive signal: timestamp of the last successful reconnect */ |
There was a problem hiding this comment.
The comment says wsLastReconnectAt is the “timestamp of the last successful reconnect”, but it’s set on every open (including the initial connection). Either adjust the wording (e.g. “last successful (re)connect”) or only set this timestamp when a reconnect actually occurred.
| /** Reactive signal: timestamp of the last successful reconnect */ | |
| /** Reactive signal: timestamp of the last successful (re)connect */ |
…-connection-badge-in-p
…-connection-badge-in-p
|
Bosun PR classification: Bosun-created.
|
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Co-authored-by: bosun-ve[bot] <262908237+bosun-ve[bot]@users.noreply.github.com>
Task-ID: b3364ece-da27-43ce-b950-6e15d057a33d\n\nAutomated PR for task b3364ece-da27-43ce-b950-6e15d057a33d\n\n---\n\nBosun-Origin: created