================================================================================
  ██████╗ ██╗████████╗    ██╗  ██╗      ██████╗    ██╗████████╗██╗  ██╗██╗   ██╗██████╗
 ██╔════╝ ██║╚══██╔══╝    ╚██╗██╔╝      ██╔══██╗   ██║╚══██╔══╝██║  ██║██║   ██║██╔══██╗
 ██║  ███╗██║   ██║        ╚███╔╝       ██║  ██║   ██║   ██║   ███████║██║   ██║██████╔╝
 ██║   ██║██║   ██║        ██╔██╗       ██║  ██║   ██║   ██║   ██╔══██║██║   ██║██╔══██╗
 ╚██████╔╝██║   ██║       ██╔╝ ██╗      ██████╔╝   ██║   ██║   ██║  ██║╚██████╔╝██████╔╝
  ╚═════╝ ╚═╝   ╚═╝       ╚═╝  ╚═╝      ╚═════╝    ╚═╝   ╚═╝   ╚═╝  ╚═╝ ╚═════╝ ╚═════╝
================================================================================
  GIT & GITHUB EN ÉQUIPE — APPLICATION FLASK COMPLÈTE AVEC DÉPLOIEMENT
  Guide ultra-détaillé pour GRAND DÉBUTANT
  Chaque action expliquée: POURQUOI? COMMENT? QUAND? QUI?
================================================================================

[BLACK_RIGHT-POINTING_TRIANGLE] À QUI S'ADRESSE CE GUIDE?
  Ce guide est écrit pour quelqu'un qui:
  - n'a jamais utilisé Git ou GitHub (ou très peu)
  - ne sait pas pourquoi il faudrait l'utiliser
  - veut comprendre AVANT de taper des commandes
  - travaille en équipe sur un projet Python/Flask
  - veut voir des situations réelles et concrètes

[BLACK_RIGHT-POINTING_TRIANGLE] COMMENT LIRE CE GUIDE?
  Chaque section suit ce format:
  ┌─ POURQUOI? ──────────── Pourquoi cette commande/feature existe-t-elle?
  ├─ QU'EST-CE QUE C'EST? ─ Explication claire du concept
  ├─ QUAND? ──────────────── Dans quelle situation l'utiliser?
  ├─ COMMENT? ────────────── Étapes concrètes, une par une
  └─ RÉSULTAT ATTENDU ────── Ce que vous devez voir à l'écran

[BLACK_RIGHT-POINTING_TRIANGLE] L'APPLICATION CONSTRUITE DANS CE GUIDE
  Nom: TaskManager API
  Description: Une API REST pour gérer des tâches (to-do list professionnelle)
  Technologies: Python 3.11 + Flask + PostgreSQL + Redis + Docker
  Déploiement: Serveur Ubuntu via Docker + GitHub Actions (CI/CD automatique)

  Ce que l'API permet de faire:
  - Créer une tâche         -> POST   /tasks
  - Lister les tâches       -> GET    /tasks
  - Voir une tâche          -> GET    /tasks/<id>
  - Modifier une tâche      -> PUT    /tasks/<id>
  - Supprimer une tâche     -> DELETE /tasks/<id>
  - Filtrer les tâches      -> GET    /tasks?done=false&priority=high
  - S'enregistrer           -> POST   /auth/register
  - Se connecter            -> POST   /auth/login
  - Vérifier la santé API   -> GET    /health

[BLACK_RIGHT-POINTING_TRIANGLE] L'ÉQUIPE
  Alice  — Lead développeuse, responsable architecture, Git, déploiement
           Ordinateur: MacBook Pro, macOS
           Rôle Git: configure tout, fait les merges dans main, gère les releases

  Bob    — Développeur backend, fonctionnalités métier
           Ordinateur: PC fixe, Ubuntu 22.04
           Rôle Git: développe des features, ouvre des Pull Requests

  Claire — Développeuse backend, spécialisée tests et qualité
           Ordinateur: PC portable, Windows 11
           Rôle Git: développe des features, écrit les tests, fait les reviews

================================================================================
PARTIE 0 — COMPRENDRE GIT AVANT DE TOUCHER UN TERMINAL
================================================================================

Avant de taper la moindre commande, il faut comprendre CE QUE FAIT Git.
Beaucoup de débutants tapent des commandes sans savoir ce qui se passe.
Résultat: ils cassent des choses et ne savent pas comment réparer.

────────────────────────────────────────────────────────────────────────────────
0.1 — LA MÉTAPHORE: Git est une machine à remonter le temps pour votre code
────────────────────────────────────────────────────────────────────────────────

  IMAGINEZ CETTE SITUATION (sans Git):
  ──────────────────────────────────────
  Lundi: Bob écrit 200 lignes de code pour la fonctionnalité de filtrage.
  Mardi: Bob continue, arrive à 350 lignes.
  Mercredi: Bob réalise que son approche est mauvaise. Il veut revenir à lundi.
  PROBLÈME: impossible. Le code de lundi n'existe plus. Bob recommence à zéro.

  AVEC GIT:
  ──────────
  Lundi: Bob sauvegarde (commit) son travail -> photo n°1
  Mardi: Bob sauvegarde (commit) -> photo n°2
  Mercredi: Bob tape: git checkout [code de lundi] -> revient à la photo n°1
  En 1 commande, le code de lundi réapparaît, intact.

  GIT = appareil photo automatique pour votre code.
  Chaque commit = une photo datée, étiquetée, indestructible.
  L'historique = l'album photo complet du projet.

  POURQUOI C'EST CRUCIAL EN ÉQUIPE?
  ───────────────────────────────────
  Sans Git, si Alice et Bob modifient le même fichier:
  -> Celui qui sauvegarde en dernier écrase le travail de l'autre.
  -> Pas de trace de qui a fait quoi.
  -> Pas moyen de voir les différences entre deux versions.

  Avec Git, Alice et Bob peuvent modifier le même fichier:
  -> Git détecte les différences et propose une fusion (merge).
  -> L'historique montre QUI a fait QUOI et QUAND.
  -> Si la fusion crée un conflit, Git signale exactement où et pourquoi.

────────────────────────────────────────────────────────────────────────────────
0.2 — LES 3 ZONES DE GIT: où vivent vos fichiers?
────────────────────────────────────────────────────────────────────────────────

  POURQUOI 3 ZONES?
  ──────────────────
  Git ne sauvegarde pas tout automatiquement à chaque modification.
  Il vous donne le contrôle: vous choisissez quoi sauvegarder et quand.
  Pour cela, il utilise 3 zones distinctes.

  LES 3 ZONES:
  ─────────────

  ┌──────────────────────────────────────────────────────────────────────────┐
  │                                                                          │
  │  ZONE 1: Working Directory (répertoire de travail)                      │
  │  ─────────────────────────────────────────────────                      │
  │  C'est votre dossier de projet tel que vous le voyez dans               │
  │  votre explorateur de fichiers ou votre éditeur de code.                │
  │                                                                          │
  │  Tout ce que vous tapez, modifiez, créez EST ICI.                       │
  │  Git voit les changements mais ne les sauvegarde PAS encore.            │
  │                                                                          │
  │  Analogie: le bureau de votre bureau physique.                          │
  │  Vous pouvez poser des feuilles, les modifier, les jeter.               │
  │  Rien n'est archivé tant que vous ne le décidez pas.                   │
  │                                                                          │
  ├──────────────────────────────────────────────────────────────────────────┤
  │                                                                          │
  │  ZONE 2: Staging Area / Index (zone de préparation)                     │
  │  ────────────────────────────────────────────────────                   │
  │  C'est une zone intermédiaire: les fichiers que vous avez               │
  │  "réservés" pour le prochain commit.                                    │
  │                                                                          │
  │  Commande pour y mettre des fichiers: git add <fichier>                 │
  │                                                                          │
  │  Analogie: une enveloppe sur votre bureau.                              │
  │  Vous glissez les feuilles dedans une par une.                          │
  │  L'enveloppe n'est pas encore envoyée — vous pouvez encore changer.    │
  │                                                                          │
  ├──────────────────────────────────────────────────────────────────────────┤
  │                                                                          │
  │  ZONE 3: Repository (dépôt Git — le dossier .git/)                     │
  │  ────────────────────────────────────────────────────                   │
  │  C'est la base de données de tous vos commits.                          │
  │  Situé dans le dossier caché .git/ à la racine de votre projet.        │
  │  Immuable: une fois committé, un état est conservé pour toujours.      │
  │                                                                          │
  │  Commande pour y déposer l'enveloppe: git commit -m "message"          │
  │                                                                          │
  │  Analogie: l'armoire à archives de votre bureau.                        │
  │  Chaque enveloppe scellée a une date, votre nom, un titre.             │
  │  Vous pouvez en ressortir n'importe laquelle à tout moment.            │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  LE FLUX COMPLET:
  ────────────────
  [Vous modifiez routes.py]
         v
  Working Directory: routes.py est "modified" (modifié mais non sauvegardé)
         v
  git add routes.py
         v
  Staging Area: routes.py est "staged" (prêt pour le prochain commit)
         v
  git commit -m "feat(routes): add task filtering"
         v
  Repository: commit créé, état sauvegardé pour l'éternité

  COMMANDES POUR VOIR L'ÉTAT DE CHAQUE ZONE:
  ────────────────────────────────────────────
  git status         <- Montre ce qui est dans chaque zone
  git diff           <- Différences dans le Working Directory (non stagé)
  git diff --staged  <- Différences dans la Staging Area (stagé, pas encore commité)
  git log            <- Liste des commits dans le Repository

────────────────────────────────────────────────────────────────────────────────
0.3 — LOCAL vs REMOTE: votre machine et GitHub
────────────────────────────────────────────────────────────────────────────────

  POURQUOI CET AUTRE NIVEAU?
  ───────────────────────────
  Le Repository local (sur votre machine) est invisible pour vos collègues.
  GitHub est le serveur partagé où tout le monde synchronise son travail.

  ┌─────────────────────────────────────────────────────────────────────┐
  │  MACHINE D'ALICE           │  GITHUB (serveur)  │  MACHINE DE BOB  │
  │  ─────────────────         │  ──────────────     │  ─────────────── │
  │  Working Directory         │                    │  Working Dir     │
  │  Staging Area         ──push──[BLACK_RIGHT-POINTING_TRIANGLE]  origin/main  [BLACK_LEFT-POINTING_TRIANGLE]──pull──  Staging  │
  │  Repository local          │  origin/develop    │  Repository local│
  │  (.git/)                   │  origin/feature/.. │  (.git/)         │
  └─────────────────────────────────────────────────────────────────────┘

  LOCAL  = votre machine (Alice, Bob ou Claire individuellement)
  REMOTE = GitHub = serveur central = "origin" dans Git

  git push -> Envoyer vos commits locaux vers GitHub
  git pull -> Récupérer les commits de GitHub vers votre machine
  git fetch -> Voir ce qui a changé sur GitHub SANS l'intégrer encore

────────────────────────────────────────────────────────────────────────────────
0.4 — LES BRANCHES: travailler en parallèle sans se gêner
────────────────────────────────────────────────────────────────────────────────

  POURQUOI LES BRANCHES?
  ───────────────────────
  Imaginez: Bob travaille sur la fonctionnalité de filtrage (2 semaines de travail).
  Claire veut corriger un bug urgent dans les routes (1 jour).

  SANS BRANCHES: Claire doit attendre Bob? Bob doit arrêter pour Claire?
  -> Impossible de travailler en parallèle proprement.

  AVEC BRANCHES:
  Bob crée -> feature/filtrage (sa propre "ligne du temps")
  Claire crée -> fix/bug-routes (sa propre "ligne du temps")
  Elles coexistent, ne se gênent pas. Quand chacun a fini, on fusionne.

  SCHÉMA VISUEL DES BRANCHES:
  ─────────────────────────────

  main      [BLACK_CIRCLE]────────────────────────────────────────────[BLACK_CIRCLE]
            │                                            ^
  develop   [BLACK_CIRCLE]──────[BLACK_CIRCLE]──────────────[BLACK_CIRCLE]──────[BLACK_CIRCLE]──────────────[BLACK_CIRCLE]
                   ^              ^      ^
  feature/A        [BLACK_CIRCLE]──[BLACK_CIRCLE]──[BLACK_CIRCLE]────────┘      │
                                         │
  feature/B                [BLACK_CIRCLE]──[BLACK_CIRCLE]──[BLACK_CIRCLE]───────┘

  Chaque [BLACK_CIRCLE] est un commit.
  Les branches partent d'un point commun et se rejoignent par un merge.

  NOS BRANCHES ET LEURS RÔLES:
  ──────────────────────────────
  main          PRODUCTION. Toujours stable. Code qui tourne sur le serveur.
                -> Jamais de commit direct. Seulement des merges depuis develop.
                -> Chaque merge = une nouvelle version déployée.

  develop       INTÉGRATION. Zone de test collective.
                -> Les features finies y arrivent via PR.
                -> CI/CD tourne les tests à chaque push.
                -> Quand stable: mergé dans main pour déploiement.

  feature/N-nom DÉVELOPPEMENT D'UNE FEATURE.
                -> Créée depuis develop par Bob ou Claire.
                -> Nommée avec le numéro de l'issue: feature/5-filtrage-taches
                -> Fusionnée dans develop via Pull Request (avec code review).

  fix/N-nom     CORRECTION D'UN BUG NON URGENT.
                -> Créée depuis develop.
                -> Même cycle que feature: PR -> review -> merge dans develop.

  hotfix/N-nom  CORRECTION D'UN BUG URGENT EN PRODUCTION.
                -> Créée depuis MAIN (pas develop!).
                -> Mergée dans main ET dans develop.
                -> Génère un tag de version patch (1.0.0 -> 1.0.1).

  release/x.x.x PRÉPARATION D'UNE RELEASE.
                -> Créée depuis develop quand la version est prête.
                -> Derniers ajustements (version bump, CHANGELOG).
                -> Mergée dans main et dans develop.

================================================================================
PARTIE 1 — ALICE CONFIGURE TOUT (JOUR 1, MATIN)
================================================================================

Alice arrive au bureau le lundi matin. Projet: zéro. Elle doit tout mettre
en place pour que Bob et Claire puissent travailler dès l'après-midi.

