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:
+80
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user