develstats

This commit is contained in:
CyberMind-FR 2025-12-29 02:02:44 +01:00
parent b20f9cbb8c
commit bd96ab1d31
52 changed files with 1431 additions and 1177 deletions

View File

@ -155,7 +155,8 @@
"Bash(luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js )",
"Bash(deploy-modules-with-theme.sh)",
"Bash(timeout 120 ./local-build.sh:*)",
"Bash(do echo \"=== admin/secubox/$category ===\")"
"Bash(do echo \"=== admin/secubox/$category ===\")",
"Bash(./secubox-tools/sync_module_versions.sh:*)"
]
}
}

View File

@ -167,6 +167,8 @@ graph TB
#### 1. Page Header (Standard)
**REQUIREMENT:** Every module view MUST begin with this compact `.sh-page-header`. Do not introduce bespoke hero sections or oversized banners; the header keeps height predictable (title + subtitle on the left, stats on the right) and guarantees consistency across SecuBox dashboards. If no stats are needed, keep the container but supply an empty `.sh-stats-grid` for future metrics.
**HTML Structure:**
```javascript
E('div', { 'class': 'sh-page-header' }, [

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -0,0 +1,31 @@
'use strict';
'require baseclass';
var tabs = [
{ id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'overview'] },
{ id: 'cache', icon: '💾', label: _('Cache'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'cache'] },
{ id: 'policies', icon: '🧭', label: _('Policies'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'policies'] },
{ id: 'statistics', icon: '📊', label: _('Statistics'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'statistics'] },
{ id: 'maintenance', icon: '🧹', label: _('Maintenance'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'maintenance'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'settings'] }
];
return baseclass.extend({
getTabs: function() {
return tabs.slice();
},
renderTabs: function(active) {
return E('div', { 'class': 'sh-nav-tabs cdn-nav-tabs' },
this.getTabs().map(function(tab) {
return E('a', {
'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''),
'href': L.url.apply(L, tab.path)
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
})
);
}
});

View File

@ -3,6 +3,7 @@
'require rpc';
'require ui';
'require secubox-theme/theme as Theme';
'require cdn-cache/nav as CdnNav';
var callCacheList = rpc.declare({
object: 'luci.cdn-cache',
@ -59,6 +60,7 @@ return view.extend({
return E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('cache'),
this.renderHero(items, domains),
this.renderDomains(domains),
this.renderCacheTable(items)

View File

@ -2,6 +2,7 @@
'require view';
'require rpc';
'require ui';
'require cdn-cache/nav as CdnNav';
var callPurgeCache = rpc.declare({
object: 'luci.cdn-cache',
@ -74,6 +75,8 @@ return view.extend({
.cdn-log-line:hover { background: rgba(6,182,212,0.1); }
`),
CdnNav.renderTabs('maintenance'),
E('div', { 'class': 'cdn-page-header' }, [
E('h2', { 'class': 'cdn-page-title' }, '🔧 Maintenance')
]),

View File

@ -2,6 +2,7 @@
'require view';
'require rpc';
'require secubox-theme/theme as Theme';
'require cdn-cache/nav as CdnNav';
var callStatus = rpc.declare({
object: 'luci.cdn-cache',
@ -72,26 +73,37 @@ return view.extend({
return E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
this.renderHero(status),
CdnNav.renderTabs('overview'),
this.renderHeader(status),
this.renderMetricGrid(stats, cacheSize),
this.renderSections(stats, cacheSize, topDomains)
]);
},
renderHero: function(status) {
return E('section', { 'class': 'cdn-hero' }, [
renderHeader: function(status) {
var stats = [
{ label: _('Service'), value: status.running ? _('Running') : _('Stopped') },
{ label: _('Uptime'), value: formatUptime(status.uptime || 0) },
{ label: _('Cache files'), value: (status.cache_files || 0).toLocaleString() }
];
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', {}, '📦 CDN Cache Control'),
E('p', {}, _('Edge caching for media, firmware and downloads')),
E('span', { 'class': 'cdn-status-badge ' + (status.running ? 'cdn-status-running' : 'cdn-status-stopped') }, [
status.running ? _('● Running on port ') + (status.listen_port || '3128') : _('○ Service stopped')
])
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '📦'),
_('CDN Cache Control')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Edge caching for media, firmware, and downloads'))
]),
E('div', { 'class': 'cdn-hero-meta' }, [
E('span', {}, _('PID: ') + (status.pid || 'N/A')),
E('span', {}, _('Uptime: ') + formatUptime(status.uptime || 0)),
E('span', {}, _('Cache files: ') + (status.cache_files || 0).toLocaleString())
])
E('div', { 'class': 'sh-stats-grid' }, stats.map(this.renderHeaderStat, this))
]);
},
renderHeaderStat: function(stat) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stat.value),
E('div', { 'class': 'sh-stat-label' }, stat.label)
]);
},

View File

