(() => { const state = { token: '', queue: '', base: location.origin, timer: null, connected: false, loading: false }; const $ = (id) => document.getElementById(id); const text = (id, value) => { const el = $(id); if (el) el.textContent = value; }; const sessionQueue = sessionStorage.getItem('orbit.console.queue') || ''; $('api-url').value = location.origin; $('queue-id').value = sessionQueue; function showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 2800); } function formatTime(value) { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date); } function shortID(id) { return id && id.length > 14 ? `${id.slice(0, 8)}…${id.slice(-4)}` : (id || '—'); } function stateClass(value) { return String(value || '').toLowerCase(); } function extractItems(payload) { return payload?.data?.items || payload?.items || []; } async function request(path, options = {}) { const headers = { Accept: 'application/json', ...(options.headers || {}) }; if (state.token) headers.Authorization = `Bearer ${state.token}`; const response = await fetch(`${state.base.replace(/\/$/, '')}${path}`, { ...options, headers }); const raw = await response.text(); let body = {}; try { body = raw ? JSON.parse(raw) : {}; } catch { body = { message: raw }; } if (!response.ok) throw new Error(body?.error?.message || body?.message || `HTTP ${response.status}`); return body; } function setConnection(connected, detail = '') { state.connected = connected; const authenticated = connected && Boolean(state.token && state.queue); $('connection-state').textContent = authenticated ? '已连接' : (connected ? '服务在线' : '未连接'); $('connection-state').classList.toggle('ready', connected); $('side-status').textContent = authenticated ? '环境已连接' : (connected ? '服务在线' : '等待连接'); $('side-endpoint').textContent = connected ? state.base : 'API 未连接'; $('side-progress').style.width = connected ? '100%' : '0%'; document.querySelector('.status-dot').classList.toggle('ready', connected); document.querySelector('.live-chip').classList.toggle('ready', connected); $('live-label').textContent = connected ? '环境在线' : '本地环境'; if (detail) showToast(detail); } async function loadHealth() { try { const [live, version] = await Promise.all([request('/health/live'), request('/version')]); text('metric-health', live?.status === 'alive' ? '在线' : '异常'); text('metric-health-sub', `Go ${version?.go_version || '—'} · API v${version?.http_protocol_version || '—'}`); text('side-status', '环境已连接'); return version; } catch (error) { text('metric-health', '离线'); text('metric-health-sub', error.message); setConnection(false); throw error; } } function renderTasks(items) { text('task-count', `${items.length} 项`); text('metric-tasks', String(items.length)); const counts = items.reduce((acc, item) => { const key = stateClass(item.state); acc[key] = (acc[key] || 0) + 1; return acc; }, {}); const total = Math.max(items.length, 1); const width = (name) => `${(counts[name] || 0) / total * 100}%`; $('task-bar-succeeded').style.width = width('succeeded'); $('task-bar-running').style.width = width('running'); $('task-bar-waiting').style.width = `${((counts.waiting || 0) + (counts.pending || 0)) / total * 100}%`; $('task-bar-other').style.width = `${((counts.failed || 0) + (counts.cancelled || 0) + (counts.needs_review || 0)) / total * 100}%`; text('metric-tasks-sub', `${counts.succeeded || 0} 成功 · ${counts.running || 0} 执行中`); const body = $('task-table'); if (!items.length) { body.innerHTML = '◇当前队列没有任务提交任务后,这里会自动显示最新状态'; return; } body.innerHTML = items.slice(0, 8).map(item => `
${escapeHTML(shortID(item.id))}${escapeHTML(item.business_key || '无业务键')}
${escapeHTML(item.handler || '—')}
${escapeHTML(item.handler_version || '')}${escapeHTML(item.state || 'UNKNOWN')}${escapeHTML(item.revision || '—')}${escapeHTML(formatTime(item.created_at))}`).join(''); } function renderSchedules(items) { text('metric-schedules', String(items.length)); const el = $('schedule-list'); if (!items.length) { el.innerHTML = '
◷
当前没有定时计划需要 schedules:read 权限
'; return; } el.innerHTML = items.slice(0, 4).map(item => `
◷
${escapeHTML(item.name || shortID(item.id))}${escapeHTML(item.queue_id ? shortID(item.queue_id) : '计划')}
${escapeHTML(item.enabled === false ? '已暂停' : '运行中')}
`).join(''); } function renderWorkflows(items) { text('metric-workflows', String(items.length)); text('metric-workflows-sub', items.length ? '最近实例' : '当前没有实例'); const el = $('workflow-list'); if (!items.length) { el.innerHTML = '
⌘
当前没有流程实例需要 workflows:read 权限
'; return; } el.innerHTML = items.slice(0, 4).map(item => `
⌘
${escapeHTML(item.business_key || shortID(item.id))}${escapeHTML(item.state || '—')}
${escapeHTML(formatTime(item.created_at))}
`).join(''); } function renderSubscriptions(items) { text('metric-subscriptions', String(items.length)); const active = items.filter(item => item.enabled !== false).length; $('subscription-meter-fill').style.width = `${items.length ? active / items.length * 100 : 0}%`; text('metric-subscriptions-sub', items.length ? `${active} 个已启用 · 交付自动恢复` : '当前没有结果订阅'); const el = $('subscription-list'); if (!items.length) { el.innerHTML = '
⌁
当前没有结果订阅需要 events:read 权限
'; return; } el.innerHTML = items.slice(0, 4).map(item => `
⌁
${escapeHTML(item.name || shortID(item.id))}待处理 ${escapeHTML(String(item.pending_count ?? 0))} · 上限 ${escapeHTML(String(item.max_pending ?? '—'))}
${item.enabled === false ? '已暂停' : '已启用'}
`).join(''); } function escapeHTML(value) { return String(value ?? '').replace(/[&<>'"]/g, char => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); } async function refresh() { if (state.loading) return; state.loading = true; $('refresh-button').classList.add('spinning'); try { await loadHealth(); if (!state.token || !state.queue) { setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; } const tasks = await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`); renderTasks(extractItems(tasks)); const optional = await Promise.allSettled([ request(`/v1/schedules?queue_id=${encodeURIComponent(state.queue)}&limit=20`), request('/v1/workflows?limit=20'), request(`/v1/subscriptions?queue_id=${encodeURIComponent(state.queue)}&limit=20`), ]); if (optional[0].status === 'fulfilled') renderSchedules(extractItems(optional[0].value)); if (optional[1].status === 'fulfilled') renderWorkflows(extractItems(optional[1].value)); if (optional[2].status === 'fulfilled') renderSubscriptions(extractItems(optional[2].value)); setConnection(true); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); text('metric-health-sub', '数据库与 Schema 正常'); } catch (error) { setConnection(false); showToast(`连接失败:${error.message}`); text('last-updated', '刷新失败'); } finally { state.loading = false; $('refresh-button').classList.remove('spinning'); } } function connect() { state.base = $('api-url').value.trim().replace(/\/$/, '') || location.origin; state.token = $('token').value.trim(); state.queue = $('queue-id').value.trim(); sessionStorage.setItem('orbit.console.queue', state.queue); refresh(); } $('connect-button').addEventListener('click', connect); $('connect-hero').addEventListener('click', () => $('connection-panel').scrollIntoView({ behavior: 'smooth', block: 'center' })); $('refresh-button').addEventListener('click', refresh); $('reveal-token').addEventListener('click', () => { const field = $('token'); field.type = field.type === 'password' ? 'text' : 'password'; $('reveal-token').textContent = field.type === 'password' ? '显示' : '隐藏'; }); $('credential-file').addEventListener('change', event => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const credential = JSON.parse(reader.result); $('token').value = credential.token || ''; $('queue-id').value = credential.queue_id || ''; showToast('凭据已读取,只保留在当前页面内'); } catch { showToast('凭据 JSON 无法解析'); } }; reader.readAsText(file); }); document.querySelectorAll('[data-scroll]').forEach(button => button.addEventListener('click', () => { const target = $(button.dataset.scroll); if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); document.querySelectorAll('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.scroll === button.dataset.scroll)); })); refresh(); state.timer = setInterval(refresh, 10000); })();