From 777d9ebaeac9688e063902037f306f2a690e5ca0 Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Mon, 27 Jul 2026 18:53:45 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20le=20sch=C3=A9ma=20de=20La=20Course=20?= =?UTF-8?q?du=20Char=20(tables,=20RPC,=20cl=C3=B4ture=20auto)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chariot_runs : meilleur temps + tracé fantôme par joueur et par jour, verrouillée comme points_log (écriture uniquement via RPC). - submit_chariot_run() : calcule la date côté serveur, borne le temps, n'écrase que si strictement meilleur. - close_daily_chariot_race() + chariot_race_closes : clôture la journée précédente et attribue les gloires du top 3 automatiquement, sans intervention d'un Archonte — planifiée via pg_cron (à activer une fois côté Dashboard Supabase). - points_log.judge_id devient nullable (un point auto n'a pas de juge) ; Le Crieur affiche "Le Tribunal" pour ces décrets-là. --- src/app/journal/journal-view.tsx | 5 +- supabase/schema.sql | 167 +++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/src/app/journal/journal-view.tsx b/src/app/journal/journal-view.tsx index 56486ab..4225390 100644 --- a/src/app/journal/journal-view.tsx +++ b/src/app/journal/journal-view.tsx @@ -6,7 +6,7 @@ import { createClient } from "@/lib/supabase/client"; type Entry = { id: number; target_id: string; - judge_id: string; + judge_id: string | null; delta: number; reason: string | null; created_at: string; @@ -55,7 +55,8 @@ export function JournalView({ return (
    {entries.map((entry) => { - const judgePseudo = pseudoById[entry.judge_id] ?? "Un Archonte"; + const judgePseudo = + entry.judge_id === null ? "Le Tribunal" : (pseudoById[entry.judge_id] ?? "Un Archonte"); const targetPseudo = pseudoById[entry.target_id] ?? "un Citoyen"; const sign = entry.delta > 0 ? `+${entry.delta}` : `${entry.delta}`; return ( diff --git a/supabase/schema.sql b/supabase/schema.sql index 7ee5f39..65b4ea7 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -484,3 +484,170 @@ end $$; -- -- (Récupérer l'uuid via Authentication → Users, ou : -- select id, pseudo from public.profiles;) + +-- 11. La Course du Char -------------------------------------------------------- +-- Mini-jeu : course de char en vue du dessus, une piste générée aléatoirement +-- par jour (calculée côté client à partir de la date Europe/Paris, jamais +-- stockée ici), chacun rejoue autant qu'il veut, seul le meilleur temps du +-- jour compte. Les points du jour sont attribués automatiquement (pas +-- d'Archonte impliqué) par une tâche planifiée pg_cron — seul mécanisme de +-- points de tout le projet qui n'attend pas de décision d'un juge. + +-- Un point attribué automatiquement n'a pas de juge : judge_id devient +-- optionnel (le Crieur affiche alors "Le Tribunal" à la place d'un pseudo). +alter table public.points_log alter column judge_id drop not null; + +create table if not exists public.chariot_runs ( + user_id uuid not null references public.profiles (id) on delete cascade, + race_date date not null, + best_time_ms integer not null check (best_time_ms between 2000 and 300000), + ghost_path jsonb not null default '[]'::jsonb, + updated_at timestamptz not null default now(), + primary key (user_id, race_date) +); + +create index if not exists chariot_runs_race_date_idx + on public.chariot_runs (race_date, best_time_ms); + +alter table public.chariot_runs enable row level security; + +-- Même modèle que points_log : verrouillée en écriture, seule la RPC +-- submit_chariot_run() (SECURITY DEFINER) peut écrire. +revoke insert, update, delete on public.chariot_runs from authenticated, anon; +grant select on public.chariot_runs to authenticated; + +drop policy if exists "chariot_runs readable by authenticated" on public.chariot_runs; +create policy "chariot_runs readable by authenticated" + on public.chariot_runs for select + to authenticated + using (true); + +-- Seul point d'entrée pour soumettre un temps : la date du jour est calculée +-- côté serveur (jamais fournie par le client, pour ne pas pouvoir soumettre +-- "pour" un autre jour), le temps est borné grossièrement (anti-triche +-- minimal, suffisant pour un jeu entre amis — pas de vérification fine par +-- piste), et on n'écrase le temps existant que s'il est strictement meilleur. +create or replace function public.submit_chariot_run(p_time_ms integer, p_ghost_path jsonb default '[]'::jsonb) +returns public.chariot_runs +language plpgsql +security definer +set search_path = public +as $$ +declare + v_race_date date; + v_row public.chariot_runs; +begin + if auth.uid() is null then + raise exception 'authentication required'; + end if; + + if p_time_ms is null or p_time_ms < 2000 or p_time_ms > 300000 then + raise exception 'invalid time_ms'; + end if; + + v_race_date := (now() at time zone 'Europe/Paris')::date; + + insert into public.chariot_runs (user_id, race_date, best_time_ms, ghost_path, updated_at) + values (auth.uid(), v_race_date, p_time_ms, coalesce(p_ghost_path, '[]'::jsonb), now()) + on conflict (user_id, race_date) do update + set best_time_ms = excluded.best_time_ms, + ghost_path = excluded.ghost_path, + updated_at = now() + where excluded.best_time_ms < public.chariot_runs.best_time_ms; + + -- On resélectionne toujours plutôt que de dépendre du RETURNING de + -- l'upsert : si la clause WHERE bloque la mise à jour (temps pas + -- meilleur), l'INSERT ne retourne aucune ligne. On renvoie dans tous les + -- cas le meilleur temps courant du joueur, qu'il vienne d'être battu ou non. + select * into v_row from public.chariot_runs + where user_id = auth.uid() and race_date = v_race_date; + + return v_row; +end; +$$; + +grant execute on function public.submit_chariot_run(integer, jsonb) to authenticated; + +do $$ +begin + alter publication supabase_realtime add table public.chariot_runs; +exception + when duplicate_object then null; +end $$; + +-- Marqueur interne : quelles journées ont déjà été clôturées (idempotence de +-- la tâche planifiée, qui tourne toutes les 15 minutes plutôt qu'une seule +-- fois à minuit pile — le changement d'heure CET/CEST décalerait un horaire +-- UTC fixe). Table strictement interne : aucune policy, jamais lue/écrite +-- par le client. +create table if not exists public.chariot_race_closes ( + race_date date primary key, + closed_at timestamptz not null default now() +); +alter table public.chariot_race_closes enable row level security; +revoke all on public.chariot_race_closes from authenticated, anon; + +-- Clôture de la journée précédente (Europe/Paris) : classe chariot_runs, +-- attribue les gloires du top 3 (ex-aequo inclus au même rang), journalise +-- dans points_log avec judge_id = null, puis marque la date comme close. +-- No-op si déjà clôturée. Volontairement pas de grant execute à +-- authenticated : uniquement appelée par pg_cron ou depuis le SQL Editor. +create or replace function public.close_daily_chariot_race() +returns void +language plpgsql +security definer +set search_path = public +as $$ +declare + v_target_date date := ((now() at time zone 'Europe/Paris')::date - 1); + r record; + v_points int; +begin + if exists (select 1 from public.chariot_race_closes where race_date = v_target_date) then + return; + end if; + + for r in + with ranked as ( + select user_id, best_time_ms, + rank() over (order by best_time_ms asc) as rnk + from public.chariot_runs + where race_date = v_target_date + ) + select * from ranked where rnk <= 3 + loop + v_points := case r.rnk when 1 then 3 when 2 then 2 when 3 then 1 else 0 end; + update public.profiles set points = points + v_points where id = r.user_id; + insert into public.points_log (target_id, judge_id, delta, reason) + values (r.user_id, null, v_points, + 'Course du Char — ' || to_char(v_target_date, 'DD/MM') || ' (rang ' || r.rnk || ')'); + end loop; + + insert into public.chariot_race_closes (race_date) values (v_target_date); +end; +$$; + +-- ⚠️ pg_cron doit être activé une fois pour toutes via le Dashboard Supabase +-- (Database → Extensions → "pg_cron" → Enable) — pas scriptable depuis ce +-- fichier, et pas garanti self-service selon le plan/la région du projet. +-- Les deux blocs ci-dessous n'échouent jamais bruyamment si l'extension n'est +-- pas encore activée (schéma "cron" inexistant) : le reste de ce script, +-- ré-exécuté en entier à chaque changement, doit toujours pouvoir passer. +do $$ +begin + perform cron.unschedule('close-daily-chariot-race'); +exception + when others then null; -- la tâche n'existe pas encore, ou pg_cron pas activé +end $$; + +do $$ +begin + perform cron.schedule( + 'close-daily-chariot-race', + '*/15 * * * *', + $cron$select public.close_daily_chariot_race();$cron$ + ); +exception + when others then + raise notice 'pg_cron indisponible : active l''extension via le Dashboard Supabase (Database → Extensions → pg_cron), puis ré-exécute ce script pour planifier la clôture automatique de la Course du Char.'; +end $$;