System Hub 0.5.1-2
This commit is contained in:
parent
4dca3c1917
commit
8c27583eba
@ -28,3 +28,15 @@
|
|||||||
|
|
||||||
- **2025-12-29 – Quick Deploy tooling**
|
- **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.
|
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.
|
||||||
|
|||||||
@ -59,6 +59,32 @@ Code/Metrics: var(--cyber-font-mono) /* JetBrains Mono */
|
|||||||
<select class="cyber-select"></select>
|
<select class="cyber-select"></select>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🧭 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
|
## 🌍 Multi-Language Support
|
||||||
|
|
||||||
### Usage Pattern
|
### Usage Pattern
|
||||||
|
|||||||
@ -7,11 +7,16 @@
|
|||||||
- Verified on router (scp + cache reset) and tagged release v0.5.0-A.
|
- 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.
|
- 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.
|
- 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
|
## In Progress
|
||||||
|
|
||||||
- Preparing follow-up refactor to deduplicate Theme initialization logic.
|
- Preparing follow-up refactor to deduplicate Theme initialization logic.
|
||||||
- Evaluating automated deployment pipeline (rsync/scp wrappers) for `secubox-tools`.
|
- 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
|
## Reminders
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-system-hub
|
PKG_NAME:=luci-app-system-hub
|
||||||
PKG_VERSION:=0.4.6
|
PKG_VERSION:=0.5.1
|
||||||
PKG_RELEASE:=3
|
PKG_RELEASE:=2
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
@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) === */
|
/* === Variables (Demo-inspired Dark Mode) === */
|
||||||
:root {
|
:root,
|
||||||
|
[data-secubox-theme="light"] {
|
||||||
/* Light Mode (less used) */
|
/* Light Mode (less used) */
|
||||||
--sh-text-primary: #0f172a;
|
--sh-text-primary: #0f172a;
|
||||||
--sh-text-secondary: #475569;
|
--sh-text-secondary: #475569;
|
||||||
@ -27,7 +28,8 @@
|
|||||||
--sh-warning: #f59e0b;
|
--sh-warning: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"],
|
||||||
|
[data-secubox-theme="dark"] {
|
||||||
/* Demo-inspired Dark Palette */
|
/* Demo-inspired Dark Palette */
|
||||||
--sh-text-primary: #fafafa;
|
--sh-text-primary: #fafafa;
|
||||||
--sh-text-secondary: #a0a0b0;
|
--sh-text-secondary: #a0a0b0;
|
||||||
@ -199,6 +201,14 @@ pre {
|
|||||||
color: var(--sh-text-primary);
|
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 {
|
.sh-nav-tab.active {
|
||||||
color: var(--sh-primary);
|
color: var(--sh-primary);
|
||||||
background: rgba(99, 102, 241, 0.1);
|
background: rgba(99, 102, 241, 0.1);
|
||||||
@ -624,4 +634,3 @@ pre {
|
|||||||
border-color: rgba(245, 158, 11, 0.45);
|
border-color: rgba(245, 158, 11, 0.45);
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,8 @@
|
|||||||
|
|
||||||
/* Dark theme (default) */
|
/* Dark theme (default) */
|
||||||
:root,
|
:root,
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"],
|
||||||
|
[data-secubox-theme="dark"] {
|
||||||
--sh-bg-primary: #0a0a0f;
|
--sh-bg-primary: #0a0a0f;
|
||||||
--sh-bg-secondary: #12121a;
|
--sh-bg-secondary: #12121a;
|
||||||
--sh-bg-tertiary: #1a1a24;
|
--sh-bg-tertiary: #1a1a24;
|
||||||
@ -47,7 +48,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme */
|
/* Light theme */
|
||||||
[data-theme="light"] {
|
[data-theme="light"],
|
||||||
|
[data-secubox-theme="light"] {
|
||||||
--sh-bg-primary: #f5f5f7;
|
--sh-bg-primary: #f5f5f7;
|
||||||
--sh-bg-secondary: #ffffff;
|
--sh-bg-secondary: #ffffff;
|
||||||
--sh-bg-tertiary: #f9fafb;
|
--sh-bg-tertiary: #f9fafb;
|
||||||
@ -262,6 +264,110 @@
|
|||||||
|
|
||||||
.sh-card-body { padding: 20px; }
|
.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 */
|
/* Component Grid */
|
||||||
.sh-components-grid {
|
.sh-components-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -19,7 +19,26 @@ return baseclass.extend({
|
|||||||
return tabs.slice();
|
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) {
|
renderTabs: function(active) {
|
||||||
|
this.ensureLuCITabsHidden();
|
||||||
return E('div', { 'class': 'sh-nav-tabs system-hub-nav-tabs' },
|
return E('div', { 'class': 'sh-nav-tabs system-hub-nav-tabs' },
|
||||||
this.getTabs().map(function(tab) {
|
this.getTabs().map(function(tab) {
|
||||||
return E('a', {
|
return E('a', {
|
||||||
|
|||||||
@ -2,10 +2,13 @@
|
|||||||
'require view';
|
'require view';
|
||||||
'require ui';
|
'require ui';
|
||||||
'require system-hub/api as API';
|
'require system-hub/api as API';
|
||||||
'require system-hub/theme as Theme';
|
'require secubox-theme/theme as Theme';
|
||||||
'require system-hub/nav as HubNav';
|
'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({
|
return view.extend({
|
||||||
statusData: {},
|
statusData: {},
|
||||||
@ -19,6 +22,8 @@ return view.extend({
|
|||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return E('div', { 'class': 'system-hub-dashboard sh-backup-view' }, [
|
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/dashboard.css') }),
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/backup.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/backup.css') }),
|
||||||
HubNav.renderTabs('backup'),
|
HubNav.renderTabs('backup'),
|
||||||
|
|||||||
@ -3,28 +3,32 @@
|
|||||||
'require ui';
|
'require ui';
|
||||||
'require dom';
|
'require dom';
|
||||||
'require poll';
|
'require poll';
|
||||||
'require system-hub.api as API';
|
'require system-hub/api as API';
|
||||||
'require system-hub.theme as Theme';
|
'require secubox-theme/theme as Theme';
|
||||||
'require system-hub/nav as HubNav';
|
'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({
|
return view.extend({
|
||||||
componentsData: [],
|
componentsData: [],
|
||||||
currentFilter: 'all',
|
currentFilter: 'all',
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
return API.getComponents();
|
||||||
API.getComponents(),
|
|
||||||
Theme.getTheme()
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
var components = (data[0] && data[0].modules) || [];
|
var components = (data && data.modules) || [];
|
||||||
var theme = data[1];
|
|
||||||
|
|
||||||
this.componentsData = components;
|
this.componentsData = components;
|
||||||
|
|
||||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
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') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/components.css') }),
|
||||||
|
|
||||||
HubNav.renderTabs('components'),
|
HubNav.renderTabs('components'),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
'require view';
|
'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/dev-status-widget as DevStatusWidget';
|
||||||
'require system-hub/nav as HubNav';
|
'require system-hub/nav as HubNav';
|
||||||
|
|
||||||
@ -8,9 +8,10 @@ return view.extend({
|
|||||||
widget: null,
|
widget: null,
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return Promise.all([
|
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
Theme.getTheme()
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
]);
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
return Theme.init({ language: shLang });
|
||||||
},
|
},
|
||||||
|
|
||||||
getWidget: function() {
|
getWidget: function() {
|
||||||
@ -22,7 +23,9 @@ return view.extend({
|
|||||||
render: function() {
|
render: function() {
|
||||||
var widget = this.getWidget();
|
var widget = this.getWidget();
|
||||||
var container = E('div', { 'class': 'system-hub-dev-status' }, [
|
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/common.css') }),
|
||||||
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||||||
HubNav.renderTabs('dev-status'),
|
HubNav.renderTabs('dev-status'),
|
||||||
this.renderHeader(),
|
this.renderHeader(),
|
||||||
this.renderSummaryGrid(),
|
this.renderSummaryGrid(),
|
||||||
|
|||||||
@ -3,13 +3,18 @@
|
|||||||
'require dom';
|
'require dom';
|
||||||
'require ui';
|
'require ui';
|
||||||
'require fs';
|
'require fs';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require system-hub/api as API';
|
||||||
'require system-hub/nav as HubNav';
|
'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({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return api.listDiagnostics();
|
return API.listDiagnostics();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
@ -17,6 +22,8 @@ return view.extend({
|
|||||||
var archives = this.currentArchives;
|
var archives = this.currentArchives;
|
||||||
|
|
||||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
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/dashboard.css') }),
|
||||||
HubNav.renderTabs('diagnostics'),
|
HubNav.renderTabs('diagnostics'),
|
||||||
|
|
||||||
@ -163,7 +170,7 @@ return view.extend({
|
|||||||
E('div', { 'class': 'spinning' })
|
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();
|
ui.hideModal();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ui.addNotification(null, E('p', {}, '✅ Archive créée: ' + result.file + ' (' + api.formatBytes(result.size) + ')'), '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' })
|
E('div', { 'class': 'spinning' })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.uploadDiagnostics(latest.name).then(function(result) {
|
API.uploadDiagnostics(latest.name).then(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
ui.addNotification(null, E('p', {}, '☁️ Archive envoyée au support (' + (result.status || 'OK') + ')'), 'info');
|
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');
|
var resultsDiv = document.getElementById('test-results');
|
||||||
|
|
||||||
resultsDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><div class="spinning"></div><div style="margin-top: 12px;">Test en cours...</div></div>';
|
resultsDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><div class="spinning"></div><div style="margin-top: 12px;">Test en cours...</div></div>';
|
||||||
api.runDiagnosticTest(type).then(function(result) {
|
API.runDiagnosticTest(type).then(function(result) {
|
||||||
var color = result.success ? '#22c55e' : '#ef4444';
|
var color = result.success ? '#22c55e' : '#ef4444';
|
||||||
var bg = result.success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
|
var bg = result.success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
|
||||||
var icon = result.success ? '✅' : '❌';
|
var icon = result.success ? '✅' : '❌';
|
||||||
@ -233,7 +240,7 @@ return view.extend({
|
|||||||
E('p', {}, 'Préparation de ' + name)
|
E('p', {}, 'Préparation de ' + name)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.downloadDiagnostic(name).then(function(result) {
|
API.downloadDiagnostic(name).then(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
ui.addNotification(null, E('p', {}, '❌ Téléchargement impossible'), 'error');
|
ui.addNotification(null, E('p', {}, '❌ Téléchargement impossible'), 'error');
|
||||||
@ -253,7 +260,7 @@ return view.extend({
|
|||||||
|
|
||||||
deleteArchive: function(name) {
|
deleteArchive: function(name) {
|
||||||
if (!confirm(_('Supprimer ') + name + ' ?')) return;
|
if (!confirm(_('Supprimer ') + name + ' ?')) return;
|
||||||
api.deleteDiagnostic(name).then(L.bind(function(result) {
|
API.deleteDiagnostic(name).then(L.bind(function(result) {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ui.addNotification(null, E('p', {}, '🗑️ Archive supprimée'), 'info');
|
ui.addNotification(null, E('p', {}, '🗑️ Archive supprimée'), 'info');
|
||||||
this.refreshArchives();
|
this.refreshArchives();
|
||||||
@ -264,7 +271,7 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
refreshArchives: function() {
|
refreshArchives: function() {
|
||||||
api.listDiagnostics().then(L.bind(function(data) {
|
API.listDiagnostics().then(L.bind(function(data) {
|
||||||
this.currentArchives = data.archives || [];
|
this.currentArchives = data.archives || [];
|
||||||
var list = document.getElementById('archives-list');
|
var list = document.getElementById('archives-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|||||||
@ -4,10 +4,13 @@
|
|||||||
'require ui';
|
'require ui';
|
||||||
'require poll';
|
'require poll';
|
||||||
'require system-hub/api as API';
|
'require system-hub/api as API';
|
||||||
'require system-hub/theme as Theme';
|
'require secubox-theme/theme as Theme';
|
||||||
'require system-hub/nav as HubNav';
|
'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({
|
return view.extend({
|
||||||
healthData: null,
|
healthData: null,
|
||||||
@ -20,6 +23,8 @@ return view.extend({
|
|||||||
this.healthData = data || {};
|
this.healthData = data || {};
|
||||||
|
|
||||||
var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [
|
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/dashboard.css') }),
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/health.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/health.css') }),
|
||||||
HubNav.renderTabs('health'),
|
HubNav.renderTabs('health'),
|
||||||
|
|||||||
@ -2,19 +2,26 @@
|
|||||||
'require view';
|
'require view';
|
||||||
'require dom';
|
'require dom';
|
||||||
'require ui';
|
'require ui';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require system-hub/api as API';
|
||||||
'require system-hub/nav as HubNav';
|
'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({
|
return view.extend({
|
||||||
load: function() {
|
load: function() {
|
||||||
return api.remoteStatus();
|
return API.remoteStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(remote) {
|
render: function(remote) {
|
||||||
this.remote = remote || {};
|
this.remote = remote || {};
|
||||||
|
|
||||||
var view = E('div', { 'class': 'system-hub-dashboard' }, [
|
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/dashboard.css') }),
|
||||||
HubNav.renderTabs('remote'),
|
HubNav.renderTabs('remote'),
|
||||||
|
|
||||||
@ -141,7 +148,7 @@ return view.extend({
|
|||||||
E('p', {}, 'Récupération en cours…'),
|
E('p', {}, 'Récupération en cours…'),
|
||||||
E('div', { 'class': 'spinning' })
|
E('div', { 'class': 'spinning' })
|
||||||
]);
|
]);
|
||||||
api.remoteCredentials().then(function(result) {
|
API.remoteCredentials().then(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
ui.showModal(_('Identifiants RustDesk'), [
|
ui.showModal(_('Identifiants RustDesk'), [
|
||||||
E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')),
|
E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')),
|
||||||
@ -159,7 +166,7 @@ return view.extend({
|
|||||||
toggleService: function() {
|
toggleService: function() {
|
||||||
if (!this.remote || !this.remote.installed) return;
|
if (!this.remote || !this.remote.installed) return;
|
||||||
var action = this.remote.running ? 'stop' : 'start';
|
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) {
|
if (res.success) {
|
||||||
this.reload();
|
this.reload();
|
||||||
ui.addNotification(null, E('p', {}, '✅ ' + action), 'info');
|
ui.addNotification(null, E('p', {}, '✅ ' + action), 'info');
|
||||||
@ -175,7 +182,7 @@ return view.extend({
|
|||||||
E('p', {}, 'Installation de RustDesk…'),
|
E('p', {}, 'Installation de RustDesk…'),
|
||||||
E('div', { 'class': 'spinning' })
|
E('div', { 'class': 'spinning' })
|
||||||
]);
|
]);
|
||||||
api.remoteInstall().then(L.bind(function(result) {
|
API.remoteInstall().then(L.bind(function(result) {
|
||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
ui.addNotification(null, E('p', {}, result.message || 'Installé'), 'info');
|
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 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;
|
var notify = document.querySelector('[data-field="notify_on_connect"]').classList.contains('active') ? 1 : 0;
|
||||||
|
|
||||||
api.remoteSaveSettings({
|
API.remoteSaveSettings({
|
||||||
allow_unattended: allow,
|
allow_unattended: allow,
|
||||||
require_approval: require,
|
require_approval: require,
|
||||||
notify_on_connect: notify
|
notify_on_connect: notify
|
||||||
|
|||||||
@ -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
|
||||||
|
});
|
||||||
@ -12,7 +12,12 @@
|
|||||||
"list_services",
|
"list_services",
|
||||||
"get_logs",
|
"get_logs",
|
||||||
"get_storage",
|
"get_storage",
|
||||||
"get_settings"
|
"get_settings",
|
||||||
|
"list_diagnostics",
|
||||||
|
"download_diagnostic",
|
||||||
|
"run_diagnostic_test",
|
||||||
|
"remote_status",
|
||||||
|
"remote_get_credentials"
|
||||||
],
|
],
|
||||||
"luci.secubox": [
|
"luci.secubox": [
|
||||||
"modules",
|
"modules",
|
||||||
@ -28,7 +33,14 @@
|
|||||||
"backup_config",
|
"backup_config",
|
||||||
"restore_config",
|
"restore_config",
|
||||||
"reboot",
|
"reboot",
|
||||||
"save_settings"
|
"save_settings",
|
||||||
|
"collect_diagnostics",
|
||||||
|
"delete_diagnostic",
|
||||||
|
"upload_diagnostics",
|
||||||
|
"remote_install",
|
||||||
|
"remote_configure",
|
||||||
|
"remote_service_action",
|
||||||
|
"remote_save_settings"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -183,22 +183,28 @@ prompt_select_app() {
|
|||||||
echo "(non-interactive shell: rerun with --app <name>)" >&2
|
echo "(non-interactive shell: rerun with --app <name>)" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "Select a LuCI app to deploy (type number or name, q to abort):"
|
|
||||||
local old_ps3=${PS3:-""}
|
local old_ps3=${PS3:-""}
|
||||||
|
local selected=""
|
||||||
PS3="Choice (q to abort): "
|
PS3="Choice (q to abort): "
|
||||||
select choice in "${apps[@]}"; do
|
{
|
||||||
if [[ "$REPLY" == "q" || "$REPLY" == "quit" ]]; then
|
echo "Select a LuCI app to deploy (type number or name, q to abort):"
|
||||||
PS3="$old_ps3"
|
select choice in "${apps[@]}"; do
|
||||||
return 1
|
if [[ "$REPLY" == "q" || "$REPLY" == "quit" ]]; then
|
||||||
fi
|
break
|
||||||
if [[ -n "$choice" ]]; then
|
fi
|
||||||
PS3="$old_ps3"
|
if [[ -n "$choice" ]]; then
|
||||||
echo "$choice"
|
selected="$choice"
|
||||||
return 0
|
break
|
||||||
fi
|
fi
|
||||||
echo "Invalid selection."
|
echo "Invalid selection." >&2
|
||||||
done
|
done
|
||||||
|
} >&2
|
||||||
PS3="$old_ps3"
|
PS3="$old_ps3"
|
||||||
|
if [[ -z "$selected" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$selected"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_app_dir() {
|
resolve_app_dir() {
|
||||||
|
|||||||
@ -104,19 +104,18 @@ for module_dir in luci-app-*/; do
|
|||||||
menu_paths=$(grep -o '"path":\s*"[^"]*"' "$menu_file" | cut -d'"' -f4)
|
menu_paths=$(grep -o '"path":\s*"[^"]*"' "$menu_file" | cut -d'"' -f4)
|
||||||
|
|
||||||
for path in $menu_paths; do
|
for path in $menu_paths; do
|
||||||
# Convert menu path to file path
|
# Locate view file anywhere in repo (supports shared menus pointing to other modules)
|
||||||
view_file="$module_dir/htdocs/luci-static/resources/view/${path}.js"
|
view_file=$(find . -path "*/htdocs/luci-static/resources/view/${path}.js" -print -quit 2>/dev/null)
|
||||||
|
|
||||||
if [ -f "$view_file" ]; then
|
if [ -n "$view_file" ] && [ -f "$view_file" ]; then
|
||||||
success "$module_name: Menu path '$path' → file exists"
|
success "$module_name: Menu path '$path' → file exists at ${view_file#./}"
|
||||||
else
|
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_guess=$(printf "%s/htdocs/luci-static/resources/view/%s" "$module_dir" "$(dirname "$path")")
|
||||||
view_dir=$(dirname "$view_file")
|
if [ -d "$view_dir_guess" ]; then
|
||||||
if [ -d "$view_dir" ]; then
|
|
||||||
echo " → Possible files in $(dirname $path):"
|
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)"
|
echo " - $(basename $f)"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user