|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SystemMonitor {
|
|
|
constructor() {
|
|
|
this.canvas = document.getElementById('network-canvas');
|
|
|
if (this.canvas) {
|
|
|
this.ctx = this.canvas.getContext('2d');
|
|
|
} else {
|
|
|
console.error('[SystemMonitor] Canvas element not found');
|
|
|
this.ctx = null;
|
|
|
}
|
|
|
this.ws = null;
|
|
|
this.updateInterval = null;
|
|
|
this.animationFrame = null;
|
|
|
this.lastPing = null;
|
|
|
|
|
|
|
|
|
this.nodes = [];
|
|
|
this.packets = [];
|
|
|
this.serverNode = null;
|
|
|
this.databaseNode = null;
|
|
|
this.clientNodes = [];
|
|
|
this.aiModelNodes = [];
|
|
|
|
|
|
|
|
|
this.systemStatus = null;
|
|
|
this.lastUpdate = null;
|
|
|
|
|
|
|
|
|
this.time = 0;
|
|
|
this.particleEffects = [];
|
|
|
|
|
|
|
|
|
this.icons = {};
|
|
|
|
|
|
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
console.log('[SystemMonitor] Initializing...');
|
|
|
|
|
|
|
|
|
this.showLoadingState();
|
|
|
|
|
|
try {
|
|
|
this.loadIcons();
|
|
|
console.log('[SystemMonitor] Icons loaded');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Icons loading failed:', error);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
this.setupCanvas();
|
|
|
console.log('[SystemMonitor] Canvas setup complete');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Canvas setup failed:', error);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
this.setupEventListeners();
|
|
|
console.log('[SystemMonitor] Event listeners setup complete');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Event listeners setup failed:', error);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
this.startAnimation();
|
|
|
console.log('[SystemMonitor] Animation started');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Animation failed:', error);
|
|
|
}
|
|
|
|
|
|
|
|
|
try {
|
|
|
this.connectWebSocket();
|
|
|
console.log('[SystemMonitor] WebSocket connection initiated');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] WebSocket connection failed:', error);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
this.startPolling();
|
|
|
console.log('[SystemMonitor] Polling started');
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Polling failed:', error);
|
|
|
}
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
this.hideLoadingState();
|
|
|
}, 1000);
|
|
|
|
|
|
console.log('[SystemMonitor] Initialization complete');
|
|
|
}
|
|
|
|
|
|
showLoadingState() {
|
|
|
const statsGrid = document.getElementById('stats-grid');
|
|
|
if (!statsGrid) return;
|
|
|
|
|
|
|
|
|
statsGrid.querySelectorAll('.stat-card').forEach(card => {
|
|
|
const details = card.querySelector('.stat-details, .models-list, .sources-summary, .requests-list');
|
|
|
if (details) {
|
|
|
details.innerHTML = '<div class="loading-spinner-small"></div>';
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
hideLoadingState() {
|
|
|
|
|
|
}
|
|
|
|
|
|
loadIcons() {
|
|
|
|
|
|
this.icons = {
|
|
|
server: this.createServerIcon(),
|
|
|
database: this.createDatabaseIcon(),
|
|
|
client: this.createClientIcon(),
|
|
|
source: this.createSourceIcon(),
|
|
|
aiModel: this.createAIModelIcon()
|
|
|
};
|
|
|
}
|
|
|
|
|
|
createServerIcon() {
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2">
|
|
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
|
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
|
|
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
|
|
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
|
|
</svg>`;
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
|
}
|
|
|
|
|
|
createDatabaseIcon() {
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2">
|
|
|
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
|
|
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
|
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
|
</svg>`;
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
|
}
|
|
|
|
|
|
createClientIcon() {
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2">
|
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
|
</svg>`;
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
|
}
|
|
|
|
|
|
createSourceIcon() {
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2">
|
|
|
<circle cx="12" cy="12" r="10"/>
|
|
|
<circle cx="12" cy="12" r="6"/>
|
|
|
<circle cx="12" cy="12" r="2"/>
|
|
|
</svg>`;
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
|
}
|
|
|
|
|
|
createAIModelIcon() {
|
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#ec4899" stroke-width="2">
|
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
|
<path d="M2 17l10 5 10-5"/>
|
|
|
<path d="M2 12l10 5 10-5"/>
|
|
|
</svg>`;
|
|
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
|
}
|
|
|
|
|
|
setupCanvas() {
|
|
|
if (!this.canvas) {
|
|
|
console.warn('[SystemMonitor] Canvas not available, skipping setup');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const resizeCanvas = () => {
|
|
|
if (!this.canvas) return;
|
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
|
this.canvas.width = rect.width;
|
|
|
this.canvas.height = rect.height;
|
|
|
this.draw();
|
|
|
};
|
|
|
|
|
|
resizeCanvas();
|
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
}
|
|
|
|
|
|
connectWebSocket() {
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
|
const wsUrl = `${protocol}//${window.location.host}/api/monitoring/ws`;
|
|
|
|
|
|
try {
|
|
|
this.ws = new WebSocket(wsUrl);
|
|
|
|
|
|
this.ws.onopen = () => {
|
|
|
console.log('[SystemMonitor] WebSocket connected');
|
|
|
this.updateConnectionStatus(true);
|
|
|
};
|
|
|
|
|
|
this.ws.onmessage = (event) => {
|
|
|
try {
|
|
|
const data = JSON.parse(event.data);
|
|
|
if (data.type === 'heartbeat') {
|
|
|
return;
|
|
|
}
|
|
|
this.updateSystemStatus(data);
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Error parsing WebSocket message:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
this.ws.onerror = (error) => {
|
|
|
console.error('[SystemMonitor] WebSocket error:', error);
|
|
|
this.updateConnectionStatus(false);
|
|
|
};
|
|
|
|
|
|
this.ws.onclose = () => {
|
|
|
console.log('[SystemMonitor] WebSocket disconnected');
|
|
|
this.updateConnectionStatus(false);
|
|
|
|
|
|
setTimeout(() => this.connectWebSocket(), 3000);
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Failed to connect WebSocket:', error);
|
|
|
this.updateConnectionStatus(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
startPolling() {
|
|
|
|
|
|
|
|
|
if (this.updateInterval) {
|
|
|
clearInterval(this.updateInterval);
|
|
|
}
|
|
|
|
|
|
this.updateInterval = setInterval(() => {
|
|
|
this.fetchSystemStatus();
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
this.fetchSystemStatus();
|
|
|
}
|
|
|
|
|
|
async fetchSystemStatus() {
|
|
|
try {
|
|
|
console.log('[SystemMonitor] Fetching system status...');
|
|
|
|
|
|
const response = await fetch('/api/monitoring/status', {
|
|
|
method: 'GET',
|
|
|
headers: {
|
|
|
'Accept': 'application/json'
|
|
|
},
|
|
|
signal: AbortSignal.timeout(10000)
|
|
|
});
|
|
|
|
|
|
console.log(`[SystemMonitor] Response status: ${response.status}`);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
if (response.status === 429) {
|
|
|
|
|
|
console.warn('[SystemMonitor] Rate limited, increasing poll interval');
|
|
|
if (this.updateInterval) {
|
|
|
clearInterval(this.updateInterval);
|
|
|
this.updateInterval = setInterval(() => {
|
|
|
this.fetchSystemStatus();
|
|
|
}, 10000);
|
|
|
}
|
|
|
this.showToast('Rate limited - slowing updates', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
const errorText = await response.text();
|
|
|
console.error(`[SystemMonitor] HTTP ${response.status}: ${errorText}`);
|
|
|
throw new Error(`HTTP ${response.status}: ${errorText.substring(0, 100)}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
console.log('[SystemMonitor] Data received:', data);
|
|
|
|
|
|
|
|
|
if (data.success === false) {
|
|
|
console.warn('[SystemMonitor] API returned success=false:', data.error);
|
|
|
this.showToast(data.error || 'API returned error', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.updateSystemStatus(data);
|
|
|
this.updateConnectionStatus(true);
|
|
|
this.lastUpdate = new Date();
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Failed to fetch system status:', error);
|
|
|
this.updateConnectionStatus(false);
|
|
|
|
|
|
|
|
|
const statusText = document.getElementById('overall-status-text');
|
|
|
if (statusText) {
|
|
|
statusText.textContent = 'Error';
|
|
|
}
|
|
|
const statusDot = document.getElementById('status-dot');
|
|
|
if (statusDot) {
|
|
|
statusDot.className = 'status-dot offline';
|
|
|
}
|
|
|
|
|
|
|
|
|
if (error.name === 'AbortError' || error.message.includes('fetch')) {
|
|
|
this.showToast('Connection timeout - check your network', 'error');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateSystemStatus(data) {
|
|
|
|
|
|
if (data && data.success === false) {
|
|
|
console.warn('[SystemMonitor] API returned success=false:', data.error);
|
|
|
this.showToast(data.error || 'API returned error', 'error');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!data) {
|
|
|
console.warn('[SystemMonitor] No data received');
|
|
|
this.showToast('No data received from server', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.systemStatus = data;
|
|
|
this.lastUpdate = new Date(data.timestamp || new Date().toISOString());
|
|
|
|
|
|
|
|
|
try {
|
|
|
this.updateHeader();
|
|
|
this.updateDatabaseStatus(data.database || {});
|
|
|
this.updateAIModels(data.ai_models || {});
|
|
|
this.updateDataSources(data.data_sources || {});
|
|
|
this.updateRequests(data.recent_requests || [], data.stats || {});
|
|
|
|
|
|
|
|
|
this.updateNetworkNodes(data);
|
|
|
|
|
|
|
|
|
this.hideLoadingState();
|
|
|
} catch (error) {
|
|
|
console.error('[SystemMonitor] Error updating UI:', error);
|
|
|
this.showToast('Error updating display', 'error');
|
|
|
}
|
|
|
|
|
|
|
|
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
|
if (!this.lastPing || Date.now() - this.lastPing > 10000) {
|
|
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
|
this.lastPing = Date.now();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateHeader() {
|
|
|
const statusBadge = document.getElementById('overall-status-badge');
|
|
|
const statusText = document.getElementById('overall-status-text');
|
|
|
const statusDot = document.getElementById('status-dot');
|
|
|
const updateEl = document.getElementById('last-update');
|
|
|
|
|
|
if (this.systemStatus) {
|
|
|
const stats = this.systemStatus.stats || {};
|
|
|
const totalSources = stats.total_sources || this.systemStatus.data_sources?.total || 0;
|
|
|
const activeSources = stats.active_sources || this.systemStatus.data_sources?.active || 0;
|
|
|
const health = totalSources > 0 ? (activeSources / totalSources) * 100 : 100;
|
|
|
|
|
|
if (health >= 80) {
|
|
|
statusText.textContent = 'Healthy';
|
|
|
statusDot.className = 'status-dot online';
|
|
|
} else if (health >= 50) {
|
|
|
statusText.textContent = 'Degraded';
|
|
|
statusDot.className = 'status-dot degraded';
|
|
|
} else {
|
|
|
statusText.textContent = 'Unhealthy';
|
|
|
statusDot.className = 'status-dot offline';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (this.lastUpdate) {
|
|
|
const secondsAgo = Math.floor((Date.now() - this.lastUpdate.getTime()) / 1000);
|
|
|
updateEl.textContent = secondsAgo < 60 ? `${secondsAgo}s ago` : `${Math.floor(secondsAgo / 60)}m ago`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateDatabaseStatus(db) {
|
|
|
const statusEl = document.getElementById('db-status');
|
|
|
const detailsEl = document.getElementById('db-details');
|
|
|
|
|
|
if (!statusEl) return;
|
|
|
|
|
|
const dot = statusEl.querySelector('.status-dot');
|
|
|
const text = statusEl.querySelector('.status-text');
|
|
|
|
|
|
if (db && db.online) {
|
|
|
if (dot) dot.className = 'status-dot online';
|
|
|
if (text) text.textContent = 'Online';
|
|
|
|
|
|
|
|
|
if (detailsEl) {
|
|
|
const dbPath = db.path || db.file_path || 'N/A';
|
|
|
const dbSize = db.size ? this.formatBytes(db.size) : 'N/A';
|
|
|
const dbTables = db.tables || db.table_count || 'N/A';
|
|
|
detailsEl.innerHTML = `
|
|
|
<div class="stat-detail-item">
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
|
|
<circle cx="12" cy="10" r="3"/>
|
|
|
</svg>
|
|
|
<span>Path: ${dbPath.length > 30 ? dbPath.substring(0, 30) + '...' : dbPath}</span>
|
|
|
</div>
|
|
|
<div class="stat-detail-item">
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
|
</svg>
|
|
|
<span>Size: ${dbSize}</span>
|
|
|
</div>
|
|
|
${dbTables !== 'N/A' ? `
|
|
|
<div class="stat-detail-item">
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
|
<line x1="3" y1="9" x2="21" y2="9"/>
|
|
|
<line x1="9" y1="21" x2="9" y2="9"/>
|
|
|
</svg>
|
|
|
<span>Tables: ${dbTables}</span>
|
|
|
</div>
|
|
|
` : ''}
|
|
|
`;
|
|
|
}
|
|
|
} else {
|
|
|
if (dot) dot.className = 'status-dot offline';
|
|
|
if (text) text.textContent = 'Offline';
|
|
|
if (detailsEl) {
|
|
|
detailsEl.innerHTML = `
|
|
|
<div class="stat-detail-item error">
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<circle cx="12" cy="12" r="10"/>
|
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
|
</svg>
|
|
|
<span>Database connection failed</span>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateAIModels(models) {
|
|
|
const total = models.total || 0;
|
|
|
const available = models.available || 0;
|
|
|
const failed = models.failed || 0;
|
|
|
|
|
|
const totalEl = document.getElementById('models-total');
|
|
|
const availableEl = document.getElementById('models-available');
|
|
|
const failedEl = document.getElementById('models-failed');
|
|
|
|
|
|
if (totalEl) totalEl.textContent = total;
|
|
|
if (availableEl) availableEl.textContent = available;
|
|
|
if (failedEl) failedEl.textContent = failed;
|
|
|
|
|
|
const listEl = document.getElementById('models-list');
|
|
|
if (!listEl) return;
|
|
|
|
|
|
listEl.innerHTML = '';
|
|
|
|
|
|
const modelsList = models.models || [];
|
|
|
if (modelsList.length === 0) {
|
|
|
listEl.innerHTML = '<div class="empty-message">No models loaded</div>';
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
modelsList.slice(0, 5).forEach(model => {
|
|
|
const item = document.createElement('div');
|
|
|
item.className = 'model-item';
|
|
|
const modelId = model.id || model.model_id || 'Unknown';
|
|
|
const modelName = modelId.split('/').pop();
|
|
|
const status = model.status || 'unknown';
|
|
|
const statusClass = (status === 'available' || status === 'healthy') ? 'available' : 'failed';
|
|
|
item.innerHTML = `
|
|
|
<span class="model-name">${modelName}</span>
|
|
|
<span class="model-status ${statusClass}">${status}</span>
|
|
|
`;
|
|
|
listEl.appendChild(item);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
updateDataSources(sources) {
|
|
|
const total = sources.total || 0;
|
|
|
const active = sources.active || 0;
|
|
|
const pools = sources.pools || 0;
|
|
|
|
|
|
const totalEl = document.getElementById('sources-total');
|
|
|
const activeEl = document.getElementById('sources-active');
|
|
|
const poolsEl = document.getElementById('sources-pools');
|
|
|
|
|
|
if (totalEl) totalEl.textContent = total;
|
|
|
if (activeEl) activeEl.textContent = active;
|
|
|
if (poolsEl) poolsEl.textContent = pools;
|
|
|
|
|
|
const summaryEl = document.getElementById('sources-summary');
|
|
|
if (!summaryEl) return;
|
|
|
|
|
|
summaryEl.innerHTML = '';
|
|
|
|
|
|
const categories = sources.categories || {};
|
|
|
if (Object.keys(categories).length === 0) {
|
|
|
summaryEl.innerHTML = '<div class="empty-message">No source categories available</div>';
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
Object.entries(categories).forEach(([category, data]) => {
|
|
|
const item = document.createElement('div');
|
|
|
item.className = 'source-category';
|
|
|
const activeCount = data.active || 0;
|
|
|
const totalCount = data.total || 0;
|
|
|
const isHealthy = activeCount > 0;
|
|
|
item.innerHTML = `
|
|
|
<span class="category-name">
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<circle cx="12" cy="12" r="10"/>
|
|
|
<circle cx="12" cy="12" r="6"/>
|
|
|
<circle cx="12" cy="12" r="2"/>
|
|
|
</svg>
|
|
|
${category}
|
|
|
</span>
|
|
|
<span class="category-count ${isHealthy ? 'success' : 'error'}">${activeCount}/${totalCount}</span>
|
|
|
`;
|
|
|
summaryEl.appendChild(item);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
updateRequests(requests, stats) {
|
|
|
const minuteCount = stats?.requests_last_minute || stats?.requests_per_minute || 0;
|
|
|
const hourCount = stats?.requests_last_hour || stats?.requests_per_hour || 0;
|
|
|
|
|
|
const minuteEl = document.getElementById('requests-minute');
|
|
|
const hourEl = document.getElementById('requests-hour');
|
|
|
|
|
|
if (minuteEl) minuteEl.textContent = minuteCount;
|
|
|
if (hourEl) hourEl.textContent = hourCount;
|
|
|
|
|
|
const listEl = document.getElementById('requests-list');
|
|
|
if (!listEl) return;
|
|
|
|
|
|
listEl.innerHTML = '';
|
|
|
|
|
|
if (!Array.isArray(requests)) {
|
|
|
requests = [];
|
|
|
}
|
|
|
|
|
|
if (requests.length === 0) {
|
|
|
listEl.innerHTML = '<div class="empty-message">No recent requests</div>';
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
requests.slice(0, 5).forEach(request => {
|
|
|
const item = document.createElement('div');
|
|
|
item.className = 'request-item';
|
|
|
const timestamp = request.timestamp || new Date().toISOString();
|
|
|
const time = new Date(timestamp);
|
|
|
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`;
|
|
|
const endpoint = request.endpoint || request.path || request.method || 'Request';
|
|
|
const method = request.method || 'GET';
|
|
|
item.innerHTML = `
|
|
|
<div class="request-info">
|
|
|
<span class="request-method">${method}</span>
|
|
|
<span class="request-endpoint">${endpoint}</span>
|
|
|
</div>
|
|
|
<span class="request-time">${timeStr}</span>
|
|
|
`;
|
|
|
listEl.appendChild(item);
|
|
|
|
|
|
|
|
|
if (endpoint && endpoint !== 'Request') {
|
|
|
this.createPacket(request);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
updateNetworkNodes(data) {
|
|
|
if (!this.canvas || this.canvas.width === 0) return;
|
|
|
|
|
|
const centerX = this.canvas.width / 2;
|
|
|
const centerY = this.canvas.height / 2;
|
|
|
|
|
|
|
|
|
this.serverNode = {
|
|
|
x: centerX,
|
|
|
y: centerY,
|
|
|
radius: 40,
|
|
|
label: 'API Server',
|
|
|
status: 'online',
|
|
|
color: '#22c55e',
|
|
|
icon: 'server',
|
|
|
type: 'server'
|
|
|
};
|
|
|
|
|
|
|
|
|
this.databaseNode = {
|
|
|
x: centerX + 200,
|
|
|
y: centerY,
|
|
|
radius: 35,
|
|
|
label: 'Database',
|
|
|
status: data.database?.online ? 'online' : 'offline',
|
|
|
color: data.database?.online ? '#3b82f6' : '#ef4444',
|
|
|
icon: 'database',
|
|
|
type: 'database'
|
|
|
};
|
|
|
|
|
|
|
|
|
this.clientNodes = [];
|
|
|
const numClients = 3;
|
|
|
const clientSpacing = 150;
|
|
|
const clientStartX = centerX - (clientSpacing * (numClients - 1)) / 2;
|
|
|
|
|
|
for (let i = 0; i < numClients; i++) {
|
|
|
this.clientNodes.push({
|
|
|
x: clientStartX + i * clientSpacing,
|
|
|
y: this.canvas.height - 80,
|
|
|
radius: 30,
|
|
|
label: `Client ${i + 1}`,
|
|
|
status: 'active',
|
|
|
color: '#8b5cf6',
|
|
|
icon: 'client',
|
|
|
type: 'client'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
this.nodes = [];
|
|
|
const sources = data.data_sources?.sources || [];
|
|
|
const numSources = Math.max(sources.length, 4);
|
|
|
const angleStep = Math.PI / (numSources + 1);
|
|
|
const sourceRadius = 250;
|
|
|
|
|
|
sources.forEach((source, index) => {
|
|
|
const angle = Math.PI + angleStep * (index + 1);
|
|
|
const x = centerX + Math.cos(angle) * sourceRadius;
|
|
|
const y = centerY + Math.sin(angle) * sourceRadius;
|
|
|
|
|
|
const status = source.status || 'active';
|
|
|
this.nodes.push({
|
|
|
x,
|
|
|
y,
|
|
|
radius: 30,
|
|
|
label: source.name || source.id || `Source ${index + 1}`,
|
|
|
status: status === 'active' ? 'online' : 'offline',
|
|
|
color: status === 'active' ? '#f59e0b' : '#ef4444',
|
|
|
icon: 'source',
|
|
|
type: 'source',
|
|
|
endpoint: source.endpoint || source.endpoint_url
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
this.aiModelNodes = [];
|
|
|
const models = data.ai_models?.models || [];
|
|
|
const numModels = Math.min(models.length, 4);
|
|
|
const modelSpacing = 80;
|
|
|
const modelStartY = centerY - (modelSpacing * (numModels - 1)) / 2;
|
|
|
|
|
|
models.slice(0, 4).forEach((model, index) => {
|
|
|
const status = model.status || 'unknown';
|
|
|
this.aiModelNodes.push({
|
|
|
x: 80,
|
|
|
y: modelStartY + index * modelSpacing,
|
|
|
radius: 25,
|
|
|
label: (model.id || model.model_id || 'Model').split('/').pop().substring(0, 15),
|
|
|
status: status === 'available' || status === 'healthy' ? 'online' : 'offline',
|
|
|
color: status === 'available' || status === 'healthy' ? '#ec4899' : '#ef4444',
|
|
|
icon: 'aiModel',
|
|
|
type: 'aiModel'
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
createPacket(request) {
|
|
|
if (!this.serverNode) return;
|
|
|
|
|
|
|
|
|
const endpoint = request.endpoint || request.path || '';
|
|
|
let fromNode, toNode, returnNode;
|
|
|
|
|
|
|
|
|
if (this.clientNodes.length > 0) {
|
|
|
|
|
|
fromNode = this.clientNodes.length > 0 ? this.clientNodes[0] : null;
|
|
|
toNode = this.serverNode;
|
|
|
|
|
|
|
|
|
if (endpoint.includes('models') || endpoint.includes('sentiment')) {
|
|
|
returnNode = this.aiModelNodes[0] || this.databaseNode;
|
|
|
} else if (endpoint.includes('database') || endpoint.includes('history')) {
|
|
|
returnNode = this.databaseNode;
|
|
|
} else if (this.nodes.length > 0) {
|
|
|
|
|
|
returnNode = this.nodes.length > 0 ? this.nodes[0] : null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const requestPacket = {
|
|
|
x: fromNode.x,
|
|
|
y: fromNode.y,
|
|
|
startX: fromNode.x,
|
|
|
startY: fromNode.y,
|
|
|
targetX: toNode.x,
|
|
|
targetY: toNode.y,
|
|
|
progress: 0,
|
|
|
speed: 0.015,
|
|
|
color: '#8b5cf6',
|
|
|
size: 6,
|
|
|
label: endpoint.split('/').pop() || 'Request',
|
|
|
type: 'request',
|
|
|
trail: []
|
|
|
};
|
|
|
|
|
|
this.packets.push(requestPacket);
|
|
|
|
|
|
|
|
|
if (returnNode) {
|
|
|
setTimeout(() => {
|
|
|
const processingPacket = {
|
|
|
x: toNode.x,
|
|
|
y: toNode.y,
|
|
|
startX: toNode.x,
|
|
|
startY: toNode.y,
|
|
|
targetX: returnNode.x,
|
|
|
targetY: returnNode.y,
|
|
|
progress: 0,
|
|
|
speed: 0.02,
|
|
|
color: '#22d3ee',
|
|
|
size: 5,
|
|
|
label: 'Processing',
|
|
|
type: 'processing',
|
|
|
trail: []
|
|
|
};
|
|
|
this.packets.push(processingPacket);
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
const responsePacket = {
|
|
|
x: returnNode.x,
|
|
|
y: returnNode.y,
|
|
|
startX: returnNode.x,
|
|
|
startY: returnNode.y,
|
|
|
targetX: toNode.x,
|
|
|
targetY: toNode.y,
|
|
|
progress: 0,
|
|
|
speed: 0.02,
|
|
|
color: '#22c55e',
|
|
|
size: 5,
|
|
|
label: 'Data',
|
|
|
type: 'response',
|
|
|
trail: []
|
|
|
};
|
|
|
this.packets.push(responsePacket);
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
const finalPacket = {
|
|
|
x: toNode.x,
|
|
|
y: toNode.y,
|
|
|
startX: toNode.x,
|
|
|
startY: toNode.y,
|
|
|
targetX: fromNode.x,
|
|
|
targetY: fromNode.y,
|
|
|
progress: 0,
|
|
|
speed: 0.015,
|
|
|
color: '#10b981',
|
|
|
size: 6,
|
|
|
label: 'Response',
|
|
|
type: 'final',
|
|
|
trail: []
|
|
|
};
|
|
|
this.packets.push(finalPacket);
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
this.createParticleEffect(fromNode.x, fromNode.y, '#10b981');
|
|
|
}, 1000);
|
|
|
}, 800);
|
|
|
}, 800);
|
|
|
}, 500);
|
|
|
}
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
this.packets = this.packets.filter(p => p.progress < 1.5);
|
|
|
}, 5000);
|
|
|
}
|
|
|
|
|
|
createParticleEffect(x, y, color) {
|
|
|
const numParticles = 12;
|
|
|
for (let i = 0; i < numParticles; i++) {
|
|
|
const angle = (Math.PI * 2 * i) / numParticles;
|
|
|
this.particleEffects.push({
|
|
|
x,
|
|
|
y,
|
|
|
vx: Math.cos(angle) * 2,
|
|
|
vy: Math.sin(angle) * 2,
|
|
|
life: 1,
|
|
|
color,
|
|
|
size: 3
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
startAnimation() {
|
|
|
const animate = () => {
|
|
|
this.update();
|
|
|
this.draw();
|
|
|
this.animationFrame = requestAnimationFrame(animate);
|
|
|
};
|
|
|
animate();
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
update() {
|
|
|
this.time += 0.016;
|
|
|
|
|
|
|
|
|
this.packets.forEach(packet => {
|
|
|
packet.progress += packet.speed;
|
|
|
|
|
|
|
|
|
const easeProgress = packet.progress < 0.5
|
|
|
? 2 * packet.progress * packet.progress
|
|
|
: 1 - Math.pow(-2 * packet.progress + 2, 2) / 2;
|
|
|
|
|
|
|
|
|
const newX = packet.startX + (packet.targetX - packet.startX) * easeProgress;
|
|
|
const newY = packet.startY + (packet.targetY - packet.startY) * easeProgress;
|
|
|
|
|
|
|
|
|
if (packet.trail) {
|
|
|
packet.trail.push({ x: packet.x, y: packet.y });
|
|
|
if (packet.trail.length > 10) {
|
|
|
packet.trail.shift();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
packet.x = newX;
|
|
|
packet.y = newY;
|
|
|
});
|
|
|
|
|
|
|
|
|
this.packets = this.packets.filter(p => p.progress < 1.2);
|
|
|
|
|
|
|
|
|
this.particleEffects.forEach(particle => {
|
|
|
particle.x += particle.vx;
|
|
|
particle.y += particle.vy;
|
|
|
particle.life -= 0.02;
|
|
|
particle.vx *= 0.95;
|
|
|
particle.vy *= 0.95;
|
|
|
});
|
|
|
|
|
|
|
|
|
this.particleEffects = this.particleEffects.filter(p => p.life > 0);
|
|
|
}
|
|
|
|
|
|
draw() {
|
|
|
if (!this.canvas || !this.ctx || this.canvas.width === 0 || this.canvas.height === 0) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
|
|
|
gradient.addColorStop(0, '#0f172a');
|
|
|
gradient.addColorStop(1, '#1e293b');
|
|
|
this.ctx.fillStyle = gradient;
|
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
|
|
|
|
|
this.drawGrid();
|
|
|
|
|
|
|
|
|
if (this.serverNode) {
|
|
|
|
|
|
if (this.databaseNode) {
|
|
|
this.drawConnection(this.serverNode, this.databaseNode, this.databaseNode.status === 'online');
|
|
|
}
|
|
|
|
|
|
|
|
|
this.nodes.forEach(node => {
|
|
|
this.drawConnection(this.serverNode, node, node.status === 'online');
|
|
|
});
|
|
|
|
|
|
|
|
|
this.clientNodes.forEach(client => {
|
|
|
this.drawConnection(this.serverNode, client, true);
|
|
|
});
|
|
|
|
|
|
|
|
|
this.aiModelNodes.forEach(model => {
|
|
|
this.drawConnection(this.serverNode, model, model.status === 'online');
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
this.packets.forEach(packet => {
|
|
|
if (packet.trail && packet.trail.length > 1) {
|
|
|
this.drawTrail(packet.trail, packet.color);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
this.packets.forEach(packet => {
|
|
|
this.drawPacket(packet);
|
|
|
});
|
|
|
|
|
|
|
|
|
this.particleEffects.forEach(particle => {
|
|
|
this.drawParticle(particle);
|
|
|
});
|
|
|
|
|
|
|
|
|
if (this.serverNode) {
|
|
|
this.drawNodeWithIcon(this.serverNode);
|
|
|
}
|
|
|
|
|
|
if (this.databaseNode) {
|
|
|
this.drawNodeWithIcon(this.databaseNode);
|
|
|
}
|
|
|
|
|
|
this.clientNodes.forEach(node => {
|
|
|
this.drawNodeWithIcon(node);
|
|
|
});
|
|
|
|
|
|
this.nodes.forEach(node => {
|
|
|
this.drawNodeWithIcon(node);
|
|
|
});
|
|
|
|
|
|
this.aiModelNodes.forEach(node => {
|
|
|
this.drawNodeWithIcon(node);
|
|
|
});
|
|
|
|
|
|
|
|
|
this.drawLegend();
|
|
|
}
|
|
|
|
|
|
drawGrid() {
|
|
|
this.ctx.strokeStyle = 'rgba(148, 163, 184, 0.05)';
|
|
|
this.ctx.lineWidth = 1;
|
|
|
|
|
|
const gridSize = 40;
|
|
|
|
|
|
|
|
|
for (let x = 0; x < this.canvas.width; x += gridSize) {
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(x, 0);
|
|
|
this.ctx.lineTo(x, this.canvas.height);
|
|
|
this.ctx.stroke();
|
|
|
}
|
|
|
|
|
|
|
|
|
for (let y = 0; y < this.canvas.height; y += gridSize) {
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(0, y);
|
|
|
this.ctx.lineTo(this.canvas.width, y);
|
|
|
this.ctx.stroke();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
drawTrail(trail, color) {
|
|
|
if (trail.length < 2) return;
|
|
|
|
|
|
this.ctx.strokeStyle = color;
|
|
|
this.ctx.lineWidth = 2;
|
|
|
this.ctx.globalAlpha = 0.3;
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(trail[0].x, trail[0].y);
|
|
|
|
|
|
for (let i = 1; i < trail.length; i++) {
|
|
|
this.ctx.lineTo(trail[i].x, trail[i].y);
|
|
|
}
|
|
|
|
|
|
this.ctx.stroke();
|
|
|
this.ctx.globalAlpha = 1;
|
|
|
}
|
|
|
|
|
|
drawParticle(particle) {
|
|
|
this.ctx.globalAlpha = particle.life;
|
|
|
this.ctx.fillStyle = particle.color;
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.globalAlpha = 1;
|
|
|
}
|
|
|
|
|
|
drawLegend() {
|
|
|
const legends = [
|
|
|
{ label: 'Request', color: '#8b5cf6' },
|
|
|
{ label: 'Processing', color: '#22d3ee' },
|
|
|
{ label: 'Response', color: '#22c55e' }
|
|
|
];
|
|
|
|
|
|
const startX = 20;
|
|
|
const startY = 20;
|
|
|
const spacing = 120;
|
|
|
|
|
|
legends.forEach((legend, index) => {
|
|
|
const x = startX + index * spacing;
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = legend.color;
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(x, startY, 6, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = '#e2e8f0';
|
|
|
this.ctx.font = '12px Arial';
|
|
|
this.ctx.textAlign = 'left';
|
|
|
this.ctx.fillText(legend.label, x + 12, startY + 4);
|
|
|
});
|
|
|
|
|
|
|
|
|
if (this.systemStatus) {
|
|
|
const stats = this.systemStatus.stats || {};
|
|
|
const overlayX = this.canvas.width - 200;
|
|
|
const overlayY = 20;
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = 'rgba(30, 41, 59, 0.9)';
|
|
|
this.ctx.fillRect(overlayX, overlayY, 180, 120);
|
|
|
|
|
|
|
|
|
this.ctx.strokeStyle = '#22c55e';
|
|
|
this.ctx.lineWidth = 2;
|
|
|
this.ctx.strokeRect(overlayX, overlayY, 180, 120);
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = '#22c55e';
|
|
|
this.ctx.font = 'bold 14px Arial';
|
|
|
this.ctx.textAlign = 'left';
|
|
|
this.ctx.fillText('System Stats', overlayX + 10, overlayY + 25);
|
|
|
|
|
|
|
|
|
const statsList = [
|
|
|
{ label: 'Active Packets:', value: this.packets.length },
|
|
|
{ label: 'Data Sources:', value: stats.active_sources || 0 },
|
|
|
{ label: 'AI Models:', value: this.aiModelNodes.length },
|
|
|
{ label: 'Clients:', value: this.clientNodes.length }
|
|
|
];
|
|
|
|
|
|
this.ctx.font = '11px Arial';
|
|
|
this.ctx.fillStyle = '#cbd5e1';
|
|
|
|
|
|
statsList.forEach((stat, index) => {
|
|
|
const y = overlayY + 50 + index * 20;
|
|
|
this.ctx.fillText(stat.label, overlayX + 10, y);
|
|
|
|
|
|
this.ctx.fillStyle = '#22d3ee';
|
|
|
this.ctx.textAlign = 'right';
|
|
|
this.ctx.fillText(String(stat.value), overlayX + 170, y);
|
|
|
|
|
|
this.ctx.fillStyle = '#cbd5e1';
|
|
|
this.ctx.textAlign = 'left';
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
drawConnection(from, to, active) {
|
|
|
|
|
|
const dashOffset = active ? -this.time * 20 : 0;
|
|
|
|
|
|
this.ctx.strokeStyle = active ? 'rgba(34, 197, 94, 0.4)' : 'rgba(239, 68, 68, 0.2)';
|
|
|
this.ctx.lineWidth = 2;
|
|
|
this.ctx.setLineDash(active ? [10, 5] : [5, 5]);
|
|
|
this.ctx.lineDashOffset = dashOffset;
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(from.x, from.y);
|
|
|
this.ctx.lineTo(to.x, to.y);
|
|
|
this.ctx.stroke();
|
|
|
|
|
|
this.ctx.setLineDash([]);
|
|
|
}
|
|
|
|
|
|
drawNodeWithIcon(node) {
|
|
|
|
|
|
const pulseScale = 1 + Math.sin(this.time * 2) * 0.1;
|
|
|
const glowRadius = node.radius * 2.5 * pulseScale;
|
|
|
|
|
|
const gradient = this.ctx.createRadialGradient(
|
|
|
node.x, node.y, 0,
|
|
|
node.x, node.y, glowRadius
|
|
|
);
|
|
|
gradient.addColorStop(0, node.color + '80');
|
|
|
gradient.addColorStop(0.5, node.color + '20');
|
|
|
gradient.addColorStop(1, 'transparent');
|
|
|
|
|
|
this.ctx.fillStyle = gradient;
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, glowRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = '#1e293b';
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
|
|
|
const borderGradient = this.ctx.createLinearGradient(
|
|
|
node.x - node.radius, node.y - node.radius,
|
|
|
node.x + node.radius, node.y + node.radius
|
|
|
);
|
|
|
borderGradient.addColorStop(0, node.color);
|
|
|
borderGradient.addColorStop(1, node.color + '80');
|
|
|
|
|
|
this.ctx.strokeStyle = borderGradient;
|
|
|
this.ctx.lineWidth = 3;
|
|
|
this.ctx.stroke();
|
|
|
|
|
|
|
|
|
this.drawNodeIcon(node);
|
|
|
|
|
|
|
|
|
const labelY = node.y + node.radius + 20;
|
|
|
const labelText = node.label.substring(0, 15);
|
|
|
|
|
|
this.ctx.font = 'bold 11px Arial';
|
|
|
this.ctx.textAlign = 'center';
|
|
|
const textWidth = this.ctx.measureText(labelText).width;
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = 'rgba(30, 41, 59, 0.8)';
|
|
|
this.ctx.fillRect(node.x - textWidth / 2 - 6, labelY - 12, textWidth + 12, 18);
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = '#e2e8f0';
|
|
|
this.ctx.fillText(labelText, node.x, labelY);
|
|
|
|
|
|
|
|
|
if (node.status === 'online') {
|
|
|
this.ctx.fillStyle = '#22c55e';
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x + node.radius - 8, node.y - node.radius + 8, 5, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
} else if (node.status === 'offline') {
|
|
|
this.ctx.fillStyle = '#ef4444';
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x + node.radius - 8, node.y - node.radius + 8, 5, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
drawNodeIcon(node) {
|
|
|
const iconSize = node.radius * 0.8;
|
|
|
this.ctx.strokeStyle = node.color;
|
|
|
this.ctx.fillStyle = node.color;
|
|
|
this.ctx.lineWidth = 2;
|
|
|
|
|
|
switch (node.type) {
|
|
|
case 'server':
|
|
|
|
|
|
this.ctx.strokeRect(node.x - iconSize / 2, node.y - iconSize / 2, iconSize, iconSize / 3);
|
|
|
this.ctx.strokeRect(node.x - iconSize / 2, node.y - iconSize / 6, iconSize, iconSize / 3);
|
|
|
this.ctx.strokeRect(node.x - iconSize / 2, node.y + iconSize / 6, iconSize, iconSize / 3);
|
|
|
break;
|
|
|
|
|
|
case 'database':
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.ellipse(node.x, node.y - iconSize / 3, iconSize / 2, iconSize / 6, 0, 0, Math.PI * 2);
|
|
|
this.ctx.stroke();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(node.x - iconSize / 2, node.y - iconSize / 3);
|
|
|
this.ctx.lineTo(node.x - iconSize / 2, node.y + iconSize / 3);
|
|
|
this.ctx.moveTo(node.x + iconSize / 2, node.y - iconSize / 3);
|
|
|
this.ctx.lineTo(node.x + iconSize / 2, node.y + iconSize / 3);
|
|
|
this.ctx.stroke();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.ellipse(node.x, node.y + iconSize / 3, iconSize / 2, iconSize / 6, 0, 0, Math.PI * 2);
|
|
|
this.ctx.stroke();
|
|
|
break;
|
|
|
|
|
|
case 'client':
|
|
|
|
|
|
this.ctx.strokeRect(node.x - iconSize / 2, node.y - iconSize / 2, iconSize, iconSize * 0.7);
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.moveTo(node.x - iconSize / 4, node.y + iconSize / 2);
|
|
|
this.ctx.lineTo(node.x + iconSize / 4, node.y + iconSize / 2);
|
|
|
this.ctx.stroke();
|
|
|
break;
|
|
|
|
|
|
case 'source':
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, iconSize / 4, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, iconSize / 2, 0, Math.PI * 2);
|
|
|
this.ctx.stroke();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, iconSize * 0.75, 0, Math.PI * 2);
|
|
|
this.ctx.stroke();
|
|
|
break;
|
|
|
|
|
|
case 'aiModel':
|
|
|
|
|
|
const nodeRadius = 3;
|
|
|
this.ctx.fillStyle = node.color;
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x - iconSize / 3, node.y - iconSize / 4, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x - iconSize / 3, node.y + iconSize / 4, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y - iconSize / 3, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x, node.y + iconSize / 3, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x + iconSize / 3, node.y - iconSize / 4, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(node.x + iconSize / 3, node.y + iconSize / 4, nodeRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
drawPacket(packet) {
|
|
|
|
|
|
const pulseScale = 1 + Math.sin(this.time * 5 + packet.progress * 10) * 0.2;
|
|
|
const glowRadius = packet.size * 4 * pulseScale;
|
|
|
|
|
|
const gradient = this.ctx.createRadialGradient(
|
|
|
packet.x, packet.y, 0,
|
|
|
packet.x, packet.y, glowRadius
|
|
|
);
|
|
|
gradient.addColorStop(0, packet.color);
|
|
|
gradient.addColorStop(0.5, packet.color + '40');
|
|
|
gradient.addColorStop(1, 'transparent');
|
|
|
|
|
|
this.ctx.fillStyle = gradient;
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(packet.x, packet.y, glowRadius, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
|
|
|
this.ctx.fillStyle = packet.color;
|
|
|
this.ctx.beginPath();
|
|
|
this.ctx.arc(packet.x, packet.y, packet.size, 0, Math.PI * 2);
|
|
|
this.ctx.fill();
|
|
|
|
|
|
|
|
|
this.ctx.strokeStyle = '#ffffff';
|
|
|
this.ctx.lineWidth = 2;
|
|
|
this.ctx.stroke();
|
|
|
|
|
|
|
|
|
if (packet.type === 'request') {
|
|
|
this.ctx.fillStyle = '#ffffff';
|
|
|
this.ctx.font = 'bold 8px Arial';
|
|
|
this.ctx.textAlign = 'center';
|
|
|
this.ctx.fillText('→', packet.x, packet.y + 3);
|
|
|
} else if (packet.type === 'response') {
|
|
|
this.ctx.fillStyle = '#ffffff';
|
|
|
this.ctx.font = 'bold 8px Arial';
|
|
|
this.ctx.textAlign = 'center';
|
|
|
this.ctx.fillText('✓', packet.x, packet.y + 3);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateConnectionStatus(connected) {
|
|
|
const statusEl = document.getElementById('connection-status');
|
|
|
if (!statusEl) return;
|
|
|
|
|
|
const dot = statusEl.querySelector('.connection-dot');
|
|
|
const text = statusEl.querySelector('.connection-text');
|
|
|
|
|
|
if (connected) {
|
|
|
if (dot) dot.className = 'connection-dot connected';
|
|
|
if (text) text.textContent = 'Connected';
|
|
|
statusEl.classList.remove('disconnected');
|
|
|
statusEl.classList.add('connected');
|
|
|
} else {
|
|
|
if (dot) dot.className = 'connection-dot disconnected';
|
|
|
if (text) text.textContent = 'Disconnected';
|
|
|
statusEl.classList.remove('connected');
|
|
|
statusEl.classList.add('disconnected');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
setupEventListeners() {
|
|
|
|
|
|
const refreshBtn = document.getElementById('refresh-btn');
|
|
|
if (refreshBtn) {
|
|
|
refreshBtn.addEventListener('click', () => {
|
|
|
console.log('[SystemMonitor] Manual refresh triggered');
|
|
|
refreshBtn.disabled = true;
|
|
|
refreshBtn.style.opacity = '0.6';
|
|
|
this.fetchSystemStatus().finally(() => {
|
|
|
setTimeout(() => {
|
|
|
refreshBtn.disabled = false;
|
|
|
refreshBtn.style.opacity = '1';
|
|
|
}, 1000);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
if (document.hidden) {
|
|
|
|
|
|
if (this.updateInterval) {
|
|
|
clearInterval(this.updateInterval);
|
|
|
}
|
|
|
if (this.animationFrame) {
|
|
|
cancelAnimationFrame(this.animationFrame);
|
|
|
this.animationFrame = null;
|
|
|
}
|
|
|
} else {
|
|
|
|
|
|
this.startPolling();
|
|
|
if (!this.animationFrame) {
|
|
|
this.startAnimation();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
showToast(message, type = 'info') {
|
|
|
const toastContainer = document.getElementById('toast-container');
|
|
|
if (!toastContainer) return;
|
|
|
|
|
|
const toast = document.createElement('div');
|
|
|
toast.className = `toast toast-${type}`;
|
|
|
toast.innerHTML = `
|
|
|
<div class="toast-content">
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
${type === 'error' ? '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>' : ''}
|
|
|
${type === 'success' ? '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>' : ''}
|
|
|
${type === 'warning' ? '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>' : ''}
|
|
|
${type === 'info' ? '<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>' : ''}
|
|
|
</svg>
|
|
|
<span>${message}</span>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
toastContainer.appendChild(toast);
|
|
|
|
|
|
|
|
|
setTimeout(() => toast.classList.add('show'), 10);
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
toast.classList.remove('show');
|
|
|
setTimeout(() => toast.remove(), 300);
|
|
|
}, 3000);
|
|
|
}
|
|
|
|
|
|
destroy() {
|
|
|
if (this.ws) {
|
|
|
this.ws.close();
|
|
|
}
|
|
|
if (this.updateInterval) {
|
|
|
clearInterval(this.updateInterval);
|
|
|
}
|
|
|
if (this.animationFrame) {
|
|
|
cancelAnimationFrame(this.animationFrame);
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
export default SystemMonitor;
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
window.SystemMonitor = SystemMonitor;
|
|
|
}
|
|
|
|
|
|
|