mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 01:03:15 +02:00
feat: Add Network Commands section with advert and floodadv
Add new menu section "Network Commands" with two special commands: - Send Advert: sends single advertisement (recommended for normal use) - Flood Advert: floods network with advertisement (for recovery only) Changes: - cli.py: Add advert() and floodadv() functions - api.py: Add POST /api/device/command and GET /api/device/commands endpoints - base.html: Add Network Commands section to slide-out menu - app.js: Add JavaScript handlers with confirmation for floodadv - README.md: Document new Network Commands feature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ A lightweight web interface for meshcore-cli, providing browser-based access to
|
||||
- 🧹 **Clean contacts** - Remove inactive contacts with configurable threshold
|
||||
- 📦 **Message archiving** - Automatic daily archiving with browse-by-date selector
|
||||
- ⚡ **Efficient polling** - Lightweight update checks every 10s, UI refreshes only when needed
|
||||
- 📡 **Network commands** - Send advertisement (advert) or flood advertisement (floodadv) for network management
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -258,6 +259,31 @@ Access the settings panel to clean up inactive contacts:
|
||||
3. Click "Clean Inactive Contacts"
|
||||
4. Confirm the action
|
||||
|
||||
### Network Commands
|
||||
|
||||
Access network commands from the slide-out menu under "Network Commands" section:
|
||||
|
||||
#### Send Advert (Recommended)
|
||||
Sends a single advertisement frame to announce your node's presence in the mesh network. This is the normal, energy-efficient way to advertise.
|
||||
|
||||
1. Click the menu icon (☰) in the navbar
|
||||
2. Click "Send Advert" under Network Commands
|
||||
3. Wait for confirmation toast
|
||||
|
||||
#### Flood Advert (Use Sparingly!)
|
||||
Sends advertisement in flooding mode, forcing all nodes to retransmit. **Use only when:**
|
||||
- Starting a completely new network
|
||||
- After device reset or firmware change
|
||||
- When routing is broken and node is not visible
|
||||
- For debugging/testing purposes
|
||||
|
||||
⚠️ **Warning:** Flood advertisement causes high airtime usage and can destabilize larger LoRa networks. A confirmation dialog will appear before execution.
|
||||
|
||||
1. Click the menu icon (☰) in the navbar
|
||||
2. Click "Flood Advert" (highlighted in warning color)
|
||||
3. Confirm you want to proceed
|
||||
4. Wait for confirmation toast
|
||||
|
||||
## Docker Commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -264,3 +264,39 @@ def remove_channel(index: int) -> Tuple[bool, str]:
|
||||
|
||||
success, stdout, stderr = _run_command(['remove_channel', str(index)])
|
||||
return success, stdout or stderr
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Special Commands (Network Advertisement)
|
||||
# =============================================================================
|
||||
|
||||
def advert() -> Tuple[bool, str]:
|
||||
"""
|
||||
Send a single advertisement frame to the mesh network.
|
||||
|
||||
This is the recommended way to announce node presence.
|
||||
Uses minimal airtime and follows normal routing rules.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
success, stdout, stderr = _run_command(['advert'])
|
||||
return success, stdout or stderr
|
||||
|
||||
|
||||
def floodadv() -> Tuple[bool, str]:
|
||||
"""
|
||||
Send advertisement in flooding mode (broadcast storm).
|
||||
|
||||
WARNING: This should be used sparingly! It causes high airtime usage
|
||||
and can destabilize larger networks. Use only for:
|
||||
- Initial network bootstrap
|
||||
- After device reset/firmware change
|
||||
- When routing is broken
|
||||
- Debug/testing purposes
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
success, stdout, stderr = _run_command(['floodadv'])
|
||||
return success, stdout or stderr
|
||||
|
||||
@@ -298,6 +298,94 @@ def get_device_info():
|
||||
}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Special Commands
|
||||
# =============================================================================
|
||||
|
||||
# Registry of available special commands
|
||||
SPECIAL_COMMANDS = {
|
||||
'advert': {
|
||||
'function': cli.advert,
|
||||
'description': 'Send single advertisement (recommended)',
|
||||
},
|
||||
'floodadv': {
|
||||
'function': cli.floodadv,
|
||||
'description': 'Flood advertisement (use sparingly!)',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@api_bp.route('/device/command', methods=['POST'])
|
||||
def execute_special_command():
|
||||
"""
|
||||
Execute a special device command.
|
||||
|
||||
JSON body:
|
||||
command (str): Command name (required) - one of: advert, floodadv
|
||||
|
||||
Returns:
|
||||
JSON with command result
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'command' not in data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Missing required field: command'
|
||||
}), 400
|
||||
|
||||
command = data['command'].strip().lower()
|
||||
|
||||
if command not in SPECIAL_COMMANDS:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Unknown command: {command}. Available commands: {", ".join(SPECIAL_COMMANDS.keys())}'
|
||||
}), 400
|
||||
|
||||
# Execute the command
|
||||
cmd_info = SPECIAL_COMMANDS[command]
|
||||
success, message = cmd_info['function']()
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'command': command,
|
||||
'message': message or f'{command} executed successfully'
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'command': command,
|
||||
'error': message
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing special command: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/device/commands', methods=['GET'])
|
||||
def list_special_commands():
|
||||
"""
|
||||
List available special commands.
|
||||
|
||||
Returns:
|
||||
JSON with list of available commands
|
||||
"""
|
||||
commands = [
|
||||
{'name': name, 'description': info['description']}
|
||||
for name, info in SPECIAL_COMMANDS.items()
|
||||
]
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'commands': commands
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/sync', methods=['POST'])
|
||||
def sync_messages():
|
||||
"""
|
||||
|
||||
@@ -221,6 +221,19 @@ function setupEventListeners() {
|
||||
document.getElementById('scanQRBtn').addEventListener('click', function() {
|
||||
showNotification('QR scanning feature coming soon! For now, manually enter the channel details.', 'info');
|
||||
});
|
||||
|
||||
// Network Commands: Advert button
|
||||
document.getElementById('advertBtn').addEventListener('click', async function() {
|
||||
await executeSpecialCommand('advert');
|
||||
});
|
||||
|
||||
// Network Commands: Flood Advert button (with confirmation)
|
||||
document.getElementById('floodadvBtn').addEventListener('click', async function() {
|
||||
if (!confirm('Flood Advertisement uses high airtime and should only be used for network recovery.\n\nAre you sure you want to proceed?')) {
|
||||
return;
|
||||
}
|
||||
await executeSpecialCommand('floodadv');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,6 +475,51 @@ async function cleanupContacts() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a special device command (advert, floodadv, etc.)
|
||||
*/
|
||||
async function executeSpecialCommand(command) {
|
||||
// Get button element to disable during execution
|
||||
const btnId = command === 'advert' ? 'advertBtn' : 'floodadvBtn';
|
||||
const btn = document.getElementById(btnId);
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/device/command', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ command: command })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(data.message || `${command} sent successfully`, 'success');
|
||||
} else {
|
||||
showNotification(`Command failed: ${data.error}`, 'danger');
|
||||
}
|
||||
|
||||
// Close offcanvas menu after command execution
|
||||
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('mainMenu'));
|
||||
if (offcanvas) {
|
||||
offcanvas.hide();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error executing ${command}:`, error);
|
||||
showNotification(`Failed to execute ${command}`, 'danger');
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup intelligent auto-refresh
|
||||
* Checks for updates regularly but only refreshes UI when new messages arrive
|
||||
|
||||
@@ -73,6 +73,29 @@
|
||||
<!-- Archive dates loaded dynamically via JavaScript -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Network Commands Section -->
|
||||
<div class="list-group-item py-2 mt-2">
|
||||
<small class="text-muted fw-bold text-uppercase">Network Commands</small>
|
||||
</div>
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="advertBtn" title="Send single advertisement (recommended for normal operation)">
|
||||
<i class="bi bi-megaphone" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<span>Send Advert</span>
|
||||
<small class="d-block text-muted">Announce presence (normal)</small>
|
||||
</div>
|
||||
</button>
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3 text-warning" id="floodadvBtn" title="Flood advertisement - use sparingly! High airtime usage.">
|
||||
<i class="bi bi-broadcast" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<span>Flood Advert</span>
|
||||
<small class="d-block text-muted">Network recovery only!</small>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="list-group-item py-2 mt-2">
|
||||
<small class="text-muted fw-bold text-uppercase">Configuration</small>
|
||||
</div>
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
|
||||
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
|
||||
<span>Settings</span>
|
||||
|
||||
Reference in New Issue
Block a user