Files
tribunal-app/src/app/admin/admin-cheat-flags.tsx
T
alexandre b7f3f8b0fa
Build and deploy / deploy (push) Failing after 32s
Add discreet anticheat mechanism for the games
2026-08-24 12:07:10 +02:00

143 lines
5.1 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
type CheatFlag = {
id: number;
user_id: string;
pseudo: string;
game: "icarus" | "melon";
score: number;
severity: "warning" | "high";
trigger_code: string;
reason: string;
details: {
duration_ms?: number | null;
actions?: number | null;
merges?: number | null;
previous_best?: number | null;
} | null;
created_at: string;
};
const GAME_LABELS: Record<CheatFlag["game"], string> = {
icarus: "Vol d'Icare",
melon: "Corne d'Abondance",
};
function formatDuration(durationMs: number | null | undefined): string | null {
if (durationMs == null || !Number.isFinite(durationMs)) return null;
if (durationMs < 1000) return `${Math.round(durationMs)} ms`;
const seconds = durationMs / 1000;
if (seconds < 60) return `${seconds.toFixed(seconds < 10 ? 1 : 0)} s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
return `${minutes} min ${remainingSeconds.toString().padStart(2, "0")} s`;
}
function FlagRow({ flag }: { flag: CheatFlag }) {
const duration = formatDuration(flag.details?.duration_ms);
const happenedAt = new Intl.DateTimeFormat("fr-FR", {
timeZone: "Europe/Paris",
dateStyle: "short",
timeStyle: "medium",
}).format(new Date(flag.created_at));
return (
<li className="marble-surface rounded-lg border border-gold/25 px-4 py-3 shadow-sm">
<div className="flex flex-wrap items-start justify-between gap-2">
<div>
<div className="flex flex-wrap items-center gap-2">
<span
className={
flag.severity === "high"
? "rounded-full border border-oxblood/40 bg-oxblood/10 px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide text-oxblood uppercase"
: "rounded-full border border-gold/40 bg-gold/10 px-2 py-0.5 text-[0.65rem] font-semibold tracking-wide text-text-marble uppercase"
}
>
{flag.severity === "high" ? "Fort" : "À vérifier"}
</span>
<span className="font-heading text-sm text-text-marble">{flag.pseudo}</span>
<span className="text-xs text-text-mut">· {GAME_LABELS[flag.game]}</span>
</div>
<p className="mt-1 text-sm text-text-marble">{flag.reason}</p>
</div>
<div className="text-right">
<p className="font-heading text-lg text-sea">{flag.score}</p>
<p className="text-[0.7rem] text-text-mut">{happenedAt}</p>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-text-mut">
{duration && <span>durée : {duration}</span>}
{flag.details?.actions != null && <span>actions : {flag.details.actions}</span>}
{flag.details?.merges != null && <span>fusions : {flag.details.merges}</span>}
{flag.details?.previous_best != null && <span>ancien record : {flag.details.previous_best}</span>}
<span className="font-mono opacity-60">{flag.trigger_code}</span>
</div>
</li>
);
}
export function AdminCheatFlags({ flags }: { flags: CheatFlag[] }) {
const [game, setGame] = useState<"all" | CheatFlag["game"]>("all");
const filtered = useMemo(
() => (game === "all" ? flags : flags.filter((flag) => flag.game === game)),
[flags, game],
);
const flaggedUsers = useMemo(() => new Set(flags.map((flag) => flag.user_id)).size, [flags]);
return (
<section className="mt-10 border-t border-gold/20 pt-8">
<div className="mb-4 flex flex-wrap items-end justify-between gap-3">
<div>
<h2 className="font-heading text-lg tracking-wide text-gold-bright uppercase">Détection de triche</h2>
<p className="mt-1 max-w-xl text-sm text-marble/60">
Les scores restent acceptés. Cette liste signale seulement les parties qui ont déclenché un contrôle
simple côté serveur.
</p>
</div>
<p className="text-xs text-marble/60">
{flags.length} signalement{flags.length > 1 ? "s" : ""} · {flaggedUsers} joueur
{flaggedUsers > 1 ? "s" : ""}
</p>
</div>
<div className="mb-4 flex flex-wrap gap-2">
{([
["all", "Tous"],
["icarus", "Icare"],
["melon", "Corne"],
] as const).map(([value, label]) => (
<button
key={value}
type="button"
onClick={() => setGame(value)}
className={`rounded-md border px-3 py-1.5 text-xs font-medium transition ${
game === value
? "border-gold/60 bg-gold/15 text-gold-bright"
: "border-gold/25 text-marble/70 hover:bg-gold/10"
}`}
>
{label}
</button>
))}
</div>
{filtered.length === 0 ? (
<p className="rounded-lg border border-gold/20 px-4 py-5 text-center text-sm text-marble/60">
Aucun signalement pour ce filtre.
</p>
) : (
<ul className="flex flex-col gap-2">
{filtered.map((flag) => (
<FlagRow key={flag.id} flag={flag} />
))}
</ul>
)}
</section>
);
}