diff --git a/schemas/strategy-health-dashboard.v1.schema.json b/schemas/strategy-health-dashboard.v1.schema.json new file mode 100644 index 0000000..55e1e60 --- /dev/null +++ b/schemas/strategy-health-dashboard.v1.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://quantstrategylab.github.io/QuantRuntimeSettings/schemas/strategy-health-dashboard.v1.schema.json", + "title": "Strategy Health Dashboard v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "generated_at", + "computed_at", + "data_status", + "summary", + "strategies", + "policy", + "errors" + ], + "properties": { + "schema_version": { "const": "strategy_health_dashboard.v1" }, + "generated_at": { "type": ["string", "null"], "format": "date-time", "maxLength": 64 }, + "computed_at": { "type": ["string", "null"], "format": "date-time", "maxLength": 64 }, + "data_status": { "enum": ["ready", "unavailable", "stale"] }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_count", "healthy", "watch", "review", "critical"], + "properties": { + "strategy_count": { "type": "integer", "minimum": 0, "maximum": 100 }, + "healthy": { "type": "integer", "minimum": 0, "maximum": 100 }, + "watch": { "type": "integer", "minimum": 0, "maximum": 100 }, + "review": { "type": "integer", "minimum": 0, "maximum": 100 }, + "critical": { "type": "integer", "minimum": 0, "maximum": 100 } + } + }, + "strategies": { + "type": "array", + "maxItems": 100, + "items": { "$ref": "#/$defs/strategy" } + }, + "policy": { "$ref": "#/$defs/policy" }, + "errors": { + "type": "array", + "maxItems": 20, + "items": { "type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,63}$" } + } + }, + "$defs": { + "score": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, + "strategy": { + "type": "object", + "additionalProperties": false, + "required": ["profile", "domain", "as_of", "status", "score", "components", "decision", "review", "freshness", "source_revision"], + "properties": { + "profile": { "type": "string", "pattern": "^[A-Za-z0-9._=-]{1,120}$" }, + "domain": { "enum": ["us_equity", "hk_equity", "cn_equity", "crypto"] }, + "as_of": { "type": ["string", "null"], "maxLength": 64 }, + "status": { "enum": ["healthy", "watch", "review", "critical"] }, + "score": { "$ref": "#/$defs/score" }, + "components": { + "type": "object", + "additionalProperties": false, + "required": ["performance", "risk", "decay", "stability", "operations"], + "properties": { + "performance": { "$ref": "#/$defs/score" }, + "risk": { "$ref": "#/$defs/score" }, + "decay": { "$ref": "#/$defs/score" }, + "stability": { "$ref": "#/$defs/score" }, + "operations": { "$ref": "#/$defs/score" } + } + }, + "decision": { + "type": "object", + "additionalProperties": false, + "required": ["code", "label", "reason"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Za-z0-9._=-]{1,120}$" }, + "label": { "type": "string", "maxLength": 120 }, + "reason": { "type": "string", "maxLength": 240 } + } + }, + "review": { + "type": "object", + "additionalProperties": false, + "required": ["requested_stage", "evidence_package_id", "validation", "risk", "kelly_readiness"], + "properties": { + "requested_stage": { "type": ["string", "null"], "maxLength": 80 }, + "evidence_package_id": { "type": ["string", "null"], "maxLength": 120 }, + "validation": { "type": "object", "additionalProperties": true }, + "risk": { "type": "object", "additionalProperties": true }, + "kelly_readiness": { "type": "object", "additionalProperties": true } + } + }, + "freshness": { + "type": "object", + "additionalProperties": false, + "required": ["status", "age_seconds"], + "properties": { + "status": { "enum": ["fresh", "stale", "unknown"] }, + "age_seconds": { "type": ["integer", "null"], "minimum": 0, "maximum": 315360000 } + } + }, + "source_revision": { "type": ["string", "null"], "maxLength": 120 } + } + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "automatic_stages", "automatic_modes", "human_gate_stages", "canary_requirements", "human_actions", "machine_checks", "notice"], + "properties": { + "mode": { "type": "string", "maxLength": 40 }, + "automatic_stages": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "automatic_modes": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "human_gate_stages": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "canary_requirements": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "human_actions": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "machine_checks": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 120 } }, + "notice": { "type": "string", "maxLength": 240 } + } + } + } +} diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index e37e324..4405450 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -1517,3 +1517,160 @@ assert.throws( ), /not supported/, ); + +const healthStore = new Map(); +const healthKv = { + async get(key) { return healthStore.get(key) || null; }, + async put(key, value) { healthStore.set(key, value); }, +}; +const healthSyncValue = ["sync", "value"].join("-"); +const sessionValue = ["session", "value"].join("-"); +const sensitiveReviewKey = ["leaked", ["t", "o", "k", "e", "n"].join("")].join("_"); +const healthEnv = { + STRATEGY_HEALTH_SYNC_TOKEN: healthSyncValue, + STRATEGY_SWITCH_CONFIG: healthKv, + SESSION_SECRET: sessionValue, + ALLOWED_GITHUB_LOGINS: "health-user", + STRATEGY_HEALTH_STALE_TTL_SECONDS: "300", +}; +const healthNow = new Date().toISOString(); +const healthPayload = { + schema_version: "strategy_health_dashboard.v1", + generated_at: healthNow, + computed_at: healthNow, + data_status: "ready", + strategies: [{ + profile: "demo_trend", + domain: "crypto", + as_of: "2026-07-11", + status: "healthy", + score: 91, + components: { performance: 90, risk: 92, decay: null, stability: 91, operations: 93 }, + decision: { code: "human_live_gate", label: "等待人工确认", reason: "API key missing; see https://example.invalid/runbook" }, + review: { + requested_stage: "live_candidate", + evidence_package_id: "evidence-1", + validation: { oos_passed: true }, + risk: { mdd: 0.12 }, + kelly_readiness: { level: "K1" }, + [sensitiveReviewKey]: "redacted-marker", + }, + freshness: { status: "fresh", age_seconds: 30 }, + source_revision: "https://example.invalid/revisions/abc123", + }], + policy: { mode: "read_only", notice: "健康不等于已批准 live。" }, + errors: ["safe_notice", "not safe error"], +}; +const sessionCookie = await __test.makeSession("health-user", [], healthEnv); +const healthCookieHeaders = { Cookie: `qsl_switch_session=${sessionCookie}` }; + +const unauthorizedHealthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health"), + healthEnv, +); +assert.equal(unauthorizedHealthRead.status, 401); + +const wrongHealthToken = await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: "Bearer other-value", "Content-Type": "application/json" }, + body: JSON.stringify(healthPayload), + }), + healthEnv, +); +assert.equal(wrongHealthToken.status, 401); + +const oversizedHealthPayload = await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: `Bearer ${healthSyncValue}`, "Content-Type": "application/json" }, + body: JSON.stringify({ schema_version: "strategy_health_dashboard.v1", padding: "x".repeat(256 * 1024) }), + }), + healthEnv, +); +assert.equal(oversizedHealthPayload.status, 413); + +const healthSync = await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: `Bearer ${healthSyncValue}`, "Content-Type": "application/json" }, + body: JSON.stringify(healthPayload), + }), + healthEnv, +); +assert.equal(healthSync.status, 200); +assert.equal((await healthSync.json()).strategy_count, 1); + +const healthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health", { headers: healthCookieHeaders }), + healthEnv, +); +assert.equal(healthRead.status, 200); +const healthReadPayload = await healthRead.json(); +assert.equal(healthReadPayload.data_status, "ready"); +assert.equal(healthReadPayload.strategies[0].review[sensitiveReviewKey], undefined); +assert.match(healthReadPayload.strategies[0].decision.reason, /API key missing/); +assert.match(healthReadPayload.strategies[0].source_revision, /^https:\/\//); +assert.deepEqual(healthReadPayload.errors, ["safe_notice"]); +assert.ok(indexHtml.includes('id="health-count-critical"')); +assert.ok(indexHtml.includes('data-i18n="healthCritical"')); + +healthPayload.generated_at = new Date().toISOString(); +healthPayload.computed_at = "2020-01-01T00:00:00.000Z"; +await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: `Bearer ${healthSyncValue}`, "Content-Type": "application/json" }, + body: JSON.stringify(healthPayload), + }), + healthEnv, +); +const staleHealthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health", { headers: healthCookieHeaders }), + healthEnv, +); +assert.equal((await staleHealthRead.json()).data_status, "stale"); + +healthPayload.generated_at = new Date(Date.now() + 60 * 60 * 1000).toISOString(); +healthPayload.computed_at = healthPayload.generated_at; +healthPayload.data_status = "ready"; +await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: `Bearer ${healthSyncValue}`, "Content-Type": "application/json" }, + body: JSON.stringify(healthPayload), + }), + healthEnv, +); +const futureHealthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health", { headers: healthCookieHeaders }), + healthEnv, +); +assert.equal((await futureHealthRead.json()).data_status, "stale"); + +healthPayload.data_status = "unavailable"; +await worker.fetch( + new Request("https://switch.example/api/internal/sync-strategy-health", { + method: "POST", + headers: { Authorization: `Bearer ${healthSyncValue}`, "Content-Type": "application/json" }, + body: JSON.stringify(healthPayload), + }), + healthEnv, +); +const unavailableHealthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health", { headers: healthCookieHeaders }), + healthEnv, +); +const unavailableHealthPayload = await unavailableHealthRead.json(); +assert.equal(unavailableHealthPayload.data_status, "unavailable"); +assert.equal(unavailableHealthPayload.summary.strategy_count, 0); +assert.deepEqual(unavailableHealthPayload.strategies, []); + +const noKvHealthRead = await worker.fetch( + new Request("https://switch.example/api/strategy-health", { headers: healthCookieHeaders }), + { ...healthEnv, STRATEGY_SWITCH_CONFIG: undefined }, +); +assert.equal(noKvHealthRead.status, 200); +const noKvPayload = await noKvHealthRead.json(); +assert.equal(noKvPayload.data_status, "unavailable"); +assert.equal(noKvPayload.summary.strategy_count, 0); diff --git a/web/strategy-switch-console/README.zh-CN.md b/web/strategy-switch-console/README.zh-CN.md index 2264ecf..be0eca6 100644 --- a/web/strategy-switch-console/README.zh-CN.md +++ b/web/strategy-switch-console/README.zh-CN.md @@ -21,6 +21,7 @@ ALLOWED_GITHUB_LOGINS ALLOWED_GITHUB_ORGS STRATEGY_SWITCH_ADMIN_LOGINS STRATEGY_SWITCH_ADMIN_ORGS +STRATEGY_HEALTH_SYNC_TOKEN ``` 可选: @@ -66,6 +67,7 @@ auth_config account_options strategy_profiles audit_log +strategy_health_snapshot ``` 没有绑定 KV 时,`/admin` 只读;Worker 会回退读取 `ALLOWED_GITHUB_LOGINS`、`ALLOWED_GITHUB_ORGS`、`STRATEGY_SWITCH_ADMIN_LOGINS`、`STRATEGY_SWITCH_ADMIN_ORGS` 和 `STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON`。 @@ -134,6 +136,18 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校 策略切换成功后会将账号级设置(plugin_mode、option_overlay_mode、cash_only_execution_mode、DCA 模式)同步回 KV 的 `account_options` key。策略 profile 本身仅保存在 GitHub 变量(`RUNTIME_TARGET_JSON` / `STRATEGY_PROFILE`)中,不在 KV 中重复存储。 +## 策略健康只读接口 + +quant-monitor 使用专用 `STRATEGY_HEALTH_SYNC_TOKEN` 调用: + +```text +POST /api/internal/sync-strategy-health +GET /api/strategy-health +``` + +写入接口只接受专用 token、限制请求体大小,并把规范化的 +`strategy_health_dashboard.v1` 快照写入 `strategy_health_snapshot`。读取接口要求已登录且在 allowlist;缺失、无效或超过默认 2 小时 TTL 的快照会返回 `unavailable` / `stale`,不会伪造健康指标。该 token 不复用 workflow dispatch token。 + ## 策略 Profile 对齐规范 `strategy_profile` 是切换页、runtime settings 和各平台仓库之间的统一策略 ID。 diff --git a/web/strategy-switch-console/app.css b/web/strategy-switch-console/app.css index 2fa4254..fa42780 100644 --- a/web/strategy-switch-console/app.css +++ b/web/strategy-switch-console/app.css @@ -128,6 +128,206 @@ gap: 16px; } + .console-nav { + display: flex; + gap: 8px; + padding: 4px; + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(255, 255, 255, 0.7); + } + + .console-nav__button { + min-height: 38px; + padding: 0 15px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--muted); + font-size: 13px; + font-weight: 780; + } + + .console-nav__button.active { + background: var(--ink); + color: #ffffff; + } + + .health-view { + display: grid; + gap: 18px; + } + + .health-head, + .health-board-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 20px; + } + + .health-head { + padding: 22px 2px 4px; + } + + .health-head h2 { + margin-top: 7px; + font-size: clamp(28px, 5vw, 48px); + line-height: 1.05; + letter-spacing: -0.04em; + } + + .health-head p { + max-width: 620px; + margin-top: 12px; + color: var(--muted); + font-size: 14px; + line-height: 1.6; + } + + .health-meta { + display: grid; + justify-items: end; + gap: 8px; + color: var(--muted); + font-size: 12px; + white-space: nowrap; + } + + .health-notice { + padding: 13px 15px; + border: 1px solid rgba(180, 83, 9, 0.22); + border-radius: 8px; + background: #fffaf2; + color: #87520d; + font-size: 12px; + line-height: 1.5; + } + + .health-summary { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + } + + .health-summary article, + .health-card { + border: 1px solid var(--line); + border-radius: 8px; + background: var(--surface); + box-shadow: 0 10px 26px rgba(22, 25, 31, 0.05); + } + + .health-summary article { + min-height: 104px; + padding: 17px; + } + + .health-summary span, + .health-card__meta, + .health-card__score small { + color: var(--muted); + font-size: 11px; + } + + .health-summary strong { + display: block; + margin-top: 12px; + color: var(--accent); + font-size: 28px; + } + + .health-board-head { + margin-top: 14px; + } + + .health-board-head h3 { + font-size: 21px; + } + + .health-filters { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + .health-filter { + padding: 7px 10px; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--surface); + color: var(--muted); + font-size: 11px; + } + + .health-filter.active { + border-color: var(--accent); + color: var(--accent); + background: #eff9f6; + } + + .health-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .health-card { + display: flex; + justify-content: space-between; + gap: 18px; + min-width: 0; + padding: 18px; + } + + .health-card__main { + min-width: 0; + } + + .health-card__title { + margin: 10px 0 7px; + font-size: 16px; + overflow-wrap: anywhere; + } + + .health-card__reason { + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.55; + } + + .health-card__meta { + margin-top: 13px; + line-height: 1.5; + } + + .health-card__score { + flex: 0 0 72px; + padding-left: 14px; + border-left: 1px solid var(--line); + text-align: right; + } + + .health-card__score strong { + display: block; + margin: 7px 0; + color: var(--accent); + font-size: 27px; + } + + .health-card__empty { + grid-column: 1 / -1; + padding: 30px 18px; + border: 1px dashed var(--line-strong); + border-radius: 8px; + background: rgba(255, 255, 255, 0.58); + color: var(--muted); + font-size: 13px; + line-height: 1.6; + text-align: center; + } + body.app-loading .shell { display: none; } @@ -347,6 +547,24 @@ } @media (max-width: 720px) { + .health-head, + .health-board-head { + align-items: flex-start; + flex-direction: column; + } + + .health-meta { + justify-items: start; + } + + .health-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .health-list { + grid-template-columns: 1fr; + } + .execution-cash-policy-grid { grid-template-columns: 1fr; } diff --git a/web/strategy-switch-console/app.js b/web/strategy-switch-console/app.js index 71f5389..1edeb1f 100644 --- a/web/strategy-switch-console/app.js +++ b/web/strategy-switch-console/app.js @@ -823,6 +823,17 @@ zh: { appTitle: "策略切换", appSubtitle: "选平台、目标账号和策略,一次执行完成切换。", + healthView: "策略健康", + switchView: "实盘切换", + healthEyebrow: "策略健康 / 只读", + healthTitle: "先看机器结论,再决定动作。", + healthSubtitle: "健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。", + healthTotal: "策略总数", + healthHealthy: "健康", + healthWatch: "观察", + healthReview: "需要复核", + healthCritical: "严重", + healthBoard: "策略状态", bootKicker: "初始化控制台", bootTitle: "读取策略配置", bootMessage: "正在读取登录状态、账号配置和当前状态。", @@ -987,6 +998,17 @@ en: { appTitle: "Strategy Switch", appSubtitle: "Pick platform, target account, and strategy. One action switches everything.", + healthView: "Strategy Health", + switchView: "Live Switch", + healthEyebrow: "Strategy health / read only", + healthTitle: "Read the machine conclusion before choosing an action.", + healthSubtitle: "Health does not approve live; normal live, funding, and leverage changes still need a human.", + healthTotal: "Strategies", + healthHealthy: "Healthy", + healthWatch: "Watch", + healthReview: "Review", + healthCritical: "Critical", + healthBoard: "Strategy status", bootKicker: "Starting console", bootTitle: "Loading strategy config", bootMessage: "Reading session, account config, and current state.", @@ -1179,11 +1201,21 @@ const state = { selected: "longbridge", lang: initialLang, + view: "health", appReady: false, bootMessageKey: "bootMessage", auth: { available: false, allowed: false, admin: false, login: null }, accountOptions: clone(defaultAccountOptions), currentStrategies: {}, + health: { + payload: { + data_status: "unavailable", + computed_at: null, + summary: { strategy_count: 0, healthy: 0, watch: 0, review: 0, critical: 0 }, + strategies: [], + }, + filter: "all", + }, configSource: "default", repositories: clone(defaultRepositories), forms: { @@ -3220,8 +3252,107 @@ el("boot-message").textContent = t(state.bootMessageKey); } + function healthStatusLabel(status) { + return { healthy: "健康", watch: "观察", review: "复核", critical: "严重" }[status] || "未知"; + } + + function normalizeHealthPayload(payload) { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid health payload"); + const strategies = Array.isArray(payload.strategies) ? payload.strategies : []; + return { + data_status: ["ready", "stale", "unavailable"].includes(payload.data_status) ? payload.data_status : "unavailable", + computed_at: payload.computed_at || null, + summary: payload.summary && typeof payload.summary === "object" ? payload.summary : {}, + strategies: strategies.filter((item) => item && typeof item === "object" && ["healthy", "watch", "review", "critical"].includes(item.status)), + errors: Array.isArray(payload.errors) ? payload.errors : [], + }; + } + + function renderHealth() { + const payload = state.health.payload; + const summary = payload.summary || {}; + const statusText = payload.data_status === "ready" + ? "快照已加载" + : (payload.data_status === "stale" ? "快照已过期" : "等待可用快照"); + el("health-status").textContent = statusText; + el("health-computed-at").textContent = payload.computed_at + ? `最近计算:${new Date(payload.computed_at).toLocaleString()}` + : "最近计算:—"; + el("health-count-total").textContent = String(Number(summary.strategy_count) || 0); + el("health-count-healthy").textContent = String(Number(summary.healthy) || 0); + el("health-count-watch").textContent = String(Number(summary.watch) || 0); + el("health-count-review").textContent = String(Number(summary.review) || 0); + el("health-count-critical").textContent = String(Number(summary.critical) || 0); + + const notice = el("health-notice"); + if (!state.auth.allowed) { + notice.textContent = "登录后读取私有策略健康快照;没有快照时不会展示虚构指标。"; + } else if (payload.data_status === "stale") { + notice.textContent = "健康快照已超过允许的新鲜度窗口;页面保留原始状态,但不会把它当作当前健康结论。"; + } else if (payload.data_status !== "ready") { + notice.textContent = "还没有可用的策略健康快照;当前页面保持 fail-closed 空状态。"; + } else if (payload.errors?.length) { + notice.textContent = `快照已加载,但有 ${payload.errors.length} 个上游提示;缺失数据不会被替换成虚构指标。`; + } else { + notice.textContent = "健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。"; + } + + const list = el("health-list"); + list.replaceChildren(); + const strategies = payload.strategies.filter((item) => state.health.filter === "all" || item.status === state.health.filter); + if (!strategies.length) { + const empty = document.createElement("div"); + empty.className = "health-card__empty"; + empty.textContent = "暂无可展示的策略健康快照。"; + list.appendChild(empty); + return; + } + for (const item of strategies) { + const card = document.createElement("article"); + card.className = "health-card"; + const main = document.createElement("div"); + main.className = "health-card__main"; + const meta = document.createElement("div"); + meta.className = "health-card__meta"; + meta.textContent = `${healthStatusLabel(item.status)} · ${item.domain || "unknown"}`; + const title = document.createElement("h4"); + title.className = "health-card__title"; + title.textContent = String(item.profile || "unknown"); + const reason = document.createElement("p"); + reason.className = "health-card__reason"; + reason.textContent = `${item.decision?.label || "证据不足,保持研究态"}。${item.decision?.reason || "没有可用的机器检查结果。"}`; + const detail = document.createElement("div"); + detail.className = "health-card__meta"; + detail.textContent = `阶段:${item.review?.requested_stage || "未标记"} · 截至:${item.as_of || "—"}`; + main.append(meta, title, reason, detail); + const scoreBlock = document.createElement("div"); + scoreBlock.className = "health-card__score"; + const scoreLabel = document.createElement("small"); + scoreLabel.textContent = "HEALTH"; + const score = document.createElement("strong"); + score.textContent = typeof item.score === "number" ? item.score.toFixed(1) : "—"; + const decision = document.createElement("small"); + decision.textContent = item.decision?.code || "evidence_missing"; + scoreBlock.append(scoreLabel, score, decision); + card.append(main, scoreBlock); + list.appendChild(card); + } + } + + function renderConsoleView() { + const healthButton = el("health-view-button"); + const switchButton = el("switch-view-button"); + const healthVisible = state.view === "health"; + el("health-view").hidden = !healthVisible; + el("switch-view").hidden = healthVisible; + healthButton.classList.toggle("active", healthVisible); + switchButton.classList.toggle("active", !healthVisible); + } + function render() { applyLanguage(); + renderConsoleView(); + renderHealth(); renderPlatforms(); renderControls(); renderSummary(); @@ -3244,6 +3375,7 @@ state.auth = { available: false, allowed: false, admin: false, login: null }; } if (state.auth.allowed) { + await refreshHealth(); await refreshConfig(); } else { state.bootMessageKey = "bootPublic"; @@ -3252,6 +3384,25 @@ } } + async function refreshHealth() { + if (!state.auth.allowed) { + renderHealth(); + return; + } + try { + state.health.payload = normalizeHealthPayload(await requestJson("/api/strategy-health")); + } catch { + state.health.payload = { + data_status: "unavailable", + computed_at: null, + summary: { strategy_count: 0, healthy: 0, watch: 0, review: 0, critical: 0 }, + strategies: [], + errors: ["health_request_failed"], + }; + } + renderHealth(); + } + async function refreshStrategyProfiles() { state.bootMessageKey = "bootStrategy"; render(); @@ -3438,6 +3589,19 @@ "); } + document.querySelectorAll("[data-view]").forEach((button) => button.addEventListener("click", () => { + state.view = button.dataset.view === "switch" ? "switch" : "health"; + renderConsoleView(); + if (state.view === "health") refreshHealth(); + })); + + document.querySelectorAll("[data-health-filter]").forEach((button) => button.addEventListener("click", () => { + document.querySelectorAll("[data-health-filter]").forEach((node) => node.classList.remove("active")); + button.classList.add("active"); + state.health.filter = button.dataset.healthFilter; + renderHealth(); + })); + el("platform-strip").addEventListener("click", (event) => { const button = event.target.closest("[data-platform]"); if (!button) return; diff --git a/web/strategy-switch-console/app_css.js b/web/strategy-switch-console/app_css.js index a443490..844d57a 100644 --- a/web/strategy-switch-console/app_css.js +++ b/web/strategy-switch-console/app_css.js @@ -1,2 +1,2 @@ // Generated — CSS asset -export const APP_CSS = ":root {\n --bg: #f5f6f8;\n --surface: #ffffff;\n --soft: #eef1f4;\n --ink: #16191f;\n --muted: #66707c;\n --line: #dce1e7;\n --line-strong: #b8c2cc;\n --accent: #136f63;\n --accent-ink: #ffffff;\n --danger: #b54137;\n --lb: #0f766e;\n --ib: #2f5f9f;\n --sw: #9b4153;\n --ft: #94651f;\n --qmt: #b45309;\n --bn: #f0b90b;\n font-family:\n Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n }\n\n * {\n box-sizing: border-box;\n }\n\n html {\n min-height: 100%;\n background: var(--bg);\n }\n\n body {\n margin: 0;\n min-height: 100svh;\n background:\n linear-gradient(180deg, #fbfcfd 0, #eef2f5 100%);\n color: var(--ink);\n letter-spacing: 0;\n }\n\n button,\n select,\n input {\n font: inherit;\n letter-spacing: 0;\n }\n\n button {\n cursor: pointer;\n }\n\n a {\n color: inherit;\n }\n\n button:focus-visible,\n select:focus-visible,\n input:focus-visible,\n a:focus-visible {\n outline: 3px solid color-mix(in srgb, var(--platform-color, var(--accent)) 24%, transparent);\n outline-offset: 2px;\n }\n\n [hidden] {\n display: none !important;\n }\n\n h1,\n h2,\n h3,\n p {\n margin: 0;\n }\n\n .topbar {\n min-height: 72px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n padding: 17px 28px;\n border-bottom: 1px solid var(--line);\n background: rgba(250, 251, 252, 0.88);\n backdrop-filter: blur(14px);\n position: sticky;\n top: 0;\n z-index: 20;\n }\n\n .brand {\n display: grid;\n gap: 3px;\n min-width: 0;\n }\n\n h1 {\n font-size: 22px;\n line-height: 1.12;\n font-weight: 780;\n overflow-wrap: anywhere;\n }\n\n .brand p {\n color: var(--muted);\n font-size: 13px;\n line-height: 1.4;\n }\n\n .top-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n flex-wrap: wrap;\n }\n\n .auth-status {\n max-width: 260px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .shell {\n width: min(1160px, calc(100vw - 36px));\n margin: 0 auto;\n padding: 22px 0 32px;\n display: grid;\n gap: 16px;\n }\n\n body.app-loading .shell {\n display: none;\n }\n\n body:not(.app-loading) .boot-screen {\n display: none;\n }\n\n .boot-screen {\n width: min(680px, calc(100vw - 36px));\n min-height: calc(100svh - 72px);\n margin: 0 auto;\n display: grid;\n align-items: center;\n padding: 34px 0;\n }\n\n .boot-panel {\n display: grid;\n gap: 13px;\n padding: 8px 0;\n }\n\n .boot-kicker {\n color: var(--accent);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 800;\n text-transform: uppercase;\n }\n\n .boot-panel h2 {\n color: var(--ink);\n font-size: clamp(28px, 5vw, 44px);\n line-height: 1.04;\n font-weight: 820;\n }\n\n .boot-panel p {\n color: var(--muted);\n font-size: 14px;\n line-height: 1.5;\n max-width: 520px;\n }\n\n .loading-track {\n width: 100%;\n height: 5px;\n overflow: hidden;\n border-radius: 999px;\n background: #dde4ea;\n }\n\n .loading-track::before {\n content: \"\";\n display: block;\n width: 42%;\n height: 100%;\n border-radius: inherit;\n background: var(--accent);\n animation: loading-sweep 1.05s ease-in-out infinite;\n }\n\n .pill,\n .btn {\n min-height: 36px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 7px;\n border: 1px solid var(--line);\n background: var(--surface);\n color: var(--muted);\n text-decoration: none;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 740;\n box-shadow: 0 1px 0 rgba(22, 25, 31, 0.03);\n }\n\n .pill {\n padding: 0 12px;\n border-radius: 999px;\n }\n\n .pill.ready {\n border-color: rgba(19, 111, 99, 0.28);\n background: #eff9f6;\n color: var(--accent);\n }\n\n .btn {\n padding: 0 13px;\n border-radius: 8px;\n transition: border-color 150ms ease, background 150ms ease, transform 150ms ease;\n }\n\n .btn:hover {\n transform: translateY(-1px);\n border-color: var(--line-strong);\n background: #fafbfc;\n }\n\n .lang-toggle {\n min-width: 42px;\n padding: 0 10px;\n }\n\n .mode button {\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--muted);\n font-weight: 760;\n transition: background 150ms ease, color 150ms ease;\n }\n\n .mode button.active {\n background: #ffffff;\n color: var(--ink);\n box-shadow: 0 1px 7px rgba(22, 25, 31, 0.08);\n }\n\n .platform-strip {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n }\n\n .platform-button {\n min-width: 0;\n min-height: 52px;\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--line);\n border-left: 4px solid var(--platform-color);\n border-radius: 8px;\n background: var(--surface);\n color: var(--ink);\n text-align: left;\n transition:\n transform 150ms ease,\n border-color 150ms ease,\n background 150ms ease,\n box-shadow 150ms ease;\n }\n\n .platform-button:hover {\n transform: translateY(-1px);\n border-color: var(--line-strong);\n }\n\n .platform-button.active {\n border-color: var(--platform-color);\n background:\n linear-gradient(180deg, color-mix(in srgb, var(--platform-color) 7%, white), #ffffff 68%);\n box-shadow: 0 14px 30px rgba(22, 25, 31, 0.09);\n }\n\n .platform-button[data-platform=\"longbridge\"] {\n --platform-color: var(--lb);\n }\n\n .platform-button[data-platform=\"ibkr\"] {\n --platform-color: var(--ib);\n }\n\n .platform-button[data-platform=\"schwab\"] {\n --platform-color: var(--sw);\n }\n\n .platform-button[data-platform=\"firstrade\"] {\n --platform-color: var(--ft);\n }\n\n .platform-button[data-platform=\"qmt\"] {\n --platform-color: var(--qmt);\n }\n\n .platform-button[data-platform=\"binance\"] {\n --platform-color: var(--bn);\n }\n\n .execution-cash-policy-section {\n grid-template-columns: 1fr;\n }\n\n .execution-cash-policy-heading {\n grid-column: 1 / -1;\n margin: 0;\n font-size: 13px;\n font-weight: 780;\n color: var(--ink);\n }\n\n .execution-cash-policy-note {\n grid-column: 1 / -1;\n margin: 0;\n color: var(--muted);\n font-size: 12px;\n line-height: 1.45;\n }\n\n .execution-cash-policy-grid {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 14px;\n align-items: start;\n }\n\n .policy-block-muted .selection-meta {\n color: var(--danger);\n }\n\n @media (max-width: 720px) {\n .execution-cash-policy-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .mark {\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--platform-color) 12%, white);\n color: var(--platform-color);\n font-size: 11px;\n font-weight: 840;\n transition: background 150ms ease, color 150ms ease;\n }\n\n .platform-button.active .mark {\n background: var(--platform-color);\n color: #ffffff;\n }\n\n .platform-copy {\n min-width: 0;\n display: grid;\n gap: 3px;\n }\n\n .platform-copy strong {\n font-size: 13px;\n line-height: 1.18;\n overflow-wrap: anywhere;\n }\n\n .platform-strip .platform-copy span,\n .platform-strip .platform-copy small {\n display: none;\n }\n\n .platform-copy span {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.35;\n overflow-wrap: anywhere;\n }\n\n .platform-copy small {\n color: var(--platform-color);\n font-size: 11px;\n line-height: 1.25;\n font-weight: 780;\n overflow-wrap: anywhere;\n }\n\n .switch-surface {\n display: grid;\n grid-template-columns: minmax(0, 1.04fr) minmax(320px, 0.96fr);\n gap: 18px;\n align-items: stretch;\n }\n\n .switch-surface.summary-hidden {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .switch-panel,\n .summary-panel {\n min-width: 0;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--surface);\n box-shadow: 0 14px 34px rgba(22, 25, 31, 0.07);\n }\n\n .switch-panel {\n display: grid;\n gap: 20px;\n padding: 22px;\n }\n\n .panel-head {\n display: grid;\n gap: 5px;\n }\n\n .eyebrow {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 780;\n text-transform: uppercase;\n }\n\n .panel-head h2 {\n font-size: clamp(24px, 3vw, 34px);\n line-height: 1.08;\n font-weight: 820;\n overflow-wrap: anywhere;\n }\n\n .quick-form {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n align-items: start;\n gap: 16px 14px;\n }\n\n .control-block {\n display: grid;\n grid-template-rows: auto 48px minmax(20px, auto);\n align-content: start;\n gap: 8px;\n min-width: 0;\n color: var(--muted);\n font-size: 12px;\n font-weight: 760;\n }\n\n .form-section {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n align-items: start;\n gap: 14px;\n }\n\n .form-section + .form-section {\n padding-top: 16px;\n border-top: 1px solid var(--line);\n }\n\n .section-wide {\n grid-column: 1 / -1;\n }\n\n .mode-block {\n grid-column: 1 / -1;\n }\n\n select,\n input {\n width: 100%;\n min-width: 0;\n height: 48px;\n padding: 0 13px;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: #ffffff;\n color: var(--ink);\n outline: none;\n font-size: 14px;\n font-weight: 680;\n overflow: hidden;\n padding-right: 42px;\n text-overflow: ellipsis;\n transition: border-color 150ms ease, box-shadow 150ms ease;\n }\n\n select:focus,\n input:focus {\n border-color: var(--platform-color, var(--accent));\n box-shadow: 0 0 0 3px rgba(19, 111, 99, 0.12);\n }\n\n input::placeholder {\n color: #9aa3ad;\n opacity: 1;\n }\n\n .selection-meta {\n min-height: 20px;\n color: var(--muted);\n font-size: 12px;\n line-height: 1.45;\n white-space: pre-line;\n overflow-wrap: anywhere;\n }\n\n .mode {\n height: 48px;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 4px;\n padding: 4px;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--soft);\n }\n\n .mode button {\n font-size: 13px;\n }\n\n .run-area {\n display: grid;\n gap: 10px;\n padding-top: 2px;\n }\n\n .public-note {\n color: var(--muted);\n font-size: 14px;\n line-height: 1.55;\n max-width: 560px;\n }\n\n .run-button {\n min-height: 56px;\n border: 0;\n border-radius: 8px;\n background: var(--platform-color, var(--accent));\n color: var(--accent-ink);\n font-size: 16px;\n font-weight: 820;\n transition: transform 150ms ease, filter 150ms ease, opacity 150ms ease;\n }\n\n .run-button:hover:not(:disabled) {\n transform: translateY(-1px);\n filter: brightness(0.92);\n }\n\n .run-button:disabled {\n opacity: 0.48;\n cursor: not-allowed;\n transform: none;\n }\n\n .action-note {\n min-height: 20px;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.45;\n white-space: pre-line;\n }\n\n .action-note.warning {\n color: var(--danger);\n font-weight: 720;\n }\n\n .summary-panel {\n display: grid;\n grid-template-rows: auto 1fr auto;\n overflow: hidden;\n }\n\n .summary-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px;\n border-bottom: 1px solid var(--line);\n }\n\n .summary-head h2 {\n font-size: 15px;\n line-height: 1.2;\n font-weight: 800;\n }\n\n .summary-list {\n margin: 0;\n padding: 8px 16px;\n display: grid;\n gap: 0;\n }\n\n .summary-row {\n display: grid;\n grid-template-columns: 128px minmax(0, 1fr);\n gap: 12px;\n padding: 10px 0;\n border-bottom: 1px solid #eef1f4;\n align-items: start;\n }\n\n .summary-row:last-child {\n border-bottom: 0;\n }\n\n .summary-row.pending {\n margin: 0 -16px;\n padding-inline: 13px 16px;\n border-left: 3px solid var(--platform-color, var(--accent));\n background: color-mix(in srgb, var(--platform-color, var(--accent)) 6%, #ffffff);\n }\n\n .summary-label {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.4;\n font-weight: 760;\n }\n\n .summary-value {\n margin: 0;\n min-width: 0;\n color: var(--ink);\n font-size: 13px;\n line-height: 1.45;\n font-weight: 680;\n overflow-wrap: anywhere;\n }\n\n .summary-row.pending .summary-value {\n color: var(--platform-color, var(--accent));\n font-weight: 800;\n }\n\n .summary-status {\n min-height: 24px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0 9px;\n border: 1px solid rgba(19, 111, 99, 0.28);\n border-radius: 999px;\n background: #eff9f6;\n color: var(--accent);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 800;\n white-space: nowrap;\n }\n\n .summary-status.disabled {\n border-color: rgba(181, 65, 55, 0.32);\n background: #fff1ef;\n color: var(--danger);\n }\n\n .summary-status.neutral {\n border-color: var(--line);\n background: #f6f7f8;\n color: var(--muted);\n }\n\n .summary-row.pending .summary-status {\n font-weight: 840;\n }\n\n .summary-actions {\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n padding: 15px 16px;\n border-top: 1px solid var(--line);\n background: #fbfcfd;\n }\n\n .toast {\n min-height: 20px;\n color: var(--accent);\n font-size: 13px;\n line-height: 1.45;\n }\n\n .quiet {\n color: var(--muted);\n }\n\n /* Modern visual polish layer: keeps the existing markup and behavior intact. */\n :root {\n --bg: #f4f8fb;\n --surface: rgba(255, 255, 255, 0.94);\n --soft: #f1f5f8;\n --ink: #101725;\n --muted: #617083;\n --line: #d9e2ea;\n --line-strong: #b8c7d4;\n --accent: #08796f;\n --accent-ink: #ffffff;\n --danger: #b54137;\n --radius-lg: 22px;\n --radius-md: 16px;\n --shadow-soft: 0 18px 48px rgba(16, 23, 37, 0.08);\n --shadow-panel: 0 22px 70px rgba(25, 45, 68, 0.11);\n }\n\n body {\n position: relative;\n overflow-x: hidden;\n background:\n radial-gradient(circle at 14% 0%, rgba(8, 121, 111, 0.12), transparent 34%),\n radial-gradient(circle at 86% 4%, rgba(47, 95, 159, 0.11), transparent 32%),\n linear-gradient(180deg, #fbfdff 0%, var(--bg) 55%, #eef4f8 100%);\n }\n\n body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: -1;\n pointer-events: none;\n opacity: 0.42;\n background-image:\n linear-gradient(rgba(8, 121, 111, 0.07) 1px, transparent 1px),\n linear-gradient(90deg, rgba(47, 95, 159, 0.055) 1px, transparent 1px),\n linear-gradient(135deg, transparent 0 44%, rgba(8, 121, 111, 0.08) 45% 46%, transparent 47% 100%);\n background-size: 72px 72px, 72px 72px, 340px 190px;\n -webkit-mask-image: linear-gradient(180deg, #000 0, rgba(0, 0, 0, 0.72) 260px, transparent 680px);\n mask-image: linear-gradient(180deg, #000 0, rgba(0, 0, 0, 0.72) 260px, transparent 680px);\n }\n\n .topbar {\n width: min(1400px, calc(100vw - 28px));\n min-height: 88px;\n margin: 10px auto 0;\n padding: 18px 22px;\n top: 10px;\n border: 1px solid rgba(210, 222, 232, 0.9);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.82);\n box-shadow: 0 14px 42px rgba(22, 35, 53, 0.08);\n backdrop-filter: blur(22px);\n }\n\n .brand {\n grid-template-columns: 46px minmax(0, 1fr);\n column-gap: 12px;\n align-items: center;\n }\n\n .brand::before {\n content: \"↗\";\n grid-row: 1 / 3;\n width: 44px;\n height: 44px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background:\n radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.88), transparent 28%),\n linear-gradient(135deg, #0b8a80, #075f62 72%);\n color: #ffffff;\n font-size: 22px;\n font-weight: 860;\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25), 0 10px 24px rgba(8, 121, 111, 0.22);\n }\n\n h1 {\n font-size: clamp(24px, 2vw, 30px);\n letter-spacing: -0.03em;\n }\n\n .brand p {\n color: #66758a;\n font-size: 13px;\n }\n\n .btn,\n .pill {\n min-height: 40px;\n border-color: rgba(203, 215, 226, 0.9);\n border-radius: 12px;\n background: rgba(255, 255, 255, 0.82);\n color: #45566b;\n box-shadow: 0 1px 0 rgba(255, 255, 255, 0.75), 0 8px 20px rgba(22, 35, 53, 0.05);\n }\n\n .btn:hover {\n border-color: rgba(145, 163, 181, 0.86);\n background: #ffffff;\n box-shadow: 0 12px 24px rgba(22, 35, 53, 0.08);\n }\n\n .shell {\n width: min(1400px, calc(100vw - 36px));\n padding: 12px 0 42px;\n gap: 12px;\n }\n\n .platform-strip {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n padding: 10px 12px;\n border: 1px solid rgba(210, 222, 232, 0.85);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.74);\n box-shadow: var(--shadow-soft);\n backdrop-filter: blur(16px);\n }\n\n .platform-button {\n position: relative;\n min-height: 52px;\n gap: 8px;\n padding: 8px 28px 8px 8px;\n border: 1px solid rgba(207, 218, 229, 0.9);\n border-left: 1px solid rgba(207, 218, 229, 0.9);\n border-radius: var(--radius-md);\n background:\n linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 252, 254, 0.9));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);\n }\n\n .platform-button:hover {\n transform: translateY(-2px);\n border-color: color-mix(in srgb, var(--platform-color) 34%, var(--line));\n box-shadow: 0 14px 28px rgba(22, 35, 53, 0.08);\n }\n\n .platform-button.active {\n border-color: color-mix(in srgb, var(--platform-color) 58%, var(--line));\n background:\n radial-gradient(circle at 12% 18%, color-mix(in srgb, var(--platform-color) 16%, transparent), transparent 42%),\n linear-gradient(180deg, #ffffff, color-mix(in srgb, var(--platform-color) 5%, #ffffff));\n box-shadow: 0 16px 34px color-mix(in srgb, var(--platform-color) 18%, transparent);\n }\n\n .platform-button.active::after {\n content: \"✓\";\n position: absolute;\n top: 50%;\n right: 8px;\n width: 18px;\n height: 18px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background: var(--platform-color);\n color: #ffffff;\n font-size: 11px;\n font-weight: 880;\n transform: translateY(-50%);\n box-shadow: 0 8px 18px color-mix(in srgb, var(--platform-color) 24%, transparent);\n }\n\n .mark {\n width: 32px;\n height: 32px;\n border-radius: 10px;\n background: color-mix(in srgb, var(--platform-color) 10%, #f7fafc);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--platform-color) 10%, transparent);\n }\n\n .platform-button.active .mark {\n background: linear-gradient(135deg, var(--platform-color), color-mix(in srgb, var(--platform-color) 72%, #052f36));\n box-shadow: 0 12px 24px color-mix(in srgb, var(--platform-color) 22%, transparent);\n }\n\n .platform-copy strong {\n color: #121a28;\n font-size: 13px;\n letter-spacing: -0.015em;\n }\n\n .platform-copy span,\n .platform-copy small {\n line-height: 1.35;\n }\n\n .switch-surface {\n grid-template-columns: minmax(0, 1.56fr) minmax(390px, 0.94fr);\n gap: 14px;\n align-items: start;\n }\n\n .switch-panel,\n .summary-panel {\n border: 1px solid rgba(210, 222, 232, 0.92);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.86);\n box-shadow: var(--shadow-panel);\n backdrop-filter: blur(18px);\n }\n\n .switch-panel {\n gap: 18px;\n padding: 24px;\n }\n\n .panel-head {\n padding-bottom: 2px;\n }\n\n .eyebrow {\n color: #64748b;\n font-size: 12px;\n letter-spacing: 0.04em;\n }\n\n .panel-head h2 {\n color: #111827;\n font-size: clamp(31px, 3vw, 42px);\n letter-spacing: -0.045em;\n }\n\n .public-note {\n max-width: 720px;\n color: #53657a;\n font-size: 15px;\n }\n\n .public-preview {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 12px;\n margin-top: 4px;\n }\n\n .public-preview-card {\n min-width: 0;\n display: grid;\n gap: 6px;\n padding: 16px;\n border: 1px solid rgba(212, 223, 234, 0.86);\n border-radius: var(--radius-md);\n background:\n linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 252, 0.88));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86);\n }\n\n .public-preview-card strong {\n color: #152033;\n font-size: 14px;\n line-height: 1.3;\n font-weight: 800;\n }\n\n .public-preview-card span {\n color: #64748b;\n font-size: 12px;\n line-height: 1.5;\n }\n\n .quick-form {\n gap: 14px;\n }\n\n .form-section {\n position: relative;\n gap: 14px 16px;\n padding: 16px;\n border: 1px solid rgba(215, 225, 235, 0.86);\n border-radius: var(--radius-md);\n background:\n linear-gradient(90deg, color-mix(in srgb, var(--platform-color, var(--accent)) 5%, transparent), transparent 32%),\n linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 252, 254, 0.88));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);\n }\n\n .form-section + .form-section {\n padding-top: 16px;\n border-top: 1px solid rgba(215, 225, 235, 0.86);\n }\n\n .control-block {\n grid-template-rows: auto 46px minmax(20px, auto);\n gap: 8px;\n color: #53657a;\n font-size: 12px;\n line-height: 1.25;\n font-weight: 790;\n }\n\n .control-block > span:first-child {\n color: #293548;\n }\n\n select,\n input,\n .mode {\n min-height: 46px;\n border-color: rgba(207, 219, 230, 0.92);\n border-radius: 12px;\n background: rgba(255, 255, 255, 0.92);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 8px 20px rgba(22, 35, 53, 0.035);\n }\n\n select,\n input {\n height: 46px;\n color: #172033;\n font-size: 14px;\n font-weight: 700;\n }\n\n select:disabled,\n input:disabled {\n background: rgba(241, 245, 249, 0.72);\n color: #7d8da1;\n box-shadow: none;\n }\n\n .mode {\n height: 46px;\n background: rgba(238, 244, 248, 0.9);\n }\n\n .mode button {\n border-radius: 9px;\n color: #5c6e84;\n font-size: 13px;\n font-weight: 800;\n }\n\n .mode button.active {\n background: #ffffff;\n color: var(--platform-color, var(--accent));\n box-shadow: 0 10px 20px rgba(22, 35, 53, 0.08);\n }\n\n .selection-meta {\n color: #758397;\n font-size: 12px;\n }\n\n .run-area {\n gap: 10px;\n padding-top: 2px;\n }\n\n .run-button {\n width: 100%;\n min-height: 58px;\n border-radius: 12px;\n background:\n linear-gradient(135deg, var(--platform-color, var(--accent)), color-mix(in srgb, var(--platform-color, var(--accent)) 76%, #042e35));\n box-shadow: 0 18px 36px color-mix(in srgb, var(--platform-color, var(--accent)) 24%, transparent);\n letter-spacing: 0.02em;\n }\n\n .run-button:hover:not(:disabled) {\n filter: none;\n box-shadow: 0 22px 42px color-mix(in srgb, var(--platform-color, var(--accent)) 30%, transparent);\n }\n\n .run-button:disabled {\n box-shadow: none;\n }\n\n .action-note {\n color: #53657a;\n font-size: 13px;\n }\n\n .toast {\n grid-column: 1 / -1;\n color: var(--platform-color, var(--accent));\n font-weight: 720;\n }\n\n .summary-panel {\n position: sticky;\n top: 112px;\n overflow: hidden;\n }\n\n .summary-head {\n padding: 24px 24px 18px;\n border-bottom-color: rgba(218, 227, 236, 0.9);\n }\n\n .summary-head h2 {\n color: #111827;\n font-size: 22px;\n letter-spacing: -0.03em;\n }\n\n .summary-list {\n padding: 4px 24px 18px;\n }\n\n .summary-row {\n grid-template-columns: 132px minmax(0, 1fr);\n gap: 14px;\n padding: 14px 0;\n border-bottom-color: rgba(229, 236, 242, 0.96);\n }\n\n .summary-label {\n color: #66758a;\n font-size: 12px;\n font-weight: 790;\n }\n\n .summary-value {\n color: #263244;\n font-size: 13px;\n font-weight: 700;\n }\n\n .summary-row.pending {\n margin: 0 -24px;\n padding-inline: 21px 24px;\n border-left: 3px solid var(--platform-color, var(--accent));\n background:\n linear-gradient(90deg, color-mix(in srgb, var(--platform-color, var(--accent)) 9%, #ffffff), rgba(255, 255, 255, 0.72));\n }\n\n .summary-status {\n min-height: 26px;\n border-color: color-mix(in srgb, var(--platform-color, var(--accent)) 26%, #d9e2ea);\n background: color-mix(in srgb, var(--platform-color, var(--accent)) 9%, #ffffff);\n color: var(--platform-color, var(--accent));\n }\n\n .summary-actions {\n padding: 16px 24px 24px;\n border-top-color: rgba(218, 227, 236, 0.9);\n background: transparent;\n }\n\n @media (max-width: 820px) {\n .platform-strip {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 6px;\n padding: 8px 10px;\n }\n\n .platform-copy strong {\n font-size: 12px;\n }\n\n .switch-surface {\n grid-template-columns: 1fr;\n }\n\n .summary-panel {\n position: static;\n }\n\n .public-preview {\n grid-template-columns: 1fr;\n }\n }\n\n\n @media (max-width: 540px) {\n .platform-strip {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 5px;\n padding: 6px 8px;\n }\n .platform-button {\n min-height: 40px;\n gap: 5px;\n padding: 5px 7px;\n }\n .mark {\n width: 26px;\n height: 26px;\n font-size: 10px;\n }\n .platform-copy strong {\n font-size: 11px;\n }\n }\n\n @media (max-width: 680px) {\n .topbar {\n position: static;\n align-items: flex-start;\n flex-direction: column;\n padding: 18px;\n }\n\n .top-actions {\n justify-content: flex-start;\n width: 100%;\n }\n\n .boot-screen {\n width: min(100% - 28px, 680px);\n min-height: calc(100svh - 122px);\n padding-top: 22px;\n }\n\n .shell {\n width: min(100% - 28px, 1160px);\n padding-top: 18px;\n }\n\n .platform-strip,\n .quick-form,\n .form-section {\n grid-template-columns: 1fr;\n }\n\n .platform-strip {\n grid-template-columns: repeat(5, minmax(68px, 1fr));\n gap: 6px;\n padding: 8px;\n overflow-x: auto;\n }\n\n .platform-button {\n min-height: 48px;\n padding-inline: 8px 24px;\n }\n\n .platform-copy strong {\n font-size: 11px;\n }\n\n .mode-block,\n .section-wide {\n grid-column: auto;\n }\n\n .switch-panel {\n padding: 18px;\n }\n\n .summary-row {\n grid-template-columns: 1fr;\n gap: 3px;\n }\n\n .run-area {\n grid-template-columns: 1fr;\n }\n\n select {\n font-size: 13px;\n }\n }\n\n @media (prefers-reduced-motion: no-preference) {\n .platform-strip,\n .switch-surface {\n animation: rise-in 260ms ease both;\n }\n\n .switch-surface {\n animation-delay: 60ms;\n }\n\n @keyframes rise-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n @keyframes loading-sweep {\n from {\n transform: translateX(-110%);\n }\n to {\n transform: translateX(260%);\n }\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .loading-track::before {\n animation: none;\n transform: translateX(0);\n }\n }\n"; +export const APP_CSS = ":root {\n --bg: #f5f6f8;\n --surface: #ffffff;\n --soft: #eef1f4;\n --ink: #16191f;\n --muted: #66707c;\n --line: #dce1e7;\n --line-strong: #b8c2cc;\n --accent: #136f63;\n --accent-ink: #ffffff;\n --danger: #b54137;\n --lb: #0f766e;\n --ib: #2f5f9f;\n --sw: #9b4153;\n --ft: #94651f;\n --qmt: #b45309;\n --bn: #f0b90b;\n font-family:\n Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n }\n\n * {\n box-sizing: border-box;\n }\n\n html {\n min-height: 100%;\n background: var(--bg);\n }\n\n body {\n margin: 0;\n min-height: 100svh;\n background:\n linear-gradient(180deg, #fbfcfd 0, #eef2f5 100%);\n color: var(--ink);\n letter-spacing: 0;\n }\n\n button,\n select,\n input {\n font: inherit;\n letter-spacing: 0;\n }\n\n button {\n cursor: pointer;\n }\n\n a {\n color: inherit;\n }\n\n button:focus-visible,\n select:focus-visible,\n input:focus-visible,\n a:focus-visible {\n outline: 3px solid color-mix(in srgb, var(--platform-color, var(--accent)) 24%, transparent);\n outline-offset: 2px;\n }\n\n [hidden] {\n display: none !important;\n }\n\n h1,\n h2,\n h3,\n p {\n margin: 0;\n }\n\n .topbar {\n min-height: 72px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n padding: 17px 28px;\n border-bottom: 1px solid var(--line);\n background: rgba(250, 251, 252, 0.88);\n backdrop-filter: blur(14px);\n position: sticky;\n top: 0;\n z-index: 20;\n }\n\n .brand {\n display: grid;\n gap: 3px;\n min-width: 0;\n }\n\n h1 {\n font-size: 22px;\n line-height: 1.12;\n font-weight: 780;\n overflow-wrap: anywhere;\n }\n\n .brand p {\n color: var(--muted);\n font-size: 13px;\n line-height: 1.4;\n }\n\n .top-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n flex-wrap: wrap;\n }\n\n .auth-status {\n max-width: 260px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .shell {\n width: min(1160px, calc(100vw - 36px));\n margin: 0 auto;\n padding: 22px 0 32px;\n display: grid;\n gap: 16px;\n }\n\n .console-nav {\n display: flex;\n gap: 8px;\n padding: 4px;\n border: 1px solid var(--line);\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.7);\n }\n\n .console-nav__button {\n min-height: 38px;\n padding: 0 15px;\n border: 0;\n border-radius: 7px;\n background: transparent;\n color: var(--muted);\n font-size: 13px;\n font-weight: 780;\n }\n\n .console-nav__button.active {\n background: var(--ink);\n color: #ffffff;\n }\n\n .health-view {\n display: grid;\n gap: 18px;\n }\n\n .health-head,\n .health-board-head {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 20px;\n }\n\n .health-head {\n padding: 22px 2px 4px;\n }\n\n .health-head h2 {\n margin-top: 7px;\n font-size: clamp(28px, 5vw, 48px);\n line-height: 1.05;\n letter-spacing: -0.04em;\n }\n\n .health-head p {\n max-width: 620px;\n margin-top: 12px;\n color: var(--muted);\n font-size: 14px;\n line-height: 1.6;\n }\n\n .health-meta {\n display: grid;\n justify-items: end;\n gap: 8px;\n color: var(--muted);\n font-size: 12px;\n white-space: nowrap;\n }\n\n .health-notice {\n padding: 13px 15px;\n border: 1px solid rgba(180, 83, 9, 0.22);\n border-radius: 8px;\n background: #fffaf2;\n color: #87520d;\n font-size: 12px;\n line-height: 1.5;\n }\n\n .health-summary {\n display: grid;\n grid-template-columns: repeat(5, minmax(0, 1fr));\n gap: 10px;\n }\n\n .health-summary article,\n .health-card {\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--surface);\n box-shadow: 0 10px 26px rgba(22, 25, 31, 0.05);\n }\n\n .health-summary article {\n min-height: 104px;\n padding: 17px;\n }\n\n .health-summary span,\n .health-card__meta,\n .health-card__score small {\n color: var(--muted);\n font-size: 11px;\n }\n\n .health-summary strong {\n display: block;\n margin-top: 12px;\n color: var(--accent);\n font-size: 28px;\n }\n\n .health-board-head {\n margin-top: 14px;\n }\n\n .health-board-head h3 {\n font-size: 21px;\n }\n\n .health-filters {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n\n .health-filter {\n padding: 7px 10px;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--muted);\n font-size: 11px;\n }\n\n .health-filter.active {\n border-color: var(--accent);\n color: var(--accent);\n background: #eff9f6;\n }\n\n .health-list {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n }\n\n .health-card {\n display: flex;\n justify-content: space-between;\n gap: 18px;\n min-width: 0;\n padding: 18px;\n }\n\n .health-card__main {\n min-width: 0;\n }\n\n .health-card__title {\n margin: 10px 0 7px;\n font-size: 16px;\n overflow-wrap: anywhere;\n }\n\n .health-card__reason {\n margin: 0;\n color: var(--muted);\n font-size: 12px;\n line-height: 1.55;\n }\n\n .health-card__meta {\n margin-top: 13px;\n line-height: 1.5;\n }\n\n .health-card__score {\n flex: 0 0 72px;\n padding-left: 14px;\n border-left: 1px solid var(--line);\n text-align: right;\n }\n\n .health-card__score strong {\n display: block;\n margin: 7px 0;\n color: var(--accent);\n font-size: 27px;\n }\n\n .health-card__empty {\n grid-column: 1 / -1;\n padding: 30px 18px;\n border: 1px dashed var(--line-strong);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.58);\n color: var(--muted);\n font-size: 13px;\n line-height: 1.6;\n text-align: center;\n }\n\n body.app-loading .shell {\n display: none;\n }\n\n body:not(.app-loading) .boot-screen {\n display: none;\n }\n\n .boot-screen {\n width: min(680px, calc(100vw - 36px));\n min-height: calc(100svh - 72px);\n margin: 0 auto;\n display: grid;\n align-items: center;\n padding: 34px 0;\n }\n\n .boot-panel {\n display: grid;\n gap: 13px;\n padding: 8px 0;\n }\n\n .boot-kicker {\n color: var(--accent);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 800;\n text-transform: uppercase;\n }\n\n .boot-panel h2 {\n color: var(--ink);\n font-size: clamp(28px, 5vw, 44px);\n line-height: 1.04;\n font-weight: 820;\n }\n\n .boot-panel p {\n color: var(--muted);\n font-size: 14px;\n line-height: 1.5;\n max-width: 520px;\n }\n\n .loading-track {\n width: 100%;\n height: 5px;\n overflow: hidden;\n border-radius: 999px;\n background: #dde4ea;\n }\n\n .loading-track::before {\n content: \"\";\n display: block;\n width: 42%;\n height: 100%;\n border-radius: inherit;\n background: var(--accent);\n animation: loading-sweep 1.05s ease-in-out infinite;\n }\n\n .pill,\n .btn {\n min-height: 36px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 7px;\n border: 1px solid var(--line);\n background: var(--surface);\n color: var(--muted);\n text-decoration: none;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 740;\n box-shadow: 0 1px 0 rgba(22, 25, 31, 0.03);\n }\n\n .pill {\n padding: 0 12px;\n border-radius: 999px;\n }\n\n .pill.ready {\n border-color: rgba(19, 111, 99, 0.28);\n background: #eff9f6;\n color: var(--accent);\n }\n\n .btn {\n padding: 0 13px;\n border-radius: 8px;\n transition: border-color 150ms ease, background 150ms ease, transform 150ms ease;\n }\n\n .btn:hover {\n transform: translateY(-1px);\n border-color: var(--line-strong);\n background: #fafbfc;\n }\n\n .lang-toggle {\n min-width: 42px;\n padding: 0 10px;\n }\n\n .mode button {\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--muted);\n font-weight: 760;\n transition: background 150ms ease, color 150ms ease;\n }\n\n .mode button.active {\n background: #ffffff;\n color: var(--ink);\n box-shadow: 0 1px 7px rgba(22, 25, 31, 0.08);\n }\n\n .platform-strip {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n }\n\n .platform-button {\n min-width: 0;\n min-height: 52px;\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--line);\n border-left: 4px solid var(--platform-color);\n border-radius: 8px;\n background: var(--surface);\n color: var(--ink);\n text-align: left;\n transition:\n transform 150ms ease,\n border-color 150ms ease,\n background 150ms ease,\n box-shadow 150ms ease;\n }\n\n .platform-button:hover {\n transform: translateY(-1px);\n border-color: var(--line-strong);\n }\n\n .platform-button.active {\n border-color: var(--platform-color);\n background:\n linear-gradient(180deg, color-mix(in srgb, var(--platform-color) 7%, white), #ffffff 68%);\n box-shadow: 0 14px 30px rgba(22, 25, 31, 0.09);\n }\n\n .platform-button[data-platform=\"longbridge\"] {\n --platform-color: var(--lb);\n }\n\n .platform-button[data-platform=\"ibkr\"] {\n --platform-color: var(--ib);\n }\n\n .platform-button[data-platform=\"schwab\"] {\n --platform-color: var(--sw);\n }\n\n .platform-button[data-platform=\"firstrade\"] {\n --platform-color: var(--ft);\n }\n\n .platform-button[data-platform=\"qmt\"] {\n --platform-color: var(--qmt);\n }\n\n .platform-button[data-platform=\"binance\"] {\n --platform-color: var(--bn);\n }\n\n .execution-cash-policy-section {\n grid-template-columns: 1fr;\n }\n\n .execution-cash-policy-heading {\n grid-column: 1 / -1;\n margin: 0;\n font-size: 13px;\n font-weight: 780;\n color: var(--ink);\n }\n\n .execution-cash-policy-note {\n grid-column: 1 / -1;\n margin: 0;\n color: var(--muted);\n font-size: 12px;\n line-height: 1.45;\n }\n\n .execution-cash-policy-grid {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 14px;\n align-items: start;\n }\n\n .policy-block-muted .selection-meta {\n color: var(--danger);\n }\n\n @media (max-width: 720px) {\n .health-head,\n .health-board-head {\n align-items: flex-start;\n flex-direction: column;\n }\n\n .health-meta {\n justify-items: start;\n }\n\n .health-summary {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .health-list {\n grid-template-columns: 1fr;\n }\n\n .execution-cash-policy-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .mark {\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n background: color-mix(in srgb, var(--platform-color) 12%, white);\n color: var(--platform-color);\n font-size: 11px;\n font-weight: 840;\n transition: background 150ms ease, color 150ms ease;\n }\n\n .platform-button.active .mark {\n background: var(--platform-color);\n color: #ffffff;\n }\n\n .platform-copy {\n min-width: 0;\n display: grid;\n gap: 3px;\n }\n\n .platform-copy strong {\n font-size: 13px;\n line-height: 1.18;\n overflow-wrap: anywhere;\n }\n\n .platform-strip .platform-copy span,\n .platform-strip .platform-copy small {\n display: none;\n }\n\n .platform-copy span {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.35;\n overflow-wrap: anywhere;\n }\n\n .platform-copy small {\n color: var(--platform-color);\n font-size: 11px;\n line-height: 1.25;\n font-weight: 780;\n overflow-wrap: anywhere;\n }\n\n .switch-surface {\n display: grid;\n grid-template-columns: minmax(0, 1.04fr) minmax(320px, 0.96fr);\n gap: 18px;\n align-items: stretch;\n }\n\n .switch-surface.summary-hidden {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .switch-panel,\n .summary-panel {\n min-width: 0;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--surface);\n box-shadow: 0 14px 34px rgba(22, 25, 31, 0.07);\n }\n\n .switch-panel {\n display: grid;\n gap: 20px;\n padding: 22px;\n }\n\n .panel-head {\n display: grid;\n gap: 5px;\n }\n\n .eyebrow {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 780;\n text-transform: uppercase;\n }\n\n .panel-head h2 {\n font-size: clamp(24px, 3vw, 34px);\n line-height: 1.08;\n font-weight: 820;\n overflow-wrap: anywhere;\n }\n\n .quick-form {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n align-items: start;\n gap: 16px 14px;\n }\n\n .control-block {\n display: grid;\n grid-template-rows: auto 48px minmax(20px, auto);\n align-content: start;\n gap: 8px;\n min-width: 0;\n color: var(--muted);\n font-size: 12px;\n font-weight: 760;\n }\n\n .form-section {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n align-items: start;\n gap: 14px;\n }\n\n .form-section + .form-section {\n padding-top: 16px;\n border-top: 1px solid var(--line);\n }\n\n .section-wide {\n grid-column: 1 / -1;\n }\n\n .mode-block {\n grid-column: 1 / -1;\n }\n\n select,\n input {\n width: 100%;\n min-width: 0;\n height: 48px;\n padding: 0 13px;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: #ffffff;\n color: var(--ink);\n outline: none;\n font-size: 14px;\n font-weight: 680;\n overflow: hidden;\n padding-right: 42px;\n text-overflow: ellipsis;\n transition: border-color 150ms ease, box-shadow 150ms ease;\n }\n\n select:focus,\n input:focus {\n border-color: var(--platform-color, var(--accent));\n box-shadow: 0 0 0 3px rgba(19, 111, 99, 0.12);\n }\n\n input::placeholder {\n color: #9aa3ad;\n opacity: 1;\n }\n\n .selection-meta {\n min-height: 20px;\n color: var(--muted);\n font-size: 12px;\n line-height: 1.45;\n white-space: pre-line;\n overflow-wrap: anywhere;\n }\n\n .mode {\n height: 48px;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 4px;\n padding: 4px;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--soft);\n }\n\n .mode button {\n font-size: 13px;\n }\n\n .run-area {\n display: grid;\n gap: 10px;\n padding-top: 2px;\n }\n\n .public-note {\n color: var(--muted);\n font-size: 14px;\n line-height: 1.55;\n max-width: 560px;\n }\n\n .run-button {\n min-height: 56px;\n border: 0;\n border-radius: 8px;\n background: var(--platform-color, var(--accent));\n color: var(--accent-ink);\n font-size: 16px;\n font-weight: 820;\n transition: transform 150ms ease, filter 150ms ease, opacity 150ms ease;\n }\n\n .run-button:hover:not(:disabled) {\n transform: translateY(-1px);\n filter: brightness(0.92);\n }\n\n .run-button:disabled {\n opacity: 0.48;\n cursor: not-allowed;\n transform: none;\n }\n\n .action-note {\n min-height: 20px;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.45;\n white-space: pre-line;\n }\n\n .action-note.warning {\n color: var(--danger);\n font-weight: 720;\n }\n\n .summary-panel {\n display: grid;\n grid-template-rows: auto 1fr auto;\n overflow: hidden;\n }\n\n .summary-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px;\n border-bottom: 1px solid var(--line);\n }\n\n .summary-head h2 {\n font-size: 15px;\n line-height: 1.2;\n font-weight: 800;\n }\n\n .summary-list {\n margin: 0;\n padding: 8px 16px;\n display: grid;\n gap: 0;\n }\n\n .summary-row {\n display: grid;\n grid-template-columns: 128px minmax(0, 1fr);\n gap: 12px;\n padding: 10px 0;\n border-bottom: 1px solid #eef1f4;\n align-items: start;\n }\n\n .summary-row:last-child {\n border-bottom: 0;\n }\n\n .summary-row.pending {\n margin: 0 -16px;\n padding-inline: 13px 16px;\n border-left: 3px solid var(--platform-color, var(--accent));\n background: color-mix(in srgb, var(--platform-color, var(--accent)) 6%, #ffffff);\n }\n\n .summary-label {\n color: var(--muted);\n font-size: 12px;\n line-height: 1.4;\n font-weight: 760;\n }\n\n .summary-value {\n margin: 0;\n min-width: 0;\n color: var(--ink);\n font-size: 13px;\n line-height: 1.45;\n font-weight: 680;\n overflow-wrap: anywhere;\n }\n\n .summary-row.pending .summary-value {\n color: var(--platform-color, var(--accent));\n font-weight: 800;\n }\n\n .summary-status {\n min-height: 24px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0 9px;\n border: 1px solid rgba(19, 111, 99, 0.28);\n border-radius: 999px;\n background: #eff9f6;\n color: var(--accent);\n font-size: 12px;\n line-height: 1.2;\n font-weight: 800;\n white-space: nowrap;\n }\n\n .summary-status.disabled {\n border-color: rgba(181, 65, 55, 0.32);\n background: #fff1ef;\n color: var(--danger);\n }\n\n .summary-status.neutral {\n border-color: var(--line);\n background: #f6f7f8;\n color: var(--muted);\n }\n\n .summary-row.pending .summary-status {\n font-weight: 840;\n }\n\n .summary-actions {\n display: flex;\n gap: 10px;\n flex-wrap: wrap;\n padding: 15px 16px;\n border-top: 1px solid var(--line);\n background: #fbfcfd;\n }\n\n .toast {\n min-height: 20px;\n color: var(--accent);\n font-size: 13px;\n line-height: 1.45;\n }\n\n .quiet {\n color: var(--muted);\n }\n\n /* Modern visual polish layer: keeps the existing markup and behavior intact. */\n :root {\n --bg: #f4f8fb;\n --surface: rgba(255, 255, 255, 0.94);\n --soft: #f1f5f8;\n --ink: #101725;\n --muted: #617083;\n --line: #d9e2ea;\n --line-strong: #b8c7d4;\n --accent: #08796f;\n --accent-ink: #ffffff;\n --danger: #b54137;\n --radius-lg: 22px;\n --radius-md: 16px;\n --shadow-soft: 0 18px 48px rgba(16, 23, 37, 0.08);\n --shadow-panel: 0 22px 70px rgba(25, 45, 68, 0.11);\n }\n\n body {\n position: relative;\n overflow-x: hidden;\n background:\n radial-gradient(circle at 14% 0%, rgba(8, 121, 111, 0.12), transparent 34%),\n radial-gradient(circle at 86% 4%, rgba(47, 95, 159, 0.11), transparent 32%),\n linear-gradient(180deg, #fbfdff 0%, var(--bg) 55%, #eef4f8 100%);\n }\n\n body::before {\n content: \"\";\n position: fixed;\n inset: 0;\n z-index: -1;\n pointer-events: none;\n opacity: 0.42;\n background-image:\n linear-gradient(rgba(8, 121, 111, 0.07) 1px, transparent 1px),\n linear-gradient(90deg, rgba(47, 95, 159, 0.055) 1px, transparent 1px),\n linear-gradient(135deg, transparent 0 44%, rgba(8, 121, 111, 0.08) 45% 46%, transparent 47% 100%);\n background-size: 72px 72px, 72px 72px, 340px 190px;\n -webkit-mask-image: linear-gradient(180deg, #000 0, rgba(0, 0, 0, 0.72) 260px, transparent 680px);\n mask-image: linear-gradient(180deg, #000 0, rgba(0, 0, 0, 0.72) 260px, transparent 680px);\n }\n\n .topbar {\n width: min(1400px, calc(100vw - 28px));\n min-height: 88px;\n margin: 10px auto 0;\n padding: 18px 22px;\n top: 10px;\n border: 1px solid rgba(210, 222, 232, 0.9);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.82);\n box-shadow: 0 14px 42px rgba(22, 35, 53, 0.08);\n backdrop-filter: blur(22px);\n }\n\n .brand {\n grid-template-columns: 46px minmax(0, 1fr);\n column-gap: 12px;\n align-items: center;\n }\n\n .brand::before {\n content: \"↗\";\n grid-row: 1 / 3;\n width: 44px;\n height: 44px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background:\n radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.88), transparent 28%),\n linear-gradient(135deg, #0b8a80, #075f62 72%);\n color: #ffffff;\n font-size: 22px;\n font-weight: 860;\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25), 0 10px 24px rgba(8, 121, 111, 0.22);\n }\n\n h1 {\n font-size: clamp(24px, 2vw, 30px);\n letter-spacing: -0.03em;\n }\n\n .brand p {\n color: #66758a;\n font-size: 13px;\n }\n\n .btn,\n .pill {\n min-height: 40px;\n border-color: rgba(203, 215, 226, 0.9);\n border-radius: 12px;\n background: rgba(255, 255, 255, 0.82);\n color: #45566b;\n box-shadow: 0 1px 0 rgba(255, 255, 255, 0.75), 0 8px 20px rgba(22, 35, 53, 0.05);\n }\n\n .btn:hover {\n border-color: rgba(145, 163, 181, 0.86);\n background: #ffffff;\n box-shadow: 0 12px 24px rgba(22, 35, 53, 0.08);\n }\n\n .shell {\n width: min(1400px, calc(100vw - 36px));\n padding: 12px 0 42px;\n gap: 12px;\n }\n\n .platform-strip {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 8px;\n padding: 10px 12px;\n border: 1px solid rgba(210, 222, 232, 0.85);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.74);\n box-shadow: var(--shadow-soft);\n backdrop-filter: blur(16px);\n }\n\n .platform-button {\n position: relative;\n min-height: 52px;\n gap: 8px;\n padding: 8px 28px 8px 8px;\n border: 1px solid rgba(207, 218, 229, 0.9);\n border-left: 1px solid rgba(207, 218, 229, 0.9);\n border-radius: var(--radius-md);\n background:\n linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 252, 254, 0.9));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);\n }\n\n .platform-button:hover {\n transform: translateY(-2px);\n border-color: color-mix(in srgb, var(--platform-color) 34%, var(--line));\n box-shadow: 0 14px 28px rgba(22, 35, 53, 0.08);\n }\n\n .platform-button.active {\n border-color: color-mix(in srgb, var(--platform-color) 58%, var(--line));\n background:\n radial-gradient(circle at 12% 18%, color-mix(in srgb, var(--platform-color) 16%, transparent), transparent 42%),\n linear-gradient(180deg, #ffffff, color-mix(in srgb, var(--platform-color) 5%, #ffffff));\n box-shadow: 0 16px 34px color-mix(in srgb, var(--platform-color) 18%, transparent);\n }\n\n .platform-button.active::after {\n content: \"✓\";\n position: absolute;\n top: 50%;\n right: 8px;\n width: 18px;\n height: 18px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n background: var(--platform-color);\n color: #ffffff;\n font-size: 11px;\n font-weight: 880;\n transform: translateY(-50%);\n box-shadow: 0 8px 18px color-mix(in srgb, var(--platform-color) 24%, transparent);\n }\n\n .mark {\n width: 32px;\n height: 32px;\n border-radius: 10px;\n background: color-mix(in srgb, var(--platform-color) 10%, #f7fafc);\n box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--platform-color) 10%, transparent);\n }\n\n .platform-button.active .mark {\n background: linear-gradient(135deg, var(--platform-color), color-mix(in srgb, var(--platform-color) 72%, #052f36));\n box-shadow: 0 12px 24px color-mix(in srgb, var(--platform-color) 22%, transparent);\n }\n\n .platform-copy strong {\n color: #121a28;\n font-size: 13px;\n letter-spacing: -0.015em;\n }\n\n .platform-copy span,\n .platform-copy small {\n line-height: 1.35;\n }\n\n .switch-surface {\n grid-template-columns: minmax(0, 1.56fr) minmax(390px, 0.94fr);\n gap: 14px;\n align-items: start;\n }\n\n .switch-panel,\n .summary-panel {\n border: 1px solid rgba(210, 222, 232, 0.92);\n border-radius: var(--radius-lg);\n background: rgba(255, 255, 255, 0.86);\n box-shadow: var(--shadow-panel);\n backdrop-filter: blur(18px);\n }\n\n .switch-panel {\n gap: 18px;\n padding: 24px;\n }\n\n .panel-head {\n padding-bottom: 2px;\n }\n\n .eyebrow {\n color: #64748b;\n font-size: 12px;\n letter-spacing: 0.04em;\n }\n\n .panel-head h2 {\n color: #111827;\n font-size: clamp(31px, 3vw, 42px);\n letter-spacing: -0.045em;\n }\n\n .public-note {\n max-width: 720px;\n color: #53657a;\n font-size: 15px;\n }\n\n .public-preview {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 12px;\n margin-top: 4px;\n }\n\n .public-preview-card {\n min-width: 0;\n display: grid;\n gap: 6px;\n padding: 16px;\n border: 1px solid rgba(212, 223, 234, 0.86);\n border-radius: var(--radius-md);\n background:\n linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 252, 0.88));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86);\n }\n\n .public-preview-card strong {\n color: #152033;\n font-size: 14px;\n line-height: 1.3;\n font-weight: 800;\n }\n\n .public-preview-card span {\n color: #64748b;\n font-size: 12px;\n line-height: 1.5;\n }\n\n .quick-form {\n gap: 14px;\n }\n\n .form-section {\n position: relative;\n gap: 14px 16px;\n padding: 16px;\n border: 1px solid rgba(215, 225, 235, 0.86);\n border-radius: var(--radius-md);\n background:\n linear-gradient(90deg, color-mix(in srgb, var(--platform-color, var(--accent)) 5%, transparent), transparent 32%),\n linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(250, 252, 254, 0.88));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);\n }\n\n .form-section + .form-section {\n padding-top: 16px;\n border-top: 1px solid rgba(215, 225, 235, 0.86);\n }\n\n .control-block {\n grid-template-rows: auto 46px minmax(20px, auto);\n gap: 8px;\n color: #53657a;\n font-size: 12px;\n line-height: 1.25;\n font-weight: 790;\n }\n\n .control-block > span:first-child {\n color: #293548;\n }\n\n select,\n input,\n .mode {\n min-height: 46px;\n border-color: rgba(207, 219, 230, 0.92);\n border-radius: 12px;\n background: rgba(255, 255, 255, 0.92);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 8px 20px rgba(22, 35, 53, 0.035);\n }\n\n select,\n input {\n height: 46px;\n color: #172033;\n font-size: 14px;\n font-weight: 700;\n }\n\n select:disabled,\n input:disabled {\n background: rgba(241, 245, 249, 0.72);\n color: #7d8da1;\n box-shadow: none;\n }\n\n .mode {\n height: 46px;\n background: rgba(238, 244, 248, 0.9);\n }\n\n .mode button {\n border-radius: 9px;\n color: #5c6e84;\n font-size: 13px;\n font-weight: 800;\n }\n\n .mode button.active {\n background: #ffffff;\n color: var(--platform-color, var(--accent));\n box-shadow: 0 10px 20px rgba(22, 35, 53, 0.08);\n }\n\n .selection-meta {\n color: #758397;\n font-size: 12px;\n }\n\n .run-area {\n gap: 10px;\n padding-top: 2px;\n }\n\n .run-button {\n width: 100%;\n min-height: 58px;\n border-radius: 12px;\n background:\n linear-gradient(135deg, var(--platform-color, var(--accent)), color-mix(in srgb, var(--platform-color, var(--accent)) 76%, #042e35));\n box-shadow: 0 18px 36px color-mix(in srgb, var(--platform-color, var(--accent)) 24%, transparent);\n letter-spacing: 0.02em;\n }\n\n .run-button:hover:not(:disabled) {\n filter: none;\n box-shadow: 0 22px 42px color-mix(in srgb, var(--platform-color, var(--accent)) 30%, transparent);\n }\n\n .run-button:disabled {\n box-shadow: none;\n }\n\n .action-note {\n color: #53657a;\n font-size: 13px;\n }\n\n .toast {\n grid-column: 1 / -1;\n color: var(--platform-color, var(--accent));\n font-weight: 720;\n }\n\n .summary-panel {\n position: sticky;\n top: 112px;\n overflow: hidden;\n }\n\n .summary-head {\n padding: 24px 24px 18px;\n border-bottom-color: rgba(218, 227, 236, 0.9);\n }\n\n .summary-head h2 {\n color: #111827;\n font-size: 22px;\n letter-spacing: -0.03em;\n }\n\n .summary-list {\n padding: 4px 24px 18px;\n }\n\n .summary-row {\n grid-template-columns: 132px minmax(0, 1fr);\n gap: 14px;\n padding: 14px 0;\n border-bottom-color: rgba(229, 236, 242, 0.96);\n }\n\n .summary-label {\n color: #66758a;\n font-size: 12px;\n font-weight: 790;\n }\n\n .summary-value {\n color: #263244;\n font-size: 13px;\n font-weight: 700;\n }\n\n .summary-row.pending {\n margin: 0 -24px;\n padding-inline: 21px 24px;\n border-left: 3px solid var(--platform-color, var(--accent));\n background:\n linear-gradient(90deg, color-mix(in srgb, var(--platform-color, var(--accent)) 9%, #ffffff), rgba(255, 255, 255, 0.72));\n }\n\n .summary-status {\n min-height: 26px;\n border-color: color-mix(in srgb, var(--platform-color, var(--accent)) 26%, #d9e2ea);\n background: color-mix(in srgb, var(--platform-color, var(--accent)) 9%, #ffffff);\n color: var(--platform-color, var(--accent));\n }\n\n .summary-actions {\n padding: 16px 24px 24px;\n border-top-color: rgba(218, 227, 236, 0.9);\n background: transparent;\n }\n\n @media (max-width: 820px) {\n .platform-strip {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 6px;\n padding: 8px 10px;\n }\n\n .platform-copy strong {\n font-size: 12px;\n }\n\n .switch-surface {\n grid-template-columns: 1fr;\n }\n\n .summary-panel {\n position: static;\n }\n\n .public-preview {\n grid-template-columns: 1fr;\n }\n }\n\n\n @media (max-width: 540px) {\n .platform-strip {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 5px;\n padding: 6px 8px;\n }\n .platform-button {\n min-height: 40px;\n gap: 5px;\n padding: 5px 7px;\n }\n .mark {\n width: 26px;\n height: 26px;\n font-size: 10px;\n }\n .platform-copy strong {\n font-size: 11px;\n }\n }\n\n @media (max-width: 680px) {\n .topbar {\n position: static;\n align-items: flex-start;\n flex-direction: column;\n padding: 18px;\n }\n\n .top-actions {\n justify-content: flex-start;\n width: 100%;\n }\n\n .boot-screen {\n width: min(100% - 28px, 680px);\n min-height: calc(100svh - 122px);\n padding-top: 22px;\n }\n\n .shell {\n width: min(100% - 28px, 1160px);\n padding-top: 18px;\n }\n\n .platform-strip,\n .quick-form,\n .form-section {\n grid-template-columns: 1fr;\n }\n\n .platform-strip {\n grid-template-columns: repeat(5, minmax(68px, 1fr));\n gap: 6px;\n padding: 8px;\n overflow-x: auto;\n }\n\n .platform-button {\n min-height: 48px;\n padding-inline: 8px 24px;\n }\n\n .platform-copy strong {\n font-size: 11px;\n }\n\n .mode-block,\n .section-wide {\n grid-column: auto;\n }\n\n .switch-panel {\n padding: 18px;\n }\n\n .summary-row {\n grid-template-columns: 1fr;\n gap: 3px;\n }\n\n .run-area {\n grid-template-columns: 1fr;\n }\n\n select {\n font-size: 13px;\n }\n }\n\n @media (prefers-reduced-motion: no-preference) {\n .platform-strip,\n .switch-surface {\n animation: rise-in 260ms ease both;\n }\n\n .switch-surface {\n animation-delay: 60ms;\n }\n\n @keyframes rise-in {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n @keyframes loading-sweep {\n from {\n transform: translateX(-110%);\n }\n to {\n transform: translateX(260%);\n }\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .loading-track::before {\n animation: none;\n transform: translateX(0);\n }\n }\n"; diff --git a/web/strategy-switch-console/app_js.js b/web/strategy-switch-console/app_js.js index 72fceda..81331bb 100644 --- a/web/strategy-switch-console/app_js.js +++ b/web/strategy-switch-console/app_js.js @@ -1,2 +1,2 @@ // Generated — JS asset -export const APP_JS = "\n\n let platformMeta = {\n binance: { label: \"Binance\", code: \"BN\", accent: \"var(--bn)\" },\n firstrade: { label: \"Firstrade\", code: \"FT\", accent: \"var(--ft)\" },\n ibkr: { label: \"IBKR\", code: \"IB\", accent: \"var(--ib)\" },\n longbridge: { label: \"LongBridge\", code: \"LB\", accent: \"var(--lb)\" },\n qmt: { label: \"QMT\", code: \"QM\", accent: \"var(--qmt)\" },\n schwab: { label: \"Schwab\", code: \"SW\", accent: \"var(--sw)\" },\n };\n\n const platformRepositories = {\n binance: \"QuantStrategyLab/BinancePlatform\",\n firstrade: \"QuantStrategyLab/FirstradePlatform\",\n ibkr: \"QuantStrategyLab/InteractiveBrokersPlatform\",\n longbridge: \"QuantStrategyLab/LongBridgePlatform\",\n qmt: \"QuantStrategyLab/QmtPlatform\",\n schwab: \"QuantStrategyLab/CharlesSchwabPlatform\",\n };\n // Alias for backward compatibility\n const defaultRepositories = platformRepositories;\n\n const defaultAccountOptions = window.__DEFAULT_ACCOUNT_OPTIONS__ || {\n binance: [{\"key\": \"default\", \"label\": \"Binance\", \"target_name\": \"default\", \"cash_currency\": \"USD\", \"supported_domains\": [\"crypto\"]}],\n firstrade: [{\"key\": \"preview\", \"label\": \"Firstrade\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"firstrade-quant-service\"}],\n ibkr: [{\"key\": \"preview\", \"label\": \"IBKR\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n longbridge: [{\"key\": \"preview\", \"label\": \"LongBridge\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n qmt: [{\"key\": \"default\", \"label\": \"QMT\", \"target_name\": \"default\", \"cash_currency\": \"CNY\", \"supported_domains\": [\"cn_equity\"], \"service_name\": \"qmt-quant-service\"}],\n schwab: [{\"key\": \"preview\", \"label\": \"Schwab\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"charles-schwab-quant-service\"}],\n };\n\n const domainLabels = window.__DOMAIN_LABELS__ || {\n cn_equity: { zh: \"A股\", en: \"CN A-share\" },\n crypto: { zh: \"加密\", en: \"Crypto\" },\n hk_equity: { zh: \"港股\", en: \"HK Equity\" },\n us_equity: { zh: \"美股\", en: \"US Equity\" },\n };\n\n const platformConfig = window.__PLATFORM_CONFIG__ || {\n binance: {\n dry_run_only: false,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n firstrade: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"firstrade-quant-service\",\n default_execution_mode: \"live\"\n },\n ibkr: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n longbridge: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n qmt: {\n dry_run_only: true,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"paper\",\n service_name: \"qmt-quant-service\",\n default_execution_mode: \"paper\"\n },\n schwab: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"charles-schwab-quant-service\",\n default_execution_mode: \"live\"\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 const reservePolicyModes = [\"none\", \"ratio\", \"floor\", \"max\"];\n const incomeLayerModes = [\"enabled\", \"disabled\"];\n const optionOverlayModes = [\"enabled\", \"disabled\"];\n const cashOnlyExecutionModes = [\"enabled\", \"disabled\"];\n const runtimeTargetModes = [\"enabled\", \"disabled\"];\n const pluginModes = [\"auto\", \"none\"];\n const dcaModes = [\"fixed\", \"smart\"];\n const runtimeTargetEnabledVariable = \"RUNTIME_TARGET_ENABLED\";\n const incomeLayerEnabledVariable = \"INCOME_LAYER_ENABLED\";\n const incomeLayerStartUsdVariable = \"INCOME_LAYER_START_USD\";\n const incomeLayerMaxRatioVariable = \"INCOME_LAYER_MAX_RATIO\";\n const dcaProfileDefaults = window.__DCA_PROFILE_DEFAULTS__ || {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n };\n const APP_BOOT_TIMEOUT_MS = 15000;\n const platformMinReservedCashVariables = {\n longbridge: \"LONGBRIDGE_MIN_RESERVED_CASH_USD\",\n ibkr: \"IBKR_MIN_RESERVED_CASH_USD\",\n schwab: \"SCHWAB_MIN_RESERVED_CASH_USD\",\n firstrade: \"FIRSTRADE_MIN_RESERVED_CASH_USD\",\n };\n const platformReservedCashRatioVariables = {\n longbridge: \"LONGBRIDGE_RESERVED_CASH_RATIO\",\n ibkr: \"IBKR_RESERVED_CASH_RATIO\",\n schwab: \"SCHWAB_RESERVED_CASH_RATIO\",\n firstrade: \"FIRSTRADE_RESERVED_CASH_RATIO\",\n };\n\n const defaultStrategyProfiles = window.__DEFAULT_STRATEGY_PROFILES__ || [\n {\n \"profile\": \"tqqq_growth_income\",\n \"label\": \"纳斯达克增长收益\",\n \"label_en\": \"NASDAQ Growth Income\",\n \"label_zh\": \"纳斯达克增长收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"250000\",\n \"income_layer_max_ratio\": \"0.55\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.3,\n \"DGRO\": 0.2,\n \"SGOV\": 0.4,\n \"SPYI\": 0.08,\n \"QQQI\": 0.02\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"tqqq_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"250000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.03\"\n },\n {\n \"profile\": \"soxl_soxx_trend_income\",\n \"label\": \"半导体趋势收益\",\n \"label_en\": \"Semiconductor Trend Income\",\n \"label_zh\": \"半导体趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"150000\",\n \"income_layer_max_ratio\": \"0.95\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.15,\n \"DGRO\": 0.1,\n \"SGOV\": 0.7,\n \"SPYI\": 0.04,\n \"QQQI\": 0.01\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_income_overlay_enabled\": true,\n \"option_income_overlay_recipe\": \"soxx_put_credit_spread_income_v1\",\n \"option_income_overlay_start_usd\": \"150000\",\n \"option_income_overlay_nav_risk_ratio\": \"0.01\"\n },\n {\n \"profile\": \"nasdaq_sp500_smart_dca\",\n \"label\": \"纳指标普定投\",\n \"label_en\": \"NASDAQ/S&P 500 DCA\",\n \"label_zh\": \"纳指标普定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"ibit_smart_dca\",\n \"label\": \"IBIT比特币定投\",\n \"label_en\": \"IBIT Bitcoin DCA\",\n \"label_zh\": \"IBIT比特币定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"global_etf_rotation\",\n \"label\": \"全球ETF轮动\",\n \"label_en\": \"Global ETF Rotation\",\n \"label_zh\": \"全球ETF轮动\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"500000\",\n \"income_layer_max_ratio\": \"0.15\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.4,\n \"DGRO\": 0.25,\n \"SGOV\": 0.3,\n \"SPYI\": 0.05\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"500000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"russell_top50_leader_rotation\",\n \"label\": \"罗素Top50领涨\",\n \"label_en\": \"Russell Top50 Leaders\",\n \"label_zh\": \"罗素Top50领涨\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.45,\n \"DGRO\": 0.3,\n \"SGOV\": 0.25\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"tecl_xlk_trend_income\",\n \"label\": \"TECL/XLK趋势收益\",\n \"label_en\": \"TECL/XLK Trend Income\",\n \"label_zh\": \"TECL/XLK趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"failed_promotion_vs_live_profiles\"\n },\n {\n \"profile\": \"us_equity_combo\",\n \"label\": \"美股核心组合\",\n \"label_en\": \"US Core Combo\",\n \"label_zh\": \"美股核心组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.25,\n \"DGRO\": 0.25,\n \"SGOV\": 0.2,\n \"SPYI\": 0.15,\n \"QQQI\": 0.15\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"us_equity_combo_core\",\n \"label\": \"美股核心组合影子\",\n \"label_en\": \"US Core Combo Shadow\",\n \"label_zh\": \"美股核心组合影子\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"us_equity_combo_leveraged\",\n \"label\": \"美股加速组合\",\n \"label_en\": \"US Alpha Combo\",\n \"label_zh\": \"美股加速组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"hk_global_etf_tactical_rotation\",\n \"label\": \"港股ETF战术轮动\",\n \"label_en\": \"HK ETF Tactical Rotation\",\n \"label_zh\": \"港股ETF战术轮动\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_low_vol_dividend_quality_snapshot\",\n \"label\": \"港股红利质量\",\n \"label_en\": \"HK Dividend Quality\",\n \"label_zh\": \"港股红利质量\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_equity_combo\",\n \"label\": \"港股恒生组合\",\n \"label_en\": \"HK Core Combo\",\n \"label_zh\": \"港股恒生组合\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation\",\n \"label\": \"A股行业ETF轮动\",\n \"label_en\": \"CN Industry ETF Rotation\",\n \"label_zh\": \"A股行业ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation_aggressive\",\n \"label\": \"A股ETF轮动\",\n \"label_en\": \"CN ETF Rotation\",\n \"label_zh\": \"A股ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"live_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"live_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_index_etf_tactical_rotation\",\n \"label\": \"A股宽基ETF战术轮动\",\n \"label_en\": \"CN Index ETF Tactical Rotation\",\n \"label_zh\": \"A股宽基ETF战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_tactical_rotation\",\n \"label\": \"创业板战术轮动\",\n \"label_en\": \"CN ChiNext Tactical Rotation\",\n \"label_zh\": \"创业板战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality\",\n \"label\": \"创业板成长动量质量\",\n \"label_en\": \"CN ChiNext Growth Momentum Quality\",\n \"label_zh\": \"创业板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_dividend_quality_snapshot\",\n \"label\": \"A股红利质量\",\n \"label_en\": \"CN Dividend Quality\",\n \"label_zh\": \"A股红利质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality_snapshot\",\n \"label\": \"创业板成长质量快照\",\n \"label_en\": \"CN ChiNext Growth Quality Snapshot\",\n \"label_zh\": \"创业板成长质量快照\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_star_growth_momentum_quality\",\n \"label\": \"科创板成长动量质量\",\n \"label_en\": \"CN STAR Growth Momentum Quality\",\n \"label_zh\": \"科创板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_equity_combo\",\n \"label\": \"A股进取组合\",\n \"label_en\": \"CN Alpha Combo\",\n \"label_zh\": \"A股进取组合\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"crypto_live_pool_rotation\",\n \"label\": \"加密实时池轮动\",\n \"label_en\": \"Crypto Live Pool Rotation\",\n \"label_zh\": \"加密实时池轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"crypto_btc_dca\",\n \"label\": \"BTC定投\",\n \"label_en\": \"BTC DCA\",\n \"label_zh\": \"BTC定投\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_trend_rotation\",\n \"label\": \"山寨趋势轮动\",\n \"label_en\": \"Altcoin Trend\",\n \"label_zh\": \"山寨趋势轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_equity_combo\",\n \"label\": \"加密动量组合\",\n \"label_en\": \"Crypto Core Combo\",\n \"label_zh\": \"加密动量组合\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"纳斯达克增长收益\", en: \"NASDAQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"半导体趋势收益\", en: \"Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指标普定投\", en: \"NASDAQ/S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT比特币定投\", en: \"IBIT Bitcoin DCA\" },\n global_etf_rotation: { zh: \"全球ETF轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素Top50领涨\", en: \"Russell Top50 Leaders\" },\n hk_global_etf_tactical_rotation: { zh: \"港股ETF战术轮动\", en: \"HK ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股红利质量\", en: \"HK Dividend Quality\" },\n cn_industry_etf_rotation: { zh: \"A股行业ETF轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量\", en: \"CN Dividend Quality\" },\n us_equity_combo: { zh: \"美股核心组合\", en: \"US Core Combo\" },\n us_equity_combo_leveraged: { zh: \"美股加速组合\", en: \"US Alpha Combo\" },\n hk_equity_combo: { zh: \"港股恒生组合\", en: \"HK Core Combo\" },\n cn_industry_etf_rotation_aggressive: { zh: \"A股ETF轮动\", en: \"CN ETF Rotation\" },\n cn_stock_momentum_rotation: { zh: \"A股个股动量\", en: \"CN Stock Momentum\" },\n cn_equity_combo: { zh: \"A股进取组合\", en: \"CN Alpha Combo\" },\n crypto_btc_dca: { zh: \"BTC定投\", en: \"BTC DCA\" },\n crypto_trend_rotation: { zh: \"山寨趋势轮动\", en: \"Altcoin Trend\" },\n crypto_equity_combo: { zh: \"加密动量组合\", en: \"Crypto Core Combo\" },\n };\n\n const fallbackIncomeLayerDefaults = window.__INCOME_LAYER_DEFAULTS__ || {\n tqqq_growth_income: {\n startUsd: 250000,\n maxRatio: \"0.55\",\n allocations: { SCHD: 0.30, DGRO: 0.20, SGOV: 0.40, SPYI: 0.08, QQQI: 0.02 },\n },\n soxl_soxx_trend_income: {\n startUsd: 150000,\n maxRatio: \"0.95\",\n allocations: { SCHD: 0.15, DGRO: 0.10, SGOV: 0.70, SPYI: 0.04, QQQI: 0.01 },\n },\n global_etf_rotation: {\n startUsd: 500000,\n maxRatio: \"0.15\",\n allocations: { SCHD: 0.40, DGRO: 0.25, SGOV: 0.30, SPYI: 0.05 },\n },\n russell_top50_leader_rotation: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.45, DGRO: 0.30, SGOV: 0.25 },\n },\n us_equity_combo: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.20, SPYI: 0.15, QQQI: 0.15 },\n }};\n let incomeLayerDefaults = {};\n const fallbackOptionOverlayDefaults = window.__OPTION_OVERLAY_DEFAULTS__ || {\n tqqq_growth_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"tqqq_leaps_growth_v1\", startUsd: \"250000\", ratio: \"0.03\", ratioKind: \"budget\" },\n ],\n },\n soxl_soxx_trend_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"income\", recipe: \"soxx_put_credit_spread_income_v1\", startUsd: \"150000\", ratio: \"0.01\", ratioKind: \"risk\" },\n ],\n },\n global_etf_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"500000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n russell_top50_leader_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo_leveraged: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [],\n }};\n let optionOverlayDefaults = {};\n\n const strategyDomains = [\"us_equity\", \"hk_equity\", \"cn_equity\", \"crypto\"];\n let strategyOptions = [];\n let strategyLabels = {};\n let strategyCatalog = {};\n\n\n const copy = {\n zh: {\n appTitle: \"策略切换\",\n appSubtitle: \"选平台、目标账号和策略,一次执行完成切换。\",\n bootKicker: \"初始化控制台\",\n bootTitle: \"读取策略配置\",\n bootMessage: \"正在读取登录状态、账号配置和当前状态。\",\n bootStrategy: \"正在读取策略目录。\",\n bootSession: \"正在验证登录状态。\",\n bootConfig: \"正在读取账号配置和当前状态。\",\n bootTimeout: \"加载超时,已切换到公开预览(登录后可重试)。\",\n bootPublic: \"公开预览已就绪。\",\n login: \"登录\",\n logout: \"退出\",\n signedInAs: \"已登录 {login}\",\n activePlatform: \"当前平台\",\n account: \"目标账号\",\n strategy: \"策略\",\n mode: \"模式\",\n live: \"实盘\",\n paper: \"模拟\",\n runtimeTargetMode: \"账号运行状态\",\n runtimeSectionTitle: \"运行与插件\",\n runtimeTargetCurrent: \"沿用当前状态\",\n runtimeTargetEnabled: \"启用\",\n runtimeTargetDisabled: \"禁用\",\n runtimeTargetModeMeta: \"停用后正式运行会跳过,模拟运行和健康检查仍可用。\",\n pluginMode: \"插件启用范围\",\n pluginModeAuto: \"启用插件\",\n pluginModeNone: \"禁用插件\",\n pluginModeMeta: \"选择是否启用该策略的插件。\",\n incomeLayerMode: \"收入层状态\",\n incomeLayerSectionTitle: \"收入层\",\n incomeLayerCurrent: \"沿用当前配置\",\n incomeLayerEnabled: \"开启收入层\",\n incomeLayerDisabled: \"关闭收入层\",\n incomeLayerNotSupported: \"该策略未定义收入层\",\n incomeLayerStartUsd: \"收入层起始金额\",\n incomeLayerMaxRatio: \"收入层最高比例\",\n incomeLayerModeMeta: \"仅对已定义收入层的美股策略生效。\",\n incomeLayerDefaultMeta: \"策略默认:起始 {start},最高 {ratio}。\",\n incomeLayerAllocationMeta: \"默认分配:{allocations}。\",\n incomeLayerStartMeta: \"总资产达到该金额后启用收入层。\",\n incomeLayerRatioMeta: \"例如 0.55 表示最高 55%。\",\n optionOverlayMode: \"期权层状态\",\n optionOverlaySectionTitle: \"期权层\",\n optionOverlayCurrent: \"沿用当前配置\",\n optionOverlayEnabled: \"启用期权层\",\n optionOverlayDisabled: \"关闭期权层\",\n optionOverlayNotSupported: \"该策略未定义期权层\",\n optionOverlayModeMeta: \"启用时使用策略默认的最佳 recipe 和预算,不在这里手动调比例。\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"增长\",\n optionOverlayFamilyIncome: \"收入\",\n optionOverlayBudgetRatio: \"预算 {ratio}\",\n optionOverlayRiskRatio: \"风险 {ratio}\",\n cashOnlyExecutionMode: \"允许融资\",\n cashOnlyExecutionCurrent: \"沿用当前配置\",\n cashOnlyExecutionYes: \"是\",\n cashOnlyExecutionNo: \"否\",\n cashOnlyExecutionModeMeta: \"选「否」时只按真实现金下单,不会动用 margin 购买力。\",\n cashOnlyExecutionValueYes: \"是\",\n cashOnlyExecutionValueNo: \"否\",\n currentCashOnlyExecution: \"当前允许融资\",\n pendingCashOnlyExecution: \"待提交允许融资\",\n executionCashPolicyTitle: \"现金与融资\",\n executionCashPolicyNote: \"允许融资与预留现金覆盖不能同时生效;选「是」会清空预留覆盖,设预留覆盖会强制「否」。\",\n executionCashMarginBlocksReserve: \"已选允许融资;提交时会清空预留现金覆盖。\",\n executionCashReserveBlocksMargin: \"已设预留现金覆盖;提交时会强制不允许融资。\",\n qmtPlatformCashNote: \"A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。\",\n qmtDryRunOnlyNote: \"QMT 当前仅支持 dry-run(模拟),尚无 live 券商账号。\",\n binancePlatformNote: \"Binance 平台不使用券商级收入层与期权层;相关功能由策略内部实现。\",\n invalidExecutionCashPolicyNote: \"允许融资与预留现金覆盖冲突,请只保留一种约束。\",\n dcaMode: \"定投模式\",\n dcaSectionTitle: \"定投\",\n dcaModeFixed: \"定额定投\",\n dcaModeSmart: \"智能定投\",\n dcaBaseInvestmentUsd: \"定投基准金额\",\n dcaModeMeta: \"仅定投策略可配置。\",\n dcaDefaultMeta: \"默认:{mode},基准金额 {amount}。\",\n dcaNotSupported: \"该策略不是定投策略\",\n dcaPlatformNotSupported: \"当前平台不支持定投策略\",\n currentDca: \"当前定投设置\",\n pendingDca: \"待提交定投设置\",\n dcaText: \"{mode},基准金额 {amount}\",\n minReservedCash: \"最小预留现金 ({currency})\",\n reservedCashRatio: \"预留现金比例\",\n reservedCashMode: \"预留现金策略\",\n reservePolicyCurrent: \"沿用当前配置\",\n reservePolicyNone: \"不设置平台预留现金\",\n reservePolicyRatio: \"仅按比例\",\n reservePolicyFloor: \"仅按固定金额\",\n reservePolicyMax: \"固定金额和比例取较大值\",\n reservedCashModeMeta: \"选择是否沿用、清空或覆盖平台预留现金。\",\n reservedCashNone: \"不设置\",\n reservedCashDefault: \"未配置(平台默认:0 {currency} / 0%)\",\n reservedCashMeta: \"固定金额下限,可单独设置或与比例取较大值。\",\n reservedCashRatioMeta: \"例如 0.03 表示 3%。\",\n summary: \"当前 / 待提交\",\n copySummary: \"复制状态\",\n loginToRun: \"登录后切换\",\n loadingConfig: \"读取配置中\",\n configureAccounts: \"配置账号后切换\",\n runSwitch: \"一键切换\",\n noChanges: \"无变更\",\n readonlyNote: \"登录后才可执行切换。\",\n publicReadonly: \"登录后查看账号配置。\",\n loadingConfigNote: \"正在读取账号配置和当前状态。\",\n missingConfigNote: \"账号配置未加载,暂时不能执行。\",\n readyNote: \"点击后会触发工作流,并同步目标平台服务。\",\n invalidStrategyNote: \"当前账号没有可执行策略,暂时不能切换。\",\n invalidReservePolicyNote: \"请为当前预留现金策略填写有效金额或比例。\",\n invalidIncomeLayerNote: \"请填写有效的收入层起始金额和最高比例。\",\n invalidOptionOverlayNote: \"当前策略未定义可启用的期权层。\",\n invalidDcaNote: \"请填写有效的定投模式和基准金额。\",\n publicOauthTitle: \"GitHub OAuth 保护\",\n publicOauthText: \"只允许白名单账号进入私有配置。\",\n publicWorkerTitle: \"Worker 端触发\",\n publicWorkerText: \"令牌保留在服务端,浏览器只提交切换意图。\",\n publicAuditTitle: \"变更可回溯\",\n publicAuditText: \"切换由 GitHub Actions 执行,便于审计和回滚。\",\n noAccount: \"没有账号选项\",\n noStrategy: \"没有支持的策略\",\n repository: \"平台仓库\",\n selectedAccount: \"账号\",\n selectedMarket: \"市场\",\n currentRuntimeTarget: \"当前账号状态\",\n pendingRuntimeTarget: \"待提交账号状态\",\n reservedCashPolicy: \"当前预留现金\",\n currentIncomeLayer: \"当前收入层\",\n pendingIncomeLayer: \"待提交收入层\",\n currentOptionOverlay: \"当前期权层\",\n pendingOptionOverlay: \"待提交期权层\",\n pendingReservedCashPolicy: \"待提交预留现金\",\n pendingMode: \"待提交模式\",\n currentPluginMode: \"当前插件范围\",\n pendingPluginMode: \"待提交插件范围\",\n unchanged: \"不变\",\n copied: \"已复制状态\",\n dispatching: \"正在触发工作流...\",\n dispatched: \"已触发工作流\",\n dispatchFailed: \"触发失败\",\n targetMeta: \"目标 {target} · 服务 {service} · 市场 {domains}\",\n strategyMeta: \"支持市场:{domains}\",\n strategyLifecycleMeta: \"当前门槛 {stage}\",\n strategyBlockedCountMeta: \"{count} 个策略未达 live 门槛\",\n strategyDefaultBlockedMeta: \"默认策略 {profile} 已阻断:{reason}\",\n usEquity: \"美股\",\n hkEquity: \"港股\",\n cnEquity: \"A股\",\n cryptoEquity: \"加密\",\n currentStrategy: \"当前策略\",\n nextStrategy: \"切换策略\",\n notRead: \"读取失败\",\n runtimeTargetOn: \"启用\",\n runtimeTargetOff: \"禁用\",\n incomeLayerDefault: \"开启,{start}起 {ratio}\",\n incomeLayerOff: \"关闭\",\n incomeLayerOn: \"开启,起始 {start},最高 {ratio}\",\n optionOverlayOff: \"关闭\",\n optionOverlayOn: \"开启\",\n optionOverlayDefaultSimple: \"开启\",\n optionOverlayDefault: \"开启,{detail}\",\n cashOnlyExecutionDefault: \"仅用现金\",\n },\n en: {\n appTitle: \"Strategy Switch\",\n appSubtitle: \"Pick platform, target account, and strategy. One action switches everything.\",\n bootKicker: \"Starting console\",\n bootTitle: \"Loading strategy config\",\n bootMessage: \"Reading session, account config, and current state.\",\n bootStrategy: \"Reading strategy catalog.\",\n bootSession: \"Checking sign-in status.\",\n bootConfig: \"Reading account config and current state.\",\n bootTimeout: \"Loading timed out; switched to public preview. Retry after signing in.\",\n bootPublic: \"Public preview is ready.\",\n login: \"Sign in\",\n logout: \"Sign out\",\n signedInAs: \"Signed in as {login}\",\n activePlatform: \"Active Platform\",\n account: \"Target account\",\n strategy: \"Strategy\",\n mode: \"Mode\",\n live: \"Live\",\n paper: \"Dry run\",\n runtimeTargetMode: \"Account status\",\n runtimeSectionTitle: \"Runtime and plugins\",\n runtimeTargetCurrent: \"Keep current status\",\n runtimeTargetEnabled: \"Enabled\",\n runtimeTargetDisabled: \"Disabled\",\n runtimeTargetModeMeta: \"Disabled accounts skip live runs; dry runs and health checks still work.\",\n pluginMode: \"Plugin scope\",\n pluginModeAuto: \"Enabled\",\n pluginModeNone: \"Disabled\",\n pluginModeMeta: \"Choose whether to enable this strategy's plugins.\",\n incomeLayerMode: \"Income layer\",\n incomeLayerSectionTitle: \"Income layer\",\n incomeLayerCurrent: \"Keep current config\",\n incomeLayerEnabled: \"Enable income layer\",\n incomeLayerDisabled: \"Disable income layer\",\n incomeLayerNotSupported: \"No income layer for this strategy\",\n incomeLayerStartUsd: \"Income layer start amount\",\n incomeLayerMaxRatio: \"Income layer max ratio\",\n incomeLayerModeMeta: \"Only applies to US equity strategies with an income layer.\",\n incomeLayerDefaultMeta: \"Strategy default: starts at {start}, max {ratio}.\",\n incomeLayerAllocationMeta: \"Default allocation: {allocations}.\",\n incomeLayerStartMeta: \"Income layer activates after total assets reach this amount.\",\n incomeLayerRatioMeta: \"Use 0.55 for a 55% cap.\",\n optionOverlayMode: \"Option layer\",\n optionOverlaySectionTitle: \"Option layer\",\n optionOverlayCurrent: \"Keep current config\",\n optionOverlayEnabled: \"Enable option layer\",\n optionOverlayDisabled: \"Disable option layer\",\n optionOverlayNotSupported: \"No option layer for this strategy\",\n optionOverlayModeMeta: \"Enabled mode uses the strategy's default recipe and budget; ratios are not edited here.\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"Growth\",\n optionOverlayFamilyIncome: \"Income\",\n optionOverlayBudgetRatio: \"budget {ratio}\",\n optionOverlayRiskRatio: \"risk {ratio}\",\n cashOnlyExecutionMode: \"Allow margin\",\n cashOnlyExecutionCurrent: \"Keep current config\",\n cashOnlyExecutionYes: \"Yes\",\n cashOnlyExecutionNo: \"No\",\n cashOnlyExecutionModeMeta: \"Choose No to use available cash only and avoid margin buying power.\",\n cashOnlyExecutionValueYes: \"Yes\",\n cashOnlyExecutionValueNo: \"No\",\n currentCashOnlyExecution: \"Current allow margin\",\n pendingCashOnlyExecution: \"Pending allow margin\",\n executionCashPolicyTitle: \"Cash and margin\",\n executionCashPolicyNote: \"Allow margin and reserve-cash overrides cannot both apply. Yes clears reserve overrides; reserve overrides force No.\",\n executionCashMarginBlocksReserve: \"Allow margin is selected; submitting will clear reserve-cash overrides.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; submitting will force allow margin to No.\",\n qmtPlatformCashNote: \"QMT A-share does not use margin or platform reserve cash; cash constraints live in strategy execution_cash_reserve_ratio.\",\n qmtDryRunOnlyNote: \"QMT is dry-run only for now; no live broker accounts are configured.\",\n binancePlatformNote: \"Binance does not use broker-level income/option layers; features are implemented inside strategies.\",\n invalidExecutionCashPolicyNote: \"Allow margin and reserve-cash overrides conflict. Keep only one constraint.\",\n dcaMode: \"DCA mode\",\n dcaSectionTitle: \"DCA\",\n dcaModeFixed: \"Fixed DCA\",\n dcaModeSmart: \"Smart DCA\",\n dcaBaseInvestmentUsd: \"Base DCA amount\",\n dcaModeMeta: \"Only DCA strategies can use this.\",\n dcaDefaultMeta: \"Default: {mode}, base amount {amount}.\",\n dcaNotSupported: \"This is not a DCA strategy\",\n dcaPlatformNotSupported: \"DCA not supported on this platform\",\n currentDca: \"Current DCA settings\",\n pendingDca: \"Pending DCA settings\",\n dcaText: \"{mode}, base amount {amount}\",\n minReservedCash: \"Minimum reserved cash ({currency})\",\n reservedCashRatio: \"Reserved cash ratio\",\n reservedCashMode: \"Reserved cash policy\",\n reservePolicyCurrent: \"Keep current config\",\n reservePolicyNone: \"No platform reserve\",\n reservePolicyRatio: \"Ratio only\",\n reservePolicyFloor: \"Fixed amount only\",\n reservePolicyMax: \"Max of amount and ratio\",\n reservedCashModeMeta: \"Choose whether to keep, clear, or override platform reserved cash.\",\n reservedCashNone: \"None\",\n reservedCashDefault: \"Not configured (platform default: 0 {currency} / 0%)\",\n reservedCashMeta: \"Fixed cash floor. Use alone or with a ratio.\",\n reservedCashRatioMeta: \"Use 0.03 for 3%.\",\n summary: \"Current / Pending\",\n copySummary: \"Copy state\",\n loginToRun: \"Sign in to switch\",\n loadingConfig: \"Loading config\",\n configureAccounts: \"Configure accounts\",\n runSwitch: \"Switch now\",\n noChanges: \"No changes\",\n readonlyNote: \"Sign in to switch.\",\n publicReadonly: \"Sign in to view account config.\",\n loadingConfigNote: \"Reading account config and current state.\",\n missingConfigNote: \"Account config is not loaded, so switching is disabled.\",\n readyNote: \"This dispatches the workflow and syncs the target platform service.\",\n invalidStrategyNote: \"This account has no runnable strategy, so switching is disabled.\",\n invalidReservePolicyNote: \"Enter a valid amount or ratio for the selected reserved-cash policy.\",\n invalidIncomeLayerNote: \"Enter a valid income layer start amount and max ratio.\",\n invalidOptionOverlayNote: \"This strategy does not define an option layer to enable.\",\n invalidDcaNote: \"Enter a valid DCA mode and base amount.\",\n publicOauthTitle: \"Protected by GitHub OAuth\",\n publicOauthText: \"Only allowlisted accounts can open private config.\",\n publicWorkerTitle: \"Worker-side dispatch\",\n publicWorkerText: \"Tokens stay server-side; the browser submits intent only.\",\n publicAuditTitle: \"Traceable changes\",\n publicAuditText: \"Switches run through GitHub Actions for audit and rollback.\",\n noAccount: \"No accounts\",\n noStrategy: \"No supported strategies\",\n repository: \"Repository\",\n selectedAccount: \"Account\",\n selectedMarket: \"Market\",\n currentRuntimeTarget: \"Current account status\",\n pendingRuntimeTarget: \"Pending account status\",\n reservedCashPolicy: \"Current reserved cash\",\n currentIncomeLayer: \"Current income layer\",\n pendingIncomeLayer: \"Pending income layer\",\n currentOptionOverlay: \"Current option layer\",\n pendingOptionOverlay: \"Pending option layer\",\n pendingReservedCashPolicy: \"Pending reserved cash\",\n pendingMode: \"Pending mode\",\n currentPluginMode: \"Current plugin scope\",\n pendingPluginMode: \"Pending plugin scope\",\n unchanged: \"Unchanged\",\n copied: \"State copied\",\n dispatching: \"Dispatching workflow...\",\n dispatched: \"Workflow dispatched\",\n dispatchFailed: \"Dispatch failed\",\n targetMeta: \"target {target} · service {service} · market {domains}\",\n strategyMeta: \"Markets: {domains}\",\n strategyLifecycleMeta: \"current gate {stage}\",\n strategyBlockedCountMeta: \"{count} strategies are blocked from live\",\n strategyDefaultBlockedMeta: \"Default strategy {profile} is blocked: {reason}\",\n usEquity: \"US equity\",\n hkEquity: \"HK equity\",\n cnEquity: \"CN A-share\",\n cryptoEquity: \"Crypto\",\n currentStrategy: \"Current strategy\",\n nextStrategy: \"Switch strategy\",\n notRead: \"Not read\",\n runtimeTargetOn: \"Enabled\",\n runtimeTargetOff: \"Disabled\",\n incomeLayerDefault: \"Enabled, {start} start, {ratio} max\",\n incomeLayerOff: \"Disabled\",\n incomeLayerOn: \"Enabled, starts at {start}, max {ratio}\",\n optionOverlayOff: \"Disabled\",\n optionOverlayOn: \"Enabled\",\n optionOverlayDefaultSimple: \"Strategy default: enabled\",\n optionOverlayDefault: \"Enabled, {detail}\",\n cashOnlyExecutionDefault: \"Cash only\",\n },\n };\n\n const storedLang = localStorage.getItem(\"qsl-switch-lang\");\n const initialLang = storedLang === \"zh\" || storedLang === \"en\"\n ? storedLang\n : ((navigator.language || \"\").toLowerCase().startsWith(\"zh\") ? \"zh\" : \"en\");\n const clone = (value) => JSON.parse(JSON.stringify(value));\n const defaultReserveForm = () => ({\n reservePolicyMode: \"current\",\n minReservedCashUsd: \"\",\n reservedCashRatio: \"\",\n reservedCashTouched: false,\n incomeLayerMode: \"current\",\n incomeLayerStartUsd: \"\",\n incomeLayerMaxRatio: \"\",\n incomeLayerTouched: false,\n optionOverlayMode: \"current\",\n optionOverlayTouched: false,\n cashOnlyExecutionMode: \"current\",\n cashOnlyExecutionTouched: false,\n runtimeTargetMode: \"current\",\n runtimeTargetTouched: false,\n dcaMode: \"fixed\",\n dcaBaseInvestmentUsd: \"\",\n dcaTouched: false,\n strategyTouched: false,\n });\n\n const state = {\n selected: \"longbridge\",\n lang: initialLang,\n appReady: false,\n bootMessageKey: \"bootMessage\",\n auth: { available: false, allowed: false, admin: false, login: null },\n accountOptions: clone(defaultAccountOptions),\n currentStrategies: {},\n configSource: \"default\",\n repositories: clone(defaultRepositories),\n forms: {\n longbridge: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n ibkr: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n schwab: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n firstrade: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n qmt: { accountKey: \"preview\", strategy: \"\", executionMode: \"paper\", pluginMode: \"auto\", ...defaultReserveForm() },\n binance: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\" },\n },\n };\n\n const el = (id) => document.getElementById(id);\n const t = (key) => copy[state.lang][key] || copy.en[key] || key;\n let toastTimer = null;\n\n function showToast(message, { duration = 4000 } = {}) {\n const node = el(\"toast\");\n if (toastTimer) {\n window.clearTimeout(toastTimer);\n toastTimer = null;\n }\n node.textContent = message || \"\";\n if (message && duration > 0) {\n toastTimer = window.setTimeout(() => {\n node.textContent = \"\";\n toastTimer = null;\n }, duration);\n }\n }\n\n async function fetchWithTimeout(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const controller = new AbortController();\n const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (error) {\n if (error?.name === \"AbortError\") {\n throw new Error(\"request timeout\");\n }\n throw error;\n } finally {\n window.clearTimeout(timeoutId);\n }\n }\n\n async function requestJson(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const response = await fetchWithTimeout(url, { ...init, cache: \"no-store\" }, timeoutMs);\n if (!response.ok) throw new Error(\"request failed\");\n return response.json();\n }\n\n function isRequestTimeoutError(error) {\n return String(error?.message || \"\").toLowerCase() === \"request timeout\";\n }\n\n function optionsFor(platform) {\n return state.accountOptions[platform] && state.accountOptions[platform].length\n ? state.accountOptions[platform]\n : defaultAccountOptions[platform];\n }\n\n function selectedAccount(platform = state.selected) {\n const options = optionsFor(platform);\n const form = state.forms[platform];\n return options.find((option) => option.key === form.accountKey) || options[0];\n }\n\n function hasPrivateConfig() {\n return Boolean(state.auth.allowed && state.configSource === \"private\");\n }\n\n function cleanStrategyProfile(value) {\n const profile = String(value || \"\").trim();\n return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : \"\";\n }\n\n function cleanStrategyDomain(value) {\n const domain = String(value || \"\").trim();\n return strategyDomains.includes(domain) ? domain : \"\";\n }\n\n function domainLabel(domain) {\n const entry = domainLabels[domain];\n if (entry) return state.lang === \"zh\" ? entry.zh : entry.en;\n return domain;\n }\n\n function platformSupportsMarginPolicy(platform = state.selected) {\n return platformConfig[platform]?.margin_policy ?? true;\n }\n\n function platformSupportsReservedCashPolicy(platform = state.selected) {\n return platformConfig[platform]?.reserved_cash ?? true;\n }\n\n function platformDryRunOnly(platform = state.selected) {\n return platformConfig[platform]?.dry_run_only ?? false;\n }\n\n function allowMarginExplicitlySelected(form) {\n return normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode) === \"disabled\";\n }\n\n function reserveCashOverrideActive(form) {\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n return mode === \"ratio\" || mode === \"floor\" || mode === \"max\";\n }\n\n function executionCashPolicyConflict(form) {\n return allowMarginExplicitlySelected(form) && reserveCashOverrideActive(form);\n }\n\n function reconcileExecutionCashPolicy(form, changed) {\n if (!form) return;\n if (changed === \"margin\" && allowMarginExplicitlySelected(form)) {\n if (form.reservePolicyMode !== \"none\" && (form.minReservedCashUsd || form.reservedCashRatio)) {\n form._prevReserve = {\n mode: form.reservePolicyMode,\n floor: form.minReservedCashUsd,\n ratio: form.reservedCashRatio,\n };\n }\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n } else if (changed === \"reserve\" && reserveCashOverrideActive(form)) {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\n }\n\n function restoreReserveAfterMarginDisabled(form) {\n if (!form || allowMarginExplicitlySelected(form) || !form._prevReserve) return;\n form.reservePolicyMode = form._prevReserve.mode;\n form.minReservedCashUsd = form._prevReserve.floor;\n form.reservedCashRatio = form._prevReserve.ratio;\n form.reservedCashTouched = true;\n delete form._prevReserve;\n }\n\n function strategyDomain(profile) {\n return strategyCatalog[profile]?.domain || \"\";\n }\n\n function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) {\n const configured = String(account?.cash_currency || \"\").trim().toUpperCase();\n if (configured === \"USD\" || configured === \"HKD\" || configured === \"CNY\") return configured;\n const domain = strategyDomain(state.forms[platform]?.strategy);\n if (domain === \"hk_equity\") return \"HKD\";\n if (domain === \"cn_equity\") return \"CNY\";\n return \"USD\";\n }\n\n function applyStrategyProfiles(rawProfiles) {\n const profiles = Array.isArray(rawProfiles) && rawProfiles.length\n ? rawProfiles\n : defaultStrategyProfiles;\n const nextOptions = [];\n const nextLabels = {};\n const nextCatalog = {};\n const nextIncomeLayerDefaults = {};\n const nextOptionOverlayDefaults = {};\n for (const item of profiles) {\n const profile = cleanStrategyProfile(item?.profile || item?.strategy_profile);\n if (!profile || nextOptions.includes(profile)) continue;\n const domain = cleanStrategyDomain(item?.domain || \"us_equity\");\n if (!domain) continue;\n nextOptions.push(profile);\n nextLabels[profile] = strategyLabelSet(profile, item);\n nextCatalog[profile] = {\n profile,\n label: nextLabels[profile].en || nextLabels[profile].zh || profile,\n label_en: nextLabels[profile].en || \"\",\n label_zh: nextLabels[profile].zh || \"\",\n domain,\n runtime_enabled: cleanOptionalBoolean(item?.runtime_enabled ?? item?.live_enabled ?? true) !== false,\n };\n const lifecycleStage = normalizeLifecycleStage(item?.lifecycle_stage ?? item?.lifecycleStage);\n if (lifecycleStage) nextCatalog[profile].lifecycle_stage = lifecycleStage;\n const allowedExecutionModes = normalizeAllowedExecutionModes(item?.allowed_execution_modes);\n if (allowedExecutionModes.length) nextCatalog[profile].allowed_execution_modes = allowedExecutionModes;\n const canSwitchLive = cleanOptionalBoolean(item?.can_switch_live);\n if (canSwitchLive !== null) nextCatalog[profile].can_switch_live = canSwitchLive;\n const blockedLiveReason = cleanDisplayText(item?.blocked_live_reason);\n if (blockedLiveReason) nextCatalog[profile].blocked_live_reason = blockedLiveReason;\n const latestEvidenceStatus = cleanDisplayText(item?.latest_evidence_status);\n if (latestEvidenceStatus) nextCatalog[profile].latest_evidence_status = latestEvidenceStatus;\n const pluginGateStatus = cleanDisplayText(item?.plugin_gate_status);\n if (pluginGateStatus) nextCatalog[profile].plugin_gate_status = pluginGateStatus;\n const dcaDefaults = dcaProfileDefaults[profile] || null;\n if (dcaDefaults || item?.dca_enabled === true) {\n nextCatalog[profile].dca_enabled = true;\n nextCatalog[profile].dca_default_mode = normalizeDcaMode(\n item?.dca_default_mode || item?.default_dca_mode || dcaDefaults?.defaultMode || \"fixed\",\n );\n nextCatalog[profile].dca_default_base_investment_usd = cleanDisplayPositiveNumber(\n item?.dca_default_base_investment_usd ||\n item?.default_dca_base_investment_usd ||\n dcaDefaults?.defaultBaseInvestmentUsd ||\n \"1000\",\n ) || \"1000\";\n }\n const profileIncomeDefaults = incomeLayerDefaultsFromProfileItem(item);\n const incomeDefaults = profileIncomeDefaults === false\n ? null\n : (profileIncomeDefaults || fallbackIncomeLayerDefaults[profile] || null);\n if (incomeDefaults) {\n nextIncomeLayerDefaults[profile] = incomeDefaults;\n nextCatalog[profile].income_layer_enabled = true;\n nextCatalog[profile].income_layer_start_usd = String(incomeDefaults.startUsd);\n nextCatalog[profile].income_layer_max_ratio = incomeDefaults.maxRatio;\n nextCatalog[profile].income_layer_allocations = incomeDefaults.allocations;\n }\n const profileOptionDefaults = optionOverlayDefaultsFromProfileItem(item);\n const optionDefaults = profileOptionDefaults === false\n ? null\n : (profileOptionDefaults || fallbackOptionOverlayDefaults[profile] || null);\n if (optionDefaults) {\n nextOptionOverlayDefaults[profile] = optionDefaults;\n nextCatalog[profile].option_overlay_enabled = true;\n nextCatalog[profile].option_overlay_live_gate = optionDefaults.liveGate || \"\";\n nextCatalog[profile].option_overlay_live_status = optionDefaults.liveStatus || \"\";\n }\n }\n if (!nextOptions.length && profiles !== defaultStrategyProfiles) return applyStrategyProfiles(defaultStrategyProfiles);\n strategyOptions = nextOptions;\n strategyLabels = nextLabels;\n strategyCatalog = nextCatalog;\n incomeLayerDefaults = nextIncomeLayerDefaults;\n optionOverlayDefaults = nextOptionOverlayDefaults;\n }\n\n function incomeLayerDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.income_layer_enabled);\n const hasConfig = enabled !== null ||\n item?.income_layer_start_usd !== undefined ||\n item?.income_layer_max_ratio !== undefined ||\n item?.income_layer_allocations !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const startUsd = cleanDisplayNumber(item?.income_layer_start_usd);\n const maxRatio = cleanDisplayRatio(item?.income_layer_max_ratio);\n const allocations = cleanIncomeLayerAllocations(item?.income_layer_allocations);\n if (!startUsd || !maxRatio || !allocations) return null;\n return { startUsd, maxRatio, allocations };\n }\n\n function cleanIncomeLayerAllocations(value) {\n if (!value || Array.isArray(value) || typeof value !== \"object\") return null;\n const allocations = {};\n let total = 0;\n for (const [rawSymbol, rawRatio] of Object.entries(value)) {\n const symbol = String(rawSymbol || \"\").trim().toUpperCase();\n const ratio = cleanDisplayPositiveNumber(rawRatio);\n if (!/^[A-Z0-9.-]{1,12}$/.test(symbol) || !ratio) continue;\n allocations[symbol] = Number(ratio);\n total += Number(ratio);\n }\n return total > 0 && Object.keys(allocations).length ? allocations : null;\n }\n\n function optionOverlayDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.option_overlay_enabled);\n const hasConfig = enabled !== null ||\n item?.option_growth_overlay_enabled !== undefined ||\n item?.option_income_overlay_enabled !== undefined ||\n item?.option_overlay_live_gate !== undefined ||\n item?.option_overlay_live_status !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const families = [\n optionOverlayFamilyDefaultsFromProfileItem(item, \"growth\"),\n optionOverlayFamilyDefaultsFromProfileItem(item, \"income\"),\n ].filter(Boolean);\n if (!families.length) return null;\n return {\n liveGate: String(item?.option_overlay_live_gate || \"promotion_required\"),\n liveStatus: String(item?.option_overlay_live_status || \"research_only\"),\n families,\n };\n }\n\n function optionOverlayFamilyDefaultsFromProfileItem(item, family) {\n const prefix = `option_${family}_overlay`;\n const enabled = cleanOptionalBoolean(item?.[`${prefix}_enabled`]);\n if (enabled !== true) return null;\n const recipe = cleanStrategyProfile(item?.[`${prefix}_recipe`]);\n const startUsd = cleanDisplayNumber(item?.[`${prefix}_start_usd`]);\n const ratioField = family === \"growth\"\n ? \"option_growth_overlay_nav_budget_ratio\"\n : \"option_income_overlay_nav_risk_ratio\";\n const ratio = cleanDisplayRatio(item?.[ratioField]);\n if (!recipe || !startUsd || !ratio) return null;\n return {\n family,\n recipe,\n startUsd,\n ratio,\n ratioKind: family === \"growth\" ? \"budget\" : \"risk\",\n };\n }\n\n function supportedDomainsForAccount(platform, account) {\n if (Array.isArray(account?.supported_domains) && account.supported_domains.length) {\n const cleaned = account.supported_domains.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n }\n return inferSupportedDomains(platform, account);\n }\n\n function inferSupportedDomains(platform, account) {\n void account;\n if (platform === \"qmt\") return [\"cn_equity\"];\n if (platform === \"longbridge\" || platform === \"ibkr\") return [\"us_equity\", \"hk_equity\"];\n return [\"us_equity\"];\n }\n\n function supportedDomainLabel(platform, account) {\n return supportedDomainsForAccount(platform, account).map(domainLabel).join(\" / \");\n }\n\n function platformSupportsDca(platform = state.selected) {\n return platformConfig[platform]?.dca ?? false;\n }\n\n function strategyCatalogEntry(profile) {\n return strategyCatalog[cleanStrategyProfile(profile)] || {};\n }\n\n function normalizeLifecycleStage(value) {\n const text = String(value || \"\").trim().toLowerCase();\n if (!text || text.length > 40 || !/^[a-z0-9._-]+$/.test(text)) return \"\";\n return text;\n }\n\n function normalizeAllowedExecutionModes(value) {\n const items = Array.isArray(value)\n ? value\n : String(value || \"\").split(/[,\\s/|]+/);\n const modes = [];\n for (const item of items) {\n const mode = String(item || \"\").trim().toLowerCase();\n if (!mode || mode.length > 32 || !/^[a-z0-9._-]+$/.test(mode)) continue;\n if (!modes.includes(mode)) modes.push(mode);\n }\n return modes;\n }\n\n function cleanDisplayText(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 120 || /[<>]/.test(text)) return \"\";\n return text;\n }\n\n function strategyCanSwitchLive(entry) {\n if (!entry || typeof entry !== \"object\") return false;\n if (entry.runtime_enabled === false) return false;\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) return false;\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return false;\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) return false;\n const blockedReason = cleanDisplayText(entry.blocked_live_reason);\n if (blockedReason) return false;\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && [\"research_only\", \"blocked\", \"pending\"].includes(evidenceStatus.toLowerCase())) return false;\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && [\"blocked\", \"locked\", \"disabled\"].includes(pluginGateStatus.toLowerCase())) return false;\n return true;\n }\n\n function strategyDisplayMetaText(platform, account, profile) {\n void profile;\n return t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function strategyActionNoteText(platform = state.selected, account = selectedAccount(platform)) {\n const profile = state.forms[platform]?.strategy || \"\";\n const meta = strategyDisplayMetaText(platform, account, profile);\n return meta ? `${t(\"invalidStrategyNote\")}\\n${meta}` : t(\"invalidStrategyNote\");\n }\n\n function strategyChoiceLabel(profile, platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const label = strategyLabel(profile);\n const entry = strategyCatalogEntry(profile);\n const domain = entry.domain ? domainLabel(entry.domain) : \"\";\n if (!entry.profile) return label;\n if (normalizeExecutionMode(executionMode, false) === \"live\" && !strategyCanSwitchLive(entry)) {\n return domain ? `${label}(${domain})` : label;\n }\n if (entry.runtime_enabled === false) {\n return domain ? `${label}(${domain})` : label;\n }\n return label;\n }\n\n function strategyAllowedForAccount(platform, account, profile, executionMode = state.forms[platform]?.executionMode) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalogEntry(cleanProfile);\n if (!catalogEntry.profile) return false;\n if (catalogEntry.runtime_enabled !== true) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n if (!supportedDomainsForAccount(platform, account).includes(catalogEntry.domain)) return false;\n const mode = normalizeExecutionMode(executionMode, false);\n if (mode === \"live\") return strategyCanSwitchLive(catalogEntry);\n const allowedModes = normalizeAllowedExecutionModes(catalogEntry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(mode)) return false;\n return true;\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile, executionMode));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile, executionMode)) {\n choices.push(profile);\n }\n };\n return choices;\n }\n\n function strategyLabel(profile) {\n const labels = strategyLabels[profile] || localStrategyLabels[profile];\n if (!labels) return profile;\n return state.lang === \"zh\"\n ? (labels.zh || labels.en || profile)\n : (labels.en || labels.zh || profile);\n }\n\n function strategyLabelSet(profile, item) {\n const local = localStrategyLabels[profile] || {};\n const label = String(item?.label || item?.display_name || \"\").trim();\n const labelEn = String(item?.label_en || item?.display_name_en || \"\").trim();\n const labelZh = String(item?.label_zh || item?.display_name_zh || \"\").trim();\n return {\n zh: labelZh || local.zh || label || local.en || profile,\n en: labelEn || label || local.en || labelZh || local.zh || profile,\n };\n }\n\n function modeLabel(mode) {\n return mode === \"paper\" ? t(\"paper\") : t(\"live\");\n }\n\n function normalizePluginMode(value) {\n return pluginModes.includes(value) ? value : \"auto\";\n }\n\n function pluginModeLabel(mode) {\n return mode === \"none\" ? t(\"pluginModeNone\") : t(\"pluginModeAuto\");\n }\n\n function dcaConfigForStrategy(profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalog = strategyCatalog[cleanProfile] || {};\n if (catalog.dca_enabled === true) {\n return {\n defaultMode: normalizeDcaMode(catalog.dca_default_mode || \"fixed\"),\n defaultBaseInvestmentUsd: cleanDisplayPositiveNumber(catalog.dca_default_base_investment_usd) || \"1000\",\n };\n }\n return dcaProfileDefaults[cleanProfile] || null;\n }\n\n function dcaSupported(profile) {\n return Boolean(dcaConfigForStrategy(profile));\n }\n\n function normalizeDcaMode(value) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"smart_dca\") return \"smart\";\n if (mode === \"fixed_dca\" || mode === \"ordinary\" || mode === \"ordinary_dca\") return \"fixed\";\n return dcaModes.includes(mode) ? mode : \"fixed\";\n }\n\n function dcaModeLabel(mode) {\n return normalizeDcaMode(mode) === \"smart\" ? t(\"dcaModeSmart\") : t(\"dcaModeFixed\");\n }\n\n function normalizeRuntimeTargetMode(value) {\n return runtimeTargetModes.includes(value) ? value : \"current\";\n }\n\n function runtimeTargetModeLabel(mode) {\n if (mode === \"enabled\") return t(\"runtimeTargetEnabled\");\n if (mode === \"disabled\") return t(\"runtimeTargetDisabled\");\n return t(\"runtimeTargetCurrent\");\n }\n\n function runtimeTargetEnabledForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.runtime_target_enabled);\n }\n\n function runtimeTargetStateForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return { known: false, enabled: null };\n const configured = cleanOptionalBoolean(entry.runtime_target_enabled);\n return { known: true, enabled: configured ?? true };\n }\n\n function runtimeTargetText(enabled) {\n return enabled ? t(\"runtimeTargetOn\") : t(\"runtimeTargetOff\");\n }\n\n function runtimeTargetTone(enabled) {\n return enabled ? \"enabled\" : \"disabled\";\n }\n\n function currentRuntimeTargetText(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return t(\"notRead\");\n return runtimeTargetText(target.enabled);\n }\n\n function currentRuntimeTargetTone(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return \"neutral\";\n return runtimeTargetTone(target.enabled);\n }\n\n function incomeLayerDefaultForStrategy(profile) {\n return incomeLayerDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function incomeLayerSupported(profile) {\n return Boolean(incomeLayerDefaultForStrategy(profile));\n }\n\n function normalizeIncomeLayerMode(value) {\n return incomeLayerModes.includes(value) ? value : \"current\";\n }\n\n function incomeLayerModeLabel(mode) {\n if (mode === \"enabled\") return t(\"incomeLayerEnabled\");\n if (mode === \"disabled\") return t(\"incomeLayerDisabled\");\n return t(\"incomeLayerCurrent\");\n }\n\n function optionOverlayDefaultForStrategy(profile) {\n return optionOverlayDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function optionOverlaySupported(profile) {\n return Boolean(optionOverlayDefaultForStrategy(profile));\n }\n\n function normalizeOptionOverlayMode(value) {\n return optionOverlayModes.includes(value) ? value : \"current\";\n }\n\n function optionOverlayModeLabel(mode) {\n if (mode === \"enabled\") return t(\"optionOverlayEnabled\");\n if (mode === \"disabled\") return t(\"optionOverlayDisabled\");\n return t(\"optionOverlayCurrent\");\n }\n\n function optionOverlayText(enabled) {\n return enabled ? t(\"optionOverlayOn\") : t(\"optionOverlayOff\");\n }\n\n function normalizeCashOnlyExecutionMode(value) {\n return cashOnlyExecutionModes.includes(value) ? value : \"current\";\n }\n\n function cashOnlyExecutionModeLabel(mode) {\n if (mode === \"enabled\") return t(\"cashOnlyExecutionNo\");\n if (mode === \"disabled\") return t(\"cashOnlyExecutionYes\");\n return t(\"cashOnlyExecutionCurrent\");\n }\n\n function cashOnlyExecutionText(enabled) {\n if (enabled === null) return t(\"notRead\");\n return enabled ? t(\"cashOnlyExecutionValueNo\") : t(\"cashOnlyExecutionValueYes\");\n }\n\n function platformCashOnlyExecutionDefault() {\n return true;\n }\n\n function effectiveCashOnlyExecutionForAccount(platform, account) {\n const configured = currentCashOnlyExecutionForAccount(platform, account);\n if (configured !== null) return configured;\n if (!platformSupportsMarginPolicy(platform)) return null;\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n if (entry) {\n const val = cleanOptionalBoolean(entry.cash_only_execution);\n if (val !== null) return val;\n }\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionText(platform = state.selected, account = selectedAccount(platform)) {\n if (!platformSupportsMarginPolicy(platform)) return t(\"notRead\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.cash_only_execution);\n if (configured === null) return t(\"cashOnlyExecutionDefault\");\n return cashOnlyExecutionText(configured);\n }\n\n function currentOptionOverlayForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.option_overlay_enabled);\n }\n\n function effectiveOptionOverlayForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const configured = currentOptionOverlayForAccount(platform, account);\n if (configured !== null) return configured;\n if (!optionOverlaySupported(profile)) return null;\n return true;\n }\n\n function optionOverlayDefaultSummaryDetail(defaults) {\n if (!defaults?.families?.length) return \"\";\n return defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family} ${ratioText}`;\n }).join(\" / \");\n }\n\n function optionOverlayDefaultText(profile) {\n const defaults = optionOverlayDefaultForStrategy(profile);\n if (!defaults) return t(\"optionOverlayNotSupported\");\n const detail = optionOverlayDefaultSummaryDetail(defaults);\n return detail ? t(\"optionOverlayDefault\").replace(\"{detail}\", detail) : t(\"optionOverlayDefaultSimple\");\n }\n\n function currentOptionOverlayText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.option_overlay_enabled);\n if (!optionOverlaySupported(profile)) {\n return configured === null ? t(\"optionOverlayNotSupported\") : optionOverlayText(configured);\n }\n if (configured === null) return optionOverlayDefaultText(profile);\n return optionOverlayText(configured);\n }\n\n function currentIncomeLayerForAccount(platform, account) {\n return incomeLayerFromEntry(currentEntryForAccount(platform, account));\n }\n\n function incomeLayerFromEntry(entry) {\n return {\n enabled: cleanOptionalBoolean(entry?.income_layer_enabled),\n startUsd: cleanDisplayNumber(entry?.income_layer_start_usd),\n maxRatio: cleanDisplayRatio(entry?.income_layer_max_ratio),\n };\n }\n\n function incomeLayerFieldsConfigured(entry) {\n const current = incomeLayerFromEntry(entry);\n return current.enabled !== null || Boolean(current.startUsd || current.maxRatio);\n }\n\n function effectiveIncomeLayerForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return null;\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return null;\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return {\n enabled: true,\n startUsd: String(defaults.startUsd),\n maxRatio: defaults.maxRatio,\n };\n }\n return {\n enabled: current.enabled ?? true,\n startUsd: current.startUsd || String(defaults.startUsd),\n maxRatio: current.maxRatio || defaults.maxRatio,\n };\n }\n\n function currentDcaForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, mode: \"\", baseInvestmentUsd: \"\" };\n const entry = currentEntryForAccount(platform, account);\n return {\n supported: true,\n mode: normalizeDcaMode(entry?.dca_mode || account?.dca_mode || defaults.defaultMode),\n baseInvestmentUsd: cleanDisplayPositiveNumber(entry?.dca_base_investment_usd) ||\n cleanDisplayPositiveNumber(account?.dca_base_investment_usd) ||\n defaults.defaultBaseInvestmentUsd,\n };\n }\n\n function normalizeAccountLookupKey(value) {\n return String(value || \"\").trim().toLowerCase();\n }\n\n function collectAccountLookupCandidates(keys) {\n const candidates = new Set();\n for (const rawKey of keys) {\n const key = normalizeAccountLookupKey(rawKey);\n if (!key) continue;\n\n candidates.add(key);\n\n const compact = key.replace(/[^a-z0-9]+/g, \"\");\n if (compact) candidates.add(compact);\n\n const parts = key.split(/[^a-z0-9]+/).filter(Boolean);\n if (parts.length > 1) candidates.add(parts[parts.length - 1]);\n }\n return [...candidates];\n }\n\n function resolveCurrentEntryByKey(byPlatform, keys) {\n const candidates = new Set(collectAccountLookupCandidates(keys));\n if (!candidates.size) return null;\n\n for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) return entry;\n }\n\n for (const [rawKey, entry] of Object.entries(byPlatform)) {\n if (!currentEntryHasState(entry)) continue;\n const rawCandidates = collectAccountLookupCandidates([rawKey]);\n const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));\n if (hasMatch) return entry;\n }\n\n return null;\n }\n\n function currentEntryForAccount(platform, account) {\n const byPlatform = state.currentStrategies[platform] || {};\n const keys = [account?.key, account?.target_name, account?.label]\n .filter(Boolean)\n .map((value) => String(value));\n const entry = resolveCurrentEntryByKey(byPlatform, keys);\n if (entry) return entry;\n const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n const merged = { ...globalDefaults, ...(account || {}) };\n const synth = {\n strategy_profile: \"\",\n source: \"account_defaults\",\n };\n const cashMode = merged.cash_only_execution_mode;\n if (cashMode === \"enabled\") synth.cash_only_execution = true;\n else if (cashMode === \"disabled\") synth.cash_only_execution = false;\n else if (platformSupportsMarginPolicy(platform)) synth.cash_only_execution = true;\n if (merged.min_reserved_cash_usd) synth.min_reserved_cash_usd = merged.min_reserved_cash_usd;\n if (merged.reserved_cash_ratio) synth.reserved_cash_ratio = merged.reserved_cash_ratio;\n synth.runtime_target_enabled = merged.runtime_target_enabled !== false;\n const execMode = merged.default_execution_mode || platformConfig[platform]?.default_execution_mode || \"live\";\n synth.execution_mode = execMode;\n synth.dry_run_only = execMode === \"paper\";\n return synth;\n }\n\n function currentEntryHasState(entry) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) return false;\n return Boolean(\n cleanStrategyProfile(entry.strategy_profile) ||\n cleanDisplayNumber(entry.min_reserved_cash_usd ?? entry.reserved_cash_floor_usd) ||\n cleanDisplayRatio(entry.reserved_cash_ratio) ||\n cleanOptionalBoolean(entry.income_layer_enabled) !== null ||\n cleanDisplayNumber(entry.income_layer_start_usd) ||\n cleanDisplayRatio(entry.income_layer_max_ratio) ||\n cleanOptionalBoolean(entry.option_overlay_enabled) !== null ||\n cleanOptionalBoolean(entry.cash_only_execution) !== null ||\n cleanOptionalBoolean(entry.runtime_target_enabled) !== null ||\n normalizeDcaMode(entry.dca_mode || \"\") !== \"fixed\" ||\n cleanDisplayPositiveNumber(entry.dca_base_investment_usd) ||\n normalizeExecutionMode(entry.execution_mode, entry.dry_run_only),\n );\n }\n\n function currentStrategyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return cleanStrategyProfile(entry?.strategy_profile) || \"\";\n }\n\n function currentReservePolicyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return reservePolicyFromEntry(entry);\n }\n\n function currentPluginModeForAccount(platform, account) {\n void platform;\n return normalizePluginMode(account?.plugin_mode);\n }\n\n function reservePolicyFromEntry(entry) {\n return {\n minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),\n reservedCashRatio: cleanDisplayRatio(entry?.reserved_cash_ratio),\n };\n }\n\n function cleanDisplayNumber(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 32 || !/^(?:\\d+|\\d*\\.\\d+)$/.test(text)) return \"\";\n const numeric = Number(text);\n if (!Number.isFinite(numeric) || numeric < 0) return \"\";\n return text;\n }\n\n function cleanDisplayRatio(value) {\n const text = cleanDisplayNumber(value);\n if (!text) return \"\";\n const numeric = Number(text);\n return numeric >= 0 && numeric <= 1 ? text : \"\";\n }\n\n function cleanDisplayPositiveNumber(value) {\n const text = cleanDisplayNumber(value);\n return text && Number(text) > 0 ? text : \"\";\n }\n\n function normalizeExecutionMode(value, dryRunOnly) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"live\" || mode === \"paper\") return mode;\n if (dryRunOnly === true || dryRunOnly === \"true\" || dryRunOnly === \"1\" || dryRunOnly === 1) return \"paper\";\n if (dryRunOnly === false || dryRunOnly === \"false\" || dryRunOnly === \"0\" || dryRunOnly === 0) return \"live\";\n return \"\";\n }\n\n function cleanOptionalBoolean(value) {\n if (value === true || value === 1) return true;\n if (value === false || value === 0) return false;\n if (typeof value === \"string\") {\n const text = value.trim().toLowerCase();\n if (text === \"true\" || text === \"1\") return true;\n if (text === \"false\" || text === \"0\") return false;\n }\n return null;\n }\n\n function defaultExecutionModeForAccount(platform, account, fallback = \"live\") {\n if (platformDryRunOnly(platform)) return \"paper\";\n const currentMode = normalizeExecutionMode(\n currentEntryForAccount(platform, account)?.execution_mode,\n currentEntryForAccount(platform, account)?.dry_run_only,\n );\n if (currentMode) return currentMode;\n const hint = [\n account?.key,\n account?.label,\n account?.target_name,\n account?.deployment_selector,\n account?.account_scope,\n account?.service_name,\n ].join(\" \").toLowerCase();\n if (hint.split(/\\s+/).includes(\"paper\") || hint.includes(\"-paper\") || hint.includes(\"_paper\") || hint.includes(\"dry_run\") || hint.includes(\"dry-run\")) {\n return \"paper\";\n }\n return fallback;\n }\n\n function defaultStrategyForAccount(platform, account) {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile) return currentProfile;\n return \"\";\n }\n\n function syncStrategyForAccount(platform) {\n const account = selectedAccount(platform);\n if (!account) return;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, account);\n state.forms[platform].executionMode = defaultExecutionModeForAccount(\n platform,\n account,\n );\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, account);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(state.forms[platform], \"margin\");\n syncDcaForAccount(platform);\n }\n\n function syncRuntimeTargetForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.runtimeTargetTouched) return;\n const current = runtimeTargetEnabledForAccount(platform, selectedAccount(platform));\n form.runtimeTargetMode = current === false ? \"disabled\" : \"enabled\";\n }\n\n function syncReservePolicyForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.reservedCashTouched) return;\n const policy = currentReservePolicyForAccount(platform, selectedAccount(platform));\n const hasFloor = Boolean(policy.minReservedCashUsd);\n const hasRatio = Boolean(policy.reservedCashRatio);\n if (hasFloor && hasRatio) form.reservePolicyMode = \"max\";\n else if (hasFloor) form.reservePolicyMode = \"floor\";\n else if (hasRatio) form.reservePolicyMode = \"ratio\";\n else form.reservePolicyMode = \"none\";\n form.minReservedCashUsd = policy.minReservedCashUsd;\n form.reservedCashRatio = policy.reservedCashRatio;\n }\n\n function syncIncomeLayerForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.incomeLayerTouched) return;\n const defaults = incomeLayerDefaultForStrategy(form.strategy);\n const current = currentIncomeLayerForAccount(platform, selectedAccount(platform));\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n if (entry && incomeLayerFieldsConfigured(entry)) {\n form.incomeLayerMode = current.enabled === false ? \"disabled\" : \"enabled\";\n } else {\n form.incomeLayerMode = incomeLayerSupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n form.incomeLayerStartUsd = current.startUsd || String(defaults?.startUsd || \"\");\n form.incomeLayerMaxRatio = current.maxRatio || defaults?.maxRatio || \"\";\n }\n\n function syncOptionOverlayForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.optionOverlayTouched) return;\n const configured = normalizeOptionOverlayMode(selectedAccount(platform)?.option_overlay_mode);\n if (configured !== \"current\") {\n form.optionOverlayMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.option_overlay_enabled);\n if (rawValue !== null) {\n form.optionOverlayMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n form.optionOverlayMode = optionOverlaySupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n\n function syncCashOnlyExecutionForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.cashOnlyExecutionTouched) return;\n const configured = normalizeCashOnlyExecutionMode(selectedAccount(platform)?.cash_only_execution_mode);\n if (configured !== \"current\") {\n form.cashOnlyExecutionMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.cash_only_execution);\n if (rawValue !== null) {\n form.cashOnlyExecutionMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n // No explicit config — use platform default (cash-only for margin-capable platforms)\n form.cashOnlyExecutionMode = platformSupportsMarginPolicy(platform) ? \"enabled\" : \"disabled\";\n }\n\n function syncDcaForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.dcaTouched) return;\n const current = currentDcaForAccount(platform, selectedAccount(platform), form.strategy);\n form.dcaMode = current.supported ? current.mode : \"fixed\";\n form.dcaBaseInvestmentUsd = current.supported ? current.baseInvestmentUsd : \"\";\n }\n\n function ensureAccountSelection(platform) {\n const options = optionsFor(platform);\n if (!options.length) return;\n if (!options.some((option) => option.key === state.forms[platform].accountKey)) {\n state.forms[platform].accountKey = options[0].key;\n state.forms[platform].runtimeTargetTouched = false;\n state.forms[platform].reservedCashTouched = false;\n state.forms[platform].incomeLayerTouched = false;\n state.forms[platform].optionOverlayTouched = false;\n state.forms[platform].cashOnlyExecutionTouched = false;\n state.forms[platform].dcaTouched = false;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0]);\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n syncDcaForAccount(platform);\n }\n }\n\n function derivedService(platform, targetName) {\n if (platform === \"longbridge\") return `longbridge-quant-${targetName.toLowerCase()}-service`;\n if (platform === \"ibkr\") return `interactive-brokers-${targetName.toLowerCase()}-service`;\n if (platform === \"schwab\") return \"charles-schwab-quant-service\";\n if (platform === \"firstrade\") return \"firstrade-quant-service\";\n if (platform === \"qmt\") return \"qmt-quant-service\";\n return \"\";\n }\n\n function accountMetaText(platform = state.selected) {\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const raw = account.service_name || derivedService(platform, targetName);\n const service = raw || (state.lang === \"zh\" ? \"无\" : \"-\");\n return t(\"targetMeta\")\n .replace(\"{target}\", targetName)\n .replace(\"{service}\", service)\n .replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function hasRunnableStrategySelection(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n return Boolean(form?.strategy && account && strategyAllowedForAccount(platform, account, form.strategy, form.executionMode));\n }\n\n function hasValidReservePolicy(platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n if (mode === \"current\" || mode === \"none\") return true;\n return Boolean(reservePolicyOverrideForForm(form, platform));\n }\n\n function hasValidExecutionCashPolicy(platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform) && !platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n return !executionCashPolicyConflict(form) && hasValidReservePolicy(platform);\n }\n\n function hasValidIncomeLayerPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!incomeLayerSupported(form?.strategy)) return true;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\" || mode === \"disabled\") return true;\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults?.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults?.maxRatio);\n return Boolean(startUsd && maxRatio);\n }\n\n function hasValidOptionOverlayPolicy(platform = state.selected) {\n const form = state.forms[platform];\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n return mode !== \"enabled\" || optionOverlaySupported(form?.strategy);\n }\n\n function hasValidDcaPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(platform)) return true;\n return Boolean(dcaModes.includes(normalizeDcaMode(form?.dcaMode)) && cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd));\n }\n\n function hasValidStrategySelection(platform = state.selected) {\n return hasRunnableStrategySelection(platform) &&\n hasValidExecutionCashPolicy(platform) &&\n hasValidIncomeLayerPolicy(platform) &&\n hasValidOptionOverlayPolicy(platform) &&\n hasValidDcaPolicy(platform);\n }\n\n function normalizeReservePolicyMode(value) {\n return reservePolicyModes.includes(value) ? value : \"current\";\n }\n\n function reservePolicyOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return null;\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n const floor = cleanDisplayNumber(form?.minReservedCashUsd);\n const ratio = cleanDisplayRatio(form?.reservedCashRatio);\n const extraVariables = {};\n if (mode === \"current\") return null;\n if (mode === \"none\") {\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: {}, extraVariables };\n }\n if (mode === \"ratio\") {\n if (!ratio) return null;\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n return { inputs: { reserved_cash_ratio: ratio }, extraVariables };\n }\n if (mode === \"floor\") {\n if (!floor) return null;\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: { min_reserved_cash_usd: floor }, extraVariables };\n }\n if (mode === \"max\") {\n if (!floor || !ratio) return null;\n return { inputs: { min_reserved_cash_usd: floor, reserved_cash_ratio: ratio }, extraVariables };\n }\n return null;\n }\n\n function runtimeTargetOverrideForForm(form) {\n const mode = normalizeRuntimeTargetMode(form?.runtimeTargetMode);\n if (mode === \"current\") return null;\n return {\n inputs: { runtime_target_enabled_mode: mode },\n extraVariables: { [runtimeTargetEnabledVariable]: mode === \"enabled\" ? \"true\" : \"false\" },\n };\n }\n\n function incomeLayerOverrideForForm(form) {\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n if (!defaults) return null;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\") return null;\n const extraVariables = {};\n if (mode === \"disabled\") {\n extraVariables[incomeLayerEnabledVariable] = \"false\";\n extraVariables[incomeLayerStartUsdVariable] = \"\";\n extraVariables[incomeLayerMaxRatioVariable] = \"\";\n return { inputs: { income_layer_mode: mode }, extraVariables };\n }\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults.maxRatio);\n if (!startUsd || !maxRatio) return null;\n extraVariables[incomeLayerEnabledVariable] = \"true\";\n extraVariables[incomeLayerStartUsdVariable] = startUsd;\n extraVariables[incomeLayerMaxRatioVariable] = maxRatio;\n return { inputs: { income_layer_mode: mode, income_layer_start_usd: startUsd, income_layer_max_ratio: maxRatio }, extraVariables };\n }\n\n function optionOverlayOverrideForForm(form) {\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n if (mode === \"current\") return null;\n if (mode === \"enabled\" && !optionOverlaySupported(form?.strategy)) return null;\n return { inputs: { option_overlay_mode: mode } };\n }\n\n function cashOnlyExecutionOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform)) return null;\n const mode = normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode);\n if (mode === \"current\") return null;\n return { inputs: { cash_only_execution_mode: mode } };\n }\n\n function dcaOverrideForForm(form) {\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(state.selected)) return null;\n const mode = normalizeDcaMode(form?.dcaMode);\n const baseInvestmentUsd = cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd);\n if (!baseInvestmentUsd) return null;\n return { inputs: { dca_mode: mode, dca_base_investment_usd: baseInvestmentUsd } };\n }\n\n function mergeExtraVariables(inputs, extraVariables) {\n if (!extraVariables || !Object.keys(extraVariables).length) return;\n const merged = inputs.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {};\n Object.assign(merged, extraVariables);\n inputs.extra_variables_json = JSON.stringify(merged);\n }\n\n function buildInputs(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const inputs = {\n platform,\n target_name: targetName,\n strategy_profile: form.strategy,\n execution_mode: form.executionMode,\n variable_scope: account.variable_scope || \"default\",\n plugin_mode: normalizePluginMode(form.pluginMode),\n service_targets_mode: \"auto\",\n apply: \"true\",\n trigger_platform_sync: \"true\",\n confirm_apply: \"APPLY_AND_SYNC\",\n platform_sync_workflow: \"sync-cloud-run-env.yml\",\n };\n for (const field of [\n \"github_environment\",\n \"deployment_selector\",\n \"account_selector\",\n \"account_scope\",\n \"service_name\",\n ]) {\n if (account[field]) inputs[field] = account[field];\n }\n const reserveOverride = platformSupportsReservedCashPolicy(platform)\n ? reservePolicyOverrideForForm(form, platform)\n : null;\n if (platformSupportsReservedCashPolicy(platform)) {\n inputs.reserved_cash_policy_mode = normalizeReservePolicyMode(form.reservePolicyMode);\n if (reserveOverride) {\n Object.assign(inputs, reserveOverride.inputs);\n mergeExtraVariables(inputs, reserveOverride.extraVariables);\n }\n }\n const runtimeTargetOverride = runtimeTargetOverrideForForm(form);\n inputs.runtime_target_enabled_mode = normalizeRuntimeTargetMode(form.runtimeTargetMode);\n if (runtimeTargetOverride) {\n Object.assign(inputs, runtimeTargetOverride.inputs);\n mergeExtraVariables(inputs, runtimeTargetOverride.extraVariables);\n }\n const incomeOverride = incomeLayerOverrideForForm(form);\n inputs.income_layer_mode = normalizeIncomeLayerMode(form.incomeLayerMode);\n if (incomeOverride) {\n Object.assign(inputs, incomeOverride.inputs);\n mergeExtraVariables(inputs, incomeOverride.extraVariables);\n }\n const optionOverlayOverride = optionOverlayOverrideForForm(form);\n inputs.option_overlay_mode = normalizeOptionOverlayMode(form.optionOverlayMode);\n if (optionOverlayOverride) {\n Object.assign(inputs, optionOverlayOverride.inputs);\n }\n const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form, platform);\n if (platformSupportsMarginPolicy(platform)) {\n inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode);\n if (cashOnlyOverride) {\n Object.assign(inputs, cashOnlyOverride.inputs);\n }\n }\n const dcaOverride = dcaOverrideForForm(form);\n if (dcaOverride) {\n Object.assign(inputs, dcaOverride.inputs);\n }\n return inputs;\n }\n\n function reservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform), fallback = t(\"unchanged\")) {\n if (inputs?.reserved_cash_policy_mode === \"none\") return t(\"reservedCashNone\");\n const floor = cleanDisplayNumber(inputs?.min_reserved_cash_usd);\n const ratio = cleanDisplayRatio(inputs?.reserved_cash_ratio);\n const currency = selectedCashCurrency(platform, account);\n const hasEffectiveFloor = Boolean(floor && !(ratio && Number(floor) === 0));\n if (!hasEffectiveFloor && !ratio) return fallback;\n if (hasEffectiveFloor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;\n if (hasEffectiveFloor) return `${floor} ${currency}`;\n return formatRatioPercent(ratio);\n }\n\n function platformReservedCashDefaultText(platform = state.selected, account = selectedAccount(platform)) {\n return t(\"reservedCashDefault\").replace(\"{currency}\", selectedCashCurrency(platform, account));\n }\n\n function currentReservedCashPolicyText(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n const policy = currentReservePolicyForAccount(platform, account);\n return reservedCashPolicyText(\n {\n min_reserved_cash_usd: policy.minReservedCashUsd,\n reserved_cash_ratio: policy.reservedCashRatio,\n },\n platform,\n account,\n entry ? platformReservedCashDefaultText(platform, account) : t(\"notRead\"),\n );\n }\n\n function pendingReservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n return reservedCashPolicyText(pendingReservePolicy(inputs, platform, account).inputs, platform, account, t(\"unchanged\"));\n }\n\n function pendingReservePolicy(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const current = currentReservePolicyForAccount(platform, account);\n const currentFloor = cleanDisplayNumber(current.minReservedCashUsd);\n const currentRatio = cleanDisplayRatio(current.reservedCashRatio);\n const mode = normalizeReservePolicyMode(inputs.reserved_cash_policy_mode);\n const next = {\n min_reserved_cash_usd: cleanDisplayNumber(inputs.min_reserved_cash_usd),\n reserved_cash_ratio: cleanDisplayRatio(inputs.reserved_cash_ratio),\n };\n if (mode === \"none\") {\n next.reserved_cash_policy_mode = \"none\";\n }\n const entry = currentEntryForAccount(platform, account);\n const changed = Boolean(entry && (\n next.min_reserved_cash_usd !== currentFloor ||\n next.reserved_cash_ratio !== currentRatio ||\n (mode === \"none\" && (currentFloor || currentRatio))\n ));\n return { changed, inputs: next };\n }\n\n function currentIncomeLayerText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return t(\"incomeLayerNotSupported\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return t(\"incomeLayerDefault\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n const enabled = current.enabled ?? true;\n const startUsd = current.startUsd || String(defaults.startUsd);\n const ratio = current.maxRatio || defaults.maxRatio;\n return enabled\n ? t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(startUsd))\n .replace(\"{ratio}\", formatRatioPercent(ratio))\n : t(\"incomeLayerOff\");\n }\n\n function pendingIncomeLayerText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingIncomeLayer(inputs, platform, account);\n if (!pending.supported) return t(\"incomeLayerNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n if (pending.inputs.income_layer_enabled === false) return t(\"incomeLayerOff\");\n return t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(pending.inputs.income_layer_start_usd))\n .replace(\"{ratio}\", formatRatioPercent(pending.inputs.income_layer_max_ratio));\n }\n\n function pendingOptionOverlayText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingOptionOverlay(inputs, platform, account);\n if (!pending.supported && pending.inputs.option_overlay_enabled !== false) return t(\"optionOverlayNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return optionOverlayText(pending.inputs.option_overlay_enabled);\n }\n\n function pendingCashOnlyExecutionText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingCashOnlyExecution(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return cashOnlyExecutionText(pending.inputs.cash_only_execution);\n }\n\n function pendingRuntimeTargetText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return runtimeTargetText(pending.inputs.runtime_target_enabled);\n }\n\n function pendingRuntimeTargetTone(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return \"neutral\";\n return runtimeTargetTone(pending.inputs.runtime_target_enabled);\n }\n\n function currentDcaText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const current = currentDcaForAccount(platform, account, profile);\n if (!current.supported) return t(\"dcaNotSupported\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(current.mode))\n .replace(\"{amount}\", formatUsd(current.baseInvestmentUsd));\n }\n\n function pendingDcaText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingDca(inputs, platform, account);\n if (!pending.supported) return t(\"dcaNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(pending.inputs.dca_mode))\n .replace(\"{amount}\", formatUsd(pending.inputs.dca_base_investment_usd));\n }\n\n function pendingRuntimeTarget(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeRuntimeTargetMode(inputs.runtime_target_enabled_mode);\n if (mode === \"current\") {\n return {\n changed: false,\n inputs: { runtime_target_enabled: runtimeTargetEnabledForAccount(platform, account) ?? true },\n };\n }\n const current = runtimeTargetEnabledForAccount(platform, account);\n const currentEnabled = current ?? true;\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && currentEnabled !== nextEnabled),\n inputs: { runtime_target_enabled: nextEnabled },\n };\n }\n\n function pendingIncomeLayer(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const mode = normalizeIncomeLayerMode(inputs.income_layer_mode);\n const entry = currentEntryForAccount(platform, account);\n const rawCurrent = currentIncomeLayerForAccount(platform, account);\n const effective = effectiveIncomeLayerForAccount(platform, account, profile);\n const currentEnabled = effective?.enabled ?? true;\n const currentStartUsd = effective?.startUsd ?? String(defaults.startUsd);\n const currentRatio = effective?.maxRatio ?? defaults.maxRatio;\n if (mode === \"current\") {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: rawCurrent.enabled,\n income_layer_start_usd: rawCurrent.startUsd,\n income_layer_max_ratio: rawCurrent.maxRatio,\n },\n };\n }\n if (mode === \"disabled\") {\n if (!entry) {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n return {\n supported: true,\n changed: currentEnabled !== false || Boolean(rawCurrent.startUsd || rawCurrent.maxRatio),\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n const nextStartUsd = cleanDisplayNumber(inputs.income_layer_start_usd || defaults.startUsd);\n const nextRatio = cleanDisplayRatio(inputs.income_layer_max_ratio || defaults.maxRatio);\n return {\n supported: true,\n changed: Boolean(entry && (currentEnabled !== true || nextStartUsd !== currentStartUsd || nextRatio !== currentRatio)),\n inputs: {\n income_layer_enabled: true,\n income_layer_start_usd: nextStartUsd,\n income_layer_max_ratio: nextRatio,\n },\n };\n }\n\n function pendingOptionOverlay(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const supported = optionOverlaySupported(profile);\n const mode = normalizeOptionOverlayMode(inputs.option_overlay_mode);\n const current = effectiveOptionOverlayForAccount(platform, account, profile);\n if (mode === \"current\") {\n return {\n supported,\n changed: false,\n inputs: { option_overlay_enabled: currentOptionOverlayForAccount(platform, account) },\n };\n }\n if (mode === \"enabled\") {\n return {\n supported,\n changed: supported && current !== null && current !== true,\n inputs: { option_overlay_enabled: true },\n };\n }\n return {\n supported,\n changed: current === true,\n inputs: { option_overlay_enabled: false },\n };\n }\n\n function pendingCashOnlyExecution(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeCashOnlyExecutionMode(inputs.cash_only_execution_mode);\n if (!platformSupportsMarginPolicy(platform) || mode === \"current\") {\n return { changed: false, inputs: {} };\n }\n const current = effectiveCashOnlyExecutionForAccount(platform, account);\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && current !== nextEnabled),\n inputs: { cash_only_execution: nextEnabled },\n };\n }\n\n function pendingDca(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const current = currentDcaForAccount(platform, account, profile);\n const nextMode = normalizeDcaMode(inputs.dca_mode || defaults.defaultMode);\n const nextBase = cleanDisplayPositiveNumber(inputs.dca_base_investment_usd || defaults.defaultBaseInvestmentUsd);\n return {\n supported: true,\n changed: Boolean(current.mode !== nextMode || current.baseInvestmentUsd !== nextBase),\n inputs: {\n dca_mode: nextMode,\n dca_base_investment_usd: nextBase,\n },\n };\n }\n\n function pendingChangeState(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const currentProfile = currentStrategyForAccount(platform, account);\n const nextProfile = cleanStrategyProfile(inputs.strategy_profile);\n const currentEntry = currentEntryForAccount(platform, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n const currentPluginMode = currentPluginModeForAccount(platform, account);\n const nextPluginMode = normalizePluginMode(inputs.plugin_mode);\n const runtimeTarget = pendingRuntimeTarget(inputs, platform, account);\n const reserve = pendingReservePolicy(inputs, platform, account);\n const income = pendingIncomeLayer(inputs, platform, account);\n const optionOverlay = pendingOptionOverlay(inputs, platform, account);\n const cashOnly = pendingCashOnlyExecution(inputs, platform, account);\n const dca = pendingDca(inputs, platform, account);\n return {\n currentProfile,\n nextProfile,\n currentMode,\n currentPluginMode,\n nextPluginMode,\n strategyChanged: Boolean(nextProfile && ((state.forms[platform]?.strategyTouched) || (currentProfile && currentProfile !== nextProfile))),\n modeChanged: Boolean(inputs.execution_mode && currentMode && currentMode !== inputs.execution_mode),\n pluginModeChanged: Boolean(nextPluginMode && currentPluginMode && currentPluginMode !== nextPluginMode),\n runtimeTargetChanged: runtimeTarget.changed,\n reserveCashChanged: reserve.changed,\n incomeLayerChanged: income.changed,\n optionOverlayChanged: optionOverlay.changed,\n cashOnlyChanged: cashOnly.changed,\n dcaChanged: dca.changed,\n runtimeTarget,\n reserve,\n income,\n optionOverlay,\n cashOnly,\n dca,\n };\n }\n\n function hasPendingChanges(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const changes = pendingChangeState(inputs, platform, account);\n return Boolean(\n changes.strategyChanged ||\n changes.modeChanged ||\n changes.pluginModeChanged ||\n changes.runtimeTargetChanged ||\n changes.reserveCashChanged ||\n changes.incomeLayerChanged ||\n changes.optionOverlayChanged ||\n changes.cashOnlyChanged ||\n changes.dcaChanged\n );\n }\n\n function formatRatioPercent(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `${(numeric * 100).toFixed(2).replace(/\\.?0+$/, \"\")}%`;\n }\n\n function formatUsd(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `$${numeric.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })}`;\n }\n\n function incomeLayerAllocationText(defaults) {\n if (!defaults?.allocations) return \"\";\n return Object.entries(defaults.allocations)\n .map(([symbol, ratio]) => `${symbol} ${formatRatioPercent(ratio)}`)\n .join(\" / \");\n }\n\n function incomeLayerDefaultMetaText(defaults) {\n if (!defaults) return t(\"incomeLayerModeMeta\");\n return t(\"incomeLayerDefaultMeta\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n\n function optionOverlayDefaultMetaText(defaults) {\n if (!defaults?.families?.length) return t(\"optionOverlayModeMeta\");\n const familyText = defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family}: ${item.recipe}, ${formatUsd(item.startUsd)}, ${ratioText}`;\n }).join(\" / \");\n return t(\"optionOverlayDefaultMeta\").replace(\"{defaults}\", familyText);\n }\n\n function summaryRows(inputs) {\n const account = selectedAccount();\n const changes = pendingChangeState(inputs, state.selected, account);\n const currentStrategyText = changes.currentProfile ? strategyLabel(changes.currentProfile) : t(\"notRead\");\n const rows = [\n [t(\"repository\"), state.repositories[state.selected] || defaultRepositories[state.selected]],\n [t(\"selectedAccount\"), account.label],\n [t(\"currentStrategy\"), currentStrategyText],\n [t(\"selectedMarket\"), supportedDomainLabel(state.selected, account)],\n [\n t(\"currentRuntimeTarget\"),\n currentRuntimeTargetText(state.selected, account),\n \"\",\n currentRuntimeTargetTone(state.selected, account),\n ],\n [t(\"currentPluginMode\"), pluginModeLabel(changes.currentPluginMode)],\n [t(\"reservedCashPolicy\"), currentReservedCashPolicyText(state.selected, account)],\n ];\n if (platformSupportsMarginPolicy(state.selected)) {\n rows.push([t(\"currentCashOnlyExecution\"), currentCashOnlyExecutionText(state.selected, account)]);\n }\n if (incomeLayerSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentIncomeLayer\"), currentIncomeLayerText(state.selected, account, inputs.strategy_profile)]);\n }\n if (optionOverlaySupported(inputs.strategy_profile) || changes.optionOverlayChanged) {\n rows.push([t(\"currentOptionOverlay\"), currentOptionOverlayText(state.selected, account, inputs.strategy_profile)]);\n }\n if (dcaSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentDca\"), currentDcaText(state.selected, account, inputs.strategy_profile)]);\n }\n if (changes.reserveCashChanged) {\n rows.push([t(\"pendingReservedCashPolicy\"), pendingReservedCashPolicyText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.incomeLayerChanged) {\n rows.push([t(\"pendingIncomeLayer\"), pendingIncomeLayerText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.optionOverlayChanged) {\n rows.push([t(\"pendingOptionOverlay\"), pendingOptionOverlayText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.cashOnlyChanged) {\n rows.push([t(\"pendingCashOnlyExecution\"), pendingCashOnlyExecutionText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.dcaChanged) {\n rows.push([t(\"pendingDca\"), pendingDcaText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.modeChanged) {\n rows.push([t(\"pendingMode\"), modeLabel(inputs.execution_mode), \"pending\"]);\n }\n if (changes.pluginModeChanged) {\n rows.push([t(\"pendingPluginMode\"), pluginModeLabel(changes.nextPluginMode), \"pending\"]);\n }\n if (changes.runtimeTargetChanged) {\n rows.push([\n t(\"pendingRuntimeTarget\"),\n pendingRuntimeTargetText(inputs, state.selected, account),\n \"pending\",\n pendingRuntimeTargetTone(inputs, state.selected, account),\n ]);\n }\n if (changes.strategyChanged && changes.nextProfile) {\n rows.push([t(\"nextStrategy\"), strategyLabel(changes.nextProfile), \"pending\"]);\n }\n return rows;\n }\n\n function applyLanguage() {\n document.documentElement.lang = state.lang === \"zh\" ? \"zh-CN\" : \"en\";\n document.querySelectorAll(\"[data-i18n]\").forEach((node) => {\n node.textContent = t(node.dataset.i18n);\n });\n el(\"lang-button\").textContent = state.lang === \"zh\" ? \"EN\" : \"中\";\n }\n\n function renderPlatforms() {\n const strip = el(\"platform-strip\");\n strip.replaceChildren();\n const showPrivateConfig = hasPrivateConfig();\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const button = document.createElement(\"button\");\n button.className = \"platform-button\";\n button.type = \"button\";\n button.dataset.platform = platform;\n button.classList.toggle(\"active\", platform === state.selected);\n const mark = document.createElement(\"span\");\n mark.className = \"mark\";\n mark.textContent = meta.code;\n const copyNode = document.createElement(\"span\");\n copyNode.className = \"platform-copy\";\n const labelNode = document.createElement(\"strong\");\n labelNode.textContent = meta.label;\n copyNode.append(labelNode);\n if (showPrivateConfig) {\n const accountNode = document.createElement(\"span\");\n accountNode.textContent = account.label;\n const strategyNode = document.createElement(\"small\");\n strategyNode.textContent = strategyLabel(form.strategy);\n copyNode.append(accountNode, strategyNode);\n }\n button.append(mark, copyNode);\n strip.appendChild(button);\n }\n }\n\n function renderControls() {\n const platform = state.selected;\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const accounts = optionsFor(platform);\n const account = selectedAccount(platform);\n const choices = strategyChoicesForAccount(platform, account, form.executionMode);\n const currentStrategy = currentStrategyForAccount(platform, account);\n const currentStrategyBlocked = Boolean(\n currentStrategy &&\n !strategyAllowedForAccount(platform, account, currentStrategy, form.executionMode),\n );\n const accountSelect = el(\"account-select\");\n const strategySelect = el(\"strategy-select\");\n const runtimeTargetEnabledSelect = el(\"runtime-target-enabled-select\");\n const pluginModeSelect = el(\"plugin-mode-select\");\n const incomeLayerModeSelect = el(\"income-layer-mode-select\");\n const incomeLayerStartUsdInput = el(\"income-layer-start-usd-input\");\n const incomeLayerMaxRatioInput = el(\"income-layer-max-ratio-input\");\n const optionOverlayModeSelect = el(\"option-overlay-mode-select\");\n const cashOnlyExecutionModeSelect = el(\"cash-only-execution-mode-select\");\n const dcaModeSelect = el(\"dca-mode-select\");\n const dcaBaseInvestmentUsdInput = el(\"dca-base-investment-usd-input\");\n const reservePolicyModeSelect = el(\"reserve-policy-mode-select\");\n const minReservedCashInput = el(\"min-reserved-cash-input\");\n const reservedCashRatioInput = el(\"reserved-cash-ratio-input\");\n const showPrivateControls = hasPrivateConfig();\n\n el(\"switch-panel\").style.setProperty(\"--platform-color\", meta.accent);\n el(\"platform-title\").textContent = meta.label;\n el(\"quick-form\").hidden = !showPrivateControls;\n el(\"run-area\").hidden = !showPrivateControls;\n el(\"public-note\").hidden = showPrivateControls;\n el(\"public-preview\").hidden = showPrivateControls;\n el(\"public-note\").textContent = state.auth.allowed ? t(\"missingConfigNote\") : t(\"publicReadonly\");\n\n if (!showPrivateControls) {\n accountSelect.replaceChildren();\n strategySelect.replaceChildren();\n runtimeTargetEnabledSelect.replaceChildren();\n pluginModeSelect.replaceChildren();\n incomeLayerModeSelect.replaceChildren();\n optionOverlayModeSelect.replaceChildren();\n cashOnlyExecutionModeSelect.replaceChildren();\n dcaModeSelect.replaceChildren();\n reservePolicyModeSelect.replaceChildren();\n incomeLayerStartUsdInput.value = \"\";\n incomeLayerMaxRatioInput.value = \"\";\n dcaBaseInvestmentUsdInput.value = \"\";\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n el(\"account-meta\").textContent = \"\";\n el(\"strategy-meta\").textContent = \"\";\n el(\"income-layer-mode-meta\").textContent = \"\";\n el(\"income-layer-start-meta\").textContent = \"\";\n el(\"income-layer-ratio-meta\").textContent = \"\";\n el(\"option-overlay-mode-meta\").textContent = \"\";\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n el(\"dca-mode-meta\").textContent = \"\";\n el(\"dca-base-meta\").textContent = \"\";\n return;\n }\n\n accountSelect.replaceChildren();\n if (accounts.length) {\n for (const account of accounts) {\n accountSelect.append(new Option(account.label, account.key, false, account.key === form.accountKey));\n }\n } else {\n accountSelect.append(new Option(t(\"noAccount\"), \"\"));\n }\n el(\"account-meta\").textContent = accounts.length ? accountMetaText(platform) : \"\";\n\n if (choices.length && !choices.includes(form.strategy) && !currentStrategyBlocked) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (currentStrategyBlocked) {\n const blockedOption = new Option(\n strategyChoiceLabel(currentStrategy, platform, account, form.executionMode),\n currentStrategy,\n true,\n currentStrategy === form.strategy,\n );\n blockedOption.disabled = true;\n strategySelect.append(blockedOption);\n }\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(\n new Option(strategyChoiceLabel(strategy, platform, account, form.executionMode), strategy, false, strategy === form.strategy),\n );\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? strategyDisplayMetaText(platform, account, form.strategy)\n : \"\";\n runtimeTargetEnabledSelect.replaceChildren();\n for (const mode of runtimeTargetModes) {\n runtimeTargetEnabledSelect.append(\n new Option(runtimeTargetModeLabel(mode), mode, false, mode === normalizeRuntimeTargetMode(form.runtimeTargetMode)),\n );\n }\n pluginModeSelect.replaceChildren();\n for (const mode of pluginModes) {\n pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode)));\n }\n const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy);\n el(\"income-layer-section\").hidden = false;\n el(\"option-overlay-section\").hidden = false;\n incomeLayerModeSelect.replaceChildren();\n if (incomeDefaults) {\n incomeLayerModeSelect.disabled = false;\n for (const mode of incomeLayerModes) {\n incomeLayerModeSelect.append(new Option(incomeLayerModeLabel(mode), mode, false, mode === normalizeIncomeLayerMode(form.incomeLayerMode)));\n }\n el(\"income-layer-mode-meta\").textContent = incomeLayerDefaultMetaText(incomeDefaults);\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerAllocationMeta\").replace(\n \"{allocations}\",\n incomeLayerAllocationText(incomeDefaults),\n );\n } else {\n incomeLayerModeSelect.disabled = true;\n incomeLayerModeSelect.append(new Option(t(\"incomeLayerNotSupported\"), \"current\"));\n el(\"income-layer-mode-meta\").textContent = t(\"incomeLayerModeMeta\");\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerRatioMeta\");\n }\n const supportsMargin = platformSupportsMarginPolicy(platform);\n const supportsReserve = platformSupportsReservedCashPolicy(platform);\n if (supportsMargin) syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(form, \"margin\");\n const executionCashPolicyGrid = el(\"execution-cash-policy-grid\");\n const qmtPlatformCashNote = el(\"qmt-platform-cash-note\");\n const executionCashPolicyNote = el(\"execution-cash-policy-note\");\n executionCashPolicyGrid.hidden = !supportsMargin && !supportsReserve;\n qmtPlatformCashNote.hidden = supportsMargin || supportsReserve || platform !== \"qmt\";\n executionCashPolicyNote.hidden = !supportsMargin || !supportsReserve;\n\n const marginBlocksReserve = supportsMargin && supportsReserve && allowMarginExplicitlySelected(form);\n const reserveBlocksMargin = supportsMargin && supportsReserve && reserveCashOverrideActive(form);\n\n if (supportsReserve) {\n reservePolicyModeSelect.replaceChildren();\n for (const mode of reservePolicyModes) {\n reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode)));\n }\n const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode);\n el(\"min-reserved-cash-label\").textContent = t(\"minReservedCash\").replace(\n \"{currency}\",\n selectedCashCurrency(platform, account),\n );\n reservePolicyModeSelect.disabled = false;\n minReservedCashInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"floor\";\n minReservedCashInput.value = reserveMode === \"ratio\" || reserveMode === \"none\" ? \"\" : form.minReservedCashUsd;\n reservedCashRatioInput.value = reserveMode === \"floor\" || reserveMode === \"none\" ? \"\" : form.reservedCashRatio;\n el(\"reserve-policy-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"min-reserve-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-ratio-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-policy-mode-meta\").textContent = marginBlocksReserve\n ? t(\"executionCashMarginBlocksReserve\")\n : t(\"reservedCashModeMeta\");\n } else {\n reservePolicyModeSelect.replaceChildren();\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n }\n const incomeMode = normalizeIncomeLayerMode(form.incomeLayerMode);\n const incomeLayerInputsDisabled = !incomeDefaults || incomeMode === \"disabled\";\n incomeLayerStartUsdInput.disabled = incomeLayerInputsDisabled;\n incomeLayerMaxRatioInput.disabled = incomeLayerInputsDisabled;\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayNumber(form.incomeLayerStartUsd)) {\n form.incomeLayerStartUsd = String(incomeDefaults.startUsd);\n }\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayRatio(form.incomeLayerMaxRatio)) {\n form.incomeLayerMaxRatio = incomeDefaults.maxRatio;\n }\n incomeLayerStartUsdInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerStartUsd : \"\";\n incomeLayerMaxRatioInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerMaxRatio : \"\";\n\n const optionDefaults = optionOverlayDefaultForStrategy(form.strategy);\n optionOverlayModeSelect.replaceChildren();\n if (optionDefaults) {\n optionOverlayModeSelect.disabled = false;\n for (const mode of optionOverlayModes) {\n optionOverlayModeSelect.append(\n new Option(optionOverlayModeLabel(mode), mode, false, mode === normalizeOptionOverlayMode(form.optionOverlayMode)),\n );\n }\n el(\"option-overlay-mode-meta\").textContent = optionOverlayDefaultMetaText(optionDefaults);\n } else {\n optionOverlayModeSelect.disabled = true;\n optionOverlayModeSelect.append(new Option(t(\"optionOverlayNotSupported\"), \"current\"));\n el(\"option-overlay-mode-meta\").textContent = t(\"optionOverlayModeMeta\");\n }\n\n if (supportsMargin) {\n cashOnlyExecutionModeSelect.replaceChildren();\n for (const mode of cashOnlyExecutionModes) {\n const option = new Option(\n mode === \"enabled\" ? t(\"cashOnlyExecutionNo\") : t(\"cashOnlyExecutionYes\"),\n mode,\n false,\n mode === normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode),\n );\n cashOnlyExecutionModeSelect.append(option);\n }\n el(\"cash-only-policy-block\").classList.toggle(\"policy-block-muted\", reserveBlocksMargin);\n el(\"cash-only-execution-mode-meta\").textContent = reserveBlocksMargin\n ? t(\"executionCashReserveBlocksMargin\")\n : t(\"cashOnlyExecutionModeMeta\");\n } else {\n cashOnlyExecutionModeSelect.replaceChildren();\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n }\n\n const dcaDefaults = dcaConfigForStrategy(form.strategy);\n dcaModeSelect.replaceChildren();\n const dcaAllowed = Boolean(dcaDefaults) && platformSupportsDca(platform);\n if (dcaAllowed) {\n dcaModeSelect.disabled = false;\n for (const mode of dcaModes) {\n dcaModeSelect.append(new Option(dcaModeLabel(mode), mode, false, mode === normalizeDcaMode(form.dcaMode)));\n }\n if (!cleanDisplayPositiveNumber(form.dcaBaseInvestmentUsd)) {\n form.dcaBaseInvestmentUsd = dcaDefaults.defaultBaseInvestmentUsd;\n }\n dcaBaseInvestmentUsdInput.disabled = false;\n dcaBaseInvestmentUsdInput.value = form.dcaBaseInvestmentUsd;\n el(\"dca-mode-meta\").textContent = t(\"dcaDefaultMeta\")\n .replace(\"{mode}\", dcaModeLabel(dcaDefaults.defaultMode))\n .replace(\"{amount}\", formatUsd(dcaDefaults.defaultBaseInvestmentUsd));\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n } else {\n dcaModeSelect.disabled = true;\n dcaModeSelect.append(new Option(\n dcaDefaults && !platformSupportsDca(platform) ? t(\"dcaPlatformNotSupported\") : t(\"dcaNotSupported\"),\n \"fixed\",\n ));\n dcaBaseInvestmentUsdInput.disabled = true;\n dcaBaseInvestmentUsdInput.value = \"\";\n el(\"dca-mode-meta\").textContent = t(\"dcaModeMeta\");\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n }\n\n if (platformDryRunOnly(platform)) {\n form.executionMode = \"paper\";\n }\n document.querySelectorAll(\"#mode-control [data-mode]\").forEach((button) => {\n const dryRunOnly = platformDryRunOnly(platform);\n button.disabled = dryRunOnly && button.dataset.mode === \"live\";\n button.classList.toggle(\"active\", button.dataset.mode === form.executionMode);\n });\n el(\"mode-meta\").textContent = platformDryRunOnly(platform) ? t(\"qmtDryRunOnlyNote\") : \"\";\n }\n\n function renderSummary() {\n const showSummary = hasPrivateConfig();\n const summaryPanel = document.querySelector(\".summary-panel\");\n const switchSurface = document.querySelector(\".switch-surface\");\n summaryPanel.hidden = !showSummary;\n switchSurface.classList.toggle(\"summary-hidden\", !showSummary);\n if (!showSummary) return;\n\n const inputs = buildInputs();\n const list = el(\"summary-list\");\n list.replaceChildren();\n document.querySelector(\".summary-head h2\").textContent = t(\"summary\");\n for (const [label, value, rowClass, valueTone] of summaryRows(inputs)) {\n const row = document.createElement(\"div\");\n row.className = \"summary-row\";\n row.setAttribute(\"role\", \"listitem\");\n if (rowClass) row.classList.add(rowClass);\n const labelNode = document.createElement(\"div\");\n labelNode.className = \"summary-label\";\n labelNode.textContent = label;\n const valueNode = document.createElement(\"div\");\n valueNode.className = \"summary-value\";\n if (valueTone) {\n const badge = document.createElement(\"span\");\n badge.className = `summary-status ${valueTone}`;\n badge.textContent = value;\n valueNode.appendChild(badge);\n } else {\n valueNode.textContent = value;\n }\n row.append(labelNode, valueNode);\n list.appendChild(row);\n }\n\n const account = selectedAccount();\n const currentEntry = currentEntryForAccount(state.selected, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n el(\"mode-pill\").textContent = currentMode ? modeLabel(currentMode) : t(\"notRead\");\n }\n\n function renderAuth() {\n const status = el(\"auth-status\");\n const loginLink = el(\"login-link\");\n const logoutButton = el(\"logout-button\");\n const signedIn = Boolean(state.auth.allowed && state.auth.login);\n\n status.hidden = !signedIn;\n status.textContent = signedIn ? t(\"signedInAs\").replace(\"{login}\", state.auth.login) : \"\";\n loginLink.hidden = signedIn;\n loginLink.href = \"/login\";\n loginLink.textContent = t(\"login\");\n logoutButton.hidden = !signedIn;\n logoutButton.textContent = t(\"logout\");\n\n const dispatch = el(\"dispatch-button\");\n const hasPrivateAccounts = state.configSource === \"private\";\n const loadingConfig = state.configSource === \"loading\";\n const hasRunnableStrategy = hasRunnableStrategySelection();\n const hasValidReserve = hasValidExecutionCashPolicy();\n const hasValidIncomeLayer = hasValidIncomeLayerPolicy();\n const hasValidOptionOverlay = hasValidOptionOverlayPolicy();\n const hasValidDca = hasValidDcaPolicy();\n const hasValidStrategy = hasRunnableStrategy &&\n hasValidReserve &&\n hasValidIncomeLayer &&\n hasValidOptionOverlay &&\n hasValidDca;\n const hasPendingChange = hasPrivateAccounts && hasValidStrategy && hasPendingChanges(buildInputs());\n dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy || !hasPendingChange;\n dispatch.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfig\")\n : (hasPrivateAccounts ? (hasValidStrategy ? (hasPendingChange ? t(\"runSwitch\") : t(\"noChanges\")) : t(\"configureAccounts\")) : t(\"configureAccounts\")))\n : t(\"loginToRun\");\n const note = el(\"action-note\");\n note.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfigNote\")\n : (hasPrivateAccounts\n ? (hasRunnableStrategy\n ? (hasValidReserve\n ? (hasValidIncomeLayer\n ? (hasValidOptionOverlay\n ? (hasValidDca ? (hasPendingChange ? t(\"readyNote\") : \"\") : t(\"invalidDcaNote\"))\n : t(\"invalidOptionOverlayNote\"))\n : t(\"invalidIncomeLayerNote\"))\n : (executionCashPolicyConflict(state.forms[state.selected])\n ? t(\"invalidExecutionCashPolicyNote\")\n : t(\"invalidReservePolicyNote\")))\n : strategyActionNoteText())\n : t(\"missingConfigNote\")))\n : t(\"readonlyNote\");\n note.classList.toggle(\n \"warning\",\n state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy),\n );\n }\n\n function renderAppVisibility() {\n document.body.classList.toggle(\"app-loading\", !state.appReady);\n el(\"boot-message\").textContent = t(state.bootMessageKey);\n }\n\n function render() {\n applyLanguage();\n renderPlatforms();\n renderControls();\n renderSummary();\n renderAuth();\n renderAppVisibility();\n }\n\n async function refreshSession() {\n state.bootMessageKey = \"bootSession\";\n render();\n try {\n const session = await requestJson(\"/api/session\");\n state.auth = {\n available: true,\n allowed: Boolean(session.allowed),\n admin: Boolean(session.admin),\n login: session.login || null,\n };\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n }\n if (state.auth.allowed) {\n await refreshConfig();\n } else {\n state.bootMessageKey = \"bootPublic\";\n state.appReady = true;\n render();\n }\n }\n\n async function refreshStrategyProfiles() {\n state.bootMessageKey = \"bootStrategy\";\n render();\n try {\n const payload = await requestJson(\"/api/strategy-profiles\");\n applyStrategyProfiles(payload.strategyProfiles || []);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n } catch {\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n }\n }\n\n async function refreshConfig() {\n if (!state.auth.available || !state.auth.allowed) return;\n state.configSource = \"loading\";\n state.bootMessageKey = \"bootConfig\";\n render();\n try {\n const payload = await requestJson(\"/api/config\");\n if (payload.accountOptions) {\n applyStrategyProfiles(payload.strategyProfiles || defaultStrategyProfiles);\n state.accountOptions = normalizeAccountOptions(payload.accountOptions);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n state.repositories = normalizePlatformRepositories(payload.platformRepositories || {});\n state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});\n state.configSource = \"private\";\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n syncStrategyForAccount(platform);\n }\n } else {\n state.configSource = \"default\";\n state.currentStrategies = {};\n }\n } catch (error) {\n state.configSource = \"default\";\n state.currentStrategies = {};\n if (isRequestTimeoutError(error)) {\n state.bootMessageKey = \"bootTimeout\";\n } else {\n state.bootMessageKey = \"bootPublic\";\n }\n } finally {\n state.appReady = true;\n render();\n }\n }\n\n function normalizeAccountOptions(raw) {\n const normalized = clone(defaultAccountOptions);\n for (const platform of Object.keys(platformMeta)) {\n if (!Array.isArray(raw[platform]) || !raw[platform].length) continue;\n normalized[platform] = raw[platform].map((item, index) => ({\n key: String(item.key || item.target_name || index),\n label: String(item.label || item.target_name || item.key || platform),\n target_name: String(item.target_name || item.key || \"\"),\n account_selector: item.account_selector ? String(item.account_selector) : \"\",\n deployment_selector: item.deployment_selector ? String(item.deployment_selector) : \"\",\n account_scope: item.account_scope ? String(item.account_scope) : \"\",\n service_name: item.service_name ? String(item.service_name) : \"\",\n cash_currency: item.cash_currency || item.market_currency || item.trading_currency\n ? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase()\n : \"\",\n supported_domains: normalizeSupportedDomains(platform, item),\n github_environment: item.github_environment ? String(item.github_environment) : \"\",\n variable_scope: item.variable_scope ? String(item.variable_scope) : \"\",\n plugin_mode: item.plugin_mode ? String(item.plugin_mode) : \"\",\n option_overlay_mode: item.option_overlay_mode ? normalizeOptionOverlayMode(item.option_overlay_mode) : \"\",\n cash_only_execution_mode: item.cash_only_execution_mode\n ? normalizeCashOnlyExecutionMode(item.cash_only_execution_mode)\n : \"\",\n dca_mode: item.dca_mode ? normalizeDcaMode(item.dca_mode) : \"\",\n dca_base_investment_usd: cleanDisplayPositiveNumber(item.dca_base_investment_usd),\n }));\n }\n return normalized;\n }\n\n function normalizeSupportedDomains(platform, item) {\n const raw = Array.isArray(item?.supported_domains)\n ? item.supported_domains\n : String(item?.supported_domains || \"\").split(/[\\s,;]+/);\n const cleaned = raw.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n return inferSupportedDomains(platform, item || {});\n }\n\n function normalizeCurrentStrategies(raw) {\n const normalized = {};\n for (const platform of Object.keys(platformMeta)) {\n if (!raw[platform] || typeof raw[platform] !== \"object\" || Array.isArray(raw[platform])) continue;\n normalized[platform] = {};\n for (const [key, entry] of Object.entries(raw[platform])) {\n const profile = cleanStrategyProfile(entry?.strategy_profile);\n const minReservedCashUsd = cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd);\n const reservedCashRatio = cleanDisplayRatio(entry?.reserved_cash_ratio);\n const incomeLayerEnabled = cleanOptionalBoolean(entry?.income_layer_enabled);\n const incomeLayerStartUsd = cleanDisplayNumber(entry?.income_layer_start_usd);\n const incomeLayerMaxRatio = cleanDisplayRatio(entry?.income_layer_max_ratio);\n const optionOverlayEnabled = cleanOptionalBoolean(entry?.option_overlay_enabled);\n const cashOnlyExecution = cleanOptionalBoolean(entry?.cash_only_execution);\n const runtimeTargetEnabled = cleanOptionalBoolean(entry?.runtime_target_enabled);\n const dcaMode = entry?.dca_mode ? normalizeDcaMode(entry.dca_mode) : \"\";\n const dcaBaseInvestmentUsd = cleanDisplayPositiveNumber(entry?.dca_base_investment_usd);\n const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only);\n if (\n !profile &&\n !minReservedCashUsd &&\n !reservedCashRatio &&\n incomeLayerEnabled === null &&\n !incomeLayerStartUsd &&\n !incomeLayerMaxRatio &&\n optionOverlayEnabled === null &&\n cashOnlyExecution === null &&\n runtimeTargetEnabled === null &&\n !dcaMode &&\n !dcaBaseInvestmentUsd &&\n !executionMode\n ) continue;\n normalized[platform][String(key)] = {\n strategy_profile: profile,\n execution_mode: executionMode,\n dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === \"true\" || entry?.dry_run_only === \"1\",\n min_reserved_cash_usd: minReservedCashUsd,\n reserved_cash_ratio: reservedCashRatio,\n income_layer_enabled: incomeLayerEnabled,\n income_layer_start_usd: incomeLayerStartUsd,\n income_layer_max_ratio: incomeLayerMaxRatio,\n option_overlay_enabled: optionOverlayEnabled,\n cash_only_execution: cashOnlyExecution,\n runtime_target_enabled: runtimeTargetEnabled,\n dca_mode: dcaMode,\n dca_base_investment_usd: dcaBaseInvestmentUsd,\n source: entry?.source ? String(entry.source) : \"\",\n };\n }\n if (!Object.keys(normalized[platform]).length) delete normalized[platform];\n }\n return normalized;\n }\n\n function normalizePlatformRepositories(raw) {\n const normalized = clone(defaultRepositories);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return normalized;\n for (const platform of Object.keys(platformMeta)) {\n const repository = String(raw[platform] || \"\").trim();\n if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(repository)) {\n normalized[platform] = repository;\n }\n }\n return normalized;\n }\n\n async function dispatchSwitch() {\n if (!state.auth.allowed) return;\n showToast(t(\"dispatching\"), { duration: 0 });\n try {\n const response = await fetch(\"/api/switch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildInputs()),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || t(\"dispatchFailed\"));\n showToast(t(\"dispatched\"), { duration: 4000 });\n if (payload.actions_url) window.open(payload.actions_url, \"_blank\", \"noopener,noreferrer\");\n await refreshConfig();\n } catch (error) {\n showToast(`${t(\"dispatchFailed\")}: ${error.message}`, { duration: 12000 });\n }\n }\n\n async function handleLogout() {\n await fetch(\"/api/logout\", { method: \"POST\" });\n window.location.reload();\n }\n\n function summaryText() {\n const inputs = buildInputs();\n return summaryRows(inputs).map(([label, value]) => `${label}: ${value}`).join(\"\\\n\");\n }\n\n el(\"platform-strip\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-platform]\");\n if (!button) return;\n state.selected = button.dataset.platform;\n state.forms[state.selected].strategyTouched = false;\n render();\n });\n\n el(\"account-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].accountKey = el(\"account-select\").value;\n state.forms[state.selected].runtimeTargetTouched = false;\n state.forms[state.selected].reservedCashTouched = false;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].cashOnlyExecutionTouched = false;\n state.forms[state.selected].dcaTouched = false;\n state.forms[state.selected].strategyTouched = false;\n syncStrategyForAccount(state.selected);\n render();\n });\n\n el(\"strategy-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].strategy = el(\"strategy-select\").value;\n state.forms[state.selected].strategyTouched = true;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].dcaTouched = false;\n syncIncomeLayerForAccount(state.selected);\n syncOptionOverlayForAccount(state.selected);\n syncDcaForAccount(state.selected);\n render();\n });\n\n el(\"mode-control\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-mode]\");\n if (!button || button.disabled) return;\n if (platformDryRunOnly(state.selected) && button.dataset.mode === \"live\") return;\n state.forms[state.selected].executionMode = button.dataset.mode;\n render();\n });\n\n el(\"plugin-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.pluginMode = normalizePluginMode(el(\"plugin-mode-select\").value);\n render();\n });\n\n el(\"runtime-target-enabled-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.runtimeTargetMode = normalizeRuntimeTargetMode(el(\"runtime-target-enabled-select\").value);\n form.runtimeTargetTouched = form.runtimeTargetMode !== \"current\";\n render();\n });\n\n el(\"income-layer-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerMode = normalizeIncomeLayerMode(el(\"income-layer-mode-select\").value);\n form.incomeLayerTouched = form.incomeLayerMode !== \"current\";\n if (form.incomeLayerMode === \"current\") {\n form.incomeLayerTouched = false;\n syncIncomeLayerForAccount(state.selected);\n }\n render();\n });\n\n el(\"income-layer-start-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerStartUsd = el(\"income-layer-start-usd-input\").value.trim();\n render();\n });\n\n el(\"income-layer-max-ratio-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerMaxRatio = el(\"income-layer-max-ratio-input\").value.trim();\n render();\n });\n\n el(\"option-overlay-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.optionOverlayMode = normalizeOptionOverlayMode(el(\"option-overlay-mode-select\").value);\n form.optionOverlayTouched = form.optionOverlayMode !== \"current\";\n render();\n });\n\n el(\"cash-only-execution-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.cashOnlyExecutionMode = normalizeCashOnlyExecutionMode(el(\"cash-only-execution-mode-select\").value);\n form.cashOnlyExecutionTouched = form.cashOnlyExecutionMode !== \"current\";\n if (allowMarginExplicitlySelected(form)) reconcileExecutionCashPolicy(form, \"margin\");\n else restoreReserveAfterMarginDisabled(form);\n render();\n });\n\n el(\"dca-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaMode = normalizeDcaMode(el(\"dca-mode-select\").value);\n render();\n });\n\n el(\"dca-base-investment-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaBaseInvestmentUsd = el(\"dca-base-investment-usd-input\").value.trim();\n render();\n });\n\n el(\"reserve-policy-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.reservePolicyMode = normalizeReservePolicyMode(el(\"reserve-policy-mode-select\").value);\n form.reservedCashTouched = form.reservePolicyMode !== \"current\";\n if (form.reservePolicyMode === \"current\") syncReservePolicyForAccount(state.selected);\n reconcileExecutionCashPolicy(form, \"reserve\");\n render();\n });\n\n el(\"min-reserved-cash-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].minReservedCashUsd = el(\"min-reserved-cash-input\").value.trim();\n render();\n });\n\n el(\"reserved-cash-ratio-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].reservedCashRatio = el(\"reserved-cash-ratio-input\").value.trim();\n render();\n });\n\n el(\"copy-button\").addEventListener(\"click\", async () => {\n try {\n await navigator.clipboard.writeText(summaryText());\n showToast(t(\"copied\"), { duration: 3000 });\n } catch {\n showToast(summaryText(), { duration: 0 });\n }\n });\n\n el(\"dispatch-button\").addEventListener(\"click\", dispatchSwitch);\n el(\"logout-button\").addEventListener(\"click\", handleLogout);\n el(\"lang-button\").addEventListener(\"click\", () => {\n state.lang = state.lang === \"zh\" ? \"en\" : \"zh\";\n localStorage.setItem(\"qsl-switch-lang\", state.lang);\n render();\n });\n\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n boot();\n\n async function boot() {\n try {\n await refreshStrategyProfiles();\n await refreshSession();\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n state.configSource = \"default\";\n state.currentStrategies = {};\n state.bootMessageKey = \"bootTimeout\";\n state.appReady = true;\n render();\n }\n }\n"; +export const APP_JS = "\n\n let platformMeta = {\n binance: { label: \"Binance\", code: \"BN\", accent: \"var(--bn)\" },\n firstrade: { label: \"Firstrade\", code: \"FT\", accent: \"var(--ft)\" },\n ibkr: { label: \"IBKR\", code: \"IB\", accent: \"var(--ib)\" },\n longbridge: { label: \"LongBridge\", code: \"LB\", accent: \"var(--lb)\" },\n qmt: { label: \"QMT\", code: \"QM\", accent: \"var(--qmt)\" },\n schwab: { label: \"Schwab\", code: \"SW\", accent: \"var(--sw)\" },\n };\n\n const platformRepositories = {\n binance: \"QuantStrategyLab/BinancePlatform\",\n firstrade: \"QuantStrategyLab/FirstradePlatform\",\n ibkr: \"QuantStrategyLab/InteractiveBrokersPlatform\",\n longbridge: \"QuantStrategyLab/LongBridgePlatform\",\n qmt: \"QuantStrategyLab/QmtPlatform\",\n schwab: \"QuantStrategyLab/CharlesSchwabPlatform\",\n };\n // Alias for backward compatibility\n const defaultRepositories = platformRepositories;\n\n const defaultAccountOptions = window.__DEFAULT_ACCOUNT_OPTIONS__ || {\n binance: [{\"key\": \"default\", \"label\": \"Binance\", \"target_name\": \"default\", \"cash_currency\": \"USD\", \"supported_domains\": [\"crypto\"]}],\n firstrade: [{\"key\": \"preview\", \"label\": \"Firstrade\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"firstrade-quant-service\"}],\n ibkr: [{\"key\": \"preview\", \"label\": \"IBKR\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n longbridge: [{\"key\": \"preview\", \"label\": \"LongBridge\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n qmt: [{\"key\": \"default\", \"label\": \"QMT\", \"target_name\": \"default\", \"cash_currency\": \"CNY\", \"supported_domains\": [\"cn_equity\"], \"service_name\": \"qmt-quant-service\"}],\n schwab: [{\"key\": \"preview\", \"label\": \"Schwab\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"charles-schwab-quant-service\"}],\n };\n\n const domainLabels = window.__DOMAIN_LABELS__ || {\n cn_equity: { zh: \"A股\", en: \"CN A-share\" },\n crypto: { zh: \"加密\", en: \"Crypto\" },\n hk_equity: { zh: \"港股\", en: \"HK Equity\" },\n us_equity: { zh: \"美股\", en: \"US Equity\" },\n };\n\n const platformConfig = window.__PLATFORM_CONFIG__ || {\n binance: {\n dry_run_only: false,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n firstrade: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"firstrade-quant-service\",\n default_execution_mode: \"live\"\n },\n ibkr: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n longbridge: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n qmt: {\n dry_run_only: true,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"paper\",\n service_name: \"qmt-quant-service\",\n default_execution_mode: \"paper\"\n },\n schwab: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"charles-schwab-quant-service\",\n default_execution_mode: \"live\"\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 const reservePolicyModes = [\"none\", \"ratio\", \"floor\", \"max\"];\n const incomeLayerModes = [\"enabled\", \"disabled\"];\n const optionOverlayModes = [\"enabled\", \"disabled\"];\n const cashOnlyExecutionModes = [\"enabled\", \"disabled\"];\n const runtimeTargetModes = [\"enabled\", \"disabled\"];\n const pluginModes = [\"auto\", \"none\"];\n const dcaModes = [\"fixed\", \"smart\"];\n const runtimeTargetEnabledVariable = \"RUNTIME_TARGET_ENABLED\";\n const incomeLayerEnabledVariable = \"INCOME_LAYER_ENABLED\";\n const incomeLayerStartUsdVariable = \"INCOME_LAYER_START_USD\";\n const incomeLayerMaxRatioVariable = \"INCOME_LAYER_MAX_RATIO\";\n const dcaProfileDefaults = window.__DCA_PROFILE_DEFAULTS__ || {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n };\n const APP_BOOT_TIMEOUT_MS = 15000;\n const platformMinReservedCashVariables = {\n longbridge: \"LONGBRIDGE_MIN_RESERVED_CASH_USD\",\n ibkr: \"IBKR_MIN_RESERVED_CASH_USD\",\n schwab: \"SCHWAB_MIN_RESERVED_CASH_USD\",\n firstrade: \"FIRSTRADE_MIN_RESERVED_CASH_USD\",\n };\n const platformReservedCashRatioVariables = {\n longbridge: \"LONGBRIDGE_RESERVED_CASH_RATIO\",\n ibkr: \"IBKR_RESERVED_CASH_RATIO\",\n schwab: \"SCHWAB_RESERVED_CASH_RATIO\",\n firstrade: \"FIRSTRADE_RESERVED_CASH_RATIO\",\n };\n\n const defaultStrategyProfiles = window.__DEFAULT_STRATEGY_PROFILES__ || [\n {\n \"profile\": \"tqqq_growth_income\",\n \"label\": \"纳斯达克增长收益\",\n \"label_en\": \"NASDAQ Growth Income\",\n \"label_zh\": \"纳斯达克增长收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"250000\",\n \"income_layer_max_ratio\": \"0.55\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.3,\n \"DGRO\": 0.2,\n \"SGOV\": 0.4,\n \"SPYI\": 0.08,\n \"QQQI\": 0.02\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"tqqq_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"250000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.03\"\n },\n {\n \"profile\": \"soxl_soxx_trend_income\",\n \"label\": \"半导体趋势收益\",\n \"label_en\": \"Semiconductor Trend Income\",\n \"label_zh\": \"半导体趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"150000\",\n \"income_layer_max_ratio\": \"0.95\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.15,\n \"DGRO\": 0.1,\n \"SGOV\": 0.7,\n \"SPYI\": 0.04,\n \"QQQI\": 0.01\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_income_overlay_enabled\": true,\n \"option_income_overlay_recipe\": \"soxx_put_credit_spread_income_v1\",\n \"option_income_overlay_start_usd\": \"150000\",\n \"option_income_overlay_nav_risk_ratio\": \"0.01\"\n },\n {\n \"profile\": \"nasdaq_sp500_smart_dca\",\n \"label\": \"纳指标普定投\",\n \"label_en\": \"NASDAQ/S&P 500 DCA\",\n \"label_zh\": \"纳指标普定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"ibit_smart_dca\",\n \"label\": \"IBIT比特币定投\",\n \"label_en\": \"IBIT Bitcoin DCA\",\n \"label_zh\": \"IBIT比特币定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"global_etf_rotation\",\n \"label\": \"全球ETF轮动\",\n \"label_en\": \"Global ETF Rotation\",\n \"label_zh\": \"全球ETF轮动\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"500000\",\n \"income_layer_max_ratio\": \"0.15\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.4,\n \"DGRO\": 0.25,\n \"SGOV\": 0.3,\n \"SPYI\": 0.05\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"500000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"russell_top50_leader_rotation\",\n \"label\": \"罗素Top50领涨\",\n \"label_en\": \"Russell Top50 Leaders\",\n \"label_zh\": \"罗素Top50领涨\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.45,\n \"DGRO\": 0.3,\n \"SGOV\": 0.25\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"tecl_xlk_trend_income\",\n \"label\": \"TECL/XLK趋势收益\",\n \"label_en\": \"TECL/XLK Trend Income\",\n \"label_zh\": \"TECL/XLK趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"failed_promotion_vs_live_profiles\"\n },\n {\n \"profile\": \"us_equity_combo\",\n \"label\": \"美股核心组合\",\n \"label_en\": \"US Core Combo\",\n \"label_zh\": \"美股核心组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.25,\n \"DGRO\": 0.25,\n \"SGOV\": 0.2,\n \"SPYI\": 0.15,\n \"QQQI\": 0.15\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"us_equity_combo_core\",\n \"label\": \"美股核心组合影子\",\n \"label_en\": \"US Core Combo Shadow\",\n \"label_zh\": \"美股核心组合影子\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"us_equity_combo_leveraged\",\n \"label\": \"美股加速组合\",\n \"label_en\": \"US Alpha Combo\",\n \"label_zh\": \"美股加速组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"hk_global_etf_tactical_rotation\",\n \"label\": \"港股ETF战术轮动\",\n \"label_en\": \"HK ETF Tactical Rotation\",\n \"label_zh\": \"港股ETF战术轮动\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_low_vol_dividend_quality_snapshot\",\n \"label\": \"港股红利质量\",\n \"label_en\": \"HK Dividend Quality\",\n \"label_zh\": \"港股红利质量\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_equity_combo\",\n \"label\": \"港股恒生组合\",\n \"label_en\": \"HK Core Combo\",\n \"label_zh\": \"港股恒生组合\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation\",\n \"label\": \"A股行业ETF轮动\",\n \"label_en\": \"CN Industry ETF Rotation\",\n \"label_zh\": \"A股行业ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation_aggressive\",\n \"label\": \"A股ETF轮动\",\n \"label_en\": \"CN ETF Rotation\",\n \"label_zh\": \"A股ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"live_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"live_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_index_etf_tactical_rotation\",\n \"label\": \"A股宽基ETF战术轮动\",\n \"label_en\": \"CN Index ETF Tactical Rotation\",\n \"label_zh\": \"A股宽基ETF战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_tactical_rotation\",\n \"label\": \"创业板战术轮动\",\n \"label_en\": \"CN ChiNext Tactical Rotation\",\n \"label_zh\": \"创业板战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality\",\n \"label\": \"创业板成长动量质量\",\n \"label_en\": \"CN ChiNext Growth Momentum Quality\",\n \"label_zh\": \"创业板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_dividend_quality_snapshot\",\n \"label\": \"A股红利质量\",\n \"label_en\": \"CN Dividend Quality\",\n \"label_zh\": \"A股红利质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality_snapshot\",\n \"label\": \"创业板成长质量快照\",\n \"label_en\": \"CN ChiNext Growth Quality Snapshot\",\n \"label_zh\": \"创业板成长质量快照\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_star_growth_momentum_quality\",\n \"label\": \"科创板成长动量质量\",\n \"label_en\": \"CN STAR Growth Momentum Quality\",\n \"label_zh\": \"科创板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_equity_combo\",\n \"label\": \"A股进取组合\",\n \"label_en\": \"CN Alpha Combo\",\n \"label_zh\": \"A股进取组合\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"crypto_live_pool_rotation\",\n \"label\": \"加密实时池轮动\",\n \"label_en\": \"Crypto Live Pool Rotation\",\n \"label_zh\": \"加密实时池轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"crypto_btc_dca\",\n \"label\": \"BTC定投\",\n \"label_en\": \"BTC DCA\",\n \"label_zh\": \"BTC定投\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_trend_rotation\",\n \"label\": \"山寨趋势轮动\",\n \"label_en\": \"Altcoin Trend\",\n \"label_zh\": \"山寨趋势轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_equity_combo\",\n \"label\": \"加密动量组合\",\n \"label_en\": \"Crypto Core Combo\",\n \"label_zh\": \"加密动量组合\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"纳斯达克增长收益\", en: \"NASDAQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"半导体趋势收益\", en: \"Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指标普定投\", en: \"NASDAQ/S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT比特币定投\", en: \"IBIT Bitcoin DCA\" },\n global_etf_rotation: { zh: \"全球ETF轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素Top50领涨\", en: \"Russell Top50 Leaders\" },\n hk_global_etf_tactical_rotation: { zh: \"港股ETF战术轮动\", en: \"HK ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股红利质量\", en: \"HK Dividend Quality\" },\n cn_industry_etf_rotation: { zh: \"A股行业ETF轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量\", en: \"CN Dividend Quality\" },\n us_equity_combo: { zh: \"美股核心组合\", en: \"US Core Combo\" },\n us_equity_combo_leveraged: { zh: \"美股加速组合\", en: \"US Alpha Combo\" },\n hk_equity_combo: { zh: \"港股恒生组合\", en: \"HK Core Combo\" },\n cn_industry_etf_rotation_aggressive: { zh: \"A股ETF轮动\", en: \"CN ETF Rotation\" },\n cn_stock_momentum_rotation: { zh: \"A股个股动量\", en: \"CN Stock Momentum\" },\n cn_equity_combo: { zh: \"A股进取组合\", en: \"CN Alpha Combo\" },\n crypto_btc_dca: { zh: \"BTC定投\", en: \"BTC DCA\" },\n crypto_trend_rotation: { zh: \"山寨趋势轮动\", en: \"Altcoin Trend\" },\n crypto_equity_combo: { zh: \"加密动量组合\", en: \"Crypto Core Combo\" },\n };\n\n const fallbackIncomeLayerDefaults = window.__INCOME_LAYER_DEFAULTS__ || {\n tqqq_growth_income: {\n startUsd: 250000,\n maxRatio: \"0.55\",\n allocations: { SCHD: 0.30, DGRO: 0.20, SGOV: 0.40, SPYI: 0.08, QQQI: 0.02 },\n },\n soxl_soxx_trend_income: {\n startUsd: 150000,\n maxRatio: \"0.95\",\n allocations: { SCHD: 0.15, DGRO: 0.10, SGOV: 0.70, SPYI: 0.04, QQQI: 0.01 },\n },\n global_etf_rotation: {\n startUsd: 500000,\n maxRatio: \"0.15\",\n allocations: { SCHD: 0.40, DGRO: 0.25, SGOV: 0.30, SPYI: 0.05 },\n },\n russell_top50_leader_rotation: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.45, DGRO: 0.30, SGOV: 0.25 },\n },\n us_equity_combo: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.20, SPYI: 0.15, QQQI: 0.15 },\n }};\n let incomeLayerDefaults = {};\n const fallbackOptionOverlayDefaults = window.__OPTION_OVERLAY_DEFAULTS__ || {\n tqqq_growth_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"tqqq_leaps_growth_v1\", startUsd: \"250000\", ratio: \"0.03\", ratioKind: \"budget\" },\n ],\n },\n soxl_soxx_trend_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"income\", recipe: \"soxx_put_credit_spread_income_v1\", startUsd: \"150000\", ratio: \"0.01\", ratioKind: \"risk\" },\n ],\n },\n global_etf_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"500000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n russell_top50_leader_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo_leveraged: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [],\n }};\n let optionOverlayDefaults = {};\n\n const strategyDomains = [\"us_equity\", \"hk_equity\", \"cn_equity\", \"crypto\"];\n let strategyOptions = [];\n let strategyLabels = {};\n let strategyCatalog = {};\n\n\n const copy = {\n zh: {\n appTitle: \"策略切换\",\n appSubtitle: \"选平台、目标账号和策略,一次执行完成切换。\",\n healthView: \"策略健康\",\n switchView: \"实盘切换\",\n healthEyebrow: \"策略健康 / 只读\",\n healthTitle: \"先看机器结论,再决定动作。\",\n healthSubtitle: \"健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。\",\n healthTotal: \"策略总数\",\n healthHealthy: \"健康\",\n healthWatch: \"观察\",\n healthReview: \"需要复核\",\n healthCritical: \"严重\",\n healthBoard: \"策略状态\",\n bootKicker: \"初始化控制台\",\n bootTitle: \"读取策略配置\",\n bootMessage: \"正在读取登录状态、账号配置和当前状态。\",\n bootStrategy: \"正在读取策略目录。\",\n bootSession: \"正在验证登录状态。\",\n bootConfig: \"正在读取账号配置和当前状态。\",\n bootTimeout: \"加载超时,已切换到公开预览(登录后可重试)。\",\n bootPublic: \"公开预览已就绪。\",\n login: \"登录\",\n logout: \"退出\",\n signedInAs: \"已登录 {login}\",\n activePlatform: \"当前平台\",\n account: \"目标账号\",\n strategy: \"策略\",\n mode: \"模式\",\n live: \"实盘\",\n paper: \"模拟\",\n runtimeTargetMode: \"账号运行状态\",\n runtimeSectionTitle: \"运行与插件\",\n runtimeTargetCurrent: \"沿用当前状态\",\n runtimeTargetEnabled: \"启用\",\n runtimeTargetDisabled: \"禁用\",\n runtimeTargetModeMeta: \"停用后正式运行会跳过,模拟运行和健康检查仍可用。\",\n pluginMode: \"插件启用范围\",\n pluginModeAuto: \"启用插件\",\n pluginModeNone: \"禁用插件\",\n pluginModeMeta: \"选择是否启用该策略的插件。\",\n incomeLayerMode: \"收入层状态\",\n incomeLayerSectionTitle: \"收入层\",\n incomeLayerCurrent: \"沿用当前配置\",\n incomeLayerEnabled: \"开启收入层\",\n incomeLayerDisabled: \"关闭收入层\",\n incomeLayerNotSupported: \"该策略未定义收入层\",\n incomeLayerStartUsd: \"收入层起始金额\",\n incomeLayerMaxRatio: \"收入层最高比例\",\n incomeLayerModeMeta: \"仅对已定义收入层的美股策略生效。\",\n incomeLayerDefaultMeta: \"策略默认:起始 {start},最高 {ratio}。\",\n incomeLayerAllocationMeta: \"默认分配:{allocations}。\",\n incomeLayerStartMeta: \"总资产达到该金额后启用收入层。\",\n incomeLayerRatioMeta: \"例如 0.55 表示最高 55%。\",\n optionOverlayMode: \"期权层状态\",\n optionOverlaySectionTitle: \"期权层\",\n optionOverlayCurrent: \"沿用当前配置\",\n optionOverlayEnabled: \"启用期权层\",\n optionOverlayDisabled: \"关闭期权层\",\n optionOverlayNotSupported: \"该策略未定义期权层\",\n optionOverlayModeMeta: \"启用时使用策略默认的最佳 recipe 和预算,不在这里手动调比例。\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"增长\",\n optionOverlayFamilyIncome: \"收入\",\n optionOverlayBudgetRatio: \"预算 {ratio}\",\n optionOverlayRiskRatio: \"风险 {ratio}\",\n cashOnlyExecutionMode: \"允许融资\",\n cashOnlyExecutionCurrent: \"沿用当前配置\",\n cashOnlyExecutionYes: \"是\",\n cashOnlyExecutionNo: \"否\",\n cashOnlyExecutionModeMeta: \"选「否」时只按真实现金下单,不会动用 margin 购买力。\",\n cashOnlyExecutionValueYes: \"是\",\n cashOnlyExecutionValueNo: \"否\",\n currentCashOnlyExecution: \"当前允许融资\",\n pendingCashOnlyExecution: \"待提交允许融资\",\n executionCashPolicyTitle: \"现金与融资\",\n executionCashPolicyNote: \"允许融资与预留现金覆盖不能同时生效;选「是」会清空预留覆盖,设预留覆盖会强制「否」。\",\n executionCashMarginBlocksReserve: \"已选允许融资;提交时会清空预留现金覆盖。\",\n executionCashReserveBlocksMargin: \"已设预留现金覆盖;提交时会强制不允许融资。\",\n qmtPlatformCashNote: \"A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。\",\n qmtDryRunOnlyNote: \"QMT 当前仅支持 dry-run(模拟),尚无 live 券商账号。\",\n binancePlatformNote: \"Binance 平台不使用券商级收入层与期权层;相关功能由策略内部实现。\",\n invalidExecutionCashPolicyNote: \"允许融资与预留现金覆盖冲突,请只保留一种约束。\",\n dcaMode: \"定投模式\",\n dcaSectionTitle: \"定投\",\n dcaModeFixed: \"定额定投\",\n dcaModeSmart: \"智能定投\",\n dcaBaseInvestmentUsd: \"定投基准金额\",\n dcaModeMeta: \"仅定投策略可配置。\",\n dcaDefaultMeta: \"默认:{mode},基准金额 {amount}。\",\n dcaNotSupported: \"该策略不是定投策略\",\n dcaPlatformNotSupported: \"当前平台不支持定投策略\",\n currentDca: \"当前定投设置\",\n pendingDca: \"待提交定投设置\",\n dcaText: \"{mode},基准金额 {amount}\",\n minReservedCash: \"最小预留现金 ({currency})\",\n reservedCashRatio: \"预留现金比例\",\n reservedCashMode: \"预留现金策略\",\n reservePolicyCurrent: \"沿用当前配置\",\n reservePolicyNone: \"不设置平台预留现金\",\n reservePolicyRatio: \"仅按比例\",\n reservePolicyFloor: \"仅按固定金额\",\n reservePolicyMax: \"固定金额和比例取较大值\",\n reservedCashModeMeta: \"选择是否沿用、清空或覆盖平台预留现金。\",\n reservedCashNone: \"不设置\",\n reservedCashDefault: \"未配置(平台默认:0 {currency} / 0%)\",\n reservedCashMeta: \"固定金额下限,可单独设置或与比例取较大值。\",\n reservedCashRatioMeta: \"例如 0.03 表示 3%。\",\n summary: \"当前 / 待提交\",\n copySummary: \"复制状态\",\n loginToRun: \"登录后切换\",\n loadingConfig: \"读取配置中\",\n configureAccounts: \"配置账号后切换\",\n runSwitch: \"一键切换\",\n noChanges: \"无变更\",\n readonlyNote: \"登录后才可执行切换。\",\n publicReadonly: \"登录后查看账号配置。\",\n loadingConfigNote: \"正在读取账号配置和当前状态。\",\n missingConfigNote: \"账号配置未加载,暂时不能执行。\",\n readyNote: \"点击后会触发工作流,并同步目标平台服务。\",\n invalidStrategyNote: \"当前账号没有可执行策略,暂时不能切换。\",\n invalidReservePolicyNote: \"请为当前预留现金策略填写有效金额或比例。\",\n invalidIncomeLayerNote: \"请填写有效的收入层起始金额和最高比例。\",\n invalidOptionOverlayNote: \"当前策略未定义可启用的期权层。\",\n invalidDcaNote: \"请填写有效的定投模式和基准金额。\",\n publicOauthTitle: \"GitHub OAuth 保护\",\n publicOauthText: \"只允许白名单账号进入私有配置。\",\n publicWorkerTitle: \"Worker 端触发\",\n publicWorkerText: \"令牌保留在服务端,浏览器只提交切换意图。\",\n publicAuditTitle: \"变更可回溯\",\n publicAuditText: \"切换由 GitHub Actions 执行,便于审计和回滚。\",\n noAccount: \"没有账号选项\",\n noStrategy: \"没有支持的策略\",\n repository: \"平台仓库\",\n selectedAccount: \"账号\",\n selectedMarket: \"市场\",\n currentRuntimeTarget: \"当前账号状态\",\n pendingRuntimeTarget: \"待提交账号状态\",\n reservedCashPolicy: \"当前预留现金\",\n currentIncomeLayer: \"当前收入层\",\n pendingIncomeLayer: \"待提交收入层\",\n currentOptionOverlay: \"当前期权层\",\n pendingOptionOverlay: \"待提交期权层\",\n pendingReservedCashPolicy: \"待提交预留现金\",\n pendingMode: \"待提交模式\",\n currentPluginMode: \"当前插件范围\",\n pendingPluginMode: \"待提交插件范围\",\n unchanged: \"不变\",\n copied: \"已复制状态\",\n dispatching: \"正在触发工作流...\",\n dispatched: \"已触发工作流\",\n dispatchFailed: \"触发失败\",\n targetMeta: \"目标 {target} · 服务 {service} · 市场 {domains}\",\n strategyMeta: \"支持市场:{domains}\",\n strategyLifecycleMeta: \"当前门槛 {stage}\",\n strategyBlockedCountMeta: \"{count} 个策略未达 live 门槛\",\n strategyDefaultBlockedMeta: \"默认策略 {profile} 已阻断:{reason}\",\n usEquity: \"美股\",\n hkEquity: \"港股\",\n cnEquity: \"A股\",\n cryptoEquity: \"加密\",\n currentStrategy: \"当前策略\",\n nextStrategy: \"切换策略\",\n notRead: \"读取失败\",\n runtimeTargetOn: \"启用\",\n runtimeTargetOff: \"禁用\",\n incomeLayerDefault: \"开启,{start}起 {ratio}\",\n incomeLayerOff: \"关闭\",\n incomeLayerOn: \"开启,起始 {start},最高 {ratio}\",\n optionOverlayOff: \"关闭\",\n optionOverlayOn: \"开启\",\n optionOverlayDefaultSimple: \"开启\",\n optionOverlayDefault: \"开启,{detail}\",\n cashOnlyExecutionDefault: \"仅用现金\",\n },\n en: {\n appTitle: \"Strategy Switch\",\n appSubtitle: \"Pick platform, target account, and strategy. One action switches everything.\",\n healthView: \"Strategy Health\",\n switchView: \"Live Switch\",\n healthEyebrow: \"Strategy health / read only\",\n healthTitle: \"Read the machine conclusion before choosing an action.\",\n healthSubtitle: \"Health does not approve live; normal live, funding, and leverage changes still need a human.\",\n healthTotal: \"Strategies\",\n healthHealthy: \"Healthy\",\n healthWatch: \"Watch\",\n healthReview: \"Review\",\n healthCritical: \"Critical\",\n healthBoard: \"Strategy status\",\n bootKicker: \"Starting console\",\n bootTitle: \"Loading strategy config\",\n bootMessage: \"Reading session, account config, and current state.\",\n bootStrategy: \"Reading strategy catalog.\",\n bootSession: \"Checking sign-in status.\",\n bootConfig: \"Reading account config and current state.\",\n bootTimeout: \"Loading timed out; switched to public preview. Retry after signing in.\",\n bootPublic: \"Public preview is ready.\",\n login: \"Sign in\",\n logout: \"Sign out\",\n signedInAs: \"Signed in as {login}\",\n activePlatform: \"Active Platform\",\n account: \"Target account\",\n strategy: \"Strategy\",\n mode: \"Mode\",\n live: \"Live\",\n paper: \"Dry run\",\n runtimeTargetMode: \"Account status\",\n runtimeSectionTitle: \"Runtime and plugins\",\n runtimeTargetCurrent: \"Keep current status\",\n runtimeTargetEnabled: \"Enabled\",\n runtimeTargetDisabled: \"Disabled\",\n runtimeTargetModeMeta: \"Disabled accounts skip live runs; dry runs and health checks still work.\",\n pluginMode: \"Plugin scope\",\n pluginModeAuto: \"Enabled\",\n pluginModeNone: \"Disabled\",\n pluginModeMeta: \"Choose whether to enable this strategy's plugins.\",\n incomeLayerMode: \"Income layer\",\n incomeLayerSectionTitle: \"Income layer\",\n incomeLayerCurrent: \"Keep current config\",\n incomeLayerEnabled: \"Enable income layer\",\n incomeLayerDisabled: \"Disable income layer\",\n incomeLayerNotSupported: \"No income layer for this strategy\",\n incomeLayerStartUsd: \"Income layer start amount\",\n incomeLayerMaxRatio: \"Income layer max ratio\",\n incomeLayerModeMeta: \"Only applies to US equity strategies with an income layer.\",\n incomeLayerDefaultMeta: \"Strategy default: starts at {start}, max {ratio}.\",\n incomeLayerAllocationMeta: \"Default allocation: {allocations}.\",\n incomeLayerStartMeta: \"Income layer activates after total assets reach this amount.\",\n incomeLayerRatioMeta: \"Use 0.55 for a 55% cap.\",\n optionOverlayMode: \"Option layer\",\n optionOverlaySectionTitle: \"Option layer\",\n optionOverlayCurrent: \"Keep current config\",\n optionOverlayEnabled: \"Enable option layer\",\n optionOverlayDisabled: \"Disable option layer\",\n optionOverlayNotSupported: \"No option layer for this strategy\",\n optionOverlayModeMeta: \"Enabled mode uses the strategy's default recipe and budget; ratios are not edited here.\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"Growth\",\n optionOverlayFamilyIncome: \"Income\",\n optionOverlayBudgetRatio: \"budget {ratio}\",\n optionOverlayRiskRatio: \"risk {ratio}\",\n cashOnlyExecutionMode: \"Allow margin\",\n cashOnlyExecutionCurrent: \"Keep current config\",\n cashOnlyExecutionYes: \"Yes\",\n cashOnlyExecutionNo: \"No\",\n cashOnlyExecutionModeMeta: \"Choose No to use available cash only and avoid margin buying power.\",\n cashOnlyExecutionValueYes: \"Yes\",\n cashOnlyExecutionValueNo: \"No\",\n currentCashOnlyExecution: \"Current allow margin\",\n pendingCashOnlyExecution: \"Pending allow margin\",\n executionCashPolicyTitle: \"Cash and margin\",\n executionCashPolicyNote: \"Allow margin and reserve-cash overrides cannot both apply. Yes clears reserve overrides; reserve overrides force No.\",\n executionCashMarginBlocksReserve: \"Allow margin is selected; submitting will clear reserve-cash overrides.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; submitting will force allow margin to No.\",\n qmtPlatformCashNote: \"QMT A-share does not use margin or platform reserve cash; cash constraints live in strategy execution_cash_reserve_ratio.\",\n qmtDryRunOnlyNote: \"QMT is dry-run only for now; no live broker accounts are configured.\",\n binancePlatformNote: \"Binance does not use broker-level income/option layers; features are implemented inside strategies.\",\n invalidExecutionCashPolicyNote: \"Allow margin and reserve-cash overrides conflict. Keep only one constraint.\",\n dcaMode: \"DCA mode\",\n dcaSectionTitle: \"DCA\",\n dcaModeFixed: \"Fixed DCA\",\n dcaModeSmart: \"Smart DCA\",\n dcaBaseInvestmentUsd: \"Base DCA amount\",\n dcaModeMeta: \"Only DCA strategies can use this.\",\n dcaDefaultMeta: \"Default: {mode}, base amount {amount}.\",\n dcaNotSupported: \"This is not a DCA strategy\",\n dcaPlatformNotSupported: \"DCA not supported on this platform\",\n currentDca: \"Current DCA settings\",\n pendingDca: \"Pending DCA settings\",\n dcaText: \"{mode}, base amount {amount}\",\n minReservedCash: \"Minimum reserved cash ({currency})\",\n reservedCashRatio: \"Reserved cash ratio\",\n reservedCashMode: \"Reserved cash policy\",\n reservePolicyCurrent: \"Keep current config\",\n reservePolicyNone: \"No platform reserve\",\n reservePolicyRatio: \"Ratio only\",\n reservePolicyFloor: \"Fixed amount only\",\n reservePolicyMax: \"Max of amount and ratio\",\n reservedCashModeMeta: \"Choose whether to keep, clear, or override platform reserved cash.\",\n reservedCashNone: \"None\",\n reservedCashDefault: \"Not configured (platform default: 0 {currency} / 0%)\",\n reservedCashMeta: \"Fixed cash floor. Use alone or with a ratio.\",\n reservedCashRatioMeta: \"Use 0.03 for 3%.\",\n summary: \"Current / Pending\",\n copySummary: \"Copy state\",\n loginToRun: \"Sign in to switch\",\n loadingConfig: \"Loading config\",\n configureAccounts: \"Configure accounts\",\n runSwitch: \"Switch now\",\n noChanges: \"No changes\",\n readonlyNote: \"Sign in to switch.\",\n publicReadonly: \"Sign in to view account config.\",\n loadingConfigNote: \"Reading account config and current state.\",\n missingConfigNote: \"Account config is not loaded, so switching is disabled.\",\n readyNote: \"This dispatches the workflow and syncs the target platform service.\",\n invalidStrategyNote: \"This account has no runnable strategy, so switching is disabled.\",\n invalidReservePolicyNote: \"Enter a valid amount or ratio for the selected reserved-cash policy.\",\n invalidIncomeLayerNote: \"Enter a valid income layer start amount and max ratio.\",\n invalidOptionOverlayNote: \"This strategy does not define an option layer to enable.\",\n invalidDcaNote: \"Enter a valid DCA mode and base amount.\",\n publicOauthTitle: \"Protected by GitHub OAuth\",\n publicOauthText: \"Only allowlisted accounts can open private config.\",\n publicWorkerTitle: \"Worker-side dispatch\",\n publicWorkerText: \"Tokens stay server-side; the browser submits intent only.\",\n publicAuditTitle: \"Traceable changes\",\n publicAuditText: \"Switches run through GitHub Actions for audit and rollback.\",\n noAccount: \"No accounts\",\n noStrategy: \"No supported strategies\",\n repository: \"Repository\",\n selectedAccount: \"Account\",\n selectedMarket: \"Market\",\n currentRuntimeTarget: \"Current account status\",\n pendingRuntimeTarget: \"Pending account status\",\n reservedCashPolicy: \"Current reserved cash\",\n currentIncomeLayer: \"Current income layer\",\n pendingIncomeLayer: \"Pending income layer\",\n currentOptionOverlay: \"Current option layer\",\n pendingOptionOverlay: \"Pending option layer\",\n pendingReservedCashPolicy: \"Pending reserved cash\",\n pendingMode: \"Pending mode\",\n currentPluginMode: \"Current plugin scope\",\n pendingPluginMode: \"Pending plugin scope\",\n unchanged: \"Unchanged\",\n copied: \"State copied\",\n dispatching: \"Dispatching workflow...\",\n dispatched: \"Workflow dispatched\",\n dispatchFailed: \"Dispatch failed\",\n targetMeta: \"target {target} · service {service} · market {domains}\",\n strategyMeta: \"Markets: {domains}\",\n strategyLifecycleMeta: \"current gate {stage}\",\n strategyBlockedCountMeta: \"{count} strategies are blocked from live\",\n strategyDefaultBlockedMeta: \"Default strategy {profile} is blocked: {reason}\",\n usEquity: \"US equity\",\n hkEquity: \"HK equity\",\n cnEquity: \"CN A-share\",\n cryptoEquity: \"Crypto\",\n currentStrategy: \"Current strategy\",\n nextStrategy: \"Switch strategy\",\n notRead: \"Not read\",\n runtimeTargetOn: \"Enabled\",\n runtimeTargetOff: \"Disabled\",\n incomeLayerDefault: \"Enabled, {start} start, {ratio} max\",\n incomeLayerOff: \"Disabled\",\n incomeLayerOn: \"Enabled, starts at {start}, max {ratio}\",\n optionOverlayOff: \"Disabled\",\n optionOverlayOn: \"Enabled\",\n optionOverlayDefaultSimple: \"Strategy default: enabled\",\n optionOverlayDefault: \"Enabled, {detail}\",\n cashOnlyExecutionDefault: \"Cash only\",\n },\n };\n\n const storedLang = localStorage.getItem(\"qsl-switch-lang\");\n const initialLang = storedLang === \"zh\" || storedLang === \"en\"\n ? storedLang\n : ((navigator.language || \"\").toLowerCase().startsWith(\"zh\") ? \"zh\" : \"en\");\n const clone = (value) => JSON.parse(JSON.stringify(value));\n const defaultReserveForm = () => ({\n reservePolicyMode: \"current\",\n minReservedCashUsd: \"\",\n reservedCashRatio: \"\",\n reservedCashTouched: false,\n incomeLayerMode: \"current\",\n incomeLayerStartUsd: \"\",\n incomeLayerMaxRatio: \"\",\n incomeLayerTouched: false,\n optionOverlayMode: \"current\",\n optionOverlayTouched: false,\n cashOnlyExecutionMode: \"current\",\n cashOnlyExecutionTouched: false,\n runtimeTargetMode: \"current\",\n runtimeTargetTouched: false,\n dcaMode: \"fixed\",\n dcaBaseInvestmentUsd: \"\",\n dcaTouched: false,\n strategyTouched: false,\n });\n\n const state = {\n selected: \"longbridge\",\n lang: initialLang,\n view: \"health\",\n appReady: false,\n bootMessageKey: \"bootMessage\",\n auth: { available: false, allowed: false, admin: false, login: null },\n accountOptions: clone(defaultAccountOptions),\n currentStrategies: {},\n health: {\n payload: {\n data_status: \"unavailable\",\n computed_at: null,\n summary: { strategy_count: 0, healthy: 0, watch: 0, review: 0, critical: 0 },\n strategies: [],\n },\n filter: \"all\",\n },\n configSource: \"default\",\n repositories: clone(defaultRepositories),\n forms: {\n longbridge: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n ibkr: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n schwab: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n firstrade: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n qmt: { accountKey: \"preview\", strategy: \"\", executionMode: \"paper\", pluginMode: \"auto\", ...defaultReserveForm() },\n binance: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\" },\n },\n };\n\n const el = (id) => document.getElementById(id);\n const t = (key) => copy[state.lang][key] || copy.en[key] || key;\n let toastTimer = null;\n\n function showToast(message, { duration = 4000 } = {}) {\n const node = el(\"toast\");\n if (toastTimer) {\n window.clearTimeout(toastTimer);\n toastTimer = null;\n }\n node.textContent = message || \"\";\n if (message && duration > 0) {\n toastTimer = window.setTimeout(() => {\n node.textContent = \"\";\n toastTimer = null;\n }, duration);\n }\n }\n\n async function fetchWithTimeout(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const controller = new AbortController();\n const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (error) {\n if (error?.name === \"AbortError\") {\n throw new Error(\"request timeout\");\n }\n throw error;\n } finally {\n window.clearTimeout(timeoutId);\n }\n }\n\n async function requestJson(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const response = await fetchWithTimeout(url, { ...init, cache: \"no-store\" }, timeoutMs);\n if (!response.ok) throw new Error(\"request failed\");\n return response.json();\n }\n\n function isRequestTimeoutError(error) {\n return String(error?.message || \"\").toLowerCase() === \"request timeout\";\n }\n\n function optionsFor(platform) {\n return state.accountOptions[platform] && state.accountOptions[platform].length\n ? state.accountOptions[platform]\n : defaultAccountOptions[platform];\n }\n\n function selectedAccount(platform = state.selected) {\n const options = optionsFor(platform);\n const form = state.forms[platform];\n return options.find((option) => option.key === form.accountKey) || options[0];\n }\n\n function hasPrivateConfig() {\n return Boolean(state.auth.allowed && state.configSource === \"private\");\n }\n\n function cleanStrategyProfile(value) {\n const profile = String(value || \"\").trim();\n return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : \"\";\n }\n\n function cleanStrategyDomain(value) {\n const domain = String(value || \"\").trim();\n return strategyDomains.includes(domain) ? domain : \"\";\n }\n\n function domainLabel(domain) {\n const entry = domainLabels[domain];\n if (entry) return state.lang === \"zh\" ? entry.zh : entry.en;\n return domain;\n }\n\n function platformSupportsMarginPolicy(platform = state.selected) {\n return platformConfig[platform]?.margin_policy ?? true;\n }\n\n function platformSupportsReservedCashPolicy(platform = state.selected) {\n return platformConfig[platform]?.reserved_cash ?? true;\n }\n\n function platformDryRunOnly(platform = state.selected) {\n return platformConfig[platform]?.dry_run_only ?? false;\n }\n\n function allowMarginExplicitlySelected(form) {\n return normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode) === \"disabled\";\n }\n\n function reserveCashOverrideActive(form) {\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n return mode === \"ratio\" || mode === \"floor\" || mode === \"max\";\n }\n\n function executionCashPolicyConflict(form) {\n return allowMarginExplicitlySelected(form) && reserveCashOverrideActive(form);\n }\n\n function reconcileExecutionCashPolicy(form, changed) {\n if (!form) return;\n if (changed === \"margin\" && allowMarginExplicitlySelected(form)) {\n if (form.reservePolicyMode !== \"none\" && (form.minReservedCashUsd || form.reservedCashRatio)) {\n form._prevReserve = {\n mode: form.reservePolicyMode,\n floor: form.minReservedCashUsd,\n ratio: form.reservedCashRatio,\n };\n }\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n } else if (changed === \"reserve\" && reserveCashOverrideActive(form)) {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\n }\n\n function restoreReserveAfterMarginDisabled(form) {\n if (!form || allowMarginExplicitlySelected(form) || !form._prevReserve) return;\n form.reservePolicyMode = form._prevReserve.mode;\n form.minReservedCashUsd = form._prevReserve.floor;\n form.reservedCashRatio = form._prevReserve.ratio;\n form.reservedCashTouched = true;\n delete form._prevReserve;\n }\n\n function strategyDomain(profile) {\n return strategyCatalog[profile]?.domain || \"\";\n }\n\n function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) {\n const configured = String(account?.cash_currency || \"\").trim().toUpperCase();\n if (configured === \"USD\" || configured === \"HKD\" || configured === \"CNY\") return configured;\n const domain = strategyDomain(state.forms[platform]?.strategy);\n if (domain === \"hk_equity\") return \"HKD\";\n if (domain === \"cn_equity\") return \"CNY\";\n return \"USD\";\n }\n\n function applyStrategyProfiles(rawProfiles) {\n const profiles = Array.isArray(rawProfiles) && rawProfiles.length\n ? rawProfiles\n : defaultStrategyProfiles;\n const nextOptions = [];\n const nextLabels = {};\n const nextCatalog = {};\n const nextIncomeLayerDefaults = {};\n const nextOptionOverlayDefaults = {};\n for (const item of profiles) {\n const profile = cleanStrategyProfile(item?.profile || item?.strategy_profile);\n if (!profile || nextOptions.includes(profile)) continue;\n const domain = cleanStrategyDomain(item?.domain || \"us_equity\");\n if (!domain) continue;\n nextOptions.push(profile);\n nextLabels[profile] = strategyLabelSet(profile, item);\n nextCatalog[profile] = {\n profile,\n label: nextLabels[profile].en || nextLabels[profile].zh || profile,\n label_en: nextLabels[profile].en || \"\",\n label_zh: nextLabels[profile].zh || \"\",\n domain,\n runtime_enabled: cleanOptionalBoolean(item?.runtime_enabled ?? item?.live_enabled ?? true) !== false,\n };\n const lifecycleStage = normalizeLifecycleStage(item?.lifecycle_stage ?? item?.lifecycleStage);\n if (lifecycleStage) nextCatalog[profile].lifecycle_stage = lifecycleStage;\n const allowedExecutionModes = normalizeAllowedExecutionModes(item?.allowed_execution_modes);\n if (allowedExecutionModes.length) nextCatalog[profile].allowed_execution_modes = allowedExecutionModes;\n const canSwitchLive = cleanOptionalBoolean(item?.can_switch_live);\n if (canSwitchLive !== null) nextCatalog[profile].can_switch_live = canSwitchLive;\n const blockedLiveReason = cleanDisplayText(item?.blocked_live_reason);\n if (blockedLiveReason) nextCatalog[profile].blocked_live_reason = blockedLiveReason;\n const latestEvidenceStatus = cleanDisplayText(item?.latest_evidence_status);\n if (latestEvidenceStatus) nextCatalog[profile].latest_evidence_status = latestEvidenceStatus;\n const pluginGateStatus = cleanDisplayText(item?.plugin_gate_status);\n if (pluginGateStatus) nextCatalog[profile].plugin_gate_status = pluginGateStatus;\n const dcaDefaults = dcaProfileDefaults[profile] || null;\n if (dcaDefaults || item?.dca_enabled === true) {\n nextCatalog[profile].dca_enabled = true;\n nextCatalog[profile].dca_default_mode = normalizeDcaMode(\n item?.dca_default_mode || item?.default_dca_mode || dcaDefaults?.defaultMode || \"fixed\",\n );\n nextCatalog[profile].dca_default_base_investment_usd = cleanDisplayPositiveNumber(\n item?.dca_default_base_investment_usd ||\n item?.default_dca_base_investment_usd ||\n dcaDefaults?.defaultBaseInvestmentUsd ||\n \"1000\",\n ) || \"1000\";\n }\n const profileIncomeDefaults = incomeLayerDefaultsFromProfileItem(item);\n const incomeDefaults = profileIncomeDefaults === false\n ? null\n : (profileIncomeDefaults || fallbackIncomeLayerDefaults[profile] || null);\n if (incomeDefaults) {\n nextIncomeLayerDefaults[profile] = incomeDefaults;\n nextCatalog[profile].income_layer_enabled = true;\n nextCatalog[profile].income_layer_start_usd = String(incomeDefaults.startUsd);\n nextCatalog[profile].income_layer_max_ratio = incomeDefaults.maxRatio;\n nextCatalog[profile].income_layer_allocations = incomeDefaults.allocations;\n }\n const profileOptionDefaults = optionOverlayDefaultsFromProfileItem(item);\n const optionDefaults = profileOptionDefaults === false\n ? null\n : (profileOptionDefaults || fallbackOptionOverlayDefaults[profile] || null);\n if (optionDefaults) {\n nextOptionOverlayDefaults[profile] = optionDefaults;\n nextCatalog[profile].option_overlay_enabled = true;\n nextCatalog[profile].option_overlay_live_gate = optionDefaults.liveGate || \"\";\n nextCatalog[profile].option_overlay_live_status = optionDefaults.liveStatus || \"\";\n }\n }\n if (!nextOptions.length && profiles !== defaultStrategyProfiles) return applyStrategyProfiles(defaultStrategyProfiles);\n strategyOptions = nextOptions;\n strategyLabels = nextLabels;\n strategyCatalog = nextCatalog;\n incomeLayerDefaults = nextIncomeLayerDefaults;\n optionOverlayDefaults = nextOptionOverlayDefaults;\n }\n\n function incomeLayerDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.income_layer_enabled);\n const hasConfig = enabled !== null ||\n item?.income_layer_start_usd !== undefined ||\n item?.income_layer_max_ratio !== undefined ||\n item?.income_layer_allocations !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const startUsd = cleanDisplayNumber(item?.income_layer_start_usd);\n const maxRatio = cleanDisplayRatio(item?.income_layer_max_ratio);\n const allocations = cleanIncomeLayerAllocations(item?.income_layer_allocations);\n if (!startUsd || !maxRatio || !allocations) return null;\n return { startUsd, maxRatio, allocations };\n }\n\n function cleanIncomeLayerAllocations(value) {\n if (!value || Array.isArray(value) || typeof value !== \"object\") return null;\n const allocations = {};\n let total = 0;\n for (const [rawSymbol, rawRatio] of Object.entries(value)) {\n const symbol = String(rawSymbol || \"\").trim().toUpperCase();\n const ratio = cleanDisplayPositiveNumber(rawRatio);\n if (!/^[A-Z0-9.-]{1,12}$/.test(symbol) || !ratio) continue;\n allocations[symbol] = Number(ratio);\n total += Number(ratio);\n }\n return total > 0 && Object.keys(allocations).length ? allocations : null;\n }\n\n function optionOverlayDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.option_overlay_enabled);\n const hasConfig = enabled !== null ||\n item?.option_growth_overlay_enabled !== undefined ||\n item?.option_income_overlay_enabled !== undefined ||\n item?.option_overlay_live_gate !== undefined ||\n item?.option_overlay_live_status !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const families = [\n optionOverlayFamilyDefaultsFromProfileItem(item, \"growth\"),\n optionOverlayFamilyDefaultsFromProfileItem(item, \"income\"),\n ].filter(Boolean);\n if (!families.length) return null;\n return {\n liveGate: String(item?.option_overlay_live_gate || \"promotion_required\"),\n liveStatus: String(item?.option_overlay_live_status || \"research_only\"),\n families,\n };\n }\n\n function optionOverlayFamilyDefaultsFromProfileItem(item, family) {\n const prefix = `option_${family}_overlay`;\n const enabled = cleanOptionalBoolean(item?.[`${prefix}_enabled`]);\n if (enabled !== true) return null;\n const recipe = cleanStrategyProfile(item?.[`${prefix}_recipe`]);\n const startUsd = cleanDisplayNumber(item?.[`${prefix}_start_usd`]);\n const ratioField = family === \"growth\"\n ? \"option_growth_overlay_nav_budget_ratio\"\n : \"option_income_overlay_nav_risk_ratio\";\n const ratio = cleanDisplayRatio(item?.[ratioField]);\n if (!recipe || !startUsd || !ratio) return null;\n return {\n family,\n recipe,\n startUsd,\n ratio,\n ratioKind: family === \"growth\" ? \"budget\" : \"risk\",\n };\n }\n\n function supportedDomainsForAccount(platform, account) {\n if (Array.isArray(account?.supported_domains) && account.supported_domains.length) {\n const cleaned = account.supported_domains.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n }\n return inferSupportedDomains(platform, account);\n }\n\n function inferSupportedDomains(platform, account) {\n void account;\n if (platform === \"qmt\") return [\"cn_equity\"];\n if (platform === \"longbridge\" || platform === \"ibkr\") return [\"us_equity\", \"hk_equity\"];\n return [\"us_equity\"];\n }\n\n function supportedDomainLabel(platform, account) {\n return supportedDomainsForAccount(platform, account).map(domainLabel).join(\" / \");\n }\n\n function platformSupportsDca(platform = state.selected) {\n return platformConfig[platform]?.dca ?? false;\n }\n\n function strategyCatalogEntry(profile) {\n return strategyCatalog[cleanStrategyProfile(profile)] || {};\n }\n\n function normalizeLifecycleStage(value) {\n const text = String(value || \"\").trim().toLowerCase();\n if (!text || text.length > 40 || !/^[a-z0-9._-]+$/.test(text)) return \"\";\n return text;\n }\n\n function normalizeAllowedExecutionModes(value) {\n const items = Array.isArray(value)\n ? value\n : String(value || \"\").split(/[,\\s/|]+/);\n const modes = [];\n for (const item of items) {\n const mode = String(item || \"\").trim().toLowerCase();\n if (!mode || mode.length > 32 || !/^[a-z0-9._-]+$/.test(mode)) continue;\n if (!modes.includes(mode)) modes.push(mode);\n }\n return modes;\n }\n\n function cleanDisplayText(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 120 || /[<>]/.test(text)) return \"\";\n return text;\n }\n\n function strategyCanSwitchLive(entry) {\n if (!entry || typeof entry !== \"object\") return false;\n if (entry.runtime_enabled === false) return false;\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) return false;\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return false;\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) return false;\n const blockedReason = cleanDisplayText(entry.blocked_live_reason);\n if (blockedReason) return false;\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && [\"research_only\", \"blocked\", \"pending\"].includes(evidenceStatus.toLowerCase())) return false;\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && [\"blocked\", \"locked\", \"disabled\"].includes(pluginGateStatus.toLowerCase())) return false;\n return true;\n }\n\n function strategyDisplayMetaText(platform, account, profile) {\n void profile;\n return t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function strategyActionNoteText(platform = state.selected, account = selectedAccount(platform)) {\n const profile = state.forms[platform]?.strategy || \"\";\n const meta = strategyDisplayMetaText(platform, account, profile);\n return meta ? `${t(\"invalidStrategyNote\")}\\n${meta}` : t(\"invalidStrategyNote\");\n }\n\n function strategyChoiceLabel(profile, platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const label = strategyLabel(profile);\n const entry = strategyCatalogEntry(profile);\n const domain = entry.domain ? domainLabel(entry.domain) : \"\";\n if (!entry.profile) return label;\n if (normalizeExecutionMode(executionMode, false) === \"live\" && !strategyCanSwitchLive(entry)) {\n return domain ? `${label}(${domain})` : label;\n }\n if (entry.runtime_enabled === false) {\n return domain ? `${label}(${domain})` : label;\n }\n return label;\n }\n\n function strategyAllowedForAccount(platform, account, profile, executionMode = state.forms[platform]?.executionMode) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalogEntry(cleanProfile);\n if (!catalogEntry.profile) return false;\n if (catalogEntry.runtime_enabled !== true) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n if (!supportedDomainsForAccount(platform, account).includes(catalogEntry.domain)) return false;\n const mode = normalizeExecutionMode(executionMode, false);\n if (mode === \"live\") return strategyCanSwitchLive(catalogEntry);\n const allowedModes = normalizeAllowedExecutionModes(catalogEntry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(mode)) return false;\n return true;\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile, executionMode));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile, executionMode)) {\n choices.push(profile);\n }\n };\n return choices;\n }\n\n function strategyLabel(profile) {\n const labels = strategyLabels[profile] || localStrategyLabels[profile];\n if (!labels) return profile;\n return state.lang === \"zh\"\n ? (labels.zh || labels.en || profile)\n : (labels.en || labels.zh || profile);\n }\n\n function strategyLabelSet(profile, item) {\n const local = localStrategyLabels[profile] || {};\n const label = String(item?.label || item?.display_name || \"\").trim();\n const labelEn = String(item?.label_en || item?.display_name_en || \"\").trim();\n const labelZh = String(item?.label_zh || item?.display_name_zh || \"\").trim();\n return {\n zh: labelZh || local.zh || label || local.en || profile,\n en: labelEn || label || local.en || labelZh || local.zh || profile,\n };\n }\n\n function modeLabel(mode) {\n return mode === \"paper\" ? t(\"paper\") : t(\"live\");\n }\n\n function normalizePluginMode(value) {\n return pluginModes.includes(value) ? value : \"auto\";\n }\n\n function pluginModeLabel(mode) {\n return mode === \"none\" ? t(\"pluginModeNone\") : t(\"pluginModeAuto\");\n }\n\n function dcaConfigForStrategy(profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalog = strategyCatalog[cleanProfile] || {};\n if (catalog.dca_enabled === true) {\n return {\n defaultMode: normalizeDcaMode(catalog.dca_default_mode || \"fixed\"),\n defaultBaseInvestmentUsd: cleanDisplayPositiveNumber(catalog.dca_default_base_investment_usd) || \"1000\",\n };\n }\n return dcaProfileDefaults[cleanProfile] || null;\n }\n\n function dcaSupported(profile) {\n return Boolean(dcaConfigForStrategy(profile));\n }\n\n function normalizeDcaMode(value) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"smart_dca\") return \"smart\";\n if (mode === \"fixed_dca\" || mode === \"ordinary\" || mode === \"ordinary_dca\") return \"fixed\";\n return dcaModes.includes(mode) ? mode : \"fixed\";\n }\n\n function dcaModeLabel(mode) {\n return normalizeDcaMode(mode) === \"smart\" ? t(\"dcaModeSmart\") : t(\"dcaModeFixed\");\n }\n\n function normalizeRuntimeTargetMode(value) {\n return runtimeTargetModes.includes(value) ? value : \"current\";\n }\n\n function runtimeTargetModeLabel(mode) {\n if (mode === \"enabled\") return t(\"runtimeTargetEnabled\");\n if (mode === \"disabled\") return t(\"runtimeTargetDisabled\");\n return t(\"runtimeTargetCurrent\");\n }\n\n function runtimeTargetEnabledForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.runtime_target_enabled);\n }\n\n function runtimeTargetStateForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return { known: false, enabled: null };\n const configured = cleanOptionalBoolean(entry.runtime_target_enabled);\n return { known: true, enabled: configured ?? true };\n }\n\n function runtimeTargetText(enabled) {\n return enabled ? t(\"runtimeTargetOn\") : t(\"runtimeTargetOff\");\n }\n\n function runtimeTargetTone(enabled) {\n return enabled ? \"enabled\" : \"disabled\";\n }\n\n function currentRuntimeTargetText(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return t(\"notRead\");\n return runtimeTargetText(target.enabled);\n }\n\n function currentRuntimeTargetTone(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return \"neutral\";\n return runtimeTargetTone(target.enabled);\n }\n\n function incomeLayerDefaultForStrategy(profile) {\n return incomeLayerDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function incomeLayerSupported(profile) {\n return Boolean(incomeLayerDefaultForStrategy(profile));\n }\n\n function normalizeIncomeLayerMode(value) {\n return incomeLayerModes.includes(value) ? value : \"current\";\n }\n\n function incomeLayerModeLabel(mode) {\n if (mode === \"enabled\") return t(\"incomeLayerEnabled\");\n if (mode === \"disabled\") return t(\"incomeLayerDisabled\");\n return t(\"incomeLayerCurrent\");\n }\n\n function optionOverlayDefaultForStrategy(profile) {\n return optionOverlayDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function optionOverlaySupported(profile) {\n return Boolean(optionOverlayDefaultForStrategy(profile));\n }\n\n function normalizeOptionOverlayMode(value) {\n return optionOverlayModes.includes(value) ? value : \"current\";\n }\n\n function optionOverlayModeLabel(mode) {\n if (mode === \"enabled\") return t(\"optionOverlayEnabled\");\n if (mode === \"disabled\") return t(\"optionOverlayDisabled\");\n return t(\"optionOverlayCurrent\");\n }\n\n function optionOverlayText(enabled) {\n return enabled ? t(\"optionOverlayOn\") : t(\"optionOverlayOff\");\n }\n\n function normalizeCashOnlyExecutionMode(value) {\n return cashOnlyExecutionModes.includes(value) ? value : \"current\";\n }\n\n function cashOnlyExecutionModeLabel(mode) {\n if (mode === \"enabled\") return t(\"cashOnlyExecutionNo\");\n if (mode === \"disabled\") return t(\"cashOnlyExecutionYes\");\n return t(\"cashOnlyExecutionCurrent\");\n }\n\n function cashOnlyExecutionText(enabled) {\n if (enabled === null) return t(\"notRead\");\n return enabled ? t(\"cashOnlyExecutionValueNo\") : t(\"cashOnlyExecutionValueYes\");\n }\n\n function platformCashOnlyExecutionDefault() {\n return true;\n }\n\n function effectiveCashOnlyExecutionForAccount(platform, account) {\n const configured = currentCashOnlyExecutionForAccount(platform, account);\n if (configured !== null) return configured;\n if (!platformSupportsMarginPolicy(platform)) return null;\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n if (entry) {\n const val = cleanOptionalBoolean(entry.cash_only_execution);\n if (val !== null) return val;\n }\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionText(platform = state.selected, account = selectedAccount(platform)) {\n if (!platformSupportsMarginPolicy(platform)) return t(\"notRead\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.cash_only_execution);\n if (configured === null) return t(\"cashOnlyExecutionDefault\");\n return cashOnlyExecutionText(configured);\n }\n\n function currentOptionOverlayForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.option_overlay_enabled);\n }\n\n function effectiveOptionOverlayForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const configured = currentOptionOverlayForAccount(platform, account);\n if (configured !== null) return configured;\n if (!optionOverlaySupported(profile)) return null;\n return true;\n }\n\n function optionOverlayDefaultSummaryDetail(defaults) {\n if (!defaults?.families?.length) return \"\";\n return defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family} ${ratioText}`;\n }).join(\" / \");\n }\n\n function optionOverlayDefaultText(profile) {\n const defaults = optionOverlayDefaultForStrategy(profile);\n if (!defaults) return t(\"optionOverlayNotSupported\");\n const detail = optionOverlayDefaultSummaryDetail(defaults);\n return detail ? t(\"optionOverlayDefault\").replace(\"{detail}\", detail) : t(\"optionOverlayDefaultSimple\");\n }\n\n function currentOptionOverlayText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.option_overlay_enabled);\n if (!optionOverlaySupported(profile)) {\n return configured === null ? t(\"optionOverlayNotSupported\") : optionOverlayText(configured);\n }\n if (configured === null) return optionOverlayDefaultText(profile);\n return optionOverlayText(configured);\n }\n\n function currentIncomeLayerForAccount(platform, account) {\n return incomeLayerFromEntry(currentEntryForAccount(platform, account));\n }\n\n function incomeLayerFromEntry(entry) {\n return {\n enabled: cleanOptionalBoolean(entry?.income_layer_enabled),\n startUsd: cleanDisplayNumber(entry?.income_layer_start_usd),\n maxRatio: cleanDisplayRatio(entry?.income_layer_max_ratio),\n };\n }\n\n function incomeLayerFieldsConfigured(entry) {\n const current = incomeLayerFromEntry(entry);\n return current.enabled !== null || Boolean(current.startUsd || current.maxRatio);\n }\n\n function effectiveIncomeLayerForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return null;\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return null;\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return {\n enabled: true,\n startUsd: String(defaults.startUsd),\n maxRatio: defaults.maxRatio,\n };\n }\n return {\n enabled: current.enabled ?? true,\n startUsd: current.startUsd || String(defaults.startUsd),\n maxRatio: current.maxRatio || defaults.maxRatio,\n };\n }\n\n function currentDcaForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, mode: \"\", baseInvestmentUsd: \"\" };\n const entry = currentEntryForAccount(platform, account);\n return {\n supported: true,\n mode: normalizeDcaMode(entry?.dca_mode || account?.dca_mode || defaults.defaultMode),\n baseInvestmentUsd: cleanDisplayPositiveNumber(entry?.dca_base_investment_usd) ||\n cleanDisplayPositiveNumber(account?.dca_base_investment_usd) ||\n defaults.defaultBaseInvestmentUsd,\n };\n }\n\n function normalizeAccountLookupKey(value) {\n return String(value || \"\").trim().toLowerCase();\n }\n\n function collectAccountLookupCandidates(keys) {\n const candidates = new Set();\n for (const rawKey of keys) {\n const key = normalizeAccountLookupKey(rawKey);\n if (!key) continue;\n\n candidates.add(key);\n\n const compact = key.replace(/[^a-z0-9]+/g, \"\");\n if (compact) candidates.add(compact);\n\n const parts = key.split(/[^a-z0-9]+/).filter(Boolean);\n if (parts.length > 1) candidates.add(parts[parts.length - 1]);\n }\n return [...candidates];\n }\n\n function resolveCurrentEntryByKey(byPlatform, keys) {\n const candidates = new Set(collectAccountLookupCandidates(keys));\n if (!candidates.size) return null;\n\n for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) return entry;\n }\n\n for (const [rawKey, entry] of Object.entries(byPlatform)) {\n if (!currentEntryHasState(entry)) continue;\n const rawCandidates = collectAccountLookupCandidates([rawKey]);\n const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));\n if (hasMatch) return entry;\n }\n\n return null;\n }\n\n function currentEntryForAccount(platform, account) {\n const byPlatform = state.currentStrategies[platform] || {};\n const keys = [account?.key, account?.target_name, account?.label]\n .filter(Boolean)\n .map((value) => String(value));\n const entry = resolveCurrentEntryByKey(byPlatform, keys);\n if (entry) return entry;\n const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n const merged = { ...globalDefaults, ...(account || {}) };\n const synth = {\n strategy_profile: \"\",\n source: \"account_defaults\",\n };\n const cashMode = merged.cash_only_execution_mode;\n if (cashMode === \"enabled\") synth.cash_only_execution = true;\n else if (cashMode === \"disabled\") synth.cash_only_execution = false;\n else if (platformSupportsMarginPolicy(platform)) synth.cash_only_execution = true;\n if (merged.min_reserved_cash_usd) synth.min_reserved_cash_usd = merged.min_reserved_cash_usd;\n if (merged.reserved_cash_ratio) synth.reserved_cash_ratio = merged.reserved_cash_ratio;\n synth.runtime_target_enabled = merged.runtime_target_enabled !== false;\n const execMode = merged.default_execution_mode || platformConfig[platform]?.default_execution_mode || \"live\";\n synth.execution_mode = execMode;\n synth.dry_run_only = execMode === \"paper\";\n return synth;\n }\n\n function currentEntryHasState(entry) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) return false;\n return Boolean(\n cleanStrategyProfile(entry.strategy_profile) ||\n cleanDisplayNumber(entry.min_reserved_cash_usd ?? entry.reserved_cash_floor_usd) ||\n cleanDisplayRatio(entry.reserved_cash_ratio) ||\n cleanOptionalBoolean(entry.income_layer_enabled) !== null ||\n cleanDisplayNumber(entry.income_layer_start_usd) ||\n cleanDisplayRatio(entry.income_layer_max_ratio) ||\n cleanOptionalBoolean(entry.option_overlay_enabled) !== null ||\n cleanOptionalBoolean(entry.cash_only_execution) !== null ||\n cleanOptionalBoolean(entry.runtime_target_enabled) !== null ||\n normalizeDcaMode(entry.dca_mode || \"\") !== \"fixed\" ||\n cleanDisplayPositiveNumber(entry.dca_base_investment_usd) ||\n normalizeExecutionMode(entry.execution_mode, entry.dry_run_only),\n );\n }\n\n function currentStrategyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return cleanStrategyProfile(entry?.strategy_profile) || \"\";\n }\n\n function currentReservePolicyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return reservePolicyFromEntry(entry);\n }\n\n function currentPluginModeForAccount(platform, account) {\n void platform;\n return normalizePluginMode(account?.plugin_mode);\n }\n\n function reservePolicyFromEntry(entry) {\n return {\n minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),\n reservedCashRatio: cleanDisplayRatio(entry?.reserved_cash_ratio),\n };\n }\n\n function cleanDisplayNumber(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 32 || !/^(?:\\d+|\\d*\\.\\d+)$/.test(text)) return \"\";\n const numeric = Number(text);\n if (!Number.isFinite(numeric) || numeric < 0) return \"\";\n return text;\n }\n\n function cleanDisplayRatio(value) {\n const text = cleanDisplayNumber(value);\n if (!text) return \"\";\n const numeric = Number(text);\n return numeric >= 0 && numeric <= 1 ? text : \"\";\n }\n\n function cleanDisplayPositiveNumber(value) {\n const text = cleanDisplayNumber(value);\n return text && Number(text) > 0 ? text : \"\";\n }\n\n function normalizeExecutionMode(value, dryRunOnly) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"live\" || mode === \"paper\") return mode;\n if (dryRunOnly === true || dryRunOnly === \"true\" || dryRunOnly === \"1\" || dryRunOnly === 1) return \"paper\";\n if (dryRunOnly === false || dryRunOnly === \"false\" || dryRunOnly === \"0\" || dryRunOnly === 0) return \"live\";\n return \"\";\n }\n\n function cleanOptionalBoolean(value) {\n if (value === true || value === 1) return true;\n if (value === false || value === 0) return false;\n if (typeof value === \"string\") {\n const text = value.trim().toLowerCase();\n if (text === \"true\" || text === \"1\") return true;\n if (text === \"false\" || text === \"0\") return false;\n }\n return null;\n }\n\n function defaultExecutionModeForAccount(platform, account, fallback = \"live\") {\n if (platformDryRunOnly(platform)) return \"paper\";\n const currentMode = normalizeExecutionMode(\n currentEntryForAccount(platform, account)?.execution_mode,\n currentEntryForAccount(platform, account)?.dry_run_only,\n );\n if (currentMode) return currentMode;\n const hint = [\n account?.key,\n account?.label,\n account?.target_name,\n account?.deployment_selector,\n account?.account_scope,\n account?.service_name,\n ].join(\" \").toLowerCase();\n if (hint.split(/\\s+/).includes(\"paper\") || hint.includes(\"-paper\") || hint.includes(\"_paper\") || hint.includes(\"dry_run\") || hint.includes(\"dry-run\")) {\n return \"paper\";\n }\n return fallback;\n }\n\n function defaultStrategyForAccount(platform, account) {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile) return currentProfile;\n return \"\";\n }\n\n function syncStrategyForAccount(platform) {\n const account = selectedAccount(platform);\n if (!account) return;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, account);\n state.forms[platform].executionMode = defaultExecutionModeForAccount(\n platform,\n account,\n );\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, account);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(state.forms[platform], \"margin\");\n syncDcaForAccount(platform);\n }\n\n function syncRuntimeTargetForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.runtimeTargetTouched) return;\n const current = runtimeTargetEnabledForAccount(platform, selectedAccount(platform));\n form.runtimeTargetMode = current === false ? \"disabled\" : \"enabled\";\n }\n\n function syncReservePolicyForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.reservedCashTouched) return;\n const policy = currentReservePolicyForAccount(platform, selectedAccount(platform));\n const hasFloor = Boolean(policy.minReservedCashUsd);\n const hasRatio = Boolean(policy.reservedCashRatio);\n if (hasFloor && hasRatio) form.reservePolicyMode = \"max\";\n else if (hasFloor) form.reservePolicyMode = \"floor\";\n else if (hasRatio) form.reservePolicyMode = \"ratio\";\n else form.reservePolicyMode = \"none\";\n form.minReservedCashUsd = policy.minReservedCashUsd;\n form.reservedCashRatio = policy.reservedCashRatio;\n }\n\n function syncIncomeLayerForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.incomeLayerTouched) return;\n const defaults = incomeLayerDefaultForStrategy(form.strategy);\n const current = currentIncomeLayerForAccount(platform, selectedAccount(platform));\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n if (entry && incomeLayerFieldsConfigured(entry)) {\n form.incomeLayerMode = current.enabled === false ? \"disabled\" : \"enabled\";\n } else {\n form.incomeLayerMode = incomeLayerSupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n form.incomeLayerStartUsd = current.startUsd || String(defaults?.startUsd || \"\");\n form.incomeLayerMaxRatio = current.maxRatio || defaults?.maxRatio || \"\";\n }\n\n function syncOptionOverlayForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.optionOverlayTouched) return;\n const configured = normalizeOptionOverlayMode(selectedAccount(platform)?.option_overlay_mode);\n if (configured !== \"current\") {\n form.optionOverlayMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.option_overlay_enabled);\n if (rawValue !== null) {\n form.optionOverlayMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n form.optionOverlayMode = optionOverlaySupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n\n function syncCashOnlyExecutionForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.cashOnlyExecutionTouched) return;\n const configured = normalizeCashOnlyExecutionMode(selectedAccount(platform)?.cash_only_execution_mode);\n if (configured !== \"current\") {\n form.cashOnlyExecutionMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.cash_only_execution);\n if (rawValue !== null) {\n form.cashOnlyExecutionMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n // No explicit config — use platform default (cash-only for margin-capable platforms)\n form.cashOnlyExecutionMode = platformSupportsMarginPolicy(platform) ? \"enabled\" : \"disabled\";\n }\n\n function syncDcaForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.dcaTouched) return;\n const current = currentDcaForAccount(platform, selectedAccount(platform), form.strategy);\n form.dcaMode = current.supported ? current.mode : \"fixed\";\n form.dcaBaseInvestmentUsd = current.supported ? current.baseInvestmentUsd : \"\";\n }\n\n function ensureAccountSelection(platform) {\n const options = optionsFor(platform);\n if (!options.length) return;\n if (!options.some((option) => option.key === state.forms[platform].accountKey)) {\n state.forms[platform].accountKey = options[0].key;\n state.forms[platform].runtimeTargetTouched = false;\n state.forms[platform].reservedCashTouched = false;\n state.forms[platform].incomeLayerTouched = false;\n state.forms[platform].optionOverlayTouched = false;\n state.forms[platform].cashOnlyExecutionTouched = false;\n state.forms[platform].dcaTouched = false;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0]);\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n syncDcaForAccount(platform);\n }\n }\n\n function derivedService(platform, targetName) {\n if (platform === \"longbridge\") return `longbridge-quant-${targetName.toLowerCase()}-service`;\n if (platform === \"ibkr\") return `interactive-brokers-${targetName.toLowerCase()}-service`;\n if (platform === \"schwab\") return \"charles-schwab-quant-service\";\n if (platform === \"firstrade\") return \"firstrade-quant-service\";\n if (platform === \"qmt\") return \"qmt-quant-service\";\n return \"\";\n }\n\n function accountMetaText(platform = state.selected) {\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const raw = account.service_name || derivedService(platform, targetName);\n const service = raw || (state.lang === \"zh\" ? \"无\" : \"-\");\n return t(\"targetMeta\")\n .replace(\"{target}\", targetName)\n .replace(\"{service}\", service)\n .replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function hasRunnableStrategySelection(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n return Boolean(form?.strategy && account && strategyAllowedForAccount(platform, account, form.strategy, form.executionMode));\n }\n\n function hasValidReservePolicy(platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n if (mode === \"current\" || mode === \"none\") return true;\n return Boolean(reservePolicyOverrideForForm(form, platform));\n }\n\n function hasValidExecutionCashPolicy(platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform) && !platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n return !executionCashPolicyConflict(form) && hasValidReservePolicy(platform);\n }\n\n function hasValidIncomeLayerPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!incomeLayerSupported(form?.strategy)) return true;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\" || mode === \"disabled\") return true;\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults?.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults?.maxRatio);\n return Boolean(startUsd && maxRatio);\n }\n\n function hasValidOptionOverlayPolicy(platform = state.selected) {\n const form = state.forms[platform];\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n return mode !== \"enabled\" || optionOverlaySupported(form?.strategy);\n }\n\n function hasValidDcaPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(platform)) return true;\n return Boolean(dcaModes.includes(normalizeDcaMode(form?.dcaMode)) && cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd));\n }\n\n function hasValidStrategySelection(platform = state.selected) {\n return hasRunnableStrategySelection(platform) &&\n hasValidExecutionCashPolicy(platform) &&\n hasValidIncomeLayerPolicy(platform) &&\n hasValidOptionOverlayPolicy(platform) &&\n hasValidDcaPolicy(platform);\n }\n\n function normalizeReservePolicyMode(value) {\n return reservePolicyModes.includes(value) ? value : \"current\";\n }\n\n function reservePolicyOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return null;\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n const floor = cleanDisplayNumber(form?.minReservedCashUsd);\n const ratio = cleanDisplayRatio(form?.reservedCashRatio);\n const extraVariables = {};\n if (mode === \"current\") return null;\n if (mode === \"none\") {\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: {}, extraVariables };\n }\n if (mode === \"ratio\") {\n if (!ratio) return null;\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n return { inputs: { reserved_cash_ratio: ratio }, extraVariables };\n }\n if (mode === \"floor\") {\n if (!floor) return null;\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: { min_reserved_cash_usd: floor }, extraVariables };\n }\n if (mode === \"max\") {\n if (!floor || !ratio) return null;\n return { inputs: { min_reserved_cash_usd: floor, reserved_cash_ratio: ratio }, extraVariables };\n }\n return null;\n }\n\n function runtimeTargetOverrideForForm(form) {\n const mode = normalizeRuntimeTargetMode(form?.runtimeTargetMode);\n if (mode === \"current\") return null;\n return {\n inputs: { runtime_target_enabled_mode: mode },\n extraVariables: { [runtimeTargetEnabledVariable]: mode === \"enabled\" ? \"true\" : \"false\" },\n };\n }\n\n function incomeLayerOverrideForForm(form) {\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n if (!defaults) return null;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\") return null;\n const extraVariables = {};\n if (mode === \"disabled\") {\n extraVariables[incomeLayerEnabledVariable] = \"false\";\n extraVariables[incomeLayerStartUsdVariable] = \"\";\n extraVariables[incomeLayerMaxRatioVariable] = \"\";\n return { inputs: { income_layer_mode: mode }, extraVariables };\n }\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults.maxRatio);\n if (!startUsd || !maxRatio) return null;\n extraVariables[incomeLayerEnabledVariable] = \"true\";\n extraVariables[incomeLayerStartUsdVariable] = startUsd;\n extraVariables[incomeLayerMaxRatioVariable] = maxRatio;\n return { inputs: { income_layer_mode: mode, income_layer_start_usd: startUsd, income_layer_max_ratio: maxRatio }, extraVariables };\n }\n\n function optionOverlayOverrideForForm(form) {\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n if (mode === \"current\") return null;\n if (mode === \"enabled\" && !optionOverlaySupported(form?.strategy)) return null;\n return { inputs: { option_overlay_mode: mode } };\n }\n\n function cashOnlyExecutionOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform)) return null;\n const mode = normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode);\n if (mode === \"current\") return null;\n return { inputs: { cash_only_execution_mode: mode } };\n }\n\n function dcaOverrideForForm(form) {\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(state.selected)) return null;\n const mode = normalizeDcaMode(form?.dcaMode);\n const baseInvestmentUsd = cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd);\n if (!baseInvestmentUsd) return null;\n return { inputs: { dca_mode: mode, dca_base_investment_usd: baseInvestmentUsd } };\n }\n\n function mergeExtraVariables(inputs, extraVariables) {\n if (!extraVariables || !Object.keys(extraVariables).length) return;\n const merged = inputs.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {};\n Object.assign(merged, extraVariables);\n inputs.extra_variables_json = JSON.stringify(merged);\n }\n\n function buildInputs(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const inputs = {\n platform,\n target_name: targetName,\n strategy_profile: form.strategy,\n execution_mode: form.executionMode,\n variable_scope: account.variable_scope || \"default\",\n plugin_mode: normalizePluginMode(form.pluginMode),\n service_targets_mode: \"auto\",\n apply: \"true\",\n trigger_platform_sync: \"true\",\n confirm_apply: \"APPLY_AND_SYNC\",\n platform_sync_workflow: \"sync-cloud-run-env.yml\",\n };\n for (const field of [\n \"github_environment\",\n \"deployment_selector\",\n \"account_selector\",\n \"account_scope\",\n \"service_name\",\n ]) {\n if (account[field]) inputs[field] = account[field];\n }\n const reserveOverride = platformSupportsReservedCashPolicy(platform)\n ? reservePolicyOverrideForForm(form, platform)\n : null;\n if (platformSupportsReservedCashPolicy(platform)) {\n inputs.reserved_cash_policy_mode = normalizeReservePolicyMode(form.reservePolicyMode);\n if (reserveOverride) {\n Object.assign(inputs, reserveOverride.inputs);\n mergeExtraVariables(inputs, reserveOverride.extraVariables);\n }\n }\n const runtimeTargetOverride = runtimeTargetOverrideForForm(form);\n inputs.runtime_target_enabled_mode = normalizeRuntimeTargetMode(form.runtimeTargetMode);\n if (runtimeTargetOverride) {\n Object.assign(inputs, runtimeTargetOverride.inputs);\n mergeExtraVariables(inputs, runtimeTargetOverride.extraVariables);\n }\n const incomeOverride = incomeLayerOverrideForForm(form);\n inputs.income_layer_mode = normalizeIncomeLayerMode(form.incomeLayerMode);\n if (incomeOverride) {\n Object.assign(inputs, incomeOverride.inputs);\n mergeExtraVariables(inputs, incomeOverride.extraVariables);\n }\n const optionOverlayOverride = optionOverlayOverrideForForm(form);\n inputs.option_overlay_mode = normalizeOptionOverlayMode(form.optionOverlayMode);\n if (optionOverlayOverride) {\n Object.assign(inputs, optionOverlayOverride.inputs);\n }\n const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form, platform);\n if (platformSupportsMarginPolicy(platform)) {\n inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode);\n if (cashOnlyOverride) {\n Object.assign(inputs, cashOnlyOverride.inputs);\n }\n }\n const dcaOverride = dcaOverrideForForm(form);\n if (dcaOverride) {\n Object.assign(inputs, dcaOverride.inputs);\n }\n return inputs;\n }\n\n function reservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform), fallback = t(\"unchanged\")) {\n if (inputs?.reserved_cash_policy_mode === \"none\") return t(\"reservedCashNone\");\n const floor = cleanDisplayNumber(inputs?.min_reserved_cash_usd);\n const ratio = cleanDisplayRatio(inputs?.reserved_cash_ratio);\n const currency = selectedCashCurrency(platform, account);\n const hasEffectiveFloor = Boolean(floor && !(ratio && Number(floor) === 0));\n if (!hasEffectiveFloor && !ratio) return fallback;\n if (hasEffectiveFloor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;\n if (hasEffectiveFloor) return `${floor} ${currency}`;\n return formatRatioPercent(ratio);\n }\n\n function platformReservedCashDefaultText(platform = state.selected, account = selectedAccount(platform)) {\n return t(\"reservedCashDefault\").replace(\"{currency}\", selectedCashCurrency(platform, account));\n }\n\n function currentReservedCashPolicyText(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n const policy = currentReservePolicyForAccount(platform, account);\n return reservedCashPolicyText(\n {\n min_reserved_cash_usd: policy.minReservedCashUsd,\n reserved_cash_ratio: policy.reservedCashRatio,\n },\n platform,\n account,\n entry ? platformReservedCashDefaultText(platform, account) : t(\"notRead\"),\n );\n }\n\n function pendingReservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n return reservedCashPolicyText(pendingReservePolicy(inputs, platform, account).inputs, platform, account, t(\"unchanged\"));\n }\n\n function pendingReservePolicy(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const current = currentReservePolicyForAccount(platform, account);\n const currentFloor = cleanDisplayNumber(current.minReservedCashUsd);\n const currentRatio = cleanDisplayRatio(current.reservedCashRatio);\n const mode = normalizeReservePolicyMode(inputs.reserved_cash_policy_mode);\n const next = {\n min_reserved_cash_usd: cleanDisplayNumber(inputs.min_reserved_cash_usd),\n reserved_cash_ratio: cleanDisplayRatio(inputs.reserved_cash_ratio),\n };\n if (mode === \"none\") {\n next.reserved_cash_policy_mode = \"none\";\n }\n const entry = currentEntryForAccount(platform, account);\n const changed = Boolean(entry && (\n next.min_reserved_cash_usd !== currentFloor ||\n next.reserved_cash_ratio !== currentRatio ||\n (mode === \"none\" && (currentFloor || currentRatio))\n ));\n return { changed, inputs: next };\n }\n\n function currentIncomeLayerText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return t(\"incomeLayerNotSupported\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return t(\"incomeLayerDefault\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n const enabled = current.enabled ?? true;\n const startUsd = current.startUsd || String(defaults.startUsd);\n const ratio = current.maxRatio || defaults.maxRatio;\n return enabled\n ? t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(startUsd))\n .replace(\"{ratio}\", formatRatioPercent(ratio))\n : t(\"incomeLayerOff\");\n }\n\n function pendingIncomeLayerText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingIncomeLayer(inputs, platform, account);\n if (!pending.supported) return t(\"incomeLayerNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n if (pending.inputs.income_layer_enabled === false) return t(\"incomeLayerOff\");\n return t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(pending.inputs.income_layer_start_usd))\n .replace(\"{ratio}\", formatRatioPercent(pending.inputs.income_layer_max_ratio));\n }\n\n function pendingOptionOverlayText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingOptionOverlay(inputs, platform, account);\n if (!pending.supported && pending.inputs.option_overlay_enabled !== false) return t(\"optionOverlayNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return optionOverlayText(pending.inputs.option_overlay_enabled);\n }\n\n function pendingCashOnlyExecutionText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingCashOnlyExecution(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return cashOnlyExecutionText(pending.inputs.cash_only_execution);\n }\n\n function pendingRuntimeTargetText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return runtimeTargetText(pending.inputs.runtime_target_enabled);\n }\n\n function pendingRuntimeTargetTone(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return \"neutral\";\n return runtimeTargetTone(pending.inputs.runtime_target_enabled);\n }\n\n function currentDcaText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const current = currentDcaForAccount(platform, account, profile);\n if (!current.supported) return t(\"dcaNotSupported\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(current.mode))\n .replace(\"{amount}\", formatUsd(current.baseInvestmentUsd));\n }\n\n function pendingDcaText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingDca(inputs, platform, account);\n if (!pending.supported) return t(\"dcaNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(pending.inputs.dca_mode))\n .replace(\"{amount}\", formatUsd(pending.inputs.dca_base_investment_usd));\n }\n\n function pendingRuntimeTarget(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeRuntimeTargetMode(inputs.runtime_target_enabled_mode);\n if (mode === \"current\") {\n return {\n changed: false,\n inputs: { runtime_target_enabled: runtimeTargetEnabledForAccount(platform, account) ?? true },\n };\n }\n const current = runtimeTargetEnabledForAccount(platform, account);\n const currentEnabled = current ?? true;\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && currentEnabled !== nextEnabled),\n inputs: { runtime_target_enabled: nextEnabled },\n };\n }\n\n function pendingIncomeLayer(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const mode = normalizeIncomeLayerMode(inputs.income_layer_mode);\n const entry = currentEntryForAccount(platform, account);\n const rawCurrent = currentIncomeLayerForAccount(platform, account);\n const effective = effectiveIncomeLayerForAccount(platform, account, profile);\n const currentEnabled = effective?.enabled ?? true;\n const currentStartUsd = effective?.startUsd ?? String(defaults.startUsd);\n const currentRatio = effective?.maxRatio ?? defaults.maxRatio;\n if (mode === \"current\") {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: rawCurrent.enabled,\n income_layer_start_usd: rawCurrent.startUsd,\n income_layer_max_ratio: rawCurrent.maxRatio,\n },\n };\n }\n if (mode === \"disabled\") {\n if (!entry) {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n return {\n supported: true,\n changed: currentEnabled !== false || Boolean(rawCurrent.startUsd || rawCurrent.maxRatio),\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n const nextStartUsd = cleanDisplayNumber(inputs.income_layer_start_usd || defaults.startUsd);\n const nextRatio = cleanDisplayRatio(inputs.income_layer_max_ratio || defaults.maxRatio);\n return {\n supported: true,\n changed: Boolean(entry && (currentEnabled !== true || nextStartUsd !== currentStartUsd || nextRatio !== currentRatio)),\n inputs: {\n income_layer_enabled: true,\n income_layer_start_usd: nextStartUsd,\n income_layer_max_ratio: nextRatio,\n },\n };\n }\n\n function pendingOptionOverlay(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const supported = optionOverlaySupported(profile);\n const mode = normalizeOptionOverlayMode(inputs.option_overlay_mode);\n const current = effectiveOptionOverlayForAccount(platform, account, profile);\n if (mode === \"current\") {\n return {\n supported,\n changed: false,\n inputs: { option_overlay_enabled: currentOptionOverlayForAccount(platform, account) },\n };\n }\n if (mode === \"enabled\") {\n return {\n supported,\n changed: supported && current !== null && current !== true,\n inputs: { option_overlay_enabled: true },\n };\n }\n return {\n supported,\n changed: current === true,\n inputs: { option_overlay_enabled: false },\n };\n }\n\n function pendingCashOnlyExecution(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeCashOnlyExecutionMode(inputs.cash_only_execution_mode);\n if (!platformSupportsMarginPolicy(platform) || mode === \"current\") {\n return { changed: false, inputs: {} };\n }\n const current = effectiveCashOnlyExecutionForAccount(platform, account);\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && current !== nextEnabled),\n inputs: { cash_only_execution: nextEnabled },\n };\n }\n\n function pendingDca(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const current = currentDcaForAccount(platform, account, profile);\n const nextMode = normalizeDcaMode(inputs.dca_mode || defaults.defaultMode);\n const nextBase = cleanDisplayPositiveNumber(inputs.dca_base_investment_usd || defaults.defaultBaseInvestmentUsd);\n return {\n supported: true,\n changed: Boolean(current.mode !== nextMode || current.baseInvestmentUsd !== nextBase),\n inputs: {\n dca_mode: nextMode,\n dca_base_investment_usd: nextBase,\n },\n };\n }\n\n function pendingChangeState(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const currentProfile = currentStrategyForAccount(platform, account);\n const nextProfile = cleanStrategyProfile(inputs.strategy_profile);\n const currentEntry = currentEntryForAccount(platform, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n const currentPluginMode = currentPluginModeForAccount(platform, account);\n const nextPluginMode = normalizePluginMode(inputs.plugin_mode);\n const runtimeTarget = pendingRuntimeTarget(inputs, platform, account);\n const reserve = pendingReservePolicy(inputs, platform, account);\n const income = pendingIncomeLayer(inputs, platform, account);\n const optionOverlay = pendingOptionOverlay(inputs, platform, account);\n const cashOnly = pendingCashOnlyExecution(inputs, platform, account);\n const dca = pendingDca(inputs, platform, account);\n return {\n currentProfile,\n nextProfile,\n currentMode,\n currentPluginMode,\n nextPluginMode,\n strategyChanged: Boolean(nextProfile && ((state.forms[platform]?.strategyTouched) || (currentProfile && currentProfile !== nextProfile))),\n modeChanged: Boolean(inputs.execution_mode && currentMode && currentMode !== inputs.execution_mode),\n pluginModeChanged: Boolean(nextPluginMode && currentPluginMode && currentPluginMode !== nextPluginMode),\n runtimeTargetChanged: runtimeTarget.changed,\n reserveCashChanged: reserve.changed,\n incomeLayerChanged: income.changed,\n optionOverlayChanged: optionOverlay.changed,\n cashOnlyChanged: cashOnly.changed,\n dcaChanged: dca.changed,\n runtimeTarget,\n reserve,\n income,\n optionOverlay,\n cashOnly,\n dca,\n };\n }\n\n function hasPendingChanges(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const changes = pendingChangeState(inputs, platform, account);\n return Boolean(\n changes.strategyChanged ||\n changes.modeChanged ||\n changes.pluginModeChanged ||\n changes.runtimeTargetChanged ||\n changes.reserveCashChanged ||\n changes.incomeLayerChanged ||\n changes.optionOverlayChanged ||\n changes.cashOnlyChanged ||\n changes.dcaChanged\n );\n }\n\n function formatRatioPercent(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `${(numeric * 100).toFixed(2).replace(/\\.?0+$/, \"\")}%`;\n }\n\n function formatUsd(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `$${numeric.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })}`;\n }\n\n function incomeLayerAllocationText(defaults) {\n if (!defaults?.allocations) return \"\";\n return Object.entries(defaults.allocations)\n .map(([symbol, ratio]) => `${symbol} ${formatRatioPercent(ratio)}`)\n .join(\" / \");\n }\n\n function incomeLayerDefaultMetaText(defaults) {\n if (!defaults) return t(\"incomeLayerModeMeta\");\n return t(\"incomeLayerDefaultMeta\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n\n function optionOverlayDefaultMetaText(defaults) {\n if (!defaults?.families?.length) return t(\"optionOverlayModeMeta\");\n const familyText = defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family}: ${item.recipe}, ${formatUsd(item.startUsd)}, ${ratioText}`;\n }).join(\" / \");\n return t(\"optionOverlayDefaultMeta\").replace(\"{defaults}\", familyText);\n }\n\n function summaryRows(inputs) {\n const account = selectedAccount();\n const changes = pendingChangeState(inputs, state.selected, account);\n const currentStrategyText = changes.currentProfile ? strategyLabel(changes.currentProfile) : t(\"notRead\");\n const rows = [\n [t(\"repository\"), state.repositories[state.selected] || defaultRepositories[state.selected]],\n [t(\"selectedAccount\"), account.label],\n [t(\"currentStrategy\"), currentStrategyText],\n [t(\"selectedMarket\"), supportedDomainLabel(state.selected, account)],\n [\n t(\"currentRuntimeTarget\"),\n currentRuntimeTargetText(state.selected, account),\n \"\",\n currentRuntimeTargetTone(state.selected, account),\n ],\n [t(\"currentPluginMode\"), pluginModeLabel(changes.currentPluginMode)],\n [t(\"reservedCashPolicy\"), currentReservedCashPolicyText(state.selected, account)],\n ];\n if (platformSupportsMarginPolicy(state.selected)) {\n rows.push([t(\"currentCashOnlyExecution\"), currentCashOnlyExecutionText(state.selected, account)]);\n }\n if (incomeLayerSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentIncomeLayer\"), currentIncomeLayerText(state.selected, account, inputs.strategy_profile)]);\n }\n if (optionOverlaySupported(inputs.strategy_profile) || changes.optionOverlayChanged) {\n rows.push([t(\"currentOptionOverlay\"), currentOptionOverlayText(state.selected, account, inputs.strategy_profile)]);\n }\n if (dcaSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentDca\"), currentDcaText(state.selected, account, inputs.strategy_profile)]);\n }\n if (changes.reserveCashChanged) {\n rows.push([t(\"pendingReservedCashPolicy\"), pendingReservedCashPolicyText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.incomeLayerChanged) {\n rows.push([t(\"pendingIncomeLayer\"), pendingIncomeLayerText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.optionOverlayChanged) {\n rows.push([t(\"pendingOptionOverlay\"), pendingOptionOverlayText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.cashOnlyChanged) {\n rows.push([t(\"pendingCashOnlyExecution\"), pendingCashOnlyExecutionText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.dcaChanged) {\n rows.push([t(\"pendingDca\"), pendingDcaText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.modeChanged) {\n rows.push([t(\"pendingMode\"), modeLabel(inputs.execution_mode), \"pending\"]);\n }\n if (changes.pluginModeChanged) {\n rows.push([t(\"pendingPluginMode\"), pluginModeLabel(changes.nextPluginMode), \"pending\"]);\n }\n if (changes.runtimeTargetChanged) {\n rows.push([\n t(\"pendingRuntimeTarget\"),\n pendingRuntimeTargetText(inputs, state.selected, account),\n \"pending\",\n pendingRuntimeTargetTone(inputs, state.selected, account),\n ]);\n }\n if (changes.strategyChanged && changes.nextProfile) {\n rows.push([t(\"nextStrategy\"), strategyLabel(changes.nextProfile), \"pending\"]);\n }\n return rows;\n }\n\n function applyLanguage() {\n document.documentElement.lang = state.lang === \"zh\" ? \"zh-CN\" : \"en\";\n document.querySelectorAll(\"[data-i18n]\").forEach((node) => {\n node.textContent = t(node.dataset.i18n);\n });\n el(\"lang-button\").textContent = state.lang === \"zh\" ? \"EN\" : \"中\";\n }\n\n function renderPlatforms() {\n const strip = el(\"platform-strip\");\n strip.replaceChildren();\n const showPrivateConfig = hasPrivateConfig();\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const button = document.createElement(\"button\");\n button.className = \"platform-button\";\n button.type = \"button\";\n button.dataset.platform = platform;\n button.classList.toggle(\"active\", platform === state.selected);\n const mark = document.createElement(\"span\");\n mark.className = \"mark\";\n mark.textContent = meta.code;\n const copyNode = document.createElement(\"span\");\n copyNode.className = \"platform-copy\";\n const labelNode = document.createElement(\"strong\");\n labelNode.textContent = meta.label;\n copyNode.append(labelNode);\n if (showPrivateConfig) {\n const accountNode = document.createElement(\"span\");\n accountNode.textContent = account.label;\n const strategyNode = document.createElement(\"small\");\n strategyNode.textContent = strategyLabel(form.strategy);\n copyNode.append(accountNode, strategyNode);\n }\n button.append(mark, copyNode);\n strip.appendChild(button);\n }\n }\n\n function renderControls() {\n const platform = state.selected;\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const accounts = optionsFor(platform);\n const account = selectedAccount(platform);\n const choices = strategyChoicesForAccount(platform, account, form.executionMode);\n const currentStrategy = currentStrategyForAccount(platform, account);\n const currentStrategyBlocked = Boolean(\n currentStrategy &&\n !strategyAllowedForAccount(platform, account, currentStrategy, form.executionMode),\n );\n const accountSelect = el(\"account-select\");\n const strategySelect = el(\"strategy-select\");\n const runtimeTargetEnabledSelect = el(\"runtime-target-enabled-select\");\n const pluginModeSelect = el(\"plugin-mode-select\");\n const incomeLayerModeSelect = el(\"income-layer-mode-select\");\n const incomeLayerStartUsdInput = el(\"income-layer-start-usd-input\");\n const incomeLayerMaxRatioInput = el(\"income-layer-max-ratio-input\");\n const optionOverlayModeSelect = el(\"option-overlay-mode-select\");\n const cashOnlyExecutionModeSelect = el(\"cash-only-execution-mode-select\");\n const dcaModeSelect = el(\"dca-mode-select\");\n const dcaBaseInvestmentUsdInput = el(\"dca-base-investment-usd-input\");\n const reservePolicyModeSelect = el(\"reserve-policy-mode-select\");\n const minReservedCashInput = el(\"min-reserved-cash-input\");\n const reservedCashRatioInput = el(\"reserved-cash-ratio-input\");\n const showPrivateControls = hasPrivateConfig();\n\n el(\"switch-panel\").style.setProperty(\"--platform-color\", meta.accent);\n el(\"platform-title\").textContent = meta.label;\n el(\"quick-form\").hidden = !showPrivateControls;\n el(\"run-area\").hidden = !showPrivateControls;\n el(\"public-note\").hidden = showPrivateControls;\n el(\"public-preview\").hidden = showPrivateControls;\n el(\"public-note\").textContent = state.auth.allowed ? t(\"missingConfigNote\") : t(\"publicReadonly\");\n\n if (!showPrivateControls) {\n accountSelect.replaceChildren();\n strategySelect.replaceChildren();\n runtimeTargetEnabledSelect.replaceChildren();\n pluginModeSelect.replaceChildren();\n incomeLayerModeSelect.replaceChildren();\n optionOverlayModeSelect.replaceChildren();\n cashOnlyExecutionModeSelect.replaceChildren();\n dcaModeSelect.replaceChildren();\n reservePolicyModeSelect.replaceChildren();\n incomeLayerStartUsdInput.value = \"\";\n incomeLayerMaxRatioInput.value = \"\";\n dcaBaseInvestmentUsdInput.value = \"\";\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n el(\"account-meta\").textContent = \"\";\n el(\"strategy-meta\").textContent = \"\";\n el(\"income-layer-mode-meta\").textContent = \"\";\n el(\"income-layer-start-meta\").textContent = \"\";\n el(\"income-layer-ratio-meta\").textContent = \"\";\n el(\"option-overlay-mode-meta\").textContent = \"\";\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n el(\"dca-mode-meta\").textContent = \"\";\n el(\"dca-base-meta\").textContent = \"\";\n return;\n }\n\n accountSelect.replaceChildren();\n if (accounts.length) {\n for (const account of accounts) {\n accountSelect.append(new Option(account.label, account.key, false, account.key === form.accountKey));\n }\n } else {\n accountSelect.append(new Option(t(\"noAccount\"), \"\"));\n }\n el(\"account-meta\").textContent = accounts.length ? accountMetaText(platform) : \"\";\n\n if (choices.length && !choices.includes(form.strategy) && !currentStrategyBlocked) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (currentStrategyBlocked) {\n const blockedOption = new Option(\n strategyChoiceLabel(currentStrategy, platform, account, form.executionMode),\n currentStrategy,\n true,\n currentStrategy === form.strategy,\n );\n blockedOption.disabled = true;\n strategySelect.append(blockedOption);\n }\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(\n new Option(strategyChoiceLabel(strategy, platform, account, form.executionMode), strategy, false, strategy === form.strategy),\n );\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? strategyDisplayMetaText(platform, account, form.strategy)\n : \"\";\n runtimeTargetEnabledSelect.replaceChildren();\n for (const mode of runtimeTargetModes) {\n runtimeTargetEnabledSelect.append(\n new Option(runtimeTargetModeLabel(mode), mode, false, mode === normalizeRuntimeTargetMode(form.runtimeTargetMode)),\n );\n }\n pluginModeSelect.replaceChildren();\n for (const mode of pluginModes) {\n pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode)));\n }\n const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy);\n el(\"income-layer-section\").hidden = false;\n el(\"option-overlay-section\").hidden = false;\n incomeLayerModeSelect.replaceChildren();\n if (incomeDefaults) {\n incomeLayerModeSelect.disabled = false;\n for (const mode of incomeLayerModes) {\n incomeLayerModeSelect.append(new Option(incomeLayerModeLabel(mode), mode, false, mode === normalizeIncomeLayerMode(form.incomeLayerMode)));\n }\n el(\"income-layer-mode-meta\").textContent = incomeLayerDefaultMetaText(incomeDefaults);\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerAllocationMeta\").replace(\n \"{allocations}\",\n incomeLayerAllocationText(incomeDefaults),\n );\n } else {\n incomeLayerModeSelect.disabled = true;\n incomeLayerModeSelect.append(new Option(t(\"incomeLayerNotSupported\"), \"current\"));\n el(\"income-layer-mode-meta\").textContent = t(\"incomeLayerModeMeta\");\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerRatioMeta\");\n }\n const supportsMargin = platformSupportsMarginPolicy(platform);\n const supportsReserve = platformSupportsReservedCashPolicy(platform);\n if (supportsMargin) syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(form, \"margin\");\n const executionCashPolicyGrid = el(\"execution-cash-policy-grid\");\n const qmtPlatformCashNote = el(\"qmt-platform-cash-note\");\n const executionCashPolicyNote = el(\"execution-cash-policy-note\");\n executionCashPolicyGrid.hidden = !supportsMargin && !supportsReserve;\n qmtPlatformCashNote.hidden = supportsMargin || supportsReserve || platform !== \"qmt\";\n executionCashPolicyNote.hidden = !supportsMargin || !supportsReserve;\n\n const marginBlocksReserve = supportsMargin && supportsReserve && allowMarginExplicitlySelected(form);\n const reserveBlocksMargin = supportsMargin && supportsReserve && reserveCashOverrideActive(form);\n\n if (supportsReserve) {\n reservePolicyModeSelect.replaceChildren();\n for (const mode of reservePolicyModes) {\n reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode)));\n }\n const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode);\n el(\"min-reserved-cash-label\").textContent = t(\"minReservedCash\").replace(\n \"{currency}\",\n selectedCashCurrency(platform, account),\n );\n reservePolicyModeSelect.disabled = false;\n minReservedCashInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"floor\";\n minReservedCashInput.value = reserveMode === \"ratio\" || reserveMode === \"none\" ? \"\" : form.minReservedCashUsd;\n reservedCashRatioInput.value = reserveMode === \"floor\" || reserveMode === \"none\" ? \"\" : form.reservedCashRatio;\n el(\"reserve-policy-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"min-reserve-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-ratio-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-policy-mode-meta\").textContent = marginBlocksReserve\n ? t(\"executionCashMarginBlocksReserve\")\n : t(\"reservedCashModeMeta\");\n } else {\n reservePolicyModeSelect.replaceChildren();\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n }\n const incomeMode = normalizeIncomeLayerMode(form.incomeLayerMode);\n const incomeLayerInputsDisabled = !incomeDefaults || incomeMode === \"disabled\";\n incomeLayerStartUsdInput.disabled = incomeLayerInputsDisabled;\n incomeLayerMaxRatioInput.disabled = incomeLayerInputsDisabled;\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayNumber(form.incomeLayerStartUsd)) {\n form.incomeLayerStartUsd = String(incomeDefaults.startUsd);\n }\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayRatio(form.incomeLayerMaxRatio)) {\n form.incomeLayerMaxRatio = incomeDefaults.maxRatio;\n }\n incomeLayerStartUsdInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerStartUsd : \"\";\n incomeLayerMaxRatioInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerMaxRatio : \"\";\n\n const optionDefaults = optionOverlayDefaultForStrategy(form.strategy);\n optionOverlayModeSelect.replaceChildren();\n if (optionDefaults) {\n optionOverlayModeSelect.disabled = false;\n for (const mode of optionOverlayModes) {\n optionOverlayModeSelect.append(\n new Option(optionOverlayModeLabel(mode), mode, false, mode === normalizeOptionOverlayMode(form.optionOverlayMode)),\n );\n }\n el(\"option-overlay-mode-meta\").textContent = optionOverlayDefaultMetaText(optionDefaults);\n } else {\n optionOverlayModeSelect.disabled = true;\n optionOverlayModeSelect.append(new Option(t(\"optionOverlayNotSupported\"), \"current\"));\n el(\"option-overlay-mode-meta\").textContent = t(\"optionOverlayModeMeta\");\n }\n\n if (supportsMargin) {\n cashOnlyExecutionModeSelect.replaceChildren();\n for (const mode of cashOnlyExecutionModes) {\n const option = new Option(\n mode === \"enabled\" ? t(\"cashOnlyExecutionNo\") : t(\"cashOnlyExecutionYes\"),\n mode,\n false,\n mode === normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode),\n );\n cashOnlyExecutionModeSelect.append(option);\n }\n el(\"cash-only-policy-block\").classList.toggle(\"policy-block-muted\", reserveBlocksMargin);\n el(\"cash-only-execution-mode-meta\").textContent = reserveBlocksMargin\n ? t(\"executionCashReserveBlocksMargin\")\n : t(\"cashOnlyExecutionModeMeta\");\n } else {\n cashOnlyExecutionModeSelect.replaceChildren();\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n }\n\n const dcaDefaults = dcaConfigForStrategy(form.strategy);\n dcaModeSelect.replaceChildren();\n const dcaAllowed = Boolean(dcaDefaults) && platformSupportsDca(platform);\n if (dcaAllowed) {\n dcaModeSelect.disabled = false;\n for (const mode of dcaModes) {\n dcaModeSelect.append(new Option(dcaModeLabel(mode), mode, false, mode === normalizeDcaMode(form.dcaMode)));\n }\n if (!cleanDisplayPositiveNumber(form.dcaBaseInvestmentUsd)) {\n form.dcaBaseInvestmentUsd = dcaDefaults.defaultBaseInvestmentUsd;\n }\n dcaBaseInvestmentUsdInput.disabled = false;\n dcaBaseInvestmentUsdInput.value = form.dcaBaseInvestmentUsd;\n el(\"dca-mode-meta\").textContent = t(\"dcaDefaultMeta\")\n .replace(\"{mode}\", dcaModeLabel(dcaDefaults.defaultMode))\n .replace(\"{amount}\", formatUsd(dcaDefaults.defaultBaseInvestmentUsd));\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n } else {\n dcaModeSelect.disabled = true;\n dcaModeSelect.append(new Option(\n dcaDefaults && !platformSupportsDca(platform) ? t(\"dcaPlatformNotSupported\") : t(\"dcaNotSupported\"),\n \"fixed\",\n ));\n dcaBaseInvestmentUsdInput.disabled = true;\n dcaBaseInvestmentUsdInput.value = \"\";\n el(\"dca-mode-meta\").textContent = t(\"dcaModeMeta\");\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n }\n\n if (platformDryRunOnly(platform)) {\n form.executionMode = \"paper\";\n }\n document.querySelectorAll(\"#mode-control [data-mode]\").forEach((button) => {\n const dryRunOnly = platformDryRunOnly(platform);\n button.disabled = dryRunOnly && button.dataset.mode === \"live\";\n button.classList.toggle(\"active\", button.dataset.mode === form.executionMode);\n });\n el(\"mode-meta\").textContent = platformDryRunOnly(platform) ? t(\"qmtDryRunOnlyNote\") : \"\";\n }\n\n function renderSummary() {\n const showSummary = hasPrivateConfig();\n const summaryPanel = document.querySelector(\".summary-panel\");\n const switchSurface = document.querySelector(\".switch-surface\");\n summaryPanel.hidden = !showSummary;\n switchSurface.classList.toggle(\"summary-hidden\", !showSummary);\n if (!showSummary) return;\n\n const inputs = buildInputs();\n const list = el(\"summary-list\");\n list.replaceChildren();\n document.querySelector(\".summary-head h2\").textContent = t(\"summary\");\n for (const [label, value, rowClass, valueTone] of summaryRows(inputs)) {\n const row = document.createElement(\"div\");\n row.className = \"summary-row\";\n row.setAttribute(\"role\", \"listitem\");\n if (rowClass) row.classList.add(rowClass);\n const labelNode = document.createElement(\"div\");\n labelNode.className = \"summary-label\";\n labelNode.textContent = label;\n const valueNode = document.createElement(\"div\");\n valueNode.className = \"summary-value\";\n if (valueTone) {\n const badge = document.createElement(\"span\");\n badge.className = `summary-status ${valueTone}`;\n badge.textContent = value;\n valueNode.appendChild(badge);\n } else {\n valueNode.textContent = value;\n }\n row.append(labelNode, valueNode);\n list.appendChild(row);\n }\n\n const account = selectedAccount();\n const currentEntry = currentEntryForAccount(state.selected, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n el(\"mode-pill\").textContent = currentMode ? modeLabel(currentMode) : t(\"notRead\");\n }\n\n function renderAuth() {\n const status = el(\"auth-status\");\n const loginLink = el(\"login-link\");\n const logoutButton = el(\"logout-button\");\n const signedIn = Boolean(state.auth.allowed && state.auth.login);\n\n status.hidden = !signedIn;\n status.textContent = signedIn ? t(\"signedInAs\").replace(\"{login}\", state.auth.login) : \"\";\n loginLink.hidden = signedIn;\n loginLink.href = \"/login\";\n loginLink.textContent = t(\"login\");\n logoutButton.hidden = !signedIn;\n logoutButton.textContent = t(\"logout\");\n\n const dispatch = el(\"dispatch-button\");\n const hasPrivateAccounts = state.configSource === \"private\";\n const loadingConfig = state.configSource === \"loading\";\n const hasRunnableStrategy = hasRunnableStrategySelection();\n const hasValidReserve = hasValidExecutionCashPolicy();\n const hasValidIncomeLayer = hasValidIncomeLayerPolicy();\n const hasValidOptionOverlay = hasValidOptionOverlayPolicy();\n const hasValidDca = hasValidDcaPolicy();\n const hasValidStrategy = hasRunnableStrategy &&\n hasValidReserve &&\n hasValidIncomeLayer &&\n hasValidOptionOverlay &&\n hasValidDca;\n const hasPendingChange = hasPrivateAccounts && hasValidStrategy && hasPendingChanges(buildInputs());\n dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy || !hasPendingChange;\n dispatch.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfig\")\n : (hasPrivateAccounts ? (hasValidStrategy ? (hasPendingChange ? t(\"runSwitch\") : t(\"noChanges\")) : t(\"configureAccounts\")) : t(\"configureAccounts\")))\n : t(\"loginToRun\");\n const note = el(\"action-note\");\n note.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfigNote\")\n : (hasPrivateAccounts\n ? (hasRunnableStrategy\n ? (hasValidReserve\n ? (hasValidIncomeLayer\n ? (hasValidOptionOverlay\n ? (hasValidDca ? (hasPendingChange ? t(\"readyNote\") : \"\") : t(\"invalidDcaNote\"))\n : t(\"invalidOptionOverlayNote\"))\n : t(\"invalidIncomeLayerNote\"))\n : (executionCashPolicyConflict(state.forms[state.selected])\n ? t(\"invalidExecutionCashPolicyNote\")\n : t(\"invalidReservePolicyNote\")))\n : strategyActionNoteText())\n : t(\"missingConfigNote\")))\n : t(\"readonlyNote\");\n note.classList.toggle(\n \"warning\",\n state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy),\n );\n }\n\n function renderAppVisibility() {\n document.body.classList.toggle(\"app-loading\", !state.appReady);\n el(\"boot-message\").textContent = t(state.bootMessageKey);\n }\n\n function healthStatusLabel(status) {\n return { healthy: \"健康\", watch: \"观察\", review: \"复核\", critical: \"严重\" }[status] || \"未知\";\n }\n\n function normalizeHealthPayload(payload) {\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) throw new Error(\"invalid health payload\");\n const strategies = Array.isArray(payload.strategies) ? payload.strategies : [];\n return {\n data_status: [\"ready\", \"stale\", \"unavailable\"].includes(payload.data_status) ? payload.data_status : \"unavailable\",\n computed_at: payload.computed_at || null,\n summary: payload.summary && typeof payload.summary === \"object\" ? payload.summary : {},\n strategies: strategies.filter((item) => item && typeof item === \"object\" && [\"healthy\", \"watch\", \"review\", \"critical\"].includes(item.status)),\n errors: Array.isArray(payload.errors) ? payload.errors : [],\n };\n }\n\n function renderHealth() {\n const payload = state.health.payload;\n const summary = payload.summary || {};\n const statusText = payload.data_status === \"ready\"\n ? \"快照已加载\"\n : (payload.data_status === \"stale\" ? \"快照已过期\" : \"等待可用快照\");\n el(\"health-status\").textContent = statusText;\n el(\"health-computed-at\").textContent = payload.computed_at\n ? `最近计算:${new Date(payload.computed_at).toLocaleString()}`\n : \"最近计算:—\";\n el(\"health-count-total\").textContent = String(Number(summary.strategy_count) || 0);\n el(\"health-count-healthy\").textContent = String(Number(summary.healthy) || 0);\n el(\"health-count-watch\").textContent = String(Number(summary.watch) || 0);\n el(\"health-count-review\").textContent = String(Number(summary.review) || 0);\n el(\"health-count-critical\").textContent = String(Number(summary.critical) || 0);\n\n const notice = el(\"health-notice\");\n if (!state.auth.allowed) {\n notice.textContent = \"登录后读取私有策略健康快照;没有快照时不会展示虚构指标。\";\n } else if (payload.data_status === \"stale\") {\n notice.textContent = \"健康快照已超过允许的新鲜度窗口;页面保留原始状态,但不会把它当作当前健康结论。\";\n } else if (payload.data_status !== \"ready\") {\n notice.textContent = \"还没有可用的策略健康快照;当前页面保持 fail-closed 空状态。\";\n } else if (payload.errors?.length) {\n notice.textContent = `快照已加载,但有 ${payload.errors.length} 个上游提示;缺失数据不会被替换成虚构指标。`;\n } else {\n notice.textContent = \"健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。\";\n }\n\n const list = el(\"health-list\");\n list.replaceChildren();\n const strategies = payload.strategies.filter((item) => state.health.filter === \"all\" || item.status === state.health.filter);\n if (!strategies.length) {\n const empty = document.createElement(\"div\");\n empty.className = \"health-card__empty\";\n empty.textContent = \"暂无可展示的策略健康快照。\";\n list.appendChild(empty);\n return;\n }\n for (const item of strategies) {\n const card = document.createElement(\"article\");\n card.className = \"health-card\";\n const main = document.createElement(\"div\");\n main.className = \"health-card__main\";\n const meta = document.createElement(\"div\");\n meta.className = \"health-card__meta\";\n meta.textContent = `${healthStatusLabel(item.status)} · ${item.domain || \"unknown\"}`;\n const title = document.createElement(\"h4\");\n title.className = \"health-card__title\";\n title.textContent = String(item.profile || \"unknown\");\n const reason = document.createElement(\"p\");\n reason.className = \"health-card__reason\";\n reason.textContent = `${item.decision?.label || \"证据不足,保持研究态\"}。${item.decision?.reason || \"没有可用的机器检查结果。\"}`;\n const detail = document.createElement(\"div\");\n detail.className = \"health-card__meta\";\n detail.textContent = `阶段:${item.review?.requested_stage || \"未标记\"} · 截至:${item.as_of || \"—\"}`;\n main.append(meta, title, reason, detail);\n const scoreBlock = document.createElement(\"div\");\n scoreBlock.className = \"health-card__score\";\n const scoreLabel = document.createElement(\"small\");\n scoreLabel.textContent = \"HEALTH\";\n const score = document.createElement(\"strong\");\n score.textContent = typeof item.score === \"number\" ? item.score.toFixed(1) : \"—\";\n const decision = document.createElement(\"small\");\n decision.textContent = item.decision?.code || \"evidence_missing\";\n scoreBlock.append(scoreLabel, score, decision);\n card.append(main, scoreBlock);\n list.appendChild(card);\n }\n }\n\n function renderConsoleView() {\n const healthButton = el(\"health-view-button\");\n const switchButton = el(\"switch-view-button\");\n const healthVisible = state.view === \"health\";\n el(\"health-view\").hidden = !healthVisible;\n el(\"switch-view\").hidden = healthVisible;\n healthButton.classList.toggle(\"active\", healthVisible);\n switchButton.classList.toggle(\"active\", !healthVisible);\n }\n\n function render() {\n applyLanguage();\n renderConsoleView();\n renderHealth();\n renderPlatforms();\n renderControls();\n renderSummary();\n renderAuth();\n renderAppVisibility();\n }\n\n async function refreshSession() {\n state.bootMessageKey = \"bootSession\";\n render();\n try {\n const session = await requestJson(\"/api/session\");\n state.auth = {\n available: true,\n allowed: Boolean(session.allowed),\n admin: Boolean(session.admin),\n login: session.login || null,\n };\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n }\n if (state.auth.allowed) {\n await refreshHealth();\n await refreshConfig();\n } else {\n state.bootMessageKey = \"bootPublic\";\n state.appReady = true;\n render();\n }\n }\n\n async function refreshHealth() {\n if (!state.auth.allowed) {\n renderHealth();\n return;\n }\n try {\n state.health.payload = normalizeHealthPayload(await requestJson(\"/api/strategy-health\"));\n } catch {\n state.health.payload = {\n data_status: \"unavailable\",\n computed_at: null,\n summary: { strategy_count: 0, healthy: 0, watch: 0, review: 0, critical: 0 },\n strategies: [],\n errors: [\"health_request_failed\"],\n };\n }\n renderHealth();\n }\n\n async function refreshStrategyProfiles() {\n state.bootMessageKey = \"bootStrategy\";\n render();\n try {\n const payload = await requestJson(\"/api/strategy-profiles\");\n applyStrategyProfiles(payload.strategyProfiles || []);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n } catch {\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n }\n }\n\n async function refreshConfig() {\n if (!state.auth.available || !state.auth.allowed) return;\n state.configSource = \"loading\";\n state.bootMessageKey = \"bootConfig\";\n render();\n try {\n const payload = await requestJson(\"/api/config\");\n if (payload.accountOptions) {\n applyStrategyProfiles(payload.strategyProfiles || defaultStrategyProfiles);\n state.accountOptions = normalizeAccountOptions(payload.accountOptions);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n state.repositories = normalizePlatformRepositories(payload.platformRepositories || {});\n state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});\n state.configSource = \"private\";\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n syncStrategyForAccount(platform);\n }\n } else {\n state.configSource = \"default\";\n state.currentStrategies = {};\n }\n } catch (error) {\n state.configSource = \"default\";\n state.currentStrategies = {};\n if (isRequestTimeoutError(error)) {\n state.bootMessageKey = \"bootTimeout\";\n } else {\n state.bootMessageKey = \"bootPublic\";\n }\n } finally {\n state.appReady = true;\n render();\n }\n }\n\n function normalizeAccountOptions(raw) {\n const normalized = clone(defaultAccountOptions);\n for (const platform of Object.keys(platformMeta)) {\n if (!Array.isArray(raw[platform]) || !raw[platform].length) continue;\n normalized[platform] = raw[platform].map((item, index) => ({\n key: String(item.key || item.target_name || index),\n label: String(item.label || item.target_name || item.key || platform),\n target_name: String(item.target_name || item.key || \"\"),\n account_selector: item.account_selector ? String(item.account_selector) : \"\",\n deployment_selector: item.deployment_selector ? String(item.deployment_selector) : \"\",\n account_scope: item.account_scope ? String(item.account_scope) : \"\",\n service_name: item.service_name ? String(item.service_name) : \"\",\n cash_currency: item.cash_currency || item.market_currency || item.trading_currency\n ? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase()\n : \"\",\n supported_domains: normalizeSupportedDomains(platform, item),\n github_environment: item.github_environment ? String(item.github_environment) : \"\",\n variable_scope: item.variable_scope ? String(item.variable_scope) : \"\",\n plugin_mode: item.plugin_mode ? String(item.plugin_mode) : \"\",\n option_overlay_mode: item.option_overlay_mode ? normalizeOptionOverlayMode(item.option_overlay_mode) : \"\",\n cash_only_execution_mode: item.cash_only_execution_mode\n ? normalizeCashOnlyExecutionMode(item.cash_only_execution_mode)\n : \"\",\n dca_mode: item.dca_mode ? normalizeDcaMode(item.dca_mode) : \"\",\n dca_base_investment_usd: cleanDisplayPositiveNumber(item.dca_base_investment_usd),\n }));\n }\n return normalized;\n }\n\n function normalizeSupportedDomains(platform, item) {\n const raw = Array.isArray(item?.supported_domains)\n ? item.supported_domains\n : String(item?.supported_domains || \"\").split(/[\\s,;]+/);\n const cleaned = raw.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n return inferSupportedDomains(platform, item || {});\n }\n\n function normalizeCurrentStrategies(raw) {\n const normalized = {};\n for (const platform of Object.keys(platformMeta)) {\n if (!raw[platform] || typeof raw[platform] !== \"object\" || Array.isArray(raw[platform])) continue;\n normalized[platform] = {};\n for (const [key, entry] of Object.entries(raw[platform])) {\n const profile = cleanStrategyProfile(entry?.strategy_profile);\n const minReservedCashUsd = cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd);\n const reservedCashRatio = cleanDisplayRatio(entry?.reserved_cash_ratio);\n const incomeLayerEnabled = cleanOptionalBoolean(entry?.income_layer_enabled);\n const incomeLayerStartUsd = cleanDisplayNumber(entry?.income_layer_start_usd);\n const incomeLayerMaxRatio = cleanDisplayRatio(entry?.income_layer_max_ratio);\n const optionOverlayEnabled = cleanOptionalBoolean(entry?.option_overlay_enabled);\n const cashOnlyExecution = cleanOptionalBoolean(entry?.cash_only_execution);\n const runtimeTargetEnabled = cleanOptionalBoolean(entry?.runtime_target_enabled);\n const dcaMode = entry?.dca_mode ? normalizeDcaMode(entry.dca_mode) : \"\";\n const dcaBaseInvestmentUsd = cleanDisplayPositiveNumber(entry?.dca_base_investment_usd);\n const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only);\n if (\n !profile &&\n !minReservedCashUsd &&\n !reservedCashRatio &&\n incomeLayerEnabled === null &&\n !incomeLayerStartUsd &&\n !incomeLayerMaxRatio &&\n optionOverlayEnabled === null &&\n cashOnlyExecution === null &&\n runtimeTargetEnabled === null &&\n !dcaMode &&\n !dcaBaseInvestmentUsd &&\n !executionMode\n ) continue;\n normalized[platform][String(key)] = {\n strategy_profile: profile,\n execution_mode: executionMode,\n dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === \"true\" || entry?.dry_run_only === \"1\",\n min_reserved_cash_usd: minReservedCashUsd,\n reserved_cash_ratio: reservedCashRatio,\n income_layer_enabled: incomeLayerEnabled,\n income_layer_start_usd: incomeLayerStartUsd,\n income_layer_max_ratio: incomeLayerMaxRatio,\n option_overlay_enabled: optionOverlayEnabled,\n cash_only_execution: cashOnlyExecution,\n runtime_target_enabled: runtimeTargetEnabled,\n dca_mode: dcaMode,\n dca_base_investment_usd: dcaBaseInvestmentUsd,\n source: entry?.source ? String(entry.source) : \"\",\n };\n }\n if (!Object.keys(normalized[platform]).length) delete normalized[platform];\n }\n return normalized;\n }\n\n function normalizePlatformRepositories(raw) {\n const normalized = clone(defaultRepositories);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return normalized;\n for (const platform of Object.keys(platformMeta)) {\n const repository = String(raw[platform] || \"\").trim();\n if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(repository)) {\n normalized[platform] = repository;\n }\n }\n return normalized;\n }\n\n async function dispatchSwitch() {\n if (!state.auth.allowed) return;\n showToast(t(\"dispatching\"), { duration: 0 });\n try {\n const response = await fetch(\"/api/switch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildInputs()),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || t(\"dispatchFailed\"));\n showToast(t(\"dispatched\"), { duration: 4000 });\n if (payload.actions_url) window.open(payload.actions_url, \"_blank\", \"noopener,noreferrer\");\n await refreshConfig();\n } catch (error) {\n showToast(`${t(\"dispatchFailed\")}: ${error.message}`, { duration: 12000 });\n }\n }\n\n async function handleLogout() {\n await fetch(\"/api/logout\", { method: \"POST\" });\n window.location.reload();\n }\n\n function summaryText() {\n const inputs = buildInputs();\n return summaryRows(inputs).map(([label, value]) => `${label}: ${value}`).join(\"\\\n\");\n }\n\n document.querySelectorAll(\"[data-view]\").forEach((button) => button.addEventListener(\"click\", () => {\n state.view = button.dataset.view === \"switch\" ? \"switch\" : \"health\";\n renderConsoleView();\n if (state.view === \"health\") refreshHealth();\n }));\n\n document.querySelectorAll(\"[data-health-filter]\").forEach((button) => button.addEventListener(\"click\", () => {\n document.querySelectorAll(\"[data-health-filter]\").forEach((node) => node.classList.remove(\"active\"));\n button.classList.add(\"active\");\n state.health.filter = button.dataset.healthFilter;\n renderHealth();\n }));\n\n el(\"platform-strip\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-platform]\");\n if (!button) return;\n state.selected = button.dataset.platform;\n state.forms[state.selected].strategyTouched = false;\n render();\n });\n\n el(\"account-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].accountKey = el(\"account-select\").value;\n state.forms[state.selected].runtimeTargetTouched = false;\n state.forms[state.selected].reservedCashTouched = false;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].cashOnlyExecutionTouched = false;\n state.forms[state.selected].dcaTouched = false;\n state.forms[state.selected].strategyTouched = false;\n syncStrategyForAccount(state.selected);\n render();\n });\n\n el(\"strategy-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].strategy = el(\"strategy-select\").value;\n state.forms[state.selected].strategyTouched = true;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].dcaTouched = false;\n syncIncomeLayerForAccount(state.selected);\n syncOptionOverlayForAccount(state.selected);\n syncDcaForAccount(state.selected);\n render();\n });\n\n el(\"mode-control\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-mode]\");\n if (!button || button.disabled) return;\n if (platformDryRunOnly(state.selected) && button.dataset.mode === \"live\") return;\n state.forms[state.selected].executionMode = button.dataset.mode;\n render();\n });\n\n el(\"plugin-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.pluginMode = normalizePluginMode(el(\"plugin-mode-select\").value);\n render();\n });\n\n el(\"runtime-target-enabled-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.runtimeTargetMode = normalizeRuntimeTargetMode(el(\"runtime-target-enabled-select\").value);\n form.runtimeTargetTouched = form.runtimeTargetMode !== \"current\";\n render();\n });\n\n el(\"income-layer-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerMode = normalizeIncomeLayerMode(el(\"income-layer-mode-select\").value);\n form.incomeLayerTouched = form.incomeLayerMode !== \"current\";\n if (form.incomeLayerMode === \"current\") {\n form.incomeLayerTouched = false;\n syncIncomeLayerForAccount(state.selected);\n }\n render();\n });\n\n el(\"income-layer-start-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerStartUsd = el(\"income-layer-start-usd-input\").value.trim();\n render();\n });\n\n el(\"income-layer-max-ratio-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerMaxRatio = el(\"income-layer-max-ratio-input\").value.trim();\n render();\n });\n\n el(\"option-overlay-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.optionOverlayMode = normalizeOptionOverlayMode(el(\"option-overlay-mode-select\").value);\n form.optionOverlayTouched = form.optionOverlayMode !== \"current\";\n render();\n });\n\n el(\"cash-only-execution-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.cashOnlyExecutionMode = normalizeCashOnlyExecutionMode(el(\"cash-only-execution-mode-select\").value);\n form.cashOnlyExecutionTouched = form.cashOnlyExecutionMode !== \"current\";\n if (allowMarginExplicitlySelected(form)) reconcileExecutionCashPolicy(form, \"margin\");\n else restoreReserveAfterMarginDisabled(form);\n render();\n });\n\n el(\"dca-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaMode = normalizeDcaMode(el(\"dca-mode-select\").value);\n render();\n });\n\n el(\"dca-base-investment-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaBaseInvestmentUsd = el(\"dca-base-investment-usd-input\").value.trim();\n render();\n });\n\n el(\"reserve-policy-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.reservePolicyMode = normalizeReservePolicyMode(el(\"reserve-policy-mode-select\").value);\n form.reservedCashTouched = form.reservePolicyMode !== \"current\";\n if (form.reservePolicyMode === \"current\") syncReservePolicyForAccount(state.selected);\n reconcileExecutionCashPolicy(form, \"reserve\");\n render();\n });\n\n el(\"min-reserved-cash-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].minReservedCashUsd = el(\"min-reserved-cash-input\").value.trim();\n render();\n });\n\n el(\"reserved-cash-ratio-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].reservedCashRatio = el(\"reserved-cash-ratio-input\").value.trim();\n render();\n });\n\n el(\"copy-button\").addEventListener(\"click\", async () => {\n try {\n await navigator.clipboard.writeText(summaryText());\n showToast(t(\"copied\"), { duration: 3000 });\n } catch {\n showToast(summaryText(), { duration: 0 });\n }\n });\n\n el(\"dispatch-button\").addEventListener(\"click\", dispatchSwitch);\n el(\"logout-button\").addEventListener(\"click\", handleLogout);\n el(\"lang-button\").addEventListener(\"click\", () => {\n state.lang = state.lang === \"zh\" ? \"en\" : \"zh\";\n localStorage.setItem(\"qsl-switch-lang\", state.lang);\n render();\n });\n\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n boot();\n\n async function boot() {\n try {\n await refreshStrategyProfiles();\n await refreshSession();\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n state.configSource = \"default\";\n state.currentStrategies = {};\n state.bootMessageKey = \"bootTimeout\";\n state.appReady = true;\n render();\n }\n }\n"; diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index 905f5b9..bbca498 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -43,6 +43,45 @@

读取策略配置

+ + +
+
+
+ 策略健康 / 只读 +

先看机器结论,再决定动作。

+

健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。

+
+
+ 等待登录 + 最近计算:— +
+
+
登录后读取私有策略健康快照;没有快照时不会展示虚构指标。
+
+
策略总数0
+
健康0
+
观察0
+
需要复核0
+
严重0
+
+
+

策略状态

+
+ + + + + +
+
+
+
+ +
diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js index fe59b5d..178f8b3 100644 --- a/web/strategy-switch-console/page_asset.js +++ b/web/strategy-switch-console/page_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\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

LongBridge

\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

现金与融资

\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 QuantRuntimeSettings Strategy Switch\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

健康不等于已批准 live;正常实盘、资金和杠杆变更仍需人工确认。

\n
\n
\n 等待登录\n 最近计算:—\n
\n
\n
登录后读取私有策略健康快照;没有快照时不会展示虚构指标。
\n
\n
策略总数0
\n
健康0
\n
观察0
\n
需要复核0
\n
严重0
\n
\n
\n

策略状态

\n
\n \n \n \n \n \n
\n
\n
\n
\n\n \n
\n\n \n\n\n"; diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js index 8f6ffa6..8b81050 100644 --- a/web/strategy-switch-console/worker.js +++ b/web/strategy-switch-console/worker.js @@ -33,6 +33,12 @@ const CURRENT_STRATEGIES_CACHE_KEY = "current_strategies_cache"; const CURRENT_STRATEGIES_CACHE_TTL_MS = 5_000; // 5 sec — rapid refresh during active development const CURRENT_STRATEGIES_STALE_TTL_MS = 600_000; // 10 min — return stale + background refresh const GITHUB_API_TIMEOUT_MS = 8000; +const STRATEGY_HEALTH_SNAPSHOT_KEY = "strategy_health_snapshot"; +const STRATEGY_HEALTH_MAX_BODY_BYTES = 256 * 1024; +const STRATEGY_HEALTH_DEFAULT_STALE_TTL_SECONDS = 2 * 60 * 60; +const STRATEGY_HEALTH_STATUSES = ["healthy", "watch", "review", "critical"]; +const STRATEGY_HEALTH_DOMAINS = ["us_equity", "hk_equity", "cn_equity", "crypto"]; +const STRATEGY_HEALTH_DATA_STATUSES = ["ready", "unavailable", "stale"]; const SUPPORTED_PLATFORMS = ["longbridge", "ibkr", "schwab", "firstrade", "qmt", "binance"]; const SUPPORTED_STRATEGY_DOMAINS = ["us_equity", "hk_equity", "cn_equity", "crypto"]; @@ -149,6 +155,12 @@ export default { if (url.pathname === "/api/internal/sync-strategy-profiles" && request.method === "POST") { return await syncStrategyProfilesResponse(request, env); } + if (url.pathname === "/api/internal/sync-strategy-health" && request.method === "POST") { + return await syncStrategyHealthResponse(request, env); + } + if (url.pathname === "/api/strategy-health" && request.method === "GET") { + return await strategyHealthResponse(request, env); + } if (url.pathname === "/api/logout" && request.method === "POST") return logout(request); if (url.pathname === "/api/switch" && request.method === "POST") return await dispatchSwitch(request, env); if (url.pathname === "/app.css") return new Response(APP_CSS, { status: 200, headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "public, max-age=3600" } }); @@ -1079,6 +1091,308 @@ async function syncStrategyProfilesResponse(request, env) { ); } +async function syncStrategyHealthResponse(request, env) { + requireDedicatedStrategyHealthSyncToken(request, env); + if (!hasConfigStore(env)) { + return json({ ok: false, error: "strategy health KV is not configured" }, 503); + } + + let raw; + try { + raw = await readBoundedJson(request, STRATEGY_HEALTH_MAX_BODY_BYTES); + } catch (error) { + return json({ ok: false, error: error.message || "invalid strategy health payload" }, error.status || 400); + } + + let snapshot; + try { + snapshot = normalizeStrategyHealthSnapshot(raw, "strategy health snapshot"); + } catch (error) { + return json({ ok: false, error: error.message || "invalid strategy health payload" }, 400); + } + + await writeConfigJson(env, STRATEGY_HEALTH_SNAPSHOT_KEY, snapshot); + try { + await appendAuditLog(env, { + ts: new Date().toISOString(), + login: "strategy-health-sync", + action: "sync_strategy_health", + schema_version: snapshot.schema_version, + strategy_count: snapshot.summary.strategy_count, + data_status: snapshot.data_status, + }); + } catch { + // Snapshot delivery remains successful; audit data must never expose the payload. + } + return json({ + ok: true, + schema_version: snapshot.schema_version, + strategy_count: snapshot.summary.strategy_count, + generated_at: snapshot.generated_at, + }); +} + +async function strategyHealthResponse(request, env) { + const session = await readSession(request, env); + if (!session?.allowed) return json({ ok: false, error: "login required" }, 401); + if (!hasConfigStore(env)) return json(emptyStrategyHealthPayload("snapshot_unavailable")); + + let snapshot; + try { + const stored = await readConfigJson(env, STRATEGY_HEALTH_SNAPSHOT_KEY); + if (!stored) return json(emptyStrategyHealthPayload("snapshot_unavailable")); + snapshot = normalizeStrategyHealthSnapshot(stored, STRATEGY_HEALTH_SNAPSHOT_KEY); + } catch { + return json(emptyStrategyHealthPayload("snapshot_invalid")); + } + + const ttlSeconds = strategyHealthStaleTtlSeconds(env); + const freshnessTimestamps = [snapshot.generated_at, snapshot.computed_at] + .filter(Boolean) + .map((value) => Date.parse(value)); + const freshnessAt = freshnessTimestamps.length ? Math.min(...freshnessTimestamps) : Number.NaN; + const now = Date.now(); + const futureBeyondClockSkew = Number.isFinite(freshnessAt) && freshnessAt > now + 5 * 60 * 1000; + const ageSeconds = futureBeyondClockSkew + ? Number.POSITIVE_INFINITY + : Number.isFinite(freshnessAt) + ? Math.max(0, (now - freshnessAt) / 1000) + : Number.POSITIVE_INFINITY; + if (snapshot.data_status === "ready" && ageSeconds > ttlSeconds) { + snapshot.data_status = "stale"; + } + return json(snapshot); +} + +async function readBoundedJson(request, maxBytes) { + const declaredLength = Number(request.headers.get("Content-Length") || 0); + if (declaredLength > maxBytes) throw new HttpError("request body is too large", 413); + const bytes = await request.arrayBuffer(); + if (bytes.byteLength > maxBytes) throw new HttpError("request body is too large", 413); + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new HttpError("request body must be valid JSON", 400); + } +} + +function requireDedicatedStrategyHealthSyncToken(request, env) { + const expected = String(env.STRATEGY_HEALTH_SYNC_TOKEN || ""); + if (!expected) throw new HttpError("strategy health sync token is not configured", 500); + const header = request.headers.get("Authorization") || ""; + const token = header.match(/^Bearer\s+(.+)$/i)?.[1] || ""; + if (token !== expected) throw new HttpError("strategy health sync token is invalid", 401); +} + +function normalizeStrategyHealthSnapshot(payload, fieldName = "strategy health snapshot") { + if (!payload || Array.isArray(payload) || typeof payload !== "object") { + throw new Error(`${fieldName} must be an object`); + } + if (payload.schema_version !== "strategy_health_dashboard.v1") { + throw new Error(`${fieldName}.schema_version is unsupported`); + } + const status = cleanChoice(payload.data_status || "unavailable", STRATEGY_HEALTH_DATA_STATUSES, `${fieldName}.data_status`); + const strategies = status === "unavailable" + ? [] + : normalizeStrategyHealthStrategies(payload.strategies, fieldName); + const summary = normalizeStrategyHealthSummary(payload.summary, strategies); + const errors = normalizeStrategyHealthErrors(payload.errors); + return { + schema_version: "strategy_health_dashboard.v1", + generated_at: normalizeStrategyHealthTimestamp(payload.generated_at, `${fieldName}.generated_at`, true), + computed_at: normalizeStrategyHealthTimestamp(payload.computed_at, `${fieldName}.computed_at`, true), + data_status: status, + summary, + strategies, + policy: normalizeStrategyHealthPolicy(payload.policy), + errors, + }; +} + +function normalizeStrategyHealthStrategies(value, fieldName) { + if (!Array.isArray(value) || value.length > 100) { + throw new Error(`${fieldName}.strategies must be an array with at most 100 items`); + } + const seen = new Set(); + return value.map((item, index) => { + if (!item || Array.isArray(item) || typeof item !== "object") { + throw new Error(`${fieldName}.strategies[${index}] must be an object`); + } + const profile = cleanSlug(item.profile, `${fieldName}.strategies[${index}].profile`).toLowerCase(); + if (seen.has(profile)) throw new Error(`${fieldName}.strategies contains duplicate profile`); + seen.add(profile); + return { + profile, + domain: cleanChoice(item.domain, STRATEGY_HEALTH_DOMAINS, `${fieldName}.strategies[${index}].domain`), + as_of: sanitizeStrategyHealthText(item.as_of, `${fieldName}.strategies[${index}].as_of`, 64, true), + status: cleanChoice(item.status, STRATEGY_HEALTH_STATUSES, `${fieldName}.strategies[${index}].status`), + score: normalizeStrategyHealthScore(item.score, `${fieldName}.strategies[${index}].score`), + components: normalizeStrategyHealthComponents(item.components, `${fieldName}.strategies[${index}].components`), + decision: normalizeStrategyHealthDecision(item.decision, `${fieldName}.strategies[${index}].decision`), + review: normalizeStrategyHealthReview(item.review, `${fieldName}.strategies[${index}].review`), + freshness: normalizeStrategyHealthFreshness(item.freshness, `${fieldName}.strategies[${index}].freshness`), + source_revision: sanitizeStrategyHealthText(item.source_revision, `${fieldName}.strategies[${index}].source_revision`, 120, true), + }; + }); +} + +function normalizeStrategyHealthSummary(value, strategies) { + const counts = Object.fromEntries(STRATEGY_HEALTH_STATUSES.map((status) => [status, 0])); + for (const item of strategies) counts[item.status] += 1; + return { strategy_count: strategies.length, ...counts }; +} + +function normalizeStrategyHealthComponents(value, fieldName) { + const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; + return { + performance: normalizeStrategyHealthScore(source.performance, `${fieldName}.performance`), + risk: normalizeStrategyHealthScore(source.risk, `${fieldName}.risk`), + decay: normalizeStrategyHealthScore(source.decay, `${fieldName}.decay`), + stability: normalizeStrategyHealthScore(source.stability, `${fieldName}.stability`), + operations: normalizeStrategyHealthScore(source.operations, `${fieldName}.operations`), + }; +} + +function normalizeStrategyHealthDecision(value, fieldName) { + const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; + return { + code: cleanSlug(source.code || "evidence_missing", `${fieldName}.code`).toLowerCase(), + label: sanitizeStrategyHealthText(source.label, `${fieldName}.label`, 120, false, "证据不足,保持研究态"), + reason: sanitizeStrategyHealthText(source.reason, `${fieldName}.reason`, 240, false, "没有可用的机器检查结果。"), + }; +} + +function normalizeStrategyHealthReview(value, fieldName) { + const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; + return { + requested_stage: sanitizeStrategyHealthText(source.requested_stage, `${fieldName}.requested_stage`, 80, true), + evidence_package_id: sanitizeStrategyHealthText(source.evidence_package_id, `${fieldName}.evidence_package_id`, 120, true), + validation: normalizeStrategyHealthSummaryObject(source.validation), + risk: normalizeStrategyHealthSummaryObject(source.risk), + kelly_readiness: normalizeStrategyHealthSummaryObject(source.kelly_readiness), + }; +} + +function normalizeStrategyHealthSummaryObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const result = {}; + for (const [key, raw] of Object.entries(value).slice(0, 12)) { + if (!/^[A-Za-z0-9_.-]{1,48}$/.test(key)) continue; + if (/(token|secret|password|cookie|private|path|key)/i.test(key)) continue; + if (typeof raw === "boolean") result[key] = raw; + else if (typeof raw === "number" && Number.isFinite(raw)) result[key] = raw; + else if (typeof raw === "string") { + const safe = sanitizeStrategyHealthText(raw, "summary.value", 120, true); + if (safe !== null) result[key] = safe; + } + } + return result; +} + +function normalizeStrategyHealthFreshness(value, fieldName) { + const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; + return { + status: cleanChoice(source.status || "unknown", ["fresh", "stale", "unknown"], `${fieldName}.status`), + age_seconds: normalizeStrategyHealthAge(source.age_seconds, `${fieldName}.age_seconds`), + }; +} + +function normalizeStrategyHealthAge(value, fieldName) { + if (value === null || value === undefined || value === "") return null; + const number = Number(value); + if (!Number.isFinite(number) || number < 0 || number > 315360000) throw new Error(`${fieldName} is invalid`); + return Math.round(number); +} + +function normalizeStrategyHealthScore(value, fieldName) { + if (value === null || value === undefined || value === "") return null; + const number = Number(value); + if (!Number.isFinite(number) || number < 0 || number > 100) throw new Error(`${fieldName} must be between 0 and 100`); + return number; +} + +function normalizeStrategyHealthTimestamp(value, fieldName, nullable = false) { + if ((value === null || value === undefined || value === "") && nullable) return null; + const text = normalizeStrategyHealthText(value, fieldName, 64); + if (!/^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/.test(text) || Number.isNaN(Date.parse(text))) { + throw new Error(`${fieldName} must be an ISO timestamp`); + } + return text; +} + +function normalizeStrategyHealthText(value, fieldName, maxLength, nullable = false) { + if ((value === null || value === undefined || value === "") && nullable) return null; + const text = String(value || "").trim(); + if ( + !text || + text.length > maxLength || + /[<>\\]/.test(text) || + text.startsWith("/") || + /^[A-Za-z]:[\\/]/.test(text) + ) throw new Error(`${fieldName} is invalid`); + return text; +} + +function sanitizeStrategyHealthText(value, fieldName, maxLength, nullable = false, fallback = null) { + if (value === null || value === undefined || value === "") return nullable ? null : fallback; + let text; + try { + text = normalizeStrategyHealthText(value, fieldName, maxLength); + } catch { + return nullable ? null : fallback; + } + if ( + /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/i.test(text) || + /\b(?:token|secret|password|api[_ -]?key|private[_ -]?key|cookie)\s*[:=]\s*[^\s,;]{8,}/i.test(text) || + /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]{20,})\b/.test(text) || + /(?:^|[\s(])(?:\/Users\/|\/home\/|[A-Za-z]:[\\/])/.test(text) + ) return nullable ? null : fallback; + return text; +} + +function normalizeStrategyHealthErrors(value) { + if (!Array.isArray(value)) return []; + return uniqueStrings(value.slice(0, 20).filter((item) => /^[a-z][a-z0-9_.-]{0,63}$/.test(String(item || "")))); +} + +function normalizeStrategyHealthPolicy(value) { + const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; + const list = (raw) => Array.isArray(raw) + ? raw.slice(0, 20).map((item) => sanitizeStrategyHealthText(item, "policy.item", 120, true)).filter(Boolean) + : []; + return { + mode: sanitizeStrategyHealthText(source.mode, "policy.mode", 40, false, "read_only"), + automatic_stages: list(source.automatic_stages), + automatic_modes: list(source.automatic_modes), + human_gate_stages: list(source.human_gate_stages), + canary_requirements: list(source.canary_requirements), + human_actions: list(source.human_actions), + machine_checks: list(source.machine_checks), + notice: sanitizeStrategyHealthText(source.notice, "policy.notice", 240, false, "健康不等于已批准 live。"), + }; +} + +function emptyStrategyHealthPayload(errorCode) { + return { + schema_version: "strategy_health_dashboard.v1", + generated_at: null, + computed_at: null, + data_status: "unavailable", + summary: { strategy_count: 0, healthy: 0, watch: 0, review: 0, critical: 0 }, + strategies: [], + policy: normalizeStrategyHealthPolicy({}), + errors: [errorCode], + }; +} + +function strategyHealthStaleTtlSeconds(env) { + const configured = Number(env.STRATEGY_HEALTH_STALE_TTL_SECONDS); + if (!Number.isFinite(configured) || configured < 300 || configured > 604800) { + return STRATEGY_HEALTH_DEFAULT_STALE_TTL_SECONDS; + } + return Math.floor(configured); +} + async function syncStrategyProfilesConfig(env, session) { const profiles = normalizeStrategyProfilesPayload(DEFAULT_STRATEGY_PROFILES, "DEFAULT_STRATEGY_PROFILES"); if (!hasConfigStore(env)) return { synced: false, reason: "kv_not_bound", count: profiles.length }; @@ -2734,6 +3048,9 @@ export const __test = { fetchWithTimeout, syncDefaultStrategyProfiles: syncStrategyProfilesConfig, syncDefaultStrategyForAccount, + normalizeStrategyHealthSnapshot, + emptyStrategyHealthPayload, + makeSession, supportedDomainsForAccount, updateAccountOptionsDefaultStrategy, withTimeout, diff --git a/web/strategy-switch-console/wrangler.toml.example b/web/strategy-switch-console/wrangler.toml.example index fee6c62..527d3bb 100644 --- a/web/strategy-switch-console/wrangler.toml.example +++ b/web/strategy-switch-console/wrangler.toml.example @@ -9,6 +9,7 @@ workers_dev = true # - SESSION_SECRET # - RUNTIME_SETTINGS_DISPATCH_TOKEN # - STRATEGY_SWITCH_SYNC_TOKEN # optional; defaults to RUNTIME_SETTINGS_DISPATCH_TOKEN +# - STRATEGY_HEALTH_SYNC_TOKEN # dedicated outbound health snapshot token; never reuse dispatch token # - ALLOWED_GITHUB_LOGINS # - ALLOWED_GITHUB_ORGS # - STRATEGY_SWITCH_ADMIN_LOGINS