# Chantier — Page admin "Mises à jour" + rapports de bug

> ## ✅ CHANTIER LIVRÉ EN PRODUCTION
> **Vérifié le 2026-05-29** — Ne plus relire pour valider la livraison.

**Date** : 2026-05-06
**Cible** : coolcare-app (puis port missioflow-app à l'identique)
**Statut** : conception validée, prêt à implémenter

---

## 1. Pourquoi

Aujourd'hui les admins métier ne savent pas ce qui change quand l'app est déployée. Quand ils rencontrent un bug, ils n'ont pas de canal structuré pour le signaler à l'équipe dev (ils passent par mail/téléphone, pas tracé).

Objectif : une page admin unique qui :
1. Liste l'historique des mises à jour de l'app (ce qui a changé, en langage métier).
2. Permet de signaler un bug rattaché à une journée de mise à jour, avec captures d'écran.

---

## 2. Décisions de conception

| Décision | Choix | Raison |
|---|---|---|
| Source des données | `git log` parsé en post-deploy | Zéro saisie manuelle. Pas de tag git car contraignant pour le build image. |
| Granularité d'affichage | Groupement **par date** de commit | Pas de tags de version → on regroupe par jour de déploiement. |
| Filtrage par type | **Visibles** : `feat:`, `fix:`, `style:`, `perf:`, et tout commit avec scope métier explicite. **Repliés** : `refactor:`, `chore:`, `docs:`, `test:`, `build:`, `ci:`. | Admin métier ne s'intéresse pas aux refactor — sauf si un refactor a introduit un bug. D'où l'accordéon "Modifications techniques" qui reste accessible. |
| Lien bug → mise à jour | Le rapport de bug est rattaché à une **journée**, pas à un commit individuel | Permet de signaler un bug même quand le coupable est un refactor caché dans l'accordéon. Dev a accès à tous les commits du jour (visibles + techniques) pour creuser. |
| Statuts bug report | `nouveau` → `en_cours` → `resolu` ou `rejete` | Cycle simple. Tout admin peut changer le statut. |
| Screenshots | 0 à 5 par rapport, JPG/PNG/WebP, 5 Mo max chacun | Réutilise le pattern `PhotoUploadHelper` (validé exemplaire à l'audit). |
| Sécurité endpoints | `requireApiCsrfToken()` + `requireAdminApiRole()` dès la 1ère ligne | Leçon directe de l'audit OWASP A01 du 2026-05-06. |

---

## 3. Schéma BDD

### Table `app_updates` — un commit = une ligne

```sql
CREATE TABLE `app_updates` (
    `id`             INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `commit_sha`     CHAR(40) NOT NULL,
    `commit_date`    DATE NOT NULL,
    `commit_datetime` DATETIME NOT NULL,
    `commit_type`    ENUM('feat','fix','style','perf','refactor','docs','chore','test','build','ci','revert','other') NOT NULL DEFAULT 'other',
    `commit_scope`   VARCHAR(64) NULL,
    `commit_subject` VARCHAR(500) NOT NULL,
    `commit_body`    TEXT NULL,
    `author`         VARCHAR(100) NOT NULL,
    `is_visible`     TINYINT(1) NOT NULL DEFAULT 0
        COMMENT '1 = affiche par defaut a l admin metier ; 0 = replie dans accordeon technique',
    `imported_at`    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_commit_sha` (`commit_sha`),
    INDEX `idx_commit_date` (`commit_date`),
    INDEX `idx_is_visible_date` (`is_visible`, `commit_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**Règle de calcul `is_visible`** (à appliquer dans le script d'import) :
```
is_visible = 1 si commit_type IN ('feat','fix','style','perf')
is_visible = 1 si commit_type = 'revert' (un revert est visible pour comprendre les regressions)
is_visible = 0 sinon
```

### Table `app_update_bug_reports` — rapport admin

```sql
CREATE TABLE `app_update_bug_reports` (
    `id`              INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `update_date`     DATE NOT NULL
        COMMENT 'Date de la mise a jour suspectee. FK logique vers app_updates.commit_date.',
    `title`           VARCHAR(100) NOT NULL
        COMMENT 'Titre court du bug, 10 chars min cote backend',
    `source_type`     ENUM('self','technicien') NOT NULL
        COMMENT 'self = admin a rencontre lui-meme ; technicien = remontee par un tech',
    `source_technicien_id` INT UNSIGNED NULL
        COMMENT 'Technicien qui a remonte le bug. Obligatoire si source_type=technicien.',
    `affected_zone`   ENUM('admin','technicien','client_portail','api_backend','email','autre') NOT NULL,
    `description`     TEXT NOT NULL,
    `error_message`   TEXT NULL
        COMMENT 'Message d erreur affiche par l app, copie-colle par l admin. Optionnel.',
    `screenshots_json` JSON NULL
        COMMENT 'Liste des paths relatifs uploads/app_update_bugs/{id}/...',
    `status`          ENUM('nouveau','en_cours','resolu','rejete') NOT NULL DEFAULT 'nouveau',
    `reported_by_id`  INT UNSIGNED NOT NULL
        COMMENT 'FK techniciens.id avec role=admin',
    `resolved_by_id`  INT UNSIGNED NULL,
    `resolution_notes` TEXT NULL,
    `created_at`      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at`      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `resolved_at`     DATETIME NULL,
    PRIMARY KEY (`id`),
    INDEX `idx_update_date` (`update_date`),
    INDEX `idx_status` (`status`),
    INDEX `idx_affected_zone` (`affected_zone`),
    CONSTRAINT `fk_bug_reported_by` FOREIGN KEY (`reported_by_id`) REFERENCES `techniciens`(`id`) ON DELETE RESTRICT,
    CONSTRAINT `fk_bug_resolved_by` FOREIGN KEY (`resolved_by_id`) REFERENCES `techniciens`(`id`) ON DELETE SET NULL,
    CONSTRAINT `fk_bug_source_tech` FOREIGN KEY (`source_technicien_id`) REFERENCES `techniciens`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

**Contrainte applicative** (vérifiée backend, pas par CHECK MySQL pour compat 8.0) :
- `source_type = 'technicien'` ⇒ `source_technicien_id NOT NULL`
- `source_type = 'self'` ⇒ `source_technicien_id` ignoré (forcé à NULL)

**Notes** :
- Pas de FK dure sur `app_updates` : on rattache à la `update_date`, pas à un commit. Si tous les commits du jour sont supprimés (cas extrême), le bug report reste valide.
- Screenshots stockés en JSON cohérent avec le pattern existant (`devis.fichiers_pdf`, `factures.fichiers_pdf`, `techniciens.documents_annexe_pdf`).
- `source_technicien_id` en `ON DELETE SET NULL` pour ne pas bloquer la suppression d'un compte tech.

---

## 4. Script d'import depuis `git log`

**Fichier** : `scripts/import_updates_from_git.php`

**Lancement** :
- Manuel : `php scripts/import_updates_from_git.php`
- Auto : à appeler en fin de pipeline GitLab CI dans le job `deploy` (après le `git pull` sur le VPS).

**Logique** :
1. Récupérer le dernier `commit_sha` importé : `SELECT commit_sha FROM app_updates ORDER BY commit_datetime DESC LIMIT 1`
2. Lancer `git log {last_sha}..HEAD --pretty=format:'%H|%cI|%an|%s|%b---END---'` (format machine-friendly)
3. Pour chaque commit :
   - Parser le sujet pour extraire `type` et `scope` via regex `/^(\w+)(?:\(([^)]+)\))?: (.+)$/`
   - Calculer `is_visible` selon la règle ci-dessus
   - INSERT IGNORE dans `app_updates`
4. Logger nombre de commits importés via `Logger::info()`.

**Idempotence** : `UNIQUE KEY commit_sha` + `INSERT IGNORE` → relancer 2× ne crée pas de doublons.

**Cas du tout premier import** : si la table est vide, importer les 1000 derniers commits (`git log -n 1000 --pretty=...`) pour avoir un historique. À ajuster.

---

## 5. Endpoints API

Tous sous `requireApiCsrfToken()` + `requireAdminApiRole()` dès la première ligne.

### `GET /api/admin/app_updates.php`
Liste les mises à jour, groupées par date, du plus récent au plus ancien.

**Réponse** :
```json
{
  "success": true,
  "data": [
    {
      "date": "2026-05-05",
      "visible": [
        { "id": 142, "type": "fix", "scope": "client-portail", "subject": "ne plus envoyer deux emails d'invitation si une invitation est déjà en cours", "author": "Taaazzz" },
        { "id": 141, "type": "feat", "scope": "admin", "subject": "...", "author": "Taaazzz" }
      ],
      "technical": [
        { "id": 140, "type": "refactor", "scope": "admin", "subject": "...", "author": "Taaazzz" }
      ],
      "bug_reports_count": 1
    },
    { "date": "2026-05-04", ... }
  ],
  "pagination": { "page": 1, "per_page": 30, "total_dates": 47 }
}
```

Pagination par lots de 30 dates (réutilise `AdminPagination` aligné chantier 2026-05-04).

### `GET /api/admin/app_update_bug_reports.php`
Liste les rapports de bug, optionnellement filtrés par `update_date` ou `status`.

### `POST /api/admin/app_update_bug_reports.php` (action=create)
Crée un rapport. Body :
- `update_date` (obligatoire, format `Y-m-d`)
- `title` (obligatoire, 10-100 chars)
- `source_type` (obligatoire, enum `self`|`technicien`)
- `source_technicien_id` (obligatoire si `source_type=technicien`, ignoré sinon)
- `affected_zone` (obligatoire, enum)
- `description` (obligatoire, 20-5000 chars)
- `error_message` (optionnel, max 2000 chars)

Screenshots uploadés séparément après création (pattern existant — id retourné, puis upload).

### `POST /api/admin/app_update_bug_reports.php` (action=upload_screenshot)
Upload un screenshot via `PhotoUploadHelper::validatePhotoFile()` → stocke dans `public/uploads/app_update_bugs/{report_id}/{photo_uniqid}.{ext}` → ajoute le path au JSON.

### `POST /api/admin/app_update_bug_reports.php` (action=update_status)
Change le statut. Si `resolu`, set `resolved_by_id` + `resolved_at` + `resolution_notes`.

### `POST /api/admin/app_update_bug_reports.php` (action=delete_screenshot)
Supprime un screenshot du JSON et du FS (avec realpath enforcement comme dans `delete_technicien_document.php`).

---

## 6. Frontend admin

### Nouvelle entrée menu
Dans `public/admin/admin_common.php` : ajouter "Mises à jour" avec icône cloche/notification, en dessous de "Monitoring".

### Page `public/admin/updates.php`

**Structure visuelle** :
```
┌─────────────────────────────────────────────────────┐
│ MISES À JOUR DE L'APPLICATION                       │
├─────────────────────────────────────────────────────┤
│                                                      │
│ ▼ 5 mai 2026                          [📝 Signaler] │
│                                                      │
│   🟢 NOUVEAU   feat(admin) — pagination unifiee...  │
│   🔴 CORRECTIF fix(client-portail) — ne plus...     │
│                                                      │
│   ▶ Modifications techniques (1)                    │
│                                                      │
│   📋 Rapports de bug (1)                            │
│      • [nouveau] "Le bouton Valider ne réagit pas"  │
│        signalé par admin@x le 06/05/26 — 2 captures │
│        [Voir détail] [Marquer en cours]             │
│                                                      │
├─────────────────────────────────────────────────────┤
│ ▼ 4 mai 2026                          [📝 Signaler] │
│   ...                                                │
└─────────────────────────────────────────────────────┘
```

**Couleurs des badges par type** (à définir avec le branding) :
- `feat` → vert "🟢 NOUVEAU"
- `fix` → rouge "🔴 CORRECTIF"
- `style` → bleu "🎨 INTERFACE"
- `perf` → violet "⚡ PERFORMANCE"
- `refactor` / `chore` / etc. → gris "🔧 TECHNIQUE" (dans accordéon replié)
- `revert` → orange "↩️ ANNULATION"

### Modale "Signaler un bug" — formulaire guidé

Objectif : forcer un minimum de structure pour que les rapports soient exploitables sans aller-retour. Tous les champs marqués 🔴 sont obligatoires.

```
┌──────────────────────────────────────────────────────────┐
│ SIGNALER UN PROBLÈME                              [X]    │
├──────────────────────────────────────────────────────────┤
│                                                           │
│ 🔴 Mise à jour concernée                                 │
│    [📅 Date sélectionnée : 5 mai 2026]   (pré-rempli)    │
│                                                           │
│ 🔴 Titre du problème (10 à 100 caractères)               │
│    ┌────────────────────────────────────────────────┐    │
│    │                                                 │    │
│    └────────────────────────────────────────────────┘    │
│    Ex : "Le bouton Valider ne réagit pas sur fiche site" │
│                                                           │
│ 🔴 Source du problème                                    │
│    ( ) Je l'ai rencontré moi-même                        │
│    (•) Un technicien me l'a remonté                      │
│        ┌────────────────────────────────────────────┐    │
│        │ ▼ Sélectionner un technicien...            │    │
│        └────────────────────────────────────────────┘    │
│                                                           │
│ 🔴 Zone concernée                                        │
│    ┌────────────────────────────────────────────────┐    │
│    │ ▼ Page admin                                    │    │
│    └────────────────────────────────────────────────┘    │
│    Choix : Page admin / Page technicien / Portail client │
│            / API & Backend / Email envoyé / Autre        │
│                                                           │
│ 🔴 Description (20 à 5000 caractères)                    │
│    ┌────────────────────────────────────────────────┐    │
│    │ 1. Ce que je faisais : ...                      │    │
│    │ 2. Ce qui devait se passer : ...                │    │
│    │ 3. Ce qui s'est passé à la place : ...          │    │
│    └────────────────────────────────────────────────┘    │
│    (Le texte ci-dessus est un placeholder, pas une       │
│     contrainte. Décrivez le problème comme vous voulez.) │
│                                                           │
│ ⚪ Message d'erreur affiché (optionnel)                  │
│    ┌────────────────────────────────────────────────┐    │
│    │                                                 │    │
│    └────────────────────────────────────────────────┘    │
│    Si l'app a affiché un message d'erreur, copiez-le ici │
│                                                           │
│ ⚪ Captures d'écran (0 à 5, max 5 Mo chacune)            │
│    [📎 Ajouter une image]                                │
│    [aperçu thumbnail 1] [supprimer]                       │
│                                                           │
│                                          [Annuler] [Envoyer]
└──────────────────────────────────────────────────────────┘
```

**Validation côté frontend** (JS) :
- `title` : `value.trim().length >= 10 && value.trim().length <= 100`
- `source_technicien_id` : requis si `source_type === 'technicien'`
- `affected_zone` : valeur in whitelist
- `description` : `value.trim().length >= 20 && value.trim().length <= 5000`
- `error_message` : `value.length <= 2000`

**Validation côté backend** : strictement identique, **avec re-vérification** (jamais faire confiance au front).

**Affichage du rapport dans la liste** :
```
┌─────────────────────────────────────────────────────────┐
│ 🔴 [nouveau] "Le bouton Valider ne réagit pas..."       │
│    📍 Page admin · 👤 Remonté par Jean Tech              │
│    Signalé par admin@x le 06/05/26 — 2 captures         │
│    [Voir détail] [Marquer en cours] [Marquer résolu]    │
└─────────────────────────────────────────────────────────┘
```

Frontend escape XSS via `escapeHtml` sur tous les champs texte affichés (`subject`, `title`, `description`, `error_message`, `resolution_notes`, `author`, nom du technicien remontant). Pattern déjà en place dans `demandes_devis.js`, `techniciens_qr.js`.

---

## 7. Étapes d'implémentation (ordre)

1. **Migration BDD** : `database/migration_app_updates_and_bug_reports.sql` + rollback associé.
2. **Script d'import** : `scripts/import_updates_from_git.php` + appel manuel pour peupler la table avec l'historique git existant (test sur dernier 1000 commits).
3. **Backend** : `src/services/AppUpdatesService.php` + `src/services/AppUpdateBugReportService.php` (logique métier isolée, testable).
4. **Endpoints API** : `public/api/admin/app_updates.php` + `public/api/admin/app_update_bug_reports.php`.
5. **Tests unit** : couverture des deux services (lecture git log mockée, parsing, import idempotent, transitions de statut).
6. **Tests intégration** : un test qui INSERT 5 commits factices, vérifie le groupement par date, le filtre `is_visible`, et un cycle complet de bug report avec upload screenshot.
7. **Frontend admin** : page `public/admin/updates.php` + JS dédié `public/js/admin/updates.js` (escapeHtml partout).
8. **Intégration menu** : ajouter dans `admin_common.php`.
9. **Hook CI** : ajouter `php scripts/import_updates_from_git.php` dans le job deploy GitLab CI après le `git pull`.
10. **Tests manuels** : créer un commit `fix:` factice, déployer, vérifier qu'il apparaît bien sur la page admin.
11. **Markdown port MissioFlow** : créer `missioflow-app/docs/CHANTIER_PAGE_UPDATES_BUG_REPORTS.md` avec ce contenu (règle `feedback_chantier_md_missioflow`).

---

## 8. Sécurité — checklist d'audit pré-merge

(Issue directe de l'audit OWASP du 2026-05-06)

- [ ] `requireApiCsrfToken()` + `requireAdminApiRole()` sur tous les endpoints
- [ ] Upload screenshots passe par `PhotoUploadHelper::validatePhotoFile()` (finfo MIME + getimagesize + dimensions + filename random)
- [ ] Suppression screenshot : `realpath` + assertion `strpos === 0` sous `/public/uploads/app_update_bugs/` (pattern `delete_technicien_document.php`)
- [ ] `update_date` format `Y-m-d` validé via `DateTime::createFromFormat`
- [ ] `status`, `source_type`, `affected_zone` validés contre whitelist d'enum côté PHP avant INSERT/UPDATE
- [ ] `title` : longueur 10-100 chars enforce backend
- [ ] `description` : longueur 20-5000 chars enforce backend
- [ ] `error_message` : longueur ≤ 2000 chars enforce backend
- [ ] `source_technicien_id` cast `(int)`, vérifié existant en BDD si `source_type=technicien`, forcé NULL si `source_type=self`
- [ ] Frontend : `escapeHtml` sur `title`, `subject`, `description`, `error_message`, `resolution_notes`, `author`, nom technicien remontant
- [ ] Pas d'`error_log("DEBUG ...")` non conditionné en prod (leçon audit 7.1)
- [ ] Tests intégration vérifient qu'un technicien non-admin ne peut pas accéder aux endpoints (403)

---

## 9. Considérations multi-tenant (port MissioFlow)

À porter sur missioflow-app après validation coolcare. Différences attendues :
- Ajouter `tenant_id` dans les deux tables (FK vers `tenants` ou équivalent missioflow).
- Filtrer toutes les requêtes par `tenant_id` du compte admin courant.
- Le script d'import git log s'applique de la même façon (les commits sont identiques entre tenants — le repo est mono).
- Les bug reports restent isolés par tenant (un admin tenant A ne voit pas les bugs du tenant B).

---

## 10. À ne pas faire dans ce chantier

- Pas de notification email/Slack quand un bug est créé. (Feature évidente mais hors scope, à prévoir dans un chantier séparé si besoin.)
- Pas d'attribution d'un bug à un dev ("assigné à"). Trop de structure pour un usage interne — on gère via le statut.
- Pas de commentaires multiples sur un bug. `description` + `resolution_notes` suffisent.
- Pas de "release notes" éditables manuellement par l'admin. La source de vérité reste git log, point.

---

**Statut** : prêt à implémenter sur validation. Markdown reste local non commit tant que le chantier n'est pas terminé (règle `feedback_markdown_chantier_avec_commit`).
