From cad4e1206ac10a1e63e850cbd4aa8e62ba981158 Mon Sep 17 00:00:00 2001 From: k2safe Date: Mon, 21 Sep 2026 01:26:30 +0800 Subject: [PATCH] feat: add schedule controls to Orbit console --- docs/08-development-plan.md | 4 +- docs/console.md | 3 +- internal/transport/httpserver/console/app.js | 45 +++++++++++++++++-- .../transport/httpserver/console/index.html | 28 ++++++++++++ .../transport/httpserver/console/styles.css | 1 + 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/docs/08-development-plan.md b/docs/08-development-plan.md index 3916c40..e8681b0 100644 --- a/docs/08-development-plan.md +++ b/docs/08-development-plan.md @@ -38,7 +38,9 @@ 已将 [Orbit Console](console.md) 编译进服务端,入口为 `/console/`。第一版读取健康、版本、任务、计划、流程和结果订阅接口,支持本地凭据 JSON、响应式总览和有界自动刷新;不直接访问数据库,也不绕过现有 Bearer Token 与动作范围。独立管理员身份和跨命名空间聚合仍不在控制台内提供。 -控制台任务详情已接入 Pulse 的任务快照、执行尝试、操作审计、取消和安全重试接口。页面操作沿用 `expected_revision`、`Idempotency-Key`、原因、证据和服务端权限校验;`NEEDS_REVIEW` 仍要求通过核查 API/SDK 提交最终结论。下一阶段继续补充分页详情、计划控制和管理身份,不把页面操作视为绕过服务端事务边界的快捷入口。 +控制台任务详情已接入 Pulse 的任务快照、执行尝试、操作审计、取消和安全重试接口。页面操作沿用 `expected_revision`、`Idempotency-Key`、原因、证据和服务端权限校验;`NEEDS_REVIEW` 仍要求通过核查 API/SDK 提交最终结论。下一阶段继续补充分页详情和管理身份,不把页面操作视为绕过服务端事务边界的快捷入口。 + +定时计划详情已接入当前版本、规则、下一次触发和 occurrence 历史;暂停/恢复沿用 `schedules:manage`、`expected_revision`、原因和幂等键。计划发布、跨命名空间管理身份和更高风险的交付控制仍按独立权限模型推进。 ## 2. 持续推进方式 diff --git a/docs/console.md b/docs/console.md index fb8b390..8733fa2 100644 --- a/docs/console.md +++ b/docs/console.md @@ -13,6 +13,7 @@ http://127.0.0.1:8080/console/ - 展示任务、定时计划、流程实例和结果订阅的最近摘要。 - 点击任务查看任务详情、执行尝试时间线、最终结果和操作审计摘要。 - 在具备对应权限时提交任务取消和安全重试,沿用服务端的版本校验、幂等键、队列范围和审计规则。 +- 点击定时计划查看当前版本、规则、下一次触发和触发记录;在具备 `schedules:manage` 权限时可暂停或恢复计划。 - 任务状态按 10 秒自动刷新;页面不会把凭据上传到独立服务,也不读取数据库。 - 页面使用当前请求方的业务权限;没有权限的模块显示为空或等待权限,不绕过服务端授权。 @@ -30,4 +31,4 @@ http://127.0.0.1:8080/console/ 控制台不直接访问数据库,也不绕过 Bearer Token。取消和人工重试会使用当前任务版本生成一次性幂等键;网络结果不明时不要重复点击,应使用原始操作身份通过 API/SDK 核对。`NEEDS_REVIEW` 任务仍须使用带证据的核查 API/SDK 完成最终裁决。控制台不创建独立管理员身份,不支持跨命名空间聚合,不代替平台的权限与审计 API。 -不能把业务 Token 长期放进浏览器本地存储,也不能让控制台直连 MySQL。后续再补独立管理身份、计划控制和交付重试等更高风险操作。 +不能把业务 Token 长期放进浏览器本地存储,也不能让控制台直连 MySQL。后续再补独立管理身份、计划版本发布和交付重试等更高风险操作。 diff --git a/internal/transport/httpserver/console/app.js b/internal/transport/httpserver/console/app.js index 5579f90..79f3e55 100644 --- a/internal/transport/httpserver/console/app.js +++ b/internal/transport/httpserver/console/app.js @@ -1,5 +1,5 @@ (() => { - const state = { token: '', queue: '', base: location.origin, timer: null, connected: false, loading: false, selectedTask: null, actionMode: '' }; + 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 sessionQueue = sessionStorage.getItem('orbit.console.queue') || ''; @@ -69,7 +69,11 @@ 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(''); + 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.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); } }); + }); } function renderWorkflows(items) { text('metric-workflows', String(items.length)); text('metric-workflows-sub', items.length ? '最近实例' : '当前没有实例'); @@ -155,6 +159,41 @@ catch (error) { $('action-error').textContent = error.message || '操作失败'; } finally { button.disabled = false; } } + function showScheduleDrawer() { $('schedule-drawer-backdrop').hidden = false; document.body.classList.add('drawer-open'); } + function closeScheduleDrawer() { $('schedule-drawer-backdrop').hidden = true; document.body.classList.remove('drawer-open'); closeScheduleAction(); } + function closeScheduleAction() { $('schedule-action-form').hidden = true; $('schedule-action-error').textContent = ''; } + function scheduleRuleLabel(rule) { if (!rule) return '—'; return rule.kind === 'CRON' ? `Cron · ${rule.cron || '—'}` : `固定间隔 · ${rule.interval_ms || '—'} ms`; } + function renderScheduleOccurrences(items, error) { + text('schedule-occurrences-count', error ? '权限不足' : `${items.length} 条`); + renderDetailList('schedule-occurrences', items, error ? '当前凭据没有 schedules:read 触发历史权限。' : '还没有触发记录。', item => `
${escapeHTML(item.decision || 'UNKNOWN')} · ${escapeHTML(formatTime(item.scheduled_at))}${escapeHTML(item.reason || '—')} · 版本 ${escapeHTML(item.schedule_version || '—')}
${item.task_id ? escapeHTML(shortID(item.task_id)) : '无任务'}
`); + } + function renderScheduleDetail(schedule) { + state.selectedSchedule = schedule; + text('schedule-detail-title', schedule.name || '定时计划'); text('schedule-detail-id', schedule.id || '—'); text('schedule-detail-state', schedule.paused ? '已暂停' : '运行中'); $('schedule-detail-state').className = `state-pill ${schedule.paused ? 'waiting' : 'succeeded'}`; + text('schedule-detail-updated', `最后更新 ${formatTime(schedule.updated_at)}`); text('schedule-detail-name', schedule.name || '—'); text('schedule-detail-queue', shortID(schedule.queue_id)); text('schedule-detail-revision', schedule.revision || '—'); text('schedule-detail-version', schedule.current_version || '—'); text('schedule-detail-next', formatTime(schedule.next_fire_at)); + const definition = schedule.current?.definition || {}; const rule = definition.rule || {}; text('schedule-detail-timezone', rule.timezone || '—'); text('schedule-detail-rule-kind', rule.kind || '—'); $('schedule-detail-rule').innerHTML = `${escapeHTML(scheduleRuleLabel(rule))}漏跑 ${escapeHTML(definition.policy?.misfire || '—')} · 重叠 ${escapeHTML(definition.policy?.overlap || '—')} · 补偿上限 ${escapeHTML(definition.policy?.catchup_limit || '—')}`; + $('schedule-actions').hidden = false; text('schedule-control-button', schedule.paused ? '恢复计划' : '暂停计划'); text('schedule-action-note', schedule.paused ? '恢复会按冻结的漏跑策略继续处理暂停期间的触发点。' : '暂停只停止未来触发,已经创建的任务继续执行。'); + } + async function openScheduleDetail(id) { + if (!id) return; + if (!state.token) { showToast('请先连接具备 schedules:read 权限的凭据'); return; } + showScheduleDrawer(); text('schedule-detail-title', '正在读取计划…'); text('schedule-detail-id', shortID(id)); $('schedule-actions').hidden = true; $('schedule-occurrences').innerHTML = '
正在读取触发记录…
'; + const encoded = encodeURIComponent(id); const [scheduleResult, occurrencesResult] = await Promise.allSettled([request(`/v1/schedules/${encoded}`), request(`/v1/schedules/${encoded}/occurrences?limit=20`)]); + if (scheduleResult.status !== 'fulfilled') { text('schedule-detail-title', '计划读取失败'); text('schedule-action-note', scheduleResult.reason?.message || '无法读取计划详情'); return; } + renderScheduleDetail(extractData(scheduleResult.value)); renderScheduleOccurrences(occurrencesResult.status === 'fulfilled' ? extractItems(occurrencesResult.value) : [], occurrencesResult.status === 'rejected'); + } + function openScheduleAction() { + if (!state.selectedSchedule) return; + const paused = state.selectedSchedule.paused; $('schedule-action-form').hidden = false; $('schedule-action-error').textContent = ''; $('schedule-action-reason').value = ''; text('schedule-action-title', paused ? '恢复计划' : '暂停计划'); text('schedule-action-description', paused ? '恢复后会按计划冻结的漏跑和重叠策略继续处理。' : '暂停只停止未来触发,不取消已经创建的任务。'); $('schedule-action-reason').focus(); + } + async function submitScheduleAction(event) { + event.preventDefault(); if (!state.selectedSchedule) return; + const reason = $('schedule-action-reason').value.trim(); if (!reason) { $('schedule-action-error').textContent = '请填写操作原因。'; return; } + const paused = state.selectedSchedule.paused; const button = $('schedule-action-submit'); button.disabled = true; $('schedule-action-error').textContent = ''; + try { await request(`/v1/schedules/${encodeURIComponent(state.selectedSchedule.id)}:${paused ? 'resume' : 'pause'}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': operationKey(paused ? 'schedule-resume' : 'schedule-pause') }, body: JSON.stringify({ expected_revision: String(state.selectedSchedule.revision), reason }) }); showToast(paused ? '计划已恢复' : '计划已暂停'); closeScheduleAction(); await refresh(); await openScheduleDetail(state.selectedSchedule.id); } + catch (error) { $('schedule-action-error').textContent = error.message || '计划控制失败'; } + finally { button.disabled = false; } + } async function refresh() { if (state.loading) return; state.loading = true; $('refresh-button').classList.add('spinning'); try { @@ -178,7 +217,7 @@ 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); - $('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')); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); } }); + $('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)); })); diff --git a/internal/transport/httpserver/console/index.html b/internal/transport/httpserver/console/index.html index 568ceda..594b187 100644 --- a/internal/transport/httpserver/console/index.html +++ b/internal/transport/httpserver/console/index.html @@ -131,6 +131,34 @@ +
diff --git a/internal/transport/httpserver/console/styles.css b/internal/transport/httpserver/console/styles.css index 12b702d..ae9aa33 100644 --- a/internal/transport/httpserver/console/styles.css +++ b/internal/transport/httpserver/console/styles.css @@ -1,2 +1,3 @@ :root{--bg:#08111f;--bg-deep:#050b14;--panel:rgba(13,25,43,.78);--panel-strong:#101f34;--line:rgba(156,183,218,.13);--text:#eef5ff;--muted:#8d9db5;--faint:#5c6d85;--cyan:#62e6d4;--blue:#70a6ff;--purple:#b29aff;--orange:#ffb36b;--red:#ff788c;--radius:22px;--shadow:0 20px 70px rgba(0,0,0,.22);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Display","PingFang SC","Microsoft YaHei",sans-serif;color:var(--text);background:var(--bg)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:1100px;background:radial-gradient(circle at 72% -5%,#173a52 0,transparent 31rem),linear-gradient(145deg,var(--bg-deep),var(--bg) 58%,#071625);min-height:100vh}.ambient{position:fixed;border-radius:999px;filter:blur(110px);opacity:.14;pointer-events:none;z-index:0}.ambient-one{width:23rem;height:23rem;background:#3c8f9c;right:8%;top:8%}.ambient-two{width:19rem;height:19rem;background:#6651a8;left:13%;bottom:0}.app-shell{display:flex;position:relative;z-index:1;min-height:100vh}.sidebar{width:254px;position:fixed;inset:0 auto 0 0;padding:31px 19px 22px;border-right:1px solid var(--line);background:rgba(5,13,25,.72);backdrop-filter:blur(28px);display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:12px;padding:0 13px 42px}.brand strong{display:block;font-size:20px;letter-spacing:.02em}.brand small{display:block;margin-top:4px;color:var(--muted);font-size:11px;letter-spacing:.14em}.brand-mark{width:32px;height:32px;position:relative;border:1px solid rgba(98,230,212,.5);border-radius:11px;transform:rotate(30deg);display:flex;align-items:center;justify-content:center;gap:3px;box-shadow:0 0 26px rgba(98,230,212,.17)}.brand-mark span{width:3px;border-radius:3px;background:var(--cyan);transform:rotate(-30deg)}.brand-mark span:nth-child(1){height:9px;opacity:.45}.brand-mark span:nth-child(2){height:17px}.brand-mark span:nth-child(3){height:11px;opacity:.7}.workspace-label{padding:0 13px 13px;color:#53657e;font-size:10px;letter-spacing:.18em;font-weight:700}.nav-list{display:grid;gap:5px}.nav-item{width:100%;border:0;color:#9aabc2;background:transparent;border-radius:13px;display:flex;align-items:center;gap:12px;padding:12px 13px;font-size:13px;text-align:left;cursor:pointer;transition:.25s}.nav-item:hover{background:rgba(117,151,191,.09);color:#e7f3ff}.nav-item.active{color:var(--text);background:linear-gradient(100deg,rgba(72,173,177,.2),rgba(74,96,159,.09));box-shadow:inset 2px 0 0 var(--cyan)}.nav-icon{font-size:18px;width:20px;text-align:center;color:#7290ac}.nav-item.active .nav-icon{color:var(--cyan)}.nav-item em{font-style:normal;font-size:10px;color:#50637e;margin-left:auto}.sidebar-bottom{margin-top:auto}.rail-card{padding:16px;margin:0 4px 23px;border:1px solid var(--line);background:rgba(15,31,51,.62);border-radius:16px}.rail-card-top{display:flex;align-items:center;gap:8px;font-size:12px}.status-dot,.live-chip i,.live-label-small i{display:inline-block;width:7px;height:7px;border-radius:50%;background:#687991}.status-dot.ready,.live-chip.ready i,.live-label-small i{background:var(--cyan);box-shadow:0 0 0 4px rgba(98,230,212,.1),0 0 12px var(--cyan)}.rail-card p{margin:9px 0 13px;color:var(--faint);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rail-progress{height:3px;border-radius:9px;background:#253650;overflow:hidden}.rail-progress span{display:block;width:0;height:100%;background:linear-gradient(90deg,var(--cyan),var(--blue));border-radius:9px;transition:.6s}.sidebar-foot{color:#71829a;font-size:10px;letter-spacing:.05em;padding:0 9px}.pulse-dot{display:inline-block;width:5px;height:5px;background:var(--purple);border-radius:50%;margin-right:5px;vertical-align:1px}.foot-version{float:right;color:#4b5f79}.main-content{width:calc(100% - 254px);margin-left:254px;padding:0 48px 35px}.topbar{height:88px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(156,183,218,.08)}.crumbs{display:flex;gap:10px;align-items:center;color:#61758f;font-size:12px}.crumbs b{font-weight:400;color:#354862}.crumbs strong{font-weight:500;color:#b6c5d9}.topbar-actions{display:flex;align-items:center;gap:19px}.live-chip{display:flex;align-items:center;gap:7px;padding:7px 11px;background:rgba(99,129,156,.1);border:1px solid var(--line);border-radius:999px;color:#9eb1c7;font-size:11px}.last-updated{color:#61748f;font-size:11px}.icon-button{width:31px;height:31px;border:1px solid var(--line);background:rgba(128,157,195,.06);border-radius:9px;color:#9eb5ce;font-size:18px;cursor:pointer;transition:.2s}.icon-button:hover{background:rgba(98,230,212,.14);color:var(--cyan);transform:rotate(45deg)}.hero{min-height:330px;display:flex;position:relative;overflow:hidden;padding:52px 0 34px}.hero-copy{position:relative;z-index:2}.eyebrow,.panel-kicker{font-size:10px;color:#7995b2;letter-spacing:.2em;font-weight:700}.eyebrow{display:flex;align-items:center;gap:9px}.eyebrow-line{width:24px;height:1px;background:var(--cyan)}h1{font-size:48px;line-height:1.14;letter-spacing:-.055em;margin:18px 0 14px;font-weight:700}h1 span{color:transparent;background:linear-gradient(90deg,#eafcff,var(--cyan) 65%,#92b9ff);-webkit-background-clip:text;background-clip:text} .hero-copy p{max-width:410px;color:var(--muted);font-size:14px;line-height:1.8;margin:0}.hero-actions{display:flex;gap:13px;margin-top:27px}.primary-button,.quiet-button,.text-button{font:inherit;cursor:pointer}.primary-button{border:0;border-radius:10px;padding:11px 16px;color:#06212b;font-size:12px;font-weight:700;background:linear-gradient(120deg,var(--cyan),#9af2dc);box-shadow:0 9px 24px rgba(98,230,212,.16);transition:.2s}.primary-button:hover{transform:translateY(-2px);box-shadow:0 13px 32px rgba(98,230,212,.24)}.primary-button span{margin-left:13px}.primary-button.compact{padding:10px 15px}.quiet-button{border:1px solid var(--line);border-radius:10px;padding:10px 15px;color:#abc0d5;background:rgba(112,152,196,.08);font-size:12px}.quiet-button:hover{color:var(--text);border-color:rgba(98,230,212,.4)}.hero-visual{position:absolute;right:7%;top:18px;width:370px;height:300px}.orbit-ring{position:absolute;left:50%;top:50%;border:1px solid rgba(106,205,203,.24);border-radius:50%;transform:translate(-50%,-50%) rotate(-19deg)}.ring-one{width:230px;height:100px}.ring-two{width:314px;height:138px;border-color:rgba(112,166,255,.19);transform:translate(-50%,-50%) rotate(32deg)}.ring-three{width:360px;height:168px;border-color:rgba(178,154,255,.12);transform:translate(-50%,-50%) rotate(-47deg)}.orbit-core{position:absolute;width:86px;height:86px;left:50%;top:50%;transform:translate(-50%,-50%);display:grid;place-items:center;border-radius:50%;background:radial-gradient(circle at 35% 30%,#42cdbd,#145c68 44%,#0b293d);box-shadow:0 0 0 9px rgba(98,230,212,.06),0 0 55px rgba(98,230,212,.33)}.orbit-core span{font-weight:800;font-size:32px;color:#dafefa;letter-spacing:-.12em;margin-left:-4px}.orbit-node{position:absolute;width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 16px var(--cyan)}.node-a{left:34px;top:111px}.node-b{right:43px;top:75px;background:var(--purple);box-shadow:0 0 16px var(--purple)}.node-c{right:91px;bottom:45px;background:var(--orange);box-shadow:0 0 16px var(--orange)}.hero-visual-label{position:absolute;right:7px;bottom:12px;display:flex;flex-direction:column;gap:4px;text-align:right}.hero-visual-label b{font-size:10px;letter-spacing:.15em;color:var(--cyan)}.hero-visual-label span{color:#627894;font-size:10px}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:17px}.metric-card,.panel{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(145deg,rgba(19,37,61,.8),rgba(10,24,42,.74));box-shadow:var(--shadow)}.metric-card{position:relative;min-height:146px;padding:19px 20px;overflow:hidden}.metric-card.metric-highlight{background:linear-gradient(145deg,rgba(24,66,73,.72),rgba(12,35,49,.86));border-color:rgba(98,230,212,.17)}.metric-top{display:flex;justify-content:space-between;align-items:center;color:#8598b0;font-size:11px}.metric-icon{width:24px;height:24px;display:grid;place-items:center;border-radius:8px;font-size:15px;background:rgba(255,255,255,.05)}.metric-icon.green{color:var(--cyan);background:rgba(98,230,212,.1)}.metric-icon.purple{color:var(--purple);background:rgba(178,154,255,.1)}.metric-icon.blue{color:var(--blue);background:rgba(112,166,255,.1)}.metric-icon.orange{color:var(--orange);background:rgba(255,179,107,.1)}.metric-card strong{display:block;margin-top:14px;font-size:28px;letter-spacing:-.04em}.metric-card small{display:block;margin-top:5px;color:#71859f;font-size:10px}.sparkline{display:flex;align-items:end;gap:3px;height:18px;position:absolute;right:20px;bottom:21px}.sparkline i{display:block;width:3px;border-radius:3px;background:currentColor;opacity:.55}.sparkline i:nth-child(1){height:7px}.sparkline i:nth-child(2){height:12px}.sparkline i:nth-child(3){height:9px}.sparkline i:nth-child(4){height:16px}.sparkline i:nth-child(5){height:11px}.sparkline i:nth-child(6){height:17px}.sparkline i:nth-child(7){height:14px}.sparkline i:nth-child(8){height:18px}.green-line{color:var(--cyan)}.blue-line{color:var(--blue)}.mini-bar{display:flex;gap:2px;height:4px;position:absolute;left:20px;right:20px;bottom:20px;background:#1c304a;border-radius:4px;overflow:hidden}.mini-bar i{width:0;transition:.45s}.mini-bar i:first-child{background:var(--cyan)}.mini-bar i:nth-child(2){background:var(--blue)}.mini-bar i:nth-child(3){background:var(--purple)}.mini-bar i:nth-child(4){background:var(--orange)}.subscription-meter{height:4px;position:absolute;left:20px;right:20px;bottom:20px;background:#1d3049;border-radius:4px;overflow:hidden}.subscription-meter span{display:block;width:0;height:100%;background:linear-gradient(90deg,var(--orange),var(--purple));transition:.45s}.content-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(300px,.75fr);gap:17px;margin-bottom:17px}.panel{padding:23px}.panel-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:15px;margin-bottom:22px}.panel-kicker{display:block;color:#687f9c;font-size:9px;margin-bottom:8px}.panel h2{margin:0;font-size:17px;letter-spacing:-.025em}.panel-tools{display:flex;align-items:center;gap:12px}.table-count,.connection-state{font-size:10px;color:#6d829d}.text-button{border:0;background:none;color:#87a5c3;font-size:11px;padding:4px 0}.text-button:hover{color:var(--cyan)}.text-button span{margin-left:6px}.table-wrap{overflow:auto}.table-wrap table{width:100%;border-collapse:collapse;min-width:540px}.table-wrap th{padding:0 10px 11px;text-align:left;color:#637896;font-size:10px;font-weight:500;border-bottom:1px solid var(--line)}.table-wrap td{padding:14px 10px;color:#b2c1d3;font-size:11px;border-bottom:1px solid rgba(156,183,218,.07)}.table-wrap tr:last-child td{border-bottom:0}.task-id{display:flex;flex-direction:column;gap:4px}.task-id b{color:#e2edf9;font-weight:550}.task-id small,.task-processor{color:#6c819b;font-size:10px}.state-pill{display:inline-flex;padding:4px 8px;border-radius:999px;background:rgba(110,140,170,.12);font-size:9px;color:#aebfd2}.state-pill.succeeded{color:var(--cyan);background:rgba(98,230,212,.1)}.state-pill.running{color:var(--blue);background:rgba(112,166,255,.1)}.state-pill.waiting,.state-pill.pending{color:var(--purple);background:rgba(178,154,255,.1)}.state-pill.failed,.state-pill.needs_review{color:var(--red);background:rgba(255,120,140,.1)}.empty-row{text-align:center!important;padding:38px 10px!important}.empty-row b{display:block;margin:7px 0 4px;color:#9aacbf;font-weight:500}.empty-row small{color:#627791}.empty-icon{display:grid;margin:auto;width:36px;height:36px;place-items:center;border-radius:12px;background:rgba(112,166,255,.08);color:var(--blue);font-size:20px}.connect-panel{background:linear-gradient(155deg,rgba(24,40,64,.9),rgba(12,24,41,.86))}.connection-state{border:1px solid rgba(255,179,107,.25);color:var(--orange);padding:5px 8px;border-radius:999px}.connection-state.ready{color:var(--cyan);border-color:rgba(98,230,212,.25);background:rgba(98,230,212,.07)}.panel-description{color:#73869e;font-size:11px;line-height:1.6;margin:-7px 0 19px}.field-label{display:block;color:#8295ad;font-size:10px;margin:13px 0 7px}.input-wrap{height:38px;display:flex;align-items:center;border:1px solid rgba(156,183,218,.15);background:rgba(4,12,23,.35);border-radius:10px;transition:.2s}.input-wrap:focus-within{border-color:rgba(98,230,212,.58);box-shadow:0 0 0 3px rgba(98,230,212,.07)}.input-prefix{width:32px;text-align:center;color:#58728e;font-size:13px}.input-wrap input{width:100%;height:100%;border:0;outline:0;background:none;color:#dceafb;font:inherit;font-size:11px}.input-wrap input::placeholder{color:#4f637c}.reveal-button{border:0;background:none;color:#6e879f;font-size:10px;cursor:pointer;margin-right:10px}.connect-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:18px}.file-button{color:#7f9ab6;font-size:10px;border:1px dashed rgba(126,158,193,.3);padding:10px 11px;border-radius:9px;cursor:pointer;white-space:nowrap}.file-button:hover{color:var(--cyan);border-color:rgba(98,230,212,.5)}.file-button input{display:none}.security-note{display:flex;gap:7px;margin-top:20px;color:#647b96;font-size:9px;line-height:1.55}.security-note span:first-child{color:var(--cyan)}.lower-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:17px}.schedule-list,.workflow-list,.subscription-list{display:grid;gap:10px}.empty-compact{display:flex;align-items:center;gap:12px;border:1px dashed rgba(126,158,193,.16);border-radius:13px;padding:18px 14px;color:#5e748f}.empty-compact>span{font-size:22px;color:#7899bc}.empty-compact b{display:block;color:#9aacc0;font-size:11px;font-weight:500}.empty-compact small{display:block;margin-top:5px;color:#607590;font-size:9px}.list-row{display:flex;align-items:center;gap:11px;padding:10px 0;border-bottom:1px solid rgba(156,183,218,.08)}.list-row:last-child{border-bottom:0}.list-avatar{width:29px;height:29px;border-radius:10px;display:grid;place-items:center;background:rgba(112,166,255,.1);color:var(--blue);font-size:13px}.list-copy{min-width:0;flex:1}.list-copy b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#c6d4e4;font-size:11px;font-weight:500}.list-copy small{display:block;margin-top:4px;color:#657b95;font-size:9px}.list-meta{text-align:right;color:#7990a9;font-size:10px}.live-label-small{display:flex;align-items:center;gap:6px;color:#7592aa;font-size:10px}.page-footer{display:flex;justify-content:space-between;padding:26px 3px 0;color:#526985;font-size:10px}.page-footer a{color:#80a7ca;text-decoration:none}.page-footer a:hover{color:var(--cyan)}.footer-orbit{color:var(--cyan);font-size:13px;margin-right:6px}.toast{position:fixed;right:28px;bottom:25px;z-index:5;transform:translateY(20px);opacity:0;pointer-events:none;padding:11px 15px;border:1px solid rgba(98,230,212,.25);background:#122b3b;color:#c9faf2;border-radius:10px;box-shadow:var(--shadow);font-size:11px;transition:.25s}.toast.show{transform:translateY(0);opacity:1}@media(max-width:1200px){body{min-width:0}.main-content{padding:0 28px}.hero-visual{right:0;opacity:.65}.lower-grid{grid-template-columns:1fr 1fr}.lower-grid .panel:last-child{grid-column:span 2}}@media(max-width:780px){.sidebar{width:70px;padding:24px 10px}.brand{padding:0 8px 32px}.brand>div:last-child,.workspace-label,.nav-item span:not(.nav-icon),.nav-item em,.sidebar-bottom{display:none}.nav-item{justify-content:center;padding:12px}.main-content{width:calc(100% - 70px);margin-left:70px;padding:0 17px}.topbar{height:70px}.last-updated{display:none}.hero{min-height:300px}.hero-visual{right:-120px;opacity:.35}h1{font-size:38px}.metric-grid{grid-template-columns:1fr 1fr}.content-grid,.lower-grid{grid-template-columns:1fr}.lower-grid .panel:last-child{grid-column:auto}} [hidden]{display:none!important}.task-row{cursor:pointer;transition:background .2s}.task-row:hover,.task-row:focus{outline:0;background:rgba(98,230,212,.055)}.task-row:focus td:first-child{box-shadow:inset 2px 0 0 var(--cyan)}.drawer-open{overflow:hidden}.drawer-backdrop{position:fixed;inset:0;z-index:20;background:rgba(2,8,16,.68);backdrop-filter:blur(8px)}.task-drawer{position:absolute;right:0;top:0;height:100%;width:min(570px,100%);overflow-y:auto;padding:29px 28px 34px;background:linear-gradient(165deg,#122943,#091828 70%);border-left:1px solid rgba(98,230,212,.18);box-shadow:-22px 0 70px rgba(0,0,0,.35);animation:drawer-in .24s ease-out}@keyframes drawer-in{from{transform:translateX(35px);opacity:.4}to{transform:translateX(0);opacity:1}}.drawer-header{display:flex;justify-content:space-between;align-items:flex-start;gap:20px}.drawer-header h2{margin:0 0 8px;font-size:22px;letter-spacing:-.03em}.drawer-id{display:block;max-width:390px;color:#6f87a2;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.drawer-status-line{display:flex;align-items:center;gap:11px;margin:22px 0 19px;color:#748aa4;font-size:10px}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;border:1px solid var(--line);border-radius:15px;overflow:hidden;margin-bottom:18px}.detail-grid>div{padding:13px 14px;background:rgba(5,15,27,.34);min-width:0}.detail-grid span{display:block;color:#637b96;font-size:9px;margin-bottom:6px}.detail-grid b{display:block;color:#d4e2f1;font-size:11px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.detail-block{border-top:1px solid rgba(156,183,218,.1);padding-top:19px;margin-top:19px}.detail-block-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;color:#b4c7db;font-size:12px}.detail-block-heading small{color:#617995;font-size:9px}.detail-list{display:grid;gap:0}.detail-list-row{position:relative;display:flex;align-items:center;gap:11px;padding:11px 0;border-bottom:1px solid rgba(156,183,218,.08)}.detail-list-row:last-child{border-bottom:0}.detail-list-row>div{min-width:0;flex:1}.detail-list-row b{display:block;color:#bdcfe1;font-size:10px;font-weight:500}.detail-list-row small{display:block;margin-top:4px;color:#6d829c;font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.detail-list-row em{font-style:normal;color:#778da7;font-size:9px}.timeline-dot{width:8px;height:8px;flex:0 0 8px;border-radius:50%;background:#657991;box-shadow:0 0 0 4px rgba(101,121,145,.1)}.timeline-dot.succeeded{background:var(--cyan);box-shadow:0 0 0 4px rgba(98,230,212,.1)}.timeline-dot.running{background:var(--blue);box-shadow:0 0 0 4px rgba(112,166,255,.1)}.timeline-dot.failed,.timeline-dot.unknown{background:var(--red);box-shadow:0 0 0 4px rgba(255,120,140,.1)}.timeline-dot.audit{background:var(--purple);box-shadow:0 0 0 4px rgba(178,154,255,.1)}.detail-loading,.detail-empty{padding:17px 0;color:#607895;font-size:10px}.detail-block pre{max-height:180px;overflow:auto;margin:0;padding:12px;border:1px solid rgba(156,183,218,.1);border-radius:11px;background:rgba(4,11,20,.48);color:#a7e9df;font:10px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word}.drawer-actions{display:flex;gap:10px;margin-top:23px;padding-top:19px;border-top:1px solid rgba(156,183,218,.1)}.danger-button{color:#ff9baa;border-color:rgba(255,120,140,.25);background:rgba(255,120,140,.07)}.danger-button:hover{color:#ffd5db;border-color:rgba(255,120,140,.5)}.drawer-note{color:#687f99;font-size:9px;line-height:1.7;margin:15px 0 0}.action-form{margin-top:21px;padding:19px;border:1px solid rgba(98,230,212,.17);border-radius:16px;background:rgba(4,14,25,.45)}.action-form-heading h3{margin:0;color:#d7e6f4;font-size:15px}.action-form p{color:#7890a9;font-size:10px;line-height:1.6;margin:10px 0 4px}.action-form textarea,.action-form input{width:100%;border:1px solid rgba(156,183,218,.15);outline:0;border-radius:9px;background:rgba(4,12,23,.45);color:#dceafb;font:inherit;font-size:10px;padding:10px;resize:vertical}.action-form textarea:focus,.action-form input:focus{border-color:rgba(98,230,212,.58);box-shadow:0 0 0 3px rgba(98,230,212,.07)}.check-row{display:flex;gap:9px;align-items:flex-start;margin-top:15px;color:#9eb3c9;font-size:10px;line-height:1.5}.check-row input{accent-color:var(--cyan);margin-top:2px}.action-form-buttons{display:flex;justify-content:flex-end;gap:9px;margin-top:18px}.action-error{min-height:15px;color:#ff9baa;font-size:10px;margin-top:9px}.action-form .quiet-button{padding:10px 14px}@media(max-width:780px){.task-drawer{width:100%;padding:24px 18px 28px}.detail-grid{grid-template-columns:1fr 1fr}} +.schedule-row{cursor:pointer;transition:background .2s;border-radius:10px;padding-left:7px;padding-right:7px;margin-left:-7px;margin-right:-7px}.schedule-row:hover,.schedule-row:focus{outline:0;background:rgba(98,230,212,.055)}.schedule-rule-card{padding:14px;border:1px solid rgba(156,183,218,.1);border-radius:12px;background:rgba(4,12,23,.38)}.schedule-rule-card b{display:block;color:#c8e5e6;font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-word}.schedule-rule-card small{display:block;margin-top:7px;color:#7189a1;font-size:9px}.timeline-dot.fired{background:var(--cyan);box-shadow:0 0 0 4px rgba(98,230,212,.1)}.timeline-dot.coalesced{background:var(--purple);box-shadow:0 0 0 4px rgba(178,154,255,.1)}.timeline-dot.skipped{background:var(--orange);box-shadow:0 0 0 4px rgba(255,179,107,.1)}