Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions static/js/crypto-belt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions static/js/filesystem-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
44 changes: 37 additions & 7 deletions static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:', {
Expand Down Expand Up @@ -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);
});
}

Expand Down Expand Up @@ -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);
});
}

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions static/js/network-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
134 changes: 134 additions & 0 deletions static/js/safe-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions static/js/security-belt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
60 changes: 58 additions & 2 deletions static/js/ui-chrome.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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) {
Expand All @@ -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);
});
}
Expand Down
Loading