Compare commits
9
Commits
d49520aecb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9aef1ff3e | ||
|
|
bfc71a7763 | ||
|
|
ee5f6b4aa6 | ||
|
|
f99cf440a7 | ||
|
|
08d3996648 | ||
|
|
5cb3900c67 | ||
|
|
56be0ab37a | ||
|
|
64b8339d14 | ||
|
|
ba04af32bb |
No files matched your search
+41
-17
@@ -1802,30 +1802,53 @@ 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'] ?? ''));
|
||||||
|
|
||||||
if ($company_logo !== '' && !preg_match('/^data:image\/(png|jpeg|jpg|webp|svg\+xml|gif);base64,/i', $company_logo)) {
|
foreach (['company_logo' => $company_logo, 'ucp_logo' => $ucp_logo] as $fieldKey => $logoVal) {
|
||||||
throw new InvalidArgumentException("Format d'image invalide (utilisez PNG, SVG, JPG ou WebP).");
|
if (!array_key_exists($fieldKey, $payload)) {
|
||||||
}
|
continue;
|
||||||
if (strlen($company_logo) > 2 * 1024 * 1024) {
|
}
|
||||||
throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo).");
|
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).");
|
||||||
|
}
|
||||||
|
if (strlen($logoVal) > 2 * 1024 * 1024) {
|
||||||
|
throw new InvalidArgumentException("Le logo est trop volumineux (max 1.5 Mo).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('company_name', $payload) && array_key_exists('company_logo', $payload)) {
|
$sets = [];
|
||||||
$stmt = $db->prepare('UPDATE settings SET company_name = :cn, company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
|
$params = [];
|
||||||
$stmt->execute([':cn' => $company_name, ':cl' => $company_logo]);
|
if (array_key_exists('company_name', $payload)) {
|
||||||
} elseif (array_key_exists('company_logo', $payload)) {
|
$sets[] = 'company_name = :cn';
|
||||||
$stmt = $db->prepare('UPDATE settings SET company_logo = :cl, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
|
$params[':cn'] = $company_name;
|
||||||
$stmt->execute([':cl' => $company_logo]);
|
}
|
||||||
} elseif (array_key_exists('company_name', $payload)) {
|
if (array_key_exists('company_logo', $payload)) {
|
||||||
$stmt = $db->prepare('UPDATE settings SET company_name = :cn, updated_at = CURRENT_TIMESTAMP WHERE id = 1');
|
$sets[] = 'company_logo = :cl';
|
||||||
$stmt->execute([':cn' => $company_name]);
|
$params[':cl'] = $company_logo;
|
||||||
|
}
|
||||||
|
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' => $company_logo === '' && !array_key_exists('company_name', $payload)
|
'message' => $msg,
|
||||||
? 'Logo entreprise réinitialisé.'
|
|
||||||
: 'Logo et identité entreprise mis à jour sur NexusPBX !',
|
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -2598,6 +2621,7 @@ 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);
|
||||||
|
|||||||
+84
-30
@@ -243,6 +243,7 @@ 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');
|
||||||
@@ -270,6 +271,21 @@ 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');
|
||||||
@@ -321,7 +337,11 @@ function triggerCompanyLogoPicker() {
|
|||||||
document.getElementById('company-logo-file-input')?.click();
|
document.getElementById('company-logo-file-input')?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCompanyLogoUpload(input) {
|
function triggerUcpLogoPicker() {
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -335,7 +355,7 @@ function handleCompanyLogoUpload(input) {
|
|||||||
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', { company_logo: dataUrl });
|
const res = await apiCall('save_company_branding', { [fieldName]: dataUrl });
|
||||||
showToast(res.message, 'success');
|
showToast(res.message, 'success');
|
||||||
await loadState();
|
await loadState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -347,9 +367,17 @@ function handleCompanyLogoUpload(input) {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearCompanyLogo() {
|
function handleCompanyLogoUpload(input) {
|
||||||
|
uploadBrandingLogo(input, 'company_logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUcpLogoUpload(input) {
|
||||||
|
uploadBrandingLogo(input, 'ucp_logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearBrandingLogo(fieldName) {
|
||||||
try {
|
try {
|
||||||
const res = await apiCall('save_company_branding', { company_logo: '' });
|
const res = await apiCall('save_company_branding', { [fieldName]: '' });
|
||||||
showToast(res.message, 'info');
|
showToast(res.message, 'info');
|
||||||
await loadState();
|
await loadState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -357,6 +385,14 @@ async function clearCompanyLogo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 || [];
|
||||||
@@ -6294,7 +6330,12 @@ 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.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
changelogList.innerHTML = data.changelog.flatMap(item => {
|
||||||
|
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 {
|
||||||
@@ -6376,11 +6417,29 @@ 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');
|
||||||
|
|
||||||
function finalizeUpdateSuccess(newVer) {
|
async function probeUpdateReady(maxAttempts = 6, delayMs = 1500) {
|
||||||
|
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({
|
||||||
@@ -6413,10 +6472,11 @@ async function executeSystemUpdate() {
|
|||||||
btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...';
|
btnModalConfirm.innerHTML = '✓ Mise à jour terminée ! Rechargement...';
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
// Attendre que le reload différé php-fpm/nginx soit terminé avant de recharger la page
|
||||||
closeUpdateConfirmModal();
|
await new Promise(r => setTimeout(r, 2500));
|
||||||
window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now();
|
await probeUpdateReady(5, 1000);
|
||||||
}, 1800);
|
closeUpdateConfirmModal();
|
||||||
|
window.location.href = window.location.origin + window.location.pathname + '?_t=' + Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -6440,18 +6500,15 @@ 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 pour vérifier si la mise à jour a en réalité bien été appliquée sur le disque
|
// on sonde le serveur à plusieurs reprises pour laisser Nginx/PHP-FPM terminer leur reload
|
||||||
await new Promise(r => setTimeout(r, 2000));
|
const probeData = await probeUpdateReady(6, 1500);
|
||||||
try {
|
if (probeData) {
|
||||||
const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
|
await finalizeUpdateSuccess(probeData.current_version);
|
||||||
const probeData = await probeRes.json();
|
return;
|
||||||
if (probeData && probeData.success && probeData.current_version) {
|
}
|
||||||
finalizeUpdateSuccess(probeData.current_version);
|
|
||||||
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);
|
||||||
@@ -6467,17 +6524,14 @@ 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 avant d'alerter si le serveur a quand même réussi à déployer les fichiers
|
// on vérifie à plusieurs reprises si le serveur a quand même réussi à déployer les fichiers
|
||||||
await new Promise(r => setTimeout(r, 2000));
|
const probeData = await probeUpdateReady(6, 1500);
|
||||||
try {
|
if (probeData) {
|
||||||
const probeRes = await fetch('/api.php?action=check_update&_t=' + Date.now());
|
await finalizeUpdateSuccess(probeData.current_version);
|
||||||
const probeData = await probeRes.json();
|
return;
|
||||||
if (probeData && probeData.success && probeData.current_version) {
|
}
|
||||||
finalizeUpdateSuccess(probeData.current_version);
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -1648,6 +1648,7 @@ $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>
|
||||||
@@ -1675,6 +1676,28 @@ $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">
|
||||||
|
|||||||
+12
-5
@@ -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 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 &)
|
(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 &)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NEW_VER="Inconnue"
|
NEW_VER="Inconnue"
|
||||||
@@ -279,10 +279,17 @@ 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"])) {
|
if (!empty($v["changelog"]) && is_array($v["changelog"])) {
|
||||||
foreach ($v["changelog"] as $line) {
|
$first = $v["changelog"][0];
|
||||||
echo " • " . $line . "\n";
|
if (is_array($first) && !empty($first["changes"])) {
|
||||||
|
foreach ($first["changes"] as $line) {
|
||||||
|
echo " • " . $line . "\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($v["changelog"] as $line) {
|
||||||
|
if (is_string($line)) echo " • " . $line . "\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'
|
'
|
||||||
|
|||||||
+4
-1
@@ -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 FROM settings LIMIT 1");
|
self::$instance->query("SELECT ucp_https_port, fail2ban_ucp_maxretry, groundwire_secret, ucp_logo 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,6 +395,9 @@ 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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,12 +95,14 @@ 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' => $email,
|
':email' => $finalEmail,
|
||||||
':grp' => $group,
|
':grp' => $group,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+7
-3
@@ -98,7 +98,9 @@ try {
|
|||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'get_state':
|
case 'get_state':
|
||||||
$branding = $db->query('SELECT company_name, company_logo FROM settings WHERE id = 1 LIMIT 1')->fetch(PDO::FETCH_ASSOC) ?: [];
|
$branding = $db->query('SELECT company_name, company_logo, ucp_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' => [
|
||||||
@@ -110,8 +112,10 @@ try {
|
|||||||
'last_login_at' => $currentUser['last_login_at'],
|
'last_login_at' => $currentUser['last_login_at'],
|
||||||
],
|
],
|
||||||
'branding' => [
|
'branding' => [
|
||||||
'company_name' => (string)($branding['company_name'] ?? ''),
|
'company_name' => (string)($branding['company_name'] ?? ''),
|
||||||
'company_logo' => (string)($branding['company_logo'] ?? ''),
|
'company_logo' => $compLogo,
|
||||||
|
'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),
|
||||||
|
|||||||
+9
-7
@@ -13,9 +13,11 @@ $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 FROM settings WHERE id = 1 LIMIT 1')->fetch(\PDO::FETCH_ASSOC) ?: [];
|
$branding = $db->query('SELECT company_name, company_logo, ucp_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();
|
||||||
@@ -136,8 +138,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 ($companyLogo !== ''): ?>
|
<?php if ($ucpEffectiveLogo !== ''): ?>
|
||||||
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-14 w-auto object-contain mb-3 rounded-xl">
|
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" 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; ?>
|
||||||
@@ -180,8 +182,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 ($companyLogo !== ''): ?>
|
<?php if ($ucpEffectiveLogo !== ''): ?>
|
||||||
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="h-9 w-auto rounded-lg object-contain shrink-0">
|
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" 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; ?>
|
||||||
@@ -430,8 +432,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 ($companyLogo !== ''): ?>
|
<?php if ($ucpEffectiveLogo !== ''): ?>
|
||||||
<img src="<?= htmlspecialchars($companyLogo) ?>" alt="Logo" class="max-h-full max-w-full object-contain">
|
<img src="<?= htmlspecialchars($ucpEffectiveLogo) ?>" 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; ?>
|
||||||
|
|||||||
+6
-9
@@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": "2.6.7",
|
"version": "2.7.3",
|
||||||
"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",
|
||||||
"version": "2.6.7",
|
"Correctif v2.7.2 : formatage uniforme du changelog dans version.json (suppression de [object Object] dans la fiche de mise à jour)",
|
||||||
"date": "2026-11-12",
|
"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)",
|
||||||
"changes": [
|
"Nouveauté v2.7.0 : logo spécifique optionnel pour le portail UCP (thème clair), avec repli automatique sur le logo principal puis NexusPBX",
|
||||||
"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",
|
"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)",
|
||||||
"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)",
|
||||||
|
|||||||
Reference in new issue
Block a user