e542b09807
Build and deploy / deploy (push) Successful in 35s
.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>
252 lines
17 KiB
Markdown
252 lines
17 KiB
Markdown
# Le Tribunal
|
||
|
||
Application web pour animer et scorer le jeu **« Le Tribunal »** entre amis pendant une semaine de vacances : comptes, rôles, leaderboard en direct, attribution de points par les juges et journal public des événements. Habillée sur le thème d'un tribunal de la Grèce antique (voir [Direction artistique](#direction-artistique)).
|
||
|
||
---
|
||
|
||
## Fonctionnalités
|
||
|
||
- 🔐 **Création de compte** par email + mot de passe (email confirmé, un compte par email)
|
||
- 👤 **Connexion / déconnexion**, pseudo affiché partout (jamais l'email)
|
||
- 🖼️ **Profil éditable** : pseudo modifiable **une seule fois** puis figé (sauf par un Archonte), photo recadrée en rond, redimensionnée et compressée côté client
|
||
- 🏆 **Leaderboard en direct** (Supabase Realtime) : podium top 3 (or/argent/bronze, gestion des égalités), flèches ▲▼ de progression depuis le dernier « round »
|
||
- ⚖️ **Rôles** : `public` / `judge`. Les Archontes (juges) attribuent des points (+/- avec montant personnalisé et motif, confirmation explicite avant envoi) et gèrent les Citoyens depuis **Le Conseil des Archontes**
|
||
- 📣 **Le crieur** : journal public et en direct de toutes les attributions de points
|
||
- 🎡 **La Roulette** : tirage au sort animé (avatars) parmi tous les membres ou les Citoyens uniquement
|
||
- 🗓️ **Le Calendrier des Dieux** : agenda de la semaine sous le patronage du Panthéon, date du Tribunal avec compte à rebours, édition réservée aux Archontes
|
||
- 📱 **Responsive**, pensé mobile-first et propre en vidéoprojection
|
||
|
||
---
|
||
|
||
## Stack technique
|
||
|
||
| Rôle | Techno |
|
||
|--------------------------|---------------------------------|
|
||
| Framework | Next.js 16 (App Router) + TypeScript |
|
||
| Style | Tailwind CSS v4 (tokens Grèce antique, voir `globals.css`) |
|
||
| Typographie | Cinzel (titres), Cormorant Garamond (citations), Manrope (interface) — `next/font/google` |
|
||
| Auth · Base · Stockage · Temps réel | Supabase (Auth, Postgres, Storage, Realtime) |
|
||
| Recadrage photo | react-easy-crop + canvas (resize/compression client) |
|
||
| Email transactionnel | SMTP custom (ex. Resend) branché sur Supabase Auth |
|
||
| Déploiement | Docker Compose + Traefik (self-hosted) + Supabase (managé) |
|
||
|
||
---
|
||
|
||
## Prérequis
|
||
|
||
- [Node.js](https://nodejs.org/) 20.19+ (Next.js 16 / React 19)
|
||
- Un compte [Supabase](https://supabase.com/) (offre gratuite suffisante)
|
||
- Un fournisseur SMTP gratuit (ex. [Resend](https://resend.com/), 100 emails/jour) — le service email intégré de Supabase a un rate limit trop bas pour un usage réel
|
||
- Pour le déploiement (optionnel en local) : un serveur avec Docker + Traefik déjà en place, voir [Déploiement](#déploiement)
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
### 1. Cloner et installer
|
||
|
||
```bash
|
||
git clone ssh://git@git.alxczl.fr:7878/valentin/tribunal-app.git
|
||
cd tribunal-app
|
||
npm install
|
||
```
|
||
|
||
### 2. Créer le projet Supabase
|
||
|
||
1. Créez un nouveau projet sur [supabase.com](https://supabase.com/).
|
||
2. Dans **SQL Editor**, exécutez tout le contenu de [`supabase/schema.sql`](supabase/schema.sql) (tables `profiles`/`points_log`/`days`/`events`/`settings`, policies RLS, triggers, RPC, bucket `avatars`, Realtime). Le script est idempotent, vous pouvez le ré-exécuter sans risque après une mise à jour.
|
||
3. Récupérez l'URL du projet et la clé `anon` dans **Project Settings → API**.
|
||
4. Dans **Authentication → Providers → Email**, **désactivez "Allow new users to sign up"** — l'inscription publique est fermée, seules les invitations créent un compte (voir [Authentification](#authentification)).
|
||
5. Dans **Authentication → URL Configuration**, mettez **Site URL** sur `http://localhost:3000/signup` (puis votre domaine + `/signup` une fois déployé), et ajoutez la même valeur dans **Redirect URLs**. C'est la page où atterrit un lien d'invitation.
|
||
6. Dans **Authentication → Settings → SMTP Settings**, branchez un fournisseur SMTP custom (ex. Resend) — le service email par défaut de Supabase est bridé à quelques emails/heure. Voir [Email transactionnel](#email-transactionnel) ci-dessous.
|
||
7. Invitez-vous vous-même depuis **Authentication → Users → Invite user**, finalisez votre compte via le lien reçu, puis désignez-vous Archonte (voir [Rôles & administration](#rôles--administration)).
|
||
|
||
### 3. Variables d'environnement
|
||
|
||
```bash
|
||
cp .env.local.example .env.local
|
||
```
|
||
|
||
```env
|
||
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxx.supabase.co
|
||
NEXT_PUBLIC_SUPABASE_ANON_KEY=votre_cle_anon
|
||
```
|
||
|
||
> ⚠️ Ne mettez **jamais** la clé `service_role` dans le front ni dans le dépôt.
|
||
|
||
### 4. Lancer en local
|
||
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
L'application tourne sur [http://localhost:3000](http://localhost:3000).
|
||
|
||
---
|
||
|
||
## Scripts
|
||
|
||
| Commande | Description |
|
||
|------------------|------------------------------------|
|
||
| `npm run dev` | Serveur de développement |
|
||
| `npm run build` | Build de production |
|
||
| `npm run start` | Lance le build de production |
|
||
| `npm run lint` | Vérification du code (ESLint) |
|
||
|
||
---
|
||
|
||
## Déploiement (Docker + Traefik)
|
||
|
||
Self-hosted, pas de Vercel. Un serveur avec Docker Compose et un reverse proxy [Traefik](https://traefik.io/) déjà en place (réseau externe partagé, voir `TRAEFIK_NETWORK`), plus un pipeline Gitea Actions qui build et déploie automatiquement à chaque push sur la branche principale.
|
||
|
||
1. Sur le serveur, copiez `.env.docker.example` en `.env.docker` et renseignez :
|
||
- `APP_DOMAIN` : le domaine sur lequel l'app doit répondre (utilisé dans le label Traefik `Host(\`${APP_DOMAIN}\`)`).
|
||
- `TRAEFIK_NETWORK` : le nom du réseau Docker externe sur lequel Traefik écoute.
|
||
- `WEBHOOK_SECRET` : secret partagé avec le workflow Gitea Actions (`openssl rand -hex 16`).
|
||
2. `docker compose up -d` démarre le conteneur `app` (Next.js standalone) et le petit serveur webhook (`docker/webhook/receive.py`) qui reçoit les déploiements.
|
||
3. Le workflow `.gitea/workflows/deploy.yml` build l'app à chaque push sur `main` et POST le build vers le webhook, qui déclenche un hot-swap (voir `docker/entrypoint.sh`) sans downtime — pas de rebuild manuel nécessaire une fois configuré.
|
||
4. **Changer de domaine** : mettez à jour `APP_DOMAIN` dans `.env.docker` sur le serveur, puis recréez le conteneur (`docker compose up -d --force-recreate app`) pour que Traefik prenne en compte le nouveau label `Host()`. Pensez aussi à mettre à jour, côté Supabase Dashboard, **Authentication → URL Configuration** (Site URL + Redirect URLs) et le template d'email d'invitation (logo en dur), sans quoi les liens d'invitation et le logo pointeront encore vers l'ancien domaine.
|
||
|
||
---
|
||
|
||
## Structure du projet
|
||
|
||
```
|
||
tribunal-app/
|
||
├─ src/
|
||
│ ├─ app/
|
||
│ │ ├─ login/page.tsx # Connexion (email + mot de passe)
|
||
│ │ ├─ signup/page.tsx # Finalisation d'un compte invité (mot de passe, pseudo, photo)
|
||
│ │ ├─ leaderboard/
|
||
│ │ │ ├─ page.tsx # Fetch initial + rôle de l'utilisateur
|
||
│ │ │ └─ leaderboard-view.tsx # Realtime, podium, liste, contrôles juges
|
||
│ │ ├─ journal/ # « Le Crieur » : fil des décrets en direct
|
||
│ │ ├─ roulette/ # « La Roulette » : tirage au sort animé
|
||
│ │ ├─ calendrier/ # « Le Calendrier des Dieux » : agenda, panthéon, date du Tribunal
|
||
│ │ ├─ admin/ # Le Conseil des Archontes : gestion des membres, reset du repère
|
||
│ │ ├─ profile/ # Édition pseudo (verrou) / photo, déconnexion
|
||
│ │ ├─ layout.tsx # Layout + header global
|
||
│ │ ├─ icon.svg # Favicon (couronne de laurier)
|
||
│ │ └─ page.tsx # Redirection selon l'état de connexion
|
||
│ ├─ components/ # Header (logo + chip utilisateur + menu), Avatar, AvatarPicker,
|
||
│ │ │ # Podium, JudgePointControls, PointsConfirmControls, icons,
|
||
│ │ │ # LaurelWreath, DiamondDivider
|
||
│ ├─ lib/
|
||
│ │ ├─ ranking.ts # Classement avec égalités + calcul des flèches
|
||
│ │ ├─ pantheon.ts # Préréglages des dieux + icônes par type d'événement
|
||
│ │ ├─ image.ts # Recadrage/compression canvas côté client
|
||
│ │ └─ supabase/ # Clients Supabase (browser / server / proxy)
|
||
│ └─ proxy.ts # Protection des routes + rafraîchissement session
|
||
├─ supabase/schema.sql # Schéma complet + policies RLS + triggers + RPC (versionné)
|
||
├─ .env.local.example
|
||
└─ README.md
|
||
```
|
||
|
||
---
|
||
|
||
## Authentification
|
||
|
||
**Sur invitation uniquement** : l'auto-inscription publique est désactivée côté Supabase (Authentication → Providers → Email → "Allow new users to sign up" décoché). Il n'y a plus de bouton « Créer un compte » sur `/login`.
|
||
|
||
Pour ajouter un membre : **Authentication → Users → Invite user** (email). Supabase lui envoie un email (via le SMTP configuré) contenant un lien d'invitation qui, une fois cliqué, établit directement une session sur `/signup` — c'est cette page qui sert alors à finaliser le compte (choix du mot de passe, du pseudo, upload de la photo). Sans session active (lien absent, invalide ou expiré), `/signup` affiche un message « invitation requise » plutôt qu'un formulaire d'inscription.
|
||
|
||
Le profil (`public.profiles`) est créé automatiquement dès l'invitation via un trigger sur `auth.users` (pseudo temporaire `"Nouveau membre"` tant que la finalisation n'a pas eu lieu). Le pseudo est purement un nom d'affichage (leaderboard, profil, journal) : l'email n'est jamais montré aux autres membres.
|
||
|
||
### Verrou de pseudo
|
||
|
||
Chaque membre peut changer son pseudo **une seule fois** depuis `/profile`. Dès l'enregistrement, `pseudo_locked` passe à `true` et le champ devient en lecture seule (« pseudo figé — contacte un Archonte pour le changer »). Un Archonte peut toujours modifier le pseudo de quiconque et, s'il le souhaite, redéverrouiller un compte depuis **Le Conseil des Archontes**.
|
||
|
||
### Rôles & administration
|
||
|
||
Deux rôles : `public` (défaut) et `judge`. Les 2 premiers juges sont désignés manuellement via le SQL Editor :
|
||
|
||
```sql
|
||
select id, pseudo from public.profiles;
|
||
update public.profiles set role = 'judge' where id = '<uuid-du-membre>';
|
||
```
|
||
|
||
Ensuite, un Archonte peut promouvoir/rétrograder d'autres membres et éditer leur pseudo depuis **Le Conseil des Archontes** (visible uniquement par les Archontes, et vérifié côté serveur — pas seulement caché en front).
|
||
|
||
### Email transactionnel
|
||
|
||
Le service email intégré de Supabase (sans SMTP custom) limite l'envoi à quelques emails par heure — largement insuffisant dès qu'on teste à plusieurs. Configurez un SMTP custom dans **Authentication → Settings → SMTP Settings**, par exemple avec [Resend](https://resend.com/) (gratuit, 100 emails/jour) :
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| Sender email | `onboarding@resend.dev` (domaine de test Resend) ou une adresse d'un domaine que vous avez vérifié |
|
||
| Host | `smtp.resend.com` |
|
||
| Port | `465` |
|
||
| Username | `resend` (en minuscules) |
|
||
| Password | votre clé API Resend |
|
||
|
||
> Avec le domaine de test `onboarding@resend.dev`, Resend ne livre qu'à l'adresse email du compte Resend lui-même. Pour envoyer à tous vos amis, vérifiez votre propre domaine dans Resend.
|
||
|
||
---
|
||
|
||
## Points, journal et classement
|
||
|
||
- **Attribution de points** : depuis L'Agora, un Archonte clique `+`/`−` (montant personnalisable, motif optionnel) — le delta reste **en attente** (affiché en couleur) jusqu'à un clic sur **Confirmer** (envoie la RPC `award_points`) ou **Annuler**. Le client n'écrit jamais directement la colonne `points` : la RPC vérifie le rôle juge côté serveur, applique le delta (négatif autorisé, pas de plancher à 0) et journalise l'opération.
|
||
- **Le crieur** (`/journal`) : fil de tous les événements (« Untel a donné +5 à Untel — motif »), visible par tous les authentifiés, alimenté par Supabase Realtime.
|
||
- **Podium** : top 3 mis en avant (or/argent/bronze), gère proprement les égalités (rangs partagés) et les groupes de moins de 3 membres.
|
||
- **Flèches de progression** : ▲/▼/= comparent le rang actuel au dernier repère. Un Archonte fige un nouveau repère via **Nouveau round** dans **Le Conseil des Archontes**.
|
||
|
||
---
|
||
|
||
## Le Calendrier des Dieux
|
||
|
||
Agenda de la semaine (`/calendrier`) : chaque journée est placée sous le patronage d'une divinité (nom, domaine, emblème SVG, couleur d'accent — 8 préréglages fournis dans `lib/pantheon.ts` : Dionysos, Arès, Athéna, Aphrodite, Hermès, Poséidon, Hadès, Zeus ; un Archonte peut en saisir d'autres librement). Chaque journée liste ses événements (activité/défi/épreuve/tribunal, heure, lieu, description).
|
||
|
||
- **Lecture** : tous les authentifiés.
|
||
- **Écriture** (créer/modifier/supprimer une journée ou un événement, définir la date du Tribunal) : réservée aux Archontes, imposée par policies RLS sur `days`/`events`/`settings` — jamais par un simple masquage des boutons côté client.
|
||
- **Bannière** : compte à rebours avant la date du Tribunal (`settings.tribunal_date`), éditable par les Archontes.
|
||
- Le jour courant est mis en valeur, les jours passés sont estompés, le jour contenant un événement de type `tribunal` reçoit un accent oxblood distinct.
|
||
- Realtime activé sur les trois tables : toute modification est visible immédiatement par tous.
|
||
|
||
---
|
||
|
||
## Sécurité
|
||
|
||
- Mots de passe hachés et gérés par Supabase Auth (jamais stockés en clair).
|
||
- **Row Level Security** activée sur `profiles` et `points_log`. La RLS étant au niveau ligne, les colonnes sensibles (`role`, `points`, `pseudo_locked`, pseudo figé) sont verrouillées par un **trigger `BEFORE UPDATE`** qui ne fait jamais confiance à ce qu'envoie le client.
|
||
- Les points ne sont modifiables que via la RPC `award_points` (`SECURITY DEFINER`), jamais par une écriture directe — même un Archonte ne peut pas modifier la colonne `points` à la main.
|
||
- `points_log` est en lecture seule pour les clients ; seule la RPC peut y écrire.
|
||
- La page `/admin` vérifie le rôle côté serveur (Server Component) avant de rendre quoi que ce soit — pas de simple masquage front.
|
||
- `days`/`events`/`settings` (Calendrier) : lecture ouverte à tous les authentifiés, écriture (insert/update/delete) réservée au rôle `judge` par policies RLS dédiées — un Citoyen qui appellerait l'API directement se ferait rejeter, pas seulement masquer les boutons.
|
||
|
||
---
|
||
|
||
## Direction artistique
|
||
|
||
Thème fixe : un tribunal égéen de nuit — marbre, or, mer sombre, sceaux de cire. Tokens couleur (`globals.css`) :
|
||
|
||
| Token | Valeur | Usage |
|
||
|---|---|---|
|
||
| `--ink` / `--ink-2` | `#0A1B33` / `#0F2748` | Fond de l'app (mer de nuit), header, boutons primaires |
|
||
| `--marble` / `--marble-2` | `#F4ECD8` / `#E9DEC2` | Surfaces claires (cartes, formulaires) |
|
||
| `--gold` / `--gold-bright` | `#C9A227` / `#E7C560` | Accents, bordures, 1ʳᵉ place |
|
||
| `--oxblood` | `#A5342A` | Blâmes (points négatifs), sceaux, erreurs |
|
||
| `--olive` | `#5E6B3B` | Honneurs (points positifs), succès |
|
||
| `--sea` | `#2E6E7E` | Accent secondaire |
|
||
| `--silver` / `--bronze` | `#C7CDD6` / `#B08D57` | Accents 2ᵉ/3ᵉ place du podium |
|
||
| `--text-marble` / `--text-mut` | `#2A2116` / `#7A6A4C` | Texte sur surfaces claires (jamais de doré sur marbre pour du texte courant — contraste insuffisant) |
|
||
|
||
Ornements en SVG/CSS inline (pas d'images bitmap) : couronne de laurier (`components/laurel-wreath.tsx`, calculée par trigonométrie), frise à méandre (`.meander-divider`), séparateur losange (`components/diamond-divider.tsx`), texture marbre (`.marble-surface`).
|
||
|
||
Vocabulaire (toujours accompagné du terme fonctionnel) : juges → **Archontes**, membres → **Citoyens**, leaderboard → **L'Agora**, journal → **Le Crieur**, points positifs/négatifs → **Honneurs**/**Blâmes**, section admin → **Le Conseil des Archontes**.
|
||
|
||
Ce thème est **codé en dur** (pas de système de re-thématisation par édition) — changer l'apparence pour une prochaine édition demande d'éditer directement les tokens et le vocabulaire.
|
||
|
||
---
|
||
|
||
## Roadmap
|
||
|
||
- [ ] **Éditions** — table `editions` (année, mood board) pour rattacher les points à une édition donnée
|
||
- [ ] **Le jeu du Tribunal** — le Char, batailles de cul sec, ostracisme, timer du Gardien (la Roulette est livrée)
|
||
- [ ] **Mode grand écran** — vue leaderboard optimisée vidéoprojecteur
|
||
|
||
---
|
||
|
||
## Licence
|
||
|
||
Projet personnel et privé, destiné à un usage entre amis.
|