From 9a0fa727383d874621a688a5a7ec2b3689973116 Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 25 Jun 2026 14:45:16 +0800 Subject: [PATCH] fix: separate reference stale matches from source health --- platform/backend/app/main.py | 27 ++++++++++++++++++++++++- platform/web/app/source-health/page.tsx | 27 +++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/platform/backend/app/main.py b/platform/backend/app/main.py index 9e84522..9a0467b 100644 --- a/platform/backend/app/main.py +++ b/platform/backend/app/main.py @@ -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, diff --git a/platform/web/app/source-health/page.tsx b/platform/web/app/source-health/page.tsx index 4ff7fde..2f2c2da 100644 --- a/platform/web/app/source-health/page.tsx +++ b/platform/web/app/source-health/page.tsx @@ -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>; + reference_only_stale_examples?: Array>; 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} -
+
{cards.map(([label, value, helper]) => (

{helper}

@@ -161,6 +165,25 @@ export default async function SourceHealthPage() { ))}
+ {(data?.reference_only_stale_matches ?? 0) > 0 ? ( +
+

參考盤待核對提醒

+

目前有台灣參考盤賽事尚未對到正式賽果

+

+ 這類資料只用來監控賠率,不會被當成正式比分來源,也不會自動補造下注推薦。正式賽果逾時若為 0,代表核心比分同步沒有被這些參考盤殘留誤判成壞掉。 +

+
+ {(data?.reference_only_stale_examples ?? []).map((item) => ( +
+

{String(item.home_team ?? '待確認')} vs {String(item.away_team ?? '待確認')}

+

來源編號:{String(item.match_id ?? '未知')}

+

目前狀態:{String(item.status ?? '待確認')}

+
+ ))} +
+
+ ) : null} +
{workerCards.map(([title, status, detail]) => (