Add HDFS browser, SSH terminal, presentation editor, lab health panel

- HDFS WebHDFS file browser (api/hdfs_api.py + HdfsView)
- In-browser SSH terminal via paramiko WebSocket bridge (api/ssh_terminal.py + SshTerminal, xterm.js)
- Presentation deck editor (text + image upload) and CRUD endpoints
- Collapsible GPU matrix + new LabHealthPanel in SideNav
- Topology fixes (edge alignment, Hadoop node, compact nodes)
- nginx ws timeout bump for long-lived SSH sessions
This commit is contained in:
mo
2026-06-26 00:47:49 +00:00
parent fefe3016ad
commit 46b51a891c
21 changed files with 1957 additions and 328 deletions
+80 -2
View File
@@ -13,7 +13,7 @@ from typing import Any
import httpx
import redis.asyncio as aioredis
from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect
from fastapi import Body, FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from agent_terminal import (
get_all_terminals,
@@ -25,11 +25,22 @@ from agent_terminal import (
)
from lab_context import collect_full_lab_context, format_context_for_agent
from presentation import build_presentation_payload, render_presentation_html
from presentation_upload import get_deck, list_decks, save_upload
from presentation_upload import (
create_deck,
delete_deck,
get_asset_path,
get_deck,
list_decks,
save_deck,
save_image,
save_upload,
)
from presentation_static import get_static_deck, list_static_decks
from storage_s3 import router as storage_s3_router
from hdfs_api import router as hdfs_router
from elasticsearch_api import router as elasticsearch_router
from sql_console import router as sql_router
from ssh_terminal import ssh_session
from node_registry import NODE_IDS, NODE_AGENT, NODE_REGISTRY, is_node_id
from node_ops import build_node_detail, probe_node, run_node_probe_task
from approval_service import (
@@ -707,6 +718,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="ATC Command Center API", lifespan=lifespan)
app.include_router(storage_s3_router)
app.include_router(hdfs_router)
app.include_router(elasticsearch_router)
app.include_router(sql_router)
app.add_middleware(
@@ -814,6 +826,67 @@ async def upload_presentation(file: UploadFile = File(...)):
deck = await save_upload(file.filename or "upload.pptx", content)
return {"ok": True, "deck": deck}
@app.post("/api/presentation/decks/new")
async def create_presentation_deck(body: dict[str, Any] | None = Body(default=None)):
body = body or {}
title = str(body.get("title") or "Untitled deck")
seed_id = body.get("from")
slides = None
if seed_id:
src = get_static_deck(seed_id) or get_deck(seed_id)
if seed_id == "live":
src = await get_presentation_data()
if src and src.get("slides"):
slides = [
{
"id": s.get("id") or f"slide-{i}",
"title": s.get("title") or f"Slide {i}",
"subtitle": s.get("subtitle") or "",
"bullets": list(s.get("bullets") or []),
"image": s.get("image") or "",
"kind": s.get("kind") or "narrative",
}
for i, s in enumerate(src["slides"], start=1)
]
if not title or title == "Untitled deck":
title = f"{src.get('title', 'Deck')} (copy)"
deck = create_deck(title, slides)
return {"ok": True, "deck": deck}
@app.put("/api/presentation/decks/{deck_id}")
async def update_presentation_deck(deck_id: str, body: dict[str, Any] = Body(...)):
deck = save_deck(deck_id, body)
if not deck:
return {"error": "deck not found or not editable"}
return {"ok": True, "deck": deck}
@app.delete("/api/presentation/decks/{deck_id}")
async def remove_presentation_deck(deck_id: str):
return {"ok": delete_deck(deck_id)}
@app.post("/api/presentation/decks/{deck_id}/image")
async def upload_presentation_image(deck_id: str, file: UploadFile = File(...)):
content = await file.read()
if len(content) > 12 * 1024 * 1024:
return {"error": "image too large (max 12MB)"}
result = save_image(deck_id, file.filename or "image.png", content)
if not result:
return {"error": "deck not found"}
return {"ok": True, **result}
@app.get("/api/presentation/decks/{deck_id}/assets/{name}")
async def get_presentation_asset(deck_id: str, name: str):
from fastapi.responses import FileResponse, Response
path = get_asset_path(deck_id, name)
if not path:
return Response(status_code=404)
return FileResponse(str(path))
@app.get("/api/workload")
async def get_workload(fast: bool = True):
return await collect_workload(fast=fast, use_cache=True)
@@ -1004,6 +1077,11 @@ async def post_prompt(body: PromptRequest):
return {"prompt_id": prompt_id, "agent_id": agent_id, "status": "dispatched"}
@app.websocket("/api/ws/ssh")
async def ws_ssh(websocket: WebSocket):
await ssh_session(websocket)
@app.websocket("/api/ws/ops")
async def ws_ops(websocket: WebSocket):
await websocket.accept()