# Changelog

Tous les changements notables de **slashbooking** sont consignés ici.

Le format suit [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) et le projet utilise [Semantic Versioning](https://semver.org/).

---

## [1.2.0] — 2026-06-01

### Added

- **Paliers Free / Payant.** Un palier gratuit et un palier payant (licence valide). Le palier payant débloque : synchronisation Google Calendar, personnalisation des modèles d'e-mail, rappels automatiques J-1. Source de vérité unique `Config::isPaid()` (= `sb_license_status === 'valid'`).

### Changed

- La synchronisation Google se met en pause si la licence n'est plus valide (downgrade) — données conservées, reprise automatique à la re-validation.
- L'édition/restauration/test des modèles d'e-mail exige une licence valide (les modèles par défaut restent utilisés en Free). Les routes mail-templates appliquent le verrou côté serveur ; la SPA verrouille la section avec un encart « version payante ».

---

## [1.1.1] — 2026-06-01

### Fixed

- **Migrations de schéma appliquées aux mises à jour, pas seulement aux activations.** Le `Migrator` tourne désormais au boot (auto-gated par l'option `sb_db_version`) : une mise à jour 1-clic depuis une version antérieure ajoute bien les nouvelles colonnes (ex. `reconnect_required`). Sans ça, la connexion Google échouait après une mise à jour faite sans réactivation manuelle du plugin.
- **Le callback OAuth admin remonte la vraie erreur du broker** (`sb_error`, ex. `google_error`) au lieu de tout masquer en « invalid_state ».

---

## [1.1.0] — 2026-05-31

### Added

- **Connexion Google Calendar en 1 clic via le broker SlashBooking.** Le plugin ne contient plus de `client_id`/`client_secret` Google. Nouveau `BrokerClient` (start/claim/refresh/validate) parlant en serveur-à-serveur au broker `https://broker.slashbox.fr` (base configurable via le filtre `sb_broker_url`). La connexion est conditionnée à une clé de licence (`sb_license_key`) validée par le broker. Nonce anti-CSRF `n` réutilisant `OAuthState`. Claim one-time : aucun token ne transite par une URL navigateur.

### Changed

- `AdminGoogleSettingsController` gère désormais la licence (`{has_license, license_status, plan, expires}`) au lieu du Client ID/Secret Google.
- `GoogleClientBuilder` rafraîchit l'access token via le broker ; ne pose plus de `client_secret` sur le client Google (les appels Calendar restent directs en Bearer). Gestion `BrokerUnavailable` (retry, tokens conservés) et `TokenRevoked` (compte marqué « reconnexion requise », données conservées).
- UI admin : carte « Licence SlashBooking » + bouton « Connecter Google Calendar » actif uniquement avec une licence valide. Assistant Google Cloud supprimé.

### Removed

- `src/Google/OAuthClient.php` (échange/refresh/authUrl désormais côté broker).
- `GoogleSetupWizard.jsx` (plus de projet Google Cloud à configurer).
- Options `sb_google_client_id` / `sb_google_client_secret` (supprimées à la migration).

### Migration

- Les `refresh_token` existants (émis par le projet GCP du client) ne sont pas rafraîchissables par le broker → les connexions Google existantes cassent à la mise à jour. `BrokerMigration` supprime les anciennes options, marque le compte « reconnexion requise » (données conservées) et affiche une notice admin « Reconnectez Google Calendar (1 clic) ».

---

## [1.0.25] — 2026-05-31

### Security

- **Rate limit du formulaire de réservation** : échoue désormais en mode fermé (`fail-closed`) quand aucune IP client n'est disponible, et ajoute un plafond global par minute en plus du quota par IP ; les clés IPv6 sont réduites au préfixe `/64` pour qu'un attaquant disposant d'un `/64` ne puisse pas générer de nouveaux compteurs.
- **Notice d'administration** quand le formulaire public n'a pas de clé secrète Cloudflare Turnstile configurée (protection anti-robot désactivée).
- **Webhook push Google durci** : rejet des canaux expirés/inactifs (ack 200 sans traitement), comparaison à temps constant du `channel-id` et du `X-Goog-Resource-Id`, et regroupement des rafales de notifications en un seul `pull` par fenêtre de 30 s.
- **Gestion réservée aux administrateurs par défaut** (filtre `slashbooking_manage_roles` pour déléguer à d'autres rôles) ; le rôle éditeur, autorisé par les révisions précédentes, est révoqué à la mise à jour.
- **Liens de décision/annulation** : affichent une page de confirmation intermédiaire en `GET` et n'effectuent la mutation (confirmer/refuser/annuler) qu'en `POST`. Les préchargeurs de lien et scanners d'e-mail ne peuvent plus déclencher un changement d'état.
- **Plus de fuite de message d'exception** sur la page de décision : message fixe pour l'utilisateur + log de la cause réelle côté serveur.
- **Séparation de domaine HMAC** : les jetons de décision et l'état OAuth dérivent désormais des sous-clés distinctes par contexte au lieu d'utiliser directement le secret racine partagé.
- **Notice « clé de chiffrement en base »** escaladée de `warning` en `error`, précisant que les tokens Google ne sont PAS protégés contre une fuite de la base tant que la constante `SLASHBOOKING_ENC_KEY` n'est pas définie.

### Migration

- Le secret racine `sb_decision_secret` est conservé, mais la dérivation de clé HMAC change : les liens de décision/annulation déjà envoyés par e-mail (validité 72 h) ne seront plus valides après la mise à jour. Ils se régénèrent automatiquement pour les nouveaux e-mails et la fenêtre se résorbe d'elle-même sous 72 h. Aucune reconnexion Google requise (réservé au Plan B / 1.1.0).

---

## [1.0.24] — 2026-05-26

### Fixed

- **Calendrier public — jours non travaillés peints "Complet" rouge au lieu de "Fermé" gris.** `loadMonth()` marquait tout jour avec 0 slots en `state: 'full'` sans distinguer un jour configuré fermé (pas de plage dans `weekly_hours[isoDay]`) d'un jour avec slots tous pris. Conséquence visuelle : samedi/dimanche sur un service Lun-Ven (config par défaut) ressortaient en `--full` rouge "Complet". Fix front-only : nouveau helper `dayIsClosed(iso, weeklyHours)` qui inspecte la map `weekly_hours` retournée par `/services` (déjà exposée par `PublicBookingController::listServices()` depuis v1.0.0) et bascule l'état sur `'closed'` quand le ISO weekday n'a pas de fenêtre. Accepte les deux shapes JSON (clés numériques vs strings — PHP int → JSON string). `currentService()` extrait l'objet service complet depuis `state.services` via `state.service` (slug). Zéro changement backend.

---

## [1.0.23] — 2026-05-26

### Fixed

- **Calendrier public — débordement des chiffres sur mobile (récurrence post-1.0.8).** Le fix de 1.0.8 avait réduit padding/gaps/font sous 480 px mais ne traitait pas la cause racine : (1) les `<button>` héritent du padding user-agent (~2 px) qui pousse le digit hors de la case `aspect-ratio: 1`, (2) les grid items ont `min-width: auto` par défaut, donc le `min-content` du bouton (digit bold + padding UA) gonfle la colonne et fait déborder le grid hors du `.sb-step` sur viewports étroits, (3) le `line-height: 1.5` hérité du widget créait du débordement vertical dans une case carrée de ~40 px. Fix : `.sb-cal-day` reçoit `padding: 0`, `min-width: 0`, `line-height: 1`, `overflow: hidden`. Nouvelle media query `@media (max-width: 380px)` qui downshift step padding (14→10 px lat.), font cellule (13→12 px), font day-of-week (10→9 px) et gap grid (2→1 px) pour iPhone SE et thèmes avec sidebar mobile.

---

## [1.0.22] — 2026-05-26

### Fixed

- **Widget de booking inutilisable pour les visiteurs non connectés sur les sites qui restreignent la REST API aux utilisateurs authentifiés.** Symptôme remonté en validation prod : étape "Projet" invisible, calendrier figé en chargement (cellules grisées), aucune date cliquable, console DevTools = `401/403` sur `GET /wp-json/slashbooking/v1/services`. Cause racine : un filtre `rest_authentication_errors` actif (plugin Disable REST API, Wordfence/iThemes/SecuPress "REST API restriction", ou snippet custom dans `functions.php`) court-circuite la pile d'authentification REST et retourne un `WP_Error` (`rest_not_logged_in`) AVANT que le `permission_callback => '__return_true'` de nos endpoints publics ne soit consulté. Fix : `Http\RestRouter` enregistre un filtre `rest_authentication_errors` au priority 99 qui clear l'erreur uniquement pour nos 6 routes publiques (`services`, `availability`, `bookings`, `cancel`, `decide`, `google/webhook`) ET uniquement quand le code d'erreur est dans une whitelist de codes "non-authentifié" connus (`rest_not_logged_in`, `rest_forbidden`, `rest_cannot_access`, `rest_login_required`, `rest_user_invalid`) — les erreurs de nonce/cookie restent strictes. Détection de route via `$_SERVER['REQUEST_URI']` avec boundary check (`/`, `?`, `&`, fin de chaîne) pour éviter qu'un nom de route soit confondu avec un préfixe d'une autre route. Compatible URL pretty (`/wp-json/...`) et fallback (`/?rest_route=...`).

---

## [1.0.21] — 2026-05-22

### Added

- **Widget Dashboard `slashbooking_dashboard`** dans le tableau de bord WP (`Admin\DashboardWidget`). Deux sections : **À valider** (statut `pending`, limite 5) et **À venir 7 jours** (statut `confirmed`, limite 5). Pour chaque réservation : date+heure formatées via `wp_date()` dans le fuseau du site, nom du client, nom du service. Badge compteur coloré (orange pour pending, bleu pour upcoming). Lien footer vers `admin.php?page=slashbooking#/bookings`. Gate sur la capability `slashbooking_view` (admin + éditeur). Styles inline scopés sous `.sb-dash` pour zéro fuite vers les autres widgets. Hook : `wp_dashboard_setup`. Fallback `wp_date() === false` → format ISO `Y-m-d H:i` pour garantir un rendu propre même si la locale échoue.

---

## [1.0.20] — 2026-05-22

### Added

- **Rôle WP "Éditeur" autorisé à utiliser le plugin.** `Capabilities::install()` accorde maintenant `slashbooking_view` + `slashbooking_manage` aux rôles `administrator` ET `editor`. Couvre le cas d'usage typique TPE : l'office manager / commercial qui gère les RDV au quotidien sans avoir besoin d'accès admin technique (Réglages WP, autres plugins, etc).
- **Migration automatique des caps sur upgrade.** Nouvelle méthode `Capabilities::syncOnUpgrade()` appelée depuis `Plugin::register()` qui compare l'option `slashbooking_caps_revision` (entier) à `Capabilities::REVISION` (constante). Si la revision stockée est inférieure, ré-appelle `install()` puis bump l'option. Évite de demander à l'utilisateur de désactiver/réactiver le plugin pour bénéficier d'un nouveau cap layout. Idempotent et cheap (single `get_option` à chaque page load).

---

## [1.0.19] — 2026-05-22

### Fixed

- **PUC ne déclenchait pas la notif de mise à jour** sur certains environnements (observé sur un site qui rapporte `get_bloginfo('version') === '7.0'`). Cause racine identifiée via debug script terrain : `Slash\Booking\Vendor\YahnisElsts\PluginUpdateChecker\v5p6\InstalledPackage::getFileHeader()` n'appelle `_cleanup_header_comment()` que si `function_exists()` retourne `true` — quand ce n'est pas le cas, la valeur du header `Version:` est stockée brute, avec les **11 espaces de padding** de mon alignement visuel. Stocké en DB sous `external_updates-slashbooking.update.version = "           1.0.18"`. `version_compare()` côté injection se trompe à cause du whitespace → le transient `update_plugins` n'est jamais alimenté → WP affiche "à jour".
- **Fix défensif** : 2 filtres `puc_request_{info,update}_result-slashbooking` qui font `trim($result->version)` après la requête PUC, indépendamment de la dispo de `_cleanup_header_comment`. Le padding du `Version:` dans `slashbooking.php` est aussi compacté à 1 espace pour réduire la surface d'erreur.

### Changed

- **Suppression du guard `is_admin()`** autour de `UpdateChecker::bootstrap()` dans `Plugin::register()`. PUC doit installer ses filtres dans tous les contextes qui peuvent rafraîchir le transient `update_plugins` — y compris WP-Cron (`DOING_CRON=true`, `is_admin()=false`). Le guard empêchait les checks programmés de tourner.
- **`readme.txt` Changelog** repassé au format wp.org standard `= X.Y.Z =` (la date est désormais dans le contenu de chaque entrée).

---

## [1.0.18] — 2026-05-22

### Added

- **`readme.txt` au format wp.org** à la racine du repo + dans le ZIP. PUC le lit lors d'un clic sur **"Afficher les détails"** dans la liste des extensions et affiche une fiche complète (Description, Installation, FAQ, Changelog, Upgrade Notice) comme pour un plugin du dépôt officiel. Format `=== H1 ===`, `== H2 ==`, listes `*`, headers `Contributors / Tags / Requires at least / Tested up to / Stable tag / Requires PHP`.

---

## [1.0.17] — 2026-05-22

### Fixed

- **Fatal PUC encore présent en v1.0.16** malgré le fix des clés de registre. Vraie cause racine : le bootstrap faisait `if (!class_exists(PucFactory::class)) require plugin-update-checker.php`, mais comme le build composer utilise `--classmap-authoritative`, `PucFactory.php` est autoloadable directement via le classmap → `class_exists` retourne `true` → le `require` est skip → `load-v5p6.php` n'est jamais exécuté → le registre `$classVersions` reste vide → lookup miss → trigger_error. Fix : toujours faire le `require_once` de l'entrypoint (idempotent), peu importe l'état de `class_exists`. Combiné au patcher des clés de registre (v1.0.16), PUC fonctionne maintenant correctement.

---

## [1.0.16] — 2026-05-22

### Fixed

- **Fatal "PUC does not support updates for plugins hosted on GitHub"** au chargement du plugin en v1.0.15. Cause : PHP-Scoper avait préfixé les clés du registre dans `load-v5p6.php` (`'Vcs\PluginUpdateChecker'` → `'Slash\Booking\Vendor\Vcs\PluginUpdateChecker'`), mais le dispatch interne de `PucFactory::buildUpdateChecker()` reconstruit la clé à runtime par concaténation de strings (`'Vcs\\' . $type . 'UpdateChecker'`) que scoper ne peut pas voir → lookup miss → `trigger_error(..., E_USER_ERROR)`. Fix : patcher scoper.inc.php qui revert les 4 clés de registre à leur forme non-préfixée. Les classes pointées par ces clés restent scopées.

**Action requise côté site cassé en v1.0.15** : remplacer le dossier `wp-content/plugins/slashbooking/` par le contenu du ZIP v1.0.16 via FTP/SSH (l'admin WP est inaccessible tant que le fatal n'est pas levé). À partir de v1.0.16, les mises à jour passent par wp-admin normalement.

---

## [1.0.15] — 2026-05-22

### Added

- **Mises à jour en 1 clic depuis wp-admin (Plugin Update Checker + GitHub Releases).** Le plugin sonde régulièrement `https://github.com/ArchSeraphin/slashbooking/releases/latest` ; WordPress affiche la notif "Mise à jour disponible" dans la liste des plugins et propose le bouton **Mettre à jour** comme pour un plugin wp.org. Aucune saisie de token côté client (repo public). PUC scopé via PHP-Scoper sous `Slash\Booking\Vendor\YahnisElsts\PluginUpdateChecker\…` pour éviter les collisions avec d'autres plugins embarquant la lib.
- **Release automatisée via GitHub Actions** (`.github/workflows/release.yml`). Push d'un tag `vX.Y.Z` → workflow build le ZIP (composer + npm + scoper + zip) → publie une release GitHub avec le ZIP en asset et la section du CHANGELOG en body. Garde-fou : le tag doit matcher `Plugin::VERSION`, sinon le workflow plante.

---

## [1.0.14] — 2026-05-22

### Changed

- **Buffer symétrique autour des événements calendrier (Google).** Les événements importés depuis le calendrier connecté reçoivent maintenant un cushion `bufferAfter` (30 min par défaut) **après** leur fin, en plus du cushion `bufferAfter` que le candidat applique déjà **avant** leur début. Résultat : un événement GCal à 14h-15h bloque les créneaux entre 13h30 et 15h30 (au lieu de 13h30 → 15h). S'applique aussi à la validation de création (`slotIsFree`) pour empêcher le bypass via POST direct.
- **Le dernier créneau peut démarrer à la fin de la plage horaire.** `SlotGenerator` itère désormais tant que `startLocal <= windowClose` (au lieu de `endLocal <= windowClose`). Si la plage se termine à 18h00 et que la durée du service est 45 min, le dernier créneau bookable démarre à 18h00 (et finit à 18h45) au lieu de 17h15.

---

## [1.0.9] — 2026-05-20

### Added

- **Éditeur de services dans le panel admin.** Nouveau tab **Services** entre Réservations et Google avec :
  - Liste de tous les services (slug, nom, durée, buffer, jours ouverts résumés, statut actif/désactivé)
  - Éditeur par service : nom, couleur, actif, durée, buffer avant/après, délai mini, horizon
  - **Édition des jours et plages horaires** : 1 toggle par jour de semaine + plages horaires multiples (matin/après-midi via inputs `time` HTML5), bouton "Ajouter une plage" et bouton supprimer par plage
- Backend : `AdminServiceController` REST (`GET /admin/services`, `GET /admin/services/{slug}`, `POST /admin/services/{slug}`) avec validation stricte des horaires (regex HH:MM, open < close, fallback sur valeur actuelle), `ServiceRepository::findAll()` et `::update()`.

---

## [1.0.8] — 2026-05-20

### Fixed

- **Slots affichaient l'ISO 8601 brut** (`2026-06-18T09:00:00+00:00`) au lieu de l'heure formatée. WordPress `get_locale()` retourne `fr_FR` (underscore) mais `Intl.DateTimeFormat` / `toLocaleTimeString` exigent BCP-47 `fr-FR` (hyphen). Sans conversion, `RangeError` était silencieusement attrapé et le fallback affichait l'ISO. Fix : `locale.replace('_', '-')`.
- **Calendrier débordait sur mobile.** Padding step réduit (20 → 14 px), gaps de grille réduits (4 → 2 px), cell font-size réduit (14 → 13 px), boutons nav réduits (36 → 32 px), légende compactée. Widget en `max-width: 100%` + marges réduites sous 520 px. Slots list passe à `minmax(88 px)` sous 480 px.

---

## [1.0.7] — 2026-05-20

### Added

- **Calendrier visuel mois-par-mois** dans le formulaire public (remplace l'input date HTML5). Navigation prev/next, légende couleurs (Disponible vert plein / Partiel vert pâle / Complet rouge / Fermé gris), respect lead time 24 h + horizon 60 jours.
- **Choix du projet inline** : `[slashbooking]` (sans paramètre) affiche les services actifs en pills (`Photovoltaïque (1h30)` / `Bornes de charge (45 min)`) et le visiteur choisit avant la date. `[slashbooking service="pv"]` continue à forcer un service (rétrocompat). `[slashbooking service="pv,irve"]` propose une liste filtrée. La durée Google Calendar suit automatiquement le service sélectionné via `Service::duration_min`.
- Step indicator dynamique (3 ou 4 étapes selon la présence du picker projet).

### Changed

- **Admin SPA background aligné WP** : `.sb-admin` passe en `background: transparent` pour hériter du gris natif de l'admin WordPress (`#f0f0f1`) au lieu de l'override `slate-50` plus blanc. Les cards conservent leur fond blanc pour le contraste.

---

## [1.0.6] — 2026-05-20

### Changed

- **Refonte design pro du panel admin et du formulaire public.** Système de tokens unifié (palette blue/emerald/orange, neutres slate, radius+shadow scale, spacing 4/8 px), system-ui stack pour zéro dépendance externe RGPD-safe.
- **Admin SPA** : header avec logo + titre + version, onglets en pills propres, KPI dashboard 4 cards en tête de la liste réservations (Total / À valider / Confirmés / À venir), table custom avec hover + status badges colorés (dot + pill), empty state illustré, boutons WP-components stylés.
- **Formulaire public** : header trust signals (sécurité données + réponse 24h), step indicator vivant 1/2/3, cards séparées par étape avec hint contextuel, date picker stylé, slots cards avec hover+focus+selected, champs avec labels au-dessus + placeholders + autocomplete sémantique (mobile keyboard), consent box dédiée, CTA orange XXL avec spinner inline, écran de succès illustré, scroll automatique du formulaire à la sélection.
- **Accessibility** : focus rings 2-3 px sur tous les controls, `prefers-reduced-motion` respecté, `role="progressbar"` + `aria-live` + `htmlFor` partout, autocomplete + types sémantiques (`tel`, `email`).
- Design system persisté dans `design-system/slashbooking/MASTER.md` pour future référence.

---

## [1.0.5] — 2026-05-20

### Fixed

- **Shortcode `[slashbooking]` ne s'affichait pas.** Deux bugs cumulés :
  1. `scoper.inc.php` finder filtre `*.php` uniquement → `src/PublicFront/assets/booking.{js,css}` étaient absents du ZIP. Fix : `bin/build-release.sh` copie maintenant ce répertoire depuis l'arbre non-scopé.
  2. `Shortcode::maybeEnqueue()` lisait un flag global set dans `render()`, mais `wp_enqueue_scripts` fire AVANT que les shortcodes ne soient rendus → enqueue never triggered. Fix : `Shortcode::render()` appelle `wp_enqueue_script` / `wp_enqueue_style` directement (WP les queue et imprime en footer). Bug latent depuis Plan 1.

---

## [1.0.4] — 2026-05-20

### Fixed

- **404 sur toutes les requêtes REST du SPA.** `setupApi()` ajoutait un `createRootURLMiddleware` pointant vers `wp-json/slashbooking/v1/`. Notre middleware était exécuté en premier (LIFO), construisait l'URL correcte, mais laissait `options.path` intact. Le rootURL middleware natif de WP s'exécutait ensuite, voyait `path` toujours string, et **réécrivait l'URL** en `wp-json/<path>` — sans le namespace. Fix : on n'override plus le rootURL ; on injecte simplement `slashbooking/v1/` dans le `path` avant que WP construise l'URL finale. Bug latent depuis Plan 2.

---

## [1.0.3] — 2026-05-20

### Fixed

- **Page admin SPA blanche.** `Assets::enqueue()` cherchait `assets/dist/admin.asset.php` / `admin.js` / `admin.css`, mais `wp-scripts` produit `index.jsx.asset.php` / `index.jsx.js` / `index.jsx.css` (filename dérivé du fichier d'entry `src/Admin/react-app/src/index.jsx`). Le check `is_file()` échouait silencieusement → aucun JS/CSS enqueué → div mount point vide. Bug latent depuis Plan 2.

---

## [1.0.2] — 2026-05-20

### Fixed

- **Activation fatal — 2e occurrence.** `Plugin::register()` appelait `rest_url()` au plugin file load (depuis `wp-settings.php`, avant que `$wp_rewrite` ne soit initialisé) → `Error: Call to a member function using_index_permalinks() on null`. Fix : `UrlBuilder` reçoit maintenant une `Closure` qui résout l'URL paresseusement à la première utilisation (= quand un `BookingNotifier` callback fire). Bug latent depuis Plan 2.

---

## [1.0.1] — 2026-05-20

### Fixed

- **Activation fatal sur fresh install.** `Plugin::register()` instanciait `DecisionTokenSigner` avec un secret vide avant que `register_activation_hook` n'ait pu seeder l'option `sb_decision_secret` → `InvalidArgumentException: Decision secret must be at least 16 characters`. Fix : `Activator::ensureDecisionSecret()` est maintenant publique et appelée en tête de `Plugin::register()` (idempotent). Bug latent depuis Plan 2.

---

## [1.0.0] — 2026-05-20

Première release stable. Périmètre V1 fermé selon `docs/superpowers/specs/2026-05-19-slashbooking-design.md`.

### Added — Plan 5 : Polish V1

- Éditeur de templates e-mail dans le dashboard admin (CodeMirror 6 + preview live + insertion de tag + envoi d'un test + restauration du défaut) pour les 6 événements (`booking.pending.client/admin`, `booking.confirmed.client`, `booking.rejected.client`, `booking.cancelled.client`, `booking.reminder.client`).
- Internationalisation complète : `languages/slashbooking.pot` généré, traduction `fr_FR` fournie.
- Conformité RGPD :
  - Privacy Exporter (`wp_privacy_personal_data_exporters`) — exporte tous les bookings matchant un e-mail.
  - Privacy Eraser (`wp_privacy_personal_data_erasers`) — anonymise via SHA-256 + `@anon.invalid`, conserve les agrégats.
  - Masquage e-mails dans `sync_log` (`a***@d***`).
  - Option `sb_legal_page_id` pour lien "Mentions légales" sous case de consentement.
  - Cron mensuel `sb/purge_old_bookings` (rétention par défaut 3 ans après `ends_at_utc`).
- Isolation des dépendances vendor via PHP-Scoper (`Slash\Booking\Vendor\Google\…`, etc.) — élimine le risque de collision avec d'autres plugins WordPress.
- Script `bin/build-release.sh` produit un ZIP de release reproductible (composer no-dev + npm build + scoping + autoload classmap + checksum SHA-256).
- Documentation `README.md` complète (walkthrough Google Cloud Console, troubleshooting).

### Added — Plan 4 : Webhook + pull Google → WP

- Push notifications Google : `WatchChannelManager` (start/stop/renew) + endpoint REST `POST /google/webhook` (vérif HMAC `X-Goog-Channel-Token`).
- Pull incrémental via `events.list` + `syncToken` : `SyncEngine` + handler Action Scheduler `sb/google_pull`.
- Reflection : ignore les events GCal créés par notre propre push (Plan 3) via `BookingRepository::findByGoogleEventId`.
- Cron `sb/watch_renew_check` (quotidien) + `sb/google_pull_all` (15 min fallback).
- Diagnostics étendus : SPA `GooglePage` montre statut watch, dernier sync, sync token ; `wp slashbooking doctor` étendu.
- Upgrade PHPStan 1.x → 2.x avec `treatPhpDocTypesAsCertain: false`.

### Added — Plan 3 : Google OAuth + push WP → GCal

- OAuth 2.0 utilisateur (refresh token chiffré via `sodium_crypto_secretbox`).
- Push WP → GCal via Action Scheduler `sb/push_gcal_event` (create / update / delete selon statut).
- Code couleur GCal : orange `colorId=6` (pending), vert `colorId=10` (confirmed), delete (rejected/cancelled).
- Journal de synchronisation `wp_sb_sync_log` (cron quotidien `sb_purge_sync_log` 30j).
- CLI `wp slashbooking doctor` (état OAuth + probe create/delete event).
- SPA admin : `GooglePage` (Configuration OAuth + Google Calendar) + `SyncLogPage`.

### Added — Plan 2 : Notifications e-mail + validation admin

- 6 templates HTML personnalisables (`wp_sb_mail_templates`) + tags `{{...}}` + fallback texte auto via `wp_strip_all_tags`.
- Pièce jointe `.ics` RFC 5545 sur l'e-mail de confirmation.
- Reminder J-1 (cron quotidien `sb_send_daily_reminders` à 10h00 site TZ).
- Validation admin : boutons HMAC dans l'e-mail (72h, idempotent) + dashboard React minimal.
- Annulation client via lien HMAC dans e-mail de confirmation.
- Capabilities WP : `slashbooking_manage`, `slashbooking_view`.

### Added — Plan 1 : Fondations

- Architecture modulaire (Domain / Persistence / Availability / Booking / Http / PublicFront / Cli).
- Modèle de données : 6 tables `wp_sb_*` (`services`, `bookings`, `busy_blocks`, `google_accounts`, `sync_log`, `mail_templates`).
- 2 services seed à l'activation (`pv` 90min, `irve` 45min).
- REST API publique : `GET /services`, `GET /availability`, `POST /bookings`.
- Bloc Gutenberg + shortcode `[slashbooking service="pv|irve"]`.
- Anti-bot : honeypot + délai min + rate-limit transient.
- Buffer 30 min + délai 24h + horizon 60 jours.

---

[1.0.0]: https://github.com/trinity/booking/releases/tag/v1.0.0
