feat: cascade navigation and zigbee presets

This commit is contained in:
CyberMind-FR 2025-12-29 14:40:22 +01:00
parent 4dea8d28e0
commit 54e0b5df6c
45 changed files with 2848 additions and 288 deletions

View File

@ -2,6 +2,8 @@
## Completed Today
- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system.
- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates.
- 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.
@ -17,7 +19,8 @@
- Preparing follow-up refactor to deduplicate Theme initialization logic.
- Evaluating automated deployment pipeline (rsync/scp wrappers) for `secubox-tools`.
- Enhancing SecuBox theme guidelines (see `.codex/THEME_CONTEXT.md`) to capture layout, state, and localization best practices before next UI sprint.
- Next TODO in focus: extract shared nav/header components into `secubox/components/` and document typings per `.codex/TODO.md` item #1.
- Next TODO in focus: rewrite Network Modes views so each mode loads its config, reacts to preferences, and stays theme-aligned (per `.codex/TODO.md` #1).
- Scaffolded MQTT Bridge module (`luci-app-mqtt-bridge`) and tracking follow-up work under `.codex/apps/mqtt-bridge/`.
## Reminders

View File

@ -0,0 +1,17 @@
# TODO MQTT Bridge
1. **Daemon Integration**
- Implement `/usr/sbin/mqtt-bridge` watcher handling USB serial adapters.
- Emit stats to `uci set mqtt-bridge.stats.*` for UI refresh.
2. **Security**
- Support TLS options (CA, client certs) in Settings.
- Add access control for pairing window.
3. **Automations**
- Add topic templates per device type (Zigbee, Modbus).
- Provide rules to forward payloads into SecuBox Alerts.
4. **Profiles**
- Promote detected presets into editable device entries (auto-populate `/etc/config/mqtt-bridge`).
- Support multiple adapters simultaneously and expose health metrics per profile.

View File

@ -0,0 +1,13 @@
# MQTT Bridge WIP
## Completed
- Scaffolded `luci-app-mqtt-bridge` with SecuBox-themed views (overview/devices/settings).
- Added RPC backend (`luci.mqtt-bridge`) and UCI defaults for broker/bridge stats.
- Added Zigbee/SMSC USB2134B preset detection (USB VID/PID scan, tty hinting, LuCI cards + docs).
## In Progress
- Flesh out real USB discovery and MQTT client integration.
- Hook pairing trigger to actual daemon and persist payload history.
## Notes
- Module is disabled by default via `secubox` config; enabling happens through SecuBox Modules page once backend daemon exists.

76
DOCS/MQTT_BRIDGE.md Normal file
View File

@ -0,0 +1,76 @@
# MQTT Bridge Module
**Version:** 0.1.0
**Status:** Draft
SecuBox MQTT Bridge exposes USB dongles and IoT sensors through a themed LuCI interface.
## Components
- **Overview** broker health, connected adapters, recent payloads.
- **Devices** paired USB devices with status (online/offline).
- **Settings** broker credentials, base topic templates, retention.
## RPC API (`luci.mqtt-bridge`)
| Method | Description |
|--------|-------------|
| `status` | Broker metrics, stored payloads, and current settings. |
| `list_devices` | Enumerates paired USB/sensor nodes. |
| `trigger_pairing` | Opens pairing window (2 minutes). |
| `apply_settings` | Persists broker/bridge configuration. |
`status` now also includes a `profiles` array describing detected USB/Zigbee presets. Each entry exposes:
| Field | Description |
| ----- | ----------- |
| `id` | Internal preset identifier (e.g. `zigbee_usb2134`). |
| `label` | Friendly adapter name from USB descriptors. |
| `vendor` / `product` | USB VID:PID pair. |
| `bus` / `device` | Linux bus/device numbers as seen in `dmesg`/`lsusb`. |
| `port` | Resolved `/dev/tty*` path when available. |
| `detected` | Boolean flag (`true` when the dongle is currently attached). |
| `notes` | Human readable hints rendered in the Devices view. |
## Files
```
luci-app-mqtt-bridge/
├── htdocs/luci-static/resources/view/mqtt-bridge/*.js
├── htdocs/luci-static/resources/mqtt-bridge/common.css
├── root/usr/libexec/rpcd/luci.mqtt-bridge
├── root/usr/share/luci/menu.d/luci-app-mqtt-bridge.json
├── root/usr/share/rpcd/acl.d/luci-app-mqtt-bridge.json
└── root/etc/config/mqtt-bridge
```
## Zigbee / SMSC USB2134B profile
The Devices tab now surfaces a preset for the "Bus 003 Device 002: ID 0424:2134 SMSC USB2134B" bridge that is commonly flashed with Zigbee coordinator firmware. The LuCI view consumes the `profiles` array explained above and displays the current detection state together with the tty hint.
To verify the dongle manually:
```bash
dmesg | tail -n 40 | grep -E '0424:2134|usb 3-1'
lsusb -d 0424:2134
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null
```
Typical kernel log:
```
[ 6456.735692] usb 3-1.1: USB disconnect, device number 3
[ 6459.021458] usb 3-1.1: new full-speed USB device number 4 using xhci-hcd
```
Match the reported Bus/Device numbers with `/sys/bus/usb/devices/*/busnum` and `/sys/bus/usb/devices/*/devnum`; the RPC helper inspects those files and publishes the resolved `/dev/tty*` path (when exported under `/sys/bus/usb/devices/*/tty`). If the adapter is not plugged in, the UI still renders the preset so operators know exactly which VID/PID pair to look for.
Once the tty node is confirmed, update `/etc/config/mqtt-bridge` and restart the bridge service to bind Zigbee traffic to the MQTT topics defined in the Settings tab.
## Next steps
- Add real daemon integration with Mosquitto.
- Support TLS and per-device topics.
- Emit SecuBox alerts on sensor thresholds.
See `.codex/apps/mqtt-bridge/TODO.md` for the evolving backlog.

View File

@ -232,6 +232,20 @@ Local CDN cache proxy for bandwidth savings.
---
#### **luci-app-mqtt-bridge** - IoT MQTT Hub
USB-aware MQTT bridge for sensors and automation gear.
**Features:**
- 🔌 Detects USB serial adapters and exposes pairing wizard
- 📡 Publishes payloads to the built-in MQTT broker with topic templates
- 🧊 Retains last payloads and surfaces metrics/clients in SecuBox theme
- 🔐 Broker credential + retention management from the UI
- 📁 Saves configuration snapshots for rollback
[View Details](luci-app-mqtt-bridge/README.md)
---
#### **luci-app-vhost-manager** - Virtual Hosts
Virtual host and local SaaS gateway management.

View File

@ -59,6 +59,7 @@ return view.extend({
return E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('cache'),
this.renderHero(items, domains),

View File

@ -47,6 +47,8 @@ return view.extend({
var self = this;
return E('div', { 'class': 'cbi-map cdn-maintenance' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
E('style', {}, `
.cdn-maintenance { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.cdn-page-header { background: linear-gradient(135deg, #0891b2, #06b6d4); color: white; padding: 24px; border-radius: 12px; margin-bottom: 24px; }

View File

@ -72,6 +72,7 @@ return view.extend({
return E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('overview'),
this.renderHeader(status),

View File

@ -42,6 +42,8 @@ return view.extend({
var exclusions = data[1].exclusions || [];
return E('div', { 'class': 'cbi-map cdn-policies' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
E('style', {}, `
.cdn-policies { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.cdn-page-header { background: linear-gradient(135deg, #0891b2, #06b6d4); color: white; padding: 24px; border-radius: 12px; margin-bottom: 24px; }

View File

@ -139,6 +139,8 @@ return view.extend({
o.default = '60';
return E('div', { 'class': 'cdn-settings-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
CdnNav.renderTabs('settings'),
m.render()
]);

View File

@ -60,6 +60,7 @@ return view.extend({
var bandwidthTrend = (data[2] && data[2].data) || [];
var view = E('div', { 'class': 'cdn-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
CdnNav.renderTabs('statistics'),
this.renderHero(stats),

View File

@ -0,0 +1,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-mqtt-bridge
PKG_VERSION:=0.1.0
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
LUCI_TITLE:=SecuBox MQTT Bridge
LUCI_DESCRIPTION:=USB-to-MQTT IoT hub with SecuBox theme
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +mqtt-client +libuci
LUCI_PKGARCH:=all
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.mqtt-bridge:root:root:755
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage

View File

@ -0,0 +1,25 @@
# SecuBox MQTT Bridge
**Version:** 0.1.0
**Status:** Draft
USB-aware MQTT orchestrator for SecuBox routers. The application discovers USB serial dongles, bridges sensor payloads to a built-in MQTT broker, and exposes dashboards/settings with SecuBox theme tokens.
## Views
- `overview.js` broker status, metrics, quick actions.
- `devices.js` USB/tasmota sensor list with pairing wizard.
- `settings.js` broker credentials, topic templates, retention options.
## RPC Methods
- `status` broker uptime, clients, last payloads.
- `list_devices` detected USB devices & pairing state.
- `apply_settings` broker credentials/storage.
- `trigger_pairing` start pairing flow for sensors.
The LuCI views depend on the SecuBox theme bundle included in `luci-theme-secubox`.
## Development Notes
See `.codex/apps/mqtt-bridge/WIP.md` for current tasks and `.codex/apps/mqtt-bridge/TODO.md` for backlog/high-level goals.

View File

@ -0,0 +1,33 @@
'use strict';
'require baseclass';
'require rpc';
var callStatus = rpc.declare({
object: 'luci.mqtt-bridge',
method: 'status',
expect: {}
});
var callListDevices = rpc.declare({
object: 'luci.mqtt-bridge',
method: 'list_devices',
expect: { devices: [] }
});
var callTriggerPairing = rpc.declare({
object: 'luci.mqtt-bridge',
method: 'trigger_pairing',
expect: {}
});
var callApplySettings = rpc.declare({
object: 'luci.mqtt-bridge',
method: 'apply_settings'
});
return baseclass.extend({
getStatus: callStatus,
listDevices: callListDevices,
triggerPairing: callTriggerPairing,
applySettings: callApplySettings
});

View File

@ -0,0 +1,716 @@
/**
* SecuBox Module - Common Styles (Design System v0.3.0)
* Shared styles for consistent SecuBox design across all modules
* Based on: https://cybermind.fr/apps/system-hub/demo.html
* Version: 0.3.0
*/
/* === Import Fonts === */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap');
/* === Design System Variables === */
:root {
/* Light Mode */
--sh-text-primary: #0f172a;
--sh-text-secondary: #475569;
--sh-bg-primary: #ffffff;
--sh-bg-secondary: #f8fafc;
--sh-bg-tertiary: #f1f5f9;
--sh-bg-card: #ffffff;
--sh-border: #e2e8f0;
--sh-hover-bg: #f8fafc;
--sh-hover-shadow: rgba(0, 0, 0, 0.1);
--sh-primary: #6366f1;
--sh-primary-end: #8b5cf6;
--sh-shadow: rgba(0, 0, 0, 0.08);
--sh-success: #22c55e;
--sh-danger: #ef4444;
--sh-warning: #f59e0b;
--sh-info: #3b82f6;
}
[data-theme="dark"] {
/* Dark Mode (Demo-inspired) */
--sh-text-primary: #fafafa;
--sh-text-secondary: #a0a0b0;
--sh-bg-primary: #0a0a0f;
--sh-bg-secondary: #12121a;
--sh-bg-tertiary: #1a1a24;
--sh-bg-card: #12121a;
--sh-border: #2a2a35;
--sh-hover-bg: #1a1a24;
--sh-hover-shadow: rgba(0, 0, 0, 0.6);
--sh-primary: #6366f1;
--sh-primary-end: #8b5cf6;
--sh-shadow: rgba(0, 0, 0, 0.4);
--sh-success: #22c55e;
--sh-danger: #ef4444;
--sh-warning: #f59e0b;
--sh-info: #3b82f6;
}
/* === Global Typography === */
body,
.module-dashboard,
.sh-page-header,
.sh-card {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
code,
.sh-mono,
.sh-id-display,
.sh-metric-value,
pre {
font-family: 'JetBrains Mono', 'Courier New', monospace;
}
/* === Page Header === */
.sh-page-header {
margin-bottom: 24px;
padding: 24px;
background: var(--sh-bg-card);
border-radius: 16px;
border: 1px solid var(--sh-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
box-shadow: 0 1px 3px var(--sh-shadow);
}
.sh-page-title {
font-size: 20px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: flex;
align-items: center;
gap: 12px;
}
.sh-page-title-icon {
font-size: 24px;
line-height: 1;
-webkit-text-fill-color: initial;
}
.sh-page-subtitle {
margin: 4px 0 0 0;
font-size: 14px;
color: var(--sh-text-secondary);
font-weight: 400;
}
/* === Stats Badges (Compact) === */
.sh-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
}
.sh-stat-badge {
background: var(--sh-bg-card);
padding: 16px;
border-radius: 12px;
border: 1px solid var(--sh-border);
text-align: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sh-stat-badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--sh-shadow);
border-color: var(--sh-primary);
}
.sh-stat-value {
font-size: 28px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: var(--sh-text-primary);
margin: 0 0 4px 0;
}
.sh-stat-label {
font-size: 12px;
color: var(--sh-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
/* === Cards === */
.sh-card {
background: var(--sh-bg-card);
border-radius: 16px;
border: 1px solid var(--sh-border);
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px var(--sh-shadow);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.sh-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
opacity: 0;
transition: opacity 0.3s ease;
}
.sh-card:hover::before {
opacity: 1;
}
.sh-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--sh-shadow);
border-color: var(--sh-primary);
}
.sh-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--sh-border);
}
.sh-card-title {
font-size: 18px;
font-weight: 600;
color: var(--sh-text-primary);
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.sh-card-title-icon {
font-size: 20px;
}
.sh-card-body {
color: var(--sh-text-primary);
}
/* === Buttons === */
.sh-btn-primary {
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.sh-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.sh-btn-secondary {
background: var(--sh-bg-tertiary);
color: var(--sh-text-primary);
border: 1px solid var(--sh-border);
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
}
.sh-btn-secondary:hover {
background: var(--sh-hover-bg);
border-color: var(--sh-primary);
}
/* === Navigation Tabs === */
.sh-nav-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
padding: 8px;
background: var(--sh-bg-secondary);
border-radius: 12px;
border: 1px solid var(--sh-border);
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
}
.sh-nav-tab {
padding: 10px 20px;
border-radius: 8px;
background: transparent;
border: none;
color: var(--sh-text-secondary);
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
font-size: 14px;
}
.sh-nav-tab:hover {
color: var(--sh-text-primary);
background: var(--sh-hover-bg);
}
.sh-nav-tab.active {
color: var(--sh-primary);
background: var(--sh-bg-card);
box-shadow: 0 2px 4px var(--sh-shadow);
}
.sh-nav-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 20%;
right: 20%;
height: 2px;
background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end));
border-radius: 2px;
}
/* === Filter Tabs === */
.sh-filter-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.sh-filter-tab {
padding: 8px 16px;
border-radius: 8px;
background: var(--sh-bg-tertiary);
border: 1px solid var(--sh-border);
color: var(--sh-text-secondary);
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 13px;
}
.sh-filter-tab:hover {
border-color: var(--sh-primary);
color: var(--sh-text-primary);
}
.sh-filter-tab.active {
background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end));
color: white;
border-color: transparent;
}
/* === Status Badges === */
.sh-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sh-badge-success {
background: rgba(34, 197, 94, 0.15);
color: var(--sh-success);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.sh-badge-danger {
background: rgba(239, 68, 68, 0.15);
color: var(--sh-danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.sh-badge-warning {
background: rgba(245, 158, 11, 0.15);
color: var(--sh-warning);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.sh-badge-info {
background: rgba(59, 130, 246, 0.15);
color: var(--sh-info);
border: 1px solid rgba(59, 130, 246, 0.3);
}
/* === Grid Layouts === */
.sh-grid {
display: grid;
gap: 20px;
}
.sh-grid-2 {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.sh-grid-3 {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.sh-grid-4 {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
/* === Utilities === */
.sh-mono {
font-family: 'JetBrains Mono', monospace;
}
.sh-text-primary {
color: var(--sh-text-primary);
}
.sh-text-secondary {
color: var(--sh-text-secondary);
}
.sh-text-success {
color: var(--sh-success);
}
.sh-text-danger {
color: var(--sh-danger);
}
.sh-text-warning {
color: var(--sh-warning);
}
/* === Responsive === */
@media (max-width: 768px) {
.sh-page-header {
flex-direction: column;
align-items: flex-start;
}
.sh-page-title {
font-size: 24px;
}
.sh-stats-grid {
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}
.sh-nav-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.sh-grid-2,
.sh-grid-3,
.sh-grid-4 {
grid-template-columns: 1fr;
}
}
/* === MQTT Bridge Specific Styles === */
.mqtt-bridge-dashboard {
--mb-bg: var(--sh-bg-secondary);
--mb-card: var(--sh-bg-card);
--mb-border: var(--sh-border);
--mb-text: var(--sh-text-primary);
--mb-muted: var(--sh-text-secondary);
--mb-accent: var(--sh-primary);
--mb-danger: var(--sh-danger);
background: var(--mb-bg);
min-height: 100vh;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
font-family: 'Inter', sans-serif;
}
.mb-card {
background: var(--mb-card);
border: 1px solid var(--mb-border);
border-radius: 18px;
padding: 24px;
box-shadow: 0 8px 26px rgba(15, 23, 42, 0.08);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.mb-card:hover {
transform: translateY(-2px);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
}
.mb-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--mb-border);
}
.mb-card-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 18px;
color: var(--mb-text);
}
.mb-hero-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-top: 12px;
}
.mb-hero-chip {
display: flex;
flex-direction: column;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--mb-border);
background: var(--sh-bg-tertiary);
gap: 6px;
}
.mb-hero-chip-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--mb-muted);
}
.mb-hero-chip strong {
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
color: var(--mb-text);
}
.mb-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
}
.mb-stat {
padding: 18px;
border-radius: 16px;
border: 1px solid var(--mb-border);
background: var(--mb-card);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.mb-stat-label {
font-size: 12px;
text-transform: uppercase;
color: var(--mb-muted);
letter-spacing: 0.08em;
font-weight: 600;
}
.mb-stat-value {
font-size: 26px;
font-weight: 700;
margin-top: 6px;
font-family: 'JetBrains Mono', monospace;
color: var(--mb-text);
}
.mb-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 11px 18px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}
.mb-btn:focus-visible {
outline: 2px solid var(--mb-accent);
outline-offset: 2px;
}
.mb-btn-primary {
background: linear-gradient(135deg, var(--mb-accent), #8b5cf6);
color: #fff;
box-shadow: 0 14px 30px rgba(99, 102, 241, 0.35);
}
.mb-btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 18px 36px rgba(99, 102, 241, 0.45);
}
.mb-btn-secondary {
background: transparent;
border-color: var(--mb-border);
color: var(--mb-text);
}
.mb-device-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--mb-border);
}
.mb-device-row:last-child {
border-bottom: none;
}
.mb-device-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.mb-device-info span {
color: var(--mb-muted);
font-size: 13px;
}
.mb-input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.mb-input {
border: 1px solid var(--mb-border);
border-radius: 10px;
padding: 10px 12px;
background: var(--mb-card);
color: var(--mb-text);
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.mb-input:focus {
border-color: var(--mb-accent);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
outline: none;
}
.mb-profile-grid {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
}
.mb-profile-card {
border: 1px dashed var(--mb-border);
border-radius: 14px;
padding: 16px;
background: rgba(99, 102, 241, 0.03);
display: flex;
flex-direction: column;
gap: 8px;
}
.mb-profile-header {
display: flex;
justify-content: space-between;
gap: 12px;
}
.mb-profile-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
color: var(--mb-muted);
font-size: 12px;
}
.mb-profile-status {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid var(--mb-border);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--mb-muted);
}
.mb-profile-status.online {
color: #22c55e;
border-color: rgba(34, 197, 94, 0.5);
background: rgba(34, 197, 94, 0.12);
}
.mb-profile-notes {
margin: 0;
color: var(--mb-muted);
font-size: 13px;
}
.mb-profile-hint {
border: 1px solid var(--mb-border);
border-radius: 12px;
padding: 12px;
background: var(--mb-card);
}
.mb-profile-hint pre {
background: rgba(15, 23, 42, 0.08);
border-radius: 10px;
padding: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
overflow-x: auto;
}
@media (max-width: 768px) {
.mqtt-bridge-dashboard {
padding: 16px;
}
.mb-card {
padding: 18px;
}
.mb-card-header {
flex-direction: column;
align-items: flex-start;
}
.mb-hero-meta {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mb-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@ -0,0 +1,39 @@
'use strict';
'require baseclass';
'require secubox-theme/cascade as Cascade';
var tabs = [
{ id: 'overview', icon: '📡', label: _('Overview'), path: ['admin', 'secubox', 'network', 'mqtt-bridge', 'overview'] },
{ id: 'devices', icon: '🔌', label: _('Devices'), path: ['admin', 'secubox', 'network', 'mqtt-bridge', 'devices'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'network', 'mqtt-bridge', 'settings'] }
];
return baseclass.extend({
renderTabs: function(active) {
return Cascade.createLayer({
id: 'mqtt-nav',
type: 'tabs',
role: 'menu',
depth: 1,
className: 'sh-nav-tabs mqtt-nav-tabs',
items: tabs.map(function(tab) {
return {
id: tab.id,
label: tab.label,
icon: tab.icon,
href: L.url.apply(L, tab.path),
state: tab.id === active ? 'active' : null
};
}),
active: active,
onSelect: function(item, ev) {
if (item.href && ev && (ev.metaKey || ev.ctrlKey))
return true;
if (item.href) {
location.href = item.href;
return false;
}
}
});
}
});

View File

@ -0,0 +1,143 @@
'use strict';
'require view';
'require ui';
'require mqtt-bridge/api as API';
'require secubox-theme/theme as Theme';
'require mqtt-bridge/nav as Nav';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return view.extend({
load: function() {
return Promise.all([
API.listDevices(),
API.getStatus()
]);
},
render: function(payload) {
var devices = (payload[0] && payload[0].devices) || [];
var status = payload[1] || {};
return E('div', { 'class': 'mqtt-bridge-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
Nav.renderTabs('devices'),
this.renderStats(status),
this.renderProfiles(status.profiles || []),
E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🔌'), _('USB & Sensors')]),
E('button', {
'class': 'mb-btn mb-btn-primary',
'click': ui.createHandlerFn(this, 'startPairing')
}, [' ', _('Pair new device')])
]),
devices.length ? devices.map(this.renderDeviceRow.bind(this)) :
E('p', { 'style': 'color:var(--mb-muted);' }, _('No devices detected yet. Plug a USB bridge or trigger pairing.'))
])
]);
},
renderProfiles: function(profiles) {
var items = profiles || [];
var cards = items.length ? items.map(this.renderProfile.bind(this)) :
[E('p', { 'style': 'color:var(--mb-muted);' },
_('No presets detected yet. Connect a Zigbee adapter or review the documentation below.'))];
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🛰️'), _('Zigbee & serial presets')])
]),
E('div', { 'class': 'mb-profile-grid' }, cards),
E('div', { 'class': 'mb-profile-hint' }, [
E('strong', {}, _('dmesg/logcat hints:')),
E('pre', {}, '[ 6456.735692] usb 3-1.1: USB disconnect, device number 3\n' +
'[ 6459.021458] usb 3-1.1: new full-speed USB device number 4 using xhci-hcd'),
E('p', {}, _('Match the Bus/Device numbers to /dev/tty* and update the MQTT bridge config if needed.'))
])
]);
},
renderProfile: function(profile) {
var detected = profile.detected;
var meta = [
(profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null,
profile.bus ? _('Bus ') + profile.bus : null,
profile.device ? _('Device ') + profile.device : null,
profile.port ? _('Port ') + profile.port : null
].filter(Boolean);
return E('div', { 'class': 'mb-profile-card' }, [
E('div', { 'class': 'mb-profile-header' }, [
E('div', {}, [
E('strong', {}, profile.label || _('USB profile')),
E('div', { 'class': 'mb-profile-meta' }, meta.map(function(entry) {
return E('span', {}, entry);
}))
]),
E('span', {
'class': 'mb-profile-status' + (detected ? ' online' : '')
}, detected ? _('Detected') : _('Waiting'))
]),
profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null
]);
},
renderStats: function(status) {
var stats = status.device_stats || {};
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '📊'), _('Device stats')])
]),
E('div', { 'class': 'mb-grid' }, [
this.stat(_('Paired devices'), stats.total || 0),
this.stat(_('Online'), stats.online || 0),
this.stat(_('USB adapters'), stats.usb || 0)
])
]);
},
stat: function(label, value) {
return E('div', { 'class': 'mb-stat' }, [
E('span', { 'class': 'mb-stat-label' }, label),
E('div', { 'class': 'mb-stat-value' }, value)
]);
},
renderDeviceRow: function(device) {
return E('div', { 'class': 'mb-device-row' }, [
E('div', { 'class': 'mb-device-info' }, [
E('strong', {}, device.name || device.serial || _('Unknown device')),
E('span', { 'style': 'color:var(--mb-muted);font-size:12px;' },
(device.protocol || 'USB') + ' • ' + (device.port || 'N/A'))
]),
E('div', { 'style': 'display:flex;gap:8px;' }, [
E('span', {
'style': 'font-size:12px;text-transform:uppercase;color:' +
(device.online ? '#22c55e' : '#f97316')
}, device.online ? _('Online') : _('Paired / offline'))
])
]);
},
startPairing: function() {
ui.showModal(_('Pairing'), [
E('p', {}, _('Listening for device join requests…')),
E('div', { 'class': 'spinning' })
]);
return API.triggerPairing().then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Pairing window opened for 2 minutes.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Unable to start pairing')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
}
});

