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'<a class="{cls}" href="{href}"{extra}>{html.escape(label)}</a>')
    return "\n".join(items)


def _page(title: str, active: str, body: str, script: str = "") -> str:
    return f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{html.escape(title)}</title>
  <style>{_BASE_CSS}</style>
</head>
<body>
  <div class="layout">
    <nav class="nav">
      <div class="brand">Argus MVP</div>
      {_nav(active)}
      <div style="margin-top:12px" class="muted">
        Token stored in browser localStorage as <code>mvp_token</code>.
      </div>
    </nav>
    <main class="main">
      {body}
    </main>
  </div>
  <script>{_BASE_JS}</script>
  <script>{script}</script>
</body>
</html>"""


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 = """
<h1>Login</h1>
<div class="card">
  <div class="muted">Paste your API token (without the <code>Bearer</code> prefix).</div>
  <div style="height:10px"></div>
  <input id="tok" placeholder="token..." />
  <div style="height:10px"></div>
  <div class="row">
    <button class="btn" id="save">Save</button>
    <button class="btn" id="clear">Clear</button>
    <a href="/ui/tasks" class="btn" style="display:inline-block">Go to Tasks</a>
  </div>
  <div style="height:10px"></div>
  <div id="msg" class="muted"></div>
</div>
""".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 = """
<h1>Tasks</h1>
<div class="card">
  <div class="row">
    <button class="btn" id="refresh">Refresh</button>
    <a class="btn" href="/ui/tasks/new" style="display:inline-block">New Task</a>
  </div>
  <div style="height:10px"></div>
  <div id="out" class="muted">Loading...</div>
