feat(bridge): Add pending contacts management API endpoints

Added two new HTTP endpoints to meshcore-bridge for managing pending contacts
(contacts awaiting manual approval when manual_add_contacts mode is enabled):

New endpoints:
- GET /pending_contacts - List all pending contacts awaiting approval
  - Parses meshcli output format: "Name: <hex_public_key>"
  - Returns JSON array with {name, public_key} objects
  - Includes raw_stdout for debugging

- POST /add_pending - Approve and add a pending contact
  - Accepts JSON body: {"selector": "<name_or_pubkey>"}
  - Validates selector is non-empty string
  - Executes meshcli add_pending command via persistent session

Additional changes:
- Added curl to mc-webui Dockerfile for testing endpoints
- Updated README with Testing Bridge API section
- Included example curl commands and expected responses

Implementation notes:
- Uses existing MeshCLISession.execute_command() - no new processes
- Same persistent session and command queue architecture
- Consistent error handling with existing /cli endpoint

Enables future UI for manual contact approval workflow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2025-12-29 08:09:35 +01:00
parent ee7dde4ca2
commit 815adb5cfa
3 changed files with 182 additions and 0 deletions

View File

@@ -3,6 +3,9 @@
FROM python:3.11-slim
# Install curl for testing
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app

View File

@@ -359,6 +359,52 @@ docker compose up -d --build
docker compose ps
```
## Testing Bridge API
The `meshcore-bridge` container exposes HTTP endpoints for pending contact management.
### Test Pending Contacts Endpoints
```bash
# List pending contacts (from inside mc-webui container or server)
curl -s http://meshcore-bridge:5001/pending_contacts | jq
# Add a pending contact
curl -s -X POST http://meshcore-bridge:5001/add_pending \
-H 'Content-Type: application/json' \
-d '{"selector":"Skyllancer"}' | jq
# Example response for GET /pending_contacts:
# {
# "success": true,
# "pending": [
# {
# "name": "Skyllancer",
# "public_key": "f9ef..."
# },
# {
# "name": "KRA Reksio mob2🐕",
# "public_key": "41d5..."
# }
# ],
# "raw_stdout": "Skyllancer: f9ef...\nKRA Reksio mob2🐕: 41d5..."
# }
# Example response for POST /add_pending:
# {
# "success": true,
# "stdout": "Contact added successfully",
# "stderr": "",
# "returncode": 0
# }
```
**Note:** These endpoints require `manual_add_contacts` mode to be enabled in meshcli:
```bash
# Enable manual contact approval (run in meshcli interactive mode)
set manual_add_contacts on
```
## Troubleshooting
### Device not found

View File

@@ -496,6 +496,139 @@ def execute_cli():
}), 500
@app.route('/pending_contacts', methods=['GET'])
def get_pending_contacts():
"""
Get list of pending contacts awaiting approval.
Response JSON:
{
"success": true,
"pending": [
{"name": "Skyllancer", "public_key": "f9ef..."},
{"name": "KRA Reksio mob2🐕", "public_key": "41d5..."}
],
"raw_stdout": "..."
}
"""
try:
# Check session health
if not meshcli_session or not meshcli_session.process:
return jsonify({
'success': False,
'error': 'meshcli session not initialized',
'pending': []
}), 503
# Execute pending_contacts command
result = meshcli_session.execute_command(['pending_contacts'], timeout=DEFAULT_TIMEOUT)
if not result['success']:
return jsonify({
'success': False,
'error': result.get('stderr', 'Command failed'),
'pending': [],
'raw_stdout': result.get('stdout', '')
}), 200
# Parse stdout
stdout = result.get('stdout', '').strip()
pending = []
if stdout:
for line in stdout.split('\n'):
line = line.strip()
# Parse lines with format: "Name: <hex_public_key>"
if ':' in line:
parts = line.split(':', 1)
if len(parts) == 2:
name = parts[0].strip()
public_key = parts[1].strip().replace(' ', '') # Remove spaces from hex
if name and public_key:
pending.append({
'name': name,
'public_key': public_key
})
return jsonify({
'success': True,
'pending': pending,
'raw_stdout': stdout
}), 200
except Exception as e:
logger.error(f"API error in /pending_contacts: {e}")
return jsonify({
'success': False,
'error': str(e),
'pending': []
}), 500
@app.route('/add_pending', methods=['POST'])
def add_pending_contact():
"""
Add a pending contact by name or public key.
Request JSON:
{
"selector": "<name_or_pubkey_prefix_or_pubkey>"
}
Response JSON:
{
"success": true,
"stdout": "...",
"stderr": "",
"returncode": 0
}
"""
try:
data = request.get_json()
if not data or 'selector' not in data:
return jsonify({
'success': False,
'stdout': '',
'stderr': 'Missing required field: selector',
'returncode': -1
}), 400
selector = data['selector']
# Validate selector is non-empty string
if not isinstance(selector, str) or not selector.strip():
return jsonify({
'success': False,
'stdout': '',
'stderr': 'selector must be a non-empty string',
'returncode': -1
}), 400
# Check session health
if not meshcli_session or not meshcli_session.process:
return jsonify({
'success': False,
'stdout': '',
'stderr': 'meshcli session not initialized',
'returncode': -1
}), 503
# Execute add_pending command
result = meshcli_session.execute_command(['add_pending', selector.strip()], timeout=DEFAULT_TIMEOUT)
return jsonify(result), 200
except Exception as e:
logger.error(f"API error in /add_pending: {e}")
return jsonify({
'success': False,
'stdout': '',
'stderr': str(e),
'returncode': -1
}), 500
if __name__ == '__main__':
logger.info(f"Starting MeshCore Bridge on port 5001")
logger.info(f"Serial port: {MC_SERIAL_PORT}")