# Audit technique Sensipode — octobre 2026

Audit initial réalisé localement le 12 octobre 2026, sur `audit/technique-2026-10`, par inspection statique et diagnostics en lecture seule. La sécurité applicative reste hors périmètre. **Cette mise à jour intègre les résultats OVH et les interventions communiqués par le propriétaire, sans nouvel audit ni nouvelle intervention distante.** Les mesures locales initiales restent historiques ; les résultats production sont identifiés séparément.

## 1. Synthèse exécutive

Le socle apparaît globalement sain pour l'usage actuel du site. Aucun sujet technique critique n'a été mis en évidence dans le périmètre audité. L'architecture Symfony classique et les choix legacy assumés ne justifient ni modernisation générale ni refonte.

Les vérifications OVH ont confirmé plusieurs reliquats historiques repérés localement ; ils ont depuis été traités après sauvegarde. La migration base64 est terminée en production, les 25 lignes de liaison invalides ont été supprimées et les contraintes SQLite sont désormais activées sur la connexion Doctrine éditoriale. La compaction a ramené la base de production d'environ 103 Mio à environ 2 Mio. Les anciens messages Messenger ont également été retirés, sans incident de messagerie actuel.

L'inventaire médias est désormais fiable dans son périmètre et strictement en lecture seule. Après retrait d'une ligne Image sans projet et sans fichier, la production présente **179 références vérifiées, toutes valides, aucune cassée ni non vérifiable**. Les 43 candidats non référencés ne constituent pas une liste de suppression. Les tâches OVH d'observation et de purge Analytics sont configurées et activées.

Le stockage éditorial SQLite et le stockage MySQL Analytics restent distincts (`src/Services/AnalyticsConnection.php::get`). Le principal sujet encore utile est de documenter et éprouver les sauvegardes/restaurations de ces deux stockages et des médias. Les chargements Doctrine observés ne justifient une correction que si une mesure distante démontre un coût réel ; les routes et autres résidus legacy relèvent d'un entretien opportuniste.

## 2. Points satisfaisants / dette déjà maîtrisée

- **SEO et URL** : métadonnées centralisées (`templates/base.html.twig`, `src/Twig/MetadataExtension.php`), sitemap filtré sur l'éligibilité publique (`SitemapController::index`), registre et redirections de slugs (`EditorialSlugRegistry`, `EditorialSlugPersistenceSubscriber`, `EditorialSlugRedirectSubscriber`). La route historique `/pole/{id}` redirige explicitement en 301 : ce n'est pas un déchet à supprimer.
- **Images éditoriales** : migration dédiée avec contrôle préalable, sauvegarde et mode sans `--apply` (`MigrateCkeditorBase64Command`, `Base64ImageMigration`, `docs/ckeditor-base64-migration.md`). `config/packages/fos_ckeditor.yaml` désactive la conversion des images collées en base64. Les images publiques disposent de variantes Liip Imagine, `srcset` et chargement différé, notamment `templates/projet/list.html.twig:29-58`.
- **Cockpit** : catalogue fermé et hiérarchie explicite, parcours DETAIL, action de publication dédiée, administration des profils et synchronisation du statut SuperAdmin au choix du profil (`PermissionCatalogue`, `UserCrudController::prepareProfilAndStatus`). Pas de nouvel audit des autorisations ici.
- **Analytics** : connexion isolée et paresseuse ; erreur contrôlée sans dépendre du SQLite éditorial ; observation et purge de 24 mois par lots de 500 (`AnalyticsRepository::purgeBefore`). Les points d'entrée OVH existent dans `bin/cron/analytics-observe.php` et `analytics-purge.php` ; configuration décrite dans `bin/cron/README.md`. Leur programmation OVH est désormais confirmée active (voir le suivi ci-dessous).
- **Assets et caches** : versions CSS/JS par date de modification, sans altérer les URL d'images (`FileModificationVersionStrategy`) ; catalogue d'icônes local versionné (`MaterialSymbolCatalog`) ; caches Doctrine spécifiques à `prod` dans `config/packages/doctrine.yaml`. Les pools déclarés ne signifient pas que toutes les requêtes métier sont mises en cache.
- **Historique local** : les 33 versions enregistrées ont toutes un fichier correspondant. Seule `Version20260922120000` n'est pas enregistrée localement ; son contenu est volontairement neutralisé, conformément à la documentation Analytics. Ne pas la considérer comme une installation Analytics SQLite manquante.

### Suivi OVH — actions réalisées et sujets clos

