diff --git a/platform/web/app/agent-verification/page.tsx b/platform/web/app/agent-verification/page.tsx index 87ddace..ebe7f61 100644 --- a/platform/web/app/agent-verification/page.tsx +++ b/platform/web/app/agent-verification/page.tsx @@ -2,22 +2,43 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function fetchJson(path: string) { +type AgentVerificationCheck = { + agent: string; + role: string; + status_label: string; + evidence?: string[]; +}; + +type AgentVerificationResponse = { + overall_label?: string; + production_ready?: boolean; + checks?: AgentVerificationCheck[]; +}; + +type GeminiUsageResponse = { + estimated_cost_usd?: number; + cap_usd?: number; +}; + +async function fetchJson(path: string): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/${path}`, { cache: 'no-store' }); if (!response.ok) throw new Error(`${path} 暫時無法回應`); - return response.json(); + return response.json() as Promise; } export default async function AgentVerificationPage() { - let verification: any = null; - let usage: any = null; + let verification: AgentVerificationResponse | null = null; + let usage: GeminiUsageResponse | null = null; let error = ''; try { - [verification, usage] = await Promise.all([fetchJson('agent-verification'), fetchJson('gemini-usage')]); + [verification, usage] = await Promise.all([ + fetchJson('agent-verification'), + fetchJson('gemini-usage'), + ]); } catch (err) { error = err instanceof Error ? err.message : 'AI 驗證資料暫時無法讀取'; } - const checks = Array.isArray(verification?.checks) ? verification.checks : []; + const checks = verification?.checks ?? []; return (
@@ -35,12 +56,12 @@ export default async function AgentVerificationPage() { ].map(([label, value, helper]) =>

{helper}

{value}

{label}

)}
- {checks.map((item: any) => ( + {checks.map((item) => (

{item.agent}

{item.role}

{item.status_label}

-
    {(item.evidence ?? []).map((line: string) =>
  • ・{line}
  • )}
+
    {(item.evidence ?? []).map((line) =>
  • ・{line}
  • )}
))}
diff --git a/platform/web/app/battle-room/page.tsx b/platform/web/app/battle-room/page.tsx index 2330165..47f3d1f 100644 --- a/platform/web/app/battle-room/page.tsx +++ b/platform/web/app/battle-room/page.tsx @@ -2,10 +2,12 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getMatches() { +type MatchRow = { match_id: string; home_team: string; away_team: string; kickoff_utc: string; status: string }; + +async function getMatches(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/matches`, { cache: 'no-store' }); if (!response.ok) throw new Error('對戰情報 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } function timeText(value: string) { @@ -13,7 +15,7 @@ function timeText(value: string) { } export default async function BattleRoomPage() { - let matches: any[] = []; + let matches: MatchRow[] = []; let error = ''; try { matches = await getMatches(); } catch (err) { error = err instanceof Error ? err.message : '對戰情報暫時無法讀取'; } const now = Date.now(); diff --git a/platform/web/app/live-score/page.tsx b/platform/web/app/live-score/page.tsx index 3cde6c7..bb760fa 100644 --- a/platform/web/app/live-score/page.tsx +++ b/platform/web/app/live-score/page.tsx @@ -2,10 +2,12 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getMatches() { +type MatchRow = { match_id: string; home_team: string; away_team: string; home_score: number | null; away_score: number | null; kickoff_utc: string; status: string; venue_city?: string | null }; + +async function getMatches(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/matches`, { cache: 'no-store' }); if (!response.ok) throw new Error('比分 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } function timeText(value: string) { @@ -13,7 +15,7 @@ function timeText(value: string) { } export default async function LiveScorePage() { - let matches: any[] = []; + let matches: MatchRow[] = []; let error = ''; try { matches = await getMatches(); } catch (err) { error = err instanceof Error ? err.message : '比分暫時無法讀取'; } const now = Date.now(); diff --git a/platform/web/app/market-coverage/page.tsx b/platform/web/app/market-coverage/page.tsx index 4635aa1..7e46a93 100644 --- a/platform/web/app/market-coverage/page.tsx +++ b/platform/web/app/market-coverage/page.tsx @@ -2,18 +2,34 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getMarketCoverage() { +type MarketCoverageItem = { + market_type: string; + label?: string; + odds_rows?: number; + match_count?: number; + bookmaker_count?: number; + passes_formal_minimum?: boolean; +}; + +type MissingMarket = { market_type: string; label?: string; reason?: string }; + +type MarketCoverageResponse = { + coverage?: MarketCoverageItem[]; + missing_markets?: MissingMarket[]; +}; + +async function getMarketCoverage(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/market-coverage`, { cache: 'no-store' }); if (!response.ok) throw new Error('盤口覆蓋 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } export default async function MarketCoveragePage() { - let data: any = null; + let data: MarketCoverageResponse | null = null; let error = ''; try { data = await getMarketCoverage(); } catch (err) { error = err instanceof Error ? err.message : '盤口覆蓋暫時無法讀取'; } - const coverage = Array.isArray(data?.coverage) ? data.coverage : []; - const missing = Array.isArray(data?.missing_markets) ? data.missing_markets : []; + const coverage = data?.coverage ?? []; + const missing = data?.missing_markets ?? []; return (
@@ -24,12 +40,12 @@ export default async function MarketCoveragePage() { {error ?

{error}

: null}
- {coverage.map((item: any) => ( + {coverage.map((item) => (

{item.label ?? item.market_type}

-

賽事:{item.match_count}

-

莊家:{item.bookmaker_count}

-

賠率列:{item.odds_rows}

+

賽事:{item.match_count ?? 0}

+

莊家:{item.bookmaker_count ?? 0}

+

賠率列:{item.odds_rows ?? 0}

{item.passes_formal_minimum ? '可進正式檢查' : '只能監控'}

))} @@ -37,7 +53,7 @@ export default async function MarketCoveragePage() {

缺少或不足玩法

- {missing.length ? missing.map((item: any) =>

{item.label}:{item.reason}

) :

目前沒有回報缺口。

} + {missing.length ? missing.map((item) =>

{item.label ?? item.market_type}:{item.reason ?? '尚未回報原因'}

) :

目前沒有回報缺口。

}
看推薦就緒度 diff --git a/platform/web/app/news/page.tsx b/platform/web/app/news/page.tsx index 5cad116..2eb131a 100644 --- a/platform/web/app/news/page.tsx +++ b/platform/web/app/news/page.tsx @@ -1,11 +1,12 @@ const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; type NewsItem = { title?: string; url?: string; source?: string; publishedAt?: string; published_at?: string; summary?: string }; +type NewsSnapshot = { status_label?: string; message?: string; items?: NewsItem[]; news?: NewsItem[] }; -async function getNews() { +async function getNews(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/news-snapshot`, { cache: 'no-store' }); if (!response.ok) throw new Error('新聞快照 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } function timeText(value?: string) { @@ -14,19 +15,14 @@ function timeText(value?: string) { } export default async function NewsPage() { - let data: any = null; + let data: NewsSnapshot | null = null; let error = ''; try { data = await getNews(); } catch (err) { error = err instanceof Error ? err.message : '新聞資料暫時無法讀取'; } - const items: NewsItem[] = Array.isArray(data?.items) ? data.items : Array.isArray(data?.news) ? data.news : []; + const items = data?.items ?? data?.news ?? []; return (
-
-

即時新聞情報

-

新聞、傷停與外部事件不能假裝即時

-

只有接入可公開引用的來源才會顯示新聞;沒有授權新聞時,系統會明確說明缺口,不用假新聞或舊新聞充數。

-

{data?.status_label ?? '新聞快照同步中'}

-
+

即時新聞情報

新聞、傷停與外部事件不能假裝即時

只有接入可公開引用的來源才會顯示新聞;沒有授權新聞時,系統會明確說明缺口,不用假新聞或舊新聞充數。

{data?.status_label ?? '新聞快照同步中'}

{error ?

{error}

: null} {items.length ?
{items.map((item) =>

{item.title}

{item.source ?? '來源待定'}|{timeText(item.publishedAt ?? item.published_at)}

{item.summary ?

{item.summary}

: null}
)}
:

{data?.message ?? '目前沒有可公開引用的新聞快照。'}

}
diff --git a/platform/web/app/recommendation-performance/page.tsx b/platform/web/app/recommendation-performance/page.tsx index 3b0c187..9ec3859 100644 --- a/platform/web/app/recommendation-performance/page.tsx +++ b/platform/web/app/recommendation-performance/page.tsx @@ -2,38 +2,30 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getPerformance() { +type MarketBucket = { market_type: string; settled_count?: number; hit_count?: number; hit_rate_percent?: number }; +type PerformanceResponse = { settled_recommendation_count?: number; hit_count?: number; miss_count?: number; hit_rate_percent?: number; summary?: string; by_market_type?: MarketBucket[]; improvement_actions?: string[] }; + +async function getPerformance(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/recommendation-performance`, { cache: 'no-store' }); if (!response.ok) throw new Error('賽後校準 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } export default async function RecommendationPerformancePage() { - let data: any = null; + let data: PerformanceResponse | null = null; let error = ''; try { data = await getPerformance(); } catch (err) { error = err instanceof Error ? err.message : '賽後校準暫時無法讀取'; } - const buckets = Array.isArray(data?.by_market_type) ? data.by_market_type : []; - const actions = Array.isArray(data?.improvement_actions) ? data.improvement_actions : []; + const buckets = data?.by_market_type ?? []; + const actions = data?.improvement_actions ?? []; return (
-
-

賽後校準室

-

推薦要跟實際賽果對帳

-

每場結束後,系統會把推薦玩法與實際結果比對,追蹤命中率、未中原因與下一輪模型調整方向。

-
+

賽後校準室

推薦要跟實際賽果對帳

每場結束後,系統會把推薦玩法與實際結果比對,追蹤命中率、未中原因與下一輪模型調整方向。

{error ?

{error}

: null} -
- {[ - ['已結算', data?.settled_recommendation_count ?? '-', '可判定推薦'], - ['命中', data?.hit_count ?? '-', '命中筆數'], - ['未中', data?.miss_count ?? '-', '未中筆數'], - ['命中率', typeof data?.hit_rate_percent === 'number' ? `${data.hit_rate_percent.toFixed(2)}%` : '-', '近端表現'], - ].map(([label, value, helper]) =>

{helper}

{value}

{label}

)} -
+
{[['已結算', data?.settled_recommendation_count ?? '-', '可判定推薦'], ['命中', data?.hit_count ?? '-', '命中筆數'], ['未中', data?.miss_count ?? '-', '未中筆數'], ['命中率', typeof data?.hit_rate_percent === 'number' ? `${data.hit_rate_percent.toFixed(2)}%` : '-', '近端表現']].map(([label, value, helper]) =>

{helper}

{value}

{label}

)}

{data?.summary ?? '尚無摘要。'}

-

各玩法表現

{buckets.map((item: any) =>
{item.market_type}
結算 {item.settled_count}|命中 {item.hit_count}|命中率 {item.hit_rate_percent}%
)}
-

模型調整方向

    {actions.map((item: string) =>
  • ・{item}
  • )}
+

各玩法表現

{buckets.map((item) =>
{item.market_type}
結算 {item.settled_count ?? 0}|命中 {item.hit_count ?? 0}|命中率 {item.hit_rate_percent ?? 0}%
)}
+

模型調整方向

    {actions.map((item) =>
  • ・{item}
  • )}
看公開收益帳本
); diff --git a/platform/web/app/recommendation-readiness/page.tsx b/platform/web/app/recommendation-readiness/page.tsx index c5cc8ab..58a84bc 100644 --- a/platform/web/app/recommendation-readiness/page.tsx +++ b/platform/web/app/recommendation-readiness/page.tsx @@ -2,43 +2,29 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getReadiness() { +type MarketStatus = { market_type: string; label?: string; match_count?: number; bookmaker_count?: number; odds_rows?: number; passes_minimum?: boolean }; +type ReadinessResponse = { formal_recommendations_allowed?: boolean; headline?: string; market_status?: MarketStatus[]; blocking_reasons?: string[]; required_actions?: string[] }; + +async function getReadiness(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/recommendation-readiness`, { cache: 'no-store' }); if (!response.ok) throw new Error('推薦就緒度 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } export default async function RecommendationReadinessPage() { - let data: any = null; + let data: ReadinessResponse | null = null; let error = ''; try { data = await getReadiness(); } catch (err) { error = err instanceof Error ? err.message : '推薦就緒度暫時無法讀取'; } - const marketStatus = Array.isArray(data?.market_status) ? data.market_status : []; - const blockers = Array.isArray(data?.blocking_reasons) ? data.blocking_reasons : []; - const actions = Array.isArray(data?.required_actions) ? data.required_actions : []; + const marketStatus = data?.market_status ?? []; + const blockers = data?.blocking_reasons ?? []; + const actions = data?.required_actions ?? []; return (
-
-

推薦就緒度

-

現在能不能稱為正式下注推薦?

-

這裡是推薦閘門。若盤口來源不足或核心玩法缺盤,就只能顯示監控候選,不能用高勝率語氣誤導使用者。

-

{data?.headline ?? '就緒度同步中'}

-
+

推薦就緒度

現在能不能稱為正式下注推薦?

這裡是推薦閘門。若盤口來源不足或核心玩法缺盤,就只能顯示監控候選,不能用高勝率語氣誤導使用者。

{data?.headline ?? '就緒度同步中'}

{error ?

{error}

: null} -
- {marketStatus.map((item: any) => ( -
-

{item.label}

-

賽事 {item.match_count} 場|莊家 {item.bookmaker_count} 家

-

最新賠率列 {item.odds_rows}

-

{item.passes_minimum ? '通過最低門檻' : '未通過最低門檻'}

-
- ))} -
-
-

阻擋原因

    {blockers.map((item: string) =>
  • ・{item}
  • )}
-

下一步

    {actions.map((item: string) =>
  • ・{item}
  • )}
-
+
{marketStatus.map((item) =>

{item.label ?? item.market_type}

賽事 {item.match_count ?? 0} 場|莊家 {item.bookmaker_count ?? 0} 家

最新賠率列 {item.odds_rows ?? 0}

{item.passes_minimum ? '通過最低門檻' : '未通過最低門檻'}

)}
+

阻擋原因

    {blockers.map((item) =>
  • ・{item}
  • )}

下一步

    {actions.map((item) =>
  • ・{item}
  • )}
回每日作戰室
); diff --git a/platform/web/app/schedule/page.tsx b/platform/web/app/schedule/page.tsx index 9317d62..d40f2dd 100644 --- a/platform/web/app/schedule/page.tsx +++ b/platform/web/app/schedule/page.tsx @@ -2,10 +2,21 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getMatches() { +type MatchRow = { + match_id: string; + home_team: string; + away_team: string; + home_score: number | null; + away_score: number | null; + kickoff_utc: string; + status: string; + venue_city?: string | null; +}; + +async function getMatches(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/matches`, { cache: 'no-store' }); if (!response.ok) throw new Error('賽程 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } function taipeiDate(value: string) { @@ -17,10 +28,10 @@ function taipeiTime(value: string) { } export default async function SchedulePage() { - let matches: any[] = []; + let matches: MatchRow[] = []; let error = ''; try { matches = await getMatches(); } catch (err) { error = err instanceof Error ? err.message : '賽程暫時無法讀取'; } - const grouped = matches.reduce>((acc, match) => { + const grouped = matches.reduce>((acc, match) => { const key = taipeiDate(match.kickoff_utc); acc[key] = [...(acc[key] ?? []), match]; return acc; diff --git a/platform/web/app/source-health/page.tsx b/platform/web/app/source-health/page.tsx index 6910ab7..845a1aa 100644 --- a/platform/web/app/source-health/page.tsx +++ b/platform/web/app/source-health/page.tsx @@ -2,10 +2,13 @@ import Link from 'next/link'; const ANALYTICS_BACKEND = process.env.ANALYTICS_BACKEND_URL || 'http://127.0.0.1:8000'; -async function getSourceHealth() { +type WorkerStatus = { status?: string; run_at?: string; message?: string }; +type SourceHealth = { status?: string; odds_rows?: number; matches?: number; finished_matches?: number; venues?: number; high_altitude_venues?: number; stale_unsettled_matches?: number; latest_odds_recorded_at?: string | null; latest_result_synced_at?: string | null; ingestion_status?: WorkerStatus | null; fixtures_status?: WorkerStatus | null; news_status?: WorkerStatus | null }; + +async function getSourceHealth(): Promise { const response = await fetch(`${ANALYTICS_BACKEND}/analytics/source-health`, { cache: 'no-store' }); if (!response.ok) throw new Error('資料健康 API 暫時無法回應'); - return response.json(); + return response.json() as Promise; } function dateText(value?: string | null) { @@ -14,47 +17,17 @@ function dateText(value?: string | null) { } export default async function SourceHealthPage() { - let data: any = null; + let data: SourceHealth | null = null; let error = ''; try { data = await getSourceHealth(); } catch (err) { error = err instanceof Error ? err.message : '資料健康暫時無法讀取'; } - - const cards = [ - ['賽事總數', data?.matches ?? '-', '目前資料庫可見賽事'], - ['已完賽', data?.finished_matches ?? '-', '可用於賽後校準'], - ['賠率列數', data?.odds_rows ?? '-', '盤口歷史資料量'], - ['逾時賽果', data?.stale_unsettled_matches ?? '-', '不為 0 時不升級正式推薦'], - ]; + const cards = [['賽事總數', data?.matches ?? '-', '目前資料庫可見賽事'], ['已完賽', data?.finished_matches ?? '-', '可用於賽後校準'], ['賠率列數', data?.odds_rows ?? '-', '盤口歷史資料量'], ['逾時賽果', data?.stale_unsettled_matches ?? '-', '不為 0 時不升級正式推薦']]; return (
-
-

資料健康總覽

-

先確認資料新鮮,再看投注候選

-

這頁檢查賽程、賽果、盤口、新聞與排程是否正常。只要資料延遲或盤口不足,首頁就只能顯示監控候選,不能包裝成正式高勝率推薦。

-
+

資料健康總覽

先確認資料新鮮,再看投注候選

這頁檢查賽程、賽果、盤口、新聞與排程是否正常。只要資料延遲或盤口不足,首頁就只能顯示監控候選,不能包裝成正式高勝率推薦。

{error ?

{error}

: null} -
- {cards.map(([label, value, helper]) => ( -
-

{helper}

-

{value}

-

{label}

-
- ))} -
-
- {[ - ['賠率同步', data?.ingestion_status?.status ?? data?.status, data?.ingestion_status?.message ?? `最近更新:${dateText(data?.latest_odds_recorded_at)}`], - ['賽程同步', data?.fixtures_status?.status ?? 'unknown', data?.fixtures_status?.message ?? `最近排程:${dateText(data?.fixtures_status?.run_at)}`], - ['新聞同步', data?.news_status?.status ?? 'unknown', data?.news_status?.message ?? `最近排程:${dateText(data?.news_status?.run_at)}`], - ].map(([title, status, detail]) => ( -
-

{title}

-

{status || 'unknown'}

-

{detail}

-
- ))} -
+
{cards.map(([label, value, helper]) =>

{helper}

{value}

{label}

)}
+
{[['賠率同步', data?.ingestion_status?.status ?? data?.status, data?.ingestion_status?.message ?? `最近更新:${dateText(data?.latest_odds_recorded_at)}`], ['賽程同步', data?.fixtures_status?.status ?? 'unknown', data?.fixtures_status?.message ?? `最近排程:${dateText(data?.fixtures_status?.run_at)}`], ['新聞同步', data?.news_status?.status ?? 'unknown', data?.news_status?.message ?? `最近排程:${dateText(data?.news_status?.run_at)}`]].map(([title, status, detail]) =>

{title}

{status || 'unknown'}

{detail}

)}
回每日作戰室
);