From 9f91856c534bc3e5b4c44fe5cf319d89c2777b7f Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Tue, 25 Aug 2026 17:02:14 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9liore=20la=20lisibilit=C3=A9=20de=20la?= =?UTF-8?q?=20D=C3=A9tection=20de=20triche=20sur=20/admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute l'avatar du joueur signalé (admin_list_members() le renvoyait déjà, juste jamais propagé), l'heure de début/fin de la partie (déduite de created_at et de la durée annoncée), et remplace la ligne de détails en flex-wrap par une grille à étiquettes qui reste lisible sur mobile. Co-Authored-By: Claude Sonnet 5 --- src/app/admin/admin-cheat-flags.tsx | 71 +++++++++++++++++++++-------- src/app/admin/page.tsx | 5 +- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/app/admin/admin-cheat-flags.tsx b/src/app/admin/admin-cheat-flags.tsx index 702f510..6e3e979 100644 --- a/src/app/admin/admin-cheat-flags.tsx +++ b/src/app/admin/admin-cheat-flags.tsx @@ -1,11 +1,13 @@ "use client"; import { useMemo, useState } from "react"; +import { Avatar } from "@/components/avatar"; type CheatFlag = { id: number; user_id: string; pseudo: string; + avatar_url: string | null; game: "icarus" | "melon"; score: number; severity: "warning" | "high"; @@ -25,6 +27,16 @@ const GAME_LABELS: Record = { melon: "Corne d'Abondance", }; +const TIME_FORMATTER = new Intl.DateTimeFormat("fr-FR", { + timeZone: "Europe/Paris", + timeStyle: "medium", +}); + +const DATE_FORMATTER = new Intl.DateTimeFormat("fr-FR", { + timeZone: "Europe/Paris", + dateStyle: "short", +}); + function formatDuration(durationMs: number | null | undefined): string | null { if (durationMs == null || !Number.isFinite(durationMs)) return null; if (durationMs < 1000) return `${Math.round(durationMs)} ms`; @@ -35,19 +47,35 @@ function formatDuration(durationMs: number | null | undefined): string | null { return `${minutes} min ${remainingSeconds.toString().padStart(2, "0")} s`; } +function DetailCell({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + 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)); + // Le signalement est écrit au moment de la soumission du score, donc + // quasi simultané à la fin réelle de la partie (délai réseau négligeable) + // — sert directement de proxy pour "heure de fin". "Début" s'en déduit en + // retranchant la durée annoncée, seulement quand elle est connue (jamais + // le cas pour missing_run_telemetry, où aucune télémétrie n'a été reçue). + const endedAt = new Date(flag.created_at); + const startedAt = + flag.details?.duration_ms != null && Number.isFinite(flag.details.duration_ms) + ? new Date(endedAt.getTime() - flag.details.duration_ms) + : null; return ( -
  • -
    -
    -
    +
  • +
    + + +
    +
    {flag.severity === "high" ? "Fort" : "À vérifier"} - {flag.pseudo} + {flag.pseudo} · {GAME_LABELS[flag.game]}

    {flag.reason}

    -
    -

    {flag.score}

    -

    {happenedAt}

    -
    +

    {flag.score}

    -
    - {duration && durée : {duration}} - {flag.details?.actions != null && actions : {flag.details.actions}} - {flag.details?.merges != null && fusions : {flag.details.merges}} - {flag.details?.previous_best != null && ancien record : {flag.details.previous_best}} +
    + + + + {flag.details?.actions != null && } + {flag.details?.merges != null && } + {flag.details?.previous_best != null && ( + + )} +
    + +
    {flag.trigger_code} + {DATE_FORMATTER.format(endedAt)}
  • ); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f82fcc5..bdc9122 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -63,10 +63,11 @@ export default async function AdminPage() { const memberRows = (members ?? []) as AdminMember[]; const flagRows = (cheatFlags ?? []) as CheatFlagRow[]; - const pseudoById = new Map(memberRows.map((member) => [member.id, member.pseudo])); + const memberById = new Map(memberRows.map((member) => [member.id, member])); const flagsWithPseudo = flagRows.map((flag) => ({ ...flag, - pseudo: pseudoById.get(flag.user_id) ?? "Un Citoyen", + pseudo: memberById.get(flag.user_id)?.pseudo ?? "Un Citoyen", + avatar_url: memberById.get(flag.user_id)?.avatar_url ?? null, })); return (