diff --git a/docs/08-development-plan.md b/docs/08-development-plan.md
index e8681b0..c4b9e90 100644
--- a/docs/08-development-plan.md
+++ b/docs/08-development-plan.md
@@ -42,6 +42,8 @@
定时计划详情已接入当前版本、规则、下一次触发和 occurrence 历史;暂停/恢复沿用 `schedules:manage`、`expected_revision`、原因和幂等键。计划发布、跨命名空间管理身份和更高风险的交付控制仍按独立权限模型推进。
+控制台五个核心模块已拆为独立 hash 视图,导航状态和页面标题随视图同步,刷新或直接打开对应 hash 后仍保持正确模块。数据请求继续由统一 Bearer Token 和当前队列范围驱动。
+
## 2. 持续推进方式
每个阶段按以下顺序推进:
diff --git a/docs/console.md b/docs/console.md
index 8733fa2..e82b044 100644
--- a/docs/console.md
+++ b/docs/console.md
@@ -14,6 +14,7 @@ http://127.0.0.1:8080/console/
- 点击任务查看任务详情、执行尝试时间线、最终结果和操作审计摘要。
- 在具备对应权限时提交任务取消和安全重试,沿用服务端的版本校验、幂等键、队列范围和审计规则。
- 点击定时计划查看当前版本、规则、下一次触发和触发记录;在具备 `schedules:manage` 权限时可暂停或恢复计划。
+- 左侧五个模块是独立的控制台视图,使用 `/console/#/overview`、`#/tasks`、`#/schedules`、`#/workflows` 和 `#/subscriptions` 定位;切换视图不会把所有模块堆在一张长页面上。
- 任务状态按 10 秒自动刷新;页面不会把凭据上传到独立服务,也不读取数据库。
- 页面使用当前请求方的业务权限;没有权限的模块显示为空或等待权限,不绕过服务端授权。
diff --git a/internal/transport/httpserver/console/app.js b/internal/transport/httpserver/console/app.js
index 79f3e55..0050b0f 100644
--- a/internal/transport/httpserver/console/app.js
+++ b/internal/transport/httpserver/console/app.js
@@ -2,6 +2,7 @@
const state = { token: '', queue: '', base: location.origin, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '', selectedSchedule: 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: '结果订阅' };
const sessionQueue = sessionStorage.getItem('orbit.console.queue') || '';
$('api-url').value = location.origin;
$('queue-id').value = sessionQueue;
@@ -19,6 +20,18 @@
function stateClass(value) { return String(value || '').toLowerCase(); }
function extractItems(payload) { return payload?.data?.items || payload?.items || []; }
function extractData(payload) { return payload?.data ?? payload ?? {}; }
+ function currentView() {
+ const raw = location.hash.replace(/^#\/?/, '').split('/')[0];
+ return pageNames[raw] ? raw : 'overview';
+ }
+ function setView(view, updateHash = true) {
+ const next = pageNames[view] ? view : 'overview';
+ 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]);
+ if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`;
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
async function request(path, options = {}) {
const headers = { Accept: 'application/json', ...(options.headers || {}) };
if (state.token) headers.Authorization = `Bearer ${state.token}`;
@@ -69,7 +82,7 @@
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.paused ? '已暂停' : '运行中')}
`).join('');
+ el.innerHTML = items.map(item => `◷
${escapeHTML(item.name || shortID(item.id))}${escapeHTML(item.queue_id ? shortID(item.queue_id) : '计划')}
${escapeHTML(item.paused ? '已暂停' : '运行中')}
`).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); } });
@@ -78,14 +91,14 @@
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('');
+ el.innerHTML = items.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('');
+ el.innerHTML = items.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])); }
function formatJSON(value) {
@@ -220,6 +233,8 @@
$('close-task-drawer').addEventListener('click', closeTaskDrawer); $('task-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'task-drawer-backdrop') closeTaskDrawer(); }); $('action-cancel').addEventListener('click', closeActionForm); $('task-action-form').addEventListener('submit', submitAction); $('task-cancel-button').addEventListener('click', () => openActionForm('cancel')); $('task-retry-button').addEventListener('click', () => openActionForm('retry')); $('close-schedule-drawer').addEventListener('click', closeScheduleDrawer); $('schedule-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'schedule-drawer-backdrop') closeScheduleDrawer(); }); $('schedule-control-button').addEventListener('click', openScheduleAction); $('schedule-action-cancel').addEventListener('click', closeScheduleAction); $('schedule-action-form').addEventListener('submit', submitScheduleAction); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); } });
$('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)); }));
+ 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);
refresh(); state.timer = setInterval(refresh, 10000);
})();
diff --git a/internal/transport/httpserver/console/index.html b/internal/transport/httpserver/console/index.html
index 594b187..fd7b333 100644
--- a/internal/transport/httpserver/console/index.html
+++ b/internal/transport/httpserver/console/index.html
@@ -18,11 +18,11 @@
CONTROL PLANE