secubox-openwrt/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.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

315 lines
8.6 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require dom';
'require system-hub/api as API';
'require system-hub/theme as Theme';
return view.extend({
services: [],
currentFilter: 'all',
searchQuery: '',
load: function() {
return Promise.all([
API.listServices(),
Theme.getTheme()
]);
},
render: function(data) {
this.services = data[0] || [];
var theme = data[1];
var container = E('div', { 'class': 'system-hub-services' }, [
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/services.css') }),
// Header with stats
this.renderHeader(),
// Filter tabs
this.renderFilterTabs(),
// Search box
this.renderSearchBox(),
// Services grid
E('div', { 'class': 'sh-services-grid', 'id': 'services-grid' })
]);
// Initial render
this.updateServicesGrid();
return container;
},
renderHeader: function() {
var stats = this.getStats();
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
'System Services'
]),
E('p', { 'class': 'sh-page-subtitle' },
'Manage system services: start, stop, restart, enable, or disable')
]),
E('div', { 'class': 'sh-stats-grid' }, [
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value', 'style': 'color: #22c55e;' }, stats.running),
E('div', { 'class': 'sh-stat-label' }, 'Running')
]),
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value', 'style': 'color: #ef4444;' }, stats.stopped),
E('div', { 'class': 'sh-stat-label' }, 'Stopped')
]),
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value', 'style': 'color: #6366f1;' }, stats.enabled),
E('div', { 'class': 'sh-stat-label' }, 'Enabled')
]),
E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stats.total),
E('div', { 'class': 'sh-stat-label' }, 'Total')
])
])
]);
},
renderFilterTabs: function() {
var self = this;
var stats = this.getStats();
return E('div', { 'class': 'sh-filter-tabs' }, [
this.createFilterTab('all', '📋', 'All Services', stats.total),
this.createFilterTab('running', '▶️', 'Running', stats.running),
this.createFilterTab('stopped', '⏹️', 'Stopped', stats.stopped),
this.createFilterTab('enabled', '✓', 'Enabled', stats.enabled),
this.createFilterTab('disabled', '✗', 'Disabled', stats.disabled)
]);
},
createFilterTab: function(filter, icon, label, count) {
var self = this;
var isActive = this.currentFilter === filter;
return E('div', {
'class': 'sh-filter-tab' + (isActive ? ' active' : ''),
'click': function() {
self.currentFilter = filter;
self.updateFilterTabs();
self.updateServicesGrid();
}
}, [
E('span', { 'class': 'sh-tab-icon' }, icon),
E('span', { 'class': 'sh-tab-label' }, label + ' (' + count + ')')
]);
},
renderSearchBox: function() {
var self = this;
return E('div', { 'style': 'margin-bottom: 24px;' }, [
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': '🔍 Search services...',
'style': 'width: 100%; padding: 12px 16px; border-radius: 8px; border: 1px solid var(--sh-border); background: var(--sh-bg-card); color: var(--sh-text-primary); font-size: 14px;',
'input': function(ev) {
self.searchQuery = ev.target.value.toLowerCase();
self.updateServicesGrid();
}
})
]);
},
updateFilterTabs: function() {
var tabs = document.querySelectorAll('.sh-filter-tab');
tabs.forEach(function(tab, index) {
var filters = ['all', 'running', 'stopped', 'enabled', 'disabled'];
if (filters[index] === this.currentFilter) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
}.bind(this));
},
updateServicesGrid: function() {
var grid = document.getElementById('services-grid');
if (!grid) return;
var filtered = this.getFilteredServices();
if (filtered.length === 0) {
dom.content(grid, [
E('div', { 'class': 'sh-empty-state' }, [
E('div', { 'class': 'sh-empty-icon' }, '📭'),
E('div', { 'class': 'sh-empty-text' },
this.searchQuery ? 'No services match your search' : 'No services found')
])
]);
return;
}
dom.content(grid, filtered.map(this.renderServiceCard, this));
},
getFilteredServices: function() {
return this.services.filter(function(service) {
// Apply filter
var matchesFilter = true;
switch (this.currentFilter) {
case 'running':
matchesFilter = service.running;
break;
case 'stopped':
matchesFilter = !service.running;
break;
case 'enabled':
matchesFilter = service.enabled;
break;
case 'disabled':
matchesFilter = !service.enabled;
break;
}
// Apply search
var matchesSearch = !this.searchQuery ||
service.name.toLowerCase().includes(this.searchQuery);
return matchesFilter && matchesSearch;
}.bind(this));
},
renderServiceCard: function(service) {
var statusClass = service.running ? 'ok' : 'error';
var statusIcon = service.running ? '▶️' : '⏹️';
var statusText = service.running ? 'Running' : 'Stopped';
var enabledIcon = service.enabled ? '✓' : '✗';
var enabledText = service.enabled ? 'Enabled' : 'Disabled';
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' }, '⚙️'),
service.name
]),
E('div', { 'class': 'sh-card-badge sh-status-badge sh-status-' + statusClass }, [
statusIcon + ' ' + statusText
])
]),
E('div', { 'class': 'sh-card-body' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 16px;' }, [
E('span', { 'style': 'font-size: 16px;' }, enabledIcon),
E('span', { 'style': 'font-weight: 600; color: var(--sh-text-secondary);' },
'Autostart: ' + enabledText)
]),
E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' },
this.renderActionButtons(service)
)
])
]);
},
renderActionButtons: function(service) {
var buttons = [];
// Start button (only if stopped)
if (!service.running) {
buttons.push(E('button', {
'class': 'sh-btn sh-btn-success',
'click': L.bind(this.performAction, this, service.name, 'start')
}, [
E('span', {}, '▶️'),
E('span', {}, 'Start')
]));
}
// Stop button (only if running)
if (service.running) {
buttons.push(E('button', {
'class': 'sh-btn sh-btn-danger',
'click': L.bind(this.performAction, this, service.name, 'stop')
}, [
E('span', {}, '⏹️'),
E('span', {}, 'Stop')
]));
}
// Restart button
buttons.push(E('button', {
'class': 'sh-btn sh-btn-warning',
'click': L.bind(this.performAction, this, service.name, 'restart')
}, [
E('span', {}, '🔄'),
E('span', {}, 'Restart')
]));
// Enable/Disable button
if (service.enabled) {
buttons.push(E('button', {
'class': 'sh-btn sh-btn-secondary',
'click': L.bind(this.performAction, this, service.name, 'disable')
}, [
E('span', {}, '✗'),
E('span', {}, 'Disable')
]));
} else {
buttons.push(E('button', {
'class': 'sh-btn sh-btn-primary',
'click': L.bind(this.performAction, this, service.name, 'enable')
}, [
E('span', {}, '✓'),
E('span', {}, 'Enable')
]));
}
return buttons;
},
getStats: function() {
var stats = {
total: this.services.length,
running: 0,
stopped: 0,
enabled: 0,
disabled: 0
};
this.services.forEach(function(service) {
if (service.running) stats.running++;
else stats.stopped++;
if (service.enabled) stats.enabled++;
else stats.disabled++;
});
return stats;
},
performAction: function(service, action) {
ui.showModal(_('Performing Action'), [
E('p', { 'class': 'spinning' }, _('Executing %s on service %s...').format(action, service))
]);
API.serviceAction(service, action).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + result.message), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Action failed: ') + err.message), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});