diff --git a/.codex/WIP.md b/.codex/WIP.md index e7643886..6ae7b105 100644 --- a/.codex/WIP.md +++ b/.codex/WIP.md @@ -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 diff --git a/.codex/apps/mqtt-bridge/TODO.md b/.codex/apps/mqtt-bridge/TODO.md new file mode 100644 index 00000000..6900ae9f --- /dev/null +++ b/.codex/apps/mqtt-bridge/TODO.md @@ -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. diff --git a/.codex/apps/mqtt-bridge/WIP.md b/.codex/apps/mqtt-bridge/WIP.md new file mode 100644 index 00000000..ac35b6d0 --- /dev/null +++ b/.codex/apps/mqtt-bridge/WIP.md @@ -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. diff --git a/DOCS/MQTT_BRIDGE.md b/DOCS/MQTT_BRIDGE.md new file mode 100644 index 00000000..87c1b38a --- /dev/null +++ b/DOCS/MQTT_BRIDGE.md @@ -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. diff --git a/README.md b/README.md index c8974c09..0f9db9e0 100644 --- a/README.md +++ b/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. diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js index 265240e6..5ee64f2b 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/cache.js @@ -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), diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js index caa85aaa..078657b2 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/maintenance.js @@ -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; } diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js index 3d602282..c33f6a09 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/overview.js @@ -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), diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js index 82af8fdf..fbc0d28b 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/policies.js @@ -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; } diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js index a8ab3c14..985556ca 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/settings.js @@ -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() ]); diff --git a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js index d62eb5f3..bad23283 100644 --- a/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js +++ b/luci-app-cdn-cache/htdocs/luci-static/resources/view/cdn-cache/statistics.js @@ -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), diff --git a/luci-app-mqtt-bridge/Makefile b/luci-app-mqtt-bridge/Makefile new file mode 100644 index 00000000..ae1d0069 --- /dev/null +++ b/luci-app-mqtt-bridge/Makefile @@ -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 + +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 diff --git a/luci-app-mqtt-bridge/README.md b/luci-app-mqtt-bridge/README.md new file mode 100644 index 00000000..8bb9c80d --- /dev/null +++ b/luci-app-mqtt-bridge/README.md @@ -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. diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/api.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/api.js new file mode 100644 index 00000000..6e74ec7a --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/api.js @@ -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 +}); diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css new file mode 100644 index 00000000..07ee65cc --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/common.css @@ -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)); + } +} diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js new file mode 100644 index 00000000..462f8961 --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js @@ -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; + } + } + }); + } +}); diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js new file mode 100644 index 00000000..60bb7005 --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/devices.js @@ -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'); + }); + } +}); diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/overview.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/overview.js new file mode 100644 index 00000000..2573a4b0 --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/overview.js @@ -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 +}); diff --git a/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js new file mode 100644 index 00000000..4691a558 --- /dev/null +++ b/luci-app-mqtt-bridge/htdocs/luci-static/resources/view/mqtt-bridge/settings.js @@ -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'); + }); + } +}); diff --git a/luci-app-mqtt-bridge/luasrc/controller/secubox/mqtt-bridge.lua b/luci-app-mqtt-bridge/luasrc/controller/secubox/mqtt-bridge.lua new file mode 100644 index 00000000..138122b2 --- /dev/null +++ b/luci-app-mqtt-bridge/luasrc/controller/secubox/mqtt-bridge.lua @@ -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 diff --git a/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge b/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge new file mode 100644 index 00000000..57bf0fa9 --- /dev/null +++ b/luci-app-mqtt-bridge/root/etc/config/mqtt-bridge @@ -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 diff --git a/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge new file mode 100755 index 00000000..b97137e9 --- /dev/null +++ b/luci-app-mqtt-bridge/root/usr/libexec/rpcd/luci.mqtt-bridge @@ -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 diff --git a/luci-app-mqtt-bridge/root/usr/share/luci/menu.d/luci-app-mqtt-bridge.json b/luci-app-mqtt-bridge/root/usr/share/luci/menu.d/luci-app-mqtt-bridge.json new file mode 100644 index 00000000..6f6630fe --- /dev/null +++ b/luci-app-mqtt-bridge/root/usr/share/luci/menu.d/luci-app-mqtt-bridge.json @@ -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" + } + } +} \ No newline at end of file diff --git a/luci-app-mqtt-bridge/root/usr/share/rpcd/acl.d/luci-app-mqtt-bridge.json b/luci-app-mqtt-bridge/root/usr/share/rpcd/acl.d/luci-app-mqtt-bridge.json new file mode 100644 index 00000000..7a154aa5 --- /dev/null +++ b/luci-app-mqtt-bridge/root/usr/share/rpcd/acl.d/luci-app-mqtt-bridge.json @@ -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"] + } + } +} diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/dashboard.css b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/dashboard.css index 25be87bb..2b7ae4dd 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/dashboard.css +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/dashboard.css @@ -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; } diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js index 2a7e367e..f3bcdc12 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/helpers.js @@ -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 || [])) ]); } diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js index e823abe7..2659da9d 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/router.js @@ -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 || ''; } }); diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/settings.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/settings.js index f0436e95..0f89eb65 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/settings.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/settings.js @@ -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'), diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js index 3797627b..f2bbb2e3 100644 --- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js +++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/wizard.js @@ -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 : ''; + } }); diff --git a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes index ed493dbf..71cf3d26 100755 --- a/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes +++ b/luci-app-network-modes/root/usr/libexec/rpcd/luci.network-modes @@ -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 diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js index 0a6d1bdb..cff54134 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js @@ -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; + } + } + }); } }); diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js index 04c79b64..c837894f 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/modules.js @@ -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(); }, diff --git a/luci-app-secubox/root/etc/config/secubox b/luci-app-secubox/root/etc/config/secubox index 6b80477a..3793192c 100644 --- a/luci-app-secubox/root/etc/config/secubox +++ b/luci-app-secubox/root/etc/config/secubox @@ -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' diff --git a/luci-app-secubox/root/etc/config/secubox.bak b/luci-app-secubox/root/etc/config/secubox.bak new file mode 100644 index 00000000..5b693c20 --- /dev/null +++ b/luci-app-secubox/root/etc/config/secubox.bak @@ -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' diff --git a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json index 636db20e..e27e652b 100644 --- a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json @@ -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" + } } -} +} \ No newline at end of file diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js index f3cc962c..a654f43e 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js @@ -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({ diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/cdn-cache-link.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/cdn-cache-link.js new file mode 100644 index 00000000..2caf93e3 --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/cdn-cache-link.js @@ -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...')); + } +}); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js index 413a56b1..d9bbedfb 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js @@ -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')) ]) ]); diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/network-modes-link.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/network-modes-link.js new file mode 100644 index 00000000..f4836a65 --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/network-modes-link.js @@ -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...')); + } +}); diff --git a/luci-app-system-hub/root/usr/share/luci/menu.d/luci-app-system-hub.json b/luci-app-system-hub/root/usr/share/luci/menu.d/luci-app-system-hub.json index 947fa949..098df2ec 100644 --- a/luci-app-system-hub/root/usr/share/luci/menu.d/luci-app-system-hub.json +++ b/luci-app-system-hub/root/usr/share/luci/menu.d/luci-app-system-hub.json @@ -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" + ] + } + } +} \ No newline at end of file diff --git a/luci-theme-secubox/Makefile b/luci-theme-secubox/Makefile index 296e44b3..689a9fbc 100644 --- a/luci-theme-secubox/Makefile +++ b/luci-theme-secubox/Makefile @@ -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 diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/cascade.js b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/cascade.js new file mode 100644 index 00000000..b93413ab --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/cascade.js @@ -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; + } +}); diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/layouts/cascade.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/layouts/cascade.css new file mode 100644 index 00000000..77c06e71 --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/layouts/cascade.css @@ -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; + } +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/secubox-theme.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/secubox-theme.css index f0c3e859..99c0fa3a 100644 --- a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/secubox-theme.css +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/secubox-theme.css @@ -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'); diff --git a/secubox-tools/quick-deploy.sh b/secubox-tools/quick-deploy.sh index cd7cd5ca..3fdf08ad 100755 --- a/secubox-tools/quick-deploy.sh +++ b/secubox-tools/quick-deploy.sh @@ -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