From 798e2e04356bcf4363b81a92d47a6f70c2814532 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 28 Dec 2025 16:58:11 +0100 Subject: [PATCH] docs: Add comprehensive CyberMood global theme system documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created GLOBAL_THEME_SYSTEM.md with complete theme specification - Added THEME_CONTEXT.md for quick AI assistant reference - Defined CyberMood design language (metallic, glass, neon aesthetics) - Provided ready-to-use templates (CSS variables, components, JS controller) - Planned multi-language support (en, fr, de, es) - Created 5-week implementation roadmap - Added 5 ready-to-use prompts for theme implementation - Updated network-modes module with travel mode support πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .claude/THEME_CONTEXT.md | 292 +++++ .codex/THEME_CONTEXT.md | 200 ++++ DOCS/GLOBAL_THEME_SYSTEM.md | 1040 +++++++++++++++++ .../resources/network-modes/api.js | 26 + .../resources/network-modes/dashboard.css | 55 + .../resources/view/network-modes/overview.js | 19 +- .../resources/view/network-modes/travel.js | 275 +++++ .../root/etc/config/network-modes | 17 + .../root/usr/libexec/rpcd/luci.network-modes | 311 ++++- .../luci/menu.d/luci-app-network-modes.json | 10 +- luci-theme-secubox/Makefile | 16 + 11 files changed, 2248 insertions(+), 13 deletions(-) create mode 100644 .claude/THEME_CONTEXT.md create mode 100644 .codex/THEME_CONTEXT.md create mode 100644 DOCS/GLOBAL_THEME_SYSTEM.md create mode 100644 luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/travel.js create mode 100644 luci-theme-secubox/Makefile 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 + +
+
+

Title

+
+
Content
+
+ + + + + + +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 + +
+
+

Title

+
+
Content
+
+ + + + + + +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