from __future__ import annotations import html import json from fastapi import FastAPI from fastapi.responses import HTMLResponse, RedirectResponse _BASE_CSS = """ :root { --bg:#0b1020; --panel:#111a33; --muted:#95a3c6; --fg:#e8eeff; --accent:#7aa2ff; --danger:#ff6b6b; --ok:#3ddc97; } * { box-sizing: border-box; } body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; background:var(--bg); color:var(--fg); } a { color:var(--accent); text-decoration:none; } .layout { display:flex; min-height:100vh; } .nav { width: 240px; padding:16px; background: linear-gradient(180deg, #0e1630, #0b1020); border-right: 1px solid rgba(255,255,255,0.06); } .brand { font-weight: 700; letter-spacing: .2px; margin-bottom: 12px; } .nav a { display:block; padding:10px 10px; border-radius:10px; color: var(--fg); opacity: .9; } .nav a.active { background: rgba(122,162,255,0.14); border: 1px solid rgba(122,162,255,0.22); } .nav a:hover { background: rgba(255,255,255,0.06); } .main { flex:1; padding: 20px 24px; } .card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 16px; } .row { display:flex; gap: 12px; align-items:center; flex-wrap: wrap; } .muted { color: var(--muted); } .btn { border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.06); color: var(--fg); padding: 10px 12px; border-radius: 10px; cursor: pointer; } .btn:hover { background: rgba(255,255,255,0.10); } .btn.danger { border-color: rgba(255,107,107,0.35); background: rgba(255,107,107,0.10); } .pill { display:inline-block; padding: 2px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.16); font-size: 12px; } .pill.ok { border-color: rgba(61,220,151,0.35); background: rgba(61,220,151,0.12); } .pill.bad { border-color: rgba(255,107,107,0.35); background: rgba(255,107,107,0.12); } textarea, input { width: 100%; color: var(--fg); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; padding: 10px 12px; outline: none; } button:disabled { opacity: .45; cursor: not-allowed; } pre { white-space: pre-wrap; word-break: break-word; } table { width:100%; border-collapse: collapse; } th, td { padding: 10px 8px; border-bottom: 1px solid rgba(255,255,255,0.08); text-align:left; } """.strip() _BASE_JS = """ function mvpTokenGet() { return (localStorage.getItem("mvp_token") || "").trim(); } function mvpTokenSet(v) { localStorage.setItem("mvp_token", (v || "").trim()); } function mvpSftpPasswordGet() { return (localStorage.getItem("mvp_sftp_password") || "").trim(); } function mvpSftpPasswordSet(v) { localStorage.setItem("mvp_sftp_password", (v || "").trim()); } async function apiFetch(path, opts) { opts = opts || {}; opts.headers = opts.headers || {}; const tok = mvpTokenGet(); if (tok) opts.headers["Authorization"] = "Bearer " + tok; return fetch(path, opts); } async function apiJson(path, opts) { const resp = await apiFetch(path, opts); const text = await resp.text(); if (!resp.ok) { const err = new Error("HTTP " + resp.status); err.status = resp.status; err.body = text; throw err; } return JSON.parse(text); } function fmtJson(obj) { try { return JSON.stringify(obj, null, 2); } catch (e) { return String(obj); } } function curOriginWithPort(port) { const proto = window.location.protocol; const host = window.location.hostname; return proto + "//" + host + ":" + port; } async function copyText(v) { if (!v) return false; try { await navigator.clipboard.writeText(v); return true; } catch (e) { // Fallback for non-secure contexts (http) or older browsers. try { const ta = document.createElement("textarea"); ta.value = v; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand("copy"); document.body.removeChild(ta); return ok; } catch (e2) { return false; } } } document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("nav-ray-dashboard"); if (el) el.href = curOriginWithPort(8265); }); """.strip() def _nav(active: str) -> str: links = [ ("login", "/ui/login", "Login"), ("tasks", "/ui/tasks", "Tasks"), ("new", "/ui/tasks/new", "New Task"), ("data", "/ui/data", "Data"), ("admin", "/ui/admin", "Admin"), ("ray", "#", "Ray Dashboard"), ] items = [] for key, href, label in links: cls = "active" if key == active else "" extra = "" if key == "ray": extra = ' id="nav-ray-dashboard" target="_blank" rel="noopener"' items.append(f'{html.escape(label)}') return "\n".join(items) def _page(title: str, active: str, body: str, script: str = "") -> str: return f""" {html.escape(title)}
{body}
""" def register_ui_routes(app: FastAPI) -> None: @app.get("/ui") async def ui_root() -> RedirectResponse: return RedirectResponse(url="/ui/tasks") @app.get("/ui/login") async def ui_login() -> HTMLResponse: body = """

Login

Paste your API token (without the Bearer prefix).
Go to Tasks
""".strip() script = """ const tokEl = document.getElementById("tok"); const msg = document.getElementById("msg"); tokEl.value = mvpTokenGet(); document.getElementById("save").onclick = () => { mvpTokenSet(tokEl.value); msg.textContent = "Saved."; }; document.getElementById("clear").onclick = () => { mvpTokenSet(""); tokEl.value = ""; msg.textContent = "Cleared."; }; """.strip() return HTMLResponse(content=_page("Login", "login", body, script)) @app.get("/ui/tasks") async def ui_tasks() -> HTMLResponse: body = """

Tasks

New Task
Loading...
""".strip() script = """ const out = document.getElementById("out"); async function refresh() { out.textContent = "Loading..."; try { const q = await apiJson("/api/v2/queue"); const completedLimit = 25; const completedOffset = Number(localStorage.getItem("mvp_completed_offset") || "0") || 0; const done = await apiJson("/api/v2/tasks?limit=" + completedLimit + "&offset=" + completedOffset + "&states=SUCCEEDED,FAILED,CANCELED"); function pill(state) { const s = String(state || ""); if (s === "SUCCEEDED") return `${s}`; if (s === "FAILED") return `${s}`; if (s === "CANCELED") return `${s}`; if (s === "RUNNING") return `${s}`; if (s === "QUEUED" || s === "PENDING_RESOURCES" || s === "SUBMITTING" || s === "SUBMITTED") return `${s}`; return `${s}`; } function row(t) { const id = t.task_id; return ` ${id} ${t.workload} ${pill(t.state)} ${t.nnodes} x ${t.n_gpus_per_node} GPU ${t.updated_at || ""} `; } const running = (q.running || []).map(row).join(""); const pending = (q.pending || []).map(row).join(""); const doneRows = (done.tasks || []).map(row).join(""); const pageNo = Math.floor(completedOffset / completedLimit) + 1; const prevDisabled = completedOffset <= 0; const nextDisabled = !done.has_more; out.innerHTML = `
Tip: configure token in Login.

Running

${running || ""}
TaskWorkloadStateResourcesUpdated
(none)

Pending

${pending || ""}
TaskWorkloadStateResourcesUpdated
(none)

Completed

Page ${pageNo}
${doneRows || ""}
TaskWorkloadStateResourcesUpdated
(none)
`; const prevBtn = document.getElementById("done-prev"); const nextBtn = document.getElementById("done-next"); if (prevBtn) prevBtn.onclick = () => { const cur = Number(localStorage.getItem("mvp_completed_offset") || "0") || 0; const next = Math.max(0, cur - completedLimit); localStorage.setItem("mvp_completed_offset", String(next)); refresh(); }; if (nextBtn) nextBtn.onclick = () => { const cur = Number(localStorage.getItem("mvp_completed_offset") || "0") || 0; const next = cur + completedLimit; localStorage.setItem("mvp_completed_offset", String(next)); refresh(); }; } catch (e) { out.textContent = "Error: " + (e.status || "") + "\\n" + (e.body || String(e)); } } document.getElementById("refresh").onclick = refresh; refresh(); """.strip() return HTMLResponse(content=_page("Tasks", "tasks", body, script)) @app.get("/ui/tasks/new") async def ui_new_task() -> HTMLResponse: ppo = """# PPO TaskSpec (YAML) workload: ppo nnodes: 2 n_gpus_per_node: 4 code_path: /private/common/code/verl/verl_repo train_file: /private/common/datasets/gsm8k/train.parquet val_file: /private/common/datasets/gsm8k/test.parquet model_id: Qwen/Qwen2.5-0.5B-Instruct """.strip() grpo = """# GRPO TaskSpec (YAML) workload: grpo nnodes: 2 n_gpus_per_node: 4 code_path: /private/common/code/verl/verl_repo train_file: /private/common/datasets/gsm8k/train.parquet val_file: /private/common/datasets/gsm8k/test.parquet model_id: Qwen/Qwen2.5-0.5B-Instruct """.strip() sft = """# SFT TaskSpec (YAML) workload: sft nnodes: 1 n_gpus_per_node: 1 code_path: /private/common/code/verl/verl_repo train_file: /private/common/datasets/gsm8k_sft/train.parquet val_file: /private/common/datasets/gsm8k_sft/test.parquet model_id: Qwen/Qwen2.5-0.5B-Instruct """.strip() body = f"""

New Task

Paste TaskSpec YAML and submit to API server. Note: code_path is required (v3.0 does not execute user code; use the common snapshot).
Back

""".strip() tpl_ppo = json.dumps(ppo) tpl_grpo = json.dumps(grpo) tpl_sft = json.dumps(sft) script = ( """ const msg = document.getElementById("msg"); const yamlEl = document.getElementById("yaml"); const TPL_PPO = __TPL_PPO__; const TPL_GRPO = __TPL_GRPO__; const TPL_SFT = __TPL_SFT__; document.getElementById("tpl-ppo").onclick = () => { yamlEl.value = TPL_PPO; msg.textContent = ""; }; document.getElementById("tpl-grpo").onclick = () => { yamlEl.value = TPL_GRPO; msg.textContent = ""; }; document.getElementById("tpl-sft").onclick = () => { yamlEl.value = TPL_SFT; msg.textContent = ""; }; document.getElementById("submit").onclick = async () => { msg.textContent = "Submitting..."; const body = yamlEl.value; const resp = await apiFetch("/api/v2/tasks", { method: "POST", headers: {"Content-Type":"text/plain"}, body }); const text = await resp.text(); if (!resp.ok) { msg.textContent = "Error: " + resp.status + "\\n" + text; return; } const obj = JSON.parse(text); msg.textContent = "OK: " + fmtJson(obj); if (obj.task_id) window.location.href = "/ui/tasks/" + obj.task_id; }; """.strip() .replace("__TPL_PPO__", tpl_ppo) .replace("__TPL_GRPO__", tpl_grpo) .replace("__TPL_SFT__", tpl_sft) ) return HTMLResponse(content=_page("New Task", "new", body, script)) @app.get("/ui/tasks/{task_id}") async def ui_task_detail(task_id: str) -> HTMLResponse: safe_id = html.escape(task_id) body = f"""

Task: {safe_id}

Logs Back
Loading...
""".strip() script = f""" document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265); const out = document.getElementById("out"); async function refresh() {{ out.textContent = "Loading..."; const resp = await apiFetch("/api/v2/tasks/{task_id}"); const text = await resp.text(); if (!resp.ok) {{ out.textContent = "Error: " + resp.status + "\\n" + text; return; }} out.textContent = fmtJson(JSON.parse(text)); }} document.getElementById("refresh").onclick = refresh; document.getElementById("cancel").onclick = async () => {{ if (!confirm("Cancel this task?")) return; const resp = await apiFetch("/api/v2/tasks/{task_id}:cancel", {{ method: "POST" }}); const text = await resp.text(); out.textContent = (resp.ok ? "Canceled.\\n" : "Error: " + resp.status + "\\n") + text; setTimeout(refresh, 800); }}; refresh(); """.strip() return HTMLResponse(content=_page(f"Task {task_id}", "tasks", body, script)) @app.get("/ui/tasks/{task_id}/logs") async def ui_task_logs(task_id: str) -> HTMLResponse: safe_id = html.escape(task_id) body = f"""

Logs: {safe_id}

Back
Loading...
""".strip() script = f""" document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265); const out = document.getElementById("out"); let timer = null; async function refresh() {{ const resp = await apiFetch("/api/v2/tasks/{task_id}/logs?tail=4000"); const text = await resp.text(); out.textContent = resp.ok ? text : ("Error: " + resp.status + "\\n" + text); }} document.getElementById("refresh").onclick = refresh; document.getElementById("auto").onchange = (e) => {{ if (e.target.checked) {{ timer = setInterval(refresh, 2000); }} else {{ if (timer) clearInterval(timer); timer = null; }} }}; refresh(); """.strip() return HTMLResponse(content=_page(f"Logs {task_id}", "tasks", body, script)) @app.get("/ui/data") async def ui_data() -> HTMLResponse: body = """

Data

User files live under your home directory. Keep long-term artifacts in models/ or datasets/.
Username
SFTPGo password
Open SFTPGo Web Client (:8081)
You can also use an SFTP client (e.g. FileZilla) with the same username/password. Host: , Port: .
Loading...
""".strip() script = """ const out = document.getElementById("out"); document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265); const u = document.getElementById("u"); const p = document.getElementById("p"); const sftpWeb = document.getElementById("sftp-web"); const sftpHost = document.getElementById("sftp-host"); const sftpPort = document.getElementById("sftp-port"); document.getElementById("copy-u").onclick = async () => { await copyText(u.value || ""); }; document.getElementById("copy-p").onclick = async () => { await copyText(p.value || ""); }; async function refresh() { const resp = await apiFetch("/api/v2/me"); const text = await resp.text(); if (!resp.ok) { out.textContent = "Error: " + resp.status + "\\n" + text; return; } const obj = JSON.parse(text); u.value = (obj.user_id || ""); const cached = mvpSftpPasswordGet(); if (cached) p.value = cached; const host = curOriginWithPort(8081); sftpWeb.href = host + "/web/client"; sftpHost.textContent = (obj.sftp && obj.sftp.host) ? obj.sftp.host : window.location.hostname; sftpPort.textContent = (obj.sftp && obj.sftp.port) ? String(obj.sftp.port) : "2022"; out.textContent = fmtJson(obj); } document.getElementById("reset-p").onclick = async () => { p.value = ""; const resp = await apiFetch("/api/v2/me/sftp:reset_password", { method: "POST" }); const text = await resp.text(); if (!resp.ok) { out.textContent = "Error: " + resp.status + "\\n" + text; return; } const obj = JSON.parse(text); p.value = obj.password || ""; mvpSftpPasswordSet(p.value); out.textContent = "SFTPGo password rotated.\\n\\n" + fmtJson(obj); }; refresh(); """.strip() return HTMLResponse(content=_page("Data", "data", body, script)) @app.get("/ui/admin") async def ui_admin() -> HTMLResponse: body = """

Admin

This page requires the admin token (set it in Login).

Create user



  
Loading...
""".strip() script = """ const out = document.getElementById("out"); const createMsg = document.getElementById("create-msg"); const userIdEl = document.getElementById("new-user-id"); const displayNameEl = document.getElementById("new-display-name"); function esc(s) { s = String(s || ""); return s.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">"); } async function refresh() { out.textContent = "Loading..."; try { const obj = await apiJson("/api/v2/users?limit=200"); const users = (obj.users || []); function row(u) { const uid = u.user_id; const tok = u.token || ""; const tokShort = tok ? (tok.length > 18 ? (tok.slice(0, 18) + "…") : tok) : ""; const created = u.created_at || ""; const updated = u.updated_at || ""; const tCreated = u.token_created_at || ""; const tUsed = u.token_last_used_at || ""; return ` ${esc(uid)} ${esc(created)} ${esc(updated)}
${esc(tokShort)}
token_created_at: ${esc(tCreated)}; last_used_at: ${esc(tUsed)}
`; } out.innerHTML = ` ${users.map(row).join("") || ""}
UserCreatedUpdatedToken
(none)
`; for (const btn of out.querySelectorAll("button[data-copy]")) { btn.onclick = async () => { await copyText(btn.getAttribute("data-copy") || ""); }; } for (const btn of out.querySelectorAll("button[data-issue]")) { btn.onclick = async () => { const uid = btn.getAttribute("data-issue"); if (!uid) return; try { const r = await apiJson("/api/v2/users/" + encodeURIComponent(uid) + "/tokens", { method: "POST" }); createMsg.textContent = "Issued token:\\n" + fmtJson(r); await refresh(); } catch (e) { createMsg.textContent = "Error issuing token: " + (e.status || "") + "\\n" + (e.body || String(e)); } }; } } catch (e) { out.textContent = "Error: " + (e.status || "") + "\\n" + (e.body || String(e)); } } document.getElementById("refresh").onclick = refresh; document.getElementById("create-user").onclick = async () => { createMsg.textContent = "Creating..."; const user_id = (userIdEl.value || "").trim(); const display_name = (displayNameEl.value || "").trim(); if (!user_id) { createMsg.textContent = "user_id is required"; return; } const payload = { user_id: user_id }; if (display_name) payload.display_name = display_name; try { const r = await apiJson("/api/v2/users", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(payload) }); createMsg.textContent = "Created:\\n" + fmtJson(r); userIdEl.value = ""; displayNameEl.value = ""; await refresh(); } catch (e) { createMsg.textContent = "Error: " + (e.status || "") + "\\n" + (e.body || String(e)); } }; refresh(); """.strip() return HTMLResponse(content=_page("Admin", "admin", body, script))