ch
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m27s

This commit is contained in:
2026-06-02 03:37:07 +03:00
parent 9540ff9d2e
commit 2695cfffb4
7 changed files with 437 additions and 1 deletions
+6 -1
View File
@@ -41,8 +41,9 @@ import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { EditUserModal } from "./edit-user-modal";
import LeagueTiersContent from "./league-tiers-content";
import ModelPerformanceContent from "./model-performance-content";
type AdminTab = "overview" | "users" | "league-tiers";
type AdminTab = "overview" | "users" | "league-tiers" | "model-performance";
// ========================
// Admin Stat Card
@@ -142,6 +143,7 @@ export default function AdminContent() {
{ key: "overview", label: t("overview") },
{ key: "users", label: t("user-management") },
{ key: "league-tiers", label: "Lig Tier" },
{ key: "model-performance", label: "Model Performansı" },
];
const getUserDisplayName = (user: AdminUserDto) => {
@@ -553,6 +555,9 @@ export default function AdminContent() {
{/* League Tiers Tab */}
{activeTab === "league-tiers" && <LeagueTiersContent />}
{/* Model Performance Tab */}
{activeTab === "model-performance" && <ModelPerformanceContent />}
<EditUserModal
user={editingUser}
isOpen={!!editingUser}
@@ -0,0 +1,329 @@
"use client";
import {
Box,
Flex,
Heading,
Text,
SimpleGrid,
Card,
VStack,
HStack,
Badge,
Spinner,
Table,
} from "@chakra-ui/react";
import {
NativeSelectRoot,
NativeSelectField,
} from "@/components/ui/forms/native-select";
import { useColorModeValue } from "@/components/ui/color-mode";
import { AnimatedCounter } from "@/components/motion";
import { useModelPerformance } from "@/lib/api/admin/use-hooks";
import type { ModelPerformanceMarketDto } from "@/lib/api/admin/types";
import { LuTarget, LuTrendingUp, LuCircleCheck, LuLayers } from "react-icons/lu";
import { useState } from "react";
const MARKET_LABELS: Record<string, string> = {
MS: "Maç Sonucu",
DC: "Çifte Şans",
OU15: "Üst/Alt 1.5",
OU25: "Üst/Alt 2.5",
OU35: "Üst/Alt 3.5",
BTTS: "Karşılıklı Gol",
HT: "İlk Yarı Sonucu",
HT_OU05: "İY Üst/Alt 0.5",
HT_OU15: "İY Üst/Alt 1.5",
HTFT: "İlk Yarı / Maç Sonu",
OE: "Tek / Çift",
CARDS: "Kart Üst/Alt",
HCAP: "Handikap",
};
const calibrationLabel = (c: string): { text: string; color: string } => {
if (c === "good") return { text: "Dürüst", color: "green" };
if (c === "overconfident") return { text: "Fazla iyimser", color: "red" };
return { text: "Düşük tahmin", color: "orange" };
};
const roiColor = (roi: number): string =>
roi > 3 ? "green" : roi < -3 ? "red" : "gray";
export default function ModelPerformanceContent() {
const [days, setDays] = useState(90);
const { data, isLoading } = useModelPerformance(days);
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
const headBg = useColorModeValue("gray.50", "whiteAlpha.50");
const perf = data?.data;
const markets = perf?.markets ?? [];
return (
<VStack align="stretch" gap={6}>
<Flex justify="space-between" align="center" flexWrap="wrap" gap={3}>
<Box>
<Heading size="md">Model Performansı (İleri Test)</Heading>
<Text fontSize="sm" color="fg.muted">
Her market için &quot;model %X dedi gerçekte %Y oldu&quot;.
Sonuçlanmış gerçek maçlardan otomatik hesaplanır (lookahead yok).
</Text>
</Box>
<NativeSelectRoot maxW="180px">
<NativeSelectField
value={String(days)}
onChange={(e) => setDays(Number(e.target.value))}
>
<option value="30">Son 30 gün</option>
<option value="90">Son 90 gün</option>
<option value="180">Son 180 gün</option>
<option value="365">Son 1 yıl</option>
</NativeSelectField>
</NativeSelectRoot>
</Flex>
{isLoading ? (
<Flex justify="center" py={12}>
<Spinner size="lg" />
</Flex>
) : !perf || markets.length === 0 ? (
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body>
<VStack gap={2} py={8}>
<Text fontWeight="semibold">Henüz yeterli sonuçlanmış veri yok</Text>
<Text fontSize="sm" color="fg.muted" textAlign="center">
Tahminler maçlar oynandıkça sonuçlanır. Güvenilir kalibrasyon
için market başına ~100 sonuçlanmış tahmin gerekir; bu birkaç
hafta içinde birikir.
</Text>
</VStack>
</Card.Body>
</Card.Root>
) : (
<>
{/* Summary cards */}
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4}>
<StatCard
icon={<LuLayers />}
label="Sonuçlanan tahmin"
value={perf.settled_runs}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuTarget />}
label="Sonuçlanan market"
value={perf.settled_markets}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuCircleCheck />}
label="Dürüst market"
value={markets.filter((m) => m.calibration === "good").length}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuTrendingUp />}
label="Kârlı market (BET)"
value={markets.filter((m) => m.bet_roi_pct > 3).length}
bg={cardBg}
border={borderColor}
/>
</SimpleGrid>
{/* Calibration + ROI table */}
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body p={0}>
<Box overflowX="auto">
<Table.Root size="sm" striped>
<Table.Header bg={headBg}>
<Table.Row>
<Table.ColumnHeader>Market</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">N</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Model %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Gerçek %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Fark
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="center">
Kalibrasyon
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Bahis
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
İsabet %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
ROI %
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{markets.map((m) => {
const cal = calibrationLabel(m.calibration);
return (
<Table.Row key={m.market}>
<Table.Cell>
<VStack align="start" gap={0}>
<Text fontWeight="semibold">{m.market}</Text>
<Text fontSize="xs" color="fg.muted">
{MARKET_LABELS[m.market] ?? m.market}
</Text>
</VStack>
</Table.Cell>
<Table.Cell textAlign="end">{m.samples}</Table.Cell>
<Table.Cell textAlign="end">
{m.shown_pct.toFixed(1)}%
</Table.Cell>
<Table.Cell textAlign="end" fontWeight="semibold">
{m.actual_pct.toFixed(1)}%
</Table.Cell>
<Table.Cell textAlign="end">
<Text
color={`${cal.color}.500`}
fontWeight="medium"
>
{m.gap > 0 ? "+" : ""}
{m.gap.toFixed(1)}
</Text>
</Table.Cell>
<Table.Cell textAlign="center">
<Badge colorPalette={cal.color} variant="subtle">
{cal.text}
</Badge>
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count}
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count > 0
? `${m.bet_hit_pct.toFixed(0)}%`
: "—"}
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count > 0 ? (
<Text
color={`${roiColor(m.bet_roi_pct)}.500`}
fontWeight="semibold"
>
{m.bet_roi_pct > 0 ? "+" : ""}
{m.bet_roi_pct.toFixed(1)}%
</Text>
) : (
"—"
)}
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
</Box>
</Card.Body>
</Card.Root>
{/* Decision rationale (why picks come out) */}
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body>
<Heading size="sm" mb={1}>
Öneri gerekçesi (karar dağılımı)
</Heading>
<Text fontSize="sm" color="fg.muted" mb={4}>
Her market için betting brain&apos;in ne karar verdiği:
BET (oyna), WATCH (izle), REJECT (ele).
</Text>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={3}>
{markets.map((m) => (
<Box
key={m.market}
p={3}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
>
<HStack justify="space-between" mb={2}>
<Text fontWeight="semibold">{m.market}</Text>
<Text fontSize="xs" color="fg.muted">
{MARKET_LABELS[m.market] ?? ""}
</Text>
</HStack>
<HStack gap={1} flexWrap="wrap">
{Object.entries(m.actions)
.sort((a, b) => b[1] - a[1])
.map(([action, count]) => (
<Badge
key={action}
variant="subtle"
colorPalette={
action === "BET"
? "green"
: action === "WATCH"
? "blue"
: action === "REJECT"
? "gray"
: "purple"
}
>
{action}: {count}
</Badge>
))}
</HStack>
</Box>
))}
</SimpleGrid>
</Card.Body>
</Card.Root>
<Text fontSize="xs" color="fg.muted">
Son güncelleme:{" "}
{perf.generated_at
? new Date(perf.generated_at).toLocaleString("tr-TR")
: "—"}{" "}
· Pencere: {perf.window_days} gün · ECE düşük = daha dürüst
</Text>
</>
)}
</VStack>
);
}
function StatCard({
icon,
label,
value,
bg,
border,
}: {
icon: React.ReactNode;
label: string;
value: number;
bg: string;
border: string;
}) {
return (
<Card.Root bg={bg} borderColor={border}>
<Card.Body>
<HStack gap={3}>
<Box color="primary.400" fontSize="xl">
{icon}
</Box>
<VStack align="start" gap={0}>
<Text fontSize="2xl" fontWeight="bold">
<AnimatedCounter value={value} />
</Text>
<Text fontSize="xs" color="fg.muted">
{label}
</Text>
</VStack>
</HStack>
</Card.Body>
</Card.Root>
);
}
@@ -1239,8 +1239,53 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
marketProbability: uiText("market-probability-short", "Piyasa"),
};
const leagueConfidence = prediction.match_info?.league_confidence;
const leagueConfStyles: Record<string, { color: string; label: string }> = {
high: {
color: "green",
label: uiText("league-conf-high", "Bu ligde model güçlü"),
},
medium: {
color: "yellow",
label: uiText("league-conf-medium", "Bu ligde model orta"),
},
low: {
color: "red",
label: uiText("league-conf-low", "Bu ligde model zayıf"),
},
};
const leagueConfMeta = leagueConfidence
? leagueConfStyles[leagueConfidence.label]
: null;
return (
<VStack align="stretch" gap={5}>
{leagueConfidence && leagueConfMeta ? (
<HStack
justify="space-between"
p={2.5}
px={3}
borderWidth="1px"
borderColor={`${leagueConfMeta.color}.300`}
bg={`${leagueConfMeta.color}.50`}
borderRadius="lg"
_dark={{ bg: `${leagueConfMeta.color}.950` }}
flexWrap="wrap"
gap={2}
>
<HStack gap={2}>
<Badge colorPalette={leagueConfMeta.color} variant="solid">
{leagueConfMeta.label}
</Badge>
<Text fontSize="xs" color="fg.muted">
{uiText("league-conf-basis", "geçmiş performans")}: ROI{" "}
{leagueConfidence.bet_roi > 0 ? "+" : ""}
{leagueConfidence.bet_roi}% · {leagueConfidence.bet_n}{" "}
{uiText("bets-short", "bahis")}
</Text>
</HStack>
</HStack>
) : null}
{isLive ? (
<Box
p={3}
+12
View File
@@ -7,6 +7,7 @@ import type {
AnalyticsOverviewDto,
LeagueTierDto,
LeagueTierStatsDto,
ModelPerformanceDto,
RetrainStatusDto,
SettingDto,
UpdateLeagueTierDto,
@@ -30,6 +31,16 @@ const getAnalyticsOverview = () => {
});
};
// Model Performance (forward-test)
const getModelPerformance = (days?: number) => {
return apiRequest<ApiResponse<ModelPerformanceDto>>({
url: "/admin/model-performance",
client: "admin",
method: "get",
params: days ? { days: String(days) } : undefined,
});
};
// Settings
const getAllSettings = () => {
return apiRequest<ApiResponse<Record<string, string>>>({
@@ -204,6 +215,7 @@ const getRetrainStatus = () => {
export const adminService = {
getAnalyticsOverview,
getModelPerformance,
getAllSettings,
updateSetting,
getAllUsageLimits,
+27
View File
@@ -134,3 +134,30 @@ export interface AnalyticsOverviewDto {
aiHealth?: Record<string, unknown>;
[key: string]: unknown;
}
// ========================
// Model Performance (Forward-Test)
// ========================
export interface ModelPerformanceMarketDto {
market: string;
samples: number;
shown_pct: number;
actual_pct: number;
gap: number;
ece: number;
calibration: "good" | "overconfident" | "underconfident";
bet_count: number;
bet_hit_pct: number;
bet_roi_pct: number;
actions: Record<string, number>;
tiers: Record<string, number>;
}
export interface ModelPerformanceDto {
window_days: number;
settled_runs: number;
settled_markets: number;
generated_at: string;
markets: ModelPerformanceMarketDto[];
}
+11
View File
@@ -12,6 +12,8 @@ import type {
export const AdminQueryKeys = {
all: ["admin"] as const,
analytics: () => [...AdminQueryKeys.all, "analytics"] as const,
modelPerformance: (days?: number) =>
[...AdminQueryKeys.all, "modelPerformance", days] as const,
settings: () => [...AdminQueryKeys.all, "settings"] as const,
usageLimits: (params?: AdminPaginationParams) =>
[...AdminQueryKeys.all, "usageLimits", params] as const,
@@ -32,6 +34,15 @@ export const useAdminAnalytics = (enabled = true) => {
});
};
// Model Performance (forward-test)
export const useModelPerformance = (days = 90, enabled = true) => {
return useQuery({
queryKey: AdminQueryKeys.modelPerformance(days),
queryFn: () => adminService.getModelPerformance(days),
enabled,
});
};
// Settings
export const useAdminSettings = () => {
return useQuery({
+7
View File
@@ -13,6 +13,13 @@ export interface MatchInfoDto {
match_date_ms: number;
league_id?: string | null;
is_top_league?: boolean;
// Backtest-derived per-league confidence (ROI + sample size). null = too little data.
league_confidence?: {
label: "high" | "medium" | "low";
bet_roi: number;
bet_n: number;
hit: number;
} | null;
sport?: SportType;
// Live snapshot — set when the match is in play (used to detect stale predictions)
status?: string | null;