Remove the inline token script blocked by script-src 'self', so the live page no longer calls the snapshot API with token=undefined. Co-authored-by: Cursor <cursoragent@cursor.com>
318 lines
14 KiB
JavaScript
318 lines
14 KiB
JavaScript
(function () {
|
|
function resolveToken() {
|
|
const fromURL = new URL(window.location.href).searchParams.get("token");
|
|
if (fromURL) return fromURL;
|
|
const meta = document.querySelector('meta[name="engineering-dashboard-token"]');
|
|
const fromMeta = meta && meta.getAttribute("content");
|
|
if (fromMeta && fromMeta !== "__TOKEN__") return fromMeta;
|
|
return "";
|
|
}
|
|
const token = resolveToken();
|
|
const state = {
|
|
snap: null,
|
|
view: "overview",
|
|
q: "",
|
|
selectedDoc: "",
|
|
scrollY: 0,
|
|
};
|
|
|
|
const el = {
|
|
content: document.getElementById("content"),
|
|
meta: document.getElementById("repo-meta"),
|
|
sources: document.getElementById("source-strip"),
|
|
status: document.getElementById("view-status"),
|
|
q: document.getElementById("q"),
|
|
navToggle: document.getElementById("nav-toggle"),
|
|
nav: document.getElementById("nav"),
|
|
};
|
|
|
|
function qs() {
|
|
const u = new URL(window.location.href);
|
|
return u.searchParams;
|
|
}
|
|
|
|
function syncURL() {
|
|
const u = new URL(window.location.href);
|
|
if (token) u.searchParams.set("token", token);
|
|
u.searchParams.set("view", state.view);
|
|
if (state.q) u.searchParams.set("q", state.q); else u.searchParams.delete("q");
|
|
if (state.selectedDoc) u.searchParams.set("doc", state.selectedDoc); else u.searchParams.delete("doc");
|
|
history.replaceState(null, "", u.toString());
|
|
}
|
|
|
|
function loadURL() {
|
|
const p = qs();
|
|
state.view = p.get("view") || "overview";
|
|
state.q = p.get("q") || "";
|
|
state.selectedDoc = p.get("doc") || "";
|
|
el.q.value = state.q;
|
|
document.querySelectorAll(".nav-btn").forEach((b) => {
|
|
b.classList.toggle("active", b.dataset.view === state.view);
|
|
});
|
|
}
|
|
|
|
async function fetchSnapshot(force) {
|
|
if (!token) {
|
|
el.status.textContent = "缺少访问令牌";
|
|
renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面");
|
|
return false;
|
|
}
|
|
el.status.textContent = state.snap ? "刷新中…" : "首次加载中…";
|
|
const url = "/api/v1/snapshot?token=" + encodeURIComponent(token) + (force ? "&refresh=1" : "");
|
|
const headers = {};
|
|
if (state.snap && state.snap.revision_id) headers["If-None-Match"] = '"' + state.snap.revision_id + '"';
|
|
const res = await fetch(url, { headers, credentials: "same-origin" });
|
|
if (res.status === 304) {
|
|
el.status.textContent = "已是最新 revision " + state.snap.revision_id;
|
|
return false;
|
|
}
|
|
if (!res.ok) {
|
|
el.status.textContent = "加载失败 HTTP " + res.status;
|
|
if (!state.snap) renderHardError("无法获取快照");
|
|
return false;
|
|
}
|
|
state.snap = await res.json();
|
|
el.status.textContent = "generation " + state.snap.generation_id + " · revision " + state.snap.revision_id;
|
|
return true;
|
|
}
|
|
|
|
function escapeText(s) {
|
|
const d = document.createElement("div");
|
|
d.textContent = s == null ? "" : String(s);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function badge(text, cls) {
|
|
return '<span class="badge ' + (cls || "") + '">' + escapeText(text) + "</span>";
|
|
}
|
|
|
|
function renderMeta() {
|
|
const s = state.snap;
|
|
if (!s) return;
|
|
el.meta.textContent = [
|
|
s.repository.root,
|
|
"branch=" + s.repository.branch,
|
|
"HEAD=" + (s.repository.head || "").slice(0, 12),
|
|
s.repository.dirty ? "dirty" : "clean",
|
|
"at " + s.generated_at,
|
|
].join(" · ");
|
|
el.sources.innerHTML = (s.sources || []).map((src) => {
|
|
const cls = src.freshness === "stale" || src.status === "degraded" ? "stale" :
|
|
src.status === "error" ? "error" : "ok";
|
|
return '<span class="chip ' + cls + '">' + escapeText(src.name + ":" + src.status + "/" + src.freshness) + "</span>";
|
|
}).join("");
|
|
}
|
|
|
|
function matchQuery(text) {
|
|
if (!state.q) return true;
|
|
return String(text || "").toLowerCase().includes(state.q.toLowerCase());
|
|
}
|
|
|
|
function renderOverview() {
|
|
const s = state.snap;
|
|
const tasks = (s.tasks || []).filter((t) => ["planned", "in_progress", "blocked"].includes(t.doc_status) || matchQuery(t.title + t.path));
|
|
const conflicts = s.conflicts || [];
|
|
let html = '<section class="panel"><h2>焦点任务</h2><div class="table-wrap"><table><thead><tr><th>任务</th><th>计划</th><th>Git</th><th>验证</th><th>实装</th></tr></thead><tbody>';
|
|
tasks.slice(0, 40).forEach((t) => {
|
|
html += "<tr><td>" + escapeText(t.title) + "<div class='meta'>" + escapeText(t.path) + "</div></td>";
|
|
html += "<td>" + badge(t.doc_status) + (t.claim && t.claim.claimed ? badge("claimed") : badge("unclaimed", "warn")) + "</td>";
|
|
html += "<td>" + escapeText((t.git && t.git.branch) || "—") + " " + escapeText((t.git && t.git.cleanliness) || "") + "</td>";
|
|
html += "<td>" + escapeText((t.verification && t.verification.result) || "unknown") + "</td>";
|
|
const dep = t.deployment || {};
|
|
html += "<td>" + escapeText(Object.keys(dep).map((k) => k + ":" + (dep[k].app_readiness || "unknown")).join(" ")) + "</td></tr>";
|
|
});
|
|
html += "</tbody></table></div></section>";
|
|
html += '<section class="panel"><h2>漂移 / 冲突</h2>';
|
|
if (!conflicts.length) html += "<p class='meta'>无冲突</p>";
|
|
else {
|
|
html += "<ul>" + conflicts.map((c) => "<li>" + badge(c.kind, "warn") + " " + escapeText(c.subject + " · " + c.field + " — " + c.summary) + "</li>").join("") + "</ul>";
|
|
}
|
|
html += "</section>";
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderModules() {
|
|
const rows = (state.snap.modules || []).filter((m) => matchQuery(m.name + (m.mutex || "")));
|
|
let html = '<section class="panel"><h2>模块</h2><div class="table-wrap"><table><thead><tr><th>模块</th><th>状态</th><th>mutex</th></tr></thead><tbody>';
|
|
rows.forEach((m) => {
|
|
html += "<tr><td>" + escapeText(m.name) + "</td><td>" + escapeText(m.status) + "</td><td>" + escapeText(m.mutex) + "</td></tr>";
|
|
});
|
|
html += "</tbody></table></div></section>";
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderTasks() {
|
|
const rows = (state.snap.tasks || []).filter((t) => matchQuery(t.title + t.path + (t.claim && t.claim.owner || "")));
|
|
let html = "";
|
|
rows.forEach((t) => {
|
|
html += '<section class="panel"><h2>' + escapeText(t.title) + "</h2>";
|
|
html += '<div class="axes">';
|
|
html += '<div class="axis"><strong>计划</strong>' + escapeText(t.doc_status) + "<br>" + escapeText((t.claim && t.claim.owner) || "unassigned") + " · " + escapeText((t.claim && t.claim.agent_kind) || "") + " · activity=" + escapeText((t.claim && t.claim.activity) || "unknown") + "</div>";
|
|
html += '<div class="axis"><strong>Git 实施</strong>' + escapeText(JSON.stringify(t.git || {})) + "</div>";
|
|
html += '<div class="axis"><strong>验证</strong>' + escapeText(JSON.stringify(t.verification || {})) + "</div>";
|
|
html += '<div class="axis"><strong>实装</strong>' + escapeText(JSON.stringify(t.deployment || {})) + "</div>";
|
|
html += "</div>";
|
|
if (t.evidence_chain && t.evidence_chain.length) {
|
|
html += "<p><strong>证据链</strong></p><ul>" + t.evidence_chain.map((p) => '<li><button type="button" class="linkish" data-doc="' + escapeText(p) + '">' + escapeText(p) + "</button></li>").join("") + "</ul>";
|
|
}
|
|
html += "</section>";
|
|
});
|
|
el.content.innerHTML = html || "<p>无匹配任务</p>";
|
|
}
|
|
|
|
function renderGit() {
|
|
const rows = (state.snap.git || []).filter((g) => matchQuery(g.name + (g.worktree || "")));
|
|
let html = '<section class="panel"><h2>分支 / worktree</h2><p class="meta">远端状态基于本地 refs,未联网刷新</p><div class="table-wrap"><table><thead><tr><th>分支</th><th>ahead/behind</th><th>worktree</th><th>备注</th></tr></thead><tbody>';
|
|
rows.forEach((g) => {
|
|
html += "<tr><td>" + escapeText(g.name) + (g.is_current ? " *" : "") + "</td><td>" + escapeText(g.ahead + "/" + g.behind) + "</td><td>" + escapeText(g.worktree || "") + "</td><td>" + escapeText(g.remote_note || "") + "</td></tr>";
|
|
});
|
|
html += "</tbody></table></div></section>";
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderDeploy() {
|
|
const rows = state.snap.environments || [];
|
|
let html = "";
|
|
rows.forEach((e) => {
|
|
html += '<section class="panel"><h2>环境 ' + escapeText(e.name) + "</h2>";
|
|
html += "<p>" + badge("container:" + (e.facets && e.facets.container_status)) + badge("ready:" + (e.facets && e.facets.app_readiness)) + badge("services:" + (e.facets && e.facets.services_ok), e.facets && e.facets.services_ok === "partial" ? "warn" : "") + "</p>";
|
|
if (e.drift_notes && e.drift_notes.length) {
|
|
html += "<ul>" + e.drift_notes.map((n) => "<li class='badge warn'>" + escapeText(n) + "</li>").join("") + "</ul>";
|
|
}
|
|
if (e.containers && e.containers.length) {
|
|
html += '<div class="table-wrap"><table><thead><tr><th>容器</th><th>状态</th><th>health</th><th>image</th></tr></thead><tbody>';
|
|
e.containers.forEach((c) => {
|
|
html += "<tr><td>" + escapeText(c.name) + "</td><td>" + escapeText(c.status) + "</td><td>" + escapeText(c.health) + "</td><td>" + escapeText(c.image) + "</td></tr>";
|
|
});
|
|
html += "</tbody></table></div>";
|
|
}
|
|
html += "</section>";
|
|
});
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderDocs() {
|
|
const rows = (state.snap.documents || []).filter((d) => {
|
|
if (!state.q) return true;
|
|
return matchQuery(d.title + d.path + (d.body || ""));
|
|
});
|
|
rows.sort((a, b) => {
|
|
const score = (d) => {
|
|
let s = 0;
|
|
if ((d.status || "") === "in_progress" || (d.status || "") === "blocked") s += 2;
|
|
if ((d.path || "").includes("short-term")) s += 1;
|
|
return -s;
|
|
};
|
|
const d = score(a) - score(b);
|
|
return d !== 0 ? d : String(a.path).localeCompare(String(b.path));
|
|
});
|
|
let html = '<section class="panel"><h2>文档</h2><div class="table-wrap"><table><thead><tr><th>标题</th><th>路径</th><th>状态</th></tr></thead><tbody>';
|
|
rows.slice(0, 200).forEach((d) => {
|
|
html += '<tr><td><button type="button" class="linkish" data-doc="' + escapeText(d.id) + '">' + escapeText(d.title) + "</button></td><td>" + escapeText(d.path) + "</td><td>" + escapeText(d.status || "") + "</td></tr>";
|
|
});
|
|
html += "</tbody></table></div></section>";
|
|
if (state.selectedDoc) {
|
|
const doc = (state.snap.documents || []).find((d) => d.id === state.selectedDoc || d.path === state.selectedDoc);
|
|
html += '<section class="panel"><h2>正文(纯文本)</h2>';
|
|
if (!doc) html += "<p>未找到文档</p>";
|
|
else {
|
|
html += "<p class='meta'>" + escapeText(doc.path) + (doc.body_truncated ? " · truncated" : "") + "</p>";
|
|
html += '<div class="pre">' + escapeText(doc.body || "") + "</div>";
|
|
}
|
|
html += "</section>";
|
|
}
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderEvidence() {
|
|
const tasks = (state.snap.tasks || []).filter((t) => (t.evidence_chain || []).length && matchQuery(t.title));
|
|
let html = '<section class="panel"><h2>证据链导航</h2>';
|
|
tasks.forEach((t) => {
|
|
html += "<h3>" + escapeText(t.title) + "</h3><ol>" + (t.evidence_chain || []).map((p) => '<li><button type="button" class="linkish" data-doc="' + escapeText(p) + '">' + escapeText(p) + "</button></li>").join("") + "</ol>";
|
|
});
|
|
html += "</section>";
|
|
el.content.innerHTML = html;
|
|
}
|
|
|
|
function renderHardError(msg) {
|
|
el.content.innerHTML = '<section class="panel"><h2>错误</h2><p class="badge bad">' + escapeText(msg) + "</p><p>无可用缓存快照。</p></section>";
|
|
}
|
|
|
|
function render() {
|
|
if (!state.snap) return;
|
|
renderMeta();
|
|
const y = window.scrollY;
|
|
switch (state.view) {
|
|
case "modules": renderModules(); break;
|
|
case "tasks": renderTasks(); break;
|
|
case "git": renderGit(); break;
|
|
case "deploy": renderDeploy(); break;
|
|
case "docs": renderDocs(); break;
|
|
case "evidence": renderEvidence(); break;
|
|
default: renderOverview();
|
|
}
|
|
window.scrollTo(0, state.scrollY || y);
|
|
syncURL();
|
|
}
|
|
|
|
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
state.view = btn.dataset.view;
|
|
state.scrollY = 0;
|
|
document.querySelectorAll(".nav-btn").forEach((b) => b.classList.toggle("active", b === btn));
|
|
if (el.nav) el.nav.classList.remove("open");
|
|
if (el.navToggle) el.navToggle.setAttribute("aria-expanded", "false");
|
|
render();
|
|
});
|
|
});
|
|
|
|
el.content.addEventListener("click", (ev) => {
|
|
const t = ev.target.closest("[data-doc]");
|
|
if (!t) return;
|
|
state.selectedDoc = t.getAttribute("data-doc");
|
|
state.view = "docs";
|
|
document.querySelectorAll(".nav-btn").forEach((b) => b.classList.toggle("active", b.dataset.view === "docs"));
|
|
render();
|
|
});
|
|
|
|
el.q.addEventListener("input", () => {
|
|
state.q = el.q.value.trim();
|
|
render();
|
|
});
|
|
|
|
if (el.navToggle) {
|
|
el.navToggle.addEventListener("click", () => {
|
|
const open = el.nav.classList.toggle("open");
|
|
el.navToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
|
});
|
|
}
|
|
|
|
window.addEventListener("popstate", () => {
|
|
loadURL();
|
|
render();
|
|
});
|
|
|
|
async function loop() {
|
|
loadURL();
|
|
if (!token) {
|
|
renderHardError("缺少 token:请使用服务启动时打印的完整 URL(含 ?token=)打开页面");
|
|
return;
|
|
}
|
|
try {
|
|
const changed = await fetchSnapshot(true);
|
|
if (changed || state.snap) render();
|
|
} catch (e) {
|
|
el.status.textContent = "网络错误";
|
|
if (!state.snap) renderHardError(String(e));
|
|
}
|
|
setInterval(async () => {
|
|
try {
|
|
const changed = await fetchSnapshot(false);
|
|
if (changed) render();
|
|
} catch (_) { /* keep last snapshot */ }
|
|
}, 4000);
|
|
}
|
|
|
|
loop();
|
|
})();
|