/**
* 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();
if (this.options.showApiHelpButton) {
this.createApiHelpButton();
}
}
createFloatingButton() {
const wrap = document.createElement('div');
wrap.id = 'fab-stack';
wrap.className = 'fab-stack';
const button = document.createElement('button');
button.id = 'status-drawer-btn';
button.className = 'status-drawer-floating-btn';
button.title = 'وضعیت سیستم | System Status';
button.setAttribute('aria-label', 'Open status panel');
button.innerHTML = `
`;
button.addEventListener('click', () => this.toggle());
wrap.appendChild(button);
document.body.appendChild(wrap);
this.buttonElement = button;
this.fabStack = wrap;
}
createApiHelpButton() {
const helpBtn = document.createElement('button');
helpBtn.id = 'api-help-fab';
helpBtn.className = 'status-drawer-floating-btn api-help-fab';
helpBtn.title = 'راهنمای API | API Help';
helpBtn.setAttribute('aria-label', 'Open API help guide');
helpBtn.innerHTML = `
`;
helpBtn.addEventListener('click', async () => {
try {
const { ConfigHelperModal } = await import('/static/shared/components/config-helper-modal.js?v=4.1');
if (!window._configHelperModal) window._configHelperModal = new ConfigHelperModal();
window._configHelperModal.show();
} catch (e) {
console.error('API help failed:', e);
}
});
(this.fabStack || document.body).appendChild(helpBtn);
this.helpButtonElement = helpBtn;
}
/**
* 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.fabStack?.classList.add('hidden');
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.fabStack?.classList.remove('hidden');
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 overviewUrl = this.options.overviewEndpoint || '/api/dashboard/overview';
const [statusRes, overviewRes] = await Promise.allSettled([
fetch(this.options.apiEndpoint),
fetch(overviewUrl),
]);
if (statusRes.status !== 'fulfilled' || !statusRes.value.ok) {
throw new Error(`HTTP ${statusRes.value?.status || 'error'}`);
}
const data = await statusRes.value.json();
if (overviewRes.status === 'fulfilled' && overviewRes.value.ok) {
data._overview = await overviewRes.value.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 || {});
this.updateApiKeys(data.api_keys || {});
this.updateRotationSummary(data.rotation || data._overview?.rotation || {});
// Update timestamp
this.updateTimestamp(data.timestamp);
}
updateApiKeys(apiKeys) {
const container = document.getElementById('api-keys-list');
if (!container) return;
const env = apiKeys.env_configured || [];
const display = apiKeys.display || `${apiKeys.env_secrets || 0} secrets + ${apiKeys.file_keys || 0} file`;
container.innerHTML = `
Ready:
🟢 ${apiKeys.configured_total || 0} total
Breakdown:
${display}
Env secrets:
${env.length ? env.join(', ') : 'none'}
API: GET /api/resources/rotation/health
`;
}
updateRotationSummary(rotation) {
const container = document.getElementById('rotation-summary');
if (!container) return;
container.innerHTML = `
Pools:
🟢 ${rotation.pools_healthy || 0} / ${rotation.pools_total || 0}
Providers:
${rotation.providers_available || 0} / ${rotation.providers_total || 0}
`;
}
/**
* 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 (provider.resource_count) {
statusText = `${provider.resource_count} providers | ${provider.success_rate || 100}% avail`;
} else 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 overview = this.lastData?._overview?.cards || {};
const v2 = overview.models_loaded_v2 ?? aiModels.models_loaded_v2 ?? aiModels.sentiment_models ?? 0;
const v4 = overview.models_loaded_v4 ?? aiModels.models_loaded_v4 ?? 0;
const total = overview.models_total ?? aiModels.models_total ?? (v2 + v4);
const catV2 = aiModels.catalog_v2 || overview.models_total_v2 || 45;
const catV4 = aiModels.catalog_v4 || overview.models_total_v4 || 18;
const hfMode = overview.hf_mode || aiModels.hf_mode || 'unknown';
const v4Mode = overview.v4_hf_mode || aiModels.v4_hf_mode || 'complement';
container.innerHTML = `
v2 Models:
🟢 ${v2} loaded
v4 Complement:
🟢 ${v4} loaded (${v4Mode})
Catalog:
${catV2} v2 + ${catV4} v4 = ${catV2 + catV4}
Loaded:
${v2 + v4} / ${total}
HF Mode:
${hfMode}
API: GET /api/models/summary
`;
}
/**
* 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;
}