diff --git a/hub/dashboard.html b/hub/dashboard.html index 5e116f2..b11b86b 100644 --- a/hub/dashboard.html +++ b/hub/dashboard.html @@ -5,113 +5,133 @@ Pioneer Bus Fleet — Console -

Pioneer Bus Fleet

-
Single-pane console — GL-XE3000 + Synology RT2600ac routers
+
+

PIONEER BUS FLEET

+
Keylink IT Fleet Connectivity Console
+
+ +
-
Last updated: --
+
Loading…
diff --git a/hub/server.py b/hub/server.py index db88b7c..e98d0a8 100644 --- a/hub/server.py +++ b/hub/server.py @@ -1,53 +1,70 @@ #!/usr/bin/env python3 -"""Busfleet Hub — registration server for kit-connect devices. +"""Busfleet Hub — fleet registration + dashboard server. Listens on :8080. Routes: + GET / — fleet dashboard (HTML) + GET /health — health check GET /api/register/ — assign tunnel port, return JSON - POST /api/authorize-key — authorize device's tunnel key - GET /health — health check + GET /api/fleet — all registered devices with + tunnel status + access links + POST /api/authorize-key — authorize device tunnel key """ import json +import os +import socket import subprocess import sys from http.server import HTTPServer, BaseHTTPRequestHandler REGISTER_SH = "/opt/busfleet-hub/register.sh" +REGISTRY = "/opt/busfleet-hub/port-registry.json" +DASHBOARD = "/opt/busfleet-hub/dashboard.html" +HUB_IP = "162.243.83.36" + + +# ── Helper: check if a TCP port is open (tunnel active) ────────── +def _port_is_open(port): + try: + s = socket.create_connection(("127.0.0.1", port), timeout=2) + s.close() + return True + except Exception: + return False class HubHandler(BaseHTTPRequestHandler): - """Handle hub registration and key authorization requests.""" + """Handle hub registration, fleet status, and dashboard requests.""" def do_GET(self): - # Route: /api/register/ + # Fleet dashboard (homepage) + if self.path in ("/", "/index.html"): + self.serve_file(DASHBOARD, "text/html; charset=utf-8") + return + + # Health check + if self.path == "/health": + self.send_json(200, '{"status":"ok","service":"busfleet-hub"}') + return + + # Device registration if self.path.startswith("/api/register/"): device_id = self.path.split("/api/register/", 1)[1].strip("/") if not device_id: self.send_json_error(400, "Missing device ID") return - - try: - result = subprocess.run( - [REGISTER_SH, device_id], - capture_output=True, text=True, timeout=15, - ) - status = 200 if result.returncode == 0 else 500 - self.send_json(status, result.stdout.strip()) - except subprocess.TimeoutExpired: - self.send_json_error(504, "Registration timed out") - except Exception as exc: - self.send_json_error(500, str(exc)) + self.call_register_script(device_id) return - # Health check - if self.path in ("/", "/health"): - self.send_json(200, '{"status":"ok","service":"busfleet-hub"}') + # Fleet status — all registered devices + if self.path == "/api/fleet": + self.serve_fleet_status() return self.send_json_error(404, "Not found") def do_POST(self): - # Route: /api/authorize-key + # Key authorization if self.path.startswith("/api/authorize-key"): content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length).decode() if content_length else "{}" @@ -64,7 +81,6 @@ class HubHandler(BaseHTTPRequestHandler): self.send_json_error(400, "Missing device_id or pubkey") return - # Basic validation: key must look like an SSH public key if not pubkey.startswith(("ssh-", "ecdsa-", "sk-")) or len(pubkey) < 80: self.send_json_error(400, "Invalid SSH public key format") return @@ -84,6 +100,67 @@ class HubHandler(BaseHTTPRequestHandler): self.send_json_error(404, "Not found") + # ── Helpers ──────────────────────────────────────────────────── + + def serve_file(self, path, content_type): + """Serve a static file.""" + if not os.path.isfile(path): + self.send_json_error(404, "File not found") + return + with open(path, "rb") as f: + data = f.read() + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def call_register_script(self, device_id): + """Run register.sh and return its output.""" + try: + result = subprocess.run( + [REGISTER_SH, device_id], + capture_output=True, text=True, timeout=15, + ) + status = 200 if result.returncode == 0 else 500 + self.send_json(status, result.stdout.strip()) + except subprocess.TimeoutExpired: + self.send_json_error(504, "Registration timed out") + except Exception as exc: + self.send_json_error(500, str(exc)) + + def serve_fleet_status(self): + """Build and return fleet status from port-registry.json + + live tunnel checks.""" + devices = [] + try: + with open(REGISTRY) as f: + registry = json.load(f) + except Exception: + registry = {} + + for dev_id, entry in registry.items(): + port = entry.get("tunnel_port", 0) + tunnel_up = _port_is_open(port) if port else False + + devices.append({ + "device_id": dev_id, + "tunnel_port": port, + "assigned": entry.get("assigned", ""), + "tunnel_up": tunnel_up, + "ssh_command": f"ssh -p {port} kitadmin@{HUB_IP}", + "dashboard_url": f"http://{HUB_IP}:{port}" if tunnel_up else None, + }) + + # Sort: tunnel_up first, then by device_id + devices.sort(key=lambda d: (not d["tunnel_up"], d["device_id"])) + + self.send_json(200, json.dumps({ + "hub": HUB_IP, + "device_count": len(devices), + "devices": devices, + })) + def send_json(self, status, body): """Send a JSON response.""" self.send_response(status) @@ -104,6 +181,7 @@ def main(): port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 server = HTTPServer(("0.0.0.0", port), HubHandler) print(f"Busfleet Hub listening on :{port}", flush=True) + print(f" Dashboard: http://{HUB_IP}:{port}/", flush=True) try: server.serve_forever() except KeyboardInterrupt: