From 47bcc41506403c03d8a8217410cab826821aae74 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 9 Sep 2026 06:16:12 +0800 Subject: [PATCH] feat(console): organize daily operations around account overview Co-Authored-By: Codex --- docs/console_information_design.zh-CN.md | 28 ++ tests/console_runtime_state_validation.mjs | 88 ++++- tests/strategy_switch_worker_validation.mjs | 8 +- web/strategy-switch-console/app.css | 194 +++++++++- web/strategy-switch-console/app.js | 383 +++++++++++++++++--- web/strategy-switch-console/app_css.js | 2 +- web/strategy-switch-console/app_js.js | 2 +- web/strategy-switch-console/index.html | 68 +++- web/strategy-switch-console/page_asset.js | 2 +- 9 files changed, 697 insertions(+), 78 deletions(-) create mode 100644 docs/console_information_design.zh-CN.md diff --git a/docs/console_information_design.zh-CN.md b/docs/console_information_design.zh-CN.md new file mode 100644 index 0000000..c93c4e1 --- /dev/null +++ b/docs/console_information_design.zh-CN.md @@ -0,0 +1,28 @@ +# 日常管理控制台 + +本次只调整信息组织和操作呈现,保留现有账户、策略权限及确认流程,不增加服务、依赖或费用。 + +## 日常需要回答的问题 + +| 页面 | 主要内容 | 次级内容 | +| --- | --- | --- | +| 运行总览(默认) | 全部已配置账户、配置开关、实际读回、记录时间;明确待确认和不一致 | 筛选、搜索、进入指定账户;确实需要人工处理的事项 | +| 账户管理 | 选择平台和账户;查看当前策略、保存状态、实际开关和调度 | 展开策略设置后调整;高级资金参数和提交前核对继续保留 | +| 研究与确认 | 选择意向平台,查看待确认候选和确认结果 | 风险解释、研究记录、诊断详情按需展开 | + +不把研究时间当作页面刷新时间;不把“配置启用”当作“运行正常”。读回缺失、过期或未明确关联账户时,保持待确认。记录时间明确表示读回时间,不冒充成交时间或最新成功周期。首页不展示原始 ticket、SHA、JSON、设计预览风险数据,也不伪造收益、余额或健康状态。 + +研究数据仍保留,移到单独页面;现有实盘恢复、停用、策略切换和研究确认保持各自的权限与提交保护。查看、搜索、筛选、刷新均为只读操作。 + +## 布局与文案 + +- 电脑:固定窄侧栏,主区域约 1120 px;四项配置统计、紧凑状态提示、账户列表。 +- 手机:顶部三项导航;统计为两列;账户列表变为纵向信息块,不依赖横向滚动;账户详情同样纵向排列。 +- 导航固定为“运行总览 / 账户管理 / 研究与确认”。首页标题“运行总览”,说明“查看已保存的配置、实际读回与最近运行记录。”。 +- 状态与操作均有中英文;每项操作使用语义化按钮和清晰焦点。主要触控目标至少 44 px。 +- 配色:背景 `#f6f7f9`、正文 `#172d35`、辅助文字 `#667680`、主色 `#10766a`、边线 `#dfe6ea`;待确认用中性色,不自动标成故障;配置不一致用浅琥珀色。 +- 字体沿用系统无衬线字体;主标题约 32 px,正文 14–15 px;手机主标题 26 px。间距以 8 px 为基本单位,圆角约 8 px,不使用装饰图表。 + +## 交互与验证 + +验证默认总览、准确定位账户、配置/实际状态分离、搜索和筛选、空结果、读取失败、刷新时间、研究切页、折叠详情、语言切换以及电脑/手机布局。线上仅检查读取和导航;提交类操作在隔离测试数据中验证,不能为了页面测试触发实盘。 diff --git a/tests/console_runtime_state_validation.mjs b/tests/console_runtime_state_validation.mjs index 5839223..bb40cf5 100644 --- a/tests/console_runtime_state_validation.mjs +++ b/tests/console_runtime_state_validation.mjs @@ -356,12 +356,13 @@ for (const sample of [ assert.equal(fn('ibkr',{}),sample.expected); }); -test('account overview exposes desired/applied/application columns and per-row application status', () => { +test('account details keep saved, deployed and application state separate', () => { const html = readFileSync(new URL('../web/strategy-switch-console/index.html', import.meta.url), 'utf8'); const app = readFileSync(new URL('../web/strategy-switch-console/app.js', import.meta.url), 'utf8'); assert.ok(html.includes('data-i18n="configuredSwitch"')); - assert.ok(html.includes('data-i18n="deployedSwitch"')); - assert.ok(html.includes('data-i18n="applicationStatus"')); + assert.ok(html.includes('class="account-facts"')); + assert.ok(app.includes('["deployedSwitch", accountDeploymentText(platform, account)]')); + assert.ok(app.includes('["applicationStatus", accountApplicationText(platform, account)]')); const overview = app.slice(app.indexOf('function renderAccountOverview'), app.indexOf('function renderControls')); assert.ok(overview.includes('currentRuntimeTargetText(platform, account)')); assert.ok(overview.includes('accountDeploymentText(platform, account)')); @@ -552,6 +553,7 @@ test('opening account settings selects the exact account without submitting or e const fn = frontendFunction('openAccountSettings', { state, el: id => nodes[id], render: () => events.push('render'), Event }); fn('ibkr', { key: 'second-account' }); assert.equal(state.selected, 'ibkr'); + assert.equal(state.view, 'accounts'); assert.equal(nodes['account-select'].value, 'second-account'); assert.equal(nodes['strategy-settings'].open, true); assert.equal(state.forms.ibkr.runtimeTargetMode, 'current'); @@ -568,7 +570,7 @@ test('decisions and account observations stay outside collapsed strategy setting } assert.ok(html.indexOf('id="promotion-decision-panel"') < html.indexOf('id="account-overview"')); assert.ok(html.indexOf('id="quick-form"') > settings); - assert.ok(html.includes('data-i18n="accountNextStep"')); + assert.ok(source.includes('["accountDetailAction", accountNextStep(platform, account).label]')); assert.equal((html.match(/id="dispatch-button"/g) || []).length, 1); }); @@ -625,3 +627,81 @@ test('hiding all platforms also hides account observations outside the settings platformMeta: { ibkr: { console_visible: false } }, hasPrivateConfig: () => true })(); assert.equal(nodes['switch-view'].hidden, true); }); + +for (const sample of [ + { configured: undefined, application: 'deploymentUnverified', unknown: true, attention: true }, + { configured: true, application: 'deploymentUnverified', unknown: true, attention: true }, + { configured: false, application: 'deploymentUnverified', unknown: true, attention: true }, + { configured: true, application: 'settingsNotApplied', unknown: false, attention: true }, + { configured: false, application: 'switchesApplied', unknown: false, attention: false }, + { configured: true, application: 'switchesApplied', observation: 'attention', unknown: false, attention: true }, +]) test('overview keeps saved settings separate from actual state '+JSON.stringify(sample), () => { + const context = { + hasPrivateConfig: () => true, + platformMeta: { binance: { label: 'Binance' }, hidden: { console_visible: false } }, + optionsFor: () => [{ key: 'account', label: 'Account' }], + currentStrategyForAccount: () => 'example', strategyLabel: () => 'Example', + runtimeTargetStateForAccount: () => ({ known: sample.configured !== undefined, enabled: sample.configured }), + accountApplicationText: () => sample.application, + accountMonitoringRecord: () => ({ freshness: { data_status: 'ready' }, execution_observation: { code: sample.observation } }), + accountMonitoringAge: () => '1 minute ago', accountMonitoringText: () => 'record', t: x => x, + }; + const rows = frontendFunction('overviewAccounts', context)(); + assert.equal(rows.length, 1, 'hidden platforms never appear'); + assert.equal(rows[0].configured, sample.configured === undefined ? 'unknown' : sample.configured ? 'enabled' : 'disabled'); + assert.equal(rows[0].unknown, sample.unknown); + assert.equal(rows[0].attention, sample.attention); + assert.equal(frontendFunction('overviewAccounts', {...context, hasPrivateConfig: () => false})().length, 0); +}); + +test('overview filters preserve unknown and search only the chosen scope', () => { + const rows = [ + { platformLabel: 'Binance', account: { label: 'Main' }, strategy: 'Crypto', configured: 'enabled', attention: true }, + { platformLabel: 'IBKR', account: { label: 'Second' }, strategy: 'Trend', configured: 'disabled', attention: false }, + { platformLabel: 'LongBridge', account: { label: 'SG' }, strategy: 'Trend', configured: 'unknown', attention: true }, + ]; + const filter = frontendFunction('filterOverviewAccounts', {}); + assert.equal(filter(rows, 'disabled', '').length, 1); + assert.equal(filter(rows, 'enabled', '').length, 1); + assert.equal(filter(rows, 'attention', '').length, 2); + assert.equal(filter(rows, 'all', ' bINAnCe ').length, 1); + assert.equal(filter(rows, 'enabled', 'Trend').length, 0); + assert.equal(filter(rows, 'all', 'not-found').length, 0); +}); + +for (const view of ['overview', 'accounts', 'research']) test(`workspace navigation isolates ${view} from the other jobs`, () => { + const nodes = {}; + const nav = { hidden: true }; + const buttons = ['overview', 'accounts', 'research'].map(workspace => ({ + dataset: { workspace }, attributes: {}, + setAttribute(key, value) { this.attributes[key] = value; }, + removeAttribute(key) { delete this.attributes[key]; }, + })); + const state = { view, appReady: true, auth: { allowed: true }, refreshing: false }; + const context = { state, t: key => key, el: id => nodes[id] ??= {}, + document: { querySelector: () => nav, querySelectorAll: () => buttons } }; + const render = frontendFunction('renderWorkspace', context); + render(); + for (const name of ['overview', 'accounts', 'research']) assert.equal(nodes[`${name}-view`].hidden, name !== view); + assert.equal(nodes['platform-strip'].hidden, view === 'overview'); + assert.equal(nodes['health-view'].hidden, view !== 'research'); + assert.equal(buttons.filter(button => button.attributes['aria-current'] === 'page').length, 1); + assert.equal(nav.hidden, false); + state.auth.allowed = false; + render(); + assert.equal(nav.hidden, true, 'private navigation must disappear on logout'); +}); + +test('viewing an account does not expand editing or change the configured switch', () => { + const state = { selected: 'longbridge', forms: { binance: { runtimeTargetMode: 'current' } } }; + const nodes = { + 'strategy-settings': { open: true }, 'workspace-title': { scrollIntoView() {} }, + 'account-select': { value: '', dispatchEvent() {}, focus() {} }, + }; + const fn = frontendFunction('openAccountSettings', { state, el: id => nodes[id], render() {}, Event }); + fn('binance', { key: 'default' }, false); + assert.equal(state.view, 'accounts'); + assert.equal(nodes['account-select'].value, 'default'); + assert.equal(nodes['strategy-settings'].open, false); + assert.equal(state.forms.binance.runtimeTargetMode, 'current'); +}); diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index cbd605c..3cf6222 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -56,7 +56,7 @@ assert.ok(__test.currentStrategiesTimeoutMs >= 8000); const renderPlatformsBody = indexHtml.match(/function renderPlatforms\(\) \{([\s\S]*?)\n \}/)?.[1] || ""; assert.ok(!renderPlatformsBody.includes("syncStrategyForAccount(")); assert.equal(indexHtml.includes(".innerHTML"), false); -assert.ok(indexHtml.includes('')); +assert.match(indexHtml, //); assert.ok(indexHtml.includes('id="boot-screen"')); assert.ok(indexHtml.includes('id="app-shell"')); assert.equal(indexHtml.includes('runtime-authority-status'), false); @@ -120,8 +120,8 @@ assert.ok(indexHtml.includes('switchSurface.classList.toggle("summary-hidden", ! assert.equal(indexHtml.match(/Generated by inject_platform_config\.py/g)?.length, 1); assert.ok(indexHtml.includes('')); assert.ok(indexHtml.includes('')); -assert.ok(indexHtml.includes('/app.js?v=operator-console-v3')); -assert.ok(indexHtml.includes('/app.css?v=operator-console-v3')); +assert.ok(indexHtml.includes('/app.js?v=console-clarity-v1')); +assert.ok(indexHtml.includes('/app.css?v=console-clarity-v1')); assert.equal(indexHtml.includes(' - +
+

