From 467b7f0f96b89252db4db141b91f76aeb4899579 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Sun, 5 Apr 2026 16:45:27 +0000 Subject: [PATCH] refresh process update --- static/js/crypto-belt.js | 10 +-- static/js/filesystem-map.js | 7 +- static/js/main.js | 44 ++++++++++-- static/js/network-stack.js | 7 +- static/js/safe-utils.js | 134 ++++++++++++++++++++++++++++++++++++ static/js/security-belt.js | 7 +- static/js/ui-chrome.js | 60 +++++++++++++++- 7 files changed, 249 insertions(+), 20 deletions(-) diff --git a/static/js/crypto-belt.js b/static/js/crypto-belt.js index 807dea1..7c70f34 100644 --- a/static/js/crypto-belt.js +++ b/static/js/crypto-belt.js @@ -1942,11 +1942,11 @@ class CryptoSubsystemVisualization { } fetchTelemetry() { - return fetch('/api/crypto-realtime', { cache: 'no-store' }) - .then((res) => { - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); - }) + return window.fetchJson('/api/crypto-realtime', { cache: 'no-store' }, { + timeoutMs: 6000, + suppressToast: true, + context: 'crypto-realtime' + }) .then((data) => { if (!data || data.error) throw new Error(data?.error || 'No crypto telemetry'); const normalized = this.normalizeTelemetry(data); diff --git a/static/js/filesystem-map.js b/static/js/filesystem-map.js index 8dc09a5..aad4fbb 100644 --- a/static/js/filesystem-map.js +++ b/static/js/filesystem-map.js @@ -193,8 +193,11 @@ class FilesystemMapVisualization { } fetchTelemetry() { - return fetch('/api/filesystem-blocks') - .then((res) => res.json()) + return window.fetchJson('/api/filesystem-blocks', { cache: 'no-store' }, { + timeoutMs: 6000, + suppressToast: true, + context: 'filesystem-blocks' + }) .then((data) => { if (!data || data.error) { throw new Error(data?.error || 'No filesystem data'); diff --git a/static/js/main.js b/static/js/main.js index 2c63d7c..6dce744 100755 --- a/static/js/main.js +++ b/static/js/main.js @@ -421,8 +421,17 @@ function drawRing1(centerX, centerY) { // Update Ring-1 with execution context data function updateRing1(centerX, centerY, baseRadius) { // Use relative path like other API calls - fetch('/api/execution-context') - .then(res => res.json()) + window.fetchJson('/api/execution-context', { cache: 'no-store' }, { + timeoutMs: 4500, + suppressToast: true, + context: 'execution-context' + }) + .then(data => { + if (!data || data.error) { + throw new Error(data?.error || 'No execution context'); + } + return data; + }) .then(data => { // Debug logging debugLog('🔄 Ring-1 Update:', { @@ -587,7 +596,7 @@ function updateRing1(centerX, centerY, baseRadius) { renderIrqStackPanel(data); }) .catch(error => { - console.error('Error fetching execution context:', error); + debugLog('Error fetching execution context:', error && error.message ? error.message : error); }); } @@ -756,13 +765,24 @@ function drawPanels(width, height) { // Load processes and kernel subsystems function loadProcessKernelMap(centerX, centerY) { - fetch('/api/process-kernel-map') - .then(res => res.json()) + window.fetchJson('/api/process-kernel-map', { cache: 'no-store' }, { + timeoutMs: 6500, + retries: 1, + context: 'process-kernel-map', + toastMessage: 'Process graph is temporarily unavailable' + }) + .then(data => { + if (!data || data.error) { + throw new Error(data?.error || 'No process map data'); + } + return data; + }) .then(data => { drawProcessKernelMap(data, centerX, centerY); }) .catch(error => { console.error('Error fetching process-kernel-map:', error); + drawProcessKernelMap({}, centerX, centerY); }); } @@ -864,8 +884,18 @@ function drawProcessKernelMap(data, centerX, centerY) { // Draw additional process lines (without circles and names) function drawProcessKernelMap2(centerX, centerY) { // Fetch all Linux processes with detailed information - fetch('/api/processes-detailed') - .then(res => res.json()) + window.fetchJson('/api/processes-detailed', { cache: 'no-store' }, { + timeoutMs: 6500, + retries: 1, + context: 'processes-detailed', + toastMessage: 'Process details are temporarily unavailable' + }) + .then(data => { + if (!data || data.error) { + throw new Error(data?.error || 'No detailed processes data'); + } + return data; + }) .then(data => { const processes = data.processes || []; const numProcesses = processes.length; diff --git a/static/js/network-stack.js b/static/js/network-stack.js index 9cc3548..67d48ed 100644 --- a/static/js/network-stack.js +++ b/static/js/network-stack.js @@ -545,8 +545,11 @@ class NetworkStackVisualization { } fetchTelemetry() { - return fetch('/api/network-stack-realtime') - .then(res => res.json()) + return window.fetchJson('/api/network-stack-realtime', { cache: 'no-store' }, { + timeoutMs: 6000, + suppressToast: true, + context: 'network-stack-realtime' + }) .then(data => { if (!data || data.error) { throw new Error(data?.error || 'No telemetry data'); diff --git a/static/js/safe-utils.js b/static/js/safe-utils.js index 39008b3..d3e3a75 100644 --- a/static/js/safe-utils.js +++ b/static/js/safe-utils.js @@ -50,6 +50,140 @@ node.innerHTML = window.sanitizeHtml(html); }; + const UX_TOAST_CONTAINER_ID = 'ux-toast-container'; + const UX_DEFAULT_TIMEOUT_MS = 8000; + const uxToastCooldown = new Map(); + + function ensureToastContainer() { + let container = document.getElementById(UX_TOAST_CONTAINER_ID); + if (container) return container; + container = document.createElement('div'); + container.id = UX_TOAST_CONTAINER_ID; + container.style.cssText = [ + 'position: fixed', + 'top: 14px', + 'right: 14px', + 'display: flex', + 'flex-direction: column', + 'gap: 8px', + 'z-index: 12000', + 'pointer-events: none' + ].join(';'); + document.body.appendChild(container); + return container; + } + + window.showToast = function showToast(message, type = 'info', opts = {}) { + const text = String(message || '').trim(); + if (!text) return; + const dedupeKey = String(opts.dedupeKey || `${type}:${text}`); + const now = Date.now(); + const cooldownMs = Number(opts.cooldownMs || 3000); + const until = uxToastCooldown.get(dedupeKey) || 0; + if (until > now) return; + uxToastCooldown.set(dedupeKey, now + cooldownMs); + + const container = ensureToastContainer(); + const toast = document.createElement('div'); + const isError = type === 'error'; + const isWarn = type === 'warn' || type === 'warning'; + const bg = isError + ? 'rgba(47, 17, 17, 0.96)' + : (isWarn ? 'rgba(44, 32, 12, 0.96)' : 'rgba(12, 18, 28, 0.95)'); + const border = isError + ? 'rgba(235, 126, 126, 0.7)' + : (isWarn ? 'rgba(244, 201, 119, 0.7)' : 'rgba(145, 180, 220, 0.45)'); + const color = isError + ? '#ffd9d9' + : (isWarn ? '#ffebc2' : '#d7e7fb'); + + toast.style.cssText = [ + 'max-width: 360px', + 'padding: 9px 11px', + 'border-radius: 6px', + `background: ${bg}`, + `border: 1px solid ${border}`, + `color: ${color}`, + 'font-family: "Share Tech Mono", monospace', + 'font-size: 11px', + 'line-height: 1.4', + 'pointer-events: auto', + 'opacity: 0', + 'transform: translateY(-4px)', + 'transition: opacity 140ms ease, transform 140ms ease' + ].join(';'); + toast.textContent = text; + container.appendChild(toast); + requestAnimationFrame(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateY(0)'; + }); + + const ttl = Math.max(1200, Number(opts.ttlMs || 3200)); + window.setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateY(-4px)'; + window.setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 160); + }, ttl); + }; + + window.fetchJson = async function fetchJson(url, fetchOptions = {}, requestOptions = {}) { + const timeoutMs = Math.max(500, Number(requestOptions.timeoutMs || UX_DEFAULT_TIMEOUT_MS)); + const retries = Math.max(0, Number(requestOptions.retries || 0)); + const retryDelayMs = Math.max(0, Number(requestOptions.retryDelayMs || 220)); + const suppressToast = Boolean(requestOptions.suppressToast); + const context = String(requestOptions.context || 'request'); + const toastMessage = String(requestOptions.toastMessage || 'Backend request failed'); + + let attempt = 0; + let lastError = null; + while (attempt <= retries) { + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); + try { + const options = { + ...fetchOptions, + signal: controller.signal + }; + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + lastError = error; + const isAbort = error && error.name === 'AbortError'; + const description = isAbort ? 'timeout' : (error && error.message ? error.message : 'unknown error'); + if (window.frontendLogger && typeof window.frontendLogger.warn === 'function') { + window.frontendLogger.warn('frontend request failed', { + context, + url: String(url || ''), + attempt, + retries, + timeoutMs, + error: description + }); + } + if (attempt < retries) { + await new Promise((resolve) => window.setTimeout(resolve, retryDelayMs)); + } else if (!suppressToast && typeof window.showToast === 'function') { + window.showToast(`${toastMessage} (${description})`, 'warn', { + dedupeKey: `req:${context}:${String(url || '')}` + }); + } + } finally { + window.clearTimeout(timeoutId); + } + attempt += 1; + } + throw lastError || new Error('Request failed'); + }; + // Sanitize all d3 html() calls automatically. if (window.d3 && window.d3.selection && window.d3.selection.prototype && !window.__d3HtmlSanitizedPatch) { window.__d3HtmlSanitizedPatch = true; diff --git a/static/js/security-belt.js b/static/js/security-belt.js index 6046e53..6e42215 100644 --- a/static/js/security-belt.js +++ b/static/js/security-belt.js @@ -229,8 +229,11 @@ class SecuritySubsystemVisualization { } fetchTelemetry() { - return fetch('/api/security-realtime') - .then((res) => res.json()) + return window.fetchJson('/api/security-realtime', { cache: 'no-store' }, { + timeoutMs: 6000, + suppressToast: true, + context: 'security-realtime' + }) .then((data) => { if (!data || data.error) { throw new Error(data?.error || 'No security data'); diff --git a/static/js/ui-chrome.js b/static/js/ui-chrome.js index 39f0132..bb03127 100644 --- a/static/js/ui-chrome.js +++ b/static/js/ui-chrome.js @@ -1,6 +1,48 @@ // UI chrome module extracted from main.js (function initUiChrome(){ const svg = d3.select("svg"); +let backendStatusNode = null; + +function ensureBackendStatusNode() { + if (backendStatusNode && backendStatusNode.parentNode) return backendStatusNode; + const node = document.createElement('div'); + node.id = 'backend-status-chip'; + node.style.cssText = [ + 'position: fixed', + 'left: 14px', + 'bottom: 14px', + 'padding: 5px 8px', + 'border-radius: 5px', + 'background: rgba(12, 18, 28, 0.88)', + 'border: 1px solid rgba(145, 180, 220, 0.4)', + 'color: #cfe2fa', + 'font-family: "Share Tech Mono", monospace', + 'font-size: 10px', + 'letter-spacing: 0.2px', + 'pointer-events: none', + 'z-index: 1100', + 'opacity: 0.85' + ].join(';'); + node.textContent = 'backend: connecting...'; + document.body.appendChild(node); + backendStatusNode = node; + return node; +} + +function setBackendStatus(online, details = '') { + const node = ensureBackendStatusNode(); + if (online) { + node.textContent = 'backend: online'; + node.style.background = 'rgba(8, 24, 18, 0.88)'; + node.style.borderColor = 'rgba(106, 210, 160, 0.58)'; + node.style.color = '#bbf1d9'; + } else { + node.textContent = details ? `backend: degraded (${details})` : 'backend: degraded'; + node.style.background = 'rgba(34, 20, 10, 0.9)'; + node.style.borderColor = 'rgba(244, 201, 119, 0.62)'; + node.style.color = '#ffe7ba'; + } +} // Update panel with real data from API function updatePanelData() { @@ -14,9 +56,14 @@ function updatePanelData() { return; } - fetch('/api/kernel-data') - .then(res => res.json()) + return window.fetchJson('/api/kernel-data', { cache: 'no-store' }, { + timeoutMs: 6000, + retries: 1, + context: 'kernel-summary', + toastMessage: 'Kernel summary is temporarily unavailable' + }) .then(data => { + setBackendStatus(true); // Update processes count const processesText = d3.select('#panel-value-2'); if (!processesText.empty() && data.processes) { @@ -36,6 +83,15 @@ function updatePanelData() { } }) .catch(error => { + setBackendStatus(false, error && error.message ? error.message : 'request failed'); + const processesText = d3.select('#panel-value-2'); + if (!processesText.empty()) { + processesText.text('N/A'); + } + const memoryText = d3.select('#panel-value-3'); + if (!memoryText.empty()) { + memoryText.text('N/A'); + } console.error('Error updating panel data:', error); }); }