Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
309
ui/components/DashboardTab.js
Normal file
309
ui/components/DashboardTab.js
Normal file
@@ -0,0 +1,309 @@
|
||||
// Dashboard Tab Component
|
||||
|
||||
import { healthService } from '../services/health.service.js';
|
||||
import { poseService } from '../services/pose.service.js';
|
||||
|
||||
export class DashboardTab {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement;
|
||||
this.statsElements = {};
|
||||
this.healthSubscription = null;
|
||||
this.statsInterval = null;
|
||||
}
|
||||
|
||||
// Initialize component
|
||||
async init() {
|
||||
this.cacheElements();
|
||||
await this.loadInitialData();
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
// Cache DOM elements
|
||||
cacheElements() {
|
||||
// System stats
|
||||
const statsContainer = this.container.querySelector('.system-stats');
|
||||
if (statsContainer) {
|
||||
this.statsElements = {
|
||||
bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'),
|
||||
samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'),
|
||||
accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'),
|
||||
hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value')
|
||||
};
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
this.statusElements = {
|
||||
apiStatus: this.container.querySelector('.api-status'),
|
||||
streamStatus: this.container.querySelector('.stream-status'),
|
||||
hardwareStatus: this.container.querySelector('.hardware-status')
|
||||
};
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
async loadInitialData() {
|
||||
try {
|
||||
// Get API info
|
||||
const info = await healthService.getApiInfo();
|
||||
this.updateApiInfo(info);
|
||||
|
||||
// Get current stats
|
||||
const stats = await poseService.getStats(1);
|
||||
this.updateStats(stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
this.showError('Failed to load dashboard data');
|
||||
}
|
||||
}
|
||||
|
||||
// Start monitoring
|
||||
startMonitoring() {
|
||||
// Subscribe to health updates
|
||||
this.healthSubscription = healthService.subscribeToHealth(health => {
|
||||
this.updateHealthStatus(health);
|
||||
});
|
||||
|
||||
// Start periodic stats updates
|
||||
this.statsInterval = setInterval(() => {
|
||||
this.updateLiveStats();
|
||||
}, 5000);
|
||||
|
||||
// Start health monitoring
|
||||
healthService.startHealthMonitoring(30000);
|
||||
}
|
||||
|
||||
// Update API info display
|
||||
updateApiInfo(info) {
|
||||
// Update version
|
||||
const versionElement = this.container.querySelector('.api-version');
|
||||
if (versionElement && info.version) {
|
||||
versionElement.textContent = `v${info.version}`;
|
||||
}
|
||||
|
||||
// Update environment
|
||||
const envElement = this.container.querySelector('.api-environment');
|
||||
if (envElement && info.environment) {
|
||||
envElement.textContent = info.environment;
|
||||
envElement.className = `api-environment env-${info.environment}`;
|
||||
}
|
||||
|
||||
// Update features status
|
||||
if (info.features) {
|
||||
this.updateFeatures(info.features);
|
||||
}
|
||||
}
|
||||
|
||||
// Update features display
|
||||
updateFeatures(features) {
|
||||
const featuresContainer = this.container.querySelector('.features-status');
|
||||
if (!featuresContainer) return;
|
||||
|
||||
featuresContainer.innerHTML = '';
|
||||
|
||||
Object.entries(features).forEach(([feature, enabled]) => {
|
||||
const featureElement = document.createElement('div');
|
||||
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
|
||||
featureElement.innerHTML = `
|
||||
<span class="feature-name">${this.formatFeatureName(feature)}</span>
|
||||
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
|
||||
`;
|
||||
featuresContainer.appendChild(featureElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update health status
|
||||
updateHealthStatus(health) {
|
||||
if (!health) return;
|
||||
|
||||
// Update overall status
|
||||
const overallStatus = this.container.querySelector('.overall-health');
|
||||
if (overallStatus) {
|
||||
overallStatus.className = `overall-health status-${health.status}`;
|
||||
overallStatus.textContent = health.status.toUpperCase();
|
||||
}
|
||||
|
||||
// Update component statuses
|
||||
if (health.components) {
|
||||
Object.entries(health.components).forEach(([component, status]) => {
|
||||
this.updateComponentStatus(component, status);
|
||||
});
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
if (health.metrics) {
|
||||
this.updateSystemMetrics(health.metrics);
|
||||
}
|
||||
}
|
||||
|
||||
// Update component status
|
||||
updateComponentStatus(component, status) {
|
||||
const element = this.container.querySelector(`[data-component="${component}"]`);
|
||||
if (element) {
|
||||
element.className = `component-status status-${status.status}`;
|
||||
element.querySelector('.status-text').textContent = status.status;
|
||||
|
||||
if (status.message) {
|
||||
element.querySelector('.status-message').textContent = status.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update system metrics
|
||||
updateSystemMetrics(metrics) {
|
||||
// CPU usage
|
||||
const cpuElement = this.container.querySelector('.cpu-usage');
|
||||
if (cpuElement && metrics.cpu_percent !== undefined) {
|
||||
cpuElement.textContent = `${metrics.cpu_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('cpu', metrics.cpu_percent);
|
||||
}
|
||||
|
||||
// Memory usage
|
||||
const memoryElement = this.container.querySelector('.memory-usage');
|
||||
if (memoryElement && metrics.memory_percent !== undefined) {
|
||||
memoryElement.textContent = `${metrics.memory_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('memory', metrics.memory_percent);
|
||||
}
|
||||
|
||||
// Disk usage
|
||||
const diskElement = this.container.querySelector('.disk-usage');
|
||||
if (diskElement && metrics.disk_percent !== undefined) {
|
||||
diskElement.textContent = `${metrics.disk_percent.toFixed(1)}%`;
|
||||
this.updateProgressBar('disk', metrics.disk_percent);
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
updateProgressBar(type, percent) {
|
||||
const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`);
|
||||
if (progressBar) {
|
||||
const fill = progressBar.querySelector('.progress-fill');
|
||||
if (fill) {
|
||||
fill.style.width = `${percent}%`;
|
||||
fill.className = `progress-fill ${this.getProgressClass(percent)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get progress class based on percentage
|
||||
getProgressClass(percent) {
|
||||
if (percent >= 90) return 'critical';
|
||||
if (percent >= 75) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Update live statistics
|
||||
async updateLiveStats() {
|
||||
try {
|
||||
// Get current pose data
|
||||
const currentPose = await poseService.getCurrentPose();
|
||||
this.updatePoseStats(currentPose);
|
||||
|
||||
// Get zones summary
|
||||
const zonesSummary = await poseService.getZonesSummary();
|
||||
this.updateZonesDisplay(zonesSummary);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update live stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update pose statistics
|
||||
updatePoseStats(poseData) {
|
||||
if (!poseData) return;
|
||||
|
||||
// Update person count
|
||||
const personCount = this.container.querySelector('.person-count');
|
||||
if (personCount) {
|
||||
personCount.textContent = poseData.total_persons || 0;
|
||||
}
|
||||
|
||||
// Update average confidence
|
||||
const avgConfidence = this.container.querySelector('.avg-confidence');
|
||||
if (avgConfidence && poseData.persons) {
|
||||
const confidences = poseData.persons.map(p => p.confidence);
|
||||
const avg = confidences.length > 0
|
||||
? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1)
|
||||
: 0;
|
||||
avgConfidence.textContent = `${avg}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update zones display
|
||||
updateZonesDisplay(zonesSummary) {
|
||||
const zonesContainer = this.container.querySelector('.zones-summary');
|
||||
if (!zonesContainer || !zonesSummary) return;
|
||||
|
||||
zonesContainer.innerHTML = '';
|
||||
|
||||
Object.entries(zonesSummary.zones).forEach(([zoneId, data]) => {
|
||||
const zoneElement = document.createElement('div');
|
||||
zoneElement.className = 'zone-item';
|
||||
zoneElement.innerHTML = `
|
||||
<span class="zone-name">${data.name || zoneId}</span>
|
||||
<span class="zone-count">${data.person_count}</span>
|
||||
`;
|
||||
zonesContainer.appendChild(zoneElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
updateStats(stats) {
|
||||
if (!stats) return;
|
||||
|
||||
// Update detection count
|
||||
const detectionCount = this.container.querySelector('.detection-count');
|
||||
if (detectionCount && stats.total_detections !== undefined) {
|
||||
detectionCount.textContent = this.formatNumber(stats.total_detections);
|
||||
}
|
||||
|
||||
// Update accuracy if available
|
||||
if (this.statsElements.accuracy && stats.average_confidence !== undefined) {
|
||||
this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Format feature name
|
||||
formatFeatureName(name) {
|
||||
return name.replace(/_/g, ' ')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Format large numbers
|
||||
formatNumber(num) {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
showError(message) {
|
||||
const errorContainer = this.container.querySelector('.error-container');
|
||||
if (errorContainer) {
|
||||
errorContainer.textContent = message;
|
||||
errorContainer.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
errorContainer.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
dispose() {
|
||||
if (this.healthSubscription) {
|
||||
this.healthSubscription();
|
||||
}
|
||||
|
||||
if (this.statsInterval) {
|
||||
clearInterval(this.statsInterval);
|
||||
}
|
||||
|
||||
healthService.stopHealthMonitoring();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user