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
8 changes: 8 additions & 0 deletions docs/console_information_design.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@
## 交互与验证

验证默认总览、准确定位账户、配置/实际状态分离、搜索和筛选、空结果、读取失败、刷新时间、研究切页、折叠详情、语言切换以及电脑/手机布局。线上仅检查读取和导航;提交类操作在隔离测试数据中验证,不能为了页面测试触发实盘。

## KV 用量与心跳审计

运行目标心跳每次仍保存完整最新快照,包括来源时间和实际读回时间;不通过停止刷新或延长有效期节省额度。只有时间更新、其他归一化状态不变时,不再向可选的滚动审计列表追加重复同步记录。首次同步、启停或策略变化、运行异常及恢复、过期读回恢复、调度读回变化仍追加审计。人工操作和其他类型的审计不受此规则影响。

这使状态不变的单次心跳同步从两次 KV 写入降为一次。以十个目标每小时各同步一次估算,这一路径每天从 480 次降为 240 次写入;这不是整个 Cloudflare 账户的实际用量。实际告警类别及总量须查看账户统计,不能从测试计数推断。

免费 KV 每日读取额度为 100,000 次,写入、删除、列表请求各为 1,000 次,北京时间 08:00 重置。优先核对触发类别并消除重复操作;套餐升级不属于代码优化的隐含步骤。参见 [Cloudflare KV 定价与额度](https://developers.cloudflare.com/kv/platform/pricing/)。
79 changes: 79 additions & 0 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3290,6 +3290,85 @@ assert.equal(runtimeTargetLifecyclePayload.policy.no_order, true);
assert.equal(runtimeTargetLifecyclePayload.policy.execution_observation_read_only, true);
assert.equal(runtimeTargetLifecyclePayload.policy.order_or_fill_evidence, "not_collected");

// Routine heartbeats must refresh their evidence time without rewriting audit_log.
const lifecycleUsageStore = new Map();
const existingOperatorAudit = { action: "manual_strategy_switch", login: "health-user", ts: controlNow };
lifecycleUsageStore.set("audit_log", JSON.stringify([existingOperatorAudit]));
const lifecycleWrites = [];
const lifecycleUsageEnv = {
...executionEvidenceEnv,
STRATEGY_SWITCH_CONFIG: {
...controlKv,
async get(key) { return lifecycleUsageStore.get(key) || null; },
async put(key, value) { lifecycleWrites.push(key); lifecycleUsageStore.set(key, value); },
},
};
async function publishCountedLifecycle(source) {
lifecycleWrites.length = 0;
const response = await worker.fetch(new Request("https://switch.example/api/internal/sync-runtime-target-lifecycle-source", {
method: "POST",
headers: { Authorization: `Bearer ${executionEvidenceSyncValue}`, "Content-Type": "application/json" },
body: JSON.stringify(source),
}), lifecycleUsageEnv);
assert.equal(response.status, 200);
}
const usageSourceKey = "runtime_target_lifecycle_source:longbridge.sg";
const usageSource = structuredClone(runtimeTargetLifecycleSourcePayload);
usageSource.generated_at = new Date(Date.now() - 120000).toISOString();
usageSource.computed_at = usageSource.generated_at;
usageSource.targets[0].deployment = {
runtime_enabled: false, scheduler_state: "paused", strategy_profile: "soxl_soxx_trend_income",
execution_mode: "dry_run", observed_at: usageSource.generated_at,
};
await publishCountedLifecycle(usageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
const firstLifecycleAudit = lifecycleUsageStore.get("audit_log");
assert.deepEqual(JSON.parse(firstLifecycleAudit).at(-1), existingOperatorAudit);
const refreshedUsageSource = structuredClone(usageSource);
refreshedUsageSource.generated_at = new Date(Date.now() - 60000).toISOString();
refreshedUsageSource.computed_at = refreshedUsageSource.generated_at;
refreshedUsageSource.targets[0].deployment.observed_at = refreshedUsageSource.generated_at;
await publishCountedLifecycle(refreshedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey]);
assert.equal(lifecycleUsageStore.get("audit_log"), firstLifecycleAudit);
const storedRefreshedUsage = JSON.parse(lifecycleUsageStore.get(usageSourceKey));
assert.equal(storedRefreshedUsage.generated_at, refreshedUsageSource.generated_at);
assert.equal(storedRefreshedUsage.targets[0].deployment.observed_at, refreshedUsageSource.generated_at);

const changedUsageSource = structuredClone(refreshedUsageSource);
changedUsageSource.targets[0].monitoring.runtime_guard = "attention";
changedUsageSource.targets[0].disposition = { code: "parked", reason_code: "runtime_guard_attention" };
await publishCountedLifecycle(changedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
await publishCountedLifecycle(refreshedUsageSource); // Recovery is also a state change.
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
const changedDeploymentSource = structuredClone(refreshedUsageSource);
changedDeploymentSource.targets[0].deployment.scheduler_state = "unknown";
await publishCountedLifecycle(changedDeploymentSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
lifecycleUsageStore.set(usageSourceKey, "invalid previous snapshot");
await publishCountedLifecycle(refreshedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
const workingLifecycleGet = lifecycleUsageEnv.STRATEGY_SWITCH_CONFIG.get;
lifecycleUsageEnv.STRATEGY_SWITCH_CONFIG.get = async (key) => {
if (key === usageSourceKey) throw new Error("synthetic previous snapshot read failure");
return workingLifecycleGet(key);
};
await publishCountedLifecycle(refreshedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
lifecycleUsageEnv.STRATEGY_SWITCH_CONFIG.get = workingLifecycleGet;
const staleUsageSource = structuredClone(refreshedUsageSource);
staleUsageSource.computed_at = new Date(Date.now() - 48 * 3600000).toISOString();
staleUsageSource.generated_at = staleUsageSource.computed_at;
lifecycleUsageStore.set(usageSourceKey, JSON.stringify(staleUsageSource));
await publishCountedLifecycle(refreshedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);
const staleDeploymentSource = structuredClone(refreshedUsageSource);
staleDeploymentSource.targets[0].deployment.observed_at = staleUsageSource.computed_at;
lifecycleUsageStore.set(usageSourceKey, JSON.stringify(staleDeploymentSource));
await publishCountedLifecycle(refreshedUsageSource);
assert.deepEqual(lifecycleWrites, [usageSourceKey, "audit_log"]);

const researchTaskSyncValue = ["research", "task", "sync"].join("-");
const researchTaskEnv = { ...controlEnv, RESEARCH_TASK_SYNC_TOKEN: researchTaskSyncValue };
const researchTaskCookie = await __test.makeSession("health-user", [], researchTaskEnv);
Expand Down
49 changes: 39 additions & 10 deletions web/strategy-switch-console/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3147,6 +3147,22 @@ function executionEvidenceSourceKey(sourceId) {
return `${EXECUTION_EVIDENCE_SOURCE_PREFIX}${sourceId}`;
}

function runtimeTargetLifecycleAuditState(source, ttlSeconds, now) {
if (!source) return null;
const state = structuredClone(source);
state.freshness = controlPlaneSnapshotFreshness(source, ttlSeconds, now).data_status;
delete state.generated_at;
delete state.computed_at;
for (const target of state.targets) {
if (target.deployment?.observed_at) {
target.deployment.observed_at = controlPlaneSnapshotFreshness({
data_status: "ready", computed_at: target.deployment.observed_at,
}, ttlSeconds, now).data_status;
}
}
return JSON.stringify(state);
}

async function syncRuntimeTargetLifecycleSourceResponse(request, env) {
// This publisher has the same narrow scope as execution evidence: sanitized
// platform status only, never credentials, accounts, orders, or commands.
Expand All @@ -3166,17 +3182,30 @@ async function syncRuntimeTargetLifecycleSourceResponse(request, env) {
} catch (error) {
return json({ ok: false, error: error.message || "invalid runtime target lifecycle payload" }, 400);
}
await writeConfigJson(env, runtimeTargetLifecycleSourceKey(source.source_id), source);
const sourceKey = runtimeTargetLifecycleSourceKey(source.source_id);
let previous = null;
try {
await appendAuditLog(env, {
ts: new Date().toISOString(),
login: "runtime-target-lifecycle-source-sync",
action: "sync_runtime_target_lifecycle_source",
source_id: source.source_id,
schema_version: source.schema_version,
target_count: source.targets.length,
data_status: source.data_status,
});
previous = normalizeRuntimeTargetLifecycleSourceSnapshot(await readConfigJson(env, sourceKey));
} catch {
// Missing/unreadable history must not prevent a fresh, valid observation.
}
// Always retain the new evidence timestamps. Only the optional rolling audit
// skips unchanged polling records; operator actions are audited separately.
await writeConfigJson(env, sourceKey, source);
try {
const ttlSeconds = executionEvidenceStaleTtlSeconds(env);
const now = Date.now();
if (runtimeTargetLifecycleAuditState(previous, ttlSeconds, now) !== runtimeTargetLifecycleAuditState(source, ttlSeconds, now)) {
await appendAuditLog(env, {
ts: new Date().toISOString(),
login: "runtime-target-lifecycle-source-sync",
action: "sync_runtime_target_lifecycle_source",
source_id: source.source_id,
schema_version: source.schema_version,
target_count: source.targets.length,
data_status: source.data_status,
});
}
} catch {
// A valid no-order snapshot remains useful when convenience audit retention fails.
}
Expand Down