QuantStrategyLab

-

低频决策台

+

管理控制台

+
@@ -45,12 +51,42 @@

管理你的量化平台

-
-

决策与账户

-
+

运行总览

+
+
+
+
已配置账户
+
配置已启用
+
配置已停用
+
实际状态待确认
+
+
+ +

+ +
+
+

账户

+
+
+ + + + +
+ +
+
+
账户配置策略运行情况最近读回账户操作
+
+ +

状态来自实际读回;保存配置不代表已经运行或成交。

+
+ + + + + +
- + diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js index 16d98bf..bd750d7 100644 --- a/web/strategy-switch-console/page_asset.js +++ b/web/strategy-switch-console/page_asset.js @@ -1,2 +1,2 @@ // Generated by python/scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const PAGE_HTML = "\n\n\n \n \n \n QuantStrategyLab 控制台\n \n \n\n\n\n\n\n
\n
\n

QuantStrategyLab

\n

低频决策台

\n
\n
\n \n \n \n \n
\n
\n\n
\n
\n 初始化控制台\n

读取策略配置

\n

正在读取登录状态、账号配置和当前状态。

\n
\n
\n
\n\n
\n
\n Q\n

管理你的量化平台

\n

配置策略,关注运行,处理重要决策。

\n 登录\n
\n
\n\n
\n

