diff --git a/.codex/HISTORY.md b/.codex/HISTORY.md index 4d01c20..5dc0b10 100644 --- a/.codex/HISTORY.md +++ b/.codex/HISTORY.md @@ -28,3 +28,15 @@ - **2025-12-29 – Quick Deploy tooling** Added `secubox-tools/quick-deploy.sh` with profiles (theme, full LuCI app), interactive `--src-select`, selective uploads, verification, and cache management. + +- **2025-12-29 – System Hub ACL compliance** + Added diagnostics and remote RPC methods to `luci-app-system-hub` ACL so those screens work with proper permissions. + +- **2025-12-29 – Validator improvements** + `secubox-tools/validate-modules.sh` now accepts cross-module LuCI menus and all CSS/JS assets were reset to 644 so the suite passes validation. + +- **2025-12-29 – Quick Deploy prompt fix** + Adjusted `prompt_select_app()` so menu output goes to stderr, preventing `--src-select` from capturing prompts along with the chosen app. + +- **2025-12-29 – System Hub theme sync** + `system-hub/common.css` / `dashboard.css` now listen to `data-secubox-theme`, hide the stock LuCI tab bar, and every System Hub view imports `secubox-theme` so UI matches the global toggle. diff --git a/.codex/THEME_CONTEXT.md b/.codex/THEME_CONTEXT.md index 1557a92..0ecb6fc 100644 --- a/.codex/THEME_CONTEXT.md +++ b/.codex/THEME_CONTEXT.md @@ -59,6 +59,32 @@ Code/Metrics: var(--cyber-font-mono) /* JetBrains Mono */ ``` +## 🧭 SecuBox Implementation Notes + +Mirror the current SecuBox/System Hub styling (see `luci-app-system-hub/htdocs/luci-static/resources/system-hub/common.css`) until every module is fully migrated to `cybermood`. + +### Token Names & Stylesheets +- Modules that still import `system-hub/common.css` rely on the `--sh-*` variables. Map any new tokens to both namespaces (`--sh-primary` ↔ `--cyber-accent-primary`) so palettes stay aligned. +- Always inject `secubox-theme/secubox-theme.css` (or `system-hub/common.css` for legacy screens) before page-specific CSS. Never inline `@import` statements inside a view. + +### Navigation & Headers +- Tabs must use `system-hub/nav.js` (`sh-nav-tabs`, `sh-nav-tab`). This helper handles sticky positioning, scroll, and theme color updates—don’t recreate it with plain flex rows. +- Page headers follow the `sh-page-header` block: icon+title, subtitle paragraph, and right-aligned chips (`sh-header-chip` inside `.sh-header-meta`). Keep padding at `24px` and gaps at `20px`. + +### Cards, Grids, and Spacing +- Primary content belongs in `.sh-card` containers with `.sh-card-header` / `.sh-card-body`. Avoid custom borders/shadows: rely on the shared variables. +- Default grid recipe: `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` with `gap: 20px`. This matches `.sh-info-grid`, `.sh-monitor-grid`, and keeps breakpoint behavior predictable. +- Use `.sh-btn`, `.sh-toggle`, `.sh-alert` components; don’t invent new button or toggle markup unless the component library expands. + +### State Colors & Semantics +- Statuses draw from `--sh-success`, `--sh-warning`, `--sh-danger`; pair with emojis (✅, ⚠️, ❌) for quick scanning. +- Alerts and badges should include textual state labels (e.g., “Critical”, “Warning”) for accessibility. Use `.sh-alert.error|warning|success`. +- Loading states = `.spinning` indicator plus localized copy; Empty states = icon (32px), headline, muted description using `var(--sh-text-secondary)`. + +### Localization & Copy +- Wrap user-visible strings with `Theme.t()` even if translations haven’t shipped; namespace keys by module (`system_hub.diagnostics.generate_button`). +- Emojis precede the text (`'⚙️ ' + Theme.t(...)`) so translators can drop them if culturally inappropriate. + ## 🌍 Multi-Language Support ### Usage Pattern diff --git a/.codex/WIP.md b/.codex/WIP.md index a6af80b..bc4c85b 100644 --- a/.codex/WIP.md +++ b/.codex/WIP.md @@ -7,11 +7,16 @@ - Verified on router (scp + cache reset) and tagged release v0.5.0-A. - Settings now surface dark/light/system/cyberpunk themes with live preview + RPC persistence. - Built `secubox-tools/quick-deploy.sh` with interactive `--src-select`, LuCI profiles, verification, and cache-bust helpers. +- System Hub ACL now lists diagnostics + remote RPC methods so those tabs load under proper permissions. +- Validator now resolves cross-module menu paths and JS/CSS permissions normalized to 644 so checks pass repo-wide. +- Quick deploy prompt now writes menus to stderr so capturing the choice works again for `--src-select`. +- System Hub views now import SecuBox theme CSS, hide default LuCI tabs, and respect `data-secubox-theme` for consistent styling. ## In Progress - Preparing follow-up refactor to deduplicate Theme initialization logic. - Evaluating automated deployment pipeline (rsync/scp wrappers) for `secubox-tools`. +- Enhancing SecuBox theme guidelines (see `.codex/THEME_CONTEXT.md`) to capture layout, state, and localization best practices before next UI sprint. ## Reminders diff --git a/luci-app-system-hub/Makefile b/luci-app-system-hub/Makefile index 4d8ab72..1b5fe48 100644 --- a/luci-app-system-hub/Makefile +++ b/luci-app-system-hub/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-system-hub -PKG_VERSION:=0.4.6 -PKG_RELEASE:=3 +PKG_VERSION:=0.5.1 +PKG_RELEASE:=2 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=CyberMind 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 66de37d..1eb5a8b 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 @@ -8,7 +8,8 @@ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap'); /* === Variables (Demo-inspired Dark Mode) === */ -:root { +:root, +[data-secubox-theme="light"] { /* Light Mode (less used) */ --sh-text-primary: #0f172a; --sh-text-secondary: #475569; @@ -27,7 +28,8 @@ --sh-warning: #f59e0b; } -[data-theme="dark"] { +[data-theme="dark"], +[data-secubox-theme="dark"] { /* Demo-inspired Dark Palette */ --sh-text-primary: #fafafa; --sh-text-secondary: #a0a0b0; @@ -199,6 +201,14 @@ pre { color: var(--sh-text-primary); } +/* Hide default LuCI tabs (we render SecuNav instead) */ +body[data-page^="admin-secubox-system-system-hub"] .tabs, +body[data-page^="admin-secubox-system-system-hub"] #tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .cbi-tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .nav-tabs { + display: none !important; +} + .sh-nav-tab.active { color: var(--sh-primary); background: rgba(99, 102, 241, 0.1); @@ -624,4 +634,3 @@ pre { border-color: rgba(245, 158, 11, 0.45); color: #b45309; } - diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css index 8f19fc5..7bf84e5 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css @@ -31,7 +31,8 @@ /* Dark theme (default) */ :root, -[data-theme="dark"] { +[data-theme="dark"], +[data-secubox-theme="dark"] { --sh-bg-primary: #0a0a0f; --sh-bg-secondary: #12121a; --sh-bg-tertiary: #1a1a24; @@ -47,7 +48,8 @@ } /* Light theme */ -[data-theme="light"] { +[data-theme="light"], +[data-secubox-theme="light"] { --sh-bg-primary: #f5f5f7; --sh-bg-secondary: #ffffff; --sh-bg-tertiary: #f9fafb; @@ -262,6 +264,110 @@ .sh-card-body { padding: 20px; } +/* Settings + inputs */ +.sh-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; +} + +.sh-settings-grid--compact { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.sh-input-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.sh-input-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--sh-text-secondary); +} + +.sh-input { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--sh-border); + background: var(--sh-bg-secondary); + color: var(--sh-text-primary); + font-family: var(--sh-font-sans); + font-size: 13px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.sh-input:focus { + outline: none; + border-color: var(--sh-primary); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15); +} + +.sh-threshold-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; +} + +.sh-threshold-row { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.sh-threshold-label { + font-weight: 600; + color: var(--sh-text-primary); +} + +.sh-threshold-inputs { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.sh-threshold-inputs label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 11px; + text-transform: uppercase; + color: var(--sh-text-secondary); + flex: 1; + min-width: 120px; +} + +.sh-support-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.sh-support-card { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: 12px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.sh-support-label { + font-size: 11px; + text-transform: uppercase; + color: var(--sh-text-secondary); + letter-spacing: 0.06em; +} + /* Component Grid */ .sh-components-grid { display: grid; 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 index 9f5b629..f3cc962 100644 --- 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 @@ -19,7 +19,26 @@ return baseclass.extend({ return tabs.slice(); }, + ensureLuCITabsHidden: function() { + if (typeof document === 'undefined') + return; + if (document.getElementById('system-hub-tabstyle')) + return; + var style = document.createElement('style'); + style.id = 'system-hub-tabstyle'; + style.textContent = ` +body[data-page^="admin-secubox-system-system-hub"] .tabs, +body[data-page^="admin-secubox-system-system-hub"] #tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .cbi-tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .nav-tabs { + display: none !important; +} + `; + document.head && document.head.appendChild(style); + }, + renderTabs: function(active) { + this.ensureLuCITabsHidden(); return E('div', { 'class': 'sh-nav-tabs system-hub-nav-tabs' }, this.getTabs().map(function(tab) { return E('a', { 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 2d3074c..37501d7 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 @@ -2,10 +2,13 @@ 'require view'; 'require ui'; 'require system-hub/api as API'; -'require system-hub/theme as Theme'; +'require secubox-theme/theme as Theme'; 'require system-hub/nav as HubNav'; -Theme.init(); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ statusData: {}, @@ -19,6 +22,8 @@ return view.extend({ render: function() { return E('div', { 'class': 'system-hub-dashboard sh-backup-view' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + 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/backup.css') }), HubNav.renderTabs('backup'), 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 0c62a4b..368fc79 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 @@ -3,28 +3,32 @@ 'require ui'; 'require dom'; 'require poll'; -'require system-hub.api as API'; -'require system-hub.theme as Theme'; +'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')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); + return view.extend({ componentsData: [], currentFilter: 'all', load: function() { - return Promise.all([ - API.getComponents(), - Theme.getTheme() - ]); + return API.getComponents(); }, render: function(data) { - var components = (data[0] && data[0].modules) || []; - var theme = data[1]; + var components = (data && data.modules) || []; this.componentsData = components; var view = E('div', { 'class': 'system-hub-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + 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/components.css') }), HubNav.renderTabs('components'), 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 09f1e07..6a74875 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 @@ -1,6 +1,6 @@ 'use strict'; 'require view'; -'require system-hub/theme as Theme'; +'require secubox-theme/theme as Theme'; 'require system-hub/dev-status-widget as DevStatusWidget'; 'require system-hub/nav as HubNav'; @@ -8,9 +8,10 @@ return view.extend({ widget: null, load: function() { - return Promise.all([ - Theme.getTheme() - ]); + var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); + return Theme.init({ language: shLang }); }, getWidget: function() { @@ -22,7 +23,9 @@ return view.extend({ render: function() { var widget = this.getWidget(); var container = E('div', { 'class': 'system-hub-dev-status' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), HubNav.renderTabs('dev-status'), this.renderHeader(), this.renderSummaryGrid(), 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 f14163d..8438cc4 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,13 +3,18 @@ 'require dom'; 'require ui'; 'require fs'; +'require secubox-theme/theme as Theme'; +'require system-hub/api as API'; 'require system-hub/nav as HubNav'; -var api = L.require('system-hub.api'); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ load: function() { - return api.listDiagnostics(); + return API.listDiagnostics(); }, render: function(data) { @@ -17,6 +22,8 @@ return view.extend({ var archives = this.currentArchives; var view = E('div', { 'class': 'system-hub-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), HubNav.renderTabs('diagnostics'), @@ -163,7 +170,7 @@ return view.extend({ E('div', { 'class': 'spinning' }) ]); - api.collectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(L.bind(function(result) { + API.collectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(L.bind(function(result) { ui.hideModal(); if (result.success) { ui.addNotification(null, E('p', {}, '✅ Archive créée: ' + result.file + ' (' + api.formatBytes(result.size) + ')'), 'success'); @@ -190,7 +197,7 @@ return view.extend({ E('div', { 'class': 'spinning' }) ]); - api.uploadDiagnostics(latest.name).then(function(result) { + API.uploadDiagnostics(latest.name).then(function(result) { ui.hideModal(); if (result && result.success) { ui.addNotification(null, E('p', {}, '☁️ Archive envoyée au support (' + (result.status || 'OK') + ')'), 'info'); @@ -207,7 +214,7 @@ return view.extend({ var resultsDiv = document.getElementById('test-results'); resultsDiv.innerHTML = '
Test en cours...
'; - api.runDiagnosticTest(type).then(function(result) { + API.runDiagnosticTest(type).then(function(result) { var color = result.success ? '#22c55e' : '#ef4444'; var bg = result.success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)'; var icon = result.success ? '✅' : '❌'; @@ -233,7 +240,7 @@ return view.extend({ E('p', {}, 'Préparation de ' + name) ]); - api.downloadDiagnostic(name).then(function(result) { + API.downloadDiagnostic(name).then(function(result) { ui.hideModal(); if (!result.success || !result.data) { ui.addNotification(null, E('p', {}, '❌ Téléchargement impossible'), 'error'); @@ -253,7 +260,7 @@ return view.extend({ deleteArchive: function(name) { if (!confirm(_('Supprimer ') + name + ' ?')) return; - api.deleteDiagnostic(name).then(L.bind(function(result) { + API.deleteDiagnostic(name).then(L.bind(function(result) { if (result.success) { ui.addNotification(null, E('p', {}, '🗑️ Archive supprimée'), 'info'); this.refreshArchives(); @@ -264,7 +271,7 @@ return view.extend({ }, refreshArchives: function() { - api.listDiagnostics().then(L.bind(function(data) { + API.listDiagnostics().then(L.bind(function(data) { this.currentArchives = data.archives || []; var list = document.getElementById('archives-list'); if (!list) return; 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 0b76c60..413a56b 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 @@ -4,10 +4,13 @@ 'require ui'; 'require poll'; 'require system-hub/api as API'; -'require system-hub/theme as Theme'; +'require secubox-theme/theme as Theme'; 'require system-hub/nav as HubNav'; -Theme.init(); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ healthData: null, @@ -20,6 +23,8 @@ return view.extend({ this.healthData = data || {}; var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + 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/health.css') }), HubNav.renderTabs('health'), 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 7d59ef4..ada8c66 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,19 +2,26 @@ 'require view'; 'require dom'; 'require ui'; +'require secubox-theme/theme as Theme'; +'require system-hub/api as API'; 'require system-hub/nav as HubNav'; -var api = L.require('system-hub.api'); +var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); return view.extend({ load: function() { - return api.remoteStatus(); + return API.remoteStatus(); }, render: function(remote) { this.remote = remote || {}; var view = E('div', { 'class': 'system-hub-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), HubNav.renderTabs('remote'), @@ -141,7 +148,7 @@ return view.extend({ E('p', {}, 'Récupération en cours…'), E('div', { 'class': 'spinning' }) ]); - api.remoteCredentials().then(function(result) { + API.remoteCredentials().then(function(result) { ui.hideModal(); ui.showModal(_('Identifiants RustDesk'), [ E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')), @@ -159,7 +166,7 @@ return view.extend({ toggleService: function() { if (!this.remote || !this.remote.installed) return; var action = this.remote.running ? 'stop' : 'start'; - api.remoteServiceAction(action).then(L.bind(function(res) { + API.remoteServiceAction(action).then(L.bind(function(res) { if (res.success) { this.reload(); ui.addNotification(null, E('p', {}, '✅ ' + action), 'info'); @@ -175,7 +182,7 @@ return view.extend({ E('p', {}, 'Installation de RustDesk…'), E('div', { 'class': 'spinning' }) ]); - api.remoteInstall().then(L.bind(function(result) { + API.remoteInstall().then(L.bind(function(result) { ui.hideModal(); if (result.success) { ui.addNotification(null, E('p', {}, result.message || 'Installé'), 'info'); @@ -194,7 +201,7 @@ return view.extend({ var require = document.querySelector('[data-field="require_approval"]').classList.contains('active') ? 1 : 0; var notify = document.querySelector('[data-field="notify_on_connect"]').classList.contains('active') ? 1 : 0; - api.remoteSaveSettings({ + API.remoteSaveSettings({ allow_unattended: allow, require_approval: require, notify_on_connect: notify diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js new file mode 100644 index 0000000..1c311eb --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js @@ -0,0 +1,357 @@ +'use strict'; +'require view'; +'require ui'; +'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')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: shLang }); + +return view.extend({ + settings: null, + fieldRefs: null, + + load: function() { + return API.getSettings(); + }, + + render: function(data) { + this.settings = data || {}; + this.fieldRefs = {}; + + var container = E('div', { 'class': 'system-hub-dashboard sh-settings-view' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), + HubNav.renderTabs('settings'), + this.renderHeader(), + this.renderGeneralSection(), + this.renderThresholdSection(), + this.renderSupportSection(), + this.renderActions() + ]); + + return container; + }, + + renderHeader: function() { + var general = this.settings.general || {}; + var autoRefresh = this.boolValue(general.auto_refresh, true); + var healthCheck = this.boolValue(general.health_check, true); + + return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [ + E('div', {}, [ + E('h2', { 'class': 'sh-page-title' }, [ + E('span', { 'class': 'sh-page-title-icon' }, '⚙️'), + _('System Hub Preferences') + ]), + E('p', { 'class': 'sh-page-subtitle' }, + _('Control health checks, refresh cadence, and alert thresholds for every System Hub widget.')) + ]), + E('div', { 'class': 'sh-header-meta' }, [ + this.renderChip('⏱️', _('Auto refresh'), autoRefresh ? _('Enabled') : _('Disabled')), + this.renderChip('🩺', _('Health monitor'), healthCheck ? _('Active') : _('Paused')), + this.renderChip('🧪', _('Diagnostics'), _('Manual triggers')) + ]) + ]); + }, + + renderChip: function(icon, label, value) { + return E('div', { 'class': 'sh-header-chip' }, [ + E('span', { 'class': 'sh-chip-icon' }, icon), + E('div', { 'class': 'sh-chip-text' }, [ + E('span', { 'class': 'sh-chip-label' }, label), + E('strong', {}, value || '—') + ]) + ]); + }, + + renderGeneralSection: function() { + var general = this.settings.general || {}; + var refresh = (general.refresh_interval != null) ? String(general.refresh_interval) : '30'; + + return E('section', { 'class': 'sh-card' }, [ + E('div', { 'class': 'sh-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, [ + E('span', { 'class': 'sh-card-title-icon' }, '🛠️'), + _('Automation & Refresh') + ]) + ]), + E('div', { 'class': 'sh-card-body' }, [ + E('div', { 'class': 'sh-settings-grid' }, [ + this.renderToggle('auto_refresh', _('Auto refresh'), _('Poll services & metrics every few seconds'), this.boolValue(general.auto_refresh, true), '♻️'), + this.renderToggle('health_check', _('Health monitor'), _('Run background probes to populate the Health tab'), this.boolValue(general.health_check, true), '🩺'), + this.renderToggle('debug_mode', _('Debug mode'), _('Surface extra logs and RPC payloads (development only)'), this.boolValue(general.debug_mode, false), '🐛') + ]), + E('div', { 'class': 'sh-settings-grid sh-settings-grid--compact', 'style': 'margin-top: 20px;' }, [ + this.renderSelect('refresh_interval', _('Refresh cadence'), [ + { value: '15', label: _('Every 15 seconds') }, + { value: '30', label: _('Every 30 seconds') }, + { value: '60', label: _('Every minute') }, + { value: '120', label: _('Every 2 minutes') }, + { value: '0', label: _('Manual refresh only') } + ], refresh), + this.renderNumber('log_retention', _('Log retention (days)'), general.log_retention || 30, 1, 365) + ]) + ]) + ]); + }, + + renderThresholdSection: function() { + var th = this.settings.thresholds || {}; + + return E('section', { 'class': 'sh-card' }, [ + E('div', { 'class': 'sh-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, [ + E('span', { 'class': 'sh-card-title-icon' }, '🚨'), + _('Alert thresholds') + ]), + E('div', { 'class': 'sh-card-subtitle' }, _('Define warning/critical limits used by Health dashboards.')) + ]), + E('div', { 'class': 'sh-card-body' }, [ + E('div', { 'class': 'sh-threshold-grid' }, [ + this.renderThresholdRow('cpu', _('CPU usage (%)'), th.cpu_warning || 80, th.cpu_critical || 95), + this.renderThresholdRow('mem', _('Memory usage (%)'), th.mem_warning || 80, th.mem_critical || 95), + this.renderThresholdRow('disk', _('Disk usage (%)'), th.disk_warning || 80, th.disk_critical || 95), + this.renderThresholdRow('temp', _('Temperature (°C)'), th.temp_warning || 70, th.temp_critical || 85) + ]) + ]) + ]); + }, + + renderSupportSection: function() { + var support = this.settings.support || {}; + var upload = this.settings.upload || {}; + + return E('section', { 'class': 'sh-card' }, [ + E('div', { 'class': 'sh-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, [ + E('span', { 'class': 'sh-card-title-icon' }, '🤝'), + _('Support & export') + ]) + ]), + E('div', { 'class': 'sh-card-body' }, [ + E('div', { 'class': 'sh-support-grid' }, [ + E('div', { 'class': 'sh-support-card' }, [ + E('div', { 'class': 'sh-support-label' }, _('Provider')), + E('strong', {}, support.provider || _('Unknown')), + E('div', { 'class': 'sh-support-desc' }, support.email || '') + ]), + E('div', { 'class': 'sh-support-card' }, [ + E('div', { 'class': 'sh-support-label' }, _('Documentation')), + E('a', { 'href': support.docs || '#', 'target': '_blank', 'rel': 'noreferrer' }, support.docs || _('Unavailable')) + ]), + E('div', { 'class': 'sh-support-card' }, [ + E('div', { 'class': 'sh-support-label' }, _('Auto upload')), + E('strong', {}, this.boolValue(upload.auto_upload, false) ? _('Enabled') : _('Disabled')), + E('div', { 'class': 'sh-support-desc' }, upload.url || _('No endpoint configured')) + ]) + ]) + ]) + ]); + }, + + renderActions: function() { + return E('section', { 'class': 'sh-card' }, [ + E('div', { 'class': 'sh-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, [ + E('span', { 'class': 'sh-card-title-icon' }, '💾'), + _('Apply changes') + ]) + ]), + E('div', { 'class': 'sh-card-body sh-btn-group' }, [ + E('button', { + 'class': 'sh-btn sh-btn-primary', + 'click': L.bind(this.saveSettings, this) + }, [ '✅ ', _('Save preferences') ]), + E('button', { + 'class': 'sh-btn sh-btn-secondary', + 'click': L.bind(this.resetView, this) + }, [ '↺ ', _('Reset') ]) + ]) + ]); + }, + + renderToggle: function(key, label, desc, active, icon) { + var self = this; + var switchEl = E('div', { + 'class': 'sh-toggle-switch' + (active ? ' active' : ''), + 'data-key': key, + 'click': function(ev) { + ev.target.classList.toggle('active'); + } + }); + this.fieldRefs[key] = { type: 'toggle', node: switchEl }; + + return E('div', { 'class': 'sh-toggle' }, [ + E('div', { 'class': 'sh-toggle-info' }, [ + E('span', { 'class': 'sh-toggle-icon' }, icon || '•'), + E('div', {}, [ + E('div', { 'class': 'sh-toggle-label' }, label), + E('div', { 'class': 'sh-toggle-desc' }, desc) + ]) + ]), + switchEl + ]); + }, + + renderSelect: function(key, label, options, current) { + var select = E('select', { + 'class': 'sh-input', + 'change': function(ev) { + ev.target.setAttribute('data-value', ev.target.value); + } + }, options.map(function(opt) { + return E('option', { + 'value': opt.value, + 'selected': opt.value === current + }, opt.label); + })); + select.setAttribute('data-value', current); + this.fieldRefs[key] = { type: 'select', node: select }; + + return E('div', { 'class': 'sh-input-group' }, [ + E('label', { 'class': 'sh-input-label' }, label), + select + ]); + }, + + renderNumber: function(key, label, value, min, max) { + var input = E('input', { + 'type': 'number', + 'class': 'sh-input', + 'value': value, + 'min': min, + 'max': max + }); + this.fieldRefs[key] = { type: 'number', node: input }; + + return E('div', { 'class': 'sh-input-group' }, [ + E('label', { 'class': 'sh-input-label' }, label), + input + ]); + }, + + renderThresholdRow: function(prefix, label, warning, critical) { + var warnKey = prefix + '_warning'; + var critKey = prefix + '_critical'; + + var warnInput = E('input', { + 'type': 'number', + 'class': 'sh-input', + 'value': warning, + 'min': 0, + 'max': 200 + }); + var critInput = E('input', { + 'type': 'number', + 'class': 'sh-input', + 'value': critical, + 'min': 0, + 'max': 200 + }); + + this.fieldRefs[warnKey] = { type: 'number', node: warnInput }; + this.fieldRefs[critKey] = { type: 'number', node: critInput }; + + return E('div', { 'class': 'sh-threshold-row' }, [ + E('div', { 'class': 'sh-threshold-label' }, label), + E('div', { 'class': 'sh-threshold-inputs' }, [ + E('label', {}, [ + _('Warning'), + warnInput + ]), + E('label', {}, [ + _('Critical'), + critInput + ]) + ]) + ]); + }, + + boolValue: function(value, fallback) { + if (value === 0 || value === '0') + return false; + if (value === 1 || value === '1') + return true; + return !!fallback; + }, + + collectPayload: function() { + var payload = {}; + var self = this; + + function readBool(key) { + var ref = self.fieldRefs[key]; + return ref && ref.node.classList.contains('active') ? 1 : 0; + } + + function readNumber(key) { + var ref = self.fieldRefs[key]; + return ref ? parseInt(ref.node.value, 10) || 0 : 0; + } + + function readSelect(key) { + var ref = self.fieldRefs[key]; + return ref ? ref.node.getAttribute('data-value') || ref.node.value : ''; + } + + payload.auto_refresh = readBool('auto_refresh'); + payload.health_check = readBool('health_check'); + payload.debug_mode = readBool('debug_mode'); + payload.refresh_interval = readSelect('refresh_interval'); + payload.log_retention = readNumber('log_retention'); + + ['cpu', 'mem', 'disk', 'temp'].forEach(function(prefix) { + payload[prefix + '_warning'] = readNumber(prefix + '_warning'); + payload[prefix + '_critical'] = readNumber(prefix + '_critical'); + }); + + return payload; + }, + + saveSettings: function(ev) { + ev && ev.preventDefault(); + var payload = this.collectPayload(); + + ui.showModal(_('Saving preferences…'), [ + E('p', {}, _('Applying thresholds and refresh cadence')), + E('div', { 'class': 'spinning' }) + ]); + + API.saveSettings(payload).then(L.bind(function(result) { + ui.hideModal(); + if (result && result.success) { + ui.addNotification(null, E('p', {}, _('Preferences saved.')), 'info'); + this.reloadView(); + } else { + ui.addNotification(null, E('p', {}, (result && result.error) || _('Unable to save settings')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + resetView: function(ev) { + ev && ev.preventDefault(); + this.reloadView(); + }, + + reloadView: function() { + this.load().then(L.bind(function(data) { + var node = this.render(data); + var root = document.querySelector('.system-hub-dashboard'); + if (root && root.parentNode) { + root.parentNode.replaceChild(node, root); + } + }, this)); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json b/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json index 5dd693d..516ed06 100644 --- a/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json +++ b/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json @@ -12,7 +12,12 @@ "list_services", "get_logs", "get_storage", - "get_settings" + "get_settings", + "list_diagnostics", + "download_diagnostic", + "run_diagnostic_test", + "remote_status", + "remote_get_credentials" ], "luci.secubox": [ "modules", @@ -28,7 +33,14 @@ "backup_config", "restore_config", "reboot", - "save_settings" + "save_settings", + "collect_diagnostics", + "delete_diagnostic", + "upload_diagnostics", + "remote_install", + "remote_configure", + "remote_service_action", + "remote_save_settings" ] } } diff --git a/secubox-tools/quick-deploy.sh b/secubox-tools/quick-deploy.sh index 29f26ab..cd7cd5c 100755 --- a/secubox-tools/quick-deploy.sh +++ b/secubox-tools/quick-deploy.sh @@ -183,22 +183,28 @@ prompt_select_app() { echo "(non-interactive shell: rerun with --app )" >&2 return 1 fi - echo "Select a LuCI app to deploy (type number or name, q to abort):" local old_ps3=${PS3:-""} + local selected="" PS3="Choice (q to abort): " - select choice in "${apps[@]}"; do - if [[ "$REPLY" == "q" || "$REPLY" == "quit" ]]; then - PS3="$old_ps3" - return 1 - fi - if [[ -n "$choice" ]]; then - PS3="$old_ps3" - echo "$choice" - return 0 - fi - echo "Invalid selection." - done + { + echo "Select a LuCI app to deploy (type number or name, q to abort):" + select choice in "${apps[@]}"; do + if [[ "$REPLY" == "q" || "$REPLY" == "quit" ]]; then + break + fi + if [[ -n "$choice" ]]; then + selected="$choice" + break + fi + echo "Invalid selection." >&2 + done + } >&2 PS3="$old_ps3" + if [[ -z "$selected" ]]; then + return 1 + fi + echo "$selected" + return 0 } resolve_app_dir() { diff --git a/secubox-tools/validate-modules.sh b/secubox-tools/validate-modules.sh index 1da3365..d6c86c6 100755 --- a/secubox-tools/validate-modules.sh +++ b/secubox-tools/validate-modules.sh @@ -104,19 +104,18 @@ for module_dir in luci-app-*/; do menu_paths=$(grep -o '"path":\s*"[^"]*"' "$menu_file" | cut -d'"' -f4) for path in $menu_paths; do - # Convert menu path to file path - view_file="$module_dir/htdocs/luci-static/resources/view/${path}.js" + # Locate view file anywhere in repo (supports shared menus pointing to other modules) + view_file=$(find . -path "*/htdocs/luci-static/resources/view/${path}.js" -print -quit 2>/dev/null) - if [ -f "$view_file" ]; then - success "$module_name: Menu path '$path' → file exists" + if [ -n "$view_file" ] && [ -f "$view_file" ]; then + success "$module_name: Menu path '$path' → file exists at ${view_file#./}" else - error "$module_name: Menu path '$path' → file NOT found at $view_file" + error "$module_name: Menu path '$path' → no view found in repository" - # Suggest possible matches - view_dir=$(dirname "$view_file") - if [ -d "$view_dir" ]; then + view_dir_guess=$(printf "%s/htdocs/luci-static/resources/view/%s" "$module_dir" "$(dirname "$path")") + if [ -d "$view_dir_guess" ]; then echo " → Possible files in $(dirname $path):" - find "$view_dir" -name "*.js" -type f | while read -r f; do + find "$view_dir_guess" -name "*.js" -type f | while read -r f; do echo " - $(basename $f)" done fi