Recover the next verified Phase 4 improvements without waiting on team teardown
The team run reached a quiescent state with no in-progress work but still had pending bookkeeping tasks, so the next safe step was to pull only the newly verified commits into main. This batch adds a frontend contract-view audit guard and the reusable contract cue UI so degradation and data-quality states are visible where the contract-first payload already exposes them. Constraint: The team snapshot still has pending bookkeeping tasks, so do not treat it as terminal cleanup-ready Rejected: Wait for terminal team shutdown before any further recovery | delays low-risk verified changes even though no workers are actively modifying code Rejected: Pull the entire worker-3 checkpoint verbatim | unnecessary risk of reintroducing snapshot-only churn when only the frontend files are needed Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep frontend contract cue rendering centralized; avoid reintroducing page-specific ad-hoc degradation badges Tested: python -m pytest web_dashboard/backend/tests/test_frontend_contract_view_audit.py web_dashboard/backend/tests/test_api_smoke.py web_dashboard/backend/tests/test_services_migration.py -q Tested: npm run build (web_dashboard/frontend) Not-tested: manual browser interaction with the new ContractCues component Not-tested: final OMX team terminal shutdown path
This commit is contained in:
parent
11cbb7ce85
commit
a245915f4e
|
|
@ -0,0 +1,34 @@
|
|||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
FRONTEND_SRC = Path(__file__).resolve().parents[2] / "frontend" / "src"
|
||||
CONTRACT_VIEW = FRONTEND_SRC / "utils" / "contractView.js"
|
||||
LEGACY_TOP_LEVEL_FIELDS = ("decision", "confidence", "quant_signal", "llm_signal")
|
||||
DIRECT_FIELD_ACCESS = re.compile(r"(?:\?|)\.\s*(decision|confidence|quant_signal|llm_signal)\b")
|
||||
|
||||
|
||||
def test_contract_view_reads_contract_result_before_compat_fields():
|
||||
source = CONTRACT_VIEW.read_text()
|
||||
|
||||
assert "getResult(payload).decision ?? getCompat(payload).decision" in source
|
||||
assert "getResult(payload).confidence ?? getCompat(payload).confidence" in source
|
||||
assert "getResult(payload).signals?.quant?.rating ?? getCompat(payload).quant_signal" in source
|
||||
assert "getResult(payload).signals?.llm?.rating ?? getCompat(payload).llm_signal" in source
|
||||
|
||||
|
||||
def test_frontend_consumers_use_contract_view_helpers_for_signal_fields():
|
||||
offenders: list[str] = []
|
||||
|
||||
for path in sorted(FRONTEND_SRC.rglob("*.js")) + sorted(FRONTEND_SRC.rglob("*.jsx")):
|
||||
if path == CONTRACT_VIEW:
|
||||
continue
|
||||
matches = {
|
||||
match.group(1)
|
||||
for match in DIRECT_FIELD_ACCESS.finditer(path.read_text())
|
||||
if match.group(1) in LEGACY_TOP_LEVEL_FIELDS
|
||||
}
|
||||
if matches:
|
||||
offenders.append(f"{path.relative_to(FRONTEND_SRC)} -> {sorted(matches)}")
|
||||
|
||||
assert offenders == []
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
getDataQualitySummary,
|
||||
getDegradationSummary,
|
||||
isDegradedPayload,
|
||||
} from '../utils/contractView'
|
||||
|
||||
const cueStyle = {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--radius-pill)',
|
||||
background: 'var(--hold-dim)',
|
||||
color: 'var(--hold)',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.4,
|
||||
}
|
||||
|
||||
function formatCode(code) {
|
||||
return String(code).replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
export default function ContractCues({ payload, style = null }) {
|
||||
const dataQuality = getDataQualitySummary(payload)
|
||||
const degradation = getDegradationSummary(payload)
|
||||
const primaryReason = degradation?.reason_codes?.[0] || null
|
||||
const dataQualityState = dataQuality?.state || null
|
||||
const items = []
|
||||
|
||||
if (isDegradedPayload(payload)) {
|
||||
items.push(primaryReason && primaryReason !== dataQualityState
|
||||
? `降级 · ${formatCode(primaryReason)}`
|
||||
: '降级结果')
|
||||
}
|
||||
|
||||
if (dataQualityState) {
|
||||
items.push(`数据 · ${formatCode(dataQualityState)}`)
|
||||
}
|
||||
|
||||
if (items.length === 0) return null
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', ...style }}>
|
||||
{items.map((item) => (
|
||||
<span key={item} style={cueStyle}>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,11 +2,10 @@ import { useState, useEffect, useRef, useCallback } from 'react'
|
|||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Card, Progress, Badge, Empty, Button, Result, message } from 'antd'
|
||||
import DecisionBadge from '../components/DecisionBadge'
|
||||
import ContractCues from '../components/ContractCues'
|
||||
import { StatusIcon } from '../components/StatusIcon'
|
||||
import {
|
||||
getConfidence,
|
||||
getDataQualitySummary,
|
||||
getDegradationSummary,
|
||||
getDecision,
|
||||
getDisplayDate,
|
||||
getErrorMessage,
|
||||
|
|
@ -36,8 +35,6 @@ export default function AnalysisMonitor() {
|
|||
const quantSignal = getQuantSignal(task)
|
||||
const confidence = getConfidence(task)
|
||||
const displayDate = getDisplayDate(task)
|
||||
const dataQuality = getDataQualitySummary(task)
|
||||
const degradation = getDegradationSummary(task)
|
||||
const errorMessage = getErrorMessage(task)
|
||||
|
||||
const fetchInitialState = useCallback(async () => {
|
||||
|
|
@ -196,16 +193,7 @@ export default function AnalysisMonitor() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{dataQuality?.state && (
|
||||
<div style={{ marginBottom: 12, fontSize: 'var(--text-sm)', color: 'var(--hold)' }}>
|
||||
数据质量: <strong>{dataQuality.state}</strong>
|
||||
</div>
|
||||
)}
|
||||
{degradation?.degraded && degradation?.reason_codes?.length > 0 && (
|
||||
<div style={{ marginBottom: 12, fontSize: 'var(--text-sm)', color: 'var(--hold)' }}>
|
||||
降级原因: <strong>{degradation.reason_codes.join(', ')}</strong>
|
||||
</div>
|
||||
)}
|
||||
<ContractCues payload={task} style={{ marginBottom: 12 }} />
|
||||
{errorMessage && (
|
||||
<div style={{ marginBottom: 12, fontSize: 'var(--text-sm)', color: 'var(--sell)' }}>
|
||||
错误: {errorMessage}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd'
|
||||
import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import ContractCues from '../components/ContractCues'
|
||||
import DecisionBadge from '../components/DecisionBadge'
|
||||
import { StatusIcon, StatusTag } from '../components/StatusIcon'
|
||||
import { getDecision, getErrorMessage } from '../utils/contractView'
|
||||
|
|
@ -107,8 +108,13 @@ export default function BatchManager() {
|
|||
{
|
||||
title: '决策',
|
||||
key: 'decision',
|
||||
width: 80,
|
||||
render: (_, record) => <DecisionBadge decision={getDecision(record)} />,
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<DecisionBadge decision={getDecision(record)} />
|
||||
<ContractCues payload={record} style={{ marginTop: 6 }} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务ID',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
DownloadOutlined, SyncOutlined, AccountBookOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { portfolioApi } from '../services/portfolioApi'
|
||||
import ContractCues from '../components/ContractCues'
|
||||
import DecisionBadge from '../components/DecisionBadge'
|
||||
import { getDecision, getDisplayDate, isCompletedLikeStatus } from '../utils/contractView'
|
||||
|
||||
|
|
@ -378,8 +379,13 @@ function RecommendationsTab() {
|
|||
render: t => <span className="text-data">{t}</span> },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', render: t => <span style={{ fontWeight: 500 }}>{t}</span> },
|
||||
{
|
||||
title: '决策', key: 'decision', width: 80,
|
||||
render: (_, record) => <DecisionBadge decision={getDecision(record)} />,
|
||||
title: '决策', key: 'decision', width: 180,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<DecisionBadge decision={getDecision(record)} />
|
||||
<ContractCues payload={record} style={{ marginTop: 6 }} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: '分析日期', key: 'analysis_date', width: 120, render: (_, record) => getDisplayDate(record) || '—' },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue