Skip to content

Commit 82838b5

Browse files
authored
Merge pull request #10239 from gadfort/web-disconnect
web: add notification when socket is gone
2 parents 2825e57 + e33cfbb commit 82838b5

6 files changed

Lines changed: 469 additions & 10 deletions

File tree

src/web/src/main.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,38 @@ import './theme.js';
2121
// ─── Status Indicator ───────────────────────────────────────────────────────
2222

2323
const statusDiv = document.getElementById('websocket-status');
24+
let disconnectTimeout = null;
25+
const DISCONNECT_DELAY_MS = 2000; // Show banner after 2 seconds of disconnection
2426

2527
function updateStatus() {
26-
const n = app.websocketManager ? app.websocketManager.pending.size : 0;
27-
if (n === 0) {
28-
statusDiv.textContent = '';
29-
statusDiv.style.display = 'none';
28+
const isConnected = app.websocketManager && app.websocketManager.isConnected;
29+
const pendingCount = app.websocketManager ? app.websocketManager.pending.size : 0;
30+
31+
if (!isConnected) {
32+
// Only show banner after a delay to avoid flashing on page load
33+
if (!disconnectTimeout) {
34+
disconnectTimeout = setTimeout(() => {
35+
if (!app.websocketManager?.isConnected) {
36+
statusDiv.innerHTML = '<div class="disconnected-banner">⚠ OpenROAD disconnected</div>';
37+
statusDiv.style.display = 'block';
38+
}
39+
}, DISCONNECT_DELAY_MS);
40+
}
3041
} else {
31-
statusDiv.textContent = `pending: ${n}`;
32-
statusDiv.style.display = '';
33-
statusDiv.style.color = n > 20 ? 'var(--error)' : 'var(--fg-bright)';
42+
// Connected - clear timeout and show pending indicator if needed
43+
if (disconnectTimeout) {
44+
clearTimeout(disconnectTimeout);
45+
disconnectTimeout = null;
46+
}
47+
48+
if (pendingCount === 0) {
49+
statusDiv.style.display = 'none';
50+
} else {
51+
statusDiv.innerHTML = `<div class="pending-indicator">pending: ${pendingCount}</div>`;
52+
statusDiv.style.display = 'block';
53+
const color = pendingCount > 20 ? 'var(--error)' : 'var(--fg-bright)';
54+
statusDiv.querySelector('.pending-indicator').style.color = color;
55+
}
3456
}
3557
}
3658

@@ -558,6 +580,9 @@ if (staticCache) {
558580
app.websocketManager = new WebSocketManager(websocketUrl, updateStatus);
559581
}
560582

583+
// Check initial connection status
584+
updateStatus();
585+
561586
// Restore saved layout or use default
562587
const savedLayout = localStorage.getItem('gl-layout');
563588
const savedVersion = parseInt(localStorage.getItem('gl-layout-version'), 10);

src/web/src/style.css

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -425,17 +425,61 @@ html, body {
425425

426426
/* Status overlay */
427427
#websocket-status {
428+
position: fixed;
429+
pointer-events: none;
430+
z-index: 10005;
431+
display: none;
432+
}
433+
434+
#websocket-status .disconnected-banner {
435+
position: fixed;
436+
top: 40px;
437+
left: 50%;
438+
transform: translateX(-50%);
439+
background: linear-gradient(135deg, var(--error) 0%, #cc3333 100%);
440+
color: white;
441+
padding: 16px 24px;
442+
border-radius: 8px;
443+
font-size: 16px;
444+
font-weight: bold;
445+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
446+
text-align: center;
447+
animation: slideDown 0.3s ease-out, pulse 2s ease-in-out infinite 0.5s;
448+
letter-spacing: 0.5px;
449+
}
450+
451+
#websocket-status .pending-indicator {
428452
position: fixed;
429453
top: 8px;
430454
right: 8px;
431-
z-index: 10000;
432455
background: var(--bg-status);
433456
color: var(--fg-secondary);
434457
padding: 4px 10px;
435458
font: 12px monospace;
436459
border-radius: 4px;
437-
pointer-events: none;
438-
display: none;
460+
display: inline-block;
461+
}
462+
463+
@keyframes slideDown {
464+
from {
465+
opacity: 0;
466+
transform: translateX(-50%) translateY(-20px);
467+
}
468+
to {
469+
opacity: 1;
470+
transform: translateX(-50%) translateY(0);
471+
}
472+
}
473+
474+
@keyframes pulse {
475+
0%, 100% {
476+
opacity: 1;
477+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
478+
}
479+
50% {
480+
opacity: 0.8;
481+
box-shadow: 0 12px 32px rgba(255, 100, 100, 0.5);
482+
}
439483
}
440484

441485
/* Coordinate readout (bottom-left of layout viewer) */

src/web/src/websocket-manager.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ export class WebSocketManager {
1717
this.onStatusChange = onStatusChange || (() => {});
1818
this.onPush = null; // callback for server-push notifications
1919
this._cache = null;
20+
this._isConnected = false;
2021
this.connect();
2122
}
2223

24+
get isConnected() {
25+
return this._isConnected || (this.socket && this.socket.readyState === WebSocket.OPEN);
26+
}
27+
2328
// Create a cache-backed instance (no WebSocket connection).
2429
static fromCache(cache, onStatusChange) {
2530
const mgr = Object.create(WebSocketManager.prototype);
@@ -31,6 +36,7 @@ export class WebSocketManager {
3136
mgr.onStatusChange = onStatusChange || (() => {});
3237
mgr.onPush = null;
3338
mgr._cache = cache;
39+
mgr._isConnected = true; // cache is always "connected"
3440
mgr.readyPromise = Promise.resolve();
3541
mgr.readyResolve = null;
3642
return mgr;
@@ -50,8 +56,10 @@ export class WebSocketManager {
5056

5157
this.socket.onopen = () => {
5258
console.log('WebSocket connected');
59+
this._isConnected = true;
5360
this.reconnectDelay = 1000;
5461
this.readyResolve();
62+
this.onStatusChange();
5563
};
5664

5765
this.socket.onmessage = (event) => {
@@ -60,6 +68,8 @@ export class WebSocketManager {
6068

6169
this.socket.onclose = () => {
6270
console.log('WebSocket closed, reconnecting...');
71+
this._isConnected = false;
72+
this.onStatusChange();
6373
for (const [id, handler] of this.pending) {
6474
handler.reject(new Error('WebSocket closed'));
6575
}

src/web/test/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ js_test(
127127
no_copy_to_bin = JS_FILES,
128128
)
129129

130+
js_test(
131+
name = "status_indicator_test",
132+
data = DOM_TEST_DATA,
133+
entry_point = "js/test-status-indicator.js",
134+
no_copy_to_bin = JS_FILES,
135+
)
136+
130137
cc_test(
131138
name = "clock_tree_report_test",
132139
srcs = ["cpp/TestClockTreeReport.cpp"],

0 commit comments

Comments
 (0)