plus aucun SMS ne part * 2) des echecs d'envoi au dernier passage -> Twilio/numeros en erreur * et t'alerter par SMS (+ email) UNE seule fois par probleme et par jour. * * Le moteur ecrit une ligne dans `moteur_log` a CHAQUE passage (meme en * simulation, meme hors fenetre). Donc si la derniere ligne date de plus de * SEUIL_HEURES, c'est que le declencheur ne tourne plus : on alerte. * * DECLENCHEMENT (independant du cron du moteur, pour survivre a sa panne) : * - Cron OVH : 0 12,18 * * * /usr/bin/php /home/eyeseefrna/pro/sms/campagnes/veille_moteur.php * - ou par URL : https://pro.mahu-opticien.fr/sms/campagnes/veille_moteur.php?cron=MAHU-veille-7f3a * - ideal : un pinger externe gratuit (cron-job.org) sur l'URL ci-dessus -> survit meme si le cron OVH tombe. * * Ne modifie AUCUN autre fichier. Lecture seule sur la base (sauf son petit * fichier d'etat anti-spam data/veille_alertes.json). * ========================================================================== */ define('MAHU_LIB_ONLY', true); // charge moteur.php en bibliotheque (fonctions + config, aucune page) require __DIR__ . '/moteur.php'; // -> mahu_db(), mahu_now(), mahu_send_sms(), constantes Twilio // ── Reglages de l'alerte (modifiables) ────────────────────────────────────── const VEILLE_TOKEN = 'MAHU-veille-7f3a'; // jeton pour le declenchement par URL // CORRECTIF 04/09/2026 : le moteur tourne toutes les heures (cron 10 * * * *). // Un seuil de 26 h laissait passer plus d'une journee de panne avant d'alerter. // A 3 h, une panne du matin est vue au controle de midi. const VEILLE_SEUIL_H = 3; // pas de passage depuis X heures => alerte const VEILLE_SEUIL_ECHECS = 3; // alerte echecs seulement a partir de X echecs dans un passage (evite le bruit d'un numero mort isole) $ALERTE_MOBILE = '+33686902956'; // numero qui recoit l'alerte SMS ('' pour desactiver le SMS) $ALERTE_EMAIL = 'contact@mahu-opticien.fr'; // email d'alerte ('' pour desactiver l'email) // ── Securite : en CLI c'est libre ; par URL, jeton obligatoire ────────────── if (PHP_SAPI !== 'cli') { if (!isset($_GET['cron']) || !hash_equals(VEILLE_TOKEN, (string)$_GET['cron'])) { http_response_code(403); header('Content-Type: text/plain; charset=utf-8'); exit("Acces refuse. Jeton requis.\n"); } header('Content-Type: text/plain; charset=utf-8'); } $pdo = mahu_db(); $maintenant = mahu_now(); $nowTs = $maintenant->getTimestamp(); // ── Lecture du dernier passage du moteur ──────────────────────────────────── $dernier = $pdo->query("SELECT lance_le, mode, envoyes, echecs FROM moteur_log ORDER BY id DESC LIMIT 1") ->fetch(PDO::FETCH_ASSOC); $problemes = []; // liste des messages de probleme $signatures = []; // cle courte par probleme (anti-spam) if (!$dernier) { $problemes[] = "Aucun passage du moteur n'est enregistre."; $signatures[] = 'aucun'; } else { $lastTs = strtotime((string)$dernier['lance_le']) ?: 0; $ecart = $lastTs ? (int) floor(($nowTs - $lastTs) / 3600) : 9999; if ($ecart >= VEILLE_SEUIL_H) { $problemes[] = "Le moteur n'a pas tourne depuis {$ecart} h (dernier passage : {$dernier['lance_le']}). Le declencheur (cron) est probablement arrete : plus aucun SMS ne part."; $signatures[] = 'stale-' . $maintenant->format('Y-m-d'); } if ((int)$dernier['echecs'] >= VEILLE_SEUIL_ECHECS) { $problemes[] = "{$dernier['echecs']} echec(s) d'envoi au dernier passage ({$dernier['lance_le']}). A verifier (Twilio, numeros, credit)."; $signatures[] = 'echecs-' . $maintenant->format('Y-m-d'); } } // ── Anti-spam : une alerte par signature (probleme + jour) ────────────────── $dir = __DIR__ . '/data'; if (!is_dir($dir)) @mkdir($dir, 0755, true); $stateFile = $dir . '/veille_alertes.json'; $deja = is_file($stateFile) ? (json_decode((string)@file_get_contents($stateFile), true) ?: []) : []; $aEnvoyer = []; foreach ($problemes as $i => $msg) { $sig = $signatures[$i]; if (!isset($deja[$sig])) $aEnvoyer[$sig] = $msg; // pas encore alerte pour ce probleme aujourd'hui } // ── Envoi de l'alerte (SMS compatible GSM-7 + email) ──────────────────────── $envoyees = 0; if ($aEnvoyer) { $corpsListe = implode("\n", array_values($aEnvoyer)); // SMS : sans accents (regle GSM-7), court, 1 lien de verification $sms = "MAHU Alerte technique\n" . $corpsListe . "\nVerifier : pro.mahu-opticien.fr/sms/campagnes/moteur.php"; $sms = veille_gsm($sms); // CORRECTIF 04/09/2026 : on retient si l'alerte est REELLEMENT partie. // Avant, l'echec d'envoi n'etait pas regarde et la signature etait marquee // quand meme : une alerte ratee faisait taire le probleme pour la journee. // Or la panne la plus probable (Twilio indisponible) est justement celle qui // fait echouer l'alerte. Le chien de garde s'endormait au pire moment. $canalOk = false; if ($ALERTE_MOBILE !== '' && function_exists('mahu_send_sms')) { try { $r = mahu_send_sms($ALERTE_MOBILE, $sms); if (is_array($r) ? !empty($r['ok']) : (bool) $r) { $canalOk = true; } } catch (Throwable $e) { $r = ['ok' => false]; } } if ($ALERTE_EMAIL !== '') { $sujet = "ALERTE moteur SMS MAHU — " . $maintenant->format('d/m/Y H:i'); $body = "Bonjour,\n\nLe chien de garde a detecte un probleme sur le moteur d'envois SMS :\n\n" . $corpsListe . "\n\nTableau de bord : https://pro.mahu-opticien.fr/sms/campagnes/moteur.php\n\n" . "-- Veille automatique MAHU\n"; $headers = "From: veille@mahu-opticien.fr\r\nContent-Type: text/plain; charset=UTF-8\r\n"; if (@mail($ALERTE_EMAIL, $sujet, $body, $headers)) { $canalOk = true; } } // Anti-spam : on ne marque le probleme comme signale que si au moins un // canal a abouti. Sinon on reessaie au passage suivant. if ($canalOk) { foreach ($aEnvoyer as $sig => $_) { $deja[$sig] = $maintenant->format('Y-m-d H:i:s'); $envoyees++; } } else { echo " ATTENTION : aucune alerte n'a pu partir (SMS et email en echec).\n"; error_log('[mahu] veille : alerte impossible a delivrer'); } } // ── Menage du fichier d'etat : on ne garde que les 7 derniers jours ───────── foreach ($deja as $sig => $t) { if (strtotime((string)$t) < $nowTs - 7 * 86400) unset($deja[$sig]); } @file_put_contents($stateFile, json_encode($deja, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); // ── Sortie lisible (cron log / navigateur) ────────────────────────────────── $etat = $problemes ? 'PROBLEME' : 'OK'; echo $maintenant->format('Y-m-d H:i:s') . " Europe/Paris | veille moteur : {$etat}\n"; if ($dernier) echo " dernier passage : {$dernier['lance_le']} (mode {$dernier['mode']}, envoyes {$dernier['envoyes']}, echecs {$dernier['echecs']})\n"; foreach ($problemes as $p) echo " - {$p}\n"; echo $problemes ? (" alertes envoyees ce passage : {$envoyees}" . ($envoyees ? '' : ' (deja alerte aujourd hui)') . "\n") : " rien a signaler\n"; /* * Rend un texte compatible GSM-7 SANS appauvrir la langue. * * CORRECTIF 04/09/2026 — l'ancienne version retirait TOUS les accents, y * compris ceux que la table GSM 03.38 accepte (a-grave, e-aigu, e-grave, * u-grave, i-grave, o-grave, c-cedille majuscule, trema...). Elle appauvrissait * le message sans rien resoudre, et laissait passer les vrais coupables : * tiret cadratin, points de suspension, guillemets francais, apostrophe * courbe, symbole euro — eux font basculer le SMS en UCS-2 (70 caracteres par * segment au lieu de 160). * * Regle Sandy : les accents sont autorises. On ne remplace que ce qui est * hors table. */ function veille_gsm(string $s): string { $map = [ // hors GSM : accents circonflexes et tremas non couverts 'â'=>'a', 'ê'=>'e', 'î'=>'i', 'ô'=>'o', 'û'=>'u', 'ë'=>'e', 'ï'=>'i', 'ç'=>'c', 'œ'=>'oe', 'Œ'=>'OE', // hors GSM : ponctuation typographique '—'=>'-', '–'=>'-', '…'=>'...', '’'=>"'", '‘'=>"'", '«'=>'"', '»'=>'"', '“'=>'"', '”'=>'"', ' '=>' ', ]; return strtr($s, $map); }