secubox-openwrt/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js
CyberMind-FR 8e53825ad5 release: v0.2.2 - Design System v0.3.0 & Comprehensive Documentation
🎨 Design System v0.3.0 (Demo-inspired)
- New dark palette: #0a0a0f, #6366f1→#8b5cf6 gradients
- Typography: Inter + JetBrains Mono
- Compact stats grid (130px min)
- Gradient text effects with background-clip
- Sticky navigation tabs
- Enhanced card borders and hover effects

📚 Comprehensive Documentation Suite
- DEVELOPMENT-GUIDELINES.md (33KB, 900+ lines)
  - 9 major sections: Design, Architecture, RPCD, ACL, JS, CSS, Errors, Validation, Deployment
  - Complete code templates and best practices
  - Common error diagnostics and solutions
- QUICK-START.md (6.4KB)
  - 8 critical rules for immediate reference
  - Quick code templates
  - Error quick fixes table
- deploy-module-template.sh (8.1KB)
  - Standardized deployment with automatic backup
  - Permission fixes, cache clearing, verification
- Updated CLAUDE.md, README.md with documentation index
- Updated .claude/README.md to v2.0

🔄 Version Updates
- luci-app-secubox: 0.1.2 → 0.2.2
- luci-app-system-hub: 0.1.1 → 0.2.2
- Updated all version strings (api.js, overview.js, CSS files)

🎯 CSS Enhancements
- common.css: Complete rewrite with demo palette
- overview.css: Dashboard header with gradient
- services.css: Updated version to 0.2.2
- components.css: Updated version to 0.2.2

🔧 Critical Rules Documented
1. RPCD naming: file = ubus object (luci. prefix)
2. Menu path = view file location
3. Permissions: 755 (RPCD), 644 (CSS/JS)
4. ALWAYS run validate-modules.sh
5. CSS variables only (no hardcode)
6. Dark mode mandatory
7. Typography: Inter + JetBrains Mono
8. Gradients: --sh-primary → --sh-primary-end

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 18:55:19 +01:00

