-
Notifications
You must be signed in to change notification settings - Fork 23
feat(portal): live WebSocket connection badge in portal header #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1e724c
9a05243
a76604a
3264aae
24870c7
199411d
83d9018
2f38c17
1a53f3f
8099d21
7825125
fc75549
24f572f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,62 @@ | ||||||||||||||||
| :root { | ||||||||||||||||
| --ws-badge-connected: #22c55e; | ||||||||||||||||
| --ws-badge-reconnecting: #f59e0b; | ||||||||||||||||
| --ws-badge-offline: #ef4444; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge { | ||||||||||||||||
| display: inline-flex; | ||||||||||||||||
| align-items: center; | ||||||||||||||||
| gap: 8px; | ||||||||||||||||
| border: 1px solid var(--border, rgba(148, 163, 184, 0.24)); | ||||||||||||||||
| border-radius: 999px; | ||||||||||||||||
| padding: 4px 10px; | ||||||||||||||||
| font-size: 12px; | ||||||||||||||||
| line-height: 1; | ||||||||||||||||
| color: var(--text-secondary, #cbd5e1); | ||||||||||||||||
| background: var(--bg-elevated, rgba(15, 23, 42, 0.24)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot { | ||||||||||||||||
| width: 8px; | ||||||||||||||||
| height: 8px; | ||||||||||||||||
| border-radius: 50%; | ||||||||||||||||
| flex: 0 0 8px; | ||||||||||||||||
| background: var(--ws-badge-offline); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.connected { | ||||||||||||||||
| background: var(--ws-badge-connected); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.reconnecting { | ||||||||||||||||
| background: var(--ws-badge-reconnecting); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.offline { | ||||||||||||||||
| background: var(--ws-badge-offline); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-label { | ||||||||||||||||
| white-space: nowrap; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-pulse { | ||||||||||||||||
| animation: connection-badge-pulse 1s ease-in-out infinite; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @keyframes connection-badge-pulse { | ||||||||||||||||
| 0% { transform: scale(1); opacity: 1; } | ||||||||||||||||
| 50% { transform: scale(1.35); opacity: 0.55; } | ||||||||||||||||
| 100% { transform: scale(1); opacity: 1; } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| @media (prefers-reduced-motion: reduce) { | |
| .connection-badge-pulse { | |
| animation: none; | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
|
|
||
| describe("portal websocket connection badge", () => { | ||
| 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"); | ||
|
Comment on lines
+6
to
+10
|
||
| expect(apiSource).toContain('"connected"'); | ||
| expect(apiSource).toContain('"reconnecting"'); | ||
| expect(apiSource).toContain('"offline"'); | ||
| expect(apiSource).toContain('wsLastReconnectAt.value = Date.now()'); | ||
| }); | ||
|
|
||
| it("site/ui/modules/api.js exports wsStatus and wsLastReconnectAt badge signals", () => { | ||
| const siteApiSource = readFileSync(resolve(process.cwd(), "site/ui/modules/api.js"), "utf8"); | ||
|
|
||
| expect(siteApiSource).toContain("export const wsStatus"); | ||
| expect(siteApiSource).toContain("export const wsLastReconnectAt"); | ||
| expect(siteApiSource).toContain('"connected"'); | ||
| expect(siteApiSource).toContain('"reconnecting"'); | ||
| expect(siteApiSource).toContain('"offline"'); | ||
| expect(siteApiSource).toContain('wsLastReconnectAt.value = Date.now()'); | ||
| }); | ||
|
|
||
| it("renders a connection badge in the portal header before settings", () => { | ||
| const appSource = readFileSync(resolve(process.cwd(), "ui/app.js"), "utf8"); | ||
|
|
||
| expect(appSource).toContain("function ConnectionBadge()"); | ||
| expect(appSource).toContain("connection-badge"); | ||
| expect(appSource).toContain("<${ConnectionBadge} />"); | ||
| expect(appSource).toContain("last reconnect"); | ||
| expect(appSource).toContain("Reconnecting..."); | ||
| }); | ||
|
|
||
| it("defines themed badge color css variables and pulse animation", () => { | ||
| const stylesSource = readFileSync(resolve(process.cwd(), "ui/styles.css"), "utf8"); | ||
|
|
||
| expect(stylesSource).toContain("--ws-badge-connected"); | ||
| expect(stylesSource).toContain("--ws-badge-reconnecting"); | ||
| expect(stylesSource).toContain("--ws-badge-offline"); | ||
| expect(stylesSource).toContain("connection-badge-dot"); | ||
| expect(stylesSource).toContain("connection-badge-pulse"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,62 @@ | ||||||||||||||||
| :root { | ||||||||||||||||
| --ws-badge-connected: #22c55e; | ||||||||||||||||
| --ws-badge-reconnecting: #f59e0b; | ||||||||||||||||
| --ws-badge-offline: #ef4444; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge { | ||||||||||||||||
| display: inline-flex; | ||||||||||||||||
| align-items: center; | ||||||||||||||||
| gap: 8px; | ||||||||||||||||
| border: 1px solid var(--border, rgba(148, 163, 184, 0.24)); | ||||||||||||||||
| border-radius: 999px; | ||||||||||||||||
| padding: 4px 10px; | ||||||||||||||||
| font-size: 12px; | ||||||||||||||||
| line-height: 1; | ||||||||||||||||
| color: var(--text-secondary, #cbd5e1); | ||||||||||||||||
| background: var(--bg-elevated, rgba(15, 23, 42, 0.24)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot { | ||||||||||||||||
| width: 8px; | ||||||||||||||||
| height: 8px; | ||||||||||||||||
| border-radius: 50%; | ||||||||||||||||
| flex: 0 0 8px; | ||||||||||||||||
| background: var(--ws-badge-offline); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.connected { | ||||||||||||||||
| background: var(--ws-badge-connected); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.reconnecting { | ||||||||||||||||
| background: var(--ws-badge-reconnecting); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-dot.offline { | ||||||||||||||||
| background: var(--ws-badge-offline); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-label { | ||||||||||||||||
| white-space: nowrap; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| .connection-badge-pulse { | ||||||||||||||||
| animation: connection-badge-pulse 1s ease-in-out infinite; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @keyframes connection-badge-pulse { | ||||||||||||||||
| 0% { transform: scale(1); opacity: 1; } | ||||||||||||||||
| 50% { transform: scale(1.35); opacity: 0.55; } | ||||||||||||||||
| 100% { transform: scale(1); opacity: 1; } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| @media (prefers-reduced-motion: reduce) { | |
| .connection-badge-pulse { | |
| animation: none; | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
site/ui/modules/api.jscurrently does not exportwsStatusorwsLastReconnectAt, so this import will throw at module-evaluation time and break the site build. Mirror the new signals + connectWebSocket/disconnectWebSocket status updates intosite/ui/modules/api.js(or stop importing these names here if the site bundle shouldn’t have the badge).