From d1c0d9297c02f51ec7c671ef1d26cb8c6da3cff7 Mon Sep 17 00:00:00 2001 From: Linrador Date: Mon, 20 Oct 2025 10:32:47 +0200 Subject: [PATCH] updated --- .env | 19 +- next.config.ts | 1 - .../admin/teams/[teamId]/TeamAdminClient.tsx | 30 +-- .../components/EditMatchMetaModal.tsx | 22 ++- .../components/EditMatchPlayersModal.tsx | 187 +++++++++--------- src/app/[locale]/components/MapVotePanel.tsx | 10 +- src/app/[locale]/components/MatchDetails.tsx | 22 ++- .../[locale]/components/MatchReadyOverlay.tsx | 1 + src/app/[locale]/page.tsx | 19 +- src/app/[locale]/schedule/page.tsx | 12 +- src/app/api/cs2/server/send-command/route.ts | 4 +- .../api/matches/[matchId]/mapvote/route.ts | 35 +++- src/app/api/ptero/send-command/route.ts | 2 +- src/app/api/user/activity/route.ts | 4 + src/generated/prisma/edge.js | 4 +- src/generated/prisma/index.js | 4 +- src/generated/prisma/wasm.js | 4 +- src/i18n/navigation.ts | 6 +- src/i18n/request.ts | 21 +- src/lib/sse-server-client.ts | 6 +- src/lib/useSSEStore.ts | 2 +- src/middleware.ts | 37 +--- src/worker/config/index.ts | 2 +- src/worker/parsers/demoParser.ts | 2 +- 24 files changed, 251 insertions(+), 205 deletions(-) diff --git a/.env b/.env index f0bd3a9..6da7274 100644 --- a/.env +++ b/.env @@ -9,19 +9,20 @@ SHARE_CODE_SECRET_KEY=6f9d4a2951b8eae35cdd3fb28e1a74550d177c3900ad1111c8e48b4e3b SHARE_CODE_IV=9f1d67b8a3c4d261fa2b7c44a1d4f9c8 STEAM_API_KEY=0B3B2BF79ECD1E9262BB118A7FEF1973 NEXTAUTH_SECRET=ironieopen -NEXTAUTH_URL=https://new.ironieopen.de -NEXT_PUBLIC_APP_URL=https://new.ironieopen.de +#NEXTAUTH_URL=https://new.ironieopen.de +#NEXT_PUBLIC_APP_URL=https://new.ironieopen.de +NEXTAUTH_URL=https://ironieopen.local +NEXT_PUBLIC_APP_URL=https://ironieopen.local AUTH_SECRET="57AUHXa+UmFrlnIEKxtrk8fLo+aZMtsa/oV6fklXkcE=" # Added by `npx auth`. Read more: https://cli.authjs.dev ALLSTAR_TOKEN=ed033ac0-5df7-482e-a322-e2b4601955d3 -PTERODACTYL_APP_API=ptla_O6Je82OvlCBFITDRgB1ZJ95AIyUSXYnVGgwRF6pO6d9 -PTERODACTYL_CLIENT_API=ptlc_c31BKDEXy63fHUxeQDahk6eeC3CL19TpG2rgao7mUl5 +PTERODACTYL_APP_API=ptla_6IcEHfK0CMiA5clzFSGXPEhczC9jTRZz7pr8sNn1iSB +PTERODACTYL_CLIENT_API=ptlc_hSYxGaVlp7dklvSWAcuGjjGfcBiCaAedKXYnI3SKMV3 PTERODACTYL_PANEL_URL=https://panel.ironieopen.de PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022 -PTERO_SERVER_SFTP_USER=army.37a11489 +PTERO_SERVER_SFTP_USER=army.acdef8fc PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM -PTERO_SERVER_ID=37a11489 - -NEXT_PUBLIC_SSE_URL=https://new.ironieopen.de/events +PTERO_SERVER_ID=acdef8fc +TRUST_PROXY=1 # META (vom CS2-Server-Plugin) NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST=new.ironieopen.de @@ -37,4 +38,4 @@ NEXT_PUBLIC_CS2_GAME_WS_SCHEME=wss NEXT_PUBLIC_CONNECT_HREF="steam://connect/94.130.66.149:27015/0000" -FACEIT_API_KEY=28ff4916-65da-4415-ba67-3d6d6b5dc850 +FACEIT_API_KEY=28ff4916-65da-4415-ba67-3d6d6b5dc850 \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 0d0ed6d..84b5796 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,7 +4,6 @@ import type { NextConfig } from 'next' import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { - output: 'standalone', images: { remotePatterns: [ { diff --git a/src/app/[locale]/admin/teams/[teamId]/TeamAdminClient.tsx b/src/app/[locale]/admin/teams/[teamId]/TeamAdminClient.tsx index 9e76538..57516bc 100644 --- a/src/app/[locale]/admin/teams/[teamId]/TeamAdminClient.tsx +++ b/src/app/[locale]/admin/teams/[teamId]/TeamAdminClient.tsx @@ -35,29 +35,31 @@ export default function TeamAdminClient({ teamId }: Props) { const steamId = session?.user?.steamId if (!steamId) return - const base = process.env.NEXT_PUBLIC_SSE_URL - const url = `${base}/events?steamId=${encodeURIComponent(steamId)}` - let es: EventSource | null = new EventSource(url, { withCredentials: false }) + const raw = process.env.NEXT_PUBLIC_APP_URL?.trim() + let origin: string + try { + origin = raw && /^https?:\/\//i.test(raw) ? new URL(raw).origin : window.location.origin + } catch { + origin = window.location.origin + } + + const u = new URL('/events', origin) // überschreibt jeden Pfad + u.searchParams.set('steamId', steamId) + + let es: EventSource | null = new EventSource(u.toString(), { withCredentials: false }) - // Listener als EventListener typisieren const onTeamUpdated: EventListener = (ev) => { try { const msg = JSON.parse((ev as MessageEvent).data as string) - if (msg.teamId === teamId) { - fetchTeam() - } - } catch (e) { - console.error('[SSE] parse error:', e) - } + if (msg.teamId === teamId) fetchTeam() + } catch (e) { console.error('[SSE] parse error:', e) } } es.addEventListener('team-updated', onTeamUpdated) - es.onerror = () => { - es?.close() - es = null + es?.close(); es = null setTimeout(() => { - const next = new EventSource(url, { withCredentials: false }) + const next = new EventSource(u.toString(), { withCredentials: false }) next.addEventListener('team-updated', onTeamUpdated) next.onerror = () => { next.close() } es = next diff --git a/src/app/[locale]/components/EditMatchMetaModal.tsx b/src/app/[locale]/components/EditMatchMetaModal.tsx index 712f79b..887e133 100644 --- a/src/app/[locale]/components/EditMatchMetaModal.tsx +++ b/src/app/[locale]/components/EditMatchMetaModal.tsx @@ -76,6 +76,16 @@ function combineLocalDateTime(dateStr: string, timeStr: string) { return dt.toISOString(); } +/** HH:MM auf 5-Minuten-Raster snappen (floor) */ +function snapTo5(timeStr: string) { + const [hhRaw, mmRaw] = (timeStr || '00:00').split(':'); + const hh = Math.max(0, Math.min(23, Number(hhRaw) || 0)); + const mm = Math.max(0, Math.min(59, Number(mmRaw) || 0)); + const mm5 = Math.floor(mm / 5) * 5; + const pad2 = (n:number)=>String(n).padStart(2,'0'); + return `${pad2(hh)}:${pad2(mm5)}`; +} + export default function EditMatchMetaModal({ show, onClose, @@ -104,6 +114,7 @@ export default function EditMatchMetaModal({ const pad2 = (n:number)=>String(n).padStart(2,'0'); const hours = Array.from({ length: 24 }, (_, i) => i); const quarters = [0, 15, 30, 45]; + const minutes5 = [0,5,10,15,20,25,30,35,40,45,50,55]; // Map-Vote öffnet: Datum & Uhrzeit (lokal) const [voteOpenDateStr, setVoteOpenDateStr] = useState(''); // YYYY-MM-DD @@ -202,7 +213,7 @@ export default function EditMatchMetaModal({ const openISO = new Date(new Date(matchISO).getTime() - leadMin * 60_000).toISOString(); const openDT = isoToLocalDateTimeStrings(openISO, userTZ); setVoteOpenDateStr(openDT.dateStr); - setVoteOpenTimeStr(openDT.timeStr); + setVoteOpenTimeStr(snapTo5(openDT.timeStr)); const boFromMeta = normalizeBestOf(j?.bestOf) setBestOf(boFromMeta) @@ -450,18 +461,19 @@ export default function EditMatchMetaModal({ {hours.map(h => )} - {/* Minuten: 00/15/30/45 */} + {/* Minuten: 5er-Raster */} + diff --git a/src/app/[locale]/components/EditMatchPlayersModal.tsx b/src/app/[locale]/components/EditMatchPlayersModal.tsx index 66c20c7..4e33f1d 100644 --- a/src/app/[locale]/components/EditMatchPlayersModal.tsx +++ b/src/app/[locale]/components/EditMatchPlayersModal.tsx @@ -1,19 +1,17 @@ -// /src/app/[locale]/components/EditMatchPlayersModal.tsx 'use client' -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useRef, useState } from 'react' import { useSession } from 'next-auth/react' import { DndContext, closestCenter, DragOverlay, type DragStartEvent, type DragEndEvent } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' -import Modal from '../components/Modal' +import Modal from '../components/Modal' import SortableMiniCard from '../components/SortableMiniCard' -import LoadingSpinner from '../components/LoadingSpinner' +import LoadingSpinner from '../components/LoadingSpinner' import { DroppableZone } from '../components/DroppableZone' import type { Player, Team } from '../../../types/team' -/* ───────────────────────── Typen ────────────────────────── */ export type EditSide = 'A' | 'B' interface Props { @@ -22,61 +20,75 @@ interface Props { matchId : string teamA : Team teamB : Team - side : EditSide // welches Team wird editiert? - initialA: string[] // bereits eingesetzte Spieler-IDs + side : EditSide + initialA: string[] initialB: string[] onSaved?: () => void } -/* ───────────────────── Komponente ──────────────────────── */ export default function EditMatchPlayersModal (props: Props) { - const { - show, onClose, matchId, - teamA, teamB, side, - initialA, initialB, - onSaved, - } = props + const { show, onClose, matchId, teamA, teamB, side, initialA, initialB, onSaved } = props /* ---- Rollen-Check --------------------------------------- */ const { data: session } = useSession() const meSteam = session?.user?.steamId - const isAdmin = session?.user?.isAdmin + const isAdmin = !!session?.user?.isAdmin const isLeader = side === 'A' ? meSteam === teamA.leader?.steamId : meSteam === teamB.leader?.steamId const canEdit = isAdmin || isLeader + /* ---- Team-Info ------------------------------------------ */ + const team = side === 'A' ? teamA : teamB + const other = side === 'A' ? teamB : teamA + const otherInit = side === 'A' ? initialB : initialA + const myInit = side === 'A' ? initialA : initialB + /* ---- States --------------------------------------------- */ - const [players, setPlayers] = useState([]) - const [selected, setSelected] = useState([]) - const [dragItem, setDragItem] = useState(null) - const [saving, setSaving] = useState(false) - const [saved, setSaved] = useState(false) + const [players, setPlayers] = useState([]) + const [selected, setSelected] = useState([]) + const [dragItem, setDragItem] = useState(null) + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - /* ---- Team-Info ------------------------------------------ */ - const team = side === 'A' ? teamA : teamB - const other = side === 'A' ? teamB : teamA - const otherInit = side === 'A' ? initialB : initialA - const myInit = side === 'A' ? initialA : initialB + // 🧷 Snapshots, die NUR beim Öffnen gesetzt werden + const myInitSnapRef = useRef([]) + const otherInitSnapRef = useRef([]) + const otherInitSetRef = useRef>(new Set()) - // 🔧 NEU: schnelles Lookup der "verbotenen" Spieler (bereits im anderen Team) - const otherInitSet = useMemo(() => new Set(otherInit), [otherInit]) + /* ---- Initialisierung NUR beim Öffnen -------------------- */ + useEffect(() => { + if (!show) return + // Inhalte zum Zeitpunkt des Öffnens sichern + myInitSnapRef.current = Array.isArray(myInit) ? [...myInit] : [] + otherInitSnapRef.current = Array.isArray(otherInit) ? [...otherInit] : [] + otherInitSetRef.current = new Set(otherInitSnapRef.current) - /* ---- Komplett-Spielerliste laden ------------------------ */ + // Selected EINMAL setzen (und nicht mehr durch Polling überschreiben) + const initialSelected = myInitSnapRef.current + .filter(id => !otherInitSetRef.current.has(id)) + .slice(0, 5) + + setSelected(initialSelected) + setSaved(false) + setError(null) + // bewusst NUR an "show" hängen (Snapshot beim Öffnen) + }, [show]) // eslint-disable-line react-hooks/exhaustive-deps + + /* ---- Team laden – nur an show + team.id koppeln --------- */ useEffect(() => { if (!show) return if (!team?.id) { - // ❗ Kein verknüpftes Team – zeig einen klaren Hinweis setPlayers([]) - setSelected([]) setError('Kein Team mit diesem Match verknüpft (fehlende Team-ID).') setLoading(false) return } + const ctrl = new AbortController() setLoading(true) setError(null) @@ -84,6 +96,7 @@ export default function EditMatchPlayersModal (props: Props) { try { const res = await fetch(`/api/team/${encodeURIComponent(team.id)}`, { cache: 'no-store', + signal: ctrl.signal, }) if (!res.ok) { setError(`Team-API: ${res.status}`) @@ -96,28 +109,32 @@ export default function EditMatchPlayersModal (props: Props) { ...(data.activePlayers ?? []), ...(data.inactivePlayers ?? []), ] - .filter((p: Player) => !!p?.steamId) - .filter((p: Player, i: number, arr: Player[]) => arr.findIndex(x => x.steamId === p.steamId) === i) + .filter((p: Player) => !!p?.steamId) + .filter((p: Player, i: number, arr: Player[]) => arr.findIndex(x => x.steamId === p.steamId) === i) - // 🔧 NEU: Spieler entfernen, die im anderen Team bereits gesetzt sind + // mit Snapshot des anderen Teams filtern (stabil während des Editierens) + const otherSet = otherInitSetRef.current const all = allRaw - .filter((p: Player) => !otherInitSet.has(p.steamId)) + .filter((p: Player) => !otherSet.has(p.steamId)) .sort((a: Player, b: Player) => (a.name || '').localeCompare(b.name || '')) setPlayers(all) - setSelected(myInit.filter(id => !otherInitSet.has(id))) - setSaved(false) - } catch (e) { - console.error('[EditMatchPlayersModal] load error:', e) - setError('Laden fehlgeschlagen') - setPlayers([]) + } catch (e: unknown) { + const name = (e as { name?: unknown })?.name + if (name !== 'AbortError') { + console.error('[EditMatchPlayersModal] load error:', e) + setError('Laden fehlgeschlagen') + setPlayers([]) + } } finally { setLoading(false) } })() - }, [show, team?.id, myInit, otherInitSet]) - /* ---- Drag’n’Drop-Handler -------------------------------- */ + return () => ctrl.abort() + }, [show, team?.id]) // ⚠️ keine Abhängigkeit von myInit/otherInit oder Sets (Refs sind stabil) + + /* ---- Drag’n’Drop ---------------------------------------- */ const onDragStart = ({ active }: DragStartEvent) => { setDragItem(players.find(p => p.steamId === active.id) ?? null) } @@ -125,19 +142,13 @@ export default function EditMatchPlayersModal (props: Props) { const onDragEnd = ({ active, over }: DragEndEvent) => { setDragItem(null) if (!over) return + const id = active.id as string + const dropZone = over.id as string // "active" | "inactive" + const already = selected.includes(id) + const toActive = dropZone === 'active' + if ((toActive && already) || (!toActive && !already)) return - const id = active.id as string - const dropZone = over.id as string // "active" | "inactive" - const already = selected.includes(id) - const toActive = dropZone === 'active' - - if ( (toActive && already) || (!toActive && !already) ) return - - setSelected(sel => - toActive - ? [...sel, id].slice(0, 5) // max 5 einsatzfähig - : sel.filter(x => x !== id), - ) + setSelected(sel => toActive ? [...sel, id].slice(0, 5) : sel.filter(x => x !== id)) } /* ---- Speichern ------------------------------------------ */ @@ -147,23 +158,18 @@ export default function EditMatchPlayersModal (props: Props) { const body = { players: [ ...selected.map(steamId => ({ steamId, teamId: team.id })), - ...otherInit.map(steamId => ({ steamId, teamId: other.id })), + ...otherInitSnapRef.current.map(steamId => ({ steamId, teamId: other.id })), ], } - const res = await fetch(`/api/matches/${matchId}`, { method : 'PUT', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify(body), }) if (!res.ok) throw new Error() - setSaved(true) - - // ⏳ 3 Sekunden warten, dann schließen und danach refreshen setTimeout(() => { onClose?.() - // onSaved (z. B. router.refresh) im nächsten Tick nach dem Schließen setTimeout(() => { onSaved?.() }, 0) }, 1500) } catch (e) { @@ -173,13 +179,11 @@ export default function EditMatchPlayersModal (props: Props) { } } - - /* ---- Listen trennen ------------------------------------- */ + /* ---- Listen --------------------------------------------- */ const active = players.filter(p => selected.includes(p.steamId)) const inactive = players.filter(p => !selected.includes(p.steamId)) /* ---- UI -------------------------------------------------- */ - return ( - {loading && } + <> + {loading && } - {!loading && error && ( -