| Sujet | Constat initial | Vérification OVH / action réalisée | État final |
|---|---|---|---|
| Images base64 | Aucun `data:image/` dans les colonnes textuelles locales ; production encore à vérifier. | `app:migrate-ckeditor-base64 --env=prod --no-debug` en production : 0 record, 0 champ, 0 occurrence, 0 erreur. Outil : `MigrateCkeditorBase64Command` et `Base64ImageMigration`. | **Clos** : migration terminée en production. |
| Relations invalides | 25 violations locales : `projet_client` 16, `projet_tag` 7, `collaborateur_pole` 2. | Mêmes violations confirmées en production : lignes de liaison dont le Projet ou Collaborateur parent avait déjà été supprimé. Après sauvegarde, suppression explicite de ces 25 lignes ; `foreign_key_check` sans violation et `quick_check = ok`. | **Clos** : réconciliation réalisée. |
| Activation des foreign keys | Le diagnostic initial Python ne permettait pas de conclure sur Doctrine. | Doctrine production retournait effectivement `foreign_keys = 0`. Correctif livré dans `config/services.yaml` : middleware natif `Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware\EnableForeignKeys`, limité à `default`, sans toucher Analytics MySQL. `tests/Doctrine/SQLiteForeignKeysTest.php` : 4 tests / 15 assertions réussies, couvrant reconnexion, CASCADE, SET NULL, RESTRICT, NO ACTION et suppressions ORM spécifiques. | **Clos** : préproduction et production vérifiées avec `foreign_keys = 1`, sans violation de `foreign_key_check`. |
| Compaction SQLite | Base locale d'environ 103 Mio, 26 362 pages, dont 25 861 libres (environ 101 Mio). | Production identique avant intervention, avec `quick_check = ok`. Après sauvegarde et VACUUM : 2 035 712 octets, 497 pages, 0 page libre, `quick_check = ok`. | **Clos** : maintenance terminée ; aucun VACUUM récurrent nécessaire à ce stade. |
| Reliquats Messenger | 203 messages locaux dans `default`, datés de septembre 2023 à mai 2025, non livrés. | Vérifiés comme anciens `SendEmailMessage`, sans nouveau message depuis ; aucun worker identifié dans l'exploitation actuelle. Le Contact envoie synchroniquement (`ContactService::sendEmail`). Après sauvegarde, les 203 reliquats ont été supprimés. | **Clos** : ni incident actuel ni chantier restant. |
| Crons Analytics | Scripts présents, programmation distante encore non vérifiée lors de l'audit. | Les quatre tâches OVH existent, sont actives et utilisent PHP 8.1 ; horaires détaillés ci-dessous. Scripts : `bin/cron/analytics-observe.php` et `analytics-purge.php`. | **Clos** : contrôle de la programmation réalisé. |
| Fiabilisation de l'inventaire médias | Anciens chemins faux et couverture incomplète dans `ImageAuditCommand`. | `app:audit:images` dérive les références des mappings Vich ; couvre les entités concernées, CKEditor, Pages et Enjeux, raster et SVG ; sépare références cassées, candidats, fichiers techniques et URL externes. Lecture seule stricte, sans génération de rapport sur disque. Sources : `ImageAuditCommand`, `tests/Command/ImageAuditCommandTest.php`. | **Clos** : outil fiabilisé ; ses limites de couverture restent explicites. |
| Référence média cassée | Une référence `Image #65.imageName` pointait vers un fichier absent localement. | Ligne sans `projet_id`, fichier absent en préproduction et production, aucun projet impacté. Ligne supprimée explicitement après sauvegarde. Production : 179 références, 179 valides, 0 cassée, 0 non vérifiable, 43 candidats non référencés. | **Clos** : aucune référence cassée dans l'inventaire final production. |

Programmation OVH vérifiée (horaires tels que configurés dans OVH ; aucun fuseau supplémentaire supposé) :

| Environnement / tâche | Script | Planification | Runtime |
|---|---|---|---|
| Production — observation | `www/bin/cron/analytics-observe.php` | `30 * * * *` | PHP 8.1 |
| Préproduction — observation | `preprod/bin/cron/analytics-observe.php` | `30 * * * *` | PHP 8.1 |
| Préproduction — purge | `preprod/bin/cron/analytics-purge.php` | `25 4 * * *` | PHP 8.1 |
| Production — purge | `www/bin/cron/analytics-purge.php` | `13 5 * * *` | PHP 8.1 |

L'observation réelle est **horaire**, malgré la mention encore « quotidienne » dans `bin/cron/README.md` : simple alignement documentaire opportuniste, pas un sujet de programmation ouvert.

