Convert 90+ LuCI view files from legacy cbi-button-* classes to KissTheme kiss-btn-* classes for consistent dark theme styling. Pattern conversions applied: - cbi-button-positive → kiss-btn-green - cbi-button-negative/remove → kiss-btn-red - cbi-button-apply → kiss-btn-cyan - cbi-button-action → kiss-btn-blue - cbi-button (plain) → kiss-btn Also replaced hardcoded colors (#080, #c00, #888, etc.) with CSS variables (--kiss-green, --kiss-red, --kiss-muted, etc.) for proper dark theme compatibility. Apps updated include: ai-gateway, auth-guardian, bandwidth-manager, cloner, config-advisor, crowdsec-dashboard, dns-provider, exposure, glances, haproxy, hexojs, iot-guard, jellyfin, ksm-manager, mac-guardian, magicmirror2, master-link, meshname-dns, metablogizer, metabolizer, mqtt-bridge, netdata-dashboard, picobrew, routes-status, secubox-admin, secubox-mirror, secubox-p2p, secubox-security-threats, service-registry, simplex, streamlit, system-hub, tor-shield, traffic-shaper, vhost-manager, vortex-dns, vortex-firewall, webradio, wireguard-dashboard, zigbee2mqtt, zkp, and more. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
503 lines
19 KiB
JavaScript
503 lines
19 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require ui';
|
||
'require dom';
|
||
'require poll';
|
||
'require system-hub/api as API';
|
||
'require secubox/kiss-theme';
|
||
|
||
return view.extend({
|
||
services: [],
|
||
serviceHealth: null,
|
||
activeFilter: 'all',
|
||
searchQuery: '',
|
||
healthCheckRunning: false,
|
||
|
||
load: function() {
|
||
return Promise.all([
|
||
API.listServices(),
|
||
API.getServiceHealth(false).catch(function() { return null; })
|
||
]);
|
||
},
|
||
|
||
render: function(data) {
|
||
this.services = this.normalizeServices(data[0]);
|
||
this.serviceHealth = data[1];
|
||
|
||
var self = this;
|
||
poll.add(function() {
|
||
return API.listServices().then(function(fresh) {
|
||
self.services = self.normalizeServices(fresh);
|
||
self.updateStats();
|
||
self.updateServicesGrid();
|
||
});
|
||
}, 30);
|
||
|
||
// Inject services-specific styles
|
||
this.injectStyles();
|
||
|
||
var content = [
|
||
this.renderHeader(),
|
||
this.renderHealthPanel(),
|
||
this.renderControls(),
|
||
E('div', { 'class': 'sh-services-grid', 'id': 'sh-services-grid' },
|
||
this.getFilteredServices().map(this.renderServiceCard, this))
|
||
];
|
||
|
||
return KissTheme.wrap(content, 'admin/secubox/system/system-hub/services');
|
||
},
|
||
|
||
renderHealthPanel: function() {
|
||
var self = this;
|
||
var health = this.serviceHealth;
|
||
var downServices = [];
|
||
var upCount = 0;
|
||
var downCount = 0;
|
||
|
||
if (health && health.services) {
|
||
health.services.forEach(function(svc) {
|
||
if (svc.s === 'down') {
|
||
downServices.push(svc);
|
||
downCount++;
|
||
} else {
|
||
upCount++;
|
||
}
|
||
});
|
||
}
|
||
|
||
var totalRoutes = upCount + downCount;
|
||
var healthPercent = totalRoutes > 0 ? Math.round((upCount / totalRoutes) * 100) : 0;
|
||
|
||
return E('div', { 'class': 'sh-health-panel', 'id': 'sh-health-panel' }, [
|
||
E('div', { 'class': 'sh-health-header' }, [
|
||
E('div', { 'class': 'sh-health-title' }, [
|
||
E('span', { 'class': 'sh-health-icon' }, '🔍'),
|
||
E('span', {}, _('HAProxy Routes Health'))
|
||
]),
|
||
E('div', { 'class': 'sh-health-actions' }, [
|
||
E('button', {
|
||
'class': 'sh-btn sh-btn-action',
|
||
'id': 'sh-health-refresh-btn',
|
||
'type': 'button',
|
||
'click': ui.createHandlerFn(this, 'refreshHealthCheck')
|
||
}, [
|
||
E('span', { 'class': 'sh-btn-icon' }, '↻'),
|
||
_('Refresh')
|
||
])
|
||
])
|
||
]),
|
||
E('div', { 'class': 'sh-health-stats' }, [
|
||
E('div', { 'class': 'sh-health-stat success' }, [
|
||
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-up' }, upCount.toString()),
|
||
E('span', { 'class': 'sh-health-stat-label' }, _('Up'))
|
||
]),
|
||
E('div', { 'class': 'sh-health-stat danger' }, [
|
||
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-down' }, downCount.toString()),
|
||
E('span', { 'class': 'sh-health-stat-label' }, _('Down'))
|
||
]),
|
||
E('div', { 'class': 'sh-health-stat' }, [
|
||
E('span', { 'class': 'sh-health-stat-value' }, totalRoutes.toString()),
|
||
E('span', { 'class': 'sh-health-stat-label' }, _('Total'))
|
||
]),
|
||
E('div', { 'class': 'sh-health-stat ' + (healthPercent >= 90 ? 'success' : healthPercent >= 70 ? 'warning' : 'danger') }, [
|
||
E('span', { 'class': 'sh-health-stat-value' }, healthPercent + '%'),
|
||
E('span', { 'class': 'sh-health-stat-label' }, _('Health'))
|
||
])
|
||
]),
|
||
downCount > 0 ? E('div', { 'class': 'sh-health-down-list', 'id': 'sh-health-down-list' }, [
|
||
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
|
||
E('div', { 'class': 'sh-health-down-items' },
|
||
downServices.slice(0, 10).map(function(svc) {
|
||
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
|
||
})
|
||
),
|
||
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
|
||
]) : E('div', { 'class': 'sh-health-all-ok' }, [
|
||
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
|
||
_('All routes are healthy')
|
||
])
|
||
]);
|
||
},
|
||
|
||
refreshHealthCheck: function() {
|
||
var self = this;
|
||
var btn = document.getElementById('sh-health-refresh-btn');
|
||
if (btn) btn.classList.add('spinning');
|
||
|
||
return API.getServiceHealth(true).then(function(health) {
|
||
self.serviceHealth = health;
|
||
self.updateHealthPanel();
|
||
if (btn) btn.classList.remove('spinning');
|
||
}).catch(function(err) {
|
||
if (btn) btn.classList.remove('spinning');
|
||
ui.addNotification(null, E('p', {}, _('Health check failed: ') + (err.message || err)), 'error');
|
||
});
|
||
},
|
||
|
||
updateHealthPanel: function() {
|
||
var panel = document.getElementById('sh-health-panel');
|
||
if (!panel) return;
|
||
|
||
var health = this.serviceHealth;
|
||
var upCount = 0;
|
||
var downCount = 0;
|
||
var downServices = [];
|
||
|
||
if (health && health.services) {
|
||
health.services.forEach(function(svc) {
|
||
if (svc.s === 'down') {
|
||
downServices.push(svc);
|
||
downCount++;
|
||
} else {
|
||
upCount++;
|
||
}
|
||
});
|
||
}
|
||
|
||
var upEl = document.getElementById('sh-health-up');
|
||
var downEl = document.getElementById('sh-health-down');
|
||
if (upEl) upEl.textContent = upCount.toString();
|
||
if (downEl) downEl.textContent = downCount.toString();
|
||
|
||
var downList = document.getElementById('sh-health-down-list');
|
||
if (downList) {
|
||
if (downCount > 0) {
|
||
dom.content(downList, [
|
||
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
|
||
E('div', { 'class': 'sh-health-down-items' },
|
||
downServices.slice(0, 10).map(function(svc) {
|
||
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
|
||
})
|
||
),
|
||
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
|
||
]);
|
||
} else {
|
||
dom.content(downList, [
|
||
E('div', { 'class': 'sh-health-all-ok' }, [
|
||
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
|
||
_('All routes are healthy')
|
||
])
|
||
]);
|
||
}
|
||
}
|
||
},
|
||
|
||
injectStyles: function() {
|
||
if (document.querySelector('#sh-services-kiss-styles')) return;
|
||
var style = document.createElement('style');
|
||
style.id = 'sh-services-kiss-styles';
|
||
style.textContent = `
|
||
.sh-page-header { margin-bottom: 24px; }
|
||
.sh-page-header-lite { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
|
||
.sh-page-title { font-size: 24px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 10px; }
|
||
.sh-page-title-icon { font-size: 28px; }
|
||
.sh-page-subtitle { color: var(--kiss-muted); margin: 4px 0 0; font-size: 14px; }
|
||
.sh-header-meta { display: flex; gap: 12px; flex-wrap: wrap; }
|
||
.sh-header-chip { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 8px; padding: 8px 14px; display: flex; align-items: center; gap: 8px; }
|
||
.sh-header-chip.success { border-color: rgba(0,200,83,0.3); }
|
||
.sh-header-chip.danger { border-color: rgba(255,23,68,0.3); }
|
||
.sh-chip-icon { font-size: 16px; }
|
||
.sh-chip-text { display: flex; flex-direction: column; }
|
||
.sh-chip-label { font-size: 10px; color: var(--kiss-muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.sh-chip-text strong { font-size: 16px; font-weight: 700; }
|
||
.sh-service-controls { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
||
.sh-service-tabs { display: flex; gap: 6px; }
|
||
.cyber-tab { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 6px; padding: 8px 14px; font-size: 12px; color: var(--kiss-muted); cursor: pointer; transition: all 0.2s; }
|
||
.cyber-tab:hover { border-color: rgba(0,200,83,0.3); color: var(--kiss-text); }
|
||
.cyber-tab.is-active { border-color: var(--kiss-green); color: var(--kiss-green); background: rgba(0,200,83,0.05); }
|
||
.sh-service-search input { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 6px; padding: 8px 14px; color: var(--kiss-text); font-size: 13px; min-width: 220px; }
|
||
.sh-service-search input:focus { outline: none; border-color: rgba(0,200,83,0.4); }
|
||
.sh-services-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
|
||
.sh-service-card { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 12px; padding: 16px; transition: all 0.2s; }
|
||
.sh-service-card:hover { border-color: rgba(0,200,83,0.2); }
|
||
.sh-service-card.running { border-left: 3px solid var(--kiss-green); }
|
||
.sh-service-card.stopped { border-left: 3px solid var(--kiss-red); }
|
||
.sh-service-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
|
||
.sh-service-head h3 { margin: 0; font-size: 15px; font-weight: 600; }
|
||
.sh-service-tag { font-size: 10px; color: var(--kiss-muted); background: rgba(255,255,255,0.04); padding: 3px 8px; border-radius: 4px; margin-top: 4px; display: inline-block; }
|
||
.sh-service-status { font-size: 10px; font-weight: 600; letter-spacing: 0.5px; padding: 4px 10px; border-radius: 4px; }
|
||
.sh-service-status.running { color: var(--kiss-green); background: rgba(0,200,83,0.1); }
|
||
.sh-service-status.stopped { color: var(--kiss-red); background: rgba(255,23,68,0.1); }
|
||
.sh-service-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.sh-btn { padding: 7px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; border: 1px solid var(--kiss-line); background: var(--kiss-bg2); color: var(--kiss-text); transition: all 0.2s; }
|
||
.sh-btn:hover { border-color: rgba(0,200,83,0.3); background: rgba(0,200,83,0.05); }
|
||
.sh-btn-ghost { background: transparent; }
|
||
.sh-btn-action { background: rgba(0,200,83,0.05); border-color: rgba(0,200,83,0.2); color: var(--kiss-green); }
|
||
.sh-empty-state { text-align: center; padding: 60px 20px; color: var(--kiss-muted); }
|
||
.sh-empty-icon { font-size: 48px; margin-bottom: 12px; }
|
||
.sh-service-detail { margin-bottom: 16px; }
|
||
.sh-service-detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--kiss-line); }
|
||
|
||
/* Health Panel Styles */
|
||
.sh-health-panel { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 12px; padding: 16px; margin-bottom: 20px; }
|
||
.sh-health-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||
.sh-health-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; }
|
||
.sh-health-icon { font-size: 20px; }
|
||
.sh-health-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
|
||
.sh-health-stat { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px 16px; min-width: 80px; text-align: center; }
|
||
.sh-health-stat.success { border-left: 3px solid var(--kiss-green); }
|
||
.sh-health-stat.danger { border-left: 3px solid var(--kiss-red); }
|
||
.sh-health-stat.warning { border-left: 3px solid #f59e0b; }
|
||
.sh-health-stat-value { display: block; font-size: 24px; font-weight: 700; color: var(--kiss-text); }
|
||
.sh-health-stat-label { display: block; font-size: 11px; color: var(--kiss-muted); text-transform: uppercase; margin-top: 4px; }
|
||
.sh-health-down-list { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--kiss-line); }
|
||
.sh-health-down-title { font-size: 12px; color: var(--kiss-muted); margin-bottom: 8px; }
|
||
.sh-health-down-items { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.sh-health-down-item { background: rgba(255,23,68,0.1); color: var(--kiss-red); padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: help; }
|
||
.sh-health-down-more { font-size: 11px; color: var(--kiss-muted); padding: 4px 8px; }
|
||
.sh-health-all-ok { display: flex; align-items: center; gap: 8px; color: var(--kiss-green); font-size: 13px; padding: 8px 0; }
|
||
.sh-health-ok-icon { font-size: 16px; }
|
||
.sh-btn-icon { margin-right: 4px; }
|
||
.sh-btn.spinning .sh-btn-icon { animation: spin 1s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
`;
|
||
document.head.appendChild(style);
|
||
},
|
||
|
||
normalizeServices: function(data) {
|
||
if (!data) return [];
|
||
if (Array.isArray(data)) return data;
|
||
if (Array.isArray(data.services)) return data.services;
|
||
return [];
|
||
},
|
||
|
||
renderHeader: function() {
|
||
var stats = this.getStats();
|
||
|
||
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
|
||
E('div', {}, [
|
||
E('h2', { 'class': 'sh-page-title' }, [
|
||
E('span', { 'class': 'sh-page-title-icon' }, '🧩'),
|
||
_('Service Control Center')
|
||
]),
|
||
E('p', { 'class': 'sh-page-subtitle' }, _('Start, stop, enable, and inspect all init.d services'))
|
||
]),
|
||
E('div', { 'class': 'sh-header-meta', 'id': 'sh-services-stats' }, [
|
||
this.renderHeaderChip(_('Total'), stats.total, '📦'),
|
||
this.renderHeaderChip(_('Running'), stats.running, '🟢', stats.running > 0 ? 'success' : ''),
|
||
this.renderHeaderChip(_('Enabled'), stats.enabled, '✅'),
|
||
this.renderHeaderChip(_('Stopped'), stats.stopped, '⏹️', stats.stopped > 0 ? 'danger' : '')
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderHeaderChip: function(label, value, icon, tone) {
|
||
return E('div', { 'class': 'sh-header-chip' + (tone ? ' ' + tone : '') }, [
|
||
E('span', { 'class': 'sh-chip-icon' }, icon),
|
||
E('div', { 'class': 'sh-chip-text' }, [
|
||
E('span', { 'class': 'sh-chip-label' }, label),
|
||
E('strong', {}, value.toString())
|
||
])
|
||
]);
|
||
},
|
||
|
||
createStatCard: function(id, label, value, tone) {
|
||
return E('div', { 'class': 'sh-service-stat ' + (tone || ''), 'id': id }, [
|
||
E('span', { 'class': 'label' }, label),
|
||
E('span', { 'class': 'value' }, value)
|
||
]);
|
||
},
|
||
|
||
renderControls: function() {
|
||
var self = this;
|
||
var filters = [
|
||
{ id: 'all', label: _('All') },
|
||
{ id: 'running', label: _('Running') },
|
||
{ id: 'stopped', label: _('Stopped') },
|
||
{ id: 'enabled', label: _('Enabled') },
|
||
{ id: 'disabled', label: _('Disabled') }
|
||
];
|
||
|
||
return E('div', { 'class': 'sh-service-controls' }, [
|
||
E('div', { 'class': 'cyber-tablist cyber-tablist--pills sh-service-tabs' },
|
||
filters.map(function(filter) {
|
||
return E('button', {
|
||
'class': 'cyber-tab cyber-tab--pill' + (self.activeFilter === filter.id ? ' is-active' : ''),
|
||
'type': 'button',
|
||
'data-filter': filter.id,
|
||
'click': function() {
|
||
self.activeFilter = filter.id;
|
||
self.updateServicesGrid();
|
||
self.refreshFilterTabs();
|
||
}
|
||
}, [
|
||
E('span', { 'class': 'cyber-tab-label' }, filter.label)
|
||
]);
|
||
})),
|
||
E('div', { 'class': 'sh-service-search' }, [
|
||
E('input', {
|
||
'type': 'text',
|
||
'placeholder': _('Search services...'),
|
||
'input': function(ev) {
|
||
self.searchQuery = (ev.target.value || '').toLowerCase();
|
||
self.updateServicesGrid();
|
||
}
|
||
})
|
||
])
|
||
]);
|
||
},
|
||
|
||
refreshFilterTabs: function() {
|
||
var tabs = document.querySelectorAll('.sh-service-tabs .cyber-tab');
|
||
tabs.forEach(function(tab) {
|
||
var match = tab.getAttribute('data-filter') === this.activeFilter;
|
||
tab.classList.toggle('is-active', match);
|
||
}, this);
|
||
},
|
||
|
||
getStats: function() {
|
||
var total = this.services.length;
|
||
var running = this.services.filter(function(s) { return s.running; }).length;
|
||
var enabled = this.services.filter(function(s) { return s.enabled; }).length;
|
||
var stopped = total - running;
|
||
return { total: total, running: running, stopped: stopped, enabled: enabled, disabled: total - enabled };
|
||
},
|
||
|
||
getFilteredServices: function() {
|
||
var ordered = this.services.slice().sort(function(a, b) {
|
||
if (a.running !== b.running)
|
||
return a.running ? -1 : 1;
|
||
if (a.enabled !== b.enabled)
|
||
return a.enabled ? -1 : 1;
|
||
return (a.name || '').localeCompare(b.name || '');
|
||
});
|
||
|
||
return ordered.filter(function(service) {
|
||
var matchesFilter = true;
|
||
switch (this.activeFilter) {
|
||
case 'running': matchesFilter = service.running; break;
|
||
case 'stopped': matchesFilter = !service.running; break;
|
||
case 'enabled': matchesFilter = service.enabled; break;
|
||
case 'disabled': matchesFilter = !service.enabled; break;
|
||
}
|
||
var matchesSearch = !this.searchQuery ||
|
||
(service.name || '').toLowerCase().includes(this.searchQuery);
|
||
return matchesFilter && matchesSearch;
|
||
}, this);
|
||
},
|
||
|
||
renderServiceCard: function(service) {
|
||
var statusClass = service.running ? 'running' : 'stopped';
|
||
var enabledLabel = service.enabled ? _('Enabled at boot') : _('Disabled');
|
||
|
||
return E('div', { 'class': 'sh-service-card ' + statusClass }, [
|
||
E('div', { 'class': 'sh-service-head' }, [
|
||
E('div', {}, [
|
||
E('h3', {}, service.name),
|
||
E('span', { 'class': 'sh-service-tag' }, enabledLabel)
|
||
]),
|
||
E('span', {
|
||
'class': 'sh-service-status ' + statusClass
|
||
}, service.running ? _('Running') : _('Stopped'))
|
||
]),
|
||
E('div', { 'class': 'sh-service-actions' }, [
|
||
this.renderActionButton(service, service.running ? 'stop' : 'start'),
|
||
this.renderActionButton(service, 'restart'),
|
||
this.renderActionButton(service, service.enabled ? 'disable' : 'enable'),
|
||
E('button', {
|
||
'class': 'sh-btn sh-btn-ghost',
|
||
'type': 'button',
|
||
'click': ui.createHandlerFn(this, 'showServiceDetails', service)
|
||
}, _('Details'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderActionButton: function(service, action) {
|
||
var labels = {
|
||
start: _('Start'),
|
||
stop: _('Stop'),
|
||
restart: _('Restart'),
|
||
enable: _('Enable'),
|
||
disable: _('Disable')
|
||
};
|
||
|
||
return E('button', {
|
||
'class': 'sh-btn sh-btn-action',
|
||
'type': 'button',
|
||
'click': ui.createHandlerFn(this, 'handleServiceAction', service, action)
|
||
}, labels[action] || action);
|
||
},
|
||
|
||
handleServiceAction: function(service, action) {
|
||
ui.showModal(_('Service action'), [
|
||
E('p', { 'class': 'spinning' }, _('Executing ') + action + ' ' + service.name + ' ...')
|
||
]);
|
||
|
||
return API.serviceAction({ service: service.name, action: action }).then(L.bind(function(result) {
|
||
ui.hideModal();
|
||
if (result && result.success === false) {
|
||
ui.addNotification(null, E('p', {}, result.message || _('Action failed')), 'error');
|
||
} else {
|
||
ui.addNotification(null, E('p', {}, result.message || _('Action completed')), 'info');
|
||
return API.listServices().then(L.bind(function(fresh) {
|
||
this.services = this.normalizeServices(fresh);
|
||
this.updateStats();
|
||
this.updateServicesGrid();
|
||
}, this));
|
||
}
|
||
}, this)).catch(function(err) {
|
||
ui.hideModal();
|
||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||
});
|
||
},
|
||
|
||
showServiceDetails: function(service) {
|
||
var details = [
|
||
{ label: _('Name'), value: service.name },
|
||
{ label: _('Enabled'), value: service.enabled ? _('Yes') : _('No') },
|
||
{ label: _('Running'), value: service.running ? _('Yes') : _('No') }
|
||
];
|
||
|
||
ui.showModal(_('Service Details'), [
|
||
E('div', { 'class': 'sh-service-detail' },
|
||
details.map(function(item) {
|
||
return E('div', { 'class': 'sh-service-detail-row' }, [
|
||
E('span', {}, item.label),
|
||
E('strong', {}, item.value)
|
||
]);
|
||
})),
|
||
E('div', { 'class': 'right' }, [
|
||
E('button', {
|
||
'class': 'kiss-btn kiss-btn-green',
|
||
'click': ui.hideModal
|
||
}, _('Close'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
updateStats: function() {
|
||
var stats = this.getStats();
|
||
var ids = {
|
||
'sh-stat-total': stats.total,
|
||
'sh-stat-running': stats.running,
|
||
'sh-stat-stopped': stats.stopped,
|
||
'sh-stat-enabled': stats.enabled
|
||
};
|
||
|
||
Object.keys(ids).forEach(function(id) {
|
||
var node = document.getElementById(id);
|
||
if (node) {
|
||
var value = node.querySelector('.value');
|
||
if (value) value.textContent = ids[id];
|
||
}
|
||
});
|
||
},
|
||
|
||
updateServicesGrid: function() {
|
||
var grid = document.getElementById('sh-services-grid');
|
||
if (!grid) return;
|
||
var filtered = this.getFilteredServices();
|
||
if (!filtered.length) {
|
||
dom.content(grid, [
|
||
E('div', { 'class': 'sh-empty-state' }, [
|
||
E('div', { 'class': 'sh-empty-icon' }, '📭'),
|
||
E('p', {}, _('No services match the current filter'))
|
||
])
|
||
]);
|
||
return;
|
||
}
|
||
dom.content(grid, filtered.map(this.renderServiceCard, this));
|
||
}
|
||
});
|