From fcb5efd753a76f6666cd087b3952436e31892113 Mon Sep 17 00:00:00 2001 From: Andy Oknen Date: Thu, 31 Jul 2025 04:51:55 +0000 Subject: [PATCH] Add timeout handling and loading state management in API calls --- src/webui/static/api.js | 16 +++++++++++++++- src/webui/static/app.js | 29 ++++++++++++++++++++++++++++- src/webui/static/index.html | 5 +++++ src/webui/static/style.css | 28 ++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/webui/static/api.js b/src/webui/static/api.js index 2c0e10e2..2b012b1e 100644 --- a/src/webui/static/api.js +++ b/src/webui/static/api.js @@ -15,12 +15,18 @@ class YggdrasilAPI { */ async callAdmin(command, args = {}) { const url = command ? `${this.baseURL}/${command}` : this.baseURL; + + // Create AbortController for timeout functionality + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + const options = { method: Object.keys(args).length > 0 ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json', }, - credentials: 'same-origin' // Include session cookies + credentials: 'same-origin', // Include session cookies + signal: controller.signal }; if (Object.keys(args).length > 0) { @@ -29,6 +35,7 @@ class YggdrasilAPI { try { const response = await fetch(url, options); + clearTimeout(timeoutId); // Clear timeout on successful response if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); @@ -42,6 +49,13 @@ class YggdrasilAPI { return data.response || data.commands; } catch (error) { + clearTimeout(timeoutId); // Clear timeout on error + + if (error.name === 'AbortError') { + console.error(`API call timeout for ${command}:`, error); + throw new Error('Request timeout - service may be unavailable'); + } + console.error(`API call failed for ${command}:`, error); throw error; } diff --git a/src/webui/static/app.js b/src/webui/static/app.js index 7e44f93a..480226d5 100644 --- a/src/webui/static/app.js +++ b/src/webui/static/app.js @@ -7,12 +7,22 @@ window.nodeInfo = null; window.peersData = null; let isLoading = false; +let isLoadingNodeInfo = false; +let isLoadingPeers = false; + + /** * Load and display node information */ async function loadNodeInfo() { + if (isLoadingNodeInfo) { + console.log('Node info request already in progress, skipping...'); + return window.nodeInfo; + } + try { + isLoadingNodeInfo = true; const info = await window.yggAPI.getSelf(); window.nodeInfo = info; updateNodeInfoDisplay(info); @@ -21,6 +31,8 @@ async function loadNodeInfo() { console.error('Failed to load node info:', error); showError('Failed to load node information: ' + error.message); throw error; + } finally { + isLoadingNodeInfo = false; } } @@ -28,7 +40,13 @@ async function loadNodeInfo() { * Load and display peers information */ async function loadPeers() { + if (isLoadingPeers) { + console.log('Peers request already in progress, skipping...'); + return window.peersData; + } + try { + isLoadingPeers = true; const data = await window.yggAPI.getPeers(); window.peersData = data; updatePeersDisplay(data); @@ -37,6 +55,8 @@ async function loadPeers() { console.error('Failed to load peers:', error); showError('Failed to load peers information: ' + error.message); throw error; + } finally { + isLoadingPeers = false; } } @@ -100,6 +120,8 @@ function updatePeersDisplay(data) { window.updateNodeInfoDisplay = updateNodeInfoDisplay; window.updatePeersDisplay = updatePeersDisplay; + + // Expose copy functions to window for access from HTML onclick handlers window.copyNodeKey = copyNodeKey; window.copyNodeAddress = copyNodeAddress; @@ -359,18 +381,23 @@ function copyPeerKey(key) { } } + + /** * Auto-refresh data */ function startAutoRefresh() { // Refresh every 30 seconds setInterval(async () => { - if (!isLoading) { + // Only proceed if individual requests are not already in progress + if (!isLoadingNodeInfo && !isLoadingPeers) { try { await Promise.all([loadNodeInfo(), loadPeers()]); } catch (error) { console.error('Auto-refresh failed:', error); } + } else { + console.log('Skipping auto-refresh - requests already in progress'); } }, 30000); } diff --git a/src/webui/static/index.html b/src/webui/static/index.html index f25cae1a..b2fb9880 100644 --- a/src/webui/static/index.html +++ b/src/webui/static/index.html @@ -14,6 +14,11 @@ + +
+
+
+
diff --git a/src/webui/static/style.css b/src/webui/static/style.css index 11597f87..aa708f53 100644 --- a/src/webui/static/style.css +++ b/src/webui/static/style.css @@ -114,6 +114,16 @@ --shadow-heavy: rgba(0, 0, 0, 0.5); } +/* Dark theme progress bar adjustments */ +[data-theme="dark"] .progress-timer-container { + background: rgba(0, 0, 0, 0.4); +} + +[data-theme="dark"] .progress-timer-bar { + background: linear-gradient(90deg, #3498db, #27ae60); + box-shadow: 0 0 10px rgba(52, 152, 219, 0.3); +} + * { margin: 0; padding: 0; @@ -128,6 +138,24 @@ body { overflow: hidden; /* Prevent body scroll */ } +/* Decorative progress bar */ +.progress-timer-container { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 4px; + background: rgba(255, 255, 255, 0.2); + z-index: 1000; +} + +.progress-timer-bar { + height: 100%; + background: linear-gradient(90deg, #3498db, #2ecc71); + width: 100%; + box-shadow: 0 0 10px rgba(52, 152, 219, 0.5); +} + .container { display: grid; grid-template-columns: 250px 1fr;