v1.8.2: Pre-flight check sudoers avec instructions de fix dans l'UI
Sur les anciens capteurs sans regle sudoers NOPASSWD pour /var/www/nebuleair_pro_4g/*, l'update echouait avec un message sudo cryptique. Nouveau: - preflight_sudo_check() en PHP teste 'sudo -n -l <script>' avant de lancer l'update (online ou offline) - Si KO: la route retourne error_type=sudoers_missing avec un message clair et la sortie technique de sudo - L'UI affiche une alerte warning structuree avec etapes numerotees, contenu du fichier /etc/sudoers.d/nebuleair pret a coller, et un bouton 'Copier le contenu' (presse-papier) - Echec immediat (<1s) au lieu d'attendre le timeout du script Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
"versions": [
|
"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",
|
"version": "1.8.1",
|
||||||
"date": "2026-05-12",
|
"date": "2026-05-12",
|
||||||
|
|||||||
@@ -926,7 +926,11 @@ function updateFirmware() {
|
|||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
clearInterval(timerInterval);
|
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();
|
resetUpdateButton();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1088,6 +1092,67 @@ function showUpdateError(message) {
|
|||||||
finalStatus.style.display = 'block';
|
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 = `
|
||||||
|
<h5 class="alert-heading"><i class="bi bi-exclamation-triangle-fill"></i> Configuration sudoers manquante</h5>
|
||||||
|
<p class="mb-2">${escapeHtml(response.message || '')}</p>
|
||||||
|
<p class="mb-1"><strong>Fix (SSH sur le capteur) :</strong></p>
|
||||||
|
<ol class="mb-2">
|
||||||
|
<li>Connectez-vous en SSH au capteur</li>
|
||||||
|
<li>Exécutez <code>sudo nano /etc/sudoers.d/nebuleair</code></li>
|
||||||
|
<li>Collez le contenu ci-dessous puis sauvez (<kbd>Ctrl+O</kbd>, <kbd>Enter</kbd>, <kbd>Ctrl+X</kbd>)</li>
|
||||||
|
<li>Exécutez <code>sudo chmod 0440 /etc/sudoers.d/nebuleair && sudo visudo -c</code></li>
|
||||||
|
<li>Relancez la mise à jour ici</li>
|
||||||
|
</ol>
|
||||||
|
<div class="position-relative">
|
||||||
|
<pre class="bg-light p-2 rounded mb-1" style="font-size: 0.8rem; max-height: 200px; overflow-y: auto;"><code id="sudoersFixContent">${escapeHtml(sudoersContent)}</code></pre>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="copySudoersFix()">
|
||||||
|
<i class="bi bi-clipboard"></i> Copier le contenu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${response.raw ? '<details class="mt-2"><summary class="small text-muted">Sortie technique sudo</summary><pre class="small mt-1 mb-0">' + escapeHtml(response.raw) + '</pre></details>' : ''}
|
||||||
|
`;
|
||||||
|
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, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
function resetUpdateButton() {
|
function resetUpdateButton() {
|
||||||
// Online update button
|
// Online update button
|
||||||
const updateBtn = document.getElementById('updateBtn');
|
const updateBtn = document.getElementById('updateBtn');
|
||||||
@@ -1190,9 +1255,13 @@ function uploadFirmware() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
finalStatus.className = 'alert alert-danger mb-3';
|
if (response.error_type === 'sudoers_missing') {
|
||||||
finalStatus.textContent = 'Erreur: ' + (response.message || 'Erreur inconnue');
|
showSudoersMissingError(response);
|
||||||
finalStatus.style.display = 'block';
|
} else {
|
||||||
|
finalStatus.className = 'alert alert-danger mb-3';
|
||||||
|
finalStatus.textContent = 'Erreur: ' + (response.message || 'Erreur inconnue');
|
||||||
|
finalStatus.style.display = 'block';
|
||||||
|
}
|
||||||
resetUploadUI();
|
resetUploadUI();
|
||||||
showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error');
|
showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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 <cmd>` is a non-interactive check: succeeds only if the user is
|
||||||
|
// allowed to run <cmd> 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.
|
// 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.
|
// Output is written to a temp log file. A 'done' marker file is created when finished.
|
||||||
if ($type == "update_firmware_start") {
|
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';
|
$logFile = '/tmp/nebuleair_firmware_update.log';
|
||||||
$doneFile = '/tmp/nebuleair_firmware_update.done';
|
$doneFile = '/tmp/nebuleair_firmware_update.done';
|
||||||
|
|
||||||
@@ -500,6 +534,19 @@ if ($type == "upload_firmware") {
|
|||||||
exit;
|
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
|
// Check file upload
|
||||||
if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) {
|
if (!isset($_FILES['firmware_file']) || $_FILES['firmware_file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
$max_upload = ini_get('upload_max_filesize');
|
$max_upload = ini_get('upload_max_filesize');
|
||||||
|
|||||||
Reference in New Issue
Block a user