/**
 * 管理员后台「文件库」页内浏览器区块(树 | 内容 | 预览三栏,含授权管理)。
 *
 * renderLibraryBrowser() 返回可嵌入任意后台布局的 HTML+JS 片段:
 * 以 #lib-root 为根、内部用 q() 作用域选择器,不与宿主页其他元素冲突。
 * 与独立页版浏览器的区别:去掉了自己的 wordmark/用户栏(由后台外壳提供),
 * 保留管理员工具(根目录创建、授权面板、独立权限开关)。
 */

export function renderLibraryBrowser(): string {
  return `
<div id="lib-root" style="display:flex;height:100%;min-height:0">
  <!-- 左:目录树 -->
  <div style="width:250px;flex-shrink:0;border-right:1px solid var(--border-soft);display:flex;flex-direction:column;background:var(--sidebar)">
    <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--border-soft)">
      <span style="font-size:13px;font-weight:600;color:var(--text)">文件库</span>
      <button id="lib-btn-new-root" class="btn hidden" style="padding:3px 10px;font-size:11px" title="新建根目录">+ 根目录</button>
    </div>
    <div id="lib-tree" style="flex:1;overflow-y:auto;padding:8px 8px 16px;font-size:13px"></div>
  </div>

  <!-- 中:节点内容 -->
  <div id="lib-main" style="flex:1;overflow-y:auto;min-width:0">
    <div class="lib-empty-hint">从左侧选择一个文件夹或项目</div>
  </div>

  <!-- 右:预览/编辑(选中文件时出现) -->
  <div id="lib-preview" style="display:none;width:44%;min-width:380px;flex-shrink:0;overflow-y:auto;border-left:1px solid var(--border-soft);padding:16px"></div>

  <div id="lib-modal-root"></div>
  <div id="lib-toast-root" style="position:fixed;bottom:16px;right:16px;z-index:50;display:flex;flex-direction:column;gap:8px"></div>
</div>

<style>
  #lib-root .lib-empty-hint { height:100%;display:flex;align-items:center;justify-content:center;color:var(--text-3);font-size:13px; }
  #lib-root .tree-item { display:flex;align-items:center;gap:4px;padding:4px 6px;border-radius:6px;cursor:pointer;color:var(--text);transition:background 120ms ease;user-select:none; }
  #lib-root .tree-item:hover { background:var(--hover); }
  #lib-root .tree-item.selected { background:var(--selected); }
  #lib-root .tree-children { border-left:1px solid var(--guide);margin-left:15px;padding-left:3px; }
  #lib-root .tree-icon { width:16px;height:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center; }
  #lib-root .tree-caret { color:var(--text-3); }
  #lib-root .tree-badge { margin-left:auto;padding-right:4px;font-size:10px;color:var(--text-3);font-family:var(--mono); }
  #lib-root .tree-empty { padding:24px 12px;text-align:center;font-size:12px;color:var(--text-3); }
  #lib-root .lib-toast { border-radius:8px;padding:8px 14px;font-size:12px;color:#fff;background:#333230;max-width:320px; }
  #lib-root .lib-toast-err { background:#7E2C26; }
  #lib-root .modal-mask { position:fixed;inset:0;z-index:40;background:rgba(26,26,24,.3);display:flex;align-items:center;justify-content:center;padding:16px; }
  #lib-root .modal-card { width:100%;max-width:440px;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:22px; }
  #lib-root pre.diff { white-space:pre-wrap;font-family:var(--mono);font-size:12px;line-height:1.7;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:10px;margin:0; }
  #lib-root pre.diff .add { display:block;color:var(--diff-add-text);background:var(--diff-add-bg); }
  #lib-root pre.diff .del { display:block;color:var(--diff-del-text);background:var(--diff-del-bg); }
  #lib-root .meta-grid { display:grid;grid-template-columns:1fr 1fr;gap:10px 24px;font-size:13px;color:var(--text-2); }
  #lib-root .meta-grid b { color:var(--text);font-weight:600; }
  #lib-root .conflict-panel { margin-top:14px;border:1px solid #E8E2C8;background:#FCFBF4;border-radius:10px;padding:14px 16px; }
  #lib-root .conflict-title { font-size:13px;font-weight:600;color:#6E6329;margin-bottom:8px; }
  #lib-root .conflict-note { font-size:11px;color:#8A8059;margin-top:8px; }
  #lib-root .export-status { font-size:11px;color:var(--text-3);font-family:var(--mono); }
</style>

<script>
(function () {
  const libRoot = document.getElementById("lib-root");
  if (!libRoot) return;
  const $ = (sel) => libRoot.querySelector(sel);
  const $$ = (sel) => [...libRoot.querySelectorAll(sel)];
  const esc = (s) => String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");

  async function api(path, opts = {}) {
    const res = await fetch(path, {
      credentials: "same-origin",
      headers: opts.body ? { "Content-Type": "application/json" } : undefined,
      ...opts,
      body: opts.body ? JSON.stringify(opts.body) : undefined,
    });
    if (res.status === 401) { location.href = "/database/admin"; throw new Error("unauthenticated"); }
    if (res.status === 204) return null;
    const text = await res.text();
    const data = text ? JSON.parse(text) : null;
    if (!res.ok) {
      const err = (data && data.error) || { code: "unknown", message: res.statusText };
      const e = new Error(err.message); e.code = err.code; e.status = res.status; Object.assign(e, err);
      throw e;
    }
    return data;
  }

  function toast(msg, kind = "info") {
    const el = document.createElement("div");
    el.className = "lib-toast" + (kind === "err" ? " lib-toast-err" : "");
    el.textContent = msg;
    $("#lib-toast-root").appendChild(el);
    setTimeout(() => el.remove(), 3600);
  }
  const ok = (m) => toast(m, "ok");
  const fail = (e) => toast(e.message || String(e), "err");

  function modal(html) {
    const root = $("#lib-modal-root");
    root.innerHTML = '<div class="modal-mask"><div class="modal-card">' + html + "</div></div>";
    root.firstElementChild.addEventListener("click", (e) => { if (e.target === e.currentTarget) closeModal(); });
  }
  function closeModal() { $("#lib-modal-root").innerHTML = ""; }

  let me = { userId: null, isWebsiteAdmin: false };
  let current = null;
  const expanded = new Set();
  let currentFile = null;

  const ICONS = {
    caretRight: '<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M9 6l6 6-6 6z"/></svg>',
    caretDown: '<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M6 9l6 6 6-6z"/></svg>',
    folder: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z"/></svg>',
    project: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>',
  };

  async function loadChildren(parentId) {
    const q = parentId === null ? "" : "?parentId=" + encodeURIComponent(parentId);
    return (await api("/database/api/nodes" + q)).nodes;
  }

  async function renderTree() {
    const tree = $("#lib-tree");
    tree.innerHTML = "";
    try {
      const roots = await loadChildren(null);
      if (roots.length === 0) {
        tree.innerHTML = '<div class="tree-empty">空文件库' +
          (me.isWebsiteAdmin ? " · 点上方「+ 根目录」开始" : "") + "</div>";
        return;
      }
      for (const node of roots) tree.appendChild(await treeItem(node, 0));
    } catch (e) { tree.innerHTML = '<div class="tree-empty">' + esc(e.message) + "</div>"; }
  }

  async function treeItem(node, depth) {
    const wrap = document.createElement("div");
    const row = document.createElement("div");
    row.className = "tree-item" + (current && current.id === node.id ? " selected" : "");
    const caret = document.createElement("span");
    caret.className = "tree-icon tree-caret";
    if (node.kind === "FOLDER") {
      caret.innerHTML = expanded.has(node.id) ? ICONS.caretDown : ICONS.caretRight;
      caret.onclick = (e) => { e.stopPropagation(); toggleExpand(node.id); };
    }
    row.appendChild(caret);
    const iconSpan = document.createElement("span");
    iconSpan.className = "tree-icon";
    iconSpan.style.color = node.kind === "PROJECT" ? "#1A1A18" : "#9C9B96";
    iconSpan.innerHTML = node.kind === "PROJECT" ? ICONS.project : ICONS.folder;
    row.appendChild(iconSpan);
    const nameSpan = document.createElement("span");
    nameSpan.className = "truncate";
    nameSpan.textContent = node.name;
    row.appendChild(nameSpan);
    if (node.role !== "MANAGE") {
      const badge = document.createElement("span");
      badge.className = "tree-badge";
      badge.textContent = node.role;
      row.appendChild(badge);
    }
    row.onclick = () => {
      if (node.kind === "FOLDER") {
        if (expanded.has(node.id)) expanded.delete(node.id); else expanded.add(node.id);
      }
      selectNode(node.id);
    };
    wrap.appendChild(row);
    if (node.kind === "FOLDER" && expanded.has(node.id)) {
      wrap.appendChild(await childrenBlock(node.id, depth));
    }
    return wrap;
  }

  async function childrenBlock(parentId, depth) {
    const block = document.createElement("div");
    block.className = "tree-children";
    const kids = await loadChildren(parentId);
    for (const kid of kids) block.appendChild(await treeItem(kid, depth + 1));
    return block;
  }

  async function toggleExpand(id) {
    if (expanded.has(id)) expanded.delete(id); else expanded.add(id);
    await renderTree();
  }

  async function selectNode(id) {
    try {
      const [{ node }, { breadcrumb }] = await Promise.all([
        api("/database/api/nodes/" + id),
        api("/database/api/nodes/" + id + "/breadcrumb"),
      ]);
      current = node; current.breadcrumb = breadcrumb;
      closePreview();
      await renderTree();
      renderMain();
    } catch (e) { fail(e); }
  }

  function crumbsHtml() {
    return current.breadcrumb.map((c) =>
      c.name === null ? "<span>…</span>" : "<span>" + esc(c.name) + "</span>"
    ).join('<span style="margin:0 4px;color:var(--border)">/</span>');
  }

  function renderMain() {
    const n = current;
    const canManage = n.role === "MANAGE";
    const canEdit = canManage || n.role === "EDIT";
    const tabs = [];
    tabs.push(["detail", "概览"]);
    if (n.kind === "PROJECT") tabs.push(["files", "文件"]);
    if (canManage) tabs.push(["grants", "授权"]);
    current.tab = current.tab && tabs.some(t => t[0] === current.tab) ? current.tab : "detail";

    $("#lib-main").innerHTML =
      '<div style="max-width:860px;margin:0 auto;padding:28px 28px 56px">' +
        '<div style="font-size:12px;color:var(--text-3);margin-bottom:6px">' + crumbsHtml() + "</div>" +
        '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px">' +
          '<div style="font-size:17px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px">' + esc(n.name) +
            '<span class="tag">' + n.kind + "</span>" +
            '<span class="tag tag-role">' + n.role + "</span>" +
          "</div>" +
          '<div style="display:flex;gap:6px">' +
            (canEdit && n.kind === "FOLDER" ? '<button class="btn" onclick="window.__lib.createChild()">+ 新建子节点</button>' : "") +
            (canManage ? '<button class="btn" onclick="window.__lib.renameCurrent()">重命名</button>' : "") +
            (canManage ? '<button class="btn btn-danger" onclick="window.__lib.deleteCurrent()">删除</button>' : "") +
          "</div>" +
        "</div>" +
        '<div class="tabs">' +
          tabs.map(([id, label]) =>
            '<button onclick="window.__lib.switchTab(\\'' + id + '\\')" class="tab' +
            (current.tab === id ? " active" : "") + '">' + label + "</button>").join("") +
        "</div>" +
        '<div id="lib-tab-body"></div>' +
      "</div>";
    renderTab();
  }

  function renderTab() {
    const body = $("#lib-tab-body");
    if (current.tab === "files") return renderFilesTab(body);
    if (current.tab === "grants") return renderGrantsTab(body);
    let extra = "";
    if (current.kind === "PROJECT") {
      extra =
        '<div class="divider"></div>' +
        '<div class="inline-form" style="justify-content:flex-start">' +
          '<span class="quiet">独立权限</span>' +
          '<b style="font-size:13px">' + (current.independentPermission ? "开启" : "关闭") + "</b>" +
          (current.role === "MANAGE"
            ? '<button class="btn" onclick="window.__lib.toggleIndependent()">' + (current.independentPermission ? "关闭" : "开启") + "</button>"
            : "") +
          '<span class="quiet">关闭时仅继承父级权限(创建者除外)</span>' +
        "</div>" +
        '<div class="divider"></div>' +
        '<div class="section-title">导出</div>' +
        '<div class="inline-form">' +
          '<select id="lib-x-target" class="select" style="width:auto"><option value="manifest">manifest(stub)</option></select>' +
          '<button class="btn" onclick="window.__lib.submitExport()">开始导出</button>' +
          '<span id="lib-x-status" class="export-status"></span>' +
        "</div>";
    }
    body.innerHTML =
      '<div class="panel">' +
        '<div class="meta-grid">' +
          "<div>类型 <b>" + current.kind + "</b></div>" +
          "<div>我的权限 <b>" + current.role + "</b></div>" +
          "<div>创建时间 " + new Date(current.createdAt).toLocaleString("zh-CN") + "</div>" +
          "<div>更新时间 " + new Date(current.updatedAt).toLocaleString("zh-CN") + "</div>" +
        "</div>" + extra +
      "</div>";
    pollExport();
  }

  /* ---------------- 节点操作 ---------------- */
  function createDialog(parentId) {
    modal(
      '<div class="modal-title">' + (parentId ? "新建子节点" : "新建根目录") + "</div>" +
      '<div class="form-row"><label class="form-label">名称</label>' +
      '<input id="lib-f-name" class="input" placeholder="例如:物理必修一"/></div>' +
      '<div class="form-row"><label class="form-label">类型</label>' +
      '<select id="lib-f-kind" class="select">' +
        '<option value="FOLDER">文件夹</option><option value="PROJECT">项目(Git 仓库)</option>' +
      "</select></div>" +
      '<div class="modal-actions">' +
        '<button class="btn" onclick="window.__lib.closeModal()">取消</button>' +
        '<button class="btn btn-primary" onclick="window.__lib.submitCreate(\\'' + (parentId ?? "") + '\\')">创建</button>' +
      "</div>"
    );
  }
  async function submitCreate(parentId) {
    try {
      await api("/database/api/nodes", { method: "POST", body: {
        parentId: parentId === "" ? null : parentId,
        kind: $("#lib-f-kind").value, name: $("#lib-f-name").value,
      }});
      closeModal(); ok("已创建");
      if (parentId) expanded.add(parentId);
      await renderTree();
    } catch (e) { fail(e); }
  }

  /* ---------------- 授权 ---------------- */
  async function renderGrantsTab(body) {
    try {
      const { grants } = await api("/database/api/nodes/" + current.id + "/grants");
      const rows = grants.map((g) =>
        "<tr>" +
          "<td>" + (g.principalType === "USER" ? "👤" : "👥") + " " + esc(g.principalId) +
            (g.isCreatorGrant ? ' <span class="quiet">(创建者)</span>' : "") + "</td>" +
          '<td class="file-meta">' + g.role + "</td>" +
          "<td style='text-align:right'>" + (g.isCreatorGrant ? "" :
            '<button class="link-danger" onclick="window.__lib.revokeGrant(\\'' + g.id + '\\')">收回</button>') + "</td>" +
        "</tr>").join("");
      body.innerHTML =
        '<div class="panel">' +
          '<table class="list"><thead><tr><th>主体</th><th>级别</th><th></th></tr></thead>' +
          "<tbody>" + rows + "</tbody></table>" +
          '<div class="divider"></div>' +
          '<div class="section-title">新增授权</div>' +
          '<div class="inline-form">' +
            '<select id="lib-g-type" class="select" style="width:110px" onchange="window.__lib.gTypeChanged()">' +
              '<option value="USER">用户</option><option value="GROUP">Group</option></select>' +
            '<input id="lib-g-principal" class="input" placeholder="用户 id"/>' +
            '<select id="lib-g-group" class="select hidden"></select>' +
            '<select id="lib-g-role" class="select" style="width:110px">' +
              '<option value="VIEW">VIEW</option><option value="EDIT">EDIT</option><option value="MANAGE">MANAGE</option></select>' +
            '<button class="btn btn-primary" onclick="window.__lib.addGrant()">授予</button>' +
          "</div>" +
          '<div class="section-note">MANAGE 仅创建者可授;创建者授权不可动(契约 8.1)</div>' +
        "</div>";
    } catch (e) { body.innerHTML = '<div class="panel quiet">' + esc(e.message) + "</div>"; }
  }

  /* ---------------- 文件 ---------------- */
  async function renderFilesTab(body) {
    try {
      const { files } = await api("/database/api/projects/" + current.id + "/files");
      const rows = files.map((f) =>
        '<tr class="file-row" onclick="window.__lib.openFile(\\'' + esc(f.path) + '\\')">' +
          '<td class="file-path">' + esc(f.path) + "</td>" +
          '<td class="file-meta" style="text-align:right">' + f.size + " B</td></tr>").join("");
      body.innerHTML =
        '<div class="panel">' +
          '<div class="inline-form" style="justify-content:space-between;margin-bottom:6px">' +
            '<div class="section-title" style="margin:0">项目文件(' + files.length + ")</div>" +
            (current.role !== "VIEW"
              ? '<div class="inline-form" style="gap:6px">' +
                '<button class="btn" onclick="window.__lib.newFileDialog()">+ 新建文件</button>' +
                '<button class="btn btn-primary" onclick="document.getElementById(\\'lib-upload-input\\').click()">上传文件</button>' +
                '<input id="lib-upload-input" type="file" class="hidden"/>' +
                "</div>"
              : "") +
          "</div>" +
          (files.length === 0 ? '<div class="quiet" style="padding:20px 0;text-align:center">空仓库 · 可新建或上传文件</div>'
            : '<table class="list">' + rows + "</table>") +
        "</div>";
      const up = $("#lib-upload-input");
      if (up) up.onchange = (e) => doUpload(e.target);
    } catch (e) { body.innerHTML = '<div class="panel quiet">' + esc(e.message) + "</div>"; }
  }

  function newFileDialog() {
    modal(
      '<div class="modal-title">新建文件</div>' +
      '<div class="form-row"><label class="form-label">路径</label>' +
      '<input id="lib-nf-path" class="input" style="font-family:var(--mono)" placeholder="docs/intro.md"/></div>' +
      '<div class="form-row"><label class="form-label">内容</label>' +
      '<textarea id="lib-nf-content" rows="8" class="textarea" placeholder="内容…"></textarea></div>' +
      '<div class="modal-actions"><button class="btn" onclick="window.__lib.closeModal()">取消</button>' +
      '<button class="btn btn-primary" onclick="window.__lib.submitNewFile()">创建</button></div>'
    );
  }
  async function submitNewFile() {
    try {
      await api("/database/api/projects/" + current.id + "/file", { method: "PUT", body: {
        path: $("#lib-nf-path").value, content: $("#lib-nf-content").value,
      }});
      closeModal(); ok("已创建"); renderTab();
    } catch (e) { fail(e); }
  }

  function u8ToBase64(bytes) {
    let bin = "";
    const CHUNK = 0x8000;
    for (let i = 0; i < bytes.length; i += CHUNK) {
      bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
    }
    return btoa(bin);
  }
  async function doUpload(input) {
    const file = input.files && input.files[0];
    input.value = "";
    if (!file) return;
    if (file.size > 10 * 1024 * 1024) return toast("文件超过 10MB 上限", "err");
    const targetPath = prompt("保存到路径(可含目录):", "材料/" + file.name);
    if (!targetPath) return;
    const bytes = new Uint8Array(await file.arrayBuffer());
    const isBinary = bytes.includes(0);
    const body = isBinary
      ? { path: targetPath, content: u8ToBase64(bytes), encoding: "base64" }
      : { path: targetPath, content: new TextDecoder("utf-8").decode(bytes), encoding: "utf8" };
    try {
      await api("/database/api/projects/" + current.id + "/file", { method: "PUT", body });
      ok("已上传 " + file.name);
      renderTab();
    } catch (e) { fail(e); }
  }

  async function openFile(path) {
    try {
      const f = await api("/database/api/projects/" + current.id + "/file?path=" + encodeURIComponent(path));
      currentFile = f;
      const canEdit = current.role !== "VIEW";
      const prev = $("#lib-preview");
      prev.style.display = "block";
      prev.innerHTML =
        '<div class="panel">' +
          '<div class="inline-form" style="justify-content:space-between;margin-bottom:10px">' +
            '<span class="file-meta">' + esc(f.path) + " @ " + esc(f.version) + "</span>" +
            '<div class="inline-form" style="gap:6px">' +
              '<a class="btn" href="/database/api/projects/' + current.id + '/file/raw?path=' +
                encodeURIComponent(f.path) + '" download>下载</a>' +
              '<button class="btn" onclick="window.__lib.showHistory()">历史</button>' +
              (canEdit ? '<button class="btn btn-danger" onclick="window.__lib.deleteFile()">删除文件</button>' : "") +
              '<button class="btn" onclick="window.__lib.closePreview()" title="关闭预览">✕</button>' +
            "</div>" +
          "</div>" +
          (f.encoding === "base64"
            ? '<div class="quiet">二进制文件(' + f.size + " B),不支持在线编辑</div>"
            : '<textarea id="lib-ef-content" rows="16" class="textarea" ' + (canEdit ? "" : "readonly") +
              ">" + esc(f.content) + "</textarea>") +
          (canEdit && f.encoding !== "base64"
            ? '<div class="modal-actions"><button class="btn btn-primary" onclick="window.__lib.saveFile()">提交修改</button></div>'
            : "") +
          '<div id="lib-conflict-zone"></div>' +
        "</div>";
    } catch (e) { fail(e); }
  }
  function closePreview() {
    const prev = $("#lib-preview");
    if (prev) { prev.style.display = "none"; prev.innerHTML = ""; }
    currentFile = null;
  }
  async function saveFile() {
    try {
      const r = await api("/database/api/projects/" + current.id + "/file/commits", { method: "POST", body: {
        path: currentFile.path, baseVersion: currentFile.version, content: $("#lib-ef-content").value,
      }});
      ok("已提交 " + r.version); await openFile(currentFile.path);
    } catch (e) {
      if (e.status === 409 && e.currentVersion) return showConflict(e.currentVersion);
      fail(e);
    }
  }
  async function showConflict(currentVersion) {
    const { diff } = await api("/database/api/projects/" + current.id + "/file/diff?path=" +
      encodeURIComponent(currentFile.path) + "&from=" + encodeURIComponent(currentFile.version) +
      "&to=" + encodeURIComponent(currentVersion));
    $("#lib-conflict-zone").innerHTML =
      '<div class="conflict-panel">' +
        '<div class="conflict-title">冲突:他人已提交 ' + esc(currentVersion) + ",差异如下(你的基版 → 最新版)</div>" +
        '<pre class="diff">' + esc(diff)
          .replace(/^\\+(.*)$/gm, '<span class="add">+$1</span>')
          .replace(/^-(.*)$/gm, '<span class="del">-$1</span>') + "</pre>" +
        '<div class="conflict-note">请人工合并后,以最新内容为全文重新提交(基版将更新为 ' + esc(currentVersion) + ")</div>" +
        '<div class="modal-actions"><button class="btn" onclick="window.__lib.acceptLatest()">载入最新内容</button></div>' +
      "</div>";
    currentFile.version = currentVersion;
  }
  async function deleteFile() {
    if (!confirm("删除文件 " + currentFile.path + "?")) return;
    try {
      await api("/database/api/projects/" + current.id + "/file?path=" + encodeURIComponent(currentFile.path),
        { method: "DELETE", body: { baseVersion: currentFile.version } });
      ok("已删除"); closePreview(); renderTab();
    } catch (e) { fail(e); }
  }
  async function showHistory() {
    const { history } = await api("/database/api/projects/" + current.id + "/file/history?path=" +
      encodeURIComponent(currentFile.path));
    modal(
      '<div class="modal-title">版本历史</div>' +
      '<div style="max-height:320px;overflow-y:auto">' +
        history.map((v) =>
          '<div style="border-top:1px solid #F5F5F4;padding:8px 0;font-size:12px">' +
            '<span class="file-meta" style="color:var(--accent)">' + esc(v.version) + "</span> " + esc(v.message) +
            '<div class="quiet">' + new Date(v.committedAt).toLocaleString("zh-CN") +
            (v.author ? " · " + esc(v.author) : "") + "</div></div>").join("") +
      "</div>" +
      '<div class="modal-actions"><button class="btn" onclick="window.__lib.closeModal()">关闭</button></div>'
    );
  }

  /* ---------------- 导出 ---------------- */
  let exportJobId = null;
  async function submitExport() {
    try {
      const r = await api("/database/api/projects/" + current.id + "/exports", { method: "POST", body: { target: $("#lib-x-target").value } });
      exportJobId = r.jobId;
      $("#lib-x-status").textContent = "任务 " + r.jobId.slice(0, 8) + "… 排队中";
      pollExport();
    } catch (e) { fail(e); }
  }
  async function pollExport() {
    if (!exportJobId) return;
    try {
      const job = await api("/database/api/exports/" + exportJobId);
      const el = $("#lib-x-status");
      if (!el) return;
      if (job.status === "DONE") {
        el.innerHTML = '完成 · <a href="/database/api/exports/' + exportJobId + '/download">下载</a>';
      } else if (job.status === "FAILED") {
        el.textContent = "失败:" + (job.error || "");
      } else {
        el.textContent = "状态:" + job.status + "…";
        setTimeout(pollExport, 1000);
      }
    } catch { /* ignore */ }
  }

  /* ---------------- 暴露给内联 onclick 的操作表 ---------------- */
  window.__lib = {
    createChild() { createDialog(current.id); },
    async renameCurrent() {
      const name = prompt("新名称", current.name);
      if (name === null) return;
      try {
        await api("/database/api/nodes/" + current.id, { method: "PATCH", body: { name } });
        ok("已重命名"); await selectNode(current.id);
      } catch (e) { fail(e); }
    },
    async deleteCurrent() {
      if (!confirm("确认删除「" + current.name + "」?软删除后不可见。")) return;
      try {
        await api("/database/api/nodes/" + current.id, { method: "DELETE" });
        ok("已删除"); current = null; await renderTree();
        $("#lib-main").innerHTML = '<div class="lib-empty-hint">从左侧选择一个文件夹或项目</div>';
        closePreview();
      } catch (e) { fail(e); }
    },
    switchTab(id) { current.tab = id; renderMain(); },
    async toggleIndependent() {
      try {
        await api("/database/api/projects/" + current.id + "/independent-permission", {
          method: "PUT", body: { enabled: !current.independentPermission },
        });
        ok("已切换"); await selectNode(current.id);
      } catch (e) { fail(e); }
    },
    async gTypeChanged() {
      const isGroup = $("#lib-g-type").value === "GROUP";
      $("#lib-g-principal").classList.toggle("hidden", isGroup);
      $("#lib-g-group").classList.toggle("hidden", !isGroup);
      if (isGroup && $("#lib-g-group").options.length === 0) {
        const { groups } = await api("/database/api/groups/search?q=");
        $("#lib-g-group").innerHTML = groups.map((g) => '<option value="' + esc(g.id) + '">' + esc(g.name) + "</option>").join("");
      }
    },
    async addGrant() {
      const type = $("#lib-g-type").value;
      const principalId = type === "GROUP" ? $("#lib-g-group").value : $("#lib-g-principal").value.trim();
      if (!principalId) return toast("请填写主体", "err");
      try {
        await api("/database/api/nodes/" + current.id + "/grants", { method: "PUT", body: {
          grants: [{ principalType: type, principalId, role: $("#lib-g-role").value }],
        }});
        ok("已授予"); renderTab();
      } catch (e) { fail(e); }
    },
    async revokeGrant(grantId) {
      try {
        await api("/database/api/nodes/" + current.id + "/grants/" + grantId, { method: "DELETE" });
        ok("已收回"); renderTab();
      } catch (e) { fail(e); }
    },
    newFileDialog, submitNewFile, openFile, closePreview, saveFile, deleteFile, showHistory,
    acceptLatest() { openFile(currentFile.path); toast("已载入最新内容,请在此基础上合并", "info"); },
    submitExport, closeModal,
  };

  /* ---------------- 初始化 ---------------- */
  (async function init() {
    try {
      me = await api("/database/api/me");
      if (me.isWebsiteAdmin) {
        const btn = $("#lib-btn-new-root");
        btn.classList.remove("hidden");
        btn.onclick = () => createDialog(null);
      }
    } catch (e) { /* 401 已由 api 处理跳转 */ }
    await renderTree();
  })();
})();
</script>`;
}
