Nombre de publications du jour, façon notification iOS, sur le raccourci /mur de /leaderboard — via wall_notes_for_date() (une lecture directe de wall_notes ne montrerait, à cause de la RLS, que ses propres notes, pas le total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wall_notes_today() devient wall_notes_for_date(p_date default null) — même garantie d'anonymat qu'avant, juste paramétrée par jour plutôt que figée sur aujourd'hui. Nouvelle RPC wall_note_days() (dates ayant au moins une note, sans contenu ni auteur) peuple un sélecteur de jour au-dessus de la grille, visible par tous — pas seulement les Archontes via l'historique complet, qui lui révèle l'auteur. wall_note_reaction_counts() gagne le même paramètre p_date. Le compteur "X/12 aujourd'hui" et l'éligibilité à publier restent toujours basés sur aujourd'hui, peu importe le jour consulté.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
L'Agora sert déjà de page d'accueil de fait (/ y redirige) mais n'offrait aucun accès direct aux autres pages sans passer par le menu déroulant du chip. Ajoute une rangée de raccourcis (Icare, Corne d'Abondance, Char, Mur de la Honte, Urne, Crieur) juste au-dessus du classement, responsive (3 colonnes mobile, 6 en ligne sur écran plus large), mêmes icônes que le menu du header pour rester cohérent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute une visionneuse plein écran (mur du jour, sa propre note, et l'historique complet côté Archontes). Corrige au passage un bug introduit avec l'ouverture du Mur aux Archontes (V14) : leur propre note du jour ne se rafraîchissait jamais après le chargement initial, le refetch la filtrait encore par rôle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pas de fichier audio dans le projet, donc un petit "pop" synthétisé via Web Audio (src/lib/melon/sound.ts), joué à chaque fusion. La hauteur baisse avec le palier de la pièce obtenue, pour que fusionner deux grosses pièces sonne différemment de deux petites.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
post_wall_note() n'a plus de vérification de rôle : un Archonte peut désormais publier une note anonyme comme n'importe quel Citoyen. La policy Storage d'upload sur wall-images perd aussi son filtre par rôle (sinon possible de poster une note mais pas d'y joindre une image). Limite quotidienne relevée de 10 à 12 notes pour couvrir tout le groupe plutôt que les seuls Citoyens.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le classement "compétition" fait partager le rang 1 à tous les ex-aequo — sans score, tout le groupe se retrouvait sur la 1ère marche du podium. Le podium ne s'affiche plus tant que le meneur n'a pas dépassé 0, sur /leaderboard, /icare et /corne.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
duration_ms/actions/merges restaient entièrement déclaratifs (calculés et envoyés par le client en une fois à la fin), donc falsifiables par un appel RPC direct. Le client envoie maintenant un ping toutes les 5s pendant la partie (ping_game_run), horodaté par le serveur, jamais par le client. submit_icarus_score/submit_melon_score corrèlent le nombre de pings et leur écart réel avec la durée annoncée (parties ≥ 15s uniquement) et signalent toute incohérence dans game_cheat_flags (missing_pings, ping_span_mismatch) — toujours en signalement silencieux, jamais de blocage, comme pour le reste de l'anti-triche.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
submit_icarus_score/submit_melon_score (appliquées directement en base, jamais commitées) sont récupérées mot pour mot via pg_get_functiondef : signalement dans game_cheat_flags plutôt que rejet serveur, le score reste toujours accepté. L'ancienne couche par rejet strict (icarus_runs/start_icarus_run) devenue orpheline est explicitement supprimée plutôt que laissée à traîner. CLAUDE.md documente le nouveau mécanisme (V12) et n'a plus aucune mention de l'ancien.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La pièce devait déjà entièrement dépasser la ligne rouge (bord bas, pas juste le sommet) pour compter dans le délai de grâce avant game over. L'échelle des tailles en bas de page passe d'une seule ligne à défilement horizontal à un flex-wrap sans scroll, avec un titre — l'intérêt d'une échelle de repère est de tout voir d'un coup d'œil. Corrige aussi une violation de react-hooks/purity (Date.now() en argument direct de useRef) introduite par le mécanisme anti-triche fusionné depuis main.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5000 restait trop haut pour être un vrai plafond réaliste : Icare abaissé à 500 (calculé sur le rythme le plus rapide théoriquement atteignable dans le jeu), Corne à 2000 (estimation plus prudente, économie de score plus dure à borner). Ajoute icarus_runs/start_icarus_run() : le serveur enregistre l'instant réel de début de partie et rejette un score incohérent avec le temps écoulé, sans jamais faire confiance à une durée envoyée par le client. Corrige aussi le nombre de jetons de /urne, peu lisible en gold-bright sur fond marbre (passe à text-sea, même convention qu'Icare/Corne).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un score falsifié envoyé directement à submit_icarus_score/submit_melon_score pouvait se convertir en vraies gloires au Tribunal (plafond abaissé de 1 000 000 à 5000, contrainte + RPC). Les fonctions cron-only award_*_points_if_due() n'étaient protégées que par un commentaire, pas un vrai REVOKE. Le bucket avatars n'avait aucune limite de taille/type. La banque complète des questions du Char (spoiler du jeu) était lisible par tout Citoyen via /char ou un appel direct — RLS restreinte aux juges, /char passe désormais par la RPC chariot_revealed_question().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La bordure était posée directement sur le conteneur aux coordonnées
physiques (300x420) : à cause du box-sizing, la bordure mangeait sur
l'espace intérieur réel (292x416) sans que matter-js le sache, donc les
pièces pouvaient visuellement dépasser le cadre. La bordure vit
maintenant sur un conteneur externe non dimensionné, qui s'ajuste
automatiquement autour du conteneur physique sans réduire sa zone utile.
Ajoute aussi l'échelle de repère (ordre de fusion, taille proportionnelle
au rayon) et le second aperçu de la pièce suivante, travaillés en même
temps.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sur mobile, taper sans glisser ne déclenche pas forcément pointermove
avant pointerup, donc la position de visée n'était jamais mise à jour
avant le lâcher : la pièce tombait à l'ancienne position plutôt qu'à
l'endroit tapé. La position se resynchronise maintenant au contact
(pointerdown) et juste avant le lâcher (pointerup).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le calcul de position ajoutait WALL_THICKNESS (20px) en plus du rayon de
la pièce, alors que les murs physiques sont déjà à ras des bords visuels
du bac. Cette marge fixe pénalisait surtout les petites pièces, qui ne
pouvaient pas s'approcher des bords. Seul le rayon compte désormais.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Petit badge "Puis" à côté du score, taille d'affichage fixe (juste
informatif, pas de position à viser) — la file glisse d'un cran à chaque
lâcher : ce qui était annoncé en second devient la pièce à viser.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TIER_OVERRIDES (id de profil → index de palier) + applyTierOverrides()
placent d'abord les Citoyens fixés à la main, puis remplissent le reste
dans l'ordre alphabétique existant. Épicure est placé en position 7.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nouveau mini-jeu à /corne : des avatars de Citoyens tombent dans un bac
(physique matter-js), deux avatars de même taille qui se touchent
fusionnent en un plus gros, jusqu'au débordement soutenu (5s de grâce,
pièces clignotantes pour prévenir) qui met fin à la partie. Même
mécanique de gloires que Le Vol d'Icare (record personnel, classement,
gel + attribution automatique au Tribunal via pg_cron).
Un palier de taille par Citoyen (pas les Archontes) plutôt qu'un nombre
arbitraire : citizens[tier] donne l'association palier → Citoyen, fixe
pour tout le monde (les Citoyens sont triés par pseudo côté serveur, un
ordre déterministe). Rendu en DOM pour réutiliser le composant Avatar
existant (étendu pour accepter une taille en pixels arbitraire) plutôt
que de redessiner des images sur canvas — position et rotation de chaque
pièce appliquées en impératif depuis la boucle physique, jamais via
setState.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La fenêtre de 3-8 colonnes du bouclier est maintenant une vraie distance :
chaque colonne rencontrée décompte, qu'elle soit touchée (cassée au lieu
de tuer) ou franchie sans contact (non cassée) — avant, seules les
colonnes touchées consommaient le bouclier, qui pouvait donc durer bien
plus longtemps que prévu.
Ajoute aussi des filets de vitesse continus autour d'Icare pendant toute
la durée du bouclier (pas seulement au bond initial de cueillette), pour
que la sensation d'accélération reste lisible même si Icare est figé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le nombre de charges (colonnes cassables) est désormais tiré entre 3 et 8
à chaque cueillette, au lieu d'un nombre fixe. Pendant que le bouclier est
actif, Icare est immobile (gravité coupée, taps/barre d'espace sans
effet) et le défilement accéléré (×1,7) : impossible d'agir sur la
trajectoire jusqu'à épuisement des charges, où le contrôle reprend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rare et exceptionnel (intervalle large de 15-25 colonnes, pas de plafond
par partie) : ramasser l'éclair donne un bouclier de 5 charges — toucher
une colonne (mortel normalement) la casse au contact et consomme une
charge, une par une, plutôt que de détruire un nombre fixe de colonnes
d'un coup à la cueillette. Cueillette animée d'un bond en avant d'Icare
avec traînée de vitesse, halo pulsant sur l'éclair, éclat de particules
sur chaque colonne cassée et liseré doré tant que le bouclier est actif.
Ajoute aussi le support clavier (barre d'espace = tap) pour jouer au
clavier sur ordinateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chaque note peut désormais porter une image facultative : compressée et
redimensionnée côté client (resizeImageToBlob, sans forcer un carré comme
pour les avatars) avant l'upload, avec une limite dure côté bucket
Supabase (3 Mo, webp/jpeg/png uniquement) en filet de sécurité. Le chemin
de stockage ne contient jamais le user_id, contrairement aux avatars,
pour ne pas faire fuiter l'auteur via l'URL publique de l'image.
Ajoute aussi des réactions emoji (👍😂😱❤️🔥) sur chaque note, avec le
même principe d'anonymat que le reste du Mur : le total par emoji est
public (wall_note_reaction_counts), mais personne ne voit qui a réagi.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un Citoyen peut désormais laisser un motif facultatif en déposant son
jeton (toujours deux étapes avant l'envoi). Le classement permet de
consulter les justifications reçues par une personne via une nouvelle RPC
urn_vote_reasons() — même principe que wall_notes_today : le contenu est
public, l'auteur ne l'est jamais, ce qui préserve la garantie de L'Urne
que personne (Archontes compris) ne voit qui a voté pour qui.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le classement (dont un nouveau podium top 3, réutilisant celui de L'Agora)
vit maintenant dans une modale entièrement détachée de la mise en page du
jeu : une mise à jour Realtime du classement pendant une partie ne peut
plus redimensionner le canvas et casser la partie en cours. Le jeu occupe
tout l'écran disponible (contain avec letterboxing) au lieu d'une carte
fixe en haut de page. Corrige aussi le score du classement, en bleu au
lieu de doré (contraste insuffisant sur fond marbre clair).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.env.docker.example suit le nouveau domaine. README/CLAUDE.md décrivaient
encore un déploiement Vercel jamais réellement utilisé : corrigés pour
refléter le vrai pipeline self-hosted (Docker Compose + Traefik + CI Gitea
Actions via webhook).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le pseudo temporaire fixe "Nouveau membre" entrait en collision (contrainte
unique) dès qu'une 2e personne était invitée avant que la 1re ait fini son
inscription ; il inclut maintenant un suffixe d'uuid. Le token d'invitation
pouvait aussi s'établir sur n'importe quelle page (pas seulement /signup),
laissant certains comptes connectés sans jamais voir le formulaire de mot
de passe — /signup affiche désormais un état de chargement au lieu d'un
écran vide, et le proxy renvoie systématiquement vers /signup tant que le
compte n'est pas finalisé.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
/calendrier disparaît de la navigation (route toujours fonctionnelle
pour les Archontes qui y accèdent directement) : la date du Tribunal,
seule info largement utile au quotidien, est reprise en en-tête de
/leaderboard. Extrait la logique de décompte dans un helper partagé
src/lib/tribunal-date.ts plutôt que de la dupliquer.
Corrige au passage un espace JSX manquant sur /urne oublié lors du
dernier commit ("Tu as voté pour Xaujourd'hui").
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.
Nouvelle page /mur : chaque Citoyen peut publier une note anonyme par
jour (dix places max sur le mur, remis à zéro chaque jour — filtrage
par date, jamais de suppression). L'anonymat n'est qu'à moitié réel :
les Archontes voient une section "Historique complet" (filtrable par
jour) avec l'auteur de chaque note, cohérent avec le thème du site où
les Archontes ont un ascendant sur les Citoyens.
Nouvelle table wall_notes + deux RPC : wall_notes_today() ne renvoie
jamais l'auteur (seul moyen de garantir l'anonymat côté serveur, la
RLS ne pouvant pas masquer une colonne pour certaines lignes) et
post_wall_note() applique les règles (un juge ne publie pas, une note
par jour, mur plafonné à 10). Pas de gel lié à la date du Tribunal :
contrairement au Char, c'est une mécanique de toute la semaine.
Nouvelle table chariot_submission_history (append-only, comme
points_log) : chariot_submissions ne garde que la valeur actuelle
(upsert), donc sans ce journal les Archontes ne voyaient jamais les
versions précédentes d'une question modifiée plusieurs fois.
submit_chariot_question() y écrit désormais à chaque changement réel.
Sur /char/questions, chaque suggestion affiche un lien "Historique (N)"
(visible seulement si la personne a déjà modifié sa proposition) qui
déplie les versions précédentes avec leur date.
Ajoute un suivi de la dernière valeur sauvegardée (comme le pattern
déjà utilisé dans admin-members-table.tsx et profile-form.tsx) : le
bouton "Enregistrer" se grise et affiche "Déjà enregistrée" tant que
le texte n'a pas été modifié depuis la dernière sauvegarde, dès le
chargement de la page si la question n'a pas bougé.
Chaque Citoyen peut proposer une question pour Le Char depuis /profile
(upsert via submit_chariot_question, figée dès la date du Tribunal
comme les scores d'Icare). Les Archontes modèrent ces suggestions
directement sur /char/questions (ajouter à la banque ou rejeter).
/admin affiche maintenant l'email de chaque membre via une nouvelle
RPC admin_list_members(), seule façon d'exposer auth.users.email sans
passer par la service_role key côté client.
Corrige au passage deux bugs découverts pendant les tests : la
policy interne de admin_list_members() référençait id/role sans les
qualifier, ambigus avec les colonnes du RETURNS TABLE (erreur
Postgres 42702) ; et l'auteur d'une suggestion s'affichait toujours
comme "un Citoyen" car le embed profiles(...) avait été mal retypé en
tableau alors qu'il est retourné en objet à l'exécution.
Les canaux Realtime (Char, Gardien, classement, journal, calendrier,
Icare) s'abonnaient juste après createClient(), avant que la session
ne soit chargée de façon asynchrone — le canal rejoignait donc
Supabase en rôle "anon" au lieu de "authenticated", et les policies
RLS "to authenticated" bloquaient silencieusement tous les événements
(l'abonnement lui-même réussissait, ce qui masquait le problème).
Ajoute waitForRealtimeAuth() dans src/lib/supabase/client.ts, qui
attend la session et pousse explicitement le token avant de
s'abonner. Vérifié avec deux sessions navigateur distinctes : les
changements se propagent maintenant réellement sans rechargement.
Partage la police JetBrains Mono (déjà utilisée sur /admin) via un
module commun src/lib/fonts.ts, et l'applique à l'entrée "Admin" du
menu déroulant. Remplace son icône générique par une nouvelle icône
terminal (IconTerminal), cohérente avec la rupture volontaire de
thème sur cette page technique.
Déplace La Roulette de sa page dédiée vers une section de /char (tout
l'outillage de la soirée du Tribunal au même endroit) et retire la
route /roulette. Corrige un bug de superposition où le pointeur doré
de la roulette passait devant le menu déroulant du header (isolation
de contexte d'empilement manquante). Renomme "Le Conseil des
Archontes" en "Admin" avec une police monospace, rupture volontaire
avec le thème grec pour cette page technique.
Page /char (16:9, projetée) pour piloter les deux jeux physiques du
Tribunal : Le Char (2 colonnes de 3 emplacements côte à côte avec un
VS et une illustration de char vu du dessus, sélection en un clic,
duels, compteur et classement des passages) et Le Gardien du Silence
(tirage aléatoire parmi les Citoyens, minuteur avec reroll auto et
manuel). Banque de questions gérée séparément sur /char/questions
(pensée pour être pilotée depuis un téléphone pendant que /char est
projetée depuis un ordinateur).
À partir du score 10, la vitesse de défilement augmente et l'écart
entre les colonnes se resserre progressivement, jusqu'à un palier à
score 50 (vitesse ×1.6, écart réduit à 75%) pour ne pas devenir
injouable sur une longue partie. Le gap est maintenant stocké par
colonne (plutôt qu'une constante globale) pour que les colonnes déjà
en place ne changent pas de taille rétroactivement.
- Écran de fin (score + Rejouer) déplacé en overlay par-dessus le jeu
(fond assombri, carte animée, confettis) plutôt qu'en dessous.
- Battement d'ailes repensé : cycle continu en boucle (visible même au
repos, sans taper) + battement plus ample au tap qui prend le dessus
brièvement. L'ancienne version ne bougeait qu'au moment du tap et
retombait trop vite pour être perceptible en jeu réel.
- Corrige une aile qui semblait ne pas bouger : les deux ailes utilisaient
des teintes différentes et l'une passait partiellement derrière le
corps ; elles sont maintenant identiques et toutes deux dessinées
au-dessus du corps. Corrige au passage un signe de rotation inversé.
- Corrige le contraste de plusieurs textes (« Ton record », instructions,
pied de page) qui utilisaient une couleur prévue pour du texte sur
marbre alors qu'ils s'affichent sur le fond sombre de la page.
Après retour d'expérience, la course de char (vue du dessus, piste
générée par jour) laisse place à un Flappy Bird grec plus simple et
plus lisible sur téléphone : Icare vole entre des colonnes de temple,
touche l'écran pour battre des ailes, échec net au premier contact
(score remis à zéro).
Record personnel all-time (plus de piste quotidienne ni de fantômes) :
les scores se figent dès que la date du Tribunal est atteinte, puis les
gloires du top 3 sont attribuées automatiquement via une tâche
planifiée pg_cron, comme pour l'ancienne Course du Char.
schema.sql nettoie explicitement l'ancienne Course du Char (tables,
RPC, tâche planifiée) avant de poser le nouveau schéma, puisqu'elle
avait déjà été appliquée en prod.
Premier mini-jeu compétitif du Tribunal (/course) : piste 2D générée
de façon déterministe à partir de la date du jour (identique pour tout
le monde, jamais stockée côté serveur), pilotage au joystick tactile
virtuel, murs + obstacles qui ralentissent temporairement sans jamais
stopper la course, fantômes du top 3 du jour. Chacun rejoue autant
qu'il veut, seul le meilleur temps compte.
Moteur de jeu maison (src/lib/chariot/) : canvas + requestAnimationFrame,
sans dépendance externe, dans la continuité de l'existant (roulette).
Navigation : nouvelle entrée "La Course du Char" (IconChariot), route
protégée par le proxy comme le reste de l'app.
computeRanksBy(items, comparator) remplace la logique de classement
"ex-aequo partagent le rang" en la rendant générique ; computeRanks
devient un simple appel avec le comparateur points desc, sans rien
changer pour l'appelant existant. Podium accepte maintenant un prop
renderValue (+ valueLabel optionnel) à la place du "points/gloires" en
dur, pour pouvoir afficher un classement par temps (Course du Char)
avec le même composant que le classement par points.
- 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à.
Un seul round sur toute la semaine : plus besoin de pouvoir
réinitialiser le repère de classement depuis l'admin. La logique des
flèches de progression (previous_rank, reset_rank_reference) reste en
place, simplement plus rien ne la déclenche.
Le lien d'invitation Supabase redirige avec les tokens dans le FRAGMENT
d'URL (#access_token=...&refresh_token=...), jamais dans un paramètre
?code=. Un fragment n'est jamais envoyé au serveur par le navigateur
(une route serveur ne peut donc pas le lire), et le client
@supabase/ssr ne le détecte pas automatiquement (orienté sessions par
cookies). /signup parse maintenant ce fragment et appelle
auth.setSession() explicitement avant de vérifier l'utilisateur.
Supprime la route src/app/auth/callback créée par erreur sur
l'hypothèse d'un flux PKCE (?code=) qui ne correspond pas à ce que
Supabase envoie réellement pour ce type de lien.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>