Limites résiduelles de l'inventaire, sans incident actuel : les **43 candidats ne sont pas des orphelins à supprimer** ; Twig, CSS, pages statiques et liens externes peuvent les utiliser. L'absence de `.git` en production empêche également le classement complet des assets versionnés. `Attachment.image` conserve un warning de mapping Vich absent, mais `attachment` contient **0 ligne en local, préproduction et production** : aucun impact actuel. Ces points ne rouvrent pas le chantier d'inventaire.

## 3. Constats nécessitant encore une action

P1 = incident avéré immédiat ; P2 = action utile planifiable ; P3 = entretien opportuniste. **Aucun sujet P1 retenu. Les sujets clos ci-dessus sont retirés du backlog.**

| Sujet | Constat / preuve | Impact | Priorité | Recommandation | Effort estimatif |
|---|---|---|---|---|---|
| Sauvegarde / restauration | Les interventions ont été précédées de sauvegardes. Le dispositif récurrent complet et une restauration éprouvée ne sont pas documentés dans les éléments de suivi. Les données éditoriales, uploads Vich/CKEditor et Analytics sont séparés (`config/packages/doctrine.yaml`, `vich_uploader.yaml`, `AnalyticsConnection`). | Capacité de reprise et cohérence base/médias à expliciter ; aucune absence de sauvegarde n'est affirmée. | P2 | Documenter le dispositif réel SQLite, médias et MySQL Analytics : méthode, fréquence, rétention, emplacement et procédure de reprise. Idéalement vérifier une restauration isolée. Forte valeur, sans refonte. | Faible à moyen |
| Performance Doctrine, conditionnelle | `ProjetController::index` charge deux fois les projets en ligne, dont `$allOnline` inutilisé. `ProjetRepository::findByFilters` ne précharge pas images/clients/pôle, parcourus dans `templates/projet/list.html.twig`. `FrontController::index` et `PoleController::showBySlug` chargent tous les projets en ligne. Volumes locaux initiaux modestes : 22 projets, 49 offres, 13 ressources, 4 pôles. | Coût potentiel de chargements redondants/N+1 ; aucune lenteur préproduction ou production mesurée. | P3 | **Aucun chantier préventif**. Mesurer seulement si un besoin apparaît ; corriger les chargements responsables si le coût réel est confirmé. Pas de pagination/cache global sans justification. | Faible à moyen, si justifié |
| Routes / résidus legacy / contrat runtime | Déclarations historiques de routes et loader CMS, ancienne route `/send`, `FormatRepository`, branches Twig et dépendances sans usage direct confirmé. `composer.json` annonce PHP `>=7.2.24` alors que `Version20261009150000` utilise `array_is_list` (PHP 8.1). Les auto-scripts Composer installent les assets et `package.json` distingue les builds public/Cockpit. | Lisibilité et reproductibilité perfectibles, sans incident constaté. | P3 | Entretien opportuniste : vérifier les usages avant retrait, expliciter le runtime et les étapes de livraison, aligner la documentation des crons. Aucune montée de version ni suppression large proposée. | Faible, par petites interventions |

## 4. Vérifications encore utiles sur préproduction / production

- **Sauvegarde et reprise** : clarifier le dispositif réellement utilisé pour SQLite, les médias et MySQL Analytics ; documenter une sauvegarde SQLite cohérente en présence d'écritures et tester si possible la restauration de l'ensemble sur un environnement isolé. Les sauvegardes ponctuelles déjà réalisées ne suffisent pas à décrire ce dispositif.
- **Performance uniquement si nécessaire** : mesurer accueil/liste/détail représentatifs et leurs requêtes si la recette ou l'usage révèle un coût sensible. Les constats statiques ne prouvent pas une lenteur distante.
- **Entretien opportuniste** : vérifier collection de routes effective et fréquentation avant toute suppression legacy ; confirmer runtime CLI/web et étapes d'installation lors d'une prochaine intervention. Les observations historiques sur `DefaultController::index`, le loader CMS et `/send` restent des pistes de rangement, pas des incidents établis.

Aucun nouveau contrôle base64, nettoyage relationnel, VACUUM, retrait Messenger, contrôle de programmation Analytics ou fiabilisation de l'inventaire n'est demandé par ce suivi : ces travaux sont terminés.

## 5. Backlog technique final recommandé

