chore: Remove technotes/ from repository

Keep folder local only, already in .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-16 08:47:13 +01:00
parent d395656445
commit 7a626ba105
6 changed files with 0 additions and 4104 deletions
-583
View File
@@ -1,583 +0,0 @@
# API Diagnostic Commands - Quick Reference
**Server**: `192.168.1.2`
**SSH**: `ssh mcwebui@192.168.1.2`
**NOTE:** Replace the above example with your own server and credentials.
This cheatsheet contains useful commands for diagnosing mc-webui and meshcore-bridge using API endpoints.
---
## Table of Contents
1. [Health Checks](#health-checks)
2. [Contact Management](#contact-management)
3. [Device Information](#device-information)
4. [Channel Management](#channel-management)
5. [Messages](#messages)
6. [Direct Messages (DM)](#direct-messages-dm)
7. [Settings](#settings)
8. [Archives](#archives)
---
## Health Checks
### Check meshcore-bridge health
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://meshcore-bridge:5001/health | jq"
```
**Response**:
```json
{
"status": "healthy",
"serial_port": "/dev/serial/by-id/usb-Espressif_Systems_heltec_wifi_lora_32_v4__16_MB_FLASH__2_MB_PSRAM__90706984A000-if00",
"advert_log": "/root/.config/meshcore/MarWoj.adverts.jsonl"
}
```
### Check mc-webui connection status
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/status | jq"
```
**Response**:
```json
{
"success": true,
"connected": true
}
```
---
## Contact Management
### Get all contacts (CLI type only, names only)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/contacts | jq"
```
**Response**:
```json
{
"success": true,
"count": 19,
"contacts": [
"SP7UNR_tdeck",
"Kosu 🦜",
"Arek",
"daniel5120 🔫",
"Szczwany-lis🦊"
]
}
```
### Get detailed contacts (all types with metadata + last_seen)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/contacts/detailed | jq"
```
**Response**:
```json
{
"success": true,
"count": 263,
"limit": 350,
"contacts": [
{
"name": "TK Zalesie Test 🦜",
"public_key_prefix": "df2027d3f2ef",
"type_label": "REP",
"path_or_mode": "Flood",
"last_seen": 1735429453,
"raw_line": "TK Zalesie Test 🦜 REP df2027d3f2ef Flood"
},
{
"name": "KRA C",
"public_key_prefix": "d103df18e0ff",
"type_label": "REP",
"path_or_mode": "Flood",
"last_seen": 1716206073
}
]
}
```
### Get pending contacts (awaiting manual approval)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://meshcore-bridge:5001/pending_contacts | jq"
```
**Response**:
```json
{
"success": true,
"pending": [
{
"name": "C3396B62",
"public_key": "c3396b628ba34b96138d962fda81e5e5450be14fa212793d55c71fba967a6262"
}
],
"raw_stdout": "MarWoj|* pending_contacts\nMarWoj|* \n C3396B62: c3396b628ba34b96138d962fda81e5e5450be14fa212793d55c71fba967a6262"
}
```
Or via mc-webui API:
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/contacts/pending | jq"
```
### Delete a contact (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/contacts/delete \
-H 'Content-Type: application/json' \
-d '{\"selector\": \"df2027d3f2ef\"}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "Contact removed successfully"
}
```
### Approve pending contact (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/contacts/pending/approve \
-H 'Content-Type: application/json' \
-d '{\"public_key\": \"c3396b628ba34b96138d962fda81e5e5450be14fa212793d55c71fba967a6262\"}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "Contact approved successfully"
}
```
---
## Device Information
### Get device info
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/device/info | jq"
```
**Response**:
```json
{
"success": true,
"info": {
"device_name": "MarWoj",
"public_key": "11009cebbd2744d33c94b980b8f2475241fd2ca6165bd623e5ef00ec6982be6a...",
"battery": "100%",
"voltage": "4.20V"
}
}
```
### Get device settings (persistent)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/device/settings | jq"
```
**Response**:
```json
{
"success": true,
"settings": {
"manual_add_contacts": false
}
}
```
### Update device settings (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/device/settings \
-H 'Content-Type: application/json' \
-d '{\"manual_add_contacts\": true}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "manual_add_contacts set to on",
"settings": {
"manual_add_contacts": true
}
}
```
---
## Channel Management
### List all channels
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/channels | jq"
```
**Response**:
```json
{
"success": true,
"channels": [
{
"index": 0,
"name": "Public",
"key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"index": 1,
"name": "Malopolska",
"key": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
]
}
```
### Get channel QR code (JSON format)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/channels/1/qr?format=json' | jq"
```
**Response**:
```json
{
"success": true,
"qr_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"channel": {
"index": 1,
"name": "Malopolska",
"key": "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
}
```
### Create new channel (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/channels \
-H 'Content-Type: application/json' \
-d '{\"name\": \"TestChannel\"}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "Channel created successfully",
"channel": {
"index": 2,
"name": "TestChannel",
"key": "auto-generated-key-here"
}
}
```
### Delete channel (DELETE request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X DELETE \
http://192.168.1.2:5000/api/channels/2 | jq"
```
**Response**:
```json
{
"success": true,
"message": "Channel removed successfully"
}
```
---
## Messages
### Get messages for current channel
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/messages | jq"
```
**Response**:
```json
{
"success": true,
"messages": [
{
"timestamp": 1735430000,
"sender": "Kosu 🦜",
"text": "Hello from mesh!",
"type": "CHAN",
"channel_idx": 1
}
],
"count": 1
}
```
### Get messages for specific channel
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/messages?channel_idx=1' | jq"
```
### Get archived messages for specific date
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/messages?archive_date=2025-12-28&channel_idx=1' | jq"
```
### Send message (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/messages \
-H 'Content-Type: application/json' \
-d '{\"text\": \"Test message\", \"channel_idx\": 1}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "Message sent successfully"
}
```
### Check for new messages (smart refresh)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/messages/updates?last_seen={\"0\":1735430000,\"1\":1735429000}' | jq"
```
**Response**:
```json
{
"success": true,
"updates": {
"0": {
"has_new": false,
"unread_count": 0
},
"1": {
"has_new": true,
"unread_count": 3
}
},
"total_unread": 3
}
```
---
## Direct Messages (DM)
### List DM conversations
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/dm/conversations | jq"
```
**Response**:
```json
{
"success": true,
"conversations": [
{
"conversation_id": "kosu_🦜",
"display_name": "Kosu 🦜",
"last_message_time": 1735430000,
"unread_count": 2
}
]
}
```
### Get DM messages for specific conversation
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/dm/messages?conversation_id=kosu_🦜&limit=50' | jq"
```
**Response**:
```json
{
"success": true,
"messages": [
{
"timestamp": 1735430000,
"sender": "Kosu 🦜",
"recipient": "MarWoj",
"text": "Private message text",
"type": "PRIV",
"pubkey_prefix": "df2027"
}
]
}
```
### Send DM (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/dm/messages \
-H 'Content-Type: application/json' \
-d '{\"recipient\": \"Kosu 🦜\", \"text\": \"Test DM\"}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "DM sent successfully"
}
```
---
## Settings
### Trigger sync (force message refresh)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST http://192.168.1.2:5000/api/sync | jq"
```
**Response**:
```json
{
"success": true,
"message": "Sync triggered successfully"
}
```
### Send advert (normal)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/device/command \
-H 'Content-Type: application/json' \
-d '{\"command\": \"advert\"}' | jq"
```
**Response**:
```json
{
"success": true,
"message": "Command executed successfully"
}
```
### Send flood advert (use sparingly!)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST \
http://192.168.1.2:5000/api/device/command \
-H 'Content-Type: application/json' \
-d '{\"command\": \"floodadv\"}' | jq"
```
---
## Archives
### List available archives
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/archives | jq"
```
**Response**:
```json
{
"success": true,
"archives": [
{
"date": "2025-12-28",
"display_date": "28 December 2025",
"file_path": "/mnt/archive/meshcore/2025-12-28.msgs"
},
{
"date": "2025-12-27",
"display_date": "27 December 2025",
"file_path": "/mnt/archive/meshcore/2025-12-27.msgs"
}
]
}
```
### Trigger manual archiving (POST request)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -X POST http://192.168.1.2:5000/api/archive/trigger | jq"
```
**Response**:
```json
{
"success": true,
"message": "Archive created successfully"
}
```
---
## Useful One-Liners
### Count contacts by type
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/contacts/detailed | jq '.contacts | group_by(.type_label) | map({type: .[0].type_label, count: length})'"
```
**Response**:
```json
[
{"type": "CLI", "count": 17},
{"type": "REP", "count": 226},
{"type": "ROOM", "count": 20}
]
```
### Get only active contacts (last_seen < 1 hour)
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://192.168.1.2:5000/api/contacts/detailed | jq --arg now \"\$(date +%s)\" '.contacts | map(select(.last_seen and (\$now | tonumber) - .last_seen < 3600))'"
```
### Check total unread messages across all channels
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s 'http://192.168.1.2:5000/api/messages/updates?last_seen={}' | jq '.total_unread'"
```
### List pending contacts with full keys
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s http://meshcore-bridge:5001/pending_contacts | jq '.pending[] | {name, key: .public_key}'"
```
---
## Quick Troubleshooting
### Check if bridge is responding
```bash
ssh mcwebui@192.168.1.2 "docker exec mc-webui curl -s -w '\nHTTP Status: %{http_code}\n' http://meshcore-bridge:5001/health"
```
### Check if mc-webui is responding
```bash
ssh mcwebui@192.168.1.2 "curl -s -w '\nHTTP Status: %{http_code}\n' http://192.168.1.2:5000/api/status"
```
### View recent bridge logs
```bash
ssh mcwebui@192.168.1.2 "docker logs --tail 50 meshcore-bridge"
```
### View recent mc-webui logs
```bash
ssh mcwebui@192.168.1.2 "docker logs --tail 50 mc-webui"
```
### Follow logs in real-time
```bash
ssh mcwebui@192.168.1.2 "docker logs -f mc-webui"
```
### Check .msgs file size
```bash
ssh mcwebui@192.168.1.2 "ls -lh ~/.config/meshcore/MarWoj.msgs"
```
### Check .adverts.jsonl file size
```bash
ssh mcwebui@192.168.1.2 "ls -lh ~/.config/meshcore/MarWoj.adverts.jsonl"
```
---
## Notes
- **Port 5001** (meshcore-bridge): Internal only, accessible via `docker exec mc-webui`
- **Port 5000** (mc-webui): Publicly accessible on server
- All POST/DELETE requests require `Content-Type: application/json` header
- Use `jq` for pretty JSON formatting
- For debugging, add `-v` flag to curl for verbose output
- Response times should be < 500ms for most endpoints
- Bridge health endpoint includes serial port and advert log paths
---
**Last updated**: 2025-12-29
**mc-webui version**: Contact Management MVP v2 with "Last Seen" feature
@@ -1,981 +0,0 @@
# Contact Management MVP v1 - Implementation Complete
**Date**: 2025-12-29
**Status**: ✅ Completed and Tested
**Branch**: `dev-2`
**Commit**: `77c72ba`
## Overview
Successfully implemented Contact Management MVP v1, a complete UI module for managing manual contact approval in mc-webui. The implementation provides persistent, user-controlled settings that survive container restarts, replacing the previous testing-only forced configuration.
## Requirements
Based on specification in `docs/UI-Contact-Management-MVP-v1.md`:
### Functional Requirements
1. **Manual Approval Toggle**
- Persistent across container restarts
- Default: OFF (automatic approval - meshcli factory default)
- User decision becomes source of truth
2. **Pending Contacts Management**
- List pending contacts awaiting approval
- Show name and truncated public key
- Approve action (must use full public_key)
- Copy full public key to clipboard
3. **Mobile-First UI**
- Touch-friendly buttons (min-height: 44px)
- Responsive card layout
- Bootstrap 5 components
- Toast notifications for user feedback
4. **Integration**
- Menu item in side navigation
- Route: `/contacts/manage`
- Consistent with existing UI patterns
### Non-Functional Requirements
- Settings must persist across container restarts
- Settings file stored in volume-mounted MC_CONFIG_DIR
- Backward compatible (defaults to meshcli factory settings)
- Real-time feedback (loading states, error handling)
## Architecture
### Settings Persistence Mechanism
**File-based persistence** via `.webui_settings.json`:
```
MC_CONFIG_DIR/
├── .webui_settings.json ← Persistent settings (NEW)
├── MeshCore.msgs
└── MeshCore.db
```
**Settings file format**:
```json
{
"manual_add_contacts": true
}
```
**Persistence flow**:
```
┌─────────────────────────────────────────────────────────┐
│ 1. User toggles manual approval in UI │
│ ↓ │
│ 2. POST /api/device/settings (mc-webui) │
│ ↓ │
│ 3. POST /set_manual_add_contacts (bridge) │
│ ├─→ Save to .webui_settings.json │
│ └─→ Apply to running meshcli session │
│ │
│ [Container Restart] │
│ │
│ 4. Bridge startup reads .webui_settings.json │
│ ↓ │
│ 5. Applies setting to new meshcli session │
│ ↓ │
│ 6. UI loads and displays persisted setting │
└─────────────────────────────────────────────────────────┘
```
### Component Architecture
```
┌────────────────────────────────────────────────────────┐
│ Web Browser │
│ ┌───────────────────────────────────────────────────┐ │
│ │ contacts.html + contacts.js │ │
│ │ - Manual approval toggle │ │
│ │ - Pending contacts list │ │
│ │ - Approve/Copy buttons │ │
│ └────────────┬──────────────────────────────────────┘ │
└───────────────┼─────────────────────────────────────────┘
│ HTTP JSON API
┌────────────────────────────────────────────────────────┐
│ mc-webui container │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Flask API (app/routes/api.py) │ │
│ │ - GET /api/contacts/pending │ │
│ │ - POST /api/contacts/pending/approve │ │
│ │ - GET /api/device/settings │ │
│ │ - POST /api/device/settings │ │
│ └────────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴──────────────────────────────────────┐ │
│ │ CLI Wrapper (app/meshcore/cli.py) │ │
│ │ - get_pending_contacts() │ │
│ │ - approve_pending_contact(public_key) │ │
│ │ - get_device_settings() │ │
│ │ - set_manual_add_contacts(enabled) │ │
│ └────────────┬──────────────────────────────────────┘ │
└───────────────┼─────────────────────────────────────────┘
│ HTTP (bridge API)
┌────────────────────────────────────────────────────────┐
│ meshcore-bridge container │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Bridge API (meshcore-bridge/bridge.py) │ │
│ │ - GET /pending_contacts │ │
│ │ - POST /add_pending │ │
│ │ - POST /set_manual_add_contacts (NEW) │ │
│ └────────────┬──────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴──────────────────────────────────────┐ │
│ │ Persistent meshcli Session │ │
│ │ - Reads .webui_settings.json on startup │ │
│ │ - Applies manual_add_contacts setting │ │
│ │ - Command queue (FIFO) │ │
│ └────────────┬──────────────────────────────────────┘ │
└───────────────┼─────────────────────────────────────────┘
│ Serial USB
MeshCore Device
```
## Implementation Details
### 1. Backend - meshcore-bridge (bridge.py)
**Added**: Settings persistence mechanism
```python
def _load_webui_settings(self) -> dict:
"""Load webui settings from .webui_settings.json file"""
settings_path = self.config_dir / ".webui_settings.json"
if not settings_path.exists():
logger.info("No webui settings file found, using defaults")
return {}
try:
with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
logger.info(f"Loaded webui settings: {settings}")
return settings
except Exception as e:
logger.error(f"Failed to load webui settings: {e}")
return {}
```
**Modified**: Session initialization to read settings
```python
def _init_session_settings(self):
"""Configure meshcli session for advert logging, message subscription, and user-configured settings"""
logger.info("Configuring meshcli session settings")
if self.process and self.process.stdin:
try:
# Core settings (always enabled)
self.process.stdin.write('set json_log_rx on\n')
self.process.stdin.write('set print_adverts on\n')
self.process.stdin.write('msgs_subscribe\n')
# User-configurable settings from .webui_settings.json
webui_settings = self._load_webui_settings()
manual_add_contacts = webui_settings.get('manual_add_contacts', False)
if manual_add_contacts:
self.process.stdin.write('set manual_add_contacts on\n')
logger.info("Session settings applied: json_log_rx=on, print_adverts=on, manual_add_contacts=on, msgs_subscribe")
else:
logger.info("Session settings applied: json_log_rx=on, print_adverts=on, manual_add_contacts=off (default), msgs_subscribe")
self.process.stdin.flush()
except Exception as e:
logger.error(f"Failed to apply session settings: {e}")
```
**Added**: New endpoint for settings update
```python
@app.route('/set_manual_add_contacts', methods=['POST'])
def set_manual_add_contacts():
"""
Enable or disable manual contact approval mode.
This setting is:
1. Saved to .webui_settings.json for persistence across container restarts
2. Applied immediately to the running meshcli session
Request JSON:
{"enabled": true/false}
Response:
{"success": true, "message": "...", "enabled": true/false}
"""
try:
data = request.get_json()
if not data or 'enabled' not in data:
return jsonify({'success': False, 'error': 'Missing required field: enabled'}), 400
enabled = data['enabled']
if not isinstance(enabled, bool):
return jsonify({'success': False, 'error': 'enabled must be a boolean'}), 400
# Save to persistent settings file
settings_path = meshcli_session.config_dir / ".webui_settings.json"
try:
if settings_path.exists():
with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
else:
settings = {}
settings['manual_add_contacts'] = enabled
with open(settings_path, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
logger.info(f"Saved manual_add_contacts={enabled} to {settings_path}")
except Exception as e:
logger.error(f"Failed to save settings file: {e}")
return jsonify({'success': False, 'error': f'Failed to save settings: {str(e)}'}), 500
# Apply setting immediately to running session
command_value = 'on' if enabled else 'off'
result = meshcli_session.execute_command(['set', 'manual_add_contacts', command_value], timeout=DEFAULT_TIMEOUT)
if not result['success']:
return jsonify({'success': False, 'error': f"Failed to apply setting: {result.get('stderr', 'Unknown error')}"}), 500
return jsonify({'success': True, 'message': f"manual_add_contacts set to {command_value}", 'enabled': enabled}), 200
except Exception as e:
logger.error(f"API error in /set_manual_add_contacts: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
```
### 2. Backend - mc-webui CLI Wrapper (cli.py)
**Added**: Four new functions for contact management
```python
def get_pending_contacts() -> Tuple[bool, List[Dict], str]:
"""Get list of contacts awaiting manual approval"""
# Proxies to bridge GET /pending_contacts
def approve_pending_contact(public_key: str) -> Tuple[bool, str]:
"""Approve and add a pending contact by public key"""
# Proxies to bridge POST /add_pending
# IMPORTANT: Always uses full public_key for compatibility
def get_device_settings() -> Tuple[bool, Dict]:
"""Get persistent device settings from .webui_settings.json"""
# Reads file directly from MC_CONFIG_DIR
def set_manual_add_contacts(enabled: bool) -> Tuple[bool, str]:
"""Enable or disable manual contact approval mode"""
# Proxies to bridge POST /set_manual_add_contacts
```
**Key Implementation Detail**: Always use full public_key for approval
```python
def approve_pending_contact(public_key: str) -> Tuple[bool, str]:
"""
Args:
public_key: Full public key of the contact to approve (REQUIRED - full key works for all contact types)
"""
# ...
response = requests.post(
f"{config.MC_BRIDGE_URL.replace('/cli', '/add_pending')}",
json={'selector': public_key.strip()}, # Full key ensures compatibility
timeout=DEFAULT_TIMEOUT + 5
)
```
**Rationale**: Testing documented in `technotes/pending-contacts-api.md` showed:
- CLI contacts: Accept name prefix, key prefix, or full key
- ROOM contacts: Only accept full public key
- **Solution**: Always use full public_key for universal compatibility
### 3. Backend - Flask API (api.py)
**Added**: Four new REST endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/contacts/pending` | GET | List pending contacts |
| `/api/contacts/pending/approve` | POST | Approve contact by public_key |
| `/api/device/settings` | GET | Get persistent settings |
| `/api/device/settings` | POST | Update manual_add_contacts |
**Request/Response Examples**:
```bash
# Get pending contacts
curl http://192.168.131.80:5000/api/contacts/pending
# Response
{
"success": true,
"pending": [
{
"name": "Szczwany-lis🔥",
"public_key": "f9ef123abc..."
}
],
"count": 1
}
# Approve contact (MUST use full public_key)
curl -X POST http://192.168.131.80:5000/api/contacts/pending/approve \
-H 'Content-Type: application/json' \
-d '{"public_key":"f9ef123abc..."}'
# Response
{
"success": true,
"message": "Contact approved successfully"
}
# Get settings
curl http://192.168.131.80:5000/api/device/settings
# Response
{
"success": true,
"settings": {
"manual_add_contacts": true
}
}
# Update settings
curl -X POST http://192.168.131.80:5000/api/device/settings \
-H 'Content-Type: application/json' \
-d '{"manual_add_contacts":true}'
# Response
{
"success": true,
"message": "manual_add_contacts set to on",
"settings": {
"manual_add_contacts": true
}
}
```
### 4. Frontend - contacts.html
**Mobile-First Responsive Design**:
```html
<!-- Manual Approval Settings Section -->
<div class="settings-section">
<h5 class="mb-3">
<i class="bi bi-shield-check"></i> Manual Contact Approval
</h5>
<p class="text-muted small mb-3">
When enabled, new contacts must be manually approved before they can communicate with your node.
</p>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="manualApprovalSwitch"
style="cursor: pointer; min-width: 3rem; min-height: 1.5rem;">
<label class="form-check-label" for="manualApprovalSwitch" style="cursor: pointer; font-weight: 500;">
<span id="switchLabel">Loading...</span>
</label>
</div>
<div class="info-badge" id="approvalInfo" style="display: none;">
<i class="bi bi-info-circle"></i> Pending contacts will only appear when manual approval is enabled.
</div>
</div>
<!-- Pending Contacts Section -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="bi bi-hourglass-split"></i> Pending Contacts
<span class="badge bg-primary rounded-pill" id="pendingCount" style="display: none;">0</span>
</h5>
<button class="btn btn-sm btn-outline-primary" id="refreshPendingBtn">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<!-- Loading State -->
<div id="pendingLoading" class="text-center py-3" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary"></div>
<span class="ms-2 text-muted">Loading pending contacts...</span>
</div>
<!-- Empty State -->
<div id="pendingEmpty" class="empty-state" style="display: none;">
<i class="bi bi-check-circle"></i>
<p class="mb-0">No pending contact requests</p>
<small class="text-muted">New contacts will appear here for approval</small>
</div>
<!-- Pending Contacts List (dynamically populated) -->
<div id="pendingList"></div>
<!-- Error State -->
<div id="pendingError" class="alert alert-danger" style="display: none;" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<span id="errorMessage">Failed to load pending contacts</span>
</div>
</div>
```
**CSS Highlights**:
```css
.pending-contact-card {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.btn-action {
min-height: 44px; /* Touch-friendly size for mobile */
font-size: 1rem;
}
.contact-key {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: #6c757d;
word-break: break-all;
}
```
### 5. Frontend - contacts.js
**Key Features**:
1. **Settings Management**
```javascript
async function loadSettings() {
const response = await fetch('/api/device/settings');
const data = await response.json();
if (data.success) {
manualApprovalEnabled = data.settings.manual_add_contacts || false;
updateApprovalUI(manualApprovalEnabled);
}
}
async function handleApprovalToggle(event) {
const enabled = event.target.checked;
const response = await fetch('/api/device/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({manual_add_contacts: enabled})
});
// Auto-reload pending contacts after toggle
setTimeout(() => loadPendingContacts(), 500);
}
```
2. **Pending Contacts List**
```javascript
function createContactCard(contact, index) {
const card = document.createElement('div');
card.className = 'pending-contact-card';
// Contact name
const nameDiv = document.createElement('div');
nameDiv.className = 'contact-name';
nameDiv.textContent = contact.name;
// Truncated public key (full key in title attribute)
const keyDiv = document.createElement('div');
keyDiv.className = 'contact-key';
const truncatedKey = contact.public_key.substring(0, 16) + '...';
keyDiv.textContent = truncatedKey;
keyDiv.title = contact.public_key; // Hover shows full key
// Approve button
const approveBtn = document.createElement('button');
approveBtn.className = 'btn btn-success btn-action flex-grow-1';
approveBtn.innerHTML = '<i class="bi bi-check-circle"></i> Approve';
approveBtn.onclick = () => approveContact(contact, index);
// Copy full key button
const copyBtn = document.createElement('button');
copyBtn.className = 'btn btn-outline-secondary btn-action';
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> Copy Full Key';
copyBtn.onclick = () => copyPublicKey(contact.public_key, copyBtn);
// ...
return card;
}
```
3. **Approve Contact** (CRITICAL: Always use full public_key)
```javascript
async function approveContact(contact, index) {
const cardEl = document.getElementById(`contact-${index}`);
// Disable buttons during approval
const buttons = cardEl.querySelectorAll('button');
buttons.forEach(btn => btn.disabled = true);
try {
const response = await fetch('/api/contacts/pending/approve', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
public_key: contact.public_key // ALWAYS use full public_key (works for CLI, ROOM, etc.)
})
});
const data = await response.json();
if (data.success) {
showToast(`Approved: ${contact.name}`, 'success');
// Remove from list with fade animation
cardEl.style.opacity = '0';
cardEl.style.transition = 'opacity 0.3s';
setTimeout(() => {
cardEl.remove();
loadPendingContacts(); // Reload to update count
}, 300);
} else {
showToast('Failed to approve: ' + data.error, 'danger');
// Re-enable buttons on failure
buttons.forEach(btn => btn.disabled = false);
}
} catch (error) {
showToast('Network error: ' + error.message, 'danger');
buttons.forEach(btn => btn.disabled = false);
}
}
```
4. **Copy to Clipboard**
```javascript
function copyPublicKey(publicKey, buttonEl) {
navigator.clipboard.writeText(publicKey).then(() => {
// Visual feedback
const originalHTML = buttonEl.innerHTML;
buttonEl.innerHTML = '<i class="bi bi-check"></i> Copied!';
buttonEl.classList.remove('btn-outline-secondary');
buttonEl.classList.add('btn-success');
setTimeout(() => {
buttonEl.innerHTML = originalHTML;
buttonEl.classList.remove('btn-success');
buttonEl.classList.add('btn-outline-secondary');
}, 2000);
showToast('Public key copied to clipboard', 'info');
}).catch(err => {
showToast('Failed to copy to clipboard', 'danger');
});
}
```
5. **Toast Notifications**
```javascript
function showToast(message, type = 'info') {
const toastEl = document.getElementById('contactToast');
const bodyEl = toastEl.querySelector('.toast-body');
bodyEl.textContent = message;
// Apply color based on type
toastEl.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-warning');
toastEl.classList.remove('text-white');
if (type === 'success' || type === 'danger' || type === 'warning') {
toastEl.classList.add(`bg-${type}`, 'text-white');
} else if (type === 'info') {
toastEl.classList.add('bg-info', 'text-white');
}
const toast = new bootstrap.Toast(toastEl, {
autohide: true,
delay: 3000
});
toast.show();
}
```
### 6. Navigation Integration
**Added to base.html** (line 73-76):
```html
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3"
onclick="window.location.href='/contacts/manage';">
<i class="bi bi-person-check" style="font-size: 1.5rem;"></i>
<span>Contact Management</span>
</button>
```
**Added route in views.py**:
```python
@views_bp.route('/contacts/manage')
def contact_management():
"""Contact Management view - manual approval settings and pending contacts list"""
return render_template(
'contacts.html',
device_name=config.MC_DEVICE_NAME,
refresh_interval=config.MC_REFRESH_INTERVAL
)
```
## Testing
### Test Environment
- **Host**: 192.168.131.80 (SSH: marek@192.168.131.80)
- **Containers**: mc-webui + meshcore-bridge
- **Device**: MeshCore on /dev/ttyUSB0
- **Network**: Active mesh network with multiple nodes
### Test 1: Basic Functionality (2025-12-29)
**Initial State**:
```bash
ssh marek@192.168.131.80 "docker exec mc-webui curl -s http://192.168.131.80:5000/api/contacts/pending | jq"
```
**Result**: 3 pending contacts visible:
- Szczwany-lis🔥
- MarioTJE🇵🇱
- Logiczny
**Action**: User approved "Szczwany-lis🔥" via UI
**Verification**:
```bash
# Check contacts list after approval
ssh marek@192.168.131.80 "docker exec meshcore-bridge curl -s http://localhost:5001/cli -X POST -H 'Content-Type: application/json' -d '{\"command\":[\"contacts\"]}' | jq"
```
**Result**: ✅ SUCCESS
- Contact "Szczwany-lis🦊" appeared in contacts list (count: 15)
- Contact no longer in pending list after refresh
- No errors in browser console or server logs
### Test 2: Settings Persistence Across Container Restart (2025-12-29)
**Step 1**: Check current setting
```bash
ssh marek@192.168.131.80 "docker exec mc-webui curl -s http://192.168.131.80:5000/api/device/settings | jq"
```
**Result**:
```json
{
"settings": {
"manual_add_contacts": true
},
"success": true
}
```
**Step 2**: Restart containers
```bash
ssh marek@192.168.131.80 "cd ~/mc-webui && docker compose restart"
```
**Output**:
```
Container meshcore-bridge Restarting
Container mc-webui Restarting
Container meshcore-bridge Started
Container mc-webui Started
```
**Step 3**: Verify setting persisted
```bash
ssh marek@192.168.131.80 "docker exec mc-webui curl -s http://192.168.131.80:5000/api/device/settings | jq"
```
**Result**: ✅ SUCCESS - Setting persisted across restart
```json
{
"settings": {
"manual_add_contacts": true
},
"success": true
}
```
**Verification in logs**:
```bash
docker compose logs meshcore-bridge | grep -i "manual_add_contacts"
```
Expected output:
```
Loaded webui settings: {'manual_add_contacts': True}
Session settings applied: json_log_rx=on, print_adverts=on, manual_add_contacts=on, msgs_subscribe
```
### Test Results Summary
| Test Case | Expected Result | Actual Result | Status |
|-----------|----------------|---------------|--------|
| Load settings on page open | Display current manual_add_contacts state | Displayed correctly | ✅ PASS |
| Toggle manual approval ON | Setting saved and applied | Setting saved, applied, UI updated | ✅ PASS |
| Toggle manual approval OFF | Setting saved and applied | Setting saved, applied, UI updated | ✅ PASS |
| Load pending contacts | Show list with name + key | 3 contacts shown correctly | ✅ PASS |
| Approve contact | Contact added, removed from pending | Approved successfully, appeared in contacts | ✅ PASS |
| Copy public key | Copy to clipboard + feedback | Copied successfully, visual feedback shown | ✅ PASS |
| Container restart | Settings persist | manual_add_contacts=true persisted | ✅ PASS |
| Bridge reads settings on startup | Setting applied to session | Setting applied correctly | ✅ PASS |
| UI shows persisted setting | Toggle reflects file state | UI correctly shows persisted state | ✅ PASS |
**Overall**: 9/9 tests PASSED ✅
## Lessons Learned
### 1. Full Public Key Requirement
**Discovery**: Different contact types (CLI, ROOM, REP, SENS) have different matching behaviors in meshcli:
- CLI contacts accept name prefix, key prefix, or full key
- ROOM contacts only accept full public key
**Solution**: Always use full public_key for approval to ensure universal compatibility.
**Code Pattern**:
```javascript
// Good - works for all contact types
body: JSON.stringify({
public_key: contact.public_key // Full key from GET /pending_contacts
})
// Bad - may fail for ROOM contacts
body: JSON.stringify({
selector: contact.name // Won't work for ROOMs
})
```
### 2. Settings Persistence Architecture
**Decision**: File-based persistence vs environment variables
**Chosen**: File-based persistence in volume-mounted directory
- ✅ User can change settings via UI
- ✅ Settings survive container restart
- ✅ No need to edit docker-compose.yml
- ✅ Future-proof for additional settings
**Alternative Rejected**: Environment variables
- ❌ Would require editing docker-compose.yml
- ❌ Would require container restart to apply
- ❌ User cannot change from UI
### 3. Settings Application Timing
**Challenge**: When to apply manual_add_contacts setting?
**Solution**: Dual application
1. **On bridge startup**: Read .webui_settings.json and apply to new session
2. **On user toggle**: Write to file AND apply to running session immediately
**Benefit**: User sees immediate effect without restart, but setting also persists.
### 4. Mobile-First Design Principles
**Applied**:
- Touch-friendly buttons (min-height: 44px)
- Large tap targets for icons
- Responsive card layout
- Toast notifications at bottom-right (thumb-accessible)
- Truncated keys with copy option (avoid horizontal scroll)
**Result**: UI works well on both desktop and mobile browsers.
### 5. Error Handling Patterns
**Pattern**: Always revert UI on failure
```javascript
async function handleApprovalToggle(event) {
const enabled = event.target.checked;
try {
// ...attempt to save
if (data.success) {
// Success - keep new state
} else {
// Failure - revert toggle
event.target.checked = !enabled;
showToast('Failed: ' + data.error, 'danger');
}
} catch (error) {
// Network error - revert toggle
event.target.checked = !enabled;
showToast('Network error', 'danger');
}
}
```
**Benefit**: UI always reflects actual server state.
### 6. Info Badge UX Pattern
**Discovery**: When manual approval is OFF, pending list is always empty (confusing to users)
**Solution**: Show info badge when manual approval is disabled:
```html
<div class="info-badge" id="approvalInfo" style="display: none;">
<i class="bi bi-info-circle"></i>
Pending contacts will only appear when manual approval is enabled.
</div>
```
**Result**: Users understand why pending list is empty.
## Documentation Updates
### README.md
- Added "Contact Management" to Key Features list
- Added comprehensive "Contact Management" section in Usage
- Renamed old section to "Managing Contacts (Cleanup)" to distinguish from new feature
### .claude/instructions.md
- Added 4 new API endpoints to reference
- Added 4 new meshcli commands (pending_contacts, add_pending, get/set manual_add_contacts)
- Updated Project Structure to include contacts.html and contacts.js
- Added "Persistent Settings" section explaining .webui_settings.json
### New Documentation Files
- This file: `technotes/UI-Contact-Management-MVP-v1-completed.md`
## Future Considerations
### 1. Additional Settings
The `.webui_settings.json` mechanism is designed to be extensible:
```json
{
"manual_add_contacts": true,
"future_setting_1": false,
"future_setting_2": "value"
}
```
### 2. Batch Operations
Currently, users must approve contacts one at a time. Future enhancement:
- "Approve All" button
- Checkbox selection for batch approval
### 3. Contact Preview
Before approval, show additional contact metadata:
- Contact type (CLI, ROOM, REP, SENS)
- First seen timestamp
- Number of connection attempts
### 4. Deny/Block Functionality
Currently, pending contacts remain pending until approved. Future enhancement:
- "Deny" button to permanently block a contact
- Blacklist management
### 5. Settings Export/Import
Allow users to export/import `.webui_settings.json` for backup or migration to other devices.
### 6. Real-Time Updates
Currently, users must click "Refresh" to see new pending contacts. Future enhancement:
- WebSocket for real-time pending contacts updates
- Auto-refresh every N seconds (configurable)
## Git Commit
**Branch**: dev-2
**Commit**: 77c72ba
**Message**:
```
feat(ui): Add Contact Management MVP with persistent settings
Implements complete Contact Management UI module as specified in
docs/UI-Contact-Management-MVP-v1.md:
Backend (meshcore-bridge):
- Added .webui_settings.json persistence mechanism
- Modified session init to read and apply user settings
- Added POST /set_manual_add_contacts endpoint
- Default: manual_add_contacts=off (meshcli factory default)
Backend (mc-webui):
- Added 4 new CLI wrapper functions (get_pending_contacts, approve_pending_contact, get/set settings)
- Added 4 new API endpoints (/api/contacts/pending, /api/contacts/pending/approve, /api/device/settings)
- Added /contacts/manage route
Frontend:
- Created contacts.html template (mobile-first responsive design)
- Created contacts.js (settings toggle, pending list, approve/copy buttons)
- Added "Contact Management" to side menu
- Toast notifications for user feedback
Features:
- Manual contact approval toggle (persistent across container restarts)
- Pending contacts list with name and truncated public key
- Approve button (sends full public_key for compatibility with all contact types)
- Copy full public key to clipboard
- Mobile-first UI (touch-friendly, Bootstrap 5)
- Real-time feedback (loading/empty/error states)
Persistence:
- Settings saved to MC_CONFIG_DIR/.webui_settings.json
- File persists in Docker volume across container restarts
- Bridge reads settings on startup and applies to meshcli session
- UI changes immediately affect both file and running session
Testing:
- Approved contact "Szczwany-lis🔥" successfully via UI
- Contact appeared in contacts list (verified via API)
- Settings persisted across container restart (verified)
Documentation:
- Updated README.md with Contact Management section
- Updated .claude/instructions.md with new endpoints and commands
```
## Conclusion
Successfully implemented Contact Management MVP v1, meeting all requirements:
**Functional Requirements**:
- Manual approval toggle (persistent across restarts)
- Pending contacts list (name + public key)
- Approve action (uses full public_key for compatibility)
- Copy to clipboard functionality
- Mobile-first responsive UI
- Side menu integration
**Non-Functional Requirements**:
- Settings persist across container restarts (.webui_settings.json)
- Settings stored in volume-mounted MC_CONFIG_DIR
- Backward compatible (defaults to meshcli factory settings)
- Real-time user feedback (loading states, toast notifications)
**Testing**:
- Basic approval workflow tested and working
- Settings persistence verified across container restart
- All edge cases handled (network errors, approval failures)
**Documentation**:
- README.md updated
- .claude/instructions.md updated
- Technical note created (this file)
**Status**: Ready for production use in dev-2 branch.
**Next Steps**: User can test in real-world scenarios and provide feedback for future iterations.
File diff suppressed because it is too large Load Diff
-342
View File
@@ -1,342 +0,0 @@
# meshcore-cli
meshcore-cli : CLI interface to MeschCore companion app over BLE, TCP or Serial
## About
meshcore-cli is a tool that connects to your companion radio node (meshcore client) over BLE, TCP or Serial and lets you interact with it from a terminal using a command line interface.
You can send commands as parameters to the meshcore-cli command (from your shell) either interactively or through a script.
There is also an interactive mode (this is the default when no command is passed). In interactive mode you can enter a contact (another client a repeater, a sensor or a room) and interact with it. For clients, interaction consists in sending/receiving messages. For repeaters, rooms or sensors it will directly give you the remote cli (you can still send messages to rooms using double quote prefix or msg command).
Note that meshcore-cli only interacts with companion radios (through BLE, Serial or TCP), you can't connect to a repeater using its serial interface.
Also, most meshcore companions only have one interface compiled in at a time. So you can't connect via Serial to a node, which has been compiled as a BLE companion.
## Install
Meshcore-cli depends on the [python meshcore](https://github.com/fdlamotte/meshcore_py) package. You can install both via `pip` or `pipx` using the command:
<pre>
$ pipx install meshcore-cli
</pre>
It will install you `meshcore-cli` and `meshcli`, which is an alias to the former.
You can use the flake under [nix](https://nixos.org/):
<pre>
$ nix run github:meshcore-dev/meshcore-cli#meshcore-cli
</pre>
If you want meshcore-cli to remember last BLE device, you should have some `$HOME/.config/meshcore` where configuration for meschcore-cli will be stored (if not it will use first device it finds).
## Usage
<pre>
$ meshcli &lt;args&gt; &lt;commands&gt;
</pre>
If using BLE, don't forget to pair your device first (using `bluetoothctl` for instance on Linux) or meshcli won't be able to communicate. There is a device selector for BLE, you'll just have to use `meshcli -S` to select your device, subsequent calls to meshcli will be send to that device.
### Configuration
Configuration files are stored in `$HOME/.config/meshcore`
If the directory exists, default ble address and history will be stored there.
If there is an initialization script file called `init`, it will be executed just before the commands provided on command line are executed (and after evaluation of the arguments).
Init files can also be defined for a given device, meshcore-cli will look for `&lt;device-name>.init` file in configuration directory (usefull to specify timeout for contacts that are behind bridges with `contact_timeout` command).
### Arguments
Arguments mostly deals with connection to the node
<pre>
-h : prints this help
-v : prints version
-j : json output (disables init file)
-D : debug
-S : scan for devices and show a selector
-l : list available ble/serial devices and exit
-T &lt;timeout&gt; : timeout for the ble scan (-S and -l) default 2s
-a &lt;address&gt; : specifies device address (can be a name)
-d &lt;name&gt; : filter meshcore devices with name or address
-P : forces pairing via the OS
-t &lt;hostname&gt; : connects via tcp/ip
-p &lt;port&gt; : specifies tcp port (default 5000)
-s &lt;port&gt; : use serial port &lt;port&gt;
-b &lt;baudrate&gt; : specify baudrate
-C : toggles classic mode for prompt
-c &lt;on/off&gt; : disables most of color output if off
</pre>
### Available Commands
Commands are given after arguments, they can be chained and some have shortcuts. Also prefixing a command with a dot `.` will force it to output json instead of synthetic result.
<pre>
?&lt;cmd&gt; may give you some more help about cmd
General commands
chat : enter the chat (interactive) mode
chat_to &lt;ct&gt; : enter chat with contact to
script &lt;filename&gt; : execute commands in filename
infos : print informations about the node i
self_telemetry : print own telemtry t
card : export this node URI e
ver : firmware version v
reboot : reboots node
sleep &lt;secs&gt; : sleeps for a given amount of secs s
wait_key : wait until user presses &lt;Enter&gt; wk
apply_to &lt;f&gt; &lt;cmds&gt; : sends cmds to contacts matching f at
Messaging
msg &lt;name&gt; &lt;msg&gt; : send message to node by name m {
wait_ack : wait an ack wa }
chan &lt;nb&gt; &lt;msg&gt; : send message to channel number &lt;nb&gt; ch
public &lt;msg&gt; : send message to public channel (0) dch
recv : reads next msg r
wait_msg : wait for a message and read it wm
sync_msgs : gets all unread msgs from the node sm
msgs_subscribe : display msgs as they arrive ms
get_channels : prints all channel info
get_channel &lt;n&gt; : get info for channel (by number or name)
set_channel n nm k : set channel info (nb, name, key)
remove_channel &lt;n&gt; : remove channel (by number or name)
scope &lt;s&gt; : sets node's flood scope
Management
advert : sends advert a
floodadv : flood advert
get &lt;param&gt; : gets a param, \"get help\" for more
set &lt;param&gt; &lt;value&gt; : sets a param, \"set help\" for more
time &lt;epoch&gt; : sets time to given epoch
clock : get current time
clock sync : sync device clock st
node_discover &lt;filter&gt; : discovers nodes based on their type nd
Contacts
contacts / list : gets contact list lc
reload_contacts : force reloading all contacts rc
contact_info &lt;ct&gt; : prints information for contact ct ci
contact_timeout &lt;ct&gt; v : sets temp default timeout for contact
share_contact &lt;ct&gt; : share a contact with others sc
export_contact &lt;ct&gt; : get a contact's URI ec
import_contact &lt;URI&gt; : import a contact from its URI ic
remove_contact &lt;ct&gt; : removes a contact from this node
path &lt;ct&gt; : diplays path for a contact
disc_path &lt;ct&gt; : discover new path and display dp
reset_path &lt;ct&gt; : resets path to a contact to flood rp
change_path &lt;ct&gt; &lt;pth&gt; : change the path to a contact cp
change_flags &lt;ct&gt; &lt;f&gt; : change contact flags (tel_l|tel_a|star)cf
req_telemetry &lt;ct&gt; : prints telemetry data as json rt
req_mma &lt;ct&gt; : requests min/max/avg for a sensor rm
req_acl &lt;ct&gt; : requests access control list for sensor
pending_contacts : show pending contacts
add_pending &lt;pending&gt; : manually add pending contact
flush_pending : flush pending contact list
Repeaters
login &lt;name&gt; &lt;pwd&gt; : log into a node (rep) with given pwd l
logout &lt;name&gt; : log out of a repeater
cmd &lt;name&gt; &lt;cmd&gt; : sends a command to a repeater (no ack) c [
wmt8 : wait for a msg (reply) with a timeout ]
req_status &lt;name&gt; : requests status from a node rs
req_neighbours &lt;name&gt; : requests for neighbours in binary form rn
trace &lt;path&gt; : run a trace, path is comma separated
</pre>
### Interactive Mode
aka Instant Message or chat mode ...
Chat mode lets you interactively interact with your node or remote nodes. It is automatically triggered when no option is given on the command line.
You'll get a prompt with the name of your node. From here you can type meshcore-cli commands. The prompt has history and a basic completion (pressing tab will display possible command or argument options).
The `to` command is specific to chat mode, it lets you enter the recipient for next command. By default you're on your node but you can enter other nodes or public rooms. Here are some examples :
- `to <dest>` : will enter dest (node or channel)
- `to /`, `to ~` : will go to the root (your node)
- `to ..` : will go to the last node (it will switch between the two last nodes, this is just a 1-depth history)
- `to !` : will switch to the node you received last message from
When you are in a node, the behaviour will depend on the node type, if you're on a chat node, it will send messages by default and you can chat. On a repeater or a room server, it will send commands (autocompletion has been set to comply with the CommonCli class of meshcore). To send a message through a room you'll have to prefix the message with a quote or use the send command.
The `/` character is used to bypass the node you have currently selected using `to`:
- `/<cmd>` issues cmd command on the root
- `/<node>/<cmd>` will send cmd to selected node
- `/<dest> <msg>` will send msg to dest (channel or node)
#### Flood Scope in interactive mode
Flood scope has recently been introduced in meshcore (from `v1.10.0`). It limits the scope of packets to regions, using transport codes in the frame.
When entering chat mode, scope will be reset to `*`, meaning classic flood.
You can switch scope using the `scope` command, or postfixing the `to` command with `%<scope>`.
Scope can also be applied to a command using `%` before the scope name. For instance `login%#Morbihan` will limit diffusion of the login command (which is usually sent flood to get the path to a repeater) to the `#Morbihan` region.
#### Channel echoes
It's sometimes interesting to know the path taken by a message received from a channel or which repeaters have repeated a sent message.
The app give you the information by listening `rx_log` from the device, when obtained the information is attached to the message and can be read.
In meshcore-cli I went lower-level by implementing channel echoes. When activated (with `/set channel_echoes on`), all the channel messages will be printed on the terminal along with the SNR and path taken. When sending a message, you'll have all the repeats from 0-hop repeaters as echoes, and when a message is received, you should see information about the received message, but also all the instances of the same message that might have reached you from another path.
In the example below, a msg has been sent between two repeaters, 21 and 25. 25 repeated the message and 21 the repeat and both echoes came back to the node with different SNRs.
```
f1down/#fdl|*> 8
#fdl f1down: 8 [25] -4.75-112
#fdl f1down: 8 [2521] 1.00-109
```
### Contact management
To receive a message from another user, it is necessary to have its public key. This key is stored on a contact list in the device, and this list has a finite size (50 when meshcore started, now over 350 for most devices).
By default contacts are automatically added to the device contact list when an advertisement is received, so as soon as you receive an advert, you can talk with your buddy.
With growing number of users, it becomes necessary to manage contact list and one of the ways is to add contacts manually to the device. This is done by turning on `manual_add_contacts`. Once this option has been turned on, a pending list is built by meshcore-cli from the received adverts. You can view the list issuing a `pending_contacts` command, flush the list using `flush_pending` or add a contact from the list with `add_pending` followed by the key of the contact or its name (both will be auto-completed with tab).
This feature only really works in interactive mode.
Note: There is also an `auto_update_contacts` setting that has nothing to do with adding contacts, it permits to automatically sync contact lists between device and meshcore-cli (when there is an update in name, location or path).
### Issuing batch commands to contacts with apply to
`apply_to <f> <cmd>` : applies cmd to contacts matching filter `<f>` it can be used to apply the same command to a pool of repeaters, or remove some contacts matching a condition.
Filter is constructed with comma separated fields :
- `u`, matches modification time `<` or `>` than a timestamp (can also be days hours or minutes ago if followed by `d`,`h` or `m`)
- `t`, matches the type (1: client, 2: repeater, 3: room, 4: sensor)
- `h`, matches number of hops
- `d`, direct, similar to `h>-1`
- `f`, flood, similar to `h<0` or `h=-1`
Commands should be written as if in interactive mode, if writing from the commandline don't forget to use commas to clearly delimit fields.
Note: Some commands like `contact_name` (aka `cn`), `reset_path` (aka `rp`), `forget_password` (aka `fp`) can be chained. There is also a `sleep` command taking an optional time parameter. The sleep will be issued after the command, it helps limiting rate through repeaters ...
#### Examples
```
# removes all clients that have not been updated in last 2 days
at u<2d,t=1 remove_contact
# gives traces to repeaters that have been updated in the last 24h and are direct
at t=2,u>1d,d cn trace
# tries to do flood login to all repeaters
at t=2 rp login
```
## Examples
<pre>
# gets info from first ble MC device it finds (was -s but now used for serial port)
$ meshcore-cli -d "" infos
INFO:meshcore:Scanning for devices
INFO:meshcore:Found device : C2:2B:A1:D5:3E:B6: MeshCore-t114_fdl
INFO:meshcore:BLE Connection started
{
"adv_type": 1,
"tx_power": 22,
"max_tx_power": 22,
"public_key": "993acd42fc779962c68c627829b32b111fa27a67d86b75c17460ff48c3102db4",
"adv_lat": 47.794,
"adv_lon": -3.428,
"radio_freq": 869.525,
"radio_bw": 250.0,
"radio_sf": 11,
"radio_cr": 5,
"name": "t114_fdl"
}
# getting time
$ meshcli -a C2:2B:A1:D5:3E:B6 clock
INFO:meshcore:BLE Connection started
Current time : 2025-04-18 08:19:26 (1744957166)
# If you're familiar with meshcli, you should have noted that
# now output is not json only, to get json output, use -j
# or prefix your commands with a dot
$ meshcli -a C2:2B:A1:D5:3E:B6 .clock
INFO:meshcore:BLE Connection started
{
"time": 1744957249
}
# Using -j, meshcli will return replies in json format ...
$ meshcli -j -a C2:2B:A1:D5:3E:B6 clock
{
"time": 1744957261
}
# So if I reboot the node, and want to set time, I can chain the commands
# and get that kind of output (even better by feeding it to jq)
$ meshcli reboot
INFO:meshcore:BLE Connection started
$ meshcli -j clock clock sync clock | jq -c
{ "time": 1715770371 }
{ "ok": "time synced" }
{ "time": 1745996105 }
# Now check if time is ok with human output (I don't read epoch time yet)
$ meshcli clock
INFO:meshcore:BLE Connection started
Current time : 2025-04-30 08:56:27 (1745996187)
# Now you'll probably want to send some messages ...
# For that, there is the msg command, wait_ack
$ meshcli msg Techo_fdl "Hello T-Echo" wa
INFO:meshcore:BLE Connection started
Msg acked
# I can check the message on the techo
$ meshcli -d Techo sm
INFO:meshcore:Scanning for devices
INFO:meshcore:Found device : DE:B6:D0:68:D5:62: MeshCore-Techo_fdl
INFO:meshcore:BLE Connection started
t114_fdl(0): Hello T-Echo
# And reply using json output for more verbosity
# here I've used jq with -cs to get a compact array
$ meshcli msg t114_fdl hello wa | jq -cs
[{"type":0,"expected_ack":"4802ed93","suggested_timeout":2970},{"code":"4802ed93"}]
# But this could have been done interactively using the chat mode
# Here from the techo side. Note that un-acked messages will be
# signaled with an ! at the start of the prompt (or red color in color mode)
$ meshcli chat
INFO:meshcore:BLE Connection started
Interactive mode, most commands from terminal chat should work.
Use "to" to selects contact, "list" to list contacts, "send" to send a message ...
Line starting with "$" or "." will issue a meshcli command.
"quit" or "q" will end interactive mode
t114_fdl(D): Hello T-Echo
EnsibsRoom> Hi
!EnsibsRoom> to t114_fdl
t114_fdl> Hi
t114_fdl(D): It took you long to reply ...
t114_fdl> I forgot to set the recipient with the to command
t114_fdl(D): It happens ...
t114_fdl>
# Loging into repeaters and sending commands is also possible
# directly from the chat, because we can use meshcli commands ;)
$ meshcli chat (pending msgs are shown at connexion ...)
INFO:meshcore:BLE Connection started
Interactive mode, most commands from terminal chat should work.
Use "to" to selects contact, "list" to list contacts, "send" to send a message ...
Line starting with "$" or "." will issue a meshcli command.
"quit" or "q" will end interactive mode
Techo_fdl(0): Cool to receive some msgs from you
Techo_fdl(D): Hi
Techo_fdl(D): I forgot to set the recipient with the to command
FdlRoom> login password
Login success
FdlRoom> clock
FdlRoom(0): 06:40 - 18/4/2025 UTC
FdlRoom>
</pre>
-515
View File
@@ -1,515 +0,0 @@
# Pending Contacts API - Technical Notes
## Overview
This document describes the implementation of pending contacts management API in `meshcore-bridge`. This feature enables manual approval of new contacts when `manual_add_contacts` mode is enabled in meshcli.
**Branch**: `dev-2`
**Status**: Implemented ✅ (API only, no UI yet)
**Date**: 2025-12-29
## Problem Statement
When meshcli runs with `manual_add_contacts on`, new contacts attempting to connect are placed in a "pending" state instead of being automatically added to the contacts list. This provides security benefits:
- **Control over network access** - Only approved contacts can communicate with the node
- **Prevention of spam/unwanted contacts** - Filter out random nodes attempting connection
- **Explicit trust model** - User decides who to trust on the mesh network
However, managing pending contacts required manual meshcli commands, which was inconvenient for web interface users.
## Solution
Added two new HTTP endpoints to `meshcore-bridge` for programmatic pending contact management:
1. **`GET /pending_contacts`** - List all contacts awaiting approval
2. **`POST /add_pending`** - Approve and add a specific pending contact
Both endpoints use the existing persistent meshcli session architecture (no new processes spawned).
## Implementation Details
### 1. Session Initialization
Modified `_init_session_settings()` in [meshcore-bridge/bridge.py:122-136](meshcore-bridge/bridge.py#L122-L136) to enable manual contact approval:
```python
def _init_session_settings(self):
"""Configure meshcli session for advert logging, message subscription, and manual contact approval"""
if self.process and self.process.stdin:
self.process.stdin.write('set json_log_rx on\n')
self.process.stdin.write('set print_adverts on\n')
self.process.stdin.write('set manual_add_contacts on\n') # NEW
self.process.stdin.write('msgs_subscribe\n')
self.process.stdin.flush()
```
**Why in init?**
- Persistent setting - applies to entire session lifetime
- No need to re-enable after watchdog restart
- Consistent behavior across all API calls
### 2. GET /pending_contacts Endpoint
**Location**: [meshcore-bridge/bridge.py:499-565](meshcore-bridge/bridge.py#L499-L565)
**Purpose**: Retrieve list of contacts awaiting manual approval
**Request**:
```http
GET /pending_contacts HTTP/1.1
Host: meshcore-bridge:5001
```
**Response** (success):
```json
{
"success": true,
"pending": [
{
"name": "Skyllancer",
"public_key": "f9ef123abc456..."
},
{
"name": "KRA Reksio mob2🐕",
"public_key": "41d5789def012..."
}
],
"raw_stdout": "Skyllancer: f9ef123abc456...\nKRA Reksio mob2🐕: 41d5789def012..."
}
```
**Response** (no pending contacts):
```json
{
"success": true,
"pending": [],
"raw_stdout": ""
}
```
**Response** (error):
```json
{
"success": false,
"error": "meshcli session not initialized",
"pending": []
}
```
**Implementation Notes**:
- Executes `pending_contacts` command via `MeshCLISession.execute_command()`
- Parses meshcli output format: `"ContactName: <hex_public_key>"`
- Removes spaces from public key hex (meshcli may insert spaces for readability)
- Only parses lines containing colon `:`
- Trims whitespace from contact names
- Returns empty array if no pending contacts exist
- Includes `raw_stdout` for debugging/troubleshooting
**Parsing Logic**:
```python
for line in stdout.split('\n'):
line = line.strip()
if ':' in line:
parts = line.split(':', 1)
if len(parts) == 2:
name = parts[0].strip()
public_key = parts[1].strip().replace(' ', '')
if name and public_key:
pending.append({'name': name, 'public_key': public_key})
```
### 3. POST /add_pending Endpoint
**Location**: [meshcore-bridge/bridge.py:568-629](meshcore-bridge/bridge.py#L568-L629)
**Purpose**: Approve and add a pending contact to the contacts list
**Request**:
```http
POST /add_pending HTTP/1.1
Host: meshcore-bridge:5001
Content-Type: application/json
{
"selector": "Skyllancer"
}
```
**Selector formats supported** (by meshcli):
- Full contact name: `"Skyllancer"` (works for CLI contacts)
- Public key prefix: `"f9ef123"` (works for CLI contacts, may not work for ROOM)
- Full public key: `"f9ef123abc456..."` (**RECOMMENDED - works for all contact types**)
**Response** (success):
```json
{
"success": true,
"stdout": "Contact added successfully",
"stderr": "",
"returncode": 0
}
```
**Response** (validation error):
```json
{
"success": false,
"stdout": "",
"stderr": "selector must be a non-empty string",
"returncode": -1
}
```
**Implementation Notes**:
- Validates `selector` is non-empty string
- Trims whitespace from selector before execution
- Executes `add_pending <selector>` command via persistent session
- Returns full command result (stdout, stderr, returncode)
- Uses default timeout (10 seconds)
**Validation**:
```python
if not isinstance(selector, str) or not selector.strip():
return jsonify({
'success': False,
'stderr': 'selector must be a non-empty string',
'returncode': -1
}), 400
```
### Important Discovery: Contact Type Differences
**🔍 Testing revealed different behavior for different contact types:**
#### CLI Contacts (Clients)
**Examples**: `StNMobile T1000e`, `ML2056`, `olekstomek`
✅ **All selector formats work:**
- Full name: `"StNMobile T1000e"` → **SUCCESS**
- Name prefix: `"StN"` → **SUCCESS**
- Public key prefix: `"2ce5514"` → **SUCCESS**
- Full public key: `"2ce5514826d39a44d23eb8eb9539676b57cae234528573c45d44bdd6ed01eeb5"` → **SUCCESS**
#### ROOM Contacts (Group Rooms)
**Examples**: `TK room cwiczebny🔆`
❌ **Name-based selectors DO NOT work:**
- Full name: `"TK room cwiczebny🔆"`**FAILED** (also UTF-8 encoding issues in shell)
- Name prefix: `"TK room"` → **FAILED**
❌ **Public key prefix DOES NOT work:**
- Prefix: `"b3fec489"` → **FAILED**
✅ **Only FULL public key works:**
- Full public key: `"b3fec489e1ee2d0277bd16de957253c8f5aa44a721df722224bbdc1edc5829b6"` → **SUCCESS**
#### Root Cause Analysis
The `add_pending` command in meshcli appears to use different matching logic for different contact types:
- **CLI contacts**: Flexible matching - accepts name prefix, key prefix, or full key
- **ROOM contacts**: Strict matching - requires exact full public key
This may be intentional behavior to prevent accidental approval of group rooms, which have different security/privacy implications than individual client contacts.
#### Recommendation for UI Implementation
**Always use full public key for `add_pending` calls:**
```javascript
// Good - works for all contact types
fetch('/add_pending', {
method: 'POST',
body: JSON.stringify({
selector: pendingContact.public_key // Full key from GET /pending_contacts
})
})
// Bad - may fail for ROOM contacts
fetch('/add_pending', {
method: 'POST',
body: JSON.stringify({
selector: pendingContact.name // Won't work for ROOMs
})
})
```
**UI Design Consideration:**
Since `GET /pending_contacts` already returns both `name` and `public_key`, the UI should:
1. Display human-readable name for user
2. **Send full public_key to API** (not name)
3. This ensures compatibility with all contact types
Example UI flow:
```
User sees: "TK room cwiczebny🔆" [Approve button]
User clicks: Approve
Frontend sends: {"selector": "b3fec489e1ee...5829b6"} ← Full public key
```
## Testing
### Prerequisites
The bridge container must have:
1. `manual_add_contacts on` enabled (automatic in session init)
2. Pending contacts available (requires other nodes trying to connect)
### Test Commands
**From host or inside mc-webui container**:
```bash
# List pending contacts
curl -s http://meshcore-bridge:5001/pending_contacts | jq
# Add a CLI contact by name (works for CLI, not ROOM)
curl -s -X POST http://meshcore-bridge:5001/add_pending \
-H 'Content-Type: application/json' \
-d '{"selector":"ML2056"}' | jq
# Add by full public key (RECOMMENDED - works for all types)
curl -s -X POST http://meshcore-bridge:5001/add_pending \
-H 'Content-Type: application/json' \
-d '{"selector":"b3fec489e1ee2d0277bd16de957253c8f5aa44a721df722224bbdc1edc5829b6"}' | jq
```
**Real-world test results** (2025-12-29):
1. **CLI contacts** - flexible matching:
```bash
# All worked successfully
curl -d '{"selector":"StNMobile T1000e"}' → ✅ SUCCESS
curl -d '{"selector":"ML2056"}' → ✅ SUCCESS
curl -d '{"selector":"2ce5514"}' → ✅ SUCCESS (prefix)
```
2. **ROOM contact** - strict matching:
```bash
# Failed attempts
curl -d '{"selector":"TK room cwiczebny🔆"}' → ❌ FAILED (UTF-8 encoding)
curl -d '{"selector":"TK room"}' → ❌ FAILED (name prefix)
curl -d '{"selector":"b3fec489"}' → ❌ FAILED (key prefix)
# Success
curl -d '{"selector":"b3fec489e1ee...5829b6"}' → ✅ SUCCESS (full key)
```
**Expected workflow**:
1. New node attempts connection
2. `GET /pending_contacts` shows the node in pending list with `name` and `public_key`
3. `POST /add_pending` with **full `public_key`** (not name!) approves the contact
4. `GET /pending_contacts` no longer shows the approved contact (moved to regular contacts)
**Best Practice**: Always use full `public_key` from the `GET /pending_contacts` response to ensure compatibility with all contact types (CLI, ROOM, REP, SENS).
## Architecture Benefits
### Reuses Persistent Session
- No new subprocess spawning
- Uses existing command queue (FIFO serialization)
- Same event-based synchronization mechanism
- Consistent error handling with other endpoints
### Thread-safe
- Commands queued through `queue.Queue()`
- Protected by `pending_lock` during response handling
- No race conditions with other CLI commands
### Consistent with /cli Endpoint
- Same request/response format
- Same timeout handling
- Same error reporting structure
## Future Work
### UI Integration (Next Phase)
**Planned features**:
1. **Pending Contacts Badge** - Notification icon showing count of pending contacts
2. **Pending Contacts Modal** - List view with approve/reject buttons
3. **Auto-refresh** - Poll `/pending_contacts` every 30-60 seconds
4. **Notifications** - Toast when new pending contacts appear
**UI Mockup**:
```
┌─────────────────────────────────────┐
│ Pending Contact Requests [X] │
├─────────────────────────────────────┤
│ Skyllancer │
│ f9ef123abc... │
│ [Approve] [Reject] │
├─────────────────────────────────────┤
│ KRA Reksio mob2🐕 │
│ 41d5789def... │
│ [Approve] [Reject] │
└─────────────────────────────────────┘
```
### Additional API Endpoints (Consideration)
**`POST /reject_pending`** - Reject a pending contact
```json
{
"selector": "Skyllancer"
}
```
**`DELETE /pending_contacts`** - Flush all pending contacts
- Executes `flush_pending` meshcli command
- Useful for mass-reject after scanning for unwanted connections
### Settings Integration
Add UI toggle in Settings modal:
```
☑ Manual Contact Approval
Require explicit approval for new contacts
```
This would execute:
```bash
# Enable
curl -X POST /cli -d '{"args":["set","manual_add_contacts","on"]}'
# Disable
curl -X POST /cli -d '{"args":["set","manual_add_contacts","off"]}'
```
## Security Considerations
### Why Manual Contact Approval?
1. **DoS Prevention** - Prevents flooding with fake contact requests
2. **Network Privacy** - Control who can see your node
3. **Trust Model** - Explicit approval creates stronger trust relationships
4. **Spam Filtering** - Reject unwanted contact attempts
### Trade-offs
**Pros**:
- Enhanced security and privacy
- User controls network access
- Prevents automatic addition of random nodes
**Cons**:
- Requires user interaction for every new contact
- May miss legitimate contacts if user doesn't check pending list regularly
- Additional management overhead
### Recommendation
**Use manual approval when**:
- Running on public/shared networks
- Privacy is a concern
- Network has history of spam/unwanted contacts
- Small, controlled mesh network
**Use auto-approval (default) when**:
- Private/trusted network environment
- Ease of use is priority
- Large mesh network where manual approval is impractical
## Meshcli Command Reference
### pending_contacts
```bash
meshcli> pending_contacts
Skyllancer: f9ef123abc456def789012345678901234567890abcdef
KRA Reksio mob2🐕: 41d5789def012345678901234567890abcdefabcdef012
```
### add_pending
```bash
# By name
meshcli> add_pending Skyllancer
Contact added successfully
# By public key prefix (first few chars)
meshcli> add_pending f9ef123
Contact added successfully
# By full public key
meshcli> add_pending f9ef123abc456def789012345678901234567890abcdef
Contact added successfully
```
### flush_pending
```bash
meshcli> flush_pending
All pending contacts removed
```
### Manual mode toggle
```bash
# Enable manual approval
meshcli> set manual_add_contacts on
manual_add_contacts set to on
# Disable (auto-approve)
meshcli> set manual_add_contacts off
manual_add_contacts set to off
# Check current setting
meshcli> get manual_add_contacts
manual_add_contacts: on
```
## Deployment
### Files Modified
1. **meshcore-bridge/bridge.py**
- Added `manual_add_contacts on` to session init ([line 131](meshcore-bridge/bridge.py#L131))
- Added `GET /pending_contacts` endpoint ([lines 499-565](meshcore-bridge/bridge.py#L499-L565))
- Added `POST /add_pending` endpoint ([lines 568-629](meshcore-bridge/bridge.py#L568-L629))
2. **Dockerfile**
- Added `curl` package for testing ([line 7](Dockerfile#L7))
3. **README.md**
- Added "Testing Bridge API" section ([lines 362-406](README.md#L362-L406))
- Documented endpoints with examples
### Commit
**Branch**: `dev-2`
**Commit**: `815adb5` - "feat(bridge): Add pending contacts management API endpoints"
### Deployment Steps
```bash
# On server
cd ~/mc-webui
git fetch
git checkout dev-2
git pull origin dev-2
docker compose up -d --build
```
### Verification
```bash
# Check logs for successful init
docker compose logs meshcore-bridge | grep manual_add_contacts
# Should see: "Session settings applied: ... manual_add_contacts=on ..."
# Test endpoint
docker exec mc-webui curl -s http://meshcore-bridge:5001/pending_contacts | jq
```
## References
- **meshcore-cli documentation**: [technotes/meshcore-cli.md](technotes/meshcore-cli.md)
- **Persistent session architecture**: [technotes/persistent-meshcli-session.md](technotes/persistent-meshcli-session.md)
- **Meshcli command reference**: `meshcli -h | grep pending` or `meshcli> ? pending_contacts`
---
**Author**: Claude Code (Anthropic)
**Date**: 2025-12-29
**Status**: API Implemented ✅ | UI Pending ⏳
-504
View File
@@ -1,504 +0,0 @@
# Persistent meshcli Session Architecture - Technical Notes
## Overview
This document describes the architectural refactor from per-request subprocess spawning to a **persistent meshcli session** in the `meshcore-bridge` container. This fundamental change enables real-time message reception, advert logging, and advanced features like pending contact management.
## Previous Architecture (Before Refactor)
### How it Worked
The original `meshcore-bridge` implementation used **subprocess.run()** for each HTTP request:
```python
def run_meshcli_command(args, timeout=DEFAULT_TIMEOUT):
result = subprocess.run(
['meshcli', '-s', MC_SERIAL_PORT] + args,
capture_output=True,
text=True,
timeout=timeout
)
return result
```
### Limitations
1. **Serial Port Conflicts** - Each command spawned a new meshcli process, risking USB device locking
2. **No Real-time Messages** - Required periodic `recv` polling (inefficient, 30-60s delay)
3. **No Advert Logging** - JSON adverts from the mesh network were discarded
4. **No Interactive Features** - Commands like `msgs_subscribe` or `manual_add_contacts` require persistent session
5. **Higher Overhead** - Process spawn/teardown for every command added latency
### Why Change Was Needed
User reported: **"od czasu zmian, czyli od ponad 1.5 godziny, nie dotarła ANI JEDNA wiadomość"**
In non-interactive mode (subprocess.run), meshcli doesn't automatically receive new messages. The `recv` command only reads what's already in the `.msgs` file, it doesn't fetch NEW messages from the radio.
## New Architecture (Persistent Session)
### Core Concept
Instead of spawning a new process per request, the bridge maintains a **single long-lived meshcli process** with:
- **stdin pipe** - Send commands
- **stdout pipe** - Receive responses and adverts
- **stderr pipe** - Monitor errors
### Key Components
#### 1. MeshCLISession Class
The `MeshCLISession` class encapsulates the entire persistent session:
```python
class MeshCLISession:
def __init__(self, serial_port, config_dir, device_name):
self.process = subprocess.Popen(
['meshcli', '-s', serial_port],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1 # Line-buffered
)
```
#### 2. Worker Threads (4 Concurrent Threads)
**a) stdout_thread** - Reads stdout line-by-line
- Parses each line as JSON
- If `payload_typename == "ADVERT"` → log to `.adverts.jsonl`
- Otherwise → append to current CLI command response buffer
**b) stderr_thread** - Reads stderr and logs errors
- Monitors `meshcli stderr: ...` messages
- TTY errors are harmless (meshcli tries to use terminal features that don't exist in pipes)
**c) stdin_thread** - Sends queued commands to stdin
- Pulls commands from thread-safe `queue.Queue`
- Writes to `process.stdin`
- Starts timeout monitor thread for each command
**d) watchdog_thread** - Monitors process health
- Checks `process.poll()` every 5 seconds
- If process crashed → cancels pending commands, restarts session
#### 3. Command Queue System
Commands are executed serially through a thread-safe queue:
```python
self.command_queue = queue.Queue()
# Client calls execute_command()
self.command_queue.put((cmd_id, command, event, response_dict))
# stdin_thread pulls from queue
cmd_id, command, event, response_dict = self.command_queue.get(timeout=1.0)
```
#### 4. Event-based Synchronization
Each command gets a `threading.Event` for completion notification:
```python
event = threading.Event()
response_dict = {
"event": event,
"response": [],
"done": False,
"error": None,
"last_line_time": time.time()
}
# Queue command
self.command_queue.put((cmd_id, command, event, response_dict))
# Wait for completion
if not event.wait(timeout):
return {'success': False, 'stderr': 'Command timeout'}
```
#### 5. Timeout-based Response Detection
Since meshcli doesn't provide end-of-response markers, we use **idle timeout detection**:
- Monitor `last_line_time` timestamp for each command
- If no new lines arrive for **300ms** → command is complete
- `event.set()` signals completion to waiting client
```python
def _monitor_response_timeout(self, cmd_id, response_dict, event, timeout_ms=300):
while not self.shutdown_flag.is_set():
time.sleep(timeout_ms / 1000.0)
with self.pending_lock:
time_since_last_line = time.time() - response_dict["last_line_time"]
if time_since_last_line >= (timeout_ms / 1000.0):
logger.info(f"Command [{cmd_id}] completed (timeout-based)")
response_dict["done"] = True
event.set()
return
```
### Session Initialization Commands
On startup, the bridge configures the meshcli session:
```python
def _init_session_settings(self):
self.process.stdin.write('set json_log_rx on\n')
self.process.stdin.write('set print_adverts on\n')
self.process.stdin.write('msgs_subscribe\n')
self.process.stdin.flush()
```
#### Command Breakdown:
1. **`set json_log_rx on`** - Enable JSON output for received messages
2. **`set print_adverts on`** - Print advertisement frames to stdout
3. **`msgs_subscribe`** - Subscribe to real-time message events (critical for instant message reception!)
### Multiplexing Logic
The `_read_stdout()` thread routes each line to the correct destination:
```python
def _read_stdout(self):
for line in iter(self.process.stdout.readline, ''):
line = line.rstrip('\n\r')
# Try to parse as JSON advert
if self._is_advert_json(line):
self._log_advert(line) # → .adverts.jsonl
continue
# Otherwise, append to current CLI response
self._append_to_current_response(line) # → HTTP response
```
### Advert Logging
JSON adverts are logged to `{device_name}.adverts.jsonl`:
```python
def _log_advert(self, json_line):
data = json.loads(json_line)
data["ts"] = time.time() # Add timestamp
with open(self.advert_log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(data, ensure_ascii=False) + '\n')
```
**File format**: JSON Lines (.jsonl) - one JSON object per line:
```json
{"payload_typename":"ADVERT","from_id":"abc123",...,"ts":1735425678.123}
{"payload_typename":"ADVERT","from_id":"def456",...,"ts":1735425680.456}
```
## Command Argument Quoting
meshcli in interactive mode requires proper quoting for arguments with spaces:
```python
def execute_command(self, args, timeout=DEFAULT_TIMEOUT):
quoted_args = []
for arg in args:
# If argument contains spaces or special chars, wrap in double quotes
if ' ' in arg or '"' in arg or "'" in arg:
escaped = arg.replace('"', '\\"')
quoted_args.append(f'"{escaped}"')
else:
quoted_args.append(arg)
command = ' '.join(quoted_args)
```
**Why not shlex.quote()?**
- `shlex.quote()` uses single quotes (`'message'`)
- meshcli treats single quotes literally, so they appear in sent messages
- **Solution**: Custom double-quote wrapping with escaped internal double quotes
## Real-time Message Reception
### The Problem (Before msgs_subscribe)
With periodic `recv` polling:
- `recv` command only reads from `.msgs` file
- It doesn't fetch NEW messages from the radio
- User reported: "od ponad 1.5 godziny, nie dotarła ANI JEDNA wiadomość"
### The Solution (msgs_subscribe)
User insight: **"W trybie interaktywnym, `msg_subscribe` włącza wyświetlanie wiadomości w momencie ich nadejścia"**
When `msgs_subscribe` is active in interactive mode:
- meshcli listens for message events from the radio
- New messages are immediately printed to stdout
- No polling needed - true event-driven architecture
### How It Works
1. Session init sends `msgs_subscribe\n` to stdin
2. meshcli subscribes to radio message events
3. When new message arrives:
- meshcli writes message to `.msgs` file
- meshcli prints message to stdout (captured by `_read_stdout` thread)
4. mc-webui detects change in `.msgs` file (file watcher or periodic stat check)
5. UI updates in real-time
## Watchdog and Auto-restart
The watchdog thread monitors process health:
```python
def _watchdog(self):
while not self.shutdown_flag.is_set():
time.sleep(5)
if self.process and self.process.poll() is not None:
logger.error(f"meshcli process died (exit code: {self.process.returncode})")
# Cancel all pending commands
with self.pending_lock:
for cmd_id, resp_dict in self.pending_commands.items():
resp_dict["error"] = "meshcli process crashed"
resp_dict["done"] = True
resp_dict["event"].set()
self.pending_commands.clear()
# Restart
self._start_session()
```
**Benefits:**
- Automatic recovery from crashes
- No manual intervention required
- Pending commands receive error responses instead of hanging
## Thread Safety
### Locks Used
1. **`self.pending_lock`** - Protects `pending_commands` dict and `current_cmd_id`
2. **`self.process_lock`** - Protects process handle (currently unused, reserved for future)
### Thread-safe Data Structures
- **`queue.Queue()`** - Thread-safe command queue (built-in locking)
## Docker Configuration Changes
### Environment Variables Added
```yaml
# docker-compose.yml
meshcore-bridge:
environment:
- MC_CONFIG_DIR=/root/.config/meshcore # For advert log path
- MC_DEVICE_NAME=${MC_DEVICE_NAME} # For .adverts.jsonl filename
- TZ=${TZ:-UTC} # Configurable timezone
```
### .env Configuration
```bash
# .env
TZ=Europe/Warsaw # Timezone for container logs (default: UTC)
```
## Benefits of Persistent Session
### Immediate Benefits
1. **Real-time Messages** - `msgs_subscribe` enables instant message reception
2. **Advert Logging** - Network advertisements logged to `.adverts.jsonl`
3. **Better Stability** - Single USB session, no serial port conflicts
4. **Lower Latency** - No process spawn/teardown overhead
### Future Possibilities
The persistent session enables advanced features that were impossible before:
1. **Pending Contact Management**
```bash
set manual_add_contacts on # Disable auto-add
pending_contacts # List pending contact requests
add_pending <pubkey> # Approve specific contact
```
2. **Interactive Configuration**
```bash
set <option> <value> # Session-persistent settings
get <option> # Query current values
```
3. **Event Streaming**
- Subscribe to various event types
- Real-time notifications without polling
4. **Stateful Operations**
- Multi-step workflows
- Command sequences with shared state
## Error Handling and Edge Cases
### 1. TTY Errors (Harmless)
```
meshcli stderr: Error: can't get controlling tty: Inappropriate ioctl for device
```
**Explanation**: meshcli tries to use `print_above()` for displaying messages, but there's no TTY in pipes.
**Impact**: None - messages are still processed and saved to `.msgs` file correctly.
**Action**: Ignore these warnings.
### 2. Command Timeout
If no response arrives within timeout (default 10s, 60s for `recv`):
```python
if not event.wait(timeout):
return {
'success': False,
'stdout': '',
'stderr': f'Command timeout after {timeout} seconds',
'returncode': -1
}
```
### 3. Process Crash
Watchdog detects crash and:
1. Cancels all pending commands with error
2. Restarts meshcli session
3. Re-applies init settings (`msgs_subscribe`, etc.)
### 4. Shutdown
Graceful shutdown:
```python
def shutdown(self):
self.shutdown_flag.set() # Signal all threads to exit
if self.process:
self.process.terminate()
self.process.wait(timeout=5)
```
## Implementation Commits
The refactor was implemented in several iterative commits:
1. **Initial Refactor** - Replaced subprocess.run with persistent Popen session
2. **Echo Marker Removal** (commit `693b211`) - Switched to timeout-based detection (meshcli doesn't support echo)
3. **Space Quoting Fix** (commit `56b7c33`) - Added shlex.quote for arguments with spaces
4. **Double Quote Fix** (commit `36badea`) - Replaced shlex.quote with custom double-quote wrapping
5. **TZ Configuration** (commit `d720d6a`) - Made timezone configurable, removed polling, added msgs_subscribe
6. **Command Name Fix** (commit `3a100e7`) - Corrected `msg_subscribe``msgs_subscribe`
## Testing and Validation
### Deployment Workflow
1. Develop locally (Windows/WSL)
2. Push to GitHub
3. Pull on test server (192.168.131.80)
4. Rebuild containers: `docker compose up -d --build`
5. Monitor logs: `docker compose logs -f meshcore-bridge`
### Success Indicators
✅ **Logs show:**
```
Session settings applied: json_log_rx=on, print_adverts=on, msgs_subscribe
meshcli session fully initialized
```
✅ **No errors:**
```
# No "Unknown command" errors
# No serial port conflicts
# No command timeouts (under normal conditions)
```
✅ **User feedback:**
```
"Działa! Widzę nowe wiadomości!! Nie masz pojęcia jak się cieszę :)"
```
## Performance Considerations
### Memory Usage
- Single meshcli process: ~20-30 MB (vs multiple spawns)
- Thread overhead: ~8 KB per thread × 4 threads = ~32 KB
- Command queue: Minimal (typically empty or 1-2 items)
### CPU Usage
- Idle CPU: Near zero (threads block on I/O)
- Active command: Single-threaded execution (serialized queue)
### Latency
- Command execution: ~50-200ms (depending on meshcli operation)
- No process spawn overhead (was ~100-300ms)
## Troubleshooting Guide
### Issue: No messages arriving
**Check:**
1. Verify `msgs_subscribe` in logs: `docker compose logs meshcore-bridge | grep msgs_subscribe`
2. Check for stderr errors: `docker compose logs meshcore-bridge | grep ERROR`
3. Verify `.msgs` file is being updated: `ls -lh ~/.config/meshcore/*.msgs`
**Solution:**
- Restart bridge: `docker compose restart meshcore-bridge`
### Issue: Commands timeout
**Check:**
1. Bridge health: `curl http://192.168.131.80:5001/health`
2. Process status: `docker compose exec meshcore-bridge ps aux`
**Solution:**
- Watchdog should auto-restart, but manual restart: `docker compose restart meshcore-bridge`
### Issue: Advert log not created
**Check:**
1. Config dir permissions: `ls -ld ~/.config/meshcore`
2. Advert log path in health endpoint: `curl http://192.168.131.80:5001/health`
**Solution:**
- Ensure `MC_CONFIG_DIR` is writable by container user
## References
- **bridge.py**: `meshcore-bridge/bridge.py` (lines 39-411)
- **docker-compose.yml**: Container configuration with environment variables
- **.env.example**: Configuration template with TZ setting
- **meshcore-cli docs**: `technotes/meshcore-cli.md`
## Conclusion
The persistent session architecture represents a fundamental shift from stateless request-response to **stateful event-driven communication** with the mesh network. This enables:
- ✅ Real-time message reception
- ✅ Network monitoring (advert logging)
- ✅ Advanced interactive features
- ✅ Better stability and performance
The architecture is production-ready and has been successfully deployed and tested on the production server (192.168.131.80).
---
**Author**: Claude Code (Anthropic)
**Date**: 2025-12-28
**Status**: Production Deployed ✅