ProxmoxBackupClientPBSClien.../docker/scripts/api-server.py
zaphod-black 0746428435 Fix Docker web dashboard with Python HTTP server + blue/purple neon theme
PROBLEM SOLVED:
The netcat-based HTTP server had a fundamental bidirectional pipe issue:
- `( read method path ) | nc -l` only connects stdout→stdin in one direction
- The `read` command couldn't receive HTTP request data from nc
- All requests returned "Not found" despite correct routing logic

SOLUTION:
- Created api-server.py: Proper Python HTTP server using http.server
- Handles dashboard (GET /), status, health, logs, and backup trigger (POST)
- Full request/response handling with proper HTTP headers
- Replaces broken netcat approach with robust solution

DASHBOARD ENHANCEMENTS:
- Changed from green terminal theme to blue/purple neon aesthetic
  - Primary: #00d4ff (cyan blue)
  - Accent: #aa00ff (purple)
  - Background: #0a0a14 (dark blue-black)
- Increased all font sizes 2-4px for better readability
- Enhanced button padding and spacing
- More visible, high-contrast interface

TECHNICAL DETAILS:
- Dockerfile: Replaced netcat-openbsd with python3 dependency
- api-server.sh: Now simple wrapper that exec's Python script
- api-server.py: Full-featured HTTP server with JSON/HTML responses
- dashboard.html: Updated color scheme and typography

TESTING:
 Dashboard loads at http://localhost:8080/
 /status endpoint returns JSON
 /health endpoint working
 /logs endpoint working
 Real-time backup monitoring ready

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 23:25:10 -06:00

217 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""
Simple HTTP API server for PBS Client dashboard and monitoring
Unix philosophy: Simple, focused, does one thing well
"""
import os
import sys
import json
import logging
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
from pathlib import Path
# Configuration
PORT = int(os.getenv('API_PORT', '8080'))
LOG_DIR = Path('/logs')
DASHBOARD_PATH = Path('/usr/local/share/dashboard.html')
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='[API] [%(asctime)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
log = logging.getLogger(__name__)
class PBSAPIHandler(BaseHTTPRequestHandler):
"""Handler for PBS Client API requests"""
def log_message(self, format, *args):
"""Override to use our logging format"""
log.info(f"{self.command} {self.path} - {format % args}")
def send_json(self, data, status=200):
"""Send JSON response"""
json_data = json.dumps(data, indent=2)
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(json_data))
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(json_data.encode())
def send_html(self, html_content, status=200):
"""Send HTML response"""
self.send_response(status)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', len(html_content))
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(html_content.encode())
def send_text(self, text_content, status=200):
"""Send plain text response"""
self.send_response(status)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', len(text_content))
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(text_content.encode())
def do_GET(self):
"""Handle GET requests"""
path = self.path.split('?')[0] # Strip query params
# Dashboard (root or /dashboard.html)
if path in ('/', '/dashboard.html'):
if DASHBOARD_PATH.exists():
html = DASHBOARD_PATH.read_text()
self.send_html(html)
else:
self.send_json({
'error': 'Dashboard not found',
'path': str(DASHBOARD_PATH)
}, status=404)
return
# Status endpoint
if path == '/status':
status_file = LOG_DIR / 'status.json'
if status_file.exists():
try:
status_data = json.loads(status_file.read_text())
self.send_json(status_data)
except json.JSONDecodeError as e:
self.send_json({
'error': 'Invalid status file',
'details': str(e)
}, status=500)
else:
# Default status if file doesn't exist yet
self.send_json({
'status': 'idle',
'last_backup': 'never',
'last_result': 'unknown',
'next_scheduled': 'unknown',
'repository': os.getenv('PBS_REPOSITORY', 'not configured'),
'hostname': os.getenv('BACKUP_HOSTNAME', os.uname().nodename)
})
return
# Health check
if path == '/health':
self.send_json({
'status': 'healthy',
'uptime': self._get_uptime(),
'timestamp': datetime.now().isoformat()
})
return
# Logs endpoint
if path == '/logs':
log_file = LOG_DIR / 'backup.log'
if log_file.exists():
# Return last 100 lines
lines = log_file.read_text().splitlines()
recent_lines = lines[-100:] if len(lines) > 100 else lines
self.send_text('\n'.join(recent_lines))
else:
self.send_text('No logs available yet')
return
# Not found - show available endpoints
self.send_json({
'error': 'Not found',
'path': path,
'available_endpoints': [
'/ - Dashboard web UI',
'/status - Current backup status (JSON)',
'/health - Health check (JSON)',
'/logs - Recent backup logs (text)',
'/backup - Trigger backup (POST)'
]
}, status=404)
def do_POST(self):
"""Handle POST requests"""
path = self.path.split('?')[0]
# Trigger backup
if path == '/backup':
# Read request body if present
content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
body = self.rfile.read(content_length)
try:
params = json.loads(body)
except json.JSONDecodeError:
params = {}
else:
params = {}
# Trigger backup script
backup_script = '/usr/local/bin/backup'
if Path(backup_script).exists():
log.info('Triggering backup via API...')
os.system(f'{backup_script} > /logs/backup.log 2>&1 &')
self.send_json({
'status': 'triggered',
'message': 'Backup started in background',
'timestamp': datetime.now().isoformat()
})
else:
self.send_json({
'error': 'Backup script not found',
'path': backup_script
}, status=500)
return
# Not found
self.send_json({
'error': 'Not found',
'path': path,
'available_endpoints': [
'/backup - Trigger backup (POST)'
]
}, status=404)
def _get_uptime(self):
"""Get container uptime"""
try:
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.read().split()[0])
hours = int(uptime_seconds // 3600)
minutes = int((uptime_seconds % 3600) // 60)
return f'{hours}h {minutes}m'
except:
return 'unknown'
def main():
"""Start the HTTP server"""
server_address = ('', PORT)
httpd = HTTPServer(server_address, PBSAPIHandler)
log.info(f'Starting API server on port {PORT}')
log.info(f'Dashboard: http://localhost:{PORT}/')
log.info(f'API endpoints: /status /health /logs /backup')
try:
httpd.serve_forever()
except KeyboardInterrupt:
log.info('Shutting down API server')
httpd.shutdown()
return 0
except Exception as e:
log.error(f'Server error: {e}')
return 1
if __name__ == '__main__':
sys.exit(main())