Fehler: {error}

- )} + {!loading && error && ( +

Fehler: {error}

+ )} - {!loading && !error && players.length === 0 && ( -

Keine Spieler gefunden.

- )} + {!loading && !error && players.length === 0 && ( +

Keine Spieler gefunden.

+ )} - {!loading && !error && players.length > 0 && ( - - {/* --- Zone: Aktuell eingestellte Spieler ------------- */} + {!loading && !error && players.length > 0 && ( + - p.steamId)} - strategy={verticalListSortingStrategy} - > + p.steamId)} strategy={verticalListSortingStrategy}> {active.map(p => ( ))} - {/* --- Zone: Verfügbar (restliche) ------------------- */} - - p.steamId)} - strategy={verticalListSortingStrategy} - > + + p.steamId)} strategy={verticalListSortingStrategy}> {inactive.map(p => ( ))} - {/* Drag-Overlay */} {dragItem && ( )} diff --git a/src/app/[locale]/components/MapVotePanel.tsx b/src/app/[locale]/components/MapVotePanel.tsx index 8aea56f..263d439 100644 --- a/src/app/[locale]/components/MapVotePanel.tsx +++ b/src/app/[locale]/components/MapVotePanel.tsx @@ -1069,6 +1069,11 @@ export default function MapVotePanel({ match }: Props) { const taken = !!status const isAvailable = !taken && isMyTurn && isOpen && !state?.locked + const bgOpacityClass = + !taken && leftIsActiveTurn + ? 'opacity-70' // sichtbarer, „weniger transparent“ + : 'opacity-30' // Standard + const intent = isAvailable ? currentStep?.action : null const intentStyles = intent === 'ban' @@ -1132,7 +1137,10 @@ export default function MapVotePanel({ match }: Props) { onTouchEnd={onTouchEnd(map)} onTouchCancel={onTouchEnd(map)} > -
+
{showProgress && ( )} diff --git a/src/app/[locale]/components/MatchDetails.tsx b/src/app/[locale]/components/MatchDetails.tsx index 22a603c..e8dbba9 100644 --- a/src/app/[locale]/components/MatchDetails.tsx +++ b/src/app/[locale]/components/MatchDetails.tsx @@ -5,6 +5,7 @@ import { useState, useEffect, useMemo, useRef } from 'react' import { useRouter } from 'next/navigation' import { useSession } from 'next-auth/react' +import MapVoteBanner from './MapVoteBanner' import Table from './Table' import PremierRankBadge from './PremierRankBadge' import CompRankBadge from './CompRankBadge' @@ -14,7 +15,6 @@ import type { EditSide } from './EditMatchPlayersModal' import type { Match, MatchPlayer } from '../../../types/match' import Button from './Button' import { MAP_OPTIONS } from '@/lib/mapOptions' -import MapVoteBanner from './MapVoteBanner' import { useSSEStore } from '@/lib/useSSEStore' import { Team } from '../../../types/team' import Alert from './Alert' @@ -911,10 +911,14 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu {/* Team A */}
- {isCommunity && match.teamA?.logo && ( + {isCommunity && ( {match.teamA.name )} @@ -951,10 +955,14 @@ export function MatchDetails({match, initialNow}: { match: Match; initialNow: nu {match.teamB?.name ?? 'Unbekannt'}
- {isCommunity && match.teamB?.logo && ( + {isCommunity && ( {match.teamB.name )} diff --git a/src/app/[locale]/components/MatchReadyOverlay.tsx b/src/app/[locale]/components/MatchReadyOverlay.tsx index 47d2604..6bcf340 100644 --- a/src/app/[locale]/components/MatchReadyOverlay.tsx +++ b/src/app/[locale]/components/MatchReadyOverlay.tsx @@ -519,6 +519,7 @@ export default function MatchReadyOverlay({ className="absolute inset-0 object-cover" decoding="async" priority + unoptimized />
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 1d71aab..f835269 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -20,6 +20,13 @@ function parseTeams(json: TeamsJson): TeamLike[] { return [] } +const resolveLogoSrc = (logo?: string | null): string => { + if (!logo) return '/assets/img/logos/cs2.webp' // Fallback im /public + if (logo.startsWith('http://') || logo.startsWith('https://')) return logo + if (logo.startsWith('/')) return logo // bereits ab Root + return `/assets/img/logos/${logo}` // Dateiname -> public-Pfad +} + export default function Dashboard() { const t = useTranslations('dashboard') @@ -156,15 +163,21 @@ export default function Dashboard() {
    {teams.map((t) => { const label = t.teamname ?? t.name ?? 'Unbenannt' - const logoPath = t.logo ? `/assets/img/logos/${t.logo}` : '/assets/img/logos/cs2.webp' + const logoSrc = resolveLogoSrc(t.logo) return (
  • -
    - {label} +
    + {label}
    {label}
    diff --git a/src/app/[locale]/schedule/page.tsx b/src/app/[locale]/schedule/page.tsx index cc80a3c..25f80f7 100644 --- a/src/app/[locale]/schedule/page.tsx +++ b/src/app/[locale]/schedule/page.tsx @@ -26,8 +26,14 @@ export default function MatchesPage() { }, []) return ( - - - +
    {/* Höhe vorgeben + min-h-0 */} + + + +
    ) } diff --git a/src/app/api/cs2/server/send-command/route.ts b/src/app/api/cs2/server/send-command/route.ts index 2da9fdb..2318dfa 100644 --- a/src/app/api/cs2/server/send-command/route.ts +++ b/src/app/api/cs2/server/send-command/route.ts @@ -30,9 +30,9 @@ export async function POST(req: NextRequest) { // Panel-URL aus ENV (wie in mapvote) const panelBase = - (process.env.PTERO_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERO_PANEL_URL ?? '').trim() + (process.env.PTERODACTYL_PANEL_URL ?? process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL ?? '').trim() if (!panelBase) { - return NextResponse.json({ error: 'PTERO_PANEL_URL not set' }, { status: 500 }) + return NextResponse.json({ error: 'PTERODACTYL_PANEL_URL not set' }, { status: 500 }) } // Server-ID aus DB (optional via Body überschreibbar) diff --git a/src/app/api/matches/[matchId]/mapvote/route.ts b/src/app/api/matches/[matchId]/mapvote/route.ts index 25782fd..54159f0 100644 --- a/src/app/api/matches/[matchId]/mapvote/route.ts +++ b/src/app/api/matches/[matchId]/mapvote/route.ts @@ -119,11 +119,11 @@ function buildPteroClientUrl(base: string, serverId: string) { async function sendServerCommand(command: string) { try { const panelBase = - process.env.PTERO_PANEL_URL || - process.env.NEXT_PUBLIC_PTERO_PANEL_URL || + process.env.PTERODACTYL_PANEL_URL || + process.env.NEXT_PUBLIC_PTERODACTYL_PANEL_URL || '' if (!panelBase) { - console.warn('[mapvote] PTERO_PANEL_URL fehlt – Command wird nicht gesendet.') + console.warn('[mapvote] PTERODACTYL_PANEL_URL fehlt – Command wird nicht gesendet.') return } @@ -419,6 +419,24 @@ type MatchLike = { type MapVoteStep = { action: 'ban' | 'pick' | 'decider'; map?: string | null; teamId?: string | null } type MapVoteStateForExport = { bestOf?: number; steps: MapVoteStep[]; locked?: boolean } +const SPECTATORS = [ + ['76561198006541937', 'Mützchen'], + ['76561197971310489', 'Fail'], + ['76561198006697116', 'Schlomo'], + ['76561198153867594', 'Luther'], + ['76561198022207129', 'Lizec'], + ['76561198132260879', 'SuperDuperDaria'], + ['76561198006470215', 'Jagarion'], + ['76561198986703551', 'captainanni'], + ['76561198063413621', 'Litboi'], + ['76561198133191395', 'Windy'], + ['76561198094445415', 'Torrul'], + ['76561199026888445', 'Flix Geduldnix'], +] as const; + +const SPECTATORS_MAP: Record = + Object.fromEntries(SPECTATORS) as Record; + function playersMapFromList(list: PlayerLike[] | undefined) { const out: Record = {} for (const p of list ?? []) { @@ -453,17 +471,21 @@ function buildMatchJson(match: MatchLike, state: MapVoteStateForExport) { const team1Players = playersMapFromList(match.teamA?.players) const team2Players = playersMapFromList(match.teamB?.players) - // 👇 hier neu: zufällige Integer-ID const rndId = makeRandomMatchId() return { - matchid: rndId, // vorher: "" + matchid: rndId, team1: { name: team1Name, players: team1Players }, team2: { name: team2Name, players: team2Players }, num_maps: bestOf, maplist, map_sides, - spectators: { players: {} as Record }, + + // ⬇️ NEU: feste Spectators mitsenden + spectators: { + players: SPECTATORS_MAP, + }, + clinch_series: true, players_per_team: 5, cvars: { @@ -473,6 +495,7 @@ function buildMatchJson(match: MatchLike, state: MapVoteStateForExport) { } } + async function exportMatchToSftpDirect(match: MatchDb, vote: VoteDb) { try { const SFTPClient = (await import('ssh2-sftp-client')).default diff --git a/src/app/api/ptero/send-command/route.ts b/src/app/api/ptero/send-command/route.ts index 619d5fe..9071c87 100644 --- a/src/app/api/ptero/send-command/route.ts +++ b/src/app/api/ptero/send-command/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server' -const PANEL = process.env.PTERO_PANEL_URL! +const PANEL = process.env.PTERODACTYL_PANEL_URL! const KEY = process.env.PTERODACTYL_CLIENT_API! const SID = process.env.PTERO_SERVER_ID! diff --git a/src/app/api/user/activity/route.ts b/src/app/api/user/activity/route.ts index 27143c8..311ca59 100644 --- a/src/app/api/user/activity/route.ts +++ b/src/app/api/user/activity/route.ts @@ -5,6 +5,10 @@ import { sessionAuthOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { sendServerSSEMessage } from '@/lib/sse-server-client' +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +import 'server-only'; + export async function POST() { const session = await getServerSession(sessionAuthOptions) const steamId = session?.user?.steamId // <-- hier definieren diff --git a/src/generated/prisma/edge.js b/src/generated/prisma/edge.js index 1a99b57..743ca91 100644 --- a/src/generated/prisma/edge.js +++ b/src/generated/prisma/edge.js @@ -439,7 +439,8 @@ const config = { "isCustomOutput": true }, "relativeEnvPaths": { - "rootEnvPath": null + "rootEnvPath": null, + "schemaEnvPath": "../../../.env" }, "relativePath": "../../../prisma", "clientVersion": "6.17.1", @@ -448,7 +449,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": false, "inlineDatasources": { "db": { "url": { diff --git a/src/generated/prisma/index.js b/src/generated/prisma/index.js index 8c20745..c03d3b1 100644 --- a/src/generated/prisma/index.js +++ b/src/generated/prisma/index.js @@ -440,7 +440,8 @@ const config = { "isCustomOutput": true }, "relativeEnvPaths": { - "rootEnvPath": null + "rootEnvPath": null, + "schemaEnvPath": "../../../.env" }, "relativePath": "../../../prisma", "clientVersion": "6.17.1", @@ -449,7 +450,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": false, "inlineDatasources": { "db": { "url": { diff --git a/src/generated/prisma/wasm.js b/src/generated/prisma/wasm.js index 57e1147..118f005 100644 --- a/src/generated/prisma/wasm.js +++ b/src/generated/prisma/wasm.js @@ -439,7 +439,8 @@ const config = { "isCustomOutput": true }, "relativeEnvPaths": { - "rootEnvPath": null + "rootEnvPath": null, + "schemaEnvPath": "../../../.env" }, "relativePath": "../../../prisma", "clientVersion": "6.17.1", @@ -448,7 +449,6 @@ const config = { "db" ], "activeProvider": "postgresql", - "postinstall": false, "inlineDatasources": { "db": { "url": { diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts index 416eb07..a5909f5 100644 --- a/src/i18n/navigation.ts +++ b/src/i18n/navigation.ts @@ -2,8 +2,6 @@ import {createNavigation} from 'next-intl/navigation'; import {routing} from './routing'; - -// Lightweight wrappers around Next.js' navigation -// APIs that consider the routing configuration + export const {Link, redirect, usePathname, useRouter, getPathname} = - createNavigation(routing); \ No newline at end of file + createNavigation(routing); diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 80179f2..c2d3ab5 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -1,15 +1,14 @@ // /src/i18n/request.ts -export const runtime = 'nodejs'; // wichtig: nicht Edge +import {getRequestConfig} from 'next-intl/server' +import {hasLocale} from 'next-intl' +import {routing} from './routing' -import {getRequestConfig} from 'next-intl/server'; -import {hasLocale} from 'next-intl'; -import {routing} from './routing'; +export default getRequestConfig(async ({requestLocale}) => { + const requested = await requestLocale + const locale = hasLocale(routing.locales, requested) ? (requested as string) : routing.defaultLocale -export default getRequestConfig(async ({locale}) => { - const effective = - hasLocale(routing.locales, locale) ? (locale as string) : routing.defaultLocale; + // ⬇️ Eine JSON pro Locale laden + const messages = (await import(`../messages/${locale}.json`)).default - const messages = (await import(`../messages/${effective}.json`)).default; - - return { locale: effective, messages }; -}); + return { locale, messages } +}) diff --git a/src/lib/sse-server-client.ts b/src/lib/sse-server-client.ts index 35c33a3..e1a4965 100644 --- a/src/lib/sse-server-client.ts +++ b/src/lib/sse-server-client.ts @@ -1,6 +1,6 @@ // /src/lib/sse-server-client.ts -const host = process.env.NEXT_PUBLIC_SSE_URL +const host = process.env.NEXTAUTH_URL export type ServerSSEMessage = { type: string @@ -8,8 +8,8 @@ export type ServerSSEMessage = { export async function sendServerSSEMessage(message: ServerSSEMessage): Promise { try { - console.log('Sending message:', message) - const res = await fetch(`http://${host}/send`, { + //console.log('Sending message:', message) + const res = await fetch(`${host}/send`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(message), diff --git a/src/lib/useSSEStore.ts b/src/lib/useSSEStore.ts index 2292e1b..6a445f6 100644 --- a/src/lib/useSSEStore.ts +++ b/src/lib/useSSEStore.ts @@ -84,7 +84,7 @@ export const useSSEStore = create((set, get) => { set({ source: null, isConnected: false }); } - const base = process.env.NEXT_PUBLIC_SSE_URL + const base = process.env.NEXT_PUBLIC_APP_URL const url = `${base}/events?steamId=${encodeURIComponent(steamId)}`; const source = new EventSource(url); diff --git a/src/middleware.ts b/src/middleware.ts index c513f33..3cd806b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,4 +1,4 @@ -// /src/middleware.ts +// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import createIntlMiddleware from 'next-intl/middleware'; @@ -7,10 +7,12 @@ import { routing } from './i18n/routing'; const handleI18n = createIntlMiddleware(routing); +// ---- Type-Guard: prüft, ob ein Objekt eine boolsche isAdmin-Property hat function hasAdminFlag(v: unknown): v is { isAdmin: boolean } { return typeof v === 'object' && v !== null && typeof (v as Record).isAdmin === 'boolean'; } + function getCurrentLocaleFromPath(pathname: string, locales: readonly string[], fallback: string) { const first = pathname.split('/')[1]; return locales.includes(first) ? first : fallback; @@ -26,10 +28,10 @@ function stripLeadingLocale(pathname: string, locales: readonly string[]) { } function isProtectedPath(pathnameNoLocale: string) { return ( - pathnameNoLocale === '/' || - pathnameNoLocale.startsWith('/settings') || - pathnameNoLocale.startsWith('/matches') || - pathnameNoLocale.startsWith('/team') || + pathnameNoLocale.startsWith('/') || + pathnameNoLocale.startsWith('/settings') || + pathnameNoLocale.startsWith('/matches') || + pathnameNoLocale.startsWith('/team') || pathnameNoLocale.startsWith('/admin') ); } @@ -52,31 +54,10 @@ export default async function middleware(req: NextRequest) { } const i18nRes = handleI18n(req); - - // 1) Rewrites -> immer auf aktuelle öffentliche Origin - const rew = i18nRes.headers.get('x-middleware-rewrite'); - if (rew) { - const t = new URL(rew, req.url); - const out = new URL(req.url); - out.pathname = t.pathname; - out.search = t.search; - return NextResponse.rewrite(out); - } - - // 2) Redirects (Location) -> ebenfalls auf aktuelle Origin mappen - const loc = i18nRes.headers.get('location'); - if (loc) { - const t = new URL(loc, req.url); - if (t.hostname !== req.nextUrl.hostname) { - const out = new URL(req.url); - out.pathname = t.pathname; - out.search = t.search; - return NextResponse.redirect(out); // 307 - } + if (i18nRes.headers.get('location') || i18nRes.headers.get('x-middleware-rewrite')) { return i18nRes; } - // 3) Geschützte Bereiche const { locales, defaultLocale } = routing; const url = req.nextUrl; const pathnameNoLocale = stripLeadingLocale(pathname, locales); @@ -86,6 +67,7 @@ export default async function middleware(req: NextRequest) { const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); + // Adminschutz (ohne any) if (pathnameNoLocale.startsWith('/admin')) { const isAdmin = hasAdminFlag(token) && token.isAdmin === true; if (!isAdmin) { @@ -96,6 +78,7 @@ export default async function middleware(req: NextRequest) { } } + // Allgemeiner Auth-Schutz if (!token) { const loginUrl = new URL('/api/auth/signin', req.url); loginUrl.searchParams.set('callbackUrl', url.toString()); diff --git a/src/worker/config/index.ts b/src/worker/config/index.ts index 6c882b2..ee6fe5e 100644 --- a/src/worker/config/index.ts +++ b/src/worker/config/index.ts @@ -12,7 +12,7 @@ export const CS2_DOWNLOADER_URL = process.env.CS2_DOWNLOADER_URL ?? 'http://localhost:4000'; export const CS2_INTERNAL_API_URL = - process.env.CS2_INTERNAL_API_URL ?? 'http://localhost:3000'; + process.env.CS2_INTERNAL_API_URL ?? 'http://localhost:30000'; export const DEMOS_ROOT = process.env.DEMOS_ROOT ?? 'demos'; diff --git a/src/worker/parsers/demoParser.ts b/src/worker/parsers/demoParser.ts index 5372715..310e88a 100644 --- a/src/worker/parsers/demoParser.ts +++ b/src/worker/parsers/demoParser.ts @@ -36,7 +36,7 @@ export async function parseDemoViaGo( const parserPath = path.resolve( __dirname, - '../../../../ironie-cs2-parser/parser_cs2-win.exe' + '../../../../ironie-cs2-parser/parser_cs2-linux' ); const decoded = decodeMatchShareCode(shareCode); const matchId = decoded.matchId.toString();