Compare commits

..
6 Commits
6 changed files with 317 additions and 62 deletions

No files matched your search

+25 -13
View File
@@ -136,8 +136,15 @@ try {
$settings['local_networks'] = [$settings['local_net']]; $settings['local_networks'] = [$settings['local_net']];
} }
$appDir = dirname(__DIR__);
$installedVerData = file_exists("{$appDir}/version.json")
? json_decode((string)file_get_contents("{$appDir}/version.json"), true)
: [];
$installedVer = $installedVerData['version'] ?? '1.4.3';
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'version' => $installedVer,
'user' => Auth::getUsername(), 'user' => Auth::getUsername(),
'settings' => $settings, 'settings' => $settings,
'extensions' => $extensions, 'extensions' => $extensions,
@@ -2233,23 +2240,28 @@ try {
case 'check_update': case 'check_update':
$appDir = dirname(__DIR__); $appDir = dirname(__DIR__);
clearstatcache(true, "{$appDir}/version.json");
$localVerJson = file_exists("{$appDir}/version.json") $localVerJson = file_exists("{$appDir}/version.json")
? json_decode((string)file_get_contents("{$appDir}/version.json"), true) ? json_decode((string)file_get_contents("{$appDir}/version.json"), true)
: ['version' => '1.2.0', 'changelog' => []]; : ['version' => '1.3.8', 'changelog' => []];
$currentVer = $localVerJson['version'] ?? '1.2.0'; $currentVer = $localVerJson['version'] ?? '1.3.8';
$remoteVer = $currentVer; $remoteVer = $currentVer;
$remoteChangelog = []; $remoteChangelog = [];
$remoteDate = ''; $remoteDate = '';
// 1. Interrogation directe via HTTP Raw (indépendant de .git et ultra-rapide) // 1. Interrogation directe via HTTP Raw avec cache-busting strict (bypass OpenResty/Cloudflare max-age)
$rawHttp = @shell_exec("curl -k -s --connect-timeout 4 \"https://git.sorcier-research.com/sorcier/NexusPBX/raw/branch/main/version.json\" 2>/dev/null"); $ts = time();
$rawHttp = @shell_exec("curl -k -s -H 'Cache-Control: no-cache, no-store' -H 'Pragma: no-cache' --connect-timeout 4 \"https://git.sorcier-research.com/sorcier/NexusPBX/raw/branch/main/version.json?_t={$ts}\" 2>/dev/null");
if (empty($rawHttp)) { if (empty($rawHttp)) {
$ctx = stream_context_create([ $ctx = stream_context_create([
'http' => ['timeout' => 4], 'http' => [
'timeout' => 4,
'header' => "Cache-Control: no-cache\r\nPragma: no-cache\r\n"
],
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false]
]); ]);
$rawHttp = @file_get_contents("https://git.sorcier-research.com/sorcier/NexusPBX/raw/branch/main/version.json", false, $ctx); $rawHttp = @file_get_contents("https://git.sorcier-research.com/sorcier/NexusPBX/raw/branch/main/version.json?_t={$ts}", false, $ctx);
} }
if (!empty($rawHttp)) { if (!empty($rawHttp)) {
@@ -2274,11 +2286,6 @@ try {
} }
$hasUpdate = version_compare($remoteVer, $currentVer, '>'); $hasUpdate = version_compare($remoteVer, $currentVer, '>');
$localHash = trim((string)shell_exec("git --git-dir={$appDir}/.git rev-parse HEAD 2>/dev/null"));
$remoteHash = trim((string)shell_exec("git --git-dir={$appDir}/.git rev-parse origin/main 2>/dev/null"));
if (!$hasUpdate && $localHash !== '' && $remoteHash !== '' && $localHash !== $remoteHash) {
$hasUpdate = true;
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
@@ -2297,13 +2304,18 @@ try {
$outputStr = implode("\n", $output); $outputStr = implode("\n", $output);
$appDir = dirname(__DIR__); $appDir = dirname(__DIR__);
clearstatcache(true, "{$appDir}/version.json");
if (function_exists('opcache_reset')) {
@opcache_reset();
}
$newVerJson = file_exists("{$appDir}/version.json") $newVerJson = file_exists("{$appDir}/version.json")
? json_decode((string)file_get_contents("{$appDir}/version.json"), true) ? json_decode((string)file_get_contents("{$appDir}/version.json"), true)
: ['version' => '1.3.0']; : ['version' => '1.3.8'];
echo json_encode([ echo json_encode([
'success' => ($code === 0), 'success' => ($code === 0),
'new_version' => $newVerJson['version'] ?? '1.3.0', 'new_version' => $newVerJson['version'] ?? '1.3.8',
'output' => $outputStr, 'output' => $outputStr,
'message' => ($code === 0) ? "Mise à jour réussie !" : "Erreur lors de la mise à jour", 'message' => ($code === 0) ? "Mise à jour réussie !" : "Erreur lors de la mise à jour",
]); ]);
+173 -26
View File
@@ -266,20 +266,28 @@ function renderCompanyBranding() {
if (loginBox) { if (loginBox) {
loginBox.classList.remove('hidden'); loginBox.classList.remove('hidden');
loginBox.classList.add('flex');
if (logo && loginLogo) { if (logo && loginLogo) {
loginLogo.src = logo; loginLogo.src = logo;
loginLogo.classList.remove('hidden'); loginLogo.classList.remove('hidden');
} else if (loginLogo) { } else if (loginLogo) {
loginLogo.src = '';
loginLogo.classList.add('hidden'); loginLogo.classList.add('hidden');
} }
if (loginName) loginName.textContent = name; if (loginName) {
loginName.textContent = name;
loginName.classList.toggle('hidden', !name);
}
} }
} else { } else {
emptyBox?.classList.remove('hidden'); emptyBox?.classList.remove('hidden');
filledBox?.classList.add('hidden'); filledBox?.classList.add('hidden');
if (logoImg) { logoImg.src = ''; logoImg.classList.add('hidden'); } if (logoImg) { logoImg.src = ''; logoImg.classList.add('hidden'); }
if (nameEl) { nameEl.textContent = ''; nameEl.classList.add('hidden'); } if (nameEl) { nameEl.textContent = ''; nameEl.classList.add('hidden'); }
loginBox?.classList.add('hidden'); if (loginBox) {
loginBox.classList.add('hidden');
loginBox.classList.remove('flex');
}
} }
} }
@@ -629,20 +637,56 @@ function drawDualAreaChart(svgId, seriesA, seriesB, maxVal, colorA, colorB, unit
const padTop = 14; const padTop = 14;
const padBottom = 18; const padBottom = 18;
const plotH = height - padTop - padBottom; const plotH = height - padTop - padBottom;
const baselineY = padTop + plotH;
const safeMax = maxVal > 0 ? maxVal : 100; const safeMax = maxVal > 0 ? maxVal : 100;
const toPoints = (arr) => arr.map((val, idx) => { const clampY = (y) => Math.max(padTop, Math.min(baselineY, y));
// Génération de courbes splines adoucies (Catmull-Rom / Bézier cubique) pour éliminer les angles pointus
const computeSmoothPaths = (arr, tension = 0.2) => {
if (!arr || arr.length === 0) return { line: '', area: '' };
const points = arr.map((val, idx) => {
const x = (idx / (arr.length - 1)) * width; const x = (idx / (arr.length - 1)) * width;
const ratio = Math.max(0, Math.min(1, val / safeMax)); const ratio = Math.max(0, Math.min(1, val / safeMax));
const y = padTop + plotH - (ratio * plotH); const y = baselineY - (ratio * plotH);
return `${x.toFixed(1)},${y.toFixed(1)}`; return { x, y };
}); });
const ptsA = toPoints(seriesA); if (points.length === 1) {
const ptsB = toPoints(seriesB); return {
line: `M ${points[0].x.toFixed(1)},${points[0].y.toFixed(1)}`,
area: `M 0,${baselineY} L ${points[0].x.toFixed(1)},${points[0].y.toFixed(1)} L ${width},${baselineY} Z`
};
}
const areaA = `0,${padTop + plotH} ` + ptsA.join(' ') + ` ${width},${padTop + plotH}`; let line = `M ${points[0].x.toFixed(1)},${points[0].y.toFixed(1)}`;
const areaB = `0,${padTop + plotH} ` + ptsB.join(' ') + ` ${width},${padTop + plotH}`;
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[i === 0 ? i : i - 1];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[i + 2 < points.length ? i + 2 : i + 1];
let cp1x = p1.x + (p2.x - p0.x) * tension;
let cp1y = p1.y + (p2.y - p0.y) * tension;
let cp2x = p2.x - (p3.x - p1.x) * tension;
let cp2y = p2.y - (p3.y - p1.y) * tension;
// Clamping vertical pour garantir que la courbe ne déborde pas sous l'axe ni au-dessus du max
cp1y = clampY(cp1y);
cp2y = clampY(cp2y);
line += ` C ${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`;
}
const area = `${line} L ${width},${baselineY} L 0,${baselineY} Z`;
return { line, area };
};
const pathA = computeSmoothPaths(seriesA);
const pathB = computeSmoothPaths(seriesB);
const gradIdA = `${svgId}-grad-a`; const gradIdA = `${svgId}-grad-a`;
const gradIdB = `${svgId}-grad-b`; const gradIdB = `${svgId}-grad-b`;
@@ -662,18 +706,18 @@ function drawDualAreaChart(svgId, seriesA, seriesB, maxVal, colorA, colorB, unit
<!-- Lignes de grille horizontales --> <!-- Lignes de grille horizontales -->
<line x1="0" y1="${padTop}" x2="${width}" y2="${padTop}" stroke="#2c323f" stroke-dasharray="3,3" stroke-width="1"/> <line x1="0" y1="${padTop}" x2="${width}" y2="${padTop}" stroke="#2c323f" stroke-dasharray="3,3" stroke-width="1"/>
<line x1="0" y1="${padTop + plotH / 2}" x2="${width}" y2="${padTop + plotH / 2}" stroke="#2c323f" stroke-dasharray="3,3" stroke-width="1"/> <line x1="0" y1="${padTop + plotH / 2}" x2="${width}" y2="${padTop + plotH / 2}" stroke="#2c323f" stroke-dasharray="3,3" stroke-width="1"/>
<line x1="0" y1="${padTop + plotH}" x2="${width}" y2="${padTop + plotH}" stroke="#3d4657" stroke-width="1"/> <line x1="0" y1="${baselineY}" x2="${width}" y2="${baselineY}" stroke="#3d4657" stroke-width="1"/>
<text x="6" y="${padTop + 10}" fill="#94a3b8" font-size="10" font-family="monospace">${Math.round(safeMax)} ${unitLabel}</text> <text x="6" y="${padTop + 10}" fill="#94a3b8" font-size="10" font-family="monospace">${Math.round(safeMax)} ${unitLabel}</text>
<text x="6" y="${padTop + plotH / 2 - 4}" fill="#64748b" font-size="10" font-family="monospace">${Math.round(safeMax / 2)} ${unitLabel}</text> <text x="6" y="${padTop + plotH / 2 - 4}" fill="#64748b" font-size="10" font-family="monospace">${Math.round(safeMax / 2)} ${unitLabel}</text>
<!-- Courbe B --> <!-- Courbe B lissée avec dégradé -->
<polygon points="${areaB}" fill="url(#${gradIdB})"/> <path d="${pathB.area}" fill="url(#${gradIdB})"/>
<polyline fill="none" stroke="${colorB}" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" points="${ptsB.join(' ')}"/> <path d="${pathB.line}" fill="none" stroke="${colorB}" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Courbe A --> <!-- Courbe A lissée avec dégradé -->
<polygon points="${areaA}" fill="url(#${gradIdA})"/> <path d="${pathA.area}" fill="url(#${gradIdA})"/>
<polyline fill="none" stroke="${colorA}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" points="${ptsA.join(' ')}"/> <path d="${pathA.line}" fill="none" stroke="${colorA}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
`; `;
} }
@@ -5735,15 +5779,26 @@ async function checkSystemUpdate(notify = false) {
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
// Si pas de clic explicite (notify === false), vérifier si un check a déjà été fait il y a moins de 24h // Si pas de clic explicite (notify === false), vérifier si un check a déjà été fait il y a moins de 24h
const installedVer = (appState && appState.version) ? appState.version : (document.querySelector('meta[name="nexus-app-version"]')?.content || '');
if (!notify) { if (!notify) {
const lastTs = parseInt(localStorage.getItem(STORAGE_KEY_TIME) || '0', 10); const lastTs = parseInt(localStorage.getItem(STORAGE_KEY_TIME) || '0', 10);
const cachedRaw = localStorage.getItem(STORAGE_KEY_DATA); const cachedRaw = localStorage.getItem(STORAGE_KEY_DATA);
if (lastTs && (Date.now() - lastTs < TWENTY_FOUR_HOURS) && cachedRaw) { if (lastTs && (Date.now() - lastTs < TWENTY_FOUR_HOURS) && cachedRaw) {
try { try {
const cached = JSON.parse(cachedRaw); const cached = JSON.parse(cachedRaw);
// Si la version locale correspond et qu'on n'a pas un vieux flag "mise à jour dispo" vers la version déjà installée
if (installedVer && (cached.current_version !== installedVer || (cached.has_update && cached.latest_version === installedVer))) {
// Cache périmé suite à une mise à jour : on purge pour rafraîchir en direct
localStorage.removeItem(STORAGE_KEY_TIME);
localStorage.removeItem(STORAGE_KEY_DATA);
} else {
applyUpdateUI(cached); applyUpdateUI(cached);
return; return;
} catch (_) {} }
} catch (_) {
localStorage.removeItem(STORAGE_KEY_TIME);
localStorage.removeItem(STORAGE_KEY_DATA);
}
} }
} else { } else {
localStorage.removeItem(STORAGE_KEY_TIME); localStorage.removeItem(STORAGE_KEY_TIME);
@@ -5759,7 +5814,7 @@ async function checkSystemUpdate(notify = false) {
} }
try { try {
const res = await fetch('/api.php?action=check_update'); const res = await fetch('/api.php?action=check_update&_t=' + Date.now());
const data = await res.json(); const data = await res.json();
// Enregistrement du timestamp de vérification et des données en cache local (24h) // Enregistrement du timestamp de vérification et des données en cache local (24h)
@@ -5841,14 +5896,57 @@ async function checkSystemUpdate(notify = false) {
} }
} }
async function runSystemUpdate() { function openUpdateConfirmModal() {
if (!confirm("Voulez-vous lancer la mise à jour de NexusPBX ?\n\nUne sauvegarde préventive automatique de vos configurations et de votre base de données sera créée.")) { const modal = document.getElementById('modal-confirm-update');
return; if (!modal) return;
let targetVer = '';
let currVer = (appState && appState.version) ? appState.version : (document.querySelector('meta[name="nexus-app-version"]')?.content || '');
try {
const cachedRaw = localStorage.getItem('nexus_update_cached_data');
if (cachedRaw) {
const cached = JSON.parse(cachedRaw);
if (cached.latest_version) targetVer = cached.latest_version;
if (cached.current_version) currVer = cached.current_version;
}
} catch (_) {}
const elTarget = document.getElementById('update-modal-target-version');
const elCurrent = document.getElementById('update-modal-current-version');
const elBadge = document.getElementById('update-modal-target-badge');
if (elTarget) elTarget.textContent = targetVer ? `v${targetVer}` : 'Dernière version';
if (elCurrent) elCurrent.textContent = currVer ? `v${currVer}` : 'vActuelle';
if (elBadge && targetVer) elBadge.textContent = `v${targetVer} disponible`;
modal.classList.remove('hidden');
modal.classList.add('flex');
} }
function closeUpdateConfirmModal() {
const modal = document.getElementById('modal-confirm-update');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
function runSystemUpdate() {
openUpdateConfirmModal();
}
async function executeSystemUpdate() {
const btnModalConfirm = document.getElementById('btn-confirm-update-action');
const btnRun = document.getElementById('btn-run-update'); const btnRun = document.getElementById('btn-run-update');
const statusText = document.getElementById('update-status-text'); const statusText = document.getElementById('update-status-text');
if (btnModalConfirm) {
btnModalConfirm.disabled = true;
btnModalConfirm.classList.add('opacity-75', 'cursor-not-allowed');
btnModalConfirm.innerHTML = '<span class="animate-spin mr-2">↻</span> Déploiement en cours...';
}
if (btnRun) { if (btnRun) {
btnRun.disabled = true; btnRun.disabled = true;
btnRun.innerHTML = '<span class="animate-spin mr-1.5">↻</span> Mise à jour en cours...'; btnRun.innerHTML = '<span class="animate-spin mr-1.5">↻</span> Mise à jour en cours...';
@@ -5860,8 +5958,12 @@ async function runSystemUpdate() {
showToast("Mise à jour en cours d'exécution en arrière-plan...", 'info'); showToast("Mise à jour en cours d'exécution en arrière-plan...", 'info');
// Purge préventive du cache local
localStorage.removeItem('nexus_update_last_check_ts');
localStorage.removeItem('nexus_update_cached_data');
try { try {
const res = await fetch('/api.php?action=run_update', { const res = await fetch('/api.php?action=run_update&_t=' + Date.now(), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}) body: JSON.stringify({})
@@ -5869,26 +5971,71 @@ async function runSystemUpdate() {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
showToast(`NexusPBX a été mis à jour avec succès en v${data.new_version} ! Rechargement...`, 'success'); const newVer = data.new_version || '';
// 1. Mettre à jour immédiatement le cache avec l'état à jour
localStorage.setItem('nexus_update_last_check_ts', Date.now().toString());
localStorage.setItem('nexus_update_cached_data', JSON.stringify({
success: true,
current_version: newVer,
latest_version: newVer,
has_update: false,
changelog: [],
release_date: ''
}));
// 2. Mettre à jour visuellement les badges immédiatement
const verTag = document.getElementById('system-version-tag');
const appBadge = document.getElementById('app-version-badge');
const headerUpdateBadge = document.getElementById('header-update-badge');
const pingDot = document.getElementById('update-ping-dot');
if (verTag && newVer) verTag.textContent = `v${newVer}`;
if (appBadge && newVer) appBadge.textContent = `v${newVer}`;
if (headerUpdateBadge) headerUpdateBadge.style.display = 'none';
if (btnRun) btnRun.style.display = 'none';
if (pingDot) pingDot.classList.add('hidden');
showToast(`NexusPBX a été mis à jour avec succès en v${newVer} ! Rechargement...`, 'success');
if (statusText) { if (statusText) {
statusText.className = 'text-emerald-400 font-bold'; statusText.className = 'text-emerald-400 font-bold';
statusText.textContent = `Mise à jour terminée avec succès (v${data.new_version}) ! Rechargement de la page...`; statusText.textContent = `Mise à jour terminée avec succès (v${newVer}) ! Rechargement...`;
} }
if (btnModalConfirm) {
btnModalConfirm.className = 'px-5 py-2.5 rounded-xl text-xs font-bold bg-emerald-600 text-white flex items-center gap-2';
btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...';
}
// 3. Rechargement avec paramètre anti-cache pour forcer le navigateur à récupérer le nouveau DOM
setTimeout(() => { setTimeout(() => {
window.location.reload(); closeUpdateConfirmModal();
}, 2500); window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now();
}, 1800);
} else { } else {
closeUpdateConfirmModal();
alert("Erreur lors de la mise à jour :\n\n" + (data.output || data.message)); alert("Erreur lors de la mise à jour :\n\n" + (data.output || data.message));
if (btnRun) { if (btnRun) {
btnRun.disabled = false; btnRun.disabled = false;
btnRun.innerHTML = 'Réessayer la mise à jour'; btnRun.innerHTML = 'Réessayer la mise à jour';
} }
if (btnModalConfirm) {
btnModalConfirm.disabled = false;
btnModalConfirm.classList.remove('opacity-75', 'cursor-not-allowed');
btnModalConfirm.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg><span>Lancer la mise à jour (1-Clic)</span>';
}
} }
} catch (e) { } catch (e) {
closeUpdateConfirmModal();
alert("Erreur réseau lors de la mise à jour : " + e.message); alert("Erreur réseau lors de la mise à jour : " + e.message);
if (btnRun) { if (btnRun) {
btnRun.disabled = false; btnRun.disabled = false;
btnRun.innerHTML = 'Réessayer la mise à jour'; btnRun.innerHTML = 'Réessayer la mise à jour';
} }
if (btnModalConfirm) {
btnModalConfirm.disabled = false;
btnModalConfirm.classList.remove('opacity-75', 'cursor-not-allowed');
btnModalConfirm.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg><span>Lancer la mise à jour (1-Clic)</span>';
}
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+112 -16
View File
@@ -10,12 +10,22 @@ use App\Auth;
Database::getConnection(); Database::getConnection();
$isAuth = Auth::isAuthenticated(); $isAuth = Auth::isAuthenticated();
$currentUser = Auth::getUsername(); $currentUser = Auth::getUsername();
$stmtSettings = Database::getConnection()->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1');
$branding = $stmtSettings ? $stmtSettings->fetch(\PDO::FETCH_ASSOC) : null;
$companyName = trim($branding['company_name'] ?? '');
$companyLogo = trim($branding['company_logo'] ?? '');
$hasBranding = !empty($companyName) || !empty($companyLogo);
$appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : [];
$appVersion = $appVerData['version'] ?? '1.4.3';
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr" class="h-full"> <html lang="fr" class="h-full">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="nexus-app-version" content="<?= htmlspecialchars($appVersion) ?>">
<title>NexusPBX — Supervision & Gestion PJSIP sur Debian</title> <title>NexusPBX — Supervision & Gestion PJSIP sur Debian</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
@@ -140,8 +150,13 @@ $currentUser = Auth::getUsername();
<div id="toast-container" class="fixed bottom-5 right-5 z-50 flex flex-col gap-2.5 max-w-md w-full pointer-events-none px-4 sm:px-0"></div> <div id="toast-container" class="fixed bottom-5 right-5 z-50 flex flex-col gap-2.5 max-w-md w-full pointer-events-none px-4 sm:px-0"></div>
<!-- ÉCRAN DE CONNEXION --> <!-- ÉCRAN DE CONNEXION -->
<div id="login-screen" class="<?= $isAuth ? 'hidden' : 'flex' ?> min-h-screen w-full items-center justify-center p-4 bg-gradient-to-br from-anthracite-950 via-anthracite-900 to-indigo-950/40"> <div id="login-screen" class="<?= $isAuth ? 'hidden' : 'flex' ?> min-h-screen w-full items-center justify-center p-4 relative overflow-hidden bg-anthracite-950">
<div class="w-full max-w-md bg-anthracite-800 border border-anthracite-700 rounded-2xl shadow-2xl p-8"> <!-- Arrière-plan personnalisé / Image de fond avec overlay sombre et léger flou -->
<div id="login-bg-layer" class="absolute inset-0 bg-cover bg-center bg-no-repeat" style="background-image: url('/assets/login-bg.jpg?v=<?= file_exists(__DIR__ . '/assets/login-bg.jpg') ? filemtime(__DIR__ . '/assets/login-bg.jpg') : 1 ?>');"></div>
<div class="absolute inset-0 bg-gradient-to-tr from-anthracite-950/70 via-anthracite-950/50 to-indigo-950/30 backdrop-blur-[1px]"></div>
<!-- Carte de connexion flottante avec effet Glassmorphism transparent et aéré -->
<div class="relative z-10 w-full max-w-md bg-anthracite-900/60 backdrop-blur-2xl border border-white/15 rounded-3xl shadow-2xl shadow-black/50 p-8 sm:p-9 transition-all">
<div class="flex items-center gap-3.5 mb-6"> <div class="flex items-center gap-3.5 mb-6">
<div class="w-12 h-12 rounded-xl bg-indigo-500/20 border border-indigo-400/40 flex items-center justify-center text-indigo-300 shadow-inner"> <div class="w-12 h-12 rounded-xl bg-indigo-500/20 border border-indigo-400/40 flex items-center justify-center text-indigo-300 shadow-inner">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -154,33 +169,28 @@ $currentUser = Auth::getUsername();
</div> </div>
</div> </div>
<!-- Aperçu Logo Entreprise sur l'écran de connexion (si configuré) --> <!-- Aperçu Logo Entreprise sur l'écran de connexion (rendu serveur natif) -->
<div id="login-company-box" class="hidden mb-5 p-3 rounded-xl bg-anthracite-950/90 border border-anthracite-700 flex items-center justify-center gap-3"> <div id="login-company-box" class="<?= $hasBranding ? 'flex' : 'hidden' ?> mb-5 p-3 rounded-xl bg-anthracite-950/45 border border-white/10 backdrop-blur-md items-center justify-center gap-3">
<img id="login-company-logo" src="" alt="Logo Entreprise" class="max-h-10 max-w-[160px] object-contain hidden"> <img id="login-company-logo" src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo Entreprise" class="max-h-10 max-w-[160px] object-contain <?= !empty($companyLogo) ? '' : 'hidden' ?>">
<span id="login-company-name" class="text-xs font-semibold text-slate-200"></span> <span id="login-company-name" class="text-xs font-semibold text-slate-200 <?= !empty($companyName) ? '' : 'hidden' ?>"><?= htmlspecialchars($companyName) ?></span>
</div> </div>
<form id="login-form" class="space-y-4"> <form id="login-form" class="space-y-4">
<div> <div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5">Identifiant</label> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5">Identifiant</label>
<input type="text" name="username" value="admin" required <input type="text" name="username" value="admin" required
class="w-full px-3.5 py-2.5 rounded-lg bg-anthracite-950 border border-anthracite-700 text-white focus:outline-none focus:border-indigo-400 text-sm"> class="w-full px-3.5 py-2.5 rounded-xl bg-anthracite-950/50 backdrop-blur-md border border-white/10 text-white placeholder-slate-400 focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20 text-sm transition">
</div> </div>
<div> <div>
<label class="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5">Mot de passe</label> <label class="block text-xs font-semibold uppercase tracking-wider text-slate-300 mb-1.5">Mot de passe</label>
<input type="password" name="password" placeholder="••••••••" required <input type="password" name="password" placeholder="••••••••" required
class="w-full px-3.5 py-2.5 rounded-lg bg-anthracite-950 border border-anthracite-700 text-white focus:outline-none focus:border-indigo-400 text-sm"> class="w-full px-3.5 py-2.5 rounded-xl bg-anthracite-950/50 backdrop-blur-md border border-white/10 text-white placeholder-slate-400 focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/20 text-sm transition">
</div> </div>
<button type="submit" <button type="submit"
class="w-full py-2.5 px-4 bg-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 text-white font-semibold rounded-lg shadow-lg shadow-indigo-600/25 transition text-sm"> class="w-full py-2.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 active:from-indigo-700 active:to-indigo-600 text-white font-semibold rounded-xl shadow-lg shadow-indigo-500/25 transition text-sm">
Se connecter au PBX Se connecter au PBX
</button> </button>
</form> </form>
<div class="mt-6 pt-5 border-t border-anthracite-700 text-xs text-slate-300 flex items-center justify-between">
<span>Accès initial par défaut :</span>
<code class="px-2.5 py-1 rounded bg-anthracite-950 border border-anthracite-700 text-indigo-300 font-mono">admin / admin123</code>
</div>
</div> </div>
</div> </div>
@@ -207,7 +217,7 @@ $currentUser = Auth::getUsername();
<div class="font-black text-white text-2xl tracking-tight leading-none"> <div class="font-black text-white text-2xl tracking-tight leading-none">
Nexus<span class="text-indigo-400">PBX</span> Nexus<span class="text-indigo-400">PBX</span>
</div> </div>
<span id="app-version-badge" class="px-1.5 py-0.5 text-[10px] font-mono font-bold tracking-wider rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v1.3.0</span> <span id="app-version-badge" class="px-1.5 py-0.5 text-[10px] font-mono font-bold tracking-wider rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v<?= htmlspecialchars($appVersion) ?></span>
</div> </div>
<div class="text-[10px] text-slate-400 font-medium tracking-wide mt-0.5">Moteur VoIP & Téléphonie IP</div> <div class="text-[10px] text-slate-400 font-medium tracking-wide mt-0.5">Moteur VoIP & Téléphonie IP</div>
</div> </div>
@@ -1577,7 +1587,7 @@ $currentUser = Auth::getUsername();
<span id="update-dot" class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span> <span id="update-dot" class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span> </span>
<h3 class="text-base font-bold text-white">Mises à Jour du Système NexusPBX</h3> <h3 class="text-base font-bold text-white">Mises à Jour du Système NexusPBX</h3>
<span id="system-version-tag" class="px-2 py-0.5 text-xs font-mono font-bold rounded-lg bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v1.3.0</span> <span id="system-version-tag" class="px-2 py-0.5 text-xs font-mono font-bold rounded-lg bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">v<?= htmlspecialchars($appVersion) ?></span>
</div> </div>
<p class="text-xs text-slate-300 mt-1">Vérifiez les nouvelles versions officielles et mettez à jour votre PBX en 1 clic sans coupure de service.</p> <p class="text-xs text-slate-300 mt-1">Vérifiez les nouvelles versions officielles et mettez à jour votre PBX en 1 clic sans coupure de service.</p>
</div> </div>
@@ -4716,6 +4726,92 @@ $currentUser = Auth::getUsername();
</div> </div>
</div> </div>
<!-- ===================================================================== -->
<!-- MODAL : CONFIRMATION MISE À JOUR SYSTEME (OTA 1-CLIC) -->
<!-- ===================================================================== -->
<div id="modal-confirm-update" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/80 backdrop-blur-sm p-3 sm:p-4 overflow-y-auto">
<div class="bg-anthracite-900 border border-indigo-500/40 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden my-6 animate-fadeIn">
<!-- Entête stylisée -->
<div class="px-6 py-5 border-b border-anthracite-800 bg-gradient-to-r from-indigo-950/50 via-anthracite-900 to-anthracite-900 flex items-start justify-between">
<div class="flex items-center gap-3.5">
<div class="w-11 h-11 rounded-xl bg-indigo-500/15 border border-indigo-500/30 text-indigo-400 flex items-center justify-center shrink-0 shadow-lg shadow-indigo-950/50">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
</div>
<div>
<div class="flex items-center gap-2">
<h3 class="text-base font-bold text-white">Mise à Jour du Système NexusPBX</h3>
<span id="update-modal-target-badge" class="px-2 py-0.5 rounded text-[10px] font-bold tracking-wide uppercase bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">OTA 1-Clic</span>
</div>
<p class="text-xs text-slate-400 mt-0.5">Déploiement officiel et rechargement sans coupure</p>
</div>
</div>
<button type="button" onclick="closeUpdateConfirmModal()" class="text-slate-400 hover:text-white p-1.5 rounded-lg hover:bg-anthracite-800 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<!-- Corps de la modale -->
<div class="p-6 space-y-4">
<!-- Info cible -->
<div class="p-3.5 rounded-xl bg-anthracite-950/90 border border-anthracite-800 flex items-center justify-between">
<div>
<div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Version Cible</div>
<div id="update-modal-target-version" class="text-base font-bold text-emerald-400 font-mono mt-0.5">v...</div>
</div>
<div class="text-right">
<div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Version Actuelle</div>
<div id="update-modal-current-version" class="text-sm font-semibold text-slate-300 font-mono mt-0.5">v...</div>
</div>
</div>
<!-- Garanties & Sauvegarde automatique -->
<div class="p-3.5 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-xs text-emerald-300 flex items-start gap-3">
<svg class="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<div>
<strong class="font-bold text-emerald-200">Sauvegarde préventive automatique incluse :</strong>
<p class="mt-0.5 text-emerald-300/90 leading-relaxed">Une sauvegarde complète de votre base SQLite et de vos configurations Asterisk sera créée avant toute modification.</p>
</div>
</div>
<!-- Ce que fait la mise à jour -->
<div class="space-y-2">
<div class="text-xs font-semibold text-slate-300">Étapes automatiques du déploiement :</div>
<ul class="text-xs text-slate-400 space-y-1.5 pl-1">
<li class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-400 shrink-0"></span>
<span>Téléchargement des derniers fichiers et correctifs du dépôt officiel.</span>
</li>
<li class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0"></span>
<span>Exécution des migrations SQLite et régénération des configurations Asterisk.</span>
</li>
<li class="flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 shrink-0"></span>
<span>Rechargement à chaud sans interruption des communications actives.</span>
</li>
</ul>
</div>
</div>
<!-- Boutons d'action -->
<div class="px-6 py-4 border-t border-anthracite-800 bg-anthracite-950/80 flex items-center justify-end gap-3">
<button type="button" onclick="closeUpdateConfirmModal()"
class="px-4 py-2.5 rounded-xl text-xs font-semibold bg-anthracite-800 hover:bg-anthracite-750 text-slate-300 border border-anthracite-700 transition">
Annuler
</button>
<button type="button" id="btn-confirm-update-action" onclick="executeSystemUpdate()"
class="px-5 py-2.5 rounded-xl text-xs font-bold bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white shadow-lg shadow-indigo-600/30 transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<span>Lancer la mise à jour (1-Clic)</span>
</button>
</div>
</div>
</div>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- MODAL : URL XML YEALINK REMOTE PHONEBOOK (cm_to_yl_ab.php) --> <!-- MODAL : URL XML YEALINK REMOTE PHONEBOOK (cm_to_yl_ab.php) -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
+2 -2
View File
@@ -93,7 +93,7 @@ echo -e "${C_GREEN} ✓ Sauvegarde de sécurité créée : $(basename "$BACKUP_
# 2. Application des derniers fichiers de code Git # 2. Application des derniers fichiers de code Git
echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}" echo -e "${C_GRAY}[2/6] Déploiement des nouveaux fichiers de code...${C_RESET}"
git reset --hard "${GIT_REMOTE}/${GIT_BRANCH}" git reset --hard FETCH_HEAD 2>/dev/null || git reset --hard "${GIT_REMOTE}/${GIT_BRANCH}"
chown -R www-data:asterisk "$APP_DIR" 2>/dev/null || true chown -R www-data:asterisk "$APP_DIR" 2>/dev/null || true
chmod 2775 "$APP_DIR/storage" "$APP_DIR/storage/backups" "$APP_DIR/storage/sounds" 2>/dev/null || true chmod 2775 "$APP_DIR/storage" "$APP_DIR/storage/backups" "$APP_DIR/storage/sounds" 2>/dev/null || true
chmod +x "$APP_DIR/scripts/update.sh" "$APP_DIR/scripts/nexus-firewall" "$APP_DIR/scripts/nexus-banner.sh" 2>/dev/null || true chmod +x "$APP_DIR/scripts/update.sh" "$APP_DIR/scripts/nexus-firewall" "$APP_DIR/scripts/nexus-banner.sh" 2>/dev/null || true
@@ -139,7 +139,7 @@ echo -e "${C_GRAY}[6/6] Rechargement à chaud d'Asterisk et des services web...$
asterisk -rx "core reload" >/dev/null 2>&1 || true asterisk -rx "core reload" >/dev/null 2>&1 || true
asterisk -rx "pjsip reload" >/dev/null 2>&1 || true asterisk -rx "pjsip reload" >/dev/null 2>&1 || true
asterisk -rx "dialplan reload" >/dev/null 2>&1 || true asterisk -rx "dialplan reload" >/dev/null 2>&1 || true
systemctl reload php*-fpm 2>/dev/null || true systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php8.2-fpm 2>/dev/null || systemctl reload php*-fpm 2>/dev/null || true
systemctl reload nginx 2>/dev/null || true systemctl reload nginx 2>/dev/null || true
NEW_VER="Inconnue" NEW_VER="Inconnue"
+5 -5
View File
@@ -1,12 +1,12 @@
{ {
"version": "1.3.7", "version": "1.4.3",
"codename": "Helios", "codename": "Helios",
"release_date": "2026-11-04", "release_date": "2026-11-04",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
"Vérification ultra-rapide des mises à jour en direct via cURL CLI et flux HTTP sans dépendre de git ou de php-curl", "Nouvelle boîte modale élégante et personnalisée pour confirmer le lancement des mises à jour (remplacement du confirm natif)",
"Bouton 'Vérifier les mises à jour' forçant le rafraîchissement immédiat du cache navigateur (bypass 24h)", "Rendu serveur natif garanti du logo et nom de l'exploitant sur la page de connexion",
"Nouvelle modale de confirmation de restauration système alignée sur le thème sombre NexusPBX", "Boîte de connexion allégée en style verre dépoli transparent (Glassmorphism backdrop-blur)",
"Remplacement des popups d'alerte natives du navigateur lors des restaurations locales et téléversées" "Nouveau fond d'écran immersif haute définition (réseau constellation)"
] ]
} }