Plan d'Alice pour la matinée:
  9h00 -> Installer Git, configurer son identité
  9h15 -> Configurer l'authentification SSH avec GitHub
  9h30 -> Créer le projet Flask et la structure de fichiers
  9h45 -> Créer le dépôt GitHub et faire le premier push
  10h00 -> Configurer les protections de branches
  10h15 -> Créer les labels, milestones, templates
  10h30 -> Inviter Bob et Claire comme collaborateurs
  10h45 -> Envoyer les instructions à Bob et Claire par Slack

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 — ALICE INSTALLE GIT
────────────────────────────────────────────────────────────────────────────────

  POURQUOI INSTALLER GIT?
  ────────────────────────
  Git est un programme qui s'installe sur votre machine, comme Word ou Chrome.
  Il n'est pas installé par défaut sur tous les systèmes.
  Sans Git installé, aucune commande git ne fonctionnera.

  QUAND LE FAIRE?
  ───────────────
  Une seule fois, la toute première fois que vous utilisez Git sur une machine.
  Si vous changez de machine: recommencer.

  COMMENT? — SUR MAC (machine d'Alice):
  ──────────────────────────────────────
  # Homebrew est un gestionnaire de paquets pour Mac.
  # Si pas installé: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

  brew install git

  # Résultat attendu: plusieurs lignes de téléchargement, puis:
  # ==> git 2.43.0 is already installed and up-to-date.
  # Ou: ==> Installing git ... [OK]

  COMMENT? — SUR UBUNTU (machine de Bob):
  ─────────────────────────────────────────
  sudo apt-get update && sudo apt-get install git -y

  COMMENT? — SUR WINDOWS (machine de Claire):
  ─────────────────────────────────────────────
  # Télécharger l'installeur: https://git-scm.com/download/win
  # Double-cliquer sur le .exe, tout laisser par défaut SAUF:
  # -> "Adjusting your PATH": choisir "Git from the command line and also from 3rd-party software"
  # -> "Choosing the default editor": choisir Visual Studio Code (si installé)
  # -> "Configuring line ending conversions": choisir "Checkout Windows-style, commit Unix-style"
  #   (très important pour éviter les conflits de fins de ligne avec Mac/Linux)
  # Cliquer "Install" puis "Finish"

  VÉRIFICATION (sur les 3 machines):
  ─────────────────────────────────────
  git --version

  CE QUE VOUS DEVEZ VOIR:
  ────────────────────────
  git version 2.43.0
  (Le numéro peut différer, l'important est qu'une version s'affiche)

  SI VOUS VOYEZ "command not found" OU "git is not recognized":
  ──────────────────────────────────────────────────────────────
  -> Git n'est pas installé correctement.
  -> Mac: vérifier que Homebrew a terminé sans erreur.
  -> Ubuntu: relancer la commande apt-get.
  -> Windows: fermer et rouvrir PowerShell/Git Bash après l'installation.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 — ALICE CONFIGURE SON IDENTITÉ GIT
────────────────────────────────────────────────────────────────────────────────

  POURQUOI CONFIGURER UNE IDENTITÉ?
  ───────────────────────────────────
  Chaque commit Git porte une signature: "Alice Martin <alice@equipe.com>".
  Sans configuration, Git ne sait pas qui vous êtes et refuse de committer.
  Sur GitHub, cette identité permet d'associer les commits au bon profil.

  ATTENTION: si l'email ne correspond pas à votre email GitHub,
  vos commits n'apparaîtront pas dans vos statistiques GitHub.

  QUAND LE FAIRE?
  ───────────────
  Une seule fois par machine, juste après l'installation de Git.
  L'option --global signifie "pour tous les projets sur cette machine".
  Sans --global, la config ne s'appliquerait qu'au projet courant.

  COMMENT? — ALICE DANS SON TERMINAL MAC:
  ─────────────────────────────────────────

  # --- Identité ---
  # Votre prénom et nom tels qu'ils apparaîtront dans chaque commit
  git config --global user.name "Alice Martin"

  # L'email DOIT correspondre à votre email de compte GitHub
  git config --global user.email "alice@equipe.com"

  # --- Éditeur de texte ---
  # Quand Git ouvre un éditeur (pour les messages de commit longs, merges...),
  # quel programme utiliser?
  # nano = simple pour débutant (Ctrl+X pour quitter, Ctrl+O pour sauvegarder)
  # vim  = puissant mais complexe (:q pour quitter, :wq pour sauvegarder et quitter)
  # code --wait = Visual Studio Code (--wait = attendre que VS Code ferme l'onglet)
  git config --global core.editor "nano"

  # --- Stratégie de fusion lors du pull ---
  # Quand vous faites git pull et qu'il y a des commits locaux ET distants,
  # comment les combiner?
  # rebase = replacer vos commits au-dessus des commits distants (historique plus propre)
  # merge  = créer un commit de merge (historique exact mais "bruité")
  # L'équipe choisit rebase pour un historique linéaire et lisible.
  git config --global pull.rebase true

  # --- Nom de la branche principale ---
  # Par convention moderne, la branche principale s'appelle "main" (pas "master").
  # Ce paramètre assure que git init crée une branche "main" par défaut.
  git config --global init.defaultBranch main

  # --- Fins de ligne ---
  # Mac et Linux utilisent LF (\n) pour terminer les lignes.
  # Windows utilise CRLF (\r\n).
  # Sans ce paramètre, Git pourrait signaler des "conflits" sur des fichiers
  # qui n'ont pas réellement changé (juste les fins de ligne).
  # input = convertir CRLF -> LF lors des commits (ne rien changer au checkout)
  git config --global core.autocrlf input    # Pour Mac et Linux

  # --- Couleurs dans le terminal ---
  # Affiche les modifications en rouge/vert, les branches en couleur, etc.
  git config --global color.ui auto

  # --- Aliases (raccourcis de commandes) ---
  # Au lieu de taper "git status", vous tapez "git st"
  # Au lieu de "git checkout", vous tapez "git co"
  # Ces alias font gagner beaucoup de temps au quotidien.
  git config --global alias.st "status"
  git config --global alias.co "checkout"
  git config --global alias.br "branch"
  git config --global alias.lg "log --oneline --graph --decorate --all"
  git config --global alias.last "log -1 HEAD --stat"
  git config --global alias.unstage "reset HEAD --"
  git config --global alias.wip "commit -am 'WIP: work in progress'"
  git config --global alias.aliases "config --get-regexp alias"
  git config --global alias.pushf "push --force-with-lease"

  VÉRIFIER QUE LA CONFIGURATION EST CORRECTE:
  ─────────────────────────────────────────────
  # Voir toute la configuration:
  git config --list

  CE QUE VOUS DEVEZ VOIR (extrait):
  ───────────────────────────────────
  user.name=Alice Martin
  user.email=alice@equipe.com
  core.editor=nano
  pull.rebase=true
  init.defaultbranch=main
  color.ui=auto
  alias.st=status
  alias.co=checkout
  alias.br=branch
  alias.lg=log --oneline --graph --decorate --all

  # Voir où est enregistrée chaque config (fichier système, global ou local):
  git config --list --show-origin

  # Voir le fichier de config directement:
  cat ~/.gitconfig

  # Voir une valeur précise:
  git config user.name
  # -> Alice Martin

  SI VOUS AVEZ FAIT UNE ERREUR:
  ───────────────────────────────
  # Modifier une valeur (même commande, nouvelle valeur):
  git config --global user.email "alice-correct@equipe.com"

  # Supprimer une valeur:
  git config --global --unset alias.test

  # Éditer le fichier directement:
  nano ~/.gitconfig

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 — ALICE CONFIGURE L'AUTHENTIFICATION SSH AVEC GITHUB
────────────────────────────────────────────────────────────────────────────────

  POURQUOI SSH ET PAS UN MOT DE PASSE?
  ──────────────────────────────────────
  GitHub a supprimé l'authentification par mot de passe pour git push/pull.
  Il faut utiliser soit SSH, soit un token HTTPS.

  SSH est recommandé car:
  -> Aucun mot de passe à taper à chaque push/pull
  -> Plus sécurisé qu'un token HTTPS stocké en clair
  -> Une fois configuré: totalement transparent (automatique)

  COMMENT FONCTIONNE SSH?
  ────────────────────────
  SSH utilise une paire de clés mathématiquement liées:
  - Clé PRIVÉE: reste sur votre machine, jamais partagée (comme votre PIN bancaire)
  - Clé PUBLIQUE: donnée à GitHub (comme le boîtier de code d'un immeuble)

  Quand vous vous connectez: votre machine prouve qu'elle a la clé privée
  qui correspond à la clé publique enregistrée sur GitHub.
  Personne ne peut usurper votre identité sans votre clé privée.

  QUAND LE FAIRE?
  ───────────────
  Une fois par machine, avant le premier push vers GitHub.

  ÉTAPE A — GÉNÉRER LA PAIRE DE CLÉS SSH:
  ─────────────────────────────────────────
  # ED25519 = algorithme moderne, plus sécurisé et plus rapide que RSA
  # -C = commentaire pour identifier la clé (utilisez votre email GitHub)
  # -f = chemin du fichier de clé (on nomme explicitement pour GitHub)
  ssh-keygen -t ed25519 -C "alice@equipe.com" -f ~/.ssh/id_ed25519_github

  CE QUE VOUS VOYEZ:
  ───────────────────
  Generating public/private ed25519 key pair.
  Enter passphrase (empty for no passphrase):

  # La passphrase est un mot de passe LOCAL qui protège votre clé privée.
  # Si quelqu'un vole votre clé privée, il ne peut pas l'utiliser sans la passphrase.
  # Pour un poste de développement personnel: vous pouvez mettre une passphrase simple.
  # Taper votre passphrase (rien n'apparaît à l'écran, c'est normal) puis Entrée.
  # Répéter la passphrase pour confirmation.

  Your identification has been saved in /home/alice/.ssh/id_ed25519_github
  Your public key has been saved in /home/alice/.ssh/id_ed25519_github.pub
  The key fingerprint is:
  SHA256:AbCdEfGhIjKlMnOpQrStUvWxYz alice@equipe.com

  # Deux fichiers créés:
  ls -la ~/.ssh/id_ed25519_github*
  # -> -rw-------  1 alice alice 411 jan 10 09:15 /home/alice/.ssh/id_ed25519_github
  # -> -rw-r--r--  1 alice alice 100 jan 10 09:15 /home/alice/.ssh/id_ed25519_github.pub
  #
  # id_ed25519_github     = clé PRIVÉE (droits 600 = seulement vous pouvez lire)
  # id_ed25519_github.pub = clé PUBLIQUE (droits 644 = lisible par tous, c'est OK)

  ÉTAPE B — ACTIVER L'AGENT SSH:
  ────────────────────────────────
  # L'agent SSH mémorise votre clé déverrouillée en RAM.
  # Sans agent: vous devrez taper la passphrase à chaque push/pull.
  # Avec agent: vous la tapez une seule fois par session.

  # Démarrer l'agent SSH:
  eval "$(ssh-agent -s)"

  CE QUE VOUS VOYEZ:
  ───────────────────
  Agent pid 23456

  # Ajouter votre clé à l'agent (il vous demande la passphrase une fois):
  ssh-add ~/.ssh/id_ed25519_github

  # Sur Mac: ajouter aussi au Trousseau macOS (persiste même après redémarrage):
  # ssh-add --apple-use-keychain ~/.ssh/id_ed25519_github

  ÉTAPE C — CONFIGURER ~/.ssh/config:
  ─────────────────────────────────────
  # Ce fichier dit à SSH: "pour github.com, utilise cette clé spécifique"
  # Sans ce fichier, SSH ne saurait pas quelle clé utiliser parmi toutes les vôtres.

  nano ~/.ssh/config

  # Contenu à taper:
  Host github.com
      HostName github.com
      User git
      IdentityFile ~/.ssh/id_ed25519_github
      AddKeysToAgent yes
      # Sur Mac, décommenter la ligne suivante pour le Trousseau:
      # UseKeychain yes

  # Sauvegarder: Ctrl+O puis Entrée, puis Ctrl+X pour quitter nano.

  # Les droits du fichier config doivent être 600 (sinon SSH refuse de l'utiliser):
  chmod 600 ~/.ssh/config

  ÉTAPE D — AJOUTER LA CLÉ PUBLIQUE SUR GITHUB:
  ────────────────────────────────────────────────
  # Afficher la clé publique (le fichier .pub, PAS le fichier sans extension):
  cat ~/.ssh/id_ed25519_github.pub

  CE QUE VOUS VOYEZ:
  ───────────────────
  ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIj alice@equipe.com

  # Copier TOUTE cette ligne (de "ssh-ed25519" jusqu'à votre email).

  # Sur GitHub (interface web):
  1. Cliquer sur votre avatar (coin supérieur droit)
  2. Cliquer "Settings"
  3. Dans le menu gauche: "SSH and GPG keys"
  4. Cliquer "New SSH key" (bouton vert)
  5. Title: "MacBook Pro Alice - 2024"
     (Un nom pour identifier cette clé — utile si vous avez plusieurs machines)
  6. Key type: "Authentication Key" (laisser par défaut)
  7. Key: coller la ligne copiée
  8. Cliquer "Add SSH key"
  9. Saisir votre mot de passe GitHub si demandé.

  ÉTAPE E — TESTER LA CONNEXION:
  ────────────────────────────────
  # Ce test vérifie que SSH peut contacter GitHub avec votre clé:
  ssh -T git@github.com

  CE QUE VOUS DEVEZ VOIR:
  ────────────────────────
  Hi alice-dev! You've successfully authenticated,
  but GitHub does not provide shell access.

  # "alice-dev" = votre nom d'utilisateur GitHub. C'est normal de voir ce message.
  # GitHub confirme: votre clé est reconnue, vous êtes bien alice-dev.

  SI VOUS VOYEZ "Permission denied (publickey)":
  ─────────────────────────────────────────────────
  -> La clé n'est pas reconnue. Vérifier:
  1. Avez-vous bien copié TOUTE la clé publique (sans coupure)?
  2. Est-ce que l'agent SSH a votre clé? -> ssh-add -l (lister les clés chargées)
  3. Est-ce que ~/.ssh/config pointe vers le bon fichier?
  4. Pour voir le détail de la connexion: ssh -vT git@github.com (mode verbose)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 — ALICE CRÉE L'APPLICATION FLASK
────────────────────────────────────────────────────────────────────────────────

  POURQUOI CRÉER L'APPLICATION AVANT LE DÉPÔT GIT?
  ──────────────────────────────────────────────────
  Git versionne du code qui EXISTE déjà. Il faut d'abord avoir quelque chose
  à versionner. Alice crée la structure de base du projet Flask, puis l'ajoutera
  à Git. C'est le point de départ pour toute l'équipe.

  ALICE CRÉE LA STRUCTURE DU PROJET:
  ─────────────────────────────────────
  # Créer le dossier racine du projet:
  mkdir taskmanager
  cd taskmanager

  # Créer l'environnement virtuel Python:
  # (isole les dépendances du projet des autres projets Python sur la machine)
  python3 -m venv venv

  # Activer l'environnement virtuel:
  source venv/bin/activate    # Mac/Linux
  # venv\Scripts\activate     # Windows

  # L'invite de commande change: (venv) alice@mac taskmanager $
  # Ce (venv) indique que l'environnement virtuel est actif.

  # Installer Flask et les dépendances:
  pip install flask flask-sqlalchemy psycopg2-binary redis flask-caching \
              gunicorn python-json-logger

  # Générer requirements.txt (liste figée des dépendances):
  pip freeze > requirements.txt

  # Installer les dépendances de développement séparément:
  pip install pytest pytest-cov pytest-flask black flake8
  pip freeze > requirements-dev.txt

  # Créer la structure de dossiers:
  mkdir -p app tests docker scripts nginx .github/workflows .github/ISSUE_TEMPLATE

  ALICE ÉCRIT LES FICHIERS DE L'APPLICATION:
  ────────────────────────────────────────────

  ── config.py ──────────────────────────────────────────────────────────────
  # Configuration de l'application Flask selon l'environnement

  import os

  class Config:
      """Configuration de base commune à tous les environnements."""
      SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-change-in-prod')
      SQLALCHEMY_TRACK_MODIFICATIONS = False
      JSON_SORT_KEYS = False
      APP_VERSION = os.environ.get('APP_VERSION', '1.0.0')

  class DevelopmentConfig(Config):
      """Configuration pour le développement local."""
      DEBUG = True
      SQLALCHEMY_DATABASE_URI = os.environ.get(
          'DATABASE_URL',
          'sqlite:///taskmanager_dev.db'   # SQLite par défaut en dev
      )
      REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')

  class TestingConfig(Config):
      """Configuration pour les tests automatisés."""
      TESTING = True
      SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'   # Base en RAM, très rapide
      WTF_CSRF_ENABLED = False

  class ProductionConfig(Config):
      """Configuration pour la production."""
      DEBUG = False
      SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
      REDIS_URL = os.environ.get('REDIS_URL')

      if not SQLALCHEMY_DATABASE_URI:
          raise ValueError("DATABASE_URL must be set in production!")

  # Dictionnaire pour sélectionner la config via variable d'environnement:
  config = {
      'development': DevelopmentConfig,
      'testing':     TestingConfig,
      'production':  ProductionConfig,
      'default':     DevelopmentConfig
  }

  ── app/__init__.py ─────────────────────────────────────────────────────────
  # Factory Flask: fonction qui crée et configure l'application

  from flask import Flask
  from flask_sqlalchemy import SQLAlchemy

  db = SQLAlchemy()

  def create_app(config_name=None):
      """Crée et configure l'application Flask.

      Args:
          config_name: 'development', 'testing' ou 'production'.
                       Si None, utilise la variable d'environnement FLASK_ENV.
      """
      import os
      from config import config

      if config_name is None:
          config_name = os.environ.get('FLASK_ENV', 'development')

      app = Flask(__name__)
      app.config.from_object(config[config_name])

      # Initialiser les extensions Flask:
      db.init_app(app)

      # Enregistrer les blueprints (groupes de routes):
      from app.routes import tasks_bp, auth_bp
      app.register_blueprint(tasks_bp)
      app.register_blueprint(auth_bp, url_prefix='/auth')

      return app

  ── app/models.py ───────────────────────────────────────────────────────────
  # Modèles de base de données SQLAlchemy

  from datetime import datetime
  from app import db

  class User(db.Model):
      """Utilisateur de l'application."""
      __tablename__ = 'users'

      id         = db.Column(db.Integer, primary_key=True)
      username   = db.Column(db.String(80),  unique=True, nullable=False)
      email      = db.Column(db.String(120), unique=True, nullable=False)
      password   = db.Column(db.String(256), nullable=False)  # Hashé avec bcrypt
      created_at = db.Column(db.DateTime, default=datetime.utcnow)
      tasks      = db.relationship('Task', backref='owner', lazy=True)

      def to_dict(self):
          return {
              'id':         self.id,
              'username':   self.username,
              'email':      self.email,
              'created_at': self.created_at.isoformat() + 'Z',
          }

  class Task(db.Model):
      """Tâche appartenant à un utilisateur."""
      __tablename__ = 'tasks'

      PRIORITY_VALUES = ('low', 'medium', 'high')

      id          = db.Column(db.Integer, primary_key=True)
      title       = db.Column(db.String(200), nullable=False)
      description = db.Column(db.Text,        nullable=True)
      done        = db.Column(db.Boolean,     default=False,    nullable=False)
      priority    = db.Column(db.String(10),  default='medium', nullable=False)
      tags        = db.Column(db.JSON,        default=list)
      user_id     = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
      created_at  = db.Column(db.DateTime, default=datetime.utcnow)
      updated_at  = db.Column(db.DateTime, default=datetime.utcnow,
                               onupdate=datetime.utcnow)

      def to_dict(self):
          return {
              'id':          self.id,
              'title':       self.title,
              'description': self.description,
              'done':        self.done,
              'priority':    self.priority,
              'tags':        self.tags or [],
              'user_id':     self.user_id,
              'created_at':  self.created_at.isoformat() + 'Z',
              'updated_at':  self.updated_at.isoformat() + 'Z',
          }

  ── app/routes.py ───────────────────────────────────────────────────────────
  # Routes Flask pour les tâches et l'authentification

  from flask import Blueprint, jsonify, request
  from datetime import datetime
  from app import db
  from app.models import Task, User

  tasks_bp = Blueprint('tasks', __name__)
  auth_bp  = Blueprint('auth', __name__)

  # ─── TÂCHES ──────────────────────────────────────────────────────────────

  @tasks_bp.route('/health', methods=['GET'])
  def health_check():
      """Endpoint de santé pour Docker et monitoring."""
      try:
          db.session.execute(db.text('SELECT 1'))
          db_status = 'ok'
      except Exception as e:
          db_status = f'error: {e}'

      return jsonify({
          'status':    'healthy' if db_status == 'ok' else 'unhealthy',
          'timestamp': datetime.utcnow().isoformat() + 'Z',
          'database':  db_status,
      }), 200 if db_status == 'ok' else 503

  @tasks_bp.route('/tasks', methods=['GET'])
  def get_tasks():
      """
      Lister les tâches avec filtres optionnels.
      Query params:
        done      = true/false
        priority  = low/medium/high
        search    = texte à rechercher dans le titre
        page      = numéro de page (défaut: 1)
        per_page  = nombre par page (défaut: 20, max: 100)
      """
      query = Task.query

      done_param = request.args.get('done')
      if done_param is not None:
          query = query.filter(Task.done == (done_param.lower() == 'true'))

      priority = request.args.get('priority')
      if priority and priority in Task.PRIORITY_VALUES:
          query = query.filter(Task.priority == priority)

      search = request.args.get('search')
      if search:
          query = query.filter(Task.title.ilike(f'%{search}%'))

      page     = request.args.get('page', 1, type=int)
      per_page = min(request.args.get('per_page', 20, type=int), 100)
      result   = query.order_by(Task.created_at.desc()).paginate(
                     page=page, per_page=per_page, error_out=False)

      return jsonify({
          'tasks':    [t.to_dict() for t in result.items],
          'total':    result.total,
          'page':     result.page,
          'per_page': result.per_page,
          'pages':    result.pages,
      })

  @tasks_bp.route('/tasks', methods=['POST'])
  def create_task():
      """Créer une nouvelle tâche."""
      data = request.get_json()
      if not data:
          return jsonify({'error': 'Request body must be JSON'}), 400
      if not data.get('title'):
          return jsonify({'error': 'title is required'}), 400
      if not isinstance(data.get('description', ''), str):
          return jsonify({'error': 'description must be a string'}), 400
      priority = data.get('priority', 'medium')
      if priority not in Task.PRIORITY_VALUES:
          return jsonify({'error': f'priority must be one of {Task.PRIORITY_VALUES}'}), 400

      task = Task(
          title=data['title'],
          description=data.get('description'),
          priority=priority,
          tags=data.get('tags', []),
          user_id=1,   # Simplifié pour l'instant (auth ajoutée en v1.1)
      )
      db.session.add(task)
      db.session.commit()
      return jsonify(task.to_dict()), 201

  @tasks_bp.route('/tasks/<int:task_id>', methods=['GET'])
  def get_task(task_id):
      """Récupérer une tâche par son ID."""
      task = Task.query.get_or_404(task_id,
             description=f'Task {task_id} not found')
      return jsonify(task.to_dict())

  @tasks_bp.route('/tasks/<int:task_id>', methods=['PUT'])
  def update_task(task_id):
      """Modifier une tâche existante."""
      task = Task.query.get_or_404(task_id)
      data = request.get_json()
      if not data:
          return jsonify({'error': 'Request body must be JSON'}), 400

      if 'title' in data:
          task.title = data['title']
      if 'description' in data:
          task.description = data['description']
      if 'done' in data:
          task.done = bool(data['done'])
      if 'priority' in data:
          if data['priority'] not in Task.PRIORITY_VALUES:
              return jsonify({'error': 'Invalid priority'}), 400
          task.priority = data['priority']
      if 'tags' in data:
          task.tags = data['tags']

      db.session.commit()
      return jsonify(task.to_dict())

  @tasks_bp.route('/tasks/<int:task_id>', methods=['DELETE'])
  def delete_task(task_id):
      """Supprimer une tâche."""
      task = Task.query.get_or_404(task_id)
      db.session.delete(task)
      db.session.commit()
      return '', 204

  # ─── AUTHENTIFICATION ────────────────────────────────────────────────────
  # (Implémentation complète en v1.1 — squelette pour v1.0)

  @auth_bp.route('/register', methods=['POST'])
  def register():
      return jsonify({'message': 'Auth endpoint — coming in v1.1'}), 501

  @auth_bp.route('/login', methods=['POST'])
  def login():
      return jsonify({'message': 'Auth endpoint — coming in v1.1'}), 501

  ── run.py ─────────────────────────────────────────────────────────────────
  # Point d'entrée de l'application

  import os
  from app import create_app, db

  app = create_app(os.environ.get('FLASK_ENV', 'development'))

  if __name__ == '__main__':
      with app.app_context():
          db.create_all()   # Créer les tables si elles n'existent pas
      app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 5000)))

  ── tests/conftest.py ───────────────────────────────────────────────────────
  # Fixtures partagées par tous les tests pytest

  import pytest
  from app import create_app, db as _db

  @pytest.fixture(scope='session')
  def app():
      """Crée une instance Flask configurée pour les tests."""
      application = create_app('testing')
      with application.app_context():
          _db.create_all()
          yield application
          _db.drop_all()

  @pytest.fixture(scope='function')
  def client(app):
      """Client HTTP de test Flask."""
      return app.test_client()

  @pytest.fixture(scope='function', autouse=True)
  def clean_db(app):
      """Vide la base de données entre chaque test."""
      yield
      with app.app_context():
          _db.session.query(__import__('app.models', fromlist=['Task']).Task).delete()
          _db.session.commit()

  ── tests/test_routes.py ────────────────────────────────────────────────────
  # Tests des routes Flask

  import json
  import pytest

  def make_task(client, title="Test Task", priority="medium"):
      """Helper: créer une tâche et retourner la réponse."""
      return client.post('/tasks',
          data=json.dumps({'title': title, 'priority': priority}),
          content_type='application/json')

  class TestHealth:
      def test_health_returns_200(self, client):
          r = client.get('/health')
          assert r.status_code == 200
          assert r.json['status'] == 'healthy'

  class TestCreateTask:
      def test_create_task_success(self, client):
          r = make_task(client, "Ma première tâche")
          assert r.status_code == 201
          assert r.json['title'] == "Ma première tâche"
          assert r.json['done'] is False
          assert r.json['priority'] == 'medium'

      def test_create_task_missing_title(self, client):
          r = client.post('/tasks',
              data=json.dumps({'description': 'sans titre'}),
              content_type='application/json')
          assert r.status_code == 400
          assert 'error' in r.json

      def test_create_task_invalid_priority(self, client):
          r = client.post('/tasks',
              data=json.dumps({'title': 'Test', 'priority': 'ultra'}),
              content_type='application/json')
          assert r.status_code == 400

  class TestGetTasks:
      def test_get_tasks_empty(self, client):
          r = client.get('/tasks')
          assert r.status_code == 200
          assert r.json['tasks'] == []
          assert r.json['total'] == 0

      def test_get_tasks_with_done_filter(self, client):
          make_task(client, "Tâche 1")
          make_task(client, "Tâche 2")
          # Marquer la première comme terminée
          r = client.get('/tasks')
          task_id = r.json['tasks'][-1]['id']   # Dernière créée (ordre DESC)
          client.put(f'/tasks/{task_id}',
              data=json.dumps({'done': True}),
              content_type='application/json')

          r = client.get('/tasks?done=true')
          assert r.status_code == 200
          assert all(t['done'] for t in r.json['tasks'])

      def test_get_tasks_with_search(self, client):
          make_task(client, "Corriger le bug Redis")
          make_task(client, "Ajouter les tests")
          r = client.get('/tasks?search=Redis')
          assert r.status_code == 200
          assert len(r.json['tasks']) == 1
          assert 'Redis' in r.json['tasks'][0]['title']

  class TestUpdateTask:
      def test_update_task_success(self, client):
          r = make_task(client, "À modifier")
          task_id = r.json['id']
          r = client.put(f'/tasks/{task_id}',
              data=json.dumps({'done': True, 'priority': 'high'}),
              content_type='application/json')
          assert r.status_code == 200
          assert r.json['done'] is True
          assert r.json['priority'] == 'high'

      def test_update_nonexistent_task(self, client):
          r = client.put('/tasks/99999',
              data=json.dumps({'done': True}),
              content_type='application/json')
          assert r.status_code == 404

  class TestDeleteTask:
      def test_delete_task_success(self, client):
          r = make_task(client, "À supprimer")
          task_id = r.json['id']
          r = client.delete(f'/tasks/{task_id}')
          assert r.status_code == 204
          r = client.get(f'/tasks/{task_id}')
          assert r.status_code == 404

  LANCER LES TESTS EN LOCAL POUR VÉRIFIER:
  ──────────────────────────────────────────
  source venv/bin/activate
  pytest tests/ -v --tb=short

  CE QUE VOUS DEVEZ VOIR:
  ────────────────────────
  ========================== test session starts ==========================
  platform darwin -- Python 3.11.8, pytest-7.4.3
  collected 12 items

  tests/test_routes.py::TestHealth::test_health_returns_200        PASSED
  tests/test_routes.py::TestCreateTask::test_create_task_success   PASSED
  tests/test_routes.py::TestCreateTask::test_missing_title         PASSED
  ...
  ========================= 12 passed in 1.23s ==========================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.5 — ALICE CRÉE LE .gitignore
