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
+11 -2
View File
@@ -6,7 +6,7 @@ Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des ol
## 1. État actuel
V1 à V9 sont livrées :
V1 à V10 sont livrées :
- **V1** : comptes, profils, leaderboard statique.
- **V2** : authentification par email réel (plus de pseudo+email interne), rôles `public`/`judge`, verrou de pseudo (modifiable une fois), section Admin.
- **V3** : attribution de points par les juges (RPC sécurisée, +/- avec confirmation explicite), journal public « le crieur », podium top 3, flèches de progression, recadrage/compression photo côté client.
@@ -16,6 +16,7 @@ V1 à V9 sont livrées :
- **V7** : outils numériques pour le vrai jeu du Tribunal (soirée physique/sociale, pas un mini-jeu digital), page **Le Jeu de l'Agora** (`/char`) — pensée **format paysage 16:9** (projetée sur grand écran) plutôt que mobile-first comme le reste de l'app. **Le Char** : 2 colonnes fixes de 3 emplacements affichées côte à côte avec un « VS » et une illustration de char vu du dessus entre les deux, affectation d'un Citoyen à un emplacement vide en un clic (liste des Citoyens libres directement cliquables, pas de menu déroulant), bouton « Retirer », boutons « Colonne A/B remporte le duel » (vide la colonne gagnante après une brève animation — les vainqueurs sortent, les perdants restent), compteur du nombre de fois que chacun est monté dans le char, avec un petit classement récapitulatif en bas de page (tout le monde, y compris ceux qui n'y sont jamais monté). La banque de questions (ajout/édition/réordonnancement/bascule afficher-masquer) vit sur une page séparée réservée aux Archontes, **`/char/questions`** — pensée pour être pilotée depuis un téléphone pendant que `/char` est projetée depuis un ordinateur ; `/char` n'affiche que la question actuellement révélée, en lecture seule. **Le Gardien du Silence** : rôle tiré au sort parmi les Citoyens (jamais les Archontes), affiché de façon discrète (barre compacte, pas une grande carte), minuteur avec reroll automatique à expiration (RPC auto-protégée, filet `pg_cron`) et bouton de reroll manuel pour les Archontes. L'app n'arbitre jamais le jeu lui-même, elle affiche/synchronise un état piloté à la main. **La Roulette** (tirage au sort animé parmi les membres, ou les Citoyens uniquement), initialement livrée en V5 sur sa propre page `/roulette`, a été fusionnée dans `/char` comme troisième section — la route et le lien de menu dédiés ont été retirés, tout l'outillage de la soirée du Tribunal vit maintenant au même endroit.
- **V8** : les Citoyens peuvent désormais proposer une question pour Le Char — une par personne (upsert), modifiable autant de fois que voulu depuis une nouvelle section sur `/profile`, jusqu'à ce que la date du Tribunal (`settings.tribunal_date`) soit atteinte (même mécanisme de gel que les scores d'Icare, no-op silencieux plutôt qu'une erreur). Les Archontes modèrent ces suggestions depuis une nouvelle section « Suggestions des Citoyens » sur `/char/questions` : chaque proposition affiche l'auteur, avec un bouton « Ajouter à la banque » (la copie dans `chariot_questions`, à la fin de l'ordre existant) et un bouton de rejet (suppression, confirmation à deux temps comme le reste de la page). Par ailleurs, `/admin` affiche maintenant l'adresse email de chaque membre (RPC `admin_list_members()`, seul moyen d'exposer `auth.users.email` aux juges sans passer par la `service_role key` côté client).
- **V9** : **Le Mur de la Honte** (`/mur`) — chaque Citoyen peut publier une note anonyme par jour (dix places maximum sur le mur, remis à zéro chaque jour — pas de suppression, juste un filtrage par date). L'anonymat n'est qu'à moitié réel : les Archontes voient une section « Historique complet » (filtrable par jour) avec l'auteur de chaque note jamais publiée, cohérent avec le thème du site (les Archontes ont un ascendant sur les Citoyens). Pas de gel lié à la date du Tribunal : contrairement au Char, c'est une mécanique de toute la semaine, pas de la seule soirée de clôture.
- **V10** : **L'Urne de l'Agora** (`/urne`) — vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque Citoyen dépose un jeton par jour sur une personne de son choix (jamais lui-même), confirmation à deux temps avant l'envoi (vote définitif pour la journée, aucune RPC de modification). 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** : contraste volontaire avec le Mur, ici les Archontes n'ont aucune vision privilégiée sur qui a voté pour qui, seul le total par personne est public. Les Archontes ne votent pas et ne peuvent pas recevoir de jetons.
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, mode grand écran.
@@ -106,6 +107,8 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè
- `admin_list_members()` — authenticated, vérifie `role = 'judge'`, seule façon d'exposer `auth.users.email` (jamais stocké dans `profiles`) sur `/admin` sans passer par la `service_role key` côté client.
- `wall_notes_today()` — authenticated, `language sql`, renvoie les notes du jour (heure de Paris) **sans `user_id`** — seul moyen de laisser un Citoyen voir les notes des autres sans jamais exposer l'auteur côté client (la RLS ne peut pas masquer une colonne pour certaines lignes).
- `post_wall_note(p_text)` — authenticated, rejette les juges (seuls les Citoyens publient), valide un texte non vide (≤ 200 caractères), puis vérifie que l'appelant n'a pas déjà publié aujourd'hui et que le mur du jour a moins de 10 notes (sinon `raise exception` dans les deux cas — pas de no-op silencieux ici, l'UI doit remonter l'erreur).
- `urn_vote_counts()` — authenticated, `language sql`, renvoie `(target_id, votes)` agrégé sur tout l'historique (pas de filtre de date, contrairement à `wall_notes_today`) — seul moyen de calculer un total public sans jamais exposer une ligne individuelle (qui a voté pour qui).
- `cast_urn_vote(p_target_id)` — authenticated, rejette les juges (ni votants ni cibles), rejette le vote pour soi-même, vérifie que l'appelant n'a pas déjà voté aujourd'hui (heure de Paris) — vote définitif, aucune RPC de modification/suppression.
### `public.wall_notes` (Le Mur de la Honte, V9)
@@ -113,6 +116,11 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè
- RLS `select` : le propriétaire voit sa propre note (sert à afficher « tu as déjà publié aujourd'hui » et à réafficher son propre texte), les juges voient tout avec l'auteur (embed `profiles(pseudo, avatar_url)`, section « Historique complet » sur `/mur`) — pas de visibilité entre Citoyens, c'est tout l'intérêt de `wall_notes_today()`.
- Pas de gel via `settings.tribunal_date` : contrairement au Char, c'est une mécanique de toute la semaine.
### `public.urn_votes` (L'Urne de l'Agora, V10)
- `id, voter_id, target_id, created_at` — journal append-only (comme `points_log`/`chariot_entries`) d'un vote par ligne, jamais un upsert : les jetons s'accumulent d'un jour sur l'autre, contrairement à `wall_notes` qui ne montre que le jour courant.
- RLS `select` : **pas d'exception juge** (contrairement à `chariot_submissions`/`wall_notes`) — le votant ne voit que sa propre ligne (pour savoir « j'ai déjà voté aujourd'hui »), personne d'autre, Archontes compris, ne peut voir qui a voté pour qui ; seul `urn_vote_counts()` expose un total agrégé, identique pour tout le monde.
---
## 4. Authentification
@@ -133,12 +141,13 @@ Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accè
| `/char` | « Le Jeu de l'Agora » — format paysage (projeté) : Le Char (2 colonnes de 3, boutons de duel), Le Gardien du Silence (tirage aléatoire, minuteur discret) et La Roulette (tirage au sort animé) |
| `/char/questions` | Banque de questions du Char (ajout/édition/réordonnancement/afficher-masquer) + modération des suggestions des Citoyens (ajouter à la banque/rejeter) — juges uniquement, pilotée depuis un téléphone pendant que `/char` est projetée |
| `/mur` | « Le Mur de la Honte » — une note anonyme par Citoyen par jour (dix places, remis à zéro chaque jour) ; les Archontes y voient en plus l'historique complet avec l'auteur de chaque note |
| `/urne` | « L'Urne de l'Agora » — un jeton par Citoyen par jour sur une autre personne, classement cumulatif public identique pour tous, Archontes compris (aucune vision privilégiée ici) |
| `/calendrier` | « Le Calendrier des Dieux » — agenda de la semaine, panthéon, date du Tribunal, édition réservée aux juges |
| `/journal` | « Le Crieur » — fil live des décrets (attributions de points), lecture pour tous |
| `/profile` | Pseudo (verrouillable), photo, déconnexion, et pour les Citoyens : proposition d'une question pour Le Char (modifiable jusqu'au Tribunal) |
| `/admin` | « Admin » — juges uniquement, **vérifié côté serveur** (Server Component), habillée d'une police monospace (rupture volontaire avec le thème grec) — gestion des membres, reset du repère de classement |
`/leaderboard`, `/profile`, `/admin`, `/journal`, `/calendrier`, `/icare`, `/char` (et donc `/char/questions`, couverte par le même préfixe), `/mur` sont protégées par `src/proxy.ts` (redirection `/login` si non connecté). L'accès juge-only de `/admin` et `/char/questions` n'est **pas** géré par le proxy (il n'a pas facilement le rôle) — la page elle-même vérifie et redirige ; même principe pour les boutons d'édition du Calendrier (RLS + vérification serveur, jamais un simple masquage front).
`/leaderboard`, `/profile`, `/admin`, `/journal`, `/calendrier`, `/icare`, `/char` (et donc `/char/questions`, couverte par le même préfixe), `/mur`, `/urne` sont protégées par `src/proxy.ts` (redirection `/login` si non connecté). L'accès juge-only de `/admin` et `/char/questions` n'est **pas** géré par le proxy (il n'a pas facilement le rôle) — la page elle-même vérifie et redirige ; même principe pour les boutons d'édition du Calendrier (RLS + vérification serveur, jamais un simple masquage front).
Le header global (`components/header.tsx`) est rendu dans `layout.tsx` pour tout utilisateur connecté : logo (couronne de laurier + « Le Tribunal », jamais masqué même à 320px) à gauche, chip utilisateur (avatar, pseudo, points, jamais masqués) à droite. **Aucun lien de navigation dans le header lui-même** — le chip ouvre un menu déroulant qui contient toutes les pages (avec icônes), fermeture au clic extérieur/Échap, entrée courante surlignée.