---
suivi: 1479
date: 2026-09-08
sujet: Diagnostic disparition staging SIWI (2e occurrence)
chantier: reprise_siwi / securite_ops
type: diagnostic
statut: poussé
hash: 654565e2
fichiers:
  - docs/suivi/SUIVI_1479_diag_disparition_staging_siwi.md
---

# SUIVI #1479 — PHASE 1 DIAGNOSTIC — disparition staging SIWI

## PROMPT ENVOYÉ

PHASE 1 lecture seule. Staging `siwi_*` vide en production (2e occurrence ;
cause jamais établie). Hypothèse prioritaire : environnement non-prod écrivant
sur la base prod. Recenser tout ce qui vide les tables, instruire
`run_tests_1324.php`, B2 (fichier commité → base prod ?), phpunit /
RefreshDatabase, chemin fantôme `/home/hectare/envol.hectare.fr`, fiche #1370
incident 22/08. Aucun code applicatif, aucune migration, aucun `.vue`, aucun
rebuild, aucun `siwi:import`. Aucun chiffre inventé.

## BASE GIT

```
git log --oneline -1 origin/main
668690d3 chore(suivi): hash fiche #1478
```

(Attendu prompt : `668690d3` — conforme.)

## FICHES LUES

- **#1370** — LEÇON : scripts diag sans garde SAPI + DocumentRoot hors
  `public/` = exécution FPM automatisée. Protection `tools/diag` + garde CLI
  `siwi:import`. Incident échantillon 22/08 **cité, cause non établie**.
  Chemin `/home/hectare` vs `/home/hectare-envol` déjà documenté. **A changé
  l’approche** : #1370 a protégé `siwi:import` et `tools/`, **pas**
  `database/data/siwi_import_sample/run_tests_1324.php` dont le `DELETE` tourne
  **avant** l’appel Artisan.
- **#1324** — création staging + `siwi:import` upsert ; CSV hors dépôt
  `/home/hectare-envol/siwi-import/` ; échantillon synthétique commité.
- **#1328** — procédure de réimport `--apply` ; pas de sauvegarde staging.
- **#1325** — page arbitrage (écritures TRAVAIL ciblées, pas de mass-delete).
- **#1340** — PDF via `storage/app/private/siwi-import` (disque distinct du
  dossier CSV). Piste de reprise fichiers, pas des CSV.
- **#1323** — prérequis `reference_siwi` ; hors vidage staging.

Aucune autre fiche n’établit la cause du 22/08 ni un dump CSV de secours.

---

## VERDICT

### 1. CAUSE ÉTABLIE (mécanisme du vidage actuel)

