Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/console_information_design.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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,不使用装饰图表。

## 交互与验证

验证默认总览、准确定位账户、配置/实际状态分离、搜索和筛选、空结果、读取失败、刷新时间、研究切页、折叠详情、语言切换以及电脑/手机布局。线上仅检查读取和导航;提交类操作在隔离测试数据中验证,不能为了页面测试触发实盘。
88 changes: 84 additions & 4 deletions tests/console_runtime_state_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)'));
Expand Down Expand Up @@ -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');
Expand All @@ -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);
});

Expand Down Expand Up @@ -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');
});
8 changes: 4 additions & 4 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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('<body class="app-loading operator-console">'));
assert.match(indexHtml, /<body class="app-loading operator-console console-clarity">/);
assert.ok(indexHtml.includes('id="boot-screen"'));
assert.ok(indexHtml.includes('id="app-shell"'));
assert.equal(indexHtml.includes('runtime-authority-status'), false);
Expand Down Expand Up @@ -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('<script src="/bootstrap-config.js"></script>'));
assert.ok(indexHtml.includes('<script src="/boot-recovery.js"></script>'));
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('<script id="platform-config">'), false);
assert.equal(indexHtml.includes("publicSummary"), false);
assert.ok(indexHtml.includes("function hasPrivateConfig()"));
Expand Down Expand Up @@ -211,7 +211,7 @@ assert.ok(indexHtml.includes('function strategyLabelSet('));
assert.ok(indexHtml.includes('function strategyDisplayMetaText('));
assert.ok(indexHtml.includes('function strategyChoiceLabel('));
assert.ok(indexHtml.includes('function strategyCanSwitchLive('));
assert.ok(indexHtml.includes("account-block"));
assert.ok(indexHtml.includes('id="account-select"'));
assert.ok(indexHtml.includes("strategy-block"));
assert.ok(indexHtml.includes("white-space: pre-line"));
assert.ok(indexHtml.includes(".form-section {"));
Expand Down
Loading