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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions python/tests/test_runtime_target_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def _snapshot(**overrides: object) -> dict[str, object]:


class RuntimeTargetLifecycleTest(unittest.TestCase):
def test_console_runtime_status_board_uses_only_the_read_only_central_api(self) -> None:
console = (ROOT / "web" / "strategy-switch-console" / "app.js").read_text(encoding="utf-8")
page = (ROOT / "web" / "strategy-switch-console" / "index.html").read_text(encoding="utf-8")

self.assertIn('id="runtime-target-lifecycle-list"', page)
self.assertIn('requestJson("/api/runtime-target-lifecycle")', console)
self.assertIn("function renderRuntimeTargetLifecycle()", console)
self.assertIn('runtimeTargetLifecycleNoOrder', console)

def test_disabled_target_remains_in_no_order_validation_lane(self) -> None:
snapshot = _snapshot()

Expand Down
18 changes: 17 additions & 1 deletion web/strategy-switch-console/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ python3 scripts/sync_strategy_switch_page_asset.py
`runtime-catalog-projection.json` 同样由 `platform-config.json` 生成,并用来源 SHA-256
防止已提交的目录资产陈旧。登录后的 `GET /api/runtime-catalog` 只返回这个**配置门禁**
投影;它不代表真实部署或交易状态。页面显示候选状态和目标执行状态时,必须分别使用
`/api/control-plane` 与 `/api/execution-evidence`,不得回退到历史 `lifecycle-matrix.json`。
`/api/control-plane`、`/api/execution-evidence` 与 `/api/runtime-target-lifecycle`,不得回退到历史
`lifecycle-matrix.json`。其中平台运行状态仅显示已接入来源;没有实际运行入口或未发布快照的平台必须
保持“待接入”,不能被界面推断为正常或已停用。

## 账号下拉配置

Expand Down Expand Up @@ -252,6 +254,20 @@ GET /api/control-plane

这个接口不是订单、paper、shadow 或 live API。`CONTROL_PLANE_SYNC_TOKEN` 必须与 OAuth、workflow dispatch、策略 root 和任何券商凭证完全分离。部署 workflow 会在 `runtime-strategy-switch` environment 提供该 secret 时,把它同步为 Worker secret;每个来源仓库需要保存同一值到其专用 GitHub Environment,URL 使用只读控制台的 `/api/internal/sync-control-plane-source`。

## 平台运行状态只读接口

已接入的券商/平台仓在自身运行监测完成后,使用与执行证据同样受限的专用同步凭据提交**脱敏状态**:

```text
POST /api/internal/sync-runtime-target-lifecycle-source
GET /api/runtime-target-lifecycle
```

来源契约为 `qsl_runtime_target_lifecycle_source_snapshot.v1`;每个目标固定为 `no_order=true`,只包含平台、
启用/停用状态、通道、运行监测、执行心跳及停车结论。Worker 按 `source_id` 保存并聚合,重复目标会被
拒绝而不是覆盖。读取接口要求登录 allowlist,来源超过默认新鲜度窗口即标为 `stale`。管理台把该信息放在
“待处理”页默认折叠的“平台运行状态”中,且只读:它不能启用目标、变更策略、访问账户或提交订单。

## M1 Shadow 建议只读接口

