Arrêt de génération et reclassification optionnelle depuis l'interface

- POST /api/stop : SIGTERM au pipeline (groupe isolé via start_new_session),
  SIGKILL du groupe après 15 s ; forwardé au worker en mode deux machines
- « Régénérer » n'implique plus la reclassification : le DTM en cache est
  réutilisé tant que la méthode ne change pas ; nouvelle case « Reclasser le
  sol » (--force-classification) et rappel de la classification existante
- Tâche marquée « Interrompu » (returncode négatif ou nul) au lieu d'Échec
This commit is contained in:
Antoine Jacquin
2026-09-12 20:27:52 +02:00
parent bde2025714
commit 8e55fc5f98
3 changed files with 188 additions and 22 deletions

View File

@ -1216,6 +1216,10 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<label class="gen-opt">
<input type="checkbox" id="genRegen"> Régénérer les tuiles existantes
</label>
<label class="gen-opt" title="Par défaut la classification du sol existante est conservée (DTM réutilisé). Cochez pour la relancer sur toutes les tuiles ; choisir une autre méthode ci-dessous reclassifie déjà les tuiles concernées.">
<input type="checkbox" id="genReclass"> Reclasser le sol
</label>
<div class="gen-hint" id="genClassInfo" hidden></div>
<label class="gen-opt col" title="Toutes pré-sélectionnées — Ctrl+clic pour modifier la sélection">
<span>Visualisations :</span>
<select id="genViz" class="gen-select" multiple size="8">
@ -1250,6 +1254,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
<div class="queue-head">
<span class="panel-title">File de génération</span>
<span id="genQueueCount"></span>
<button id="genStop" class="icon-btn" title="Arrêter la génération en cours" hidden>■</button>
<button id="genQueueClear" class="icon-btn" title="Retirer les tâches terminées">Vider</button>
</div>
<div id="genQueueBody"></div>
@ -1638,6 +1643,7 @@ body.zooming .ltile { will-change: transform; }
.gen-opt.col { flex-direction: column; align-items: stretch; gap: 4px; }
.gen-opt.col > span { color: var(--muted); font-size: 11px; }
.gen-opt select { flex: 1; min-width: 0; }
.gen-hint { color: var(--muted); font-size: 11px; padding: 0 2px; }
.gen-run { display: flex; gap: 8px; margin-top: 2px; }
.gen-run #genGo { flex: 1; }
@ -1658,7 +1664,7 @@ body.zooming .ltile { will-change: transform; }
background: var(--accent); animation: gen-pulse 1.1s ease-in-out infinite;
}
.gen-task.done .gen-task-dot { background: #39d98a; }
.gen-task.failed .gen-task-dot { background: var(--danger); }
.gen-task.failed .gen-task-dot, .gen-task.stopped .gen-task-dot { background: var(--danger); }
@keyframes gen-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
.gen-task-label {
font-size: 12px; flex: 1; min-width: 120px;
@ -1667,7 +1673,7 @@ body.zooming .ltile { will-change: transform; }
.gen-task-state { font-size: 11px; white-space: nowrap; }
.gen-task.running .gen-task-state { color: var(--accent); }
.gen-task.done .gen-task-state { color: #39d98a; }
.gen-task.failed .gen-task-state { color: var(--danger); }
.gen-task.failed .gen-task-state, .gen-task.stopped .gen-task-state { color: var(--danger); }
.gen-task-time {
font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums;
}
@ -2404,6 +2410,7 @@ function regenerateCurrentTile() {
document.getElementById('genRegen').checked = true;
genGo.style.display = '';
showGenBar('1 tuile à régénérer — choisissez les options puis lancez');
updateGenClassInfo();
}
// ============================================================
@ -2419,12 +2426,15 @@ const genMsg = document.getElementById('genMsg');
const genGo = document.getElementById('genGo');
const genCancel = document.getElementById('genCancel');
const genRegen = document.getElementById('genRegen');
const genReclass = document.getElementById('genReclass');
const genClassInfo = document.getElementById('genClassInfo');
const genClass = document.getElementById('genClass');
const genIgnClasses = document.getElementById('genIgnClasses');
const genViz = document.getElementById('genViz');
const genBareEarth = document.getElementById('genBareEarth');
const genQueue = document.getElementById('genQueue');
const genBadge = document.getElementById('genBadge');
const genStop = document.getElementById('genStop');
const genQueueBody = document.getElementById('genQueueBody');
const genQueueClear = document.getElementById('genQueueClear');
const genQueueCount = document.getElementById('genQueueCount');
@ -2442,6 +2452,24 @@ function showGenBar(msg) {
genBar.classList.add('show');
}
// Classification déjà présente sur les tuiles visées : guide le choix
// « Reclasser le sol » (décoché = la classification existante est conservée).
function updateGenClassInfo() {
if (!genClassInfo) return;
const methods = new Set();
for (const pair of (pendingTiles || [])) {
for (const t of TILES) {
if (t.col === pair[0] && t.row === pair[1] && t.meta && t.meta.method)
methods.add(t.meta.method);
}
}
if (!methods.size) { genClassInfo.hidden = true; return; }
genClassInfo.textContent = 'Classification existante : ' +
[...methods].map(methodLabel).join(', ') +
' — conservée si « Reclasser le sol » est décoché et que la méthode ne change pas';
genClassInfo.hidden = false;
}
fetch('/api/status').then(r => {
apiAvailable = r.ok;
if (!apiAvailable) hideExportTab();
@ -2512,6 +2540,7 @@ map.on('mouseup', e => {
if (drawRect) { drawRect.remove(); drawRect = null; }
pendingTiles = (d.cells || []).map(c => [c.col, c.row]);
pendingCompleteAll = false; // zone dessinée : le scope global est levé
updateGenClassInfo();
(d.cells || []).forEach(c => ghostCells.push(
L.polygon(c.corners, { color: '#e94560', weight: 1, dashArray: '4 4', fillOpacity: 0.08, pane: 'genDraw' }).addTo(map)
));
@ -2563,6 +2592,7 @@ function toggleGenSel(t) {
pendingRegen = true;
pendingCompleteAll = false;
updateGenSelBar();
updateGenClassInfo();
}
function setGenSelMode(on) {
@ -2623,6 +2653,7 @@ btnComplete.addEventListener('click', () => {
}).then(r => r.json()).then(d => {
pendingTiles = (d.cells || []).map(c => [c.col, c.row]);
pendingCompleteAll = true;
updateGenClassInfo();
(d.cells || []).forEach(c => ghostCells.push(
L.polygon(c.corners, { color: '#e94560', weight: 1, dashArray: '4 4', fillOpacity: 0.08, pane: 'genDraw' }).addTo(map)
));
@ -2652,7 +2683,8 @@ genGo.addEventListener('click', () => {
all_missing: pendingCompleteAll,
ground_class: genClass ? genClass.value : 'ign',
ign_classes: genIgnClasses ? genIgnClasses.value : 'sol',
bare_earth: genBareEarth ? genBareEarth.checked : false })
bare_earth: genBareEarth ? genBareEarth.checked : false,
reclassify: genReclass ? genReclass.checked : false })
}).then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.detail || ('HTTP ' + r.status))))
.then(() => {
const verb = isRegen ? 'régénération'
@ -2669,6 +2701,7 @@ genCancel.addEventListener('click', () => {
genBar.classList.remove('show');
clearGhosts();
pendingTiles = []; pendingRegen = false; pendingCompleteAll = false;
updateGenClassInfo();
clearGenSel();
if (genMode) exitGenMode();
});
@ -2943,6 +2976,8 @@ function genTaskEl(t) {
: 'En cours…';
} else if (t.state === 'done') {
state.textContent = 'Terminé';
} else if (t.state === 'stopped') {
state.textContent = 'Interrompu';
} else {
state.textContent = 'Échec (code ' + t.returncode + ')';
}
@ -2988,6 +3023,7 @@ function renderGenQueue() {
const running = genTasks.filter(t => t.state === 'running').length;
genBadge.hidden = running === 0;
genBadge.textContent = running > 0 ? String(running) : '';
if (genStop) { genStop.hidden = running === 0; genStop.disabled = false; }
genQueueCount.textContent = genTasks.length
? (running ? running + ' en cours / ' + genTasks.length : genTasks.length + ' tâche(s)')
: '';
@ -3008,7 +3044,10 @@ function finishRunningTasks(d) {
let changed = false;
for (const t of genTasks) {
if (t.state !== 'running') continue;
t.state = d.returncode === 0 ? 'done' : 'failed';
// returncode négatif = tué par /api/stop ; null = serveur relancé en plein
// run (job disparu) : dans les deux cas la tâche est interrompue, pas échouée.
t.state = d.returncode === 0 ? 'done'
: (d.returncode === null || d.returncode < 0) ? 'stopped' : 'failed';
t.returncode = d.returncode;
changed = true;
}
@ -3154,6 +3193,19 @@ genQueueClear.addEventListener('click', () => {
saveGenTasks(); renderGenQueue();
});
// Arrêt de la génération en cours : SIGTERM au pipeline côté serveur, qui
// nettoie ses workers et ses processus PDAL ; le sondage marque ensuite la
// tâche « Interrompu » (returncode négatif).
if (genStop) genStop.addEventListener('click', () => {
genStop.disabled = true;
fetch('/api/stop', { method: 'POST' })
.then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.detail || ('HTTP ' + r.status))))
.catch(msg => {
genStop.disabled = false;
showGenBar('Erreur arrêt : ' + msg);
});
});
renderGenQueue();
// Reprend le suivi si une tâche tournait avant un rechargement de la page,
// adopte une génération lancée hors interface (API directe), ou réaffiche le
@ -3175,7 +3227,8 @@ else fetch('/api/status').then(r => r.ok ? r.json() : null).then(s => {
genTasks.push({
id: Date.now(), label: 'Dernière génération',
time: now, started: s.started,
state: s.returncode === 0 ? 'done' : 'failed', returncode: s.returncode,
state: s.returncode === 0 ? 'done' : s.returncode < 0 ? 'stopped' : 'failed',
returncode: s.returncode,
log: [], logOpen: false, tiles: s.tiles,
});
saveGenTasks(); renderGenQueue();