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) :
+
+ - Connectez-vous en SSH au capteur
+ - Exécutez
sudo nano /etc/sudoers.d/nebuleair
+ - Collez le contenu ci-dessous puis sauvez (Ctrl+O, Enter, Ctrl+X)
+ - Exécutez
sudo chmod 0440 /etc/sudoers.d/nebuleair && sudo visudo -c
+ - Relancez la mise à jour ici
+
+
+
${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');