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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user