updated build
This commit is contained in:
parent
5a3faaf1fe
commit
15d369b76f
@ -1041,6 +1041,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={getTeamLogo(teamLeft?.logo)}
|
||||
alt={teamLeft?.name ?? 'Team'}
|
||||
className="w-10 h-10 rounded-full border bg-white dark:bg-neutral-900 object-contain"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
) : <div className="w-10 h-10" />}
|
||||
|
||||
@ -1263,6 +1265,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={bg}
|
||||
alt={label}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/80 via-black/65 to-black/80" />
|
||||
@ -1274,6 +1278,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
alt="Picker-Team"
|
||||
className={`absolute ${cornerPos} w-6 h-6 rounded-full object-contain bg-white/90 border border-white/70 shadow-sm`}
|
||||
style={{ zIndex: 25 }}
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1303,6 +1309,8 @@ export default function MapVotePanel({ match }: Props) {
|
||||
src={mapLogo}
|
||||
alt={label}
|
||||
className="max-h-[70%] max-w-[88%] object-contain drop-shadow-lg"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<span className="px-2 py-0.5 rounded-md text-white/90 font-semibold text-xs md:text-sm">
|
||||
{label}
|
||||
|
||||
@ -30,9 +30,9 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// Panel-URL aus ENV (wie in mapvote)
|
||||
const panelBase =
|
||||
(process.env.PTERODACTYL_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL ?? '').trim()
|
||||
(process.env.PTERO_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERO_PANEL_URL ?? '').trim()
|
||||
if (!panelBase) {
|
||||
return NextResponse.json({ error: 'PTERODACTYL_PANEL_URL not set' }, { status: 500 })
|
||||
return NextResponse.json({ error: 'PTERO_PANEL_URL not set' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Server-ID aus DB (optional via Body überschreibbar)
|
||||
|
||||
@ -1,45 +1,119 @@
|
||||
// /src/app/api/matches/[matchid]/_builders.ts
|
||||
// /src/app/api/matches/[matchId]/_builders.ts
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
/* ---------- schlanke Typen für die Eingaben ---------- */
|
||||
type UserLike = {
|
||||
steamId: string
|
||||
name?: string | null
|
||||
avatar?: string | null
|
||||
location?: string | null
|
||||
premierRank?: number | null
|
||||
isAdmin?: boolean | null
|
||||
createdAt?: Date | null
|
||||
}
|
||||
|
||||
type TeamLike = {
|
||||
id?: string | null
|
||||
name?: string | null
|
||||
logo?: string | null
|
||||
leader?: UserLike | null
|
||||
}
|
||||
|
||||
type TeamUserLink = {
|
||||
steamId: string
|
||||
team?: { name?: string | null; logo?: string | null } | null
|
||||
}
|
||||
|
||||
type PlayerEntry = {
|
||||
steamId: string
|
||||
user: UserLike
|
||||
stats?: unknown
|
||||
team?: { name?: string | null } | null
|
||||
}
|
||||
|
||||
type BaseTimes = {
|
||||
matchDate?: Date | string | null
|
||||
demoDate?: Date | string | null
|
||||
createdAt?: Date | string | null
|
||||
}
|
||||
|
||||
type MatchCommunityFutureInput = BaseTimes & {
|
||||
id: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
matchType: string
|
||||
map?: string | null
|
||||
roundCount?: number | null
|
||||
scoreA?: number | null
|
||||
scoreB?: number | null
|
||||
teamA?: TeamLike | null
|
||||
teamB?: TeamLike | null
|
||||
teamAUsers: TeamUserLink[]
|
||||
teamBUsers: TeamUserLink[]
|
||||
}
|
||||
|
||||
type MatchDefaultInput = MatchCommunityFutureInput & {
|
||||
players: PlayerEntry[]
|
||||
}
|
||||
|
||||
/* ---------- View-Model-Helfer ---------- */
|
||||
export type PlayerVM = {
|
||||
steamId: string
|
||||
name: string
|
||||
avatar: string
|
||||
location: string | null
|
||||
premierRank: number | null
|
||||
isAdmin: boolean
|
||||
createdAt: Date | null
|
||||
}
|
||||
|
||||
/** Klein, konsistent, Frontend-freundlich */
|
||||
export function toPlayerVM(u: any) {
|
||||
export function toPlayerVM(u: UserLike): PlayerVM {
|
||||
return {
|
||||
steamId : u.steamId,
|
||||
name : u.name ?? 'Unbekannt',
|
||||
avatar : u.avatar ?? '/assets/img/avatars/default.png',
|
||||
location : u.location ?? null,
|
||||
steamId: u.steamId,
|
||||
name: u.name ?? 'Unbekannt',
|
||||
avatar: u.avatar ?? '/assets/img/avatars/default.png',
|
||||
location: u.location ?? null,
|
||||
premierRank: u.premierRank ?? null,
|
||||
isAdmin : u.isAdmin ?? false,
|
||||
createdAt : u.createdAt ?? null,
|
||||
isAdmin: !!u.isAdmin,
|
||||
createdAt: u.createdAt ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export const mapMP = (p: any) => ({
|
||||
user : toPlayerVM(p.user),
|
||||
export const mapMP = (p: { user: UserLike; stats?: unknown }) => ({
|
||||
user: toPlayerVM(p.user),
|
||||
stats: p.stats ?? null,
|
||||
})
|
||||
|
||||
/* ---------- Zeit-Helfer ---------- */
|
||||
/** Startzeit (matchDate || demoDate || createdAt) als Timestamp */
|
||||
export function computeStartTs(m: any) {
|
||||
export function computeStartTs(m: BaseTimes): number {
|
||||
const base = m.matchDate ?? m.demoDate ?? m.createdAt
|
||||
return new Date(base).getTime()
|
||||
const d =
|
||||
base instanceof Date
|
||||
? base
|
||||
: typeof base === 'string'
|
||||
? new Date(base)
|
||||
: new Date(0)
|
||||
return d.getTime()
|
||||
}
|
||||
|
||||
export function isFuture(m: any) {
|
||||
export function isFuture(m: BaseTimes): boolean {
|
||||
return computeStartTs(m) > Date.now()
|
||||
}
|
||||
|
||||
/* ---------- Payload-Builder ---------- */
|
||||
/**
|
||||
* COMMUNITY + ZUKUNFT:
|
||||
* – zeige den ganzen Roster aus teamAUsers / teamBUsers (ohne Stats),
|
||||
* damit man die Aufstellung bearbeiten kann.
|
||||
*/
|
||||
export async function buildCommunityFuturePayload(m: any) {
|
||||
export async function buildCommunityFuturePayload(m: MatchCommunityFutureInput) {
|
||||
const aIds: string[] = Array.from(
|
||||
new Set((m.teamAUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean))
|
||||
new Set((m.teamAUsers ?? []).map(u => String(u?.steamId)).filter(Boolean))
|
||||
)
|
||||
const bIds: string[] = Array.from(
|
||||
new Set((m.teamBUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean))
|
||||
new Set((m.teamBUsers ?? []).map(u => String(u?.steamId)).filter(Boolean))
|
||||
)
|
||||
const ids: string[] = [...aIds, ...bIds]
|
||||
|
||||
@ -49,8 +123,7 @@ export async function buildCommunityFuturePayload(m: any) {
|
||||
})
|
||||
: []
|
||||
|
||||
type PlayerVM = ReturnType<typeof toPlayerVM>
|
||||
const byId = new Map<string, PlayerVM>(users.map(u => [u.steamId, toPlayerVM(u)]))
|
||||
const byId = new Map<string, PlayerVM>(users.map(u => [u.steamId, toPlayerVM(u as unknown as UserLike)]))
|
||||
|
||||
const mapToPlayers = (list: string[]) =>
|
||||
list
|
||||
@ -65,32 +138,41 @@ export async function buildCommunityFuturePayload(m: any) {
|
||||
const startTs = computeStartTs(m)
|
||||
const editableUntil = startTs - 60 * 60 * 1000 // 1h vor Start/Vote
|
||||
|
||||
const firstA = m.teamAUsers?.[0]
|
||||
const firstB = m.teamBUsers?.[0]
|
||||
|
||||
return {
|
||||
id : m.id,
|
||||
title : m.title,
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
description: m.description ?? null,
|
||||
matchType : m.matchType,
|
||||
map : m.map ?? null,
|
||||
matchType: m.matchType,
|
||||
map: m.map ?? null,
|
||||
roundCount: m.roundCount ?? null,
|
||||
scoreA : m.scoreA ?? null,
|
||||
scoreB : m.scoreB ?? null,
|
||||
demoDate : m.demoDate ?? null,
|
||||
matchDate : (m.matchDate ?? m.demoDate ?? m.createdAt)?.toISOString?.() ?? null,
|
||||
scoreA: m.scoreA ?? null,
|
||||
scoreB: m.scoreB ?? null,
|
||||
demoDate: m.demoDate ?? null,
|
||||
matchDate:
|
||||
(m.matchDate ?? m.demoDate ?? m.createdAt) &&
|
||||
(m.matchDate ?? m.demoDate ?? m.createdAt) instanceof Date
|
||||
? (m.matchDate as Date)?.toISOString?.() ??
|
||||
(m.demoDate as Date)?.toISOString?.() ??
|
||||
(m.createdAt as Date)?.toISOString?.()
|
||||
: null,
|
||||
editableUntil,
|
||||
|
||||
teamA: {
|
||||
id : m.teamA?.id ?? null,
|
||||
name : m.teamA?.name ?? m.teamAUsers[0]?.team?.name ?? 'Team A',
|
||||
logo : m.teamA?.logo ?? m.teamAUsers[0]?.team?.logo ?? null,
|
||||
score : m.scoreA ?? null,
|
||||
id: m.teamA?.id ?? null,
|
||||
name: m.teamA?.name ?? firstA?.team?.name ?? 'Team A',
|
||||
logo: m.teamA?.logo ?? firstA?.team?.logo ?? null,
|
||||
score: m.scoreA ?? null,
|
||||
leader: m.teamA?.leader ? toPlayerVM(m.teamA.leader) : undefined,
|
||||
players: teamAPlayers,
|
||||
},
|
||||
teamB: {
|
||||
id : m.teamB?.id ?? null,
|
||||
name : m.teamB?.name ?? m.teamBUsers[0]?.team?.name ?? 'Team B',
|
||||
logo : m.teamB?.logo ?? m.teamBUsers[0]?.team?.logo ?? null,
|
||||
score : m.scoreB ?? null,
|
||||
id: m.teamB?.id ?? null,
|
||||
name: m.teamB?.name ?? firstB?.team?.name ?? 'Team B',
|
||||
logo: m.teamB?.logo ?? firstB?.team?.logo ?? null,
|
||||
score: m.scoreB ?? null,
|
||||
leader: m.teamB?.leader ? toPlayerVM(m.teamB.leader) : undefined,
|
||||
players: teamBPlayers,
|
||||
},
|
||||
@ -102,53 +184,58 @@ export async function buildCommunityFuturePayload(m: any) {
|
||||
* – zeige nur die Spieler, die dem Match zugeordnet sind (über teamAUsers/BUsers gematcht).
|
||||
* – Stats bleiben erhalten (kommen aus m.players.include.stats).
|
||||
*/
|
||||
export function buildDefaultPayload(m: any) {
|
||||
const teamAIds = new Set(m.teamAUsers.map((u: any) => u.steamId))
|
||||
const teamBIds = new Set(m.teamBUsers.map((u: any) => u.steamId))
|
||||
export function buildDefaultPayload(m: MatchDefaultInput) {
|
||||
const teamAIds = new Set((m.teamAUsers ?? []).map(u => u.steamId))
|
||||
const teamBIds = new Set((m.teamBUsers ?? []).map(u => u.steamId))
|
||||
|
||||
const playersA = (m.players ?? [])
|
||||
.filter((p: any) => teamAIds.has(p.steamId))
|
||||
.map((p: any) => ({
|
||||
.filter(p => teamAIds.has(p.steamId))
|
||||
.map(p => ({
|
||||
user: toPlayerVM(p.user),
|
||||
stats: p.stats ?? null,
|
||||
team: p.team?.name ?? 'Team A',
|
||||
}))
|
||||
|
||||
const playersB = (m.players ?? [])
|
||||
.filter((p: any) => teamBIds.has(p.steamId))
|
||||
.map((p: any) => ({
|
||||
.filter(p => teamBIds.has(p.steamId))
|
||||
.map(p => ({
|
||||
user: toPlayerVM(p.user),
|
||||
stats: p.stats ?? null,
|
||||
team: p.team?.name ?? 'Team B',
|
||||
}))
|
||||
|
||||
const matchDate = m.demoDate ?? m.matchDate ?? m.createdAt
|
||||
const baseDate = m.demoDate ?? m.matchDate ?? m.createdAt
|
||||
const iso =
|
||||
baseDate instanceof Date ? baseDate.toISOString() : null
|
||||
|
||||
const firstA = m.teamAUsers?.[0]
|
||||
const firstB = m.teamBUsers?.[0]
|
||||
|
||||
return {
|
||||
id : m.id,
|
||||
title : m.title,
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
description: m.description ?? null,
|
||||
matchType : m.matchType,
|
||||
map : m.map ?? null,
|
||||
matchType: m.matchType,
|
||||
map: m.map ?? null,
|
||||
roundCount: m.roundCount ?? null,
|
||||
scoreA : m.scoreA ?? null,
|
||||
scoreB : m.scoreB ?? null,
|
||||
demoDate : m.demoDate ?? null,
|
||||
matchDate : matchDate?.toISOString?.() ?? null,
|
||||
scoreA: m.scoreA ?? null,
|
||||
scoreB: m.scoreB ?? null,
|
||||
demoDate: m.demoDate ?? null,
|
||||
matchDate: iso,
|
||||
|
||||
teamA: {
|
||||
id : m.teamA?.id ?? null,
|
||||
name : m.teamA?.name ?? m.teamAUsers[0]?.team?.name ?? 'Team A',
|
||||
logo : m.teamA?.logo ?? m.teamAUsers[0]?.team?.logo ?? null,
|
||||
score : m.scoreA ?? null,
|
||||
id: m.teamA?.id ?? null,
|
||||
name: m.teamA?.name ?? firstA?.team?.name ?? 'Team A',
|
||||
logo: m.teamA?.logo ?? firstA?.team?.logo ?? null,
|
||||
score: m.scoreA ?? null,
|
||||
leader: m.teamA?.leader ? toPlayerVM(m.teamA.leader) : undefined,
|
||||
players: playersA,
|
||||
},
|
||||
teamB: {
|
||||
id : m.teamB?.id ?? null,
|
||||
name : m.teamB?.name ?? m.teamBUsers[0]?.team?.name ?? 'Team B',
|
||||
logo : m.teamB?.logo ?? m.teamBUsers[0]?.team?.logo ?? null,
|
||||
score : m.scoreB ?? null,
|
||||
id: m.teamB?.id ?? null,
|
||||
name: m.teamB?.name ?? firstB?.team?.name ?? 'Team B',
|
||||
logo: m.teamB?.logo ?? firstB?.team?.logo ?? null,
|
||||
score: m.scoreB ?? null,
|
||||
leader: m.teamB?.leader ? toPlayerVM(m.teamB.leader) : undefined,
|
||||
players: playersB,
|
||||
},
|
||||
|
||||
@ -14,11 +14,77 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
type Ctx = { params: Promise<{ matchId: string }> } // 🔹 gemeinsamer Ctx-Typ
|
||||
|
||||
type UserLike = {
|
||||
steamId: string
|
||||
name?: string | null
|
||||
avatar?: string | null
|
||||
location?: string | null
|
||||
premierRank?: number | null
|
||||
isAdmin?: boolean | null
|
||||
}
|
||||
|
||||
type TeamLike = {
|
||||
id: string
|
||||
name?: string | null
|
||||
logo?: string | null
|
||||
leaderId?: string | null
|
||||
leader?: UserLike | null
|
||||
}
|
||||
|
||||
type TeamUser = UserLike & {
|
||||
team?: { id?: string | null; name?: string | null; logo?: string | null } | null
|
||||
}
|
||||
|
||||
type VoteStepDb = {
|
||||
id: string
|
||||
order: number
|
||||
action: MapVoteAction
|
||||
teamId: string | null
|
||||
map: string | null
|
||||
chosenAt: Date | null
|
||||
chosenBy: string | null
|
||||
}
|
||||
|
||||
type VoteDb = {
|
||||
id: string
|
||||
matchId: string
|
||||
bestOf: number
|
||||
mapPool: string[]
|
||||
currentIdx: number
|
||||
locked: boolean
|
||||
opensAt: Date | null
|
||||
adminEditingBy: string | null
|
||||
adminEditingSince: Date | null
|
||||
steps: VoteStepDb[]
|
||||
}
|
||||
|
||||
type MatchDb = {
|
||||
id: string
|
||||
matchType: string
|
||||
title?: string | null
|
||||
map?: string | null
|
||||
matchDate: Date | null
|
||||
demoDate: Date | null
|
||||
createdAt: Date
|
||||
teamA: (TeamLike & { leader?: UserLike | null }) | null
|
||||
teamB: (TeamLike & { leader?: UserLike | null }) | null
|
||||
teamAUsers: TeamUser[]
|
||||
teamBUsers: TeamUser[]
|
||||
players: Array<{ steamId: string; user: UserLike }>
|
||||
mapVote?: VoteDb | null
|
||||
}
|
||||
|
||||
type MapVisual = { label: string; bg: string; logo?: string }
|
||||
|
||||
|
||||
/* -------------------- Konstanten -------------------- */
|
||||
|
||||
const ACTION_MAP: Record<MapVoteAction, 'ban'|'pick'|'decider'> = {
|
||||
BAN: 'ban', PICK: 'pick', DECIDER: 'decider',
|
||||
}
|
||||
// --- kleine Utils
|
||||
const toIso = (d?: Date | null) => (d ? d.toISOString() : null)
|
||||
const byOrder = <T extends { order: number }>(a: T, b: T) => a.order - b.order
|
||||
|
||||
/* -------------------- Helper -------------------- */
|
||||
|
||||
@ -53,11 +119,11 @@ function buildPteroClientUrl(base: string, serverId: string) {
|
||||
async function sendServerCommand(command: string) {
|
||||
try {
|
||||
const panelBase =
|
||||
process.env.PTERODACTYL_PANEL_URL ||
|
||||
process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL ||
|
||||
process.env.PTERO_PANEL_URL ||
|
||||
process.env.NEXT_PUBLIC_PTERO_PANEL_URL ||
|
||||
''
|
||||
if (!panelBase) {
|
||||
console.warn('[mapvote] PTERODACTYL_PANEL_URL fehlt – Command wird nicht gesendet.')
|
||||
console.warn('[mapvote] PTERO_PANEL_URL fehlt – Command wird nicht gesendet.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -152,90 +218,90 @@ function buildSteps(bestOf: number, teamAId: string, teamBId: string) {
|
||||
] as const
|
||||
}
|
||||
|
||||
function shapeState(vote: any, match?: any) {
|
||||
const steps = [...vote.steps]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((s: any) => ({
|
||||
order : s.order,
|
||||
action : mapActionToApi(s.action),
|
||||
teamId : s.teamId,
|
||||
map : s.map,
|
||||
chosenAt: s.chosenAt ? s.chosenAt.toISOString() : null,
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
}))
|
||||
function shapeState(vote: VoteDb, match?: Pick<MatchDb, 'matchDate' | 'demoDate'>) {
|
||||
const steps = [...vote.steps].sort(byOrder).map(s => ({
|
||||
order: s.order,
|
||||
action: mapActionToApi(s.action),
|
||||
teamId: s.teamId,
|
||||
map: s.map,
|
||||
chosenAt: toIso(s.chosenAt),
|
||||
chosenBy: s.chosenBy ?? null,
|
||||
}))
|
||||
|
||||
const opensAtDate = vote.opensAt ? new Date(vote.opensAt) : null
|
||||
const baseDate = match?.matchDate ?? match?.demoDate ?? null
|
||||
const baseDate = match?.matchDate ?? match?.demoDate ?? null
|
||||
const leadMinutes =
|
||||
opensAtDate && baseDate
|
||||
? Math.max(0, Math.round((new Date(baseDate).getTime() - opensAtDate.getTime()) / 60000))
|
||||
? Math.max(0, Math.round(((baseDate as Date).getTime() - opensAtDate.getTime()) / 60000))
|
||||
: null
|
||||
|
||||
return {
|
||||
bestOf : vote.bestOf,
|
||||
mapPool : vote.mapPool as string[],
|
||||
bestOf: vote.bestOf,
|
||||
mapPool: vote.mapPool,
|
||||
currentIndex: vote.currentIdx,
|
||||
locked : vote.locked as boolean,
|
||||
opensAt : opensAtDate ? opensAtDate.toISOString() : null,
|
||||
locked: vote.locked,
|
||||
opensAt: toIso(vote.opensAt),
|
||||
leadMinutes,
|
||||
steps,
|
||||
adminEdit: vote.adminEditingBy
|
||||
? { enabled: true, by: vote.adminEditingBy as string,
|
||||
since: vote.adminEditingSince ? new Date(vote.adminEditingSince).toISOString() : null }
|
||||
? {
|
||||
enabled: true,
|
||||
by: vote.adminEditingBy,
|
||||
since: toIso(vote.adminEditingSince),
|
||||
}
|
||||
: { enabled: false, by: null, since: null },
|
||||
}
|
||||
}
|
||||
|
||||
// Leader -> Player-Shape fürs Frontend
|
||||
function shapeLeader(leader: any | null) {
|
||||
function shapeLeader(leader: UserLike | null | undefined) {
|
||||
if (!leader) return null
|
||||
return {
|
||||
steamId : leader.steamId,
|
||||
name : leader.name ?? '',
|
||||
avatar : leader.avatar ?? '',
|
||||
location : leader.location ?? undefined,
|
||||
steamId: leader.steamId,
|
||||
name: leader.name ?? '',
|
||||
avatar: leader.avatar ?? '',
|
||||
location: leader.location ?? undefined,
|
||||
premierRank: leader.premierRank ?? undefined,
|
||||
isAdmin : leader.isAdmin ?? undefined,
|
||||
isAdmin: leader.isAdmin ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Teams-Payload (mit Spielern) zusammenbauen
|
||||
function shapeUser(u: any) {
|
||||
function shapeUser(u: TeamUser | null | undefined) {
|
||||
if (!u) return null
|
||||
return {
|
||||
steamId : u.steamId,
|
||||
name : u.name ?? '',
|
||||
avatar : u.avatar ?? '',
|
||||
location : u.location ?? undefined,
|
||||
steamId: u.steamId,
|
||||
name: u.name ?? '',
|
||||
avatar: u.avatar ?? '',
|
||||
location: u.location ?? undefined,
|
||||
premierRank: u.premierRank ?? undefined,
|
||||
isAdmin : u.isAdmin ?? undefined,
|
||||
isAdmin: u.isAdmin ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function buildTeamsPayloadFromMatch(match: any) {
|
||||
const teamAPlayers = (match.teamAUsers ?? []).map(shapeUser).filter(Boolean)
|
||||
const teamBPlayers = (match.teamBUsers ?? []).map(shapeUser).filter(Boolean)
|
||||
function buildTeamsPayloadFromMatch(match: MatchDb) {
|
||||
const teamAPlayers = (match.teamAUsers ?? []).map(shapeUser).filter(Boolean) as Array<ReturnType<typeof shapeUser>>
|
||||
const teamBPlayers = (match.teamBUsers ?? []).map(shapeUser).filter(Boolean) as Array<ReturnType<typeof shapeUser>>
|
||||
|
||||
return {
|
||||
teamA: {
|
||||
id : match.teamA?.id ?? null,
|
||||
name : match.teamA?.name ?? null,
|
||||
logo : match.teamA?.logo ?? null,
|
||||
leader : shapeLeader(match.teamA?.leader ?? null),
|
||||
id: match.teamA?.id ?? null,
|
||||
name: match.teamA?.name ?? null,
|
||||
logo: match.teamA?.logo ?? null,
|
||||
leader: shapeLeader(match.teamA?.leader ?? null),
|
||||
players: teamAPlayers,
|
||||
},
|
||||
teamB: {
|
||||
id : match.teamB?.id ?? null,
|
||||
name : match.teamB?.name ?? null,
|
||||
logo : match.teamB?.logo ?? null,
|
||||
leader : shapeLeader(match.teamB?.leader ?? null),
|
||||
id: match.teamB?.id ?? null,
|
||||
name: match.teamB?.name ?? null,
|
||||
logo: match.teamB?.logo ?? null,
|
||||
leader: shapeLeader(match.teamB?.leader ?? null),
|
||||
players: teamBPlayers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureVote(matchId: string) {
|
||||
const match = await prisma.match.findUnique({
|
||||
async function ensureVote(matchId: string): Promise<{ match: MatchDb | null; vote: VoteDb | null }> {
|
||||
const match = (await prisma.match.findUnique({
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
teamA: { include: { leader: { select: { steamId: true, name: true, avatar: true, location: true, premierRank: true, isAdmin: true } } } },
|
||||
@ -245,47 +311,35 @@ async function ensureVote(matchId: string) {
|
||||
players: { include: { user: true } },
|
||||
mapVote: { include: { steps: true } },
|
||||
},
|
||||
});
|
||||
})) as unknown as MatchDb | null
|
||||
|
||||
if (!match) return { match: null, vote: null };
|
||||
if (!match) return { match: null, vote: null }
|
||||
|
||||
// Aktive Maps lt. Konfiguration (ohne Lobby)
|
||||
const CURRENT_ACTIVE = MAP_OPTIONS
|
||||
.filter(m => m.active && m.key !== 'lobby_mapvote')
|
||||
.map(m => m.key);
|
||||
const CURRENT_ACTIVE = MAP_OPTIONS.filter(m => m.active && m.key !== 'lobby_mapvote').map(m => m.key)
|
||||
|
||||
if (match.mapVote) {
|
||||
let vote = match.mapVote;
|
||||
|
||||
// bereits gewählte Maps im Pool behalten (auch wenn jetzt inaktiv)
|
||||
const chosen = vote.steps.map(s => s.map).filter(Boolean) as string[];
|
||||
const desiredPool = Array.from(new Set([...CURRENT_ACTIVE, ...chosen]));
|
||||
|
||||
// Pool ggf. aktualisieren
|
||||
if (
|
||||
desiredPool.length !== vote.mapPool.length ||
|
||||
desiredPool.some(k => !vote.mapPool.includes(k))
|
||||
) {
|
||||
vote = await prisma.mapVote.update({
|
||||
let vote = match.mapVote
|
||||
const chosen = vote.steps.map(s => s.map).filter(Boolean) as string[]
|
||||
const desiredPool = Array.from(new Set([...CURRENT_ACTIVE, ...chosen]))
|
||||
if (desiredPool.length !== vote.mapPool.length || desiredPool.some(k => !vote.mapPool.includes(k))) {
|
||||
vote = (await prisma.mapVote.update({
|
||||
where: { id: vote.id },
|
||||
data: { mapPool: desiredPool },
|
||||
include: { steps: true },
|
||||
});
|
||||
})) as unknown as VoteDb
|
||||
}
|
||||
return { match, vote };
|
||||
return { match, vote }
|
||||
}
|
||||
|
||||
// Neu anlegen
|
||||
const bestOf = match.matchType === 'community' ? 3 : 1;
|
||||
const mapPool = CURRENT_ACTIVE; // neu: ohne Lobby & nur aktuelle Aktive
|
||||
const opensAt = voteOpensAt({ matchDate: match.matchDate ?? null, demoDate: match.demoDate ?? null });
|
||||
const bestOf = match.matchType === 'community' ? 3 : 1
|
||||
const mapPool = CURRENT_ACTIVE
|
||||
const opensAt = voteOpensAt({ matchDate: match.matchDate, demoDate: match.demoDate })
|
||||
const firstIsA = typeof randomInt === 'function' ? randomInt(0, 2) === 0 : Math.random() < 0.5
|
||||
const firstTeamId = firstIsA ? match.teamA!.id : match.teamB!.id
|
||||
const secondTeamId = firstIsA ? match.teamB!.id : match.teamA!.id
|
||||
const stepsDef = buildSteps(bestOf, firstTeamId, secondTeamId)
|
||||
|
||||
const firstIsA = (typeof randomInt === 'function') ? randomInt(0, 2) === 0 : Math.random() < 0.5;
|
||||
const firstTeamId = firstIsA ? match.teamA!.id : match.teamB!.id;
|
||||
const secondTeamId = firstIsA ? match.teamB!.id : match.teamA!.id;
|
||||
const stepsDef = buildSteps(bestOf, firstTeamId, secondTeamId);
|
||||
|
||||
const created = await prisma.mapVote.create({
|
||||
const created = (await prisma.mapVote.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
bestOf,
|
||||
@ -296,9 +350,9 @@ async function ensureVote(matchId: string) {
|
||||
steps: { create: stepsDef.map(s => ({ order: s.order, action: s.action as MapVoteAction, teamId: s.teamId })) },
|
||||
},
|
||||
include: { steps: true },
|
||||
});
|
||||
})) as unknown as VoteDb
|
||||
|
||||
return { match, vote: created };
|
||||
return { match, vote: created }
|
||||
}
|
||||
|
||||
function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | null }>) {
|
||||
@ -308,7 +362,7 @@ function computeAvailableMaps(mapPool: string[], steps: Array<{ map: string | nu
|
||||
|
||||
/* ---------- Visuals: deterministisches zufälliges Bild pro Map & Match ---------- */
|
||||
|
||||
function buildMapVisuals(matchId: string, mapPool: string[]) {
|
||||
function buildMapVisuals(matchId: string, mapPool: string[]): Record<string, MapVisual> {
|
||||
const visuals: Record<string, { label: string; bg: string; logo?: string }> = {}
|
||||
for (const key of mapPool) {
|
||||
const opt = MAP_OPTIONS.find(o => o.key === key)
|
||||
@ -327,72 +381,55 @@ function buildMapVisuals(matchId: string, mapPool: string[]) {
|
||||
return visuals
|
||||
}
|
||||
|
||||
|
||||
function uniq<T>(arr: T[]) {
|
||||
return Array.from(new Set(arr))
|
||||
}
|
||||
|
||||
function collectParticipants(match: any): string[] {
|
||||
const a = (match.teamAUsers ?? []).map((u: any) => u?.steamId).filter(Boolean)
|
||||
const b = (match.teamBUsers ?? []).map((u: any) => u?.steamId).filter(Boolean)
|
||||
// ❗ keine Leader/activePlayers/match.players mehr – NUR echte Teamspieler
|
||||
function collectParticipants(match: Pick<MatchDb, 'teamAUsers' | 'teamBUsers'>): string[] {
|
||||
const a = (match.teamAUsers ?? []).map(u => u.steamId).filter(Boolean)
|
||||
const b = (match.teamBUsers ?? []).map(u => u.steamId).filter(Boolean)
|
||||
return Array.from(new Set<string>([...a, ...b]))
|
||||
}
|
||||
|
||||
async function persistMatchPlayers(match: any) {
|
||||
// Teilnehmer ermitteln (du hast schon collectParticipants)
|
||||
const participants = collectParticipants(match); // string[] der steamIds
|
||||
async function persistMatchPlayers(match: Pick<MatchDb, 'id' | 'teamA' | 'teamB' | 'teamAUsers' | 'teamBUsers'>) {
|
||||
const participants = collectParticipants(match)
|
||||
const aIds = new Set((match.teamAUsers ?? []).map(u => u.steamId))
|
||||
const bIds = new Set((match.teamBUsers ?? []).map(u => u.steamId))
|
||||
|
||||
// teamId pro Spieler bestimmen (A oder B), sonst null
|
||||
const aIds = new Set((match.teamAUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean));
|
||||
const bIds = new Set((match.teamBUsers ?? []).map((u: any) => String(u?.steamId)).filter(Boolean));
|
||||
|
||||
const ops = participants.map((steamId) => {
|
||||
const onTeamA = aIds.has(String(steamId));
|
||||
const onTeamB = bIds.has(String(steamId));
|
||||
const teamId =
|
||||
onTeamA ? match.teamA?.id
|
||||
: onTeamB ? match.teamB?.id
|
||||
: null;
|
||||
|
||||
// Upsert je Spieler fürs Match
|
||||
const ops = participants.map(steamId => {
|
||||
const teamId =
|
||||
aIds.has(steamId) ? match.teamA?.id
|
||||
: bIds.has(steamId) ? match.teamB?.id
|
||||
: null
|
||||
return prisma.matchPlayer.upsert({
|
||||
where: { matchId_steamId: { matchId: match.id, steamId } },
|
||||
update: { teamId }, // falls sich die Team-Zuordnung ändert
|
||||
update: { teamId },
|
||||
create: { matchId: match.id, steamId, teamId },
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.$transaction(ops);
|
||||
})
|
||||
})
|
||||
await prisma.$transaction(ops)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ---------- Export-Helfer ---------- */
|
||||
|
||||
type PlayerLike = { user?: { steamId: string, name?: string | null }, steamId?: string, name?: string | null }
|
||||
type PlayerLike = { user?: { steamId: string; name?: string | null }; steamId?: string; name?: string | null }
|
||||
type MatchLike = {
|
||||
id: string | number
|
||||
bestOf?: number
|
||||
teamA?: { name?: string | null, players?: PlayerLike[] | any[] }
|
||||
teamB?: { name?: string | null, players?: PlayerLike[] | any[] }
|
||||
teamA?: { name?: string | null; players?: PlayerLike[] }
|
||||
teamB?: { name?: string | null; players?: PlayerLike[] }
|
||||
}
|
||||
type MapVoteStep = { action: 'ban' | 'pick' | 'decider', map?: string | null, teamId?: string | null }
|
||||
type MapVoteStateForExport = { bestOf?: number, steps: MapVoteStep[], locked?: boolean }
|
||||
type MapVoteStep = { action: 'ban' | 'pick' | 'decider'; map?: string | null; teamId?: string | null }
|
||||
type MapVoteStateForExport = { bestOf?: number; steps: MapVoteStep[]; locked?: boolean }
|
||||
|
||||
function sanitizeFilePart(s?: string | null) {
|
||||
return (s ?? 'team').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
||||
}
|
||||
function playersMapFromList(list: PlayerLike[] | any[] | undefined) {
|
||||
function playersMapFromList(list: PlayerLike[] | undefined) {
|
||||
const out: Record<string, string> = {}
|
||||
for (const p of list ?? []) {
|
||||
const sid = (p?.user?.steamId ?? (p as any)?.steamId) as string | undefined
|
||||
const sid = p.user?.steamId ?? p.steamId
|
||||
if (!sid) continue
|
||||
const name = (p?.user?.name ?? p?.name ?? 'Player') as string
|
||||
const name = p.user?.name ?? p.name ?? 'Player'
|
||||
out[sid] = name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function toDeMapName(key: string) {
|
||||
if (key.startsWith('de_')) return key
|
||||
return `de_${key}`
|
||||
@ -436,34 +473,30 @@ function buildMatchJson(match: MatchLike, state: MapVoteStateForExport) {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
async function exportMatchToSftpDirect(match: MatchDb, vote: VoteDb) {
|
||||
try {
|
||||
const SFTPClient = (await import('ssh2-sftp-client')).default
|
||||
const mLike: MatchLike = {
|
||||
id: match.id,
|
||||
bestOf: (vote?.bestOf ?? (match.matchType === 'community' ? 3 : 1)),
|
||||
bestOf: vote?.bestOf ?? (match.matchType === 'community' ? 3 : 1),
|
||||
teamA: { name: match.teamA?.name ?? 'Team_1', players: match.teamAUsers ?? [] },
|
||||
teamB: { name: match.teamB?.name ?? 'Team_2', players: match.teamBUsers ?? [] },
|
||||
}
|
||||
const sLike: MapVoteStateForExport = {
|
||||
bestOf: vote.bestOf,
|
||||
steps: [...vote.steps]
|
||||
.sort((a: any, b: any) => a.order - b.order)
|
||||
.map((s: any) => ({ action: mapActionToApi(s.action), map: s.map, teamId: s.teamId })),
|
||||
steps: [...vote.steps].sort(byOrder).map(s => ({ action: mapActionToApi(s.action), map: s.map, teamId: s.teamId })),
|
||||
locked: vote.locked,
|
||||
}
|
||||
|
||||
if (!sLike.locked) return
|
||||
const bestOf = (mLike.bestOf ?? sLike.bestOf ?? (match.matchType === 'community' ? 3 : 1))
|
||||
const bestOf = mLike.bestOf ?? sLike.bestOf ?? (match.matchType === 'community' ? 3 : 1)
|
||||
const chosen = (sLike.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||
if (chosen.length < bestOf) return false
|
||||
|
||||
// ⬇️ JSON bauen (enthält cs2MatchId/rndId)
|
||||
const json = buildMatchJson(mLike, sLike)
|
||||
const jsonStr = JSON.stringify(json, null, 2)
|
||||
const filename = `${match.id}.json`
|
||||
|
||||
// --- SFTP Upload wie gehabt ---
|
||||
const url = process.env.PTERO_SERVER_SFTP_URL || ''
|
||||
const user = process.env.PTERO_SERVER_SFTP_USER
|
||||
const pass = process.env.PTERO_SERVER_SFTP_PASSWORD
|
||||
@ -485,17 +518,13 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
|
||||
const sftp = new SFTPClient()
|
||||
await sftp.connect({ host, port, username: user, password: pass })
|
||||
|
||||
const remotePath = `/game/csgo/${filename}`
|
||||
await sftp.put(Buffer.from(jsonStr, 'utf8'), remotePath)
|
||||
await sftp.end()
|
||||
|
||||
console.log(`[mapvote] Export OK → ${remotePath}`)
|
||||
|
||||
// erst aktuelles Match beenden/entladen …
|
||||
await unloadCurrentMatch()
|
||||
|
||||
// … dann das neue laden
|
||||
await sendServerCommand(`matchzy_loadmatch ${filename}`)
|
||||
|
||||
if (typeof json.matchid === 'number') {
|
||||
@ -517,28 +546,25 @@ async function exportMatchToSftpDirect(match: any, vote: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function writeLiveStateToServerConfig(match: any, vote: any, mapVisuals: Record<string, any>) {
|
||||
async function writeLiveStateToServerConfig(match: MatchDb, _vote: VoteDb, mapVisuals: Record<string, MapVisual>) {
|
||||
try {
|
||||
// gewählte Maps (PICK/DECIDER), erste Map extrahieren
|
||||
const chosen = deriveChosenSteps(vote)
|
||||
const first = chosen[0]
|
||||
const key = first?.map ?? null
|
||||
const label = key ? (mapVisuals?.[key]?.label ?? key) : null
|
||||
const bg = key ? (mapVisuals?.[key]?.bg ?? `/assets/img/maps/${key}/1.jpg`) : null
|
||||
const chosen = deriveChosenSteps(_vote)
|
||||
const first = chosen[0]
|
||||
const key = first?.map ?? null
|
||||
const label = key ? (mapVisuals?.[key]?.label ?? key) : null
|
||||
const bg = key ? (mapVisuals?.[key]?.bg ?? `/assets/img/maps/${key}/1.jpg`) : null
|
||||
|
||||
const participants = collectParticipants(match)
|
||||
|
||||
// Wir gehen davon aus, dass es "default" gibt.
|
||||
await prisma.serverConfig.update({
|
||||
where: { id: 'default' },
|
||||
data: {
|
||||
activeMatchId: match.id,
|
||||
activeMapKey : key,
|
||||
activeMapKey: key,
|
||||
activeMapLabel: label,
|
||||
activeMapBg : bg,
|
||||
activeMapBg: bg,
|
||||
activeParticipants: participants,
|
||||
activeSince : new Date(),
|
||||
// z.B. auf 2h Sichtbarkeit begrenzen (optional)
|
||||
activeSince: new Date(),
|
||||
bannerExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
@ -558,27 +584,20 @@ async function writeLiveStateToServerConfig(match: any, vote: any, mapVisuals: R
|
||||
}
|
||||
}
|
||||
|
||||
/** DRY: Wird in jedem "locked"-Pfad aufgerufen */
|
||||
async function afterVoteLocked(match: any, vote: any, mapVisuals: Record<string, any>) {
|
||||
// 1) Spieler festschreiben
|
||||
async function afterVoteLocked(match: MatchDb, vote: VoteDb, mapVisuals: Record<string, MapVisual>) {
|
||||
await persistMatchPlayers(match)
|
||||
|
||||
// 2) Serverexport + Matchzy-Load
|
||||
const ok = await exportMatchToSftpDirect(match, vote) // ← wartet bis Load versucht wurde
|
||||
|
||||
// 3) Nur wenn Export/Load ok war: Live-State + SSE "server-config-updated"
|
||||
const ok = await exportMatchToSftpDirect(match, vote)
|
||||
if (ok) {
|
||||
await writeLiveStateToServerConfig(match, vote, mapVisuals)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ---------- kleine Helfer für match-ready Payload ---------- */
|
||||
|
||||
function deriveChosenSteps(vote: any) {
|
||||
const steps = [...vote.steps].sort((a: any, b: any) => a.order - b.order)
|
||||
return steps.filter((s: any) => (s.action === 'PICK' || s.action === 'DECIDER') && s.map)
|
||||
function deriveChosenSteps(vote: Pick<VoteDb, 'steps'>) {
|
||||
const steps = [...vote.steps].sort(byOrder)
|
||||
return steps.filter(s => (s.action === 'PICK' || s.action === 'DECIDER') && !!s.map)
|
||||
}
|
||||
|
||||
/* -------------------- GET -------------------- */
|
||||
@ -672,7 +691,11 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
const updated = await prisma.mapVote.findUnique({
|
||||
where: { id: vote.id },
|
||||
include: { steps: true },
|
||||
})
|
||||
}) as (VoteDb | null);
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json({ message: 'Vote nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
@ -680,7 +703,7 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||
|
||||
// ➕ match-ready senden (erste Map + Teilnehmer)
|
||||
if (updated?.locked) {
|
||||
@ -734,7 +757,11 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
const updated = await prisma.mapVote.findUnique({
|
||||
where: { id: vote.id },
|
||||
include: { steps: true },
|
||||
})
|
||||
}) as (VoteDb | null);
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json({ message: 'Vote nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
@ -742,7 +769,7 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||
|
||||
// ➕ match-ready senden
|
||||
if (updated?.locked) {
|
||||
@ -834,7 +861,11 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
const updated = await prisma.mapVote.findUnique({
|
||||
where : { id: vote.id },
|
||||
include: { steps: true },
|
||||
})
|
||||
}) as (VoteDb | null);
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json({ message: 'Vote nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
await sendServerSSEMessage({
|
||||
type: 'map-vote-updated',
|
||||
@ -842,7 +873,7 @@ export async function POST(req: NextRequest, ctx: Ctx) {
|
||||
})
|
||||
|
||||
const teams = buildTeamsPayloadFromMatch(match)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated!.mapPool)
|
||||
const mapVisuals = buildMapVisuals(match.id, updated.mapPool)
|
||||
|
||||
// Falls durch diesen Schritt locked wurde → Export & match-ready
|
||||
if (updated?.locked) {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// /src/app/api/ptero/send-command/route.ts
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const PANEL = process.env.PTERO_PANEL_URL!
|
||||
@ -10,7 +12,7 @@ export async function POST(req: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { command } = await req.json() as { command?: string }
|
||||
const { command } = (await req.json()) as { command?: string }
|
||||
if (!command || typeof command !== 'string') {
|
||||
return NextResponse.json({ error: 'command required' }, { status: 400 })
|
||||
}
|
||||
@ -18,22 +20,30 @@ export async function POST(req: Request) {
|
||||
const r = await fetch(`${PANEL}/api/client/servers/${SID}/command`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${KEY}`,
|
||||
Authorization: `Bearer ${KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ command }),
|
||||
// wichtig: keine Caching-Probleme
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
const msg = await r.text().catch(() => '')
|
||||
return NextResponse.json({ error: 'Pterodactyl error', details: msg }, { status: r.status })
|
||||
return NextResponse.json(
|
||||
{ error: 'Pterodactyl error', details: msg },
|
||||
{ status: r.status },
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message ?? 'unknown error' }, { status: 500 })
|
||||
} catch (e: unknown) {
|
||||
const message =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === 'string'
|
||||
? e
|
||||
: 'unknown error'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,61 +4,87 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
// Helper: Prisma-User -> Player
|
||||
const toPlayer = (u: any) => ({
|
||||
steamId : u?.steamId ?? '',
|
||||
name : u?.name ?? 'Unbekannt',
|
||||
avatar : u?.avatar ?? null,
|
||||
location : u?.location ?? undefined,
|
||||
premierRank: u?.premierRank ?? undefined,
|
||||
isAdmin : u?.isAdmin ?? undefined,
|
||||
// ---- schlanke DTO-Typen (nur was wir wirklich benutzen) ----
|
||||
type DbUser = {
|
||||
steamId?: string | null
|
||||
name?: string | null
|
||||
avatar?: string | null
|
||||
location?: string | null
|
||||
premierRank?: number | null
|
||||
isAdmin?: boolean | null
|
||||
} | null | undefined
|
||||
|
||||
type PlayerDTO = {
|
||||
steamId: string
|
||||
name: string
|
||||
avatar: string | null
|
||||
location?: string
|
||||
premierRank?: number
|
||||
isAdmin?: boolean
|
||||
}
|
||||
|
||||
type DbMatchPlayer = {
|
||||
user: DbUser
|
||||
stats?: unknown
|
||||
}
|
||||
|
||||
type MatchPlayerDTO = {
|
||||
user: PlayerDTO
|
||||
stats?: unknown
|
||||
}
|
||||
|
||||
// ---- Helper: Prisma-User -> PlayerDTO (ohne any) ----
|
||||
const toPlayer = (u: DbUser): PlayerDTO => ({
|
||||
steamId : (u?.steamId ?? '') as string,
|
||||
name : (u?.name ?? 'Unbekannt') as string,
|
||||
avatar : (u?.avatar ?? null) as string | null,
|
||||
location : (u?.location ?? undefined) ?? undefined,
|
||||
premierRank: typeof u?.premierRank === 'number' ? u!.premierRank : undefined,
|
||||
isAdmin : typeof u?.isAdmin === 'boolean' ? u!.isAdmin : undefined,
|
||||
})
|
||||
|
||||
// Helper: Prisma-MatchPlayer -> MatchPlayer
|
||||
const toMatchPlayer = (p: any) => ({
|
||||
// ---- Helper: Prisma-MatchPlayer -> MatchPlayerDTO (ohne any) ----
|
||||
const toMatchPlayer = (p: DbMatchPlayer): MatchPlayerDTO => ({
|
||||
user : toPlayer(p.user),
|
||||
stats: p.stats ?? undefined,
|
||||
})
|
||||
|
||||
// ---- Helper: optionales m.date typsicher abgreifen ----
|
||||
function safeOptionalDate(v: unknown): Date | null {
|
||||
if (v && typeof v === 'object' && 'date' in v) {
|
||||
const d = (v as { date?: unknown }).date
|
||||
return d instanceof Date ? d : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const matches = await prisma.match.findMany({
|
||||
where : { matchType: 'community' },
|
||||
orderBy: { demoDate: 'desc' },
|
||||
|
||||
include: {
|
||||
teamA: {
|
||||
include: { leader: true },
|
||||
},
|
||||
teamB: {
|
||||
include: { leader: true },
|
||||
},
|
||||
teamA: { include: { leader: true } },
|
||||
teamB: { include: { leader: true } },
|
||||
players: {
|
||||
include: {
|
||||
user : true,
|
||||
stats: true,
|
||||
team : true,
|
||||
},
|
||||
include: { user: true, stats: true, team: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const formatted = matches.map(m => {
|
||||
const matchDate =
|
||||
m.demoDate ??
|
||||
(m as any).date ??
|
||||
m.createdAt
|
||||
const matchDate = m.demoDate ?? safeOptionalDate(m) ?? m.createdAt
|
||||
|
||||
const teamAId = m.teamA?.id ?? null
|
||||
const teamBId = m.teamB?.id ?? null
|
||||
|
||||
const teamAPlayers = m.players
|
||||
.filter(p => (p.teamId ?? p.team?.id) === teamAId)
|
||||
.map(toMatchPlayer)
|
||||
.map(p => toMatchPlayer({ user: p.user, stats: p.stats }))
|
||||
|
||||
const teamBPlayers = m.players
|
||||
.filter(p => (p.teamId ?? p.team?.id) === teamBId)
|
||||
.map(toMatchPlayer)
|
||||
.map(p => toMatchPlayer({ user: p.user, stats: p.stats }))
|
||||
|
||||
return {
|
||||
id : m.id,
|
||||
@ -77,7 +103,6 @@ export async function GET() {
|
||||
logo : m.teamA?.logo ?? null,
|
||||
score : m.scoreA,
|
||||
leader : m.teamA?.leader ? toPlayer(m.teamA.leader) : undefined,
|
||||
// -> neu:
|
||||
players: teamAPlayers,
|
||||
},
|
||||
|
||||
@ -87,11 +112,8 @@ export async function GET() {
|
||||
logo : m.teamB?.logo ?? null,
|
||||
score : m.scoreB,
|
||||
leader : m.teamB?.leader ? toPlayer(m.teamB.leader) : undefined,
|
||||
// -> neu:
|
||||
players: teamBPlayers,
|
||||
},
|
||||
|
||||
// -> Top-Level "players" wurde entfernt
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -5,12 +5,12 @@ import { prisma } from "@/lib/prisma";
|
||||
import { decrypt } from '@/lib/crypto';
|
||||
import { processUserMatches } from '../tasks/processUserMatchesTask';
|
||||
import { refreshUserBansTask } from '../tasks/refreshUserBansTask';
|
||||
import { refreshFaceitProfilesTask } from '../tasks/refreshFaceitProfilesTask'; // ⬅️ NEU
|
||||
import { refreshFaceitProfilesTask } from '../tasks/refreshFaceitProfilesTask';
|
||||
import { log } from '../lib/logger';
|
||||
|
||||
let runningMatches = false;
|
||||
let runningBans = false;
|
||||
let runningFaceit = false; // ⬅️ NEU
|
||||
let runningFaceit = false;
|
||||
|
||||
export function startCS2MatchCron() {
|
||||
log.info('🚀 CS2-CronJob Runner gestartet!');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user