────────────────────────────────────────────────────────────────────────────────

  POURQUOI LE .gitignore?
  ────────────────────────
  Sans .gitignore, git add . ajouterait TOUT au dépôt, y compris:
  - Le dossier venv/ (centaines de MB de bibliothèques Python -> inutile en dépôt)
  - Le fichier .env (contient vos mots de passe -> CATASTROPHIQUE à partager!)
  - Les fichiers __pycache__/ (générés automatiquement, inutiles à versionner)
  - Les fichiers .DS_Store (fichiers Mac cachés, inutiles pour Bob et Claire)

  QUAND LE CRÉER?
  ───────────────
  AVANT le premier git add. Si vous oubliez et committez venv/ ou .env,
  ils sont dans l'historique pour toujours (même si vous les supprimez ensuite).

  COMMENT?
  ─────────
  nano .gitignore

  # Contenu du .gitignore:
  # Les # sont des commentaires.
  # Chaque ligne est un patron de fichier/dossier à ignorer.

  # ══ SECRETS — JAMAIS dans Git! ═══════════════════════════════════════════
  .env
  .env.*
  !.env.example
  # .env contient vos mots de passe de base de données, clés API...
  # Si quelqu'un accède à votre dépôt GitHub (même privé), il ne doit
  # jamais trouver vos secrets. !.env.example = exception: le template
  # sans valeurs sensibles EST versionné (guide pour les nouveaux devs).

  # ══ ENVIRONNEMENTS VIRTUELS PYTHON ═══════════════════════════════════════
  venv/
  .venv/
  env/
  ENV/
  # Le venv contient des centaines de fichiers Python précompilés.
  # Chaque développeur crée son propre venv depuis requirements.txt.
  # Le versionner alourdirait le dépôt de centaines de MB pour rien.

  # ══ CACHE PYTHON ══════════════════════════════════════════════════════════
  __pycache__/
  *.py[cod]
  *.pyc
  # Fichiers bytecode générés automatiquement par Python.
  # Différents selon la version de Python -> conflits garantis en équipe.

  # ══ TESTS ET COUVERTURE ═══════════════════════════════════════════════════
  .pytest_cache/
  .coverage
  coverage.xml
  htmlcov/
  # Générés par pytest et coverage, spécifiques à votre machine.

  # ══ BASE DE DONNÉES LOCALE ════════════════════════════════════════════════
  *.db
  *.sqlite
  *.sqlite3
  instance/
  # La base de données locale de dev/test ne doit pas être versionnée.
  # Chaque dev a sa propre base. La prod a la sienne sur le serveur.

  # ══ LOGS ══════════════════════════════════════════════════════════════════
  logs/
  *.log
  # Les logs sont spécifiques à chaque machine et session.

  # ══ ÉDITEURS ══════════════════════════════════════════════════════════════
  .idea/           # JetBrains (PyCharm)
  .vscode/         # Visual Studio Code
  *.swp            # Vim
  .DS_Store        # macOS
  Thumbs.db        # Windows

  # ══ DOCKER ════════════════════════════════════════════════════════════════
  docker-compose.override.yml
  # Overrides Docker locaux (ports différents, volumes locaux...)

  # ══ AUTRES ════════════════════════════════════════════════════════════════
  .scannerwork/    # SonarQube
  dist/
  build/
  *.egg-info/

  VÉRIFIER QUE .gitignore FONCTIONNE:
  ────────────────────────────────────

  # Est-ce que .env serait ignoré?
  echo "SECRET=test" > .env
  git check-ignore -v .env
  # -> .gitignore:3:.env    .env
  # [OK] Confirmé: .env est ignoré.

  # Est-ce que venv/ serait ignoré?
  git check-ignore -v venv/
  # -> .gitignore:10:venv/    venv/
  # [OK] Confirmé: venv/ est ignoré.

  # Voir tous les fichiers ignorés:
  git status --ignored

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.6 — ALICE CRÉE LE FICHIER .env.example
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN .env.example?
  ──────────────────────────
  Le vrai .env est ignoré par Git (il contient les vrais secrets).
  Mais Bob et Claire ont besoin de savoir QUELLES variables existent
  pour créer leur propre .env.

  La solution: un fichier .env.example qui liste toutes les variables
  SANS les vraies valeurs. Ce fichier EST versionné dans Git.

  Quand Bob arrive, il fait:
    cp .env.example .env
    nano .env   # Il remplit ses propres valeurs

  COMMENT?
  ─────────
  nano .env.example

  Contenu:
  # ══════════════════════════════════════════════════════════════════════════
  # .env.example — TEMPLATE DE CONFIGURATION TaskManager
  # ══════════════════════════════════════════════════════════════════════════
  # 1. Copier ce fichier: cp .env.example .env
  # 2. Remplir les valeurs dans .env
  # 3. NE JAMAIS committer le fichier .env dans Git!
  # ══════════════════════════════════════════════════════════════════════════

  # ── Application Flask ───────────────────────────────────────────────────
  FLASK_ENV=development          # development | testing | production
  FLASK_DEBUG=1                  # 1=actif (dev uniquement), 0=inactif (prod)
  FLASK_APP=run.py
  SECRET_KEY=changez-moi-avec-un-vrai-secret-aleatoire
  APP_VERSION=1.0.0
  PORT=5000

  # ── Base de données PostgreSQL ──────────────────────────────────────────
  # En développement local avec Docker:
  POSTGRES_DB=taskmanager
  POSTGRES_USER=taskmanager
  POSTGRES_PASSWORD=votre-mot-de-passe-local-ici
  POSTGRES_HOST=postgres         # "postgres" si Docker, "localhost" si natif
  POSTGRES_PORT=5432
  DATABASE_URL=postgresql://taskmanager:votre-mot-de-passe-local-ici@postgres:5432/taskmanager

  # ── Redis ───────────────────────────────────────────────────────────────
  REDIS_HOST=redis               # "redis" si Docker, "localhost" si natif
  REDIS_PORT=6379
  REDIS_URL=redis://redis:6379/0

  # ── pgAdmin (interface web PostgreSQL, développement uniquement) ─────────
  PGADMIN_EMAIL=admin@equipe.com
  PGADMIN_PASSWORD=pgadmin-password
  PGADMIN_PORT=5050

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.7 — ALICE INITIALISE GIT ET FAIT LE PREMIER COMMIT
────────────────────────────────────────────────────────────────────────────────

  POURQUOI git init?
  ───────────────────
  git init transforme un dossier ordinaire en dépôt Git.
  Il crée un dossier caché .git/ qui contiendra TOUT l'historique.
  Sans git init, aucune commande Git ne fonctionnera dans ce dossier.

  QUAND LE FAIRE?
  ───────────────
  Une seule fois, au tout début du projet, avant le premier commit.

  COMMENT?
  ─────────
  # S'assurer d'être dans le bon dossier:
  pwd
  # -> /home/alice/taskmanager

  # Initialiser Git:
  git init

  CE QUE VOUS VOYEZ:
  ───────────────────
  Initialized empty Git repository in /home/alice/taskmanager/.git/

  # Le dossier .git/ vient d'être créé:
  ls -la
  # -> drwxr-xr-x  .git/
  # -> -rw-r--r--  .env.example
  # -> -rw-r--r--  .gitignore
  # -> ...

  # Voir ce que contient .git/:
  ls .git/
  # -> branches/  config  description  HEAD  hooks/  info/  objects/  refs/
  # HEAD     = fichier qui indique sur quelle branche vous êtes actuellement
  # objects/ = base de données des commits, arbres, blobs
  # refs/    = pointeurs vers les branches et tags
  # hooks/   = scripts automatiques (pre-commit, post-merge...)

  VOIR L'ÉTAT ACTUEL:
  ────────────────────
  git status

  CE QUE VOUS VOYEZ:
  ───────────────────
  On branch main

  No commits yet

  Untracked files:
    (use "git add <file>..." to include in what will be committed)
          .env.example
          .gitignore
          README.md
          app/
          config.py
          run.py
          requirements.txt
          requirements-dev.txt
          tests/

  nothing added to commit but untracked files present

  # "Untracked" = Git voit ces fichiers mais ne les suit pas encore.
  # Le dossier venv/ N'APPARAÎT PAS -> .gitignore fonctionne!
  # Le fichier .env N'APPARAÎT PAS -> .gitignore fonctionne!

  CRÉER UN README.md:
  ─────────────────────
  nano README.md

  Contenu:
  # TaskManager API

  API REST de gestion de tâches en Flask + PostgreSQL.

  ## Stack
  - Python 3.11 + Flask 3.0
  - PostgreSQL 15 + SQLAlchemy
  - Redis (cache)
  - Docker + Docker Compose

  ## Démarrage rapide
  ```bash
  cp .env.example .env
  # Remplir les valeurs dans .env
  docker-compose up -d
  curl http://localhost:5000/health
  ```

  ## Endpoints
  | Méthode | URL              | Description          |
  |---------|------------------|----------------------|
  | GET     | /health          | Santé de l'API       |
  | GET     | /tasks           | Lister les tâches    |
  | POST    | /tasks           | Créer une tâche      |
  | GET     | /tasks/<id>      | Voir une tâche       |
  | PUT     | /tasks/<id>      | Modifier une tâche   |
  | DELETE  | /tasks/<id>      | Supprimer une tâche  |

  ## Équipe
  - Alice (Lead) — alice@equipe.com
  - Bob — bob@equipe.com
  - Claire — claire@equipe.com

  AJOUTER LES FICHIERS AU STAGE (ZONE D'INDEX):
  ───────────────────────────────────────────────
  # POURQUOI git add avant git commit?
  # Git ne sait pas automatiquement ce que vous voulez inclure dans un commit.
  # git add = "mettre cette modification dans la prochaine photo (commit)".
  # Cela permet de faire plusieurs commits logiques même si vous avez modifié
  # beaucoup de fichiers en même temps.

  # Ajouter TOUS les fichiers non ignorés:
  git add .

  # Vérifier ce qui est dans le stage:
  git status

  CE QUE VOUS VOYEZ:
  ───────────────────
  On branch main

  No commits yet

  Changes to be committed:
    (use "git rm --cached <file>..." to unstage)
          new file: .env.example
          new file: .gitignore
          new file: README.md
          new file: app/__init__.py
          new file: app/models.py
          new file: app/routes.py
          new file: config.py
          new file: requirements.txt
          new file: requirements-dev.txt
          new file: run.py
          new file: tests/conftest.py
          new file: tests/test_routes.py

  # "Changes to be committed" = dans le stage, prêt pour le commit.
  # Le fichier .env n'est PAS là (ignoré par .gitignore). [OK]

  CRÉER LE PREMIER COMMIT:
  ─────────────────────────
  # POURQUOI des messages de commit structurés?
  # Dans 6 mois, quand Bob cherche "qui a ajouté la validation de priorité?",
  # il fait: git log --grep="priority"
  # Un bon message permet de retrouver n'importe quelle modification en secondes.
  #
  # CONVENTION CONVENTIONAL COMMITS (utilisée par l'équipe):
  # Format: <type>(<portée>): <description courte>
  #
  # Types:
  # feat     = nouvelle fonctionnalité
  # fix      = correction de bug
  # docs     = documentation seulement
  # style    = formatage (pas de changement fonctionnel)
  # refactor = refactorisation (ni feat ni fix)
  # test     = ajout/modification de tests
  # chore    = maintenance, dépendances, config
  # ci       = CI/CD, GitHub Actions
  # perf     = amélioration de performance
  # revert   = annulation d'un commit précédent

  git commit -m "feat: initial TaskManager API setup

  Complete Flask application with:
  - Task model (title, description, done, priority, tags)
  - User model (for future auth in v1.1)
  - REST routes: CRUD tasks + /health endpoint
  - Filtering by done, priority, search term
  - Pagination with configurable page/per_page
  - SQLAlchemy with multi-environment config
  - pytest test suite (12 tests, 3 test classes)
  - .gitignore and .env.example
  - requirements.txt and requirements-dev.txt"

  CE QUE VOUS VOYEZ:
  ───────────────────
  [main (root-commit) a3f8c1d] feat: initial TaskManager API setup
   12 files changed, 347 insertions(+)
   create mode 100644 .env.example
   create mode 100644 .gitignore
   create mode 100644 README.md
   create mode 100644 app/__init__.py
   create mode 100644 app/models.py
   create mode 100644 app/routes.py
   create mode 100644 config.py
   create mode 100644 requirements.txt
   create mode 100644 requirements-dev.txt
   create mode 100644 run.py
   create mode 100644 tests/conftest.py
   create mode 100644 tests/test_routes.py

  # a3f8c1d = les 7 premiers caractères du hash SHA-1 du commit.
  # C'est l'identifiant unique de ce commit. Vous le verrez partout.

  VOIR L'HISTORIQUE:
  ───────────────────
  git log

  CE QUE VOUS VOYEZ:
  ───────────────────
  commit a3f8c1d4e5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0 (HEAD -> main)
  Author: Alice Martin <alice@equipe.com>
  Date:   Mon Jan 10 09:45:00 2024 +0100

      feat: initial TaskManager API setup

      Complete Flask application with:
      - Task model (title, description, done, priority, tags)
      ...

  # HEAD -> main = vous êtes sur la branche main, au dernier commit.

  git log --oneline
  # -> a3f8c1d (HEAD -> main) feat: initial TaskManager API setup

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.8 — ALICE CRÉE LE DÉPÔT GITHUB ET LE CONNECTE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI GITHUB?
  ─────────────────
  Le dépôt local n'existe que sur la machine d'Alice.
  Bob et Claire ne peuvent pas y accéder.
  GitHub = copie du dépôt sur un serveur accessible par toute l'équipe.

  COMMENT CRÉER LE DÉPÔT SUR GITHUB (interface web):
  ────────────────────────────────────────────────────
  1. Aller sur https://github.com
  2. Se connecter au compte de l'équipe (ou compte personnel d'Alice)
  3. Cliquer sur le "+" en haut à droite -> "New repository"
  4. Remplir le formulaire:
     ┌─────────────────────────────────────────────────────────────────┐
     │ Owner:              equipe                                      │
     │ Repository name:    taskmanager                                 │
     │ Description:        API REST de gestion de tâches Flask        │
     │ Visibility:         [BLACK_CIRCLE] Private                                   │
     │                     [WHITE_CIRCLE] Public                                    │
     │ Initialize this repository with:                                │
     │   [ ] Add a README file    <- NON (on a déjà le nôtre)           │
     │   [ ] Add .gitignore       <- NON (on a déjà le nôtre)           │
     │   [ ] Choose a license     <- NON pour l'instant                  │
     └─────────────────────────────────────────────────────────────────┘
  5. Cliquer "Create repository"

  POURQUOI PRIVATE et non PUBLIC?
  ────────────────────────────────
  Le code source d'un projet commercial ne doit pas être public.
  Private = seuls les collaborateurs invités peuvent voir le dépôt.
  Public = n'importe qui sur Internet peut lire le code.

  GITHUB AFFICHE DES INSTRUCTIONS:
  ──────────────────────────────────
  …or push an existing repository from the command line:
    git remote add origin git@github.com:equipe/taskmanager.git
    git branch -M main
    git push -u origin main

  ALICE EXÉCUTE CES COMMANDES:
  ─────────────────────────────
  # ÉTAPE A: Connecter le dépôt local au dépôt GitHub ("origin"):
  # "remote" = référence à un dépôt distant
  # "origin"  = nom conventionnel du dépôt GitHub principal
  # C'est juste un alias: "origin" = git@github.com:equipe/taskmanager.git
  git remote add origin git@github.com:equipe/taskmanager.git

  # Vérifier que le remote est bien configuré:
  git remote -v
  # -> origin  git@github.com:equipe/taskmanager.git (fetch)
  # -> origin  git@github.com:equipe/taskmanager.git (push)
  # "fetch" et "push" peuvent être différents (rare) mais ici identiques.

  # ÉTAPE B: S'assurer que la branche s'appelle "main":
  git branch -M main

  # ÉTAPE C: Premier push avec -u (--set-upstream):
  # -u lie la branche locale "main" à la branche distante "origin/main".
  # Après ce premier push: git push et git pull fonctionnent sans arguments.
  git push -u origin main

  CE QUE VOUS VOYEZ:
  ───────────────────
  Enumerating objects: 14, done.
  Counting objects: 100% (14/14), done.
  Delta compression using up to 8 threads
  Compressing objects: 100% (11/11), done.
  Writing objects: 100% (14/14), 8.23 KiB | 8.23 MiB/s, done.
  Total 14 (delta 0), reused 0 (delta 0), pack-reused 0
  To git@github.com:equipe/taskmanager.git
   * [new branch]      main -> main
  Branch 'main' set up to track remote branch 'main' from 'origin'.

  # Sur GitHub: actualiser la page -> le code d'Alice est visible!

  INVITER BOB ET CLAIRE COMME COLLABORATEURS:
  ─────────────────────────────────────────────
  # Sur GitHub (interface web):
  # Settings -> Collaborators -> Add people
  # Chercher: bob-dev -> Add bob-dev to this repository
  # Chercher: claire-dev -> Add claire to this repository
  #
  # Bob et Claire reçoivent un email d'invitation.
  # Ils doivent accepter avant de pouvoir cloner.

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 1.9 — ALICE CRÉE ET PROTÈGE LA BRANCHE develop
════════════════════════════════════════════════════════════════════════════════

  POURQUOI develop EXISTE SÉPARÉMENT DE main?
  ─────────────────────────────────────────────
  Imaginez un supermarché. La réserve (develop) = on y reçoit et prépare
  tout. La salle de vente (main) = seulement ce qui est prêt pour les clients.
  On ne reçoit pas les livraisons directement dans la salle de vente.

  Même logique:
  - Les features de Bob et Claire arrivent dans develop.
  - On teste, on corrige, on valide dans develop.
  - Quand develop est stable: on déploie en production via main.
  - Les clients (utilisateurs de l'API) ne voient que ce qui est dans main.

  CRÉER LA BRANCHE develop:
  ──────────────────────────
  # Être sur main (point de départ de develop):
  git checkout main
  # -> Already on 'main'  (ou "Switched to branch 'main'")

  # Créer develop et s'y placer:
  git checkout -b develop

  # Explication de -b: "créer la branche ET se placer dessus"
  # Équivalent en 2 commandes:
  #   git branch develop    <- créer
  #   git checkout develop  <- se placer

  # Syntaxe moderne (Git >= 2.23, même résultat):
  # git switch -c develop

  CE QUE VOUS VOYEZ:
  ───────────────────
  Switched to a new branch 'develop'

  # Pousser develop sur GitHub avec liaison remote:
  git push -u origin develop

  CE QUE VOUS VOYEZ:
  ───────────────────
  Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
  To git@github.com:equipe/taskmanager.git
   * [new branch]      develop -> develop
  Branch 'develop' set up to track remote branch 'develop' from 'origin'.

  # Vérifier les branches:
  git branch -a

  CE QUE VOUS VOYEZ:
  ───────────────────
  * develop
    main
    remotes/origin/develop
    remotes/origin/main

  # * = la branche sur laquelle vous êtes actuellement (develop).
  # remotes/origin/... = les branches côté GitHub.

  ═══════════════════════════════════════════════════════════════════════════
  PROTÉGER main ET develop SUR GITHUB (INTERFACE WEB COMPLÈTE)
  ═══════════════════════════════════════════════════════════════════════════

  POURQUOI PROTÉGER LES BRANCHES?
  ────────────────────────────────
  Sans protection:
  -> Bob peut faire git push origin main directement (sans review, sans tests).
  -> Claire peut faire git push --force origin develop (écraser tout l'historique).
  -> N'importe qui peut merger n'importe quoi n'importe quand.

  Avec la protection de branche:
  -> Toute modification de main OU develop passe par une Pull Request.
  -> Les tests doivent passer avant le merge.
  -> Au moins 1 (develop) ou 2 (main) personnes doivent approuver.
  -> Même Alice (admin) est soumise aux mêmes règles.

  ACCÈS:
  github.com/equipe/taskmanager -> Settings -> Branches -> Add branch protection rule

  ─────────────────────────────────────────────────────────────────────────────
  RÈGLE 1: PROTECTION DE LA BRANCHE main (PRODUCTION)
  ─────────────────────────────────────────────────────────────────────────────

  Branch name pattern: main
  (Ce pattern s'applique exactement à la branche nommée "main")

  ┌─ Option: Require a pull request before merging ─────────────────────────┐
  │                                                                          │
  │  POURQUOI: Personne ne peut pousser directement sur main.                │
  │  Toute modification passe par une Pull Request (= revue de code).       │
  │  Sans ça: Bob pourrait faire git push origin main et casser la prod.    │
  │                                                                          │
  │  [x] Activé                                                               │
  │                                                                          │
  │  ┌─ Sous-option: Require approvals ───────────────────────────────────┐ │
  │  │ Nombre: 2                                                           │ │
  │  │ POURQUOI 2 pour main? Un changement en production est critique.    │ │
  │  │ Deux paires d'yeux valent mieux qu'une. Si Alice ouvre une PR,     │ │
  │  │ Bob ET Claire doivent approuver. Ou Bob ouvre -> Alice ET Claire.   │ │
  │  └─────────────────────────────────────────────────────────────────── ┘ │
  │                                                                          │
  │  ┌─ Sous-option: Dismiss stale PR approvals ──────────────────────────┐ │
  │  │ [x] Activé                                                           │ │
  │  │ POURQUOI: Si Alice approuve la PR de Bob puis Bob push un nouveau  │ │
  │  │ commit, l'approbation d'Alice est annulée. Elle doit re-valider.   │ │
  │  │ Évite: "J'avais approuvé mais le code a changé après..."          │ │
  │  └────────────────────────────────────────────────────────────────── ─┘ │
  │                                                                          │
  │  ┌─ Sous-option: Require review from code owners ─────────────────────┐ │
  │  │ [x] Activé                                                           │ │
  │  │ POURQUOI: Le fichier CODEOWNERS définit des "propriétaires" par    │ │
  │  │ dossier. Si Bob modifie docker/, Alice (propriétaire de docker/)   │ │
  │  │ est automatiquement ajoutée comme reviewer obligatoire.            │ │
  │  └───────────────────────────────────────────────────────────────────┘ │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Require status checks to pass before merging ──────────────────┐
  │                                                                          │
  │  POURQUOI: Avant de merger, les vérifications automatiques (tests,      │
  │  lint, SonarQube...) doivent toutes être vertes.                        │
  │  Si les tests échouent: le bouton "Merge" est GRISÉ sur GitHub.        │
  │  Résultat: impossible de merger du code cassé en production.            │
  │                                                                          │
  │  [x] Activé                                                               │
  │                                                                          │
  │  ┌─ Sous-option: Require branches to be up to date ──────────────────┐  │
  │  │ [x] Activé                                                          │  │
  │  │ POURQUOI: La branche feature doit inclure tous les commits de     │  │
  │  │ develop/main. Évite: "ça marchait sur ma branche mais pas après   │  │
  │  │ le merge car j'avais une ancienne version de utils.py."           │  │
  │  └────────────────────────────────────────────────────────────────── ┘  │
  │                                                                          │
  │  Status checks requis:                                                   │
  │  + ci/lint          (flake8 + black)                                    │
  │  + ci/tests         (pytest 100% passé)                                 │
  │  + ci/sonarqube     (Quality Gate A)                                    │
  │  + ci/build-docker  (build Docker réussi)                               │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Require conversation resolution before merging ────────────────┐
  │                                                                          │
  │  POURQUOI: Tous les commentaires de review doivent être résolus.        │
  │  Évite: merger une PR dont un reviewer a écrit "ce code a un bug"       │
  │  sans que le bug ait été corrigé.                                        │
  │                                                                          │
  │  [x] Activé                                                               │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Require signed commits ────────────────────────────────────────┐
  │                                                                          │
  │  POURQUOI: Chaque commit doit être signé avec GPG. Prouve que le commit │
  │  vient vraiment d'Alice/Bob/Claire (impossible à usurper).              │
  │  Sur GitHub: les commits signés affichent un badge "Verified" vert.    │
  │                                                                          │
  │  [x] Activé pour main                                                     │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Include administrators ────────────────────────────────────────┐
  │                                                                          │
  │  POURQUOI: Sans cette option, Alice (admin) peut bypasser toutes les    │
  │  règles. Avec: même Alice doit respecter le processus.                  │
  │  Message fort: "personne n'est au-dessus des règles, même le lead".    │
  │                                                                          │
  │  [x] Activé                                                               │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Do not allow bypassing the above settings ─────────────────────┐
  │                                                                          │
  │  POURQUOI: Renforce "Include administrators". Absolument personne       │
  │  ne peut contourner. Garantie totale d'intégrité du processus.         │
  │                                                                          │
  │  [x] Activé                                                               │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Restrict who can push to matching branches ────────────────────┐
  │                                                                          │
  │  POURQUOI: Même via PR, seule Alice peut déclencher le merge dans main. │
  │  Bob et Claire ouvrent des PRs mais ne peuvent pas cliquer "Merge".    │
  │  Alice est le dernier garde-fou avant la production.                    │
  │                                                                          │
  │  [x] Activé                                                               │
  │  Allowed actors: @alice-dev                                             │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Allow force pushes ────────────────────────────────────────────┐
  │                                                                          │
  │  POURQUOI DÉSACTIVÉ: git push --force REMPLACE l'historique de la      │
  │  branche. Sur main: CATASTROPHE. L'historique de production serait      │
  │  perdu. Cette option doit rester désactivée sur toutes les branches     │
  │  partagées.                                                              │
  │                                                                          │
  │  [ ] Désactivé                                                            │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Option: Allow deletions ───────────────────────────────────────────────┐
  │                                                                          │
  │  POURQUOI DÉSACTIVÉ: Personne ne peut supprimer main, même Alice.      │
  │  git push origin --delete main serait bloqué.                          │
  │                                                                          │
  │  [ ] Désactivé                                                            │
  │                                                                          │
  └──────────────────────────────────────────────────────────────────────────┘

  -> Cliquer "Create" / "Save changes"

  ─────────────────────────────────────────────────────────────────────────────
  RÈGLE 2: PROTECTION DE LA BRANCHE develop (INTÉGRATION)
  ─────────────────────────────────────────────────────────────────────────────

  Branch name pattern: develop

  ┌─ Require a pull request before merging ─────────────────────────────────┐
  │  [x] Activé                                                               │
  │  Require approvals: 1   <- 1 seul reviewer suffit (pas 2 comme main)    │
  │  POURQUOI 1 et pas 2?   develop n'est pas la production. Les erreurs   │
  │  dans develop sont détectées par les tests CI avant d'atteindre main.  │
  │  Exiger 2 reviewers sur develop ralentirait trop le travail.           │
  │  [x] Dismiss stale PR approvals: Activé                                  │
  │  [ ] Require review from CODEOWNERS: Désactivé (plus souple que main)   │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Require status checks to pass ─────────────────────────────────────────┐
  │  [x] Activé                                                               │
  │  [x] Require branches to be up to date: Activé                           │
  │  Status checks requis:                                                   │
  │  + ci/lint                                                              │
  │  + ci/tests                                                             │
  │  (SonarQube et build Docker pas obligatoires ici -> plus rapide)        │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Require conversation resolution ───────────────────────────────────────┐
  │  [x] Activé — toujours, même sur develop                                 │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Require signed commits ────────────────────────────────────────────────┐
  │  [ ] Désactivé sur develop — exiger GPG sur develop serait trop pénalisant│
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Include administrators ────────────────────────────────────────────────┐
  │  [x] Activé — Alice aussi doit passer par une PR sur develop             │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌─ Allow force pushes ────────────────────────────────────────────────────┐
  │  [ ] Désactivé — jamais de force push sur une branche partagée           │
  └──────────────────────────────────────────────────────────────────────────┘

  -> Cliquer "Create" / "Save changes"

  TABLEAU COMPARATIF DES PROTECTIONS:
  ─────────────────────────────────────
  Option                              main     develop
  ─────────────────────────────────── ──────── ────────
  PR obligatoire                      Oui      Oui
  Nombre d'approbations               2        1
  CODEOWNERS reviewer obligatoire     Oui      Non
  Tests CI obligatoires               Oui      Oui
  SonarQube Quality Gate obligatoire  Oui      Non
  Build Docker obligatoire            Oui      Non
  Branche à jour obligatoire          Oui      Oui
  Conversations résolues              Oui      Oui
  Commits signés GPG                  Oui      Non
  Admins soumis aux règles            Oui      Oui
  Force push autorisé                 Non      Non
  Suppression autorisée               Non      Non
  Seul à pouvoir merger               Alice    Tous (via PR)

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 1.10 — ALICE CRÉE LES LABELS SUR GITHUB
════════════════════════════════════════════════════════════════════════════════

  POURQUOI LES LABELS?
  ─────────────────────
  Sans labels, la liste des issues ressemble à:
  #1 Bug dans les routes
  #2 Ajouter la pagination
  #3 Tests qui échouent
  #4 Améliorer les logs

  Impossible de répondre à: "Quels sont les bugs urgents?" ou
  "Combien d'améliorations sont en attente?".

  Avec labels:
  #1 [bug][priority: high] Bug dans les routes
  #2 [enhancement][priority: medium] Ajouter la pagination
  #3 [bug][tests][blocked] Tests qui échouent
  #4 [enhancement][priority: low] Améliorer les logs

  En 1 clic: "label:bug label:priority: high" -> Liste des bugs urgents.

  ACCÈS:
  github.com/equipe/taskmanager -> Issues -> Labels -> New label

  PROCÉDURE POUR CHAQUE LABEL:
  ──────────────────────────────
  1. Cliquer "New label"
  2. Taper le nom
  3. Cliquer le carré de couleur, entrer le code hex
  4. Taper la description
  5. Cliquer "Create label"

  OU VIA GITHUB CLI (plus rapide pour créer tous les labels d'un coup):
  gh label create "bug"            --color "#d73a4a" --description "Comportement incorrect"
  gh label create "enhancement"    --color "#a2eeef" --description "Nouvelle fonctionnalité"
  gh label create "documentation"  --color "#0075ca" --description "Documentation uniquement"
  gh label create "breaking change"--color "#7c3aed" --description "Change l'API existante"
  gh label create "priority: high" --color "#e11d48" --description "Traiter en priorité absolue"
  gh label create "priority: medium" --color "#f97316" --description "Sprint courant ou suivant"
  gh label create "priority: low"  --color "#86efac" --description "Nice-to-have, quand possible"
  gh label create "in progress"    --color "#fde68a" --description "Quelqu'un travaille dessus"
  gh label create "blocked"        --color "#6b7280" --description "Bloqué par une dépendance"
  gh label create "wontfix"        --color "#ffffff" --description "Ne sera pas implémenté"
  gh label create "duplicate"      --color "#cfd3d7" --description "Doublon d'une issue existante"
  gh label create "good first issue" --color "#7057ff" --description "Idéal pour débutant"
  gh label create "help wanted"    --color "#008672" --description "Aide extérieure souhaitée"
  gh label create "flask"          --color "#059669" --description "Routes, blueprints, Flask"
  gh label create "database"       --color "#0284c7" --description "PostgreSQL, SQLAlchemy, modèles"
  gh label create "tests"          --color "#7c3aed" --description "pytest, couverture, fixtures"
  gh label create "ci/cd"          --color "#6366f1" --description "GitHub Actions, déploiement"
  gh label create "docker"         --color "#0369a1" --description "Dockerfiles, docker-compose"
  gh label create "security"       --color "#dc2626" --description "Vulnérabilité, authentification"

  COMMENT UTILISER LES LABELS AU QUOTIDIEN:
  ──────────────────────────────────────────
  Quand Bob crée une issue pour un bug de routing Flask urgent:
  -> Labels: bug + flask + priority: high

  Quand Claire crée une issue pour améliorer les tests:
  -> Labels: enhancement + tests + priority: medium

  Quand une issue bloque une autre:
  -> Labels: blocked (+ ajouter en commentaire: "Bloqué par #12")

  Quand on décide de ne pas implémenter quelque chose:
  -> Labels: wontfix + fermer l'issue + expliquer pourquoi en commentaire

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 1.11 — ALICE CRÉE LES MILESTONES (JALONS)
════════════════════════════════════════════════════════════════════════════════

  POURQUOI LES MILESTONES?
  ─────────────────────────
  Un Milestone regroupe des issues autour d'un objectif commun et daté.
  Il répond à: "Qu'est-ce qui doit être fait pour livrer la v1.0.0?"
  GitHub calcule automatiquement l'avancement: "14/20 issues fermées = 70%"

  Différence avec les labels:
  Label     = catégorisation transversale (type, priorité, état)
  Milestone = regroupement pour une livraison spécifique à une date

  Une issue peut avoir des labels ET appartenir à un milestone:
  #5 [enhancement][flask][priority: high] -> Milestone: v1.0.0

  ACCÈS:
  github.com/equipe/taskmanager -> Issues -> Milestones -> New milestone

  PROCÉDURE POUR CHAQUE MILESTONE:
  ──────────────────────────────────
  1. Cliquer "New milestone"
  2. Remplir: Title, Due date, Description
  3. Cliquer "Create milestone"

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  MILESTONE: v1.0.0 — MVP (Minimum Viable Product)                       │
  │  Due date: 2024-02-01                                                   │
  │                                                                          │
  │  Description:                                                            │
  │  Première version stable et déployable en production.                   │
  │  Fonctionnalités: CRUD tâches complet, filtrage, pagination, /health.  │
  │  Critères de sortie: tous les tests passent, coverage >= 90%,          │
  │  image Docker construite et poussée sur ghcr.io.                        │
  │                                                                          │
  │  Issues prévues:                                                         │
  │  #1  CRUD tâches (GET/POST/PUT/DELETE)          [À assigner -> Bob]      │
  │  #2  Endpoint /health                           [À assigner -> Alice]    │
  │  #3  Filtrage et pagination GET /tasks          [À assigner -> Bob]      │
  │  #4  Tests complets (12 -> objectif 20 tests)   [À assigner -> Claire]   │
  │  #5  Configuration Docker (dev/test/prod)       [À assigner -> Alice]    │
  │  #6  Pipeline CI/CD GitHub Actions              [À assigner -> Alice]    │
  │                                                                          │
  │  Avancement: 0/6 issues fermées = 0%                                   │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  MILESTONE: v1.1.0 — Authentification                                   │
  │  Due date: 2024-03-01                                                   │
  │                                                                          │
  │  Description:                                                            │
  │  Système d'authentification JWT complet.                                │
  │  Les utilisateurs pourront créer un compte, se connecter,              │
  │  et gérer uniquement leurs propres tâches.                              │
  │                                                                          │
  │  Issues prévues (à créer après v1.0.0):                                │
  │  - Modèle User + hash de mot de passe (bcrypt)                         │
  │  - POST /auth/register, POST /auth/login                               │
  │  - Génération et validation de tokens JWT                              │
  │  - Middleware d'authentification (décorateur @login_required)          │
  │  - Association tâches -> utilisateur (Task.user_id)                     │
  │  - Tests d'authentification                                             │
  │                                                                          │
  │  Avancement: 0/6 issues fermées = 0%                                   │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  MILESTONE: v1.2.0 — Performance & Cache                                │
  │  Due date: 2024-04-01                                                   │
  │                                                                          │
  │  Description:                                                            │
  │  Intégration Redis pour le cache et la gestion des sessions.           │
  │  Objectif: réduire de 60% le temps de réponse de GET /tasks.          │
  │  Rate limiting pour protéger l'API des abus.                           │
  │                                                                          │
  │  Issues prévues:                                                         │
  │  - Cache Redis pour GET /tasks (Flask-Caching)                         │
  │  - Invalidation du cache lors des POST/PUT/DELETE                      │
  │  - Rate limiting par IP (Flask-Limiter)                                │
  │  - Index PostgreSQL sur Task.created_at et Task.done                   │
  │  - Benchmarks avant/après (k6 ou locust)                               │
  │                                                                          │
  │  Avancement: 0/5 issues fermées = 0%                                   │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  MILESTONE: v2.0.0 — Architecture en couches                            │
  │  Due date: 2024-06-01                                                   │
  │                                                                          │
  │  Description:                                                            │
  │  Refactorisation majeure: séparation en couches (routes, services,     │
  │  repositories). Migration vers une structure plus maintenable.          │
  │  BREAKING CHANGES prévus (nouvelle structure de réponse API).          │
  │                                                                          │
  │  Issues: à définir lors du sprint planning de mars.                    │
  │  Avancement: 0/0 issues fermées = N/A                                  │
  └──────────────────────────────────────────────────────────────────────────┘

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 1.12 — ALICE CRÉE LES TEMPLATES D'ISSUES ET DE PR
════════════════════════════════════════════════════════════════════════════════

  POURQUOI DES TEMPLATES?
  ────────────────────────
  Sans template, Bob crée une issue: "Ça marche pas."
  Avec template, Bob est guidé: Description, Reproduction, Environnement...

  Les templates garantissent que chaque issue/PR contient les informations
  nécessaires pour travailler dessus sans aller chercher des détails.

  CRÉER LE TEMPLATE D'ISSUE BUG REPORT:
  ──────────────────────────────────────
  # Dans le terminal d'Alice:
  nano .github/ISSUE_TEMPLATE/bug_report.md

  Contenu:
  ---
  name: [BUG] Bug Report
  about: Signaler un comportement incorrect de l'application
  title: '[BUG] '
  labels: bug
  assignees: ''
  ---

  ## [BUG] Description du bug
  <!-- Décrivez clairement et précisément le problème -->

  ## [LISTE] Étapes pour reproduire
  1. Appeler `POST /tasks` avec `{"title": "test", "description": 123}`
  2. Observer la réponse
  3. Voir l'erreur

  ## [OK] Comportement attendu
  <!-- Que devrait-il se passer? -->
  Retourner `400 Bad Request` avec un message d'erreur clair.

  ## [X] Comportement actuel
  <!-- Que se passe-t-il réellement? -->
  Retourne `500 Internal Server Error`.

  ## [MONDE] Environnement
  - OS: [ex: Ubuntu 22.04 / macOS 14 / Windows 11]
  - Python: [ex: 3.11.8]
  - Flask: [ex: 3.0.0]
  - Branch: [ex: develop]
  - Commit: [ex: a3f8c1d]

  ## [PAPERCLIP] Logs / Screenshot
  ```
  Coller les logs pertinents ici (depuis docker-compose logs -f api)
  ```

  ## [LIEN] Issues liées
  <!-- Lier d'autres issues si pertinent: "Lié à #12" -->

  CRÉER LE TEMPLATE D'ISSUE FEATURE REQUEST:
  ───────────────────────────────────────────
  nano .github/ISSUE_TEMPLATE/feature_request.md

  Contenu:
  ---
  name: * Feature Request
  about: Proposer une nouvelle fonctionnalité ou amélioration
  title: '[FEAT] '
  labels: enhancement
  assignees: ''
  ---

  ## [IDEE] Problème / Besoin
  <!-- Quel problème cette feature résout-elle?
       Ex: "Je dois souvent chercher les tâches urgentes non terminées.
       Actuellement je dois filtrer manuellement dans la liste complète." -->

  ## [OBJECTIF] Solution proposée
  <!-- Décrivez la solution que vous proposez.
       Soyez précis sur le comportement attendu. -->
  Ajouter le paramètre `?priority=high` à `GET /tasks` pour filtrer
  par niveau de priorité.

  Exemple d'utilisation:
  ```bash
  GET /tasks?priority=high&done=false
  # Retourne toutes les tâches urgentes non terminées
  ```

  ## [SYNC] Alternatives considérées
  <!-- Avez-vous envisagé d'autres approches? Pourquoi les avoir écartées? -->

  ## [GRAPHIQUE] Impact attendu
  - Priorité suggérée: [ ] Low [x] Medium [ ] High
  - Milestone suggéré: [ ] v1.0.0 [x] v1.1.0 [ ] v1.2.0

  ## [PAPERCLIP] Contexte supplémentaire
  <!-- Mockups, exemples d'autres APIs, références... -->

  CRÉER LE TEMPLATE DE PULL REQUEST:
  ────────────────────────────────────
  nano .github/PULL_REQUEST_TEMPLATE.md

  Contenu:
  ## [LISTE] Description
  <!-- Expliquez QUOI et POURQUOI (pas comment — le code montre comment) -->

  ## [LIEN] Issue liée
  Closes #<!-- Numéro de l'issue -->

  ## [LABEL] Type de changement
  - [ ] [BUG] Bug fix (correction non cassante)
  - [ ] * Feature (nouvelle fonctionnalité non cassante)
  - [ ] [IMPACT] Breaking change (modification incompatible avec v. précédente)
  - [ ] [DOCS] Documentation uniquement
  - [ ] [OUTIL] Refactoring (ni feature ni bug)
  - [ ] [TEST] Tests uniquement
  - [ ] [VERROUILLE] Sécurité

  ## [TEST] Tests
  - [ ] J'ai ajouté/modifié des tests pour couvrir mes changements
  - [ ] Tous les tests existants passent encore (`pytest tests/ -v`)
  - [ ] Coverage ne régresse pas (vérifier avec `pytest --cov=app`)
  - [ ] J'ai testé manuellement avec `curl` ou Postman/Insomnia

  ## [CAMERA_WITH_FLASH] Captures d'écran (si changement visible)
  <!-- Avant / Après -->

  ## [OK] Checklist avant review
  - [ ] Mon code suit le style de l'équipe (flake8 + black)
  - [ ] Mes commits suivent Conventional Commits
  - [ ] J'ai mis à jour la documentation si nécessaire
  - [ ] J'ai résolu tous les conflits de merge
  - [ ] J'ai testé en local (docker-compose up -> curl /health)

  ## [SPEECH_BALLOON] Notes pour le reviewer
  <!-- Ce sur quoi vous voulez particulièrement de l'attention -->

  CRÉER LE FICHIER CODEOWNERS:
  ─────────────────────────────
  # POURQUOI CODEOWNERS?
  # Définit automatiquement qui doit review selon le dossier modifié.
  # Si Bob modifie docker/: Alice est auto-ajoutée comme reviewer obligatoire.
  # Si Claire modifie tests/: aucun reviewer supplémentaire (elle en est responsable).

  nano .github/CODEOWNERS

  Contenu:
  # Propriétaire par défaut de tout le dépôt (fallback):
  *                         @alice-dev @bob-dev @claire-dev

  # Alice = responsable DevOps, infrastructure, déploiement:
  docker/                   @alice-dev
  docker-compose*.yml       @alice-dev
  .github/workflows/        @alice-dev
  .github/CODEOWNERS        @alice-dev
  nginx/                    @alice-dev
  scripts/                  @alice-dev

  # Claire = responsable qualité et tests:
  tests/                    @claire-dev

  # Toute l'équipe doit approuver les changements de config critique:
  config.py                 @alice-dev @bob-dev @claire-dev
  requirements.txt          @alice-dev @bob-dev @claire-dev
  requirements-dev.txt      @alice-dev @bob-dev @claire-dev

  # Alice = responsable sécurité:
  SECURITY.md               @alice-dev

  COMMITER TOUS CES FICHIERS:
  ────────────────────────────
  git add .github/
  git status
  # Vérifier que les bons fichiers sont stagés

  git commit -m "chore(github): add PR/issue templates and CODEOWNERS

  - Bug report template with reproduction steps
  - Feature request template with impact assessment
  - PR template with comprehensive checklist
  - CODEOWNERS: Alice=DevOps, Claire=tests, all=config"

  git push origin develop
  # Pousser sur develop (pas main — main ne reçoit que les releases)

================================================================================
PARTIE 2 — BOB ET CLAIRE REJOIGNENT LE PROJET (JOUR 1, APRÈS-MIDI)
================================================================================

Alice envoie un message Slack à 10h45:
"Tout est prêt! Bob: Ubuntu, Claire: Windows. Suivez vos sections.
Rendez-vous à 14h pour vérifier que tout le monde peut créer une tâche via curl."

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 — BOB CONFIGURE SON POSTE (UBUNTU)
────────────────────────────────────────────────────────────────────────────────

  BOB DANS SON TERMINAL UBUNTU:

  ÉTAPE A — Installer Git:
  ─────────────────────────
  sudo apt-get update && sudo apt-get install git -y
  git --version
  # -> git version 2.43.0

  ÉTAPE B — Configurer son identité:
  ─────────────────────────────────────
  git config --global user.name "Bob Dupont"
  git config --global user.email "bob@equipe.com"
  git config --global core.editor "nano"
  git config --global pull.rebase true
  git config --global init.defaultBranch main
  git config --global core.autocrlf input     # Linux: input (pas true comme Windows)
  git config --global color.ui auto
  git config --global alias.st status
  git config --global alias.co checkout
  git config --global alias.br branch
  git config --global alias.lg "log --oneline --graph --decorate --all"
  git config --global alias.last "log -1 HEAD --stat"
  git config --global alias.pushf "push --force-with-lease"

  ÉTAPE C — Générer sa clé SSH:
  ──────────────────────────────
  ssh-keygen -t ed25519 -C "bob@equipe.com" -f ~/.ssh/id_ed25519_github
  # Saisir une passphrase, la confirmer

  eval "$(ssh-agent -s)"
  ssh-add ~/.ssh/id_ed25519_github

  nano ~/.ssh/config
  # Contenu:
  Host github.com
      HostName github.com
      User git
      IdentityFile ~/.ssh/id_ed25519_github
      AddKeysToAgent yes
  chmod 600 ~/.ssh/config

  # Ajouter la clé publique sur GitHub:
  cat ~/.ssh/id_ed25519_github.pub
  # -> Copier cette ligne

  # Sur GitHub: avatar -> Settings -> SSH and GPG keys -> New SSH key
  # Title: "PC Ubuntu Bob 2024"
  # Coller la clé -> Add SSH key

  # Tester:
  ssh -T git@github.com
  # -> Hi bob-dev! You've successfully authenticated...

  ÉTAPE D — Accepter l'invitation de collaboration:
  ──────────────────────────────────────────────────
  # Bob a reçu un email d'invitation de GitHub.
  # Cliquer sur le lien dans l'email -> "Accept invitation"
  # Ou: github.com/equipe/taskmanager -> cliquer "Accept"

  ÉTAPE E — Cloner le dépôt:
  ───────────────────────────
  # POURQUOI cloner et pas git init?
  # git clone télécharge le dépôt COMPLET depuis GitHub:
  # - Tout le code
  # - Tout l'historique des commits
  # - Toutes les branches existantes
  # - Le remote "origin" est déjà configuré automatiquement
  # C'est différent de git init qui crée un dépôt VIDE local.

  cd ~
  git clone git@github.com:equipe/taskmanager.git
  cd taskmanager/

  CE QUE VOUS VOYEZ:
  ───────────────────
  Cloning into 'taskmanager'...
  remote: Enumerating objects: 18, done.
  remote: Counting objects: 100% (18/18), done.
  remote: Compressing objects: 100% (14/14), done.
  Receiving objects: 100% (18/18), 12.45 KiB | 3.11 MiB/s, done.

  # Voir les branches disponibles:
  git branch -a

  CE QUE VOUS VOYEZ:
  ───────────────────
  * main
    remotes/origin/HEAD -> origin/main
    remotes/origin/develop
    remotes/origin/main

  # main est la branche locale par défaut après clone.
  # origin/develop existe sur GitHub mais pas encore localement.

  # Créer la branche locale develop qui suit origin/develop:
  git checkout develop

  CE QUE VOUS VOYEZ:
  ───────────────────
  Branch 'develop' set up to track remote branch 'develop' from 'origin'.
  Switched to a new branch 'develop'

  ÉTAPE F — Configurer l'environnement local:
  ─────────────────────────────────────────────
  cp .env.example .env
  nano .env
  # Bob remplace les valeurs:
  # POSTGRES_PASSWORD=bob-local-password-2024
  # SECRET_KEY=bob-secret-key-longue-et-aleatoire

  python3 -m venv venv
  source venv/bin/activate
  pip install -r requirements.txt -r requirements-dev.txt

  # Démarrer les services avec Docker:
  docker-compose up -d

  # Tester:
  curl http://localhost:5000/health
  # -> {"status": "healthy", "timestamp": "...", "database": "ok"}   [OK]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.2 — CLAIRE CONFIGURE SON POSTE (WINDOWS 11)
────────────────────────────────────────────────────────────────────────────────

  CLAIRE SUR WINDOWS 11 (dans Git Bash):

  ÉTAPE A — Installer Git for Windows:
  ──────────────────────────────────────
  # Télécharger: https://git-scm.com/download/win
  # Options importantes lors de l'installation:
  # [OK] "Git from the command line and also from 3rd-party software"
  # [OK] "Use Visual Studio Code as Git's default editor"
  # [OK] "Override the default branch name for new repos" -> taper "main"
  # [OK] "Checkout Windows-style, commit Unix-style line endings"  <- CRUCIAL!
  # Terminer l'installation, puis ouvrir "Git Bash".

  ÉTAPE B — Configurer son identité:
  ─────────────────────────────────────
  git config --global user.name "Claire Leblanc"
  git config --global user.email "claire@equipe.com"
  git config --global core.editor "nano"
  git config --global pull.rebase true
  git config --global init.defaultBranch main
  git config --global core.autocrlf true
  # Windows = true: Git convertit CRLF->LF au commit, LF->CRLF au checkout.
  # Résultat: Claire voit CRLF dans son éditeur (normal pour Windows),
  # mais Git stocke LF (universel). Pas de conflits de fins de ligne.
  git config --global color.ui auto
  git config --global alias.lg "log --oneline --graph --decorate --all"

  ÉTAPE C — SSH (même processus, dans Git Bash):
  ────────────────────────────────────────────────
  ssh-keygen -t ed25519 -C "claire@equipe.com" -f ~/.ssh/id_ed25519_github
  eval "$(ssh-agent -s)"
  ssh-add ~/.ssh/id_ed25519_github
  cat ~/.ssh/id_ed25519_github.pub  # Copier -> GitHub -> New SSH key

  ÉTAPE D — Cloner et configurer:
  ─────────────────────────────────
  cd /c/Users/claire/Documents
  git clone git@github.com:equipe/taskmanager.git
  cd taskmanager/
  git checkout develop
  cp .env.example .env
  # Éditer avec VS Code: code .env  (et non Notepad classique!)

  # Docker Desktop doit être installé et en cours d'exécution.
  docker-compose up -d
  curl http://localhost:5000/health   # [OK]

================================================================================
PARTIE 3 — WORKFLOW QUOTIDIEN: SCÉNARIOS COMPLETS ALICE, BOB ET CLAIRE
================================================================================

Les premières configurations sont terminées. On est maintenant au Jour 2.
Bob et Claire développent leurs premières features.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 1 — BOB: DÉVELOPPE LE FILTRAGE PAR DATES (Issue #3)
Durée: 3 jours | Branche: feature/3-filtrage-dates-tri
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Alice a créé l'issue #3 sur GitHub:
  Title:    [FEAT] Filtrage GET /tasks par plage de dates et tri
  Labels:   enhancement, flask, priority: medium
  Milestone: v1.0.0
  Assignee: @bob-dev

  Voici le processus complet pas à pas :

    ---

    ## Créer l'issue #3 sur GitHub — Interface Web

    ### ACCÈS
    ```
    github.com/equipe/taskmanager -> onglet "Issues" -> bouton vert "New issue"
    ```

    ---

    ### ÉTAPE 1 — Choisir le template

    GitHub affiche la liste des templates créés par Alice :

    ```
    [BUG] Bug Report          -> [Get started]
    * Feature Request     -> [Get started]   <- Alice clique ici
    ```

    ---

    ### ÉTAPE 2 — Remplir le titre

    Dans le champ **Title** en haut :
    ```
    [FEAT] Filtrage GET /tasks par plage de dates et tri
    ```

    ---

    ### ÉTAPE 3 — Remplir le corps (template pré-rempli)

    Le template Feature Request s'affiche. Alice remplit chaque section :

    ```markdown
    ## [IDEE] Problème / Besoin
    Impossible de filtrer les tâches par date de création.
    Pour les rapports hebdomadaires, on a besoin de voir uniquement
    les tâches créées entre deux dates précises.

    ## [OBJECTIF] Solution proposée
    Ajouter les paramètres suivants à GET /tasks :
    - ?from=YYYY-MM-DD  -> tâches créées après cette date
    - ?to=YYYY-MM-DD    -> tâches créées avant cette date
    - ?sort=created_at|title  -> champ de tri
    - ?order=asc|desc   -> sens du tri (défaut: desc)

    Exemple :
    GET /tasks?from=2024-01-01&to=2024-01-31&sort=title&order=asc

    ## [SYNC] Alternatives considérées
    Filtrage côté client (rejeté : trop de données à transférer).

    ## [GRAPHIQUE] Impact attendu
    - Priorité : [x] Medium
    - Milestone : [x] v1.0.0
    ```

    ---

    ### ÉTAPE 4 — Panneau latéral droit (les métadonnées)

    Alice remplit chaque section du panneau à droite :

    **Assignees** -> cliquer -> taper `bob` -> sélectionner `@bob-dev`
    ```
    [UTILISATEUR] bob-dev
    ```

    **Labels** -> cliquer -> cocher un par un :
    ```
    [x] enhancement
    [x] flask
    [x] priority: medium
    ```

    **Milestone** -> cliquer -> sélectionner :
    ```
    [CHEQUERED_FLAG] v1.0.0 — MVP
    ```

    **Projects** (optionnel) -> sélectionner le Kanban -> colonne **To Do**

    ---

    ### ÉTAPE 5 — Soumettre

    Cliquer le bouton vert **"Submit new issue"**

    ---

    ### RÉSULTAT

    GitHub crée l'issue et affiche :

    ```
    Issue #3 créée [OK]

    [FEAT] Filtrage GET /tasks par plage de dates et tri

    Labels:    enhancement   flask   priority: medium
    Milestone: v1.0.0 — 0 of 6 issues closed
    Assignee:  @bob-dev

    GitHub envoie une notification à Bob par email :
    "You have been assigned to issue #3 in equipe/taskmanager"
    ```

    ---

    ### CE QUE BOB VOIT DE SON CÔTÉ

    Sur GitHub -> notifications (cloche [NOTIF]) :
    ```
    equipe/taskmanager — You were assigned #3
    [FEAT] Filtrage GET /tasks par plage de dates et tri
    ```

    Bob peut alors commenter l'issue pour confirmer :
    ```
    Je prends cette issue. Je commence la branche feature/3-filtrage-dates-tri.
    Livraison estimée : vendredi.
    ```
    Et faire glisser la carte dans le Kanban de **To Do** -> **In Progress**.

────────────────────────────────────────────────────────────────────────────────
  BOB — JOUR 1 MATIN: Créer la branche depuis develop
────────────────────────────────────────────────────────────────────────────────

  POURQUOI PARTIR DE develop ET PAS DE main?
  ────────────────────────────────────────────
  main = code en production. Les nouvelles features ne vont pas directement
  en production. Elles passent d'abord par develop (tests, intégration).
  Partir de develop = avoir le code le plus récent de l'équipe comme base,
  ce qui minimise les conflits futurs lors du merge.

  ÉTAPE 1 — SE METTRE À JOUR:
  ─────────────────────────────
  # Toujours faire git pull AVANT de créer une branche.
  # Sinon: votre branche est basée sur une version ancienne.

  git checkout develop
  git pull origin develop

  CE QUE VOUS VOYEZ (si tout est à jour):
  ────────────────────────────────────────
  Already up to date.

  ÉTAPE 2 — CRÉER LA BRANCHE FEATURE:
  ──────────────────────────────────────
  # Convention: feature/[numéro-issue]-[description-courte]
  git checkout -b feature/3-filtrage-dates-tri

  # Pousser immédiatement sur GitHub:
  # POURQUOI? La branche devient visible -> les autres savent que Bob
  # travaille sur l'issue #3. Sauvegarde en cas de crash machine.
  git push -u origin feature/3-filtrage-dates-tri

  # Sur GitHub: mettre le label "in progress" sur l'issue #3.

────────────────────────────────────────────────────────────────────────────────
  BOB — JOUR 1 AU 3: Développement avec commits atomiques
────────────────────────────────────────────────────────────────────────────────

  RÈGLE DES COMMITS ATOMIQUES:
  ──────────────────────────────
  Un commit = UNE modification logique. Exemples:
  [OK] "feat(routes): add date range filtering"  <- un seul sujet
  [OK] "test(routes): add tests for date filter" <- un seul sujet
  [X] "add filtering, fix bug, clean up code"   <- trop de sujets mélangés

  Avantage: chaque commit peut être reviewé, annulé (git revert),
  ou isolé (git bisect) indépendamment.

  # --- Commit 1: filtre par date ---
  git add app/routes.py
  git diff --staged   # TOUJOURS vérifier avant de committer
  git commit -m "feat(routes): add date range filtering to GET /tasks

  Add ?from=YYYY-MM-DD and ?to=YYYY-MM-DD query parameters.
  Both optional, combinable with existing filters.
  Returns 400 with clear message if date format invalid.
  Part of #3"

  # --- Commit 2: tri ---
  git add app/routes.py
  git commit -m "feat(routes): add sort/order parameters to GET /tasks

  Add ?sort=created_at|title and ?order=asc|desc.
  Whitelist validation: only allowed fields accepted.
  Default: sort=created_at, order=desc.
  Part of #3"

  # --- Commit 3: tests ---
  git add tests/test_routes.py
  git commit -m "test(routes): add tests for date filtering and sort

  - test_filter_by_date_range: valid dates
  - test_filter_invalid_date: 400 with clear message
  - test_sort_asc_desc: both directions
  - test_from_after_to: 400 edge case
  Coverage: routes.py 96%
  Part of #3"

  # Pousser régulièrement:
  git push origin feature/3-filtrage-dates-tri

────────────────────────────────────────────────────────────────────────────────
  BOB — JOUR 2 MATIN: Interruption urgente -> utiliser git stash
────────────────────────────────────────────────────────────────────────────────

  SITUATION:
  ──────────
  Bob a des modifications en cours dans routes.py (code à moitié écrit).
  Alice lui signale sur Slack: "Bug critique #7 dans develop, peux-tu corriger?"
  Bob ne peut pas changer de branche avec des modifications non commitées.

  POURQUOI git stash ET PAS git commit?
  ────────────────────────────────────────
  Un commit représente du code TERMINÉ et FONCTIONNEL.
  Du code à moitié écrit = mauvais commit.
  git stash = tiroir temporaire: on range, on traite l'urgence, on reprend.

  git status
  # -> modified: app/routes.py, app/utils.py

  # Mettre de côté avec un message descriptif:
  git stash save "WIP: validation tri paramètres - validation manquante"

  CE QUE VOUS VOYEZ:
  ───────────────────
  Saved working directory and index state On feature/3-filtrage-dates-tri:
  WIP: validation tri paramètres - validation manquante

  git status
  # -> nothing to commit, working tree clean   [OK]

  # Aller corriger le bug urgent:
  git checkout develop
  git pull origin develop
  git checkout -b fix/7-tags-vide-crash

  # [Bob corrige le bug]
  git add app/routes.py tests/test_routes.py
  git commit -m "fix(routes): handle empty list [] for tags in POST /tasks

  if not data.get('tags') was falsy for []. Fixed with isinstance check.
  Added regression test. Fixes #7"

  git push -u origin fix/7-tags-vide-crash
  # [PR ouverte -> Alice la merge rapidement]

  # REPRENDRE LE TRAVAIL SUR LA FEATURE:
  git checkout feature/3-filtrage-dates-tri

  # Restaurer le stash:
  git stash pop
  # "pop" = appliquer LE stash ET le supprimer de la liste

  CE QUE VOUS VOYEZ:
  ───────────────────
  On branch feature/3-filtrage-dates-tri
  Changes not staged for commit:
    modified: app/routes.py
    modified: app/utils.py
  Dropped stash@{0}
  # [OK] Le travail en cours est restauré exactement là où Bob s'était arrêté!

────────────────────────────────────────────────────────────────────────────────
  BOB — FIN JOUR 2: Mettre à jour la branche depuis develop (rebase)
────────────────────────────────────────────────────────────────────────────────

  SITUATION:
  ──────────
  Pendant que Bob était sur son fix, Alice a mergé 2 PRs dans develop.
  Bob doit intégrer ces nouveaux commits pour éviter des conflits plus tard.

  POURQUOI REBASE ET PAS MERGE?
  ───────────────────────────────
  MERGE dans la branche feature:
  -> Crée un commit de merge ("Merge develop into feature/...")
  -> L'historique devient "enchevêtré" avec des branches visuelles
  -> Bruyant et difficile à lire

  REBASE sur develop:
  -> Rejoue vos commits AU-DESSUS des nouveaux commits de develop
  -> L'historique reste parfaitement linéaire
  -> La PR sera plus propre à reviewer

  RÈGLE D'OR: ne rebaser que des branches locales/personnelles.
  Ne JAMAIS rebaser une branche que d'autres personnes utilisent.

  git fetch origin   # Télécharger sans intégrer

  # Voir ce qui a changé dans develop:
  git log HEAD..origin/develop --oneline
  # -> m3n4o5p fix(routes): handle empty list for tags (via PR)
  # -> l2m3n4o feat(models): add Task.updated_at field (Claire)

  git rebase origin/develop

  CE QUE VOUS VOYEZ (sans conflit):
  ───────────────────────────────────
  Successfully rebased and updated refs/heads/feature/3-filtrage-dates-tri.

  # Pousser (force-with-lease car l'historique a été réécrit):
  git push --force-with-lease origin feature/3-filtrage-dates-tri
  # --force-with-lease = force push SÉCURISÉ:
  # Vérifie qu'aucun autre push n'a eu lieu sur cette branche entre-temps.
  # Toujours préférer --force-with-lease à --force.

────────────────────────────────────────────────────────────────────────────────
  BOB — JOUR 3: Vérifications finales et ouverture de la Pull Request
────────────────────────────────────────────────────────────────────────────────

  VÉRIFICATIONS AVANT D'OUVRIR LA PR:
  ──────────────────────────────────────
  source venv/bin/activate

  # Tous les tests passent?
  pytest tests/ -v --cov=app --cov-report=term-missing
  # -> 18 passed, coverage 94%   [OK]

  # Code style correct?
  flake8 app/ tests/ --max-line-length=120
  # -> (aucune sortie = aucune erreur)   [OK]

  black --check app/ tests/
  # -> All done! *   [OK]

  # Tester manuellement les nouveaux endpoints:
  docker-compose up -d
  curl "http://localhost:5000/tasks?from=2024-01-01&to=2024-01-31"
  # -> {"tasks": [...], "total": 3, "page": 1, ...}   [OK]

  curl "http://localhost:5000/tasks?sort=title&order=asc"
  # -> {"tasks": [...en ordre alphabétique...]}   [OK]

  curl "http://localhost:5000/tasks?from=2024-13-01"   # Date invalide
  # -> {"error": "Invalid 'from' date format. Use YYYY-MM-DD"}, 400   [OK]

  # Pousser les derniers commits:
  git push origin feature/3-filtrage-dates-tri

  OUVRIR LA PULL REQUEST SUR GITHUB:
  ────────────────────────────────────
  # Sur GitHub: bannière "Compare & pull request" -> cliquer.
  # Ou via GitHub CLI:
  gh pr create \
    --base develop \
    --title "feat(routes): add date range filtering and sort to GET /tasks" \
    --body "Closes #3" \
    --reviewer alice-dev,claire-dev \
    --label "enhancement,flask" \
    --milestone "v1.0.0"

  # Remplir le template PR:
  # - Description: quoi et pourquoi
  # - Closes #3 (ferme l'issue automatiquement au merge)
  # - Cocher toutes les cases de la checklist
  # - Type: * Feature

  Body (template pré-rempli):
  ## Description
  Implémentation du filtrage des tâches via query parameters.
  - Filtrage par statut done/not done: ?done=true/false
  - Filtrage par plage de dates: ?from=YYYY-MM-DD&to=YYYY-MM-DD
  - Recherche dans le titre: ?search=keyword
  - Pagination: ?page=N&per_page=N

  ## Type de changement
  - [x] Nouvelle fonctionnalité (ajout non cassant)

  ## Tests
  - [x] J'ai ajouté des tests qui prouvent que la feature fonctionne
  - [x] Les tests existants passent avec mes changements
  - [x] Coverage: routes.py 96%

  ## Issues liées
  Closes #5

  Labels: enhancement, flask
  Assignees: @bob-dev
  Reviewers: @alice-dev, @claire-dev
  Milestone: v1.0.0

  -> Create pull request

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 2 — ALICE ET CLAIRE FONT LA CODE REVIEW DE LA PR DE BOB
════════════════════════════════════════════════════════════════════════════════

  POURQUOI LA CODE REVIEW?
  ─────────────────────────
  -> Deuxième paire d'yeux: on voit des bugs qu'on ne voit pas dans son propre code
  -> Partage de connaissance: Alice et Claire apprennent l'implémentation de Bob
  -> Qualité du code maintenu collectivement
  -> Bus factor réduit: si Bob part, les autres connaissent ce code

────────────────────────────────────────────────────────────────────────────────
  CLAIRE FAIT SA REVIEW
────────────────────────────────────────────────────────────────────────────────

  # CLAIRE TESTE LA PR EN LOCAL:
  git fetch origin
  git checkout feature/3-filtrage-dates-tri
  # Ou via CLI: gh pr checkout 8

  pytest tests/ -v   # -> 18 passed [OK]
  curl "http://localhost:5000/tasks?sort=password"   # Champ non autorisé

  # CLAIRE POSTE SES COMMENTAIRES SUR GITHUB (onglet "Files changed"):

  Commentaire sur routes.py (validation sort):
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ La validation du sort est bonne mais la SORTABLE_FIELDS devrait être  │
  │ une constante de classe ou une variable de module pour éviter de la   │
  │ redéfinir si on ajoute un 2ème endpoint de tri. Suggestion:           │
  │                                                                         │
  │ # En haut du fichier:                                                  │
  │ SORTABLE_FIELDS = frozenset({'created_at', 'updated_at', 'title'})   │
  └─────────────────────────────────────────────────────────────────────────┘

  Commentaire sur test_routes.py:
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ Manque un test pour ?from > ?to (date début après date fin).          │
  │ Je suggère de retourner 400 plutôt qu'une liste vide silencieuse.     │
  └─────────────────────────────────────────────────────────────────────────┘

  # Claire -> "Request changes"

────────────────────────────────────────────────────────────────────────────────
  BOB RÉPOND ET CORRIGE
────────────────────────────────────────────────────────────────────────────────

  git checkout feature/3-filtrage-dates-tri
  # [Bob fait les corrections demandées]

  git add app/routes.py tests/test_routes.py
  git commit -m "refactor(routes): address PR review feedback

  - Move SORTABLE_FIELDS to module-level frozenset constant
  - Add test and 400 response for ?from > ?to edge case

  Co-authored-by: Claire Leblanc <claire@equipe.com>"

  git push origin feature/3-filtrage-dates-tri

  # Sur GitHub: Bob résout chaque commentaire ("Resolve conversation")
  # et demande une nouvelle review ("Re-request review").

  ALICE ET CLAIRE APPROUVENT:
  ────────────────────────────
  Alice  -> "Approve" — "LGTM [OK] Corrections propres, whitelist en constante."
  Claire -> "Approve" — "Parfait. Tests couvrent le cas limite from>to. [OK]"

  # Tous les checks GitHub Actions sont verts:
  # [OK] ci/lint   [OK] ci/tests (22 passed)   [OK] 2 approvals

  ALICE MERGE LA PR (Squash and merge):
  ──────────────────────────────────────
  # POURQUOI Squash and merge?
  # Compresse tous les commits de la feature en 1 seul dans develop.
  # Develop reste propre: 1 commit = 1 feature complète.

  # Alice édite le message du squash:
  feat(routes): add date range filtering and sort to GET /tasks (#8)

  Add ?from, ?to, ?sort, ?order to GET /tasks.
  Whitelist validation for sort field.
  Returns 400 for invalid dates and from > to edge case.
  Co-authored-by: Bob Dupont <bob@equipe.com>
  Closes #3

  # -> "Confirm squash and merge"
  # -> GitHub ferme l'issue #3 automatiquement
  # -> GitHub propose "Delete branch" -> Alice clique (nettoyage)

  BOB NETTOIE SA BRANCHE LOCALE:
  ────────────────────────────────
  git checkout develop
  git pull origin develop   # Récupérer le squash commit

  git branch -d feature/3-filtrage-dates-tri
  # -d vérifie que la branche est bien mergée (sinon erreur -> utiliser -D)

  git fetch --prune   # Supprimer les références aux branches remote supprimées

  git branch -a
  # -> * develop    main    remotes/origin/develop    remotes/origin/main
  # feature/3 a disparu [OK]

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 3 — CLAIRE: DÉVELOPPEMENT PARALLÈLE ET RÉSOLUTION DE CONFLIT
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  En parallèle de Bob (Scénario 1), Claire améliore les tests (Issue #4).
  Elle modifie aussi app/routes.py pour ajouter des docstrings.
  Bob mergera sa PR en premier. Claire doit ensuite résoudre le conflit.

────────────────────────────────────────────────────────────────────────────────
  CLAIRE — Création de branche et développement
────────────────────────────────────────────────────────────────────────────────

  # Claire part de develop (même base que Bob au départ):
  git checkout develop
  git pull origin develop
  git checkout -b feature/4-amelioration-tests-coverage
  git push -u origin feature/4-amelioration-tests-coverage

  # [Claire ajoute des tests dans tests/test_models.py et conftest.py]
  git add tests/
  git commit -m "test: add comprehensive model and fixture tests

  - TestUser: creation, to_dict, constraints
  - TestTask: all fields, defaults, PRIORITY_VALUES
  - conftest: add user fixture for future auth tests
  Coverage: models.py 100% (+12%)"

  # [Claire modifie routes.py pour ajouter des docstrings]
  git add app/routes.py
  git commit -m "docs(routes): add comprehensive docstrings to route functions

  Includes parameter descriptions, return types and error codes
  for all endpoints. Compatible with Swagger auto-generation."

  # Entre-temps, Bob a mergé sa PR -> develop contient maintenant son code.
  # Bob a aussi modifié app/routes.py.
  # CONFLIT IMMINENT.

────────────────────────────────────────────────────────────────────────────────
  CLAIRE — Rebase et résolution du conflit
────────────────────────────────────────────────────────────────────────────────

  git fetch origin

  # Voir ce qui a changé dans develop:
  git log HEAD..origin/develop --oneline
  # -> q4r5s6t feat(routes): add date range filtering and sort (#8) [Bob]

  # Rebaser:
  git rebase origin/develop

  CE QUE VOUS VOYEZ:
  ───────────────────
  Auto-merging app/routes.py
  CONFLICT (content): Merge conflict in app/routes.py
  error: could not apply a1b2c3d... docs(routes): add comprehensive docstrings
  hint: Resolve all conflicts manually, then run "git rebase --continue".
  hint: Or run "git rebase --abort" to go back to before the rebase.

  git status
  # -> both modified: app/routes.py

  # CLAIRE OUVRE routes.py (avec VS Code ou nano) ET VOIT:
  @tasks_bp.route('/tasks', methods=['GET'])
  def get_tasks():
  <<<<<<< HEAD
      """Lister les tâches (version Bob avec filtres dates et tri)."""
      query = Task.query
      # [code de Bob avec filtres]
      from_date = request.args.get('from')
      if from_date:
          ...
  =======
      """Lister les tâches."""
      query = Task.query
      # [code original sans les filtres de Bob]
  >>>>>>> a1b2c3d (docs(routes): add comprehensive docstrings)

  # EXPLICATION:
  # <<<<<<< HEAD  = version dans develop (code de Bob + filtres)
  # ======= = séparateur
  # >>>>>>> = version du commit de Claire (sans les filtres de Bob)

  # RÉSOLUTION: garder le code de Bob (HEAD) ET améliorer sa docstring.
  # Claire supprime les marqueurs et combine les deux versions:

  @tasks_bp.route('/tasks', methods=['GET'])
  def get_tasks():
      """
      Lister les tâches avec filtres optionnels et pagination.

      Query parameters:
        done (bool):    true/false — filtrer par statut
        priority (str): low/medium/high — filtrer par priorité
        search (str):   texte à chercher dans le titre
        from (str):     YYYY-MM-DD — tâches créées après cette date
        to (str):       YYYY-MM-DD — tâches créées avant cette date
        sort (str):     created_at|updated_at|title — champ de tri
        order (str):    asc|desc — sens du tri (défaut: desc)
        page (int):     numéro de page (défaut: 1)
        per_page (int): résultats par page (défaut: 20, max: 100)

      Returns:
        200: {tasks, total, page, per_page, pages}
        400: si format de date invalide ou from > to
      """
      query = Task.query
      # [code complet de Bob avec filtres, intact]

  # Vérifier qu'aucun marqueur ne reste:
  grep -n "<<<<<<\|=======\|>>>>>>" app/routes.py
  # -> (aucune sortie = propre)   [OK]

  git add app/routes.py
  git rebase --continue
  # Git ouvre nano pour éditer le message du commit -> améliorer et sauvegarder.

  CE QUE VOUS VOYEZ:
  ───────────────────
  Successfully rebased and updated refs/heads/feature/4-amelioration-tests-coverage.

  pytest tests/ -v
  # -> 24 passed   [OK]

  git push --force-with-lease origin feature/4-amelioration-tests-coverage
  # [Claire ouvre sa PR -> review -> merge dans develop]

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 4 — ALICE: HOTFIX URGENT EN PRODUCTION (Issue #15)
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  La v1.0.0 est déployée. Alerte 16h: "DELETE /tasks/99 retourne 500
  au lieu de 404 quand la tâche n'existe pas."
  Bug critique: les clients voient des erreurs serveur.

  POURQUOI UNE BRANCHE hotfix/ DEPUIS main ET PAS DEPUIS develop?
  ─────────────────────────────────────────────────────────────────
  develop contient des features en cours (incomplètes).
  Merger develop -> main déploierait ces features non finies. DANGEREUX.
  Un hotfix part de MAIN (état exact de la prod), corrige SEULEMENT le bug.

  ALICE:
  ───────
  git checkout main
  git pull origin main

  git checkout -b hotfix/15-delete-404-retourne-500

  # ALICE CORRIGE:
  nano app/__init__.py
  # Ajouter les gestionnaires d'erreur JSON:
  @app.errorhandler(404)
  def not_found(error):
      return jsonify({'error': 'Resource not found', 'code': 404}), 404

  @app.errorhandler(405)
  def method_not_allowed(error):
      return jsonify({'error': 'Method not allowed', 'code': 405}), 405

  @app.errorhandler(500)
  def internal_error(error):
      db.session.rollback()
      return jsonify({'error': 'Internal server error', 'code': 500}), 500

  # Test de régression:
  nano tests/test_routes.py
  # Ajouter:
  def test_delete_nonexistent_returns_json_404(self, client):
      r = client.delete('/tasks/99999')
      assert r.status_code == 404
      assert r.content_type == 'application/json'
      assert r.json['error'] == 'Resource not found'

  pytest tests/ -v -k "delete"
  # -> 2 passed   [OK]

  git add app/__init__.py tests/test_routes.py
  git commit -m "fix: return JSON error responses for 404/405/500

  Flask default handlers returned HTML.
  All error handlers now return consistent JSON format.
  Added regression test for DELETE non-existent resource.
  Fixes #15"

  git push -u origin hotfix/15-delete-404-retourne-500

  # PR vers main -> review express de Bob -> Merge:
  gh pr create --base main --title "fix: JSON error handlers (URGENT)" \
    --body "Fixes #15" --label "bug,priority: high"

  # Alice merge dans main, crée le tag v1.0.1:
  git checkout main && git pull origin main
  git tag -a v1.0.1 -m "v1.0.1 — Hotfix: JSON error handlers. Fixes #15"
  git push origin v1.0.1

  # IMPORTANT: merger aussi dans develop!
  git checkout develop
  git pull origin develop
  git merge --no-ff hotfix/15-delete-404-retourne-500 \
    -m "Merge hotfix/15 into develop"
  git push origin develop

  # Nettoyer la branche hotfix:
  git branch -d hotfix/15-delete-404-retourne-500
  git push origin --delete hotfix/15-delete-404-retourne-500

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 5 — ALICE: PRÉPARER ET DÉPLOYER LA RELEASE v1.0.0
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Toutes les issues du milestone v1.0.0 sont fermées.
  develop est stable, les 24 tests passent.
  C'est l'heure de déployer en production.

  POURQUOI UNE BRANCHE release/ ET PAS MERGER develop DIRECTEMENT?
  ─────────────────────────────────────────────────────────────────
  -> Derniers ajustements (numéro de version, CHANGELOG) sans bloquer develop
  -> develop peut continuer à recevoir des features v1.1.0 en parallèle
  -> "Gel de release": période de stabilisation avant la prod

────────────────────────────────────────────────────────────────────────────────
  ALICE — Créer la branche de release
────────────────────────────────────────────────────────────────────────────────

  git checkout develop
  git pull origin develop

  # Vérifier que tout est bon:
  pytest tests/ -v
  # -> 24 passed   [OK]

  # Aucune issue v1.0.0 encore ouverte?
  gh issue list --milestone "v1.0.0" --state open
  # -> No issues found   [OK]

  git checkout -b release/1.0.0
  git push -u origin release/1.0.0

  # Mettre à jour le numéro de version:
  nano config.py
  # APP_VERSION = "1.0.0"

  # Créer/mettre à jour CHANGELOG.md:
  nano CHANGELOG.md

  Contenu:
  # Changelog — TaskManager API

  ## [1.0.0] — 2024-02-01 — MVP

  ### Nouvelles fonctionnalités
  - CRUD complet des tâches (GET/POST/PUT/DELETE /tasks)
  - Filtrage: done, priority, search, plage de dates
  - Tri: sort (created_at/title), order (asc/desc)
  - Pagination configurable (page, per_page)
  - Endpoint /health avec vérification de la base de données
  - Gestionnaires d'erreur JSON (404/405/500)

  ### Tests
  - 24 tests, couverture 94%

  ### Infrastructure
  - Dockerfiles multi-stages (dev/test/prod)
  - Docker Compose dev/test/prod
  - GitHub Actions CI/CD
  - Protection des branches main et develop

  git add config.py CHANGELOG.md
  git commit -m "chore(release): prepare v1.0.0

  - Bump APP_VERSION to 1.0.0
  - Update CHANGELOG.md with all v1.0.0 changes"
  git push origin release/1.0.0

────────────────────────────────────────────────────────────────────────────────
  ALICE — Merger dans main, tagger, déployer
────────────────────────────────────────────────────────────────────────────────

  # Merger release -> main:
  git checkout main
  git pull origin main
  git merge --no-ff release/1.0.0 -m "chore: release v1.0.0"
  git push origin main

  # Créer le tag annoté:
  git tag -a v1.0.0 -m "Release v1.0.0 — MVP TaskManager API

  Features: CRUD, filtrage, tri, pagination, /health, erreurs JSON
  Tests: 24 passed, coverage 94%
  Docker: image multi-stage ~185MB"

  git push origin v1.0.0
  # -> Déclenche le workflow CD de GitHub Actions!
  # -> Build de l'image Docker, push sur ghcr.io, déploiement automatique.

  # Merger release -> develop aussi:
  git checkout develop
  git merge --no-ff release/1.0.0 -m "chore: merge release/1.0.0 back into develop"
  git push origin develop

  # Créer la release GitHub:
  gh release create v1.0.0 \
    --title "v1.0.0 — MVP TaskManager API [BRAVO]" \
    --notes-file CHANGELOG.md \
    --target main

  # Nettoyer:
  git branch -d release/1.0.0
  git push origin --delete release/1.0.0

================================================================================
PARTIE 4 — CI/CD: TESTS AUTOMATIQUES ET DÉPLOIEMENT EN PRODUCTION
================================================================================

  POURQUOI CI/CD?
  ────────────────
  Sans CI/CD:
  -> Bob pousse du code cassé sur develop -> les autres le clonent
  -> Déploiement = SSH sur le serveur, commandes à la main -> erreurs humaines
  -> Personne ne vérifie le style de code systématiquement

  Avec CI/CD:
  -> Chaque push = tests automatiques -> le code cassé est détecté immédiatement
  -> Chaque tag version = déploiement automatique en production
  -> Alice n'a jamais besoin de se connecter au serveur pour déployer

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.1 — CONFIGURER LES SECRETS GITHUB ACTIONS
────────────────────────────────────────────────────────────────────────────────

  ACCÈS:
  Settings -> Secrets and variables -> Actions -> New repository secret

  Secrets à créer:
  ────────────────
  PROD_HOST        -> IP ou domaine du serveur de production
  PROD_USER        -> Utilisateur SSH (ex: ubuntu)
  PROD_SSH_KEY     -> Clé privée SSH pour se connecter au serveur
  PROD_SECRET_KEY  -> SECRET_KEY Flask pour la production
  PROD_DB_PASSWORD -> Mot de passe PostgreSQL production
  SLACK_WEBHOOK_URL -> URL webhook Slack pour les notifications

  # Générer la clé SSH pour GitHub Actions:
  ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key -N ""
  # -N "" = pas de passphrase (Actions ne peut pas saisir de passphrase)
  # Ajouter deploy_key.pub dans ~/.ssh/authorized_keys sur le serveur prod
  # Mettre le contenu de deploy_key dans le secret PROD_SSH_KEY

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.2 — WORKFLOW CI (TESTS SUR CHAQUE PUSH ET PR)
────────────────────────────────────────────────────────────────────────────────

  ALICE CRÉE .github/workflows/ci.yml:
  ──────────────────────────────────────
  nano .github/workflows/ci.yml

  Contenu:
  name: CI — Lint et Tests

  on:
    push:
      branches: ['**']           # Chaque push sur n'importe quelle branche
    pull_request:
      branches: [main, develop]  # PRs vers main ou develop

  jobs:

    lint:
      name: Lint (flake8 + black)
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4

        - uses: actions/setup-python@v5
          with:
            python-version: '3.11'
            cache: 'pip'

        - run: pip install black flake8

        - name: Vérifier le formatage (black)
          run: black --check app/ tests/

        - name: Lint (flake8)
          run: flake8 app/ tests/ --max-line-length=120

    tests:
      name: Tests (pytest)
      runs-on: ubuntu-latest
      needs: lint   # Ne démarre que si lint réussit

      services:
        postgres:
          image: postgres:15-alpine
          env:
            POSTGRES_DB:       taskmanager_test
            POSTGRES_USER:     testuser
            POSTGRES_PASSWORD: testpassword
          options: >-
            --health-cmd pg_isready
            --health-interval 10s
            --health-retries 5
          ports: ['5432:5432']

        redis:
          image: redis:7-alpine
          options: >-
            --health-cmd "redis-cli ping"
            --health-interval 10s
          ports: ['6379:6379']

      steps:
        - uses: actions/checkout@v4

        - uses: actions/setup-python@v5
          with:
            python-version: '3.11'
            cache: 'pip'

        - run: pip install -r requirements.txt -r requirements-dev.txt

        - name: Créer le .env de test
          run: |
            cat > .env << 'EOF'
            FLASK_ENV=testing
            SECRET_KEY=ci-test-key-not-for-prod
            DATABASE_URL=postgresql://testuser:testpassword@localhost:5432/taskmanager_test
            REDIS_URL=redis://localhost:6379/0
            EOF

        - name: Lancer les tests avec couverture
          run: |
            pytest tests/ -v \
              --cov=app \
              --cov-report=xml \
              --cov-report=term-missing \
              --junit-xml=test-results.xml

        - name: Publier les résultats de tests
          uses: dorny/test-reporter@v1
          if: always()
          with:
            name: Résultats Pytest
            path: test-results.xml
            reporter: java-junit

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.3 — WORKFLOW CD (BUILD DOCKER + DÉPLOIEMENT PROD)
────────────────────────────────────────────────────────────────────────────────

  ALICE CRÉE .github/workflows/deploy.yml:
  ──────────────────────────────────────────
  nano .github/workflows/deploy.yml

  Contenu:
  name: CD — Build Docker et Déploiement Production

  on:
    push:
      tags: ['v*.*.*']   # Seulement sur les tags de version

  env:
    IMAGE: ghcr.io/equipe/taskmanager-api

  jobs:

    build-and-push:
      name: Build et Push image Docker
      runs-on: ubuntu-latest
      permissions:
        contents: read
        packages: write
      outputs:
        tags: ${{ steps.meta.outputs.tags }}

      steps:
        - uses: actions/checkout@v4

        - uses: docker/login-action@v3
          with:
            registry: ghcr.io
            username: ${{ github.actor }}
            password: ${{ secrets.GITHUB_TOKEN }}

        - id: meta
          uses: docker/metadata-action@v5
          with:
            images: ${{ env.IMAGE }}
            tags: |
              type=semver,pattern={{version}}
              type=semver,pattern={{major}}.{{minor}}
              type=raw,value=latest
              type=sha,prefix=sha-

        - uses: docker/setup-buildx-action@v3

        - uses: docker/build-push-action@v5
          with:
            context: .
            file: docker/Dockerfile
            target: production
            push: true
            tags: ${{ steps.meta.outputs.tags }}
            cache-from: type=gha
            cache-to:   type=gha,mode=max
            build-args: APP_VERSION=${{ github.ref_name }}

    deploy-production:
      name: Déploiement Production
      runs-on: ubuntu-latest
      needs: build-and-push
      environment:
        name: production
        url: https://api.taskmanager.equipe.com/health

      steps:
        - uses: appleboy/ssh-action@v1
          with:
            host:     ${{ secrets.PROD_HOST }}
            username: ${{ secrets.PROD_USER }}
            key:      ${{ secrets.PROD_SSH_KEY }}
            script: |
              set -e
              cd /opt/taskmanager

              echo "${{ secrets.GITHUB_TOKEN }}" | \
                docker login ghcr.io -u ${{ github.actor }} --password-stdin

              docker pull ghcr.io/equipe/taskmanager-api:${{ github.ref_name }}
              docker-compose -f docker-compose.prod.yml up -d --no-deps api

              sleep 15

              STATUS=$(curl -sf http://localhost/api/health | \
                python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")

              if [ "$STATUS" = "healthy" ]; then
                echo "[OK] Déploiement ${{ github.ref_name }} réussi!"
              else
                echo "[X] Santé API échouée. Rollback..."
                docker-compose -f docker-compose.prod.yml rollback
                exit 1
              fi

        - name: Notifier Slack
          if: always()
          uses: 8398a7/action-slack@v3
          with:
            status: ${{ job.status }}
            text: "${{ job.status == 'success' && '[OK]' || '[X]' }} Déploiement *${{ github.ref_name }}*"
          env:
            SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

  # Commiter et pousser les workflows:
  git add .github/workflows/
  git commit -m "ci: add CI (lint+tests) and CD (docker+deploy) workflows"
  git push origin develop

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 4.4 — CONFIGURATION DU SERVEUR DE PRODUCTION
════════════════════════════════════════════════════════════════════════════════

  SUR LE SERVEUR UBUNTU (Alice se connecte via SSH):
  ────────────────────────────────────────────────────

  # Installer Docker:
  curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh
  sudo usermod -aG docker ubuntu && newgrp docker
  sudo apt-get install -y docker-compose-plugin

  # Créer le dossier de l'app:
  sudo mkdir -p /opt/taskmanager && sudo chown ubuntu:ubuntu /opt/taskmanager
  cd /opt/taskmanager

  # Créer le .env de production (JAMAIS dans Git!):
  nano .env.prod
  # Contenu:
  FLASK_ENV=production
  SECRET_KEY=GENEREE-AVEC-openssl-rand-hex-32-tres-longue
  POSTGRES_DB=taskmanager
  POSTGRES_USER=taskmanager
  POSTGRES_PASSWORD=MOT-DE-PASSE-FORT-UNIQUE-POUR-PROD
  DATABASE_URL=postgresql://taskmanager:MOT-DE-PASSE@postgres:5432/taskmanager
  REDIS_URL=redis://redis:6379/0
  APP_VERSION=1.0.0

  chmod 600 .env.prod

  # Créer docker-compose.prod.yml:
  cat > docker-compose.prod.yml << 'EOF'
  version: '3.8'

  services:
    nginx:
      image: nginx:1.25-alpine
      ports: ['80:80', '443:443']
      volumes:
        - ./nginx/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro
        - ./nginx/ssl:/etc/nginx/ssl:ro
      depends_on: [api]
      restart: unless-stopped

    api:
      image: ghcr.io/equipe/taskmanager-api:latest
      env_file: .env.prod
      depends_on: [postgres, redis]
      restart: unless-stopped
      healthcheck:
        test: ['CMD', 'curl', '-f', 'http://localhost:5000/health']
        interval: 30s
        timeout: 10s
        retries: 3

    postgres:
      image: postgres:15-alpine
      env_file: .env.prod
      volumes:
        - postgres_data:/var/lib/postgresql/data
      restart: unless-stopped

    redis:
      image: redis:7-alpine
      command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
      volumes:
        - redis_data:/data
      restart: unless-stopped

  volumes:
    postgres_data:
    redis_data:
  EOF

  # Premier déploiement manuel:
  docker login ghcr.io -u alice-dev
  docker-compose -f docker-compose.prod.yml up -d

  curl http://localhost/api/health
  # -> {"status": "healthy", "database": "ok"}   [OK]

================================================================================
PARTIE 5 — FONCTIONNALITÉS GIT AVANCÉES
================================================================================

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 6 — BOB: TROUVER UN BUG AVEC git bisect
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Un utilisateur signale: "GET /tasks?priority=high ne retourne rien."
  Il y a 15 commits depuis la dernière version qui fonctionnait.
  Lequel a introduit le bug?

  POURQUOI git bisect?
  ─────────────────────
  15 commits = potentiellement des centaines de lignes à inspecter.
  Bisect = recherche binaire: 15 commits -> 4 tests max pour trouver le coupable!

  BOB:
  ─────
  git bisect start
  git bisect bad HEAD                # HEAD = le bug existe ici
  git bisect good v1.0.0             # v1.0.0 = pas de bug ici

  CE QUE VOUS VOYEZ:
  ───────────────────
  Bisecting: 7 revisions left to test after this (roughly 3 steps)
  [m3n4o5p] feat(routes): add date range filtering

  # Git se place au commit du milieu. Bob teste:
  pytest tests/ -k "priority" -v
  # -> PASSED (pas de bug ici)
  git bisect good

  # Git va dans la 2ème moitié...
  # [p4q5r6s] refactor(models): update Task.priority default value
  pytest tests/ -k "priority" -v
  # -> FAILED (bug ici!)
  git bisect bad

  # Encore une itération...
  git bisect good   # ou bad selon le résultat

  CE QUE VOUS VOYEZ FINALEMENT:
  ──────────────────────────────
  p4q5r6s is the first bad commit
  Author: Alice Martin <alice@equipe.com>
      refactor(models): update Task.priority default value

  git show p4q5r6s
  # -> Alice avait changé 'medium' en 'Medium' (majuscule)!
  # -> La comparaison avec 'high' échoue -> filtre retourne rien

  git bisect reset   # Revenir à HEAD

  # Bob ouvre l'issue et prépare le fix:
  git checkout -b fix/16-priority-case-sensitive
  # [Corriger la casse dans models.py]
  git commit -m "fix(models): lowercase priority default value

  Changed 'Medium' back to 'medium'. Priority comparison is case-sensitive.
  Fixes #16"

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 7 — ALICE: NETTOYER L'HISTORIQUE AVEC git rebase -i
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Alice a 7 commits sur feature/5-docker dont plusieurs "WIP" et corrections
  de typos. Elle veut nettoyer avant d'ouvrir la PR.

  POURQUOI NETTOYER?
  ───────────────────
  Les reviewers lisent chaque commit. Un historique propre facilite la review.
  L'historique de develop sera plus lisible pour l'avenir.

  L'HISTORIQUE ACTUEL:
  ─────────────────────
  git log --oneline
  # -> s7t8u9v fix typo in nginx.conf
  # -> r6s7t8u WIP: docker prod presque fini
  # -> q5r6s7t add docker-compose.prod.yml
  # -> p4q5r6s fix docker-compose.yml env vars
  # -> o3p4q5r WIP: docker-compose first draft
  # -> n2o3p4q add Dockerfile production
  # -> m1n2o3p add Dockerfile.dev
  # -> [commit develop]

  git rebase -i HEAD~7
  # Git ouvre nano:

  pick m1n2o3p add Dockerfile.dev
  pick n2o3p4q add Dockerfile production
  pick o3p4q5r WIP: docker-compose first draft
  pick p4q5r6s fix docker-compose.yml env vars
  pick q5r6s7t add docker-compose.prod.yml
  pick r6s7t8u WIP: docker prod presque fini
  pick s7t8u9v fix typo in nginx.conf

  # Alice modifie:
  pick m1n2o3p add Dockerfile.dev
  squash n2o3p4q add Dockerfile production
  # -> Les 2 Dockerfiles en 1 commit

  pick o3p4q5r WIP: docker-compose first draft
  fixup p4q5r6s fix docker-compose.yml env vars
  squash q5r6s7t add docker-compose.prod.yml
  fixup r6s7t8u WIP: docker prod presque fini
  fixup s7t8u9v fix typo in nginx.conf
  # -> Les 5 docker-compose en 1 commit

  # Sauvegarder -> Git demande les messages des squash commits:

  # Message commit 1:
  feat(docker): add multi-stage Dockerfiles for dev and production

  - Dockerfile.dev: hot-reload, outils de dev
  - Dockerfile: multi-stage, image ~185MB, non-root user

  # Message commit 2:
  feat(docker): add Docker Compose configurations

  - docker-compose.yml: dev (Flask + PostgreSQL + Redis + pgAdmin)
  - docker-compose.test.yml: CI/CD avec base de test isolée
  - docker-compose.prod.yml: avec Nginx reverse proxy

  # RÉSULTAT:
  git log --oneline
  # -> v2u3v4w feat(docker): add Docker Compose configurations
  # -> u1v2w3x feat(docker): add multi-stage Dockerfiles
  # -> [commit develop]
  # 7 commits -> 2 commits propres [OK]

  git push --force-with-lease origin feature/5-docker

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 8 — CLAIRE: TRAVAILLER EN PARALLÈLE AVEC git worktree
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Claire travaille sur feature/4 (modifications nombreuses, pas de stash possible).
  Alice lui demande de corriger un bug urgent sur develop en parallèle.

  POURQUOI git worktree?
  ───────────────────────
  Un seul dépôt Git mais DEUX dossiers de travail.
  Pas de stash nécessaire. Pas de perte de contexte.
  Les deux dossiers partagent le même .git/ et le même historique.

  CLAIRE:
  ────────
  # Créer un 2ème dossier de travail sur develop:
  git worktree add ../taskmanager-hotfix develop

  # Résultat:
  ls ~
  # -> taskmanager/           (feature/4 — en cours)
  # -> taskmanager-hotfix/    (develop — pour le fix urgent)

  # Terminal 1 — continuer feature/4 normalement
  # Terminal 2:
  cd ~/taskmanager-hotfix
  git checkout -b fix/17-conftest-scope-bug
  # [Corriger le bug]
  git commit -m "fix(tests): correct session scope in conftest"
  git push -u origin fix/17-conftest-scope-bug
  # [PR -> merge]

  # Quand le fix est terminé:
  cd ~/taskmanager
  git worktree remove ../taskmanager-hotfix

  # [OK] Claire reprend feature/4 exactement là où elle s'était arrêtée.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 9 — BOB: RÉCUPÉRER UN COMMIT PERDU AVEC git reflog
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Bob a accidentellement fait git reset --hard HEAD~3 et a perdu 3 commits.
  Panique! Son travail de 2 heures a disparu.

  POURQUOI git reflog PEUT TOUT RÉCUPÉRER?
  ──────────────────────────────────────────
  Git ne supprime jamais immédiatement les données.
  Le reflog = journal de TOUS les déplacements de HEAD sur votre machine.
  Même après un reset --hard, les commits existent encore dans .git/objects.
  git reflog permet de les retrouver et de les restaurer.

  BOB:
  ─────
  # Voir le reflog:
  git reflog

  CE QUE VOUS VOYEZ:
  ───────────────────
  a3f8c1d HEAD@{0}: reset: moving to HEAD~3        <- le reset accidentel
  b7d2e4f HEAD@{1}: commit: test: add tests for sort
  c8f2d1a HEAD@{2}: commit: feat: add sort parameter
  d9g3e2b HEAD@{3}: commit: feat: add date range filter
  e0h4f3c HEAD@{4}: checkout: moving from develop to feature/3

  # Les commits b7d2e4f, c8f2d1a, d9g3e2b existent encore!

  # Récupérer en revenant à l'état avant le reset:
  git reset --hard b7d2e4f
  # Ou: git reset --hard HEAD@{1}

  CE QUE VOUS VOYEZ:
  ───────────────────
  HEAD is now at b7d2e4f test: add tests for sort

  git log --oneline -5
  # -> b7d2e4f test: add tests for sort
  # -> c8f2d1a feat: add sort parameter
  # -> d9g3e2b feat: add date range filter
  # Les 3 commits sont de retour!   [OK]

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 10 — ALICE: APPLIQUER UN SEUL COMMIT avec git cherry-pick
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Claire a fait un commit dans develop qui corrige un bug de validation
  que main a aussi. Alice veut appliquer CE SEUL commit sur main
  sans merger tout develop (develop a des features incomplètes).

  POURQUOI cherry-pick ET PAS merge?
  ────────────────────────────────────
  merge = toute la branche source.
  cherry-pick = seulement le ou les commits spécifiés.

  ALICE:
  ───────
  # Trouver le hash du commit de Claire dans develop:
  git log develop --oneline | grep "fix(routes)"
  # -> e5f6g7h fix(routes): validate description field type

  # Se placer sur main:
  git checkout main

  # Appliquer seulement ce commit:
  git cherry-pick e5f6g7h

  CE QUE VOUS VOYEZ:
  ───────────────────
  [main k9l0m1n] fix(routes): validate description field type
   1 file changed, 4 insertions(+)

  # Le commit a été "copié" sur main avec un nouveau hash.
  git push origin main

  # Note: ce fix est maintenant dans main ET dans develop.
  # Pas de duplication problématique car Git gère les contenus identiques.

================================================================================
PARTIE 6 — TABLEAUX DE BORD ET RESPONSABILITÉS
================================================================================

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  ALICE — Lead dev / DevOps / Responsable Git-GitHub                    │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS (setup initial):                                   │
  │  [OK] Crée et configure le dépôt GitHub (privé)                           │
  │  [OK] Protège main (2 approbations, GPG, SonarQube)                       │
  │  [OK] Protège develop (1 approbation, CI tests)                           │
  │  [OK] Crée labels (19 labels catégorisés)                                 │
  │  [OK] Crée milestones (v1.0.0 -> v2.0.0)                                  │
  │  [OK] Crée les templates PR/Issues                                         │
  │  [OK] Configure CODEOWNERS                                                 │
  │  [OK] Configure secrets GitHub Actions                                     │
  │  [OK] Configure environment "production" avec protection                  │
  │  [OK] Configure le serveur de production (Docker, Nginx, .env)            │
  │  [OK] Crée les workflows CI (ci.yml) et CD (deploy.yml)                   │
  │                                                                          │
  │  FAIT À CHAQUE SPRINT:                                                  │
  │  [OK] Crée les issues + labels + milestone pour le sprint                 │
  │  [OK] Review des PRs de Bob et Claire                                     │
  │  [OK] Merge les PRs validées dans develop                                 │
  │  [OK] Gère les hotfixes si bug critique en prod                           │
  │  [OK] Prépare la release (branche release/, CHANGELOG, tag)               │
  │  [OK] Merge release dans main (déploiement) et develop                    │
  │  [OK] Crée la GitHub Release officielle                                   │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES PAR ALICE:                               │
  │  git checkout main && git pull && git merge release/X                  │
  │  git tag -a vX.X.X && git push origin vX.X.X                          │
  │  gh pr merge [id] --squash                                              │
  │  git checkout develop && git merge --no-ff hotfix/X                    │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  BOB — Développeur backend / Features métier                           │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS (setup):                                           │
  │  [OK] Installe Git, configure identité et SSH                             │
  │  [OK] Clone le dépôt et crée la branche develop locale                   │
  │  [OK] Configure son .env local et son environnement Python                │
  │                                                                          │
  │  FAIT À CHAQUE FEATURE (cycle complet):                                │
  │  [OK] git checkout develop && git pull origin develop                     │
  │  [OK] git checkout -b feature/N-description                               │
  │  [OK] git push -u origin feature/N-description                            │
  │  [OK] [développer avec commits atomiques: git add + git commit]          │
  │  [OK] git stash si interruption urgente                                    │
  │  [OK] git fetch && git rebase origin/develop (mise à jour régulière)      │
  │  [OK] pytest + flake8 + black avant la PR                                 │
  │  [OK] gh pr create (ou interface web GitHub)                               │
  │  [OK] Répondre aux commentaires de review                                 │
  │  [OK] Après merge: git branch -d feature/N && git fetch --prune           │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES PAR BOB:                                 │
  │  git checkout develop && git pull                                        │
  │  git checkout -b feature/N-xxx                                          │
  │  git add [fichiers] && git commit -m "type(scope): description"        │
  │  git push / git push --force-with-lease                                 │
  │  git stash save "WIP: description" / git stash pop                     │
  │  git rebase origin/develop                                              │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  CLAIRE — Développeuse backend / Responsable qualité et tests          │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  MÊME CYCLE DE FEATURE QUE BOB +                                       │
  │                                                                          │
  │  RESPONSABILITÉS SPÉCIFIQUES:                                          │
  │  [OK] Review des PRs: tester en local, commenter avec du code concret     │
  │  [OK] S'assurer que coverage ne régresse jamais en dessous de 90%         │
  │  [OK] Maintenir tests/conftest.py et les fixtures partagées               │
  │  [OK] Signaler les problèmes de qualité via issues ou commentaires PR     │
  │                                                                          │
  │  COMMANDES SPÉCIFIQUES DE CLAIRE:                                      │
  │  pytest tests/ -v --cov=app --cov-report=html (rapport HTML)           │
  │  gh pr checkout [id]   (tester une PR localement)                      │
  │  git worktree add (travailler sur 2 branches simultanément)            │
  └──────────────────────────────────────────────────────────────────────────┘

================================================================================
PARTIE 7 — GESTION DES PROJETS GITHUB: KANBAN, DISCUSSIONS, WIKI
================================================================================

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 7.1 — ALICE CRÉE UN PROJET KANBAN
════════════════════════════════════════════════════════════════════════════════

  POURQUOI UN PROJET KANBAN?
  ───────────────────────────
  Les issues et PRs sont des listes. Un Kanban = visualisation par état.
  L'équipe voit en un coup d'œil: "Qu'est-ce qui est en cours? Qu'est-ce qui
  est bloqué? Qu'est-ce qui est prêt à merger?"

  ACCÈS:
  github.com/equipe/taskmanager -> Projects -> New project -> Board

  COLONNES À CRÉER:
  ──────────────────
  [ENTREE] Backlog     -> Issues créées, pas encore planifiées pour ce sprint
  [OBJECTIF] To Do       -> Issues planifiées pour ce sprint, non commencées
  [OUTIL] In Progress -> Issues en cours de développement (quelqu'un a la branche)
  [RECHERCHE] In Review   -> PR ouverte, en attente de review
  [OK] Done        -> Mergé dans develop ce sprint

  AUTOMATISATIONS:
  ─────────────────
  Settings du projet -> Workflows:
  - "Item added to project" -> Status: Backlog
  - "Item reopened" -> Status: To Do
  - "Item closed" -> Status: Done
  - "Pull request merged" -> Status: Done

  Manuellement: Bob glisse l'issue #3 de "To Do" vers "In Progress"
  quand il crée sa branche. Claire glisse vers "In Review" quand la PR est ouverte.

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 7.2 — UTILISER LES DISCUSSIONS GITHUB
════════════════════════════════════════════════════════════════════════════════

  POURQUOI DISCUSSIONS ET PAS ISSUES?
  ─────────────────────────────────────
  Issue -> problème à résoudre ou tâche à faire -> se ferme quand c'est résolu.
  Discussion -> échange d'idées, questions, décisions d'architecture -> reste ouverte.

  Exemples de discussions utiles pour l'équipe:
  -> "Doit-on utiliser JWT ou sessions pour l'auth en v1.1?" (Architecture)
  -> "Comment on gère les tests d'intégration avec PostgreSQL réel?" (Q&A)
  -> "Partager nos alias Git utiles" (General)

  ACCÈS: github.com/equipe/taskmanager -> Discussions -> New discussion

  CATÉGORIES À CRÉER:
  ────────────────────
  [ANNONCE] Announcements -> Alice annonce les releases, changements importants
  [SPEECH_BALLOON] General       -> Discussion libre
  [HAPPY_PERSON_RAISING_ONE_HAND] Q&A          -> Questions techniques (réponses marquées comme "réponses")
  [IDEE] Ideas        -> Propositions de features futures
  [CONSTRUCTION] Architecture  -> Décisions techniques importantes (ADR)

  EXEMPLE D'ADR (Architecture Decision Record) EN DISCUSSION:
  ─────────────────────────────────────────────────────────────
  Title: [ADR-001] Choisir entre JWT et Sessions Flask pour l'authentification

  ## Contexte
  On développe l'authentification en v1.1. Deux approches possibles:
  JWT (stateless) ou Sessions Flask (stateful avec Redis).

  ## Options considérées

  **Option A: JWT (JSON Web Tokens)**
  - [OK] Stateless: pas de stockage serveur nécessaire
  - [OK] Fonctionne avec des clients mobiles
  - [X] Révocation difficile (il faut une liste noire)
  - [X] Si la clé secrète est compromise: tous les tokens sont invalides

  **Option B: Sessions Flask + Redis**
  - [OK] Révocation immédiate (supprimer la session de Redis)
  - [OK] Intégration naturelle avec Flask
  - [X] Stateful: Redis requis, overhead réseau
  - [X] Ne scale pas horizontalement sans Redis partagé

  ## Décision
  JWT pour v1.1 (API REST pure, pas de frontend web nécessitant des cookies).
  Revoir en v2.0 si on ajoute un dashboard web.

  [L'équipe vote avec des emoji [BIEN][MAL] et commente]

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 7.3 — WIKI GITHUB
════════════════════════════════════════════════════════════════════════════════

  POURQUOI UN WIKI?
  ──────────────────
  Le README = première impression du projet pour un nouveau venu.
  Le Wiki = documentation technique détaillée (installation, architecture, APIs).

  ACCÈS: github.com/equipe/taskmanager -> Wiki -> Create the first page

  STRUCTURE DU WIKI DE L'ÉQUIPE:
  ────────────────────────────────
  Home               -> Vue d'ensemble, liens rapides vers les autres pages
  Installation       -> Prérequis, installation étape par étape
  Architecture       -> Diagramme des composants, choix techniques
  API Reference      -> Documentation de tous les endpoints
  Git Workflow       -> Comment travailler avec Git dans cette équipe
  Déploiement        -> Comment déployer une nouvelle version
  Troubleshooting    -> Problèmes fréquents et leurs solutions

  CLONER LE WIKI EN LOCAL (pour éditer avec son éditeur):
  ─────────────────────────────────────────────────────────
  git clone git@github.com:equipe/taskmanager.wiki.git
  # Éditer les fichiers .md dans son éditeur
  git add . && git commit -m "docs: add API reference page"
  git push

================================================================================
PARTIE 8 — SÉCURITÉ ET BONNES PRATIQUES
================================================================================

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 8.1 — CONFIGURER DEPENDABOT (ALERTES DE SÉCURITÉ AUTOMATIQUES)
════════════════════════════════════════════════════════════════════════════════

  POURQUOI DEPENDABOT?
  ─────────────────────
  Les dépendances Python (Flask, SQLAlchemy...) ont parfois des vulnérabilités
  de sécurité découvertes après leur publication.
  Dependabot surveille vos dépendances et ouvre automatiquement des PRs
  pour mettre à jour les versions vulnérables.

  ALICE CRÉE .github/dependabot.yml:
  ────────────────────────────────────
  nano .github/dependabot.yml

  Contenu:
  version: 2
  updates:

    - package-ecosystem: "pip"
      directory: "/"
      schedule:
        interval: "weekly"
        day: "monday"
        time: "09:00"
        timezone: "Europe/Paris"
      open-pull-requests-limit: 5
      reviewers: ["alice-dev"]
      labels: ["ci/cd", "chore"]
      commit-message:
        prefix: "chore(deps):"

    - package-ecosystem: "github-actions"
      directory: "/"
      schedule:
        interval: "monthly"
      reviewers: ["alice-dev"]
      labels: ["ci/cd"]

════════════════════════════════════════════════════════════════════════════════
ÉTAPE 8.2 — CRÉER UN FICHIER SECURITY.md
════════════════════════════════════════════════════════════════════════════════

  # POURQUOI?
  # Explique comment signaler une vulnérabilité de sécurité.
  # Sans ce fichier, les chercheurs en sécurité ouvrent des issues publiques
  # (ce qui expose la faille avant qu'elle soit corrigée).

  nano SECURITY.md

  Contenu:
  # Politique de Sécurité

  ## Versions supportées
  | Version | Supportée |
  |---------|-----------|
  | 1.0.x   | [OK]        |
  | < 1.0   | [X]        |

  ## Signaler une vulnérabilité
  **NE PAS ouvrir une issue publique pour les vulnérabilités de sécurité.**

  Envoyer un email à: security@equipe.com
  Ou utiliser: GitHub -> Security -> Report a vulnerability (privé)

  Nous nous engageons à:
  - Accuser réception sous 48h
  - Fournir une mise à jour de statut sous 7 jours
  - Corriger les vulnérabilités critiques sous 30 jours
  - Vous notifier avant la publication du correctif

================================================================================
PARTIE 9 — CHEATSHEET COMPLET GIT & GITHUB
================================================================================

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONFIGURATION (une seule fois par machine)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git config --global user.name "Prénom Nom"
  git config --global user.email "email@domaine.com"
  git config --global core.editor "nano"
  git config --global pull.rebase true
  git config --global init.defaultBranch main
  git config --global core.autocrlf input       # Mac/Linux
  git config --global core.autocrlf true        # Windows
  git config --list                              # Voir la config complète

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DÉMARRAGE D'UN PROJET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git init                                    # Nouveau dépôt local
  git clone git@github.com:org/repo.git       # Cloner depuis GitHub (SSH)
  git remote add origin git@github.com:org/repo.git  # Connecter au remote
  git remote -v                               # Voir les remotes
  git push -u origin main                     # Premier push

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ÉTAT ET INSPECTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git status                                  # État du working directory
  git diff                                    # Diff non stagé
  git diff --staged                           # Diff stagé (avant commit)
  git diff main..develop                      # Diff entre 2 branches
  git log --oneline --graph --decorate --all  # Historique graphique complet
  git log --author="Bob" --since="1 week ago" # Filtrer l'historique
  git log -p -- app/routes.py                 # Historique d'un fichier
  git log -S "create_task"                    # Commits qui ont touché ce texte
  git show a3f8c1d                            # Détail d'un commit
  git blame app/routes.py                     # Qui a écrit quelle ligne

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SAUVEGARDER (STAGE ET COMMIT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git add routes.py                           # Stager un fichier
  git add app/                                # Stager un dossier
  git add .                                   # Stager tout
  git add -p                                  # Stager interactivement (hunk par hunk)
  git restore --staged routes.py             # Dé-stager un fichier
  git commit -m "type(scope): description"    # Committer
  git commit --amend --no-edit               # Modifier le dernier commit (pas encore pushé)
  git commit --amend -m "nouveau message"     # Changer le message du dernier commit

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BRANCHES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git branch                                  # Lister les branches locales
  git branch -a                               # Lister toutes (local + remote)
  git branch -vv                              # Avec upstream et statut
  git checkout -b feature/3-ma-feature        # Créer + se placer dessus
  git switch -c feature/3-ma-feature          # Idem (syntaxe moderne)
  git checkout develop                        # Changer de branche
  git branch -d feature/3                     # Supprimer branche locale (si mergée)
  git branch -D feature/3                     # Forcer la suppression
  git push origin --delete feature/3          # Supprimer la branche remote
  git fetch --prune                           # Nettoyer les refs remote obsolètes

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SYNCHRONISATION AVEC GITHUB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git fetch origin                            # Télécharger sans intégrer
  git pull origin develop                     # Télécharger ET intégrer
  git push origin feature/3                   # Pousser une branche
  git push -u origin feature/3                # Pousser + définir upstream
  git push --force-with-lease                 # Force push sécurisé
  git log HEAD..origin/develop --oneline      # Voir ce qui est en avance sur GitHub

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MERGE ET REBASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git rebase origin/develop                   # Rebaser sur develop à jour
  git rebase -i HEAD~5                        # Rebase interactif (5 derniers commits)
  git rebase --continue                       # Continuer après résolution de conflit
  git rebase --abort                          # Annuler le rebase en cours
  git merge --no-ff feature/3                 # Merge avec commit de merge visible
  git merge --squash feature/3                # Squash avant commit manuel
  git cherry-pick a3f8c1d                     # Appliquer un commit spécifique

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANNULER DES MODIFICATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git restore routes.py                       # Annuler modifs non stagées
  git restore --staged routes.py              # Dé-stager (garder les modifs)
  git revert a3f8c1d                          # Annuler en créant un nouveau commit
  git reset --soft HEAD~1                     # Défaire commit, garder stage
  git reset --mixed HEAD~1                    # Défaire commit, garder fichiers
  git reset --hard HEAD~1                     # Défaire commit ET les modifs (DANGER!)
  git reflog                                  # Retrouver les commits "perdus"
  git reset --hard HEAD@{2}                   # Revenir à un état du reflog

  [ATTENTION]  RÈGLES D'OR:
  -> reset --hard: NE JAMAIS sur des commits déjà pushés partagés.
  -> git revert: sûr sur n'importe quelle branche partagée.
  -> git reset: seulement sur les commits locaux non pushés.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STASH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git stash save "WIP: description du travail en cours"  # Mettre de côté
  git stash list                              # Lister les stashs
  git stash pop                               # Restaurer et supprimer du stash
  git stash apply stash@{0}                   # Restaurer sans supprimer
  git stash drop stash@{0}                    # Supprimer un stash
  git stash show -p stash@{0}                 # Voir le contenu d'un stash
  git stash branch feature/new stash@{0}      # Créer une branche depuis un stash

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TAGS ET RELEASES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git tag                                     # Lister les tags
  git tag -a v1.0.0 -m "Release v1.0.0"       # Créer un tag annoté
  git push origin v1.0.0                      # Pousser un tag
  git push origin --tags                      # Pousser tous les tags
  git tag -d v1.0.0                           # Supprimer tag local
  git push origin --delete v1.0.0             # Supprimer tag remote
  git checkout v1.0.0                         # Se placer sur un tag (detached HEAD)
  git describe --tags                         # Voir la version actuelle

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OUTILS AVANCÉS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  git bisect start && git bisect bad && git bisect good v1.0.0  # Trouver un bug
  git bisect reset                            # Terminer bisect
  git worktree add ../autre-dossier develop   # 2ème dossier de travail
  git worktree remove ../autre-dossier        # Supprimer le worktree
  git grep "create_task" app/                 # Chercher du texte dans le code
  git shortlog -sn --no-merges                # Contributions par auteur

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GITHUB CLI (gh) — COMMANDES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  gh auth login                               # S'authentifier sur GitHub
  gh repo clone equipe/taskmanager            # Cloner
  gh pr create --base develop --title "..."   # Créer une PR
  gh pr list                                  # Lister les PRs
  gh pr checkout 8                            # Checkout une PR localement
  gh pr review 8 --approve --body "LGTM!"     # Approuver
  gh pr review 8 --request-changes            # Demander des changements
  gh pr merge 8 --squash                      # Merger en squash
  gh issue create --title "..." --label "bug" # Créer une issue
  gh issue list --milestone "v1.0.0"          # Lister issues d'un milestone
  gh issue close 3                            # Fermer une issue
  gh release create v1.0.0 --title "..."      # Créer une release
  gh workflow list                            # Lister les workflows Actions
  gh run list                                 # Lister les runs récents
  gh run view [id]                            # Voir les logs d'un run
  gh label create "bug" --color "#d73a4a"     # Créer un label

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ERREURS FRÉQUENTES ET SOLUTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ERREUR: "Your branch is behind 'origin/develop' by 3 commits"
  CAUSE:   D'autres commits ont été ajoutés sur develop depuis votre dernier pull.
  FIX:     git pull origin develop (ou git pull --rebase)

  ERREUR: "error: Your local changes would be overwritten by merge"
  CAUSE:   Vous avez des modifications non commitées qui entrent en conflit.
  FIX:     git stash && git pull && git stash pop
           OU: git add . && git commit -m "WIP" && git pull

  ERREUR: "CONFLICT (content): Merge conflict in app/routes.py"
  CAUSE:   Deux personnes ont modifié le même fichier aux mêmes lignes.
  FIX:     Ouvrir le fichier, supprimer les marqueurs <<<<, ====, >>>>
           Garder la bonne version, git add routes.py, git rebase --continue

  ERREUR: "Permission denied (publickey)"
  CAUSE:   SSH n'est pas configuré ou la clé n'est pas dans l'agent.
  FIX:     eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_github
           Vérifier que la clé .pub est dans GitHub Settings -> SSH keys

  ERREUR: "Updates were rejected because the tip of your current branch is behind"
  CAUSE:   Remote a des commits que vous n'avez pas en local.
  FIX:     git pull --rebase origin [branche]
           PUIS: git push

  ERREUR: "fatal: refusing to merge unrelated histories"
  CAUSE:   Deux dépôts sans historique commun (ex: git init + git clone).
  FIX:     git pull origin main --allow-unrelated-histories
           (puis résoudre les conflits)

  ERREUR: On a committé des secrets (.env, mots de passe)
  CAUSE:   Oubli de .gitignore ou mauvais git add .
  FIX URGENT:
           1. Changer IMMÉDIATEMENT les mots de passe/clés exposés
           2. git rm --cached .env && git commit -m "remove accidental .env"
           3. git push
           4. Pour effacer de l'historique: git filter-repo --invert-paths --path .env
              (outil externe: pip install git-filter-repo)
           5. github.com/org/repo -> Settings -> Danger Zone -> "Clear commit cache"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FLUX QUOTIDIEN RÉSUMÉ (EN 12 ÉTAPES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Chaque matin:
  1.  git checkout develop
  2.  git pull origin develop                   (se mettre à jour)
  3.  git checkout -b feature/N-description     (nouvelle branche)
  4.  git push -u origin feature/N-description  (publier la branche)

  Pendant le développement:
  5.  [coder, modifier des fichiers]
  6.  git add [fichiers modifiés]
  7.  git diff --staged                         (vérifier avant commit)
  8.  git commit -m "type(scope): description" (committer atomiquement)
  9.  git push origin feature/N                 (sauvegarder sur GitHub)
  10. [répéter 5-9 pour chaque modification logique]

  Quand la feature est terminée:
  11. git fetch && git rebase origin/develop    (se mettre à jour)
  12. pytest + flake8 + black                   (vérifier la qualité)
  13. gh pr create --base develop               (ouvrir la PR)
  14. [attendre la review, répondre aux commentaires]
  15. [après merge: git branch -d feature/N && git fetch --prune]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONVENTION CONVENTIONAL COMMITS (RAPPEL COMPLET)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Format: <type>(<scope>): <description courte>
          [ligne vide]
          [description longue optionnelle]
          [ligne vide]
          [BREAKING CHANGE: description] OU [Closes #N]

  Types:
  feat      -> Nouvelle fonctionnalité             -> bump mineur (1.0.0 -> 1.1.0)
  fix       -> Correction de bug                   -> bump patch  (1.0.0 -> 1.0.1)
  docs      -> Documentation seulement             -> pas de bump
  style     -> Formatage, espaces (pas fonctionnel)-> pas de bump
  refactor  -> Refactorisation (ni feat ni fix)    -> pas de bump
  test      -> Ajout/modification de tests         -> pas de bump
  chore     -> Maintenance, dépendances, config    -> pas de bump
  ci        -> Configuration CI/CD                 -> pas de bump
  perf      -> Amélioration de performance         -> bump patch
  revert    -> Annulation d'un commit précédent    -> selon le contenu
  BREAKING! -> Marqué avec "!" ou BREAKING CHANGE  -> bump majeur (1.0.0 -> 2.0.0)

  Scopes pour notre projet:
  (routes)  -> Routes Flask / endpoints API
  (models)  -> Modèles SQLAlchemy
  (tests)   -> Fichiers de tests
  (docker)  -> Dockerfiles et docker-compose
  (ci)      -> GitHub Actions workflows
  (deps)    -> Dépendances (requirements.txt)
  (config)  -> Fichiers de configuration
  (release) -> Préparation d'une release

  Exemples valides:
  feat(routes): add date range filtering to GET /tasks
  fix(models): correct priority default value casing
  test(routes): add edge case tests for sort parameter
  chore(deps): upgrade Flask to 3.0.3
  ci(workflows): add SonarQube quality gate step
  feat!: change task response format (BREAKING CHANGE)
  docs(readme): update installation instructions

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LES 10 RÈGLES D'OR DE L'ÉQUIPE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. TOUJOURS partir de develop pour créer une branche feature.
     -> git checkout develop && git pull && git checkout -b feature/N

  2. JAMAIS committer directement sur main ou develop.
     -> Toujours passer par une PR avec au moins 1 review.

  3. JAMAIS committer des secrets (.env, mots de passe, clés API).
     -> Utiliser .gitignore + .env.example.

  4. TOUJOURS faire git pull avant de créer une branche.
     -> Évite de partir d'une base obsolète.

  5. Un commit = une modification logique.
     -> Facilite la review, git bisect, git revert.

  6. TOUJOURS vérifier git diff --staged avant git commit.
     -> S'assurer de committer ce qu'on pense committer.

  7. Rebaser sa branche sur develop avant d'ouvrir une PR.
     -> Évite les conflits massifs lors du merge.

  8. JAMAIS git push --force sans --lease.
     -> Utiliser git push --force-with-lease.
     -> JAMAIS de force push sur main ou develop.

  9. Supprimer les branches après merge.
     -> git branch -d feature/N && git fetch --prune
     -> Garder le dépôt propre.

  10. Une PR = un titre clair + une issue liée + checklist complète.
      -> "Closes #N" dans le message pour la fermeture automatique.

================================================================================
FIN DU GUIDE
================================================================================

Ce guide couvre l'intégralité du workflow Git & GitHub pour une équipe de 3
développeurs travaillant sur une application Flask avec déploiement en production.

Résumé de ce qui a été couvert:
  [OK] Installation et configuration Git (Mac, Ubuntu, Windows)
  [OK] SSH et authentification GitHub
  [OK] Application Flask complète (routes, modèles, tests, config)
  [OK] Initialisation du dépôt et premier commit
  [OK] Protection de branches (main et develop) avec explication de chaque option
  [OK] Labels GitHub (19 labels catégorisés avec utilisation concrète)
  [OK] Milestones GitHub (4 versions avec issues et avancement)
  [OK] Templates Issues et PR, CODEOWNERS
  [OK] Cycle feature complet (Bob): branche -> commits -> stash -> rebase -> PR
  [OK] Code review complète (Alice + Claire): commentaires inline, corrections, merge
  [OK] Conflits de merge: causes, résolution, rebase --continue
  [OK] Hotfix en production: depuis main, merge dans main et develop, tag patch
  [OK] Release: branche release/, CHANGELOG, merge dans main, tag annoté
  [OK] CI/CD: GitHub Actions (lint, tests avec PostgreSQL/Redis, build Docker, déploiement)
  [OK] Configuration serveur de production (Docker, Nginx, docker-compose.prod.yml)
  [OK] git bisect: trouver un bug en 4 itérations
  [OK] git rebase -i: nettoyer l'historique avant une PR
  [OK] git worktree: travailler sur 2 branches simultanément
  [OK] git reflog: récupérer des commits "perdus"
  [OK] git cherry-pick: appliquer un commit spécifique
  [OK] Projets Kanban, Discussions, Wiki GitHub
  [OK] Dependabot et SECURITY.md
  [OK] Cheatsheet complet (toutes les commandes)
  [OK] 10 règles d'or de l'équipe

================================================================================