diff --git a/.claude/THEME_CONTEXT.md b/.claude/THEME_CONTEXT.md
new file mode 100644
index 00000000..eb60a097
--- /dev/null
+++ b/.claude/THEME_CONTEXT.md
@@ -0,0 +1,292 @@
+# Claude Context: Global Theme Implementation
+
+**FOR CLAUDE CODE AI ASSISTANT**
+
+## π― When Asked to Work on Theme/UI
+
+If the user asks you to:
+- "Create a global theme"
+- "Unify the design"
+- "Implement CyberMood theme"
+- "Add multi-language support"
+- "Make it look like the website"
+
+**IMPORTANT**: Read `DOCS/GLOBAL_THEME_SYSTEM.md` first!
+
+## π¨ Quick Design Reference
+
+### Color Palette
+
+```css
+/* Use these variables in all new code */
+Primary: var(--cyber-accent-primary) /* #667eea */
+Secondary: var(--cyber-accent-secondary) /* #06b6d4 */
+Background: var(--cyber-bg-primary) /* #0a0e27 */
+Surface: var(--cyber-surface) /* #252b4a */
+Text: var(--cyber-text-primary) /* #e2e8f0 */
+Success: var(--cyber-success) /* #10b981 */
+Danger: var(--cyber-danger) /* #ef4444 */
+```
+
+### Typography
+
+```css
+Display/Headers: var(--cyber-font-display) /* Orbitron */
+Body Text: var(--cyber-font-body) /* Inter */
+Code/Metrics: var(--cyber-font-mono) /* JetBrains Mono */
+```
+
+### Component Classes
+
+```html
+
+
+
+
+
+
+
+
+Active
+
+
+
+
+```
+
+## π Multi-Language Support
+
+### Usage Pattern
+
+```javascript
+'require cybermood/theme as Theme';
+
+// Initialize theme
+Theme.init();
+
+// Set language
+Theme.setLanguage('fr'); // en, fr, de, es
+
+// Translate strings
+var title = Theme.t('dashboard.title');
+var welcome = Theme.t('dashboard.welcome', { name: 'SecuBox' });
+```
+
+### Translation Keys Structure
+
+```
+common.* - Common UI strings (loading, error, success, etc.)
+dashboard.* - Dashboard-specific strings
+modules.* - Module names
+settings.* - Settings page strings
+[module_name].* - Module-specific strings
+```
+
+## ποΈ Creating New Components
+
+### Always Use Theme System
+
+```javascript
+// β DON'T: Create components manually
+E('div', { style: 'background: #667eea; padding: 16px;' }, 'Content');
+
+// β
DO: Use theme components
+Theme.createCard({
+ title: Theme.t('card.title'),
+ icon: 'π―',
+ content: E('div', {}, 'Content'),
+ variant: 'primary'
+});
+```
+
+### Component Template
+
+```javascript
+// Create a new themed component
+renderMyComponent: function() {
+ return E('div', { 'class': 'cyber-container' }, [
+ // Always load theme CSS first
+ E('link', {
+ 'rel': 'stylesheet',
+ 'href': L.resource('cybermood/cybermood.css')
+ }),
+
+ // Use theme components
+ Theme.createCard({
+ title: Theme.t('component.title'),
+ icon: 'β‘',
+ content: this.renderContent(),
+ variant: 'success'
+ })
+ ]);
+}
+```
+
+## π Implementation Prompts
+
+### Prompt 1: Create Global Theme Package
+
+```
+Create the luci-theme-cybermood package following the structure in
+DOCS/GLOBAL_THEME_SYSTEM.md. Include:
+
+1. Package structure with Makefile
+2. CSS variable system (variables.css)
+3. Core components (cards.css, buttons.css, forms.css)
+4. Theme controller JavaScript (cybermood.js)
+5. Default translations (en.json, fr.json)
+
+Use the ready-to-use templates from the documentation.
+Apply CyberMood design aesthetic (metallic, glass effects, neon accents).
+Ensure dark theme as default with light and cyberpunk variants.
+```
+
+### Prompt 2: Migrate Module to Global Theme
+
+```
+Migrate luci-app-[MODULE-NAME] to use the global CyberMood theme:
+
+1. Remove module-specific CSS files (keep only module-unique styles)
+2. Import cybermood.css in all views
+3. Update all components to use cyber-* classes
+4. Replace E() calls with Theme.create*() methods where appropriate
+5. Replace hardcoded strings with Theme.t() translations
+6. Test dark/light theme switching
+7. Verify responsive design
+
+Reference GLOBAL_THEME_SYSTEM.md for component usage examples.
+```
+
+### Prompt 3: Add Multi-Language Support
+
+```
+Add multi-language support to [MODULE-NAME]:
+
+1. Extract all user-facing strings to translation keys
+2. Create translation files for en, fr, de, es
+3. Use Theme.t() for all strings
+4. Add language selector to settings
+5. Test language switching
+
+Follow the translation structure in GLOBAL_THEME_SYSTEM.md.
+Use meaningful translation keys (e.g., 'dashboard.active_modules').
+```
+
+### Prompt 4: Create New Themed Component
+
+```
+Create a new [COMPONENT-TYPE] component following CyberMood design:
+
+1. Use CSS variables for all colors (var(--cyber-*))
+2. Apply glass effect with backdrop-filter
+3. Add hover animations (transform, glow effects)
+4. Support dark/light themes
+5. Make it responsive
+6. Add to cybermood/components/
+
+Style should match: metallic gradients, neon accents, smooth animations.
+Reference existing components in GLOBAL_THEME_SYSTEM.md.
+```
+
+### Prompt 5: Implement Responsive Dashboard
+
+```
+Create a responsive dashboard layout using CyberMood theme:
+
+1. Use cyber-grid for layout (auto-responsive)
+2. Create cards with Theme.createCard()
+3. Add stats with animated counters
+4. Include theme toggle button
+5. Add language selector
+6. Support mobile (320px) to desktop (1920px+)
+
+Follow the dashboard example in GLOBAL_THEME_SYSTEM.md.
+Use metallic gradients for stats, glass effects for cards.
+```
+
+## β οΈ Critical Rules
+
+1. **NEVER hardcode colors**: Always use CSS variables
+ ```css
+ /* β BAD */
+ background: #667eea;
+
+ /* β
GOOD */
+ background: var(--cyber-accent-primary);
+ ```
+
+2. **ALWAYS support dark/light themes**: Test both
+ ```css
+ /* Automatically handled by data-theme attribute */
+ [data-theme="light"] { /* overrides */ }
+ ```
+
+3. **ALWAYS use translation keys**: No hardcoded strings
+ ```javascript
+ /* β BAD */
+ E('h1', {}, 'Dashboard');
+
+ /* β
GOOD */
+ E('h1', {}, Theme.t('dashboard.title'));
+ ```
+
+4. **ALWAYS load theme CSS**: First element in render
+ ```javascript
+ E('link', { 'rel': 'stylesheet', 'href': L.resource('cybermood/cybermood.css') })
+ ```
+
+5. **PREFER theme components**: Over manual E() creation
+ ```javascript
+ /* β ACCEPTABLE but not preferred */
+ E('div', { 'class': 'card' }, content);
+
+ /* β
PREFERRED */
+ Theme.createCard({ content: content });
+ ```
+
+## π Before You Start
+
+1. Read `DOCS/GLOBAL_THEME_SYSTEM.md`
+2. Check if `luci-theme-cybermood` package exists
+3. Review existing themed modules for patterns
+4. Test on both dark and light themes
+5. Verify responsive on mobile/desktop
+
+## π Related Documentation
+
+- **Main Guide**: `DOCS/GLOBAL_THEME_SYSTEM.md`
+- **Development Guidelines**: `DOCS/DEVELOPMENT-GUIDELINES.md`
+- **Quick Start**: `DOCS/QUICK-START.md`
+- **Website Reference**: `http://192.168.8.191/luci-static/secubox/` (deployed demo)
+
+## π¨ Visual References
+
+Look at these for design inspiration:
+- SecuBox website: Modern, metallic, glass effects
+- System Hub module: Dashboard layout, stats cards
+- Network Modes: Header design, mode badges
+- Existing help.css: Button styles, animations
+
+## β
Quality Checklist
+
+Before marking theme work complete:
+
+- [ ] Uses CSS variables (no hardcoded colors)
+- [ ] Supports dark/light/cyberpunk themes
+- [ ] All strings use Theme.t() translations
+- [ ] Components use cyber-* classes
+- [ ] Responsive (mobile to 4K)
+- [ ] Glass effects applied (backdrop-filter)
+- [ ] Hover animations work smoothly
+- [ ] Accessibility: keyboard navigation works
+- [ ] Performance: < 50KB CSS bundle
+- [ ] Browser tested: Chrome, Firefox, Safari
+
+---
+
+**Remember**: The goal is a unified, beautiful, responsive, multi-language CyberMood aesthetic across ALL SecuBox modules. Think: Cyberpunk meets modern minimalism. π―
diff --git a/.codex/THEME_CONTEXT.md b/.codex/THEME_CONTEXT.md
new file mode 100644
index 00000000..1557a92c
--- /dev/null
+++ b/.codex/THEME_CONTEXT.md
@@ -0,0 +1,200 @@
+# Codex Context: Global Theme Implementation
+
+**FOR CODEX CODING AGENT**
+
+## π― When Asked to Work on Theme/UI
+
+If the user asks you to:
+- βCreate a global themeβ
+- βUnify the designβ
+- βImplement CyberMood themeβ
+- βAdd multi-language supportβ
+- βMake it look like the websiteβ
+
+**IMPORTANT**: Read `DOCS/GLOBAL_THEME_SYSTEM.md` first!
+
+## π¨ Quick Design Reference
+
+### Color Palette
+
+```css
+/* Use these variables in all new code */
+Primary: var(--cyber-accent-primary) /* #667eea */
+Secondary: var(--cyber-accent-secondary) /* #06b6d4 */
+Background: var(--cyber-bg-primary) /* #0a0e27 */
+Surface: var(--cyber-surface) /* #252b4a */
+Text: var(--cyber-text-primary) /* #e2e8f0 */
+Success: var(--cyber-success) /* #10b981 */
+Danger: var(--cyber-danger) /* #ef4444 */
+```
+
+### Typography
+
+```css
+Display/Headers: var(--cyber-font-display) /* Orbitron */
+Body Text: var(--cyber-font-body) /* Inter */
+Code/Metrics: var(--cyber-font-mono) /* JetBrains Mono */
+```
+
+### Component Classes
+
+```html
+
+
+
+
+
+
+
+
+Active
+
+
+
+
+```
+
+## π Multi-Language Support
+
+### Usage Pattern
+
+```javascript
+'require cybermood/theme as Theme';
+
+// Initialize theme
+Theme.init();
+
+// Set language
+Theme.setLanguage('fr'); // en, fr, de, es
+
+// Translate strings
+var title = Theme.t('dashboard.title');
+var welcome = Theme.t('dashboard.welcome', { name: 'SecuBox' });
+```
+
+### Translation Keys Structure
+
+```
+common.* - Common UI strings (loading, error, success, etc.)
+dashboard.* - Dashboard-specific strings
+modules.* - Module names
+settings.* - Settings page strings
+[module_name].* - Module-specific strings
+```
+
+## ποΈ Creating New Components
+
+### Always Use Theme System
+
+```javascript
+// β DON'T: Create components manually
+E('div', { style: 'background: #667eea; padding: 16px;' }, 'Content');
+
+// β
DO: Use theme components
+Theme.createCard({
+ title: Theme.t('card.title'),
+ icon: 'π―',
+ content: E('div', {}, 'Content'),
+ variant: 'primary'
+});
+```
+
+### Component Template
+
+```javascript
+// Create a new themed component
+renderMyComponent: function() {
+ return E('div', { 'class': 'cyber-container' }, [
+ // Always load theme CSS first
+ E('link', {
+ 'rel': 'stylesheet',
+ 'href': L.resource('cybermood/cybermood.css')
+ }),
+
+ // Use theme components
+ Theme.createCard({
+ title: Theme.t('component.title'),
+ icon: 'β‘',
+ content: this.renderContent(),
+ variant: 'success'
+ })
+ ]);
+}
+```
+
+## π Implementation Prompts
+
+### Prompt 1: Create Global Theme Package
+
+```
+Create the luci-theme-cybermood package following the structure in
+DOCS/GLOBAL_THEME_SYSTEM.md. Include:
+
+1. Package structure with Makefile
+2. CSS variable system (variables.css)
+3. Core components (cards.css, buttons.css, forms.css)
+4. Theme controller JavaScript (cybermood.js)
+5. Default translations (en.json, fr.json)
+
+Use the ready-to-use templates from the documentation.
+Apply CyberMood design aesthetic (metallic, glass effects, neon accents).
+Ensure dark theme as default with light and cyberpunk variants.
+```
+
+### Prompt 2: Migrate Module to Global Theme
+
+```
+Migrate luci-app-[MODULE-NAME] to use the global CyberMood theme:
+
+1. Remove module-specific CSS files (keep only module-unique styles)
+2. Import cybermood.css in all views
+3. Update all components to use cyber-* classes
+4. Replace E() calls with Theme.create*() methods where appropriate
+5. Replace hardcoded strings with Theme.t() translations
+6. Test dark/light theme switching
+7. Verify responsive design
+```
+
+### Prompt 3: Add Multi-Language Support
+
+```
+Add multi-language support to [MODULE-NAME]:
+
+1. Extract all user-facing strings to translation keys
+2. Create translation files for en, fr, de, es
+3. Use Theme.t() for all strings
+4. Add language selector to settings
+5. Test language switching
+```
+
+### Prompt 4: Create New Themed Component
+
+```
+Create a new [COMPONENT-TYPE] component following CyberMood design:
+
+1. Use CSS variables for all colors (var(--cyber-*))
+2. Apply glass effect with backdrop-filter
+3. Add hover animations (transform, glow effects)
+4. Support dark/light themes
+5. Make it responsive
+6. Add to cybermood/components/
+```
+
+### Prompt 5: Implement Responsive Dashboard
+
+```
+Create a responsive dashboard layout using CyberMood theme:
+
+1. Use Theme.createCard for each section
+2. Add quick stats, charts placeholders, alerts, and actions
+3. Support breakpoints at 1440px, 1024px, 768px, 480px
+4. Use CSS grid + flex combos from GLOBAL_THEME_SYSTEM.md
+5. Ensure all copy uses Theme.t()
+```
+
+> **Always align new work with `cybermood/theme.js`, `cybermood.css`, and the prompts above.**
diff --git a/DOCS/GLOBAL_THEME_SYSTEM.md b/DOCS/GLOBAL_THEME_SYSTEM.md
new file mode 100644
index 00000000..d384e94f
--- /dev/null
+++ b/DOCS/GLOBAL_THEME_SYSTEM.md
@@ -0,0 +1,1040 @@
+# SecuBox Global Theme System
+
+**Version:** 1.0.0
+**Date:** 2025-12-28
+**Status:** Planning & Specification
+
+## π― Vision
+
+Create a unified, dynamic, responsive, and modern "CyberMood" design system for all SecuBox modules with multi-language support, inspired by the SecuBox marketing website aesthetic.
+
+## π¨ CyberMood Design Language
+
+### Core Aesthetic Principles
+
+**"CyberMood"** = Cyberpunk meets Modern Minimalism
+- **Metallic & Glass**: Reflective surfaces, glassmorphism effects
+- **Neon Accents**: Electric blues, purples, cyans with glow effects
+- **Dark Base**: Deep backgrounds with subtle gradients
+- **Dynamic Motion**: Smooth animations, particle effects, flowing gradients
+- **Information Dense**: Modern dashboard layouts with data visualization
+- **Responsive Flow**: Adapts seamlessly from mobile to desktop
+
+### Visual Identity
+
+```
+Primary Palette:
+ Base: #0a0e27 (Deep Space Blue)
+ Surface: #151932 (Dark Slate)
+ Accent: #667eea (Electric Blue)
+ Secondary: #764ba2 (Cyber Purple)
+ Success: #10b981 (Emerald)
+ Warning: #f59e0b (Amber)
+ Danger: #ef4444 (Red)
+ Info: #06b6d4 (Cyan)
+
+Metallic Gradients:
+ Steel: linear-gradient(135deg, #434343 0%, #000000 100%)
+ Chrome: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%)
+ Gold: linear-gradient(135deg, #f9d423 0%, #ff4e50 100%)
+ Cyber: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
+
+Glass Effects:
+ Blur: backdrop-filter: blur(10px)
+ Opacity: rgba(255, 255, 255, 0.05)
+ Border: 1px solid rgba(255, 255, 255, 0.1)
+ Shadow: 0 8px 32px rgba(0, 0, 0, 0.37)
+
+Typography:
+ Display: 'Orbitron' (cyberpunk headers)
+ Body: 'Inter' (clean readability)
+ Mono: 'JetBrains Mono' (code & metrics)
+
+Animation:
+ Speed: 0.3s ease-in-out (standard)
+ Bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55)
+ Smooth: cubic-bezier(0.4, 0, 0.2, 1)
+```
+
+## π Current State Analysis
+
+### Existing Themes
+
+```
+luci-app-secubox/htdocs/luci-static/resources/secubox/
+βββ secubox.css # Base SecuBox styles (7.0KB)
+βββ dashboard.css # Dashboard-specific (9.5KB)
+βββ common.css # Shared utilities (8.4KB)
+βββ modules.css # Modules page (7.5KB)
+βββ alerts.css # Alerts page (5.1KB)
+βββ monitoring.css # Monitoring page (3.0KB)
+βββ help.css # Help system (7.0KB)
+βββ theme.js # Theme JavaScript (2.0KB)
+
+luci-app-system-hub/htdocs/luci-static/resources/system-hub/
+βββ dashboard.css # System Hub dashboard (18.2KB)
+βββ common.css # System Hub common (8.4KB)
+βββ theme.js # System Hub theme (similar to secubox)
+
+luci-app-network-modes/htdocs/luci-static/resources/network-modes/
+βββ dashboard.css # Network Modes dashboard (18.2KB)
+βββ common.css # Network Modes common (8.4KB)
+```
+
+### Problems with Current Approach
+
+1. **Fragmentation**: Each module has its own CSS files
+2. **Duplication**: Common styles repeated across modules
+3. **Inconsistency**: Slightly different color values, spacing, etc.
+4. **Maintenance**: Changes require updating multiple files
+5. **Bundle Size**: Duplicate CSS loaded per module (~50KB total)
+6. **No Centralized Theme**: Can't switch themes globally
+
+## ποΈ Proposed Architecture
+
+### Global Theme Structure
+
+```
+luci-theme-cybermood/ # NEW: Global theme package
+βββ Makefile
+βββ README.md
+βββ htdocs/luci-static/resources/
+ βββ cybermood/
+ βββ core/
+ β βββ variables.css # CSS custom properties
+ β βββ reset.css # Normalize/reset
+ β βββ typography.css # Font definitions
+ β βββ animations.css # Keyframes & transitions
+ β βββ utilities.css # Helper classes
+ βββ components/
+ β βββ buttons.css # Button styles
+ β βββ cards.css # Card components
+ β βββ forms.css # Form elements
+ β βββ tables.css # Data tables
+ β βββ modals.css # Modal dialogs
+ β βββ tooltips.css # Tooltips
+ β βββ badges.css # Status badges
+ β βββ alerts.css # Alert messages
+ β βββ charts.css # Chart containers
+ β βββ navigation.css # Nav elements
+ βββ layouts/
+ β βββ dashboard.css # Dashboard layout
+ β βββ grid.css # Grid system
+ β βββ responsive.css # Breakpoints
+ βββ themes/
+ β βββ dark.css # Dark theme (default)
+ β βββ light.css # Light theme
+ β βββ cyberpunk.css # High-contrast cyber
+ βββ i18n/
+ β βββ en.json # English strings
+ β βββ fr.json # French strings
+ β βββ de.json # German strings
+ β βββ es.json # Spanish strings
+ βββ cybermood.css # Main bundle (imports all)
+ βββ cybermood.min.css # Minified version
+ βββ cybermood.js # Theme controller
+```
+
+### Module Integration
+
+```javascript
+// In each module's view file
+'use strict';
+'require view';
+'require cybermood/theme as Theme';
+
+return view.extend({
+ render: function() {
+ // Apply theme
+ Theme.apply('dark');
+
+ // Use theme components
+ return E('div', { 'class': 'cyber-container' }, [
+ Theme.createCard({
+ title: _('Module Title'),
+ icon: 'π―',
+ content: this.renderContent()
+ })
+ ]);
+ }
+});
+```
+
+## π¨ Ready-to-Use Templates
+
+### 1. CSS Variables (variables.css)
+
+```css
+/**
+ * CyberMood Design System - CSS Variables
+ * Version: 1.0.0
+ */
+
+:root {
+ /* ========================================
+ Colors - Base Palette
+ ======================================== */
+
+ /* Dark Theme (Default) */
+ --cyber-bg-primary: #0a0e27;
+ --cyber-bg-secondary: #151932;
+ --cyber-bg-tertiary: #1e2139;
+ --cyber-surface: #252b4a;
+ --cyber-surface-light: #2d3454;
+
+ /* Text Colors */
+ --cyber-text-primary: #e2e8f0;
+ --cyber-text-secondary: #94a3b8;
+ --cyber-text-muted: #64748b;
+ --cyber-text-inverse: #0a0e27;
+
+ /* Accent Colors */
+ --cyber-accent-primary: #667eea;
+ --cyber-accent-primary-end: #764ba2;
+ --cyber-accent-secondary: #06b6d4;
+ --cyber-accent-tertiary: #8b5cf6;
+
+ /* Semantic Colors */
+ --cyber-success: #10b981;
+ --cyber-success-light: #34d399;
+ --cyber-warning: #f59e0b;
+ --cyber-warning-light: #fbbf24;
+ --cyber-danger: #ef4444;
+ --cyber-danger-light: #f87171;
+ --cyber-info: #06b6d4;
+ --cyber-info-light: #22d3ee;
+
+ /* Metallic Gradients */
+ --cyber-gradient-steel: linear-gradient(135deg, #434343 0%, #000000 100%);
+ --cyber-gradient-chrome: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%);
+ --cyber-gradient-cyber: linear-gradient(135deg, var(--cyber-accent-primary) 0%, var(--cyber-accent-primary-end) 100%);
+ --cyber-gradient-success: linear-gradient(135deg, var(--cyber-success) 0%, var(--cyber-success-light) 100%);
+ --cyber-gradient-danger: linear-gradient(135deg, var(--cyber-danger) 0%, var(--cyber-danger-light) 100%);
+
+ /* Glass Effects */
+ --cyber-glass-bg: rgba(255, 255, 255, 0.05);
+ --cyber-glass-border: rgba(255, 255, 255, 0.1);
+ --cyber-glass-blur: 10px;
+ --cyber-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
+
+ /* ========================================
+ Typography
+ ======================================== */
+
+ --cyber-font-display: 'Orbitron', 'Inter', sans-serif;
+ --cyber-font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ --cyber-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
+
+ --cyber-font-size-xs: 0.75rem; /* 12px */
+ --cyber-font-size-sm: 0.875rem; /* 14px */
+ --cyber-font-size-base: 1rem; /* 16px */
+ --cyber-font-size-lg: 1.125rem; /* 18px */
+ --cyber-font-size-xl: 1.25rem; /* 20px */
+ --cyber-font-size-2xl: 1.5rem; /* 24px */
+ --cyber-font-size-3xl: 1.875rem; /* 30px */
+ --cyber-font-size-4xl: 2.25rem; /* 36px */
+
+ --cyber-font-weight-light: 300;
+ --cyber-font-weight-normal: 400;
+ --cyber-font-weight-medium: 500;
+ --cyber-font-weight-semibold: 600;
+ --cyber-font-weight-bold: 700;
+
+ /* ========================================
+ Spacing
+ ======================================== */
+
+ --cyber-space-xs: 0.25rem; /* 4px */
+ --cyber-space-sm: 0.5rem; /* 8px */
+ --cyber-space-md: 1rem; /* 16px */
+ --cyber-space-lg: 1.5rem; /* 24px */
+ --cyber-space-xl: 2rem; /* 32px */
+ --cyber-space-2xl: 3rem; /* 48px */
+ --cyber-space-3xl: 4rem; /* 64px */
+
+ /* ========================================
+ Border Radius
+ ======================================== */
+
+ --cyber-radius-sm: 0.25rem; /* 4px */
+ --cyber-radius-md: 0.5rem; /* 8px */
+ --cyber-radius-lg: 0.75rem; /* 12px */
+ --cyber-radius-xl: 1rem; /* 16px */
+ --cyber-radius-full: 9999px;
+
+ /* ========================================
+ Shadows
+ ======================================== */
+
+ --cyber-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --cyber-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
+ --cyber-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
+ --cyber-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
+ --cyber-shadow-glow: 0 0 20px rgba(102, 126, 234, 0.5);
+ --cyber-shadow-glow-success: 0 0 20px rgba(16, 185, 129, 0.5);
+ --cyber-shadow-glow-danger: 0 0 20px rgba(239, 68, 68, 0.5);
+
+ /* ========================================
+ Transitions
+ ======================================== */
+
+ --cyber-transition-fast: 0.15s ease-in-out;
+ --cyber-transition-base: 0.3s ease-in-out;
+ --cyber-transition-slow: 0.5s ease-in-out;
+ --cyber-transition-bounce: 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ --cyber-transition-smooth: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* ========================================
+ Z-Index Layers
+ ======================================== */
+
+ --cyber-z-base: 0;
+ --cyber-z-dropdown: 1000;
+ --cyber-z-sticky: 1100;
+ --cyber-z-fixed: 1200;
+ --cyber-z-modal-backdrop: 1300;
+ --cyber-z-modal: 1400;
+ --cyber-z-popover: 1500;
+ --cyber-z-tooltip: 1600;
+}
+
+/* ========================================
+ Light Theme Override
+ ======================================== */
+
+[data-theme="light"] {
+ --cyber-bg-primary: #f8fafc;
+ --cyber-bg-secondary: #f1f5f9;
+ --cyber-bg-tertiary: #e2e8f0;
+ --cyber-surface: #ffffff;
+ --cyber-surface-light: #f8fafc;
+
+ --cyber-text-primary: #0f172a;
+ --cyber-text-secondary: #475569;
+ --cyber-text-muted: #64748b;
+ --cyber-text-inverse: #ffffff;
+
+ --cyber-glass-bg: rgba(255, 255, 255, 0.8);
+ --cyber-glass-border: rgba(0, 0, 0, 0.1);
+ --cyber-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+}
+
+/* ========================================
+ Cyberpunk Theme (High Contrast)
+ ======================================== */
+
+[data-theme="cyberpunk"] {
+ --cyber-bg-primary: #000000;
+ --cyber-bg-secondary: #0a0a0a;
+ --cyber-accent-primary: #00ffff;
+ --cyber-accent-primary-end: #ff00ff;
+ --cyber-success: #00ff00;
+ --cyber-danger: #ff0000;
+ --cyber-shadow-glow: 0 0 30px rgba(0, 255, 255, 0.8);
+}
+```
+
+### 2. Card Component Template (cards.css)
+
+```css
+/**
+ * CyberMood Card Component
+ */
+
+.cyber-card {
+ background: var(--cyber-glass-bg);
+ backdrop-filter: blur(var(--cyber-glass-blur));
+ border: 1px solid var(--cyber-glass-border);
+ border-radius: var(--cyber-radius-xl);
+ padding: var(--cyber-space-lg);
+ box-shadow: var(--cyber-glass-shadow);
+ transition: all var(--cyber-transition-base);
+ position: relative;
+ overflow: hidden;
+}
+
+.cyber-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: var(--cyber-gradient-cyber);
+ opacity: 0;
+ transition: opacity var(--cyber-transition-base);
+}
+
+.cyber-card:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--cyber-shadow-xl), var(--cyber-shadow-glow);
+ border-color: var(--cyber-accent-primary);
+}
+
+.cyber-card:hover::before {
+ opacity: 1;
+}
+
+.cyber-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--cyber-space-md);
+ padding-bottom: var(--cyber-space-md);
+ border-bottom: 1px solid var(--cyber-glass-border);
+}
+
+.cyber-card-title {
+ font-family: var(--cyber-font-display);
+ font-size: var(--cyber-font-size-xl);
+ font-weight: var(--cyber-font-weight-semibold);
+ color: var(--cyber-text-primary);
+ display: flex;
+ align-items: center;
+ gap: var(--cyber-space-sm);
+ margin: 0;
+}
+
+.cyber-card-icon {
+ font-size: var(--cyber-font-size-2xl);
+ filter: drop-shadow(0 0 10px currentColor);
+}
+
+.cyber-card-body {
+ color: var(--cyber-text-secondary);
+ line-height: 1.6;
+}
+
+.cyber-card-footer {
+ margin-top: var(--cyber-space-lg);
+ padding-top: var(--cyber-space-md);
+ border-top: 1px solid var(--cyber-glass-border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+/* Card Variants */
+.cyber-card--success {
+ border-color: var(--cyber-success);
+}
+
+.cyber-card--success:hover {
+ box-shadow: var(--cyber-shadow-xl), var(--cyber-shadow-glow-success);
+}
+
+.cyber-card--danger {
+ border-color: var(--cyber-danger);
+}
+
+.cyber-card--danger:hover {
+ box-shadow: var(--cyber-shadow-xl), var(--cyber-shadow-glow-danger);
+}
+
+.cyber-card--compact {
+ padding: var(--cyber-space-md);
+}
+
+.cyber-card--flat {
+ background: var(--cyber-surface);
+ backdrop-filter: none;
+}
+```
+
+### 3. JavaScript Theme Controller (cybermood.js)
+
+```javascript
+/**
+ * CyberMood Theme Controller
+ * Version: 1.0.0
+ */
+
+'use strict';
+
+var CyberMoodTheme = {
+ version: '1.0.0',
+ currentTheme: 'dark',
+ currentLang: 'en',
+ translations: {},
+
+ /**
+ * Initialize theme system
+ */
+ init: function() {
+ console.log('π¨ CyberMood Theme System v' + this.version);
+
+ // Load saved theme preference
+ var savedTheme = this.getSavedTheme();
+ if (savedTheme) {
+ this.apply(savedTheme);
+ }
+
+ // Load saved language
+ var savedLang = this.getSavedLang();
+ if (savedLang) {
+ this.setLanguage(savedLang);
+ }
+
+ // Add theme toggle listener
+ this.attachThemeToggle();
+
+ // Initialize animations
+ this.initAnimations();
+
+ return this;
+ },
+
+ /**
+ * Apply theme
+ * @param {string} theme - Theme name: 'dark', 'light', 'cyberpunk'
+ */
+ apply: function(theme) {
+ document.documentElement.setAttribute('data-theme', theme);
+ this.currentTheme = theme;
+ this.saveTheme(theme);
+
+ // Trigger theme change event
+ var event = new CustomEvent('themechange', { detail: { theme: theme } });
+ document.dispatchEvent(event);
+
+ console.log('β
Theme applied:', theme);
+ },
+
+ /**
+ * Toggle between dark and light themes
+ */
+ toggle: function() {
+ var newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
+ this.apply(newTheme);
+ },
+
+ /**
+ * Create themed card component
+ * @param {object} options - Card options
+ * @returns {Element} Card element
+ */
+ createCard: function(options) {
+ var opts = options || {};
+ var variant = opts.variant || '';
+
+ var card = E('div', {
+ 'class': 'cyber-card' + (variant ? ' cyber-card--' + variant : '')
+ });
+
+ // Header
+ if (opts.title || opts.icon) {
+ var header = E('div', { 'class': 'cyber-card-header' });
+
+ var title = E('h3', { 'class': 'cyber-card-title' }, [
+ opts.icon ? E('span', { 'class': 'cyber-card-icon' }, opts.icon) : null,
+ opts.title || ''
+ ]);
+
+ header.appendChild(title);
+
+ if (opts.actions) {
+ header.appendChild(opts.actions);
+ }
+
+ card.appendChild(header);
+ }
+
+ // Body
+ if (opts.content) {
+ var body = E('div', { 'class': 'cyber-card-body' }, [opts.content]);
+ card.appendChild(body);
+ }
+
+ // Footer
+ if (opts.footer) {
+ var footer = E('div', { 'class': 'cyber-card-footer' }, [opts.footer]);
+ card.appendChild(footer);
+ }
+
+ return card;
+ },
+
+ /**
+ * Create button with theme
+ * @param {object} options - Button options
+ * @returns {Element} Button element
+ */
+ createButton: function(options) {
+ var opts = options || {};
+ var classes = ['cyber-btn'];
+
+ if (opts.variant) classes.push('cyber-btn--' + opts.variant);
+ if (opts.size) classes.push('cyber-btn--' + opts.size);
+ if (opts.block) classes.push('cyber-btn--block');
+
+ return E('button', {
+ 'class': classes.join(' '),
+ 'click': opts.onClick || null,
+ 'disabled': opts.disabled || false
+ }, [
+ opts.icon ? E('span', { 'class': 'cyber-btn-icon' }, opts.icon) : null,
+ opts.label || ''
+ ]);
+ },
+
+ /**
+ * Create badge component
+ * @param {string} text - Badge text
+ * @param {string} variant - Badge variant
+ * @returns {Element} Badge element
+ */
+ createBadge: function(text, variant) {
+ return E('span', {
+ 'class': 'cyber-badge cyber-badge--' + (variant || 'default')
+ }, text);
+ },
+
+ /**
+ * Set language
+ * @param {string} lang - Language code (en, fr, de, es)
+ */
+ setLanguage: function(lang) {
+ var self = this;
+
+ // Load translation file
+ return fetch(L.resource('cybermood/i18n/' + lang + '.json'))
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(translations) {
+ self.translations = translations;
+ self.currentLang = lang;
+ self.saveLang(lang);
+
+ // Trigger language change event
+ var event = new CustomEvent('langchange', { detail: { lang: lang } });
+ document.dispatchEvent(event);
+
+ console.log('β
Language set:', lang);
+ })
+ .catch(function(error) {
+ console.error('β Failed to load language:', lang, error);
+ });
+ },
+
+ /**
+ * Translate string
+ * @param {string} key - Translation key
+ * @param {object} params - Parameters for interpolation
+ * @returns {string} Translated string
+ */
+ t: function(key, params) {
+ var translation = this.translations[key] || key;
+
+ // Simple parameter interpolation
+ if (params) {
+ Object.keys(params).forEach(function(param) {
+ translation = translation.replace('{' + param + '}', params[param]);
+ });
+ }
+
+ return translation;
+ },
+
+ /**
+ * Initialize animations
+ */
+ initAnimations: function() {
+ // Add entrance animations to elements
+ var elements = document.querySelectorAll('.cyber-animate');
+ var observer = new IntersectionObserver(function(entries) {
+ entries.forEach(function(entry) {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('cyber-animate--visible');
+ }
+ });
+ }, { threshold: 0.1 });
+
+ elements.forEach(function(el) {
+ observer.observe(el);
+ });
+ },
+
+ /**
+ * Attach theme toggle button
+ */
+ attachThemeToggle: function() {
+ var self = this;
+ var toggle = document.querySelector('[data-theme-toggle]');
+
+ if (toggle) {
+ toggle.addEventListener('click', function() {
+ self.toggle();
+ });
+ }
+ },
+
+ /**
+ * Save theme to localStorage
+ */
+ saveTheme: function(theme) {
+ try {
+ localStorage.setItem('cybermood-theme', theme);
+ } catch (e) {}
+ },
+
+ /**
+ * Get saved theme from localStorage
+ */
+ getSavedTheme: function() {
+ try {
+ return localStorage.getItem('cybermood-theme');
+ } catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Save language to localStorage
+ */
+ saveLang: function(lang) {
+ try {
+ localStorage.setItem('cybermood-lang', lang);
+ } catch (e) {}
+ },
+
+ /**
+ * Get saved language from localStorage
+ */
+ getSavedLang: function() {
+ try {
+ return localStorage.getItem('cybermood-lang') || 'en';
+ } catch (e) {
+ return 'en';
+ }
+ }
+};
+
+// Auto-initialize on load
+if (typeof window !== 'undefined') {
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ CyberMoodTheme.init();
+ });
+ } else {
+ CyberMoodTheme.init();
+ }
+}
+
+return CyberMoodTheme;
+```
+
+## π Multi-Language System
+
+### Translation File Structure
+
+```json
+// en.json (English)
+{
+ "common": {
+ "loading": "Loading...",
+ "error": "Error",
+ "success": "Success",
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "edit": "Edit",
+ "close": "Close",
+ "confirm": "Confirm"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "welcome": "Welcome to {name}",
+ "modules": "Modules",
+ "active_modules": "Active Modules",
+ "system_status": "System Status",
+ "health_score": "Health Score"
+ },
+ "modules": {
+ "network_modes": "Network Modes",
+ "system_hub": "System Hub",
+ "client_guardian": "Client Guardian",
+ "bandwidth_manager": "Bandwidth Manager"
+ }
+}
+```
+
+```json
+// fr.json (French)
+{
+ "common": {
+ "loading": "Chargement...",
+ "error": "Erreur",
+ "success": "Succès",
+ "cancel": "Annuler",
+ "save": "Enregistrer",
+ "delete": "Supprimer",
+ "edit": "Modifier",
+ "close": "Fermer",
+ "confirm": "Confirmer"
+ },
+ "dashboard": {
+ "title": "Tableau de bord",
+ "welcome": "Bienvenue dans {name}",
+ "modules": "Modules",
+ "active_modules": "Modules actifs",
+ "system_status": "Γtat du systΓ¨me",
+ "health_score": "Score de santΓ©"
+ },
+ "modules": {
+ "network_modes": "Modes rΓ©seau",
+ "system_hub": "Hub système",
+ "client_guardian": "Gardien client",
+ "bandwidth_manager": "Gestionnaire de bande passante"
+ }
+}
+```
+
+## π Implementation Plan
+
+### Phase 1: Foundation (Week 1)
+
+**Tasks:**
+1. Create `luci-theme-cybermood` package
+2. Implement CSS variable system
+3. Create core components (cards, buttons, forms)
+4. Set up build/minification process
+
+**Deliverables:**
+- `/luci-theme-cybermood/htdocs/luci-static/resources/cybermood/`
+- `cybermood.css` (main stylesheet)
+- `cybermood.js` (theme controller)
+- `variables.css` (design tokens)
+
+### Phase 2: Component Library (Week 2)
+
+**Tasks:**
+1. Build all reusable components
+2. Create component documentation
+3. Implement dark/light/cyberpunk themes
+4. Add animations and transitions
+
+**Components:**
+- Cards, Buttons, Forms, Tables
+- Modals, Tooltips, Badges, Alerts
+- Charts, Gauges, Progress bars
+- Navigation elements
+
+### Phase 3: Module Migration (Week 3)
+
+**Tasks:**
+1. Update `luci-app-secubox` to use global theme
+2. Update `luci-app-system-hub`
+3. Update `luci-app-network-modes`
+4. Update remaining modules
+
+**Migration Pattern:**
+```javascript
+// Before:
+'require secubox/theme as Theme';
+
+// After:
+'require cybermood/theme as Theme';
+```
+
+### Phase 4: Multi-Language (Week 4)
+
+**Tasks:**
+1. Create translation files (en, fr, de, es)
+2. Implement language switcher UI
+3. Update all modules with translation keys
+4. Add RTL support for Arabic
+
+**Implementation:**
+```javascript
+// Usage in modules:
+Theme.t('dashboard.welcome', { name: 'SecuBox' });
+// Output: "Welcome to SecuBox" (en) or "Bienvenue dans SecuBox" (fr)
+```
+
+### Phase 5: Testing & Refinement (Week 5)
+
+**Tasks:**
+1. Cross-browser testing
+2. Mobile responsiveness testing
+3. Performance optimization
+4. Accessibility audit (WCAG 2.1)
+5. User acceptance testing
+
+## π Usage Examples
+
+### Example 1: Dashboard with Global Theme
+
+```javascript
+'use strict';
+'require view';
+'require cybermood/theme as Theme';
+
+return view.extend({
+ render: function() {
+ return E('div', { 'class': 'cyber-container' }, [
+ // Apply theme CSS
+ E('link', {
+ 'rel': 'stylesheet',
+ 'href': L.resource('cybermood/cybermood.css')
+ }),
+
+ // Theme toggle button
+ E('button', {
+ 'data-theme-toggle': '',
+ 'class': 'cyber-btn cyber-btn--icon',
+ 'title': Theme.t('common.toggle_theme')
+ }, 'π'),
+
+ // Language selector
+ E('select', {
+ 'class': 'cyber-select',
+ 'change': function(ev) {
+ Theme.setLanguage(ev.target.value);
+ }
+ }, [
+ E('option', { 'value': 'en' }, 'English'),
+ E('option', { 'value': 'fr' }, 'FranΓ§ais'),
+ E('option', { 'value': 'de' }, 'Deutsch'),
+ E('option', { 'value': 'es' }, 'EspaΓ±ol')
+ ]),
+
+ // Header
+ E('h1', { 'class': 'cyber-title' },
+ Theme.t('dashboard.title')),
+
+ // Stats cards
+ E('div', { 'class': 'cyber-grid cyber-grid--3' }, [
+ Theme.createCard({
+ title: Theme.t('dashboard.active_modules'),
+ icon: 'π¦',
+ content: E('div', { 'class': 'cyber-stat' }, [
+ E('div', { 'class': 'cyber-stat-value' }, '12'),
+ E('div', { 'class': 'cyber-stat-label' },
+ Theme.t('modules.total'))
+ ]),
+ variant: 'success'
+ }),
+
+ Theme.createCard({
+ title: Theme.t('dashboard.health_score'),
+ icon: 'β€οΈ',
+ content: E('div', { 'class': 'cyber-stat' }, [
+ E('div', { 'class': 'cyber-stat-value' }, '98%'),
+ Theme.createBadge('Excellent', 'success')
+ ])
+ }),
+
+ Theme.createCard({
+ title: Theme.t('dashboard.system_status'),
+ icon: 'β‘',
+ content: E('div', { 'class': 'cyber-stat' }, [
+ E('div', { 'class': 'cyber-stat-value' },
+ Theme.t('common.online')),
+ Theme.createBadge('Active', 'info')
+ ])
+ })
+ ])
+ ]);
+ }
+});
+```
+
+### Example 2: Form with Theme
+
+```javascript
+renderForm: function() {
+ return Theme.createCard({
+ title: Theme.t('settings.configuration'),
+ icon: 'βοΈ',
+ content: E('form', { 'class': 'cyber-form' }, [
+ E('div', { 'class': 'cyber-form-group' }, [
+ E('label', { 'class': 'cyber-label' },
+ Theme.t('settings.hostname')),
+ E('input', {
+ 'type': 'text',
+ 'class': 'cyber-input',
+ 'placeholder': Theme.t('settings.enter_hostname')
+ })
+ ]),
+
+ E('div', { 'class': 'cyber-form-group' }, [
+ E('label', { 'class': 'cyber-label' },
+ Theme.t('settings.enable_feature')),
+ E('label', { 'class': 'cyber-switch' }, [
+ E('input', { 'type': 'checkbox' }),
+ E('span', { 'class': 'cyber-switch-slider' })
+ ])
+ ])
+ ]),
+ footer: E('div', { 'class': 'cyber-form-actions' }, [
+ Theme.createButton({
+ label: Theme.t('common.cancel'),
+ variant: 'secondary'
+ }),
+ Theme.createButton({
+ label: Theme.t('common.save'),
+ variant: 'primary'
+ })
+ ])
+ });
+}
+```
+
+## π― Success Criteria
+
+1. **Unified Look**: All modules use consistent design
+2. **Performance**: < 50KB total CSS bundle (minified)
+3. **Responsive**: Works on mobile (320px) to 4K (3840px)
+4. **Accessible**: WCAG 2.1 AA compliant
+5. **Multi-language**: 4+ languages supported
+6. **Theme Switching**: < 100ms theme change
+7. **Browser Support**: Chrome 90+, Firefox 88+, Safari 14+
+
+## π Migration Checklist
+
+### Per Module:
+
+- [ ] Remove module-specific CSS files
+- [ ] Import global `cybermood.css`
+- [ ] Update components to use cyber-* classes
+- [ ] Replace hardcoded strings with `Theme.t()` calls
+- [ ] Test dark/light/cyberpunk themes
+- [ ] Test all supported languages
+- [ ] Verify responsive breakpoints
+- [ ] Run accessibility audit
+- [ ] Update documentation
+
+### Global:
+
+- [ ] Create luci-theme-cybermood package
+- [ ] Implement all core components
+- [ ] Create translation files
+- [ ] Set up build process
+- [ ] Create migration guide
+- [ ] Update all 15 modules
+- [ ] Performance testing
+- [ ] User acceptance testing
+- [ ] Production deployment
+
+## π References
+
+- **Design Inspiration**: SecuBox Website (https://secubox.cybermood.eu)
+- **LuCI Theme System**: `/feeds/luci/themes/`
+- **CSS Variables Spec**: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
+- **i18n Best Practices**: https://www.w3.org/International/
+
+## π Next Steps
+
+1. **Review & Approve**: Get stakeholder approval on design direction
+2. **Prototype**: Create visual mockups in Figma/similar
+3. **Build**: Implement Phase 1 (Foundation)
+4. **Test**: Internal QA on test router
+5. **Deploy**: Roll out to production
+
+---
+
+**Status**: π Planning
+**Priority**: π₯ High
+**Effort**: 4-5 weeks
+**Impact**: π― All modules unified
diff --git a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js
index 1f58f068..372a6204 100644
--- a/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js
+++ b/luci-app-network-modes/htdocs/luci-static/resources/network-modes/api.js
@@ -95,6 +95,18 @@ var callRouterConfig = rpc.declare({
expect: { }
});
+var callTravelConfig = rpc.declare({
+ object: 'luci.network-modes',
+ method: 'travel_config',
+ expect: { }
+});
+
+var callTravelScan = rpc.declare({
+ object: 'luci.network-modes',
+ method: 'travel_scan_networks',
+ expect: { networks: [] }
+});
+
var callUpdateSettings = rpc.declare({
object: 'luci.network-modes',
method: 'update_settings'
@@ -237,6 +249,18 @@ return baseclass.extend({
'Signal amplification'
]
},
+ travel: {
+ id: 'travel',
+ name: 'Travel Router',
+ icon: 'βοΈ',
+ description: 'Portable router for hotels and conferences. Clones WAN MAC and creates a secure personal hotspot.',
+ features: [
+ 'Hotel WiFi client + scan wizard',
+ 'MAC clone to bypass captive portals',
+ 'Private WPA3 hotspot for your devices',
+ 'Isolated NAT + DHCP sandbox'
+ ]
+ },
sniffer: {
id: 'sniffer',
name: 'Sniffer Mode',
@@ -275,6 +299,8 @@ return baseclass.extend({
getApConfig: callApConfig,
getRelayConfig: callRelayConfig,
getRouterConfig: callRouterConfig,
+ getTravelConfig: callTravelConfig,
+ scanTravelNetworks: callTravelScan,
updateSettings: function(mode, settings) {
var payload = Object.assign({}, settings || {}, { mode: mode });
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 da438222..d2f618c1 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
@@ -132,6 +132,12 @@
border-color: rgba(16, 185, 129, 0.3);
}
+.nm-mode-badge.travel {
+ background: rgba(251, 191, 36, 0.15);
+ color: var(--nm-accent-amber);
+ border-color: rgba(251, 191, 36, 0.35);
+}
+
.nm-mode-badge.router {
background: rgba(249, 115, 22, 0.15);
color: var(--nm-router-color);
@@ -200,6 +206,7 @@
.nm-mode-card.accesspoint { --mode-color: var(--nm-ap-color); }
.nm-mode-card.relay { --mode-color: var(--nm-relay-color); }
.nm-mode-card.router { --mode-color: var(--nm-router-color); }
+.nm-mode-card.travel { --mode-color: var(--nm-accent-amber); }
.nm-mode-card:hover {
border-color: var(--mode-color);
@@ -331,6 +338,12 @@
margin-bottom: 20px;
}
+.nm-form-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px;
+}
+
.nm-form-label {
display: block;
font-size: 13px;
@@ -387,6 +400,48 @@
font-size: 13px;
}
+.nm-scan-results {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.nm-scan-card {
+ border: 1px solid var(--nm-border);
+ border-radius: var(--nm-radius);
+ background: var(--nm-bg-tertiary);
+ padding: 12px;
+ text-align: left;
+ cursor: pointer;
+ transition: border-color 0.2s, transform 0.2s;
+}
+
+.nm-scan-card:hover {
+ border-color: var(--nm-accent-orange);
+ transform: translateY(-2px);
+}
+
+.nm-scan-ssid {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.nm-scan-meta {
+ display: flex;
+ gap: 8px;
+ font-size: 11px;
+ color: var(--nm-text-muted);
+}
+
+.nm-empty {
+ text-align: center;
+ padding: 16px;
+ color: var(--nm-text-muted);
+ border: 1px dashed var(--nm-border);
+ border-radius: var(--nm-radius);
+}
+
/* Toggle Switch */
.nm-toggle {
display: flex;
diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js
index d2becb1a..9ce4a7c2 100644
--- a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js
+++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js
@@ -54,10 +54,11 @@ return view.extend({
var currentMode = status.current_mode || 'router';
var modeInfos = {
- sniffer: api.getModeInfo('sniffer'),
+ router: api.getModeInfo('router'),
accesspoint: api.getModeInfo('accesspoint'),
relay: api.getModeInfo('relay'),
- router: api.getModeInfo('router')
+ travel: api.getModeInfo('travel'),
+ sniffer: api.getModeInfo('sniffer')
};
var currentModeInfo = modeInfos[currentMode];
@@ -127,7 +128,7 @@ return view.extend({
E('th', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'π Bridge'),
E('th', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'π‘ Access Point'),
E('th', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'π Repeater'),
- E('th', {}, 'βοΈ Travel Router')
+ E('th', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'βοΈ Travel Router')
])
]),
E('tbody', {}, [
@@ -137,7 +138,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'L2 Forwarding'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'WiFi Hotspot'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'WiFi Extender'),
- E('td', {}, 'Portable WiFi')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'Hotel / Travel kit')
]),
E('tr', {}, [
E('td', { 'class': 'feature-label' }, 'WAN Ports'),
@@ -145,7 +146,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'All bridged'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, '1 uplink'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'WiFi'),
- E('td', {}, 'WiFi/Ethernet')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'WiFi or USB')
]),
E('tr', {}, [
E('td', { 'class': 'feature-label' }, 'LAN Ports'),
@@ -153,7 +154,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'All ports'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'All ports'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'All ports'),
- E('td', {}, 'All ports')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'All ports')
]),
E('tr', {}, [
E('td', { 'class': 'feature-label' }, 'WiFi Role'),
@@ -161,7 +162,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'Optional AP'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'AP only'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'Client + AP'),
- E('td', {}, 'Client + AP')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'Client + AP')
]),
E('tr', {}, [
E('td', { 'class': 'feature-label' }, 'DHCP Server'),
@@ -169,7 +170,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'No'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'No'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'Yes'),
- E('td', {}, 'Yes')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'Yes')
]),
E('tr', {}, [
E('td', { 'class': 'feature-label' }, 'NAT'),
@@ -177,7 +178,7 @@ return view.extend({
E('td', { 'class': currentMode === 'bridge' ? 'active-mode' : '' }, 'Disabled'),
E('td', { 'class': currentMode === 'accesspoint' ? 'active-mode' : '' }, 'Disabled'),
E('td', { 'class': currentMode === 'relay' ? 'active-mode' : '' }, 'Enabled'),
- E('td', {}, 'Enabled')
+ E('td', { 'class': currentMode === 'travel' ? 'active-mode' : '' }, 'Enabled')
])
])
])
diff --git a/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js
new file mode 100644
index 00000000..2fe41820
--- /dev/null
+++ b/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js
@@ -0,0 +1,275 @@
+'use strict';
+'require view';
+'require dom';
+'require ui';
+'require network-modes.api as api';
+'require network-modes.helpers as helpers';
+'require secubox/help as Help';
+
+return view.extend({
+ title: _('Travel Router Mode'),
+
+ load: function() {
+ return api.getTravelConfig();
+ },
+
+ render: function(data) {
+ var config = data || {};
+ var client = config.client || {};
+ var hotspot = config.hotspot || {};
+ var lan = config.lan || {};
+ var interfaces = config.available_interfaces || ['wlan0', 'wlan1'];
+ var radios = config.available_radios || ['radio0', 'radio1'];
+
+ var view = E('div', { 'class': 'network-modes-dashboard' }, [
+ E('div', { 'class': 'nm-header' }, [
+ E('div', { 'class': 'nm-logo' }, [
+ E('div', { 'class': 'nm-logo-icon', 'style': 'background: linear-gradient(135deg,#fbbf24,#f97316)' }, 'βοΈ'),
+ E('div', { 'class': 'nm-logo-text' }, ['Travel ', E('span', { 'style': 'background: linear-gradient(135deg,#f97316,#fb923c); -webkit-background-clip:text; -webkit-text-fill-color:transparent;' }, 'Router')])
+ ]),
+ Help.createHelpButton('network-modes', 'header', {
+ icon: 'π',
+ label: _('Travel help'),
+ modal: true
+ })
+ ]),
+
+ E('div', { 'class': 'nm-alert nm-alert-info' }, [
+ E('span', { 'class': 'nm-alert-icon' }, 'π'),
+ E('div', {}, [
+ E('div', { 'class': 'nm-alert-title' }, _('Portable security for hotels & events')),
+ E('div', { 'class': 'nm-alert-text' },
+ _('Connect the router as a WiFi client, clone the WAN MAC if needed, and broadcast your own encrypted hotspot for trusted devices.'))
+ ])
+ ]),
+
+ // Client WiFi
+ E('div', { 'class': 'nm-card' }, [
+ E('div', { 'class': 'nm-card-header' }, [
+ E('div', { 'class': 'nm-card-title' }, [
+ E('span', { 'class': 'nm-card-title-icon' }, 'π‘'),
+ _('Client WiFi Uplink')
+ ]),
+ E('div', { 'class': 'nm-card-badge' }, client.ssid ? _('Connected to ') + client.ssid : _('No uplink configured'))
+ ]),
+ E('div', { 'class': 'nm-card-body' }, [
+ E('div', { 'class': 'nm-form-grid' }, [
+ this.renderSelectField(_('Client interface'), 'travel-client-iface', interfaces, client.interface || 'wlan1'),
+ this.renderSelectField(_('Client radio'), 'travel-client-radio', radios, client.radio || 'radio1'),
+ this.renderSelectField(_('Encryption'), 'travel-encryption', [
+ 'sae-mixed', 'sae', 'psk2', 'psk-mixed', 'none'
+ ], client.encryption || 'sae-mixed')
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('SSID / BSSID')),
+ E('input', { 'class': 'nm-input', 'id': 'travel-client-ssid', 'value': client.ssid || '', 'placeholder': _('Hotel WiFi name') }),
+ E('div', { 'class': 'nm-form-hint' }, _('Click a scanned network below to autofill'))
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('Password / captive portal token')),
+ E('input', { 'class': 'nm-input', 'type': 'password', 'id': 'travel-client-password', 'value': client.password || '' }),
+ E('div', { 'class': 'nm-form-hint' }, _('Leave empty for open WiFi or captive portal'))
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('WAN MAC clone')),
+ E('input', {
+ 'class': 'nm-input',
+ 'id': 'travel-mac-clone',
+ 'value': client.clone_mac || '',
+ 'placeholder': 'AA:BB:CC:DD:EE:FF'
+ }),
+ E('div', { 'class': 'nm-form-hint' }, _('Copy the MAC of the laptop/room card if the hotel locks access'))
+ ]),
+ E('div', { 'class': 'nm-btn-group' }, [
+ E('button', {
+ 'class': 'nm-btn',
+ 'data-action': 'travel-scan',
+ 'type': 'button'
+ }, [
+ E('span', {}, 'π'),
+ _('Scan networks')
+ ]),
+ E('span', { 'id': 'travel-scan-status', 'class': 'nm-text-muted' }, _('Last scan: never'))
+ ]),
+ E('div', { 'class': 'nm-scan-results', 'id': 'travel-scan-results' }, [
+ E('div', { 'class': 'nm-empty' }, _('No scan results yet'))
+ ])
+ ])
+ ]),
+
+ // Hotspot
+ E('div', { 'class': 'nm-card' }, [
+ E('div', { 'class': 'nm-card-header' }, [
+ E('div', { 'class': 'nm-card-title' }, [
+ E('span', { 'class': 'nm-card-title-icon' }, 'π₯'),
+ _('Personal Hotspot')
+ ]),
+ E('div', { 'class': 'nm-card-badge' }, _('WPA3 / WPA2 mixed'))
+ ]),
+ E('div', { 'class': 'nm-card-body' }, [
+ E('div', { 'class': 'nm-form-grid' }, [
+ this.renderSelectField(_('Hotspot radio'), 'travel-hotspot-radio', radios, hotspot.radio || 'radio0')
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('Hotspot SSID')),
+ E('input', { 'class': 'nm-input', 'id': 'travel-hotspot-ssid', 'value': hotspot.ssid || 'SecuBox-Travel' })
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('Hotspot password')),
+ E('input', { 'class': 'nm-input', 'type': 'text', 'id': 'travel-hotspot-password', 'value': hotspot.password || 'TravelSafe123!' })
+ ])
+ ])
+ ]),
+
+ // LAN / DHCP
+ E('div', { 'class': 'nm-card' }, [
+ E('div', { 'class': 'nm-card-header' }, [
+ E('div', { 'class': 'nm-card-title' }, [
+ E('span', { 'class': 'nm-card-title-icon' }, 'π‘οΈ'),
+ _('LAN & DHCP Sandbox')
+ ])
+ ]),
+ E('div', { 'class': 'nm-card-body' }, [
+ E('div', { 'class': 'nm-form-grid' }, [
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('LAN Gateway IP')),
+ E('input', { 'class': 'nm-input', 'id': 'travel-lan-ip', 'value': lan.subnet || '10.77.0.1' })
+ ]),
+ E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, _('LAN Netmask')),
+ E('input', { 'class': 'nm-input', 'id': 'travel-lan-mask', 'value': lan.netmask || '255.255.255.0' })
+ ])
+ ]),
+ E('div', { 'class': 'nm-form-hint' }, _('Each trip gets its own private /24 network to avoid overlapping hotel ranges.'))
+ ])
+ ]),
+
+ // Actions
+ E('div', { 'class': 'nm-btn-group' }, [
+ E('button', {
+ 'class': 'nm-btn nm-btn-primary',
+ 'type': 'button',
+ 'data-action': 'travel-save'
+ }, [
+ E('span', {}, 'πΎ'),
+ _('Save travel settings')
+ ]),
+ E('button', {
+ 'class': 'nm-btn',
+ 'type': 'button',
+ 'data-action': 'travel-preview'
+ }, [
+ E('span', {}, 'π'),
+ _('Preview configuration')
+ ])
+ ])
+ ]);
+
+ var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') });
+ document.head.appendChild(cssLink);
+
+ this.bindTravelActions(view);
+
+ return view;
+ },
+
+ renderSelectField: function(label, id, options, selected) {
+ return E('div', { 'class': 'nm-form-group' }, [
+ E('label', { 'class': 'nm-form-label' }, label),
+ E('select', { 'class': 'nm-select', 'id': id },
+ options.map(function(opt) {
+ return E('option', { 'value': opt, 'selected': opt === selected }, opt);
+ })
+ )
+ ]);
+ },
+
+ bindTravelActions: function(container) {
+ var scanBtn = container.querySelector('[data-action="travel-scan"]');
+ if (scanBtn)
+ scanBtn.addEventListener('click', ui.createHandlerFn(this, 'scanNetworks', container));
+
+ var saveBtn = container.querySelector('[data-action="travel-save"]');
+ if (saveBtn)
+ saveBtn.addEventListener('click', ui.createHandlerFn(this, 'saveTravelSettings', container));
+
+ var previewBtn = container.querySelector('[data-action="travel-preview"]');
+ if (previewBtn)
+ previewBtn.addEventListener('click', ui.createHandlerFn(helpers, helpers.showGeneratedConfig, 'travel'));
+ },
+
+ scanNetworks: function(container) {
+ var statusEl = container.querySelector('#travel-scan-status');
+ if (statusEl)
+ statusEl.textContent = _('Scanning...');
+
+ return api.scanTravelNetworks().then(L.bind(function(result) {
+ if (statusEl)
+ statusEl.textContent = _('Last scan: ') + new Date().toLocaleTimeString();
+ this.populateScanResults(container, (result && result.networks) || []);
+ }, this)).catch(function(err) {
+ if (statusEl)
+ statusEl.textContent = _('Scan failed');
+ ui.addNotification(null, E('p', {}, err.message || err), 'error');
+ });
+ },
+
+ populateScanResults: function(container, networks) {
+ var list = container.querySelector('#travel-scan-results');
+ if (!list)
+ return;
+ list.innerHTML = '';
+
+ if (!networks.length) {
+ list.appendChild(E('div', { 'class': 'nm-empty' }, _('No networks detected')));
+ return;
+ }
+
+ networks.slice(0, 8).forEach(L.bind(function(net) {
+ var card = E('button', {
+ 'class': 'nm-scan-card',
+ 'type': 'button'
+ }, [
+ E('div', { 'class': 'nm-scan-ssid' }, net.ssid || _('Hidden SSID')),
+ E('div', { 'class': 'nm-scan-meta' }, [
+ E('span', {}, net.channel ? _('Ch. ') + net.channel : ''),
+ E('span', {}, net.signal || ''),
+ E('span', {}, net.encryption || '')
+ ])
+ ]);
+
+ card.addEventListener('click', ui.createHandlerFn(this, 'selectScannedNetwork', container, net));
+ list.appendChild(card);
+ }, this));
+ },
+
+ selectScannedNetwork: function(container, network) {
+ var ssidInput = container.querySelector('#travel-client-ssid');
+ if (ssidInput)
+ ssidInput.value = network.ssid || '';
+
+ if (network.encryption) {
+ var encSelect = container.querySelector('#travel-encryption');
+ if (encSelect && Array.prototype.some.call(encSelect.options, function(opt) { return opt.value === network.encryption; }))
+ encSelect.value = network.encryption;
+ }
+ },
+
+ saveTravelSettings: function(container) {
+ var payload = {
+ client_interface: container.querySelector('#travel-client-iface') ? container.querySelector('#travel-client-iface').value : '',
+ client_radio: container.querySelector('#travel-client-radio') ? container.querySelector('#travel-client-radio').value : '',
+ hotspot_radio: container.querySelector('#travel-hotspot-radio') ? container.querySelector('#travel-hotspot-radio').value : '',
+ ssid: container.querySelector('#travel-client-ssid') ? container.querySelector('#travel-client-ssid').value : '',
+ password: container.querySelector('#travel-client-password') ? container.querySelector('#travel-client-password').value : '',
+ encryption: container.querySelector('#travel-encryption') ? container.querySelector('#travel-encryption').value : 'sae-mixed',
+ hotspot_ssid: container.querySelector('#travel-hotspot-ssid') ? container.querySelector('#travel-hotspot-ssid').value : '',
+ hotspot_password: container.querySelector('#travel-hotspot-password') ? container.querySelector('#travel-hotspot-password').value : '',
+ clone_mac: container.querySelector('#travel-mac-clone') ? container.querySelector('#travel-mac-clone').value : '',
+ lan_subnet: container.querySelector('#travel-lan-ip') ? container.querySelector('#travel-lan-ip').value : '',
+ lan_netmask: container.querySelector('#travel-lan-mask') ? container.querySelector('#travel-lan-mask').value : ''
+ };
+
+ return helpers.persistSettings('travel', payload);
+ }
+});
diff --git a/luci-app-network-modes/root/etc/config/network-modes b/luci-app-network-modes/root/etc/config/network-modes
index 22c12ae1..c162c736 100644
--- a/luci-app-network-modes/root/etc/config/network-modes
+++ b/luci-app-network-modes/root/etc/config/network-modes
@@ -49,3 +49,20 @@ config mode 'router'
option https_frontend '0'
option frontend_type 'nginx'
list frontend_domains ''
+
+config mode 'travel'
+ option name 'Travel Router'
+ option description 'Portable router with WiFi client uplink and personal hotspot'
+ option enabled '0'
+ option client_radio 'radio1'
+ option client_interface 'wlan1'
+ option hotspot_radio 'radio0'
+ option hotspot_interface 'wlan0'
+ option ssid ''
+ option password ''
+ option encryption 'sae-mixed'
+ option hotspot_ssid 'SecuBox-Travel'
+ option hotspot_password 'TravelSafe123!'
+ option clone_mac ''
+ option lan_subnet '10.77.0.1'
+ option lan_netmask '255.255.255.0'
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 5dfa19c1..7bede045 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
@@ -131,6 +131,19 @@ get_modes() {
json_add_boolean "https_frontend" "$(uci -q get network-modes.router.https_frontend || echo 0)"
json_add_string "frontend_type" "$(uci -q get network-modes.router.frontend_type)"
json_close_object
+
+ # Travel mode
+ json_add_object
+ json_add_string "id" "travel"
+ json_add_string "name" "$(uci -q get network-modes.travel.name || echo 'Travel Router')"
+ json_add_string "description" "$(uci -q get network-modes.travel.description || echo 'Portable router with WiFi uplink and personal hotspot')"
+ json_add_string "icon" "βοΈ"
+ json_add_boolean "active" "$([ "$current_mode" = "travel" ] && echo 1 || echo 0)"
+ json_add_string "client_interface" "$(uci -q get network-modes.travel.client_interface || echo 'wlan1')"
+ json_add_string "hotspot_interface" "$(uci -q get network-modes.travel.hotspot_interface || echo 'wlan0')"
+ json_add_boolean "mac_clone_enabled" "$([ -n "$(uci -q get network-modes.travel.clone_mac)" ] && echo 1 || echo 0)"
+ json_add_string "hotspot_ssid" "$(uci -q get network-modes.travel.hotspot_ssid || echo 'SecuBox-Travel')"
+ json_close_object
json_close_array
json_dump
@@ -360,6 +373,135 @@ get_router_config() {
json_dump
}
+# Get travel router configuration
+get_travel_config() {
+ json_init
+
+ local client_iface=$(uci -q get network-modes.travel.client_interface || echo "wlan1")
+ local hotspot_iface=$(uci -q get network-modes.travel.hotspot_interface || echo "wlan0")
+ local client_radio=$(uci -q get network-modes.travel.client_radio || echo "radio1")
+ local hotspot_radio=$(uci -q get network-modes.travel.hotspot_radio || echo "radio0")
+ local clone_mac=$(uci -q get network-modes.travel.clone_mac || echo "")
+
+ json_add_string "mode" "travel"
+ json_add_string "name" "$(uci -q get network-modes.travel.name || echo 'Travel Router')"
+ json_add_string "description" "$(uci -q get network-modes.travel.description || echo 'Portable WiFi router for hotels and coworking spaces')"
+
+ json_add_object "client"
+ json_add_string "interface" "$client_iface"
+ json_add_string "radio" "$client_radio"
+ json_add_string "ssid" "$(uci -q get network-modes.travel.ssid || echo '')"
+ json_add_string "encryption" "$(uci -q get network-modes.travel.encryption || echo 'sae-mixed')"
+ json_add_string "password" "$(uci -q get network-modes.travel.password || echo '')"
+ json_add_string "clone_mac" "$clone_mac"
+ json_add_boolean "mac_clone_enabled" "$([ -n "$clone_mac" ] && echo 1 || echo 0)"
+ json_close_object
+
+ json_add_object "hotspot"
+ json_add_string "interface" "$hotspot_iface"
+ json_add_string "radio" "$hotspot_radio"
+ json_add_string "ssid" "$(uci -q get network-modes.travel.hotspot_ssid || echo 'SecuBox-Travel')"
+ json_add_string "password" "$(uci -q get network-modes.travel.hotspot_password || echo 'TravelSafe123!')"
+ json_add_string "band" "$(uci -q get network-modes.travel.hotspot_band || echo 'dual')"
+ json_close_object
+
+ json_add_object "lan"
+ json_add_string "subnet" "$(uci -q get network-modes.travel.lan_subnet || echo '10.77.0.1')"
+ json_add_string "netmask" "$(uci -q get network-modes.travel.lan_netmask || echo '255.255.255.0')"
+ json_close_object
+
+ json_add_array "available_interfaces"
+ for iface in $(ls /sys/class/net/ 2>/dev/null | grep -E '^wl|^wlan' || true); do
+ json_add_string "" "$iface"
+ done
+ json_close_array
+
+ json_add_array "available_radios"
+ for radio in $(uci show wireless 2>/dev/null | grep "=wifi-device" | cut -d'.' -f2 | cut -d'=' -f1); do
+ json_add_string "" "$radio"
+ done
+ json_close_array
+
+ json_dump
+}
+
+# Scan nearby WiFi networks for travel mode
+travel_scan_networks() {
+ json_init
+ json_add_array "networks"
+
+ local iface=$(uci -q get network-modes.travel.client_interface || echo "wlan1")
+ local scan_output
+ scan_output="$(iwinfo "$iface" scan 2>/dev/null || true)"
+
+ if [ -z "$scan_output" ]; then
+ json_close_array
+ json_dump
+ return
+ fi
+
+ local bssid=""
+ local ssid=""
+ local channel=""
+ local signal=""
+ local encryption=""
+ local quality=""
+
+ while IFS= read -r line; do
+ case "$line" in
+ Cell*)
+ if [ -n "$bssid" ]; then
+ json_add_object
+ json_add_string "ssid" "${ssid:-Unknown}"
+ json_add_string "bssid" "$bssid"
+ json_add_string "channel" "${channel:-?}"
+ json_add_string "signal" "${signal:-N/A}"
+ json_add_string "quality" "${quality:-N/A}"
+ json_add_string "encryption" "${encryption:-Unknown}"
+ json_close_object
+ fi
+ bssid=$(printf '%s\n' "$line" | awk '{print $5}')
+ ssid=""
+ channel=""
+ signal=""
+ encryption=""
+ quality=""
+ ;;
+ *"ESSID:"*)
+ ssid=$(printf '%s\n' "$line" | sed -n 's/.*ESSID: "\(.*\)".*/\1/p')
+ ;;
+ *"Channel:"*)
+ channel=$(printf '%s\n' "$line" | awk -F'Channel:' '{print $2}' | awk '{print $1}')
+ ;;
+ *"Signal:"*)
+ signal=$(printf '%s\n' "$line" | awk -F'Signal:' '{print $2}' | awk '{print $1" "$2}')
+ if echo "$line" | grep -q 'Quality'; then
+ quality=$(printf '%s\n' "$line" | sed -n 's/.*Quality: \([0-9\/]*\).*/\1/p')
+ fi
+ ;;
+ *"Encryption:"*)
+ encryption=$(printf '%s\n' "$line" | sed -n 's/.*Encryption: //p')
+ ;;
+ esac
+ done </dev/null
+ uci set network.wan=interface
+ uci set network.wan.proto='dhcp'
+ uci set network.wan.device="$client_iface"
+
+ uci set network.lan=interface
+ uci set network.lan.proto='static'
+ uci set network.lan.device='br-lan'
+ uci set network.lan.ipaddr="$lan_ip"
+ uci set network.lan.netmask="$lan_netmask"
+
+ uci set dhcp.lan=dhcp
+ uci set dhcp.lan.interface='lan'
+ uci set dhcp.lan.start='60'
+ uci set dhcp.lan.limit='80'
+ uci set dhcp.lan.leasetime='6h'
+
+ uci set firewall.@zone[0]=zone
+ uci set firewall.@zone[0].name='lan'
+ uci set firewall.@zone[0].input='ACCEPT'
+ uci set firewall.@zone[0].output='ACCEPT'
+ uci set firewall.@zone[0].forward='ACCEPT'
+
+ uci set firewall.@zone[1]=zone
+ uci set firewall.@zone[1].name='wan'
+ uci set firewall.@zone[1].network='wan'
+ uci set firewall.@zone[1].input='REJECT'
+ uci set firewall.@zone[1].output='ACCEPT'
+ uci set firewall.@zone[1].forward='REJECT'
+ uci set firewall.@zone[1].masq='1'
+ uci set firewall.@zone[1].mtu_fix='1'
+
+ uci delete wireless.travel_sta 2>/dev/null
+ uci set wireless.travel_sta=wifi-iface
+ uci set wireless.travel_sta.device="$client_radio"
+ uci set wireless.travel_sta.mode='sta'
+ uci set wireless.travel_sta.network='wan'
+ [ -n "$travel_ssid" ] && uci set wireless.travel_sta.ssid="$travel_ssid" || uci delete wireless.travel_sta.ssid 2>/dev/null
+ uci set wireless.travel_sta.encryption="$travel_encryption"
+ if [ -n "$travel_password" ]; then
+ uci set wireless.travel_sta.key="$travel_password"
+ else
+ uci delete wireless.travel_sta.key 2>/dev/null
+ fi
+ if [ -n "$clone_mac" ]; then
+ uci set wireless.travel_sta.macaddr="$clone_mac"
+ else
+ uci delete wireless.travel_sta.macaddr 2>/dev/null
+ fi
+
+ uci delete wireless.travel_ap 2>/dev/null
+ uci set wireless.travel_ap=wifi-iface
+ uci set wireless.travel_ap.device="$hotspot_radio"
+ uci set wireless.travel_ap.mode='ap'
+ uci set wireless.travel_ap.network='lan'
+ uci set wireless.travel_ap.ssid="$hotspot_ssid"
+ uci set wireless.travel_ap.encryption='sae-mixed'
+ uci set wireless.travel_ap.key="$hotspot_password"
+ uci set wireless.travel_ap.ieee80211w='1'
+ uci set wireless.travel_ap.hidden='0'
+ ;;
+
+ accesspoint)
# Access Point mode: Bridge, no NAT, DHCP client
# Delete WAN
uci delete network.wan 2>/dev/null
@@ -668,6 +884,31 @@ update_settings() {
[ -n "$dns_over_https" ] && uci set network-modes.router.dns_over_https="$dns_over_https"
[ -n "$letsencrypt" ] && uci set network-modes.router.letsencrypt="$letsencrypt"
;;
+ travel)
+ json_get_var client_interface client_interface
+ json_get_var client_radio client_radio
+ json_get_var hotspot_radio hotspot_radio
+ json_get_var ssid ssid
+ json_get_var password password
+ json_get_var encryption encryption
+ json_get_var hotspot_ssid hotspot_ssid
+ json_get_var hotspot_password hotspot_password
+ json_get_var clone_mac clone_mac
+ json_get_var lan_subnet lan_subnet
+ json_get_var lan_netmask lan_netmask
+
+ [ -n "$client_interface" ] && uci set network-modes.travel.client_interface="$client_interface"
+ [ -n "$client_radio" ] && uci set network-modes.travel.client_radio="$client_radio"
+ [ -n "$hotspot_radio" ] && uci set network-modes.travel.hotspot_radio="$hotspot_radio"
+ [ -n "$ssid" ] && uci set network-modes.travel.ssid="$ssid"
+ [ -n "$password" ] && uci set network-modes.travel.password="$password"
+ [ -n "$encryption" ] && uci set network-modes.travel.encryption="$encryption"
+ [ -n "$hotspot_ssid" ] && uci set network-modes.travel.hotspot_ssid="$hotspot_ssid"
+ [ -n "$hotspot_password" ] && uci set network-modes.travel.hotspot_password="$hotspot_password"
+ [ -n "$clone_mac" ] && uci set network-modes.travel.clone_mac="$clone_mac"
+ [ -n "$lan_subnet" ] && uci set network-modes.travel.lan_subnet="$lan_subnet"
+ [ -n "$lan_netmask" ] && uci set network-modes.travel.lan_netmask="$lan_netmask"
+ ;;
*)
json_add_boolean "success" 0
json_add_string "error" "Invalid mode"
@@ -1415,6 +1656,49 @@ config forwarding
option src 'lan'
option dest 'wan'"
;;
+ travel)
+ local travel_ssid=$(uci -q get network-modes.travel.ssid || echo "HotelWiFi")
+ local hotspot_ssid=$(uci -q get network-modes.travel.hotspot_ssid || echo "SecuBox-Travel")
+ local lan_ip=$(uci -q get network-modes.travel.lan_subnet || echo "10.77.0.1")
+ config="# Travel Router Mode
+# /etc/config/network
+
+config interface 'wan'
+ option proto 'dhcp'
+ option device 'wlan-sta'
+
+config interface 'lan'
+ option proto 'static'
+ option ipaddr '$lan_ip'
+ option netmask '255.255.255.0'
+ option device 'br-lan'
+
+# /etc/config/wireless
+
+config wifi-iface 'travel_sta'
+ option device 'radio1'
+ option mode 'sta'
+ option network 'wan'
+ option ssid '$travel_ssid'
+ option encryption 'sae-mixed'
+ option key 'hotel-password'
+
+config wifi-iface 'travel_ap'
+ option device 'radio0'
+ option mode 'ap'
+ option network 'lan'
+ option ssid '$hotspot_ssid'
+ option encryption 'sae-mixed'
+ option key 'TravelSafe123!'
+
+# /etc/config/firewall
+config zone
+ option name 'wan'
+ option input 'REJECT'
+ option output 'ACCEPT'
+ option forward 'REJECT'
+ option masq '1'"
+ ;;
esac
json_add_string "config" "$config"
@@ -1503,6 +1787,21 @@ get_available_modes() {
json_close_array
json_close_object
+ # Travel router mode
+ json_add_object
+ json_add_string "id" "travel"
+ json_add_string "name" "Travel Router"
+ json_add_string "description" "Portable hotspot WiFi depuis l'ethernet ou WiFi hΓ΄tel"
+ json_add_string "icon" "βοΈ"
+ json_add_boolean "current" "$([ "$current_mode" = "travel" ] && echo 1 || echo 0)"
+ json_add_array "features"
+ json_add_string "" "Client WiFi + scan SSID"
+ json_add_string "" "Clone MAC WAN"
+ json_add_string "" "Hotspot privΓ© WPA3"
+ json_add_string "" "NAT + DHCP isolΓ©"
+ json_close_array
+ json_close_object
+
# Bridge mode
json_add_object
json_add_string "id" "bridge"
@@ -1532,7 +1831,7 @@ set_mode() {
# Validate mode
case "$target_mode" in
- router|accesspoint|relay|bridge)
+ router|accesspoint|relay|bridge|sniffer|travel)
;;
*)
json_add_boolean "success" 0
@@ -1735,7 +2034,7 @@ rollback() {
# Main dispatcher
case "$1" in
list)
- echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"},"validate_pcap_filter":{"filter":"str"},"cleanup_old_pcaps":{}}'
+ echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"travel_config":{},"travel_scan_networks":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"},"validate_pcap_filter":{"filter":"str"},"cleanup_old_pcaps":{}}'
;;
call)
case "$2" in
@@ -1778,6 +2077,12 @@ case "$1" in
router_config)
get_router_config
;;
+ travel_config)
+ get_travel_config
+ ;;
+ travel_scan_networks)
+ travel_scan_networks
+ ;;
update_settings)
update_settings
;;
diff --git a/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json b/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json
index 65f8cce7..11db76df 100644
--- a/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json
+++ b/luci-app-network-modes/root/usr/share/luci/menu.d/luci-app-network-modes.json
@@ -49,6 +49,14 @@
"path": "network-modes/relay"
}
},
+ "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,
@@ -65,4 +73,4 @@
"path": "network-modes/settings"
}
}
-}
\ No newline at end of file
+}
diff --git a/luci-theme-secubox/Makefile b/luci-theme-secubox/Makefile
new file mode 100644
index 00000000..f8c365b5
--- /dev/null
+++ b/luci-theme-secubox/Makefile
@@ -0,0 +1,16 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-theme-secubox
+PKG_VERSION:=1.0.0
+PKG_RELEASE:=1
+PKG_LICENSE:=Apache-2.0
+PKG_MAINTAINER:=CyberMind
+
+LUCI_TITLE:=LuCI - SecuBox CyberMood Theme
+LUCI_DESCRIPTION:=Global CyberMood design system (CSS/JS/i18n) shared by all SecuBox dashboards.
+LUCI_DEPENDS:=+luci-base
+LUCI_PKGARCH:=all
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot