# CHANGELOG — leperelion.fr

Journal des changements, du plus récent au plus ancien. Une entrée par session de travail.

## 2026-06-08 — Documents : visualiseur intégré (au lieu d'ouvrir Google Drive)

- **Backend** : `GET /drive/files/:id/view` sert le fichier **en ligne** (`Content-Disposition:
  inline`) via le compte partagé (refactor `streamDriveFile(disposition)`) ; docs Google natifs
  exportés en PDF.
- **Frontend** (`/drive`) : « Ouvrir » n'ouvre plus `drive.google.com` (qui demandait un compte) ;
  ouvre une **fenêtre d'aperçu intégrée** : images `<img>`, PDF/Docs Google/texte en `<iframe>`,
  vidéo/audio en lecteur, sinon proposition de téléchargement. Boutons Télécharger + Fermer.
- **Vérifié** : `/view` = inline, `/download` = attachment (en-têtes contrôlés).

## 2026-06-08 — Documents : déplacer fichiers/dossiers

- **Backend** : `POST /drive/files/:id/move` (drive:write) — déplace via `files.update`
  (`addParents` cible / `removeParents` parents actuels) ; garde-fous (pas dans soi-même, no-op si
  déjà dans le dossier cible).
- **Frontend** (`/drive`) : bouton **« Déplacer »** par élément (couper) → bandeau « ouvrez le
  dossier cible puis Déplacer ici » → **« Déplacer ici »** (colle dans le dossier courant), avec
  Annuler ; désactivé si même dossier / dans soi-même / pendant une recherche. Arborescence
  rafraîchie après.
- **Vérifié** : déplacement d'un dossier dans un autre (création, move, contrôle, corbeille).

## 2026-06-08 — Alerte de zone : suppression du lien OpenStreetMap

- `DEFAULT_GEOFENCE_MESSAGE` ne contient plus `{link}` (lien OSM). Variable `{link}` retirée de
  l'aide et de l'exemple côté `/tracking` (la substitution reste gérée pour compat). Message
  personnalisé existant nettoyé en base (regexp_replace de `\s*{link}`).

## 2026-06-08 — Commandes OwnTracks : « Localiser maintenant » + géofence côté téléphone

- **Modèle** `Device.cmdReportLocation` / `cmdSyncWaypoints` (file de commandes). Migration
  `device_commands`.
- **Backend** : l'endpoint `POST /track/owntracks/:token` renvoie désormais un tableau de
  **commandes** (au lieu de `[]`) selon les drapeaux : `{_type:cmd, action:reportLocation}` et
  `{_type:cmd, action:setWaypoints, …}` (la zone), puis les consomme. Accepte aussi les messages
  `_type:transition` (stockés comme position → alimente les alertes). Endpoints super_admin
  `POST …/locate` et `POST …/sync-waypoints` ; le PUT/DELETE geofence arme la synchro du waypoint.
- **Frontend** (`/tracking`) : bouton **« 📍 Localiser maintenant »** (près de Rafraîchir) +
  **« Renvoyer la zone au téléphone »** dans la carte d'alerte de zone, avec note sur « Remote
  commands » à activer dans OwnTracks.