View File

@ -0,0 +1,116 @@
'use strict';
'require view';
'require ui';
'require mqtt-bridge/api as API';
'require secubox-theme/theme as Theme';
'require mqtt-bridge/nav as Nav';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return view.extend({
load: function() {
return API.getStatus();
},
render: function(data) {
var status = data || {};
var container = E('div', { 'class': 'mqtt-bridge-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
Nav.renderTabs('overview'),
this.renderHero(status),
this.renderStats(status),
this.renderRecentPayloads(status)
]);
return container;
},
renderHero: function(status) {
var meta = [
{ label: _('Broker'), value: status.broker || 'Mosquitto' },
{ label: _('Clients'), value: status.clients || 0 },
{ label: _('Last USB Event'), value: status.last_event || _('n/a') }
];
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '📡'), _('MQTT Bridge')]),
E('div', { 'style': 'display:flex;gap:8px;' }, [
E('button', { 'class': 'mb-btn mb-btn-primary', 'click': ui.createHandlerFn(this, 'startPairing') }, ['🔌 ', _('Pair device')])
])
]),
E('p', { 'style': 'color:var(--mb-muted);margin-bottom:12px;' },
_('USB-to-MQTT hub for bringing Zigbee, serial and modbus devices into SecuBox.')),
E('div', { 'class': 'mb-hero-meta' }, meta.map(function(item) {
return E('div', { 'class': 'mb-hero-chip' }, [
E('span', { 'class': 'mb-hero-chip-label' }, item.label),
E('strong', {}, item.value)
]);
}))
]);
},
renderStats: function(status) {
var stats = [
{ label: _('USB adapters'), value: status.adapters || 0 },
{ label: _('Paired devices'), value: (status.device_stats && status.device_stats.total) || 0 },
{ label: _('Online devices'), value: (status.device_stats && status.device_stats.online) || 0 },
{ label: _('Topics/s'), value: status.messages_per_sec || 0 },
{ label: _('Stored payloads'), value: status.retained || 0 },
{ label: _('Bridge uptime'), value: status.uptime || _('') }
];
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '📊'), _('Metrics')])
]),
E('div', { 'class': 'mb-grid' }, stats.map(function(stat) {
return E('div', { 'class': 'mb-stat' }, [
E('span', { 'class': 'mb-stat-label' }, stat.label),
E('div', { 'class': 'mb-stat-value' }, stat.value)
]);
}))
]);
},
renderRecentPayloads: function(status) {
var payloads = status.recent_payloads || [];
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '📝'), _('Recent payloads')])
]),
payloads.length ? E('div', {}, payloads.map(function(entry) {
return E('div', { 'class': 'mb-device-row' }, [
E('div', { 'class': 'mb-device-info' }, [
E('strong', {}, entry.topic),
E('span', { 'style': 'color:var(--mb-muted);font-size:13px;' }, entry.timestamp)
]),
E('code', { 'style': 'font-family:\"JetBrains Mono\",monospace;font-size:12px;' }, entry.payload)
]);
})) : E('p', { 'style': 'color:var(--mb-muted);' }, _('No payloads yet'))
]);
},
startPairing: function() {
ui.showModal(_('Pairing'), [
E('p', {}, _('Listening for device join requests…')),
E('div', { 'class': 'spinning' })
]);
return API.triggerPairing().then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Pairing window opened for 2 minutes.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Unable to start pairing')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,89 @@
'use strict';
'require view';
'require mqtt-bridge/api as API';
'require secubox-theme/theme as Theme';
'require mqtt-bridge/nav as Nav';
'require ui';
'require form';
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: lang });
return view.extend({
load: function() {
return API.getStatus().then(function(status) {
return status.settings || {};
});
},
render: function(settings) {
var container = E('div', { 'class': 'mqtt-bridge-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
Nav.renderTabs('settings'),
this.renderSettingsCard(settings || {})
]);
return container;
},
renderSettingsCard: function(settings) {
return E('div', { 'class': 'mb-card' }, [
E('div', { 'class': 'mb-card-header' }, [
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '⚙️'), _('MQTT Settings')])
]),
E('div', { 'class': 'mb-grid' }, [
this.input('broker-host', _('Broker host'), settings.host || '127.0.0.1'),
this.input('broker-port', _('Port'), settings.port || 1883),
this.input('username', _('Username'), settings.username || '', 'text'),
this.input('password', _('Password'), settings.password || '', 'password'),
this.input('base-topic', _('Base topic'), settings.base_topic || 'secubox/+/state'),
this.input('retention', _('Retention (days)'), settings.retention || 7, 'number')
]),
E('div', { 'style': 'margin-top:16px;' }, [
E('button', { 'class': 'mb-btn mb-btn-primary', 'click': ui.createHandlerFn(this, 'saveSettings') }, ['💾 ', _('Save settings')])
])
]);
},
input: function(id, label, value, type) {
return E('div', { 'class': 'mb-input-group' }, [
E('label', { 'class': 'mb-stat-label', 'for': id }, label),
E('input', {
'class': 'mb-input',
'id': id,
'type': type || 'text',
'value': value
})
]);
},
saveSettings: function() {
var payload = {
host: document.getElementById('broker-host').value,
port: parseInt(document.getElementById('broker-port').value, 10) || 1883,
username: document.getElementById('username').value,
password: document.getElementById('password').value,
base_topic: document.getElementById('base-topic').value,
retention: parseInt(document.getElementById('retention').value, 10) || 7
};
ui.showModal(_('Saving MQTT settings'), [
E('p', {}, _('Applying broker configuration…')),
E('div', { 'class': 'spinning' })
]);
return API.applySettings(payload).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Settings saved. Restarting bridge if required.')), 'info');
} else {
ui.addNotification(null, E('p', {}, result.error || _('Save failed')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
}
});

