- assets/header.svg: animated SVG header (twinkling stars, orbiting service dots, Atlas A logo with glow, float/pulse animations, corner accents) - README.md: animated banner, full 39-app service catalog, Mermaid architecture, AI stack table, tech stack, quick-deploy, repo structure, security model - ARCHITECTURE.md: one-box philosophy, 5-tier service layers, data flow diagram, Caddy SSO pattern, AI mesh topology, Docker Compose topology - SERVICES.md: all 55 containers + 20 systemd services documented - CHANGELOG.md: full build history from v0.5 to v2.0 - assets/logo.svg: Atlas A logo with gradient - deploy/install.sh: one-command installer (Docker + Caddy + Tailscale + stacks) - deploy/compose/atlas-core.yml: Authentik + Nextcloud + Stalwart + Forgejo + Vaultwarden - deploy/caddy/Caddyfile.template: Authentik SSO pattern, sanitized - deploy/README.md: full deployment guide - server/: sanitized atlas_brain.py, cockpit_shim.py, intelligence.py, meridian.py, atlas_router_api.py, cli-atlas.py (all secrets stripped, env var patterns) - server/systemd/: 6 atlas-* service unit files - server/README.md: deployment and configuration guide Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
4.9 KiB
Python
170 lines
4.9 KiB
Python
#!/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.atlascorporation.nl; 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()
|