@ -3,6 +3,7 @@
'require rpc';
'require ui';
'require form';
'require cdn-cache/nav as CdnNav';
var callPolicies = rpc.declare({
object: 'luci.cdn-cache',
@ -68,6 +69,8 @@ return view.extend({
.cdn-empty { text-align: center; padding: 40px; color: #64748b; }
`),
CdnNav.renderTabs('policies'),
E('div', { 'class': 'cdn-page-header' }, [
E('h2', { 'class': 'cdn-page-title' }, '📋 Policies de Cache'),
E('p', { 'style': 'margin: 0; opacity: 0.9;' }, 'Règles de mise en cache par domaine et type de fichier')

View File

@ -3,6 +3,7 @@
'require form';
'require uci';
'require rpc';
'require cdn-cache/nav as CdnNav';
var callSetEnabled = rpc.declare({
object: 'luci.cdn-cache',
@ -137,6 +138,9 @@ return view.extend({
o.datatype = 'uinteger';
o.default = '60';
return m.render();
return E('div', { 'class': 'cdn-settings-page' }, [
CdnNav.renderTabs('settings'),
m.render()
]);
}
});

View File

@ -2,6 +2,7 @@
'require view';
'require rpc';
'require secubox-theme/theme as Theme';
'require cdn-cache/nav as CdnNav';
var callStats = rpc.declare({
object: 'luci.cdn-cache',
@ -60,6 +61,7 @@ return view.extend({
var view = E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('statistics'),
this.renderHero(stats),
this.renderMetrics(stats),
this.renderTrendSection(_('Bandwidth Savings'), bandwidthTrend, '#06b6d4', function(d) {

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -136,25 +136,17 @@ function createStepper(steps, active) {
}
function createNavigationTabs(activeId) {
var base = 'admin/secubox/network/modes/';
return E('nav', { 'class': 'nm-nav-tabs' }, [
E('div', { 'class': 'cyber-tablist' },
NAV_BLUEPRINT.map(function(item) {
var cls = 'cyber-tab';
if (activeId === item.id)
cls += ' is-active';
return E('a', {
'class': cls,
'href': L.url(base + item.id),
'aria-current': activeId === item.id ? 'page' : null
}, [
E('span', { 'class': 'cyber-tab-icon' }, item.icon),
E('span', { 'class': 'cyber-tab-label' }, _(item.labelKey))
]);
})
)
]);
return E('div', { 'class': 'sh-nav-tabs network-modes-nav-tabs' },
NAV_BLUEPRINT.map(function(item) {
return E('a', {
'class': 'sh-nav-tab' + (activeId === item.id ? ' active' : ''),
'href': L.url('admin', 'secubox', 'network', 'modes', item.id)
}, [
E('span', { 'class': 'sh-tab-icon' }, item.icon),
E('span', { 'class': 'sh-tab-label' }, _(item.labelKey))
]);
})
);
}
return baseclass.extend({

View File

@ -104,21 +104,7 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }),
helpers.createNavigationTabs('overview'),
// Header
E('div', { 'class': 'nm-header' }, [
E('div', { 'class': 'nm-logo' }, [
E('div', { 'class': 'nm-logo-icon' }, '🌐'),
E('div', { 'class': 'nm-logo-text' }, ['Network ', E('span', {}, 'Configuration')])
]),
E('div', { 'class': 'nm-mode-badge ' + currentMode }, [
E('span', { 'class': 'nm-mode-dot' }),
currentModeInfo ? currentModeInfo.name : currentMode
]),
Help.createHelpButton('network-modes', 'header', {
icon: '📖',
label: _('Help')
})
]),
this.renderHeader(status, currentModeInfo),
// Current Mode Display Card
E('div', { 'class': 'nm-current-mode-card' }, [
@ -377,6 +363,34 @@ return view.extend({
return view;
},
renderHeader: function(status, currentModeInfo) {
var modeName = currentModeInfo ? currentModeInfo.name : (status.current_mode || 'router');
var stats = [
{ label: _('Mode'), value: modeName },
{ label: _('WAN IP'), value: status.wan_ip || _('Unknown') },
{ label: _('LAN IP'), value: status.lan_ip || _('Unknown') }
];
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🌐'),
_('Network Configuration')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Switch between curated router, bridge, relay, and travel modes.'))
]),
E('div', { 'class': 'sh-stats-grid' }, stats.map(this.renderHeaderStat, this))
]);
},
renderHeaderStat: function(stat) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stat.value || '-'),
E('div', { 'class': 'sh-stat-label' }, stat.label)
]);
},
handleSaveApply: null,
handleSave: null,

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -0,0 +1,30 @@
'use strict';
'require baseclass';
var tabs = [
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
{ id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] },
{ id: 'monitoring', icon: '📡', label: _('Monitoring'), path: ['admin', 'secubox', 'monitoring', 'overview'] },
{ id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'settings'] }
];
return baseclass.extend({
getTabs: function() {
return tabs.slice();
},
renderTabs: function(active) {
return E('div', { 'class': 'sh-nav-tabs secubox-nav-tabs' },
this.getTabs().map(function(tab) {
return E('a', {
'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''),
'href': L.url.apply(L, tab.path)
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
})
);
}
});

View File

@ -4,6 +4,7 @@
'require dom';
'require secubox/api as API';
'require secubox/theme as Theme';
'require secubox/nav as SecuNav';
'require poll';
// Load CSS (base theme variables first)
@ -41,19 +42,13 @@ return view.extend({
render: function(data) {
var self = this;
var container = E('div', { 'class': 'secubox-alerts-page' });
// Header
container.appendChild(this.renderHeader());
// Filters and controls
container.appendChild(this.renderControls());
// Stats overview
container.appendChild(this.renderStats());
// Alerts list
container.appendChild(this.renderAlertsList());
var container = E('div', { 'class': 'secubox-alerts-page' }, [
SecuNav.renderTabs('alerts'),
this.renderHeader(),
this.renderControls(),
this.renderStats(),
this.renderAlertsList()
]);
// Auto-refresh
poll.add(function() {
@ -66,6 +61,7 @@ return view.extend({
},
renderHeader: function() {
var self = this;
return E('div', { 'class': 'secubox-page-header' }, [
E('div', {}, [
E('h2', {}, '⚠️ System Alerts'),

View File

@ -5,6 +5,7 @@
'require poll';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
// Load theme resources once
document.head.appendChild(E('link', {
@ -48,6 +49,7 @@ return view.extend({
render: function() {
var container = E('div', { 'class': 'secubox-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/dashboard.css') }),
SecuNav.renderTabs('dashboard'),
this.renderHeader(),
this.renderStatsGrid(),
this.renderMainLayout()
@ -64,25 +66,37 @@ return view.extend({
},
renderHeader: function() {
var status = this.dashboardData.status || {};
return E('header', { 'class': 'sb-header' }, [
E('div', { 'class': 'sb-header-info' }, [
E('div', { 'class': 'sb-header-icon' }, '🚀'),
E('div', {}, [
E('h1', { 'class': 'sb-title' }, 'SecuBox Control Center'),
E('p', { 'class': 'sb-subtitle' }, 'Security · Network · System Automation')
])
]),
E('div', { 'class': 'sb-header-meta' }, [
this.renderBadge('v' + (status.version || this.getSystemVersion())),
this.renderBadge('⏱ ' + API.formatUptime(status.uptime || this.getSystemUptime())),
this.renderBadge('🖥 ' + (status.hostname || 'SecuBox'), 'sb-badge-ghost')
])
]);
},
var status = this.dashboardData.status || {};
var counts = this.dashboardData.counts || {};
var moduleStats = this.getModuleStats();
var alertsCount = (this.alertsData.alerts || []).length;
var healthScore = (this.healthData.overall && this.healthData.overall.score) || 0;
renderBadge: function(text, extraClass) {
return E('span', { 'class': 'sb-badge ' + (extraClass || '') }, text);
var stats = [
{ label: _('Modules'), value: counts.total || moduleStats.total || 0 },
{ label: _('Running'), value: counts.running || moduleStats.running || 0 },
{ label: _('Alerts'), value: alertsCount },
{ label: _('Health'), value: healthScore + '/100' }
];
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🚀'),
_('SecuBox Control Center')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Security · Network · System automation'))
]),
E('div', { 'class': 'sh-stats-grid' }, stats.map(this.renderHeaderStat, this))
]);
},
renderHeaderStat: function(stat) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stat.value.toString()),
E('div', { 'class': 'sh-stat-label' }, stat.label)
]);
},
renderStatsGrid: function() {

View File

@ -4,6 +4,7 @@
'require dom';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
'require poll';
// Load global theme CSS
@ -43,18 +44,15 @@ return view.extend({
var self = this;
var modules = this.modulesData;
var container = E('div', { 'class': 'secubox-modules-page' });
// Header with stats
container.appendChild(this.renderHeader(modules));
// Filter tabs
container.appendChild(this.renderFilterTabs());
// Modules grid
container.appendChild(E('div', { 'id': 'modules-grid', 'class': 'secubox-modules-grid' },
this.renderModuleCards(modules, 'all')
));
var container = E('div', { 'class': 'secubox-modules-page' }, [
SecuNav.renderTabs('modules'),
this.renderHeader(modules),
this.renderFilterTabs(),
E('div', {
'id': 'modules-grid',
'class': 'secubox-modules-grid'
}, this.renderModuleCards(modules, 'all'))
]);
// Auto-refresh
poll.add(function() {

View File

@ -5,6 +5,7 @@
'require poll';
'require secubox/api as API';
'require secubox-theme/theme as Theme';
'require secubox/nav as SecuNav';
// Respect LuCI language/theme preferences
var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
@ -56,6 +57,7 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/monitoring.css') }),
SecuNav.renderTabs('monitoring'),
this.renderHero(),
this.renderChartsGrid(),
this.renderCurrentStatsCard()

View File

@ -5,6 +5,7 @@
'require ui';
'require secubox/api as API';
'require secubox/theme as Theme';
'require secubox/nav as SecuNav';
return view.extend({
load: function() {
@ -25,6 +26,8 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
SecuNav.renderTabs('settings'),
// Modern header
E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [

View File

@ -76,7 +76,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -89,7 +89,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -0,0 +1,35 @@
'use strict';
'require baseclass';
var tabs = [
{ id: 'overview', icon: '📊', label: _('Overview'), path: ['admin', 'secubox', 'system', 'system-hub', 'overview'] },
{ id: 'services', icon: '🧩', label: _('Services'), path: ['admin', 'secubox', 'system', 'system-hub', 'services'] },
{ id: 'logs', icon: '📜', label: _('Logs'), path: ['admin', 'secubox', 'system', 'system-hub', 'logs'] },
{ id: 'backup', icon: '💾', label: _('Backup'), path: ['admin', 'secubox', 'system', 'system-hub', 'backup'] },
{ id: 'components', icon: '🧱', label: _('Components'), path: ['admin', 'secubox', 'system', 'system-hub', 'components'] },
{ id: 'diagnostics', icon: '🧪', label: _('Diagnostics'), path: ['admin', 'secubox', 'system', 'system-hub', 'diagnostics'] },
{ id: 'health', icon: '❤️', label: _('Health'), path: ['admin', 'secubox', 'system', 'system-hub', 'health'] },
{ id: 'remote', icon: '📡', label: _('Remote'), path: ['admin', 'secubox', 'system', 'system-hub', 'remote'] },
{ id: 'dev-status', icon: '🚀', label: _('Dev Status'), path: ['admin', 'secubox', 'system', 'system-hub', 'dev-status'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'system', 'system-hub', 'settings'] }
];
return baseclass.extend({
getTabs: function() {
return tabs.slice();
},
renderTabs: function(active) {
return E('div', { 'class': 'sh-nav-tabs system-hub-nav-tabs' },
this.getTabs().map(function(tab) {
return E('a', {
'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''),
'href': L.url.apply(L, tab.path)
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
})
);
}
});

View File

@ -3,6 +3,7 @@
'require ui';
'require system-hub/api as API';
'require system-hub/theme as Theme';
'require system-hub/nav as HubNav';
Theme.init();
@ -15,6 +16,7 @@ return view.extend({
var container = E('div', { 'class': 'system-hub-dashboard sh-backup-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/backup.css') }),
HubNav.renderTabs('backup'),
this.renderHero(),
E('div', { 'class': 'sh-backup-grid' }, [
this.renderBackupCard(),

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub.api as API';
'require system-hub.theme as Theme';
'require system-hub/nav as HubNav';
return view.extend({
componentsData: [],
@ -26,7 +27,8 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/components.css') }),
// Header with filter tabs
HubNav.renderTabs('components'),
E('div', { 'class': 'sh-components-header' }, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-title-icon' }, '🧩'),

View File

@ -2,6 +2,7 @@
'require view';
'require system-hub/theme as Theme';
'require system-hub/dev-status-widget as DevStatusWidget';
'require system-hub/nav as HubNav';
return view.extend({
widget: null,
@ -22,6 +23,7 @@ return view.extend({
var widget = this.getWidget();
var container = E('div', { 'class': 'system-hub-dev-status' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
HubNav.renderTabs('dev-status'),
this.renderHeader(),
this.renderSummaryGrid(),
E('div', { 'class': 'sh-dev-status-widget-shell' }, [

View File

@ -3,6 +3,7 @@
'require dom';
'require ui';
'require fs';
'require system-hub/nav as HubNav';
var api = L.require('system-hub.api');
@ -17,6 +18,7 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
HubNav.renderTabs('diagnostics'),
// Collect Diagnostics
E('div', { 'class': 'sh-card' }, [

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require system-hub/theme as Theme';
'require system-hub/nav as HubNav';
Theme.init();
@ -21,6 +22,7 @@ return view.extend({
var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/health.css') }),
HubNav.renderTabs('health'),
this.renderHero(),
this.renderMetricGrid(),
this.renderSummaryPanels(),
@ -176,4 +178,3 @@ return view.extend({
ui.addNotification(null, E('p', {}, _('Full health check started (see alerts).')), 'info');
}
});

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
@ -31,6 +32,7 @@ return view.extend({
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/logs.css') }),
HubNav.renderTabs('logs'),
this.renderHero(),
this.renderControls(),
this.renderBody()

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
@ -31,7 +32,8 @@ return view.extend({
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') }),
this.renderHeroHeader(),
HubNav.renderTabs('overview'),
this.renderPageHeader(),
this.renderInfoGrid(),
this.renderResourceMonitors(),
this.renderQuickStatus()
@ -52,46 +54,47 @@ return view.extend({
return container;
},
renderHeroHeader: function() {
renderPageHeader: function() {
var uptime = this.sysInfo.uptime_formatted || '0d 0h 0m';
var hostname = this.sysInfo.hostname || 'OpenWrt';
var kernel = this.sysInfo.kernel || '';
var kernel = this.sysInfo.kernel || '-';
var score = (this.healthData.score || 0);
return E('section', { 'class': 'sh-hero' }, [
E('div', { 'class': 'sh-hero-title' }, [
E('div', { 'class': 'sh-hero-icon' }, '⚙️'),
E('div', {}, [
E('h1', {}, _('System Control Center')),
E('p', {}, _('Unified telemetry & orchestration'))
])
var stats = [
{ label: _('Uptime'), value: uptime },
{ label: _('Hostname'), value: hostname },
{ label: _('Kernel'), value: kernel, copy: kernel },
{ label: _('Health'), value: score + '/100' }
];
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
_('System Control Center')
]),
E('p', { 'class': 'sh-page-subtitle' }, _('Unified telemetry & orchestration'))
]),
E('div', { 'class': 'sh-hero-meta' }, [
this.renderBadge('⏱ ' + uptime, 'sh-badge'),
this.renderBadge('🖥 ' + hostname, 'sh-badge'),
this.renderBadge(kernel, 'sh-badge ghost', { copy: kernel })
]),
E('div', { 'class': 'sh-hero-score' }, [
E('div', { 'class': 'sh-score-value', 'id': 'sh-score-value' }, score),
E('span', {}, '/100'),
E('div', { 'class': 'sh-score-label', 'id': 'sh-score-label' }, this.getScoreLabel(score))
])
E('div', { 'class': 'sh-stats-grid' }, stats.map(this.renderHeaderStat, this))
]);
},
renderBadge: function(text, cls, opts) {
var node = E('span', { 'class': cls }, text);
if (opts && opts.copy) {
node.classList.add('sh-badge-copy');
node.addEventListener('click', function() {
if (navigator.clipboard && opts.copy) {
navigator.clipboard.writeText(opts.copy).then(function() {
ui.addNotification(null, E('p', {}, _('Copied to clipboard')), 'info');
});
}
renderHeaderStat: function(stat) {
var badge = E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, stat.value || '-'),
E('div', { 'class': 'sh-stat-label' }, stat.label)
]);
if (stat.copy && navigator.clipboard) {
badge.style.cursor = 'pointer';
badge.addEventListener('click', function() {
navigator.clipboard.writeText(stat.copy).then(function() {
ui.addNotification(null, E('p', {}, _('Copied to clipboard')), 'info');
});
});
}
return node;
return badge;
},
renderInfoGrid: function() {

View File

@ -2,6 +2,7 @@
'require view';
'require dom';
'require ui';
'require system-hub/nav as HubNav';
var api = L.require('system-hub.api');
@ -15,6 +16,7 @@ return view.extend({
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
HubNav.renderTabs('remote'),
// RustDesk Section
E('div', { 'class': 'sh-card sh-remote-card' }, [

View File

@ -5,6 +5,7 @@
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/nav as HubNav';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
@ -28,6 +29,7 @@ return view.extend({
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') }),
HubNav.renderTabs('services'),
this.renderHeader(),
this.renderControls(),
E('div', { 'class': 'sh-services-grid', 'id': 'sh-services-grid' },

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -1,667 +1,179 @@
/* VHost Manager Dashboard Styles * Version: 0.3.0
*/
:root {
--vh-primary: #06b6d4;
--vh-secondary: #0891b2;
--vh-dark: #0a0f14;
--vh-darker: #050810;
--vh-light: #15181f;
--vh-border: #202530;
--vh-success: #10b981;
--vh-warning: #f59e0b;
--vh-danger: #ef4444;
--vh-info: #3b82f6;
--vh-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
.vhost-page {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
background: var(--sh-bg-primary, #0a0f1f);
color: var(--sh-text-primary, #e2e8f0);
}
/* Main Container * Version: 0.3.0
*/
.vhost-manager-container {
background: linear-gradient(135deg, var(--vh-dark) 0%, var(--vh-darker) 100%);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
.vhost-nav-tabs {
position: sticky;
top: 0;
z-index: 5;
}
/* Header */
.vhost-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid var(--vh-border);
.vhost-card-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.vhost-title {
font-size: 24px;
font-weight: 700;
background: var(--vh-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
gap: 12px;
.vhost-card {
background: var(--sh-bg-card, rgba(17, 24, 39, 0.92));
border: 1px solid var(--sh-border, rgba(148, 163, 184, 0.2));
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 12px 30px rgba(2, 6, 23, 0.35);
}
.vhost-title::before {
content: "🌐";
font-size: 28px;
-webkit-text-fill-color: initial;
.vhost-card-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 16px;
}
/* Stats Grid */
.vhost-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
.vhost-card-meta {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--sh-text-secondary, #94a3b8);
word-break: break-all;
}
.vh-stat-card {
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 8px;
padding: 16px;
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
.vhost-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
border: 1px solid rgba(255, 255, 255, 0.16);
}
.vh-stat-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--vh-gradient);
.vhost-pill.success {
border-color: rgba(34, 197, 94, 0.35);
color: #34d399;
}
.vh-stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.2);
.vhost-pill.warn {
border-color: rgba(245, 158, 11, 0.35);
color: #fbbf24;
}
.vh-stat-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #999;
margin-bottom: 8px;
.vhost-pill.danger {
border-color: rgba(248, 113, 113, 0.35);
color: #f87171;
}
.vh-stat-value {
font-size: 28px;
font-weight: 700;
background: var(--vh-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
.vhost-table {
width: 100%;
border-collapse: collapse;
}
.vh-stat-icon {
position: absolute;
top: 16px;
right: 16px;
font-size: 32px;
opacity: 0.3;
.vhost-table th,
.vhost-table td {
padding: 12px 10px;
border-bottom: 1px solid rgba(148, 163, 184, 0.15);
text-align: left;
}
/* Virtual Hosts List */
.vhost-list {
display: grid;
gap: 16px;
margin-bottom: 24px;
.vhost-table th {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sh-text-secondary, #94a3b8);
}
.vhost-item {
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 8px;
padding: 20px;
transition: all 0.2s;
.vhost-table tbody tr:hover {
background: rgba(59, 130, 246, 0.08);
}
.vhost-item:hover {
border-color: var(--vh-primary);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.2);
}
.vhost-item.active {
border-left: 4px solid var(--vh-primary);
}
.vhost-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.vhost-domain {
font-size: 18px;
font-weight: 700;
color: var(--vh-primary);
display: flex;
align-items: center;
gap: 8px;
}
.vhost-domain::before {
content: "🔗";
font-size: 20px;
}
.vhost-status {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.vhost-status.online {
background: rgba(16, 185, 129, 0.2);
color: var(--vh-success);
}
.vhost-status.offline {
background: rgba(156, 163, 175, 0.2);
color: #9ca3af;
}
.vhost-status.error {
background: rgba(239, 68, 68, 0.2);
color: var(--vh-danger);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-dot.online {
animation: pulse-status 2s infinite;
}
@keyframes pulse-status {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.vhost-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.vhost-detail {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 11px;
text-transform: uppercase;
color: #666;
letter-spacing: 0.5px;
}
.detail-value {
font-size: 14px;
color: #fff;
font-weight: 500;
.vhost-empty {
padding: 30px;
text-align: center;
border: 1px dashed rgba(148, 163, 184, 0.4);
border-radius: 12px;
color: var(--sh-text-secondary, #94a3b8);
}
.vhost-actions {
display: flex;
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--vh-border);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* SSL Certificate Badge */
.ssl-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
.vhost-form-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
margin-top: 12px;
}
.ssl-badge.valid {
background: rgba(16, 185, 129, 0.2);
color: var(--vh-success);
.vhost-form-grid label {
display: block;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--sh-text-secondary, #94a3b8);
margin-bottom: 6px;
}
.ssl-badge.expired {
background: rgba(245, 158, 11, 0.2);
color: var(--vh-warning);
.vhost-form-grid input,
.vhost-form-grid select {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.25);
background: rgba(15, 23, 42, 0.6);
color: var(--sh-text-primary, #f8fafc);
font-size: 14px;
}
.ssl-badge.none {
background: rgba(156, 163, 175, 0.2);
color: #9ca3af;
.vhost-log-terminal {
background: #050816;
border-radius: 12px;
border: 1px solid rgba(15, 118, 230, 0.4);
box-shadow: inset 0 0 24px rgba(14, 165, 233, 0.12);
font-family: 'JetBrains Mono', monospace;
color: #12f7d6;
font-size: 12px;
line-height: 1.5;
max-height: 420px;
overflow: auto;
padding: 16px;
}
.ssl-badge::before {
content: "🔒";
font-size: 12px;
.vhost-status-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Service Redirects */
.redirect-list {
display: grid;
gap: 12px;
.vhost-status-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--sh-text-secondary, #a0aec0);
}
.redirect-item {
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 6px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
.vhost-status-item strong {
color: var(--sh-text-primary, #f1f5f9);
}
.redirect-route {
display: flex;
align-items: center;
gap: 12px;
font-family: 'Courier New', monospace;
.vhost-filter-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.redirect-from {
color: var(--vh-warning);
font-weight: 600;
}
.redirect-arrow {
color: #666;
font-size: 20px;
}
.redirect-to {
color: var(--vh-success);
font-weight: 600;
}
.redirect-type {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.redirect-type.proxy {
background: rgba(6, 182, 212, 0.2);
color: var(--vh-primary);
}
.redirect-type.dns {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
/* Service Templates */
.service-templates {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
margin-top: 16px;
}
.service-template {
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.service-template:hover {
border-color: var(--vh-primary);
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
.service-icon {
font-size: 40px;
margin-bottom: 12px;
}
.service-name {
font-size: 14px;
font-weight: 600;
color: #fff;
margin-bottom: 4px;
}
.service-desc {
font-size: 11px;
color: #999;
}
/* Nginx/HAProxy Config Preview */
.config-preview {
background: var(--vh-dark);
border: 1px solid var(--vh-border);
border-radius: 6px;
padding: 16px;
font-family: 'Courier New', monospace;
font-size: 13px;
overflow-x: auto;
margin-top: 16px;
}
.config-line {
color: #ccc;
line-height: 1.6;
}
.config-keyword {
color: var(--vh-primary);
font-weight: 600;
}
.config-value {
color: var(--vh-success);
}
.config-comment {
color: #666;
font-style: italic;
}
/* Let's Encrypt Setup */
.acme-setup {
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.acme-status {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.acme-icon {
font-size: 32px;
}
.acme-info {
flex: 1;
}
.acme-title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin-bottom: 4px;
}
.acme-subtitle {
font-size: 13px;
color: #999;
}
.acme-domains {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.domain-tag {
padding: 6px 12px;
background: var(--vh-dark);
border: 1px solid var(--vh-border);
border-radius: 6px;
font-size: 12px;
color: #fff;
font-family: 'Courier New', monospace;
}
.domain-tag.verified {
border-color: var(--vh-success);
background: rgba(16, 185, 129, 0.1);
}
/* Action Buttons */
.vh-btn {
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.vh-btn-primary {
background: var(--vh-gradient);
color: white;
}
.vh-btn-primary:hover {
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
transform: translateY(-1px);
}
.vh-btn-secondary {
background: var(--vh-light);
color: #ccc;
border: 1px solid var(--vh-border);
}
.vh-btn-secondary:hover {
background: var(--vh-border);
}
.vh-btn-danger {
background: var(--vh-danger);
color: white;
}
.vh-btn-danger:hover {
background: #dc2626;
}
.vh-btn-icon {
font-size: 14px;
}
/* Form Elements */
.vh-form-group {
margin-bottom: 20px;
}
.vh-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #ccc;
font-size: 14px;
}
.vh-input {
width: 100%;
padding: 10px 14px;
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 6px;
color: #fff;
font-size: 14px;
transition: border-color 0.2s;
}
.vh-input:focus {
outline: none;
border-color: var(--vh-primary);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1);
}
.vh-select {
width: 100%;
padding: 10px 14px;
background: var(--vh-light);
border: 1px solid var(--vh-border);
border-radius: 6px;
color: #fff;
font-size: 14px;
cursor: pointer;
}
/* Info Boxes */
.vh-info-box {
background: var(--vh-light);
border-left: 4px solid var(--vh-info);
border-radius: 6px;
padding: 16px;
margin-bottom: 20px;
display: flex;
gap: 12px;
}
.vh-info-box.warning {
border-left-color: var(--vh-warning);
}
.vh-info-box.danger {
border-left-color: var(--vh-danger);
}
.vh-info-box.success {
border-left-color: var(--vh-success);
}
.vh-info-icon {
font-size: 24px;
flex-shrink: 0;
}
.vh-info-content {
flex: 1;
}
.vh-info-title {
font-weight: 600;
color: #fff;
margin-bottom: 4px;
}
.vh-info-text {
font-size: 13px;
color: #999;
line-height: 1.5;
}
/* Responsive */
@media (max-width: 768px) {
.vhost-stats {
grid-template-columns: 1fr;
}
.service-templates {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.vhost-details {
grid-template-columns: 1fr;
}
.redirect-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.redirect-route {
flex-direction: column;
align-items: flex-start;
}
.redirect-arrow {
transform: rotate(90deg);
}
}
/* Loading State */
.vh-loading {
text-align: center;
padding: 40px;
color: #999;
}
.vh-loading::before {
content: "⚙️";
font-size: 48px;
display: block;
margin-bottom: 16px;
animation: spin 2s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Empty State */
.vh-empty {
text-align: center;
padding: 60px 20px;
color: #666;
}
.vh-empty::before {
content: "🏠";
font-size: 64px;
display: block;
margin-bottom: 16px;
opacity: 0.5;
}
.vh-empty-title {
font-size: 18px;
font-weight: 600;
color: #999;
margin-bottom: 8px;
}
.vh-empty-text {
font-size: 14px;
color: #666;
.vhost-filter-tags .vhost-pill {
padding: 4px 10px;
}

View File

@ -0,0 +1,32 @@
'use strict';
'require baseclass';
return baseclass.extend({
tabs: [
{ id: 'overview', icon: '📊', label: _('Overview'), path: ['admin', 'secubox', 'services', 'vhosts', 'overview'] },
{ id: 'vhosts', icon: '🗂️', label: _('Virtual Hosts'), path: ['admin', 'secubox', 'services', 'vhosts', 'vhosts'] },
{ id: 'internal', icon: '🏠', label: _('Internal Services'), path: ['admin', 'secubox', 'services', 'vhosts', 'internal'] },
{ id: 'certificates', icon: '🔐', label: _('Certificates'), path: ['admin', 'secubox', 'services', 'vhosts', 'certificates'] },
{ id: 'ssl', icon: '⚙️', label: _('SSL/TLS'), path: ['admin', 'secubox', 'services', 'vhosts', 'ssl'] },
{ id: 'redirects', icon: '↪️', label: _('Redirects'), path: ['admin', 'secubox', 'services', 'vhosts', 'redirects'] },
{ id: 'logs', icon: '📜', label: _('Logs'), path: ['admin', 'secubox', 'services', 'vhosts', 'logs'] }
],
renderTabs: function(active) {
return E('div', { 'class': 'sh-nav-tabs vhost-nav-tabs' },
this.getTabs().map(function(tab) {
return E('a', {
'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''),
'href': L.url.apply(L, tab.path)
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
})
);
},
getTabs: function() {
return this.tabs.slice();
}
});

View File

@ -2,163 +2,213 @@
'require view';
'require ui';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
function normalizeCerts(payload) {
if (Array.isArray(payload))
return payload;
if (payload && Array.isArray(payload.certificates))
return payload.certificates;
return [];
}
function daysUntil(dateStr) {
if (!dateStr)
return null;
var ts = Date.parse(dateStr);
if (isNaN(ts))
return null;
return Math.round((ts - Date.now()) / (1000 * 60 * 60 * 24));
}
function formatDate(dateStr) {
if (!dateStr)
return _('N/A');
try {
return new Date(dateStr).toLocaleString();
} catch (err) {
return dateStr;
}
}
return L.view.extend({
load: function() {
return Promise.all([
API.listCerts()
API.listCerts(),
API.getStatus()
]);
},
render: function(data) {
var certs = data[0] || [];
var certs = normalizeCerts(data[0]);
var status = data[1] || {};
var v = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('SSL Certificates')),
E('div', { 'class': 'cbi-map-descr' }, _('Manage Let\'s Encrypt SSL certificates'))
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('certificates'),
this.renderHeader(certs, status),
this.renderRequestCard(),
this.renderCertTable(certs)
]);
// Request new certificate section
var requestSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Request New Certificate')),
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Domain')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'class': 'cbi-input-text',
'id': 'cert-domain',
'placeholder': 'example.com'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Email')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'email',
'class': 'cbi-input-text',
'id': 'cert-email',
'placeholder': 'admin@example.com'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function(ev) {
ev.preventDefault();
var domain = document.getElementById('cert-domain').value;
var email = document.getElementById('cert-email').value;
if (!domain || !email) {
ui.addNotification(null, E('p', _('Domain and email are required')), 'error');
return;
}
ui.addNotification(null, E('p', _('Requesting certificate... This may take a few minutes.')), 'info');
API.requestCert(domain, email).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + _('Certificate obtained successfully')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', '✗ ' + result.message), 'error');
}
});
}
}, _('Request Certificate'))
])
])
])
]);
v.appendChild(requestSection);
// Certificates list
if (certs.length > 0) {
var certsSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Installed Certificates'))
]);
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Domain')),
E('th', { 'class': 'th' }, _('Issuer')),
E('th', { 'class': 'th' }, _('Expires')),
E('th', { 'class': 'th' }, _('Actions'))
])
]);
certs.forEach(function(cert) {
var expiresDate = new Date(cert.expires);
var daysLeft = Math.floor((expiresDate - new Date()) / (1000 * 60 * 60 * 24));
var expiresColor = daysLeft < 7 ? 'red' : (daysLeft < 30 ? 'orange' : 'green');
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, cert.domain),
E('td', { 'class': 'td' }, cert.issuer || 'N/A'),
E('td', { 'class': 'td' }, [
E('span', { 'style': 'color: ' + expiresColor }, cert.expires),
E('br'),
E('small', {}, daysLeft + ' ' + _('days remaining'))
]),
E('td', { 'class': 'td' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function(ev) {
ui.showModal(_('Certificate Details'), [
E('div', { 'class': 'cbi-section' }, [
E('p', {}, [
E('strong', {}, _('Domain: ')),
E('span', {}, cert.domain)
]),
E('p', {}, [
E('strong', {}, _('Subject: ')),
E('span', {}, cert.subject || 'N/A')
]),
E('p', {}, [
E('strong', {}, _('Issuer: ')),
E('span', {}, cert.issuer || 'N/A')
]),
E('p', {}, [
E('strong', {}, _('Expires: ')),
E('span', {}, cert.expires)
]),
E('p', {}, [
E('strong', {}, _('File: ')),
E('code', {}, cert.cert_file)
])
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Close'))
])
]);
}
}, _('Details'))
])
]));
});
certsSection.appendChild(table);
v.appendChild(certsSection);
} else {
v.appendChild(E('div', { 'class': 'cbi-section' }, [
E('p', { 'style': 'font-style: italic; text-align: center; padding: 20px' },
_('No SSL certificates installed'))
]));
}
return v;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
renderHeader: function(certs, status) {
var expiringSoon = certs.filter(function(cert) {
var days = daysUntil(cert.expires);
return days !== null && days <= 30;
}).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🔐'),
_('SSL Certificates')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Request Let\'s Encrypt certificates and monitor expiry across all proxies.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStatBadge(certs.length, _('Installed')),
this.renderStatBadge(expiringSoon, _('Expiring < 30d')),
this.renderStatBadge(status.acme_available ? _('ACME Ready') : _('ACME Missing'), _('Automation')),
this.renderStatBadge(status.nginx_running ? _('Nginx OK') : _('Nginx down'), _('Web server'))
])
]);
},
renderStatBadge: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderRequestCard: function() {
var domainInput = E('input', { 'type': 'text', 'placeholder': 'cloud.example.com' });
var emailInput = E('input', { 'type': 'email', 'placeholder': 'admin@example.com' });
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🪄', _('Request Certificate')]),
E('p', { 'class': 'vhost-card-meta' }, _('Issue a Let\'s Encrypt certificate using HTTP-01 validation.')),
E('div', { 'class': 'vhost-form-grid' }, [
E('div', {}, [
E('label', {}, _('Domain')),
domainInput
]),
E('div', {}, [
E('label', {}, _('Contact Email')),
emailInput
])
]),
E('div', { 'class': 'vhost-actions' }, [
E('button', {
'class': 'sh-btn-primary',
'click': this.requestCert.bind(this, domainInput, emailInput)
}, _('Request certificate'))
])
]);
},
renderCertTable: function(certs) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['📋', _('Installed Certificates')]),
certs.length ? E('table', { 'class': 'vhost-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('Domain')),
E('th', {}, _('Issuer')),
E('th', {}, _('Expires')),
E('th', {}, _('Status')),
E('th', {}, _('Actions'))
])),
E('tbody', {},
certs.map(this.renderCertRow, this))
]) : E('div', { 'class': 'vhost-empty' }, _('No certificates issued yet.'))
]);
},
renderCertRow: function(cert) {
var days = daysUntil(cert.expires);
var pill = 'success';
var label = _('Valid');
if (days === null) {
pill = 'danger';
label = _('Unknown');
} else if (days <= 7) {
pill = 'danger';
label = _('Expiring in %d days').format(days);
} else if (days <= 30) {
pill = 'warn';
label = _('Renew soon (%d days)').format(days);
}
return E('tr', {}, [
E('td', {}, cert.domain),
E('td', {}, cert.issuer || _('Unknown')),
E('td', {}, formatDate(cert.expires)),
E('td', {}, E('span', { 'class': 'vhost-pill ' + pill }, label)),
E('td', {}, E('button', {
'class': 'cbi-button cbi-button-action',
'click': function(ev) {
ev.preventDefault();
ui.showModal(_('Certificate Details'), [
E('p', {}, [
E('strong', {}, _('Domain: ')),
E('span', {}, cert.domain)
]),
E('p', {}, [
E('strong', {}, _('Subject: ')),
E('span', {}, cert.subject || _('Unknown'))
]),
E('p', {}, [
E('strong', {}, _('Issuer: ')),
E('span', {}, cert.issuer || _('Unknown'))
]),
E('p', {}, [
E('strong', {}, _('Expires: ')),
E('span', {}, formatDate(cert.expires))
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-neutral',
'click': ui.hideModal
}, _('Close'))
])
]);
}
}, _('Details')))
]);
},
requestCert: function(domainInput, emailInput, ev) {
if (ev)
ev.preventDefault();
var domain = domainInput.value.trim();
var email = emailInput.value.trim();
if (!domain || !email) {
ui.addNotification(null, E('p', _('Domain and email are required')), 'error');
return;
}
ui.addNotification(null, E('p', _('Requesting certificate... This may take a few minutes.')), 'info');
API.requestCert(domain, email).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', '✓ ' + _('Certificate obtained successfully')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', '✗ ' + (result.message || _('Request failed'))), 'error');
}
});
}
});

View File

@ -1,34 +1,95 @@
'use strict';
'require view';
'require vhost-manager.api as api';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
var SERVICES = [
{ icon: '🖥️', name: _('LuCI UI'), domain: 'router.local', backend: 'http://127.0.0.1:80', category: _('Core'), description: _('Expose the management UI behind nginx with optional SSL and auth.') },
{ icon: '📈', name: _('Netdata'), domain: 'metrics.local', backend: 'http://127.0.0.1:19999', category: _('Monitoring'), description: _('High-resolution telemetry for CPU, memory, and interfaces.') },
{ icon: '🛡️', name: _('CrowdSec'), domain: 'crowdsec.local', backend: 'http://127.0.0.1:8080', category: _('Security'), description: _('Review bouncer decisions and live intrusion alerts.') },
{ icon: '🏠', name: _('Home Assistant'), domain: 'home.local', backend: 'http://192.168.1.13:8123', category: _('Automation'), description: _('Publish your smart-home UI securely with SSL and auth.') },
{ icon: '🎬', name: _('Media Server'), domain: 'media.local', backend: 'http://192.168.1.12:8096', category: _('Entertainment'), description: _('Jellyfin or Plex front-end available via a friendly hostname.') },
{ icon: '🗄️', name: _('Nextcloud'), domain: 'cloud.local', backend: 'http://192.168.1.20:80', category: _('Productivity'), description: _('Bring private SaaS back on-prem with HTTPS and caching headers.') }
];
return view.extend({
load: function() { return api.getInternalHosts(); },
render: function(data) {
var hosts = data.hosts || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🏠 Internal Virtual Hosts'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Self-hosted services accessible from your local network.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Service'),
E('th', {style:'padding:12px'}, 'Domain'),
E('th', {style:'padding:12px'}, 'Backend'),
E('th', {style:'padding:12px'}, 'SSL'),
E('th', {style:'padding:12px'}, 'Status')
])
].concat(hosts.map(function(h) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-weight:600'}, h.name),
E('td', {style:'padding:12px;font-family:monospace;color:#10b981'}, h.domain),
E('td', {style:'padding:12px;font-family:monospace;color:#64748b'}, h.backend),
E('td', {style:'padding:12px;text-align:center'}, h.ssl ? '🔒' : '🔓'),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(h.enabled?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, h.enabled ? 'Active' : 'Disabled'))
]);
})))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
load: function() {
return Promise.all([
API.listVHosts()
]);
},
render: function(data) {
var vhosts = data[0] || [];
var active = {};
vhosts.forEach(function(v) {
active[v.domain] = true;
});
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('internal'),
this.renderHeader(vhosts),
this.renderServices(active)
]);
},
renderHeader: function(vhosts) {
var configured = vhosts.filter(function(vhost) {
return SERVICES.some(function(s) { return s.domain === vhost.domain; });
}).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🏠'),
_('Internal Service Catalog')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Pre-built recipes for publishing popular LAN services with SSL, auth, and redirects.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStat(SERVICES.length, _('Templates')),
this.renderStat(configured, _('Configured'))
])
]);
},
renderStat: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderServices: function(active) {
return E('div', { 'class': 'vhost-card-grid' },
SERVICES.map(function(service) {
var isActive = !!active[service.domain];
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, [service.icon, service.name]),
E('div', { 'class': 'vhost-card-meta' }, service.category),
E('p', { 'class': 'vhost-card-meta' }, service.description),
E('div', { 'class': 'vhost-card-meta' }, _('Domain: %s').format(service.domain)),
E('div', { 'class': 'vhost-card-meta' }, _('Backend: %s').format(service.backend)),
E('div', { 'class': 'vhost-actions' }, [
E('span', { 'class': 'vhost-pill ' + (isActive ? 'success' : '') },
isActive ? _('Published') : _('Not configured')),
E('a', {
'class': 'sh-btn-secondary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'vhosts')
}, isActive ? _('Manage') : _('Create'))
])
]);
})
);
}
});

View File

@ -1,7 +1,14 @@
'use strict';
'require view';
'require form';
'require ui';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return L.view.extend({
load: function() {
@ -11,58 +18,122 @@ return L.view.extend({
},
render: function(data) {
var vhosts = data[0] || [];
this.vhosts = data[0] || [];
this.domainSelect = this.createDomainSelect();
this.lineSelect = this.createLineSelect();
this.logOutput = E('pre', { 'class': 'vhost-log-terminal' }, _('Select a domain to view logs'));
var m = new form.Map('vhost_manager', _('Access Logs'),
_('View nginx access logs for virtual hosts'));
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('logs'),
this.renderHeader(),
this.renderControls()
]);
},
var s = m.section(form.NamedSection, '__logs', 'logs');
s.anonymous = true;
s.addremove = false;
renderHeader: function() {
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '📜'),
_('Access Logs')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Tail nginx access logs per virtual host without SSHing into the router.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStat(this.vhosts.length, _('Domains')),
this.renderStat('50-500', _('Lines'))
])
]);
},
var o;
renderStat: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
o = s.option(form.ListValue, 'domain', _('Select Domain'));
o.rmempty = false;
vhosts.forEach(function(vhost) {
o.value(vhost.domain, vhost.domain);
});
if (vhosts.length === 0) {
o.value('', _('No virtual hosts configured'));
renderControls: function() {
if (!this.vhosts.length) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🪵', _('Logs')]),
E('div', { 'class': 'vhost-empty' }, _('No virtual hosts configured yet, logs unavailable.'))
]);
}
o = s.option(form.ListValue, 'lines', _('Number of Lines'));
o.value('50', '50');
o.value('100', '100');
o.value('200', '200');
o.value('500', '500');
o.default = '50';
this.domainSelect.addEventListener('change', this.fetchLogs.bind(this));
this.lineSelect.addEventListener('change', this.fetchLogs.bind(this));
s.render = L.bind(function(view, section_id) {
var domain = this.section.formvalue(section_id, 'domain');
var lines = parseInt(this.section.formvalue(section_id, 'lines')) || 50;
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🧾', _('Viewer')]),
E('div', { 'class': 'vhost-form-grid' }, [
E('div', {}, [
E('label', {}, _('Domain')),
this.domainSelect
]),
E('div', {}, [
E('label', {}, _('Lines')),
this.lineSelect
])
]),
E('div', { 'class': 'vhost-actions' }, [
E('button', {
'class': 'sh-btn-secondary',
'click': this.fetchLogs.bind(this)
}, _('Refresh'))
]),
this.logOutput
]);
},
if (!domain || vhosts.length === 0) {
return E('div', { 'class': 'cbi-section' }, [
E('p', { 'style': 'font-style: italic' }, _('No virtual hosts to display logs for'))
]);
}
createDomainSelect: function() {
var select = E('select', {});
this.vhosts.forEach(function(vhost, idx) {
select.appendChild(E('option', {
'value': vhost.domain,
'selected': idx === 0
}, vhost.domain));
});
return select;
},
return API.getAccessLogs(domain, lines).then(L.bind(function(data) {
var logs = data.logs || [];
createLineSelect: function() {
var select = E('select', {});
[50, 100, 200, 500].forEach(function(value) {
select.appendChild(E('option', {
'value': value,
'selected': value === 50
}, value.toString()));
});
return select;
},
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Access Logs for: ') + domain),
E('pre', {
'style': 'background: #000; color: #0f0; padding: 10px; overflow: auto; max-height: 500px; font-size: 11px; font-family: monospace'
}, logs.length > 0 ? logs.join('\n') : _('No logs available'))
]);
}, this));
}, this, this);
fetchLogs: function(ev) {
if (ev)
ev.preventDefault();
return m.render();
var domain = this.domainSelect.value;
var lines = parseInt(this.lineSelect.value, 10) || 50;
if (!domain) {
this.logOutput.textContent = _('Select a domain to view logs');
return Promise.resolve();
}
this.logOutput.textContent = _('Loading logs for %s ...').format(domain);
return API.getAccessLogs(domain, lines).then(function(result) {
var logs = (result.logs || []).join('\n');
this.logOutput.textContent = logs || _('No logs available');
}.bind(this)).catch(function(err) {
var message = err && err.message ? err.message : _('Unable to load logs');
ui.addNotification(null, E('p', message), 'error');
this.logOutput.textContent = message;
}.bind(this));
},
handleSaveApply: null,

View File

@ -1,10 +1,42 @@
'use strict';
'require view';
'require poll';
'require ui';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
return L.view.extend({
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
function normalizeCerts(payload) {
if (Array.isArray(payload))
return payload;
if (payload && Array.isArray(payload.certificates))
return payload.certificates;
return [];
}
function formatDate(value) {
if (!value)
return _('N/A');
try {
return new Date(value).toLocaleDateString();
} catch (err) {
return value;
}
}
function daysUntil(dateStr) {
if (!dateStr)
return null;
var ts = Date.parse(dateStr);
if (isNaN(ts))
return null;
return Math.round((ts - Date.now()) / (1000 * 60 * 60 * 24));
}
return view.extend({
load: function() {
return Promise.all([
API.getStatus(),
@ -16,118 +48,172 @@ return L.view.extend({
render: function(data) {
var status = data[0] || {};
var vhosts = data[1] || [];
var certs = data[2] || [];
var certs = normalizeCerts(data[2]);
var v = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('VHost Manager - Overview')),
E('div', { 'class': 'cbi-map-descr' }, _('Nginx reverse proxy and SSL certificate management'))
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('overview'),
this.renderHeader(status, vhosts, certs),
this.renderHealth(status),
this.renderVhostTable(vhosts, certs),
this.renderCertWatch(certs)
]);
// Status section
var statusSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('System Status')),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left', 'width': '33%' }, [
E('strong', {}, _('Nginx: ')),
E('span', {}, status.nginx_running ?
E('span', { 'style': 'color: green' }, '● ' + _('Running')) :
E('span', { 'style': 'color: red' }, '● ' + _('Stopped'))
),
E('br'),
E('small', {}, _('Version: ') + (status.nginx_version || 'unknown'))
]),
E('div', { 'class': 'td left', 'width': '33%' }, [
E('strong', {}, _('ACME/SSL: ')),
E('span', {}, status.acme_available ?
E('span', { 'style': 'color: green' }, '✓ ' + _('Available')) :
E('span', { 'style': 'color: orange' }, '✗ ' + _('Not installed'))
),
E('br'),
E('small', {}, status.acme_version || 'N/A')
]),
E('div', { 'class': 'td left', 'width': '33%' }, [
E('strong', {}, _('Virtual Hosts: ')),
E('span', { 'style': 'font-size: 1.5em; color: #0088cc' }, String(status.vhost_count || 0))
])
])
])
]);
v.appendChild(statusSection);
// Quick stats
var sslCount = 0;
var authCount = 0;
var wsCount = 0;
vhosts.forEach(function(vhost) {
if (vhost.ssl) sslCount++;
if (vhost.auth) authCount++;
if (vhost.websocket) wsCount++;
});
var statsSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Virtual Hosts Summary')),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td left', 'width': '25%' }, [
E('strong', {}, '🔒 SSL Enabled: '),
E('span', {}, String(sslCount))
]),
E('div', { 'class': 'td left', 'width': '25%' }, [
E('strong', {}, '🔐 Auth Protected: '),
E('span', {}, String(authCount))
]),
E('div', { 'class': 'td left', 'width': '25%' }, [
E('strong', {}, '🔌 WebSocket: '),
E('span', {}, String(wsCount))
]),
E('div', { 'class': 'td left', 'width': '25%' }, [
E('strong', {}, '📜 Certificates: '),
E('span', {}, String(certs.length))
])
])
])
]);
v.appendChild(statsSection);
// Recent vhosts
if (vhosts.length > 0) {
var vhostSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Virtual Hosts'))
]);
var table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Domain')),
E('th', { 'class': 'th' }, _('Backend')),
E('th', { 'class': 'th' }, _('Features')),
E('th', { 'class': 'th' }, _('SSL Expires'))
])
]);
vhosts.slice(0, 10).forEach(function(vhost) {
var features = [];
if (vhost.ssl) features.push('🔒 SSL');
if (vhost.auth) features.push('🔐 Auth');
if (vhost.websocket) features.push('🔌 WS');
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, vhost.domain),
E('td', { 'class': 'td' }, vhost.backend),
E('td', { 'class': 'td' }, features.join(' ')),
E('td', { 'class': 'td' }, vhost.ssl_expires || 'N/A')
]));
});
vhostSection.appendChild(table);
v.appendChild(vhostSection);
}
return v;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
renderHeader: function(status, vhosts, certs) {
var sslEnabled = vhosts.filter(function(v) { return v.ssl; }).length;
var expiringSoon = certs.filter(function(cert) {
var days = daysUntil(cert.expires);
return days !== null && days <= 30;
}).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🌐'),
_('VHost Manager')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Reverse proxy, SSL automation and hardened headers for SecuBox deployments.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStatBadge(status.vhost_count || vhosts.length, _('Virtual Hosts')),
this.renderStatBadge(sslEnabled, _('TLS Enabled')),
this.renderStatBadge(expiringSoon, _('Expiring Certs'))
])
]);
},
renderStatBadge: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderHealth: function(status) {
var items = [
{ label: _('Nginx'), value: status.nginx_running ? _('Running') : _('Stopped'),
pill: status.nginx_running ? 'success' : 'danger' },
{ label: _('Version'), value: status.nginx_version || _('Unknown') },
{ label: _('ACME'), value: status.acme_available ? _('Available') : _('Missing'),
pill: status.acme_available ? 'success' : 'warn' }
];
return E('div', { 'class': 'vhost-card-grid' }, [
E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🧭', _('Control Center')]),
E('p', { 'class': 'vhost-card-meta' },
_('Quick navigation to key areas.')),
E('div', { 'class': 'vhost-actions' }, [
E('a', {
'class': 'sh-btn-primary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'vhosts')
}, _('Manage VHosts')),
E('a', {
'class': 'sh-btn-secondary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'certificates')
}, _('Certificates')),
E('a', {
'class': 'sh-btn-secondary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'logs')
}, _('Access Logs'))
])
]),
E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🩺', _('Runtime Health')]),
E('div', { 'class': 'vhost-status-list' },
items.map(function(item) {
return E('div', { 'class': 'vhost-status-item' }, [
E('span', {}, item.label),
item.pill ? E('span', { 'class': 'vhost-pill ' + item.pill }, item.value) :
E('strong', {}, item.value)
]);
})
)
])
]);
},
renderVhostTable: function(vhosts, certs) {
var certMap = {};
certs.forEach(function(cert) {
certMap[cert.domain] = cert;
});
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['📁', _('Published Domains')]),
vhosts.length ? E('table', { 'class': 'vhost-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('Domain')),
E('th', {}, _('Backend')),
E('th', {}, _('Features')),
E('th', {}, _('Certificate'))
])),
E('tbody', {},
vhosts.map(function(vhost) {
var cert = certMap[vhost.domain];
var features = [
vhost.ssl ? _('SSL') : null,
vhost.auth ? _('Auth') : null,
vhost.websocket ? _('WebSocket') : null
].filter(Boolean);
return E('tr', {}, [
E('td', {}, vhost.domain || _('Unnamed')),
E('td', { 'class': 'vhost-card-meta' }, vhost.backend || '-'),
E('td', {}, features.length ? features.join(' · ') : _('None')),
E('td', {}, cert ? formatDate(cert.expires) : _('No cert'))
]);
})
)
]) : E('div', { 'class': 'vhost-empty' }, _('No virtual hosts configured yet.'))
]);
},
renderCertWatch: function(certs) {
if (!certs.length)
return '';
var top = certs.slice().sort(function(a, b) {
return (Date.parse(a.expires) || 0) - (Date.parse(b.expires) || 0);
}).slice(0, 3);
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['⏳', _('Certificate Watchlist')]),
E('div', { 'class': 'vhost-card-grid' },
top.map(function(cert) {
var days = daysUntil(cert.expires);
var pill = 'success';
var label = _('Valid');
if (days === null) {
pill = 'danger';
label = _('Unknown expiry');
} else if (days <= 7) {
pill = 'danger';
label = _('Expiring in %d days').format(days);
} else if (days <= 30) {
pill = 'warn';
label = _('Renew in %d days').format(days);
}
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🔐', cert.domain]),
E('div', { 'class': 'vhost-card-meta' }, cert.issuer || _('Unknown issuer')),
E('div', { 'class': 'vhost-card-meta' }, formatDate(cert.expires)),
E('span', { 'class': 'vhost-pill ' + pill }, label)
]);
})
),
E('div', { 'class': 'vhost-actions' }, [
E('a', {
'class': 'sh-btn-secondary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'certificates')
}, _('View certificates'))
])
]);
}
});

View File

@ -1,29 +1,86 @@
'use strict';
'require view';
'require vhost-manager.api as api';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
var RULES = [
{ icon: '☁️', name: _('Nextcloud → LAN'), from: 'cloud.example.com', to: 'https://nextcloud.lan', code: '301', description: _('Force remote users towards the LAN-hosted Nextcloud instance when DNS interception is active.') },
{ icon: '🕹️', name: _('Steam CDN cache'), from: '*.cdn.steamstatic.com', to: 'http://steamcache.lan', code: '302', description: _('Redirect bulky downloads to an on-prem cache appliance to save WAN bandwidth.') },
{ icon: '📺', name: _('YouTube → Invidious'), from: 'youtube.com/*', to: 'https://invidious.lan', code: '307', description: _('Privacy-friendly redirect of YouTube links to your Invidious deployment.') },
{ icon: '📮', name: _('Mail failover'), from: 'mail.example.com', to: 'https://mx-backup.lan', code: '302', description: _('Gracefully fail over SaaS webmail to an alternate local service during outages.') }
];
return view.extend({
load: function() { return api.getRedirects(); },
render: function(data) {
var redirects = data.redirects || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '↪️ External Service Redirects'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Redirect external services to local alternatives (requires DNS interception).'),
E('div', {style:'display:grid;gap:16px'}, redirects.map(function(r) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;opacity:'+(r.enabled?'1':'0.5')}, [
E('div', {style:'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px'}, [
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, r.name),
E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(r.enabled?'#f59e0b20;color:#f59e0b':'#64748b20;color:#64748b')}, r.enabled ? 'Active' : 'Disabled')
]),
E('div', {style:'color:#94a3b8;font-size:13px;margin-bottom:12px'}, r.description),
E('div', {style:'display:flex;align-items:center;gap:16px;padding:12px;background:#0f172a;border-radius:8px'}, [
E('span', {style:'font-family:monospace;color:#ef4444;text-decoration:line-through'}, r.external_domain),
E('span', {style:'font-size:20px'}, '→'),
E('span', {style:'font-family:monospace;color:#10b981'}, r.local_domain)
])
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
load: function() {
return Promise.all([
API.listVHosts()
]);
},
render: function(data) {
var vhosts = data[0] || [];
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('redirects'),
this.renderHeader(vhosts),
this.renderTemplates()
]);
},
renderHeader: function(vhosts) {
var redirectCount = vhosts.filter(function(vhost) {
return vhost.backend && vhost.backend.indexOf('return ') === 0;
}).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '↪️'),
_('Redirect Rules')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Build captive portal style redirects and clean vanity links from a central place.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStat(RULES.length, _('Templates')),
this.renderStat(redirectCount, _('Active'))
])
]);
},
renderStat: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderTemplates: function() {
return E('div', { 'class': 'vhost-card-grid' },
RULES.map(function(rule) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, [rule.icon, rule.name]),
E('p', { 'class': 'vhost-card-meta' }, rule.description),
E('div', { 'class': 'vhost-card-meta' }, _('From: %s').format(rule.from)),
E('div', { 'class': 'vhost-card-meta' }, _('To: %s').format(rule.to)),
E('div', { 'class': 'vhost-actions' }, [
E('span', { 'class': 'vhost-pill' }, _('HTTP %s').format(rule.code)),
E('a', {
'class': 'sh-btn-secondary',
'href': L.url('admin', 'secubox', 'services', 'vhosts', 'vhosts')
}, _('Create rule'))
])
]);
})
);
}
});

View File

@ -1,30 +1,157 @@
'use strict';
'require view';
'require vhost-manager.api as api';
'require ui';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return view.extend({
load: function() { return api.getCertificates(); },
render: function(data) {
var certs = data.certificates || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🔒 SSL Certificates'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Manage SSL/TLS certificates for your virtual hosts.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
certs.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Domain'),
E('th', {style:'padding:12px'}, 'Expiry'),
E('th', {style:'padding:12px'}, 'Status')
])
].concat(certs.map(function(c) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-family:monospace'}, c.domain),
E('td', {style:'padding:12px;color:#94a3b8'}, c.expiry || 'Unknown'),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e'}, 'Valid'))
]);
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No certificates found')
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
load: function() {
return Promise.all([
API.getStatus()
]);
},
render: function(data) {
var status = data[0] || {};
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('ssl'),
this.renderHeader(status),
this.renderBaseline(),
this.renderHeaders(),
this.renderActions(status)
]);
},
renderHeader: function(status) {
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
_('SSL / TLS Configuration')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Baseline cipher suites, headers, and reload helpers for hardened deployments.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStat(_('TLS1.2+'), _('Min version')),
this.renderStat(_('OCSP stapling'), _('Status')),
this.renderStat(status.nginx_running ? _('Running') : _('Stopped'), _('nginx'))
])
]);
},
renderStat: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderBaseline: function() {
var snippets = [
{
icon: '🔐',
title: _('TLS Versions'),
body: [
'ssl_protocols TLSv1.2 TLSv1.3;',
'ssl_prefer_server_ciphers on;'
],
note: _('Disable legacy TLSv1.0/1.1 to prevent downgrade attacks.')
},
{
icon: '🧮',
title: _('Cipher Suites'),
body: [
'ssl_ciphers \'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256\';'
],
note: _('Prefer AEAD/GCM suites that provide forward secrecy.')
},
{
icon: '🧷',
title: _('HSTS Policy'),
body: [
'add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;'
],
note: _('Force HTTPS everywhere and preload in browsers.')
},
{
icon: '📡',
title: _('OCSP Stapling'),
body: [
'ssl_stapling on;',
'ssl_stapling_verify on;'
],
note: _('Cache CA responses to speed up TLS handshakes.')
}
];
return E('div', { 'class': 'vhost-card-grid' },
snippets.map(function(item) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, [item.icon, item.title]),
E('pre', { 'class': 'vhost-card-meta' }, item.body.join('\n')),
E('p', { 'class': 'vhost-card-meta' }, item.note)
]);
})
);
},
renderHeaders: function() {
var headers = [
{ title: 'Content-Security-Policy', desc: _('Restrict scripts, frames, and media to vetted origins. Example: default-src \'self\'.') },
{ title: 'Permissions-Policy', desc: _('Opt-in sensors (camera, microphone, geolocation) per vhost.') },
{ title: 'Referrer-Policy', desc: _('Use strict-origin-when-cross-origin to reduce leakage.') },
{ title: 'X-Frame-Options', desc: _('Block clickjacking with DENY or SAMEORIGIN.') }
];
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🧱', _('Security Headers')]),
E('div', { 'class': 'vhost-status-list' },
headers.map(function(header) {
return E('div', { 'class': 'vhost-status-item' }, [
E('strong', {}, header.title),
E('span', { 'class': 'vhost-card-meta' }, header.desc)
]);
})
)
]);
},
renderActions: function(status) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🔄', _('Apply configuration')]),
E('p', { 'class': 'vhost-card-meta' }, _('After updating snippets in /etc/nginx/conf.d include files, reload nginx to apply safely.')),
E('div', { 'class': 'vhost-actions' }, [
E('span', { 'class': 'vhost-pill ' + (status.nginx_running ? 'success' : 'danger') },
status.nginx_running ? _('nginx running') : _('nginx stopped')),
E('button', {
'class': 'sh-btn-primary',
'click': this.reloadNginx
}, _('Reload nginx'))
])
]);
},
reloadNginx: function(ev) {
ev.preventDefault();
ui.addNotification(null, E('p', _('Reloading nginx...')), 'info');
API.reloadNginx().then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', _('Nginx reloaded successfully')), 'info');
} else {
ui.addNotification(null, E('p', '✗ ' + (result.message || _('Reload failed'))), 'error');
}
});
}
});

View File

@ -3,25 +3,66 @@
'require ui';
'require form';
'require vhost-manager/api as API';
'require secubox-theme/theme as Theme';
'require vhost-manager/ui as VHostUI';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
function normalizeCerts(payload) {
if (Array.isArray(payload))
return payload;
if (payload && Array.isArray(payload.certificates))
return payload.certificates;
return [];
}
function formatDate(value) {
if (!value)
return _('N/A');
try {
return new Date(value).toLocaleDateString();
} catch (err) {
return value;
}
}
return L.view.extend({
load: function() {
return Promise.all([
API.listVHosts()
API.listVHosts(),
API.listCerts()
]);
},
render: function(data) {
var vhosts = data[0] || [];
var certs = normalizeCerts(data[1]);
var m = new form.Map('vhost_manager', _('Virtual Hosts'),
_('Manage nginx reverse proxy virtual hosts'));
var m = this.buildForm();
return E('div', { 'class': 'vhost-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('vhost-manager/dashboard.css') }),
VHostUI.renderTabs('vhosts'),
this.renderHeader(vhosts),
this.renderList(vhosts, certs),
E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['📝', _('Virtual Host Form')]),
m.render()
])
]);
},
buildForm: function() {
var m = new form.Map('vhost_manager', null, null);
var s = m.section(form.GridSection, 'vhost', _('Virtual Hosts'));
s.anonymous = false;
s.addremove = true;
s.sortable = true;
s.modaltitle = function(section_id) {
return _('Edit VHost: ') + section_id;
};
@ -30,32 +71,27 @@ return L.view.extend({
o = s.option(form.Value, 'domain', _('Domain'));
o.rmempty = false;
o.placeholder = 'example.com';
o.description = _('Domain name for this virtual host');
o.placeholder = 'app.example.com';
o.description = _('Public hostname for this proxy.');
o = s.option(form.Value, 'backend', _('Backend URL'));
o.rmempty = false;
o.placeholder = 'http://192.168.1.100:8080';
o.description = _('Backend server URL to proxy to');
o.description = _('Upstream origin (HTTP/HTTPS/WebSocket).');
// Test backend button
o.renderWidget = function(section_id, option_index, cfgvalue) {
var widget = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
var testBtn = E('button', {
'class': 'cbi-button cbi-button-action',
'style': 'margin-left: 10px',
'click': function(ev) {
ev.preventDefault();
var backend = this.parentNode.querySelector('input').value;
if (!backend) {
ui.addNotification(null, E('p', _('Please enter a backend URL')), 'warning');
return;
}
ui.addNotification(null, E('p', _('Testing backend connectivity...')), 'info');
API.testBackend(backend).then(function(result) {
if (result.reachable) {
ui.addNotification(null, E('p', '✓ ' + _('Backend is reachable')), 'info');
@ -65,22 +101,20 @@ return L.view.extend({
});
}
}, _('Test'));
widget.appendChild(testBtn);
return widget;
};
o = s.option(form.Flag, 'ssl', _('Enable SSL'));
o.default = o.disabled;
o.description = _('Enable HTTPS (requires valid SSL certificate)');
o.description = _('Serve HTTPS (requires certificate).');
o = s.option(form.Flag, 'auth', _('Enable Authentication'));
o.default = o.disabled;
o.description = _('Require HTTP basic authentication');
o.description = _('Protect with HTTP basic auth.');
o = s.option(form.Value, 'auth_user', _('Auth Username'));
o.depends('auth', '1');
o.placeholder = 'admin';
o = s.option(form.Value, 'auth_pass', _('Auth Password'));
o.depends('auth', '1');
@ -88,11 +122,9 @@ return L.view.extend({
o = s.option(form.Flag, 'websocket', _('WebSocket Support'));
o.default = o.disabled;
o.description = _('Enable WebSocket protocol upgrade headers');
o.description = _('Forward upgrade headers for WS backends.');
// Custom actions
s.addModalOptions = function(s, section_id, ev) {
// Get form values
s.addModalOptions = function(s, section_id) {
var domain = this.section.formvalue(section_id, 'domain');
var backend = this.section.formvalue(section_id, 'backend');
var ssl = this.section.formvalue(section_id, 'ssl') === '1';
@ -104,11 +136,10 @@ return L.view.extend({
return;
}
// Call API to add vhost
API.addVHost(domain, backend, ssl, auth, websocket).then(function(result) {
if (result.success) {
ui.addNotification(null, E('p', _('VHost created successfully')), 'info');
if (result.reload_required) {
ui.showModal(_('Reload Nginx?'), [
E('p', {}, _('Configuration changed. Reload nginx to apply?')),
@ -139,6 +170,71 @@ return L.view.extend({
});
};
return m.render();
return m;
},
renderHeader: function(vhosts) {
var sslEnabled = vhosts.filter(function(v) { return v.ssl; }).length;
var authEnabled = vhosts.filter(function(v) { return v.auth; }).length;
var websocketEnabled = vhosts.filter(function(v) { return v.websocket; }).length;
return E('div', { 'class': 'sh-page-header' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🗂️'),
_('Virtual Hosts')
]),
E('p', { 'class': 'sh-page-subtitle' },
_('Publish LAN services through SecuBox with SSL, auth, and WebSocket support.'))
]),
E('div', { 'class': 'sh-stats-grid' }, [
this.renderStatBadge(vhosts.length, _('Defined')),
this.renderStatBadge(sslEnabled, _('TLS')),
this.renderStatBadge(authEnabled, _('Auth')),
this.renderStatBadge(websocketEnabled, _('WebSocket'))
])
]);
},
renderStatBadge: function(value, label) {
return E('div', { 'class': 'sh-stat-badge' }, [
E('div', { 'class': 'sh-stat-value' }, value.toString()),
E('div', { 'class': 'sh-stat-label' }, label)
]);
},
renderList: function(vhosts, certs) {
var certMap = {};
certs.forEach(function(cert) {
certMap[cert.domain] = cert;
});
if (!vhosts.length) {
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['📂', _('Configured VHosts')]),
E('div', { 'class': 'vhost-empty' }, _('No vhosts yet — add your first reverse proxy below.'))
]);
}
return E('div', { 'class': 'vhost-card-grid' },
vhosts.map(function(vhost) {
return this.renderVhostCard(vhost, certMap[vhost.domain]);
}, this)
);
},
renderVhostCard: function(vhost, cert) {
var pills = [];
if (vhost.ssl) pills.push(E('span', { 'class': 'vhost-pill success' }, _('SSL')));
if (vhost.auth) pills.push(E('span', { 'class': 'vhost-pill warn' }, _('Auth')));
if (vhost.websocket) pills.push(E('span', { 'class': 'vhost-pill' }, _('WebSocket')));
return E('div', { 'class': 'vhost-card' }, [
E('div', { 'class': 'vhost-card-title' }, ['🌐', vhost.domain || _('Unnamed')]),
E('div', { 'class': 'vhost-card-meta' }, vhost.backend || _('No backend defined')),
pills.length ? E('div', { 'class': 'vhost-filter-tags' }, pills) : '',
E('div', { 'class': 'vhost-card-meta' },
cert ? _('Certificate expires %s').format(formatDate(cert.expires)) : _('No certificate detected'))
]);
}
});

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}

View File

@ -81,7 +81,7 @@ pre {
}
.sh-page-title {
font-size: 28px;
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
@ -94,7 +94,7 @@ pre {
}
.sh-page-title-icon {
font-size: 32px;
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}