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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
// /app/api/matches/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/app/lib/prisma'
@ -12,12 +13,43 @@ export async function GET() {
include: {
user: true,
stats: true,
team: true,
},
},
},
})
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(matches)
return NextResponse.json(formatted)
} catch (err) {
console.error('GET /matches failed:', err)
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({
where: { steamId: steamId },
select: {
teamId: true,
team: true,
match: {
select: {
@ -26,6 +27,8 @@ export async function GET(req: NextRequest) {
matchType: true,
teamAId: true,
teamBId: true,
winnerTeam: true,
demoData: true,
},
},
stats: true,
@ -45,13 +48,28 @@ export async function GET(req: NextRequest) {
const kd = deaths > 0 ? (kills / deaths).toFixed(2) : '∞';
const rankOld = stats?.rankOld ?? 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 demoData = match.demoData as any;
// Spielerteam: CT oder T
let playerTeam: string | null = null;
let isInTeamA = false;
let isInTeamB = false;
// Spieler war Team A, wenn seine teamId == match.teamAId
const isTeamA = mp.team === 'CT';
const scoreLeft = isTeamA ? match.scoreA : match.scoreB;
const scoreRight = isTeamA ? match.scoreB : match.scoreA;
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 {
id: match.id,
@ -65,6 +83,8 @@ export async function GET(req: NextRequest) {
kills,
deaths,
kd,
winnerTeam: match.winnerTeam ?? null,
team: mp.team?.name ?? null,
};
});

View File

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

View File

@ -11,7 +11,7 @@ import InvitePlayersModal from './InvitePlayersModal'
import Modal from './Modal'
import { Player, Team } from '../types/team'
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 { useTeamManager } from '../hooks/useTeamManager'
import Button from './Button'
@ -51,7 +51,7 @@ export default function TeamMemberView({
setInactivePlayers,
}: Props) {
const { data: session } = useSession()
const { socket } = useWS()
const { source, connect } = useSSE()
const [kickCandidate, setKickCandidate] = 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)
useEffect(() => {
if (!socket || !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))
);
})
if (session?.user?.steamId) {
connect(session.user.steamId)
}
}
}, [session?.user?.steamId])
socket.addEventListener('message', handleMessage)
return () => socket.removeEventListener('message', handleMessage)
}, [socket, team?.id])
useEffect(() => {
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) => {

View File

@ -13,6 +13,8 @@ interface Match {
map: string;
date: string;
score: string;
winnerTeam?: string;
team?: 'CT' | 'T';
matchType: string;
rating: string;
kills: number;
@ -50,7 +52,17 @@ export default function UserMatchesTable() {
<Table.Body>
{matches.map((m) => {
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
let scoreClass = '';

View File

@ -65,12 +65,21 @@ export const authOptions = (req: NextRequest): NextAuthOptions => ({
return session
},
async redirect({ url, baseUrl }) {
if (url.includes('/api/auth/signout')) {
return `${baseUrl}/` // Zurück zur Startseite
redirect({ url, baseUrl }) {
const isSignIn = url.startsWith(`${baseUrl}/api/auth/signin`);
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 current = get().source
if (current) return current // bereits verbunden
if (current) return current
const source = new EventSource(`http://localhost:3001/events?steamId=${steamId}`)
source.onopen = () => {
console.log('[SSE] Verbunden')
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 = {}
/**
* Prisma Client JS version: 6.9.0
* Query Engine version: 81e4af48011447c3cc503a190e86995b66d2a28e
* Prisma Client JS version: 6.10.1
* Query Engine version: 9b628578b3b7cae625e8c927178f15a170e74a9c
*/
Prisma.prismaVersion = {
client: "6.9.0",
engine: "81e4af48011447c3cc503a190e86995b66d2a28e"
client: "6.10.1",
engine: "9b628578b3b7cae625e8c927178f15a170e74a9c"
}
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",
"types": "index.d.ts",
"browser": "index-browser.js",
@ -141,6 +141,6 @@
},
"./*": "./*"
},
"version": "6.9.0",
"version": "6.10.1",
"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 = {
type: 'compacted';
plan: object;
plan: {};
arguments: Record<string, {}>[];
nestedSelection: string[];
keys: string[];
@ -255,6 +255,7 @@ declare type ComputedFieldsMap = {
declare type ConnectionInfo = {
schemaName?: string;
maxBindValues?: number;
supportsRelationJoins: boolean;
};
declare type ConnectorType = 'mysql' | 'mongodb' | 'sqlite' | 'postgresql' | 'postgres' | 'prisma+postgres' | 'sqlserver' | 'cockroachdb';
@ -1153,10 +1154,22 @@ declare type Error_2 = {
column?: string;
} | {
kind: 'UniqueConstraintViolation';
fields: string[];
constraint?: {
fields: string[];
} | {
index: string;
} | {
foreignKey: {};
};
} | {
kind: 'NullConstraintViolation';
fields: string[];
constraint?: {
fields: string[];
} | {
index: string;
} | {
foreignKey: {};
};
} | {
kind: 'ForeignKeyConstraintViolation';
constraint?: {
@ -1189,8 +1202,19 @@ declare type Error_2 = {
} | {
kind: 'TooManyConnections';
cause: string;
} | {
kind: 'ValueOutOfRange';
cause: string;
} | {
kind: 'MissingFullTextSearchIndex';
} | {
kind: 'SocketTimeout';
} | {
kind: 'InconsistentColumnData';
cause: string;
} | {
kind: 'TransactionAlreadyClosed';
cause: string;
} | {
kind: 'postgres';
code: string;
@ -1211,6 +1235,10 @@ declare type Error_2 = {
*/
extendedCode: number;
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;
@ -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>;
@ -2661,7 +2689,7 @@ declare type PrismaPromiseTransaction<PayloadType = unknown> = PrismaPromiseBatc
export declare const PrivateResultType: unique symbol;
declare type Provider = 'mysql' | 'postgres' | 'sqlite';
declare type Provider = 'mysql' | 'postgres' | 'sqlite' | 'sqlserver';
declare namespace Public {
export {
@ -2699,7 +2727,7 @@ declare interface Queryable<Query, Result> extends AdapterInfo {
}
declare type QueryCompiler = {
compile(request: string): string;
compile(request: string): {};
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])
ledTeam Team? @relation("TeamLeader")
matchesAsTeamA Match[] @relation("TeamAPlayers")
matchesAsTeamB Match[] @relation("TeamBPlayers")
premierRank Int?
authCode String?
lastKnownShareCode String?
@ -49,12 +52,13 @@ model Team {
activePlayers String[]
inactivePlayers String[]
leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId])
members User[] @relation("UserTeam")
invites TeamInvite[]
matchesAsTeamA Match[] @relation("Match_TeamA")
matchesAsTeamB Match[] @relation("Match_TeamB")
matchPlayers MatchPlayer[]
leader User? @relation("TeamLeader", fields: [leaderId], references: [steamId])
members User[] @relation("UserTeam")
invites TeamInvite[]
matchPlayers MatchPlayer[]
matchesAsTeamA Match[] @relation("MatchTeamA")
matchesAsTeamB Match[] @relation("MatchTeamB")
}
model TeamInvite {
@ -98,9 +102,13 @@ model Match {
scoreB Int?
teamAId String?
teamA Team? @relation("MatchTeamA", fields: [teamAId], references: [id])
teamBId String?
teamA Team? @relation("Match_TeamA", fields: [teamAId], references: [id])
teamB Team? @relation("Match_TeamB", fields: [teamBId], references: [id])
teamB Team? @relation("MatchTeamB", fields: [teamBId], references: [id])
teamAUsers User[] @relation("TeamAPlayers")
teamBUsers User[] @relation("TeamBPlayers")
filePath String?
demoFile DemoFile?

View File

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

View File

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

View File

@ -1,12 +1,15 @@
import cron from 'node-cron';
import { prisma } from '../app/lib/prisma.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 { encodeMatch, decodeMatchShareCode } from 'csgo-sharecode';
import { log } from '../../scripts/cs2-cron-runner.js';
import { getNextShareCodeFromAPI } from './getNextShareCodeFromAPI.js';
import { updatePremierRanksForUser } from './updatePremierRanks';
import fs from 'fs';
import path from 'path';
let isRunning = false;
@ -64,7 +67,7 @@ async function runMatchCheck() {
},
});
await sendServerWebSocketMessage({
await sendServerSSEMessage({
type: 'expired-sharecode',
targetUserIds: [user.steamId],
message: notification.message,
@ -117,6 +120,22 @@ async function runMatchCheck() {
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({
...user,
lastKnownShareCode: shareCode,
@ -170,7 +189,7 @@ async function runMatchCheck() {
},
});
await sendServerWebSocketMessage({
await sendServerSSEMessage({
type: 'new-cs2-match',
targetUserIds: [user.steamId],
message: notification.message,

View File

@ -3,6 +3,7 @@ import path from 'path';
import { Match, User } from '@/generated/prisma';
import { parseAndStoreDemo } from './parseAndStoreDemo';
import { log } from '../../scripts/cs2-cron-runner.js';
import { prisma } from '../app/lib/prisma.js';
export async function runDownloaderForUser(user: User): Promise<{
newMatches: Match[];
@ -37,7 +38,19 @@ export async function runDownloaderForUser(user: User): Promise<{
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 match = await parseAndStoreDemo(absolutePath, steamId, shareCode);