Sélection de tuiles au clic, file d'attente et rafraîchissement fiable
- Clic sur la carte pour choisir des dalles précises, même sans données existantes (via /api/cell) ; les zones dessinées s'ajoutent à la sélection au lieu de la remplacer - Une demande lancée pendant un run part en file d'attente côté serveur et démarre à la fin du travail en cours (plus de refus 409), file vidable depuis l'interface - Tuiles et interface toujours fraîches : images servies sans cache navigateur (revalidation 304), rechargement de la carte seulement une fois le rebuild de l'index terminé - serve-webapp.sh : sous-commande update retirée, documentation de déploiement corrigée en conséquence
This commit is contained in:
@ -12,8 +12,11 @@ Fonctionnalités :
|
||||
réordonnancement par glisser-déposer (persisté en localStorage).
|
||||
- Panneau d'infos par tuile : résolution, méthode de classification du sol
|
||||
(lue depuis output/DTM/*_dtm_method.txt), dates et tailles des fichiers.
|
||||
- Génération de nouvelles zones via l'API web (./run.sh --serve) :
|
||||
dessin d'un rectangle → téléchargement IGN + traitement.
|
||||
- Génération via l'API web (./run.sh --serve) : dessin d'un rectangle
|
||||
(téléchargement IGN + traitement) ou sélection cumulative au clic —
|
||||
chaque tuile individuelle, avec ou sans données existantes ; les zones
|
||||
et les clics s'additionnent, et une demande lancée pendant un run part
|
||||
en file d'attente côté serveur (jamais de coupure du travail en place).
|
||||
- Onglet Export : sélection de dalles sur la carte → mosaïque jointive
|
||||
(image ou PDF multi-couches) téléchargeable pour consultation sur
|
||||
téléphone (/api/export de la webapp, module export.py).
|
||||
@ -1294,7 +1297,7 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<!-- Actions Zone / Sélection / Compléter : réservées à cet onglet -->
|
||||
<div class="gen-actions">
|
||||
<button id="btnGenZone" title="Générer une nouvelle zone : dessiner un rectangle, téléchargement IGN + traitement" hidden>+ Zone</button>
|
||||
<button id="btnGenSel" title="Régénérer des tuiles précises : cliquer des dalles sur la carte pour les ajouter ou les retirer" hidden>↻ Sélectionner des tuiles</button>
|
||||
<button id="btnGenSel" title="Sélectionner des tuiles précises, à générer ou régénérer : un clic sur la carte ajoute ou retire la dalle (les zones dessinées s'ajoutent à la sélection)" hidden>Sélectionner des tuiles</button>
|
||||
<button id="btnComplete" title="Compléter : générer les visualisations manquantes sur toutes les dalles présentes dans input/ (sans téléchargement)" hidden><span class="ok-ic">✓</span> Compléter</button>
|
||||
</div>
|
||||
|
||||
@ -1342,7 +1345,8 @@ _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="genStop" class="icon-btn" title="Arrêter la génération en cours (la file d'attente n'est pas touchée)" hidden>■</button>
|
||||
<button id="genQueueDrop" class="icon-btn" title="Retirer les demandes en attente (elles démarreraient après le run en cours sinon)" hidden>⏏</button>
|
||||
<button id="genQueueClear" class="icon-btn" title="Retirer les tâches terminées">Vider</button>
|
||||
</div>
|
||||
<div id="genQueueBody"></div>
|
||||
@ -1779,6 +1783,9 @@ body.zooming .leaflet-lidarTiles-pane { will-change: transform; }
|
||||
.gen-task.running .gen-task-dot {
|
||||
background: var(--accent); animation: gen-pulse 1.1s ease-in-out infinite;
|
||||
}
|
||||
.gen-task.queued .gen-task-dot {
|
||||
background: var(--muted); animation: gen-pulse 2.2s ease-in-out infinite;
|
||||
}
|
||||
.gen-task.done .gen-task-dot { background: #39d98a; }
|
||||
.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; } }
|
||||
@ -1788,6 +1795,7 @@ body.zooming .leaflet-lidarTiles-pane { will-change: transform; }
|
||||
}
|
||||
.gen-task-state { font-size: 11px; white-space: nowrap; }
|
||||
.gen-task.running .gen-task-state { color: var(--accent); }
|
||||
.gen-task.queued .gen-task-state { color: var(--muted); }
|
||||
.gen-task.done .gen-task-state { color: #39d98a; }
|
||||
.gen-task.failed .gen-task-state, .gen-task.stopped .gen-task-state { color: var(--danger); }
|
||||
.gen-task-time {
|
||||
@ -2053,7 +2061,7 @@ function registerTile(t) {
|
||||
}).addTo(map);
|
||||
mk.on('click', () => {
|
||||
if (exportMode) { toggleExportSel(t); return; }
|
||||
if (genSelMode) { toggleGenSel(t); return; }
|
||||
if (genSelMode) { toggleGenSelCell(t.col, t.row, t.corners); return; }
|
||||
if (!genMode) showTileInfo(t);
|
||||
});
|
||||
const el = mk.getElement();
|
||||
@ -2799,7 +2807,10 @@ function saveViewHashSoon() {
|
||||
}
|
||||
window.addEventListener('pagehide', () => { clearTimeout(viewHashTimer); saveViewHash(); });
|
||||
map.on('moveend', () => { updateImages(); saveViewHashSoon(); });
|
||||
map.on('click', hideTileInfo);
|
||||
map.on('click', e => {
|
||||
if (genSelMode) { pickGenSelCell(e.latlng); return; }
|
||||
hideTileInfo();
|
||||
});
|
||||
map.whenReady(() => layoutTiles());
|
||||
|
||||
// ============================================================
|
||||
@ -2975,12 +2986,16 @@ const genBadge = document.getElementById('genBadge');
|
||||
const genStop = document.getElementById('genStop');
|
||||
const genQueueBody = document.getElementById('genQueueBody');
|
||||
const genQueueClear = document.getElementById('genQueueClear');
|
||||
const genQueueDrop = document.getElementById('genQueueDrop');
|
||||
const genQueueCount = document.getElementById('genQueueCount');
|
||||
let genMode = false, drawStart = null, drawRect = null;
|
||||
let pendingTiles = [], pendingRegen = false, pendingCompleteAll = false;
|
||||
const ghostCells = [];
|
||||
let genSelMode = false;
|
||||
const genSel = new Set(); // clés "col,row" des dalles retenues pour la régénération
|
||||
const genSel = new Set(); // clés "col,row" des dalles retenues (clics + zones cumulés)
|
||||
const genSelCorners = new Map(); // clé "col,row" → coins GPS (dalles sans données)
|
||||
const genSelPolys = new Map(); // clé "col,row" → polygone Leaflet de surlignage
|
||||
const GEN_MAX_CELLS = 400; // garde-fou serveur : ~400 km² max par demande
|
||||
|
||||
// Affiche la barre de confirmation de génération, dans l'onglet Génération
|
||||
// (bascule automatique sur cet onglet).
|
||||
@ -3041,7 +3056,7 @@ function exitGenMode() {
|
||||
genBtn.addEventListener('click', () => {
|
||||
if (genMode) { exitGenMode(); return; }
|
||||
clearGhosts();
|
||||
clearGenSel();
|
||||
pendingCompleteAll = false; // nouvelle zone : le scope global « Compléter » est levé
|
||||
genBar.classList.remove('show');
|
||||
genMode = true;
|
||||
map.dragging.disable();
|
||||
@ -3076,37 +3091,60 @@ map.on('mouseup', e => {
|
||||
viz: selectedGenViz() })
|
||||
}).then(r => r.json()).then(d => {
|
||||
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é
|
||||
// La zone S'AJOUTE à la sélection en cours (zones successives, clics
|
||||
// individuels) : les tuiles déjà retenues sont conservées, jamais perdues.
|
||||
let added = 0;
|
||||
for (const c of (d.cells || [])) {
|
||||
const k = genSelKey(c.col, c.row);
|
||||
if (!genSel.has(k)) { genSel.add(k); added++; }
|
||||
if (c.corners) genSelCorners.set(k, c.corners);
|
||||
}
|
||||
refreshGenSelHighlights();
|
||||
pendingTiles = genSelCells();
|
||||
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)
|
||||
));
|
||||
if (!pendingTiles.length) {
|
||||
if (!genSel.size) {
|
||||
genMsg.textContent = 'Zone déjà complète pour les visualisations choisies' +
|
||||
(genRegen.checked ? '' : ' (cochez « Régénérer » pour forcer le retraitement)');
|
||||
genGo.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
genMsg.textContent = pendingTiles.length + ' tuile(s) à ' +
|
||||
(genRegen.checked ? 'régénérer' : 'générer ou compléter (visualisations manquantes)') +
|
||||
(d.capped ? ' (zone tronquée à 400)' : '');
|
||||
genGo.style.display = '';
|
||||
genMsg.textContent = 'Zone : ' + added + ' tuile(s) ajoutée(s)' +
|
||||
(d.capped ? ' (zone tronquée à 400)' : '') +
|
||||
' — ' + genSel.size + ' tuile(s) sélectionnée(s) au total' +
|
||||
(genSel.size > GEN_MAX_CELLS
|
||||
? ' (maximum ' + GEN_MAX_CELLS + ' par lancement : retirez des tuiles)' : '');
|
||||
genGo.style.display = genSel.size > GEN_MAX_CELLS ? 'none' : '';
|
||||
}).catch(() => { showGenBar('API indisponible'); });
|
||||
});
|
||||
|
||||
// « Sélectionner des tuiles » : mode régénération ciblée — le clic sur une
|
||||
// dalle l'ajoute ou la retire (surlignage bleu), toutes résolutions confondues
|
||||
// (les sous-tuiles 0,2 m comptent pour une seule dalle). La barre de
|
||||
// confirmation permet ensuite de choisir les couches et les options avant de
|
||||
// lancer la régénération (--force --force-classification) sur la sélection.
|
||||
// « Sélectionner des tuiles » : sélection cumulative, tuile par tuile — le
|
||||
// clic sur la carte ajoute ou retire la dalle de 1 km sous le curseur, avec
|
||||
// ou sans données existantes (téléchargement + génération à la validation ;
|
||||
// les sous-tuiles 0,2 m comptent pour une seule dalle). Les zones dessinées
|
||||
// s'ajoutent à la même sélection. La barre de confirmation permet de choisir
|
||||
// les couches et les options avant de lancer (--force implicite sur une
|
||||
// sélection clic par clic : retraitement ciblé des tuiles choisies).
|
||||
function genSelKey(col, row) { return col + ',' + row; }
|
||||
|
||||
function refreshGenSelHighlights() {
|
||||
const markerCells = new Set();
|
||||
for (const entry of markers.values()) {
|
||||
const k = genSelKey(entry.t.col, entry.t.row);
|
||||
markerCells.add(k);
|
||||
const div = entryDiv(entry);
|
||||
if (!div) continue;
|
||||
div.classList.toggle('genSel', genSel.has(genSelKey(entry.t.col, entry.t.row)));
|
||||
if (div) div.classList.toggle('genSel', genSel.has(k));
|
||||
}
|
||||
// Dalles sans données (aucun marqueur) : surlignage en polygone
|
||||
for (const [k, poly] of genSelPolys) {
|
||||
if (!genSel.has(k) || markerCells.has(k)) { poly.remove(); genSelPolys.delete(k); }
|
||||
}
|
||||
for (const k of genSel) {
|
||||
if (markerCells.has(k) || genSelPolys.has(k)) continue;
|
||||
const corners = genSelCorners.get(k);
|
||||
if (!corners) continue;
|
||||
genSelPolys.set(k, L.polygon(corners,
|
||||
{ color: '#5aa9e6', weight: 1.5, fillOpacity: 0.15, pane: 'genDraw' }).addTo(map));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3115,16 +3153,21 @@ function genSelCells() {
|
||||
}
|
||||
|
||||
function updateGenSelBar() {
|
||||
showGenBar(genSel.size
|
||||
? genSel.size + ' tuile(s) sélectionnée(s) — options puis Lancer'
|
||||
: 'Mode sélection : clic sur dalle = ajouter / retirer');
|
||||
genGo.style.display = genSel.size ? '' : 'none';
|
||||
showGenBar(!genSel.size
|
||||
? 'Mode sélection : clic sur la carte = ajouter / retirer une tuile'
|
||||
: genSel.size > GEN_MAX_CELLS
|
||||
? genSel.size + ' tuile(s) sélectionnée(s) — maximum ' + GEN_MAX_CELLS + ' par lancement'
|
||||
: genSel.size + ' tuile(s) sélectionnée(s) — options puis Lancer');
|
||||
genGo.style.display = genSel.size && genSel.size <= GEN_MAX_CELLS ? '' : 'none';
|
||||
}
|
||||
|
||||
function toggleGenSel(t) {
|
||||
const key = genSelKey(t.col, t.row);
|
||||
function toggleGenSelCell(col, row, corners) {
|
||||
const key = genSelKey(col, row);
|
||||
if (genSel.has(key)) genSel.delete(key);
|
||||
else genSel.add(key);
|
||||
else {
|
||||
genSel.add(key);
|
||||
if (corners) genSelCorners.set(key, corners);
|
||||
}
|
||||
refreshGenSelHighlights();
|
||||
pendingTiles = genSelCells();
|
||||
pendingRegen = true;
|
||||
@ -3133,14 +3176,26 @@ function toggleGenSel(t) {
|
||||
updateGenClassInfo();
|
||||
}
|
||||
|
||||
// Clic sur la carte hors dalle affichée : le serveur convertit le point
|
||||
// WGS84 en dalle L93 de 1 km — permet de sélectionner des tuiles qui
|
||||
// n'existent pas encore (aucun marqueur, aucune donnée).
|
||||
function pickGenSelCell(latlng) {
|
||||
fetch('/api/cell?lat=' + latlng.lat + '&lng=' + latlng.lng)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if (d && d.col !== null && d.col !== undefined && d.row !== undefined)
|
||||
toggleGenSelCell(d.col, d.row, d.corners);
|
||||
}).catch(() => { /* hors ligne : le clic suivant réessaiera */ });
|
||||
}
|
||||
|
||||
function setGenSelMode(on) {
|
||||
genSelMode = on;
|
||||
document.body.classList.toggle('genSel', on);
|
||||
if (btnGenSel) {
|
||||
btnGenSel.classList.toggle('primary', on);
|
||||
btnGenSel.innerHTML = on
|
||||
? '✓ Sélection en cours — cliquer des dalles'
|
||||
: '↻ Sélectionner des tuiles';
|
||||
? '✓ Sélection en cours — cliquer sur la carte'
|
||||
: 'Sélectionner des tuiles';
|
||||
}
|
||||
// Mobile : le tiroir latéral couvre la carte — on le referme pour laisser
|
||||
// la place aux clics (l'utilisateur le rouvre via ☰ pour valider).
|
||||
@ -3149,7 +3204,7 @@ function setGenSelMode(on) {
|
||||
}
|
||||
if (hintChip) {
|
||||
hintChip.innerHTML = on
|
||||
? 'Mode sélection : clic sur dalle = ajouter / retirer<br>Échap pour terminer la sélection'
|
||||
? 'Mode sélection : clic sur la carte = ajouter / retirer une tuile<br>La sélection se cumule avec les zones dessinées — Échap pour terminer'
|
||||
: hintDefaultHtml;
|
||||
}
|
||||
if (on) {
|
||||
@ -3167,6 +3222,7 @@ function clearGenSel() {
|
||||
if (genSelMode) setGenSelMode(false);
|
||||
if (genSel.size) {
|
||||
genSel.clear();
|
||||
genSelCorners.clear();
|
||||
refreshGenSelHighlights();
|
||||
}
|
||||
}
|
||||
@ -3224,13 +3280,18 @@ genGo.addEventListener('click', () => {
|
||||
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(() => {
|
||||
.then(d => {
|
||||
const verb = isRegen ? 'régénération'
|
||||
: pendingCompleteAll ? 'complétion' : 'génération';
|
||||
pendingCompleteAll = false;
|
||||
pendingRegen = false;
|
||||
if (fromSel) clearGenSel();
|
||||
return startGenTask(nTiles + ' tuile(s) — ' + verb);
|
||||
const label = nTiles + ' tuile(s) — ' + verb;
|
||||
// Un run est déjà en cours : la demande est partie en file d'attente
|
||||
// côté serveur et démarrera à sa fin, sans couper le travail en place.
|
||||
if (d && d['demarré'] === false && d.en_file)
|
||||
return startGenTask(label, { qid: d.qid, position: d.en_file });
|
||||
return startGenTask(label);
|
||||
})
|
||||
.catch(msg => { genGo.style.display = 'none'; showGenBar('Erreur : ' + msg); });
|
||||
});
|
||||
@ -3507,7 +3568,11 @@ function genTaskEl(t) {
|
||||
label.title = t.label; label.textContent = t.label;
|
||||
const state = document.createElement('span'); state.className = 'gen-task-state';
|
||||
const tiles = t.tiles || [];
|
||||
if (t.state === 'running') {
|
||||
if (t.state === 'queued') {
|
||||
state.textContent = t.position
|
||||
? 'En file — position ' + t.position
|
||||
: 'En file d\'attente';
|
||||
} else if (t.state === 'running') {
|
||||
const finished = tiles.filter(x => x.state === 'done' || x.state === 'failed').length;
|
||||
state.textContent = tiles.length
|
||||
? 'En cours — ' + finished + '/' + tiles.length + ' tuile(s)'
|
||||
@ -3559,21 +3624,27 @@ function renderGenQueue() {
|
||||
// Plus récentes en haut
|
||||
for (const t of [...genTasks].reverse()) genQueueBody.appendChild(genTaskEl(t));
|
||||
const running = genTasks.filter(t => t.state === 'running').length;
|
||||
const queued = genTasks.filter(t => t.state === 'queued').length;
|
||||
genBadge.hidden = running === 0;
|
||||
genBadge.textContent = running > 0 ? String(running) : '';
|
||||
if (genStop) { genStop.hidden = running === 0; genStop.disabled = false; }
|
||||
const active = running + queued;
|
||||
genQueueCount.textContent = genTasks.length
|
||||
? (running ? running + ' en cours / ' + genTasks.length : genTasks.length + ' tâche(s)')
|
||||
? (active ? active + ' en cours / ' + genTasks.length : genTasks.length + ' tâche(s)')
|
||||
: '';
|
||||
genQueue.hidden = genTasks.length === 0;
|
||||
}
|
||||
|
||||
function startGenTask(labelText) {
|
||||
function startGenTask(labelText, opts) {
|
||||
const now = new Date();
|
||||
const qid = opts ? opts.qid : undefined;
|
||||
genTasks.push({
|
||||
id: Date.now(), label: labelText,
|
||||
time: now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
|
||||
state: 'running', returncode: null, log: [], logOpen: false, tiles: [],
|
||||
// Demande partie en file d'attente serveur : suivie jusqu'à son tour
|
||||
state: qid !== undefined ? 'queued' : 'running',
|
||||
qid: qid, position: opts ? opts.position : undefined,
|
||||
returncode: null, log: [], logOpen: false, tiles: [],
|
||||
});
|
||||
saveGenTasks(); renderGenQueue(); pollGenStatus();
|
||||
}
|
||||
@ -3664,7 +3735,12 @@ function mergeTilesData(data) {
|
||||
}
|
||||
}
|
||||
if (changed || metaChanged) renderLayerPanel();
|
||||
if (changed) { syncTileLayers(); layoutTiles(); }
|
||||
if (changed) {
|
||||
syncTileLayers(); layoutTiles();
|
||||
// Une dalle qui vient d'apparaître prend le relais du polygone de
|
||||
// sélection (surlignage du marqueur désormais possible)
|
||||
refreshGenSelHighlights();
|
||||
}
|
||||
}
|
||||
function pollLiveTiles() {
|
||||
const q = liveTilesStamp != null ? '?stamp=' + liveTilesStamp : '';
|
||||
@ -3683,8 +3759,31 @@ function pollGenStatus() {
|
||||
// Réponse d'erreur (ex: machine de traitement injoignable) : ignorer,
|
||||
// le sondage reprendra — ne pas marquer les tâches en échec.
|
||||
if (typeof d.running !== 'boolean') return;
|
||||
// Demandes en file côté serveur : bouton de retrait
|
||||
if (genQueueDrop) genQueueDrop.hidden = !(d.queue && d.queue.length);
|
||||
// Tâches en file : démarrent quand le serveur attaque leur run (qid),
|
||||
// « Interrompu » si la demande est retirée sans avoir tourné.
|
||||
const serverQids = new Set((d.queue || []).map(q => q.id));
|
||||
for (const t of genTasks) {
|
||||
if (t.state !== 'queued') continue;
|
||||
const gone = !serverQids.has(t.qid);
|
||||
if (d.running && d.qid === t.qid) {
|
||||
// Le run de cette demande a démarré : les tâches « en cours » plus
|
||||
// anciennes appartenaient au run précédent, forcément terminé (le
|
||||
// serveur n'exécute qu'un run à la fois).
|
||||
for (const o of genTasks) {
|
||||
if (o !== t && o.state === 'running') { o.state = 'done'; o.returncode = 0; }
|
||||
}
|
||||
t.state = 'running'; t.started = d.started; t.position = undefined;
|
||||
} else if (d.running && d.qid == null && gone) {
|
||||
t.state = 'running'; t.started = d.started; t.position = undefined;
|
||||
} else if (!d.running && gone) {
|
||||
t.state = 'stopped';
|
||||
}
|
||||
}
|
||||
const running = genTasks.filter(t => t.state === 'running');
|
||||
if (!running.length) {
|
||||
const queued = genTasks.filter(t => t.state === 'queued');
|
||||
if (!running.length && !queued.length) {
|
||||
clearInterval(genPollTimer); genPollTimer = null;
|
||||
return;
|
||||
}
|
||||
@ -3695,8 +3794,14 @@ function pollGenStatus() {
|
||||
if (d.tiles && d.tiles.length) t.tiles = d.tiles;
|
||||
}
|
||||
if (!d.running) {
|
||||
clearInterval(genPollTimer); genPollTimer = null;
|
||||
finishRunningTasks(d);
|
||||
if (genTasks.some(t => t.state === 'queued')) {
|
||||
// Des demandes attendent encore leur tour : poursuivre la veille
|
||||
// (le rechargement de fin de run attendra la fin de la file).
|
||||
saveGenTasks(); renderGenQueue();
|
||||
return;
|
||||
}
|
||||
clearInterval(genPollTimer); genPollTimer = null;
|
||||
// Sauvegarde la vue courante pour qu'elle survive au rechargement
|
||||
// (sinon fitView() recadre sur l'ensemble des tuiles et le zoom part à zéro).
|
||||
try {
|
||||
@ -3715,23 +3820,31 @@ function pollGenStatus() {
|
||||
// Après un run : reconstruire vignettes et carte depuis le cache à la demande
|
||||
// (/api/sync — sur la webapp légère, les tuiles proviennent de la machine de
|
||||
// traitement), puis recharger. /api/sync se réduit à un rebuild local : même
|
||||
// flux partout. Rechargement de secours après 30 min.
|
||||
// flux partout. Le rechargement attend la fin de CE rebuild (done >= started,
|
||||
// horodatage serveur retourné au POST) : sans cela, un sondage trop tôt
|
||||
// lirait le done du rebuild précédent et rechargerait une carte périmée.
|
||||
// Rechargement de secours après 30 min.
|
||||
function syncThenReload() {
|
||||
fetch('/api/sync', { method: 'POST' }).then(r => r.ok ? r.json() : null).then(() => {
|
||||
const t0 = Date.now();
|
||||
const check = setInterval(() => {
|
||||
fetch('/api/sync').then(r => r.ok ? r.json() : null).then(s => {
|
||||
if (!s || !s.running || Date.now() - t0 > 30 * 60 * 1000) {
|
||||
clearInterval(check);
|
||||
location.reload();
|
||||
}
|
||||
}).catch(() => { clearInterval(check); location.reload(); });
|
||||
}, 2000);
|
||||
}).catch(() => location.reload());
|
||||
fetch('/api/sync', { method: 'POST' })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
const started = (d && d.started) || 0;
|
||||
const t0 = Date.now();
|
||||
const check = setInterval(() => {
|
||||
fetch('/api/sync').then(r => r.ok ? r.json() : null).then(s => {
|
||||
const finished = s && !s.running
|
||||
&& (!started || (s.done || 0) >= started);
|
||||
if (finished || (s && s.error) || Date.now() - t0 > 30 * 60 * 1000) {
|
||||
clearInterval(check);
|
||||
location.reload();
|
||||
}
|
||||
}).catch(() => { clearInterval(check); location.reload(); });
|
||||
}, 2000);
|
||||
}).catch(() => location.reload());
|
||||
}
|
||||
|
||||
genQueueClear.addEventListener('click', () => {
|
||||
genTasks = genTasks.filter(t => t.state === 'running');
|
||||
genTasks = genTasks.filter(t => t.state === 'running' || t.state === 'queued');
|
||||
saveGenTasks(); renderGenQueue();
|
||||
});
|
||||
|
||||
@ -3748,15 +3861,37 @@ if (genStop) genStop.addEventListener('click', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Retrait des demandes en file d'attente côté serveur : le run en cours
|
||||
// continue, les demandes non démarrées sont abandonnées.
|
||||
if (genQueueDrop) genQueueDrop.addEventListener('click', () => {
|
||||
genQueueDrop.disabled = true;
|
||||
fetch('/api/queue/clear', { method: 'POST' })
|
||||
.then(r => r.ok ? r.json() : r.json().then(j => Promise.reject(j.detail || ('HTTP ' + r.status))))
|
||||
.then(() => {
|
||||
genQueueDrop.disabled = false;
|
||||
genQueueDrop.hidden = true;
|
||||
if (!genPollTimer) pollGenStatus(); // marquer les tâches « Interrompu »
|
||||
})
|
||||
.catch(msg => {
|
||||
genQueueDrop.disabled = false;
|
||||
showGenBar('Erreur file d\'attente : ' + 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
|
||||
// dernier run terminé après un redémarrage du serveur (état rechargé du
|
||||
// fichier .generation.job.json). La clé `started` évite les doublons au
|
||||
// rechargement : un même run n'est adopté qu'une fois.
|
||||
if (genTasks.some(t => t.state === 'running')) pollGenStatus();
|
||||
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à 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));
|
||||
if (s.qid != null && trackedQids.has(s.qid)) return;
|
||||
if ((s.queue || []).some(q => trackedQids.has(q.id))) return;
|
||||
const now = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||
if (s.running) {
|
||||
genTasks.push({
|
||||
|
||||
Reference in New Issue
Block a user