diff --git a/technotes/API-Diagnostic-Commands.md b/technotes/API-Diagnostic-Commands.md deleted file mode 100644 index 1d7bf70..0000000 --- a/technotes/API-Diagnostic-Commands.md +++ /dev/null @@ -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 diff --git a/technotes/UI-Contact-Management-MVP-v1-completed.md b/technotes/UI-Contact-Management-MVP-v1-completed.md deleted file mode 100644 index 6bb8f84..0000000 --- a/technotes/UI-Contact-Management-MVP-v1-completed.md +++ /dev/null @@ -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 Contact Approval -
-

- When enabled, new contacts must be manually approved before they can communicate with your node. -

- -
- - -
- - -
- - -
-
-
- Pending Contacts - -
- -
- - - - - - - - -
- - - -
-``` - -**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 = ' 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 = ' 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 = ' 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 - -``` - -**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 - -``` - -**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. diff --git a/technotes/UI-Contact-Management-MVP-v2-completed.md b/technotes/UI-Contact-Management-MVP-v2-completed.md deleted file mode 100644 index bacfe61..0000000 --- a/technotes/UI-Contact-Management-MVP-v2-completed.md +++ /dev/null @@ -1,1179 +0,0 @@ -# Contact Management MVP v2 - Implementation Complete - -**Date**: 2025-12-29 -**Status**: βœ… Completed (Pending Testing) -**Branch**: `dev-2` -**Related**: Builds on [UI-Contact-Management-MVP-v1-completed.md](UI-Contact-Management-MVP-v1-completed.md) - -## Overview - -Successfully implemented Contact Management MVP v2, which adds comprehensive management of existing contacts to the mc-webui interface. Users can now view, search, filter, and delete all contact types (CLI, REP, ROOM, SENS) with a mobile-first responsive UI. - -## Requirements - -Based on specification in `docs/UI-Contact-Management-MVP-v2.md`: - -### Functional Requirements -1. **Existing Contacts Panel** - - Display all contacts (CLI, REP, ROOM, SENS) - - Show contact name, type, public key prefix, and path - - Capacity counter (X / 350) with color-coded warnings - - Delete functionality with confirmation modal - -2. **Search and Filter** - - Client-side search by name or public key prefix - - Filter by contact type (All / CLI / REP / ROOM / SENS) - - Real-time filtering as user types - -3. **UX Requirements** - - Mobile-first design (touch-friendly buttons) - - Loading states (spinner/placeholder) - - Delete confirmation modal (prevent accidental deletions) - - Color-coded type badges for visual distinction - -### Technical Requirements -- Use existing `/api/contacts/detailed` endpoint pattern -- Proxy to meshcore-bridge via HTTP (no direct meshcli access) -- Vanilla JavaScript (no frameworks) -- Bootstrap 5 for UI components -- All code comments in English - -## Architecture - -### New Components - -``` -Contact Management v2 -β”œβ”€β”€ Backend (mc-webui) -β”‚ β”œβ”€β”€ app/meshcore/cli.py -β”‚ β”‚ β”œβ”€β”€ get_all_contacts_detailed() β†’ Parse meshcli contacts output -β”‚ β”‚ └── delete_contact(selector) β†’ Execute remove_contact command -β”‚ └── app/routes/api.py -β”‚ β”œβ”€β”€ GET /api/contacts/detailed β†’ Fetch all contacts with details -β”‚ └── POST /api/contacts/delete β†’ Delete contact by selector -β”‚ -└── Frontend - β”œβ”€β”€ app/templates/contacts.html - β”‚ β”œβ”€β”€ Existing Contacts section (search, filter, list, counter) - β”‚ └── Delete Confirmation Modal - └── app/static/js/contacts.js - β”œβ”€β”€ loadExistingContacts() - β”œβ”€β”€ applyFilters() β†’ Search + type filter - β”œβ”€β”€ renderExistingList() - β”œβ”€β”€ createExistingContactCard() - β”œβ”€β”€ showDeleteModal() - └── confirmDelete() -``` - -## Implementation Details - -### 1. Backend - Parser (`cli.py::get_all_contacts_detailed()`) - -**Challenge**: Parse variable-width text table output from `meshcli contacts` - -**Input format**: -``` -MarWoj|* contacts -KRA C REP d103df18e0ff Flood -TK Zalesie Test 🦜 REP df2027d3f2ef Flood -daniel5120 πŸ”« CLI 4563b1621b58 1e93d90faa7c2e49df8f -Szczwany-lis🦊 CLI 02332896a4a6 Flood -> 263 contacts in device -``` - -**Parsing strategy**: -1. **Work backwards from end** - Rightmost columns have predictable format -2. **Use public_key_prefix as anchor** - 12 hex chars are unique and reliable -3. **Extract name carefully** - Handle spaces, Unicode, special chars -4. **Validate extracted data** - Check type and hex format - -**Key code snippet**: -```python -def get_all_contacts_detailed() -> Tuple[bool, List[Dict], int, str]: - """Parse meshcli contacts output into structured data""" - - # Split by whitespace - parts = stripped.split() - if len(parts) < 4: - continue # Malformed line - - # Extract from right to left - path_or_mode = parts[-1] - public_key_prefix = parts[-2] - type_label = parts[-3] - - # Use public key as anchor to find name - pubkey_pos = stripped.rfind(public_key_prefix) - before_pubkey = stripped[:pubkey_pos].rstrip() - - # Type is last word before pubkey - type_pos = before_pubkey.rfind(type_label) - if type_pos != -1: - name = before_pubkey[:type_pos].strip() - - # Validate - if type_label not in ['CLI', 'REP', 'ROOM', 'SENS']: - type_label = 'UNKNOWN' - - if not re.match(r'^[a-fA-F0-9]{12}$', public_key_prefix): - continue # Skip invalid - - contact = { - 'name': name, - 'public_key_prefix': public_key_prefix.lower(), - 'type_label': type_label, - 'path_or_mode': path_or_mode, - 'raw_line': line # Preserve for debugging - } -``` - -**Edge cases handled**: -- βœ… Unicode emoji in names (🦜, 🦊, πŸ”«, etc.) -- βœ… Polish characters (Łasin, GdaΕ„sk) -- βœ… Spaces in names ("TK Zalesie Test 🦜") -- βœ… Type keyword in name ("CLI Test Node") -- βœ… Variable spacing between columns -- βœ… Hex path vs "Flood" mode -- βœ… Final count line extraction - -**Testing**: Parsed 263 real contacts successfully (mix of CLI, REP, ROOM types with Unicode) - -### 2. Backend - Delete Function (`cli.py::delete_contact()`) - -**meshcli command**: `remove_contact ` - -**Implementation**: -```python -def delete_contact(selector: str) -> Tuple[bool, str]: - """ - Delete a contact using meshcli remove_contact command. - - Args: - selector: Contact selector (name, public_key_prefix, or full public key) - Using public_key_prefix is recommended for reliability. - """ - success, stdout, stderr = _run_command(['remove_contact', selector.strip()]) - - if success: - message = stdout.strip() or f"Contact {selector} removed successfully" - return True, message - else: - error = stderr.strip() or "Failed to remove contact" - return False, error -``` - -**Selector options**: -- Name (works for most contacts) -- Public key prefix (12 hex chars - **recommended**) -- Full public key - -**Recommendation**: Always use `public_key_prefix` for reliability across all contact types. - -### 3. API Endpoints (`api.py`) - -#### GET /api/contacts/detailed - -Returns detailed list of ALL contacts (CLI, REP, ROOM, SENS). - -**Response**: -```json -{ - "success": true, - "count": 263, - "limit": 350, - "contacts": [ - { - "name": "TK Zalesie Test 🦜", - "public_key_prefix": "df2027d3f2ef", - "type_label": "REP", - "path_or_mode": "Flood", - "raw_line": "..." - } - ] -} -``` - -**Notes**: -- Different from `/api/contacts` which returns only CLI contact names -- Provides complete metadata needed for UI rendering -- Includes device capacity info (count / limit) - -#### POST /api/contacts/delete - -Deletes a contact by selector. - -**Request**: -```json -{ - "selector": "df2027d3f2ef" // public_key_prefix recommended -} -``` - -**Response** (success): -```json -{ - "success": true, - "message": "Contact removed successfully" -} -``` - -**Response** (error): -```json -{ - "success": false, - "error": "Contact not found" -} -``` - -### 4. Frontend - HTML Template (`contacts.html`) - -**Added sections**: - -1. **Existing Contacts Section** - - Header with counter badge and refresh button - - Search input (filter by name or public_key_prefix) - - Type filter dropdown (All / CLI / REP / ROOM / SENS) - - Contact cards list (dynamically populated) - - Loading/empty/error states - -2. **Delete Confirmation Modal** - - Bootstrap modal with danger theme - - Shows contact name and public_key_prefix - - Warns "This action cannot be undone" - - Cancel / Delete Contact buttons - -**CSS highlights**: -```css -/* Existing contact cards */ -.existing-contact-card { - background-color: white; - border: 1px solid #dee2e6; - border-radius: 0.5rem; - padding: 1rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: box-shadow 0.2s; -} - -.existing-contact-card:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -/* Counter badge colors */ -.counter-ok { background-color: #28a745; } /* Green: < 300 */ -.counter-warning { background-color: #ffc107; } /* Yellow: 300-339 */ -.counter-alarm { background-color: #dc3545; } /* Red: >= 340 */ - -/* Pulse animation for alarm state */ -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -.counter-alarm { - animation: pulse 1.5s infinite; -} -``` - -**Type badge colors**: -- CLI: Blue (`bg-primary`) -- REP: Green (`bg-success`) -- ROOM: Cyan (`bg-info`) -- SENS: Yellow (`bg-warning`) - -### 5. Frontend - JavaScript Logic (`contacts.js`) - -**New state variables**: -```javascript -let existingContacts = []; // All contacts from API -let filteredContacts = []; // After applying search/filter -let contactToDelete = null; // Contact pending deletion -``` - -**Key functions**: - -#### loadExistingContacts() -```javascript -async function loadExistingContacts() { - // Show loading state - const response = await fetch('/api/contacts/detailed'); - const data = await response.json(); - - existingContacts = data.contacts || []; - filteredContacts = [...existingContacts]; - - updateCounter(data.count, data.limit); - applyFilters(); // Render with current filters -} -``` - -#### updateCounter() -```javascript -function updateCounter(count, limit) { - counterEl.textContent = `${count} / ${limit}`; - - // Color logic - if (count >= 340) { - counterEl.classList.add('counter-alarm'); // Red pulsing - } else if (count >= 300) { - counterEl.classList.add('counter-warning'); // Yellow - } else { - counterEl.classList.add('counter-ok'); // Green - } -} -``` - -#### applyFilters() -```javascript -function applyFilters() { - const searchTerm = searchInput.value.toLowerCase(); - const selectedType = typeFilter.value; // ALL, CLI, REP, ROOM, SENS - - filteredContacts = existingContacts.filter(contact => { - // Type filter - if (selectedType !== 'ALL' && contact.type_label !== selectedType) { - return false; - } - - // Search filter (name or public_key_prefix) - if (searchTerm) { - const nameMatch = contact.name.toLowerCase().includes(searchTerm); - const keyMatch = contact.public_key_prefix.toLowerCase().includes(searchTerm); - return nameMatch || keyMatch; - } - - return true; - }); - - renderExistingList(filteredContacts); -} -``` - -#### createExistingContactCard() -```javascript -function createExistingContactCard(contact, index) { - const card = document.createElement('div'); - card.className = 'existing-contact-card'; - - // Name + Type badge - const nameDiv = document.createElement('div'); - nameDiv.textContent = contact.name; - - const typeBadge = document.createElement('span'); - typeBadge.className = 'badge type-badge'; - typeBadge.textContent = contact.type_label; - - // Color-code by type - switch (contact.type_label) { - case 'CLI': typeBadge.classList.add('bg-primary'); break; - case 'REP': typeBadge.classList.add('bg-success'); break; - case 'ROOM': typeBadge.classList.add('bg-info'); break; - case 'SENS': typeBadge.classList.add('bg-warning'); break; - } - - // Public key - const keyDiv = document.createElement('div'); - keyDiv.className = 'contact-key'; - keyDiv.textContent = contact.public_key_prefix; - - // Action buttons (Copy Key + Delete) - const copyBtn = createButton('Copy Key', () => copyContactKey(...)); - const deleteBtn = createButton('Delete', () => showDeleteModal(contact)); - - return card; -} -``` - -#### confirmDelete() -```javascript -async function confirmDelete() { - const response = await fetch('/api/contacts/delete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - selector: contactToDelete.public_key_prefix // Use prefix for reliability - }) - }); - - if (data.success) { - showToast(`Deleted: ${contactToDelete.name}`, 'success'); - modal.hide(); - - // Reload contacts list - setTimeout(() => loadExistingContacts(), 500); - } -} -``` - -## User Workflows - -### Workflow 1: View All Contacts - -1. User navigates to Contact Management page -2. Page auto-loads existing contacts via `GET /api/contacts/detailed` -3. Parser extracts structured data from meshcli output -4. Frontend renders contact cards with: - - Name (bold) - - Type badge (color-coded) - - Public key prefix (monospace) - - Action buttons (Copy, Delete) -5. Counter badge shows "263 / 350" (green) - -### Workflow 2: Search for a Contact - -1. User types "Zalesie" in search box -2. `applyFilters()` triggered on input event -3. Filters `existingContacts` by: - - Name contains "zalesie" (case-insensitive) - - OR public_key_prefix contains "zalesie" -4. `renderExistingList()` re-renders with filtered results -5. Results update instantly as user types - -### Workflow 3: Filter by Type - -1. User selects "REP" from type dropdown -2. `applyFilters()` triggered on change event -3. Filters contacts where `type_label === 'REP'` -4. Only repeaters shown in list -5. Counter badge still shows total count (not filtered count) - -### Workflow 4: Delete a Contact - -1. User clicks red "Delete" button on contact card -2. `showDeleteModal(contact)` opens Bootstrap modal -3. Modal displays: - - Contact name: "TK Zalesie Test 🦜" - - Public key: "df2027d3f2ef" - - Warning: "This action cannot be undone" -4. User clicks "Delete Contact" button -5. `confirmDelete()` sends `POST /api/contacts/delete` -6. Request body: `{"selector": "df2027d3f2ef"}` -7. Backend executes `meshcli remove_contact df2027d3f2ef` -8. On success: - - Toast notification: "Deleted: TK Zalesie Test 🦜" - - Modal closes - - Contact list auto-refreshes after 500ms -9. Counter badge updates to "262 / 350" - -### Workflow 5: Monitor Capacity - -**Scenario A: Normal usage (< 300 contacts)** -- Counter badge: "150 / 350" (green background) -- No warnings - -**Scenario B: Approaching limit (300-339 contacts)** -- Counter badge: "315 / 350" (yellow background) -- User notices warning color - -**Scenario C: Critical (β‰₯ 340 contacts)** -- Counter badge: "342 / 350" (red background, pulsing animation) -- User should delete some contacts soon - -## Contacts Parser - Technical Deep Dive - -### Problem - -`meshcli contacts` outputs a text table with: -- Variable-width columns (not fixed positions) -- Names containing spaces, Unicode emoji, special chars -- No clear delimiters between columns - -### Solution: Backward Parsing with Anchor - -**Step 1**: Split by whitespace -```python -parts = stripped.split() -# ['TK', 'Zalesie', 'Test', '🦜', 'REP', 'df2027d3f2ef', 'Flood'] -``` - -**Step 2**: Extract rightmost columns (predictable) -```python -path_or_mode = parts[-1] # 'Flood' -public_key_prefix = parts[-2] # 'df2027d3f2ef' -type_label = parts[-3] # 'REP' -``` - -**Step 3**: Use public_key_prefix as anchor -```python -pubkey_pos = stripped.rfind('df2027d3f2ef') -# Find position in original string (preserves spacing) - -before_pubkey = stripped[:pubkey_pos].rstrip() -# 'TK Zalesie Test 🦜 REP' -``` - -**Step 4**: Extract name (everything before type) -```python -type_pos = before_pubkey.rfind('REP') -name = before_pubkey[:type_pos].strip() -# 'TK Zalesie Test 🦜' -``` - -**Why this works**: -- Public key is unique 12-hex pattern (reliable anchor) -- Working from right to left avoids variable-length name issues -- Preserves Unicode by working with full strings -- Handles spaces in names naturally - -### Validation - -```python -# Type validation -if type_label not in ['CLI', 'REP', 'ROOM', 'SENS']: - type_label = 'UNKNOWN' - -# Public key format validation -if not re.match(r'^[a-fA-F0-9]{12}$', public_key_prefix): - continue # Skip malformed line -``` - -### Count Extraction - -```python -# Extract total count from final line -# "> 263 contacts in device" -if line.strip().startswith('>') and 'contacts in device' in line: - try: - total_count = int(re.search(r'> (\d+) contacts', line).group(1)) - except: - pass # Fallback to len(contacts) -``` - -## Testing Plan - -### Manual Testing Checklist - -**Load contacts:** -- [ ] Open Contact Management page -- [ ] Verify contacts list loads -- [ ] Check counter badge shows correct count -- [ ] Verify counter color (green/yellow/red based on count) - -**Search functionality:** -- [ ] Type contact name in search box -- [ ] Verify results filter in real-time -- [ ] Type public key prefix -- [ ] Verify filtering by key works -- [ ] Clear search box -- [ ] Verify all contacts reappear - -**Type filter:** -- [ ] Select "CLI" from dropdown -- [ ] Verify only CLI contacts shown (blue badges) -- [ ] Select "REP" -- [ ] Verify only REP contacts shown (green badges) -- [ ] Select "ROOM" -- [ ] Verify only ROOM contacts shown (cyan badges) -- [ ] Select "All Types" -- [ ] Verify all contacts shown - -**Delete contact:** -- [ ] Click "Delete" button on any contact -- [ ] Verify modal appears with correct contact info -- [ ] Click "Cancel" -- [ ] Verify modal closes, contact still in list -- [ ] Click "Delete" again -- [ ] Click "Delete Contact" button -- [ ] Verify success toast appears -- [ ] Verify contact removed from list -- [ ] Verify counter decrements - -**Copy functionality:** -- [ ] Click "Copy Key" button -- [ ] Verify toast "Key copied to clipboard" -- [ ] Paste in text editor -- [ ] Verify correct public_key_prefix pasted - -**Edge cases:** -- [ ] Test with 0 contacts (empty state) -- [ ] Test with 350 contacts (limit reached) -- [ ] Test with contacts containing Unicode -- [ ] Test network error (disconnect bridge) -- [ ] Test parser with malformed output - -### Logging - -Check logs for delete operations: - -```bash -# mc-webui container -docker compose logs -f mc-webui | grep -i "delete" - -# meshcore-bridge container (where remove_contact executes) -docker compose logs -f meshcore-bridge | grep -i "remove_contact" -``` - -**Expected log entries**: -``` -mc-webui: POST /api/contacts/delete {"selector": "df2027d3f2ef"} -meshcore-bridge: Executing command: ['remove_contact', 'df2027d3f2ef'] -meshcore-bridge: Command succeeded: Contact removed -``` - -## Documentation Updates - -### README.md - -Added new subsection "Existing Contacts" under Contact Management (lines 350-394): - -**Documented**: -- Counter badge (green/yellow/red logic) -- Search functionality -- Type filter options -- Copy public key feature -- Delete workflow with warning -- Capacity monitoring guidelines - -### Technotes - -This file serves as comprehensive technical documentation for v2 implementation. - -## Git Commit - -**Branch**: dev-2 -**Commit message** (to be created): -``` -feat(ui): Contact Management v2 (existing contacts + delete + counter) - -Implements existing contacts management as specified in -docs/UI-Contact-Management-MVP-v2.md: - -Backend (mc-webui): -- Added contacts output parser in cli.py::get_all_contacts_detailed() -- Parses meshcli contacts table output (handles Unicode, spaces, variable width) -- Added cli.py::delete_contact(selector) wrapper for remove_contact command -- Added GET /api/contacts/detailed endpoint (all contact types with metadata) -- Added POST /api/contacts/delete endpoint (delete by selector) - -Frontend: -- Extended contacts.html with Existing Contacts section -- Added search input (filter by name or public_key_prefix) -- Added type filter dropdown (All / CLI / REP / ROOM / SENS) -- Added contact cards with type badges (color-coded: CLI=blue, REP=green, ROOM=cyan, SENS=yellow) -- Added counter badge with capacity warnings (green < 300, yellow 300-339, red >= 340) -- Added delete confirmation modal (Bootstrap modal, danger theme) -- Implemented contacts.js logic (load, search, filter, delete) - -Features: -- Mobile-first design (touch-friendly buttons, responsive cards) -- Real-time search and filtering (client-side) -- Capacity monitoring (X / 350 with color-coded warnings) -- Delete with confirmation (prevents accidental deletions) -- Copy public key to clipboard -- Loading/empty/error states - -Parser: -- Best-effort parsing of variable-width text table -- Backward parsing strategy (work from right to left) -- Uses public_key_prefix as anchor for name extraction -- Handles Unicode emoji, Polish chars, spaces in names -- Validates type and hex format -- Tested with 263 real contacts (CLI, REP, ROOM mix) - -Documentation: -- Updated README.md with Existing Contacts section -- Created technotes/UI-Contact-Management-MVP-v2-completed.md - -Related: UI-Contact-Management-MVP-v1-completed.md -``` - -## "Last Seen" Feature Implementation - -### Overview - -After completing the basic Contact Management v2, an additional enhancement was requested to show when each contact was last active. This provides valuable information about which contacts are currently reachable on the mesh network. - -### Requirements - -**User Request**: "Czy na kafelku z kontaktem moΕΌemy dodaΔ‡ datΔ™ 'last seen'? Czy taka informacja jest Ε‚atwo dostΔ™pna?" - -**Goal**: Display "last seen" timestamp on contact cards with: -- Relative time format ("5 minutes ago", "2 hours ago", etc.) -- Activity status indicators (🟒 active, 🟑 recent, πŸ”΄ inactive) -- Data fetched from meshcli's `apply_to` command with `contact_info` filter - -### Architecture - -#### Data Source Discovery - -Initial investigation found that `meshcli contacts` command only returns: -- NAME -- TYPE -- PUBKEY_PREFIX -- PATH_OR_MODE - -**No timestamp information available.** - -User discovered `apply_to t=TYPE contact_info` command which returns detailed JSON including: -- `last_advert` - Unix timestamp when contact was last seen -- `lastmod` - Unix timestamp when contact was last modified -- Full public_key, GPS coordinates, path info, etc. - -**Decision**: Use `last_advert` as "last seen" timestamp (subject to future review). - -#### Command Syntax - -```bash -# Get detailed info for all CLI contacts -apply_to t=1 contact_info - -# Get detailed info for all REP contacts -apply_to t=2 contact_info - -# And so on for ROOM (t=3) and SENS (t=4) -``` - -**Important discoveries**: -1. **Comma-separated types DON'T WORK** through bridge: `apply_to t=1,t=2,t=3 contact_info` returns "0 matches" -2. **Output format is NDJSON** (newline-delimited JSON), not JSON array -3. **JSON is prettified** (multi-line), not strictly line-delimited -4. **Must call separately for each type**: t=1, t=2, t=3, t=4 - -#### Output Format (NDJSON) - -```json -{ - "public_key": "df2027d3f2ef45a9...", - "type": 2, - "flags": 0, - "out_path_len": 0, - "out_path": "", - "adv_name": "TK Zalesie Test 🦜", - "last_advert": 1735429453, - "adv_lat": 50.123456, - "adv_lon": 19.654321, - "lastmod": 1735428000 -} -{ - "public_key": "d103df18e0ff12ab...", - "type": 2, - ... -} -``` - -### Implementation - -#### Backend: NDJSON Parser (`cli.py::get_contacts_with_last_seen()`) - -**Challenge**: Parse prettified NDJSON output where each JSON object spans multiple lines. - -**Failed Approach #1**: Line-by-line parsing -```python -# Tried to parse each line as JSON - FAILED -# Prettified JSON breaks across multiple lines -for line in stdout.splitlines(): - try: - contact = json.loads(line) # JSONDecodeError! -``` - -**Failed Approach #2**: Skip non-JSON lines -```python -# Tried to detect JSON lines and skip prompts - FAILED -# Still doesn't handle multi-line JSON objects -if line.strip().startswith('{'): - contact = json.loads(line) # Still fails on prettified JSON -``` - -**Successful Approach #3**: Brace-matching algorithm -```python -def get_contacts_with_last_seen() -> Tuple[bool, Dict[str, Dict], str]: - """ - Get detailed contact information including last_advert timestamps. - Uses 'apply_to t=TYPE contact_info' command to fetch metadata - for all contact types (CLI, REP, ROOM, SENS). - """ - contacts_dict = {} - - # Call separately for each type (comma-separated doesn't work) - for contact_type in ['t=1', 't=2', 't=3', 't=4']: - success, stdout, stderr = _run_command(['apply_to', contact_type, 'contact_info']) - - if not success: - logger.warning(f"apply_to {contact_type} contact_info failed: {stderr}") - continue - - # Parse prettified JSON using brace-matching - json_objects = [] - depth = 0 - start_idx = None - - # Walk character-by-character through output - for i, char in enumerate(stdout): - if char == '{': - if depth == 0: - start_idx = i # Mark start of JSON object - depth += 1 - elif char == '}': - depth -= 1 - if depth == 0 and start_idx is not None: - # Found complete JSON object - json_str = stdout[start_idx:i+1] - try: - contact = json.loads(json_str) - if 'public_key' in contact: - json_objects.append(contact) - except json.JSONDecodeError: - pass # Skip malformed JSON - start_idx = None - - # Add to contacts dict - for contact in json_objects: - contacts_dict[contact['public_key']] = contact - - logger.info(f"Parsed {len(json_objects)} contacts from {contact_type}") - - return True, contacts_dict, "" -``` - -**Why this works**: -- Depth counter tracks brace nesting level -- When depth reaches 0, we have a complete `{...}` object -- Handles both single-line and multi-line JSON -- Skips any prompt echoes or non-JSON text -- Works regardless of JSON formatting - -**Test results**: -- βœ… 17 CLI contacts parsed (t=1) -- βœ… 226 REP contacts parsed (t=2) -- βœ… 20 ROOM contacts parsed (t=3) -- βœ… 0 SENS contacts parsed (t=4, none present) -- **Total: 263 contacts successfully parsed** - -#### API Endpoint Enhancement (`api.py`) - -Modified `GET /api/contacts/detailed` to merge `last_seen` data: - -```python -@api_bp.route('/contacts/detailed', methods=['GET']) -def get_contacts_detailed_api(): - # Get basic contacts list - success, contacts, total_count, error = cli.get_all_contacts_detailed() - - # Get detailed contact info with last_advert timestamps - success_detailed, contacts_detailed, error_detailed = cli.get_contacts_with_last_seen() - - if success_detailed: - # Merge last_advert data with contacts - # Match by public_key_prefix (first 12 chars of full public_key) - for contact in contacts: - prefix = contact.get('public_key_prefix', '').lower() - - # Find matching contact in detailed data - for full_key, details in contacts_detailed.items(): - if full_key.lower().startswith(prefix): - # Add last_seen timestamp - contact['last_seen'] = details.get('last_advert', None) - break - else: - # If detailed fetch failed, log warning but still return contacts without last_seen - logger.warning(f"Failed to get last_seen data: {error_detailed}") - - return jsonify({ - 'success': True, - 'contacts': contacts, - 'count': total_count, - 'limit': 350 - }), 200 -``` - -**Matching strategy**: -- Use `public_key_prefix` (12 hex chars) from basic contacts list -- Match against full `public_key` from detailed data using `startswith()` -- Fallback gracefully if detailed fetch fails (contacts still shown without timestamps) - -#### Frontend: Relative Time Display (`contacts.js`) - -**Utility function #1**: Format Unix timestamp as relative time -```javascript -function formatRelativeTime(timestamp) { - if (!timestamp) return 'Never'; - - const now = Math.floor(Date.now() / 1000); - const diffSeconds = now - timestamp; - - if (diffSeconds < 0) return 'Just now'; // Clock skew - if (diffSeconds < 60) return 'Just now'; - if (diffSeconds < 3600) { - const minutes = Math.floor(diffSeconds / 60); - return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; - } - if (diffSeconds < 86400) { - const hours = Math.floor(diffSeconds / 3600); - return `${hours} hour${hours !== 1 ? 's' : ''} ago`; - } - if (diffSeconds < 2592000) { - const days = Math.floor(diffSeconds / 86400); - return `${days} day${days !== 1 ? 's' : ''} ago`; - } - if (diffSeconds < 31536000) { - const months = Math.floor(diffSeconds / 2592000); - return `${months} month${months !== 1 ? 's' : ''} ago`; - } - const years = Math.floor(diffSeconds / 31536000); - return `${years} year${years !== 1 ? 's' : ''} ago`; -} -``` - -**Utility function #2**: Get activity status indicator -```javascript -function getActivityStatus(timestamp) { - if (!timestamp) { - return { - icon: '⚫', - color: '#6c757d', - title: 'Never seen' - }; - } - - const now = Math.floor(Date.now() / 1000); - const diffSeconds = now - timestamp; - - // Active (< 5 minutes) - if (diffSeconds < 300) { - return { - icon: '🟒', - color: '#28a745', - title: 'Active (seen recently)' - }; - } - - // Recent (< 1 hour) - if (diffSeconds < 3600) { - return { - icon: '🟑', - color: '#ffc107', - title: 'Recent activity' - }; - } - - // Inactive (> 1 hour) - return { - icon: 'πŸ”΄', - color: '#dc3545', - title: 'Inactive' - }; -} -``` - -**Contact card rendering**: -```javascript -// Last seen row (with activity status indicator) -const lastSeenDiv = document.createElement('div'); -lastSeenDiv.className = 'text-muted small d-flex align-items-center gap-1'; - -if (contact.last_seen) { - const status = getActivityStatus(contact.last_seen); - const relativeTime = formatRelativeTime(contact.last_seen); - - const statusIcon = document.createElement('span'); - statusIcon.textContent = status.icon; - statusIcon.style.fontSize = '0.9rem'; - statusIcon.title = status.title; // Tooltip on hover - - const timeText = document.createElement('span'); - timeText.textContent = `Last seen: ${relativeTime}`; - - lastSeenDiv.appendChild(statusIcon); - lastSeenDiv.appendChild(timeText); -} else { - // No last_seen data available - const statusIcon = document.createElement('span'); - statusIcon.textContent = '⚫'; - - const timeText = document.createElement('span'); - timeText.textContent = 'Last seen: Unknown'; - - lastSeenDiv.appendChild(statusIcon); - lastSeenDiv.appendChild(timeText); -} - -card.appendChild(lastSeenDiv); -``` - -### Debugging Journey - -#### Problem #1: All contacts showing "Unknown" - -**Symptom**: After initial implementation, all contacts showed "Last seen: Unknown" - -**Logs**: -``` -Executing command: ['apply_to', 't=1,t=2,t=3,t=4', 'contact_info'] -Response: 0 matches in contacts -``` - -**Root cause**: Comma-separated types don't work through bridge - -**Fix**: Separate calls for each type -```python -for contact_type in ['t=1', 't=2', 't=3', 't=4']: - success, stdout, stderr = _run_command(['apply_to', contact_type, 'contact_info']) -``` - -#### Problem #2: NDJSON format not recognized - -**Symptom**: Line-based parser returned 0 contacts despite receiving data - -**User testing** (interactive session): -```bash -MarWoj|* apply_to t=1 contact_info -{ - "public_key": "4563b1621b58...", - "type": 1, - "adv_name": "daniel5120 πŸ”«", - "last_advert": 1734645823, - ... -} -``` - -**Discovery**: -1. Output is prettified JSON (multi-line), not line-delimited -2. Command works interactively but comma syntax fails through bridge -3. Each contact is a separate JSON object (not array) - -**Failed fix**: Line-by-line NDJSON parsing -```python -# Tried skipping non-JSON lines -for line in stdout.splitlines(): - if line.strip().startswith('{'): - contact = json.loads(line) # Still fails! -``` - -**Successful fix**: Brace-matching algorithm (see implementation above) - -#### Problem #3: Timestamp accuracy question - -**User observation**: "KRA C" (repeater connected at 0 hops) shows "Last seen: 1 year ago" - -**Question**: Is `last_advert` the right field to use, or should we use `lastmod`? - -**Status**: User will investigate at Meshcore source level and report back - -**Current implementation**: Using `last_advert` field (can be changed to `lastmod` if needed) - -### Test Results - -**Production testing on http://192.168.131.80:5000:** - -βœ… **Data fetched successfully**: -- 17 CLI contacts parsed from t=1 -- 226 REP contacts parsed from t=2 -- 20 ROOM contacts parsed from t=3 -- 0 SENS contacts parsed from t=4 (none exist) -- **Total: 263 contacts with timestamps** - -βœ… **UI displays correctly**: -- "TK Zalesie Test 🦜" shows 🟑 "Last seen: 52 minutes ago" -- "KRA C" shows πŸ”΄ "Last seen: 1 year ago" -- Relative time formatting works (minutes, hours, days, months, years) -- Activity indicators show correct colors - -βœ… **Edge cases handled**: -- Contacts without timestamp show ⚫ "Unknown" -- Future timestamps (clock skew) show "Just now" -- Parser handles Unicode in names (emoji preserved) - -### Commits - -**Commit 1**: Initial "Last Seen" implementation -``` -feat(contacts): Add 'Last Seen' timestamp display with activity indicators - -- Added get_contacts_with_last_seen() in cli.py to fetch detailed contact info -- Uses 'apply_to t=TYPE contact_info' command for each contact type -- Merges last_advert timestamps with existing contacts list in API endpoint -- Added formatRelativeTime() and getActivityStatus() frontend functions -- Display relative time ("5 minutes ago") with color-coded indicators (πŸŸ’πŸŸ‘πŸ”΄) -- Activity thresholds: < 5min active, < 1hr recent, > 1hr inactive -``` - -**Commit 2**: Debug logging added -``` -debug(contacts): Add detailed logging to diagnose last_seen matching issue - -- Added logging for command execution and response data -- Log contact counts parsed per type -- Preview first 500 chars of command output -``` - -**Commit 3**: Fix NDJSON parsing with separate calls -``` -fix(contacts): Fix NDJSON parsing and use separate calls per contact type - -- Changed from comma-separated t=1,t=2,t=3,t=4 to separate calls -- Implemented line-by-line NDJSON parsing -- Skip prompt echoes and summary lines -``` - -**Commit 4**: Brace-matching parser -``` -debug(contacts): Change to brace-matching JSON parser with output preview - -- Walk character-by-character looking for complete JSON objects -- Match opening/closing braces with depth counter -- Works for both single-line and prettified JSON -- Added output preview logging (first 500 chars) -``` - -**Commit 5**: Cleanup -``` -cleanup(contacts): Remove debug logging from last_seen feature - -- Removed excessive debug logging after successful implementation -- Kept essential info logging for monitoring -``` - -### Pending Items - -1. **Timestamp field verification**: User to check at Meshcore source whether `last_advert` or `lastmod` is more appropriate for "last seen" display -2. **Performance monitoring**: Monitor API response time with 263 contacts (currently instant) -3. **Potential optimization**: Cache `contact_info` data for 30-60 seconds to reduce redundant calls - -## Conclusion - -Successfully implemented Contact Management v2, adding comprehensive existing contacts management to mc-webui: - -βœ… **Backend**: -- Robust parser for meshcli contacts output -- Handles Unicode, spaces, variable widths -- DELETE endpoint for contact removal -- NDJSON parser for `apply_to contact_info` output (brace-matching algorithm) -- Fetches detailed contact metadata including last_advert timestamps - -βœ… **Frontend**: -- Mobile-first responsive design -- Real-time search and filtering -- Color-coded counter badge (green/yellow/red) -- Delete confirmation modal -- Type badges for visual distinction -- "Last Seen" timestamps with relative time formatting -- Activity status indicators (🟒 active, 🟑 recent, πŸ”΄ inactive, ⚫ unknown) - -βœ… **UX**: -- Touch-friendly buttons (min-height: 44px) -- Loading/empty/error states -- Toast notifications for feedback -- Clipboard copy functionality -- Relative time display ("5 minutes ago", "2 hours ago", etc.) -- Hover tooltips for activity status - -βœ… **Testing**: -- Parsed 263 real contacts successfully -- Handles all contact types (CLI, REP, ROOM, SENS) -- Unicode-safe (emoji, Polish chars) -- Fetched and displayed 263 timestamps (17 CLI + 226 REP + 20 ROOM) -- Brace-matching parser handles prettified multi-line JSON - -βœ… **Documentation**: -- README.md updated (added "Last Seen" feature description) -- .claude/instructions.md updated (added apply_to contact_info documentation) -- Complete technical notes (this file) - -βœ… **Commits**: -- 5 commits for "Last Seen" feature (initial impl, debug, fixes, cleanup) -- Complete git history documenting the debugging journey - -**Status**: βœ… Implementation complete with "Last Seen" enhancement - -**Pending**: User to verify timestamp field choice (last_advert vs lastmod) at Meshcore source level - -**Next Steps**: User should merge dev-2 branch to dev after reviewing changes diff --git a/technotes/meshcore-cli.md b/technotes/meshcore-cli.md deleted file mode 100644 index 472bb08..0000000 --- a/technotes/meshcore-cli.md +++ /dev/null @@ -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: - -
-$ pipx install meshcore-cli
-
- -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/): - -
-$ nix run github:meshcore-dev/meshcore-cli#meshcore-cli
-
- -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 - -
-$ meshcli <args> <commands>
-
- -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 - -
-    -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
-
- -### 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. - -
-    ?<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
-
- -### 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 ` : 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`: -- `/` issues cmd command on the root -- `//` will send cmd to selected node -- `/ ` 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 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 ` : applies cmd to contacts matching filter `` 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 - -
-# 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>
-
\ No newline at end of file diff --git a/technotes/pending-contacts-api.md b/technotes/pending-contacts-api.md deleted file mode 100644 index 8b8ed94..0000000 --- a/technotes/pending-contacts-api.md +++ /dev/null @@ -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: "` -- 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 ` 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 ⏳ diff --git a/technotes/persistent-meshcli-session.md b/technotes/persistent-meshcli-session.md deleted file mode 100644 index 547edfd..0000000 --- a/technotes/persistent-meshcli-session.md +++ /dev/null @@ -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 # Approve specific contact - ``` - -2. **Interactive Configuration** - ```bash - set