Compare commits

..
3 Commits
9 changed files with 62 additions and 178 deletions

No files matched your search

+14 -38
View File
@@ -1802,53 +1802,30 @@ try {
case 'save_company_branding': case 'save_company_branding':
$company_name = trim((string)($payload['company_name'] ?? '')); $company_name = trim((string)($payload['company_name'] ?? ''));
$company_logo = trim((string)($payload['company_logo'] ?? '')); $company_logo = trim((string)($payload['company_logo'] ?? ''));
$ucp_logo = trim((string)($payload['ucp_logo'] ?? ''));
foreach (['company_logo' => $company_logo, 'ucp_logo' => $ucp_logo] as $fieldKey => $logoVal) { if ($company_logo !== '' && !preg_match('/^data:image\/(png|jpeg|jpg|webp|svg\+xml|gif);base64,/i', $company_logo)) {
if (!array_key_exists($fieldKey, $payload)) {
continue;
}
if ($logoVal !== '' && !preg_match('/^data:image\/(png|jpeg|jpg|webp|svg\+xml|gif);base64,/i', $logoVal)) {
throw new InvalidArgumentException("Format d'image invalide (utilisez PNG, SVG, JPG ou WebP)."); throw new InvalidArgumentException("Format d'image invalide (utilisez PNG, SVG, JPG ou WebP).");
} }
if (strlen($logoVal) > 2 * 1024 * 1024) { if (strlen($company_logo) > 2 * 1024 * 1024) {
throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo)."); throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo).");
} }
}
$sets = []; if (array_key_exists('company_name', $payload) && array_key_exists('company_logo', $payload)) {
$params = []; $stmt = $db->prepare('UPDATE settings SET company_name = :cn, company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
if (array_key_exists('company_name', $payload)) { $stmt->execute([':cn' => $company_name, ':cl' => $company_logo]);
$sets[] = 'company_name = :cn'; } elseif (array_key_exists('company_logo', $payload)) {
$params[':cn'] = $company_name; $stmt = $db->prepare('UPDATE settings SET company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
} $stmt->execute([':cl' => $company_logo]);
if (array_key_exists('company_logo', $payload)) { } elseif (array_key_exists('company_name', $payload)) {
$sets[] = 'company_logo = :cl'; $stmt = $db->prepare('UPDATE settings SET company_name = :cn, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
$params[':cl'] = $company_logo; $stmt->execute([':cn' => $company_name]);
}
if (array_key_exists('ucp_logo', $payload)) {
$sets[] = 'ucp_logo = :ul';
$params[':ul'] = $ucp_logo;
}
if ($sets) {
$sets[] = 'updated_at = CURRENT_TIMESTAMP';
$stmt = $db->prepare('UPDATE settings SET ' . implode(', ', $sets) . ' WHERE id = 1');
$stmt->execute($params);
}
$msg = 'Logo et identité entreprise mis à jour sur NexusPBX !';
if (array_key_exists('ucp_logo', $payload) && !array_key_exists('company_logo', $payload) && !array_key_exists('company_name', $payload)) {
$msg = $ucp_logo === ''
? 'Logo UCP réinitialisé (hérite du logo principal).'
: 'Logo spécifique UCP mis à jour !';
} elseif (array_key_exists('company_logo', $payload) && $company_logo === '' && !array_key_exists('company_name', $payload) && !array_key_exists('ucp_logo', $payload)) {
$msg = 'Logo entreprise réinitialisé.';
} }
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'message' => $msg, 'message' => $company_logo === '' && !array_key_exists('company_name', $payload)
? 'Logo entreprise réinitialisé.'
: 'Logo et identité entreprise mis à jour sur NexusPBX !',
]); ]);
break; break;
@@ -2621,7 +2598,6 @@ try {
break; break;
case 'run_update': case 'run_update':
@set_time_limit(300);
$output = []; $output = [];
$code = 0; $code = 0;
exec("sudo /var/www/nexus-pbx/scripts/update.sh 2>&1", $output, $code); exec("sudo /var/www/nexus-pbx/scripts/update.sh 2>&1", $output, $code);
+24 -78
View File
@@ -243,7 +243,6 @@ function renderAll() {
function renderCompanyBranding() { function renderCompanyBranding() {
const s = appState.settings || {}; const s = appState.settings || {};
const logo = (s.company_logo || '').trim(); const logo = (s.company_logo || '').trim();
const ucpLogo = (s.ucp_logo || '').trim();
const name = (s.company_name || '').trim(); const name = (s.company_name || '').trim();
const emptyBox = document.getElementById('sidebar-company-empty'); const emptyBox = document.getElementById('sidebar-company-empty');
@@ -271,21 +270,6 @@ function renderCompanyBranding() {
} }
} }
const ucpPreviewImg = document.getElementById('settings-ucp-preview-img');
const ucpPreviewEmpty = document.getElementById('settings-ucp-preview-empty');
if (ucpPreviewImg && ucpPreviewEmpty) {
if (ucpLogo) {
ucpPreviewImg.src = ucpLogo;
ucpPreviewImg.classList.remove('hidden');
ucpPreviewEmpty.classList.add('hidden');
} else {
ucpPreviewImg.src = '';
ucpPreviewImg.classList.add('hidden');
ucpPreviewEmpty.textContent = logo ? 'Hérite du logo principal' : 'NexusPBX par défaut';
ucpPreviewEmpty.classList.remove('hidden');
}
}
if (logo || name) { if (logo || name) {
emptyBox?.classList.add('hidden'); emptyBox?.classList.add('hidden');
filledBox?.classList.remove('hidden'); filledBox?.classList.remove('hidden');
@@ -337,11 +321,7 @@ function triggerCompanyLogoPicker() {
document.getElementById('company-logo-file-input')?.click(); document.getElementById('company-logo-file-input')?.click();
} }
function triggerUcpLogoPicker() { function handleCompanyLogoUpload(input) {
document.getElementById('ucp-logo-file-input')?.click();
}
function uploadBrandingLogo(input, fieldName) {
const file = input?.files?.[0]; const file = input?.files?.[0];
if (!file) return; if (!file) return;
@@ -355,7 +335,7 @@ function uploadBrandingLogo(input, fieldName) {
reader.onload = async (ev) => { reader.onload = async (ev) => {
const dataUrl = String(ev.target?.result || ''); const dataUrl = String(ev.target?.result || '');
try { try {
const res = await apiCall('save_company_branding', { [fieldName]: dataUrl }); const res = await apiCall('save_company_branding', { company_logo: dataUrl });
showToast(res.message, 'success'); showToast(res.message, 'success');
await loadState(); await loadState();
} catch (err) { } catch (err) {
@@ -367,17 +347,9 @@ function uploadBrandingLogo(input, fieldName) {
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
function handleCompanyLogoUpload(input) { async function clearCompanyLogo() {
uploadBrandingLogo(input, 'company_logo');
}
function handleUcpLogoUpload(input) {
uploadBrandingLogo(input, 'ucp_logo');
}
async function clearBrandingLogo(fieldName) {
try { try {
const res = await apiCall('save_company_branding', { [fieldName]: '' }); const res = await apiCall('save_company_branding', { company_logo: '' });
showToast(res.message, 'info'); showToast(res.message, 'info');
await loadState(); await loadState();
} catch (err) { } catch (err) {
@@ -385,14 +357,6 @@ async function clearBrandingLogo(fieldName) {
} }
} }
function clearCompanyLogo() {
clearBrandingLogo('company_logo');
}
function clearUcpLogo() {
clearBrandingLogo('ucp_logo');
}
function renderSummaryCards() { function renderSummaryCards() {
const exts = appState.extensions || []; const exts = appState.extensions || [];
const grps = appState.groups || []; const grps = appState.groups || [];
@@ -6330,12 +6294,7 @@ async function checkSystemUpdate(notify = false) {
if (pingDot) pingDot.classList.remove('hidden'); if (pingDot) pingDot.classList.remove('hidden');
if (changelogList && Array.isArray(data.changelog) && data.changelog.length > 0) { if (changelogList && Array.isArray(data.changelog) && data.changelog.length > 0) {
changelogList.innerHTML = data.changelog.flatMap(item => { changelogList.innerHTML = data.changelog.map(item => `<li>${escapeHtml(item)}</li>`).join('');
if (item && typeof item === 'object' && Array.isArray(item.changes)) {
return item.changes.map(c => `<li>${escapeHtml(String(c))}</li>`);
}
return [`<li>${escapeHtml(String(item ?? ''))}</li>`];
}).join('');
if (changelogContainer) changelogContainer.classList.remove('hidden'); if (changelogContainer) changelogContainer.classList.remove('hidden');
} }
} else { } else {
@@ -6417,29 +6376,11 @@ async function executeSystemUpdate() {
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');
// Pause du polling métriques pour éviter un 502 Nginx pendant le reload php-fpm/nginx
stopMetricsPolling();
// Purge préventive du cache local // Purge préventive du cache local
localStorage.removeItem('nexus_update_last_check_ts'); localStorage.removeItem('nexus_update_last_check_ts');
localStorage.removeItem('nexus_update_cached_data'); localStorage.removeItem('nexus_update_cached_data');
async function probeUpdateReady(maxAttempts = 6, delayMs = 1500) { function finalizeUpdateSuccess(newVer) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, delayMs));
try {
const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
if (!probeRes.ok) continue;
const probeData = await probeRes.json();
if (probeData && probeData.success && probeData.current_version) {
return probeData;
}
} catch (_) {}
}
return null;
}
async function finalizeUpdateSuccess(newVer) {
newVer = String(newVer || '').replace(/^v/, ''); newVer = String(newVer || '').replace(/^v/, '');
localStorage.setItem('nexus_update_last_check_ts', Date.now().toString()); localStorage.setItem('nexus_update_last_check_ts', Date.now().toString());
localStorage.setItem('nexus_update_cached_data', JSON.stringify({ localStorage.setItem('nexus_update_cached_data', JSON.stringify({
@@ -6472,11 +6413,10 @@ async function executeSystemUpdate() {
btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...'; btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...';
} }
// Attendre que le reload différé php-fpm/nginx soit terminé avant de recharger la page setTimeout(() => {
await new Promise(r => setTimeout(r, 2500));
await probeUpdateReady(5, 1000);
closeUpdateConfirmModal(); closeUpdateConfirmModal();
window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now(); window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now();
}, 1800);
} }
try { try {
@@ -6500,15 +6440,18 @@ async function executeSystemUpdate() {
} }
// Si la réponse n'était pas un JSON valide ou a été interrompue par le rechargement de php-fpm/nginx, // Si la réponse n'était pas un JSON valide ou a été interrompue par le rechargement de php-fpm/nginx,
// on sonde le serveur à plusieurs reprises pour laisser Nginx/PHP-FPM terminer leur reload // on sonde le serveur pour vérifier si la mise à jour a en réalité bien été appliquée sur le disque
const probeData = await probeUpdateReady(6, 1500); await new Promise(r => setTimeout(r, 2000));
if (probeData) { try {
await finalizeUpdateSuccess(probeData.current_version); const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
const probeData = await probeRes.json();
if (probeData && probeData.success && probeData.current_version) {
finalizeUpdateSuccess(probeData.current_version);
return; return;
} }
} catch (_) {}
// En cas d'échec réel avéré // En cas d'échec réel avéré
startMetricsPolling();
closeUpdateConfirmModal(); closeUpdateConfirmModal();
const errMsg = (data && (data.output || data.message)) ? (data.output || data.message) : (rawText.slice(0, 150) || "Erreur inconnue"); const errMsg = (data && (data.output || data.message)) ? (data.output || data.message) : (rawText.slice(0, 150) || "Erreur inconnue");
alert("Erreur lors de la mise à jour :\n\n" + errMsg); alert("Erreur lors de la mise à jour :\n\n" + errMsg);
@@ -6524,14 +6467,17 @@ async function executeSystemUpdate() {
} catch (e) { } catch (e) {
// En cas d'interruption réseau du socket (Safari WebKit DOMException ou timeout), // En cas d'interruption réseau du socket (Safari WebKit DOMException ou timeout),
// on vérifie à plusieurs reprises si le serveur a quand même réussi à déployer les fichiers // on vérifie avant d'alerter si le serveur a quand même réussi à déployer les fichiers
const probeData = await probeUpdateReady(6, 1500); await new Promise(r => setTimeout(r, 2000));
if (probeData) { try {
await finalizeUpdateSuccess(probeData.current_version); const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
const probeData = await probeRes.json();
if (probeData && probeData.success && probeData.current_version) {
finalizeUpdateSuccess(probeData.current_version);
return; return;
} }
} catch (_) {}
startMetricsPolling();
closeUpdateConfirmModal(); closeUpdateConfirmModal();
alert("Erreur réseau lors de la mise à jour : " + (e.message || "Connexion interrompue")); alert("Erreur réseau lors de la mise à jour : " + (e.message || "Connexion interrompue"));
if (btnRun) { if (btnRun) {
-23
View File
@@ -1648,7 +1648,6 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
<form id="settings-form" class="space-y-4"> <form id="settings-form" class="space-y-4">
<!-- Identité & Logo de l'Exploitant --> <!-- Identité & Logo de l'Exploitant -->
<input type="file" id="company-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleCompanyLogoUpload(this)"> <input type="file" id="company-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleCompanyLogoUpload(this)">
<input type="file" id="ucp-logo-file-input" accept="image/png,image/svg+xml,image/jpeg,image/webp,image/gif" class="hidden" onchange="handleUcpLogoUpload(this)">
<div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3.5"> <div class="p-4 rounded-xl bg-anthracite-950 border border-anthracite-700 space-y-3.5">
<div class="flex items-center justify-between pb-2 border-b border-anthracite-700/60"> <div class="flex items-center justify-between pb-2 border-b border-anthracite-700/60">
<div> <div>
@@ -1676,28 +1675,6 @@ $isDevEnv = file_exists(__DIR__ . '/../.dev')
class="w-full px-3 py-2 rounded-lg bg-anthracite-900 border border-anthracite-700 text-sm text-white"> class="w-full px-3 py-2 rounded-lg bg-anthracite-900 border border-anthracite-700 text-sm text-white">
</div> </div>
</div> </div>
<!-- Logo spécifique UCP (optionnel, fond clair) -->
<div class="pt-3 border-t border-anthracite-800 flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-3 min-w-0">
<div id="settings-ucp-preview-slot" class="w-32 h-12 rounded-lg bg-white/95 border border-dashed border-slate-300 flex items-center justify-center p-1.5 shrink-0">
<img id="settings-ucp-preview-img" src="" alt="Aperçu Logo UCP" class="max-h-9 max-w-full object-contain hidden">
<span id="settings-ucp-preview-empty" class="text-[10px] text-slate-500 italic text-center leading-tight">Hérite du logo principal</span>
</div>
<div class="min-w-0">
<div class="text-xs font-semibold text-slate-200">Logo UCP (Portail Utilisateur — optionnel)</div>
<p class="text-[11px] text-slate-400">Si vide, l'UCP réutilise le logo principal (ou NexusPBX). Utile pour importer un logo foncé adapté au thème clair UCP.</p>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<button type="button" onclick="triggerUcpLogoPicker()" class="px-3 py-1.5 rounded-lg bg-slate-700 hover:bg-slate-600 text-xs text-white font-semibold transition">
Importer Logo UCP
</button>
<button type="button" onclick="clearUcpLogo()" class="px-2.5 py-1.5 rounded-lg bg-anthracite-800 hover:bg-red-500/20 border border-anthracite-700 text-xs text-slate-300 hover:text-red-300 transition">
Effacer
</button>
</div>
</div>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+4 -11
View File
@@ -261,7 +261,7 @@ if [ -t 1 ]; then
systemctl reload nginx 2>/dev/null || true systemctl reload nginx 2>/dev/null || true
fail2ban-client reload >/dev/null 2>&1 || true fail2ban-client reload >/dev/null 2>&1 || true
else else
(nohup bash -c "sleep 1 && (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; fail2ban-client reload >/dev/null 2>&1 || true" </dev/null >/dev/null 2>&1 &) (nohup bash -c "sleep 2 && (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; fail2ban-client reload >/dev/null 2>&1 || true" >/dev/null 2>&1 &)
fi fi
NEW_VER="Inconnue" NEW_VER="Inconnue"
@@ -279,17 +279,10 @@ echo ""
if [ -f "$APP_DIR/version.json" ]; then if [ -f "$APP_DIR/version.json" ]; then
echo -e "${C_CYAN}${C_BOLD}Nouveautés de cette version :${C_RESET}" echo -e "${C_CYAN}${C_BOLD}Nouveautés de cette version :${C_RESET}"
php -r ' php -r '
$v = json_decode(@file_get_contents("/var/www/nexus-pbx/version.json"), true); $v = json_decode(file_get_contents("/var/www/nexus-pbx/version.json"), true);
if (!empty($v["changelog"]) && is_array($v["changelog"])) { if (!empty($v["changelog"])) {
$first = $v["changelog"][0];
if (is_array($first) && !empty($first["changes"])) {
foreach ($first["changes"] as $line) {
echo " • " . $line . "\n";
}
} else {
foreach ($v["changelog"] as $line) { foreach ($v["changelog"] as $line) {
if (is_string($line)) echo " • " . $line . "\n"; echo " • " . $line . "\n";
}
} }
} }
' '
+1 -4
View File
@@ -48,7 +48,7 @@ class Database
// Si la base existe déjà, éviter d'exécuter l'intégralité du DDL à chaque requête HTTP // Si la base existe déjà, éviter d'exécuter l'intégralité du DDL à chaque requête HTTP
// ce qui sature SQLite de verrous exclusifs inutiles. On s'assure des colonnes requises. // ce qui sature SQLite de verrous exclusifs inutiles. On s'assure des colonnes requises.
try { try {
self::$instance->query("SELECT ucp_https_port, fail2ban_ucp_maxretry, groundwire_secret, ucp_logo FROM settings LIMIT 1"); self::$instance->query("SELECT ucp_https_port, fail2ban_ucp_maxretry, groundwire_secret FROM settings LIMIT 1");
self::$instance->query("SELECT groundwire_enabled FROM extensions LIMIT 1"); self::$instance->query("SELECT groundwire_enabled FROM extensions LIMIT 1");
self::$instance->query("SELECT id FROM ucp_users LIMIT 1"); self::$instance->query("SELECT id FROM ucp_users LIMIT 1");
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -395,9 +395,6 @@ class Database
if (!in_array('company_logo', $existing, true)) { if (!in_array('company_logo', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN company_logo TEXT NOT NULL DEFAULT ''"); $db->exec("ALTER TABLE settings ADD COLUMN company_logo TEXT NOT NULL DEFAULT ''");
} }
if (!in_array('ucp_logo', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN ucp_logo TEXT NOT NULL DEFAULT ''");
}
if (!in_array('default_moh_sound_id', $existing, true)) { if (!in_array('default_moh_sound_id', $existing, true)) {
$db->exec("ALTER TABLE settings ADD COLUMN default_moh_sound_id INTEGER DEFAULT 2"); $db->exec("ALTER TABLE settings ADD COLUMN default_moh_sound_id INTEGER DEFAULT 2");
} }
+1 -3
View File
@@ -95,14 +95,12 @@ class PhonebookManager
$existing = $findStmt->fetch(PDO::FETCH_ASSOC); $existing = $findStmt->fetch(PDO::FETCH_ASSOC);
if ($existing) { if ($existing) {
$existingEmail = trim((string)($existing['email'] ?? ''));
$finalEmail = $existingEmail !== '' ? $existingEmail : $email;
$updateStmt->execute([ $updateStmt->execute([
':id' => (int)$existing['id'], ':id' => (int)$existing['id'],
':ext_id' => $extId, ':ext_id' => $extId,
':name' => $extName, ':name' => $extName,
':num' => $extNum, ':num' => $extNum,
':email' => $finalEmail, ':email' => $email,
':grp' => $group, ':grp' => $group,
]); ]);
} else { } else {
+2 -6
View File
@@ -98,9 +98,7 @@ try {
switch ($action) { switch ($action) {
case 'get_state': case 'get_state':
$branding = $db->query('SELECT company_name, company_logo, ucp_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: []; $branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: [];
$compLogo = trim((string)($branding['company_logo'] ?? ''));
$ucpLogo = trim((string)($branding['ucp_logo'] ?? ''));
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'data' => [ 'data' => [
@@ -113,9 +111,7 @@ try {
], ],
'branding' => [ 'branding' => [
'company_name' => (string)($branding['company_name'] ?? ''), 'company_name' => (string)($branding['company_name'] ?? ''),
'company_logo' => $compLogo, 'company_logo' => (string)($branding['company_logo'] ?? ''),
'ucp_logo' => $ucpLogo,
'effective_logo' => $ucpLogo !== '' ? $ucpLogo : $compLogo,
], ],
'csrf_token' => UcpAuth::getCsrfToken(), 'csrf_token' => UcpAuth::getCsrfToken(),
'extensions' => UcpManager::getExtensionsState($db, $allowedExts, $ucpUserId), 'extensions' => UcpManager::getExtensionsState($db, $allowedExts, $ucpUserId),
+7 -9
View File
@@ -13,11 +13,9 @@ $ucpEnabled = UcpAuth::isUcpEnabled($db);
$currentUser = $ucpEnabled ? UcpAuth::getCurrentUser($db) : null; $currentUser = $ucpEnabled ? UcpAuth::getCurrentUser($db) : null;
$isAuth = ($currentUser !== null); $isAuth = ($currentUser !== null);
$branding = $db->query('SELECT company_name, company_logo, ucp_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(\PDO::FETCH_ASSOC) ?: []; $branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(\PDO::FETCH_ASSOC) ?: [];
$companyName = trim((string)($branding['company_name'] ?? '')); $companyName = trim((string)($branding['company_name'] ?? ''));
$companyLogo = trim((string)($branding['company_logo'] ?? '')); $companyLogo = trim((string)($branding['company_logo'] ?? ''));
$ucpLogo = trim((string)($branding['ucp_logo'] ?? ''));
$ucpEffectiveLogo = $ucpLogo !== '' ? $ucpLogo : $companyLogo;
$appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : []; $appVerData = file_exists(__DIR__ . '/../version.json') ? json_decode((string)file_get_contents(__DIR__ . '/../version.json'), true) : [];
$appVersion = $appVerData['version'] ?? '2.5.0'; $appVersion = $appVerData['version'] ?? '2.5.0';
$csrfToken = UcpAuth::getCsrfToken(); $csrfToken = UcpAuth::getCsrfToken();
@@ -138,8 +136,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<div class="absolute inset-0 bg-white/10"></div> <div class="absolute inset-0 bg-white/10"></div>
<div class="relative z-10 w-full max-w-md bg-white/85 backdrop-blur-xl rounded-3xl p-8 shadow-xl shadow-indigo-900/10 border border-white/80"> <div class="relative z-10 w-full max-w-md bg-white/85 backdrop-blur-xl rounded-3xl p-8 shadow-xl shadow-indigo-900/10 border border-white/80">
<div class="flex flex-col items-center text-center mb-7"> <div class="flex flex-col items-center text-center mb-7">
<?php if ($ucpEffectiveLogo !== ''): ?> <?php if ($companyLogo !== ''): ?>
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="h-14 w-auto object-contain mb-3 rounded-xl"> <img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-14 w-auto object-contain mb-3 rounded-xl">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-14 w-auto object-contain mb-3"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-14 w-auto object-contain mb-3">
<?php endif; ?> <?php endif; ?>
@@ -182,8 +180,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<header class="sticky top-0 z-30 bg-white/95 backdrop-blur border-b border-slate-200"> <header class="sticky top-0 z-30 bg-white/95 backdrop-blur border-b border-slate-200">
<div class="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-4"> <div class="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-4">
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
<?php if ($ucpEffectiveLogo !== ''): ?> <?php if ($companyLogo !== ''): ?>
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="h-9 w-auto rounded-lg object-contain shrink-0"> <img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-9 w-auto rounded-lg object-contain shrink-0">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-9 w-auto object-contain shrink-0"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="h-9 w-auto object-contain shrink-0">
<?php endif; ?> <?php endif; ?>
@@ -432,8 +430,8 @@ $csrfToken = UcpAuth::getCsrfToken();
<div class="ucp-print-avoid-break ucp-print-row rounded-2xl bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 text-white p-5 sm:p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div class="ucp-print-avoid-break ucp-print-row rounded-2xl bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 text-white p-5 sm:p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4 min-w-0"> <div class="flex items-center gap-4 min-w-0">
<div class="h-14 w-14 sm:h-16 sm:w-16 rounded-2xl bg-white p-2 flex items-center justify-center shrink-0 shadow-md"> <div class="h-14 w-14 sm:h-16 sm:w-16 rounded-2xl bg-white p-2 flex items-center justify-center shrink-0 shadow-md">
<?php if ($ucpEffectiveLogo !== ''): ?> <?php if ($companyLogo !== ''): ?>
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain"> <img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain">
<?php else: ?> <?php else: ?>
<img src="/assets/logo.png?v=1" alt="NexusPBX" class="max-h-full max-w-full object-contain"> <img src="/assets/logo.png?v=1" alt="NexusPBX" class="max-h-full max-w-full object-contain">
<?php endif; ?> <?php endif; ?>
+9 -6
View File
@@ -1,14 +1,17 @@
{ {
"version": "2.7.3", "version": "2.6.7",
"codename": "Pulsar UCP", "codename": "Pulsar UCP",
"release_date": "2026-11-12", "release_date": "2026-11-12",
"min_php_version": "8.2", "min_php_version": "8.2",
"changelog": [ "changelog": [
"Correctif v2.7.3 : conservation de l'adresse email renseignée sur un contact interne de l'Annuaire lors de la synchronisation des postes", {
"Correctif v2.7.2 : formatage uniforme du changelog dans version.json (suppression de [object Object] dans la fiche de mise à jour)", "version": "2.6.7",
"Correctif v2.7.1 : suppression de l'erreur Nginx 502 transitoire après mise à jour 1-Clic (pause du polling métriques, sonde multi-essais avant rechargement)", "date": "2026-11-12",
"Nouveauté v2.7.0 : logo spécifique optionnel pour le portail UCP (thème clair), avec repli automatique sur le logo principal puis NexusPBX", "changes": [
"UI v2.6.7 : remplacement de l'ensemble des icônes emoji par des icônes vectorielles SVG uniformes sur Admin & UCP (sans modifier le menu latéral Admin)", "UI Admin & UCP : Remplacement de l'ensemble des icônes emoji par des icônes vectorielles SVG uniformes (style Heroicons), sans modifier le menu latéral Admin",
"Correction d'un problème de syntaxe JS sur le badge d'avertissement TLS dans app.js"
]
},
"UI v2.6.6 : menu déroulant personnalisé sombre avec icônes SVG Heroicons pour toutes les destinations d'appel (sans emojis natifs)", "UI v2.6.6 : menu déroulant personnalisé sombre avec icônes SVG Heroicons pour toutes les destinations d'appel (sans emojis natifs)",
"UI v2.6.5 : refonte des intitulés de destinations audio (Musique d'attente & Annonce vocale en boucle continue), nouveau wallpaper clair UCP et placeholder générique", "UI v2.6.5 : refonte des intitulés de destinations audio (Musique d'attente & Annonce vocale en boucle continue), nouveau wallpaper clair UCP et placeholder générique",
"Correctif v2.6.4 : prise en charge multi-groupes dans l'annuaire UCP & Admin (un poste appartenant à plusieurs groupes apparaît bien dans chacun des onglets correspondants)", "Correctif v2.6.4 : prise en charge multi-groupes dans l'annuaire UCP & Admin (un poste appartenant à plusieurs groupes apparaît bien dans chacun des onglets correspondants)",