Update theme selector and deploy tooling
This commit is contained in:
parent
9f23940fe5
commit
4dca3c1917
@ -22,3 +22,9 @@
|
||||
|
||||
- **2025-12-29 – v0.5.0-A UI polish**
|
||||
Monitoring hero + modules filter migrated to SecuNav styling, alerts buttons use `sh-btn`, Help page adopts shared header and navbar Bonus tab, and overall theme consistency is verified on router.
|
||||
|
||||
- **2025-12-29 – Theme selector live preview**
|
||||
Settings now expose dark/light/system/cyberpunk options, preview changes instantly, and save preferences via the new `set_theme` RPC.
|
||||
|
||||
- **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.
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
# TODO (Codex)
|
||||
|
||||
1. **Theme Selector**
|
||||
- Extend SecuBox Settings to expose all Theme Manager variants (dark/light/system/cyberpunk).
|
||||
- Live preview when flipping options; persist via RPC.
|
||||
|
||||
2. **Component Library**
|
||||
1. **Component Library**
|
||||
- Extract header chips/nav tabs into standalone modules under `luci-static/resources/secubox/components/`.
|
||||
- Provide TypeScript typings (or JS docstrings) for easier reuse.
|
||||
|
||||
3. **Validation Scripts**
|
||||
2. **Validation Scripts**
|
||||
- Add `npm run lint:ui` (eslint + prettier) for LuCI JS.
|
||||
- Add `npm run check:luci` to run `lua -l luci.dispatcher` before SCP deploys.
|
||||
|
||||
4. **Docs**
|
||||
3. **Docs**
|
||||
- Update `.codex/context.md` with quick deployment recipes (`deploy-secubox-v0.1.2.sh`, etc.).
|
||||
- Record router credentials requirements (currently warns about missing root password).
|
||||
|
||||
5. **Automation**
|
||||
4. **Automation**
|
||||
- Create `secubox-tools/deploy-theme-only.sh` for CSS/JS pushes (no RPC).
|
||||
- Add `make snapshot` script to package updated LuCI app for feeds.
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
- Unified Monitoring + Modules filters and Help view with SecuNav styling.
|
||||
- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks.
|
||||
- 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.
|
||||
|
||||
## In Progress
|
||||
|
||||
|
||||
@ -167,6 +167,41 @@ ssh root@192.168.8.191 "chmod 644 /www/luci-static/resources/**/*.css"
|
||||
ssh root@192.168.8.191 "rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* && /etc/init.d/rpcd restart && /etc/init.d/uhttpd restart"
|
||||
```
|
||||
|
||||
### Quick Deploy Helper
|
||||
```bash
|
||||
# IPK install via opkg (auto SCP + install)
|
||||
./secubox-tools/quick-deploy.sh --ipk bin/packages/luci-app-secubox.ipk
|
||||
|
||||
# APK install on newer images
|
||||
./secubox-tools/quick-deploy.sh --apk dist/secubox-theme.apk
|
||||
|
||||
# Push local source directory to /www/luci-static
|
||||
./secubox-tools/quick-deploy.sh --src luci-app-secubox/htdocs --target-path /www/luci-static
|
||||
|
||||
# Clone Git repo and deploy (branch optional)
|
||||
./secubox-tools/quick-deploy.sh --git https://github.com/CyberMindStudio/secubox-theme.git --branch main
|
||||
|
||||
# Selective push (only CSS + Settings view)
|
||||
./secubox-tools/quick-deploy.sh --src luci-app-secubox/htdocs \
|
||||
--include luci-static/resources/secubox/secubox.css \
|
||||
--include luci-static/resources/view/secubox/settings.js
|
||||
|
||||
# Root tree updates (rpcd, ACLs, etc.)
|
||||
./secubox-tools/quick-deploy.sh --src luci-app-secubox/root --force-root
|
||||
|
||||
# Legacy theme profile (mirrors deploy-theme-system.sh)
|
||||
./secubox-tools/quick-deploy.sh --profile theme
|
||||
|
||||
# Deploy complete LuCI app (root + htdocs)
|
||||
./secubox-tools/quick-deploy.sh --profile luci-app --src luci-app-secubox
|
||||
|
||||
# Browse LuCI apps and auto-pick one
|
||||
./secubox-tools/quick-deploy.sh --list-apps
|
||||
./secubox-tools/quick-deploy.sh --app secubox
|
||||
./secubox-tools/quick-deploy.sh --src-select # interactive picker (TTY only)
|
||||
```
|
||||
*Flags:* `--include` limits uploads, `--force-root` writes relative to `/`, `--profile` triggers opinionated bundles (`theme`, `luci-app`), `--app <name>` auto-resolves `luci-app-<name>`, `--list-apps` prints detected apps, `--src-select` shows the same picker interactively, `--no-auto-profile` disables automatic LuCI detection when using `--src`, `--no-cache-bust` skips clearing `/tmp/luci-*`, `--no-verify` disables post-copy checksum checks, `--router root@192.168.8.191` overrides the target, and `--post "rm -rf /tmp/luci-*"` runs extra remote commands. Environment variables `ROUTER`, `TARGET_PATH`, `CACHE_BUST`, `VERIFY`, `SSH_OPTS`, and `SCP_OPTS` can be exported ahead of time.
|
||||
|
||||
### Debug
|
||||
```bash
|
||||
# Test RPCD
|
||||
|
||||
@ -119,6 +119,13 @@ var callGetTheme = rpc.declare({
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callSetTheme = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'set_theme',
|
||||
params: ['theme'],
|
||||
expect: { success: false, theme: '' }
|
||||
});
|
||||
|
||||
var callDismissAlert = rpc.declare({
|
||||
object: 'luci.secubox',
|
||||
method: 'dismiss_alert',
|
||||
@ -177,6 +184,7 @@ return baseclass.extend({
|
||||
quickAction: callQuickAction,
|
||||
getDashboardData: callDashboardData,
|
||||
getTheme: callGetTheme,
|
||||
setTheme: callSetTheme,
|
||||
dismissAlert: callDismissAlert,
|
||||
clearAlerts: callClearAlerts,
|
||||
fixPermissions: callFixPermissions,
|
||||
|
||||
@ -567,6 +567,182 @@
|
||||
color: var(--sb-text-muted);
|
||||
}
|
||||
|
||||
/* Settings preference showcase */
|
||||
.secubox-pref-wrapper {
|
||||
margin: 20px 0 30px;
|
||||
padding: 22px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(14, 165, 233, 0.08)) border-box,
|
||||
var(--sb-bg-card);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
box-shadow: 0 20px 45px rgba(14, 19, 34, 0.45);
|
||||
}
|
||||
|
||||
.secubox-pref-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.secubox-pref-kicker {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
font-size: 11px;
|
||||
margin: 0 0 6px;
|
||||
color: var(--sb-text-muted);
|
||||
}
|
||||
|
||||
.secubox-pref-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: var(--sb-text);
|
||||
}
|
||||
|
||||
.secubox-pref-subtitle {
|
||||
margin: 6px 0 0;
|
||||
color: var(--sb-text-muted);
|
||||
}
|
||||
|
||||
.secubox-pref-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.secubox-pref-card {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
||||
min-height: 150px;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.secubox-pref-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.secubox-pref-card.success {
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.secubox-pref-card.danger {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
.secubox-pref-card.info {
|
||||
border-color: rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.secubox-pref-icon {
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.secubox-pref-label {
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--sb-text-muted);
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.secubox-pref-value {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--sb-text);
|
||||
}
|
||||
|
||||
.secubox-pref-detail {
|
||||
margin: 8px 0 0;
|
||||
color: var(--sb-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.theme-preview-panel {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.3);
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
}
|
||||
|
||||
/* Settings form details */
|
||||
.secubox-settings-page .cbi-map {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-section {
|
||||
margin: 0 0 28px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 18px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
box-shadow: 0 10px 30px rgba(8, 12, 20, 0.4);
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-section legend,
|
||||
.secubox-settings-page .cbi-section h3 {
|
||||
margin: 0 0 18px;
|
||||
font-size: 18px;
|
||||
color: var(--sb-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-value {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-value:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-value-title label,
|
||||
.secubox-settings-page .cbi-value-title {
|
||||
color: var(--sb-text);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-value-field {
|
||||
color: var(--sb-text-muted);
|
||||
}
|
||||
|
||||
.secubox-settings-page select,
|
||||
.secubox-settings-page input[type="text"],
|
||||
.secubox-settings-page input[type="number"] {
|
||||
width: 100%;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
color: var(--sb-text);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.secubox-settings-page select:focus,
|
||||
.secubox-settings-page input[type="text"]:focus,
|
||||
.secubox-settings-page input[type="number"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--sb-primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.secubox-settings-page .cbi-section-descr,
|
||||
.secubox-settings-page .cbi-value-description {
|
||||
color: var(--sb-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.secubox-health-grid {
|
||||
|
||||
@ -4,11 +4,13 @@
|
||||
|
||||
/**
|
||||
* SecuBox Theme Manager
|
||||
* Manages dark/light/system theme switching across SecuBox and all modules
|
||||
* Version: 1.0.0
|
||||
* Manages dark/light/system/cyberpunk theme switching across SecuBox modules
|
||||
* Version: 1.1.0
|
||||
*/
|
||||
|
||||
console.log('🎨 SecuBox Theme Manager v1.0.0 loaded');
|
||||
var SUPPORTED_THEMES = ['dark', 'light', 'system', 'cyberpunk'];
|
||||
|
||||
console.log('🎨 SecuBox Theme Manager v1.1.0 loaded');
|
||||
|
||||
return baseclass.extend({
|
||||
/**
|
||||
@ -20,6 +22,9 @@ return baseclass.extend({
|
||||
|
||||
return API.getTheme().then(function(data) {
|
||||
var themePref = data.theme || 'dark';
|
||||
if (SUPPORTED_THEMES.indexOf(themePref) === -1) {
|
||||
themePref = 'dark';
|
||||
}
|
||||
self.applyTheme(themePref);
|
||||
|
||||
// Listen for system theme changes if preference is 'system'
|
||||
@ -37,20 +42,21 @@ return baseclass.extend({
|
||||
|
||||
/**
|
||||
* Apply theme to the page
|
||||
* @param {string} theme - Theme preference: 'dark', 'light', or 'system'
|
||||
* @param {string} theme - Theme preference: 'dark', 'light', 'system', or 'cyberpunk'
|
||||
*/
|
||||
applyTheme: function(theme) {
|
||||
var effectiveTheme = theme;
|
||||
var selectedTheme = SUPPORTED_THEMES.indexOf(theme) > -1 ? theme : 'dark';
|
||||
var effectiveTheme = selectedTheme;
|
||||
|
||||
// If 'system', detect from OS
|
||||
if (theme === 'system' && window.matchMedia) {
|
||||
if (selectedTheme === 'system' && window.matchMedia) {
|
||||
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
// Apply theme to document root
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
||||
|
||||
console.log('🎨 Theme applied:', theme, '(effective:', effectiveTheme + ')');
|
||||
console.log('🎨 Theme applied:', selectedTheme, '(effective:', effectiveTheme + ')');
|
||||
},
|
||||
|
||||
/**
|
||||
@ -63,7 +69,7 @@ return baseclass.extend({
|
||||
|
||||
/**
|
||||
* Get theme preference from backend
|
||||
* @returns {Promise<string>} Theme preference ('dark', 'light', or 'system')
|
||||
* @returns {Promise<string>} Theme preference ('dark', 'light', 'system', or 'cyberpunk')
|
||||
*/
|
||||
getTheme: function() {
|
||||
return API.getTheme().then(function(data) {
|
||||
@ -72,5 +78,18 @@ return baseclass.extend({
|
||||
console.error('Failed to load theme preference:', err);
|
||||
return 'dark';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply and persist theme preference
|
||||
* @param {string} theme
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
setTheme: function(theme) {
|
||||
this.applyTheme(theme);
|
||||
return API.setTheme(theme).catch(function(err) {
|
||||
console.error('Failed to persist theme preference:', err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -12,6 +12,79 @@ var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: secuLang });
|
||||
|
||||
var THEME_CHOICES = ['dark', 'light', 'system', 'cyberpunk'];
|
||||
|
||||
function sanitizeTheme(theme) {
|
||||
return THEME_CHOICES.indexOf(theme) > -1 ? theme : 'dark';
|
||||
}
|
||||
|
||||
function getMainValue(option, fallback) {
|
||||
var val = uci.get('secubox', 'main', option);
|
||||
return (val != null && val !== '') ? val : fallback;
|
||||
}
|
||||
|
||||
function getMainBool(option, fallback) {
|
||||
var defaultValue = fallback ? '1' : '0';
|
||||
return getMainValue(option, defaultValue) !== '0';
|
||||
}
|
||||
|
||||
function getThemeLabel(theme) {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return _('Light');
|
||||
case 'system':
|
||||
return _('System Preference');
|
||||
case 'cyberpunk':
|
||||
return _('Cyberpunk');
|
||||
default:
|
||||
return _('Dark (Default)');
|
||||
}
|
||||
}
|
||||
|
||||
function getThemeDescription(theme) {
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return _('Bright and clean layout for well-lit spaces.');
|
||||
case 'system':
|
||||
return _('Follows your OS or browser preference automatically.');
|
||||
case 'cyberpunk':
|
||||
return _('Neon purples, synth glow effects, and extra contrast.');
|
||||
default:
|
||||
return _('Modern neon-friendly dark interface (default).');
|
||||
}
|
||||
}
|
||||
|
||||
function describeThemeChoice(theme) {
|
||||
var label = getThemeLabel(theme);
|
||||
var desc = getThemeDescription(theme);
|
||||
return desc ? label + ' - ' + desc : label;
|
||||
}
|
||||
|
||||
function formatRefreshLabel(interval) {
|
||||
switch (interval) {
|
||||
case '15':
|
||||
return _('Every 15 seconds');
|
||||
case '30':
|
||||
return _('Every 30 seconds');
|
||||
case '60':
|
||||
return _('Every minute');
|
||||
case '0':
|
||||
return _('Manual refresh only');
|
||||
default:
|
||||
return _('Every %s seconds').format(interval || '30');
|
||||
}
|
||||
}
|
||||
|
||||
function describeAutomation(autoDiscovery, autoStart) {
|
||||
if (autoDiscovery && autoStart)
|
||||
return _('New modules are discovered and auto-started.');
|
||||
if (autoDiscovery && !autoStart)
|
||||
return _('Discovery is on, but modules require manual start.');
|
||||
if (!autoDiscovery && autoStart)
|
||||
return _('Manual discovery, but auto-start once registered.');
|
||||
return _('Fully manual provisioning workflow.');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
@ -23,10 +96,23 @@ return view.extend({
|
||||
|
||||
render: function(data) {
|
||||
var status = data[1] || {};
|
||||
var theme = data[2];
|
||||
var theme = sanitizeTheme(data[2]);
|
||||
var versionPref = getMainValue('version', '0.1.2');
|
||||
var refreshPref = getMainValue('refresh_interval', '30');
|
||||
var notificationsPref = getMainBool('notifications', true);
|
||||
var autoDiscoveryPref = getMainBool('auto_discovery', true);
|
||||
var autoStartPref = getMainBool('auto_start', false);
|
||||
var secuboxEnabled = (typeof status.enabled !== 'undefined') ? !!status.enabled : getMainBool('enabled', true);
|
||||
var versionDisplay = (status.version && status.version !== 'unknown') ? status.version : versionPref;
|
||||
var moduleCount = (status.modules_total || status.modules_total === 0) ? status.modules_total : '—';
|
||||
var m, s, o;
|
||||
|
||||
// Create wrapper container with modern header
|
||||
var versionChip = this.renderHeaderChip('🏷️', _('Version'), versionDisplay || '—', 'neutral');
|
||||
var statusChip = this.renderHeaderChip('⚡', _('Status'), secuboxEnabled ? _('On') : _('Off'),
|
||||
secuboxEnabled ? 'success' : 'danger');
|
||||
var modulesChip = this.renderHeaderChip('🧩', _('Modules'), moduleCount);
|
||||
|
||||
var container = E('div', { 'class': 'secubox-settings-page' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
|
||||
@ -43,11 +129,18 @@ return view.extend({
|
||||
'Configure global settings for the SecuBox security suite')
|
||||
]),
|
||||
E('div', { 'class': 'sh-header-meta' }, [
|
||||
this.renderHeaderChip('🏷️', _('Version'), status.version || '0.1.2'),
|
||||
this.renderHeaderChip('⚡', _('Status'), status.enabled ? _('On') : _('Off'), status.enabled ? 'success' : 'danger'),
|
||||
this.renderHeaderChip('🧩', _('Modules'), status.modules_count || '14')
|
||||
versionChip,
|
||||
statusChip,
|
||||
modulesChip
|
||||
])
|
||||
])
|
||||
]),
|
||||
this.renderPreferenceShowcase({
|
||||
theme: theme,
|
||||
refresh: refreshPref,
|
||||
notifications: notificationsPref,
|
||||
autoDiscovery: autoDiscoveryPref,
|
||||
autoStart: autoStartPref
|
||||
})
|
||||
]);
|
||||
|
||||
// Create form
|
||||
@ -66,7 +159,10 @@ return view.extend({
|
||||
o = s.option(form.Value, 'version', '📦 Version',
|
||||
'Current SecuBox version (read-only)');
|
||||
o.readonly = true;
|
||||
o.default = '0.1.2';
|
||||
o.default = versionPref;
|
||||
o.cfgvalue = function() {
|
||||
return versionDisplay;
|
||||
};
|
||||
|
||||
// Dashboard Settings Section
|
||||
s = m.section(form.TypedSection, 'secubox', '📊 Dashboard Settings');
|
||||
@ -75,10 +171,94 @@ return view.extend({
|
||||
|
||||
o = s.option(form.ListValue, 'theme', '🎨 Dashboard Theme',
|
||||
'Choose the visual theme for the SecuBox dashboard');
|
||||
o.value('dark', 'Dark (Default) - Modern dark interface');
|
||||
o.value('light', 'Light - Bright and clean');
|
||||
o.value('system', 'System Preference - Auto detect');
|
||||
THEME_CHOICES.forEach(function(choice) {
|
||||
o.value(choice, describeThemeChoice(choice));
|
||||
});
|
||||
o.default = 'dark';
|
||||
o.currentThemePref = theme;
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
var widget = form.ListValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
|
||||
var select = widget.querySelector('select');
|
||||
if (!select)
|
||||
return widget;
|
||||
|
||||
var initialSelection = sanitizeTheme(this.currentThemePref || cfgvalue);
|
||||
var lastPersisted = initialSelection;
|
||||
var previewLabel = E('strong', { 'class': 'theme-preview-label' }, getThemeLabel(initialSelection));
|
||||
var previewHint = E('p', {
|
||||
'class': 'theme-preview-hint',
|
||||
'style': 'margin: 4px 0; font-size: 0.9em;'
|
||||
}, getThemeDescription(initialSelection));
|
||||
var statusLine = E('p', {
|
||||
'class': 'theme-preview-status',
|
||||
'style': 'margin: 0; font-size: 0.85em; color: var(--sh-muted, #94a3b8);'
|
||||
}, _('Synced with router preferences.'));
|
||||
|
||||
var previewPanel = E('div', {
|
||||
'class': 'theme-preview-panel',
|
||||
'style': 'margin-top: 10px; padding: 10px; border: 1px dashed var(--sh-border, #475569); border-radius: 10px;'
|
||||
}, [
|
||||
E('div', {
|
||||
'class': 'theme-preview-title',
|
||||
'style': 'font-weight: 600; font-size: 0.95em;'
|
||||
}, _('Live preview & instant save')),
|
||||
E('div', {
|
||||
'class': 'theme-preview-current',
|
||||
'style': 'margin-top: 6px; font-size: 0.95em;'
|
||||
}, [
|
||||
E('span', { 'style': 'color: var(--sh-muted, #94a3b8);' }, _('Current choice: ')),
|
||||
previewLabel
|
||||
]),
|
||||
previewHint,
|
||||
statusLine
|
||||
]);
|
||||
|
||||
widget.appendChild(previewPanel);
|
||||
|
||||
function updatePreview(choice) {
|
||||
previewLabel.textContent = getThemeLabel(choice);
|
||||
previewHint.textContent = getThemeDescription(choice);
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
statusLine.textContent = message;
|
||||
}
|
||||
|
||||
function persistTheme(choice) {
|
||||
var targetTheme = sanitizeTheme(choice);
|
||||
if (targetTheme === lastPersisted) {
|
||||
Theme.applyTheme(targetTheme);
|
||||
setStatus(_('Theme already active.'));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
select.disabled = true;
|
||||
setStatus(_('Saving theme preference...'));
|
||||
return Theme.setTheme(targetTheme).then(function() {
|
||||
lastPersisted = targetTheme;
|
||||
setStatus(_('Theme updated and saved via RPC.'));
|
||||
}).catch(function(err) {
|
||||
console.error('Failed to save SecuBox theme via RPC', err);
|
||||
ui.addNotification(null, E('p', _('Unable to update theme preference. Please try again.')), 'error');
|
||||
select.value = lastPersisted;
|
||||
updatePreview(lastPersisted);
|
||||
Theme.applyTheme(lastPersisted);
|
||||
setStatus(_('Reverted to previous theme.'));
|
||||
}).finally(function() {
|
||||
select.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
select.addEventListener('change', function(ev) {
|
||||
var nextTheme = sanitizeTheme(ev.target.value);
|
||||
updatePreview(nextTheme);
|
||||
persistTheme(nextTheme);
|
||||
});
|
||||
|
||||
// Ensure preview reflects initial value from backend
|
||||
updatePreview(initialSelection);
|
||||
return widget;
|
||||
};
|
||||
|
||||
o = s.option(form.ListValue, 'refresh_interval', '🔄 Auto-Refresh Interval',
|
||||
'How often to refresh dashboard data automatically');
|
||||
@ -239,7 +419,9 @@ return view.extend({
|
||||
|
||||
// Render form and append to container
|
||||
return m.render().then(L.bind(function(formElement) {
|
||||
container.appendChild(formElement);
|
||||
var formWrapper = E('div', { 'class': 'secubox-settings-form' }, formElement);
|
||||
container.appendChild(formWrapper);
|
||||
this.bindStatusChip(formElement, statusChip);
|
||||
return container;
|
||||
}, this));
|
||||
},
|
||||
@ -253,5 +435,79 @@ return view.extend({
|
||||
E('strong', {}, display)
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderPreferenceShowcase: function(prefs) {
|
||||
var cards = [
|
||||
this.renderPreferenceCard('🎨', _('Theme Preference'),
|
||||
getThemeLabel(prefs.theme),
|
||||
getThemeDescription(prefs.theme)),
|
||||
this.renderPreferenceCard('🔄', _('Auto Refresh'),
|
||||
formatRefreshLabel(prefs.refresh),
|
||||
_('Controls dashboard polling cadence.')),
|
||||
this.renderPreferenceCard('🔔', _('Notifications'),
|
||||
prefs.notifications ? _('Enabled') : _('Disabled'),
|
||||
prefs.notifications ? _('Browser alerts will be shown for module events.') :
|
||||
_('Silences browser alerts but logging continues.'),
|
||||
prefs.notifications ? 'success' : 'danger'),
|
||||
this.renderPreferenceCard('🤖', _('Automation'),
|
||||
prefs.autoStart ? _('Auto-start On') : _('Manual start'),
|
||||
describeAutomation(prefs.autoDiscovery, prefs.autoStart),
|
||||
prefs.autoStart ? 'info' : '')
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'secubox-pref-wrapper' }, [
|
||||
E('div', { 'class': 'secubox-pref-header' }, [
|
||||
E('div', { 'class': 'secubox-pref-title-block' }, [
|
||||
E('p', { 'class': 'secubox-pref-kicker' }, _('Configuration Snapshot')),
|
||||
E('h3', { 'class': 'secubox-pref-title' }, _('Current Preferences')),
|
||||
E('p', { 'class': 'secubox-pref-subtitle' },
|
||||
_('Key SecuBox preferences at a glance.'))
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'secubox-pref-grid' },
|
||||
cards.filter(function(card) { return !!card; }))
|
||||
]);
|
||||
},
|
||||
|
||||
renderPreferenceCard: function(icon, label, value, detail, tone) {
|
||||
return E('div', { 'class': 'secubox-pref-card sh-card' + (tone ? ' ' + tone : '') }, [
|
||||
E('div', { 'class': 'secubox-pref-icon' }, icon),
|
||||
E('div', { 'class': 'secubox-pref-body' }, [
|
||||
E('p', { 'class': 'secubox-pref-label' }, label),
|
||||
E('p', { 'class': 'secubox-pref-value' }, value),
|
||||
detail ? E('p', { 'class': 'secubox-pref-detail' }, detail) : null
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
updateHeaderChip: function(chip, value, tone) {
|
||||
if (!chip)
|
||||
return;
|
||||
|
||||
var valueEl = chip.querySelector('strong');
|
||||
if (valueEl)
|
||||
valueEl.textContent = value;
|
||||
|
||||
chip.classList.remove('success', 'danger', 'warning', 'info', 'neutral');
|
||||
if (tone)
|
||||
chip.classList.add(tone);
|
||||
},
|
||||
|
||||
bindStatusChip: function(formElement, chip) {
|
||||
if (!formElement || !chip)
|
||||
return;
|
||||
var toggle = formElement.querySelector('input[name="cbid.secubox.secubox.enabled"]');
|
||||
if (!toggle)
|
||||
return;
|
||||
|
||||
var self = this;
|
||||
var sync = function() {
|
||||
var isOn = toggle.checked;
|
||||
self.updateHeaderChip(chip, isOn ? _('On') : _('Off'), isOn ? 'success' : 'danger');
|
||||
};
|
||||
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,9 +1,29 @@
|
||||
config secubox 'main'
|
||||
option enabled '1'
|
||||
option version '0.1.2'
|
||||
option version '0.5.0-A'
|
||||
option auto_discovery '1'
|
||||
option auto_start '0'
|
||||
option notifications '1'
|
||||
option notify_module_start '1'
|
||||
option notify_module_stop '1'
|
||||
option notify_alerts '1'
|
||||
option notify_health_issues '1'
|
||||
option refresh_interval '30'
|
||||
option show_system_stats '1'
|
||||
option show_module_grid '1'
|
||||
option theme 'dark'
|
||||
option require_auth '1'
|
||||
option audit_logging '1'
|
||||
option audit_retention '30'
|
||||
option debug_mode '0'
|
||||
option api_timeout '30'
|
||||
option max_modules '20'
|
||||
option cpu_warning '70'
|
||||
option cpu_critical '85'
|
||||
option memory_warning '70'
|
||||
option memory_critical '85'
|
||||
option disk_warning '70'
|
||||
option disk_critical '85'
|
||||
|
||||
# Module definitions - populated dynamically when modules are installed
|
||||
# Each module adds its own section on install
|
||||
|
||||
@ -8,9 +8,45 @@
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
get_pkg_version() {
|
||||
local version=""
|
||||
|
||||
# OpenWrt 25.12+ uses apk packages
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
local apk_line=$(apk info -v luci-app-secubox 2>/dev/null | grep -E '^luci-app-secubox-[0-9]' | head -n1)
|
||||
if [ -n "$apk_line" ]; then
|
||||
version=$(echo "$apk_line" | sed 's/^luci-app-secubox-//' | awk '{print $1}' | sed 's/-r[0-9]*$//')
|
||||
else
|
||||
version=$(apk info luci-app-secubox 2>/dev/null | awk -F': ' '/^Version/ {print $2; exit}')
|
||||
fi
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Legacy opkg metadata
|
||||
local ctrl="/usr/lib/opkg/info/luci-app-secubox.control"
|
||||
if [ -f "$ctrl" ]; then
|
||||
awk -F': ' '/^Version/ { print $2; exit }' "$ctrl"
|
||||
version=$(awk -F': ' '/^Version/ { print $2; exit }' "$ctrl")
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
version=$(opkg list-installed luci-app-secubox 2>/dev/null | awk '{print $3}' | sed 's/-r[0-9]*$//' | head -n1)
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
local cfg_version="$(uci -q get secubox.main.version)"
|
||||
if [ -n "$cfg_version" ]; then
|
||||
echo "$cfg_version"
|
||||
elif [ -f "/usr/share/secubox/VERSION" ]; then
|
||||
head -n1 /usr/share/secubox/VERSION
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
@ -138,6 +174,7 @@ get_status() {
|
||||
json_init
|
||||
json_add_string "version" "$PKG_VERSION"
|
||||
json_add_string "hostname" "$(uci -q get system.@system[0].hostname || echo 'SecuBox')"
|
||||
json_add_boolean "enabled" "$(uci -q get secubox.main.enabled || echo 1)"
|
||||
|
||||
# System info
|
||||
local uptime=$(cat /proc/uptime | cut -d. -f1)
|
||||
@ -966,6 +1003,36 @@ get_theme() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Persist theme setting
|
||||
set_theme() {
|
||||
local input theme="dark"
|
||||
|
||||
read -r input
|
||||
[ -n "$input" ] && json_load "$input"
|
||||
json_get_var theme theme "dark"
|
||||
|
||||
case "$theme" in
|
||||
dark|light|system|cyberpunk)
|
||||
;;
|
||||
*)
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "message" "Invalid theme"
|
||||
json_dump
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
uci -q set secubox.main.theme="$theme"
|
||||
uci -q commit secubox
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "theme" "$theme"
|
||||
json_add_string "message" "Theme updated"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Dismiss a specific alert
|
||||
dismiss_alert() {
|
||||
local alert_id="$1"
|
||||
@ -1069,6 +1136,9 @@ case "$1" in
|
||||
json_close_object
|
||||
json_add_object "get_theme"
|
||||
json_close_object
|
||||
json_add_object "set_theme"
|
||||
json_add_string "theme" "string"
|
||||
json_close_object
|
||||
json_add_object "dismiss_alert"
|
||||
json_add_string "alert_id" "string"
|
||||
json_close_object
|
||||
@ -1161,6 +1231,9 @@ case "$1" in
|
||||
get_theme)
|
||||
get_theme
|
||||
;;
|
||||
set_theme)
|
||||
set_theme
|
||||
;;
|
||||
dismiss_alert)
|
||||
read -r input
|
||||
json_load "$input"
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"enable_module",
|
||||
"disable_module",
|
||||
"quick_action",
|
||||
"set_theme",
|
||||
"dismiss_alert",
|
||||
"clear_alerts",
|
||||
"fix_permissions"
|
||||
|
||||
555
secubox-tools/quick-deploy.sh
Executable file
555
secubox-tools/quick-deploy.sh
Executable file
@ -0,0 +1,555 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROUTER="${ROUTER:-root@192.168.8.191}"
|
||||
TARGET_PATH="${TARGET_PATH:-/www/luci-static}"
|
||||
SSH_OPTS=${SSH_OPTS:--o RequestTTY=no -o ForwardX11=no}
|
||||
SCP_OPTS=${SCP_OPTS:-}
|
||||
CACHE_BUST=${CACHE_BUST:-1}
|
||||
VERIFY=${VERIFY:-1}
|
||||
FORCE_ROOT="false"
|
||||
INCLUDE_PATHS=()
|
||||
VERIFY_ERRORS=0
|
||||
PROFILE=""
|
||||
APP_NAME=""
|
||||
APP_PATH=""
|
||||
AUTO_PROFILE=${AUTO_PROFILE:-1}
|
||||
LIST_APPS=0
|
||||
REMOTE_HASH_CMD=""
|
||||
|
||||
MODE=""
|
||||
PKG_PATH=""
|
||||
SRC_PATH=""
|
||||
GIT_URL=""
|
||||
GIT_BRANCH=""
|
||||
POST_CMD=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: quick-deploy.sh [options]
|
||||
|
||||
Deploy packages or source archives to the development router.
|
||||
|
||||
Options (choose one source):
|
||||
--ipk <file.ipk> Upload + install an IPK via opkg.
|
||||
--apk <file.apk> Upload + install an APK via apk add.
|
||||
--src <path> Tar + upload a local directory to --target-path.
|
||||
--git <repo_url> Clone repo (optionally --branch) then upload.
|
||||
--profile <name> Use a predefined deployment profile (e.g. theme, luci-app).
|
||||
--app <name> Shortcut for --profile luci-app; auto-resolves `luci-app-<name>`
|
||||
--list-apps List detected `luci-app-*` directories and exit.
|
||||
|
||||
Common flags:
|
||||
--router <user@host> Override router target (default root@192.168.8.191).
|
||||
--target-path <path> Destination for source uploads (default /www/luci-static).
|
||||
--include <subpath> Repeatable. Only include matching subpaths when using --src/--git.
|
||||
--branch <name> Git branch/tag when using --git.
|
||||
--no-cache-bust Skip clearing /tmp/luci-* after deploy.
|
||||
--no-verify Skip post-deploy file verification.
|
||||
--force-root Allow --src to write directly under /. Use with caution.
|
||||
--no-auto-profile Disable automatic LuCI app detection when using --src.
|
||||
--post <command> Extra remote command to run after deploy.
|
||||
-h, --help Show this message.
|
||||
|
||||
Environment variables:
|
||||
ROUTER, TARGET_PATH, SSH_OPTS, SCP_OPTS can also be exported ahead of time.
|
||||
USAGE
|
||||
exit 1
|
||||
}
|
||||
|
||||
log() {
|
||||
echo -e "[$(date +'%H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
remote_exec() {
|
||||
ssh $SSH_OPTS "$ROUTER" "$@"
|
||||
}
|
||||
|
||||
copy_file() {
|
||||
scp $SCP_OPTS "$1" "$ROUTER:$2"
|
||||
}
|
||||
|
||||
join_path() {
|
||||
local base="$1"
|
||||
local rel="$2"
|
||||
if [[ "$base" == "/" ]]; then
|
||||
echo "/$rel" | sed 's#//\+#/#g'
|
||||
else
|
||||
echo "$base/$rel" | sed 's#//\+#/#g'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_remote_hash() {
|
||||
if [[ -n "$REMOTE_HASH_CMD" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local cmd
|
||||
cmd=$(remote_exec "for c in sha1sum sha256sum md5sum; do if command -v \$c >/dev/null 2>&1; then echo \$c; break; fi; done") || true
|
||||
if [[ -z "$cmd" ]]; then
|
||||
log "⚠️ No hash utility found on router; skipping verification"
|
||||
VERIFY=0
|
||||
return 1
|
||||
fi
|
||||
REMOTE_HASH_CMD="$cmd"
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_remote() {
|
||||
local dir="$1"
|
||||
local base="$TARGET_PATH"
|
||||
[[ "$FORCE_ROOT" == "true" ]] && base="/"
|
||||
ensure_remote_hash || return
|
||||
local -a candidates
|
||||
if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then
|
||||
for inc in "${INCLUDE_PATHS[@]}"; do
|
||||
local path="$dir/$inc"
|
||||
if [[ -d "$path" ]]; then
|
||||
while IFS= read -r f; do
|
||||
candidates+=("$f")
|
||||
done < <(find "$path" -type f -not -path '*/.git/*' | sort)
|
||||
elif [[ -f "$path" ]]; then
|
||||
candidates+=("$path")
|
||||
fi
|
||||
done
|
||||
else
|
||||
while IFS= read -r f; do
|
||||
candidates+=("$f")
|
||||
done < <(find "$dir" -type f -not -path '*/.git/*' | sort)
|
||||
fi
|
||||
local -a samples
|
||||
for f in "${candidates[@]}"; do
|
||||
samples+=("$f")
|
||||
[[ ${#samples[@]} -ge 5 ]] && break
|
||||
done
|
||||
if [[ ${#samples[@]} -eq 0 ]]; then
|
||||
log "No files to verify"
|
||||
return
|
||||
fi
|
||||
log "Verifying ${#samples[@]} files on router..."
|
||||
for file in "${samples[@]}"; do
|
||||
local rel=${file#$dir/}
|
||||
local local_sum=$($REMOTE_HASH_CMD "$file" | awk '{print $1}')
|
||||
local remote_path=$(join_path "$base" "$rel")
|
||||
local remote_sum
|
||||
remote_sum=$(remote_exec "if [ -f '$remote_path' ]; then $REMOTE_HASH_CMD '$remote_path' | awk '{print \\$1}'; fi") || true
|
||||
if [[ -z "$remote_sum" ]]; then
|
||||
log "⚠️ Missing remote file: $remote_path"
|
||||
VERIFY_ERRORS=1
|
||||
elif [[ "$remote_sum" != "$local_sum" ]]; then
|
||||
log "⚠️ Mismatch for $remote_path"
|
||||
log " local: $local_sum"
|
||||
log " remote: $remote_sum"
|
||||
VERIFY_ERRORS=1
|
||||
else
|
||||
log "✅ $rel"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
collect_luci_apps() {
|
||||
find . -maxdepth 1 -type d -name 'luci-app-*' | LC_ALL=C sort
|
||||
}
|
||||
|
||||
list_luci_apps() {
|
||||
local apps=()
|
||||
while IFS= read -r d; do
|
||||
apps+=("${d#./}")
|
||||
done < <(collect_luci_apps)
|
||||
if [[ ${#apps[@]} -eq 0 ]]; then
|
||||
log "No luci-app-* directories detected in $(pwd)"
|
||||
return 1
|
||||
fi
|
||||
log "Available LuCI apps:"
|
||||
for d in "${apps[@]}"; do
|
||||
printf ' - %s\n' "$d"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
prompt_select_app() {
|
||||
local apps=()
|
||||
while IFS= read -r d; do
|
||||
apps+=("${d#./}")
|
||||
done < <(collect_luci_apps)
|
||||
if [[ ${#apps[@]} -eq 0 ]]; then
|
||||
echo "No LuCI apps discovered." >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -t 0 ]]; then
|
||||
printf 'Available apps:%s' "\n"
|
||||
for d in "${apps[@]}"; do
|
||||
printf ' - %s\n' "$d"
|
||||
done
|
||||
echo "(non-interactive shell: rerun with --app <name>)" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Select a LuCI app to deploy (type number or name, q to abort):"
|
||||
local old_ps3=${PS3:-""}
|
||||
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
|
||||
PS3="$old_ps3"
|
||||
}
|
||||
|
||||
resolve_app_dir() {
|
||||
local input="$1"
|
||||
if [[ -z "$input" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ -d "$input" ]]; then
|
||||
echo "$input"
|
||||
return 0
|
||||
fi
|
||||
if [[ -d "luci-app-$input" ]]; then
|
||||
echo "luci-app-$input"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
normalize_app_path() {
|
||||
local input="$1"
|
||||
local candidate=""
|
||||
|
||||
if [[ -z "$input" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If absolute path, trust it
|
||||
if [[ "$input" == /* ]]; then
|
||||
candidate="$input"
|
||||
else
|
||||
candidate="$PWD/$input"
|
||||
fi
|
||||
|
||||
if [[ -d "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to raw input (in case caller already passed ./relative)
|
||||
if [[ -d "$input" ]]; then
|
||||
if [[ "$input" == /* ]]; then
|
||||
echo "$input"
|
||||
else
|
||||
echo "$PWD/$input"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
deploy_profile_theme() {
|
||||
log "🎨 Deploying theme profile to $ROUTER"
|
||||
local files=(
|
||||
"luci-app-secubox/root/usr/libexec/rpcd/luci.secubox:/usr/libexec/rpcd/"
|
||||
"luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json:/usr/share/rpcd/acl.d/"
|
||||
"luci-app-secubox/htdocs/luci-static/resources/secubox/api.js:/www/luci-static/resources/secubox/"
|
||||
"luci-app-secubox/htdocs/luci-static/resources/secubox/theme.js:/www/luci-static/resources/secubox/"
|
||||
"luci-app-secubox/htdocs/luci-static/resources/secubox/secubox.css:/www/luci-static/resources/secubox/"
|
||||
"luci-app-secubox/htdocs/luci-static/resources/view/secubox/dashboard.js:/www/luci-static/resources/view/secubox/"
|
||||
"luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme.js:/www/luci-static/resources/system-hub/"
|
||||
"luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css:/www/luci-static/resources/system-hub/"
|
||||
"luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js:/www/luci-static/resources/view/system-hub/"
|
||||
)
|
||||
for entry in "${files[@]}"; do
|
||||
local src=${entry%%:*}
|
||||
local dest=${entry##*:}
|
||||
log "Copying $src -> $dest"
|
||||
copy_file "$src" "$dest"
|
||||
done
|
||||
log "Setting permissions + restarting rpcd"
|
||||
remote_exec "chmod +x /usr/libexec/rpcd/luci.secubox && \\
|
||||
chmod 644 /www/luci-static/resources/secubox/*.{js,css} 2>/dev/null || true && \\
|
||||
chmod 644 /www/luci-static/resources/system-hub/*.{js,css} 2>/dev/null || true && \\
|
||||
chmod 644 /www/luci-static/resources/view/secubox/*.js 2>/dev/null || true && \\
|
||||
chmod 644 /www/luci-static/resources/view/system-hub/*.js 2>/dev/null || true && \\
|
||||
rm -rf /tmp/luci-* && /etc/init.d/rpcd restart"
|
||||
}
|
||||
|
||||
deploy_profile_luci_app() {
|
||||
local app_dir="$1"
|
||||
if [[ -z "$app_dir" || ! -d "$app_dir" ]]; then
|
||||
echo "Error: --profile luci-app requires --src to point at the application root" >&2
|
||||
exit 1
|
||||
fi
|
||||
local app_name=$(basename "$app_dir")
|
||||
log "📦 Deploying LuCI app $app_name"
|
||||
local prev_target="$TARGET_PATH"
|
||||
local prev_force="$FORCE_ROOT"
|
||||
local prev_includes=("${INCLUDE_PATHS[@]}")
|
||||
INCLUDE_PATHS=()
|
||||
local root_src="$app_dir/root"
|
||||
local htdocs_src="$app_dir/htdocs"
|
||||
if [[ -d "$root_src" ]]; then
|
||||
FORCE_ROOT="true"
|
||||
TARGET_PATH="/"
|
||||
upload_source_dir "$root_src"
|
||||
fi
|
||||
if [[ -d "$htdocs_src" ]]; then
|
||||
FORCE_ROOT="false"
|
||||
TARGET_PATH="/www"
|
||||
upload_source_dir "$htdocs_src"
|
||||
fi
|
||||
remote_exec "rm -rf /tmp/luci-*"
|
||||
TARGET_PATH="$prev_target"
|
||||
FORCE_ROOT="$prev_force"
|
||||
INCLUDE_PATHS=("${prev_includes[@]}")
|
||||
}
|
||||
|
||||
cleanup_tmp=""
|
||||
trap '[[ -n "$cleanup_tmp" && -d "$cleanup_tmp" ]] && rm -rf "$cleanup_tmp"' EXIT
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--router)
|
||||
ROUTER="$2"; shift 2 ;;
|
||||
--target-path)
|
||||
TARGET_PATH="$2"; shift 2 ;;
|
||||
--include)
|
||||
INCLUDE_PATHS+=("$2"); shift 2 ;;
|
||||
--ipk)
|
||||
MODE="ipk"; PKG_PATH="$2"; shift 2 ;;
|
||||
--apk)
|
||||
MODE="apk"; PKG_PATH="$2"; shift 2 ;;
|
||||
--src)
|
||||
MODE="src"; SRC_PATH="$2"; shift 2 ;;
|
||||
--src-select)
|
||||
MODE="src"; SRC_PATH=""; shift ;;
|
||||
--git)
|
||||
MODE="git"; GIT_URL="$2"; shift 2 ;;
|
||||
--profile)
|
||||
PROFILE="$2"; shift 2 ;;
|
||||
--app)
|
||||
APP_NAME="$2"; shift 2 ;;
|
||||
--list-apps)
|
||||
LIST_APPS=1; shift ;;
|
||||
--branch)
|
||||
GIT_BRANCH="$2"; shift 2 ;;
|
||||
--post)
|
||||
POST_CMD="$2"; shift 2 ;;
|
||||
--no-cache-bust)
|
||||
CACHE_BUST=0; shift ;;
|
||||
--no-verify)
|
||||
VERIFY=0; shift ;;
|
||||
--force-root)
|
||||
FORCE_ROOT="true"; shift ;;
|
||||
--no-auto-profile)
|
||||
AUTO_PROFILE=0; shift ;;
|
||||
-h|--help)
|
||||
usage ;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$APP_NAME" == "list" ]]; then
|
||||
LIST_APPS=1
|
||||
APP_NAME=""
|
||||
fi
|
||||
|
||||
if [[ $LIST_APPS -eq 1 ]]; then
|
||||
list_luci_apps
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "$APP_NAME" ]]; then
|
||||
APP_PATH=$(resolve_app_dir "$APP_NAME") || true
|
||||
if [[ -z "$APP_PATH" ]]; then
|
||||
echo "Unable to locate LuCI app '$APP_NAME'" >&2
|
||||
list_luci_apps
|
||||
exit 1
|
||||
fi
|
||||
SRC_PATH=$(normalize_app_path "$APP_PATH") || {
|
||||
echo "Unable to normalize app path '$APP_PATH'" >&2
|
||||
exit 1
|
||||
}
|
||||
PROFILE="luci-app"
|
||||
MODE=""
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "src" && -z "$SRC_PATH" ]]; then
|
||||
if [[ -t 0 ]]; then
|
||||
selection=$(prompt_select_app) || { echo "Aborting." >&2; exit 1; }
|
||||
SRC_PATH=$(normalize_app_path "$selection") || {
|
||||
echo "Unable to locate LuCI app directory for '$selection'" >&2
|
||||
exit 1
|
||||
}
|
||||
PROFILE="${PROFILE:-luci-app}"
|
||||
MODE=""
|
||||
log "Selected LuCI app path: $SRC_PATH"
|
||||
else
|
||||
list_luci_apps
|
||||
exit 1
|
||||
fi
|
||||
elif [[ -n "$SRC_PATH" && ! -d "$SRC_PATH" ]]; then
|
||||
echo "Specified --src path '$SRC_PATH' not found."
|
||||
if [[ -t 0 ]]; then
|
||||
selection=$(prompt_select_app) || { echo "Aborting." >&2; exit 1; }
|
||||
SRC_PATH=$(normalize_app_path "$selection") || {
|
||||
echo "Unable to locate LuCI app directory for '$selection'" >&2
|
||||
exit 1
|
||||
}
|
||||
PROFILE="${PROFILE:-luci-app}"
|
||||
MODE=""
|
||||
log "Selected LuCI app path: $SRC_PATH"
|
||||
else
|
||||
list_luci_apps
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$PROFILE" && "$MODE" == "src" && "$AUTO_PROFILE" -eq 1 && -n "$SRC_PATH" && ( -d "$SRC_PATH/root" || -d "$SRC_PATH/htdocs" ) ]]; then
|
||||
PROFILE="luci-app"
|
||||
MODE=""
|
||||
log "Auto-detected LuCI app at $SRC_PATH (use --no-auto-profile to disable)."
|
||||
fi
|
||||
|
||||
if [[ -z "$MODE" && -z "$PROFILE" ]]; then
|
||||
echo "Error: specify one of --ipk/--apk/--src/--git or --profile" >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ -n "$MODE" && -n "$PROFILE" ]]; then
|
||||
echo "Error: --profile cannot be combined with other source options" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORCE_ROOT" == "true" && "$MODE" != "src" && "$PROFILE" != "luci-app" ]]; then
|
||||
echo "Error: --force-root is only valid with --src" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORCE_ROOT" == "true" && "$PROFILE" != "luci-app" ]]; then
|
||||
log "⚠️ Force root mode enabled: archives will extract relative to /"
|
||||
fi
|
||||
|
||||
if [[ "$MODE" =~ ^(ipk|apk)$ && ! -f "$PKG_PATH" ]]; then
|
||||
echo "Error: package file not found: $PKG_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "git" && -z "$GIT_URL" ]]; then
|
||||
echo "Error: --git requires a repository URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install_ipk() {
|
||||
local file="$1"
|
||||
local remote="/tmp/$(basename "$file")"
|
||||
log "Uploading $file to $remote"
|
||||
copy_file "$file" "$remote"
|
||||
log "Installing via opkg"
|
||||
remote_exec "if command -v opkg >/dev/null 2>&1; then opkg install --force-reinstall $remote; else echo 'opkg not available' >&2; exit 1; fi"
|
||||
remote_exec "rm -f $remote"
|
||||
}
|
||||
|
||||
install_apk() {
|
||||
local file="$1"
|
||||
local remote="/tmp/$(basename "$file")"
|
||||
log "Uploading $file to $remote"
|
||||
copy_file "$file" "$remote"
|
||||
log "Installing via apk"
|
||||
remote_exec "if command -v apk >/dev/null 2>&1; then apk add --allow-untrusted $remote; else echo 'apk not available' >&2; exit 1; fi"
|
||||
remote_exec "rm -f $remote"
|
||||
}
|
||||
|
||||
upload_source_dir() {
|
||||
local dir="$1"
|
||||
local archive
|
||||
archive=$(mktemp /tmp/secubox-src-XXXX.tar.gz)
|
||||
log "Packing $dir"
|
||||
if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then
|
||||
( cd "$dir" && tar -czf "$archive" "${INCLUDE_PATHS[@]}" )
|
||||
else
|
||||
tar -C "$dir" -czf "$archive" .
|
||||
fi
|
||||
local remote="/tmp/$(basename "$archive")"
|
||||
log "Uploading archive to $remote"
|
||||
copy_file "$archive" "$remote"
|
||||
local extract_target="$TARGET_PATH"
|
||||
if [[ "$FORCE_ROOT" == "true" ]]; then
|
||||
extract_target="/"
|
||||
fi
|
||||
log "Extracting to $extract_target"
|
||||
remote_exec "mkdir -p $extract_target && tar -xzf $remote -C $extract_target && rm -f $remote"
|
||||
if [[ "$CACHE_BUST" -eq 1 ]]; then
|
||||
remote_exec "rm -rf /tmp/luci-*"
|
||||
fi
|
||||
rm -f "$archive"
|
||||
if [[ "$VERIFY" -eq 1 ]]; then
|
||||
verify_remote "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
clone_and_upload() {
|
||||
cleanup_tmp=$(mktemp -d /tmp/secubox-git-XXXX)
|
||||
log "Cloning $GIT_URL"
|
||||
if [[ -n "$GIT_BRANCH" ]]; then
|
||||
git clone --depth 1 --branch "$GIT_BRANCH" "$GIT_URL" "$cleanup_tmp"
|
||||
else
|
||||
git clone --depth 1 "$GIT_URL" "$cleanup_tmp"
|
||||
fi
|
||||
upload_source_dir "$cleanup_tmp"
|
||||
}
|
||||
|
||||
if [[ -n "$PROFILE" ]]; then
|
||||
case "$PROFILE" in
|
||||
theme|theme-system)
|
||||
deploy_profile_theme ;;
|
||||
luci-app)
|
||||
if [[ -z "$SRC_PATH" ]]; then
|
||||
if [[ -t 0 ]]; then
|
||||
selection=$(prompt_select_app) || { echo "Aborting." >&2; exit 1; }
|
||||
SRC_PATH=$(normalize_app_path "$selection") || {
|
||||
echo "Unable to locate LuCI app directory for '$selection'" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
list_luci_apps
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
deploy_profile_luci_app "$SRC_PATH" ;;
|
||||
*)
|
||||
echo "Unknown profile: $PROFILE" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
else
|
||||
case "$MODE" in
|
||||
ipk)
|
||||
install_ipk "$PKG_PATH" ;;
|
||||
apk)
|
||||
install_apk "$PKG_PATH" ;;
|
||||
src)
|
||||
upload_source_dir "$SRC_PATH" ;;
|
||||
git)
|
||||
clone_and_upload ;;
|
||||
*)
|
||||
echo "Unsupported mode: $MODE" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ -n "$POST_CMD" ]]; then
|
||||
log "Running post-deploy command: $POST_CMD"
|
||||
remote_exec "$POST_CMD"
|
||||
fi
|
||||
|
||||
if [[ "$VERIFY" -eq 1 && $VERIFY_ERRORS -ne 0 ]]; then
|
||||
log "⚠️ Verification reported differences. Inspect logs above."
|
||||
fi
|
||||
|
||||
log "Deployment complete ✅"
|
||||
Loading…
Reference in New Issue
Block a user