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>
217 lines
7.2 KiB
Python
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())
|