This commit is contained in:
Linrador 2025-06-19 23:07:19 +02:00
parent de67f784a3
commit 2675c6363c
32 changed files with 1622 additions and 499 deletions

72
package-lock.json generated
View File

@ -15,7 +15,7 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.9.0", "@prisma/client": "^6.10.1",
"csgo-sharecode": "^3.1.2", "csgo-sharecode": "^3.1.2",
"datatables.net": "^2.2.2", "datatables.net": "^2.2.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -51,7 +51,7 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.0", "eslint-config-next": "15.3.0",
"prisma": "^6.9.0", "prisma": "^6.10.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.4", "tsx": "^4.19.4",
@ -1550,9 +1550,9 @@
"license": "Licensed under MIT and Preline UI Fair Use License" "license": "Licensed under MIT and Preline UI Fair Use License"
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz",
"integrity": "sha512-Gg7j1hwy3SgF1KHrh0PZsYvAaykeR0PaxusnLXydehS96voYCGt1U5zVR31NIouYc63hWzidcrir1a7AIyCsNQ==", "integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -1572,9 +1572,9 @@
} }
}, },
"node_modules/@prisma/config": { "node_modules/@prisma/config": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz",
"integrity": "sha512-Wcfk8/lN3WRJd5w4jmNQkUwhUw0eksaU/+BlAJwPQKW10k0h0LC9PD/6TQFmqKVbHQL0vG2z266r0S1MPzzhbA==", "integrity": "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -1582,53 +1582,53 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz",
"integrity": "sha512-bFeur/qi/Q+Mqk4JdQ3R38upSYPebv5aOyD1RKywVD+rAMLtRkmTFn28ZuTtVOnZHEdtxnNOCH+bPIeSGz1+Fg==", "integrity": "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz",
"integrity": "sha512-im0X0bwDLA0244CDf8fuvnLuCQcBBdAGgr+ByvGfQY9wWl6EA+kRGwVk8ZIpG65rnlOwtaWIr/ZcEU5pNVvq9g==", "integrity": "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.9.0", "@prisma/debug": "6.10.1",
"@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
"@prisma/fetch-engine": "6.9.0", "@prisma/fetch-engine": "6.10.1",
"@prisma/get-platform": "6.9.0" "@prisma/get-platform": "6.10.1"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", "version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz",
"integrity": "sha512-Qp9gMoBHgqhKlrvumZWujmuD7q4DV/gooEyPCLtbkc13EZdSz2RsGUJ5mHb3RJgAbk+dm6XenqG7obJEhXcJ6Q==", "integrity": "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz",
"integrity": "sha512-PMKhJdl4fOdeE3J3NkcWZ+tf3W6rx3ht/rLU8w4SXFRcLhd5+3VcqY4Kslpdm8osca4ej3gTfB3+cSk5pGxgFg==", "integrity": "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.9.0", "@prisma/debug": "6.10.1",
"@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
"@prisma/get-platform": "6.9.0" "@prisma/get-platform": "6.10.1"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz",
"integrity": "sha512-/B4n+5V1LI/1JQcHp+sUpyRT1bBgZVPHbsC4lt4/19Xp4jvNIVcq5KYNtQDk5e/ukTSjo9PZVAxxy9ieFtlpTQ==", "integrity": "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.9.0" "@prisma/debug": "6.10.1"
} }
}, },
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
@ -6237,15 +6237,15 @@
"peer": true "peer": true
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "6.9.0", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.9.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz",
"integrity": "sha512-resJAwMyZREC/I40LF6FZ6rZTnlrlrYrb63oW37Gq+U+9xHwbyMSPJjKtM7VZf3gTO86t/Oyz+YeSXr3CmAY1Q==", "integrity": "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/config": "6.9.0", "@prisma/config": "6.10.1",
"@prisma/engines": "6.9.0" "@prisma/engines": "6.10.1"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"

View File

@ -18,7 +18,7 @@
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@preline/dropdown": "^3.0.1", "@preline/dropdown": "^3.0.1",
"@preline/tooltip": "^3.0.0", "@preline/tooltip": "^3.0.0",
"@prisma/client": "^6.9.0", "@prisma/client": "^6.10.1",
"csgo-sharecode": "^3.1.2", "csgo-sharecode": "^3.1.2",
"datatables.net": "^2.2.2", "datatables.net": "^2.2.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -54,7 +54,7 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.0", "eslint-config-next": "15.3.0",
"prisma": "^6.9.0", "prisma": "^6.10.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.19.4", "tsx": "^4.19.4",

View File

