mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
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:
@@ -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
@@ -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 <args> <commands>
|
||||
</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 `<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 <timeout> : timeout for the ble scan (-S and -l) default 2s
|
||||
-a <address> : specifies device address (can be a name)
|
||||
-d <name> : filter meshcore devices with name or address
|
||||
-P : forces pairing via the OS
|
||||
-t <hostname> : connects via tcp/ip
|
||||
-p <port> : specifies tcp port (default 5000)
|
||||
-s <port> : use serial port <port>
|
||||
-b <baudrate> : specify baudrate
|
||||
-C : toggles classic mode for prompt
|
||||
-c <on/off> : 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>
|
||||
?<cmd> may give you some more help about cmd
|
||||
General commands
|
||||
chat : enter the chat (interactive) mode
|
||||
chat_to <ct> : enter chat with contact to
|
||||
script <filename> : 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 <secs> : sleeps for a given amount of secs s
|
||||
wait_key : wait until user presses <Enter> wk
|
||||
apply_to <f> <cmds> : sends cmds to contacts matching f at
|
||||
Messaging
|
||||
msg <name> <msg> : send message to node by name m {
|
||||
wait_ack : wait an ack wa }
|
||||
chan <nb> <msg> : send message to channel number <nb> ch
|
||||
public <msg> : 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 <n> : get info for channel (by number or name)
|
||||
set_channel n nm k : set channel info (nb, name, key)
|
||||
remove_channel <n> : remove channel (by number or name)
|
||||
scope <s> : sets node's flood scope
|
||||
Management
|
||||
advert : sends advert a
|
||||
floodadv : flood advert
|
||||
get <param> : gets a param, \"get help\" for more
|
||||
set <param> <value> : sets a param, \"set help\" for more
|
||||
time <epoch> : sets time to given epoch
|
||||
clock : get current time
|
||||
clock sync : sync device clock st
|
||||
node_discover <filter> : discovers nodes based on their type nd
|
||||
Contacts
|
||||
contacts / list : gets contact list lc
|
||||
reload_contacts : force reloading all contacts rc
|
||||
contact_info <ct> : prints information for contact ct ci
|
||||
contact_timeout <ct> v : sets temp default timeout for contact
|
||||
share_contact <ct> : share a contact with others sc
|
||||
export_contact <ct> : get a contact's URI ec
|
||||
import_contact <URI> : import a contact from its URI ic
|
||||
remove_contact <ct> : removes a contact from this node
|
||||
path <ct> : diplays path for a contact
|
||||
disc_path <ct> : discover new path and display dp
|
||||
reset_path <ct> : resets path to a contact to flood rp
|
||||
change_path <ct> <pth> : change the path to a contact cp
|
||||
change_flags <ct> <f> : change contact flags (tel_l|tel_a|star)cf
|
||||
req_telemetry <ct> : prints telemetry data as json rt
|
||||
req_mma <ct> : requests min/max/avg for a sensor rm
|
||||
req_acl <ct> : requests access control list for sensor
|
||||
pending_contacts : show pending contacts
|
||||
add_pending <pending> : manually add pending contact
|
||||
flush_pending : flush pending contact list
|
||||
Repeaters
|
||||
login <name> <pwd> : log into a node (rep) with given pwd l
|
||||
logout <name> : log out of a repeater
|
||||
cmd <name> <cmd> : sends a command to a repeater (no ack) c [
|
||||
wmt8 : wait for a msg (reply) with a timeout ]
|
||||
req_status <name> : requests status from a node rs
|
||||
req_neighbours <name> : requests for neighbours in binary form rn
|
||||
trace <path> : 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>
|
||||
@@ -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 ⏳
|
||||
@@ -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 ✅
|
||||
Reference in New Issue
Block a user