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
+106
View File
@@ -163,3 +163,109 @@ def get_deck(deck_id: str) -> dict[str, Any] | None:
if not path.exists():
return None
return json.loads(path.read_text())
def _blank_slide(idx: int = 1) -> dict[str, Any]:
return {
"id": f"slide-{idx}",
"title": f"Slide {idx}",
"subtitle": "",
"bullets": ["Double-click to edit this point"],
"image": "",
"kind": "narrative",
}
def create_deck(title: str = "Untitled deck", slides: list[dict[str, Any]] | None = None) -> dict[str, Any]:
"""Create a new editable deck (optionally seeded from existing slides)."""
_ensure_dir()
deck_id = str(uuid.uuid4())[:8]
deck_dir = PRESENTATIONS_DIR / deck_id
(deck_dir / "assets").mkdir(parents=True, exist_ok=True)
seeded = [dict(s) for s in (slides or [])] or [_blank_slide(1)]
payload = {
"id": deck_id,
"filename": "",
"source": "editor",
"editable": True,
"ts": datetime.now(timezone.utc).isoformat(),
"title": (title or "Untitled deck").strip()[:120] or "Untitled deck",
"subtitle": "Editable deck",
"slide_count": len(seeded),
"slides": seeded,
}
(deck_dir / "meta.json").write_text(json.dumps(payload, indent=2, default=str))
(deck_dir / "deck.json").write_text(json.dumps(payload, default=str))
return payload
def save_deck(deck_id: str, body: dict[str, Any]) -> dict[str, Any] | None:
"""Overwrite an existing editable deck's content. Returns None if missing."""
deck_dir = PRESENTATIONS_DIR / deck_id
meta_path = deck_dir / "meta.json"
if not meta_path.exists():
return None
existing = json.loads(meta_path.read_text())
incoming_slides = body.get("slides")
clean_slides: list[dict[str, Any]] = []
for i, s in enumerate(incoming_slides or [], start=1):
if not isinstance(s, dict):
continue
bullets = [str(b).strip()[:400] for b in (s.get("bullets") or []) if str(b).strip()]
clean_slides.append({
"id": str(s.get("id") or f"slide-{i}"),
"title": str(s.get("title") or f"Slide {i}")[:200],
"subtitle": str(s.get("subtitle") or "")[:300],
"bullets": bullets,
"image": str(s.get("image") or "")[:300],
"kind": str(s.get("kind") or "narrative")[:40],
})
if not clean_slides:
clean_slides = [_blank_slide(1)]
existing.update({
"title": str(body.get("title") or existing.get("title") or "Untitled deck")[:120],
"subtitle": str(body.get("subtitle") or existing.get("subtitle") or "")[:300],
"editable": True,
"source": existing.get("source") or "editor",
"ts": datetime.now(timezone.utc).isoformat(),
"slides": clean_slides,
"slide_count": len(clean_slides),
})
meta_path.write_text(json.dumps(existing, indent=2, default=str))
(deck_dir / "deck.json").write_text(json.dumps(existing, default=str))
return existing
def delete_deck(deck_id: str) -> bool:
import shutil
deck_dir = PRESENTATIONS_DIR / deck_id
if not deck_dir.exists():
return False
shutil.rmtree(deck_dir, ignore_errors=True)
return True
def save_image(deck_id: str, filename: str, content: bytes) -> dict[str, Any] | None:
"""Store an image in the deck's assets folder; return its served URL."""
deck_dir = PRESENTATIONS_DIR / deck_id
if not deck_dir.exists():
return None
assets = deck_dir / "assets"
assets.mkdir(parents=True, exist_ok=True)
ext = ""
if "." in filename:
ext = "." + _safe_name(filename.rsplit(".", 1)[-1].lower())[:8]
name = f"{uuid.uuid4().hex[:12]}{ext}"
(assets / name).write_bytes(content)
return {"name": name, "url": f"/api/presentation/decks/{deck_id}/assets/{name}"}
def get_asset_path(deck_id: str, name: str) -> Path | None:
safe = _safe_name(name)
path = PRESENTATIONS_DIR / deck_id / "assets" / safe
if not path.exists() or not path.is_file():
return None
return path