</div>
""".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 `<span class="pill ok">${s}</span>`;
    if (s === "FAILED") return `<span class="pill bad">${s}</span>`;
    if (s === "CANCELED") return `<span class="pill">${s}</span>`;
    if (s === "RUNNING") return `<span class="pill ok">${s}</span>`;
    if (s === "QUEUED" || s === "PENDING_RESOURCES" || s === "SUBMITTING" || s === "SUBMITTED") return `<span class="pill">${s}</span>`;
    return `<span class="pill">${s}</span>`;
  }
  function row(t) {
    const id = t.task_id;
    return `<tr>
      <td><a href="/ui/tasks/${id}">${id}</a></td>
      <td>${t.workload}</td>
      <td>${pill(t.state)}</td>
      <td>${t.nnodes} x ${t.n_gpus_per_node} GPU</td>
      <td>${t.updated_at || ""}</td>
    </tr>`;
  }

  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 = `
    <div class="muted">Tip: configure token in <a href="/ui/login">Login</a>.</div>
    <div style="height:10px"></div>
    <h3>Running</h3>
    <table><thead><tr><th>Task</th><th>Workload</th><th>State</th><th>Resources</th><th>Updated</th></tr></thead><tbody>${running || "<tr><td colspan=5 class=muted>(none)</td></tr>"}</tbody></table>
    <div style="height:12px"></div>
    <h3>Pending</h3>
    <table><thead><tr><th>Task</th><th>Workload</th><th>State</th><th>Resources</th><th>Updated</th></tr></thead><tbody>${pending || "<tr><td colspan=5 class=muted>(none)</td></tr>"}</tbody></table>
    <div style="height:12px"></div>
    <h3>Completed</h3>
    <div class="row" style="justify-content: space-between; margin-bottom: 8px;">
      <div class="muted">Page ${pageNo}</div>
      <div class="row">
        <button class="btn" id="done-prev" ${prevDisabled ? "disabled" : ""}>Prev</button>
        <button class="btn" id="done-next" ${nextDisabled ? "disabled" : ""}>Next</button>
      </div>
    </div>
    <table><thead><tr><th>Task</th><th>Workload</th><th>State</th><th>Resources</th><th>Updated</th></tr></thead><tbody>${doneRows || "<tr><td colspan=5 class=muted>(none)</td></tr>"}</tbody></table>
  `;

  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  # 任务类型（必填）：ppo｜grpo｜sft
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        grpo = """# GRPO TaskSpec (YAML)
workload: grpo  # 任务类型（必填）：ppo｜grpo｜sft（grpo 会自动启用对应的算法配置）
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        sft = """# SFT TaskSpec (YAML)
workload: sft  # 任务类型（必填）：ppo｜grpo｜sft
code_path: /private/common/code/verl/verl_repo  # 代码路径（必填）：v3.0 固定使用 common 下的 verl 快照（不支持用户自定义代码）
model_id: Qwen/Qwen2.5-0.5B-Instruct  # 基础模型（必填）：HuggingFace 模型 ID 或 /private/... 本地模型路径
train_file: /private/common/datasets/gsm8k_sft/train.parquet  # 训练数据（必填）：parquet 文件路径（支持 /private/common/datasets 或 /private/users/<user>/datasets）
val_file: /private/common/datasets/gsm8k_sft/test.parquet  # 验证数据（必填）：parquet 文件路径（VERL 侧会用来构建 val dataset，不能为 null）

# nnodes: 2  # 训练节点数（可选，默认：2；单机可设 1）
# n_gpus_per_node: 4  # 每节点 GPU 数（可选，默认：4；单卡可设 1）

# total_epochs: 1  # 总训练 epoch（可选，默认：1）
# total_training_steps: null  # 总训练 step（可选，默认：null；不传则让 VERL 按 epochs 和数据长度自动推导）
# save_freq: 10  # checkpoint 保存频率（step）（可选，默认：10）
# test_freq: null  # 验证频率（step）（可选，默认：null；训练端会当成 -1=不验证）

# trainer_device: cpu  # 仅 SFT 生效：driver 侧 device（可选，默认：cpu）
# submission_id: ""  # Ray submission_id（可选，默认空；通常由服务自动生成，无需填写）
""".strip()
        body = f"""
<h1>New Task</h1>
<div class="card">
  <div class="muted">Paste TaskSpec YAML and submit to API server. Note: <code>code_path</code> is required (v3.0 does not execute user code; use the common snapshot).</div>
  <div style="height:10px"></div>
  <div class="row">
    <button class="btn" id="tpl-ppo">PPO example</button>
    <button class="btn" id="tpl-grpo">GRPO example</button>
    <button class="btn" id="tpl-sft">SFT example</button>
  </div>
  <div style="height:10px"></div>
  <textarea id="yaml" rows="16">{html.escape(ppo)}</textarea>
  <div style="height:10px"></div>
  <div class="row">
    <button class="btn" id="submit">Submit</button>
    <a class="btn" href="/ui/tasks" style="display:inline-block">Back</a>
  </div>
  <div style="height:10px"></div>
  <pre id="msg" class="muted"></pre>
</div>
""".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"""
<h1>Task: <code>{safe_id}</code></h1>
<div class="card">
  <div class="row">
    <a class="btn" href="/ui/tasks/{safe_id}/logs" style="display:inline-block">Logs</a>
    <button class="btn" id="refresh">Refresh</button>
    <button class="btn danger" id="cancel">Cancel</button>
    <a class="btn" href="/ui/tasks" style="display:inline-block">Back</a>
  </div>
  <div style="height:10px"></div>
  <pre id="out" class="muted">Loading...</pre>
</div>
<div style="height:12px"></div>
<div class="card">
  <h3 style="margin-top:0">TaskSpec (YAML)</h3>
  <div class="muted">Resolved TaskSpec (includes default values; submission_id reflects latest attempt when available).</div>
  <div style="height:10px"></div>
  <pre id="spec" class="muted">Loading...</pre>
</div>
""".strip()
        script = f"""
document.getElementById("nav-ray-dashboard").href = curOriginWithPort(8265);
const out = document.getElementById("out");
const spec = document.getElementById("spec");
async function refresh() {{
  out.textContent = "Loading...";
  spec.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));

  const resp2 = await apiFetch("/api/v2/tasks/{task_id}/spec");
  const text2 = await resp2.text();
  spec.textContent = resp2.ok ? text2 : ("Error: " + resp2.status + "\\n" + text2);
}}
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"""
<h1>Logs: <code>{safe_id}</code></h1>
<div class="card">
  <div class="row">
    <button class="btn" id="refresh">Refresh</button>
    <label class="muted">Auto refresh <input type="checkbox" id="auto" /></label>
    <a class="btn" href="/ui/tasks/{safe_id}" style="display:inline-block">Back</a>
  </div>
  <div style="height:10px"></div>
  <pre id="out" class="muted">Loading...</pre>
