# DEPLOIEMENT.md — app.iradaventure.ma

Configuration serveur réelle, éprouvée par les projets jumeaux (app Ruwad, app Mobdie).
Le pas-à-pas complet côté cPanel est dans `DEPLOIEMENT-IRADA-PAS-A-PAS.md` (dossier parent,
hors dépôt). Ce document fige les valeurs à utiliser.

## 1. Hébergement

| Élément | Valeur |
|---|---|
| cPanel | CapConnect — `saturn.capconnect.com:2083` |
| Compte | `mobdiema` (home `/home/mobdiema`) |
| Docroot | `/home/mobdiema/public_html/app.iradaventure.ma/public` |
| Base MySQL | `mobdiema_irada_app` |
| PHP | 8.3 (voir §2 — ligne AddHandler obligatoire) |
| DNS | chez CapConnect — le Zone Editor cPanel fonctionne pour `iradaventure.ma` (contrairement à mobdie.ma) |

## 2. public/.htaccess — pièges connus

Le sous-domaine hérite du `.htaccess` parent en **ea-php82**. Sans la ligne suivante **en tête**
de `public/.htaccess`, l'app tourne en PHP 8.2 et casse :

```apache
AddHandler application/x-httpd-ea-php83 .php .php8 .phtml
```

Ajouter aussi explicitement :

```apache
DirectoryIndex index.php
```

et conserver les en-têtes de sécurité compatibles Filament déjà présents dans le fichier.

## 3. Crons (à créer tels quels dans cPanel)

```cron
* * * * * /opt/cpanel/ea-php83/root/usr/bin/php -d memory_limit=512M /home/mobdiema/public_html/app.iradaventure.ma/artisan queue:work --stop-when-empty --max-time=55 --tries=1 --timeout=900 >/dev/null 2>&1
* * * * * /opt/cpanel/ea-php83/root/usr/bin/php /home/mobdiema/public_html/app.iradaventure.ma/artisan schedule:run >/dev/null 2>&1
```

Le planificateur porte, depuis la v1.4, deux tâches de plus (déclarées dans `routes/console.php`,
rien à créer dans cPanel) : le rappel du dimanche 18 h et la soumission automatique des semaines le
lundi 6 h (§5.6).

### La chaîne des délais (v3.14.1) — l'ordre n'est pas négociable

Quatre plafonds se surveillent l'un l'autre. Ils doivent rester **strictement croissants**, du plus
court au plus long :

| # | Plafond | Où | Valeur |
|---|---|---|---|
| 1 | Appel HTTP à l'API Claude | `.env` → `ANTHROPIC_TIMEOUT` | `300` (défaut du socle : 120) |
| 2 | Exécution du Job d'analyse | `.env` → `AI_JOB_TIMEOUT_SECONDS` | `840` (défaut du socle) |
| 3 | Superviseur de file | cron → `queue:work --timeout=900` | `900` |
| 4 | Reprise d'un job réputé mort | `.env` → `DB_QUEUE_RETRY_AFTER` | `960` |

Le maillon le plus court doit **toujours** être celui de l'appel réseau : c'est le seul qui échoue
proprement, avec un message français affiché dans la fenêtre d'analyse. Si l'un des maillons
suivants cède le premier, il **tue** le processus en plein `curl_exec` — l'utilisateur voit une
fenêtre qui tourne sans fin, et `failed_jobs` une `TimeoutExceededException` sans cause lisible.
C'est exactement ce qui arrivait avant la v3.14.1, où le plafond du Job (2) valait **280 s en
dur** — soit MOINS que le délai d'un seul appel HTTP en production (300 s) : la chaîne était
inversée, et l'analyse d'un gros dossier d'AO ne pouvait qu'être tuée.

`ANTHROPIC_TIMEOUT` (1) et `DB_QUEUE_RETRY_AFTER` (4) sont déjà posées sur les serveurs de
production : **rien à faire à la mise à jour**. `AI_JOB_TIMEOUT_SECONDS` (2) est facultative — sans
elle, le socle applique 840 s, ce qui respecte déjà l'ordre. Ces valeurs ne sont à revoir que si
l'on change le `--timeout` du cron.

## 3 bis. Rester connecté — ce qu'il ne faut pas casser

Deux durées, pas une : la session dure 30 jours (`SESSION_LIFETIME=43200`) et le cookie
« rester connecté », coché par défaut à la connexion, ré-authentifie silencieusement quand elle
expire. Trois conditions, toutes vérifiées par la suite de tests :