\n \n
\n

决策与账户

\n
\n
\n\n \n\n\n \n \n\n \n\n
\n
\n 目标平台\n

LongBridge

\n
\n\n \n

左右滑动查看实际状态与下一步

\n\n \n\n

实际开关和调度以记录时间为准,不代表已经成交。

\n
\n 策略设置需要启停或调整策略时展开\n \n
\n
\n
\n
\n 01\n
选择范围平台、账号、策略与目标环境
\n
\n \n\n \n\n
\n 模式\n
\n \n \n
\n \n
\n\n \n
\n 高级设置\n

插件、收入层和期权的选择不授予运行许可;只使用策略已绑定的能力。

\n

\n
\n
\n 02\n
运行保护停用、插件和附加层都受同一计划约束
\n
\n\n\n \n\n
\n\n
\n
\n 03\n
策略附加层仅使用策略已定义的默认边界
\n
\n \n\n \n\n \n
\n\n
\n \n
\n\n
\n
\n 04\n
资本边界现金预留优先于融资;两者不能同时覆盖
\n
\n

允许融资与预留现金覆盖不能同时生效。

\n \n
\n \n\n \n\n \n\n \n
\n
\n\n
\n \n\n \n
\n
\n
\n\n
\n \n

登录后才可提交受限配置。

\n
\n
\n\n \n
\n
\n
\n \n
\n\n \n \n\n\n"; +export const PAGE_HTML = "\n\n\n \n \n \n QuantStrategyLab 控制台\n \n \n\n\n\n\n\n
\n
\n Q\n

QuantStrategyLab

\n

管理控制台

\n
\n \n
\n \n \n \n \n
\n
\n\n
\n
\n 初始化控制台\n

读取策略配置

\n

正在读取登录状态、账号配置和当前状态。

\n
\n
\n
\n\n
\n
\n Q\n

管理你的量化平台

\n

配置策略,关注运行,处理重要决策。

\n 登录\n
\n
\n\n
\n

\n
\n

运行总览

\n
\n
\n\n
\n
\n
已配置账户
\n
配置已启用
\n
配置已停用
\n
实际状态待确认
\n
\n
\n i\n

\n \n
\n
\n

账户

\n
\n
\n \n \n \n \n
\n \n
\n
\n
账户配置策略运行情况最近读回账户操作
\n
\n \n

状态来自实际读回;保存配置不代表已经运行或成交。

\n
\n \n\n \n\n\n \n
\n \n \n\n \n \n
\n\n \n \n\n\n";