Le script commité `database/data/siwi_import_sample/run_tests_1324.php`
exécute, **sans garde `PHP_SAPI`**, un `DELETE` sans `WHERE` sur les quatre
tables staging **avant** tout import. Les logs de production fournis (04, 05
et 06/09, SAPI `fpm-fcgi`) correspondent **ligne pour ligne** à ce script :
`siwi.import_refuse_hors_cli` (appel Artisan bloqué par le garde #1370) puis
erreur `Attempt to read property "facture_ht" on null` à la ligne 85.

Preuve d’ordre d’exécution (le crash L85 **implique** que L51–55 ont déjà
couru dans la même requête) :

- L51–55 : vidage inconditionnel des quatre tables.
- L65 puis L77 : `Artisan::call('siwi:import', …)` → `PHP_SAPI !== 'cli'` →
  log `siwi.import_refuse_hors_cli` + `FAILURE` — **aucun ré-écrit**.
- L83–85 : `first()` sur `G3000` → `null` → accès `facture_ht`.

Ce mécanisme **explique** :

- la destruction **sélective** des seules tables `siwi_*` (les autres tables
  métier survivent) ;
- l’absence de `truncate` applicatif SIWI et l’absence de log destructeur
  côté `siwi:import` (le `DELETE` n’est pas journalisé) ;
- la répétition (script toujours dans le dépôt, toujours sans garde SAPI,
  `database/` sans `.htaccess` Deny — contrairement à `tools/` depuis #1370) ;
- le changement de symptôme depuis #1370 : **avant** le garde CLI, le même
  script **rechargeait l’échantillon** après le `DELETE` (pollution) ;
  **après**, l’import FPM est refusé → tables **vides**.

Attribution process → base de production : **établie si et seulement si**
les lignes de log citées sont bien dans le `laravel.log` de l’install qui
sert `hectareenvol`. `base_path()` journalisé n’est **pas** lu depuis un
`config:cache` ; c’est le répertoire réel de l’Application PHP. Voir C1.

### 2. HYPOTHÈSES CLASSÉES PAR VRAISEMBLANCE

1. **DocumentRoot / scanner FPM** (très vraisemblable, même vecteur que
   #1370) : une requête HTTP exécute le `.php` hors `public/` parce que le
   DocumentRoot Apache n’est pas `public/`, ou qu’une vhost/alias pointe
   encore vers une ancienne arborescence. Aucune route Laravel n’inclut ce
   fichier.
2. **Seconde install / symlink `/home/hectare` vs `/home/hectare-envol`**
   (vraisemblable, déjà #1370 OBJET 5) : le process FPM a `base_path`
   fantôme ; s’il partage le `.env` ou un symlink vers les mêmes fichiers,
   c’est **la même base**. Le `ls` actuel en échec n’infirme pas l’existence
   au 04–06/09 (renommage, symlink retiré).
3. **`.env` local / hors prod pointant vers `hectareenvol.mysql.db`**
   (hypothèse d’entrée, **non étayée par le dépôt**) : aucun fichier
   **commité** ne contient `hectareenvol` ni `hectareenvol.mysql.db` (B2 =
   **NON**). Un `.env` gitignoré local **pourrait** le faire ; ce n’est pas
   démontrable ici. Un `RefreshDatabase` / `migrate:fresh` sur cette
   connexion viderait **toute** la base — contredit par la survie des
   autres tables **sauf** si le vecteur est précisément `run_tests_1324.php`
   lancé en local contre la prod (alors `PHP_SAPI` serait `cli`, l’import
   **ne** serait **pas** refusé, le staging serait **repeuplé par
   l’échantillon**, pas vide). Les logs `fpm-fcgi` + `import_refuse_hors_cli`
   **écartent** un lancement CLI local comme cause du **vidage actuel**.

### 3. PISTES ÉCARTÉES (avec preuve)

| Piste | Preuve d’exclusion |
|---|---|
| `siwi:import` destructif / truncate | Upsert uniquement (`SiwiImportService` L1085–1208). Garde CLI L40–48. Aucun `truncate` SIWI dans `app/`. |
| `dev:purge-factures` | `PurgeFacturesTest.php` L16–19 : `isProduction()` → `FAILURE` en première instruction ; tables visées = factures, pas `siwi_*`. |
| `migrate:fresh` / `migrate:refresh` / `db:wipe` | Grep dépôt (hors `vendor/`, hors `_tmp_*`) : **zéro** occurrence. Un fresh viderait **toutes** les tables, pas seulement `siwi_*`. |
| `RefreshDatabase` / tests phpunit contre MySQL prod | Trait présent dans **157** fichiers `tests/` ; connexion forcée `sqlite` / `:memory:` (`phpunit.xml` L26–27). Un RefreshDatabase MySQL viderait **toute** la base. Survie des autres tables + SAPI `fpm-fcgi` (phpunit = CLI) **excluent** cette cause pour l’incident actuel. |
| `DatabaseTruncation` / `DatabaseMigrations` | **Zéro** occurrence dans `tests/`. |
| Migrations SIWI récentes | Dernière migration SIWI `2026_08_14_170000` ; `down()` = `dropIfExists` uniquement si rollback explicite. Aucune migration SIWI après le 14/08. |
| Truncate dans `SiwiImportCommand` / `SiwiImportService` | Confirmé absent (mesure Robin + relecture). |
| Seeders / factories / jobs / observers SIWI | Aucun seeder `siwi_*` ; aucun Job ; `AppServiceProvider` n’observe aucun modèle SIWI (L88–93 = TMA/Marche/Avenant/Facture/ProgrammeEtape/Reservation). |
| Nettoyage CSV par l’import | Aucun `unlink` / `File::delete` / `Storage::delete` / `rename` dans les classes `Siwi*`. Le dossier CSV vide n’est **pas** expliqué par le code d’import. |

**B2 — un fichier commité pointe-t-il vers la base de production ?**

**NON.** Grep `hectareenvol` et `hectareenvol.mysql.db` sur le dépôt commité
(`*.php`, `*.xml`, `*.yml`, `*.yaml`, `*.json`, `*.md`, `.env.example`,
`phpunit.xml`, `config/`) : **aucune occurrence**. Défauts : `.env.example`
`DB_HOST=127.0.0.1` / `DB_DATABASE=erp_immo` ; `config/database.php` mysql
`host` défaut `127.0.0.1`, `database` défaut `laravel`. Pas de
`docker-compose`, `Dockerfile`, `Makefile`, `.github/workflows`,
`.gitlab-ci.yml`, `.devcontainer`, `.env.testing`.

---

## A. QUI PEUT VIDER CES TABLES

### A1 — Inventaire des écritures `siwi_*`

| Fichier | Nature | Vidage massif ? |
|---|---|---|
| `database/data/siwi_import_sample/run_tests_1324.php` L51–55 | `DB::table(...)->delete()` sans WHERE sur les 4 tables | **OUI — seul** |
| `app/Console/Commands/SiwiImportCommand.php` | CLI ; délègue au service ; garde SAPI L40–48 | Non (upsert) |
| `app/Services/SiwiImportService.php` | `upsertOperation/Marche/Mapping/Document` L1085–1208 ; `marquerProgrammeNonRepris` / `annulerProgrammeNonRepris` = UPDATE ciblés | Non |
| `app/Services/SiwiImportPageService.php` | UPDATE TRAVAIL (notes, poste retenu, suggestion, contrôle) ; L1837–1848 assert tables | Non |
| `app/Services/SiwiRepriseActionService.php` | UPDATE `etat_cloture`, liaison programme, SharePoint | Non |
| `app/Services/SiwiDocumentDepotService.php` | UPDATE `depot_statut` / SharePoint item | Non |
| `app/Http/Controllers/SiwiImportController.php` | Délègue aux services ci-dessus ; lecture PDF | Non |
| `app/Policies/SiwiImportPolicy.php` | Autorisation GS_GROUPE via `m365GroupNames()` brut | N/A |
| `app/Models/Siwi{Marche,Operation,Document,MappingPoste}.php` | Eloquent `$table` ; pas de boot/deleting | Non |
| `app/Providers/AppServiceProvider.php` L59 | `Gate::policy(SiwiOperation, SiwiImportPolicy)` | N/A |
| Migrations `2026_08_13_160000` à `2026_08_14_170000` | `Schema::create` / `table` ; `down()` = `dropIfExists` | Seulement si rollback |
| `database/data/siwi_import_sample/build_sample.php` / `build_corrupt.php` | Écrit des CSV **disque**, pas la DB | Non |
| Seeders / factories | Aucune référence `siwi_*` | Non |
| `routes/console.php` | Aucun `siwi:import` | Non |
| Jobs / listeners / observers SIWI | Absents | Non |

Contrôleurs / page : peuvent modifier des **lignes** TRAVAIL, jamais vider
le staging.

### A2 — Recherche exhaustive (tout le dépôt, pas seulement SIWI)

Motifs cherchés : `truncate`, `TRUNCATE`, `->delete()` sans where,
`DB::statement('DELETE`, `DROP TABLE`, `forceDelete`, `migrate:fresh`,
`migrate:refresh`, `db:wipe`, `RefreshDatabase`, `DatabaseMigrations`,
`DatabaseTruncation`, `FOREIGN_KEY_CHECKS=0`. Périmètre : `app/`,
`database/`, `tests/`, `routes/`, `config/`, `bootstrap/`, `resources/`,
`tools/`, `docs/`, racine, `*.sh`, `*.php`, `*.yml`, `*.yaml`, `*.json`,
CI.

**Destructive SQL / Artisan :**

- `DB::table($t)->delete()` sans where sur **siwi_*** : **uniquement**
  `run_tests_1324.php` L52–55.
- Autre mass-delete : `AvisImpositionCsvImporter.php` L252–254 (tables
  avis, pas SIWI) ; `PurgeFacturesTest.php` L22–31 (`truncate` factures,
  garde prod L16).
- `FOREIGN_KEY_CHECKS=0` : uniquement `PurgeFacturesTest.php` L22.
- `migrate:fresh` / `migrate:refresh` / `db:wipe` : **aucune** occurrence
  dans les sources du projet (le trait `RefreshDatabase` vendor appelle
  `migrate:fresh` en interne — voir B4).
- `forceDelete` : Commercial / documents facture / avis — pas `siwi_*`.

**Pourquoi les autres tables ont survécu (explication, pas supposition) :**
un `migrate:fresh` / `db:wipe` / `RefreshDatabase` MySQL détruirait le
schéma métier entier. L’état observé (staging SIWI vide, reste de l’ERP
présent) est le résultat **exact** des quatre `delete()` de
`run_tests_1324.php`, pas d’un wipe global.

### A3 — `run_tests_1324.php` (lecture intégrale)

Fichier commité dans `990f819f` (#1324, 2026-08-13). **Jamais modifié
depuis.** Aucune référence à ce fichier dans `app/`, `routes/`, `tests/`,
`resources/` (grep `run_tests_1324` et `siwi_import_sample` hors le
répertoire lui-même : vide).

**Ligne par ligne (effet DB) :**

| Lignes | Action |
|---|---|
| 14–17 | `vendor/autoload` + `bootstrap/app.php` + `Kernel::bootstrap()` — charge le `.env` **de l’install qui exécute le fichier**. Aucune vérification `APP_ENV` / `PHP_SAPI`. |
| 19 | `$path = realpath(__DIR__)` = dossier échantillon du dépôt (CSV synthétiques), **pas** `/home/hectare-envol/siwi-import`. |
| 51–55 | **`DELETE` sans WHERE** `siwi_documents`, `siwi_mapping_postes`, `siwi_marches`, `siwi_operations`. **Aucun log.** |
| 57–60 | `parseDecimalPoint` — pas d’écriture. |
| 62–70 | `Artisan::call('siwi:import', ['--path' => $path])` dry-run. Depuis #1370, hors CLI → refus, counts restent 0. |
| 72–74 | `COUNT` tables ERP (lecture). |
| 76–80 | `Artisan::call(..., '--apply' => true)` — **refusé hors CLI** ; n’écrit pas. |
| 82–85 | `siwi_marches` WHERE `ref_siwi=G3000` → `null` → **fatal** `facture_ht` on null. **Fin de script.** |
| 87–195 | Jamais atteints en FPM depuis #1370 (crash L85). En CLI réussi : UPDATE `G1000`, réimport, `marquerProgrammeNonRepris`, tentatives CSV corrompus. |

**Écrit-il / supprime-t-il / remplace-t-il ?**

- **Supprime : oui, toujours, en premier, les 4 tables.**
- **Remplace :** seulement si `siwi:import --apply` aboutit (CLI). Le path
  est l’échantillon synthétique du dépôt, pas les CSV prod.
- **Depuis #1370 en FPM :** supprime, **ne remplace pas**.

**Comment est-il atteint depuis le web ?**

- **Pas** via une route Laravel (`routes/web.php` L331–380 = page reprise,
  aucune inclusion du script).
- **Pas** via un test PHPUnit.
- Script **autonome** : `require` vendor + bootstrap. N’importe quel moteur
  PHP qui ouvre ce chemin l’exécute.
- Vecteur FPM (établi pour `tools/diag` par #1370 OBJET 1) : si DocumentRoot
  = racine projet (pas `public/`), une URL du type
  `/database/data/siwi_import_sample/run_tests_1324.php` exécute le fichier.
  `public/.htaccess` ne protège que ce qui passe par `public/`.
- `#1370` a posé `tools/.htaccess` + `tools/diag/.htaccess` (`Require all
  denied`). **Aucun `.htaccess` sous `database/`.** Le scanner / robot qui
  frappait `tools/diag` a un successeur naturel ici.
- Preuve d’exécution FPM : logs Robin (SAPI `fpm-fcgi` + fichier:ligne 85).

**Pourquoi `base_path` = `/home/hectare/envol.hectare.fr` ?**

`bootstrap/app.php` L18 : `Application::configure(basePath: dirname(__DIR__))`.
`base_path()` = répertoire contenant `bootstrap/`, tel que résolu par PHP
pour **cette** exécution. Ce n’est **pas** une valeur de `config/siwi.php`
ni d’un cache de config. Voir C1.

### A4 — Nettoyage du dossier CSV `/home/hectare-envol/siwi-import/`

**Non trouvé dans le code.**

- `SiwiImportCommand.php` : option `--path` défaut
  `/home/hectare-envol/siwi-import` (L30, L52) ; lecture `EXPECTED.json`
  via `File::get` (L64) ; **pas** de delete/move/unlink.
- `SiwiImportService.php` L90–99 : exige la **présence** des 4 CSV, sinon
  `RuntimeException` **avant** écriture. Ne les déplace pas.
- Grep `unlink` / `File::delete` / `Storage::delete` / `File::move` dans
  `**/Siwi*.php` : **vide**.
- Deux emplacements distincts : CSV import =
  `config/siwi.php` L10 `SIWI_IMPORT_PATH` défaut
  `/home/hectare-envol/siwi-import` ; PDF =
  `config/filesystems.php` L58–62 `storage_path('app/private/siwi-import')`.
  Vider le dossier CSV **n’efface pas** les PDF Storage.

Le dossier vide depuis le 14/08 07h42 (mesure Robin, jour des lots
#1340–#1359) n’a **pas** d’explication dans le code d’import. Pistes
externes (non établies) : déplacement manuel post-import, autre script hors
dépôt, nettoyage hébergeur. **Manque : aucune sauvegarde CSV dans le
dépôt.**

---

## B. HYPOTHÈSE ENVIRONNEMENT CROISÉ

### B1 — Fichiers de configuration DB commités

| Fichier | Présent ? | Contenu pertinent |
|---|---|---|
| `config/database.php` | oui | `default` = `env('DB_CONNECTION', 'sqlite')` L20 ; mysql host `env('DB_HOST', '127.0.0.1')` L50 ; database `env('DB_DATABASE', 'laravel')` L52 |
| `.env.example` | oui | `DB_CONNECTION=mysql` L23 ; `DB_HOST=127.0.0.1` L24 ; `DB_DATABASE=erp_immo` L26 |
| `phpunit.xml` | oui | `DB_CONNECTION=sqlite` L26 ; `DB_DATABASE=:memory:` L27 ; `DB_URL=""` L28 ; **pas** `force="true"` |
| `.env.testing` | **absent** | — |
| `docker-compose*.yml` / `Dockerfile` / `Makefile` | **absents** | — |
| `.github/workflows/` / `.gitlab-ci.yml` | **absents** | — |
| `.devcontainer/` | **absent** | — |
| `.env` / `.env.production` | gitignorés (`.gitignore` L3–5) | **non lus, non demandés** |

`composer.json` script `setup` (L44–50) : `artisan migrate --force` en local
après copie `.env.example` — pas un cron prod.

### B2 — Référence en dur à la base de production ?

**NON.** Voir verdict. Aucun `hectareenvol` / `hectareenvol.mysql.db` dans
les fichiers commités.

### B3 — phpunit.xml et repli MySQL

`phpunit.xml` L26–27 :

```
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
```

`config/database.php` L20 : si `DB_CONNECTION` est absent, défaut
**sqlite**, pas mysql.

PHPUnit 10+ (projet : `phpunit/phpunit ^12.5.12`, `composer.json` L25) :
une balise `<env>` **sans** `force="true"` **n’écrase pas** une variable
déjà présente dans l’environnement **processus**. Risque résiduel : un
`export DB_CONNECTION=mysql` dans le shell **avant** `phpunit` ferait
ignorer sqlite. Le `.env` Laravel **n’est pas** ce cas : Dotenv ne
surcharge pas les variables déjà définies par PHPUnit.

Si `pdo_sqlite` est absent et que sqlite est bien la connexion : les tests
`RefreshDatabase` échouent ou `markTestSkipped` (ex.
`ComptaTableauBord1478FeatureTest.php` L23–30,
`CompleterSaisieFactureTaskSyncTest.php` L30–32) — **ils ne basculent pas
silencieusement sur mysql**.

**Ils ne tombent pas sur mysql par absence de pdo_sqlite.** Le seul repli
dangereux est un `DB_CONNECTION` **déjà** dans l’environnement OS. Même
dans ce cas, `RefreshDatabase` viderait **toute** la base — incompatible
avec l’observation actuelle.

### B4 — Traits de reset DB dans `tests/`

- `RefreshDatabase` : **157 fichiers**.
- `DatabaseMigrations` : **0**.
- `DatabaseTruncation` : **0**.

Connexion : celle du container Laravel après phpunit.xml = sqlite mémoire
sauf env OS forcé (B3). `tests/TestCase.php` est vide (L7–10) : **aucune**
connexion forcée, **aucun** garde production.

### B5 — TestCase / CreatesApplication / garde prod

- `tests/TestCase.php` : classe vide, pas de `setUp` global.
- Pas de `tests/CreatesApplication.php` (Laravel 11+ : bootstrap via
  `bootstrap/app.php`).
- **Aucun mécanisme** n’empêche un test de toucher la production si la
  connexion MySQL prod est celle du process. **Défaut majeur confirmé**
  (isole, n’explique pas le vidage **sélectif** actuel).

### B6 — Garde-fou global migrations destructives en production ?

**Non, isolé.**

- `PurgeFacturesTest.php` L16–19 : `app()->isProduction()` — **cette
  commande seulement**.
- Aucun `DB::prohibitDestructiveCommands()` / `PreventDestructiveCommands`
  dans `AppServiceProvider` ni `bootstrap/app.php`.
- `SiwiImportCommand` : garde **SAPI CLI**, pas `APP_ENV=production`.
- `run_tests_1324.php` : **aucune** garde.

---

## C. LE CHEMIN FANTÔME

### C1 — `/home/hectare/envol.hectare.fr` vs `/home/hectare-envol/…`

`base_path()` vient de `bootstrap/app.php` L18
(`dirname(__DIR__)` du fichier `bootstrap/app.php` chargé). Ce n’est pas
`config/siwi.php` (défaut `/home/hectare-envol/siwi-import`, autre chemin).

`bootstrap/cache/.gitignore` ignore `*` sauf `packages.php`,
`services.php`, `.gitignore`. **`config.php` n’est pas commité.** Un
`config:cache` (interdit sur OVH, règle projet) figerait host/database
`.env` **mais pas** `base_path()`. Piste « cache de config explique le
chemin fantôme » : **écartée pour C1**. Elle resterait pertinente pour des
identifiants DB périmés — à vérifier par présence du fichier, pas par
lecture de son contenu ici.

Commande (présence uniquement, ne pas `cat` le fichier s’il contient des
secrets) :

```
ls -la /home/hectare-envol/envol.hectare.fr/bootstrap/cache/config.php
ls -la /home/hectare/envol.hectare.fr 2>&1
ls -la /home/hectare 2>&1
readlink -f /home/hectare-envol/envol.hectare.fr
php -r "require 'vendor/autoload.php'; \$app=require 'bootstrap/app.php'; echo \$app->basePath();"
```

(La dernière : CLI de l’install **courante**, SAPI `cli` — comparer au
`base_path` des logs FPM.)

### C2 — Occurrences dans le dépôt

Chemin exact `/home/hectare/envol.hectare.fr` : **uniquement** la fiche
`docs/suivi/SUIVI_1370_protection_diag_et_import_staging.md` L67–72 (déjà
le mystère). **Aucun durcissement dans `app/`.**

`/home/hectare/` hors `-envol` ailleurs : `KomiDoc_Invoices_To_SageIntacct_Invoices.php`
commente `/home/hectare/SAGE/IMPORT` (autre arborescence Sage, hors SIWI).
`PROJECT.md` / passation / tools/diag : chemin **avec** `-envol`.

### C3 — Indices d’une seconde installation

- #1370 OBJET 5 : hypothèse dominante déjà posée (copie / ancien
  déploiement, scanner, `storage/logs` propre).
- Logs `siwi.import_refuse_hors_cli` avec `base_path` fantôme **dans** le
  journal que Robin identifie comme prod → soit même fichiers (symlink /
  DocumentRoot alias), soit copie du `.env` prod sur l’ancienne
  arborescence (alors **même base**, hypothèse B confirmée pour **cette**
  copie).
- `ls` actuel en échec : l’arborescence a pu disparaître **après** les
  04–06/09. Ne pas conclure qu’elle n’a jamais existé.
- `crontab` SSH absent (mesure Robin) n’exclut pas un cron panel OVH ou un
  hit HTTP.

---

## D. CHRONOLOGIE

### D1 — Faits SIWI depuis #1324 (commits, dates)

| Date | Hash | Lot |
|---|---|---|
| 2026-08-13 | `435dac01` | #1323 prérequis `reference_siwi` |
| 2026-08-13 | `990f819f` | #1324 tables + `siwi:import` + **`run_tests_1324.php` (DELETE)** + échantillon |
| 2026-08-13 | `bf2f3e19` | #1325 page arbitrage |
| 2026-08-13 | `410dbbe7` | #1326 actions ERP |
| 2026-08-13 | `3fe52d5d` | #1328 qualification + bascule ; doc réimport `--apply` |
| 2026-08-13 | `98c21f9d` … `c113ec0c` | #1332–#1337 commune, TVA, hors périmètre, payé HT, PDF dialog |
| 2026-08-14 | `10782e46` … `f6d2e711` | #1338–#1359 PDF storage, bilan diag, poste, Lot F dépôt, fournisseur. **Dossier CSV daté 14/08 07h42 (mesure Robin).** |
| 2026-08-22 | — | **Incident échantillon** (cité #1370 ; **pas de commit**, cause non établie dans la fiche) |
| 2026-08-31 | `f0380602` | #1370 garde CLI `siwi:import` + Deny `tools/` ; **pas** de garde sur `run_tests_1324.php` |
| 2026-08-31 | `cf1b97bf` | #1375 vignette programme créé |
| 2026-09-04 / 05 / 06 | — | 3× FPM : `siwi.import_refuse_hors_cli` + fatal L85 `run_tests_1324.php` (mesure Robin) |
| 2026-09-08 | ce lot | Diagnostic #1479 |

### D2 — Restitution LITTÉRALE fiche #1370 sur l’incident du 22/08

Commentaire de code `SiwiImportCommand.php` L24 :

`/** Au-delà : --apply refuse sans --force-replace (#1370, incident échantillon 22/08). */`

Avertissement runtime L91–95 :

`⚠ ATTENTION — mode --apply : les CSV vont UPSERT le staging siwi_* existant.`
`  Les lignes absentes du CSV ne sont PAS effacées, mais un jeu échantillon`
`  peut polluer / masquer un staging réel (incident 22/08/2026).`

Fiche `docs/suivi/SUIVI_1370_protection_diag_et_import_staging.md` :

- L54 : `Si staging siwi_marches > 50 et --apply sans --force-replace → refus + log siwi.import_apply_refuse_staging_peuple.`
- L90–91 (LEÇON) : `Import staging sans --force-replace sur base peuplée = risque échantillon.`

**La fiche #1370 n’établit aucune cause** du 22/08 (pas de responsable, pas
de SAPI, pas de path). Elle décrit un **symptôme** : jeu échantillon qui
**pollue / masque** un staging réel — donc un staging **non vide** après
coup, inverse de l’incident actuel. Cohérent avec `run_tests_1324.php` **en
CLI** (ou FPM **avant** le garde #1370) : DELETE puis import échantillon.
Non démontré pour le 22/08 (pas de log cité dans #1370).

### D3 — Provenance CSV / sauvegarde (#1324, #1328, #1370)

- #1324 L73 : `CSV hors dépôt : /home/hectare-envol/siwi-import/`
- #1324 L61–62 : chiffres recette prod non vérifiés depuis Cursor ; échantillon **synthétique** uniquement.
- #1328 L62–63 : même path pour dry-run / apply ; recette « à faire par Robin ».
- #1370 : inventaire PDF indexés ; **pas** de copie CSV.

**Aucun** des trois ne mentionne SharePoint, un dump SQL staging, ou un
second emplacement de secours pour les CSV sources. Les CSV de
`database/data/siwi_import_sample/` sont **synthétiques** (G1000, G3000,
OP-SOLDE…) — **inutilisables** pour restaurer la prod (recette
`SiwiImportService::RECIPE_NB_MARCHES = 1405`, constante de code, vs
échantillon réduit).

### D4 — Sauvegarde du staging SIWI ?

**Aucune** commande, script ou procédure de dump `siwi_*` dans le dépôt.
**Manque** à signaler.

---

## E. REMÉDIATION — À PROPOSER, PAS À EXÉCUTER

### E1 — Staging non destructible par accident

1. **Retirer ou inarmer `run_tests_1324.php`** (priorité absolue) : garde
   `PHP_SAPI !== 'cli'` en **première** instruction, **ou** suppression du
   fichier, **et** `.htaccess` Deny sur `database/` (comme `tools/` #1370).
   Le garde CLI de `siwi:import` est **insuffisant** : le `DELETE` est
   **avant** l’Artisan::call.
2. Refus d’import si CSV absents **avant** toute écriture : déjà le cas
   dans `SiwiImportService` L96–99 — **sans effet** sur le script de test
   qui delete d’abord.
3. Journaliser tout `DELETE`/truncate staging (aujourd’hui silencieux).
4. Sauvegarde automatique (dump 4 tables) avant tout remplacement / avant
   `--force-replace`.
5. Refus si dossier source vide **et** si le path est l’échantillon du
   dépôt alors que `APP_ENV=production`.

### E2 — Empêcher un environnement non-prod de toucher la base prod

B2 = NON sur le dépôt ; le risque restant est un `.env` **non commité**.

1. Compte MySQL prod : hôte autorisé = **uniquement** le cluster OVH
   (`hectareenvol.mysql.db`), pas `0.0.0.0` / IP bureautiques. (À vérifier
   côté panel OVH, hors code.)
2. `DB::prohibitDestructiveCommands()` si `APP_ENV=production` (fresh,
   wipe, drop) — n’aurait **pas** bloqué les `delete()` Eloquent du script.
3. PHPUnit : `<env ... force="true"/>` sur `DB_CONNECTION` / `DB_DATABASE`.
4. Garde runtime : refuser bootstrap destructif si
   `DB_HOST` contient `mysql.db` **et** (`PHP_SAPI !== 'cli'` **ou**
   `APP_ENV !== 'production'`). À concevoir en lot suivant.
5. Ne jamais copier le `.env` prod sur une seconde arborescence.

### E3 — `run_tests_1324.php`

**Oui : le retirer du dépôt ou le rendre inexécutable** (garde SAPI + Deny
Apache + hors webroot). Un script de test qui bootstrap Laravel et vide
quatre tables, commité sous `database/`, exécutable FPM, est une anomalie
opératoire. C’est le seul élément qui s’exécute de façon inexpliquée **et**
qui a le pouvoir de vider exactement ces tables.

### E4 — Procédure de reprise (ne pas lancer dans ce lot)

1. **Ne pas** `siwi:import` tant que E1 n’est pas en place (un réimport
   sans CSV prod échoue ; un réimport échantillon **polluerait** — 22/08).
2. Chercher les CSV d’origine **hors** serveur : export SIWI métier chez
   l’émetteur, PC de Robin, e-mail, SharePoint **si** quelqu’un les y a
   déposés (aucune trace dans les fiches).
3. Vérifier `storage/app/private/siwi-import/` (PDF #1340) — ne restaure
   **pas** les tables, utile pour le dépôt documentaire après réimport.
4. Une fois CSV retrouvés : les copier dans le dépôt **privé** ou
   SharePoint **et** un dump SQL `siwi_*` versionné hors mutualisé, **puis**
   `siwi:import --path=… --apply --force-replace` en CLI uniquement.
5. L’échantillon `database/data/siwi_import_sample/` **n’est pas** une
   sauvegarde prod.

### E5 — Commandes de diagnostic complémentaires (lecture seule)

Ne rien écrire. Ne pas lancer `siwi:import`. Ne pas `cat` de `.env`.

```
# 1. Confirmer que le log FPM est bien CELUI de l'install qui sert le site
#    (prouve process → base). Coller pwd + ls du fichier log, pas le .env.
pwd
ls -la storage/logs/laravel.log
grep -n "run_tests_1324\|siwi.import_refuse_hors_cli\|facture_ht" storage/logs/laravel.log

# 2. Access log Apache / OVH : le script a-t-il été hit en HTTP ?
grep -R "run_tests_1324" ~/logs /home/hectare-envol/logs 2>/dev/null
# Prouve : URL exacte + User-Agent (scanner vs humain) + horaire.

# 3. Cache config interdit (présence seulement — ne pas afficher le contenu)
ls -la bootstrap/cache/config.php
# Présent → chemins/identifiants figés possibles (pas base_path). Absent → piste écartée.

# 4. Seconde arborescence / symlink
ls -la /home/hectare /home/hectare/envol.hectare.fr 2>&1
ls -la /home/hectare-envol/envol.hectare.fr
readlink -f /home/hectare-envol/envol.hectare.fr
# Panel OVH : DocumentRoot du vhost (public/ ou racine projet ?).

# 5. PHP FPM base_path actuel (CLI ≠ preuve FPM, mais calibre l'install SSH)
php -r "echo PHP_SAPI, PHP_EOL, dirname(__DIR__, 0);"
php -r "require __DIR__.'/vendor/autoload.php'; \$a=require __DIR__.'/bootstrap/app.php'; echo \$a->basePath(), PHP_EOL;"

# 6. Autres PHP autonomes sous database/ encore exécutables (même trou)
find database -name '*.php' -not -path 'database/migrations/*' -not -path 'database/seeders/*' -not -path 'database/factories/*'
# run_tests_1324.php, build_sample.php, build_corrupt.php, journal : aucun PHP_SAPI.

# 7. PDF encore là ? (pas les CSV)
ls -la storage/app/private/siwi-import | head
# Prouve : le dépôt documentaire disque a survécu ou non (indépendant des tables).

# 8. InnoDB UPDATE_TIME NULL n'est PAS une preuve d'absence de DELETE
#    (souvent NULL sur InnoDB). Préférer :
#    SHOW TABLE STATUS LIKE 'siwi_%';
#    et si binlog OVH disponible (souvent non en mutualisé) :
#    mysqlbinlog … | grep -i 'siwi_'
#    Un DELETE FROM siwi_marches sans WHERE dans le binlog clôturerait l'attribution.

# 9. Sans coller le .env : question oui/non
#    Le DB_HOST de cette install (SSH hectare-envol) est-il bien
#    hectareenvol.mysql.db ? (echo uniquement le host, jamais user/password)
php -r "echo 'SAPI='.PHP_SAPI.PHP_EOL; require 'vendor/autoload.php'; \$a=require 'bootstrap/app.php'; \$a->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); echo 'host='.config('database.connections.mysql.host').PHP_EOL; echo 'db='.config('database.connections.mysql.database').PHP_EOL;"
# Attention : n'affiche que host + nom de base, pas le mot de passe.
```

La commande 9 affiche host + nom de base (pas un secret de compte). Si
Robin refuse même cela, s’en tenir aux questions oui/non panel OVH.

---

## LEÇON

Le garde CLI posé sur `siwi:import` (#1370) a **changé le symptôme** d’un
script de test commité qui vide le staging **avant** d’appeler cette
commande : pollution par échantillon → tables vides. Protéger la commande
sans protéger l’appelant destructif est un trou. `tools/` a reçu Deny
Apache ; `database/data/` non. Un `DELETE` sans journal est invisible dans
`laravel.log` jusqu’à l’exception **suivante**. `information_schema.UPDATE_TIME`
InnoDB à NULL ne prouve pas l’absence de DML.

## DÉPLOIEMENT-TEST

Aucun code applicatif. Après `git pull` : lire cette fiche. **Ne pas**
réimporter. **Ne pas** exécuter `run_tests_1324.php`. Coller les sorties
E5 (1)(2)(4) avant tout lot PHASE 2.
