fix: type missing product pages
This commit is contained in:
@@ -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<T>(path: string): Promise<T> {
|
||||
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<T>;
|
||||
}
|
||||
|
||||
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<AgentVerificationResponse>('agent-verification'),
|
||||
fetchJson<GeminiUsageResponse>('gemini-usage'),
|
||||
]);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'AI 驗證資料暫時無法讀取';
|
||||
}
|
||||
const checks = Array.isArray(verification?.checks) ? verification.checks : [];
|
||||
const checks = verification?.checks ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -35,12 +56,12 @@ export default async function AgentVerificationPage() {
|
||||
].map(([label, value, helper]) => <article key={label} className="panel-glow rounded-2xl p-5"><p className="text-xs font-semibold tracking-[0.2em] text-[#8a6b58]">{helper}</p><p className="mt-3 text-2xl font-black text-[#7d2a15]">{value}</p><p className="mt-1 text-sm text-[#6f4f3c]">{label}</p></article>)}
|
||||
</section>
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
{checks.map((item: any) => (
|
||||
{checks.map((item) => (
|
||||
<article key={item.agent} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">
|
||||
<p className="dot-matrix text-sm font-bold text-[#7d2a15]">{item.agent}</p>
|
||||
<h2 className="mt-2 text-xl font-black text-[#3f2f25]">{item.role}</h2>
|
||||
<p className="mt-2 rounded-full bg-[#fff8e6] px-3 py-1 text-xs font-black text-[#7d2a15]">{item.status_label}</p>
|
||||
<ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{(item.evidence ?? []).map((line: string) => <li key={line}>・{line}</li>)}</ul>
|
||||
<ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{(item.evidence ?? []).map((line) => <li key={line}>・{line}</li>)}</ul>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@@ -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<MatchRow[]> {
|
||||
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<MatchRow[]>;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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<MatchRow[]> {
|
||||
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<MatchRow[]>;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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<MarketCoverageResponse> {
|
||||
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<MarketCoverageResponse>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
@@ -24,12 +40,12 @@ export default async function MarketCoveragePage() {
|
||||
</section>
|
||||
{error ? <p className="rounded-2xl border border-[#e7a49a] bg-[#fff0e8] p-4 text-sm font-bold text-[#b83822]">{error}</p> : null}
|
||||
<section className="grid gap-4 lg:grid-cols-3">
|
||||
{coverage.map((item: any) => (
|
||||
{coverage.map((item) => (
|
||||
<article key={item.market_type} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">
|
||||
<p className="dot-matrix text-sm font-bold text-[#7d2a15]">{item.label ?? item.market_type}</p>
|
||||
<p className="mt-3 text-sm text-[#7a5b46]">賽事:<b className="text-[#3f2f25]">{item.match_count}</b> 場</p>
|
||||
<p className="mt-1 text-sm text-[#7a5b46]">莊家:<b className="text-[#3f2f25]">{item.bookmaker_count}</b> 家</p>
|
||||
<p className="mt-1 text-sm text-[#7a5b46]">賠率列:<b className="text-[#3f2f25]">{item.odds_rows}</b></p>
|
||||
<p className="mt-3 text-sm text-[#7a5b46]">賽事:<b className="text-[#3f2f25]">{item.match_count ?? 0}</b> 場</p>
|
||||
<p className="mt-1 text-sm text-[#7a5b46]">莊家:<b className="text-[#3f2f25]">{item.bookmaker_count ?? 0}</b> 家</p>
|
||||
<p className="mt-1 text-sm text-[#7a5b46]">賠率列:<b className="text-[#3f2f25]">{item.odds_rows ?? 0}</b></p>
|
||||
<p className={`mt-3 rounded-full px-3 py-1 text-xs font-black ${item.passes_formal_minimum ? 'bg-[#e9f8ef] text-[#167a47]' : 'bg-[#fff7d6] text-[#8a6400]'}`}>{item.passes_formal_minimum ? '可進正式檢查' : '只能監控'}</p>
|
||||
</article>
|
||||
))}
|
||||
@@ -37,7 +53,7 @@ export default async function MarketCoveragePage() {
|
||||
<section className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">
|
||||
<p className="dot-matrix text-sm font-bold text-[#7d2a15]">缺少或不足玩法</p>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
{missing.length ? missing.map((item: any) => <p key={item.market_type} className="rounded-2xl bg-[#fff8e6] p-4 text-sm text-[#7a5b46]"><b className="text-[#3f2f25]">{item.label}</b>:{item.reason}</p>) : <p className="text-sm text-[#7a5b46]">目前沒有回報缺口。</p>}
|
||||
{missing.length ? missing.map((item) => <p key={item.market_type} className="rounded-2xl bg-[#fff8e6] p-4 text-sm text-[#7a5b46]"><b className="text-[#3f2f25]">{item.label ?? item.market_type}</b>:{item.reason ?? '尚未回報原因'}</p>) : <p className="text-sm text-[#7a5b46]">目前沒有回報缺口。</p>}
|
||||
</div>
|
||||
</section>
|
||||
<Link href="/recommendation-readiness" className="inline-flex rounded-full bg-[#d1432d] px-5 py-3 text-sm font-black text-white">看推薦就緒度</Link>
|
||||
|
||||
@@ -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<NewsSnapshot> {
|
||||
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<NewsSnapshot>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8">
|
||||
<p className="dot-matrix text-sm font-bold text-[#b83822]">即時新聞情報</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-[#3f2f25]">新聞、傷停與外部事件不能假裝即時</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">只有接入可公開引用的來源才會顯示新聞;沒有授權新聞時,系統會明確說明缺口,不用假新聞或舊新聞充數。</p>
|
||||
<p className="mt-4 rounded-full bg-white/70 px-4 py-2 text-sm font-bold text-[#7d2a15]">{data?.status_label ?? '新聞快照同步中'}</p>
|
||||
</section>
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8"><p className="dot-matrix text-sm font-bold text-[#b83822]">即時新聞情報</p><h1 className="mt-3 text-4xl font-black text-[#3f2f25]">新聞、傷停與外部事件不能假裝即時</h1><p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">只有接入可公開引用的來源才會顯示新聞;沒有授權新聞時,系統會明確說明缺口,不用假新聞或舊新聞充數。</p><p className="mt-4 rounded-full bg-white/70 px-4 py-2 text-sm font-bold text-[#7d2a15]">{data?.status_label ?? '新聞快照同步中'}</p></section>
|
||||
{error ? <p className="rounded-2xl border border-[#e7a49a] bg-[#fff0e8] p-4 text-sm font-bold text-[#b83822]">{error}</p> : null}
|
||||
{items.length ? <section className="grid gap-4 lg:grid-cols-2">{items.map((item) => <a key={item.url ?? item.title} href={item.url} target="_blank" rel="noreferrer" className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5 transition hover:border-[#b83822]"><p className="text-lg font-black text-[#3f2f25]">{item.title}</p><p className="mt-2 text-xs text-[#7a5b46]">{item.source ?? '來源待定'}|{timeText(item.publishedAt ?? item.published_at)}</p>{item.summary ? <p className="mt-3 text-sm leading-6 text-[#6f4f3c]">{item.summary}</p> : null}</a>)}</section> : <section className="rounded-3xl border border-dashed border-[#d8b58c] bg-white/70 p-6"><p className="text-sm leading-7 text-[#7a5b46]">{data?.message ?? '目前沒有可公開引用的新聞快照。'}</p></section>}
|
||||
</div>
|
||||
|
||||
@@ -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<PerformanceResponse> {
|
||||
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<PerformanceResponse>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8">
|
||||
<p className="dot-matrix text-sm font-bold text-[#b83822]">賽後校準室</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-[#3f2f25]">推薦要跟實際賽果對帳</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">每場結束後,系統會把推薦玩法與實際結果比對,追蹤命中率、未中原因與下一輪模型調整方向。</p>
|
||||
</section>
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8"><p className="dot-matrix text-sm font-bold text-[#b83822]">賽後校準室</p><h1 className="mt-3 text-4xl font-black text-[#3f2f25]">推薦要跟實際賽果對帳</h1><p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">每場結束後,系統會把推薦玩法與實際結果比對,追蹤命中率、未中原因與下一輪模型調整方向。</p></section>
|
||||
{error ? <p className="rounded-2xl border border-[#e7a49a] bg-[#fff0e8] p-4 text-sm font-bold text-[#b83822]">{error}</p> : null}
|
||||
<section className="grid gap-4 md:grid-cols-4">
|
||||
{[
|
||||
['已結算', 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]) => <article key={label} className="panel-glow rounded-2xl p-5"><p className="text-xs font-semibold tracking-[0.2em] text-[#8a6b58]">{helper}</p><p className="mt-3 text-3xl font-black text-[#7d2a15]">{value}</p><p className="mt-1 text-sm text-[#6f4f3c]">{label}</p></article>)}
|
||||
</section>
|
||||
<section className="grid gap-4 md:grid-cols-4">{[['已結算', 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]) => <article key={label} className="panel-glow rounded-2xl p-5"><p className="text-xs font-semibold tracking-[0.2em] text-[#8a6b58]">{helper}</p><p className="mt-3 text-3xl font-black text-[#7d2a15]">{value}</p><p className="mt-1 text-sm text-[#6f4f3c]">{label}</p></article>)}</section>
|
||||
<p className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5 text-sm leading-7 text-[#6f4f3c]">{data?.summary ?? '尚無摘要。'}</p>
|
||||
<section className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">各玩法表現</p><div className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">{buckets.map((item: any) => <article key={item.market_type} className="rounded-2xl bg-[#fff8e6] p-4 text-sm text-[#7a5b46]"><b className="text-[#3f2f25]">{item.market_type}</b><br />結算 {item.settled_count}|命中 {item.hit_count}|命中率 {item.hit_rate_percent}%</article>)}</div></section>
|
||||
<section className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">模型調整方向</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{actions.map((item: string) => <li key={item}>・{item}</li>)}</ul></section>
|
||||
<section className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">各玩法表現</p><div className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">{buckets.map((item) => <article key={item.market_type} className="rounded-2xl bg-[#fff8e6] p-4 text-sm text-[#7a5b46]"><b className="text-[#3f2f25]">{item.market_type}</b><br />結算 {item.settled_count ?? 0}|命中 {item.hit_count ?? 0}|命中率 {item.hit_rate_percent ?? 0}%</article>)}</div></section>
|
||||
<section className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">模型調整方向</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{actions.map((item) => <li key={item}>・{item}</li>)}</ul></section>
|
||||
<Link href="/proof-of-yield" className="inline-flex rounded-full bg-[#d1432d] px-5 py-3 text-sm font-black text-white">看公開收益帳本</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<ReadinessResponse> {
|
||||
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<ReadinessResponse>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8">
|
||||
<p className="dot-matrix text-sm font-bold text-[#b83822]">推薦就緒度</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-[#3f2f25]">現在能不能稱為正式下注推薦?</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">這裡是推薦閘門。若盤口來源不足或核心玩法缺盤,就只能顯示監控候選,不能用高勝率語氣誤導使用者。</p>
|
||||
<p className={`mt-5 inline-flex rounded-full px-4 py-2 text-sm font-black ${data?.formal_recommendations_allowed ? 'bg-[#e9f8ef] text-[#167a47]' : 'bg-[#fff7d6] text-[#8a6400]'}`}>{data?.headline ?? '就緒度同步中'}</p>
|
||||
</section>
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8"><p className="dot-matrix text-sm font-bold text-[#b83822]">推薦就緒度</p><h1 className="mt-3 text-4xl font-black text-[#3f2f25]">現在能不能稱為正式下注推薦?</h1><p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">這裡是推薦閘門。若盤口來源不足或核心玩法缺盤,就只能顯示監控候選,不能用高勝率語氣誤導使用者。</p><p className={`mt-5 inline-flex rounded-full px-4 py-2 text-sm font-black ${data?.formal_recommendations_allowed ? 'bg-[#e9f8ef] text-[#167a47]' : 'bg-[#fff7d6] text-[#8a6400]'}`}>{data?.headline ?? '就緒度同步中'}</p></section>
|
||||
{error ? <p className="rounded-2xl border border-[#e7a49a] bg-[#fff0e8] p-4 text-sm font-bold text-[#b83822]">{error}</p> : null}
|
||||
<section className="grid gap-4 lg:grid-cols-3">
|
||||
{marketStatus.map((item: any) => (
|
||||
<article key={item.market_type} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">
|
||||
<p className="dot-matrix text-sm font-bold text-[#7d2a15]">{item.label}</p>
|
||||
<p className="mt-3 text-sm text-[#7a5b46]">賽事 {item.match_count} 場|莊家 {item.bookmaker_count} 家</p>
|
||||
<p className="mt-1 text-sm text-[#7a5b46]">最新賠率列 {item.odds_rows}</p>
|
||||
<p className={`mt-3 rounded-full px-3 py-1 text-xs font-black ${item.passes_minimum ? 'bg-[#e9f8ef] text-[#167a47]' : 'bg-[#fff7d6] text-[#8a6400]'}`}>{item.passes_minimum ? '通過最低門檻' : '未通過最低門檻'}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<article className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">阻擋原因</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{blockers.map((item: string) => <li key={item}>・{item}</li>)}</ul></article>
|
||||
<article className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">下一步</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{actions.map((item: string) => <li key={item}>・{item}</li>)}</ul></article>
|
||||
</section>
|
||||
<section className="grid gap-4 lg:grid-cols-3">{marketStatus.map((item) => <article key={item.market_type} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">{item.label ?? item.market_type}</p><p className="mt-3 text-sm text-[#7a5b46]">賽事 {item.match_count ?? 0} 場|莊家 {item.bookmaker_count ?? 0} 家</p><p className="mt-1 text-sm text-[#7a5b46]">最新賠率列 {item.odds_rows ?? 0}</p><p className={`mt-3 rounded-full px-3 py-1 text-xs font-black ${item.passes_minimum ? 'bg-[#e9f8ef] text-[#167a47]' : 'bg-[#fff7d6] text-[#8a6400]'}`}>{item.passes_minimum ? '通過最低門檻' : '未通過最低門檻'}</p></article>)}</section>
|
||||
<section className="grid gap-4 lg:grid-cols-2"><article className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">阻擋原因</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{blockers.map((item) => <li key={item}>・{item}</li>)}</ul></article><article className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">下一步</p><ul className="mt-3 space-y-2 text-sm leading-6 text-[#7a5b46]">{actions.map((item) => <li key={item}>・{item}</li>)}</ul></article></section>
|
||||
<Link href="/daily-card" className="inline-flex rounded-full bg-[#d1432d] px-5 py-3 text-sm font-black text-white">回每日作戰室</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<MatchRow[]> {
|
||||
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<MatchRow[]>;
|
||||
}
|
||||
|
||||
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<Record<string, any[]>>((acc, match) => {
|
||||
const grouped = matches.reduce<Record<string, MatchRow[]>>((acc, match) => {
|
||||
const key = taipeiDate(match.kickoff_utc);
|
||||
acc[key] = [...(acc[key] ?? []), match];
|
||||
return acc;
|
||||
|
||||
@@ -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<SourceHealth> {
|
||||
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<SourceHealth>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8">
|
||||
<p className="dot-matrix text-sm font-bold text-[#b83822]">資料健康總覽</p>
|
||||
<h1 className="mt-3 text-4xl font-black text-[#3f2f25]">先確認資料新鮮,再看投注候選</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">這頁檢查賽程、賽果、盤口、新聞與排程是否正常。只要資料延遲或盤口不足,首頁就只能顯示監控候選,不能包裝成正式高勝率推薦。</p>
|
||||
</section>
|
||||
<section className="rounded-[2rem] border border-[#e7c89b] bg-[#fff8e6]/95 p-6 md:p-8"><p className="dot-matrix text-sm font-bold text-[#b83822]">資料健康總覽</p><h1 className="mt-3 text-4xl font-black text-[#3f2f25]">先確認資料新鮮,再看投注候選</h1><p className="mt-3 max-w-3xl text-sm leading-7 text-[#6f4f3c]">這頁檢查賽程、賽果、盤口、新聞與排程是否正常。只要資料延遲或盤口不足,首頁就只能顯示監控候選,不能包裝成正式高勝率推薦。</p></section>
|
||||
{error ? <p className="rounded-2xl border border-[#e7a49a] bg-[#fff0e8] p-4 text-sm font-bold text-[#b83822]">{error}</p> : null}
|
||||
<section className="grid gap-4 md:grid-cols-4">
|
||||
{cards.map(([label, value, helper]) => (
|
||||
<article key={label} className="panel-glow rounded-2xl p-5">
|
||||
<p className="text-xs font-semibold tracking-[0.2em] text-[#8a6b58]">{helper}</p>
|
||||
<p className="mt-3 text-3xl font-black text-[#7d2a15]">{value}</p>
|
||||
<p className="mt-1 text-sm text-[#6f4f3c]">{label}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="grid gap-4 lg:grid-cols-3">
|
||||
{[
|
||||
['賠率同步', 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]) => (
|
||||
<article key={title} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5">
|
||||
<p className="dot-matrix text-sm font-bold text-[#7d2a15]">{title}</p>
|
||||
<p className="mt-2 text-2xl font-black text-[#3f2f25]">{status || 'unknown'}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-[#7a5b46]">{detail}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="grid gap-4 md:grid-cols-4">{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><p className="mt-3 text-3xl font-black text-[#7d2a15]">{value}</p><p className="mt-1 text-sm text-[#6f4f3c]">{label}</p></article>)}</section>
|
||||
<section className="grid gap-4 lg:grid-cols-3">{[['賠率同步', 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]) => <article key={String(title)} className="rounded-3xl border border-[#eadcb9] bg-white/75 p-5"><p className="dot-matrix text-sm font-bold text-[#7d2a15]">{title}</p><p className="mt-2 text-2xl font-black text-[#3f2f25]">{status || 'unknown'}</p><p className="mt-2 text-sm leading-6 text-[#7a5b46]">{detail}</p></article>)}</section>
|
||||
<Link href="/daily-card" className="inline-flex rounded-full bg-[#d1432d] px-5 py-3 text-sm font-black text-white">回每日作戰室</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user