show escrow accounts
This commit is contained in:
@@ -29,6 +29,9 @@ export default async function LoginPage({ searchParams }: Props) {
|
||||
id="username"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
className="mt-1 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 shadow-sm outline-none ring-zinc-400 focus:ring-2 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-50 dark:ring-zinc-500"
|
||||
required
|
||||
/>
|
||||
@@ -45,6 +48,9 @@ export default async function LoginPage({ searchParams }: Props) {
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
className="mt-1 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-zinc-900 shadow-sm outline-none ring-zinc-400 focus:ring-2 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-50 dark:ring-zinc-500"
|
||||
required
|
||||
/>
|
||||
|
||||
+14
-1
@@ -30,6 +30,7 @@ import {
|
||||
type MatchmakerLogSource,
|
||||
} from "@/lib/matchmaker-log-source";
|
||||
import { readMatchmakerLogFile } from "@/lib/matchmaker-logs-server";
|
||||
import { replayFileExists } from "@/lib/match-replays-server";
|
||||
import { fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger";
|
||||
import {
|
||||
analyzeMatchLogsForMatches,
|
||||
@@ -202,6 +203,7 @@ export default async function Home({
|
||||
let matchmakerError: string | null = null;
|
||||
let auditEntries: AuditLogEntry[] = [];
|
||||
let auditError: string | null = null;
|
||||
let matchIdsWithReplay: number[] = [];
|
||||
if (tab === "matchmaker") {
|
||||
const mkRaw = firstSearchParam(sp.mklog);
|
||||
matchmakerSource = parseMatchmakerLogParam(mkRaw);
|
||||
@@ -228,7 +230,7 @@ export default async function Home({
|
||||
} else {
|
||||
const usersRes = await supabase
|
||||
.from("users")
|
||||
.select("id, created_at, username, email, ip_address, cc, rc, last_logged_at")
|
||||
.select("id, created_at, username, email, ip_address, cc, rc, mmr, last_logged_at")
|
||||
.order("id", { ascending: false })
|
||||
.limit(500);
|
||||
|
||||
@@ -307,6 +309,16 @@ export default async function Home({
|
||||
|
||||
statsBundle = await loadDashboardStatsBundle(supabase);
|
||||
|
||||
if (tab === "matches" && pageAccess.matches) {
|
||||
const checks = await Promise.all(
|
||||
matches.map(async (m) => {
|
||||
const id = Number(m.id);
|
||||
return (await replayFileExists(id)) ? id : null;
|
||||
}),
|
||||
);
|
||||
matchIdsWithReplay = checks.filter((id): id is number => id !== null);
|
||||
}
|
||||
|
||||
if (tab === "ledger") {
|
||||
const lfromRaw = firstSearchParam(sp.lfrom);
|
||||
const ltoRaw = firstSearchParam(sp.lto);
|
||||
@@ -455,6 +467,7 @@ export default async function Home({
|
||||
canWritePlayers={canWritePlayers}
|
||||
canWriteLedger={canWriteLedger}
|
||||
isAdmin={account.isAdmin}
|
||||
matchIdsWithReplay={matchIdsWithReplay}
|
||||
/>
|
||||
{editUser ? (
|
||||
<EditUserCcRcOverlay
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ReplayViewer } from "@/components/replay-viewer";
|
||||
import { canReadPage } from "@/lib/auth/permissions";
|
||||
import { logPageAccess } from "@/lib/auth/log-page-access";
|
||||
import { requireSession } from "@/lib/auth/require-session";
|
||||
import { parseMatchIdParam } from "@/lib/match-logs-server";
|
||||
import { readMatchReplayFile } from "@/lib/match-replays-server";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ matchId: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { matchId } = await params;
|
||||
return {
|
||||
title: `Match ${matchId} replay · Kick Kings Admin`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function MatchReplayPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ matchId: string }>;
|
||||
}) {
|
||||
const account = await requireSession();
|
||||
if (
|
||||
!canReadPage(account, "matches") &&
|
||||
!canReadPage(account, "analysis")
|
||||
) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
const { matchId: raw } = await params;
|
||||
const matchId = parseMatchIdParam(raw);
|
||||
if (matchId == null) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
await logPageAccess(account, "match-replay", { matchId });
|
||||
|
||||
const result = await readMatchReplayFile(matchId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-dvh flex-col bg-[#0d1117] text-zinc-100">
|
||||
<header className="flex shrink-0 items-center gap-2 border-b border-zinc-700/80 bg-[#161b22] px-3 py-2">
|
||||
<div className="flex gap-1.5 pr-2" aria-hidden="true">
|
||||
<span className="h-3 w-3 rounded-full bg-[#ff5f56]" />
|
||||
<span className="h-3 w-3 rounded-full bg-[#ffbd2e]" />
|
||||
<span className="h-3 w-3 rounded-full bg-[#27c93f]" />
|
||||
</div>
|
||||
<p className="min-w-0 flex-1 truncate font-mono text-xs text-zinc-300">
|
||||
<span className="text-emerald-400/90">soccar</span>
|
||||
<span className="text-zinc-500"> — </span>
|
||||
<span className="text-zinc-100">match_{matchId}.json</span>
|
||||
</p>
|
||||
<Link
|
||||
href="/?tab=matches"
|
||||
className="shrink-0 rounded border border-zinc-600 bg-zinc-800 px-2.5 py-1 font-mono text-xs text-zinc-200 transition hover:bg-zinc-700 hover:text-white"
|
||||
>
|
||||
Back to matches
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col p-4">
|
||||
{!result.ok ? (
|
||||
<p className="font-mono text-sm text-red-400">
|
||||
<span className="text-red-500/80">error:</span> {result.message}
|
||||
</p>
|
||||
) : (
|
||||
<ReplayViewer
|
||||
initialReplay={result.replay}
|
||||
initialFileName={result.fileName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user