From 45a2b361f288e6962b8115c760d966ad43ea4ef9 Mon Sep 17 00:00:00 2001 From: k2safe Date: Mon, 21 Sep 2026 03:49:38 +0800 Subject: [PATCH] feat: auto configure console queue --- docs/21-container-deployment.md | 3 ++ docs/console.md | 12 ++++++-- internal/transport/httpserver/console.go | 12 ++++++++ internal/transport/httpserver/console/app.js | 30 +++++++++++++++---- .../transport/httpserver/console/index.html | 4 +-- .../transport/httpserver/console/styles.css | 2 +- internal/transport/httpserver/console_test.go | 24 +++++++++++++++ 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/docs/21-container-deployment.md b/docs/21-container-deployment.md index 69413bb..d10bc5e 100644 --- a/docs/21-container-deployment.md +++ b/docs/21-container-deployment.md @@ -65,6 +65,7 @@ docker run --rm \ docker run --rm \ --env ORBIT_MYSQL_DSN \ --env ORBIT_HTTP_ADDR=0.0.0.0:8080 \ + --env ORBIT_CONSOLE_QUEUE_ID=queue_xxx \ --env ORBIT_RUNTIME_BATCH_LIMIT=100 \ --env ORBIT_WORKFLOW_TICK_INTERVAL=1s \ --env ORBIT_SCHEDULE_TICK_INTERVAL=1s \ @@ -79,6 +80,8 @@ docker run --rm \ registry.example/orbit:dev ``` +`ORBIT_CONSOLE_QUEUE_ID` 是控制台的可选默认队列范围,只用于打开 `/console/` 时预填队列 ID,不是认证凭据。服务端仍要求业务请求携带有权限的 Bearer Token;多队列部署可以省略此变量,改为在浏览器选择凭据 JSON,由其中的 `queue_id` 自动填充。 + 生产编排应先完成迁移和 `verify`,再启动新节点。服务收到 `SIGTERM` 后停止接收新连接并在有限窗口内关闭 HTTP、WebSocket 和后台循环;编排器的终止宽限期必须覆盖该窗口。启动后才能把节点加入流量池。 ## 3. 探针和监听边界 diff --git a/docs/console.md b/docs/console.md index c5c72dc..f50b8f5 100644 --- a/docs/console.md +++ b/docs/console.md @@ -9,7 +9,7 @@ http://127.0.0.1:8080/console/ ## 当前能力 - 读取 `/health/live`、`/health/ready` 和 `/version` 展示服务、Schema 与工具链状态。 -- 通过用户选择的本地凭据 JSON 或手工 Token/队列 ID 调用现有授权 API。 +- 通过用户选择的本地凭据 JSON 或手工 Token/队列范围调用现有授权 API;队列范围优先从凭据 JSON 或服务端默认配置自动填充。 - 展示任务、定时计划、流程实例和结果订阅的最近摘要。 - 点击任务查看任务详情、执行尝试时间线、最终结果和操作审计摘要。 - 在具备对应权限时提交任务取消和安全重试,沿用服务端的版本校验、幂等键、队列范围和审计规则。 @@ -31,7 +31,15 @@ http://127.0.0.1:8080/console/ http://127.0.0.1:8080/console/ ``` -点击“选择凭据 JSON”,选择 `orbit-admin bootstrap` 生成的私有凭据文件,例如 `.local/local.credentials.json`,再点击“连接”。文件只在浏览器内解析,Token 保留在当前页面会话中,不写入 Orbit 数据库。 +启动 Orbit 后,API 地址默认使用当前控制台页面所在的地址,不需要手工填写。点击“选择凭据 JSON”,选择 `orbit-admin bootstrap` 生成的私有凭据文件,例如 `.local/local.credentials.json`,队列范围和 Token 会自动读取,再点击“连接”。浏览器不能在没有用户选择的情况下读取本机文件,这是浏览器的安全边界;文件只在当前页面内解析,Token 不会上传到 Orbit Console,也不写入 Orbit 数据库。 + +如果一个部署只服务一个默认队列,可以在服务端配置队列范围,页面打开后会自动使用它: + +```sh +export ORBIT_CONSOLE_QUEUE_ID='queue_xxx' +``` + +`GET /console/config` 只返回这个非敏感的默认队列 ID,不返回 Token、不读取数据库。凭据 JSON 中的 `queue_id` 优先级更高,可以覆盖默认值。没有队列配置时仍可查看健康和版本;读取任务、计划、流程及订阅需要同时提供 Bearer Token 和队列范围。平台不会根据 Token 猜测队列,因为一个 Token 可能被授予多个队列,自动猜测会造成数据范围错误。 ## 安全边界 diff --git a/internal/transport/httpserver/console.go b/internal/transport/httpserver/console.go index d1d5692..ceae11d 100644 --- a/internal/transport/httpserver/console.go +++ b/internal/transport/httpserver/console.go @@ -2,8 +2,10 @@ package httpserver import ( "embed" + "encoding/json" "io/fs" "net/http" + "os" ) // consoleAssets 将控制台静态资源编译进 Orbit,避免运行容器依赖 Node.js、外部 CDN @@ -30,6 +32,16 @@ func RegisterConsole(server *http.Server) { mux.HandleFunc("GET /console", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/console/", http.StatusPermanentRedirect) }) + mux.HandleFunc("GET /console/config", func(w http.ResponseWriter, r *http.Request) { + // 队列 ID 只是控制台的默认范围,不是凭据。Token 仍由每个授权 API + // 自己校验;这里不返回任何秘密,也不访问数据库。 + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Content-Type-Options", "nosniff") + _ = json.NewEncoder(w).Encode(struct { + DefaultQueueID string `json:"default_queue_id"` + }{DefaultQueueID: os.Getenv("ORBIT_CONSOLE_QUEUE_ID")}) + }) mux.Handle("GET /console/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") diff --git a/internal/transport/httpserver/console/app.js b/internal/transport/httpserver/console/app.js index 7df09f0..7908663 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: '', selectedSchedule: null, selectedWorkflow: null, workflowActionMode: '', workflowRecoveryMode: '', workflowRecoveryContext: null, selectedSubscription: null, subscriptionActionMode: '', selectedDelivery: null }; + const state = { token: '', queue: '', base: location.origin, consoleConfig: 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: '结果订阅' }; @@ -41,6 +41,22 @@ if (!response.ok) throw new Error(body?.error?.message || body?.message || `HTTP ${response.status}`); return body; } + async function loadConsoleConfig() { + try { + const response = await fetch('/console/config', { headers: { Accept: 'application/json' }, cache: 'no-store' }); + if (!response.ok) return; + state.consoleConfig = await response.json(); + const defaultQueue = String(state.consoleConfig?.default_queue_id || '').trim(); + if (!state.queue && defaultQueue) { + state.queue = defaultQueue; + $('queue-id').value = defaultQueue; + sessionStorage.setItem('orbit.console.queue', defaultQueue); + text('queue-hint', '已使用服务端默认队列配置;也可选择凭据 JSON 覆盖。'); + } + } catch { + // 连接表单仍可使用手工 API 地址和凭据,不因可选配置失败而阻断页面。 + } + } function setConnection(connected, detail = '') { state.connected = connected; const authenticated = connected && Boolean(state.token && state.queue); @@ -412,17 +428,21 @@ 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(); + state.base = $('api-url').value.trim().replace(/\/$/, '') || location.origin; state.token = $('token').value.trim(); state.queue = $('queue-id').value.trim() || String(state.consoleConfig?.default_queue_id || '').trim(); + $('queue-id').value = state.queue; + sessionStorage.setItem('orbit.console.queue', state.queue); + if (!state.token) showToast('服务状态可以直接查看;选择凭据 JSON 后可读取业务数据'); + else if (!state.queue) showToast('队列会从凭据 JSON 自动读取,也可配置 ORBIT_CONSOLE_QUEUE_ID'); + 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')); $('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); $('close-workflow-drawer').addEventListener('click', closeWorkflowDrawer); $('workflow-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'workflow-drawer-backdrop') closeWorkflowDrawer(); }); $('workflow-control-button').addEventListener('click', () => openWorkflowAction(state.selectedWorkflow?.paused ? 'RESUME' : 'PAUSE')); $('workflow-cancel-button').addEventListener('click', () => openWorkflowAction('CANCEL')); $('workflow-action-cancel').addEventListener('click', closeWorkflowAction); $('workflow-action-form').addEventListener('submit', submitWorkflowAction); $('close-subscription-drawer').addEventListener('click', closeSubscriptionDrawer); $('subscription-drawer-backdrop').addEventListener('click', event => { if (event.target.id === 'subscription-drawer-backdrop') closeSubscriptionDrawer(); }); $('subscription-control-button').addEventListener('click', openSubscriptionAction); $('subscription-action-cancel').addEventListener('click', closeSubscriptionAction); $('subscription-action-form').addEventListener('submit', submitSubscriptionAction); $('subscription-retry-cancel').addEventListener('click', closeSubscriptionRetry); $('subscription-retry-form').addEventListener('submit', submitSubscriptionRetry); document.addEventListener('keydown', event => { if (event.key === 'Escape') { if (!$('task-action-form').hidden) closeActionForm(); else if (!$('schedule-action-form').hidden) closeScheduleAction(); else if (!$('workflow-action-form').hidden) closeWorkflowAction(); else if (!$('subscription-action-form').hidden) closeSubscriptionAction(); else if (!$('subscription-retry-form').hidden) closeSubscriptionRetry(); else if (!$('task-drawer-backdrop').hidden) closeTaskDrawer(); else if (!$('schedule-drawer-backdrop').hidden) closeScheduleDrawer(); else if (!$('workflow-drawer-backdrop').hidden) closeWorkflowDrawer(); else if (!$('subscription-drawer-backdrop').hidden) closeSubscriptionDrawer(); } }); $('workflow-recovery-actions').addEventListener('click', event => { const button = event.target.closest('[data-recovery-action]'); if (!button) return; let context = {}; try { context = JSON.parse(button.dataset.recoveryContext || '{}'); } catch { showToast('恢复按钮上下文无效,请重新读取流程'); return; } openWorkflowRecovery(button.dataset.recoveryAction, context); }); $('workflow-recovery-cancel').addEventListener('click', closeWorkflowRecovery); $('workflow-recovery-form').addEventListener('submit', submitWorkflowRecovery); document.addEventListener('keydown', event => { if (event.key === 'Escape' && !$('workflow-recovery-form').hidden) closeWorkflowRecovery(); }); $('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); }); + $('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); const queue = String(credential.queue_id || state.consoleConfig?.default_queue_id || '').trim(); $('token').value = credential.token || ''; $('queue-id').value = queue; state.queue = queue; sessionStorage.setItem('orbit.console.queue', queue); text('queue-hint', queue ? '队列已从凭据 JSON 自动读取;无需手工填写。' : '凭据未附带队列,请配置 ORBIT_CONSOLE_QUEUE_ID。'); showToast(queue ? '凭据已读取,队列已自动填充' : '凭据已读取,但缺少队列配置'); } catch { showToast('凭据 JSON 无法解析'); } }; reader.readAsText(file); }); 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); + loadConsoleConfig().finally(() => refresh()); state.timer = setInterval(refresh, 10000); })(); diff --git a/internal/transport/httpserver/console/index.html b/internal/transport/httpserver/console/index.html index 7347d3a..740bf9b 100644 --- a/internal/transport/httpserver/console/index.html +++ b/internal/transport/httpserver/console/index.html @@ -76,8 +76,8 @@
ENVIRONMENT

