fix: separate reference stale matches from source health
All checks were successful
2026 World Cup Quant Platform - Production Deployment / Code Quality, Security Gate & Testing (push) Successful in 4m15s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Gitea CD (push) Successful in 3m50s

This commit is contained in:
ogt
2026-06-25 14:45:16 +08:00
parent 2c2b761658
commit 9a0fa72738
2 changed files with 51 additions and 3 deletions

View File

@@ -712,7 +712,10 @@ class SourceHealthResponse(BaseModel):
formal_provider_blocker: str | None = None
upcoming_odds_matches: int = 0
stale_unsettled_matches: int = 0
reference_only_stale_matches: int = 0
stale_unsettled_threshold_hours: int = 3
stale_unsettled_examples: list[dict[str, Any]] = Field(default_factory=list)
reference_only_stale_examples: list[dict[str, Any]] = Field(default_factory=list)
odds_rows: int
matches: int
finished_matches: int
@@ -802,12 +805,16 @@ async def analytics_source_health() -> SourceHealthResponse:
matches = int(match_count_result.scalar_one() or 0)
finished_matches = int(finished_count_result.scalar_one() or 0)
stale_unsettled_matches = int(stale_unsettled_result.scalar_one() or 0)
reference_only_stale_matches = 0
stale_unsettled_examples: list[dict[str, Any]] = []
reference_only_stale_examples: list[dict[str, Any]] = []
try:
logical_matches = await _query_match_list(limit=5000)
if logical_matches:
matches = len(logical_matches)
finished_matches = sum(1 for item in logical_matches if item.get('status') == '已完賽')
logical_stale = 0
logical_reference_stale = 0
for item in logical_matches:
if item.get('status') == '已完賽':
continue
@@ -819,8 +826,23 @@ async def analytics_source_health() -> SourceHealthResponse:
if kickoff_at.tzinfo is None:
kickoff_at = kickoff_at.replace(tzinfo=timezone.utc)
if kickoff_at < now - timedelta(hours=stale_threshold_hours):
logical_stale += 1
example = {
'match_id': item.get('match_id'),
'home_team': item.get('home_team'),
'away_team': item.get('away_team'),
'kickoff_utc': kickoff_at.isoformat(),
'status': item.get('status'),
}
if str(item.get('match_id') or '').startswith('tsl-'):
logical_reference_stale += 1
if len(reference_only_stale_examples) < 5:
reference_only_stale_examples.append(example)
else:
logical_stale += 1
if len(stale_unsettled_examples) < 5:
stale_unsettled_examples.append(example)
stale_unsettled_matches = logical_stale
reference_only_stale_matches = logical_reference_stale
except Exception:
pass
venues = int(venue_count_result.scalar_one() or 0)
@@ -873,7 +895,10 @@ async def analytics_source_health() -> SourceHealthResponse:
formal_provider_blocker=formal_provider_blocker,
upcoming_odds_matches=upcoming_odds_matches,
stale_unsettled_matches=stale_unsettled_matches,
reference_only_stale_matches=reference_only_stale_matches,
stale_unsettled_threshold_hours=stale_threshold_hours,
stale_unsettled_examples=stale_unsettled_examples,
reference_only_stale_examples=reference_only_stale_examples,
odds_rows=odds_rows,
matches=matches,
finished_matches=finished_matches,

View File

@@ -37,6 +37,9 @@ type SourceHealth = {
venues?: number;
high_altitude_venues?: number;
stale_unsettled_matches?: number;
reference_only_stale_matches?: number;
stale_unsettled_examples?: Array<Record<string, unknown>>;
reference_only_stale_examples?: Array<Record<string, unknown>>;
latest_odds_recorded_at?: string | null;
latest_result_synced_at?: string | null;
ingestion_status?: WorkerStatus | null;
@@ -93,7 +96,8 @@ export default async function SourceHealthPage() {
['賽事總數', data?.matches ?? '-', '目前資料庫可見賽事'],
['已完賽', data?.finished_matches ?? '-', '可用於賽後校準'],
['賠率列數', data?.odds_rows ?? '-', '盤口歷史資料量'],
['逾時賽果', data?.stale_unsettled_matches ?? '-', '不為 0 時不升級正式推薦'],
['正式賽果逾時', data?.stale_unsettled_matches ?? '-', '不為 0 時不升級正式推薦'],
['參考盤待核對', data?.reference_only_stale_matches ?? '-', '台灣參考盤殘留,不等於正式比分壞掉'],
];
const workerCards = [
[
@@ -151,7 +155,7 @@ export default async function SourceHealthPage() {
) : null}
</section>
<section className="grid gap-4 md:grid-cols-4">
<section className="grid gap-4 md:grid-cols-5">
{cards.map(([label, value, helper]) => (
<article key={String(label)} className="panel-glow rounded-2xl p-5">
<p className="text-xs font-semibold tracking-[0.2em] text-[#8a6b58]">{helper}</p>
@@ -161,6 +165,25 @@ export default async function SourceHealthPage() {
))}
</section>
{(data?.reference_only_stale_matches ?? 0) > 0 ? (
<section className="rounded-3xl border border-[#eadcb9] bg-[#fff8e6] p-5">
<p className="dot-matrix text-sm font-bold text-[#7d2a15]"></p>
<h2 className="mt-2 text-2xl font-black text-[#3f2f25]"></h2>
<p className="mt-2 text-sm leading-7 text-[#7a5b46]">
0
</p>
<div className="mt-4 grid gap-3 md:grid-cols-2">
{(data?.reference_only_stale_examples ?? []).map((item) => (
<div key={String(item.match_id)} className="rounded-2xl border border-[#eadcb9] bg-white/75 p-4 text-sm leading-6 text-[#5f4330]">
<p className="font-black text-[#7d2a15]">{String(item.home_team ?? '待確認')} vs {String(item.away_team ?? '待確認')}</p>
<p>{String(item.match_id ?? '未知')}</p>
<p>{String(item.status ?? '待確認')}</p>
</div>
))}
</div>
</section>
) : null}
<section className="grid gap-4 lg:grid-cols-3">
{workerCards.map(([title, status, detail]) => (
<article key={String(title)} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">