M1 把 P0 已生成的 `qsl.selection_decision.v1` 呈现给人工;它不创建策略、不会开始
Expand Down
213 changes: 213 additions & 0 deletions web/strategy-switch-console/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,31 @@
executionEvidenceDetail: "策略:{strategy} · 数据:{data} · 执行:{execution} · Shadow:{shadow} · Paper:{paper}",
executionEvidenceNoOrder: "固定边界:只读证据;不包含账户、订单、资金或 P6 实盘授权。",
executionEvidenceNext: "下一步",
runtimeTargetLifecycleBoard: "按需查看平台运行状态",
runtimeTargetLifecycleLoginNotice: "登录后读取私有平台运行状态;页面不会改变启用/停用或提交订单。",
runtimeTargetLifecycleStaleNotice: "平台运行状态已超过允许的新鲜度窗口;请先检查监测链路。",
runtimeTargetLifecycleUnavailableNotice: "还没有可用的平台运行状态;缺失数据不会被推断成已停用或正常。",
runtimeTargetLifecycleUpstreamNotice: "平台运行状态已加载,但有 {count} 个上游提示;请先核对。",
runtimeTargetLifecycleEmpty: "暂无可展示的平台运行状态。",
runtimeTargetLifecycleMeta: "{platform} · {state} · 通道:{mode}",
runtimeTargetLifecycleDetail: "运行监测:{guard} · 执行心跳:{heartbeat}",
runtimeTargetLifecycleNoOrder: "固定边界:只读状态;不会启用目标、变更策略或提交订单。",
runtimeTargetLifecycleNext: "当前处理",
runtimeTargetLifecycleStateEnabled: "已启用",
runtimeTargetLifecycleStateDisabled: "已停用",
runtimeTargetLifecycleCheckPass: "通过",
runtimeTargetLifecycleCheckAttention: "需处理",
runtimeTargetLifecycleCheckNotDue: "未到检查时间",
runtimeTargetLifecycleCheckNotApplicable: "不适用",
runtimeTargetLifecycleCheckUnavailable: "不可用",
runtimeTargetLifecycleDispositionEnabled: "持续监控",
runtimeTargetLifecycleDispositionDisabled: "无执行验证",
runtimeTargetLifecycleDispositionParked: "已暂停",
runtimeTargetLifecycleReasonNone: "监测正常",
runtimeTargetLifecycleReasonDisabled: "按配置停用,仍持续验证",
runtimeTargetLifecycleReasonRuntimeGuard: "运行监测需要复核",
runtimeTargetLifecycleReasonHeartbeat: "执行心跳需要复核",
runtimeTargetLifecycleReasonUnavailable: "监测数据不可用",
m0ResearchBoard: "按需查看 M0 研究观察",
m0ResearchLoginNotice: "登录后读取私有 M0 研究台账;缺失台账不会被补成研究结论。",
m0ResearchStaleNotice: "M0 研究观察已超过有效期,仅保留为历史上下文。",
Expand Down Expand Up @@ -1142,6 +1167,31 @@
executionEvidenceDetail: "strategy: {strategy} · data: {data} · execution: {execution} · shadow: {shadow} · paper: {paper}",
executionEvidenceNoOrder: "Fixed boundary: read-only evidence; no account, order, funds, or P6 live authority.",
executionEvidenceNext: "NEXT",
runtimeTargetLifecycleBoard: "View platform runtime status on demand",
runtimeTargetLifecycleLoginNotice: "Sign in to read private platform runtime status. This page cannot change target state or place orders.",
runtimeTargetLifecycleStaleNotice: "Platform runtime status is outside its freshness window. Check the monitoring path first.",
runtimeTargetLifecycleUnavailableNotice: "No usable platform runtime status is available. Missing data never implies a disabled or healthy target.",
runtimeTargetLifecycleUpstreamNotice: "Platform runtime status loaded with {count} upstream notice(s); review them first.",
runtimeTargetLifecycleEmpty: "No platform runtime status is available to display.",
runtimeTargetLifecycleMeta: "{platform} · {state} · lane: {mode}",
runtimeTargetLifecycleDetail: "runtime guard: {guard} · execution heartbeat: {heartbeat}",
runtimeTargetLifecycleNoOrder: "Fixed boundary: read-only status; it cannot enable a target, change a strategy, or place an order.",
runtimeTargetLifecycleNext: "CURRENT DISPOSITION",
runtimeTargetLifecycleStateEnabled: "enabled",
runtimeTargetLifecycleStateDisabled: "disabled",
runtimeTargetLifecycleCheckPass: "pass",
runtimeTargetLifecycleCheckAttention: "attention",
runtimeTargetLifecycleCheckNotDue: "not due",
runtimeTargetLifecycleCheckNotApplicable: "not applicable",
runtimeTargetLifecycleCheckUnavailable: "unavailable",
runtimeTargetLifecycleDispositionEnabled: "continue monitoring",
runtimeTargetLifecycleDispositionDisabled: "disabled validation",
runtimeTargetLifecycleDispositionParked: "parked",
runtimeTargetLifecycleReasonNone: "monitoring normal",
runtimeTargetLifecycleReasonDisabled: "intentionally disabled; validation continues",
runtimeTargetLifecycleReasonRuntimeGuard: "runtime guard needs review",
runtimeTargetLifecycleReasonHeartbeat: "execution heartbeat needs review",
runtimeTargetLifecycleReasonUnavailable: "monitoring data unavailable",
m0ResearchBoard: "View M0 research observations on demand",
m0ResearchLoginNotice: "Sign in to read the private M0 research ledger. A missing ledger never implies a research conclusion.",
m0ResearchStaleNotice: "These M0 research observations are beyond their validity window and remain historical context only.",
Expand Down Expand Up @@ -1473,6 +1523,16 @@
errors: [],
},
},
runtimeTargetLifecycle: {
payload: {
data_status: "unavailable",
computed_at: null,
summary: { target_count: 0, enabled: 0, disabled: 0, attention: 0 },
targets: [],
policy: { lifecycle_status_read_only: true, no_order: true },
errors: [],
},
},
researchTasks: {
payload: {
data_status: "unavailable",
Expand Down Expand Up @@ -4113,6 +4173,132 @@
}
}

function normalizeRuntimeTargetLifecyclePayload(payload) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
throw new Error("invalid runtime target lifecycle payload");
}
const targets = Array.isArray(payload.targets) ? payload.targets : [];
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 : {},
targets: targets.filter((entry) => entry && typeof entry === "object"
&& entry.target && typeof entry.target === "object"
&& entry.target.target && typeof entry.target.target === "object"
&& entry.target.monitoring && typeof entry.target.monitoring === "object"
&& entry.target.disposition && typeof entry.target.disposition === "object"),
policy: payload.policy && typeof payload.policy === "object" ? payload.policy : {},
errors: Array.isArray(payload.errors) ? payload.errors : [],
};
}

function runtimeTargetLifecycleCheckLabel(value) {
return {
pass: t("runtimeTargetLifecycleCheckPass"),
attention: t("runtimeTargetLifecycleCheckAttention"),
not_due: t("runtimeTargetLifecycleCheckNotDue"),
not_applicable: t("runtimeTargetLifecycleCheckNotApplicable"),
unavailable: t("runtimeTargetLifecycleCheckUnavailable"),
}[value] || t("runtimeTargetLifecycleCheckUnavailable");
}

function runtimeTargetLifecycleStateLabel(value) {
return value === "enabled"
? t("runtimeTargetLifecycleStateEnabled")
: t("runtimeTargetLifecycleStateDisabled");
}

function runtimeTargetLifecycleDispositionLabel(value) {
return {
continue_enabled_monitoring: t("runtimeTargetLifecycleDispositionEnabled"),
continue_disabled_validation: t("runtimeTargetLifecycleDispositionDisabled"),
parked: t("runtimeTargetLifecycleDispositionParked"),
}[value] || t("runtimeTargetLifecycleDispositionParked");
}

function runtimeTargetLifecycleReasonLabel(value) {
return {
none: t("runtimeTargetLifecycleReasonNone"),
target_intentionally_disabled: t("runtimeTargetLifecycleReasonDisabled"),
runtime_guard_attention: t("runtimeTargetLifecycleReasonRuntimeGuard"),
execution_heartbeat_attention: t("runtimeTargetLifecycleReasonHeartbeat"),
monitoring_unavailable: t("runtimeTargetLifecycleReasonUnavailable"),
}[value] || t("runtimeTargetLifecycleReasonUnavailable");
}

function renderRuntimeTargetLifecycle() {
const payload = state.runtimeTargetLifecycle.payload;
const notice = el("runtime-target-lifecycle-notice");
if (!state.auth.allowed) {
notice.textContent = t("runtimeTargetLifecycleLoginNotice");
} else if (payload.data_status === "stale") {
notice.textContent = t("runtimeTargetLifecycleStaleNotice");
} else if (payload.data_status !== "ready") {
notice.textContent = t("runtimeTargetLifecycleUnavailableNotice");
} else if (payload.errors?.length) {
notice.textContent = t("runtimeTargetLifecycleUpstreamNotice").replace("{count}", String(payload.errors.length));
} else {
notice.textContent = payload.policy?.notice || t("runtimeTargetLifecycleNoOrder");
}

const list = el("runtime-target-lifecycle-list");
list.replaceChildren();
if (!payload.targets.length) {
const empty = document.createElement("div");
empty.className = "health-card__empty";
empty.textContent = t("runtimeTargetLifecycleEmpty");
list.appendChild(empty);
return;
}

const targets = [...payload.targets].sort((left, right) => {
const leftParked = left.target?.disposition?.code === "parked" ? 0 : 1;
const rightParked = right.target?.disposition?.code === "parked" ? 0 : 1;
if (leftParked !== rightParked) return leftParked - rightParked;
return String(left.target?.target_id || "").localeCompare(String(right.target?.target_id || ""));
});
for (const entry of targets) {
const target = entry.target || {};
const configuration = target.target || {};
const monitoring = target.monitoring || {};
const disposition = target.disposition || {};
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 = t("runtimeTargetLifecycleMeta")
.replace("{platform}", configuration.platform || "unknown")
.replace("{state}", runtimeTargetLifecycleStateLabel(configuration.configured_state))
.replace("{mode}", configuration.execution_mode || "unknown");
const title = document.createElement("h4");
title.className = "health-card__title";
title.textContent = String(target.target_id || "unknown");
const reason = document.createElement("p");
reason.className = "health-card__reason";
reason.textContent = runtimeTargetLifecycleReasonLabel(disposition.reason_code);
const detail = document.createElement("div");
detail.className = "health-card__meta";
detail.textContent = t("runtimeTargetLifecycleDetail")
.replace("{guard}", runtimeTargetLifecycleCheckLabel(monitoring.runtime_guard))
.replace("{heartbeat}", runtimeTargetLifecycleCheckLabel(monitoring.execution_heartbeat));
main.append(meta, title, reason, detail);

const stateBlock = document.createElement("div");
stateBlock.className = "health-card__score";
const label = document.createElement("small");
label.textContent = t("runtimeTargetLifecycleNext");
const current = document.createElement("strong");
current.textContent = runtimeTargetLifecycleDispositionLabel(disposition.code);
const freshness = document.createElement("small");
freshness.textContent = entry.freshness?.data_status || "unknown";
stateBlock.append(label, current, freshness);
card.append(main, stateBlock);
list.appendChild(card);
}
}