1. **Sauvegarde / restauration** : une courte fiche du dispositif réel couvrant SQLite, médias et MySQL Analytics ; idéalement un essai de restauration isolé.
2. **Performance Doctrine, seulement sur preuve** : mesure préproduction/production si un coût réel est suspecté, puis correction ciblée si justifiée. Sinon, aucun chantier.
3. **Entretien legacy opportuniste** : routes/résidus après vérification d'usage, contrat runtime et documentation d'installation ; alignement de la mention quotidienne des crons avec l'observation horaire réelle. Non prioritaire.

Les huit sujets marqués **clos** ne constituent plus des actions restantes. Aucune campagne de suppression des 43 candidats médias ni tâche VACUUM récurrente n'est recommandée.

## 6. Points volontairement non retenus

- **Legacy assumé** : Symfony 5.4.45, EasyAdmin 3.5.23, ORM 2.20.2, DBAL 3.9.4, CKEditorBundle 2.4.0 et elFinderBundle 12.6 sont les versions du lock, pas une preuve d'incident. `sensio/framework-extra-bundle` est marqué abandonné dans `composer.lock` mais sert encore aux annotations : pas de retrait isolé. Aucun inventaire de vulnérabilités ou calendrier de support externe n'a été effectué.
- **Dépendances/résidus sans preuve d'usage actuel** : TinyMCE et VerifyEmail sont déclarés, sans appel applicatif direct trouvé ; `FormatRepository` vise une entité supprimée par l'historique ; `RoleType`, anciennes branches Twig de filtres et loader CMS constituent des candidats de rangement. Vérifier les usages indirects avant suppression. Gain insuffisant pour en faire un chantier prioritaire.
- **Duplication locale des CRUD/publications**, annotations historiques, style hétérogène et trait `Common` : lisibilité explicite conforme au projet. Pas de service générique, de refactoring global ni de réécriture des formulaires imbriqués proposés.
- **Fichiers générés ≠ déchets** : 299 fichiers de cache Liip locaux (~10,3 Mio), 763 assets de bundles (~14,3 Mio) ; ils sont régénérables et ignorés par Git. Les uploads et dossiers CKEditor sont des données environnementales non versionnées. Les 16 images suivies par Git sont à distinguer de ces données ; les 4 139 fichiers sous `public/icons` sont un catalogue volontairement versionné. Ne rien supprimer sur un simple comptage.
- **Candidats médias** : les 43 fichiers sans référence connue en production ne sont pas une liste de suppression. Les ressources HTML, templates, pages statiques et anciens liens externes peuvent encore les utiliser ; le classement Git est incomplet sur cette installation.

### Traçabilité de l’audit initial et du suivi

Les commandes suivantes appartiennent à l’audit local initial ; elles n’ont pas été rejouées pour cette mise à jour :

- `git status --short`, `git branch --show-current`, `git ls-files` ciblé sur assets et fichiers d'exploitation ; état initial propre.
- `rg --files`, recherches `rg -n`, lectures `cat`, `head` et `sed` sur `src/`, `config/`, `templates/`, `migrations/`, `docs/`, `bin/cron/`, manifestes Composer/npm et quelques implémentations installées de Doctrine/Symfony. Aucun contenu de `.env.local` ou journal applicatif lu ou affiché.
- Scripts inline `python3 - <<'PY' … PY` : lecture de `composer.lock` (versions, marqueur `abandoned`, contraintes PHP), inventaire/count/tailles des fichiers, comparaison des références Vich au disque, comparaison des migrations enregistrées aux fichiers.
- Connexion SQLite **`mode=ro`** : `PRAGMA quick_check`, `page_count`, `freelist_count`, `page_size`, `foreign_keys`, `foreign_key_check`, `table_info`, `foreign_key_list(user)` ; SELECT de comptages par table, recherche de `data:image/` dans les colonnes textuelles, agrégats de métadonnées Messenger et versions de migrations. Aucun email, mot de passe, corps de message ou contenu éditorial exporté.
- Contrôle final : `git diff --check`, `git status --short` et vérification que seul ce document diffère. Pas de suite de tests, cache Symfony, build CSS, commande métier d'écriture, accès OVH ou diagnostic réseau.

Les résultats OVH, suppressions après sauvegarde, VACUUM, tests ciblés du correctif FK et livraisons mentionnés dans le suivi sont ceux communiqués par le propriétaire. Cette mise à jour documentaire n’a effectué aucune de ces opérations. Seules les lectures de `AGENTS.md`, de la demande et du document, ainsi que les contrôles Git locaux (`status`, branche, `diff --check` et liste des fichiers modifiés) ont été exécutés ; seul ce document a été modifié.
