diff --git a/VERSION b/VERSION index a8fdfda..53adb84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.1 +1.8.2 diff --git a/changelog.json b/changelog.json index 5fc5b67..66edd23 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,24 @@ { "versions": [ + { + "version": "1.8.2", + "date": "2026-05-12", + "changes": { + "features": [ + "Pre-flight check sudoers avant lancement de l'update (online et offline): détecte les capteurs sans règle NOPASSWD et affiche une alerte claire avec la commande de fix", + "Bouton 'Copier le contenu' pour le bloc sudoers à coller (presse-papier)" + ], + "improvements": [ + "Détection précoce: l'erreur sudo apparaît immédiatement (en < 1s) au lieu d'attendre l'échec du script en background", + "Message d'erreur user-friendly avec étapes numérotées au lieu de l'erreur cryptique de sudo" + ], + "fixes": [], + "compatibility": [ + "Aucun impact sur les capteurs sains: si sudo NOPASSWD est correctement configuré, le pre-flight passe en <100ms" + ] + }, + "notes": "Sur les anciens capteurs installés avant l'ajout de la règle sudoers /var/www/nebuleair_pro_4g/* dans installation_part1.sh, l'update via web UI était silencieusement cassé. Désormais l'UI explique exactement quoi faire en SSH pour réparer." + }, { "version": "1.8.1", "date": "2026-05-12", diff --git a/html/admin.html b/html/admin.html index bc70e6c..cb24e89 100755 --- a/html/admin.html +++ b/html/admin.html @@ -926,7 +926,11 @@ function updateFirmware() { success: function(response) { if (!response.success) { clearInterval(timerInterval); - showUpdateError('Impossible de lancer la mise à jour: ' + (response.error || 'erreur inconnue')); + if (response.error_type === 'sudoers_missing') { + showSudoersMissingError(response); + } else { + showUpdateError('Impossible de lancer la mise à jour: ' + (response.message || response.error || 'erreur inconnue')); + } resetUpdateButton(); return; } @@ -1088,6 +1092,67 @@ function showUpdateError(message) { finalStatus.style.display = 'block'; } +// Specific error display for the "sudoers missing" pre-flight failure. +// Shows a clear explanation and the exact SSH commands to apply the fix, +// with a copy-to-clipboard button so the user can paste it on the sensor. +function showSudoersMissingError(response) { + const finalStatus = document.getElementById('updateFinalStatus'); + const fixCommand = `sudo nano /etc/sudoers.d/nebuleair +# Then paste the content shown below, save (Ctrl+O, Enter, Ctrl+X), then: +sudo chmod 0440 /etc/sudoers.d/nebuleair +sudo visudo -c`; + + const sudoersContent = `# NebuleAir Pro 4G sudo rules +ALL ALL=(ALL) NOPASSWD: /usr/bin/nmcli, /usr/sbin/reboot +www-data ALL=(ALL) NOPASSWD: /usr/bin/git pull +www-data ALL=(ALL) NOPASSWD: /usr/bin/ssh +www-data ALL=(ALL) NOPASSWD: /usr/bin/python3 * +www-data ALL=(ALL) NOPASSWD: /bin/systemctl * +www-data ALL=(ALL) NOPASSWD: /usr/bin/pkill * +www-data ALL=(ALL) NOPASSWD: /var/www/nebuleair_pro_4g/*`; + + finalStatus.className = 'alert alert-warning mb-3'; + finalStatus.innerHTML = ` +
Configuration sudoers manquante
+

${escapeHtml(response.message || '')}

+

Fix (SSH sur le capteur) :

+
    +
  1. Connectez-vous en SSH au capteur
  2. +
  3. Exécutez sudo nano /etc/sudoers.d/nebuleair
  4. +
  5. Collez le contenu ci-dessous puis sauvez (Ctrl+O, Enter, Ctrl+X)
  6. +
  7. Exécutez sudo chmod 0440 /etc/sudoers.d/nebuleair && sudo visudo -c
  8. +
  9. Relancez la mise à jour ici
  10. +
+
+
${escapeHtml(sudoersContent)}
+ +
+ ${response.raw ? '
Sortie technique sudo
' + escapeHtml(response.raw) + '
' : ''} + `; + finalStatus.style.display = 'block'; +} + +function copySudoersFix() { + const content = document.getElementById('sudoersFixContent').textContent; + navigator.clipboard.writeText(content).then(() => { + showToast('Contenu copié dans le presse-papier', 'success'); + }).catch(() => { + showToast('Échec de la copie — sélectionnez et copiez manuellement', 'warning'); + }); +} + +function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function resetUpdateButton() { // Online update button const updateBtn = document.getElementById('updateBtn'); @@ -1190,9 +1255,13 @@ function uploadFirmware() { } if (!response.success) { - finalStatus.className = 'alert alert-danger mb-3'; - finalStatus.textContent = 'Erreur: ' + (response.message || 'Erreur inconnue'); - finalStatus.style.display = 'block'; + if (response.error_type === 'sudoers_missing') { + showSudoersMissingError(response); + } else { + finalStatus.className = 'alert alert-danger mb-3'; + finalStatus.textContent = 'Erreur: ' + (response.message || 'Erreur inconnue'); + finalStatus.style.display = 'block'; + } resetUploadUI(); showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error'); return; diff --git a/html/launcher.php b/html/launcher.php index a782502..183672f 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -428,9 +428,43 @@ if ($type == "update_firmware") { ]); } +// Pre-flight: check that www-data can sudo the firmware update script without a password. +// On older sensors the sudoers rule for /var/www/nebuleair_pro_4g/* may be missing, +// in which case sudo would block on a password prompt and the update silently fails. +// Returns ['ok' => true] on success, or ['ok' => false, 'message' => ...] with a fix hint. +function preflight_sudo_check($scriptPath) { + // `sudo -n -l ` is a non-interactive check: succeeds only if the user is + // allowed to run via sudo WITHOUT a password. Doesn't actually run anything. + exec('sudo -n -l ' . escapeshellarg($scriptPath) . ' 2>&1', $out, $rc); + if ($rc === 0) return ['ok' => true]; + + $output = implode("\n", $out); + $msg = "Configuration sudoers manquante sur ce capteur (www-data n'a pas le droit d'exécuter le script de mise à jour sans mot de passe). " + . "Voir l'erreur ci-dessous pour appliquer le fix."; + return [ + 'ok' => false, + 'error_type' => 'sudoers_missing', + 'message' => $msg, + 'raw' => $output + ]; +} + // Start firmware update in background, returns immediately so the UI can poll progress. // Output is written to a temp log file. A 'done' marker file is created when finished. if ($type == "update_firmware_start") { + // Pre-flight: fail fast with a clear message if sudoers is misconfigured + $preflight = preflight_sudo_check('/var/www/nebuleair_pro_4g/update_firmware.sh'); + if (!$preflight['ok']) { + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'error_type' => $preflight['error_type'], + 'message' => $preflight['message'], + 'raw' => $preflight['raw'] + ]); + exit; + } + $logFile = '/tmp/nebuleair_firmware_update.log'; $doneFile = '/tmp/nebuleair_firmware_update.done'; @@ -500,6 +534,19 @@ if ($type == "upload_firmware") { exit; } + // Pre-flight sudoers check: fail fast before the user uploads the ZIP only + // to discover their sensor has no sudo NOPASSWD rule. + $preflight = preflight_sudo_check('/var/www/nebuleair_pro_4g/update_firmware_from_file.sh'); + if (!$preflight['ok']) { + echo json_encode([ + 'success' => false, + 'error_type' => $preflight['error_type'], + 'message' => $preflight['message'], + 'raw' => $preflight['raw'] + ]); + exit; + } + // Check file upload if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) { $max_upload = ini_get('upload_max_filesize');