- **Vérifié** : 1ᵉʳ contact OwnTracks renvoie reportLocation + setWaypoints ; 2ᵉ contact renvoie `[]`.
- ⚠️ Mode HTTP : commandes appliquées **au prochain contact** du téléphone (pas instantané).
- **Zone visible par les admins** : `GET /track/devices` inclut un résumé `geofence`
  (refLat/refLng/radiusM/enabled) ; la carte affiche le cercle de zone pour **tous** les rôles
  (le super_admin garde l'édition ; les admins/users le voient en lecture).

## 2026-06-08 — Alerte de batterie faible (WhatsApp)

- **Modèle** `BatteryAlert` (par appareil : `threshold` défaut 20 %, `notifyNumber`, `message?`,
  `enabled`, état `lastBelow`/`lastNotifiedAt`). Migration `add_battery_alert`.
- **Backend** : `lib/battery-alert.ts` (`DEFAULT_BATTERY_MESSAGE` + `formatBattery` variables
  `{device}` `{battery}` `{threshold}` ; `evaluateBatteryAlert` : alerte au **passage sous le
  seuil**, ré-armement après recharge ≥ seuil+5 %, anti-spam 5 min). Branché dans l'ingestion
  OwnTracks (quand `batt` présent). Endpoints super_admin
  `GET/PUT/DELETE /track/devices/:id/battery-alert` + `…/test`.
- **Frontend** (`/tracking`) : carte « 🔋 Alerte batterie faible » (seuil, conversation cible,
  message personnalisable + sélecteur d'icônes, activer, Enregistrer/Test/Supprimer).
- **Vérifié** : 80 % = pas d'alerte ; 15 % (< seuil) = alerte WhatsApp envoyée ; endpoint test OK.

## 2026-06-08 — Alerte de zone : message personnalisable + bouton Test

- **Modèle** `Geofence.message` (modèle de message, nullable). Migration `geofence_message`.
- **Backend** : `lib/geofence.ts` expose `DEFAULT_GEOFENCE_MESSAGE` + `formatAlert(template, data)`
  avec variables `{device}` `{distance}` `{radius}` `{link}` `{lat}` `{lng}` ; utilisé par l'alerte
  réelle. PUT geofence accepte `message`. Nouvel endpoint
  `POST /track/devices/:id/geofence/test` (super_admin) : envoie un message d'exemple
  (préfixe « 🧪 (Test) ») vers la conversation choisie.
- **Frontend** (`/tracking`) : zone de texte « Message envoyé » (avec aide sur les variables) +
  bouton **« Envoyer un test »**.
- **Vérifié** : test envoyé en réel (message personnalisé avec variables + message par défaut).
- **Visibilité du point de référence** : la carte inclut désormais le point de référence dans son
  cadrage (cercle vert toujours visible) ; statut clair **« ✓ enregistré » / « ● modifié »** sous
  le point, bouton **« Voir sur la carte »**. (L'enregistrement fonctionnait déjà ; le point
  était simplement hors champ.)
- **Sélecteur d'icônes** pour le message d'alerte : bouton « 😀 Insérer une icône » → palette
  d'emojis (⚠️🚨🔔📍🚗…) insérés **au curseur** dans le textarea.

## 2026-06-08 — Journal de connexions (Super Admin)

- **Modèle** `LoginEvent` (userId?, username, success, kind `password`/`impersonation`, actor?, ip,
  userAgent, createdAt). Migration `add_login_event`. `app.set("trust proxy", true)` → IP réelle
  via X-Forwarded-For (derrière Apache).
- **Backend** : helper `lib/audit.ts` (`recordLogin`) ; enregistrement sur **login réussi**,
  **login échoué** (auth.routes) et **impersonation** (users.routes). Module `audit` :
  `GET /audit/logins` (super_admin, paginé `limit`/`offset` + `total`).
- **Frontend** : page `/admin/logs` (super_admin) — tableau date/utilisateur/type/résultat/IP/
  appareil, pagination, cartes sur mobile ; lien sidebar 📜. Page protégée (redirige les autres).
- **Vérifié** : login OK + échec enregistrés (IP, navigateur) ; admin → 403 sur `/audit/logins`.

## 2026-06-07 — Alerte de zone (geofence → WhatsApp)

- **Modèle** `Geofence` (une par appareil : `refLat/refLng`, `radiusM` défaut 50, `notifyNumber`,
  `enabled`, état `lastInside`/`lastNotifiedAt`). Migration `add_geofence`. Helper `lib/geo.ts`
  (haversine).
- **Backend** : `lib/geofence.ts` évalué **à chaque position OwnTracks** ; n'alerte qu'au
  **franchissement sortant** (inside→outside), 1ʳᵉ position = calibrage sans alerte, **anti-spam**
  5 min, re-arme quand l'appareil revient en zone. Envoi via `sendText` (WhatsApp). N'interrompt
  jamais l'ingestion. Endpoints super_admin : `GET/PUT/DELETE /track/devices/:id/geofence`.
- **Frontend** (`/tracking`, super_admin) : carte affiche le **cercle de zone** + point de
  référence ; **clic sur la carte** pour définir le point (ou « Dernière position ») ; formulaire
  rayon + numéro WhatsApp + activer. Message d'alerte : nom appareil, distance, lien OSM.
- **Vérifié de bout en bout (WhatsApp lié)** : position dans la zone = calibrage ; sortie >50 m =
  alerte WhatsApp réellement envoyée. Cible du geofence = **conversation choisie** (jid stocké tel
  quel, plus de nettoyage en chiffres) sélectionnable dans `/tracking`. `sendText` durci avec
  **retries (0/2/4 s)** : les échecs « Connection Closed » des 1ʳᵉˢ minutes venaient de
  l'instabilité du socket Baileys juste après la liaison (reconnexions), résolus depuis.

## 2026-06-07 — WhatsApp (Evolution API, gratuit/Baileys)

- **Nouveau service** : **Evolution API v2.3.7** installé (archive, sans Docker) dans
  `/var/www/leperelion.fr/evolution-api`, lancé sous **PM2 `evolution-api`** sur **localhost:4003**
  (non exposé publiquement), base PostgreSQL dédiée **`evolution`** (+ rôle), **sans Redis**
  (`CACHE_LOCAL_ENABLED`). Instance WhatsApp **`leperelion`** (Baileys) créée. API protégée par
  `apikey`. ⚠️ Baileys = WhatsApp Web non officiel → **risque de bannissement** (numéro secondaire
  recommandé).
- **Backend leperelion** : `lib/evolution.ts` (client : state, QR, logout, findChats,
  findMessages, sendText) + module `whatsapp` (`/whatsapp`) : `GET /status`, `POST /connect`
  (QR, super_admin), `DELETE /logout` (super_admin), `GET /chats`, `GET /messages?jid=`,
  `POST /send`. Capacités `whatsapp:read` (tous) / `whatsapp:write` (admin/super_admin). Config
  Evolution dans `backend/.env` (`EVOLUTION_URL|API_KEY|INSTANCE`).
- **Frontend** : page `/whatsapp` — liaison par **QR code** (super_admin), puis **messagerie**
  (liste des conversations + fil + réponse), mobile-first. Lien sidebar 💬 + carte dashboard.
- **Vérifié** : `status` (configured/connecting), `connect` renvoie un QR base64. Messagerie/envoi
  à valider une fois le numéro lié.

## 2026-06-07 — Localisation : filtre par jour + pagination

- **Filtre par jour** : champ `date` (≤ aujourd'hui) à côté des plages rapides ; charge la fenêtre
  `[jour 00:00, jour+1 00:00)`. Les boutons de plage le réinitialisent.
- **Pagination** de la liste d'historique (50/page, ‹ Précédent / Suivant ›, « Page X / Y ») —
  utile pour les journées chargées. Requête `&limit=5000`.
- **Perf carte** : la trace complète reste tracée en **polyline**, mais les **marqueurs détaillés
  ne sont rendus que pour la page courante** (+ dernière position + point ciblé), évitant de poser
  des milliers de marqueurs.

## 2026-06-07 — Localisation : liste d'historique des positions

- Page `/tracking` : ajout d'une **liste chronologique** (plus récent d'abord) des positions de la
  plage sélectionnée (par défaut **24 h**), repliable (`<details>`), avec heure + batterie +
  précision. Clic sur une ligne → **centre la carte** sur ce point (prop `focus` de `MapView`,
  marqueur rouge), et lien **↗** vers OpenStreetMap pour ce point exact. (Le backend
  `GET /track/devices/:id/points?from&to` renvoyait déjà l'historique.)

## 2026-06-07 — Bascule en modèle « compte partagé »

- **Changement de modèle** : on passe de « chacun connecte son propre Google » à **un seul compte
  partagé = celui du Super Admin**. Les admins/users voient (et modifient selon droits) l'agenda,
  les mails, les Documents et la localisation **du Super Admin**.
- **Backend** : `lib/data-owner.ts` (`getDataOwnerId` = super_admin actif le plus ancien) +
  `getOwnerGoogleClient()`. Les modules **calendar / mail / drive** utilisent désormais le client
  Google du **propriétaire** (et non plus `req.user.sub`). **Tracking** : lecture des appareils du
  propriétaire pour tout utilisateur ; **gestion d'appareils** (créer/jeton/régénérer/supprimer)
  réservée au super_admin (`config:manage`). **Connexion Google** (`/google/status|oauth/start|
  disconnect`) réservée au super_admin.
- **Frontend** : « Paramètres » retiré du menu pour les non-super_admin (et page protégée →
  redirection) ; gestion d'appareils masquée sauf super_admin sur `/tracking`.
- **Vérifié** : Julie (admin) → agenda/mail/Drive/localisation du Super Admin en 200 ;
  `/google/status`, `POST /track/devices`, `/users` → 403.

## 2026-06-07 — Impersonation (« Se connecter en tant que »)

- **Backend** : helper `lib/session.ts` (cookies de session partagés). JWT étendu d'un claim
  optionnel `imp` (id du super_admin impersonateur), propagé au refresh. `POST /users/:id/impersonate`
  (capacité `users:manage`) ouvre une session pour le compte cible avec `imp`. `POST
  /auth/stop-impersonation` revient au compte d'origine. `GET /auth/me` expose `impersonating`.
- **Frontend** : bouton **« Se connecter en tant que »** dans `/admin/users` (pour les autres
  comptes actifs), **bandeau orange** persistant en haut (« Vous voyez le site en tant que … ·
  Revenir à mon compte »). Rechargement complet pour réinitialiser l'identité (sidebar/droits).
- **Vérifié** : super_admin → impersonate admin → `/auth/me` = compte cible + `impersonating:true`,
  accès `/users` **refusé (403)** sous l'identité admin, puis `stop-impersonation` → retour
  super_admin + accès rétabli.

## 2026-06-07 — Localisation du téléphone (OwnTracks + carte)

- **Backend** : module `tracking` (`/track`). Ingestion **OwnTracks** `POST /track/owntracks/:token`
  (non-JWT, authentifié par le jeton de l'appareil dans l'URL ; ne stocke que les messages
  `_type:"location"` → `LocationPoint`, met à jour `lastSeenAt`, répond `[]`). Gestion d'appareils
  (auth requise, **propriété stricte** : chacun ne voit que les siens) : `GET/POST /track/devices`,
  `GET /track/devices/:id/token`, `POST …/regenerate`, `DELETE …`, `GET …/points?from&to`.
- **Frontend** : page `/tracking` (« Localisation ») mobile-first. **Carte Leaflet + OpenStreetMap**
  (`leaflet` + `react-leaflet`, chargée en `dynamic ssr:false`) affichant la **trace des
  déplacements** (polyline + points, dernière position en évidence) ; plages Aujourd'hui/24 h/7 j,
  rafraîchissement auto 30 s. Gestion d'appareils + **instructions de configuration OwnTracks**
  (URL avec jeton, copier, régénérer). Lien sidebar 📍, carte dashboard.
- **Vérifié** : création d'appareil → envois OwnTracks simulés (trajet) → stockage + lecture des
  points → suppression. Page `/tracking` à 200.
- ⚠️ Côté utilisateur : installer **OwnTracks** (Android), mode **Private HTTP**, coller l'URL.

## 2026-06-06 — « Documents » + arborescence sans racine

- Renommage **« Drive » → « Documents »** côté utilisateur (sidebar, carte dashboard, titre de
  page). Route inchangée (`/drive`).
- Arborescence : on affiche **directement les dossiers de la racine** (suppression du nœud
  « Mon Drive » au-dessus) via un composant `RootTree`. Le fil d'Ariane garde un point de retour
  « Documents ».

## 2026-06-06 — Drive : panneau arborescence + libellé « Supprimer »

- **Arborescence** : panneau latéral (gauche sur desktop, repliable sur mobile via « Arborescence »)
  affichant les dossiers en **arbre dépliable** (chargement à la demande, dossiers uniquement).
  Sélectionner un nœud met à jour la liste + le fil d'Ariane ; le dossier courant est surligné.
  L'arbre se rafraîchit après création/renommage/suppression de dossier (`treeKey`). Layout
  2 colonnes responsive (`md:flex-row`).
- Le bouton de suppression (liste) affiche désormais le texte **« Supprimer »** (au lieu de la
  croix rouge ✕ qui prêtait à confusion avec « fermer »).

## 2026-06-06 — Drive : correctif navigation dossiers

- Cliquer un dossier **navigue en place** (et non plus ouverture d'un nouvel onglet) : les liens
  « Télécharger »/« Ouvrir » (webViewLink, nouvel onglet) ne s'affichent plus que pour les
  **fichiers**. Ajout d'un chevron « › » sur les dossiers. Comportement type explorateur.

## 2026-06-06 — Google Drive (explorateur + gestion de fichiers)

- **Rôles** : capacités `drive:read` (tous) et `drive:write` (admin/super_admin) ajoutées à
  `permissions.ts`. **Scope** `…/auth/drive` ajouté à `GOOGLE_SCOPES` → **re-consentement requis**.
- `isAuthError` détecte désormais le **scope insuffisant** (403 `insufficientPermissions` /
  `ACCESS_TOKEN_SCOPE_INSUFFICIENT`) → 409 `reconnect` (l'UI invite à reconnecter Google).
- **Backend** : `multer` ajouté. Module `drive` (`/drive`) :
  - `GET /drive/files` (`drive:read`) — liste d'un dossier (folderId, défaut root) ou recherche (q).
  - `GET /drive/files/:id/download` (`drive:read`) — stream du fichier (export PDF pour docs Google).
  - `POST /drive/files` (`drive:write`, multipart) — upload dans un dossier.
  - `POST /drive/folders` (`drive:write`) — créer un dossier.
  - `PATCH /drive/files/:id` (`drive:write`) — renommer.
  - `DELETE /drive/files/:id` (`drive:write`) — mettre à la corbeille (trashed).
- **Frontend** : page `/drive` mobile-first — explorateur avec **fil d'Ariane**, recherche,
  icônes par type, télécharger / ouvrir dans Drive ; pour admin/super_admin : importer, nouveau
  dossier, renommer, corbeille. Lien « Drive » 🗂️ dans la sidebar, carte dashboard.
- **Vérifié** : avant re-consentement, `/drive/files` renvoie bien 409 `reconnect`.
  ⚠️ Prérequis utilisateur : activer **Drive API** dans Google Cloud + **reconnecter** Google.

## 2026-06-06 — Boîte mail (Gmail) : lecture + tri/archivage

- **Backend** : helper d'erreur Google partagé `sendGoogleError` (dans `lib/google-user.ts`).
  Module `mail` (`/mail`) :
  - `GET /mail/messages` (`mail:read`) — liste INBOX (from/subject/date/snippet/unread),
    pagination via `pageToken`/`nextPageToken` (metadata par message en parallèle).
  - `GET /mail/messages/:id` (`mail:read`) — contenu complet : entêtes, corps text/html
    (décodage base64url, parcours récursif des parts), pièces jointes listées.
  - Actions `mail:write` : `/read`, `/unread`, `/archive` (modify labels) et `/trash`.
- **Frontend** : page `/mail` mobile-first — liste (objet/expéditeur/date, non-lus en gras +
  pastille), « Charger plus », vue détail avec **HTML rendu en iframe sandboxée** (scripts
  désactivés = sûr) ou texte brut, pièces jointes listées. Actions lu/non-lu/archiver/corbeille
  visibles pour admin/super_admin. Lien « Boîte mail » ✉️ dans la sidebar, carte dashboard.
- **Vérifié** : liste (5 messages réels + pagination) et lecture d'un message (HTML extrait) OK.
  Actions d'écriture non testées sur la boîte réelle (pour ne pas la modifier).

## 2026-06-06 — Agenda : vues Semaine et Mois + navigation

- **Backend** : `GET /calendar/events` accepte désormais `timeMax` (en plus de `timeMin`) pour
  charger une **plage** précise (semaine ou mois) ; `maxResults` par défaut relevé à 250 (max 2500).
- **Frontend** (`/agenda`, mobile-first) : sélecteur de vue **Semaine / Mois**, navigation
  **‹ précédent / suivant ›** et bouton **Aujourd'hui**.
  - **Semaine** (par défaut) : les 7 jours de la semaine en cours empilés (lundi→dimanche),
    chacun listant ses RDV ; jour courant mis en évidence.
  - **Mois** : grille complète (semaines × 7 jours, lundi en tête), pastille sur les jours avec
    RDV, jours hors mois grisés ; clic sur un jour → détail des RDV sous la grille.
  - Création (bouton « + RDV » par jour, formulaire pré-rempli) et suppression conservées
    (admin/super_admin).
- **Vérifié** : filtrage par plage correct (RDV présent dans la plage du jour, absent hors plage),
  cycle création/suppression OK.

## 2026-06-06 — Agenda (Google Calendar) : lecture + création + suppression

- **Liaison Google effectuée** par Guillaume.Lion (compte christian.lion.51450@gmail.com,
  scopes calendar + gmail.modify + identité ; refresh token chiffré stocké).
- **Backend** : `lib/google-user.ts` (`getUserGoogleClient(userId)` réutilisable +
  `GoogleNotConnectedError` + détection `invalid_grant` → reconnexion). Module `calendar`
  (`/calendar`) :
  - `GET /calendar/events` (capacité `agenda:read`) — événements à venir (primary, singleEvents,
    triés par date).
  - `POST /calendar/events` (`agenda:write`) — création (summary/start/end/location/description).
  - `DELETE /calendar/events/:id` (`agenda:write`).
  - Erreurs Google traduites : 409 `not_connected` / `reconnect`, 502 sinon.
- **Frontend** : page `/agenda` mobile-first (liste groupée par jour, format fr-FR ; formulaire
  « + Nouveau RDV » et suppression visibles pour admin/super_admin ; états non-connecté /
  reconnexion avec lien vers `/settings`). Lien « Agenda » 📅 dans la sidebar, carte cliquable
  sur le dashboard.
- **Vérifié bout en bout** (vrai compte Google) : lecture 200, cycle création (201) → relecture →
  suppression (200). RDV de test nettoyé.

## 2026-06-06 — Page de configuration Super Admin (réglages des connexions)

- **Modèle** `AppSetting` (clé/valeur, `isSecret` → chiffré AES-GCM). Migration `add_app_setting`.
- **Service** `lib/config-store.ts` : lecture/écriture des réglages ; `getGoogleConfig()` fait
  primer la base sur le `.env`. `lib/google.ts` refactoré en **async** pour lire cette config
  (au lieu de lire `env` directement) ; `google.routes.ts` adapté (`await`).
- **API** `/config` (capacité `config:manage` = super_admin) : `GET /config` (secrets masqués,
  expose `clientSecretSet`), `PUT /config/google` (clientId, clientSecret optionnel — vide =
  inchangé, redirectUri). Le Client Secret n'est **jamais** renvoyé en clair.
- **Frontend** : page `/admin/config` (formulaire Google : ID client, secret, URI de redirection),
  lien « Configuration » dans la sidebar (super_admin only).
- **Vérifié** : GET/PUT super_admin OK, secret chiffré en base, non-admin → 403, masquage du
  secret, `/google/status` passe à `configured:true` une fois les valeurs saisies. Données de
  test nettoyées.
- ➡️ Conséquence : les identifiants Google se saisissent désormais dans `/admin/config`
  (plus besoin de toucher au `.env`).

## 2026-06-06 — Liaison Google OAuth (connexion / déconnexion)

- **Backend** : `lib/google.ts` (client OAuth2 `googleapis`, scopes `openid email profile` +
  `calendar` + `gmail.modify`). Module `google` monté sur `/google` :
  - `GET /google/status` (auth) — état de la liaison de l'utilisateur (configured/connected).
  - `GET /google/oauth/start` (auth) — redirige vers le consentement Google (`access_type=offline`,
    `prompt=consent`, `state` signé JWT 10 min portant l'userId).
  - `GET /google/oauth/callback` — échange le code, lit l'identité via `id_token`
    (`verifyIdToken`), **chiffre et stocke le refresh token** (`GoogleAccount`, upsert
    userId+googleSub), redirige vers `/settings?google=connected`.
  - `POST /google/disconnect` (auth) — révoque côté Google (best-effort) + supprime la liaison.
- **Frontend** : page `/settings` (Paramètres) avec carte « Compte Google » (connecter via
  navigation vers `/backend/google/oauth/start`, déconnecter, état + feedback `?google=...`),
  lien « Paramètres » ajouté à la sidebar.
- En attente des identifiants : `GOOGLE_CLIENT_ID`/`SECRET` vides → l'API renvoie
  `configured:false` et l'UI affiche « non configuré ». Redirect URI attendu :
  `https://leperelion.fr/backend/google/oauth/callback`.

## 2026-06-06 — Navigation en sidebar + mobile-first

- **Mobile-first** acté comme principe d'interface (noté dans `CLAUDE.md` et
  `04_ARCHITECTURE_TECHNIQUE.md`) : styles de base mobile, variantes `sm:`/`md:`/`lg:` ensuite.
- **Sidebar gauche** : tiroir coulissant sur mobile (bouton ☰ + voile), fixe dès `md`.
- Refonte du frontend autour d'un **layout authentifié partagé** `app/(app)/` :
  `layout.tsx` (vérifie `/auth/me`, fournit le contexte user, shell + barre mobile),
  `Sidebar.tsx`, `user-context.tsx`. Pages `dashboard`, `profile`, `admin/users` déplacées sous
  ce groupe (URLs inchangées) et simplifiées (plus de fetch `/auth/me` ni de logout dupliqués ;
  déconnexion et navigation dans la sidebar). Liste des comptes : cartes empilées sur mobile,
  tableau dès `sm`. La page de connexion `/` reste hors layout.

## 2026-06-06 — Système de rôles à 3 niveaux + interface d'administration

- **Rôles** : enum `Role` étendu à `super_admin | admin | user` (migration `add_super_admin_role`
  via `migrate diff` + `migrate deploy` ; `ALTER TYPE "Role" ADD VALUE`). Guillaume.Lion passé en
  `super_admin` (UPDATE séparé après migration).
- **Autorisation** : `backend/src/lib/permissions.ts` (matrice rôle → capacités), middlewares
  `requireRole` / `requireCapability` dans `middleware/auth.ts`. `lib/password.ts` centralise
  bcrypt (cost 12).
- **Gestion des comptes** (`/users`, super_admin only via `requireCapability('users:manage')`) :
  GET liste, POST créer, PATCH (rôle/displayName/isActive), POST reset mot de passe, DELETE.
  Garde-fous anti-verrouillage : pas de retrait/désactivation/suppression du **dernier
  super_admin**, pas d'auto-suppression.
- **Mot de passe en libre-service** : `POST /auth/password` (vérifie le mot de passe actuel).
- Script `create-user` : accepte désormais `super_admin`.
- **Frontend** : `/admin/users` (tableau de gestion des comptes, accès super_admin),
  `/profile` (changement de mot de passe), liens depuis `/dashboard` (+ affichage du rôle).
- **Vérifié** (HTTPS) : login super_admin → /users 200 ; user → /users 403 ; changement mot de
  passe 401/200 ; rétrogradation/auto-suppression du dernier super_admin → 409 ; pages 200.

## 2026-06-06 — Page de connexion + durcissement HTTPS

- **HSTS** ajouté au vhost HTTPS (`Strict-Transport-Security`) pour forcer HTTPS côté navigateur
  et éviter l'avertissement « non sécurisé » (le certificat était déjà valide ;
  `verify return code: 0`). Reload Apache après `configtest`.
- Modèle `User` : ajout de `username` (unique, identifiant de connexion), `email` rendu optionnel.
  Migration `add_username` générée via `migrate diff` (car `migrate dev` non interactif) puis
  appliquée avec `migrate deploy`.
- **Auth backend** : module `auth` (`POST /auth/login`, `/auth/refresh`, `GET /auth/me`,
  `POST /auth/logout`) — JWT access+refresh en cookies httpOnly, bcrypt, validation zod.
  Middleware `requireAuth`. Script `create-user` (les comptes sont créés par l'admin).
- **Frontend** : page d'accueil = **formulaire de connexion** (nom d'utilisateur + mot de passe)
  appelant `/backend/auth/login` ; page `/dashboard` protégée (vérifie `/auth/me`, bouton
  déconnexion). Build OK, process PM2 redémarrés.
- Décision : login **username + mot de passe** uniquement (pas de bouton « connexion Google »).
- Compte **admin** initial créé : `Guillaume.Lion` (role=admin) avec mot de passe temporaire
  transmis hors doc — à changer dès que la page profil/changement de mot de passe existera.

## 2026-06-06 — Initialisation du projet

- Audit de l'infrastructure serveur existante (à préserver) :
  - Apache2 reverse proxy + Let's Encrypt ; PostgreSQL 17 ; PM2 ; ports occupés 3000/3001/4000/4001.
  - Sites existants : woogalf.fr, planifik.fr, glamz, /var/www/html.
- Cadrage du projet avec l'utilisateur :
  - Outil personnel multi-utilisateur (cercle restreint), chacun lie son Google.
  - Fonctions : agenda/RDV (Calendar), boîte mail (Gmail), suivi de position du téléphone.
  - Stack identique aux autres sites ; ports dédiés 3002 (front) / 4002 (back) ; base dédiée.
- Création de la documentation de gestion :
  - `CLAUDE.md`, `00_VISION_PROJET.md`, `01_ROLES_ET_DROITS.md`, `02_FONCTIONNALITES_MVP.md`,
    `03_FONCTIONNALITES_V2_V3.md`, `04_ARCHITECTURE_TECHNIQUE.md`, `05_INTEGRATION_GOOGLE.md`,
    `06_MODELE_DONNEES.md`, `07_DEPLOIEMENT_INFRA.md`, `08_QUESTIONS_RESTANTES.md`,
    `AVANCEMENT.md`, `CHANGELOG.md`, `.gitignore`.
- Création de la base PostgreSQL dédiée `leperelion` + rôle `leperelion` (LOGIN, CREATEDB, sans
  superuser). Connexion testée OK. Bases existantes (woogalf/planifik/glamz) vérifiées intactes.
- Création des dossiers `backend/`, `frontend/`, `uploads/` et du `backend/.env` (chmod 600,
  gitignoré) contenant la chaîne de connexion DB.
- Décisions complémentaires : géoloc téléphone **reportée** ; Gmail **lire+gérer sans envoi**
  (`gmail.modify`) ; comptes créés par l'**admin** ; **projet Google Cloud dédié**.
  DNS vérifié (leperelion.fr + www → 51.91.251.0).
- **Backend** scaffoldé : Express 5 + TS + Prisma 7 (pattern Prisma 7 de woogalf :
  `generator prisma-client` + `prisma.config.ts`, client généré dans `src/generated/prisma`,
  adapter-pg). Schéma initial (`User`, `GoogleAccount`, `RefreshSession`, `Device`,
  `LocationPoint`) migré en base (`migrate dev --name init`). Endpoint `/health`. Secrets JWT +
  clé de chiffrement générés. Build OK.
- **Frontend** scaffoldé : Next.js 16.2.7 + React 19 + Tailwind 4 (App Router), port 3002,
  page d'accueil placeholder. Build OK.
- **PM2** : `leperelion-backend` (port 4002) et `leperelion-frontend` (port 3002) démarrés,
  stables (0 restart), `pm2 save`. Processus des autres sites non touchés.
- Fichiers vhost Apache prêts dans `deploy/` (HTTP + HTTPS).
- **Mise en ligne** : vhosts copiés dans sites-available, activés (`a2ensite`), `configtest` OK,
  reload Apache. Certificat Let's Encrypt obtenu via `certbot certonly --apache`
  (leperelion.fr + www, exp. 2026-09-04, renouvellement auto). https://leperelion.fr répond
  (200 + placeholder), `/backend/health` proxifié vers :4002 OK. Sites woogalf/planifik et
  leurs process PM2 vérifiés **intacts** après chaque reload.
