- roster.js : store /roster/job-skills (LISTE = source unique des compétences requises),
enrichissement required_skills[] + required_skill (1re, rétro-compat), solveur techCovers
(le tech doit les avoir TOUTES) ; /roster/reserve (bloc placeholder priorité moyenne, soft) ;
publishWeek modes draft/submit/approve/publish ; matérialiseur de quarts récurrents
(source:pattern) ; décodage des entités HTML aux points de service
- route_solver.py : skill_ok list-aware (toutes les compétences requises)
- dispatch.js : suggestSlots ignoreReserved (une réservation ne bloque pas la recherche de créneau)
- conversation.js : agentName sur le fil (commentaire au nom du tech, pas « Targo Ops »)
- legacy-dispatch-sync.js : postTicketLegacy(actorEmail, actorName) — commentaire attribué
- field-app.html + server.js : commentaire + fil de ticket NATIFS (restent sur OPS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch 2 de l'audit (perf backend), items sûrs :
- roster.js : fetchTechnicians caché 30 s (appelé par endpoint + generate + materialize +
tech-positions polling 25 s pour des données quasi statiques). invalidateTechCache() sur
les 6 écritures Dispatch Technician (coût/effic/skills/pause/traccar/weekly) → pas de stale
après édition. Vérifié : call1 71 ms → call2 5 ms (caché).
- legacy-dispatch-sync.js : garde _inFlight sur le tick d'import ET la veille → un passage
long ne se relance plus par-dessus (évitait doublons Dispatch Job + contention ERPNext).
- conversation.js : jeton d'ingestion webhook mémoïsé (mailIngestToken) au lieu d'un
fs.readFileSync SYNCHRONE à chaque courriel/canal entrant (bloquait la boucle d'événements).
NON fait (par prudence) : paralléliser les erp.list séquentiels de generate() (séquencement
DÉLIBÉRÉ anti-ECONNRESET Frappe, cf. feedback_hub_crash_502) ; batch N+1 Customer du triage
(refactor risqué d'une chaîne de fallback stateful — reporté) ; jitter des pollers (502 boot
déjà mitigé).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ingest geocode fallback called Mapbox with only a broad territory
bbox guard — so a wrong-city guess (e.g. "Rue Elsie" → Valleyfield, both
in-territory) was accepted. Rebuilt the chain to be bounded:
infra (fibre/placemarks) → RQA exact → OSM (validated) → postal-code
centroid → phone-match / town-centre (existing last resorts)
- geocodeNominatim(): free OpenStreetMap geocoder (User-Agent per OSM
policy, cached, in-territory) — replaces Mapbox in the ingest path.
- OSM result is ACCEPTED only if within 10 km of the postal-code centroid
(geocodeCentroid, RQA average for the full postal code); otherwise
rejected → the centroid is used. So a J0S1H0 address can never land in
Valleyfield again.
- postal_centroid is the guaranteed bounded fallback.
Note on connectors: the fibre table is a PERMANENT direct MySQL link
(cfg.LEGACY_DB); placemarks come via the permanent PHP bridge
ops_placemarks.php — both already wired into resolveDevCoords.
Verified: J0S1H0 centroid = 45.0857,-74.1795 (Huntingdon, 3471 addrs);
OSM reachable and returns in-town coords for known streets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: "102-4 Rue Elsie, Huntingdon J0S1H0" (a NEW street, absent from RQA)
showed in Valleyfield. The F `fibre` table had the correct placemark
(45.091152,-74.180855) but the job was stuck at 45.264,-74.144.
Root causes:
1. _parseAddr extracted civic "102" from "102-4" → fibreMatch queried
`terrain LIKE '102%'`, matching MANY other streets (Lost Nation,
Dalhousie, Ridge…) and burying/missing the Elsie `terrain '102-4'`.
Now captures the full "NNN-N" civic → `terrain LIKE '102-4%'` (precise)
and cleans the street ("Rue Elsie", no "-4" leftover).
2. resolveDevCoords let the (stale/wrong, account-fallback) delivery
coords win before fibreMatch. Now a STREET-CONFIRMED fibre match is
the top authority (_streetConfirmed) — "se fier à la base infra".
3. Existing wrong jobs never self-corrected (sync only fills missing
coords). Added geolocateJobs refreshFibre mode + ?refreshFibre=1:
re-checks already-coordinated jobs, overwrites ONLY when fibre
confirms the street AND it moved >100m.
Ran the correction: 11 jobs fixed (all fibre placemarks), incl.
LEG-253136 moved 19 km to Huntingdon. Verified read-back.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).
Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hub (lib/roster.js, vision.js, legacy-dispatch-sync.js, server.js) + Ops + pont legacy.
- Capacité par jour AM/PM (Soir = réserve garde/urgence, jamais offerte) calculée
client-side sur les techs visibles -> suit le filtre de compétence.
- Modèle de durée ADDITIF (caractéristiques, tableur inline) + auto-détection
DÉTERMINISTE par mots-clés (sans IA permanente) ; est_min branché sur capacité + pool.
- Capture terrain passive : endpoints publics /field (job/tech/checkpoint/ts/photo/
device/vision), tokens HMAC signés sans PII ; dérive actual_start/end. UI hébergée
public/field-app.html (liste/carte Mapbox/Street View/photo/scan MLKit->Gemini).
- Chrono job (start/finish), repositionnement carte (set-location), vue satellite,
Street View clic-droit, année devant les dates dues groupées.
- Sync techniciens : rapport de réconciliation 3 systèmes (staff legacy / Dispatch
Technician / groupe Authentik), application MANUELLE, zéro écriture Authentik (+11 fiches).
- vision.js : extractEquipment() réutilisable (marque/modèle/série/MAC/codes-barres).
- Pont legacy (ops_reassign.php) : désassignation reflétée, fermeture ticket, retour
au pool ; notification courriel à l'assignation.
Déployé sur le hub ; ce commit aligne le repo sur l'état en production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problème de fond : quand ticket.delivery_id ne se joignait pas, le pont retombait sur l'adresse de
FACTURATION du compte (résidence) → sujet + géocodage faux. Or les 124 tickets ouverts-3301 ont tous un
delivery côté compte (98 via ticket.delivery_id, 26 via le compte).
- fetchTargoTickets : résolution delivery robuste — COALESCE(ticket.delivery_id, delivery du compte AVEC
coords [plus récent], delivery du compte [plus récent]) → l'adresse de SERVICE est toujours disponible.
- buildJob préfère déjà svcAddr (delivery) au billAddr → sujet + géocodage utilisent le service.
- Rafraîchit le `subject` des jobs encore au pool (open + non assigné) pour refléter l'adresse de service
(corrige les anciens sujets basés sur la facturation) ; ne touche pas un job déjà dispatché.
Résultat (re-sync) : delivery 26→37, no_coords 6→1, 0 erreur. Jobs ouverts affichent l'adresse de service
(« Camping Sandysun · 32 Bellevue », « Lac des pins · 25 Érable », « Franklin · 35 rue Hilltop »…).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symptôme : un job de camping (« Lac des pins | Anton Rimerov ») pointait sur la RÉSIDENCE du client
(428 Rue George, Lasalle = 45.58,-73.73) au lieu du camping. Le pont géocodait l'adresse de compte.
- buildJob : détection camping en PRIORITÉ MAX via le registre camping_registry — signal = sujet (label
explicite, prioritaire) puis ville/adresse de delivery. Garde-fou : le texte doit contenir « camping » OU
un mot-clé de LIEU spécifique (évite les faux positifs de patronyme, ex. « Daniel Dauphinais »). coord_src='camping'.
La branche update fait écraser les coords existantes par le camping (comme delivery). 20 jobs ouverts re-coordonnés.
- camping_dispatch_backfill.sql : corrige les jobs DÉJÀ dispatchés (que le sync ne re-traite plus car le ticket
legacy a quitté le pool ouvert-3301) → 4 Lac des Pins + 2 SandySun. Anton Rimerov/Germaine Thibert → 45.0624,-73.9113 ✓.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- address-search.js : expose searchAddressesRpc() → RPC Postgres `search_addresses` (pg_trgm), la MÊME
recherche que l'autocomplete de disponibilité fibre. Trouve les rues que l'ilike manquait (générique géré
par la colonne odonyme_recompose_long + phase 2 trigram), priorise les CP J0L/J0S (territoire).
- geocodeRQA() (bridge) bascule de l'ilike vers la RPC. Garde-fou : la phase 2 trigram dérive quand le
civique est absent du RQA (« 2245 René-Vinet » → « Rue Grenet, Montréal »). On n'accepte un résultat que si
le civique concorde + un token de nom de rue correspond + (territoire J0L/J0S OU CP/ville legacy concordants).
Vérifié sur les données réelles : accepte 494 Av Curry / 3055 Routhier / 228 Principale / 61 Jean-François ;
rejette René-Vinet→Grenet/Panet, chemin Ridge→Ferme, rue West→Perras (bons faux positifs écartés).
- Le faible compte RQA (8) = haute précision (l'ilike comptait 17 dont des faux positifs). Mapbox couvre le
reste (rues neuves/civiques absents) ; ~109/125 (87 %) coordonnés ; les « aucune » = campings/villes mal écrites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pont (legacy-dispatch-sync.js) :
- Import des coordonnées par job via cascade : table legacy `delivery` (point de service exact,
JOIN ticket.delivery_id) > Service Location ERPNext > géocodage RQA > géocodage Mapbox.
Validation bornes Québec (coord()). Couverture 153/172 (89%).
- Géocodage RQA corrigé : retrait du générique de voie (Rue/Rang/Chemin absent de
odonyme_recompose_normal) + code postal non accolé au terme (sinon ilike ne matche jamais).
- Repli Mapbox geocoding pour rues trop récentes pour le RQA (MAPBOX_TOKEN).
- Backfill + UPGRADE : coords delivery écrasent des coords SL moins précises (jamais l'inverse).
- Comptabilité honnête : vérifie r.ok sur create/update (erp ne throw pas) → errors + error_samples.
- Verrou de sérialisation sync() : tick + runs manuels ne se chevauchent plus (frappe_pg).
- Subject tronqué à 140 (champ Data) → corrige CharacterLengthExceededError sur jobs sans SL.
- Observabilité : coord_src tally + error_samples dans le résumé.
Ops Planification (éditeur de journée) :
- travelBetween() consulte une matrice Mapbox Matrix chargée à l'ouverture (loadDayRoute) →
temps de trajet ROUTIERS RÉELS ; réordonnancement instantané sans nouvelle requête.
Repli haversine si Mapbox indispo. Indicateur 🚗 réel vs 📏 vol d'oiseau.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ROOT CAUSE : LEGACY_DB_HOST='legacy-db' = réplica figé au 2026-04-07 → tickets fermés/réassignés depuis
paraissaient encore ouverts (ex. 244659). Fix infra : .env LEGACY_DB_HOST=10.100.80.100 (DB live, SELECT-only).
- closeResolved() : tout DJ issu du pont (open/assigned/On Hold) dont le ticket legacy est 'closed' → status 'Completed'.
Appelé à chaque sync + route POST /dispatch/legacy-sync/close-resolved. Résultat 1er run live : 125 ouverts réels,
102 créés, 44 fermés (dont LEG-244659). NB In Progress non touché.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Couleurs liées aux skills (éditable/cohérent) : hub deptToSkill() déduit une compétence du type legacy
→ /roster/unassigned-jobs renvoie required_skill ; PlanificationPage colore la carte par getTagColor(required_skill)
(même couleur que le chip skill) ; bordure 5px
- Fil complet du ticket : hub /dispatch/legacy-sync/ticket-thread (ticket_msg + auteur staff, HTML nettoyé) ;
api legacyTicketThread ; RightPanel bouton « 💬 Voir le fil / commentaires » (chargé au clic, messages+auteurs+dates)
- Order-by du pool dispatch : useBottomPanel.bottomSort (date|city|priority) + dropdown ⇅ dans BottomPanel
(ville = 2e segment adresse / token sujet avant |)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pont : extrait le lien connect_ministra.php du ticket_msg (déjà posté par le wizard legacy à la vente)
→ champ legacy_activation_url sur le Dispatch Job (backfill des 10 tickets TV). Read-only legacy, aucun 2e
chemin d'écriture → zéro risque de double-facturation (cf. analyse processus).
- store _mapJob : expose legacyActivationUrl ; RightPanel : bouton violet « 📺 Activer STB (Ministra) »
(MÊME lien que le tech reçoit, ouvre connect_ministra.php avec tid/did/sid/cr/m intacts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pont : stocke legacy_dept (dept osTicket) sur le Dispatch Job + backfill des 70 existants
- useHelpers.jobColor/legacyDeptColor : palette comme legacy (Installation vert, Réparation or,
Télé rose, Téléphonie vert pâle, Désinstallation rouge foncé) ; tech en pause = rouge vif prioritaire
- store _mapJob : expose legacyDept + legacyTicketId
- RightPanel : champ « Ticket legacy » (#id · dept) — le client est déjà un lien cliquable vers la fiche
- doc mise à jour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tire régulièrement les tickets ouverts assignés au compte « Tech Targo » (staff 3301)
de la DB legacy MariaDB et crée/maj un Dispatch Job ERPNext (pool à répartir).
- lib/legacy-dispatch-sync.js : fetch (status=open AND assign_to=3301) + mapping
customer (legacy_account_id) / Service Location (coords) / job_type (dept) /
scheduled_date (epoch→America/Toronto) / start_time (am|pm|HH:MM) / priority
- Idempotent via Custom Field Dispatch Job.legacy_ticket_id (lookup avant create) ;
ne clobbe pas le travail du répartiteur (maj date seulement si encore open+non assigné)
- SÉQUENTIEL (frappe_pg) ; endpoints GET preview (dry-run) + POST run
- Récurrence opt-in : startSync() au boot, LEGACY_DISPATCH_SYNC=on + _MIN=15
- server.js : route /dispatch/legacy-sync + startSync()
- doc docs/features/legacy-dispatch-bridge.md + index
Mise en service : 70 tickets importés (0 erreur), récurrence 15 min active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>