feat: show application ownership and scoped console management
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
(() => {
|
||||
const state = { token: '', authenticated: false, queue: '', base: location.origin, consoleConfig: null, retention: null, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null };
|
||||
const state = { token: '', authenticated: false, queue: '', base: location.origin, consoleConfig: null, scope: null, scopeReady: false, scopeNamespace: '', mutations: 0, retention: null, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null };
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const text = (id, value) => { const el = $(id); if (el) el.textContent = value; };
|
||||
const pageNames = { overview: '总览', tasks: '任务执行', schedules: '定时计划', workflows: '流程实例', subscriptions: '结果订阅', retention: '保留策略', archives: '归档作业', applications: '应用接入' };
|
||||
@@ -26,10 +26,12 @@
|
||||
document.querySelectorAll('.page-view').forEach(page => page.classList.toggle('active', page.dataset.page === next));
|
||||
document.querySelectorAll('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.view === next));
|
||||
text('page-title', pageNames[next]);
|
||||
$('application-scope').hidden = next === 'applications';
|
||||
text('scope-hint', ['workflows', 'retention', 'archives'].includes(next) ? '当前应用环境 · 不受队列筛选影响' : '任务、计划和订阅按所选队列显示');
|
||||
if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
if (next === 'retention' && state.authenticated) loadRetention();
|
||||
if (next === 'archives' && state.authenticated) loadArchives();
|
||||
if (next === 'retention' && state.scopeReady) loadRetention();
|
||||
if (next === 'archives' && state.scopeReady) loadArchives();
|
||||
if (next === 'applications' && state.authenticated) window.orbitApplications.load();
|
||||
}
|
||||
function redirectToLogin() {
|
||||
@@ -43,13 +45,18 @@
|
||||
state.authenticated = true; return true;
|
||||
}
|
||||
async function request(path, options = {}) {
|
||||
const headers = { Accept: 'application/json', ...(options.headers || {}) };
|
||||
const scoped = path.startsWith('/v1/') || path === '/console/scope' || path.startsWith('/console/scope?') || path.startsWith('/console/retention') || path.startsWith('/console/archive/');
|
||||
const headers = { Accept: 'application/json', ...(scoped && state.scopeNamespace ? { 'X-Orbit-Namespace': state.scopeNamespace, 'X-Orbit-Console': '1' } : {}), ...(options.headers || {}) };
|
||||
const mutation = options.method && options.method !== 'GET';
|
||||
if (mutation) state.mutations++;
|
||||
try {
|
||||
const response = await fetch(`${state.base.replace(/\/$/, '')}${path}`, { ...options, headers, credentials: 'same-origin' });
|
||||
const raw = await response.text(); let body = {};
|
||||
try { body = raw ? JSON.parse(raw) : {}; } catch { body = { message: raw }; }
|
||||
if (response.status === 401) { state.authenticated = false; redirectToLogin(); throw new Error('登录已过期,请重新登录'); }
|
||||
if (!response.ok) { const error = new Error(body?.error?.message || body?.message || `HTTP ${response.status}`); error.status = response.status; throw error; }
|
||||
return body;
|
||||
} finally { if (mutation) state.mutations--; }
|
||||
}
|
||||
async function loadConsoleConfig() {
|
||||
try {
|
||||
@@ -65,6 +72,97 @@
|
||||
// 配置只是已登录会话的默认队列,失败时业务 API 会返回明确错误。
|
||||
}
|
||||
}
|
||||
const scopeUI = { applications: [], nextApplication: '', nextQueue: '', busy: false };
|
||||
function scopeBadge() {
|
||||
const e = state.scope?.entry;
|
||||
if (!e) return '';
|
||||
return `<span class="application-badge" title="${escapeHTML(e.application + ' / ' + e.environment + ' / ' + e.namespace)}"><i></i><b>${escapeHTML(e.application)}</b><em>${escapeHTML(e.environment)}</em></span>`;
|
||||
}
|
||||
function appendScopeOptions(select, items, selected, more, kind) {
|
||||
select.replaceChildren();
|
||||
for (const item of items) {
|
||||
const option = document.createElement('option');
|
||||
option.value = kind === 'application' ? item.namespace_id : item.id;
|
||||
option.textContent = kind === 'application' ? `${item.application} · ${item.environment} / ${item.namespace}${item.enabled ? '' : ' · 已停用'}` : item.name;
|
||||
option.disabled = kind === 'application' && !item.enabled;
|
||||
select.append(option);
|
||||
}
|
||||
if (!items.length) select.append(new Option('暂无队列', ''));
|
||||
if (more) select.append(new Option('加载更多…', '__more__'));
|
||||
select.value = selected;
|
||||
}
|
||||
async function loadScopeApplications() {
|
||||
const page = extractData(await request(`/console/applications${scopeUI.nextApplication ? `?after=${encodeURIComponent(scopeUI.nextApplication)}` : ''}`));
|
||||
for (const item of page.items || []) if (!scopeUI.applications.some(e => e.namespace_id === item.namespace_id)) scopeUI.applications.push(item);
|
||||
scopeUI.nextApplication = page.next || '';
|
||||
appendScopeOptions($('scope-application'), scopeUI.applications, state.scope.entry.namespace_id, scopeUI.nextApplication, 'application');
|
||||
}
|
||||
async function loadScope() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
state.scopeNamespace = params.get('application') || '';
|
||||
try {
|
||||
const scope = extractData(await request('/console/scope'));
|
||||
state.scope = scope;
|
||||
// 指定管理员在默认应用内也使用显式管理范围,普通账号继续沿用原有队列权限。
|
||||
if (scope.can_switch && !state.scopeNamespace) {
|
||||
state.scopeNamespace = scope.entry.namespace_id;
|
||||
state.scope = extractData(await request('/console/scope'));
|
||||
}
|
||||
const queues = state.scope.queues;
|
||||
const requested = params.get('queue');
|
||||
const preferred = requested || state.consoleConfig?.default_queue_id;
|
||||
state.queue = queues.some(q => q.id === preferred) ? preferred : (queues[0]?.id || '');
|
||||
if (requested && !queues.some(q => q.id === requested)) {
|
||||
const queue = extractData(await request(`/v1/queues/${encodeURIComponent(requested)}`));
|
||||
queues.push({ id: queue.id, name: queue.name }); state.queue = requested;
|
||||
}
|
||||
scopeUI.nextQueue = state.scope.next || '';
|
||||
scopeUI.applications = [state.scope.entry];
|
||||
appendScopeOptions($('scope-application'), scopeUI.applications, scope.entry.namespace_id, '', 'application');
|
||||
appendScopeOptions($('scope-queue'), queues, state.queue, scopeUI.nextQueue, 'queue');
|
||||
$('scope-application').disabled = !scope.can_switch;
|
||||
$('scope-queue').disabled = !queues.length;
|
||||
if (scope.can_switch) {
|
||||
try { await loadScopeApplications(); } catch (error) { text('scope-error', '应用目录读取失败:' + error.message); }
|
||||
}
|
||||
document.querySelectorAll('.detail-scope').forEach(el => { el.innerHTML = scopeBadge(); });
|
||||
$('retention-scope').innerHTML = scopeBadge();
|
||||
text('scope-summary', `${scope.entry.application} / ${scope.entry.environment} / ${scope.entry.namespace}`);
|
||||
state.scopeReady = true; renderConnectionSummary();
|
||||
} catch (error) {
|
||||
state.queue = ''; state.scopeReady = false;
|
||||
text('scope-error', `当前应用范围不可用:${error.message}。可点击“重置范围”返回默认应用。`);
|
||||
}
|
||||
}
|
||||
function navigateScope(namespace, queue = '') {
|
||||
if (state.mutations || retentionUI.busy || archiveUI.busy || retentionUI.pendingSave || archiveUI.pending) { showToast('有操作尚未确认,请先核对完成后再切换应用'); return false; }
|
||||
if (retentionUI.dirty || archiveUI.dirty || document.querySelector('.drawer-backdrop:not([hidden])')) { showToast('请先保存配置并关闭详情,再切换查看范围'); return false; }
|
||||
const url = new URL(location.href); url.searchParams.delete('application'); url.searchParams.delete('queue');
|
||||
if (namespace) url.searchParams.set('application', namespace);
|
||||
if (queue) url.searchParams.set('queue', queue);
|
||||
// 完整导航隔离不同范围的请求、详情及幂等状态;后退和刷新都恢复 URL 中的范围。
|
||||
location.assign(url.href); return true;
|
||||
}
|
||||
$('scope-application').addEventListener('change', async event => {
|
||||
const value = event.target.value; event.target.value = state.scope?.entry.namespace_id || '';
|
||||
if (scopeUI.busy) return;
|
||||
if (value !== '__more__') { navigateScope(value); return; }
|
||||
scopeUI.busy = true;
|
||||
try { await loadScopeApplications(); } catch (error) { showToast(error.message); } finally { scopeUI.busy = false; }
|
||||
});
|
||||
$('scope-queue').addEventListener('change', async event => {
|
||||
const value = event.target.value; event.target.value = state.queue;
|
||||
if (scopeUI.busy) return;
|
||||
if (value !== '__more__') { navigateScope(state.scopeNamespace, value); return; }
|
||||
scopeUI.busy = true;
|
||||
try {
|
||||
const page = extractData(await request(`/console/scope?after=${encodeURIComponent(scopeUI.nextQueue)}`));
|
||||
for (const queue of page.queues) if (!state.scope.queues.some(q => q.id === queue.id)) state.scope.queues.push(queue);
|
||||
scopeUI.nextQueue = page.next || '';
|
||||
appendScopeOptions($('scope-queue'), state.scope.queues, state.queue, scopeUI.nextQueue, 'queue');
|
||||
} catch (error) { showToast(error.message); } finally { scopeUI.busy = false; }
|
||||
});
|
||||
$('scope-reset').addEventListener('click', () => navigateScope(''));
|
||||
const retentionUI = { busy: false, dirty: false, cursor: '', batch: 0, count: 0, done: false, download: '', pendingSave: null };
|
||||
function syncRetentionControls() {
|
||||
document.querySelectorAll('#retention-form input, #retention-form textarea, #retention-form select, #retention-form button').forEach(el => { el.disabled = retentionUI.busy; });
|
||||
@@ -221,7 +319,7 @@
|
||||
text('archive-executor', archiveUI.configured ? '已配置持久卷' : '未配置持久卷');
|
||||
$('archive-executor').classList.toggle('ready', archiveUI.configured);
|
||||
$('archive-plan-chips').innerHTML = archiveUI.plans.map(p => `<span class="${p.enabled ? 'enabled' : ''}">${archiveLabels[p.kind]} · ${p.enabled ? '开启' : '关闭'}</span>`).join('');
|
||||
$('archive-job-list').innerHTML = jobs.length ? jobs.map(j => `<article class="archive-job"><div class="archive-job-head"><div><b>${escapeHTML(archiveLabels[j.kind] || j.kind)}</b><span class="state-pill ${stateClass(j.state)}">${escapeHTML(archiveStates[j.state] || j.state)}</span></div><time>${escapeHTML(formatTime(j.created_at))}</time></div><code class="archive-job-id">${escapeHTML(j.id)}</code><div class="archive-progress"><span><b>${j.batches}</b> 批已确认</span><span><b>${j.roots.toLocaleString('zh-CN')}</b> 条根记录</span><span><b>${archiveSize(j.bytes)}</b> / ${j.max_bytes / 1048576} MiB</span><span>策略 v${escapeHTML(j.policy_revision)}</span></div>${j.last_sha256 ? `<details><summary>查看文件校验摘要</summary><code class="archive-digest">${escapeHTML(j.last_sha256)}</code></details>` : ''}${j.error_code ? `<p class="archive-job-error">${escapeHTML(archiveErrors[j.error_code] || j.error_code)}</p>` : ''}${j.state === 'RUNNING' ? `<p class="archive-job-note">领取次数 ${j.attempts} · 租约至 ${escapeHTML(formatTime(j.lease_until))},中断后可接管。</p>` : ''}${j.state === 'FAILED' ? `<div class="archive-job-actions"><button type="button" class="secondary-button compact" data-archive-action="retry" data-id="${escapeHTML(j.id)}">从原作业重试 ↻</button><button type="button" class="text-button" data-archive-action="abandon" data-id="${escapeHTML(j.id)}">关闭此作业</button></div>` : ''}</article>`).join('') : '<div class="empty-row archive-empty"><span class="empty-icon">▤</span><b>还没有归档作业</b><small>保存并开启周期后,系统会按保留窗口生成作业。</small></div>';
|
||||
$('archive-job-list').innerHTML = jobs.length ? jobs.map(j => `<article class="archive-job"><div class="archive-job-head"><div><b>${escapeHTML(archiveLabels[j.kind] || j.kind)}</b><span class="state-pill ${stateClass(j.state)}">${escapeHTML(archiveStates[j.state] || j.state)}</span></div><time>${escapeHTML(formatTime(j.created_at))}</time></div>${scopeBadge()}<code class="archive-job-id">${escapeHTML(j.id)}</code><div class="archive-progress"><span><b>${j.batches}</b> 批已确认</span><span><b>${j.roots.toLocaleString('zh-CN')}</b> 条根记录</span><span><b>${archiveSize(j.bytes)}</b> / ${j.max_bytes / 1048576} MiB</span><span>策略 v${escapeHTML(j.policy_revision)}</span></div>${j.last_sha256 ? `<details><summary>查看文件校验摘要</summary><code class="archive-digest">${escapeHTML(j.last_sha256)}</code></details>` : ''}${j.error_code ? `<p class="archive-job-error">${escapeHTML(archiveErrors[j.error_code] || j.error_code)}</p>` : ''}${j.state === 'RUNNING' ? `<p class="archive-job-note">领取次数 ${j.attempts} · 租约至 ${escapeHTML(formatTime(j.lease_until))},中断后可接管。</p>` : ''}${j.state === 'FAILED' ? `<div class="archive-job-actions"><button type="button" class="secondary-button compact" data-archive-action="retry" data-id="${escapeHTML(j.id)}">从原作业重试 ↻</button><button type="button" class="text-button" data-archive-action="abandon" data-id="${escapeHTML(j.id)}">关闭此作业</button></div>` : ''}</article>`).join('') : '<div class="empty-row archive-empty"><span class="empty-icon">▤</span><b>还没有归档作业</b><small>保存并开启周期后,系统会按保留窗口生成作业。</small></div>';
|
||||
syncArchiveControls();
|
||||
}
|
||||
async function loadArchives() {
|
||||
@@ -317,8 +415,8 @@
|
||||
$('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 = '<tr><td colspan="5" class="empty-row"><span class="empty-icon">◇</span><b>当前队列没有任务</b><small>提交任务后,这里会自动显示最新状态</small></td></tr>'; return; }
|
||||
body.innerHTML = items.slice(0, 8).map(item => `<tr class="task-row" data-task-id="${escapeHTML(item.id)}" tabindex="0" title="打开任务详情"><td><div class="task-id"><b>${escapeHTML(shortID(item.id))}</b><small>${escapeHTML(item.business_key || '无业务键')}</small></div></td><td class="task-processor">${escapeHTML(item.handler || '—')}<br><small>${escapeHTML(item.handler_version || '')}</small></td><td><span class="state-pill ${stateClass(item.state)}">${escapeHTML(item.state || 'UNKNOWN')}</span></td><td>${escapeHTML(item.revision || '—')}</td><td>${escapeHTML(formatTime(item.created_at))}</td></tr>`).join('');
|
||||
if (!items.length) { body.innerHTML = '<tr><td colspan="6" class="empty-row"><span class="empty-icon">◇</span><b>当前队列没有任务</b><small>提交任务后,这里会自动显示最新状态</small></td></tr>'; return; }
|
||||
body.innerHTML = items.map(item => `<tr class="task-row" data-task-id="${escapeHTML(item.id)}" tabindex="0" title="打开任务详情"><td><div class="task-id"><b>${escapeHTML(shortID(item.id))}</b><small>${escapeHTML(item.business_key || '无业务键')}</small></div></td><td>${scopeBadge()}</td><td class="task-processor">${escapeHTML(item.handler || '—')}<br><small>${escapeHTML(item.handler_version || '')}</small></td><td><span class="state-pill ${stateClass(item.state)}">${escapeHTML(item.state || 'UNKNOWN')}</span></td><td>${escapeHTML(item.revision || '—')}</td><td>${escapeHTML(formatTime(item.created_at))}</td></tr>`).join('');
|
||||
body.querySelectorAll('.task-row').forEach(row => {
|
||||
row.addEventListener('click', () => openTaskDetail(row.dataset.taskId));
|
||||
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openTaskDetail(row.dataset.taskId); } });
|
||||
@@ -326,8 +424,8 @@
|
||||
}
|
||||
function renderSchedules(items) {
|
||||
text('schedule-page-hint', `${items.length} 项 · 点击计划查看详情`);
|
||||
const el = $('schedule-list'); if (!items.length) { el.innerHTML = '<div class="empty-compact"><span>◷</span><div><b>当前没有定时计划</b><small>需要 schedules:read 权限</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row schedule-row" data-schedule-id="${escapeHTML(item.id)}" tabindex="0" title="打开计划详情"><div class="list-avatar">◷</div><div class="list-copy"><b>${escapeHTML(item.name || shortID(item.id))}</b><small>${escapeHTML(item.queue_id ? shortID(item.queue_id) : '计划')}</small></div><div class="list-meta">${escapeHTML(item.paused ? '已暂停' : '运行中')}</div></div>`).join('');
|
||||
const el = $('schedule-list'); if (!items.length) { el.innerHTML = '<div class="empty-compact"><span>◷</span><div><b>当前没有定时计划</b><small>当前应用与队列还没有创建计划</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row schedule-row" data-schedule-id="${escapeHTML(item.id)}" tabindex="0" title="打开计划详情"><div class="list-avatar">◷</div><div class="list-copy"><b>${escapeHTML(item.name || shortID(item.id))}</b><small>${escapeHTML(item.queue_id ? shortID(item.queue_id) : '计划')}</small></div><div class="list-application">${scopeBadge()}</div><div class="list-meta">${escapeHTML(item.paused ? '已暂停' : '运行中')}</div></div>`).join('');
|
||||
el.querySelectorAll('.schedule-row').forEach(row => {
|
||||
row.addEventListener('click', () => openScheduleDetail(row.dataset.scheduleId));
|
||||
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openScheduleDetail(row.dataset.scheduleId); } });
|
||||
@@ -335,8 +433,8 @@
|
||||
}
|
||||
function renderWorkflows(items) {
|
||||
text('metric-workflows', String(items.length)); text('metric-workflows-sub', items.length ? '最近实例' : '当前没有实例');
|
||||
const el = $('workflow-list'); if (!items.length) { el.innerHTML = '<div class="empty-compact"><span>⌘</span><div><b>当前没有流程实例</b><small>需要 workflows:read 权限</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row workflow-row" data-workflow-id="${escapeHTML(item.id)}" tabindex="0" title="打开流程详情"><div class="list-avatar">⌘</div><div class="list-copy"><b>${escapeHTML(item.business_key || shortID(item.id))}</b><small>${escapeHTML(item.state || '—')} · ${escapeHTML(item.version || '—')}</small></div><div class="list-meta">${escapeHTML(formatTime(item.created_at))}</div></div>`).join('');
|
||||
const el = $('workflow-list'); if (!items.length) { el.innerHTML = '<div class="empty-compact"><span>⌘</span><div><b>当前没有流程实例</b><small>当前应用环境还没有启动流程</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row workflow-row" data-workflow-id="${escapeHTML(item.id)}" tabindex="0" title="打开流程详情"><div class="list-avatar">⌘</div><div class="list-copy"><b>${escapeHTML(item.business_key || shortID(item.id))}</b><small>${escapeHTML(item.state || '—')} · ${escapeHTML(item.version || '—')}</small></div><div class="list-application">${scopeBadge()}</div><div class="list-meta">${escapeHTML(formatTime(item.created_at))}</div></div>`).join('');
|
||||
el.querySelectorAll('.workflow-row').forEach(row => {
|
||||
row.addEventListener('click', () => openWorkflowDetail(row.dataset.workflowId));
|
||||
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openWorkflowDetail(row.dataset.workflowId); } });
|
||||
@@ -346,8 +444,8 @@
|
||||
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 = '<div class="empty-compact"><span>⌁</span><div><b>当前没有结果订阅</b><small>需要 events:read 权限</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row subscription-row" data-subscription-id="${escapeHTML(item.id)}" tabindex="0" title="打开订阅详情"><div class="list-avatar">⌁</div><div class="list-copy"><b>${escapeHTML(item.name || shortID(item.id))}</b><small>待处理 ${escapeHTML(String(item.pending_count ?? 0))} · 上限 ${escapeHTML(String(item.max_pending ?? '—'))}</small></div><div class="list-meta">${item.enabled === false ? '已暂停' : '已启用'}</div></div>`).join('');
|
||||
const el = $('subscription-list'); if (!items.length) { el.innerHTML = '<div class="empty-compact"><span>⌁</span><div><b>当前没有结果订阅</b><small>当前应用与队列还没有创建订阅</small></div></div>'; return; }
|
||||
el.innerHTML = items.map(item => `<div class="list-row subscription-row" data-subscription-id="${escapeHTML(item.id)}" tabindex="0" title="打开订阅详情"><div class="list-avatar">⌁</div><div class="list-copy"><b>${escapeHTML(item.name || shortID(item.id))}</b><small>待处理 ${escapeHTML(String(item.pending_count ?? 0))} · 上限 ${escapeHTML(String(item.max_pending ?? '—'))}</small></div><div class="list-application">${scopeBadge()}</div><div class="list-meta">${item.enabled === false ? '已暂停' : '已启用'}</div></div>`).join('');
|
||||
el.querySelectorAll('.subscription-row').forEach(row => {
|
||||
row.addEventListener('click', () => openSubscriptionDetail(row.dataset.subscriptionId));
|
||||
row.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSubscriptionDetail(row.dataset.subscriptionId); } });
|
||||
@@ -672,19 +770,22 @@
|
||||
try {
|
||||
await loadHealth();
|
||||
if (currentView() === 'applications') { await window.orbitApplications.load(); setConnection(true); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); return; }
|
||||
if (!state.authenticated || !state.queue) { renderRuntimeStats(null, null, null, null); setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; }
|
||||
const tasks = await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`); const taskItems = extractItems(tasks); renderTasks(taskItems);
|
||||
if (!state.authenticated || !state.scopeReady) { renderRuntimeStats(null, null, null, null); setConnection(true); text('last-updated', `服务在线 · ${formatTime(new Date())}`); return; }
|
||||
const tasks = state.queue ? await request(`/v1/tasks?queue_id=${encodeURIComponent(state.queue)}&limit=20`) : { items: [] }; const taskItems = extractItems(tasks); renderTasks(taskItems);
|
||||
const optional = await Promise.allSettled([
|
||||
request(`/v1/schedules?queue_id=${encodeURIComponent(state.queue)}&limit=20`),
|
||||
state.queue ? request(`/v1/schedules?queue_id=${encodeURIComponent(state.queue)}&limit=20`) : Promise.resolve({ items: [] }),
|
||||
request('/v1/workflows?limit=20'),
|
||||
request(`/v1/subscriptions?queue_id=${encodeURIComponent(state.queue)}&limit=20`),
|
||||
state.queue ? request(`/v1/subscriptions?queue_id=${encodeURIComponent(state.queue)}&limit=20`) : Promise.resolve({ items: [] }),
|
||||
]);
|
||||
const scheduleItems = optional[0].status === 'fulfilled' ? extractItems(optional[0].value) : null;
|
||||
const workflowItems = optional[1].status === 'fulfilled' ? extractItems(optional[1].value) : null;
|
||||
const subscriptionItems = optional[2].status === 'fulfilled' ? extractItems(optional[2].value) : null;
|
||||
if (scheduleItems) renderSchedules(scheduleItems);
|
||||
if (workflowItems) renderWorkflows(workflowItems);
|
||||
if (subscriptionItems) renderSubscriptions(subscriptionItems);
|
||||
renderSchedules(scheduleItems || []);
|
||||
if (!scheduleItems) $('schedule-list').textContent = '定时计划读取失败,请刷新重试或检查权限';
|
||||
renderWorkflows(workflowItems || []);
|
||||
if (!workflowItems) $('workflow-list').textContent = '流程实例读取失败,请刷新重试或检查权限';
|
||||
renderSubscriptions(subscriptionItems || []);
|
||||
if (!subscriptionItems) $('subscription-list').textContent = '结果订阅读取失败,请刷新重试或检查权限';
|
||||
setConnection(true); renderRuntimeStats(taskItems, scheduleItems, workflowItems, subscriptionItems); 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'); }
|
||||
@@ -709,6 +810,6 @@
|
||||
document.querySelectorAll('[data-view]').forEach(button => button.addEventListener('click', event => { event.preventDefault(); setView(button.dataset.view); }));
|
||||
window.addEventListener('hashchange', () => setView(currentView(), false));
|
||||
setView(currentView(), false);
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(() => Promise.all([loadRetention(), refresh(), currentView() === 'archives' ? loadArchives() : Promise.resolve()])); }).catch(() => redirectToLogin());
|
||||
state.timer = setInterval(() => { if (state.authenticated) { refresh(); if (currentView() === 'archives') loadArchives(); } }, 10000);
|
||||
ensureSession().then(ok => { if (ok) return loadConsoleConfig().then(loadScope).then(() => Promise.all([state.scopeReady ? loadRetention() : Promise.resolve(), refresh(), state.scopeReady && currentView() === 'archives' ? loadArchives() : Promise.resolve()])); }).catch(() => redirectToLogin());
|
||||
state.timer = setInterval(() => { if (state.authenticated) { refresh(); if (state.scopeReady && currentView() === 'archives') loadArchives(); } }, 10000);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user