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) <noreply@anthropic.com>
This commit is contained in:
139
html/admin.html
139
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, '<span style="color: #28a745;">✓</span>')
|
||||
.replace(/✗/g, '<span style="color: #dc3545;">✗</span>')
|
||||
.replace(/⚠/g, '<span style="color: #ffc107;">⚠</span>')
|
||||
.replace(/ℹ/g, '<span style="color: #17a2b8;">ℹ</span>');
|
||||
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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user