From 11ac2b184a6546f7a7b8de6739af6a181e35b60c Mon Sep 17 00:00:00 2001 From: PaulVua Date: Tue, 12 May 2026 18:00:10 +0200 Subject: [PATCH] v1.8.1: Alignement upload offline sur le flow online (UX + self-heal) L'upload offline avait deux defauts vs l'update online: - pas de self-heal des services (pas de Step 3c equivalent) - ancienne UX synchrone (spinner sans feedback pendant 60-90s) Maintenant: - update_firmware_from_file.sh: nouveau Step 4c qui appelle setup_services.sh (alignement avec online) - launcher.php upload_firmware: lance le script en background et reutilise le mecanisme log/done de l'update online - admin.html uploadFirmware: apres l'upload du ZIP, bascule sur le meme systeme de polling/progress que l'online (avec mapping d'etapes specifique au script offline) - Detection de fin par substring 'completed successfully!' (matche les 2 markers finaux differents) Fix au passage: le bouton 'Upload & Install' restait bloque sur 'Installing...' apres succes. Co-Authored-By: Claude Opus 4.7 (1M context) --- VERSION | 2 +- changelog.json | 23 ++++++ html/admin.html | 139 +++++++++++++++++++++++++---------- html/launcher.php | 28 +++++-- update_firmware_from_file.sh | 15 ++++ 5 files changed, 158 insertions(+), 49 deletions(-) diff --git a/VERSION b/VERSION index 27f9cd3..a8fdfda 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.0 +1.8.1 diff --git a/changelog.json b/changelog.json index a4ddf87..5fc5b67 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,28 @@ { "versions": [ + { + "version": "1.8.1", + "date": "2026-05-12", + "changes": { + "features": [ + "Upload offline (ZIP): même UX live que l'update online (progress bar dynamique, label étape, timer, logs techniques repliables)", + "Upload offline: self-heal via Step 4c qui appelle setup_services.sh (alignement complet avec l'online)" + ], + "improvements": [ + "Frontend: mapping des étapes spécifique au mode (UPDATE_STEPS_ONLINE / UPDATE_STEPS_OFFLINE) selon le script lancé", + "Frontend: interpolation sub-step gère désormais Step 3c (online) et Step 4c (offline) de la même façon", + "Backend: route upload_firmware lance maintenant le script en background et réutilise le même mécanisme de log/done que l'update online", + "Détection de fin: substring 'completed successfully!' (matche les deux scripts qui ont des markers finaux légèrement différents)" + ], + "fixes": [ + "Le bouton 'Upload & Install' restait bloqué sur 'Installing...' après succès — resetUpdateButton remet maintenant les deux boutons à zéro" + ], + "compatibility": [ + "Aucun risque sur les capteurs existants: même logique, juste l'UX qui change côté UI" + ] + }, + "notes": "Suite v1.8.0: alignement de l'upload offline sur le flow online. Les deux chemins partagent maintenant le même mécanisme de polling, la même progress bar, et le même self-heal Step 3c/4c. Plus de divergence entre les deux modes." + }, { "version": "1.8.0", "date": "2026-05-12", diff --git a/html/admin.html b/html/admin.html index 825db14..bc70e6c 100755 --- a/html/admin.html +++ b/html/admin.html @@ -853,8 +853,8 @@ function update_config(param, value){ } // Mapping of step markers detected in the log -> progress %, step label, and weight -// for sub-step interpolation. The script normally takes ~80-100s end-to-end. -const UPDATE_STEPS = [ +// for sub-step interpolation. The online script normally takes ~80-100s end-to-end. +const UPDATE_STEPS_ONLINE = [ { marker: 'Step 1:', percent: 5, label: 'Téléchargement du firmware' }, { marker: 'Step 2:', percent: 12, label: 'Mise à jour de la configuration BDD' }, { marker: 'Step 3:', percent: 18, label: 'Vérification des permissions' }, @@ -862,9 +862,24 @@ const UPDATE_STEPS = [ { marker: 'Step 4:', percent: 70, label: 'Redémarrage des services' }, { marker: 'Step 5:', percent: 94, label: 'Vérification système' }, { marker: 'Step 6:', percent: 98, label: 'Nettoyage des logs' }, - { marker: 'Update completed successfully!', percent: 100, label: '✅ Terminé !' } + { marker: 'completed successfully!', percent: 100, label: '✅ Terminé !' } ]; +// Offline upload uses a different shell script with different step numbering. +const UPDATE_STEPS_OFFLINE = [ + { marker: 'Step 1:', percent: 5, label: 'Validation du package' }, + { marker: 'Step 2:', percent: 12, label: 'Synchronisation des fichiers' }, + { marker: 'Step 3:', percent: 20, label: 'Mise à jour de la configuration BDD' }, + { marker: 'Step 4:', percent: 25, label: 'Vérification des permissions' }, + { marker: 'Step 4c:', percent: 30, label: 'Reconfiguration des services systemd' }, + { marker: 'Step 5:', percent: 75, label: 'Redémarrage des services' }, + { marker: 'Step 6:', percent: 94, label: 'Vérification système' }, + { marker: 'Step 7:', percent: 98, label: 'Nettoyage des fichiers temporaires' }, + { marker: 'completed successfully!', percent: 100, label: '✅ Terminé !' } +]; + +let UPDATE_STEPS = UPDATE_STEPS_ONLINE; + let updatePollState = null; function updateFirmware() { @@ -886,6 +901,7 @@ function updateFirmware() { const reloadBtn = document.getElementById('reloadBtn'); // Reset UI + UPDATE_STEPS = UPDATE_STEPS_ONLINE; updateBtn.disabled = true; updateBtnText.textContent = 'Updating...'; updateSpinner.style.display = 'inline-block'; @@ -992,16 +1008,24 @@ function refreshProgressFromContent(allContent) { let percent = bestStep.percent; let label = bestStep.label; - // Sub-step interpolation: within Step 3c (25 -> 70) and Step 4 (70 -> 94) - // we count finer-grained markers to make progress feel smoother on slow steps. - if (bestStep.marker === 'Step 3c:') { + // Sub-step interpolation: the longest steps benefit from finer-grained + // progress to make the bar feel alive on slow stages. + // - Online "Step 3c:" / Offline "Step 4c:" both run setup_services.sh + // - Online "Step 4:" / Offline "Step 5:" both restart services + if (bestStep.marker === 'Step 3c:' || bestStep.marker === 'Step 4c:') { const startedCount = (allContent.match(/Started [\w-]+(?: timer| service)/g) || []).length; - // Up to ~11 services are started by setup_services.sh -> step 3c spans 25-65 - percent = Math.min(65, 25 + startedCount * 4); - } else if (bestStep.marker === 'Step 4:') { + // setup_services.sh starts ~11 services. Online: 25 -> 65. Offline: 30 -> 70. + const base = bestStep.percent; + const span = (bestStep.marker === 'Step 3c:') ? 40 : 40; + percent = Math.min(base + span, base + startedCount * 4); + } else if ( + (UPDATE_STEPS === UPDATE_STEPS_ONLINE && bestStep.marker === 'Step 4:') || + (UPDATE_STEPS === UPDATE_STEPS_OFFLINE && bestStep.marker === 'Step 5:') + ) { const restartedCount = (allContent.match(/Restarting enabled service:/g) || []).length; - // ~7 services restarted in step 4 -> spans 70-92 - percent = Math.min(92, 70 + restartedCount * 3); + // ~7 services restarted -> spans base -> base+21 + const base = bestStep.percent; + percent = Math.min(base + 21, base + restartedCount * 3); } setProgress(percent, label); @@ -1065,12 +1089,23 @@ function showUpdateError(message) { } function resetUpdateButton() { + // Online update button const updateBtn = document.getElementById('updateBtn'); const updateBtnText = document.getElementById('updateBtnText'); const updateSpinner = document.getElementById('updateSpinner'); - updateBtn.disabled = false; - updateBtnText.textContent = 'Update firmware'; - updateSpinner.style.display = 'none'; + if (updateBtn) updateBtn.disabled = false; + if (updateBtnText) updateBtnText.textContent = 'Update firmware'; + if (updateSpinner) updateSpinner.style.display = 'none'; + + // Offline upload button (in case the run was triggered via uploadFirmware) + const uploadBtn = document.getElementById('uploadBtn'); + const uploadBtnText = document.getElementById('uploadBtnText'); + const uploadSpinner = document.getElementById('uploadSpinner'); + const uploadProgressBar = document.getElementById('uploadProgressBar'); + if (uploadBtn) uploadBtn.disabled = false; + if (uploadBtnText) uploadBtnText.textContent = 'Upload & Install'; + if (uploadSpinner) uploadSpinner.style.display = 'none'; + if (uploadProgressBar) uploadProgressBar.style.display = 'none'; } function uploadFirmware() { @@ -1106,8 +1141,10 @@ function uploadFirmware() { const progress = document.getElementById('uploadProgress'); const updateOutput = document.getElementById('updateOutput'); const updateOutputContent = document.getElementById('updateOutputContent'); + const finalStatus = document.getElementById('updateFinalStatus'); + const reloadBtn = document.getElementById('reloadBtn'); - // Show loading state + // Reset and show UI uploadBtn.disabled = true; uploadBtnText.textContent = 'Uploading...'; uploadSpinner.style.display = 'inline-block'; @@ -1115,15 +1152,18 @@ function uploadFirmware() { progress.style.width = '0%'; progress.textContent = '0%'; updateOutput.style.display = 'block'; - updateOutputContent.textContent = 'Uploading firmware file...\n'; + updateOutputContent.innerHTML = ''; + finalStatus.style.display = 'none'; + finalStatus.className = 'alert mb-3'; + reloadBtn.style.display = 'none'; + setProgress(0, 'Téléversement du fichier...'); // Build FormData const formData = new FormData(); formData.append('firmware_file', file); - // Use XMLHttpRequest for upload progress const xhr = new XMLHttpRequest(); - xhr.timeout = 300000; // 5 minutes + xhr.timeout = 300000; // 5 minutes for the upload itself xhr.upload.addEventListener('progress', function(e) { if (e.lengthComputable) { @@ -1132,44 +1172,63 @@ function uploadFirmware() { progress.textContent = pct + '%'; if (pct >= 100) { uploadBtnText.textContent = 'Installing...'; - updateOutputContent.textContent = 'Upload complete. Installing firmware...\n'; } } }); xhr.addEventListener('load', function() { + let response; try { - const response = JSON.parse(xhr.responseText); - if (response.success && response.output) { - const formattedOutput = response.output - .replace(/✓/g, '') - .replace(/✗/g, '') - .replace(/⚠/g, '') - .replace(/ℹ/g, ''); - updateOutputContent.innerHTML = formattedOutput; - showToast('Firmware updated: ' + (response.old_version || '?') + ' → ' + (response.new_version || '?'), 'success'); - document.getElementById('reloadBtn').style.display = 'inline-block'; - } else { - updateOutputContent.textContent = 'Error: ' + (response.message || 'Unknown error'); - showToast('Upload failed: ' + (response.message || 'Unknown error'), 'error'); - } + response = JSON.parse(xhr.responseText); } catch (e) { - updateOutputContent.textContent = 'Error parsing response: ' + xhr.responseText; - showToast('Update failed: invalid server response', 'error'); + finalStatus.className = 'alert alert-danger mb-3'; + finalStatus.textContent = 'Erreur: réponse serveur invalide'; + finalStatus.style.display = 'block'; + resetUploadUI(); + showToast('Upload failed: invalid server response', 'error'); + return; } - resetUploadUI(); + + if (!response.success) { + 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; + } + + // Upload + extraction OK → script is running in background, switch to polling + progressBar.style.display = 'none'; // hide upload bar, the update progress bar takes over + showToast('Upload OK, installation en cours...', 'info'); + + // Use offline step mapping + UPDATE_STEPS = UPDATE_STEPS_OFFLINE; + + // Initial timer + const startTime = Date.now(); + updateTimer(startTime); + const timerInterval = setInterval(() => updateTimer(startTime), 1000); + + updatePollState = { offset: 0, allContent: '', startTime, timerInterval }; + setProgress(2, 'Démarrage de l\'installation...'); + pollUpdateProgress(); }); xhr.addEventListener('error', function() { - updateOutputContent.textContent = 'Network error during upload'; - showToast('Upload failed: network error', 'error'); + finalStatus.className = 'alert alert-danger mb-3'; + finalStatus.textContent = 'Erreur réseau pendant l\'upload'; + finalStatus.style.display = 'block'; resetUploadUI(); + showToast('Upload failed: network error', 'error'); }); xhr.addEventListener('timeout', function() { - updateOutputContent.textContent = 'Upload timed out (5 min limit)'; - showToast('Upload timed out', 'error'); + finalStatus.className = 'alert alert-danger mb-3'; + finalStatus.textContent = 'Upload interrompu (timeout 5 min)'; + finalStatus.style.display = 'block'; resetUploadUI(); + showToast('Upload timed out', 'error'); }); function resetUploadUI() { diff --git a/html/launcher.php b/html/launcher.php index 641421d..a782502 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -578,19 +578,31 @@ if ($type == "upload_firmware") { $new_version = trim(file_get_contents("$source_dir/VERSION")); - // Execute update script - $command = "sudo /var/www/nebuleair_pro_4g/update_firmware_from_file.sh '$source_dir' 2>&1"; - $output = shell_exec($command); + // Launch update script in background, reusing the same log/done file mechanism + // as the online update. The frontend can then poll update_firmware_progress + // to get a live view and final status. + $logFile = '/tmp/nebuleair_firmware_update.log'; + $doneFile = '/tmp/nebuleair_firmware_update.done'; - // Cleanup (also done in script, but just in case) - shell_exec("rm -rf $tmp_dir"); + @file_put_contents($logFile, ''); + @unlink($doneFile); + + // Note: $source_dir is escaped because it can contain a version-derived folder name. + // $tmp_dir is removed by the script itself (Step 7) but we also clean here as a safety net + // — done AFTER the script finishes, so we chain it inside the background block. + $cmd = '(sudo /var/www/nebuleair_pro_4g/update_firmware_from_file.sh ' + . escapeshellarg($source_dir) . ' > ' + . escapeshellarg($logFile) . ' 2>&1; ' + . 'echo "EXIT_CODE=$?" >> ' . escapeshellarg($logFile) . '; ' + . 'rm -rf ' . escapeshellarg($tmp_dir) . '; ' + . 'touch ' . escapeshellarg($doneFile) . ') > /dev/null 2>&1 &'; + shell_exec($cmd); echo json_encode([ 'success' => true, - 'output' => $output, + 'started_at' => time(), 'old_version' => $old_version, - 'new_version' => $new_version, - 'timestamp' => date('Y-m-d H:i:s') + 'new_version' => $new_version ]); exit; } diff --git a/update_firmware_from_file.sh b/update_firmware_from_file.sh index a80bb2a..32bd08e 100644 --- a/update_firmware_from_file.sh +++ b/update_firmware_from_file.sh @@ -144,6 +144,21 @@ if [ "${APACHE_CHANGED:-false}" = true ]; then print_status "✓ Apache reloaded" fi +# Step 4c: Reconcile systemd services with the repo (self-heal) +# Same logic as update_firmware.sh: re-run setup_services.sh so any missing, +# masked, or newly-added service in the canonical setup script is installed. +# git config core.fileMode=false strips +x, so chmod first. +print_status "" +print_status "Step 4c: Reconciling systemd services with setup_services.sh..." +SETUP_SERVICES="$TARGET_DIR/services/setup_services.sh" +if [ -f "$SETUP_SERVICES" ]; then + chmod +x "$SETUP_SERVICES" + "$SETUP_SERVICES" + check_status "Setup services reconciliation" +else + print_status "⚠ setup_services.sh not found, skipping" +fi + # Step 5: Restart critical services print_status "" print_status "Step 5: Managing system services..."