guanghu-ice-heart/server-tools/zhulan-remote-cell/ui/app.js

213 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const $ = (id) => document.getElementById(id);
const state = { csrf: "", requests: [], selected: null };
const eventNames = {
request_created: "铸澜提交申请",
human_approved: "主人批准边界",
human_rejected: "主人拒绝申请",
human_revoked: "主人撤销能力",
capability_picked_up: "铸澜领取临时能力",
capability_verified: "运行层校验能力",
capability_expired: "临时能力到期",
request_expired: "申请单到期"
};
async function api(path, options = {}) {
const headers = { "Content-Type": "application/json", ...(options.headers || {}) };
const response = await fetch(path, { credentials: "same-origin", ...options, headers });
let data = {};
try { data = await response.json(); } catch (_) {}
if (!response.ok) throw new Error(data.error || `HTTP_${response.status}`);
return data;
}
function toast(message) {
const node = $("toast");
node.textContent = message;
node.classList.add("show");
window.setTimeout(() => node.classList.remove("show"), 2600);
}
function humanAction(value) {
return ({ read: "读取", edit: "编辑", test: "运行测试", commit: "提交", push_candidate: "推送候选分支" })[value] || value;
}
function stateLabel(value) {
return ({ PENDING: "等待批准", APPROVED: "已批准", REJECTED: "已拒绝", REVOKED: "已撤销", EXPIRED: "已过期" })[value] || value;
}
function renderRequest(request) {
state.selected = request;
$("empty-state").classList.add("hidden");
$("request-card").classList.remove("hidden");
$("request-id").textContent = request.id;
$("request-description").textContent = request.description;
$("persona").textContent = request.persona_id;
$("development-id").textContent = request.development_id;
$("repository").textContent = request.repository;
$("branch").textContent = request.branch;
$("base-sha").textContent = request.base_sha;
$("actions").replaceChildren(...request.actions.map((action) => {
const node = document.createElement("span");
node.className = "chip";
node.textContent = humanAction(action);
return node;
}));
$("paths").replaceChildren(...request.paths.map((path) => {
const node = document.createElement("span");
node.className = "path";
node.textContent = path;
return node;
}));
const label = stateLabel(request.state);
for (const id of ["request-state", "card-state"]) {
const node = $(id);
node.textContent = label;
node.className = `state ${request.state === "APPROVED" ? "approved" : request.state === "REJECTED" ? "rejected" : "waiting"}`;
}
$("decision-area").classList.toggle("hidden", request.state !== "PENDING");
$("revoke-area").classList.toggle("hidden", request.state !== "APPROVED");
loadReceipts(request.id);
}
function renderEmpty(message, detail) {
state.selected = null;
$("request-card").classList.add("hidden");
const empty = $("empty-state");
empty.classList.remove("hidden");
empty.querySelector("h3").textContent = message;
empty.querySelector("p").textContent = detail;
$("request-state").textContent = "湖面安静";
}
async function loadReceipts(requestId) {
try {
const data = await api(`api/v1/owner/requests/${encodeURIComponent(requestId)}/receipts`);
const nodes = data.receipts.map((receipt) => {
const li = document.createElement("li");
li.className = receipt.result === "PASS" || receipt.result === "APPROVED" || receipt.result === "PENDING" ? "pass" : "fail";
const dot = document.createElement("i");
const body = document.createElement("div");
const title = document.createElement("strong");
title.textContent = eventNames[receipt.event] || receipt.event;
const detail = document.createElement("span");
detail.textContent = `${receipt.created_at} · ${receipt.result} · ${receipt.receipt_hash.slice(0, 12)}`;
body.append(title, detail);
li.append(dot, body);
return li;
});
$("timeline").replaceChildren(...nodes);
} catch (_) {
$("timeline").innerHTML = "<li class='fail'><i></i><div><strong>回执读取失败</strong><span>运行层没有返回可验证结果。</span></div></li>";
}
}
async function loadRequests(preferredId = "") {
const data = await api("api/v1/owner/requests");
state.requests = data.requests;
const preferred = preferredId && data.requests.find((item) => item.id === preferredId);
const pending = data.requests.find((item) => item.state === "PENDING");
const selected = preferred || pending || data.requests[0];
if (selected) renderRequest(selected);
else renderEmpty("暂无真实申请", "铸澜提交结构化申请后会出现在这里;公开建单本身没有执行权。");
}
async function restoreSession() {
try {
const session = await api("api/v1/owner/session");
state.csrf = session.csrf;
$("auth-title").textContent = `主人 ${session.owner_login} · 已验证`;
$("auth-detail").textContent = "审批会话有效。任何扩大仓库、路径、动作或期限的请求都必须重新批准。";
$("login-open").classList.add("hidden");
$("logout").classList.remove("hidden");
const preferred = new URLSearchParams(location.search).get("request") || "";
await loadRequests(preferred);
const oauthReturn = new URLSearchParams(location.search).get("oauth_return");
if (oauthReturn) {
try {
const normalized = oauthReturn.replace(/-/g, "+").replace(/_/g, "/");
const decoded = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
if (decoded.startsWith("/oauth/authorize?")) location.assign(`/zhulan${decoded}`);
} catch (_) {}
}
} catch (_) {
state.csrf = "";
$("login-open").classList.remove("hidden");
$("logout").classList.add("hidden");
}
}
$("login-open").addEventListener("click", () => $("login-dialog").showModal());
$("login-form").addEventListener("submit", async (event) => {
event.preventDefault();
$("login-error").textContent = "";
const button = event.currentTarget.querySelector("button[type=submit]");
button.disabled = true;
try {
const result = await api("api/v1/owner/login", {
method: "POST",
body: JSON.stringify({ username: $("username").value, password: $("password").value })
});
state.csrf = result.csrf;
$("password").value = "";
$("login-dialog").close();
toast("代码频道身份验证通过");
await restoreSession();
} catch (_) {
$("login-error").textContent = "账号验证失败。没有创建审批会话。";
} finally { button.disabled = false; }
});
$("approve").addEventListener("click", async () => {
if (!state.selected || state.selected.state !== "PENDING") return;
const exact = `${state.selected.repository}\n${state.selected.branch}\n${state.selected.paths.join("")}`;
if (!window.confirm(`批准这一段开发?\n\n${exact}\n\n批准不会开放 root 或任意 shell。`)) return;
$("approve").disabled = true;
try {
await api(`api/v1/owner/requests/${state.selected.id}/approve`, {
method: "POST",
headers: { "X-CSRF-Token": state.csrf },
body: JSON.stringify({ ttl_seconds: Number($("ttl").value) })
});
toast("已批准。铸澜可领取一次临时能力。");
await restoreSession();
} catch (error) { toast(`批准失败:${error.message}`); }
finally { $("approve").disabled = false; }
});
$("reject").addEventListener("click", async () => {
if (!state.selected || state.selected.state !== "PENDING") return;
const reason = window.prompt("写一句拒绝原因,铸澜会据此修正申请:", "范围需要重新确认");
if (reason === null) return;
try {
await api(`api/v1/owner/requests/${state.selected.id}/reject`, {
method: "POST",
headers: { "X-CSRF-Token": state.csrf },
body: JSON.stringify({ reason })
});
toast("已拒绝并留下回执。");
await restoreSession();
} catch (error) { toast(`拒绝失败:${error.message}`); }
});
$("revoke").addEventListener("click", async () => {
if (!state.selected || state.selected.state !== "APPROVED") return;
if (!window.confirm("立即撤销这段开发能力?已连接会话会在下一次调用时被服务器驳回。")) return;
try {
await api(`api/v1/owner/requests/${state.selected.id}/revoke`, {
method: "POST",
headers: { "X-CSRF-Token": state.csrf },
body: JSON.stringify({ reason: "主人在审批端主动撤销" })
});
toast("能力已撤销,原连接不再有效。");
await restoreSession();
} catch (error) { toast(`撤销失败:${error.message}`); }
});
$("logout").addEventListener("click", async () => {
try {
await api("api/v1/owner/logout", { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: "{}" });
} catch (_) {}
location.reload();
});
restoreSession();