@ -25,6 +25,9 @@ model User {
team Team? @relation("UserTeam", fields: [teamId], references: [id]) team Team? @relation("UserTeam", fields: [teamId], references: [id])
ledTeam Team? @relation("TeamLeader") ledTeam Team? @relation("TeamLeader")
matchesAsTeamA Match[] @relation("TeamAPlayers")
matchesAsTeamB Match[] @relation("TeamBPlayers")
premierRank Int? premierRank Int?
authCode String? authCode String?
lastKnownShareCode String? lastKnownShareCode String?
@ -52,9 +55,10 @@ model Team {
leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId]) leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId])
members User[] @relation("UserTeam") members User[] @relation("UserTeam")
invites TeamInvite[] invites TeamInvite[]
matchesAsTeamA Match[] @relation("Match_TeamA")
matchesAsTeamB Match[] @relation("Match_TeamB")
matchPlayers MatchPlayer[] matchPlayers MatchPlayer[]
matchesAsTeamA Match[] @relation("MatchTeamA")
matchesAsTeamB Match[] @relation("MatchTeamB")
} }
model TeamInvite { model TeamInvite {
@ -98,9 +102,13 @@ model Match {
scoreB Int? scoreB Int?
teamAId String? teamAId String?
teamA Team? @relation("MatchTeamA", fields: [teamAId], references: [id])
teamBId String? teamBId String?
teamA Team? @relation("Match_TeamA", fields: [teamAId], references: [id]) teamB Team? @relation("MatchTeamB", fields: [teamBId], references: [id])
teamB Team? @relation("Match_TeamB", fields: [teamBId], references: [id])
teamAUsers User[] @relation("TeamAPlayers")
teamBUsers User[] @relation("TeamBPlayers")
filePath String? filePath String?
demoFile DemoFile? demoFile DemoFile?

View File

@ -14,8 +14,6 @@ export async function GET(_: Request, context: { params: { id: string } }) {
const match = await prisma.match.findUnique({ const match = await prisma.match.findUnique({
where: { id }, where: { id },
include: { include: {
teamA: true,
teamB: true,
players: { players: {
include: { include: {
user: true, user: true,
@ -23,6 +21,16 @@ export async function GET(_: Request, context: { params: { id: string } }) {
team: true, team: true,
}, },
}, },
teamAUsers: {
include: {
team: true,
},
},
teamBUsers: {
include: {
team: true,
},
},
}, },
}) })
@ -30,49 +38,38 @@ export async function GET(_: Request, context: { params: { id: string } }) {
return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 }) return NextResponse.json({ error: 'Match nicht gefunden' }, { status: 404 })
} }
const teamAIds = new Set(match.teamAUsers.map(u => u.steamId));
const teamBIds = new Set(match.teamBUsers.map(u => u.steamId));
const playersA = match.players const playersA = match.players
.filter(p => p.teamId === match.teamAId) .filter(p => teamAIds.has(p.steamId))
.map(p => ({ .map(p => ({
user: p.user, user: p.user,
stats: p.stats, stats: p.stats,
team: p.team?.name ?? 'CT', team: p.team?.name ?? 'Team A',
})) }));
const playersB = match.players const playersB = match.players
.filter(p => p.teamId === match.teamBId) .filter(p => teamBIds.has(p.steamId))
.map(p => ({ .map(p => ({
user: p.user, user: p.user,
stats: p.stats, stats: p.stats,
team: p.team?.name ?? 'T', team: p.team?.name ?? 'Team B',
})) }));
const teamA = match.teamA const teamA = {
? { name: match.teamAUsers[0]?.team?.name ?? 'Team A',
id: match.teamA.id, logo: null,
name: match.teamA.name, score: match.scoreA,
logo: match.teamA.logo, players: playersA,
players: playersA, };
}
: {
id: null,
name: 'CT',
logo: null,
players: playersA,
}
const teamB = match.teamB const teamB = {
? { name: match.teamBUsers[0]?.team?.name ?? 'Team B',
id: match.teamB.id, logo: null,
name: match.teamB.name, score: match.scoreB,
logo: match.teamB.logo, players: playersB,
players: playersB, };
}
: {
id: null,
name: 'T',
logo: null,
players: playersB,
}
return NextResponse.json({ return NextResponse.json({
id: match.id, id: match.id,
@ -81,11 +78,9 @@ export async function GET(_: Request, context: { params: { id: string } }) {
demoDate: match.demoDate, demoDate: match.demoDate,
matchType: match.matchType, matchType: match.matchType,
map: match.map, map: match.map,
scoreA: match.scoreA,
scoreB: match.scoreB,
teamA, teamA,
teamB, teamB,
}) });
} catch (err) { } catch (err) {
console.error(`GET /matches/${id} failed:`, err) console.error(`GET /matches/${id} failed:`, err)
return NextResponse.json({ error: 'Failed to load match' }, { status: 500 }) return NextResponse.json({ error: 'Failed to load match' }, { status: 500 })
@ -105,20 +100,21 @@ export async function PUT(req: NextRequest, context: { params: { id: string } })
const body = await req.json() const body = await req.json()
const { title, description, matchDate, players } = body const { title, description, matchDate, players } = body
const user = await prisma.user.findUnique({
where: { steamId: userId },
include: { ledTeam: true },
});
const match = await prisma.match.findUnique({ const match = await prisma.match.findUnique({
where: { id }, where: { id },
include: { });
teamA: { include: { leader: true } },
teamB: { include: { leader: true } },
}
})
if (!match) { if (!match) {
return NextResponse.json({ error: 'Match not found' }, { status: 404 }) return NextResponse.json({ error: 'Match not found' }, { status: 404 })
} }
const isTeamLeaderA = match.teamA?.leaderId === userId const isTeamLeaderA = match.teamAId && user?.ledTeam?.id === match.teamAId;
const isTeamLeaderB = match.teamB?.leaderId === userId const isTeamLeaderB = match.teamBId && user?.ledTeam?.id === match.teamBId;
if (!isAdmin && !isTeamLeaderA && !isTeamLeaderB) { if (!isAdmin && !isTeamLeaderA && !isTeamLeaderB) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
@ -161,8 +157,6 @@ export async function PUT(req: NextRequest, context: { params: { id: string } })
const updated = await prisma.match.findUnique({ const updated = await prisma.match.findUnique({
where: { id }, where: { id },
include: { include: {
teamA: true,
teamB: true,
players: { players: {
include: { include: {
user: true, user: true,

View File

@ -1,3 +1,4 @@
// /app/api/matches/route.ts
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/app/lib/prisma' import { prisma } from '@/app/lib/prisma'
@ -12,12 +13,43 @@ export async function GET() {
include: { include: {
user: true, user: true,
stats: true, stats: true,
team: true,
}, },
}, },
}, },
}) })
return NextResponse.json(matches) const formatted = matches.map(match => ({
id: match.id,
map: match.map,
demoDate: match.demoDate,
matchType: match.matchType,
scoreA: match.scoreA,
scoreB: match.scoreB,
winnerTeam: match.winnerTeam ?? null,
teamA: {
id: match.teamA?.id ?? null,
name: match.teamA?.name ?? 'CT',
logo: match.teamA?.logo ?? null,
score: match.scoreA,
},
teamB: {
id: match.teamB?.id ?? null,
name: match.teamB?.name ?? 'T',
logo: match.teamB?.logo ?? null,
score: match.scoreB,
},
players: match.players.map(p => ({
steamId: p.steamId,
name: p.user?.name,
avatar: p.user?.avatar,
stats: p.stats,
teamId: p.teamId,
teamName: p.team?.name ?? null,
})),
}));
return NextResponse.json(formatted)
} catch (err) { } catch (err) {
console.error('GET /matches failed:', err) console.error('GET /matches failed:', err)
return NextResponse.json({ error: 'Failed to load matches' }, { status: 500 }) return NextResponse.json({ error: 'Failed to load matches' }, { status: 500 })

View File

@ -15,6 +15,7 @@ export async function GET(req: NextRequest) {
const matchPlayers = await prisma.matchPlayer.findMany({ const matchPlayers = await prisma.matchPlayer.findMany({
where: { steamId: steamId }, where: { steamId: steamId },
select: { select: {
teamId: true,
team: true, team: true,
match: { match: {
select: { select: {
@ -26,6 +27,8 @@ export async function GET(req: NextRequest) {
matchType: true, matchType: true,
teamAId: true, teamAId: true,
teamBId: true, teamBId: true,
winnerTeam: true,
demoData: true,
}, },
}, },
stats: true, stats: true,
@ -45,13 +48,28 @@ export async function GET(req: NextRequest) {
const kd = deaths > 0 ? (kills / deaths).toFixed(2) : '∞'; const kd = deaths > 0 ? (kills / deaths).toFixed(2) : '∞';
const rankOld = stats?.rankOld ?? null; const rankOld = stats?.rankOld ?? null;
const rankNew = stats?.rankNew ?? null; const rankNew = stats?.rankNew ?? null;
const rankChange = typeof rankNew === 'number' && typeof rankOld === 'number' ? rankNew - rankOld : null; const rankChange =
typeof rankNew === 'number' && typeof rankOld === 'number'
? rankNew - rankOld
: null;
const matchType = match.matchType ?? 'community'; const matchType = match.matchType ?? 'community';
const demoData = match.demoData as any;
// Spieler war Team A, wenn seine teamId == match.teamAId // Spielerteam: CT oder T
const isTeamA = mp.team === 'CT'; let playerTeam: string | null = null;
const scoreLeft = isTeamA ? match.scoreA : match.scoreB; let isInTeamA = false;
const scoreRight = isTeamA ? match.scoreB : match.scoreA; let isInTeamB = false;
if (demoData?.teamA?.players && demoData?.teamB?.players) {
isInTeamA = demoData.teamA.players.some((p: any) => p?.steamId === steamId);
isInTeamB = demoData.teamB.players.some((p: any) => p?.steamId === steamId);
if (isInTeamA) playerTeam = 'CT';
if (isInTeamB) playerTeam = 'T';
}
const scoreLeft = isInTeamA ? match.scoreA : match.scoreB;
const scoreRight = isInTeamB ? match.scoreB : match.scoreA;
return { return {
id: match.id, id: match.id,
@ -65,6 +83,8 @@ export async function GET(req: NextRequest) {
kills, kills,
deaths, deaths,
kd, kd,
winnerTeam: match.winnerTeam ?? null,
team: mp.team?.name ?? null,
}; };
}); });

View File

@ -45,15 +45,14 @@ export default function CompRankBadge({ rank }: Props) {
return ( return (
<Tooltip content={altText}> <Tooltip content={altText}>
<div style={{ position: 'relative', width: 70, height: 40 }}> <Image
<Image src={`/assets/img/skillgroups/${imageName}`}
src={`/assets/img/skillgroups/${imageName}`} alt={altText}
alt={altText} width={60}
fill height={60}
style={{ objectFit: 'contain' }} sizes="(max-width: 768px) 100px, 70px"
sizes="(max-width: 768px) 100px, 70px" style={{ objectFit: 'contain' }} // ← korrekt!
/> />
</div>
</Tooltip> </Tooltip>
); );
} }

View File

@ -11,7 +11,7 @@ import InvitePlayersModal from './InvitePlayersModal'
import Modal from './Modal' import Modal from './Modal'
import { Player, Team } from '../types/team' import { Player, Team } from '../types/team'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useWS } from '@/app/lib/useSSEStore' import { useSSE } from '@/app/lib/useSSEStore'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { useTeamManager } from '../hooks/useTeamManager' import { useTeamManager } from '../hooks/useTeamManager'
import Button from './Button' import Button from './Button'
@ -51,7 +51,7 @@ export default function TeamMemberView({
setInactivePlayers, setInactivePlayers,
}: Props) { }: Props) {
const { data: session } = useSession() const { data: session } = useSession()
const { socket } = useWS() const { source, connect } = useSSE()
const [kickCandidate, setKickCandidate] = useState<Player | null>(null) const [kickCandidate, setKickCandidate] = useState<Player | null>(null)
const [promoteCandidate, setPromoteCandidate] = useState<Player | null>(null) const [promoteCandidate, setPromoteCandidate] = useState<Player | null>(null)
@ -67,44 +67,51 @@ export default function TeamMemberView({
const [logoFile, setLogoFile] = useState<File | null>(null) const [logoFile, setLogoFile] = useState<File | null>(null)
useEffect(() => { useEffect(() => {
if (!socket || !team?.id) return if (session?.user?.steamId) {
connect(session.user.steamId)
const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
const relevantTypes = [
'team-updated',
'team-kick',
'team-kick-other',
'team-member-joined',
'team-member-left',
'team-leader-changed',
'team-renamed',
'team-logo-updated',
]
if (relevantTypes.includes(data.type) && typeof data.teamId === 'string') {
fetch(`/api/team/${encodeURIComponent(data.teamId)}`)
.then((res) => res.json())
.then((data) => {
setactivePlayers(
(data.activePlayers ?? [])
.filter((p: Player) => p?.name)
.sort((a: Player, b: Player) => a.name.localeCompare(b.name))
);
setInactivePlayers(
(data.inactivePlayers ?? [])
.filter((p: Player) => p?.name)
.sort((a: Player, b: Player) => a.name.localeCompare(b.name))
);
})
} }
} }, [session?.user?.steamId])
socket.addEventListener('message', handleMessage)
return () => socket.removeEventListener('message', handleMessage) useEffect(() => {
}, [socket, team?.id]) if (!source || !team?.id) return
const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
const relevantTypes = [
'team-updated',
'team-kick',
'team-kick-other',
'team-member-joined',
'team-member-left',
'team-leader-changed',
'team-renamed',
'team-logo-updated',
]
if (relevantTypes.includes(data.type) && typeof data.teamId === 'string') {
fetch(`/api/team/${encodeURIComponent(data.teamId)}`)
.then((res) => res.json())
.then((data) => {
setactivePlayers(
(data.activePlayers ?? [])
.filter((p: Player) => p?.name)
.sort((a: Player, b: Player) => a.name.localeCompare(b.name))
);
setInactivePlayers(
(data.inactivePlayers ?? [])
.filter((p: Player) => p?.name)
.sort((a: Player, b: Player) => a.name.localeCompare(b.name))
);
})
}
}
source.addEventListener('message', handleMessage)
return () => source.removeEventListener('message', handleMessage)
}, [source, team?.id])
const handleDragStart = (event: any) => { const handleDragStart = (event: any) => {

View File

@ -13,6 +13,8 @@ interface Match {
map: string; map: string;
date: string; date: string;
score: string; score: string;
winnerTeam?: string;
team?: 'CT' | 'T';
matchType: string; matchType: string;
rating: string; rating: string;
kills: number; kills: number;
@ -50,7 +52,17 @@ export default function UserMatchesTable() {
<Table.Body> <Table.Body>
{matches.map((m) => { {matches.map((m) => {
const mapInfo = mapNameMap[m.map] ?? mapNameMap['lobby_mapveto']; const mapInfo = mapNameMap[m.map] ?? mapNameMap['lobby_mapveto'];
const [left, right] = m.score.split(':').map(s => parseInt(s.trim(), 10)); const [scoreCT, scoreT] = m.score.split(':').map(s => parseInt(s.trim(), 10));
let left = scoreCT;
let right = scoreT;
// Score-Reihenfolge anhand des eigenen Teams und Sieger drehen
if (m.team === 'T') {
left = scoreT;
right = scoreCT;
}
// Score-Farbe bestimmen // Score-Farbe bestimmen
let scoreClass = ''; let scoreClass = '';

View File

@ -65,12 +65,21 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
return session return session
}, },
async redirect({ url, baseUrl }) { redirect({ url, baseUrl }) {
if (url.includes('/api/auth/signout')) { const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`);
return `${baseUrl}/` // Zurück zur Startseite const isSignOut = url.startsWith(`${baseUrl}/api/auth/signout`);
if (isSignOut) {
return `${baseUrl}/`; // Nach Logout auf Startseite
} }
return `${baseUrl}/dashboard`
}, // Standard-Redirect nach Login
if (isSignIn || url === baseUrl) {
return `${baseUrl}/dashboard`; // z.B. Dashboard als Startpunkt
}
return url.startsWith(baseUrl) ? url : baseUrl;
}
}, },
}) })

View File

@ -12,12 +12,11 @@ export const useSSE = create<SSEState>((set, get) => {
const connect = (steamId: string): EventSource | undefined => { const connect = (steamId: string): EventSource | undefined => {
const current = get().source const current = get().source
if (current) return current // bereits verbunden if (current) return current
const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`) const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`)
source.onopen = () => { source.onopen = () => {
console.log('[SSE] Verbunden')
set({ source, isConnected: true }) set({ source, isConnected: true })
} }

File diff suppressed because one or more lines are too long

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.9.0 * Prisma Client JS version: 6.10.1
* Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e * Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.9.0", client: "6.10.1",
engine: "81e4af48011447c3cc503a190e86995b66d2a28e" engine: "9b628578b3b7cae625e8c927178f15a170e74a9c"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"name": "prisma-client-7dc6d5614c138eb3659cdd5dbb69f5d3b2be84ae9983dbfa39f9833b9ad6da8c", "name": "prisma-client-81fe4a88a75a445ee239171472edc8b1edb558143434347db08d32212685268e",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"browser": "index-browser.js", "browser": "index-browser.js",
@ -141,6 +141,6 @@
}, },
"./*": "./*" "./*": "./*"
}, },
"version": "6.9.0", "version": "6.10.1",
"sideEffects": false "sideEffects": false
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -209,7 +209,7 @@ declare const ColumnTypeEnum: {
declare type CompactedBatchResponse = { declare type CompactedBatchResponse = {
type: 'compacted'; type: 'compacted';
plan: object; plan: {};
arguments: Record<string, {}>[]; arguments: Record<string, {}>[];
nestedSelection: string[]; nestedSelection: string[];
keys: string[]; keys: string[];
@ -255,6 +255,7 @@ declare type ComputedFieldsMap = {
declare type ConnectionInfo = { declare type ConnectionInfo = {
schemaName?: string; schemaName?: string;
maxBindValues?: number; maxBindValues?: number;
supportsRelationJoins: boolean;
}; };
declare type ConnectorType = 'mysql' | 'mongodb' | 'sqlite' | 'postgresql' | 'postgres' | 'prisma+postgres' | 'sqlserver' | 'cockroachdb'; declare type ConnectorType = 'mysql' | 'mongodb' | 'sqlite' | 'postgresql' | 'postgres' | 'prisma+postgres' | 'sqlserver' | 'cockroachdb';
@ -1153,10 +1154,22 @@ declare type Error_2 = {
column?: string; column?: string;
} | { } | {
kind: 'UniqueConstraintViolation'; kind: 'UniqueConstraintViolation';
fields: string[]; constraint?: {
fields: string[];
} | {
index: string;
} | {
foreignKey: {};
};
} | { } | {
kind: 'NullConstraintViolation'; kind: 'NullConstraintViolation';
fields: string[]; constraint?: {
fields: string[];
} | {
index: string;
} | {
foreignKey: {};
};
} | { } | {
kind: 'ForeignKeyConstraintViolation'; kind: 'ForeignKeyConstraintViolation';
constraint?: { constraint?: {
@ -1189,8 +1202,19 @@ declare type Error_2 = {
} | { } | {
kind: 'TooManyConnections'; kind: 'TooManyConnections';
cause: string; cause: string;
} | {
kind: 'ValueOutOfRange';
cause: string;
} | {
kind: 'MissingFullTextSearchIndex';
} | { } | {
kind: 'SocketTimeout'; kind: 'SocketTimeout';
} | {
kind: 'InconsistentColumnData';
cause: string;
} | {
kind: 'TransactionAlreadyClosed';
cause: string;
} | { } | {
kind: 'postgres'; kind: 'postgres';
code: string; code: string;
@ -1211,6 +1235,10 @@ declare type Error_2 = {
*/ */
extendedCode: number; extendedCode: number;
message: string; message: string;
} | {
kind: 'mssql';
code: number;
message: string;
}; };
declare type ErrorCapturingFunction<T> = T extends (...args: infer A) => Promise<infer R> ? (...args: A) => Promise<Result_4<ErrorCapturingInterface<R>>> : T extends (...args: infer A) => infer R ? (...args: A) => Result_4<ErrorCapturingInterface<R>> : T; declare type ErrorCapturingFunction<T> = T extends (...args: infer A) => Promise<infer R> ? (...args: A) => Promise<Result_4<ErrorCapturingInterface<R>>> : T extends (...args: infer A) => infer R ? (...args: A) => Result_4<ErrorCapturingInterface<R>> : T;
@ -2383,7 +2411,7 @@ export declare const objectEnumValues: {
}; };
}; };
declare const officialPrismaAdapters: readonly ["@prisma/adapter-planetscale", "@prisma/adapter-neon", "@prisma/adapter-libsql", "@prisma/adapter-d1", "@prisma/adapter-pg", "@prisma/adapter-pg-worker"]; declare const officialPrismaAdapters: readonly ["@prisma/adapter-planetscale", "@prisma/adapter-neon", "@prisma/adapter-libsql", "@prisma/adapter-d1", "@prisma/adapter-pg", "@prisma/adapter-mssql"];
export declare type Omission = Record<string, boolean | Skip>; export declare type Omission = Record<string, boolean | Skip>;
@ -2661,7 +2689,7 @@ declare type PrismaPromiseTransaction<PayloadType = unknown> = PrismaPromiseBatc
export declare const PrivateResultType: unique symbol; export declare const PrivateResultType: unique symbol;
declare type Provider = 'mysql' | 'postgres' | 'sqlite'; declare type Provider = 'mysql' | 'postgres' | 'sqlite' | 'sqlserver';
declare namespace Public { declare namespace Public {
export { export {
@ -2699,7 +2727,7 @@ declare interface Queryable<Query, Result> extends AdapterInfo {
} }
declare type QueryCompiler = { declare type QueryCompiler = {
compile(request: string): string; compile(request: string): {};
compileBatch(batchRequest: string): BatchResponse; compileBatch(batchRequest: string): BatchResponse;
}; };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,9 @@ model User {
team Team? @relation("UserTeam", fields: [teamId], references: [id]) team Team? @relation("UserTeam", fields: [teamId], references: [id])
ledTeam Team? @relation("TeamLeader") ledTeam Team? @relation("TeamLeader")
matchesAsTeamA Match[] @relation("TeamAPlayers")
matchesAsTeamB Match[] @relation("TeamBPlayers")
premierRank Int? premierRank Int?
authCode String? authCode String?
lastKnownShareCode String? lastKnownShareCode String?
@ -49,12 +52,13 @@ model Team {
activePlayers String[] activePlayers String[]
inactivePlayers String[] inactivePlayers String[]
leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId]) leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId])
members User[] @relation("UserTeam") members User[] @relation("UserTeam")
invites TeamInvite[] invites TeamInvite[]
matchesAsTeamA Match[] @relation("Match_TeamA") matchPlayers MatchPlayer[]
matchesAsTeamB Match[] @relation("Match_TeamB")
matchPlayers MatchPlayer[] matchesAsTeamA Match[] @relation("MatchTeamA")
matchesAsTeamB Match[] @relation("MatchTeamB")
} }
model TeamInvite { model TeamInvite {
@ -98,9 +102,13 @@ model Match {
scoreB Int? scoreB Int?
teamAId String? teamAId String?
teamA Team? @relation("MatchTeamA", fields: [teamAId], references: [id])
teamBId String? teamBId String?
teamA Team? @relation("Match_TeamA", fields: [teamAId], references: [id]) teamB Team? @relation("MatchTeamB", fields: [teamBId], references: [id])
teamB Team? @relation("Match_TeamB", fields: [teamBId], references: [id])
teamAUsers User[] @relation("TeamAPlayers")
teamBUsers User[] @relation("TeamBPlayers")
filePath String? filePath String?
demoFile DemoFile? demoFile DemoFile?

View File

@ -20,12 +20,12 @@ exports.Prisma = Prisma
exports.$Enums = {} exports.$Enums = {}
/** /**
* Prisma Client JS version: 6.9.0 * Prisma Client JS version: 6.10.1
* Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e * Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c
*/ */
Prisma.prismaVersion = { Prisma.prismaVersion = {
client: "6.9.0", client: "6.10.1",
engine: "81e4af48011447c3cc503a190e86995b66d2a28e" engine: "9b628578b3b7cae625e8c927178f15a170e74a9c"
} }
Prisma.PrismaClientKnownRequestError = () => { Prisma.PrismaClientKnownRequestError = () => {

View File

@ -38,21 +38,24 @@ interface DemoMatchData {
map: string; map: string;
filePath: string; filePath: string;
meta: { meta: {
tickRate: number; demoDate?: Date;
duration: number; teamA?: {
map: string; name: string;
players: PlayerStatsExtended[]; score: number;
scoreCT?: number; players: PlayerStatsExtended[];
scoreT?: number; };
teamB?: {
name: string;
score: number;
players: PlayerStatsExtended[];
};
winnerTeam?: string; winnerTeam?: string;
roundCount?: number; roundCount?: number;
roundHistory?: { round: number; winner: string; winReason: string }[]; roundHistory?: { round: number; winner: string; winReason: string }[];
demoDate?: Date;
teamCT: PlayerStatsExtended[];
teamT: PlayerStatsExtended[];
}; };
} }
export async function parseAndStoreDemo( export async function parseAndStoreDemo(
demoPath: string, demoPath: string,
steamId: string, steamId: string,
@ -62,6 +65,7 @@ export async function parseAndStoreDemo(
if (!parsed) return null; if (!parsed) return null;
let actualDemoPath = demoPath; let actualDemoPath = demoPath;
if (parsed.map && parsed.map !== 'unknownmap' && demoPath.includes('unknownmap')) { if (parsed.map && parsed.map !== 'unknownmap' && demoPath.includes('unknownmap')) {
const oldName = path.basename(demoPath); const oldName = path.basename(demoPath);
const newName = oldName.replace('unknownmap', parsed.map); const newName = oldName.replace('unknownmap', parsed.map);
@ -92,9 +96,57 @@ export async function parseAndStoreDemo(
const existing = await prisma.match.findUnique({ const existing = await prisma.match.findUnique({
where: { id: parsed.matchId }, where: { id: parsed.matchId },
}); });
if (existing) return null; if (existing) return null;
const teamAIds: string[] = [];
const teamBIds: string[] = [];
const allPlayers = [
...(parsed.meta.teamA?.players || []),
...(parsed.meta.teamB?.players || []),
];
for (const player of allPlayers) {
let playerUser = await prisma.user.findUnique({
where: { steamId: player.steamId },
});
let steamProfile = null;
if (!playerUser?.name || !playerUser?.avatar) {
steamProfile = await fetchSteamProfile(player.steamId).catch(() => null);
await delay(5000);
}
const isPremier = path.basename(actualDemoPath).toLowerCase().endsWith('_premier.dem');
const updatedFields: Partial<{ name: string; avatar: string; premierRank: number }> = {};
if (!playerUser) {
await prisma.user.create({
data: {
steamId: player.steamId,
name: steamProfile?.name ?? player.name,
avatar: steamProfile?.avatar ?? undefined,
premierRank: isPremier ? player.rankNew ?? undefined : undefined,
},
});
} else {
if (steamProfile?.name && playerUser.name !== steamProfile.name) updatedFields.name = steamProfile.name;
if (steamProfile?.avatar && playerUser.avatar !== steamProfile.avatar) updatedFields.avatar = steamProfile.avatar;
if (Object.keys(updatedFields).length > 0) {
await prisma.user.update({
where: { steamId: player.steamId },
data: updatedFields,
});
}
}
if (parsed.meta.teamA?.players.some(p => p.steamId === player.steamId)) {
teamAIds.push(player.steamId);
} else if (parsed.meta.teamB?.players.some(p => p.steamId === player.steamId)) {
teamBIds.push(player.steamId);
}
}
const match = await prisma.match.create({ const match = await prisma.match.create({
data: { data: {
id: parsed.matchId, id: parsed.matchId,
@ -106,12 +158,18 @@ export async function parseAndStoreDemo(
: demoPath.endsWith('_competitive.dem') : demoPath.endsWith('_competitive.dem')
? 'competitive' ? 'competitive'
: 'community', : 'community',
scoreA: parsed.meta.scoreCT, scoreA: parsed.meta.teamA?.score,
scoreB: parsed.meta.scoreT, scoreB: parsed.meta.teamB?.score,
winnerTeam: parsed.meta.winnerTeam ?? null, winnerTeam: parsed.meta.winnerTeam ?? null,
roundCount: parsed.meta.roundCount ?? null, roundCount: parsed.meta.roundCount ?? null,
roundHistory: parsed.meta.roundHistory ?? undefined, roundHistory: parsed.meta.roundHistory ?? undefined,
demoDate: parsed.meta.demoDate ?? null, demoDate: parsed.meta.demoDate ?? null,
teamAUsers: {
connect: teamAIds.map(steamId => ({ steamId })),
},
teamBUsers: {
connect: teamBIds.map(steamId => ({ steamId })),
},
}, },
}); });
@ -125,50 +183,11 @@ export async function parseAndStoreDemo(
}, },
}); });
for (const player of parsed.meta.players) { for (const player of allPlayers) {
let playerUser = await prisma.user.findUnique({
where: { steamId: player.steamId },
});
let steamProfile = null;
if (!playerUser?.name || !playerUser?.avatar) {
steamProfile = await fetchSteamProfile(player.steamId).catch(() => null);
await delay(5000);
}
const isPremier = path.basename(actualDemoPath).toLowerCase().endsWith('_premier.dem');
const updatedFields: Partial<{ name: string; avatar: string; premierRank: number }> = {};
if (!playerUser) {
await prisma.user.create({
data: {
steamId: player.steamId,
name: steamProfile?.name ?? player.name,
avatar: steamProfile?.avatar ?? undefined,
premierRank: isPremier ? player.rankNew ?? undefined : undefined,
},
});
} else {
if (steamProfile?.name && playerUser.name !== steamProfile.name) {
updatedFields.name = steamProfile.name;
}
if (steamProfile?.avatar && playerUser.avatar !== steamProfile.avatar) {
updatedFields.avatar = steamProfile.avatar;
}
if (Object.keys(updatedFields).length > 0) {
await prisma.user.update({
where: { steamId: player.steamId },
data: updatedFields,
});
}
}
const teamId = const teamId =
parsed.meta.teamCT && player.team === 'CT' match.teamAId && parsed.meta.teamA?.players.some(p => p.steamId === player.steamId)
? match.teamAId ? match.teamAId
: parsed.meta.teamT && player.team === 'T' : match.teamBId && parsed.meta.teamB?.players.some(p => p.steamId === player.steamId)
? match.teamBId ? match.teamBId
: undefined; : undefined;
@ -185,7 +204,7 @@ export async function parseAndStoreDemo(
matchId_steamId: { matchId_steamId: {
matchId: match.id, matchId: match.id,
steamId: player.steamId, steamId: player.steamId,
} },
}, },
update: { update: {
kills: player.kills, kills: player.kills,
@ -261,6 +280,8 @@ async function parseDemoViaGo(filePath: string, shareCode: string): Promise<Demo
if (code === 0) { if (code === 0) {
try { try {
const parsed = JSON.parse(output); const parsed = JSON.parse(output);
console.log(parsed.teamA.players);
console.log(parsed.teamB.players);
resolve({ resolve({
matchId, matchId,
map: parsed.map, map: parsed.map,

View File

@ -1,12 +1,15 @@
import cron from 'node-cron'; import cron from 'node-cron';
import { prisma } from '../app/lib/prisma.js'; import { prisma } from '../app/lib/prisma.js';
import { runDownloaderForUser } from './runDownloaderForUser.js'; import { runDownloaderForUser } from './runDownloaderForUser.js';
import { sendServerWebSocketMessage } from '../app/lib/sse-server-client.js'; import { sendServerSSEMessage } from '../app/lib/sse-server-client.js';
import { decrypt } from '../app/lib/crypto.js'; import { decrypt } from '../app/lib/crypto.js';
import { encodeMatch, decodeMatchShareCode } from 'csgo-sharecode'; import { encodeMatch, decodeMatchShareCode } from 'csgo-sharecode';
import { log } from '../../scripts/cs2-cron-runner.js'; import { log } from '../../scripts/cs2-cron-runner.js';
import { getNextShareCodeFromAPI } from './getNextShareCodeFromAPI.js'; import { getNextShareCodeFromAPI } from './getNextShareCodeFromAPI.js';
import { updatePremierRanksForUser } from './updatePremierRanks'; import { updatePremierRanksForUser } from './updatePremierRanks';
import fs from 'fs';
import path from 'path';
let isRunning = false; let isRunning = false;
@ -64,7 +67,7 @@ async function runMatchCheck() {
}, },
}); });
await sendServerWebSocketMessage({ await sendServerSSEMessage({
type: 'expired-sharecode', type: 'expired-sharecode',
targetUserIds: [user.steamId], targetUserIds: [user.steamId],
message: notification.message, message: notification.message,
@ -117,6 +120,22 @@ async function runMatchCheck() {
const shareCode = encodeMatch(matchInfo); const shareCode = encodeMatch(matchInfo);
const expectedFilename = `${matchInfo.matchId}.dem`;
const expectedFilePath = path.join(process.cwd(), 'demos', expectedFilename);
if (fs.existsSync(expectedFilePath)) {
log(`[${user.steamId}] 📁 Match ${matchInfo.matchId} wurde bereits als Datei gespeichert übersprungen`);
await prisma.user.update({
where: { steamId: user.steamId },
data: { lastKnownShareCode: nextShareCode },
});
latestKnownCode = nextShareCode;
nextShareCode = await getNextShareCodeFromAPI(user.steamId, decryptedAuthCode, latestKnownCode);
continue;
}
const result = await runDownloaderForUser({ const result = await runDownloaderForUser({
...user, ...user,
lastKnownShareCode: shareCode, lastKnownShareCode: shareCode,
@ -170,7 +189,7 @@ async function runMatchCheck() {
}, },
}); });
await sendServerWebSocketMessage({ await sendServerSSEMessage({
type: 'new-cs2-match', type: 'new-cs2-match',
targetUserIds: [user.steamId], targetUserIds: [user.steamId],
message: notification.message, message: notification.message,

View File

@ -3,6 +3,7 @@ import path from 'path';
import { Match, User } from '@/generated/prisma'; import { Match, User } from '@/generated/prisma';
import { parseAndStoreDemo } from './parseAndStoreDemo'; import { parseAndStoreDemo } from './parseAndStoreDemo';
import { log } from '../../scripts/cs2-cron-runner.js'; import { log } from '../../scripts/cs2-cron-runner.js';
import { prisma } from '../app/lib/prisma.js';
export async function runDownloaderForUser(user: User): Promise<{ export async function runDownloaderForUser(user: User): Promise<{
newMatches: Match[]; newMatches: Match[];
@ -37,7 +38,19 @@ export async function runDownloaderForUser(user: User): Promise<{
return { newMatches: [], latestShareCode: shareCode }; return { newMatches: [], latestShareCode: shareCode };
} }
log(`[${steamId}] 📂 Analysiere: ${path.basename(demoPath)}`); const filename = path.basename(demoPath);
const matchId = filename.replace(/\.dem$/, '');
const existing = await prisma.match.findUnique({
where: { id: matchId },
});
if (existing) {
log(`[${steamId}] 🔁 Match ${matchId} wurde bereits analysiert übersprungen`, 'info');
return { newMatches: [], latestShareCode: shareCode };
}
log(`[${steamId}] 📂 Analysiere: ${filename}`);
const absolutePath = path.resolve(__dirname, '../../../cs2-demo-downloader', demoPath); const absolutePath = path.resolve(__dirname, '../../../cs2-demo-downloader', demoPath);
const match = await parseAndStoreDemo(absolutePath, steamId, shareCode); const match = await parseAndStoreDemo(absolutePath, steamId, shareCode);