planif: Suggérer ne fige plus — timeout solveur + dialogue toujours fermable

- roster API : AbortController (timeout 45s, 90s pour /optimize-plan) → un solveur/hub lent ou
  injoignable lève au lieu de laisser l'await pendre à l'infini (suggestDlg.building coincé à true).
  Au-delà → repli heuristique local, dialogue utilisable.
- q-dialog Suggérer : @hide remet building=false (jamais coincé-ouvert au ré-open).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-07 09:56:12 -04:00
parent 77dd426343
commit 8586b9310a
2 changed files with 18 additions and 20 deletions

View File

@ -5,25 +5,23 @@
*/ */
import { HUB_URL as HUB } from 'src/config/hub' import { HUB_URL as HUB } from 'src/config/hub'
async function jget (path) { // Toute requête a un TIMEOUT (AbortController) : un solveur/hub lent ou injoignable ne doit JAMAIS
const r = await fetch(HUB + path) // laisser l'interface figée (ex. « Suggérer » qui tourne sans fin). Au-delà, on lève → repli local.
if (!r.ok) throw new Error('Roster API ' + r.status) async function _fetch (path, init = {}, ms = 45000) {
return r.json() const ctl = new AbortController()
} const timer = setTimeout(() => ctl.abort(), ms)
async function jpost (path, body) { try {
const r = await fetch(HUB + path, { const r = await fetch(HUB + path, { ...init, signal: ctl.signal })
method: 'POST', if (!r.ok) throw new Error('Roster API ' + r.status)
headers: { 'Content-Type': 'application/json' }, return await r.json()
body: JSON.stringify(body || {}), } catch (e) {
}) if (e && e.name === 'AbortError') throw new Error('Roster API timeout (' + Math.round(ms / 1000) + 's) — serveur trop lent ou injoignable')
if (!r.ok) throw new Error('Roster API ' + r.status) throw e
return r.json() } finally { clearTimeout(timer) }
}
async function jput (path, body) {
const r = await fetch(HUB + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })
if (!r.ok) throw new Error('Roster API ' + r.status)
return r.json()
} }
async function jget (path, ms) { return _fetch(path, {}, ms) }
async function jpost (path, body, ms) { return _fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) }, ms) }
async function jput (path, body, ms) { return _fetch(path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) }, ms) }
export const listTechnicians = () => jget('/roster/technicians') export const listTechnicians = () => jget('/roster/technicians')
export const listTemplates = () => jget('/roster/templates') export const listTemplates = () => jget('/roster/templates')
@ -105,7 +103,7 @@ export const unassignedJobs = () => jget('/roster/unassigned-jobs')
// Optimisation de tournées (VRPTW + compétences + temps sur place) via le solveur OR-Tools. payload = { jobs[], vehicles[], ... } // Optimisation de tournées (VRPTW + compétences + temps sur place) via le solveur OR-Tools. payload = { jobs[], vehicles[], ... }
export const optimizeRoutes = (payload) => jpost('/roster/optimize-routes', payload) export const optimizeRoutes = (payload) => jpost('/roster/optimize-routes', payload)
// P4 — orchestration COMPLÈTE côté hub (pool+quarts+occupation+règles d'adresse) : { dates?|days?, tech_ids?, opts, dur_overrides, job_windows } → { plan[], unassigned[], assumed_shift } // P4 — orchestration COMPLÈTE côté hub (pool+quarts+occupation+règles d'adresse) : { dates?|days?, tech_ids?, opts, dur_overrides, job_windows } → { plan[], unassigned[], assumed_shift }
export const optimizePlanHub = (payload) => jpost('/roster/optimize-plan', payload) export const optimizePlanHub = (payload) => jpost('/roster/optimize-plan', payload, 90000) // solveur VRP : timeout 90s → sinon repli local (jamais figé)
// Proxys OSRM auto-hébergé (points [[lon,lat],…]) — tracés + matrices SANS Mapbox payant. // Proxys OSRM auto-hébergé (points [[lon,lat],…]) — tracés + matrices SANS Mapbox payant.
export const osrmRoute = (points) => jpost('/roster/osrm-route', { points }) // → { km, mins, geometry, legs[] (temps entre arrêts) } export const osrmRoute = (points) => jpost('/roster/osrm-route', { points }) // → { km, mins, geometry, legs[] (temps entre arrêts) }
export const osrmTable = (points) => jpost('/roster/osrm-table', { points }) // → { durations (s), distances (m) } même forme que Mapbox Matrix export const osrmTable = (points) => jpost('/roster/osrm-table', { points }) // → { durations (s), distances (m) } même forme que Mapbox Matrix

View File

@ -1054,7 +1054,7 @@
</div> </div>
<!-- D : revue de la répartition suggérée (surcouche) déplacer/retirer avant d'appliquer --> <!-- D : revue de la répartition suggérée (surcouche) déplacer/retirer avant d'appliquer -->
<q-dialog v-model="suggestDlg.open" :maximized="$q.screen.lt.sm"> <q-dialog v-model="suggestDlg.open" :maximized="$q.screen.lt.sm" @hide="suggestDlg.building = false">
<q-card :style="$q.screen.lt.sm ? 'width:100%;max-width:100%;height:100%;display:flex;flex-direction:column' : 'width:940px;max-width:96vw;max-height:90vh;display:flex;flex-direction:column'"> <q-card :style="$q.screen.lt.sm ? 'width:100%;max-width:100%;height:100%;display:flex;flex-direction:column' : 'width:940px;max-width:96vw;max-height:90vh;display:flex;flex-direction:column'">
<q-card-section class="row items-center q-py-sm" style="border-bottom:1px solid #e2e8f0"> <q-card-section class="row items-center q-py-sm" style="border-bottom:1px solid #e2e8f0">
<q-icon name="auto_awesome" color="amber-7" size="22px" class="q-mr-sm" /> <q-icon name="auto_awesome" color="amber-7" size="22px" class="q-mr-sm" />