连接环境

未连接

凭据只在当前浏览器会话中使用,不会上传到 Orbit Console。

-
↗
-
#
+
↗
+
#
选择凭据 JSON 后自动填充;也可由服务端配置默认队列。
●
▣当前页面只调用公开 API 和授权接口,不读取数据库。
diff --git a/internal/transport/httpserver/console/styles.css b/internal/transport/httpserver/console/styles.css index 4af7e51..9a14f12 100644 --- a/internal/transport/httpserver/console/styles.css +++ b/internal/transport/httpserver/console/styles.css @@ -1,4 +1,4 @@ -: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}} +: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}.field-label small{margin-left:6px;color:#62cfc5;font-size:9px;font-weight:500}.field-hint{margin:-2px 2px 0;color:#607791;font-size:9px;line-height:1.5}.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)} .page-view{display:none}.page-view.active{display:block}.content-grid.single-column{grid-template-columns:1fr}.page-view>.content-grid{min-height:360px}.page-view>.content-grid>.panel{min-height:330px}.page-view>.content-grid>.wide-panel{min-height:420px} diff --git a/internal/transport/httpserver/console_test.go b/internal/transport/httpserver/console_test.go index 29762c9..7e4fb32 100644 --- a/internal/transport/httpserver/console_test.go +++ b/internal/transport/httpserver/console_test.go @@ -1,6 +1,7 @@ package httpserver import ( + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -32,3 +33,26 @@ func TestConsoleRedirectsWithoutTouchingAPIAuthentication(t *testing.T) { t.Fatalf("redirect = %d %q", w.Code, w.Header().Get("Location")) } } + +func TestConsoleConfigReadsOnlyDefaultQueue(t *testing.T) { + t.Setenv("ORBIT_CONSOLE_QUEUE_ID", "queue-default") + server := New("127.0.0.1:0", nil) + r := httptest.NewRequest(http.MethodGet, "/console/config", nil) + w := httptest.NewRecorder() + server.Handler.ServeHTTP(w, r) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", w.Code) + } + if got := w.Header().Get("Cache-Control"); got != "no-store" { + t.Fatalf("cache control = %q, want no-store", got) + } + var payload struct { + DefaultQueueID string `json:"default_queue_id"` + } + if err := json.NewDecoder(w.Body).Decode(&payload); err != nil { + t.Fatalf("decode config: %v", err) + } + if payload.DefaultQueueID != "queue-default" { + t.Fatalf("default queue = %q, want queue-default", payload.DefaultQueueID) + } +}