- `SESSION_EXPIRE_ON_CLOSE=false` — sinon le cookie meurt à la fermeture du navigateur ;
- `SESSION_SECURE_COOKIE=true` en production, `SameSite=Lax`, `HttpOnly=true` ;
- le service worker (`public/sw.js`) **ne met rien en cache** : un service worker qui met en cache
  la page de connexion est la cause classique d'une application installée qui « déconnecte » ses
  utilisateurs alors que la session est intacte. Ne pas y ajouter de gestionnaire `fetch`.

## 4. E-mail

- SMTP : `saturn.capconnect.com`, port `465`, chiffrement `smtps` — **jamais** `mail.iradaventure.ma`.
- Boîtes à créer côté cPanel : `noreply@`, `facturation@`, `gerance@` (`contact@` existe déjà).
- SPF, DKIM et DMARC du domaine sont **déjà publiés et valides** — rien à modifier.
- Les identifiants IMAP/SMTP des boîtes gérées (contact@, facturation@, boîtes nominatives)
  ne vont **pas** dans le `.env` : ils se saisissent dans l'écran « Boîtes e-mail »
  (réservé au super-admin) et sont chiffrés en base.

## 5. Variables .env de production

Chaque variable est commentée dans `.env.example`. Valeurs notables :

| Variable | Valeur production |
|---|---|
| `APP_ENV` / `APP_DEBUG` | `production` / `false` |
| `APP_URL` / `APP_DOMAIN` | `https://app.iradaventure.ma` / `app.iradaventure.ma` |
| `DB_DATABASE` | `mobdiema_irada_app` |
| `CACHE_STORE` | `file` |
| `SESSION_ENCRYPT` | `true` |
| `SESSION_SECURE_COOKIE` | `true` — **obligatoire**, le site est en HTTPS (v1.4 §8.3) |
| `SESSION_LIFETIME` | `43200` (30 jours — v1.4 §8) |
| `SESSION_EXPIRE_ON_CLOSE` | `false` — sinon l'application installée redemande le mot de passe à chaque ouverture |
| `SESSION_SAME_SITE` / `SESSION_HTTP_ONLY` | `lax` / `true` |
| `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | notifications push (v1.4 §5.4) — générer **une seule fois** avec `php artisan irada:generer-cles-push` |
| `VAPID_SUBJECT` | `mailto:gerance@iradaventure.ma` |
| `QUEUE_CONNECTION` | `database` |
| `DB_QUEUE_RETRY_AFTER` | `960` (doit dépasser le `--timeout=900` du worker) |
| `ANTHROPIC_API_KEY` | clé du compte (jamais commitée) |
| `ANTHROPIC_MODEL` | `claude-sonnet-5` (identifiant exact) |
| `ANTHROPIC_TIMEOUT` | `300` (doit rester SOUS `AI_JOB_TIMEOUT_SECONDS` — §3, chaîne des délais) |
| `AI_JOB_TIMEOUT_SECONDS` | `840` (doit rester SOUS le `--timeout=900` du worker — §3, chaîne des délais) |
| `AI_MAX_DOCUMENT_CHARS` | `200000` (garde-fou de taille des analyses IA) |
| `MAIL_MAILER` / `MAIL_HOST` / `MAIL_PORT` / `MAIL_SCHEME` | `smtp` / `saturn.capconnect.com` / `465` / `smtps` |
| `MAIL_USERNAME` / `MAIL_PASSWORD` | boîte `noreply@iradaventure.ma` |
| `MAIL_FROM_ADDRESS` / `MAIL_FROM_NAME` | `noreply@iradaventure.ma` / `IRADA VENTURE` |
| `MAIL_REPLY_TO_ADDRESS` | `facturation@iradaventure.ma` |
| `MAIL_BCC_ADDRESS` | `facturation@iradaventure.ma` |
| `MAIL_GERANCE_ADDRESS` | `gerance@iradaventure.ma` |
| `SEED_SUPERADMIN_PASSWORD` / `SEED_JAWAD_PASSWORD` / `SEED_ATIF_PASSWORD` | voir §6 |

## 6. Mots de passe initiaux — IMPORTANT

Le seeding est rejoué sur le serveur et doit produire les mêmes identifiants. **Les empreintes
(hash) des mots de passe voyagent dans le dump** `database/dump-initial.sql` : les mots de
passe effectifs en production sont donc **ceux du `.env` du Mac au moment de la génération du
dump** (`irada:generer-dump`), pas ceux du `.env` du serveur. Chaque compte doit changer son
mot de passe à la première connexion (imposé par l'application). Aucun mot de passe n'est
écrit dans ce dépôt.

## 7. Mise en production des données

1. Sur le Mac : `php artisan irada:reprendre` (lit `GESTION IRADA_1.xlsx` — le fichier Excel
   ne quitte jamais le Mac), puis `php artisan irada:generer-dump` → produit
   `database/dump-initial.sql` + `database/controle-reprise.json` (embarqués dans le zip).
2. Sur le serveur : `php artisan migrate --force` puis
   `php artisan irada:charger-dump --no-interaction` (remplace `db:seed`).
3. Contrôle : `php artisan irada:controler-reprise` — compare la base aux compteurs du
   classeur et affiche le tableau des écarts.
4. Après la bascule : le fichier Excel est archivé en lecture seule, toute saisie se fait
   dans l'app. Régénérer le dump à J-1 avant la bascule définitive.

## 8. Interdits absolus côté serveur

- `public_html/fr/**` (plateforme comités) et la base `mobdiema_comites` : **intouchables**.
- Les crons existants des autres applications : ne pas les modifier.
- Les autres domaines et bases du compte : hors périmètre.

## 9. Mise à jour d'une installation existante — par version

Le tronc commun d'une mise à jour manuelle est toujours le même : déposer l'archive, extraire,
puis `php artisan migrate --force`, `optimize:clear`, `config:cache`, `route:cache`,
`view:cache`. Cette section ne note que ce qui **s'ajoute** à ce tronc, version par version.

### v3.13.2 — facture PDF : derniers réglages (options, N° AO, pied de page, une page)

**Se livre aux deux installations, à l'identique. Une migration additive** (trois colonnes
nullables/booléennes — `php artisan migrate --force` du tronc commun suffit, aucune donnée
modifiée). Aucune commande de reprise, aucune dépendance composer nouvelle.

Après mise à jour, vérification en 30 secondes : générer le PDF d'une facture existante — le
pied légal (ICE/RC/IF/TP + banque) doit être **au bas de la feuille**, et une facture courante
tenir sur **une page**. Dans le formulaire, « Options d'affichage » propose désormais le
**N° appel d'offres** (pré-rempli depuis l'AO du marché) et les bascules **sous-totaux de
section** / **numéros de lignes** (à oui par défaut : rien ne change sans geste explicite).

### v3.13.1 — factures : bouton « Émettre » (sortir une facture du brouillon)

**Se livre aux deux installations, à l'identique. AUCUNE migration, aucune commande de reprise,
aucune dépendance composer nouvelle.** Le tronc commun suffit.

Après mise à jour, vérification en 30 secondes : *Ventes → Factures* — sur une facture en
**Brouillon** (badge gris), le menu de ligne propose **« Émettre »** (confirmation : le numéro
devient définitif — art. 145 CGI) ; « Marquer encaissée » n'apparaît qu'une fois la facture
émise. Le retour « Repasser en brouillon » n'est visible que connecté en super-admin.

### v3.13.0 — factures : sections, éléments à la carte, numérotation assainie

**Se livre aux deux installations, à l'identique. Aucune commande de reprise, aucune dépendance
composer nouvelle.**

**Deux migrations, purement additives** (`php artisan migrate --force` du tronc commun suffit) :
elles ajoutent des colonnes **nullables** aux factures et aux lignes de facture/devis
(`section_title`, remises, acompte, retenue de garantie, période, n° de BC, motif d'exonération,
notes de pied). **Aucune donnée existante n'est modifiée** ; les factures existantes rendent un
PDF identique.

Après mise à jour, vérification en 30 secondes : *Ventes → Factures → Nouvelle facture* — le champ
**Numéro** doit suggérer la vraie suite de l'année (chez IRADA : plus jamais de 906-2026 ; la bande
technique 900+ des factures « bis » de la reprise est ignorée et reste intouchée en base).

### v3.12.0 — blog (tableaux, mise en page, RTL) · vitesse de l'admin · vitrine

**Se livre aux deux installations, à l'identique. AUCUNE migration, aucune commande de reprise,
aucune dépendance composer nouvelle.** Le tronc commun suffit.

Deux gestes **facultatifs** après la mise à jour, à faire depuis l'admin :

1. **Contenu → Vitrine → Réseaux sociaux** : le champ « Instagram (2e compte) » est nouveau.
   Pour IRADA, l'ancien compte `irada_venture` est remplacé par deux comptes —
   `https://www.instagram.com/irada_incubateur/` (champ Instagram) et
   `https://www.instagram.com/irada_prod/` (2e compte). Laissé vide, rien ne change à l'affichage.
2. **Les tableaux des anciens articles de blog sont à re-saisir.** Ils n'ont jamais été stockés :
   l'assainisseur HTML les découpait à l'enregistrement (c'est le bug corrigé par ce lot). Les
   tableaux saisis à partir de maintenant sont conservés.

⚠️ **Un rechargement complet du navigateur (Cmd/Ctrl + Maj + R) est recommandé la première fois** :
le panneau d'administration passe en navigation client (mode SPA) et la façon dont ses scripts sont
chargés change. Les téléchargements (PDF de paie, rapports de marché, relevés, justificatifs,
sauvegardes) restent hors de ce mécanisme — c'est déclaré et testé, mais c'est le premier point à
vérifier si quelque chose se comporte bizarrement après cette version.

### v3.11.0 — le fonctionnement interne devient une dimension

**Se livre aux deux installations, à l'identique. Aucune commande de reprise.**

⚠️ **C'est la première migration de ce socle qui ÉCRIT DANS LES DONNÉES.** Elle ne supprime rien —
elle change à quoi les lignes sont rattachées (projet « Fonctionnement interne » → sans marché) —
mais il n'y a **pas de `down()`**, et il n'y en aura pas : la seule marche arrière autorisée est la
restauration de la sauvegarde.

1. **SAUVEGARDE DE LA BASE, D'ABORD. Vérifiée présente et non vide.**

   ```bash
   php artisan backup:run --only-db
   ls -lh storage/app/*/     # l'archive doit exister ET peser quelque chose
   ```

2. **Relever le repère AVANT la migration** : *Marchés & appels d'offres → Répartition des coûts*,
   mois en cours, noter le **« Coût total de l'entreprise »**. C'est le nombre qui prouvera que
   rien n'a bougé.

3. **La migration** :

   ```bash
   php artisan migrate --force
   ```

   Rien d'autre : aucun seeder à rejouer, aucune commande `reprise:`.

4. **Le contrôle, immédiatement après** : rouvrir le même écran, même mois. Le
   « Coût total de l'entreprise » doit être **identique au centime**, et l'addition
   `marchés + fonctionnement interne = total` doit tomber juste (l'écran affiche un bandeau rouge
   sinon — il ne doit pas apparaître).

   **Si un chiffre s'écarte : restaurer la sauvegarde et le signaler.** Ne rien rattraper à la
   main : un montant faux qui ressemble à un montant juste est le pire des deux mondes.

5. **Les trois vérifications d'usage** : « Fonctionnement interne » ne figure plus dans aucune
   liste de marchés ; *Ma semaine* ouvre sur la ligne « Fonctionnement interne » et y accepte une
   saisie ; *RH → Feuilles de temps*, filtre « Fonctionnement interne (sans marché) », retrouve
   bien les heures d'avant.

### v3.10.0 — jours fériés + menu simplifié

**Se livre aux deux installations, à l'identique.** Aucune commande de reprise.

1. **Une seule migration** (table `holidays`) :

   ```bash
   php artisan migrate --force
   ```

2. **Puis, obligatoirement**, les nouvelles permissions `holidays.*` — sans ce passage, l'écran
   « Jours fériés » reste invisible à tout le monde sauf au super-admin :

   ```bash
   php artisan db:seed --class="Database\Seeders\RolesAndPermissionsSeeder" --force
   ```

   Ce seeder ajoute et resynchronise rôles et permissions ; il ne touche aucune donnée métier.

3. La **Partie B** (fusion des groupes de navigation, réordonnancement Trésorerie / RH) est du
   **code seul** : rien à lancer.

   ⚠️ Si l'installation avait masqué « Marchés », « Appels d'offres » ou « Rapports » dans
   *Réglages → Groupes de navigation*, ces trois libellés n'existent plus : les écrans
   **réapparaissent** dans le menu, sous « Marchés & appels d'offres ». Rien n'est perdu — re-décocher
   le nouveau groupe si l'installation ne veut pas le voir.

4. **Côté exploitant, une fois connecté** : *RH → Jours fériés* → « Proposer les jours fériés
   marocains » (année en cours), puis **valider** les dates retenues. Tant qu'un férié reste
   « Proposé », il n'a **aucun** effet sur les feuilles de temps : c'est le comportement voulu.