function normalizeResearchTaskPayload(payload) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid research task payload");
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
Expand Down Expand Up @@ -4305,6 +4491,7 @@
renderM0Research();
renderAdaptiveSelection();
renderExecutionEvidence();
renderRuntimeTargetLifecycle();
renderResearchTasks();
renderHealth();
renderPlatforms();
Expand Down Expand Up @@ -4335,6 +4522,7 @@
await refreshM0Research();
await refreshAdaptiveSelection();
await refreshExecutionEvidence();
await refreshRuntimeTargetLifecycle();
await refreshResearchTasks();
await refreshHealth();
await refreshConfig();
Expand Down Expand Up @@ -4503,6 +4691,28 @@
renderExecutionEvidence();
}

async function refreshRuntimeTargetLifecycle() {
if (!state.auth.allowed) {
renderRuntimeTargetLifecycle();
return;
}
try {
state.runtimeTargetLifecycle.payload = normalizeRuntimeTargetLifecyclePayload(
await requestJson("/api/runtime-target-lifecycle"),
);
} catch {
state.runtimeTargetLifecycle.payload = {
data_status: "unavailable",
computed_at: null,
summary: { target_count: 0, enabled: 0, disabled: 0, attention: 0 },
targets: [],
policy: { lifecycle_status_read_only: true, no_order: true },
errors: ["runtime_target_lifecycle_request_failed"],
};
}
renderRuntimeTargetLifecycle();
}

async function refreshResearchTasks() {
if (!state.auth.allowed) {
renderResearchTasks();
Expand Down Expand Up @@ -4716,6 +4926,9 @@
refreshControlPlane();
refreshOwnerDecisions();
refreshM0Research();
refreshAdaptiveSelection();
refreshExecutionEvidence();
refreshRuntimeTargetLifecycle();
refreshResearchTasks();
}
if (state.view === "health") refreshHealth();
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/app_js.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions web/strategy-switch-console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ <h2 id="control-plane-view-title" data-i18n="controlPlaneTitle">只处理需要
<div id="execution-evidence-notice" class="health-notice" role="status" aria-live="polite">登录后读取已验证执行证据;页面不会推断 paper 或 live 状态。</div>
<div class="health-list" id="execution-evidence-list"></div>
</details>
<details class="control-disclosure">
<summary data-i18n="runtimeTargetLifecycleBoard">按需查看平台运行状态</summary>
<div id="runtime-target-lifecycle-notice" class="health-notice" role="status" aria-live="polite">登录后读取已验证的平台运行状态;页面不会启用目标或提交订单。</div>
<div class="health-list" id="runtime-target-lifecycle-list"></div>
</details>
<details class="control-disclosure">
<summary data-i18n="researchTaskBoard">按需查看研究任务队列</summary>
<div id="research-task-notice" class="health-notice" role="status" aria-live="polite">登录后读取已验证研究任务;不会运行或调参。</div>
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/page_asset.js

Large diffs are not rendered by default.