#!/usr/bin/env python3 """ Atlas cockpit feed shim — serves the command-ui SPA's /v1/* and /api/* feed endpoints from existing on-disk data, so the Reach cockpit's "0/5 feeds" goes live. Unknown paths proxy through to control-room (:5057) so nothing regresses. Sits behind Caddy + Authentik on command.example.com; trusts upstream. """ import json, os, subprocess, urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer PORT = 7797 CONTROL_ROOM = "http://127.0.0.1:5057" DASH = "/opt/atlas/dashboard" DATA = "/opt/atlas/data" SETUP = "/opt/atlas/setup" def _read_json(path, default): try: with open(path, encoding="utf-8") as f: return json.load(f) except Exception: return default def _read_jsonl(path, limit=50): out = [] try: with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if line: try: out.append(json.loads(line)) except Exception: pass except Exception: pass return out[-limit:][::-1] def feed_today(): return _read_json(f"{DASH}/today.json", {"score": 0, "rationale": []}) def feed_missions(): m = _read_json(f"{DASH}/state/missions.json", []) return {"missions": m} if isinstance(m, list) else m def feed_reach(): d = _read_json(f"{DATA}/devices.json", {}) nodes = list(d.values()) if isinstance(d, dict) else (d or []) return {"nodes": nodes} def feed_hub(): return _read_json(f"{SETUP}/hub-feed.json", {"items": []}) def feed_academy(): return _read_json(f"{SETUP}/academy-feed.json", {"items": []}) def feed_notifications(): return {"notifications": _read_jsonl(f"{DATA}/notifications.jsonl")} def feed_services(): return _read_json(f"{DASH}/state/services.json", {"services": []}) def feed_canvas(): return _read_json(f"{DASH}/state/canvas.json", {"items": []}) def feed_tax(): return _read_json(f"{DASH}/state/tax.json", {"status": "calm", "items": []}) def feed_devices_live(): return feed_reach() def feed_disk(): try: out = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5).stdout parts = out.strip().splitlines()[-1].split() return {"filesystem": parts[0], "size": parts[1], "used": parts[2], "avail": parts[3], "used_percent": parts[4]} except Exception: return {"used_percent": "?"} def feed_git(): return {"ok": True, "branch": "main", "dirty": False} def feed_search(): return {"results": []} ROUTES = { "/v1/today": feed_today, "/v1/missions": feed_missions, "/v1/reach/nodes": feed_reach, "/v1/feeds/hub": feed_hub, "/v1/feeds/academy": feed_academy, "/v1/notifications": feed_notifications, "/v1/canvas": feed_canvas, "/v1/tax": feed_tax, "/v1/devices/live": feed_devices_live, "/api/services": feed_services, "/api/disk": feed_disk, "/api/git": feed_git, } class Handler(BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, obj, status=200): body = json.dumps(obj, default=str).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _proxy(self, path): try: req = urllib.request.Request(CONTROL_ROOM + path) auth = self.headers.get("Authorization") if auth: req.add_header("Authorization", auth) with urllib.request.urlopen(req, timeout=15) as r: data = r.read() self.send_response(r.status) self.send_header("Content-Type", r.headers.get("Content-Type", "application/json")) self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) except urllib.error.HTTPError as e: self._send({"ok": False, "error": f"upstream {e.code}"}, e.code) except Exception as e: self._send({"ok": False, "error": str(e)}, 502) def do_GET(self): path = self.path.split("?")[0].rstrip("/") or "/" if path == "/healthz": return self._send({"ok": True, "shim": "cockpit"}) if path.startswith("/v1/search"): return self._send(feed_search()) fn = ROUTES.get(path) if fn: try: return self._send(fn()) except Exception as e: return self._send({"ok": False, "error": str(e)}) return self._proxy(self.path) def do_POST(self): self._proxy(self.path) if __name__ == "__main__": print(f"cockpit-shim on :{PORT}", flush=True) ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()