</div>
""".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 = """
<h1>Data</h1>
<div class="card">
  <div class="muted">User files live under your home directory. Keep long-term artifacts in <code>models/</code> or <code>datasets/</code>.</div>
  <div style="height:14px"></div>

  <div class="row">
    <div style="flex:1; min-width:260px">
      <div class="muted">Username</div>
      <div style="height:6px"></div>
      <div class="row" style="gap:8px">
        <input id="u" readonly />
        <button class="btn" id="copy-u">Copy</button>
      </div>
    </div>
    <div style="flex:1; min-width:260px">
      <div class="muted">SFTPGo password</div>
      <div style="height:6px"></div>
      <div class="row" style="gap:8px">
        <input id="p" placeholder="Click Reset to generate..." />
        <button class="btn" id="copy-p">Copy</button>
        <button class="btn" id="reset-p">Reset</button>
      </div>
    </div>
  </div>

  <div style="height:12px"></div>
  <div class="row">
    <a class="btn" id="sftp-web" target="_blank" rel="noopener" href="#">Open SFTPGo Web Client (:8081)</a>
  </div>

  <div style="height:12px"></div>
  <div class="muted">
    You can also use an SFTP client (e.g. FileZilla) with the same username/password.
    Host: <code id="sftp-host"></code>, Port: <code id="sftp-port"></code>.
  </div>

  <div style="height:14px"></div>
  <pre id="out" class="muted">Loading...</pre>
</div>
""".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 = """
<h1>Admin</h1>
<div class="card">
  <div class="muted">
    This page requires the <code>admin</code> token (set it in <a href="/ui/login">Login</a>).
  </div>
  <div style="height:14px"></div>

  <h3>Create user</h3>
  <div class="row">
    <input id="new-user-id" placeholder="user_id (e.g. alice)" style="max-width:320px" />
    <input id="new-display-name" placeholder="display_name (optional)" style="max-width:320px" />
    <button class="btn" id="create-user">Create</button>
  </div>
  <div style="height:10px"></div>
  <pre id="create-msg" class="muted"></pre>

  <div style="height:14px"></div>
  <div class="row">
    <button class="btn" id="refresh">Refresh</button>
  </div>
  <div style="height:10px"></div>
  <div id="out" class="muted">Loading...</div>
</div>
""".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("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;");
}

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 `<tr>
        <td><code>${esc(uid)}</code></td>
        <td class="muted">${esc(created)}</td>
        <td class="muted">${esc(updated)}</td>
        <td>
          <div class="row" style="gap:8px">
            <code title="${esc(tok)}">${esc(tokShort)}</code>
            <button class="btn" data-copy="${esc(tok)}">Copy</button>
            <button class="btn" data-issue="${esc(uid)}">Issue token</button>
          </div>
          <div class="muted" style="margin-top:6px">token_created_at: ${esc(tCreated)}; last_used_at: ${esc(tUsed)}</div>
        </td>
      </tr>`;
    }
    out.innerHTML = `
      <table>
        <thead><tr><th>User</th><th>Created</th><th>Updated</th><th>Token</th></tr></thead>
        <tbody>${users.map(row).join("") || "<tr><td colspan=4 class=muted>(none)</td></tr>"}</tbody>
      </table>
    `;
    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))
