diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 21014706..c4cda34b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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:*)" ] } } diff --git a/DOCS/DEVELOPMENT-GUIDELINES.md b/DOCS/DEVELOPMENT-GUIDELINES.md index d87ff5a6..5544d294 100644 --- a/DOCS/DEVELOPMENT-GUIDELINES.md +++ b/DOCS/DEVELOPMENT-GUIDELINES.md @@ -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' }, [ diff --git a/luci-app-auth-guardian/htdocs/luci-static/resources/auth-guardian/common.css b/luci-app-auth-guardian/htdocs/luci-static/resources/auth-guardian/common.css index aefda44a..b662c04f 100644 --- a/luci-app-auth-guardian/htdocs/luci-static/resources/auth-guardian/common.css +++ b/luci-app-auth-guardian/htdocs/luci-static/resources/auth-guardian/common.css @@ -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; } diff --git a/luci-app-bandwidth-manager/htdocs/luci-static/resources/bandwidth-manager/common.css b/luci-app-bandwidth-manager/htdocs/luci-static/resources/bandwidth-manager/common.css index aefda44a..b662c04f 100644 --- a/luci-app-bandwidth-manager/htdocs/luci-static/resources/bandwidth-manager/common.css +++ b/luci-app-bandwidth-manager/htdocs/luci-static/resources/bandwidth-manager/common.css @@ -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; } diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/common.css b/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/common.css index aefda44a..b662c04f 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/common.css +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/common.css @@ -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; } diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js b/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js new file mode 100644 index 00000000..29fc3ac9 --- /dev/null +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js @@ -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) + ]); + }) + ); + } +}); diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js index 4b73fe8c..265240e6 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js @@ -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) diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js index 8ef188a3..caa85aaa 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js @@ -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') ]), diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js index adfecaf0..b32125a2 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js @@ -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) ]); }, diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js index 78dfa367..82af8fdf 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js @@ -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') diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js index 1623b200..a8ab3c14 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js @@ -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() + ]); } }); diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js index 8e5a670d..d62eb5f3 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js @@ -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) { diff --git a/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/common.css b/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/common.css index aefda44a..b662c04f 100644 --- a/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/common.css +++ b/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/common.css @@ -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; } diff --git a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/common.css b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/common.css index aefda44a..b662c04f 100644 --- a/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/common.css +++ b/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/common.css @@ -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; } diff --git a/luci-app-ksm-manager/htdocs/luci-static/resources/ksm-manager/common.css b/luci-app-ksm-manager/htdocs/luci-static/resources/ksm-manager/common.css index aefda44a..b662c04f 100644 --- a/luci-app-ksm-manager/htdocs/luci-static/resources/ksm-manager/common.css +++ b/luci-app-ksm-manager/htdocs/luci-static/resources/ksm-manager/common.css @@ -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; } diff --git a/luci-app-media-flow/htdocs/luci-static/resources/media-flow/common.css b/luci-app-media-flow/htdocs/luci-static/resources/media-flow/common.css index aefda44a..b662c04f 100644 --- a/luci-app-media-flow/htdocs/luci-static/resources/media-flow/common.css +++ b/luci-app-media-flow/htdocs/luci-static/resources/media-flow/common.css @@ -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; } diff --git a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/common.css b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/common.css index aefda44a..b662c04f 100644 --- a/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/common.css +++ b/luci-app-netdata-dashboard/htdocs/luci-static/resources/netdata-dashboard/common.css @@ -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; } diff --git a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/common.css b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/common.css index aefda44a..b662c04f 100644 --- a/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/common.css +++ b/luci-app-netifyd-dashboard/htdocs/luci-static/resources/netifyd-dashboard/common.css @@ -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; } diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/common.css b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/common.css index aefda44a..b662c04f 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/common.css +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/common.css @@ -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; } diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js index f1c4fd69..c3125667 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js @@ -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({ diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js index 8c63ec0c..67f1a629 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js @@ -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, diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css index aefda44a..b662c04f 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css @@ -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; } diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js new file mode 100644 index 00000000..20157cd0 --- /dev/null +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js @@ -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) + ]); + }) + ); + } +}); diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js index 007efbe4..7ce773ba 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/alerts.js @@ -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'), diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js index ea0c6b2f..008b1a17 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js @@ -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() { diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js index baebd237..b7b83beb 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js @@ -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() { diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js index 6d2177a8..e65b8466 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/monitoring.js @@ -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() diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js index ed728beb..e480014a 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/settings.js @@ -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', {}, [ diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/common.css b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/common.css index bcdedcdb..a1eaef5d 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/common.css +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/common.css @@ -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; } diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js new file mode 100644 index 00000000..9f5b6293 --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js @@ -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) + ]); + }) + ); + } +}); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js index 6f07e83a..2243babb 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js @@ -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(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js index 218959d1..c78b796d 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js @@ -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' }, '๐Ÿงฉ'), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js index e1a182b9..09f1e076 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js @@ -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' }, [ diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js index 03d67adc..f14163d7 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js @@ -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' }, [ diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js index 4189e585..0b76c60a 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js @@ -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'); } }); - diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js index cf452150..12e34a03 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js @@ -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() diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index fca3780d..88e5593d 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -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() { diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js index fdc150bf..7d59ef42 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js @@ -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' }, [ diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js index ba63e6f4..8762aa3b 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js @@ -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' }, diff --git a/luci-app-traffic-shaper/htdocs/luci-static/resources/traffic-shaper/common.css b/luci-app-traffic-shaper/htdocs/luci-static/resources/traffic-shaper/common.css index aefda44a..b662c04f 100644 --- a/luci-app-traffic-shaper/htdocs/luci-static/resources/traffic-shaper/common.css +++ b/luci-app-traffic-shaper/htdocs/luci-static/resources/traffic-shaper/common.css @@ -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; } diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/common.css b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/common.css index aefda44a..b662c04f 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/common.css +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/common.css @@ -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; } diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/dashboard.css b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/dashboard.css index 0d128a4c..0cd48d8c 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/dashboard.css +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/dashboard.css @@ -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; } diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/ui.js b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/ui.js new file mode 100644 index 00000000..a035155c --- /dev/null +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/vhost-manager/ui.js @@ -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(); + } +}); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/certificates.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/certificates.js index 174c351d..00b6600f 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/certificates.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/certificates.js @@ -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'); + } + }); + } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/internal.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/internal.js index 25d04c84..903a5132 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/internal.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/internal.js @@ -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')) + ]) + ]); + }) + ); + } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/logs.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/logs.js index 572bd9bf..3a7cbfc7 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/logs.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/logs.js @@ -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, diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/overview.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/overview.js index eeec0c5c..a9ce21c3 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/overview.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/overview.js @@ -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')) + ]) + ]); + } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/redirects.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/redirects.js index a8171214..9aa334a4 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/redirects.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/redirects.js @@ -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')) + ]) + ]); + }) + ); + } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/ssl.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/ssl.js index fa1c64fe..3b0bbae0 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/ssl.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/ssl.js @@ -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'); + } + }); + } }); diff --git a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js index 562a7a9f..2f83e2f4 100644 --- a/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js +++ b/luci-app-vhost-manager/htdocs/luci-static/resources/view/vhost-manager/vhosts.js @@ -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')) + ]); } }); diff --git a/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/common.css b/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/common.css index aefda44a..b662c04f 100644 --- a/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/common.css +++ b/luci-app-wireguard-dashboard/htdocs/luci-static/resources/wireguard-dashboard/common.css @@ -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; } diff --git a/templates/common-css-template.css b/templates/common-css-template.css index aefda44a..b662c04f 100644 --- a/templates/common-css-template.css +++ b/templates/common-css-template.css @@ -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; }