Ajoute L'Urne de l'Agora
Build and deploy / deploy (push) Successful in 36s

Nouvelle page /urne : vote quotidien inspiré du vote à l'urne de
l'Athènes antique. Chaque Citoyen dépose un jeton par jour sur une
autre personne (jamais lui-même), avec confirmation à deux temps
avant l'envoi — le vote est définitif pour la journée, aucune RPC de
modification. Les jetons s'accumulent sur toute la semaine dans un
classement public et cumulatif, identique pour tout le monde y
compris les Archontes : contraste volontaire avec Le Mur de la Honte,
ici personne (Archontes compris) ne voit qui a voté pour qui, seul le
total par personne est public.

Nouvelle table urn_votes (append-only, comme points_log) + deux RPC :
urn_vote_counts() agrège les votes sans jamais exposer une ligne
individuelle, cast_urn_vote() applique les règles (pas de vote pour
un juge ni pour soi-même, un vote par jour). Les Archontes ne votent
pas et ne peuvent pas recevoir de jetons.
This commit is contained in:
Valentin ROBIN
2026-08-23 18:02:12 +02:00
parent ab599d55c9
commit d02f1543e9
7 changed files with 391 additions and 2 deletions
+109
View File
@@ -1131,3 +1131,112 @@ end;
$$;
grant execute on function public.post_wall_note(text) to authenticated;
-- 15. L'Urne de l'Agora -------------------------------------------------------
-- Vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque jour
-- chaque Citoyen dépose un jeton sur une personne de son choix. Contrairement
-- au Mur de la Honte, les jetons s'accumulent sur toute la semaine dans un
-- classement public et cumulatif — identique pour tout le monde, Archontes
-- compris : contrairement à chariot_submissions/wall_notes, les Archontes
-- n'ont ici AUCUNE vision privilégiée sur qui a voté pour qui, seul le total
-- par personne est public. Les Archontes ne peuvent ni voter ni recevoir de
-- jetons.
create table if not exists public.urn_votes (
id bigint generated always as identity primary key,
voter_id uuid not null references public.profiles (id) on delete cascade,
target_id uuid not null references public.profiles (id) on delete cascade,
created_at timestamptz not null default now()
);
alter table public.urn_votes enable row level security;
revoke insert, update, delete on public.urn_votes from authenticated, anon;
grant select on public.urn_votes to authenticated;
-- Pas d'exception juge ici (contrairement à chariot_submissions/wall_notes) :
-- même les Archontes ne voient que leur propre ligne (pour savoir "j'ai déjà
-- voté aujourd'hui") — qui a voté pour qui ne regarde personne d'autre.
drop policy if exists "urn_votes readable by voter only" on public.urn_votes;
create policy "urn_votes readable by voter only"
on public.urn_votes for select to authenticated
using (voter_id = auth.uid());
do $$
begin
alter publication supabase_realtime add table public.urn_votes;
exception
when duplicate_object then null;
end $$;
-- Seul moyen de calculer un total public par personne sans jamais exposer
-- une ligne individuelle (qui a voté pour qui) à qui que ce soit. Agrégé sur
-- tout l'historique (pas de filtre de date, contrairement à wall_notes_today
-- qui ne montre que le jour courant) : les jetons s'accumulent toute la
-- semaine.
create or replace function public.urn_vote_counts()
returns table (target_id uuid, votes bigint)
language sql
security definer
set search_path = public
as $$
select v.target_id, count(*) as votes
from public.urn_votes v
group by v.target_id;
$$;
grant execute on function public.urn_vote_counts() to authenticated;
-- Seul point d'entrée pour voter. Un vote par Citoyen par jour, définitif
-- (aucune RPC de modification/suppression).
create or replace function public.cast_urn_vote(p_target_id uuid)
returns public.urn_votes
language plpgsql
security definer
set search_path = public
as $$
declare
v_caller_role text;
v_target_role text;
v_today date := (now() at time zone 'Europe/Paris')::date;
v_already_voted boolean;
v_row public.urn_votes;
begin
if auth.uid() is null then
raise exception 'authentication required';
end if;
select role into v_caller_role from public.profiles where id = auth.uid();
if v_caller_role = 'judge' then
raise exception 'only citizens can vote';
end if;
if p_target_id = auth.uid() then
raise exception 'cannot vote for yourself';
end if;
select role into v_target_role from public.profiles where id = p_target_id;
if v_target_role is null then
raise exception 'target member not found';
end if;
if v_target_role = 'judge' then
raise exception 'cannot vote for a judge';
end if;
select exists (
select 1 from public.urn_votes
where voter_id = auth.uid()
and (created_at at time zone 'Europe/Paris')::date = v_today
) into v_already_voted;
if v_already_voted then
raise exception 'already voted today';
end if;
insert into public.urn_votes (voter_id, target_id)
values (auth.uid(), p_target_id)
returning * into v_row;
return v_row;
end;
$$;
grant execute on function public.cast_urn_vote(uuid) to authenticated;