// GitLab Webhook Dashboard - 前端逻辑 // 数据源: // GET /api/queue -> { pending: [], current: { task, startedAt, log } | null } // GET /api/history -> { scripts: [], history: { scriptPath: [record, ...] } } const POLL_QUEUE_MS = 1500; // 队列 + 当前执行 轮询周期 const POLL_HISTORY_MS = 4000; // 历史 轮询周期 const LOG_TAIL_CHARS = 12000; // 前端日志渲染尾部最大字符数 const state = { pending: [], current: null, history: {}, // { scriptPath: [record, ...] } scripts: [], // 配置中的全部脚本 autoscroll: true, }; // ============ Helpers ============ const $ = (sel) => document.querySelector(sel); const fmtTime = (iso) => { if (!iso) return "-"; const d = new Date(iso); if (isNaN(d.getTime())) return iso; const pad = (n) => String(n).padStart(2, "0"); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const fmtFullTime = (iso) => { if (!iso) return "-"; const d = new Date(iso); if (isNaN(d.getTime())) return iso; const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const shortCommit = (s) => { if (!s) return "unknown"; return s.length > 7 ? s.slice(0, 7) : s; }; const escapeHtml = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[c])); // ============ Connection status ============ function setConn(ok, text) { const dot = $("#conn-dot"); const txt = $("#conn-text"); dot.classList.remove("dot-green", "dot-red", "dot-grey"); dot.classList.add(ok ? "dot-green" : "dot-red"); txt.textContent = text; } // ============ Fetchers ============ async function fetchJSON(url) { const r = await fetch(url, { cache: "no-store" }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); } async function pollQueue() { try { const data = await fetchJSON("/api/queue"); state.pending = data.pending || []; state.current = data.current || null; setConn(true, "已连接"); renderQueue(); renderRunning(); } catch (e) { setConn(false, "连接失败"); console.warn("queue poll failed:", e); } } async function pollHistory() { try { const data = await fetchJSON("/api/history"); state.scripts = data.scripts || []; state.history = data.history || {}; renderHistory(); } catch (e) { console.warn("history poll failed:", e); } } // ============ Renderers ============ function renderQueue() { const list = $("#pending-list"); const badge = $("#pending-count"); badge.textContent = String(state.pending.length); if (state.pending.length === 0) { list.innerHTML = `

暂无等待任务

`; return; } list.innerHTML = state.pending.map((p) => `
${escapeHtml(p.repo)} 等待中
分支 ${escapeHtml(p.ref)} commit ${escapeHtml(shortCommit(p.commit))}
${escapeHtml(p.script)}
入队时间: ${escapeHtml(fmtFullTime(p.queuedAt))}
`).join(""); } function renderRunning() { const body = $("#running-body"); const stateBadge = $("#running-state"); const sizeEl = $("#log-size"); if (!state.current) { stateBadge.textContent = "空闲"; stateBadge.className = "badge badge-grey"; body.innerHTML = `

当前没有任务在执行

`; sizeEl.textContent = ""; return; } const c = state.current; stateBadge.textContent = "执行中"; stateBadge.className = "badge badge-warn"; body.innerHTML = `
仓库
${escapeHtml(c.task.repo)}
分支
${escapeHtml(c.task.ref)}
Commit
${escapeHtml(shortCommit(c.task.commit))}
脚本
${escapeHtml(c.task.script)}
开始
${escapeHtml(fmtFullTime(c.startedAt))}

  `;

  const logBox = $("#live-log");
  // 仅渲染尾部以避免长日志卡顿
  const fullLog = c.log || "";
  const tail = fullLog.length > LOG_TAIL_CHARS
    ? fullLog.slice(fullLog.length - LOG_TAIL_CHARS)
    : fullLog;
  logBox.textContent = tail || "(等待脚本输出…)";
  sizeEl.textContent = fullLog.length
    ? `${(fullLog.length / 1024).toFixed(1)} KB`
    : "";

  if (state.autoscroll) {
    logBox.scrollTop = logBox.scrollHeight;
  }
}

function statusPill(status) {
  const map = {
    success: ["status-success", "成功"],
    failed:  ["status-failed",  "失败"],
    timeout: ["status-timeout", "超时"],
    skipped: ["status-skipped", "跳过"],
    running: ["status-running", "运行中"],
  };
  const [cls, text] = map[status] || ["status-skipped", status || "-"];
  return `${text}`;
}

function renderHistory() {
  const root = $("#history-list");
  if (!state.scripts.length) {
    root.innerHTML = `

配置文件中未声明任何脚本

`; return; } // 当前正在执行的任务 ID(用于高亮) const liveKey = state.current ? `${state.current.task.script}|${state.current.task.repo}|${state.current.task.ref}` : null; root.innerHTML = state.scripts.map((script) => { const records = state.history[script] || []; const rows = records.length ? records.slice().reverse().map((r) => { const liveMarker = (liveKey && liveKey === `${r.script}|${r.repo}|${r.ref}` && r.status === "running") ? "row-running" : ""; const logId = `log-${r.id}`; return ` ${escapeHtml(fmtFullTime(r.startedAt))} ${escapeHtml(shortCommit(r.commit))} ${escapeHtml(r.repo)} ${escapeHtml(r.ref)} ${statusPill(r.status)} ${escapeHtml(r.duration || (r.status === "running" ? "进行中" : "-"))} ${escapeHtml(r.reason || "")} ${r.log ? `` : `无日志`} ${r.log ? ` ` : ""} `; }).join("") : `尚无执行记录`; return `

${escapeHtml(script)}(${records.length})

${rows}
开始时间Commit仓库/分支 状态耗时原因日志
`; }).join(""); // 绑定展开日志按钮 root.querySelectorAll(".toggle-log").forEach((btn) => { btn.addEventListener("click", () => { const target = document.getElementById(btn.dataset.target); if (!target) return; const visible = target.style.display !== "none"; target.style.display = visible ? "none" : "block"; btn.textContent = visible ? "查看日志" : "收起日志"; }); }); } // ============ Wiring ============ $("#refresh-btn").addEventListener("click", () => { pollQueue(); pollHistory(); }); $("#log-autoscroll").addEventListener("click", (e) => { state.autoscroll = !state.autoscroll; e.currentTarget.classList.toggle("active", state.autoscroll); }); pollQueue(); pollHistory(); setInterval(pollQueue, POLL_QUEUE_MS); setInterval(pollHistory, POLL_HISTORY_MS);