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 { useSearchParams } from 'react-router-dom'
|
||||||
import { Card, Progress, Badge, Empty, Button, Result, message } from 'antd'
|
import { Card, Progress, Badge, Empty, Button, Result, message } from 'antd'
|
||||||
import DecisionBadge from '../components/DecisionBadge'
|
import DecisionBadge from '../components/DecisionBadge'
|
||||||
|
import ContractCues from '../components/ContractCues'
|
||||||
import { StatusIcon } from '../components/StatusIcon'
|
import { StatusIcon } from '../components/StatusIcon'
|
||||||
import {
|
import {
|
||||||
getConfidence,
|
getConfidence,
|
||||||
getDataQualitySummary,
|
|
||||||
getDegradationSummary,
|
|
||||||
getDecision,
|
getDecision,
|
||||||
getDisplayDate,
|
getDisplayDate,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
|
|
@ -36,8 +35,6 @@ export default function AnalysisMonitor() {
|
||||||
const quantSignal = getQuantSignal(task)
|
const quantSignal = getQuantSignal(task)
|
||||||
const confidence = getConfidence(task)
|
const confidence = getConfidence(task)
|
||||||
const displayDate = getDisplayDate(task)
|
const displayDate = getDisplayDate(task)
|
||||||
const dataQuality = getDataQualitySummary(task)
|
|
||||||
const degradation = getDegradationSummary(task)
|
|
||||||
const errorMessage = getErrorMessage(task)
|
const errorMessage = getErrorMessage(task)
|
||||||
|
|
||||||
const fetchInitialState = useCallback(async () => {
|
const fetchInitialState = useCallback(async () => {
|
||||||
|
|
@ -196,16 +193,7 @@ export default function AnalysisMonitor() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{dataQuality?.state && (
|
<ContractCues payload={task} style={{ marginBottom: 12 }} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<div style={{ marginBottom: 12, fontSize: 'var(--text-sm)', color: 'var(--sell)' }}>
|
<div style={{ marginBottom: 12, fontSize: 'var(--text-sm)', color: 'var(--sell)' }}>
|
||||||
错误: {errorMessage}
|
错误: {errorMessage}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd'
|
import { Table, Button, Progress, Result, Card, message, Popconfirm, Tooltip } from 'antd'
|
||||||
import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
|
import ContractCues from '../components/ContractCues'
|
||||||
import DecisionBadge from '../components/DecisionBadge'
|
import DecisionBadge from '../components/DecisionBadge'
|
||||||
import { StatusIcon, StatusTag } from '../components/StatusIcon'
|
import { StatusIcon, StatusTag } from '../components/StatusIcon'
|
||||||
import { getDecision, getErrorMessage } from '../utils/contractView'
|
import { getDecision, getErrorMessage } from '../utils/contractView'
|
||||||
|
|
@ -107,8 +108,13 @@ export default function BatchManager() {
|
||||||
{
|
{
|
||||||
title: '决策',
|
title: '决策',
|
||||||
key: 'decision',
|
key: 'decision',
|
||||||
width: 80,
|
width: 180,
|
||||||
render: (_, record) => <DecisionBadge decision={getDecision(record)} />,
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<DecisionBadge decision={getDecision(record)} />
|
||||||
|
<ContractCues payload={record} style={{ marginTop: 6 }} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '任务ID',
|
title: '任务ID',
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DownloadOutlined, SyncOutlined, AccountBookOutlined,
|
DownloadOutlined, SyncOutlined, AccountBookOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { portfolioApi } from '../services/portfolioApi'
|
import { portfolioApi } from '../services/portfolioApi'
|
||||||
|
import ContractCues from '../components/ContractCues'
|
||||||
import DecisionBadge from '../components/DecisionBadge'
|
import DecisionBadge from '../components/DecisionBadge'
|
||||||
import { getDecision, getDisplayDate, isCompletedLikeStatus } from '../utils/contractView'
|
import { getDecision, getDisplayDate, isCompletedLikeStatus } from '../utils/contractView'
|
||||||
|
|
||||||
|
|
@ -378,8 +379,13 @@ function RecommendationsTab() {
|
||||||
render: t => <span className="text-data">{t}</span> },
|
render: t => <span className="text-data">{t}</span> },
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name', render: t => <span style={{ fontWeight: 500 }}>{t}</span> },
|
{ title: '名称', dataIndex: 'name', key: 'name', render: t => <span style={{ fontWeight: 500 }}>{t}</span> },
|
||||||
{
|
{
|
||||||
title: '决策', key: 'decision', width: 80,
|
title: '决策', key: 'decision', width: 180,
|
||||||
render: (_, record) => <DecisionBadge decision={getDecision(record)} />,
|
render: (_, record) => (
|
||||||
|
<div>
|
||||||
|
<DecisionBadge decision={getDecision(record)} />
|
||||||
|
<ContractCues payload={record} style={{ marginTop: 6 }} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{ title: '分析日期', key: 'analysis_date', width: 120, render: (_, record) => getDisplayDate(record) || '—' },
|
{ title: '分析日期', key: 'analysis_date', width: 120, render: (_, record) => getDisplayDate(record) || '—' },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue