@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. Habillée sur le thème d'un tribunal de la Grèce antique (voir section 6). ## 1. État actuel V1 à V5 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. - **V4** : direction artistique Grèce antique (marbre/or/mer de nuit, polices Cinzel/Cormorant Garamond/Manrope, vocabulaire Archontes/Citoyens/Agora/Crieur), header global avec chip utilisateur. - **V5** : navigation entièrement repliée dans le menu déroulant du chip (header épuré : logo + chip seulement), page **La Roulette** (tirage au sort animé), page **Le Calendrier des Dieux** (agenda de la semaine, panthéon, date du Tribunal), boutons de validation des points unifiés (`PointsConfirmControls`) entre podium et classement. Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, le jeu du Tribunal lui-même (Char, 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`. ### `public.days` / `public.events` / `public.settings` (Calendrier des Dieux) - `days` : `id, date (unique), god_name, god_domain, description, created_at`. - `events` : `id, day_id (fk days), title, description, type ('activite'|'defi'|'epreuve'|'tribunal'), start_time, location, created_by (fk profiles, forcé par trigger), created_at`. - `settings` : ligne unique (`id boolean primary key default true`, contrainte `check (id)`) — `tribunal_date`. - RLS : `SELECT` ouvert à tout authentifié ; `INSERT`/`UPDATE`/`DELETE` réservés au rôle `judge`, vérifié par policy (`exists (select 1 from profiles where id = auth.uid() and role = 'judge')`) — pas de trigger `BEFORE UPDATE` ici car il n'y a pas de colonne à protéger *dans une ligne par ailleurs modifiable par tous* (contrairement à `profiles`) : toute la table est verrouillée en écriture aux juges. ### 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 **Sur invitation uniquement**, plus d'auto-inscription publique : "Allow new users to sign up" est désactivé côté Supabase. Un Archonte invite via Authentication → Users → Invite user ; le lien reçu établit une session directement sur `/signup`, qui sert alors à finaliser le compte (mot de passe, pseudo, photo) — sans session valide, la page affiche un message « invitation requise ». Le profil est créé dès l'invitation par le trigger `handle_new_user` (pseudo temporaire `"Nouveau membre"` jusqu'à finalisation). Nécessite un SMTP custom 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` | « L'Agora » — podium top 3 + liste, Realtime, contrôles de points pour les juges | | `/roulette` | « La Roulette » — tirage au sort animé parmi les membres (ou Citoyens uniquement) | | `/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 | | `/admin` | « Le Conseil des Archontes » — juges uniquement, **vérifié côté serveur** (Server Component) — gestion des membres, reset du repère de classement | `/leaderboard`, `/profile`, `/admin`, `/journal`, `/roulette`, `/calendrier` 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 ; 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. --- ## 6. Look & feel — Grèce antique Thème **fixe et codé en dur** (pas de système de re-thématisation par édition — voir roadmap). Un tribunal égéen de nuit : marbre, or, mer sombre, sceaux de cire. Tokens couleur (`globals.css`, exposés comme utilities Tailwind via `@theme inline`) : - `--ink #0A1B33` / `--ink-2 #0F2748` : fond de l'app (dégradés radiaux mer de nuit), header, boutons primaires. - `--marble #F4ECD8` / `--marble-2 #E9DEC2` : surfaces claires (`.marble-surface`, classe utilitaire avec léger dégradé/grain). - `--gold #C9A227` / `--gold-bright #E7C560` : accents, bordures, 1ʳᵉ place. **Jamais** pour du texte courant sur `marble` (contraste insuffisant) — utiliser `--text-marble` à la place. - `--oxblood #A5342A` : blâmes (points négatifs), sceaux, erreurs. - `--olive #5E6B3B` : honneurs (points positifs), succès. - `--sea #2E6E7E` : accent secondaire. - `--silver #C7CDD6` / `--bronze #B08D57` : accents 2ᵉ/3ᵉ place du podium. - `--text-marble #2A2116` / `--text-mut #7A6A4C` : texte sur surfaces claires. `components/points-confirm-controls.tsx` : boutons ✓/✕ compacts (32px, icône 16px) de validation/annulation des points en attente — utilisés à l'identique par le podium et le reste du classement (`JudgePointControls`). Ne pas dupliquer cette UI ailleurs, toujours passer par ce composant. Polices (`next/font/google`, variables CSS, exposées comme `font-heading`/`font-serif`/`font-sans`) : **Cinzel** (titres, noms, chiffres de score — majuscules, `tracking-[0.1em]`), **Cormorant Garamond** (citations/sous-titres, italique), **Manrope** (interface, corps de texte). Ornements en SVG/CSS inline (jamais d'image bitmap) : - `components/laurel-wreath.tsx` — couronne de laurier calculée par trigonométrie (deux arcs symétriques de feuilles), utilisée en logo, sur le rang 1 du podium, etc. Le favicon (`src/app/icon.svg`) est une version statique des mêmes coordonnées. - `.meander-divider` (classe globale) — frise à méandre grecque en `background-image` SVG data-URI, séparateur discret. - `components/diamond-divider.tsx` — séparateur ◆ avec filets dégradés. - `.marble-surface` (classe globale) — texture marbre via dégradés (pas d'image de bruit). Convention de carte : `marble-surface rounded-2xl border border-gold/40 shadow-xl` (formulaires, podium) ou `rounded-lg border-gold/25` pour les lignes de liste. Bouton primaire : `bg-ink-2 text-gold-bright font-heading uppercase hover:bg-ink`. Bouton secondaire : `border border-gold/30 text-text-marble hover:bg-gold/10`. Vocabulaire diégétique (toujours accompagné du terme fonctionnel dans le code/les commentaires, pour que ça reste maintenable) : juges/admins → **Archontes**, membres → **Citoyens**, leaderboard → **L'Agora**, journal des points → **Le Crieur** (décrets), points positifs/négatifs → **Honneurs**/**Blâmes**, points → **gloires**, section admin → **Le Conseil des Archontes**. --- ## 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** : table `editions` (année, mood board) ; les points deviendront rattachables à une édition donnée. Le thème visuel (Grèce antique) reste fixe, ce n'est pas un système de re-thématisation par édition. - **Le jeu du Tribunal** : le Char (zone de pénalité), batailles de cul sec, ostracisme, timer du Gardien. (La Roulette est livrée en V5.) - **Mode grand écran** : vue leaderboard optimisée pour vidéoprojecteur. - **Déploiement** : pas encore fait (Vercel + Supabase managé prévus, voir README).