show escrow accounts

This commit is contained in:
2026-09-06 14:04:16 +00:00
parent aca0c17e4d
commit c5084170b8
11 changed files with 325 additions and 34 deletions
+6
View File
@@ -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
View File
@@ -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
+80
View File
@@ -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>
);
}