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));