Ajoute le schéma de La Course du Char (tables, RPC, clôture auto)
- 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à.
This commit is contained in:
@@ -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 (
|
||||
<ol className="flex flex-col gap-2">
|
||||
{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 (
|
||||
|
||||
@@ -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 $$;
|
||||
|
||||
Reference in New Issue
Block a user