@AGENTS.md # Le Tribunal Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des olympiades pendant une semaine de vacances : comptes, rôles, leaderboard en direct, attribution de points par des juges, journal public des événements. Ré-thématisable par édition (un thème différent chaque année). ## 1. État actuel V1, V2 et V3 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 Administration. - **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. Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/thèmes, le jeu du Tribunal lui-même (Char, roulette, ostracisme, timer du Gardien), mode grand écran. --- ## 2. Stack technique - **Framework** : Next.js 16 (App Router) + TypeScript - **Style** : Tailwind CSS v4 (design tokens en variables CSS, voir `globals.css`) - **Auth + Base + Stockage + Temps réel** : Supabase (Auth, Postgres, Storage, Realtime) - **Recadrage photo** : `react-easy-crop` + canvas natif (resize/compression client, jamais d'upload de l'image brute) - **Déploiement** : Vercel (front) + Supabase (managé) — pas encore déployé, tourne en local pour l'instant Next.js 16 a renommé `middleware.ts` en `proxy.ts` (fichier `src/proxy.ts`, fonction `proxy`). `src/lib/supabase/middleware.ts` est un nom de fichier utilitaire interne, sans rapport avec cette convention. --- ## 3. Modèle de données (`supabase/schema.sql`, source de vérité) Le script est **idempotent** : toujours le ré-exécuter en entier après une modification, jamais de migration incrémentale séparée. ### `public.profiles` (liée à `auth.users`, créée automatiquement par un trigger sur `auth.users`) | Colonne | Notes | |-----------------|-------------------------------------------------------------------| | `id` | = `auth.users.id` | | `pseudo` | affiché partout, unique (insensible à la casse) | | `avatar_url` | URL publique Storage (`{user_id}.webp`) | | `points` | jamais modifiable directement — uniquement via RPC `award_points` | | `role` | `public` (défaut) ou `judge` | | `pseudo_locked` | passe à `true` automatiquement au 1er changement de pseudo | | `previous_rank` | repère de classement, mis à jour par `reset_rank_reference()` | ### `public.points_log` (journal, lecture seule côté client) `id, target_id, judge_id, delta, reason, created_at` — écrit uniquement par la RPC `award_points`. ### Colonnes sensibles : RLS + trigger, jamais confiance au client La RLS est **au niveau ligne** : elle ne peut pas exprimer « cette colonne seulement si tel rôle ». Le verrouillage fin des colonnes (`role`, `points`, `pseudo_locked`, pseudo figé, `previous_rank`) passe par le trigger `enforce_profile_update()` : - `points` : bloqué pour **tout le monde**, y compris les juges, en écriture directe. Seule la RPC `award_points()` (SECURITY DEFINER) peut la modifier, via un flag de session (`app.bypass_points_lock`) qui autorise explicitement CETTE écriture précise. - `role`, `previous_rank` : modifiables uniquement par un `judge`. - `pseudo` : modifiable une fois par le propriétaire (auto-lock ensuite), ou à tout moment par un `judge`. - `auth.uid() is null` (SQL Editor, migrations) bypass le trigger : ces accès sont déjà fiables par construction. Ce pattern (RLS pour l'accès à la ligne + trigger `BEFORE UPDATE` pour l'accès aux colonnes) est la convention du projet — le reproduire pour toute nouvelle colonne sensible plutôt que d'inventer autre chose. ### RPC exposées - `is_pseudo_taken(p_pseudo)` — anon + authenticated, ne renvoie qu'un booléen (l'anon ne peut pas lire `profiles`). - `award_points(p_target_id, p_delta, p_reason)` — authenticated, vérifie `role = 'judge'` côté serveur, jamais côté client. - `reset_rank_reference()` — authenticated, vérifie `role = 'judge'`, fige le classement courant dans `previous_rank`. --- ## 4. Authentification Email + mot de passe (Supabase Auth impose l'unicité de l'email). Confirmation d'email **activée** — nécessite un SMTP custom configuré côté Supabase (le service intégré est trop limité en volume, voir README). Le pseudo est un simple nom d'affichage stocké dans `profiles`, jamais l'email n'est montré aux autres membres. --- ## 5. Pages & navigation | Route | Contenu | |----------------|---------------------------------------------------------------------------| | `/` | Redirige vers `/leaderboard` si connecté, sinon `/login` | | `/login` | Email + mot de passe | | `/signup` | Email + mot de passe + pseudo + photo (recadrée) ; gère l'attente de confirmation email | | `/leaderboard` | Podium top 3 + liste, Realtime, contrôles de points pour les juges | | `/journal` | « Le crieur » — fil live des attributions de points, lecture pour tous | | `/profile` | Pseudo (verrouillable), photo, déconnexion | | `/admin` | Juges uniquement, **vérifié côté serveur** (Server Component) — gestion des membres, reset du repère de classement | `/leaderboard`, `/profile`, `/admin`, `/journal` sont protégées par `src/proxy.ts` (redirection `/login` si non connecté). L'accès juge-only de `/admin` n'est **pas** géré par le proxy (il n'a pas facilement le rôle) — c'est la page elle-même qui vérifie et redirige. --- ## 6. Look & feel Design tokens en variables CSS (`globals.css`) : `--navy #0F2748`, `--gold #C9A227`, `--ivory #F4ECD8`, `--ink #2A2116`. Ne jamais coder une couleur en dur dans un composant — passer par ces tokens pour que le re-thème par édition reste possible. Convention de carte : `rounded-xl border border-navy/10 bg-white shadow-sm` (formulaires, podium) ou `rounded-lg` pour les lignes de liste. Bouton primaire : `bg-navy text-ivory hover:bg-navy/90`. Bouton secondaire : `border border-navy/20 hover:bg-navy/5`. --- ## 7. Conventions - Toujours passer par les policies RLS + trigger ; ne jamais exposer la `service_role key` côté client. - Toute nouvelle colonne sensible sur `profiles` (ou une future table) suit le pattern RLS+trigger de la section 3, pas un `grant`/`revoke` par colonne (qui ne peut pas distinguer les rôles applicatifs). - Aperçus locaux d'image avant upload (crop, fichier choisi) = `blob:`/`data:` URL → toujours `unoptimized` sur `next/image` ou une balise `` classique (`next/image` ne sait pas résoudre ces schémas côté serveur). - Commandes : `npm run dev`, `npm run build`, `npm run lint`. - Le schéma SQL (`supabase/schema.sql`) est la seule source de vérité pour la base — le tenir à jour à chaque changement de données/sécurité, et le garder idempotent. --- ## 8. Roadmap (pas encore codé) - **Éditions & thèmes** : table `editions` (année, thème, mood board, vocabulaire) ; les points deviendront rattachables à une édition. - **Le jeu du Tribunal** : le Char (zone de pénalité), batailles de cul sec, ostracisme, roulette, timer du Gardien. - **Mode grand écran** : vue leaderboard optimisée pour vidéoprojecteur. - **Déploiement** : pas encore fait (Vercel + Supabase managé prévus, voir README).