diff --git a/public/api.php b/public/api.php index 932a990..4e64a02 100644 --- a/public/api.php +++ b/public/api.php @@ -432,6 +432,82 @@ try { ]); break; + case 'check_extension_call_status': + $extension = trim((string)($payload['extension'] ?? '')); + if ($extension === '') { + throw new InvalidArgumentException("Extension requise."); + } + + $inCall = false; + $callDetails = ''; + try { + $ami = AmiClient::fromDatabase($db); + $rawChannels = $ami->runCommand("core show channels concise"); + $channelLines = array_filter(explode("\n", trim($rawChannels))); + foreach ($channelLines as $cline) { + $parts = explode('!', trim($cline)); + $chanName = $parts[0] ?? ''; + if (preg_match('#^PJSIP/' . preg_quote($extension, '#') . '[-/]#i', $chanName)) { + $inCall = true; + $state = $parts[4] ?? 'Up'; + $callerId = $parts[7] ?? ''; + $duration = $parts[10] ?? ''; + $callDetails = "Canal : {$chanName}"; + if ($callerId !== '') { + $callDetails .= " • Interlocuteur : {$callerId}"; + } + if ($duration !== '') { + $callDetails .= " • Durée : {$duration}"; + } + break; + } + } + + if (!$inCall) { + $rawHints = $ami->runCommand("core show hints {$extension}"); + if (preg_match('/' . preg_quote($extension, '/') . '@[^\n]*State:(InUse|Busy|Ringing)/i', $rawHints, $m)) { + $inCall = true; + $callDetails = "Ligne active : " . ($m[1] === 'Ringing' ? 'En sonnerie' : 'En communication'); + } + } + } catch (\Throwable $e) { + // AMI indisponible + } + + echo json_encode([ + 'success' => true, + 'extension' => $extension, + 'in_call' => $inCall, + 'call_details' => $callDetails, + ]); + break; + + case 'reboot_extension': + $extension = trim((string)($payload['extension'] ?? '')); + if ($extension === '') { + throw new InvalidArgumentException("Extension requise."); + } + + $stmt = $db->prepare('SELECT extension, name FROM extensions WHERE extension = :ext'); + $stmt->execute([':ext' => $extension]); + $extRow = $stmt->fetch(); + if (!$extRow) { + throw new InvalidArgumentException("Poste {$extension} introuvable."); + } + + try { + $ami = AmiClient::fromDatabase($db); + $cmdOutput = $ami->runCommand("pjsip send notify reboot endpoint " . escapeshellarg($extension)); + echo json_encode([ + 'success' => true, + 'message' => "Ordre de redémarrage envoyé au poste {$extension} ({$extRow['name']}).", + 'details' => trim($cmdOutput), + ]); + } catch (\Throwable $e) { + throw new RuntimeException("Erreur lors de l'envoi de la commande de redémarrage : " . $e->getMessage()); + } + break; + case 'save_group': $id = isset($payload['id']) && $payload['id'] !== '' ? (int)$payload['id'] : null; $extension = trim((string)($payload['extension'] ?? '')); diff --git a/public/assets/app.js b/public/assets/app.js index 47e64fc..7ade917 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -951,9 +951,10 @@ function renderExtensionsTable() { ${escapeHtml(ext.secret)} - - - + + + + `; @@ -1109,6 +1110,90 @@ async function deleteExtension(id, optNum = null) { } } +let pendingRebootExtension = null; + +async function promptRebootExtension(id) { + const ext = (appState.extensions || []).find(e => Number(e.id) === Number(id)); + if (!ext) return; + + pendingRebootExtension = ext; + + document.getElementById('reboot-modal-ext-num').textContent = ext.extension; + document.getElementById('reboot-modal-ext-name').textContent = ext.name || ''; + + const checkingEl = document.getElementById('reboot-modal-checking'); + const warningEl = document.getElementById('reboot-modal-call-warning'); + const normalEl = document.getElementById('reboot-modal-normal-info'); + const btnAction = document.getElementById('btn-confirm-reboot-action'); + const btnLabel = document.getElementById('reboot-confirm-btn-label'); + + checkingEl.classList.remove('hidden'); + warningEl.classList.add('hidden'); + normalEl.classList.add('hidden'); + + btnAction.disabled = true; + btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-anthracite-700 text-slate-400 transition cursor-not-allowed flex items-center gap-2'; + btnLabel.textContent = 'Vérification en cours...'; + + openModal('modal-confirm-reboot-extension'); + + try { + const res = await apiCall('check_extension_call_status', { extension: ext.extension }); + checkingEl.classList.add('hidden'); + btnAction.disabled = false; + + if (res.in_call) { + warningEl.classList.remove('hidden'); + normalEl.classList.add('hidden'); + + const detailsEl = document.getElementById('reboot-modal-call-details'); + if (detailsEl) { + detailsEl.textContent = res.call_details || 'Communication active détectée sur ce combiné.'; + } + + btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-rose-600 hover:bg-rose-500 text-white shadow-lg shadow-rose-600/30 transition flex items-center gap-2'; + btnLabel.textContent = "Forcer le redémarrage (Couper l'appel)"; + } else { + warningEl.classList.add('hidden'); + normalEl.classList.remove('hidden'); + + btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-600/30 transition flex items-center gap-2'; + btnLabel.textContent = 'Confirmer le redémarrage'; + } + } catch (err) { + checkingEl.classList.add('hidden'); + normalEl.classList.remove('hidden'); + btnAction.disabled = false; + btnAction.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-600/30 transition flex items-center gap-2'; + btnLabel.textContent = 'Confirmer le redémarrage'; + } +} + +function closeRebootModal() { + pendingRebootExtension = null; + closeModal('modal-confirm-reboot-extension'); +} + +async function confirmRebootExtension() { + if (!pendingRebootExtension) return; + const ext = pendingRebootExtension; + const btnAction = document.getElementById('btn-confirm-reboot-action'); + const btnLabel = document.getElementById('reboot-confirm-btn-label'); + + if (btnAction) btnAction.disabled = true; + if (btnLabel) btnLabel.textContent = 'Envoi de l\'ordre...'; + + try { + const res = await apiCall('reboot_extension', { extension: ext.extension }); + closeRebootModal(); + showToast(res.message, 'success'); + } catch (err) { + showToast(err.message, 'error'); + } finally { + if (btnAction) btnAction.disabled = false; + } +} + function syncEncryptionToggle(transport, currentVal = null) { const sel = document.getElementById('ext-encryption'); if (!sel) return; diff --git a/public/index.php b/public/index.php index 2f22ce4..3f70a9c 100644 --- a/public/index.php +++ b/public/index.php @@ -5367,6 +5367,90 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev') + + + + + diff --git a/src/ConfigGenerator.php b/src/ConfigGenerator.php index 479a919..a4899d2 100644 --- a/src/ConfigGenerator.php +++ b/src/ConfigGenerator.php @@ -1903,12 +1903,21 @@ class ConfigGenerator [check-sync] Event=>check-sync\;reboot=false +[reboot] +Event=>check-sync\;reboot=true + [yealink-check-cfg] Event=>check-sync\;reboot=false +[yealink-reboot] +Event=>check-sync\;reboot=true + [snom-check-cfg] Event=>check-sync\;reboot=false +[snom-reboot] +Event=>check-sync\;reboot=true + [polycom-check-cfg] Event=>check-sync