/**
* Status Drawer - Slide-out panel from right side
* Shows ONLY: Resources, Endpoints, Providers status
* Real-time updates, NO CPU/Memory stats
*/
class StatusDrawer {
constructor(options = {}) {
this.options = {
apiEndpoint: options.apiEndpoint || '/api/system/status',
updateInterval: options.updateInterval || 3000, // 3 seconds
...options
};
this.isOpen = false;
this.pollTimer = null;
this.lastData = null;
this.drawerElement = null;
this.buttonElement = null;
this.createDrawer();
this.createFloatingButton();
}
/**
* Create floating button
*/
createFloatingButton() {
const button = document.createElement('button');
button.id = 'status-drawer-btn';
button.className = 'status-drawer-floating-btn';
button.setAttribute('aria-label', 'Open status panel');
button.innerHTML = `
`;
button.addEventListener('click', () => this.toggle());
document.body.appendChild(button);
this.buttonElement = button;
}
/**
* Create drawer panel - ENHANCED with detailed provider metrics
*/
createDrawer() {
const drawer = document.createElement('div');
drawer.id = 'status-drawer';
drawer.className = 'status-drawer status-drawer-enhanced';
drawer.innerHTML = `
`;
document.body.appendChild(drawer);
this.drawerElement = drawer;
// Close button
drawer.querySelector('.drawer-close').addEventListener('click', () => this.close());
// Refresh button
drawer.querySelector('#refresh-status').addEventListener('click', () => this.fetchStatus());
// Collapsible sections
drawer.querySelectorAll('.section-title.collapsible').forEach(title => {
title.addEventListener('click', (e) => {
const target = title.dataset.target;
const content = document.getElementById(target);
if (content) {
content.classList.toggle('collapsed');
title.classList.toggle('collapsed');
}
});
});
}
/**
* Toggle drawer
*/
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
/**
* Open drawer
*/
open() {
if (this.isOpen) return;
this.isOpen = true;
this.drawerElement.classList.add('open');
this.buttonElement.classList.add('hidden');
// Start polling
this.startPolling();
}
/**
* Close drawer
*/
close() {
if (!this.isOpen) return;
this.isOpen = false;
this.drawerElement.classList.remove('open');
this.buttonElement.classList.remove('hidden');
// Stop polling
this.stopPolling();
}
/**
* Start polling (only when open)
*/
startPolling() {
if (!this.isOpen) return;
this.fetchStatus();
this.pollTimer = setTimeout(() => this.startPolling(), this.options.updateInterval);
}
/**
* Stop polling
*/
stopPolling() {
if (this.pollTimer) {
clearTimeout(this.pollTimer);
this.pollTimer = null;
}
}
/**
* Fetch status from API
*/
async fetchStatus() {
if (!this.isOpen) return;
try {
const response = await fetch(this.options.apiEndpoint);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
this.updateUI(data);
} catch (error) {
console.error('Status Drawer: Failed to fetch:', error);
this.showError();
}
}
/**
* Update UI with data - ENHANCED
*/
updateUI(data) {
this.lastData = data;
// Update all providers with detailed metrics
this.updateProvidersDetailed(data.providers_detailed || data.services || []);
// Update AI models
this.updateAIModels(data.ai_models || {});
// Update infrastructure
this.updateInfrastructure(data.infrastructure || {});
// Update resource breakdown
this.updateResourceBreakdown(data.resource_breakdown || {});
// Update error details
this.updateErrorDetails(data.error_details || []);
// Update performance
this.updatePerformance(data.performance || {});
// Update timestamp
this.updateTimestamp(data.timestamp);
}
/**
* Update providers with detailed metrics
*/
updateProvidersDetailed(providers) {
const container = document.getElementById('providers-list');
if (!container) return;
if (!providers.length) {
container.innerHTML = 'No providers configured
';
return;
}
container.innerHTML = providers.map(provider => {
const isOnline = provider.status === 'online' || provider.status === 'active';
const statusEmoji = isOnline ? '🟢' :
provider.status === 'rate_limited' ? '🔴' :
provider.status === 'degraded' ? '🟡' : '⚫';
let statusText = '';
if (isOnline) {
statusText = `${provider.response_time_ms || 0}ms | Success: ${provider.success_rate || 100}%`;
if (provider.last_check) {
const elapsed = Math.floor((Date.now() / 1000) - new Date(provider.last_check).getTime() / 1000);
statusText += ` | Last: ${elapsed}s ago`;
}
} else if (provider.status === 'rate_limited') {
statusText = `Rate Limited (${provider.status_code || 429})`;
if (provider.cached_until) {
statusText += ` | Cached ${provider.cached_until}`;
}
} else if (provider.status === 'degraded') {
statusText = provider.error || 'Degraded performance';
} else {
statusText = provider.error || 'Offline';
}
const resourceInfo = provider.resource_count ? ` | ${provider.resource_count} resources` : '';
return `
${statusEmoji}
${provider.name}
${statusText}${resourceInfo}
`;
}).join('');
}
/**
* Update AI models section
*/
updateAIModels(aiModels) {
const container = document.getElementById('ai-models-list');
if (!container) return;
const transformersStatus = aiModels.transformers_loaded ? '🟢 Loaded (CPU mode)' : '🔴 Not loaded';
const sentimentModels = aiModels.sentiment_models || 0;
const hfApiStatus = aiModels.hf_api_active ? '🟢 Active' : '🔴 Inactive';
container.innerHTML = `
Transformers:
${transformersStatus}
Sentiment Models:
${sentimentModels} available
HuggingFace API:
${hfApiStatus}
`;
}
/**
* Update infrastructure section
*/
updateInfrastructure(infrastructure) {
const container = document.getElementById('infrastructure-list');
if (!container) return;
const dbStatus = infrastructure.database_status || 'unknown';
const dbEntries = infrastructure.database_entries || 0;
const workerStatus = infrastructure.background_worker || 'unknown';
const workerNextRun = infrastructure.worker_next_run || 'N/A';
const wsStatus = infrastructure.websocket_active ? '🟢 Active' : '⚫ Inactive';
container.innerHTML = `
Database:
${dbStatus === 'online' ? '🟢' : '🔴'} SQLite (${dbEntries} cached)
Background Worker:
${workerStatus === 'active' ? '🟢' : '⚫'} ${workerNextRun}
WebSocket:
${wsStatus}
`;
}
/**
* Update resource breakdown section
*/
updateResourceBreakdown(breakdown) {
const container = document.getElementById('resources-breakdown');
if (!container) return;
const total = breakdown.total || 0;
const bySource = breakdown.by_source || {};
const byCategory = breakdown.by_category || {};
let sourceHTML = '';
for (const [source, count] of Object.entries(bySource)) {
sourceHTML += `
${source}:
${count}
`;
}
let categoryHTML = '';
for (const [category, count] of Object.entries(byCategory)) {
categoryHTML += `
${category}:
${count} online
`;
}
container.innerHTML = `
Total: ${total}+ resources
${sourceHTML}
By Category:
${categoryHTML}
`;
}
/**
* Update error details section
*/
updateErrorDetails(errors) {
const container = document.getElementById('error-list');
if (!container) return;
if (!errors || errors.length === 0) {
container.innerHTML = 'No recent errors
';
return;
}
container.innerHTML = errors.map(error => `
${error.provider || 'Unknown'}: ${error.count || 1}x ${error.type || 'error'}
${error.message || 'Unknown error'}
${error.action ? `
Action: ${error.action}
` : ''}
`).join('');
}
/**
* Update performance section
*/
updatePerformance(performance) {
const container = document.getElementById('performance-metrics');
if (!container) return;
const avgResponse = performance.avg_response_ms || 0;
const fastest = performance.fastest_provider || 'N/A';
const fastestTime = performance.fastest_time_ms || 0;
const cacheHit = performance.cache_hit_rate || 0;
container.innerHTML = `
Avg Response:
${avgResponse}ms
Fastest:
${fastest} (${fastestTime}ms)
Cache Hit:
${cacheHit}%
`;
}
/**
* Update endpoints
*/
updateEndpoints(endpoints) {
const container = document.getElementById('endpoints-status');
if (!container) return;
if (!endpoints.length) {
container.innerHTML = 'No endpoints
';
return;
}
container.innerHTML = endpoints.map(endpoint => {
const statusClass = endpoint.status === 'online' ? 'status-online' : 'status-offline';
return `
${endpoint.path}
${endpoint.avg_response_ms ? `${endpoint.avg_response_ms.toFixed(0)}ms` : '--'} •
${endpoint.success_rate ? `${endpoint.success_rate.toFixed(1)}%` : '--'}
`;
}).join('');
}
/**
* Update providers
*/
updateProviders(services) {
const container = document.getElementById('providers-status');
if (!container) return;
if (!services.length) {
container.innerHTML = 'No providers
';
return;
}
container.innerHTML = services.map(service => {
const statusClass = service.status === 'online' ? 'status-online' : 'status-offline';
return `
${service.name}
${service.response_time_ms ? `${service.response_time_ms.toFixed(0)}ms` : 'Offline'}
`;
}).join('');
}
/**
* Update coins
*/
updateCoins(coins) {
const container = document.getElementById('coins-status');
if (!container) return;
if (!coins.length) {
container.innerHTML = 'No coins
';
return;
}
container.innerHTML = coins.map(coin => {
const statusClass = coin.status === 'online' ? 'status-online' : 'status-offline';
return `
${coin.symbol}
${coin.price ? `$${coin.price.toLocaleString()}` : 'Unavailable'}
`;
}).join('');
}
/**
* Update timestamp
*/
updateTimestamp(timestamp) {
const element = document.getElementById('last-update-time');
if (element) {
const date = new Date(timestamp * 1000);
element.textContent = date.toLocaleTimeString();
}
}
/**
* Show error state
*/
showError() {
const sections = ['resources-summary', 'endpoints-status', 'providers-status', 'coins-status'];
sections.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.innerHTML = 'Failed to load
';
}
});
}
/**
* Destroy drawer
*/
destroy() {
this.stopPolling();
if (this.drawerElement) this.drawerElement.remove();
if (this.buttonElement) this.buttonElement.remove();
}
}
// Export
if (typeof window !== 'undefined') {
window.StatusDrawer = StatusDrawer;
}