View File

@ -0,0 +1,15 @@
local i18n = require "luci.i18n"
local _ = i18n.translate
module("luci.controller.secubox.mqtt-bridge", package.seeall)
function index()
if not nixio.fs.access("/etc/config/mqtt-bridge") then
return
end
local root = {"admin", "secubox", "network", "mqtt-bridge"}
entry(root, firstchild(), _("MQTT Bridge"), 10).dependent = true
entry(root + {"overview"}, view("mqtt-bridge/overview"), _("Overview"), 10).leaf = true
entry(root + {"devices"}, view("mqtt-bridge/devices"), _("Devices"), 20).leaf = true
entry(root + {"settings"}, view("mqtt-bridge/settings"), _("Settings"), 90).leaf = true
end

View File

@ -0,0 +1,18 @@
config broker 'broker'
option host '127.0.0.1'
option port '1883'
option username 'secubox'
option password 'secubox'
config bridge 'bridge'
option base_topic 'secubox/+/state'
option retention '7'
config stats 'stats'
option clients '0'
option mps '0'
option retained '0'
option uptime '0s'
config payloads 'payloads'
# push payload sections dynamically

View File

@ -0,0 +1,209 @@
#!/bin/sh
# SecuBox MQTT Bridge RPC backend
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
ZIGBEE_VENDOR="0424"
ZIGBEE_PRODUCT="2134"
ZIGBEE_PROFILE_ID="zigbee_usb2134"
find_usb_tty() {
local base="$1"
local sub node tty
for sub in "$base" "$base"/* "$base"/*/*; do
[ -d "$sub/tty" ] || continue
for node in "$sub"/tty/*; do
[ -e "$node" ] || continue
tty="$(basename "$node")"
if [ -e "/dev/$tty" ]; then
echo "/dev/$tty"
return 0
fi
done
done
return 1
}
append_zigbee_profile() {
local matched=0
local dev
for dev in /sys/bus/usb/devices/*; do
[ -f "$dev/idVendor" ] || continue
[ -f "$dev/idProduct" ] || continue
local vendor product
vendor="$(cat "$dev/idVendor" 2>/dev/null)"
product="$(cat "$dev/idProduct" 2>/dev/null)"
[ "$vendor" = "$ZIGBEE_VENDOR" ] || continue
[ "$product" = "$ZIGBEE_PRODUCT" ] || continue
matched=1
local busnum devnum label port
busnum="$(cat "$dev/busnum" 2>/dev/null)"
devnum="$(cat "$dev/devnum" 2>/dev/null)"
if [ -f "$dev/product" ]; then
label="$(cat "$dev/product")"
else
label="SMSC USB2134B"
fi
port="$(find_usb_tty "$dev")"
json_add_object
json_add_string "id" "$ZIGBEE_PROFILE_ID"
json_add_string "label" "$label Zigbee Bridge"
json_add_string "vendor" "$vendor"
json_add_string "product" "$product"
[ -n "$busnum" ] && json_add_string "bus" "$busnum"
[ -n "$devnum" ] && json_add_string "device" "$devnum"
[ -n "$port" ] && json_add_string "port" "$port"
json_add_boolean "detected" 1
json_add_string "notes" "Detected via USB bus $busnum device $devnum"
json_close_object
done
if [ "$matched" -eq 0 ]; then
json_add_object
json_add_string "id" "$ZIGBEE_PROFILE_ID"
json_add_string "label" "SMSC USB2134B Zigbee Bridge"
json_add_string "vendor" "$ZIGBEE_VENDOR"
json_add_string "product" "$ZIGBEE_PRODUCT"
json_add_boolean "detected" 0
json_add_string "notes" "Connect the dongle (Bus 003 Device 002: ID 0424:2134) to auto-fill tty port"
json_close_object
fi
}
status() {
json_init
json_add_string "broker" "$(uci -q get mqtt-bridge.broker.host || echo 'localhost')"
json_add_int "clients" "$(uci -q get mqtt-bridge.stats.clients || echo 0)"
local adapters="$(ls /dev/ttyUSB* 2>/dev/null | wc -l)"
json_add_int "adapters" "$adapters"
json_add_int "messages_per_sec" "$(uci -q get mqtt-bridge.stats.mps || echo 0)"
json_add_int "retained" "$(uci -q get mqtt-bridge.stats.retained || echo 0)"
json_add_string "uptime" "$(uci -q get mqtt-bridge.stats.uptime || echo '0s')"
json_add_string "last_event" "$(uci -q get mqtt-bridge.stats.last_event || echo '')"
# Device statistics
local total=0 online=0
while true; do
local ser=$(uci -q get mqtt-bridge.device.@device[$total].serial)
[ -z "$ser" ] && break
local is_on=$(uci -q get mqtt-bridge.device.@device[$total].online || echo 0)
[ "$is_on" = "1" ] && online=$((online + 1))
total=$((total + 1))
done
json_add_object "device_stats"
json_add_int "total" "$total"
json_add_int "online" "$online"
json_add_int "usb" "$adapters"
json_close_object
json_add_array "recent_payloads"
local idx=0
while true; do
local topic=$(uci -q get mqtt-bridge.payloads.@payload[$idx].topic)
[ -z "$topic" ] && break
json_add_object
json_add_string "topic" "$topic"
json_add_string "payload" "$(uci -q get mqtt-bridge.payloads.@payload[$idx].data)"
json_add_string "timestamp" "$(uci -q get mqtt-bridge.payloads.@payload[$idx].time)"
json_close_object
idx=$((idx + 1))
done
json_close_array
json_add_object "settings"
json_add_string "host" "$(uci -q get mqtt-bridge.broker.host || echo '127.0.0.1')"
json_add_int "port" "$(uci -q get mqtt-bridge.broker.port || echo 1883)"
json_add_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')"
json_add_string "password" ""
json_add_string "base_topic" "$(uci -q get mqtt-bridge.bridge.base_topic || echo 'secubox/+/state')"
json_add_int "retention" "$(uci -q get mqtt-bridge.bridge.retention || echo 7)"
json_close_object
json_add_array "profiles"
append_zigbee_profile
json_close_array
json_dump
}
list_devices() {
json_init
json_add_array "devices"
local idx=0
while true; do
local serial=$(uci -q get mqtt-bridge.device.@device[$idx].serial)
[ -z "$serial" ] && break
json_add_object
json_add_string "serial" "$serial"
json_add_string "name" "$(uci -q get mqtt-bridge.device.@device[$idx].name)"
json_add_string "protocol" "$(uci -q get mqtt-bridge.device.@device[$idx].protocol || echo 'USB')"
json_add_string "port" "$(uci -q get mqtt-bridge.device.@device[$idx].port || echo '-')"
json_add_boolean "online" "$(uci -q get mqtt-bridge.device.@device[$idx].online || echo 0)"
json_close_object
idx=$((idx + 1))
done
json_close_array
json_dump
}
trigger_pairing() {
json_init
touch /tmp/mqtt-bridge-pairing
json_add_boolean "success" 1
json_add_string "message" "pairing_window_open"
json_dump
}
apply_settings() {
read input
json_load "$input"
json_get_var host host
json_get_var port port
json_get_var username username
json_get_var password password
json_get_var base_topic base_topic
json_get_var retention retention
json_cleanup
[ -n "$host" ] && uci set mqtt-bridge.broker.host="$host"
[ -n "$port" ] && uci set mqtt-bridge.broker.port="$port"
[ -n "$username" ] && uci set mqtt-bridge.broker.username="$username"
[ -n "$password" ] && uci set mqtt-bridge.broker.password="$password"
[ -n "$base_topic" ] && uci set mqtt-bridge.bridge.base_topic="$base_topic"
[ -n "$retention" ] && uci set mqtt-bridge.bridge.retention="$retention"
uci commit mqtt-bridge
json_init
json_add_boolean "success" 1
json_add_string "message" "settings_saved"
json_dump
}
case "$1" in
list)
cat <<'JSON'
{
"status": {},
"list_devices": {},
"trigger_pairing": {},
"apply_settings": {}
}
JSON
;;
call)
case "$2" in
status) status ;;
list_devices) list_devices ;;
trigger_pairing) trigger_pairing ;;
apply_settings) apply_settings ;;
*)
json_init
json_add_boolean "success" 0
json_add_string "error" "unknown_method"
json_dump
;;
esac
;;
esac

View File

@ -0,0 +1,38 @@
{
"admin/secubox/network/mqtt-bridge": {
"title": "MQTT Bridge",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [
"luci-app-mqtt-bridge"
]
}
},
"admin/secubox/network/mqtt-bridge/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "mqtt-bridge/overview"
}
},
"admin/secubox/network/mqtt-bridge/devices": {
"title": "Devices",
"order": 20,
"action": {
"type": "view",
"path": "mqtt-bridge/devices"
}
},
"admin/secubox/network/mqtt-bridge/settings": {
"title": "Settings",
"order": 90,
"action": {
"type": "view",
"path": "mqtt-bridge/settings"
}
}
}

View File

@ -0,0 +1,23 @@
{
"luci-app-mqtt-bridge": {
"description": "SecuBox MQTT Bridge permissions",
"read": {
"ubus": {
"luci.mqtt-bridge": [
"status",
"list_devices"
]
},
"uci": ["mqtt-bridge"]
},
"write": {
"ubus": {
"luci.mqtt-bridge": [
"trigger_pairing",
"apply_settings"
]
},
"uci": ["mqtt-bridge"]
}
}
}

View File

@ -683,6 +683,28 @@
width: 100%;
}
.nm-hero-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.nm-hero-chip {
background: rgba(255,255,255,0.1);
border-radius: 12px;
padding: 6px 12px;
border: 1px solid rgba(255,255,255,0.2);
}
.nm-hero-chip-label {
display: block;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255,255,255,0.75);
}
.nm-hero-badge {
min-width: 120px;
}

View File

@ -22,12 +22,8 @@ function isToggleActive(node) {
}
function persistSettings(mode, payload) {
ui.showModal(_('Saving settings...'), [
E('p', { 'class': 'spinning' }, _('Applying configuration changes...'))
]);
var current = (window.L && L.state && L.state.network_modes_current) || null;
return api.updateSettings(mode, payload).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, result.message || _('Settings updated')), 'info');
} else {
@ -69,16 +65,23 @@ function showGeneratedConfig(mode) {
function createHero(options) {
var gradient = options.gradient || 'linear-gradient(135deg,#0f172a,#1d4ed8)';
var meta = options.meta || [];
return E('div', {
'class': 'nm-hero',
'style': 'background:' + gradient + ';border-radius:16px;padding:20px;margin-bottom:24px;color:#f8fafc;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;'
'style': 'background:' + gradient + ';border-radius:16px;padding:18px 22px;margin-bottom:20px;color:#f8fafc;display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:16px;'
}, [
E('div', {}, [
E('div', { 'style': 'font-size:32px;margin-bottom:4px;' }, options.icon || '🌐'),
E('h2', { 'style': 'margin:0;font-size:24px;' }, options.title || _('Network Mode')),
E('p', { 'style': 'margin:4px 0 0;color:#cbd5f5;max-width:460px;' }, options.subtitle || '')
E('div', { 'style': 'font-size:26px;margin-bottom:4px;' }, options.icon || '🌐'),
E('h2', { 'style': 'margin:0;font-size:22px;' }, options.title || _('Network Mode')),
E('p', { 'style': 'margin:4px 0 0;color:#cbd5f5;max-width:520px;font-size:13px;' }, options.subtitle || ''),
meta.length ? E('div', { 'class': 'nm-hero-meta' }, meta.map(function(item) {
return E('div', { 'class': 'nm-hero-chip' }, [
E('span', { 'class': 'nm-hero-chip-label' }, item.label || ''),
E('strong', {}, item.value || '—')
]);
})) : null
]),
E('div', { 'style': 'display:flex;gap:12px;align-items:center;flex-wrap:wrap;' }, (options.actions || []))
E('div', { 'style': 'display:flex;gap:10px;align-items:center;flex-wrap:wrap;' }, (options.actions || []))
]);
}

View File

@ -31,11 +31,15 @@ return view.extend({
title: _('Router Mode'),
load: function() {
return api.getRouterConfig();
return Promise.all([
api.getRouterConfig(),
api.getStatus()
]);
},
render: function(data) {
var config = data || {};
render: function(payload) {
var config = (payload && payload[0]) || {};
var status = (payload && payload[1]) || {};
var wanConfig = config.wan || {};
var lanConfig = config.lan || {};
var fwConfig = config.firewall || {};
@ -54,6 +58,11 @@ return view.extend({
title: _('Router Mode'),
subtitle: _('Full router stack with NAT, firewall, transparent proxying, HTTPS reverse proxy, and virtual hosts.'),
gradient: 'linear-gradient(135deg,#f97316,#fb923c)',
meta: [
{ label: _('WAN Protocol'), value: (wanConfig.protocol || 'DHCP').toUpperCase() },
{ label: _('WAN IP'), value: this.lookupInterfaceIp(status, wanConfig.interface || 'wan') },
{ label: _('LAN IP'), value: lanConfig.ip_address || this.lookupInterfaceIp(status, lanConfig.interface || 'lan') }
],
actions: heroActions
});
@ -61,9 +70,9 @@ return view.extend({
'style': 'display:flex;flex-wrap:wrap;gap:12px;margin-bottom:24px;'
}, [
helpers.createStatBadge({ label: _('WAN Protocol'), value: (wanConfig.protocol || 'DHCP').toUpperCase() }),
helpers.createStatBadge({ label: _('LAN Gateway'), value: lanConfig.ip_address || '192.168.1.1' }),
helpers.createStatBadge({ label: _('LAN Gateway'), value: lanConfig.ip_address || this.lookupInterfaceIp(status, 'lan') || '—' }),
helpers.createStatBadge({ label: _('Firewall'), value: fwConfig.enabled !== false ? _('Active') : _('Disabled') }),
helpers.createStatBadge({ label: _('Proxy'), value: proxyConfig.enabled ? (proxyConfig.type || 'squid') : _('Disabled') })
helpers.createStatBadge({ label: _('Proxy'), value: proxyConfig.enabled ? (proxyConfig.type || _('Enabled')) : _('Disabled') })
]);
var wanSection = helpers.createSection({
@ -331,5 +340,13 @@ return view.extend({
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
lookupInterfaceIp: function(status, match) {
var ifaces = (status && status.interfaces) || [];
var target = match;
return (ifaces.find(function(item) {
return item.name === target || item.name === (target === 'lan' ? 'br-lan' : target);
}) || {}).ip || '';
}
});

View File

@ -8,11 +8,17 @@
return view.extend({
load: function() {
return Promise.all([
uci.load('network-modes')
uci.load('network-modes'),
api.getCurrentMode()
]);
},
render: function() {
render: function(data) {
var currentMode = (data && data[1] && data[1].mode) || 'router';
if (typeof L !== 'undefined') {
L.state = L.state || {};
L.state.network_modes_current = currentMode;
}
var m, s, o;
m = new form.Map('network-modes', _('Network Modes Settings'),

View File

@ -5,6 +5,7 @@
'require dom';
'require poll';
'require network-modes.helpers as helpers';
'require network-modes/api as API';
'require secubox-theme/theme as Theme';
var callGetAvailableModes = rpc.declare({
@ -61,13 +62,15 @@ return view.extend({
load: function() {
return Promise.all([
callGetAvailableModes(),
callGetCurrentMode()
callGetCurrentMode(),
API.getStatus()
]);
},
render: function(data) {
var modes = data[0].modes || [];
var currentModeData = data[1] || {};
var status = data[2] || {};
var hero = helpers.createHero({
icon: '🌐',
@ -98,6 +101,7 @@ return view.extend({
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }),
helpers.createNavigationTabs('wizard'),
this.renderStatusBadges(status, currentModeData),
hero,
stepper,
currentModeData.rollback_active ? this.renderRollbackBanner(currentModeData) : this.renderCurrentMode(currentModeData),
@ -112,6 +116,65 @@ return view.extend({
return container;
},
renderStatusBadges: function(status, currentMode) {
var chips = [
{ label: _('Version'), value: status.version || _('unknown') },
{ label: _('Mode'), value: currentMode.mode_name || currentMode.current_mode || _('') },
{ label: _('WAN IP'), value: this.lookupInterfaceIp(status, 'wan') || _('Unknown') },
{ label: _('LAN IP'), value: this.lookupInterfaceIp(status, 'lan') || _('Unknown') }
];
return E('div', { 'class': 'nm-hero-meta', 'style': 'margin-bottom:12px;' }, chips.map(function(chip) {
return E('div', { 'class': 'nm-hero-chip' }, [
E('span', { 'class': 'nm-hero-chip-label' }, chip.label),
E('strong', {}, chip.value)
]);
}));
},
renderPendingModeCard: function(data, modes) {
if (!data.pending_mode)
return null;
var info = modes.find(function(mode) { return mode.id === data.pending_mode; }) || {
id: data.pending_mode,
name: data.pending_mode_name || data.pending_mode,
description: ''
};
return helpers.createSection({
title: _('Planned Mode'),
icon: '🗂️',
badge: info.name,
body: [
E('p', { 'style': 'color:#94a3b8;' }, info.description || _('Ready to apply when you need to test this template.')),
E('div', { 'class': 'nm-btn-group' }, [
E('button', {
'class': 'nm-btn',
'click': ui.createHandlerFn(this, 'handlePreviewPending', info)
}, '📝 ' + _('Review Changes')),
E('button', {
'class': 'nm-btn nm-btn-primary',
'click': ui.createHandlerFn(this, 'handleApplyPending', info)
}, '✅ ' + _('Apply Planned Mode'))
])
]
});
},
renderBackupCard: function(data) {
var lastBackup = data.last_backup_time || _('Not yet created');
if (!data.last_backup_time && !data.pending_mode)
return null;
return helpers.createSection({
title: _('Safety & Backups'),
icon: '💾',
body: [
E('p', {}, _('Latest snapshot: ') + (data.last_backup_time || _('Not available'))),
data.pending_mode ? E('p', { 'style': 'color:#94a3b8;' },
_('Applying the planned mode will create a fresh backup before changes.')) : null,
E('div', { 'style': 'margin-top:10px;font-family:monospace;font-size:12px;' }, data.last_backup || '')
]
});
},
renderCurrentMode: function(data) {
return helpers.createSection({
title: _('Current Mode'),
@ -258,7 +321,10 @@ return view.extend({
}
content.push(E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [
E('button', { 'class': 'nm-btn', 'click': ui.hideModal }, _('Cancel')),
E('button', {
'class': 'nm-btn',
'click': function() { ui.hideModal(); window.location.reload(); }
}, _('Cancel')),
' ',
E('button', { 'class': 'nm-btn nm-btn-primary', 'click': L.bind(this.handleApplyMode, this, mode) }, _('Apply Mode'))
]));
@ -346,10 +412,34 @@ return view.extend({
timerElem.textContent = _('Time left: ') + minutes + 'm ' + seconds + 's';
}
}, this));
}, this), 1);
}, this), 1);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
handleReset: null,
handlePreviewPending: function(modeInfo) {
ui.showModal(_('Previewing pending mode…'), [
E('p', { 'class': 'spinning' }, _('Loading diff preview'))
]);
return callPreviewChanges().then(L.bind(function(preview) {
this.showPreviewModal(modeInfo, preview);
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
handleApplyPending: function(modeInfo) {
return this.handleApplyMode(modeInfo);
},
lookupInterfaceIp: function(status, match) {
var ifaces = (status && status.interfaces) || [];
var iface = ifaces.find(function(item) {
return item.name === match || item.name === (match === 'lan' ? 'br-lan' : match);
});
return iface && iface.ip ? iface.ip : '';
}
});

View File

@ -21,6 +21,14 @@ CONFIG_FILE="/etc/config/network-modes"
BACKUP_DIR="/etc/network-modes-backup"
PCAP_DIR="/var/log/pcap"
is_module_enabled() {
local enabled
enabled=$(uci -q get secubox.network_modes.enabled)
[ -z "$enabled" ] && return 0
[ "$enabled" = "1" ] && return 0
return 1
}
# Get current status
get_status() {
json_init
@ -516,6 +524,14 @@ EOF
# Apply mode change (with actual network reconfiguration and rollback timer)
apply_mode() {
if ! is_module_enabled; then
json_init
json_add_boolean "success" 0
json_add_string "error" "module_disabled"
json_add_string "message" "Network Modes module is disabled in SecuBox. Enable it before applying changes."
json_dump
return
fi
json_init
local pending_mode=$(uci -q get network-modes.config.pending_mode || echo "")
@ -531,8 +547,11 @@ apply_mode() {
# Backup current config
mkdir -p "$BACKUP_DIR"
local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
local timestamp="$(date +%Y%m%d_%H%M%S)"
local backup_file="$BACKUP_DIR/backup_${timestamp}.tar.gz"
tar -czf "$backup_file" /etc/config/network /etc/config/wireless /etc/config/firewall /etc/config/dhcp 2>/dev/null
uci set network-modes.config.last_backup="$backup_file"
uci set network-modes.config.last_backup_time="$(date '+%Y-%m-%d %H:%M:%S')"
# Apply network configuration based on mode
case "$pending_mode" in
@ -812,7 +831,7 @@ update_settings() {
read input
json_load "$input"
json_get_var mode mode
json_init
case "$mode" in
@ -1007,14 +1026,28 @@ update_settings() {
esac
uci commit network-modes
[ "$mode" = "accesspoint" ] && apply_accesspoint_features
if [ "$mode" = "sniffer" ]; then
local current_mode=$(uci -q get network-modes.config.current_mode || echo "")
[ "$current_mode" = "sniffer" ] && start_packet_capture
local current_mode=$(uci -q get network-modes.config.current_mode || echo "")
local module_active=1
if ! is_module_enabled; then
module_active=0
fi
if [ "$module_active" = "1" ] && [ "$mode" = "accesspoint" ] && [ "$current_mode" = "accesspoint" ]; then
apply_accesspoint_features
fi
if [ "$module_active" = "1" ] && [ "$mode" = "sniffer" ] && [ "$current_mode" = "sniffer" ]; then
start_packet_capture
elif [ "$mode" = "sniffer" ]; then
stop_packet_capture
fi
json_add_boolean "success" 1
json_add_string "message" "Settings updated for $mode mode"
if [ "$module_active" != "1" ]; then
json_add_string "message" "Module disabled in SecuBox. Preferences saved for $mode template."
elif [ "$mode" = "$current_mode" ]; then
json_add_string "message" "Settings updated for $mode mode"
else
json_add_string "message" "Settings saved for $mode template (current mode: $current_mode)"
fi
json_dump
}
@ -1934,7 +1967,12 @@ get_current_mode() {
json_add_string "description" "$(uci -q get network-modes.$current_mode.description || echo "")"
json_add_string "last_change" "$last_change"
json_add_string "pending_mode" "$pending_mode"
if [ -n "$pending_mode" ]; then
json_add_string "pending_mode_name" "$(uci -q get network-modes.$pending_mode.name || echo "$pending_mode")"
fi
json_add_int "rollback_timer" "$rollback_timer"
json_add_string "last_backup" "$(uci -q get network-modes.config.last_backup || echo '')"
json_add_string "last_backup_time" "$(uci -q get network-modes.config.last_backup_time || echo '')"
# Check if rollback is active
if [ -f "/tmp/network-mode-rollback.pid" ]; then

View File

@ -1,5 +1,6 @@
'use strict';
'require baseclass';
'require secubox-theme/cascade as Cascade';
var tabs = [
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
@ -16,16 +17,30 @@ return baseclass.extend({
},
renderTabs: function(active) {
return E('div', { 'class': 'sh-nav-tabs secubox-nav-tabs' },
this.getTabs().map(function(tab) {
return E('a', {
'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''),
'href': L.url.apply(L, tab.path)
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
})
);
return Cascade.createLayer({
id: 'secubox-main-nav',
type: 'tabs',
role: 'menu',
depth: 1,
className: 'sh-nav-tabs secubox-nav-tabs',
items: this.getTabs().map(function(tab) {
return {
id: tab.id,
label: tab.label,
icon: tab.icon,
href: L.url.apply(L, tab.path),
state: tab.id === active ? 'active' : null
};
}),
active: active,
onSelect: function(item, ev) {
if (item.href && ev && (ev.metaKey || ev.ctrlKey))
return true;
if (item.href) {
location.href = item.href;
return false;
}
}
});
}
});

View File

@ -5,6 +5,7 @@
'require secubox/api as API';
'require secubox/theme as Theme';
'require secubox/nav as SecuNav';
'require secubox-theme/cascade as Cascade';
'require poll';
// Load global theme CSS
@ -27,6 +28,8 @@ Theme.init({ language: secuLang });
return view.extend({
modulesData: [],
currentFilter: 'all',
filterLayer: null,
load: function() {
return this.refreshData();
@ -44,7 +47,11 @@ return view.extend({
var self = this;
var modules = this.modulesData;
var container = E('div', { 'class': 'secubox-modules-page' }, [
var defaultFilter = this.currentFilter || 'all';
var container = E('div', {
'class': 'secubox-modules-page',
'data-cascade-root': 'modules'
}, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }),
SecuNav.renderTabs('modules'),
@ -52,8 +59,12 @@ return view.extend({
this.renderFilterTabs(),
E('div', {
'id': 'modules-grid',
'class': 'secubox-modules-grid'
}, this.renderModuleCards(modules, 'all'))
'class': 'secubox-modules-grid sb-cascade-layer',
'data-cascade-layer': 'view',
'data-cascade-role': 'modules',
'data-cascade-depth': '3',
'data-cascade-filter': defaultFilter
}, this.renderModuleCards(modules, defaultFilter))
]);
// Auto-refresh
@ -98,28 +109,31 @@ return view.extend({
{ id: 'system', label: _('System'), icon: '⚙️' }
];
var filterButtons = tabs.map(function(tab) {
return E('button', {
'class': 'sh-nav-tab secubox-module-tab' + (tab.id === 'all' ? ' active' : ''),
'data-filter': tab.id,
'type': 'button',
'click': function(ev) {
document.querySelectorAll('.secubox-filter-tabs .sh-nav-tab[data-filter]').forEach(function(el) {
el.classList.remove('active');
});
ev.currentTarget.classList.add('active');
self.filterModules(tab.id);
}
}, [
E('span', { 'class': 'sh-tab-icon' }, tab.icon),
E('span', { 'class': 'sh-tab-label' }, tab.label)
]);
this.filterLayer = Cascade.createLayer({
id: 'secubox-module-filters',
type: 'tabs',
role: 'categories',
depth: 2,
className: 'secubox-filter-tabs sh-nav-tabs secubox-nav-tabs secubox-module-tabs',
items: tabs.map(function(tab) {
return {
id: tab.id,
label: tab.label,
icon: tab.icon,
state: tab.id === self.currentFilter ? 'active' : null
};
}),
active: this.currentFilter,
onSelect: function(item, ev) {
ev.preventDefault();
self.filterModules(item.id);
}
});
return E('div', { 'class': 'secubox-filter-tabs sh-nav-tabs secubox-nav-tabs secubox-module-tabs' }, filterButtons);
return this.filterLayer;
},
renderModuleCards: function(modules, filter) {
renderModuleCards: function(modules, filter) {
var self = this;
var filtered = filter === 'all' ? modules :
@ -181,6 +195,13 @@ renderModuleCards: function(modules, filter) {
return E('div', {
'class': 'secubox-module-card secubox-module-' + statusClass,
'data-cascade-item': module.id || module.name,
'data-cascade-category': module.category || 'other',
'data-cascade-status': status,
'data-module-installed': module.installed ? '1' : '0',
'data-module-enabled': module.enabled ? '1' : '0',
'data-module-access': statusClass,
'data-cascade-depth': '4',
'style': 'border-left: 4px solid ' + (module.color || '#64748b')
}, [
// Card Header
@ -221,7 +242,12 @@ renderModuleCards: function(modules, filter) {
]),
// Card Actions
E('div', { 'class': 'secubox-module-card-actions' },
E('div', {
'class': 'secubox-module-card-actions sb-cascade-layer',
'data-cascade-layer': 'actions',
'data-cascade-role': 'module-actions',
'data-cascade-depth': '5'
},
this.renderModuleActions(module))
]);
},
@ -233,29 +259,41 @@ renderModuleCards: function(modules, filter) {
if (!module.installed) {
actions.push(
E('button', {
'class': 'secubox-btn secubox-btn-secondary secubox-btn-sm',
'class': 'secubox-btn secubox-btn-secondary secubox-btn-sm sb-cascade-item',
'data-cascade-action': 'install',
'data-module-target': module.id,
'disabled': true
}, '📥 Install')
}, [
E('span', { 'class': 'sb-cascade-label' }, '📥 Install')
])
);
} else {
// Enable/Disable button (v0.3.1)
if (module.enabled) {
actions.push(
E('button', {
'class': 'secubox-btn secubox-btn-danger secubox-btn-sm',
'class': 'secubox-btn secubox-btn-danger secubox-btn-sm sb-cascade-item',
'data-cascade-action': 'disable',
'data-module-target': module.id,
'click': function() {
self.disableModule(module);
}
}, '⏹️ Désactiver')
}, [
E('span', { 'class': 'sb-cascade-label' }, '⏹️ Désactiver')
])
);
} else {
actions.push(
E('button', {
'class': 'secubox-btn secubox-btn-success secubox-btn-sm',
'class': 'secubox-btn secubox-btn-success secubox-btn-sm sb-cascade-item',
'data-cascade-action': 'enable',
'data-module-target': module.id,
'click': function() {
self.enableModule(module);
}
}, '▶️ Activer')
}, [
E('span', { 'class': 'sb-cascade-label' }, '▶️ Activer')
])
);
}
@ -265,8 +303,12 @@ renderModuleCards: function(modules, filter) {
actions.push(
E('a', {
'href': L.url(dashboardPath),
'class': 'secubox-btn secubox-btn-primary secubox-btn-sm'
}, '📊 Dashboard')
'class': 'secubox-btn secubox-btn-primary secubox-btn-sm sb-cascade-item',
'data-cascade-action': 'navigate',
'data-module-target': module.id
}, [
E('span', { 'class': 'sb-cascade-label' }, '📊 Dashboard')
])
);
}
}
@ -280,7 +322,7 @@ renderModuleCards: function(modules, filter) {
'netdata': 'admin/secubox/netdata/dashboard',
'netifyd': 'admin/secubox/netifyd/overview',
'wireguard': 'admin/secubox/wireguard/overview',
'network_modes': 'admin/secubox/network-modes/overview',
'network_modes': 'admin/secubox/network/modes/overview',
'client_guardian': 'admin/secubox/client-guardian/overview',
'system_hub': 'admin/secubox/system-hub/overview',
'bandwidth_manager': 'admin/secubox/bandwidth-manager/overview',
@ -289,7 +331,8 @@ renderModuleCards: function(modules, filter) {
'vhost_manager': 'admin/secubox/vhosts/overview',
'traffic_shaper': 'admin/secubox/traffic-shaper/overview',
'cdn_cache': 'admin/secubox/cdn-cache/overview',
'ksm_manager': 'admin/secubox/ksm-manager/overview'
'ksm_manager': 'admin/secubox/ksm-manager/overview',
'mqtt_bridge': 'admin/secubox/network/mqtt-bridge/overview'
};
return paths[moduleId] || null;
},
@ -368,9 +411,14 @@ renderModuleCards: function(modules, filter) {
},
filterModules: function(category) {
this.currentFilter = category || this.currentFilter || 'all';
var grid = document.getElementById('modules-grid');
if (grid) {
dom.content(grid, this.renderModuleCards(this.modulesData, category));
grid.setAttribute('data-cascade-filter', this.currentFilter);
dom.content(grid, this.renderModuleCards(this.modulesData, this.currentFilter));
}
if (this.filterLayer) {
Cascade.setActiveItem(this.filterLayer, this.currentFilter);
}
},
@ -423,9 +471,7 @@ renderModuleCards: function(modules, filter) {
},
updateModulesGrid: function() {
var activeTab = document.querySelector('.secubox-filter-tabs .cyber-tab.is-active[data-filter]');
var filter = activeTab ? activeTab.getAttribute('data-filter') : 'all';
this.filterModules(filter);
this.filterModules(this.currentFilter || 'all');
this.updateHeaderStats();
},

View File

@ -124,6 +124,18 @@ config module 'cdn_cache'
option installed '0'
option enabled '0'
config module 'mqtt_bridge'
option name 'MQTT Bridge'
option description 'USB to MQTT IoT hub'
option category 'network'
option icon '📡'
option color '#0ea5e9'
option package 'luci-app-mqtt-bridge'
option version '0.1.0'
option config 'mqtt-bridge'
option installed '0'
option enabled '0'
config module 'bandwidth_manager'
option name 'Bandwidth Manager'
option description 'QoS and bandwidth quotas'

View File

@ -0,0 +1,209 @@
config secubox 'main'
option enabled '1'
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
config module 'crowdsec'
option name 'CrowdSec Dashboard'
option description 'Collaborative threat intelligence'
option category 'security'
option icon '🛡️'
option color '#22c55e'
option package 'luci-app-crowdsec-dashboard'
option version '0.0.9'
option config 'crowdsec'
option installed '0'
option enabled '0'
config module 'netdata'
option name 'Netdata Dashboard'
option description 'Real-time system monitoring'
option category 'monitoring'
option icon '📊'
option color '#00ab44'
option package 'luci-app-netdata-dashboard'
option version '0.0.9'
option config 'netdata'
option installed '0'
option enabled '0'
config module 'netifyd'
option name 'Netifyd Dashboard'
option description 'Deep packet inspection'
option category 'security'
option icon '🔍'
option color '#8b5cf6'
option package 'luci-app-netifyd-dashboard'
option version '0.0.9'
option config 'netifyd'
option installed '0'
option enabled '0'
config module 'wireguard'
option name 'WireGuard Dashboard'
option description 'Modern VPN with QR codes'
option category 'network'
option icon '🔒'
option color '#06b6d4'
option package 'luci-app-wireguard-dashboard'
option version '0.0.9'
option config 'wireguard'
option installed '0'
option enabled '0'
config module 'network_modes'
option name 'Network Modes'
option description 'Network topology configuration'
option category 'network'
option icon '🌐'
option color '#f97316'
option package 'luci-app-network-modes'
option version '0.0.9'
option config 'network_modes'
option installed '0'
option enabled '0'
config module 'client_guardian'
option name 'Client Guardian'
option description 'NAC and captive portal'
option category 'security'
option icon '👁️'
option color '#ef4444'
option package 'luci-app-client-guardian'
option version '0.0.9'
option config 'client_guardian'
option installed '0'
option enabled '0'
config module 'system_hub'
option name 'System Hub'
option description 'Unified control center'
option category 'system'
option icon '⚙️'
option color '#6366f1'
option package 'luci-app-system-hub'
option version '0.0.9'
option config 'system_hub'
option installed '0'
option enabled '0'
config module 'cdn_cache'
option name 'CDN Cache'
option description 'Local caching proxy'
option category 'network'
option icon '📦'
option color '#06b6d4'
option package 'luci-app-cdn-cache'
option version '0.0.9'
option config 'cdn-cache'
option installed '0'
option enabled '0'
config module 'mqtt_bridge'
option name 'MQTT Bridge'
option description 'USB to MQTT IoT hub'
option category 'iot'
option icon '📡'
option color '#0ea5e9'
option package 'luci-app-mqtt-bridge'
option version '0.1.0'
option config 'mqtt-bridge'
option installed '0'
option enabled '0'
config module 'bandwidth_manager'
option name 'Bandwidth Manager'
option description 'QoS and bandwidth quotas'
option category 'network'
option icon '📡'
option color '#3b82f6'
option package 'luci-app-bandwidth-manager'
option version '0.0.9'
option config 'bandwidth_manager'
option installed '0'
option enabled '0'
config module 'auth_guardian'
option name 'Auth Guardian'
option description 'Advanced authentication system'
option category 'security'
option icon '🔑'
option color '#f59e0b'
option package 'luci-app-auth-guardian'
option version '0.0.9'
option config 'auth_guardian'
option installed '0'
option enabled '0'
config module 'media_flow'
option name 'Media Flow'
option description 'Media traffic detection and optimization'
option category 'network'
option icon '▶️'
option color '#ec4899'
option package 'luci-app-media-flow'
option version '0.0.9'
option config 'media_flow'
option installed '0'
option enabled '0'
config module 'vhost_manager'
option name 'Virtual Host Manager'
option description 'Virtual host configuration'
option category 'system'
option icon '🖥️'
option color '#8b5cf6'
option package 'luci-app-vhost-manager'
option version '0.0.9'
option config 'vhost_manager'
option installed '0'
option enabled '0'
config module 'traffic_shaper'
option name 'Traffic Shaper'
option description 'Advanced traffic shaping'
option category 'network'
option icon '📈'
option color '#10b981'
option package 'luci-app-traffic-shaper'
option version '0.0.9'
option config 'traffic_shaper'
option installed '0'
option enabled '0'
config module 'ksm_manager'
option name 'KSM Manager'
option description 'Key Storage & HSM management'
option category 'security'
option icon '🔐'
option color '#ef4444'
option package 'luci-app-ksm-manager'
option version '0.0.9'
option config 'ksm_manager'
option installed '0'
option enabled '0'

View File

@ -2,155 +2,244 @@
"admin/secubox": {
"title": "SecuBox",
"order": 25,
"action": {"type": "firstchild"}
"action": {
"type": "firstchild"
}
},
"admin/secubox/dashboard": {
"title": "Dashboard",
"order": 10,
"action": {"type": "view", "path": "secubox/dashboard"}
"action": {
"type": "view",
"path": "secubox/dashboard"
}
},
"admin/secubox/modules": {
"title": "Modules",
"order": 20,
"action": {"type": "view", "path": "secubox/modules"}
"action": {
"type": "view",
"path": "secubox/modules"
}
},
"admin/secubox/alerts": {
"title": "Alerts",
"order": 22,
"action": {"type": "view", "path": "secubox/alerts"}
"action": {
"type": "view",
"path": "secubox/alerts"
}
},
"admin/secubox/settings": {
"title": "Settings",
"order": 25,
"action": {"type": "view", "path": "secubox/settings"}
"action": {
"type": "view",
"path": "secubox/settings"
}
},
"admin/secubox/security": {
"title": "Security & Access",
"order": 30,
"action": {"type": "firstchild"}
"action": {
"type": "firstchild"
}
},
"admin/secubox/monitoring": {
"title": "Monitoring & Analytics",
"order": 35,
"action": {"type": "view", "path": "secubox/monitoring"}
"action": {
"type": "view",
"path": "secubox/monitoring"
}
},
"admin/secubox/network": {
"title": "Network & Connectivity",
"order": 40,
"action": {
"type": "firstchild"
}
},
"admin/secubox/network/cdn-cache": {
"title": "CDN Cache",
"order": 20,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [
"luci-app-cdn-cache"
]
}
},
"admin/secubox/network/cdn-cache/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "cdn-cache/overview"
}
},
"admin/secubox/network/cdn-cache/cache": {
"title": "Cache",
"order": 20,
"action": {
"type": "view",
"path": "cdn-cache/cache"
}
},
"admin/secubox/network/cdn-cache/policies": {
"title": "Policies",
"order": 30,
"action": {
"type": "view",
"path": "cdn-cache/policies"
}
},
"admin/secubox/network/cdn-cache/statistics": {
"title": "Statistics",
"order": 40,
"action": {
"type": "view",
"path": "cdn-cache/statistics"
}
},
"admin/secubox/network/cdn-cache/maintenance": {
"title": "Maintenance",
"order": 50,
"action": {
"type": "view",
"path": "cdn-cache/maintenance"
}
},
"admin/secubox/network/cdn-cache/settings": {
"title": "Settings",
"order": 90,
"action": {
"type": "view",
"path": "cdn-cache/settings"
}
},
"admin/secubox/network": {
"title": "Network & Connectivity",
"order": 40,
"action": {"type": "firstchild"}
},
"admin/secubox/network/modes": {
"title": "Network Modes",
"order": 10,
"action": {"type": "firstchild"},
"depends": {
"acl": ["luci-app-network-modes"]
}
},
"admin/secubox/network/modes/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "network-modes/overview"}
},
"admin/secubox/network/modes/wizard": {
"title": "Mode Wizard",
"order": 20,
"action": {"type": "view", "path": "network-modes/wizard"}
},
"admin/secubox/network/modes/router": {
"title": "Router Mode",
"order": 30,
"action": {"type": "view", "path": "network-modes/router"}
},
"admin/secubox/network/modes/multiwan": {
"title": "Multi-WAN Mode",
"order": 35,
"action": {"type": "view", "path": "network-modes/multiwan"}
},
"admin/secubox/network/modes/doublenat": {
"title": "Double NAT Mode",
"order": 37,
"action": {"type": "view", "path": "network-modes/doublenat"}
},
"admin/secubox/network/modes/accesspoint": {
"title": "Access Point Mode",
"order": 40,
"action": {"type": "view", "path": "network-modes/accesspoint"}
},
"admin/secubox/network/modes/relay": {
"title": "Relay Mode",
"order": 50,
"action": {"type": "view", "path": "network-modes/relay"}
},
"admin/secubox/network/modes/vpnrelay": {
"title": "VPN Relay Mode",
"order": 52,
"action": {"type": "view", "path": "network-modes/vpnrelay"}
},
"admin/secubox/network/modes/travel": {
"title": "Travel Mode",
"order": 55,
"action": {"type": "view", "path": "network-modes/travel"}
},
"admin/secubox/network/modes/sniffer": {
"title": "Sniffer Mode",
"order": 60,
"action": {"type": "view", "path": "network-modes/sniffer"}
},
"admin/secubox/network/modes/settings": {
"title": "Settings",
"order": 90,
"action": {"type": "view", "path": "network-modes/settings"}
},
"admin/secubox/network/cdn-cache": {
"title": "CDN Cache",
"order": 20,
"action": {"type": "firstchild"},
"depends": {"acl": ["luci-app-cdn-cache"]}
},
"admin/secubox/network/cdn-cache/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "cdn-cache/overview"}
},
"admin/secubox/network/cdn-cache/cache": {
"title": "Cache",
"order": 20,
"action": {"type": "view", "path": "cdn-cache/cache"}
},
"admin/secubox/network/cdn-cache/policies": {
"title": "Policies",
"order": 30,
"action": {"type": "view", "path": "cdn-cache/policies"}
},
"admin/secubox/network/cdn-cache/statistics": {
"title": "Statistics",
"order": 40,
"action": {"type": "view", "path": "cdn-cache/statistics"}
},
"admin/secubox/network/cdn-cache/maintenance": {
"title": "Maintenance",
"order": 50,
"action": {"type": "view", "path": "cdn-cache/maintenance"}
},
"admin/secubox/network/cdn-cache/settings": {
"title": "Settings",
"order": 90,
"action": {"type": "view", "path": "cdn-cache/settings"}
},
"admin/secubox/system": {
"title": "System & Performance",
"order": 50,
"action": {"type": "firstchild"}
"action": {
"type": "firstchild"
}
},
"admin/secubox/services": {
"title": "Services & Applications",
"order": 60,
"action": {"type": "firstchild"}
"action": {
"type": "firstchild"
}
},
"admin/secubox/help": {
"title": "Bonus · Help à SecuBox",
"title": "Bonus \u00b7 Help \u00e0 SecuBox",
"order": 99,
"action": {"type": "view", "path": "secubox/help"}
"action": {
"type": "view",
"path": "secubox/help"
}
},
"admin/secubox/network/network-modes": {
"title": "Network Modes",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [
"luci-app-network-modes"
]
}
},
"admin/secubox/network/network-modes/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "network-modes/overview"
}
},
"admin/secubox/network/network-modes/wizard": {
"title": "Mode Wizard",
"order": 20,
"action": {
"type": "view",
"path": "network-modes/wizard"
}
},
"admin/secubox/network/network-modes/router": {
"title": "Router Mode",
"order": 30,
"action": {
"type": "view",
"path": "network-modes/router"
}
},
"admin/secubox/network/network-modes/multiwan": {
"title": "Multi-WAN Mode",
"order": 35,
"action": {
"type": "view",
"path": "network-modes/multiwan"
}
},
"admin/secubox/network/network-modes/doublenat": {
"title": "Double NAT Mode",
"order": 37,
"action": {
"type": "view",
"path": "network-modes/doublenat"
}
},
"admin/secubox/network/network-modes/accesspoint": {
"title": "Access Point Mode",
"order": 40,
"action": {
"type": "view",
"path": "network-modes/accesspoint"
}
},
"admin/secubox/network/network-modes/relay": {
"title": "Relay Mode",
"order": 50,
"action": {
"type": "view",
"path": "network-modes/relay"
}
},
"admin/secubox/network/network-modes/vpnrelay": {
"title": "VPN Relay Mode",
"order": 52,
"action": {
"type": "view",
"path": "network-modes/vpnrelay"
}
},
"admin/secubox/network/network-modes/travel": {
"title": "Travel Mode",
"order": 55,
"action": {
"type": "view",
"path": "network-modes/travel"
}
},
"admin/secubox/network/network-modes/sniffer": {
"title": "Sniffer Mode",
"order": 60,
"action": {
"type": "view",
"path": "network-modes/sniffer"
}
},
"admin/secubox/network/network-modes/settings": {
"title": "Settings",
"order": 90,
"action": {
"type": "view",
"path": "network-modes/settings"
}
}
}
}

View File

@ -11,7 +11,9 @@ var tabs = [
{ id: 'health', icon: '❤️', label: _('Health'), path: ['admin', 'secubox', 'system', 'system-hub', 'health'] },
{ id: 'remote', icon: '📡', label: _('Remote'), path: ['admin', 'secubox', 'system', 'system-hub', 'remote'] },
{ id: 'dev-status', icon: '🚀', label: _('Dev Status'), path: ['admin', 'secubox', 'system', 'system-hub', 'dev-status'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'system', 'system-hub', 'settings'] }
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'system', 'system-hub', 'settings'] },
{ id: 'network-modes', icon: '🌐', label: _('Network Modes'), path: ['admin', 'secubox', 'network', 'modes', 'overview'] },
{ id: 'cdn-cache', icon: '📦', label: _('CDN Cache'), path: ['admin', 'secubox', 'network', 'cdn-cache', 'overview'] }
];
return baseclass.extend({

View File

@ -0,0 +1,12 @@
'use strict';
'require view';
return view.extend({
load: function() {
window.location.href = L.url('admin', 'secubox', 'network', 'cdn-cache', 'overview');
return Promise.resolve();
},
render: function() {
return E('div', {}, _('Redirecting to CDN Cache...'));
}
});

View File

@ -154,7 +154,7 @@ return view.extend({
}, _('Run full health check')),
E('button', {
'class': 'sh-btn',
'click': function() { window.location.hash = '#admin/secubox/network/network-modes'; }
'click': function() { window.location.hash = '#admin/secubox/network/modes/overview'; }
}, _('Open Network Modes'))
])
]);

View File

@ -0,0 +1,12 @@
'use strict';
'require view';
return view.extend({
load: function() {
window.location.href = L.url('admin', 'secubox', 'network', 'modes', 'overview');
return Promise.resolve();
},
render: function() {
return E('div', {}, _('Redirecting to Network Modes...'));
}
});

View File

@ -1,92 +1,120 @@
{
"admin/secubox/system/system-hub": {
"title": "System Hub",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": ["luci-app-system-hub"]
}
},
"admin/secubox/system/system-hub/overview": {
"title": "Overview",
"order": 1,
"action": {
"type": "view",
"path": "system-hub/overview"
}
},
"admin/secubox/system/system-hub/services": {
"title": "Services",
"order": 2,
"action": {
"type": "view",
"path": "system-hub/services"
}
},
"admin/secubox/system/system-hub/logs": {
"title": "System Logs",
"order": 3,
"action": {
"type": "view",
"path": "system-hub/logs"
}
},
"admin/secubox/system/system-hub/backup": {
"title": "Backup & Restore",
"order": 4,
"action": {
"type": "view",
"path": "system-hub/backup"
}
},
"admin/secubox/system/system-hub/components": {
"title": "Components",
"order": 5,
"action": {
"type": "view",
"path": "system-hub/components"
}
},
"admin/secubox/system/system-hub/diagnostics": {
"title": "Diagnostics",
"order": 6,
"action": {
"type": "view",
"path": "system-hub/diagnostics"
}
},
"admin/secubox/system/system-hub/health": {
"title": "System Health",
"order": 7,
"action": {
"type": "view",
"path": "system-hub/health"
}
},
"admin/secubox/system/system-hub/remote": {
"title": "Remote Management",
"order": 8,
"action": {
"type": "view",
"path": "system-hub/remote"
}
},
"admin/secubox/system/system-hub/settings": {
"title": "Settings",
"order": 9,
"action": {
"type": "view",
"path": "system-hub/settings"
}
},
"admin/secubox/system/system-hub/dev-status": {
"title": "Development Status",
"order": 10,
"action": {
"type": "view",
"path": "system-hub/dev-status"
}
}
}
"admin/secubox/system/system-hub": {
"title": "System Hub",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [
"luci-app-system-hub"
]
}
},
"admin/secubox/system/system-hub/overview": {
"title": "Overview",
"order": 1,
"action": {
"type": "view",
"path": "system-hub/overview"
}
},
"admin/secubox/system/system-hub/services": {
"title": "Services",
"order": 2,
"action": {
"type": "view",
"path": "system-hub/services"
}
},
"admin/secubox/system/system-hub/logs": {
"title": "System Logs",
"order": 3,
"action": {
"type": "view",
"path": "system-hub/logs"
}
},
"admin/secubox/system/system-hub/backup": {
"title": "Backup & Restore",
"order": 4,
"action": {
"type": "view",
"path": "system-hub/backup"
}
},
"admin/secubox/system/system-hub/components": {
"title": "Components",
"order": 5,
"action": {
"type": "view",
"path": "system-hub/components"
}
},
"admin/secubox/system/system-hub/diagnostics": {
"title": "Diagnostics",
"order": 6,
"action": {
"type": "view",
"path": "system-hub/diagnostics"
}
},
"admin/secubox/system/system-hub/health": {
"title": "System Health",
"order": 7,
"action": {
"type": "view",
"path": "system-hub/health"
}
},
"admin/secubox/system/system-hub/remote": {
"title": "Remote Management",
"order": 8,
"action": {
"type": "view",
"path": "system-hub/remote"
}
},
"admin/secubox/system/system-hub/settings": {
"title": "Settings",
"order": 9,
"action": {
"type": "view",
"path": "system-hub/settings"
}
},
"admin/secubox/system/system-hub/dev-status": {
"title": "Development Status",
"order": 10,
"action": {
"type": "view",
"path": "system-hub/dev-status"
}
},
"admin/secubox/system/system-hub/network-modes": {
"title": "Network Modes",
"order": 10,
"action": {
"type": "view",
"path": "system-hub/network-modes-link"
},
"depends": {
"acl": [
"luci-app-network-modes"
]
}
},
"admin/secubox/system/system-hub/cdn-cache": {
"title": "CDN Cache",
"order": 11,
"action": {
"type": "view",
"path": "system-hub/cdn-cache-link"
},
"depends": {
"acl": [
"luci-app-cdn-cache"
]
}
}
}

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-secubox
PKG_VERSION:=0.4.6
PKG_VERSION:=0.4.7
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>

View File

@ -0,0 +1,147 @@
'use strict';
'require baseclass';
/**
* SecuBox Cascade runtime helper.
* Generates layered menus/tabs/categories that stay in sync with dataset state.
*/
function assign(target, source) {
if (!source)
return target;
Object.keys(source).forEach(function(key) {
target[key] = source[key];
});
return target;
}
return baseclass.extend({
createLayer: function(config) {
var layer = this._createLayerNode(config);
this._renderItems(layer, config);
return layer;
},
decorateLayer: function(element, meta) {
if (!element)
return element;
element.classList.add('sb-cascade-layer');
if (meta && meta.type)
element.setAttribute('data-cascade-layer', meta.type);
if (meta && meta.depth)
element.setAttribute('data-cascade-depth', String(meta.depth));
if (meta && meta.role)
element.setAttribute('data-cascade-role', meta.role);
if (meta && meta.label)
element.setAttribute('data-cascade-label', meta.label);
return element;
},
updateLayer: function(layer, config) {
if (!layer)
return;
if (config && config.items)
this._renderItems(layer, config);
if (config && config.active)
this.setActiveItem(layer, config.active);
},
setActiveItem: function(layer, id) {
if (!layer)
return;
var items = layer.querySelectorAll('[data-cascade-item]');
Array.prototype.forEach.call(items, function(node) {
if (node.getAttribute('data-cascade-item') === id) {
node.setAttribute('data-state', 'active');
layer.setAttribute('data-cascade-active', id);
} else {
node.removeAttribute('data-state');
}
});
},
_createLayerNode: function(config) {
var node = config.element || E(config.tag || 'div', {});
node.classList.add('sb-cascade-layer');
if (config.className)
config.className.split(' ').forEach(function(cls) {
if (cls)
node.classList.add(cls);
});
node.setAttribute('data-cascade-layer', config.type || 'generic');
if (config.role)
node.setAttribute('data-cascade-role', config.role);
if (config.depth)
node.setAttribute('data-cascade-depth', String(config.depth));
if (config.label)
node.setAttribute('data-cascade-label', config.label);
if (config.id)
node.setAttribute('data-cascade-id', config.id);
return node;
},
_renderItems: function(layer, config) {
var self = this;
layer.textContent = '';
var items = config.items || [];
items.forEach(function(item) {
layer.appendChild(self._createItemNode(config, item));
});
layer.setAttribute('data-cascade-count', items.length);
if (config.active)
this.setActiveItem(layer, config.active);
},
_createItemNode: function(config, item) {
var tag = item.tag || config.itemTag || (item.href ? 'a' : 'button');
var attrs = assign({}, item.attrs || {});
var id = item.id || item.value || item.label || String(Math.random());
attrs['data-cascade-item'] = id;
if (item.category)
attrs['data-cascade-category'] = item.category;
if (item.status)
attrs['data-cascade-status'] = item.status;
if (item.badge !== undefined)
attrs['data-cascade-badge'] = item.badge;
if (item.state)
attrs['data-state'] = item.state;
if (item.href && tag === 'a')
attrs.href = item.href;
if (tag === 'button')
attrs.type = attrs.type || 'button';
if (config.role)
attrs['data-cascade-role'] = config.role;
var classNames = ['sb-cascade-item'];
if (config.itemClass)
classNames.push(config.itemClass);
if (item.className)
classNames.push(item.className);
if (item.class)
classNames.push(item.class);
if (attrs['class']) {
classNames.push(attrs['class']);
delete attrs['class'];
}
attrs['class'] = classNames.join(' ');
var children = [];
if (item.icon)
children.push(E('span', { 'class': 'sb-cascade-icon' }, item.icon));
children.push(E('span', { 'class': 'sb-cascade-label' }, item.label || ''));
if (item.badge !== undefined && item.badge !== null)
children.push(E('span', { 'class': 'sb-cascade-badge' }, item.badge));
var node = E(tag, attrs, children);
var clickHandler = item.onSelect || config.onSelect;
if (typeof clickHandler === 'function') {
node.addEventListener('click', function(ev) {
var result = clickHandler(item, ev);
if (result === false)
ev.preventDefault();
});
}
return node;
}
});

View File

@ -0,0 +1,198 @@
/**
* SecuBox Cascade Layout System
* Provides cascading layers for menus, tabs, categories and contextual views.
* SPDX-License-Identifier: Apache-2.0
*/
[data-cascade-root],
.sb-cascade-root {
display: flex;
flex-direction: column;
gap: var(--cyber-space-md, 1rem);
}
.sb-cascade-layer {
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: var(--cyber-space-sm, 0.5rem);
position: relative;
}
.sb-cascade-layer[data-cascade-layer="menu"],
.sb-cascade-layer[data-cascade-layer="tabs"],
.sb-cascade-layer[data-cascade-layer="categories"] {
padding: calc(var(--cyber-space-sm, 0.5rem) + 2px);
background: rgba(15, 23, 42, 0.85);
border-radius: var(--cyber-radius-lg, 20px);
border: 1px solid rgba(148, 163, 184, 0.15);
backdrop-filter: blur(14px);
box-shadow: 0 18px 36px rgba(2, 6, 23, 0.45);
}
.sb-cascade-layer[data-cascade-layer="menu"]::after,
.sb-cascade-layer[data-cascade-layer="tabs"]::after {
content: '';
position: absolute;
inset: 6px;
border-radius: inherit;
border: 1px solid rgba(255, 255, 255, 0.05);
pointer-events: none;
}
.sb-cascade-layer[data-cascade-layer="view"] {
display: grid;
gap: var(--cyber-space-md, 1rem);
}
.sb-cascade-layer[data-cascade-layer="actions"] {
display: flex;
flex-wrap: wrap;
gap: var(--cyber-space-sm, 0.5rem);
justify-content: flex-end;
}
.sb-cascade-layer[data-cascade-depth="1"] {
gap: var(--cyber-space-md, 1rem);
}
.sb-cascade-layer[data-cascade-depth="2"] {
gap: var(--cyber-space-sm, 0.5rem);
}
.sb-cascade-layer[data-cascade-depth="3"] {
gap: var(--cyber-space-xs, 0.25rem);
}
.sb-cascade-item {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--cyber-space-xs, 0.25rem);
padding: 0.55rem 1.1rem;
border-radius: var(--cyber-radius-sm, 10px);
border: 1px solid rgba(148, 163, 184, 0.35);
background: transparent;
color: var(--cyber-text-secondary, #94a3b8);
font-family: var(--cyber-font-body, 'Inter', sans-serif);
font-size: var(--cyber-text-sm, 0.875rem);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
transition:
color var(--cyber-transition, 0.22s ease),
background var(--cyber-transition, 0.22s ease),
border-color var(--cyber-transition, 0.22s ease),
box-shadow var(--cyber-transition-fast, 0.12s ease),
transform var(--cyber-transition-fast, 0.12s ease);
cursor: pointer;
text-decoration: none;
}
.sb-cascade-layer[data-cascade-layer="categories"] .sb-cascade-item {
text-transform: none;
letter-spacing: 0.02em;
}
.sb-cascade-item:hover {
border-color: rgba(99, 102, 241, 0.6);
color: var(--cyber-text-primary, #e2e8f0);
box-shadow: 0 10px 24px rgba(11, 22, 63, 0.45);
transform: translateY(-1px);
}
.sb-cascade-item[data-state="active"] {
color: var(--cyber-text-inverse, #0a0e27);
background: var(--cyber-gradient-primary, linear-gradient(135deg, #667eea, #764ba2));
border-color: transparent;
box-shadow: 0 18px 36px rgba(102, 126, 234, 0.45);
}
.sb-cascade-item[data-state="disabled"],
.sb-cascade-item[disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.sb-cascade-item[data-state="pending"] {
border-color: rgba(6, 182, 212, 0.5);
color: var(--cyber-accent-secondary, #06b6d4);
}
.sb-cascade-icon {
font-size: 1.1rem;
line-height: 1;
}
.sb-cascade-badge {
min-width: 1.5rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
font-size: 0.7rem;
line-height: 1.2;
background: rgba(15, 118, 110, 0.18);
color: #5eead4;
border: 1px solid rgba(94, 234, 212, 0.35);
}
.sb-cascade-layer[data-cascade-layer="view"][data-cascade-role="modules"] {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.sb-cascade-layer[data-cascade-layer="view"][data-cascade-role="modules"][data-cascade-filter="list"] {
grid-template-columns: 1fr;
}
.sb-cascade-layer[data-cascade-layer="view"][data-cascade-role="modules"]::before {
content: attr(data-cascade-context);
display: none;
}
.sb-cascade-layer[data-cascade-layer="view"] [data-cascade-item] {
position: relative;
}
.sb-cascade-layer[data-cascade-layer="view"] [data-module-enabled="0"] {
opacity: 0.85;
}
.sb-cascade-layer[data-cascade-layer="view"] [data-module-installed="0"] {
opacity: 0.6;
}
.sb-cascade-layer[data-cascade-layer="actions"] .sb-cascade-item {
text-transform: none;
}
.sb-cascade-layer[data-cascade-layer="actions"] .sb-cascade-item[data-cascade-action="enable"] {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.6);
color: #4ade80;
}
.sb-cascade-layer[data-cascade-layer="actions"] .sb-cascade-item[data-cascade-action="disable"] {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.5);
color: #f87171;
}
.sb-cascade-layer[data-cascade-layer="actions"] .sb-cascade-item[data-cascade-action="navigate"] {
background: rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.4);
color: #c7d2fe;
}
@media (max-width: 768px) {
.sb-cascade-layer[data-cascade-layer="menu"],
.sb-cascade-layer[data-cascade-layer="tabs"],
.sb-cascade-layer[data-cascade-layer="categories"] {
overflow-x: auto;
flex-wrap: nowrap;
scroll-snap-type: x mandatory;
}
.sb-cascade-item {
scroll-snap-align: start;
}
}

View File

@ -17,6 +17,7 @@
@import url('./layouts/dashboard.css');
@import url('./layouts/grid.css');
@import url('./layouts/responsive.css');
@import url('./layouts/cascade.css');
@import url('./themes/dark.css');
@import url('./themes/light.css');

View File

@ -1,7 +1,7 @@
#!/bin/bash
set -euo pipefail
ROUTER="${ROUTER:-root@192.168.8.191}"
ROUTER="${ROUTER:-root@192.168.8.205}"
TARGET_PATH="${TARGET_PATH:-/www/luci-static}"
SSH_OPTS=${SSH_OPTS:--o RequestTTY=no -o ForwardX11=no}
SCP_OPTS=${SCP_OPTS:-}
@ -131,7 +131,7 @@ verify_remote() {
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
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