docs: Add comprehensive CyberMood global theme system documentation
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
6314884f00
commit
798e2e0435
292
.claude/THEME_CONTEXT.md
Normal file
292
.claude/THEME_CONTEXT.md
Normal file
@ -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
|
||||
<!-- Cards -->
|
||||
<div class="cyber-card">
|
||||
<div class="cyber-card-header">
|
||||
<h3 class="cyber-card-title">Title</h3>
|
||||
</div>
|
||||
<div class="cyber-card-body">Content</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<button class="cyber-btn cyber-btn--primary">Primary</button>
|
||||
<button class="cyber-btn cyber-btn--secondary">Secondary</button>
|
||||
|
||||
<!-- Badges -->
|
||||
<span class="cyber-badge cyber-badge--success">Active</span>
|
||||
|
||||
<!-- Forms -->
|
||||
<input type="text" class="cyber-input" />
|
||||
<select class="cyber-select"></select>
|
||||
```
|
||||
|
||||
## 🌍 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. 🎯
|
||||
200
.codex/THEME_CONTEXT.md
Normal file
200
.codex/THEME_CONTEXT.md
Normal file
@ -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
|
||||
<!-- Cards -->
|
||||
<div class="cyber-card">
|
||||
<div class="cyber-card-header">
|
||||
<h3 class="cyber-card-title">Title</h3>
|
||||
</div>
|
||||
<div class="cyber-card-body">Content</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<button class="cyber-btn cyber-btn--primary">Primary</button>
|
||||
<button class="cyber-btn cyber-btn--secondary">Secondary</button>
|
||||
|
||||
<!-- Badges -->
|
||||
<span class="cyber-badge cyber-badge--success">Active</span>
|
||||
|
||||
<!-- Forms -->
|
||||
<input type="text" class="cyber-input" />
|
||||
<select class="cyber-select"></select>
|
||||
```
|
||||
|
||||
## 🌍 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.**
|
||||
1040
DOCS/GLOBAL_THEME_SYSTEM.md
Normal file
1040
DOCS/GLOBAL_THEME_SYSTEM.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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')
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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'
|
||||
|
||||
@ -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 <<EOF
|
||||
$scan_output
|
||||
EOF
|
||||
|
||||
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
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Apply mode change (with actual network reconfiguration and rollback timer)
|
||||
apply_mode() {
|
||||
json_init
|
||||
@ -426,8 +568,82 @@ apply_mode() {
|
||||
apply_transparent_proxy_rules
|
||||
deploy_nginx_vhosts
|
||||
;;
|
||||
travel)
|
||||
local client_iface=$(uci -q get network-modes.travel.client_interface || echo "wlan1")
|
||||
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 travel_ssid=$(uci -q get network-modes.travel.ssid || echo "")
|
||||
local travel_password=$(uci -q get network-modes.travel.password || echo "")
|
||||
local travel_encryption=$(uci -q get network-modes.travel.encryption || echo "sae-mixed")
|
||||
local hotspot_ssid=$(uci -q get network-modes.travel.hotspot_ssid || echo "SecuBox-Travel")
|
||||
local hotspot_password=$(uci -q get network-modes.travel.hotspot_password || echo "TravelSafe123!")
|
||||
local clone_mac=$(uci -q get network-modes.travel.clone_mac || echo "")
|
||||
local lan_ip=$(uci -q get network-modes.travel.lan_subnet || echo "10.77.0.1")
|
||||
local lan_netmask=$(uci -q get network-modes.travel.lan_netmask || echo "255.255.255.0")
|
||||
|
||||
accesspoint)
|
||||
uci delete network.wan 2>/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
|
||||
;;
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
luci-theme-secubox/Makefile
Normal file
16
luci-theme-secubox/Makefile
Normal file
@ -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 <contact@cybermind.fr>
|
||||
|
||||
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
|
||||
Loading…
Reference in New Issue
Block a user