feat(control): control center — kill switch, advisory, ODP, audit + Operate-surface design - #73
Conversation
…, and ODP state Wire the already-built control-plane endpoints (GET/POST /control/kill-switch, GET /control/advisory-report, GET /control/odp-state) into a single operator panel at /control: - Kill switch: toggle + effective-source (runtime override vs config default) - Advisory report: totals, recovery-rate, per (state, action_type) buckets, mode breakdown — the gate data for flipping control_mode to automatic - ODP data plane: ingest/stream/DLQ/store/outbox health, per-section degrade Adds useKillSwitch/useSetKillSwitch/useAdvisoryReport/useOdpState hooks, navigation registration, and regression-contract assertions.
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a client-side Control Center with kill-switch management, advisory reports, ODP health panels, a paginated audit ledger, API hooks, navigation wiring, and regression coverage. ChangesControl Center
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ControlCenterPage
participant ReactQueryHooks
participant ControlAPI
participant ConfirmationDialog
ControlCenterPage->>ReactQueryHooks: fetch kill-switch, advisory, and ODP state
ReactQueryHooks->>ControlAPI: issue configured queries
ControlAPI-->>ControlCenterPage: return control-center data
ControlCenterPage->>ConfirmationDialog: request enable confirmation
ConfirmationDialog-->>ControlCenterPage: confirm action
ControlCenterPage->>ReactQueryHooks: submit kill-switch update
ReactQueryHooks->>ControlAPI: update engagement state
ControlAPI-->>ReactQueryHooks: return mutation result
ReactQueryHooks-->>ControlCenterPage: invalidate kill-switch query
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Health of changed files: 4.3 → 6.4 (+2.0) 📋 At a glance ✅ Health gate: passed 📌 Before you merge
🔎 More signals (4)🗺️ Change map flowchart LR
subgraph PR ["Changed in this PR (3 with dependents)"]
f_frontend_lib_api_hooks_ts[".../api/hooks.ts 🔥"]:::changed
f_frontend_lib_navigation_ts["frontend/lib/navigation.ts 🔥"]:::changed
f_frontend_scripts_check_control_plane_regressions_mjs["frontend/scripts/check-control-plane-regressions.mjs 🔥"]:::changed
end
f_backend_api_v1_workflows_py[".../v1/workflows.py"]
f_frontend_lib_api_hooks_ts --> f_backend_api_v1_workflows_py
f_backend_models___init___py["backend/models/__init__.py"]
f_frontend_lib_api_hooks_ts --> f_backend_models___init___py
f_backend_schemas_workflow_py["backend/schemas/workflow.py"]
f_frontend_lib_api_hooks_ts --> f_backend_schemas_workflow_py
f_backend_workflow_capability_projection_py["backend/workflow/capability_projection.py"]
f_frontend_lib_api_hooks_ts --> f_backend_workflow_capability_projection_py
f_frontend_lib_navigation_ts --> f_backend_api_v1_workflows_py
f_frontend_lib_navigation_ts --> f_backend_models___init___py
f_frontend_lib_navigation_ts --> f_backend_schemas_workflow_py
f_frontend_lib_navigation_ts --> f_backend_workflow_capability_projection_py
f_frontend_app__app__dashboard_page_tsx[".../dashboard/page.tsx"]
f_frontend_scripts_check_control_plane_regressions_mjs --> f_frontend_app__app__dashboard_page_tsx
more(["+74 more dependents"])
PR --> more
w_frontend_lib_api_types_ts(["⚠️ .../api/types.ts changed together 12×, not in PR"]):::warn
f_frontend_lib_api_hooks_ts -.- w_frontend_lib_api_types_ts
w_frontend_lib_api_endpoints_ts(["⚠️ .../api/endpoints.ts changed together 11×, not in PR"]):::warn
f_frontend_lib_api_hooks_ts -.- w_frontend_lib_api_endpoints_ts
classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Solid arrows: code that imports the changed files (79 direct dependents, from the last indexed snapshot). Dashed: history/tests. 🔥 Hotspots touched (3)
🔗 Hidden coupling (1 file)
💀 Dead code (10 findings)
7 more
👀 Suggested reviewers @2233admin 📊 See the full report for this PR |
…n, auto-refresh Round out the control plane panel into an operator-grade surface: - Audit ledger (4th panel): paginated control_actions table with action/state/mode/executed/outcome/reason/time + manual refresh - Kill-switch engage now requires a confirmation dialog (dangerous global short-circuit); disengage stays one-click - Auto-refresh: kill-switch 30s, ODP 15s, advisory 60s (hooks accept refetchInterval option) - Humanized numbers: ms->s/min for idle lag, thousands separators - Per-section degrade notes (store/outbox) surface backend hints - Regression contract updated for the new interaction surface
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/scripts/check-control-plane-regressions.mjs (1)
164-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the advisory polling interval.
The page configures advisory polling with
refetchInterval: 60_000, but this test only asserts the 30-second and 15-second intervals. Add the 60-second assertion so removal of advisory refresh fails the regression test.Proposed test update
assert.match(page, /refetchInterval: 30_000/) +assert.match(page, /refetchInterval: 60_000/) assert.match(page, /refetchInterval: 15_000/)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/scripts/check-control-plane-regressions.mjs` around lines 164 - 175, Update the assertions in the regression test to also match the advisory polling interval `refetchInterval: 60_000`, alongside the existing 30-second and 15-second interval checks, so removal of advisory refresh is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/app/`(app)/control/page.tsx:
- Around line 319-329: Update the kill-switch mutation flow around
handleKillToggle and confirmEngage to render setKill.error in the kill-switch
panel or confirmation dialog when either update fails. Move confirmation-dialog
closure out of confirmEngage and into the mutation’s onSuccess callback,
preserving the dialog when the request is rejected so the operator can see the
failure.
---
Nitpick comments:
In `@frontend/scripts/check-control-plane-regressions.mjs`:
- Around line 164-175: Update the assertions in the regression test to also
match the advisory polling interval `refetchInterval: 60_000`, alongside the
existing 30-second and 15-second interval checks, so removal of advisory refresh
is detected.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 69b140ff-7cca-4082-93e2-9440e01a36a2
📒 Files selected for processing (4)
frontend/app/(app)/control/page.tsxfrontend/lib/api/hooks.tsfrontend/lib/navigation.tsfrontend/scripts/check-control-plane-regressions.mjs
Surface-first redesign (claude-design doctrine): - Status strip (Monitor): 4 glanceable cells — kill state, automation gate, ODP availability, ledger volume; no card chrome - Kill-switch cockpit (Operate): raised bg-ops-panel dark surface with large destructive/secondary action button instead of a buried mini-switch; engage warning banner, source-of-truth mono readout - ODP data plane: compact 5-cell grid with per-section degrade reasons and availability count (x/5) - Advisory report: gate-eligibility badge column (mostly-recovered => do not automate, mostly-persisted => eligible) + inline totals row - Audit ledger stays compact Command/Inspect with pagination Kept: engage confirmation dialog, auto-refresh intervals, formatMs/ formatNum humanization. All existing regression assertions still pass.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/app/`(app)/control/page.tsx:
- Around line 447-452: Update the kill-state StripCell using kill.data so
loading or failed queries with undefined data display an unknown value and muted
indicator instead of “未熔断” and green. Preserve the existing engaged/not-engaged
labels and bad/good tones when kill.data is available.
- Around line 257-260: Update gateEligible to require persisted to exceed half
of bucket.total in addition to the existing recovery_rate threshold and
persisted > 0 check. Apply the same majority-persisted eligibility condition to
the related eligibility logic around the alternate referenced section, or reuse
the backend eligibility decision if that flow already exposes it.
- Around line 453-457: Update the automation-status StripCell call to pass
dot={true} alongside its existing dotTone, ensuring the indicator renders while
preserving the current label, value, and tone logic.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4eca6d3b-d214-4ec1-89a4-e8291bd91aa1
📒 Files selected for processing (1)
frontend/app/(app)/control/page.tsx
| function gateEligible(bucket: AdvisoryReport['buckets'][number]): boolean { | ||
| if (bucket.recovery_rate == null) return false | ||
| return bucket.recovery_rate < 0.8 && bucket.persisted > 0 | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Require a majority-persisted bucket before showing eligibility.
Line 259 labels a bucket as eligible when recovery_rate is 0.79 and persisted is 1. That bucket is still mostly recovered, but the page description says mostly recovered buckets must not be automated.
Require persisted to exceed half of total, or consume the same eligibility decision from the backend.
Proposed fix
function gateEligible(bucket: AdvisoryReport['buckets'][number]): boolean {
- if (bucket.recovery_rate == null) return false
- return bucket.recovery_rate < 0.8 && bucket.persisted > 0
+ return bucket.total > 0 && bucket.persisted > bucket.total / 2
}Also applies to: 558-566
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/app/`(app)/control/page.tsx around lines 257 - 260, Update
gateEligible to require persisted to exceed half of bucket.total in addition to
the existing recovery_rate threshold and persisted > 0 check. Apply the same
majority-persisted eligibility condition to the related eligibility logic around
the alternate referenced section, or reuse the backend eligibility decision if
that flow already exposes it.
| <StripCell | ||
| label="执行熔断开关" | ||
| value={kill.data?.engaged ? '已熔断' : '未熔断'} | ||
| dot | ||
| dotTone={kill.data?.engaged ? 'bad' : 'good'} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not report an unknown kill state as disabled.
While the query is loading or fails, kill.data is undefined. This expression then displays 未熔断 with a green indicator. Show an unknown value with a muted indicator until the query returns data.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/app/`(app)/control/page.tsx around lines 447 - 452, Update the
kill-state StripCell using kill.data so loading or failed queries with undefined
data display an unknown value and muted indicator instead of “未熔断” and green.
Preserve the existing engaged/not-engaged labels and bad/good tones when
kill.data is available.
| <StripCell | ||
| label="自动化门禁" | ||
| value={qualifiedBuckets == null ? '—' : `${qualifiedBuckets} 类可自动化`} | ||
| dotTone={qualifiedBuckets && qualifiedBuckets > 0 ? 'warn' : 'muted'} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Render the automation status indicator.
StripCell renders its dot only when dot is true. This call provides dotTone but omits dot, so the automation status has no indicator.
Proposed fix
<StripCell
label="自动化门禁"
value={qualifiedBuckets == null ? '—' : `${qualifiedBuckets} 类可自动化`}
+ dot
dotTone={qualifiedBuckets && qualifiedBuckets > 0 ? 'warn' : 'muted'}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <StripCell | |
| label="自动化门禁" | |
| value={qualifiedBuckets == null ? '—' : `${qualifiedBuckets} 类可自动化`} | |
| dotTone={qualifiedBuckets && qualifiedBuckets > 0 ? 'warn' : 'muted'} | |
| /> | |
| <StripCell | |
| label="自动化门禁" | |
| value={qualifiedBuckets == null ? '—' : `${qualifiedBuckets} 类可自动化`} | |
| dot | |
| dotTone={qualifiedBuckets && qualifiedBuckets > 0 ? 'warn' : 'muted'} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/app/`(app)/control/page.tsx around lines 453 - 457, Update the
automation-status StripCell call to pass dot={true} alongside its existing
dotTone, ensuring the indicator renders while preserving the current label,
value, and tone logic.
做什么
把已建好但零 UI 的三个 control-plane 端点接成一个操作面板(/control 控制中心),并做了一轮全量优化:
/control/kill-switch/control/advisory-report/control/odp-state/control/actions对应 WIRING_GAP_LEDGER W3(control-plane 操作面板 UI——后端+client 早已就绪,差最后一公里 UI)。
全量优化点
改动
frontend/app/(app)/control/page.tsx(新,20KB):四面板页面frontend/lib/api/hooks.ts:+4 hooks(useKillSwitch / useSetKillSwitch / useAdvisoryReport / useOdpState),支持 refetchIntervalfrontend/lib/navigation.ts:sidebar match + ROUTE_LABELS 注册 /controlfrontend/scripts/check-control-plane-regressions.mjs:回归契约断言验证(真实数据链路已通)
pnpm exec tsc --noEmitexit 0pnpm exec eslint(4 文件)0 error 0 warningnpm run check:control-plane:新增 test 通过(1 个既有失败studio node selector为上游遗留,stash 验证与本次改动无关)GET /control→ 200;带 token 实测 4 端点全部返回真实数据(kill-switch 未熔断 / advisory 0 台账 / odp 区块降级 / actions 分页 meta)备注
kill-switch 为有副作用的全局熔断,UI 上开启强制确认;advisory 报告只读不执行(后端每次读取自带懒评估)。本地开发用
NEXT_PUBLIC_API_AUTH_TOKEN(gitignored .env.local)或浏览器 localStorageapiAuthToken注入 token。