346 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require dom';
'require ui';
'require poll';
'require system-hub/api as API';
'require system-hub/theme as Theme';
return view.extend({
healthData: null,
load: function() {
return Promise.all([
API.getHealth(),
Theme.getTheme()
]);
},
render: function(data) {
this.healthData = data[0] || {};
var theme = data[1];
var container = E('div', { 'class': 'system-hub-health' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }),
// Header with health score
this.renderHeader(),
// System Metrics Grid
E('div', { 'class': 'sh-metrics-grid' }, [
this.renderMetricCard('CPU', this.healthData.cpu),
this.renderMetricCard('Memory', this.healthData.memory),
this.renderMetricCard('Disk', this.healthData.disk),
this.renderMetricCard('Temperature', this.healthData.temperature)
]),
// Network & Services Info
E('div', { 'class': 'sh-info-grid' }, [
this.renderNetworkCard(),
this.renderServicesCard()
]),
// Recommendations
this.healthData.recommendations && this.healthData.recommendations.length > 0
? this.renderRecommendationsCard()
: E('div'),
// Actions
this.renderActionsCard()
]);
// Setup auto-refresh
poll.add(L.bind(function() {
return API.getHealth().then(L.bind(function(refreshData) {
this.healthData = refreshData || {};
this.updateDashboard();
}, this));
}, this), 30);
return container;
},
renderHeader: function() {
var score = this.healthData.score || 0;
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
var scoreLabel = score >= 80 ? 'Excellent' : (score >= 60 ? 'Bon' : (score >= 40 ? 'Attention' : 'Critique'));
return E('div', { 'class': 'sh-overview-header' }, [
E('div', { 'class': 'sh-overview-title' }, [
E('h2', {}, [
E('span', { 'class': 'sh-title-icon' }, '💚'),
' Health Monitor'
]),
E('p', { 'class': 'sh-overview-subtitle' },
'Surveillance en temps réel de la santé du système')
]),
E('div', { 'class': 'sh-overview-score' }, [
E('div', { 'class': 'sh-score-circle sh-score-' + scoreClass }, [
E('div', { 'class': 'sh-score-value' }, score),
E('div', { 'class': 'sh-score-label' }, scoreLabel)
])
])
]);
},
renderMetricCard: function(type, data) {
if (!data) return E('div');
var config = this.getMetricConfig(type, data);
return E('div', { 'class': 'sh-metric-card sh-metric-' + config.status }, [
E('div', { 'class': 'sh-metric-header' }, [
E('span', { 'class': 'sh-metric-icon' }, config.icon),
E('span', { 'class': 'sh-metric-title' }, config.title)
]),
E('div', { 'class': 'sh-metric-value' }, config.value),
E('div', { 'class': 'sh-metric-progress' }, [
E('div', {
'class': 'sh-metric-progress-bar',
'style': 'width: ' + config.percentage + '%; background: ' + config.color
})
]),
E('div', { 'class': 'sh-metric-details' }, config.details)
]);
},
getMetricConfig: function(type, data) {
switch(type) {
case 'CPU':
return {
icon: '🔥',
title: 'CPU Usage',
value: (data.usage || 0) + '%',
percentage: data.usage || 0,
status: data.status || 'ok',
color: this.getStatusColor(data.usage || 0),
details: 'Load: ' + (data.load_1m || '0') + ' ' + (data.cores || 0) + ' cores'
};
case 'Memory':
var usedMB = ((data.used_kb || 0) / 1024).toFixed(0);
var totalMB = ((data.total_kb || 0) / 1024).toFixed(0);
return {
icon: '💾',
title: 'Memory',
value: (data.usage || 0) + '%',
percentage: data.usage || 0,
status: data.status || 'ok',
color: this.getStatusColor(data.usage || 0),
details: usedMB + ' MB / ' + totalMB + ' MB used'
};
case 'Disk':
var usedGB = ((data.used_kb || 0) / 1024 / 1024).toFixed(1);
var totalGB = ((data.total_kb || 0) / 1024 / 1024).toFixed(1);
return {
icon: '💿',
title: 'Disk Space',
value: (data.usage || 0) + '%',
percentage: data.usage || 0,
status: data.status || 'ok',
color: this.getStatusColor(data.usage || 0),
details: usedGB + ' GB / ' + totalGB + ' GB used'
};
case 'Temperature':
return {
icon: '🌡',
title: 'Temperature',
value: (data.value || 0) + '°C',
percentage: Math.min((data.value || 0), 100),
status: data.status || 'ok',
color: this.getTempColor(data.value || 0),
details: 'Status: ' + (data.status || 'unknown')
};
default:
return {
icon: '📊',
title: type,
value: 'N/A',
percentage: 0,
status: 'unknown',
color: '#64748b',
details: 'No data'
};
}
},
getStatusColor: function(usage) {
if (usage >= 90) return '#ef4444';
if (usage >= 75) return '#f59e0b';
if (usage >= 50) return '#3b82f6';
return '#22c55e';
},
getTempColor: function(temp) {
if (temp >= 80) return '#ef4444';
if (temp >= 70) return '#f59e0b';
if (temp >= 60) return '#3b82f6';
return '#22c55e';
},
renderNetworkCard: function() {
var wan_status = this.healthData.network ? this.healthData.network.wan_up : false;
return E('div', { 'class': 'sh-info-card' }, [
E('div', { 'class': 'sh-info-card-header' }, [
E('h3', {}, [
E('span', { 'style': 'margin-right: 8px;' }, '🌐'),
'Network Status'
])
]),
E('div', { 'class': 'sh-info-card-body' }, [
E('div', { 'class': 'sh-info-list' }, [
this.renderInfoRow('📡', 'WAN Connection',
E('span', {
'class': 'sh-status-badge sh-status-' + (wan_status ? 'ok' : 'error')
}, wan_status ? ' Connected' : ' Disconnected')
),
this.renderInfoRow('🔌', 'Network Mode',
this.healthData.network ? this.healthData.network.status : 'unknown'
)
])
])
]);
},
renderServicesCard: function() {
var running = this.healthData.services ? this.healthData.services.running : 0;
var failed = this.healthData.services ? this.healthData.services.failed : 0;
var total = running + failed;
return E('div', { 'class': 'sh-info-card' }, [
E('div', { 'class': 'sh-info-card-header' }, [
E('h3', {}, [
E('span', { 'style': 'margin-right: 8px;' }, ''),
'System Services'
])
]),
E('div', { 'class': 'sh-info-card-body' }, [
E('div', { 'class': 'sh-info-list' }, [
this.renderInfoRow('', 'Running',
E('span', { 'class': 'sh-status-badge sh-status-ok' }, running + ' services')
),
this.renderInfoRow('', 'Failed',
failed > 0
? E('span', { 'class': 'sh-status-badge sh-status-error' }, failed + ' services')
: E('span', { 'class': 'sh-status-badge sh-status-ok' }, 'None')
),
this.renderInfoRow('📊', 'Health Rate',
total > 0 ? ((running / total * 100).toFixed(0) + '%') : 'N/A'
)
])
])
]);
},
renderRecommendationsCard: function() {
return E('div', { 'class': 'sh-card' }, [
E('div', { 'class': 'sh-card-header' }, [
E('h3', { 'class': 'sh-card-title' }, [
E('span', { 'class': 'sh-card-title-icon' }, '💡'),
'Recommendations'
]),
E('div', { 'class': 'sh-card-badge', 'style': 'background: #f59e0b;' },
this.healthData.recommendations.length)
]),
E('div', { 'class': 'sh-card-body' },
this.healthData.recommendations.map(function(rec) {
return E('div', {
'style': 'display: flex; gap: 12px; align-items: flex-start; padding: 14px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-bottom: 10px;'
}, [
E('span', { 'style': 'font-size: 24px;' }, ''),
E('div', { 'style': 'flex: 1; color: var(--sh-text-primary);' }, rec)
]);
})
)
]);
},
renderActionsCard: function() {
return E('div', { 'class': 'sh-card' }, [
E('div', { 'class': 'sh-card-header' }, [
E('h3', { 'class': 'sh-card-title' }, [
E('span', { 'class': 'sh-card-title-icon' }, '🛠'),
'Actions'
])
]),
E('div', { 'class': 'sh-card-body' }, [
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
E('button', {
'class': 'sh-btn sh-btn-primary',
'click': L.bind(this.generateReport, this)
}, [
E('span', {}, '📋'),
E('span', {}, 'Generate Report')
]),
E('button', {
'class': 'sh-btn sh-btn-secondary',
'click': function() {
ui.addNotification(null, E('p', ' Email feature coming soon'), 'info');
}
}, [
E('span', {}, '📧'),
E('span', {}, 'Send Email')
]),
E('button', {
'class': 'sh-btn sh-btn-secondary',
'click': function() {
ui.addNotification(null, E('p', ' PDF export coming soon'), 'info');
}
}, [
E('span', {}, '📥'),
E('span', {}, 'Download PDF')
])
])
])
]);
},
renderInfoRow: function(icon, label, value) {
return E('div', { 'class': 'sh-info-row' }, [
E('span', { 'class': 'sh-info-icon' }, icon),
E('span', { 'class': 'sh-info-label' }, label),
E('span', { 'class': 'sh-info-value' }, value)
]);
},
updateDashboard: function() {
var metricsGrid = document.querySelector('.sh-metrics-grid');
if (metricsGrid) {
dom.content(metricsGrid, [
this.renderMetricCard('CPU', this.healthData.cpu),
this.renderMetricCard('Memory', this.healthData.memory),
this.renderMetricCard('Disk', this.healthData.disk),
this.renderMetricCard('Temperature', this.healthData.temperature)
]);
}
// Update health score
var scoreValue = document.querySelector('.sh-score-value');
var scoreCircle = document.querySelector('.sh-score-circle');
if (scoreValue && scoreCircle) {
var score = this.healthData.score || 0;
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
scoreValue.textContent = score;
scoreCircle.className = 'sh-score-circle sh-score-' + scoreClass;
}
},
generateReport: function() {
ui.showModal(_('Generating Report'), [
E('p', {}, 'Generating health report...'),
E('div', { 'class': 'spinning' })
]);
setTimeout(function() {
ui.hideModal();
ui.addNotification(null, E('p', ' Report generation feature coming soon'), 'info');
}, 1000);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});