diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index 0c8ecdd..ab7cddd 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -3533,6 +3533,17 @@ function saveGenTasks() { try { sessionStorage.setItem(GEN_QKEY, JSON.stringify(genTasks)); } catch (e) { /* privé */ } } +// Runs dont l'utilisateur a retiré la tâche « finie » de la liste : on les +// mémorise (par horodatage serveur `started`) pour que le rechargement de la +// page — qui réadopte la « dernière génération » depuis .generation.job.json +// — ne les re-crée pas à chaque rafraîchissement. +const GEN_DKEY = 'lidarGenDismissed'; +let dismissedRuns = new Set(); +try { dismissedRuns = new Set(JSON.parse(sessionStorage.getItem(GEN_DKEY) || '[]')); } catch (e) { dismissedRuns = new Set(); } +function saveDismissed() { + try { sessionStorage.setItem(GEN_DKEY, JSON.stringify([...dismissedRuns])); } catch (e) { /* privé */ } +} + function genStepEl(s) { const el = document.createElement('span'); el.className = 'gen-step ' + s.state; @@ -3658,6 +3669,10 @@ function finishRunningTasks(d) { t.state = d.returncode === 0 ? 'done' : (d.returncode === null || d.returncode < 0) ? 'stopped' : 'failed'; t.returncode = d.returncode; + // Sans identité de run capturée (tâche lancée immédiatement, jamais en + // file) : on mémorise l'horodatage du run pour pouvoir la retirer sans + // qu'elle soit réadoptée au rechargement de la page. + if (t.started === undefined) t.started = d.started; changed = true; } if (changed) saveGenTasks(); @@ -3844,8 +3859,15 @@ function syncThenReload() { } genQueueClear.addEventListener('click', () => { + // Les tâches retirées (finies, interrompues, échecs) sont mémorisées : sans + // cela le rechargement réadopterait la « dernière génération » du serveur + // (.generation.job.json) et les ferait réapparaître. + for (const t of genTasks) { + if (t.state === 'running' || t.state === 'queued') continue; + if (t.started !== undefined) dismissedRuns.add(t.started); + } genTasks = genTasks.filter(t => t.state === 'running' || t.state === 'queued'); - saveGenTasks(); renderGenQueue(); + saveGenTasks(); saveDismissed(); renderGenQueue(); }); // Arrêt de la génération en cours : SIGTERM au pipeline côté serveur, qui @@ -3887,6 +3909,8 @@ renderGenQueue(); if (genTasks.some(t => t.state === 'running' || t.state === 'queued')) pollGenStatus(); else fetch('/api/status').then(r => r.ok ? r.json() : null).then(s => { if (!s || genTasks.some(t => t.started === s.started)) return; + // Déjà retirée manuellement de la liste : ne pas réadopter ce run. + if (s.started != null && dismissedRuns.has(s.started)) return; // Déjà suivie par une tâche (en cours ou en file côté serveur) : pas // d'adoption en doublon d'un run lancé depuis cet onglet. const trackedQids = new Set(genTasks.filter(t => t.qid !== undefined).map(t => t.qid)); diff --git a/lidar_pipeline/tests/test_index.py b/lidar_pipeline/tests/test_index.py index 8d4b87d..5acf71b 100644 --- a/lidar_pipeline/tests/test_index.py +++ b/lidar_pipeline/tests/test_index.py @@ -255,6 +255,11 @@ def test_build_index_generates_html(tmp_path): # un run terminé réapparaît dans la file avec ses tuiles, sans doublon. assert "Dernière génération" in app_js assert "t.started === s.started" in app_js + # Vidage de la file : les runs retirés sont mémorisés (par horodatage + # serveur) pour que le rechargement ne réadopte pas la « dernière + # génération » du serveur et ne refasse pas réapparaître les tâches finies. + assert "dismissedRuns" in app_js + assert "saveDismissed()" in app_js # Bouton « Compléter » : visualisations manquantes des dalles présentes # dans input/ (scope all_missing envoyé au preview et au generate). # « + Zone » et « Compléter » ne vivent que dans l'onglet Génération,