diff --git a/public/assets/app.js b/public/assets/app.js index 7525941..a1cd727 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -6394,6 +6394,33 @@ async function refreshClusterStatus() { const secretInput = document.getElementById('cluster-secret'); if (secretInput && st.cluster_secret) secretInput.value = st.cluster_secret; + const blfExpiryContainer = document.getElementById('cluster-blf-expiry-container'); + const blfExpiryInput = document.getElementById('cluster-blf-expiry'); + if (blfExpiryContainer && blfExpiryInput) { + if (st.enabled) { + blfExpiryContainer.classList.remove('hidden'); + blfExpiryInput.value = st.blf_expiry || 120; + } else { + blfExpiryContainer.classList.add('hidden'); + blfExpiryInput.value = st.blf_expiry || 1800; + } + } + + if (chkEnabled && !chkEnabled.dataset.blfBound) { + chkEnabled.dataset.blfBound = '1'; + chkEnabled.addEventListener('change', () => { + const c = document.getElementById('cluster-blf-expiry-container'); + const inp = document.getElementById('cluster-blf-expiry'); + if (chkEnabled.checked) { + if (c) c.classList.remove('hidden'); + if (inp && (!inp.value || inp.value === '1800')) inp.value = 120; + } else { + if (c) c.classList.add('hidden'); + if (inp) inp.value = 1800; + } + }); + } + // Badges & Cartes const headerBadge = document.getElementById('cluster-header-state-badge'); const navBadge = document.getElementById('nav-badge-cluster'); @@ -6584,6 +6611,7 @@ async function saveClusterSettings(e) { peer_port: parseInt(form.elements['peer_port'] ? form.elements['peer_port'].value : 8443, 10) || 8443, cluster_secret: form.elements['cluster_secret'].value.trim(), vrid: parseInt(form.elements['vrid'].value, 10) || 51, + blf_expiry: parseInt(form.elements['blf_expiry'] ? form.elements['blf_expiry'].value : 120, 10) || 120, auto_sync: form.elements['auto_sync'].checked ? 1 : 0 }; diff --git a/public/index.php b/public/index.php index 37f905c..7abd29f 100644 --- a/public/index.php +++ b/public/index.php @@ -2939,6 +2939,24 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev') + + +
diff --git a/scripts/cluster/notify_master.sh b/scripts/cluster/notify_master.sh index 0eed385..9f635d0 100755 --- a/scripts/cluster/notify_master.sh +++ b/scripts/cluster/notify_master.sh @@ -55,5 +55,14 @@ if [ -n "$VIP_CLEAN" ] && command -v arping >/dev/null 2>&1; then arping -c 5 -U -I "$IFACE" "$VIP_CLEAN" >> "$LOG_FILE" 2>&1 || true fi +# 5. Envoi automatique du signal check-sync silencieux aux téléphones enregistrés +# Force Yealink, Fanvil, Snom à réémettre instantanément tous leurs SUBSCRIBE BLF sur ce serveur actif +if pidof asterisk >/dev/null 2>&1; then + echo "[$TIMESTAMP] [CLUSTER] Envoi check-sync de réveil aux postes pour réabonnement immédiat des BLF..." >> "$LOG_FILE" + for ep in $(/usr/sbin/asterisk -rx "pjsip show endpoints" 2>/dev/null | grep -o 'Endpoint: [0-9]*' | awk '{print $2}'); do + /usr/sbin/asterisk -rx "pjsip send notify check-sync endpoint $ep" >> "$LOG_FILE" 2>&1 || true + done +fi + echo "[$TIMESTAMP] [CLUSTER] Prise de relais MASTER terminée avec succès." >> "$LOG_FILE" exit 0 diff --git a/src/ClusterManager.php b/src/ClusterManager.php index 62b00cd..f238575 100644 --- a/src/ClusterManager.php +++ b/src/ClusterManager.php @@ -42,6 +42,7 @@ class ClusterManager 'cluster_secret' => bin2hex(random_bytes(16)), 'vrid' => 51, 'priority' => 100, + 'blf_expiry' => 120, 'sync_interval' => 15, 'auto_sync' => 1, 'last_sync_at' => null, @@ -52,6 +53,9 @@ class ClusterManager if (empty($row['peer_port'])) { $row['peer_port'] = 8443; } + if (empty($row['blf_expiry'])) { + $row['blf_expiry'] = 120; + } return $row; } @@ -70,6 +74,7 @@ class ClusterManager $clusterSecret = trim((string)($data['cluster_secret'] ?? '')); $vrid = max(1, min(255, (int)($data['vrid'] ?? 51))); $priority = max(1, min(254, (int)($data['priority'] ?? ($role === 'master' ? 100 : 90)))); + $blfExpiry = max(30, min(3600, (int)($data['blf_expiry'] ?? 120))); $syncInterval = max(5, min(3600, (int)($data['sync_interval'] ?? 15))); $autoSync = !empty($data['auto_sync']) ? 1 : 0; @@ -130,6 +135,7 @@ class ClusterManager cluster_secret = :secret, vrid = :vrid, priority = :prio, + blf_expiry = :blf_expiry, sync_interval = :sync_int, auto_sync = :auto_sync, updated_at = CURRENT_TIMESTAMP @@ -137,23 +143,34 @@ class ClusterManager '); $stmt->execute([ - ':enabled' => $enabled, - ':role' => $role, - ':vip' => $virtualIp, - ':vmask' => $virtualNetmask, - ':iface' => $interface, - ':peer' => $peerIp, - ':peer_port' => $peerPort, - ':secret' => $clusterSecret, - ':vrid' => $vrid, - ':prio' => $priority, - ':sync_int' => $syncInterval, - ':auto_sync' => $autoSync, + ':enabled' => $enabled, + ':role' => $role, + ':vip' => $virtualIp, + ':vmask' => $virtualNetmask, + ':iface' => $interface, + ':peer' => $peerIp, + ':peer_port' => $peerPort, + ':secret' => $clusterSecret, + ':vrid' => $vrid, + ':prio' => $priority, + ':blf_expiry' => $blfExpiry, + ':sync_int' => $syncInterval, + ':auto_sync' => $autoSync, ]); // Application automatique de la configuration Keepalived $applyResult = $this->applyKeepalivedConfig(); + // Régénération de la configuration PJSIP et rechargement Asterisk avec le nouveau délai d'abonnement + try { + $gen = new ConfigGenerator($this->db); + $gen->writeAllConfigs(); + shell_exec('asterisk -rx "pjsip reload" 2>/dev/null || true'); + shell_exec('asterisk -rx "dialplan reload" 2>/dev/null || true'); + } catch (\Throwable $e) { + error_log("[CLUSTER] Erreur régénération configs Asterisk : " . $e->getMessage()); + } + return [ 'success' => true, 'message' => 'Configuration Haute Disponibilité enregistrée et appliquée.', @@ -723,6 +740,7 @@ class ClusterManager virtual_ip = :virtual_ip, virtual_netmask = :virtual_netmask, vrid = :vrid, + blf_expiry = :blf_expiry, last_sync_at = CURRENT_TIMESTAMP, last_sync_status = :status, last_sync_message = :msg @@ -737,6 +755,7 @@ class ClusterManager ':virtual_ip' => $localClusterSettings['virtual_ip'] ?? '', ':virtual_netmask' => $localClusterSettings['virtual_netmask'] ?? '24', ':vrid' => $localClusterSettings['vrid'] ?? 51, + ':blf_expiry' => (int)($localClusterSettings['blf_expiry'] ?? 120), ':status' => 'synced', ':msg' => 'Base et sons synchronisés depuis le Master', ]); diff --git a/src/ConfigGenerator.php b/src/ConfigGenerator.php index 291a8df..f923434 100644 --- a/src/ConfigGenerator.php +++ b/src/ConfigGenerator.php @@ -15,8 +15,9 @@ class ConfigGenerator public const MOH_CONF_PATH = '/etc/asterisk/musiconhold.custom.conf'; public const RTP_CONF_PATH = '/etc/asterisk/rtp.conf'; public const FEATURES_CONF_PATH = '/etc/asterisk/features.conf'; - public const VOICEMAIL_CONF_PATH = '/etc/asterisk/voicemail.custom.conf'; - public const BACKUP_DIR = __DIR__ . '/../storage/backups'; + public const VOICEMAIL_CONF_PATH = '/etc/asterisk/voicemail.custom.conf'; + public const PJSIP_NOTIFY_CONF_PATH = '/etc/asterisk/pjsip_notify.conf'; + public const BACKUP_DIR = __DIR__ . '/../storage/backups'; public function __construct(private PDO $db) {} @@ -187,6 +188,18 @@ class ConfigGenerator $lines[] = "; ==========================================================================="; $lines[] = "; POSTES TÉLÉPHONIQUES (EXTENSIONS)"; $lines[] = "; ==========================================================================="; + + // Détection de l'état du Cluster Haute Disponibilité (HA) + $clusterSt = []; + try { + $clusterSt = $this->db->query('SELECT enabled, blf_expiry FROM cluster_settings WHERE id = 1')->fetch(PDO::FETCH_ASSOC) ?: []; + } catch (\Throwable $e) {} + $isHaActive = !empty($clusterSt['enabled']); + // En mode Haute Disponibilité, on plafonne l'expiration des AOR/souscriptions sur la valeur configurée (par défaut 120s) + // afin que les téléphones se réabonnent très vite en cas de bascule. Sans HA, on conserve le standard 1800s (30 min). + $regExpiration = $isHaActive ? max(30, min(3600, (int)($clusterSt['blf_expiry'] ?? 120))) : 1800; + $subMinExpiry = $isHaActive ? 30 : 0; + foreach ($extensions as $ext) { $num = trim((string)$ext['extension']); $name = str_replace('"', '', trim((string)$ext['name'])); @@ -274,9 +287,9 @@ class ConfigGenerator $lines[] = "type = aor"; $lines[] = "max_contacts = {$maxContacts}"; $lines[] = "remove_existing = yes"; - $lines[] = "default_expiration = 120"; - $lines[] = "maximum_expiration = 300"; - $lines[] = "minimum_expiration = 60"; + $lines[] = "default_expiration = {$regExpiration}"; + $lines[] = "maximum_expiration = {$regExpiration}"; + $lines[] = "minimum_expiration = " . ($isHaActive ? 30 : 60); $lines[] = "qualify_frequency = 30"; if ($hasVm) { $lines[] = "mailboxes = {$num}@default"; @@ -1874,6 +1887,39 @@ class ConfigGenerator return implode("\n", $lines) . "\n"; } + /** + * Génère la configuration des événements SIP Notify (/etc/asterisk/pjsip_notify.conf) + * Profils check-sync pour Yealink, Fanvil, Snom, Polycom, etc. + */ + public function generatePjsipNotifyConf(): string + { + return <<check-sync\;reboot=false + +[yealink-check-cfg] +Event=>check-sync\;reboot=false + +[snom-check-cfg] +Event=>check-sync\;reboot=false + +[polycom-check-cfg] +Event=>check-sync + +[cisco-check-cfg] +Event=>check-sync + +[aastra-check-cfg] +Event=>check-sync + +EOF; + } + public function writeAllConfigs(): array { if (!is_dir(self::BACKUP_DIR)) { @@ -1886,6 +1932,7 @@ class ConfigGenerator $rtpContent = $this->generateRtpConf(); $featuresContent = $this->generateFeaturesConf(); $voicemailContent = $this->generateVoicemailConf(); + $notifyContent = $this->generatePjsipNotifyConf(); $this->writeAtomicWithPermissions(self::PJSIP_CONF_PATH, $pjsipContent); $this->writeAtomicWithPermissions(self::EXTENSIONS_CONF_PATH, $extensionsContent); @@ -1893,6 +1940,7 @@ class ConfigGenerator $this->writeAtomicWithPermissions(self::RTP_CONF_PATH, $rtpContent); $this->writeAtomicWithPermissions(self::FEATURES_CONF_PATH, $featuresContent); $this->writeAtomicWithPermissions(self::VOICEMAIL_CONF_PATH, $voicemailContent); + $this->writeAtomicWithPermissions(self::PJSIP_NOTIFY_CONF_PATH, $notifyContent); // S'assurer que pjsip.conf et extensions.conf incluent bien les fichiers .custom.conf SystemChecker::ensureAsteriskIncludes(); diff --git a/src/Database.php b/src/Database.php index fbff0ab..37d1f40 100644 --- a/src/Database.php +++ b/src/Database.php @@ -329,6 +329,7 @@ class Database cluster_secret VARCHAR(64) NOT NULL DEFAULT '', vrid INTEGER NOT NULL DEFAULT 51, priority INTEGER NOT NULL DEFAULT 100, + blf_expiry INTEGER NOT NULL DEFAULT 120, sync_interval INTEGER NOT NULL DEFAULT 15, auto_sync INTEGER NOT NULL DEFAULT 1, last_sync_at DATETIME DEFAULT NULL, @@ -813,6 +814,11 @@ class Database $db->exec("ALTER TABLE cluster_settings ADD COLUMN peer_port INTEGER NOT NULL DEFAULT 8443"); } catch (\Throwable $e) {} + // Migration de la colonne blf_expiry si elle n'existe pas encore + try { + $db->exec("ALTER TABLE cluster_settings ADD COLUMN blf_expiry INTEGER NOT NULL DEFAULT 120"); + } catch (\Throwable $e) {} + $count = (int)$db->query('SELECT COUNT(*) FROM cluster_settings')->fetchColumn(); if ($count === 0) { $defaultSecret = bin2hex(random_bytes(16));