Connection
-Please sign in to start the WebSocket session.
-API Contract
All requests follow the structure below. Use app.sendAction(action, payload) from the browser console after authentication.
diff --git a/dashboard/app.js b/dashboard/app.js index 7f1b5d9..b11221c 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -10,6 +10,7 @@ class DashboardApp { statusDot: document.getElementById('statusDot'), connectionStatus: document.getElementById('connectionStatus'), sessionSummary: document.getElementById('sessionSummary'), + logoutButton: document.getElementById('logoutButton'), requestTemplate: document.getElementById('requestTemplate'), responseTemplate: document.getElementById('responseTemplate'), incomingMessage: document.getElementById('incomingMessage') @@ -22,6 +23,8 @@ class DashboardApp { this.username = null; this.pendingRequests = new Map(); this.reconnectTimer = null; + this.tokenRefreshTimer = null; + this.refreshInFlight = false; this.renderStaticTemplates(); this.attachEventListeners(); @@ -35,6 +38,12 @@ class DashboardApp { this.submitLogin(); }); } + + if (this.elements.logoutButton) { + this.elements.logoutButton.addEventListener('click', () => { + this.logout(); + }); + } } renderStaticTemplates() { @@ -113,6 +122,7 @@ class DashboardApp { this.token = parsed.token; this.tokenExpiry = expiresAt; this.username = parsed.username || null; + this.scheduleTokenRefresh(); this.updateSessionSummary(); this.hideLoginOverlay(); this.connectWebSocket(); @@ -216,6 +226,8 @@ class DashboardApp { } catch (error) { console.warn('Failed to persist session', error); } + + this.scheduleTokenRefresh(); } clearSession() { @@ -225,12 +237,17 @@ class DashboardApp { this.pendingRequests.clear(); clearTimeout(this.reconnectTimer); this.reconnectTimer = null; + clearTimeout(this.tokenRefreshTimer); + this.tokenRefreshTimer = null; + this.refreshInFlight = false; try { window.localStorage.removeItem(this.sessionKey); } catch (error) { console.warn('Failed to clear session', error); } + + this.updateSessionSummary(); } connectWebSocket(resetPending = false) { @@ -412,12 +429,14 @@ class DashboardApp { if (!this.token) { this.elements.sessionSummary.textContent = 'Please sign in to start the WebSocket session.'; + this.toggleLogoutButton(false); return; } const expires = this.tokenExpiry ? this.tokenExpiry.toISOString() : 'unknown'; const username = this.username ? this.username : 'user'; this.elements.sessionSummary.textContent = `Signed in as ${username}. Token valid until ${expires}.`; + this.toggleLogoutButton(true); } showLoginOverlay(message) { @@ -474,6 +493,78 @@ class DashboardApp { return; body.classList.toggle('needs-auth', requireAuth); } + + toggleLogoutButton(visible) { + if (!this.elements.logoutButton) + return; + this.elements.logoutButton.classList.toggle('hidden', !visible); + } + + logout() { + this.clearSession(); + if (this.socket && this.socket.readyState === WebSocket.OPEN) + this.socket.close(); + this.updateConnectionStatus('Logged out', 'connecting'); + this.updateSessionSummary(); + this.showLoginOverlay('You have been logged out.'); + } + + scheduleTokenRefresh() { + clearTimeout(this.tokenRefreshTimer); + this.tokenRefreshTimer = null; + + if (!this.token || !this.tokenExpiry) + return; + + const now = Date.now(); + const expiryTime = this.tokenExpiry.getTime(); + const leadTimeMs = 60 * 1000; // refresh one minute before expiry + const delay = Math.max(expiryTime - leadTimeMs - now, 5 * 1000); + + this.tokenRefreshTimer = setTimeout(() => { + this.refreshToken(); + }, delay); + } + + async refreshToken() { + if (!this.token || this.refreshInFlight) + return; + + this.refreshInFlight = true; + + try { + const response = await fetch('/evdash/api/refresh', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: this.token }) + }); + + const data = await response.json(); + if (!response.ok || !data.success) + throw new Error(data && data.error ? data.error : 'refreshFailed'); + + if (!data.token || !data.expiresAt) + throw new Error('Invalid response from server.'); + + this.persistSession({ + token: data.token, + expiresAt: data.expiresAt, + username: this.username + }); + this.updateSessionSummary(); + } catch (error) { + console.warn('Token refresh failed', error); + this.clearSession(); + this.updateConnectionStatus('Authentication required', 'error'); + if (this.socket && this.socket.readyState === WebSocket.OPEN) + this.socket.close(); + this.showLoginOverlay('Session expired. Please sign in again.'); + } finally { + this.refreshInFlight = false; + } + } } window.app = new DashboardApp(); diff --git a/dashboard/index.html b/dashboard/index.html index 2a64a08..7943cc4 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -33,18 +33,33 @@ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: #ffffff; padding: 2.5rem 1.5rem 2rem; - text-align: center; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } - header h1 { - margin: 0 0 0.5rem; + .header-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + } + + .brand h1 { + margin: 0 0 0.4rem; font-weight: 600; letter-spacing: 0.02em; } - header p { + .brand p { margin: 0; + opacity: 0.9; + } + + .session-panel { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem 1.5rem; } main { @@ -72,6 +87,7 @@ align-items: center; gap: 0.75rem; font-weight: 600; + margin: 0; } .status-dot { @@ -247,22 +263,49 @@ .hidden { display: none !important; } + + .session-details { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.95rem; + } + + button.ghost { + border: 1px solid rgba(255, 255, 255, 0.55); + color: #ffffff; + background: transparent; + padding: 0.35rem 0.85rem; + border-radius: 999px; + font-size: 0.9rem; + } + + button.ghost:hover { + border-color: #ffffff; + background: rgba(255, 255, 255, 0.12); + }
- - Awaiting login… -
+Please sign in to start the WebSocket session.
-All requests follow the structure below. Use app.sendAction(action, payload) from the browser console after authentication.