feat: cascade navigation and zigbee presets
This commit is contained in:
parent
4dea8d28e0
commit
54e0b5df6c
@ -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
|
||||
|
||||
|
||||
17
.codex/apps/mqtt-bridge/TODO.md
Normal file
17
.codex/apps/mqtt-bridge/TODO.md
Normal 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.
|
||||
13
.codex/apps/mqtt-bridge/WIP.md
Normal file
13
.codex/apps/mqtt-bridge/WIP.md
Normal 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
76
DOCS/MQTT_BRIDGE.md
Normal 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.
|
||||
14
README.md
14
README.md
@ -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.
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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()
|
||||
]);
|
||||
|
||||
@ -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),
|
||||
|
||||
17
luci-app-mqtt-bridge/Makefile
Normal file
17
luci-app-mqtt-bridge/Makefile
Normal 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
|
||||
25
luci-app-mqtt-bridge/README.md
Normal file
25
luci-app-mqtt-bridge/README.md
Normal 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.
|
||||
@ -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
|
||||
});
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
18
luci-app-mqtt-bridge/root/etc/config/mqtt-bridge
Normal file
18
luci-app-mqtt-bridge/root/etc/config/mqtt-bridge
Normal 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
|
||||
209
luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge
Executable file
209
luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge
Executable 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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 || []))
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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 || '';
|
||||
}
|
||||
});
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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 : '';
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
209
luci-app-secubox/root/etc/config/secubox.bak
Normal file
209
luci-app-secubox/root/etc/config/secubox.bak
Normal 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'
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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...'));
|
||||
}
|
||||
});
|
||||
@ -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'))
|
||||
])
|
||||
]);
|
||||
|
||||
@ -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...'));
|
||||
}
|
||||
});
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user