Spaces:
Paused
Paused
| (function () { | |
| const statusLabels = { | |
| pending: "排队中", | |
| running: "执行中", | |
| cancel_requested: "停止中", | |
| completed: "已完成", | |
| stopped: "已停止", | |
| failed: "失败", | |
| idle: "未启动" | |
| }; | |
| function renderLogLine(log) { | |
| const line = document.createElement("div"); | |
| const level = (log.level || "INFO").toLowerCase(); | |
| line.className = `log-line level-${level}`; | |
| const meta = document.createElement("span"); | |
| meta.className = "log-meta"; | |
| const owner = log.student_id || "system"; | |
| meta.textContent = `${log.created_at} · ${owner} · ${log.scope} · ${log.level}`; | |
| const message = document.createElement("span"); | |
| message.textContent = log.message || ""; | |
| line.append(meta, message); | |
| return line; | |
| } | |
| function bindLogStream() { | |
| const shell = document.querySelector("[data-log-stream-url]"); | |
| const consoleNode = document.getElementById("log-console"); | |
| if (!shell || !consoleNode || !window.EventSource) { | |
| return; | |
| } | |
| const streamUrl = shell.getAttribute("data-log-stream-url"); | |
| const eventSource = new EventSource(streamUrl); | |
| eventSource.onmessage = function (event) { | |
| try { | |
| const payload = JSON.parse(event.data); | |
| if (consoleNode.querySelector(".muted")) { | |
| consoleNode.innerHTML = ""; | |
| } | |
| consoleNode.appendChild(renderLogLine(payload)); | |
| while (consoleNode.children.length > 360) { | |
| consoleNode.removeChild(consoleNode.firstChild); | |
| } | |
| consoleNode.scrollTop = consoleNode.scrollHeight; | |
| } catch (_error) { | |
| // Ignore malformed frames and keep the stream alive. | |
| } | |
| }; | |
| } | |
| function updateUserStatus(data) { | |
| const taskText = document.getElementById("task-status-text"); | |
| const taskPill = document.getElementById("task-status-pill"); | |
| const courseCount = document.getElementById("course-count"); | |
| const status = (data.task && data.task.status) || "idle"; | |
| if (taskText) { | |
| taskText.textContent = statusLabels[status] || status; | |
| } | |
| if (taskPill) { | |
| taskPill.className = `status-pill status-${status}`; | |
| taskPill.textContent = statusLabels[status] || status; | |
| } | |
| if (courseCount && Array.isArray(data.courses)) { | |
| courseCount.textContent = String(data.courses.length); | |
| } | |
| } | |
| function updateAdminStatus(data) { | |
| const users = document.getElementById("stat-users"); | |
| const running = document.getElementById("stat-running"); | |
| const pending = document.getElementById("stat-pending"); | |
| const parallel = document.getElementById("parallel-limit-input"); | |
| if (users) { | |
| users.textContent = String(data.stats.users_count || 0); | |
| } | |
| if (running) { | |
| running.textContent = String(data.stats.running_count || 0); | |
| } | |
| if (pending) { | |
| pending.textContent = String(data.stats.pending_count || 0); | |
| } | |
| if (parallel) { | |
| parallel.value = String(data.parallel_limit || parallel.value); | |
| } | |
| } | |
| function bindStatusPolling() { | |
| const shell = document.querySelector("[data-status-url]"); | |
| if (!shell) { | |
| return; | |
| } | |
| const statusUrl = shell.getAttribute("data-status-url"); | |
| const isAdmin = shell.classList.contains("admin-dashboard"); | |
| async function refresh() { | |
| try { | |
| const response = await fetch(statusUrl, { headers: { "X-Requested-With": "fetch" } }); | |
| if (!response.ok) { | |
| return; | |
| } | |
| const payload = await response.json(); | |
| if (!payload.ok) { | |
| return; | |
| } | |
| if (isAdmin) { | |
| updateAdminStatus(payload); | |
| } else { | |
| updateUserStatus(payload); | |
| } | |
| } catch (_error) { | |
| // Leave the current UI state as-is if polling fails. | |
| } | |
| } | |
| refresh(); | |
| window.setInterval(refresh, 7000); | |
| } | |
| bindLogStream(); | |
| bindStatusPolling(); | |
| })(); | |