Last F Do Stuff gap was wireless CPEs (signal, signal-AP, CCQ, TX/RX rate,
airMAX capacity). Empirically confirmed the hub cannot reach the CPE mgmt
network (all probes timed out) — so, exactly like the fiber TP-Link path,
this goes through n8n.
- hub: ticket-collab.airosSignal({serial|ip}) + GET /collab/airos-signal.
Resolves mgmt IP/login from Service Equipment, calls n8n webhook
get_signal_airos (mirror of F device_ajax/airos_ac_ajax.php: airOS auth +
status.cgi parse), normalizes signal/signalAp/ccq/tx-rx rate/airMAX
capacity/model/version/uptime. Degrades gracefully: 404 -> registered:false.
- ops: EquipmentDetail shows a "Do Stuff en direct (sans-fil)" panel for
wireless equipment (isWireless heuristic), rendering the same tiles + feeding
"Copier pour le ticket". Shows an "en attente" note until the webhook exists.
Verified: hub live returns {ok:false,registered:false} for a real CPE (SN-772);
SPA build clean, leak-check 0 secrets, deployed (index.c4ccc86f.js). The one
remaining hop is creating the n8n get_signal_airos workflow (spec in docs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standardize the device-ops entry so users coming from F aren't lost:
- New shared DoStuffButton.vue (canonical "Do Stuff" label + icon): resolves
the device by serial>service_location>customer and opens the ONE device view
(EquipmentDetail). Reused in SubscriptionDetail (replaces bespoke button).
- DeviceStrip (fiche): tooltip hint + right-click "Do Stuff — ouvrir l'appareil"
(works even for devices without ACS data, e.g. wireless), so the term is
discoverable on the customer card too.
Bring the canonical device view to F's function set (device_view.php):
- On-demand "Do Stuff en direct (F)" fetch (/collab/dostuff) surfaces the fields
the passive poller lacks: VoIP, IPTV, distance, WAN IP, line profile, firmware,
mgmt GUI — reserved tech-3 XGS-PON, ~30 s, never auto (OLT load).
- "Copier pour le ticket" (F's copy button): plaintext device summary to clipboard.
Verified: SPA build clean, leak-check 0 secrets, deployed to /opt/ops-app
(index.969e330c.js live, 303508 B served, nginx.conf preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Niveau 2 de Request 1 : à partir des traces GPS, détecte QUELS véhicules ont
séjourné sur un job, puis SUGGÈRE (jamais auto) :
- ASSISTANT : le véhicule du tech assigné était là ET un autre a séjourné → propose
d'ajouter le tech de cet autre véhicule comme assistant.
- MAUVAISE AFFECTATION : le véhicule du tech assigné n'a PAS été vu mais un autre a
fait la visite → le tech a pris un autre camion (mapping non mis à jour).
Garde-fous : séjour (pas passage), fenêtre du jour, garde MÊME-ADRESSE (un véhicule
présent pour SON propre job co-localisé n'est pas un assistant). Cache traces/jour + 1 h/job.
geofence.js : onsiteReview(job) + dismissOnsite ; roster.js : GET /roster/job/:name/onsite
+ POST .../onsite-dismiss ; api/roster : jobOnsite/onsiteDismiss ; GeofenceTimeline : bloc
suggestions avec [Ajouter comme assistant] (addAssistant) / [Assigner ce véhicule au tech]
(setTechTraccarDevice, si device non mappé) / [Ignorer] (dismiss) — confirmées, non destructives.
Mauvaise-affectation vers le véhicule d'UN AUTRE tech = ambigu → pas d'auto-bouton (flag + Ignorer).
Vérifié live : 45 devices ; LEG-255021 → « 37 - Anthony » = tech assigné, 0 suggestion (OK) ;
scan du jour : 31 jobs, 11 avec véhicule sur place, 3 mauvaises affectations détectées.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demande user : une façon SIMPLE d'accéder au Do Stuff. Ajout d'un bouton « Do Stuff —
appareil » sur le détail du service (SubscriptionDetail), comme le bouton Do Stuff de F.
Résout l'appareil du service (Service Equipment par lieu de service, sinon par client ;
préfère un appareil actif avec n° de série) et navigate('Service Equipment', name) →
la fiche ouvre EquipmentDetail = la vue « Do Stuff » (signal Rx/Tx, débit WAN en direct,
actions device_action). Marche pour fibre ET sans-fil (l'appareil résolu s'ouvre ; les
stats airMAX sans-fil s'y afficheront quand leur lecteur sera branché).
Build spa OK, md5 match. Rendu live = fiche sous SSO (prod).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demande user : garder l'historique du véhicule au moment du hit géofence — rester exact
même si le tech change de véhicule un autre jour. Les événements (en_route/on_site/departed)
enregistrent désormais deviceId+deviceName ; rec.device = véhicule du passage. runScan
(live) fige le device du fix courant ; reconcileFromTrack (rejeu trace) fige le device
rejoué ce jour-là. timeline() renvoie device ; GeofenceTimeline affiche « Véhicule : … ».
Vérifié : hub OK, timeline() expose device (null sur les 351 events antérieurs = normal ;
capturé pour les prochains passages). Build spa OK, md5 match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demande user : afficher les véhicules Traccar avec les initiales du TECHNICIEN assigné,
et ne retomber sur les initiales du device Traccar QUE si aucun tech n'est assigné au véhicule.
- hub /roster/tech-positions : part désormais de TOUS les devices Traccar (pas seulement ceux
liés à un tech). Pour chaque device localisé : techName si un tech y est assigné (via
traccar_device_id), sinon deviceName ; renvoie techName + deviceName + name résolu (tech>device).
Exclut l'île nulle (0,0 = jamais localisé). Live : 23 positions (14 tech, 9 device-only).
- RouteMap : libellé marqueur = initials(techName || deviceName || name) (tech prioritaire).
- PlanificationPage dayLivePositions : les véhicules SANS tech s'affichent aussi (gris),
filtrés par visibilité seulement s'ils ont un tech.
PlanificationPage.vue = co-édité → DÉPLOYÉ (build) mais NON commité (hunk à isoler).
Vérifié : endpoint live OK ; build spa OK, md5 match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le module carte du job (JobMapModule, réutilisé volet Tournées + détail ticket) avait
un repère + tracé GPS Traccar mais AUCUN itinéraire in-app (seulement un lien Google
externe). Ajout d'un toggle « Itinéraire routier » : trace le trajet routier RÉEL via
l'OSRM auto-hébergé (roster.osrmRoute → geometry) sur la carte MapLibre + affiche km/min,
et cadre la vue sur le trajet. Départ = position live du tech assigné (traccarLive),
sinon géoloc du navigateur ; destination = coords du SERVICE (GPS de l'adresse de service,
désormais fiables). Lien « Navigation » Google conservé pour la voix sur téléphone.
Réutilise l'infra existante (OSRM /roster/osrm-route, MapLibre/OSM auto-hébergé, ZÉRO
Mapbox). Vérifié : build spa OK, 0 erreur HMR. Rendu live (fiche/ticket) = SSO côté prod.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug (capture LEG-254850 « Patrick Leclair », qui a DÉMÉNAGÉ Saint-Louis→Franklin) :
l'adresse affichée + le pin carte pointaient l'ANCIENNE propriété. Cause : buildJob
prenait address+coords de la DELIVERY legacy (t.dv_*, périmée), alors que
service_location est apparié par la VILLE du ticket (= l'adresse pour laquelle le
billet a été créé).
Fix : on écrase address+coords par la Service Location LIÉE UNIQUEMENT si la delivery
correspond à une AUTRE Service Location DU MÊME CLIENT (déménagement/multi-adresses).
Un SITE EXTERNE (ex. cabane à sucre) ne matche aucune SL → on garde l'adresse du billet.
+ postal_code au fetch SL. Discriminant via _slCache (déjà peuplé, pas de re-fetch).
Backfill prod : 519 jobs open/assigned → 15 « propre-autre-adresse » corrigés (pin <1 km
d'une autre SL du client), 26 sites externes gardés, ~31 même-ville laissés (précision).
LEG-254850 corrigé à part (→ Franklin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le copilote STAFF peut répondre à toute question client. 5 lecteurs ajoutés
(audience:staff), réutilisant les getters du répondeur client (agent.execTool) :
- customer_overview : résumé 360 (identité + SOLDE + abonnements + tickets ouverts)
- customer_balance : solde du compte + factures récentes
- customer_subscriptions / customer_equipment
- customer_history : tickets passés → « un problème est-il déjà survenu ? »
Client résolu via find_customer (id C-xxxx) ; prompt mis à jour (règle QUESTIONS CLIENT).
Corrige aussi un bug LATENT : geminiChat n'avait pas reasoningEffort:'none' →
avec 35 outils le « thinking » de gemini-2.5-flash mangeait maxTokens et renvoyait
une réponse VIDE (« Aucune action détectée »). Thinking coupé + maxTokens 900→1200.
Vérifié live : « solde de C-LPB4 » → « 0 $, aucune facture impayée » ; « résume le
compte » → 360 (Louis-Paul Bourdon, solde, forfaits, 4 tickets) ; « un problème
déjà arrivé ? » → « 7 problèmes, dont 4 ouverts ». Régression OK (check_service, suspend).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remplace le 2e champ « rechercher un client » du chat par une liste d'options
que l'ASSISTANT propose DANS le fil (comme un menu de désambiguïsation). Plus
naturel, plus simple : on écrit une seule chose, et si le client est ambigu
l'assistant liste les candidats probables — un clic les envoie comme tour suivant.
Backend (staff-agent.js + agent-tools.json) :
- lecteur find_customer(query) → CANDIDATS via /collab/customer-search (réutilise
l'endpoint de l'ancien typeahead). 1 candidat → l'assistant prend l'id ; >1 →
il demande « lequel ? » sans réénumérer (l'UI affiche la liste).
- execToolPlan capte les candidats (find_customer + check_service ambigu) dans
ctx.options ; plan() renvoie { options } ; prompt : règle « identifier le client ».
Frontend (OrchestratorDialog.vue + MainLayout.vue) :
- retire le q-select entitySearch (+ la prop et le binding MainLayout) ; composer
= un seul champ. Rend d.options en q-list cliquable ; pickOption envoie
« label (id) » comme tour suivant ; options effacées à l'envoi/au reset.
Vérifié live (dev→hub prod) : « redémarre le modem du client Tremblay » → 1 champ,
8 options cliquables + « lequel ? » ; clic « Tremblay (Filtr-Aqua) Sylvain » →
tour « … (C-SYLVT…) » envoyé, options effacées, l'assistant continue ; 0 erreur console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Louis : un reboot coupe l'accès du client au moins 3 min (pas ~2). L'aperçu de
device_action(reboot) le dit désormais clairement et reboot/speed passent en
severity 'high' (l'ONU redémarre = vraie coupure), aux 2 chemins (TR-069 tech-3
+ SNMP tech-2). Rappel : le reboot ne part JAMAIS sans clic Confirmer humain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le reboot d'olt-ops = SNMP Raisecom (tech-2) seulement → pour un ONU tech-3
(TPLG, la plus grosse flotte) device_action(reboot) tombait en « non exécutable »
et l'assistant proposait un déplacement. Ajout : devices.rebootBySerial(serial)
(résout le n° de série → _id ACS via fetchDeviceDetails, pose une tâche GenieACS
{name:reboot} + connection_request). device_action route le reboot vers TR-069
(params.via=tr069) quand olt-ops ne peut pas ; sinon SNMP (tech-2) inchangé.
Toujours staged → confirmation humaine avant le moindre redémarrage.
Vérifié live (staging, aucun reboot tiré) : « redémarre le modem de C-LPB4 »
→ device_action staged « Redémarrer le modem (TR-069) — ONU TPLGC4160688 ».
Résolution série→ACS validée (TPLGC4160688 → E4FAC4-Device2-…). 1er reboot réel
= déclenché par l'utilisateur via le bouton Confirmer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
techGaps prend un bufferH optionnel : rétrécit chaque trou d'un tampon de
trajet (15 min) aux bords qui touchent un JOB existant (pas aux bords début/
fin de quart) → on n'offre plus au client un RDV collé entre 2 interventions.
Appliqué à bookingSlots (offres client ; 0 si ignorePolicy = confirm/interne)
et fitBooking (Option B : la dispo proposée par le client doit laisser le
trajet). firstFitStart (drag-drop) garde bufferH=0 → placement inchangé.
Vérifié live : /book garde 20 offres agrégées ; 694 créneaux/tech avec tampon
vs 850 sans → les créneaux serrés entre 2 jobs ne sont plus proposés.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le trajet entre jobs était compté comme du temps LIBRE → un tech dont la
journée est pleine de route entre villes (ex. Saint-Anicet ↔ Huntingdon)
paraissait « avoir de la place ». Corrigé aux 2 endroits :
- techOccupancy (bandes « Trouver un créneau » + resource_availability de
l'assistant) : free_h = quart − occupé − TRAJET. Le trajet est estimé le
long de la tournée (jobs pointés dans l'ordre horaire puis jobs sans heure
à la suite — sinon une journée de jobs legacy sans heure paraît sans route),
borné au temps restant (jamais négatif). Nouveaux champs travel_h (jour+total).
- suggestSlots : réserve le trajet RÉEL — arriver depuis le job précédent ET
repartir vers le job suivant (avant : buffer fixe 15 min à l'avant seulement,
le travelMin calculé n'était qu'affiché). Bord = début/fin de quart → 0 ;
bord = job → trajet estimé (coords) ou repli 15 min si coords du nouveau job
inconnues. Un créneau n'est offert que si le trou contient arrivée+job+départ.
Helper travelH() partagé (barème existant ≈ dist×1,5 min/km, borné 5–90).
Vérifié live : Houssam Nabout lun 20/07 (5 jobs 2 villes) passe de « 1 h libre /
90% » à « 0 h libre / 100% plein » ; techs à 1 job ou même lieu → trajet 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le chat STAFF peut désormais lancer les 7 opérations OLT à distance (Do Stuff)
par langage naturel : reboot · speed · suspend · unsuspend · activate · replace
· remove. Un seul outil device_action(action, serial|customer_id, [profileid],
[new_sn]) → résout l'ONU du client au besoin, APERÇU via olt-ops.plan (effet,
cible, avertissements, can_run), puis exécution via olt-ops.run.
Sécurité (écriture réseau conséquente) :
- staged comme WRITE → aperçu + bouton « Confirmer et exécuter » (jamais auto).
- double-verrou : confirm humain + run({confirm:true}) du module + idempotencyKey
(fixé au plan → un double-clic rejoue la même clé) + acteur journalisé.
- permission reboot_provision (capacité « Équipement » existante, assignable).
- can_run honoré : une action non branchée pour la techno de l'ONU (ex. reboot
tech-3 = TR-069) n'est PAS proposée — l'assistant l'explique.
- prompt : APPELER l'outil EST la proposition (pas de « voulez-vous ? » en texte).
Vérifié live : NL « suspends l'internet de C-LPB4 » → device_action(suspend)
staged, severity high, cible TPLGC4160688/OLT résolue. GET /staff-agent/tools
liste device_action (15 outils).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- OrchestratorDialog restructuré en vraie fenêtre de chat : carte à hauteur
fixe (76vh), en-tête + conversation qui défile + composer épinglé en bas.
On écrit SOUS la discussion (comme tout chat), l'input se vide à l'envoi,
auto-défilement vers le bas à chaque nouveau message. Confirmation du plan
déplacée en ligne dans le fil (plus de pied de page). État initial « Comment
puis-je aider ? » + exemples. Recherche client/adresse intégrée au composer.
- MonAujourdhui : « Allouer un technicien » → « Assistant » (icône auto_awesome),
ouvre le chat vierge ; visible aussi pour les rôles Boîte de réception.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complète le diagnostic : nouveau lecteur check_service (customer|query) → état LIVE du service via ticket-collab.serviceStatus
(modem/ONU en ligne/hors ligne, Rx dBm, abonnement) + conseil « correctif à distance vs déplacement ». Prompt : pour un
problème de CONNEXION (pas d'internet / lent / signal / hors ligne), appeler check_service AVANT de proposer un déplacement
— si en ligne → tenter un redémarrage à distance ; si hors ligne + signal faible/nul → déplacement (réparation) justifié ;
annoncer le constat (en ligne/hors ligne · Rx) avant de recommander. Évite les camions inutiles.
Vérifié : check_service C-LPB4 → « Hors ligne » (tr069) ; outil listé dans /staff-agent/tools. Déployé, hub sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avant : l'assistant posait une question (« Souhaitez-vous que je crée… ? ») mais le dialogue était mono-coup — impossible
de répondre (pas de fil, chaque envoi repartait de zéro). Maintenant c'est une conversation :
- Backend staff-agent.plan(text, email, history) : accepte les tours précédents (user/assistant, 10 derniers) → contexte ;
POST /staff-agent {text, history}.
- OrchestratorDialog : fil de bulles (user/assistant), le champ se vide après envoi pour RÉPONDRE, historique renvoyé à
chaque tour, bouton « Envoyer » en mode conversation. Le plan (actions à confirmer) suit le dernier tour.
Vérifié : (backend) « vérifier signal C-LPB4 » → diagnostic (réparation, 12 techs/431h) ; « oui, crée » AVEC history →
propose create_job(C-LPB4, Réparation). (UI) 4 bulles [user,assistant,user,assistant], champ vidé, action « Créer un job »
affichée, 0 erreur console. Déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complète la boucle diagnostic (raison→compétence→RESSOURCE dispo→créneau) demandée : nouveau lecteur resource_availability
(skill[, after_date, days]) → nb de techs QUALIFIÉS + heures libres sur la période (via dispatch.techOccupancy). Prompt mis
à jour : après resolve_skill, appeler resource_availability (combien dispo) puis find_slot (créneaux) avant de proposer.
Vérifié : skill=réparation → 12 techs, 431 h libres/7j ; outil listé dans /staff-agent/tools. Déployé, hub sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suite au « Failed to create dispatch job » : au-delà du fix nextJobRef (6022cdf), on rend l'assistant plus INTERACTIF
et on capte l'adresse tapée :
- create_job accepte désormais 'address' (texte libre) → transmis à agentCreateDispatchJob → stocké sur le Dispatch Job
(avant, l'adresse dictée était perdue si pas de lieu de service lié). Vérifié : job.address = « 2338 rue Ste-Clotilde ».
- Nouveau lecteur resolve_skill (raison → compétence(s) requise(s) via skill-resolver) → l'agent DIAGNOSTIQUE la ressource
nécessaire. Vérifié : « voir signal fibre » → installation.
- Prompt système : pour une intervention, procéder par étapes — (1) cerner la raison (question si vague), (2) resolve_skill,
(3) find_slot (dispo réelle), (4) SEULEMENT ensuite proposer create_job/assign_tech/proposer_rdv_client + résumer le
diagnostic avant l'action. L'agent ne saute plus à la création.
Déployé (3 fichiers), hub sain, resolve_skill listé dans /staff-agent/tools.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug : nextJobRef() faisait 'for..of r.data' alors que la liste ERPNext est sous r.data.data (comme partout ailleurs
dans dispatch.js). r.data étant un objet → 'object is not iterable' → catch → repli ymd+'-001' à CHAQUE appel. Donc le
1er job du jour passe, les suivants COLLISIONNENT sur le même ticket_id (autoname) → POST rejeté → createDispatchJob
lève 'Failed to create dispatch job'. Touchait TOUTE création via createDispatchJob (agent NL create_job, /dispatch/
create-job, chaînes d'installation), pas seulement l'assistant.
Fix : itérer r.data.data. Vérifié : 2 créations consécutives → refs 20260719-001 puis -002 (distincts, incrémentés),
plus d'erreur 'not iterable'. Déployé, hub sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L'action ouvrait déjà le copilote Gemini (staff-agent) mais sans autocomplétion → il fallait taper le client exactement.
Ajout d'un typeahead d'ENTITÉ dans le chat (OrchestratorDialog, prop entity-search) : on tape → suggestions live
(nom · adresse · téléphone · courriel via /collab/customer-search) → le choix INJECTE l'entité RÉSOLUE « client <id>
(<nom> — <adresse>) » dans la demande, que Gemini extrait pour agir (proposer_rdv_client / assign_tech). Activé sur le
copilote STAFF (MainLayout entity-search) ; graine « Allouer un technicien : » (MonAujourdhui). Séparateur propre après « : ».
Vérifié en dev : « tremblay » → 8 suggestions → choix → prompt « Allouer un technicien : client C-SYLVT… (Tremblay…) » ;
endpoint /collab/customer-search OK (12 matches) ; 0 erreur console. Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Réconciliation de l'audit : isole et commite UNIQUEMENT mon travail déployé-non-commité dans 7 fichiers co-édités,
sans toucher au working tree (donc sans capturer le travail en cours des autres sessions, qui reste non commité) :
- server.js : SSE event 'ready' + retry:3000 ; routes /olt/onu/plan|run + /olt/wifi-clients.
- conversation.js : endpoint /conversations/my-inbox-counts (accueil : en attente / suivis / en retard).
- useConversations.js : armSSE (reconnexion+resync+repli poll 25s) + cleanup.
- PlanificationPage.vue : chips géofence (Lane 1c) + sélecteur « Générer l'horaire » (Move 3) + isLate (arrivée en retard).
- SettingsPage.vue : section « Thème & couleurs » (ThemeEditor).
- IssueDetail.vue + ConversationPanel.vue : props source-issue/customer/service-location/phone (« Proposer au client »).
EXCLUS (restent non commités, propriété d'autres sessions) : /service-location, /conversations/msg-visibility, retrait
champ Mapbox, fix conv-message 'belongs', hiérarchie parent_incident, JobMediaModule, etc. Vérifié : 0 marqueur étranger
dans le staged. Tout est déjà LIVE ; ceci n'aligne que le dépôt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Composant autonome créé lors de la consolidation Thème→Paramètres (redirect routeur /theme→/settings déjà commité en
23f0f72). Fichier 100% mien, sans mélange → seul élément sûr à commiter de l'audit ; le point de montage (import dans
SettingsPage.vue) reste non commité car ce fichier est co-édité (voir rapport d'audit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L'accueil ne chargeait qu'au montage. Ajout : refetch toutes les 60 s + immédiat au retour sur l'onglet
(visibilitychange), avec nettoyage à onUnmounted. dash() conserve la valeur précédente pendant le refetch → pas de
clignotement vers « … ». Les compteurs « en attente de réponse / en retard / interventions » restent à jour sans
rechargement. Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avant : une fois confirmé, le lien /book était un cul-de-sac (« déjà confirmé »). Maintenant le client peut, depuis SON lien :
- « Modifier mon rendez-vous » → ré-ouvre le sélecteur (mêmes créneaux) → nouveau choix écrase l'ancien.
- « Annuler mon rendez-vous » (2 clics de sécurité) → POST /book/api/cancel : libère le créneau, repasse le job en
« À reporter » (open, sans tech/heure) pour recontact ; le token reste valide (« Reprendre un rendez-vous »).
Réduit les appels au support pour reporter/annuler. Vérifié : les 3 états (confirmé→Modifier→sélecteur ; annuler 2 clics→
annulé→reprendre), 0 erreur console ; live à msg.gigafibre.ca/book, /book/api/cancel token-gated (404 sans token). Hub sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
À la confirmation d'un créneau (confirmWindow, donc Option A « choisir » ET Option B « proposer 3 » qui aboutit), on
texte le client : « votre rendez-vous technique est confirmé : <date> à <heure> (technicien X) ». Déclenché par SA
sélection (transactionnel), fire-and-forget (ne bloque pas la réponse). Complète le .ics — le client a une trace immédiate.
Téléphone résolu depuis Customer.mobile_no. Déployé, hub sain ; pas de test d'envoi réel (client réel).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Après confirmation d'un créneau (Option A), le client obtient un bouton .ics (Apple/Google/Outlook) pré-rempli :
date/heure + durée, « Rendez-vous Gigafibre — <service> », technicien en description, adresse en LOCATION. Réduit les
oublis/no-shows côté client. Heure flottante (fuseau de l'appareil = Québec). Vérifié : flux confirmer→lien, .ics
bien formé (DTSTART/DTEND/SUMMARY), 0 erreur console ; live à msg.gigafibre.ca/book. Hub redémarré, sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enrichit la carte « Interventions aujourd'hui » (au-delà du simple découpage par statut) : croise l'heure de début
prévue avec l'état géofence live → pastille ROUGE « N en retard » quand le début est passé de > 20 min mais le tech
n'est PAS montré sur place (ni parti). Signal proactif de retard/no-show pour le répartiteur. Réutilise /roster/geofence-states.
Propre (composant seul, aucun changement hub). Vérifié en dev : rendu 3 cartes, aucune erreur console (0 job aujourd'hui
en dev → pastille masquée, correct). Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Le décompte « exact » a révélé que la métrique elle-même était faible : _assign ERPNext quasi vide ici (1726 « open »
tous non assignés). Corrigé pour refléter la RÉALITÉ, via /conversations/my-inbox-counts :
- Boîte : « en attente de réponse » = conversations actives dont le DERNIER message vient du client (212) + contexte
« N actives » — vraiment actionnable, vs « 540 total » ou « 1726 tickets ouverts ».
- Tickets sur moi : basé sur le SUIVI (le vrai « Mes tickets » d'OPS), pas _assign ; + « en retard » (ouvert > 48 h) en rouge.
Endpoint hub my-inbox-counts (conversation.js, déployé — co-édité, non commité ici). Vérifié en dev : 212 en attente / 540
actives / 0 suivis, 0 erreur console. Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 3/3. Avant : '/' = « Tableau de bord » réservé (requires view_dashboard_kpi) → un agent de service n'avait pas
d'accueil pertinent. Maintenant '/' = « Accueil » visible par tous ; l'accueil PERSONNEL (MonAujourdhui) s'affiche pour
chacun, et les sections GESTION sont gated dans la page :
- KPI (stats) + Administration + Activité récente → can('view_dashboard_kpi')
- Charge 2 semaines (occupation) → can('view_all_jobs')
Résultat : un CSR voit un accueil épuré (sa boîte / ses tickets) ; un gestionnaire garde le tableau complet.
Embarque aussi la consolidation déjà déployée que ces fichiers portaient : Équipe + Thème → Paramètres (nav retirée +
routes redirigées vers /settings).
Vérifié en dev : nav « Accueil » (plus « Tableau de bord »), sections gestion rendues pour superuser, MonAujourdhui présent,
0 erreur console. Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 2/3. Avant : zéro créneau en ligne → cul-de-sac « nous vous contacterons ». Maintenant : le client saisit
jusqu'à 3 disponibilités libres (date + Matin/Après-midi/Soir) → soumises en mode:rank → fitBooking tente de placer,
sinon enregistre en 'Proposé' pour le répartiteur. Le client n'est jamais coincé.
Vérifié : repli (mock empty) rend le formulaire + soumet OK ; aucune régression avec créneaux (8 tuiles + bascule 2 modes) ;
0 erreur console ; live à msg.gigafibre.ca/book (marqueurs présents). Hub redémarré, sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 1/3 des améliorations. AvailabilityByReason (dialogue partagé, monté dans le ticket ET la conversation) n'offrait
que « Trouver un créneau » (assigner soi-même). Ajout du 2e chemin — laisser le CLIENT choisir — là où le diagnostic se fait :
- Bouton « Proposer au client » → api proposeAppointment → hub /roster/book/propose : résout OU **crée** un Dispatch Job
ouvert (via createDispatchJob) puis envoie le lien /book par texto. Toast avec « Copier le lien » si pas de SMS.
- roster.js : /roster/book/propose crée le job ouvert si aucun (client ou sujet requis) + pose required_skill.
- Props de contexte passées par les 2 panneaux (source_issue/customer/service_location/phone) — édition additive des
panneaux co-édités = NON commitée ici (déployée via le bundle).
Vérifié : endpoint 404 attendu sans contexte ; « Proposer au client » compilé dans le bundle ; build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applique « ne pas réutiliser une UX/métrique faible telle quelle » :
- Boîte de réception : remplace le compteur de session 'unread' (démarrait à 0, trompeur) par un VRAI décompte via
/conversations/inbox-tickets — « à trier » (non assignés) + total ouvert. Tickets sur moi = _assign contient mon courriel.
- Interventions aujourd'hui : ajoute l'ÉTAT TERRAIN LIVE (en route / sur place via /roster/geofence-states, pastilles
qui pulsent) en plus du découpage par statut.
- Barre d'ACTIONS réelles : « Allouer un technicien » → ouvre le copilote NL PRÉ-REMPLI (event ops:assistant → MainLayout),
« Nouvelle vente », « Rechercher ⌘K ». Le pont ops:assistant est ajouté dans MainLayout (propre).
- États chargement (« … ») / vide, focus clavier, prefers-reduced-motion.
Vérifié en dev : rendu, 199 à trier (donnée réelle), event allocate + palette OK, 0 erreur console. Build OK, leak-clean, déployé.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Concrétise « chacun voit ses outils, pas les 24 » : bandeau personnel dont CHAQUE carte est gated par capacité —
- Ma boîte (view_clients) : notifications non lues → /communications
- Tickets sur moi (view_all_tickets|view_own_tickets) : Issues ouvertes filtrées sur _assign = mon courriel → /tickets
- Interventions aujourd'hui (view_all_jobs) : Dispatch Jobs du jour groupés par statut → /planification
Salutation + date FR localisées (America/Toronto). Données via endpoints EXISTANTS (erp listDocs + cloche), filtrage
« à moi » côté client, dégradé gracieux (« — ») si indispo. Additif (monté au-dessus de OutageAlertsPanel), styles --ops-*.
Vérifié en dev : rendu + 3 cartes + salutation nommée + 0 erreur console. Build OK, leak-clean, déployé.
Reste (différé) : atterrissage par rôle au niveau ROUTAGE (les non-managers arrivent ailleurs que sur '/') + sources
« à moi » dédiées (non-lus par canal, mes jobs par tech) au lieu du filtrage client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pont agent→client vers la page /book livrée : au lieu d'assigner soi-même, le staff laisse le CLIENT choisir.
- Hub POST /roster/book/propose : résout le job ({job}|{ticket}|{customer}) → assure booking_token → envoie le lien
/book par texto (formulation « nouveau RDV »). 404 « créez d'abord l'intervention » si aucun job ouvert.
- Outil NL proposer_rdv_client (agent-tools.json + WRITES staff-agent.js, plan→confirm→run) → l'agent le déclenche
depuis ⌘K en langage naturel. staff-agent était DÉJÀ déployé (assign_tech direct + find_slot existaient déjà).
Vérifié live : outil listé dans /staff-agent/tools ; /roster/book/propose répond (404 attendu sans job).
Reste (différé, nécessite édition des parents co-édités → deploy-only) : bouton « Proposer au client » in-ticket/conversation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Réécrit BOOK_HTML : marque Gigafibre (vert #00C853, esprit gigafibre.ca) au lieu du bleu générique, mobile-first, 2 modes
via bascule segmentée —
- « Choisir un créneau » (Option A) : sélection unique → hold EXCLUSIF 5 min avec compte à rebours visible → confirmer ;
à l'expiration le créneau redevient disponible (message + rechargement).
- « Proposer mes dispos » (Option B) : jusqu'à 3 créneaux classés → 1er possible confirmé, sinon enregistrés en 'Proposé'.
Consomme les endpoints publics /book/api/{options,hold,submit} (déjà déployés). Vérifié : rendu + les 2 flux (mock),
puis live à msg.gigafibre.ca/book (marqueurs présents). Hub redémarré, sain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complète le backend RDV 2 voies (les 2 modes ont maintenant leur chemin, tous gardés F + hold 5 min) :
- POST /book/api/hold : hold EXCLUSIF 5 min à la sélection d'un créneau (Option A) ; release possible ; retour au
pool à l'expiration (bookingSlots re-soustrait les holds vivants).
- POST /book/api/submit mode:'pick' : le client confirme le créneau choisi → confirmWindow (garde F : si le ticket
est déjà assigné à un autre tech dans F, enregistré en 'Proposé' au lieu d'écraser).
- (mode:'rank' = Option B « le client propose 3 dispos » inchangé : fitBooking → confirmWindow.)
Routes PUBLIC via le préfixe /book existant (aucun changement server.js). Vérifié live : token bidon → 404 lien invalide.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Découverte du mapping : la confirmation de RDV (confirmWindow + POST /roster/book/confirm) écrivait assigned_tech
SANS la garde « déjà assigné dans F » que /roster/assign-job applique → une confirmation client (ou staff) pouvait
écraser une assignation faite manuellement dans F. Même classe que le bug double-assignation.
- fConflict(legacyId, techId) : garde PARTAGÉE extraite (ticketAssignState), réutilisée par les 3 chemins d'écriture
(assign-job refactorisé pour l'utiliser → plus de dérive).
- confirmWindow (chemin CLIENT, pas de force) : si conflit F, N'ÉCRASE PAS — enregistre le créneau choisi
(booking_status Proposé) et laisse le répartiteur confirmer le bon tech. Message client « nous confirmerons sous peu ».
- POST /roster/book/confirm (chemin STAFF) : 409 conflict sauf force=true → l'OPS réaffiche « déjà assigné dans F à X ».
- hold_minutes défaut 10 → 5 (décision Louis) : réservation exclusive 5 min → retour au pool → indicatif (revalidé
à la confirmation via la garde ci-dessus).
Déployé (hub redémarré, sain, SSE clients se reconnectent via 187115c/89f7efc).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Même classe de bug que 187115c, sur deux flux SSE qui manquaient la reconnexion :
- TechTasksPage (app terrain) : AUCUN onerror → au restart du hub (502→CLOSED) le technicien perdait TOUT le
temps réel (nouvelles tâches, changements de statut) jusqu'au rechargement manuel. Ajout reconnexion 3 s + resync
loadTasks() à la RE-connexion (le hub ne rejoue pas les événements manqués).
- OutageAlertsPanel : au restart, tombait en repli poll 30 s et n'en sortait JAMAIS (ni retour temps réel, ni
resync immédiat). Ajout reconnexion 3 s + à la RE-connexion : arrêt du poll + fetchActive() pour rattraper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cause (diagnostic) : docker restart du hub à chaque déploiement détruit tous les clients SSE en mémoire. Les flux courriels/
cloche utilisaient un EventSource brut SANS onerror/reconnexion → le 502 pendant le restart les met CLOSED définitivement
(plus de reconnexion native) → morts jusqu'au rechargement manuel. Le dispatch se reconnectait mais ne resynchronisait jamais.
Fix (client) :
- useSSE : nouvelle option onReconnect() appelée à chaque RE-connexion (pas au 1er open) → resync après coupure.
- useNotifications (cloche) : reconnexion auto si le flux meurt (CLOSED → relance 3 s).
Côté co-édité déployé via bundle (non commité ici) : useConversations (inbox) = reconnexion + resync fetchList + repli poll 25 s
sur les 3 connexions SSE ; PlanificationPage = onReconnect → reloadOccupancy+reloadPool ; server.js = event 'ready' + retry:3000.
Hub 'ready' vérifié live. Reste (durabilité, différé) : persister _watchSnap pour rejouer les deltas Legacy manqués pendant la coupure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lane 3 (durcissement) : OnuActionsPanel (suspendre/rétablir/redémarrer/vitesse/remplacer/retirer — écritures réseau live,
suspendre coupe le client) était monté SANS permission dans EquipmentDetail. Désormais gaté can('manage_settings') + sorti
du bloc olt_name (garde propre serial+olt_ip → visible même sans olt_name). Bouton « Supprimer cet équipement » aussi gaté.
(Panel déjà monté depuis un travail antérieur ; ici = gating + placement.)
Lane 1c (déployé via bundle, PlanificationPage co-édité non commité) : puces géofence live En route/Arrivé/Reparti sur les
cartes Jour (kanban) + lignes éditeur de journée, via l'endpoint /roster/geofence-states existant mais jamais appelé
(geofenceStates + refreshGeofenceStates hooké dans reloadOccupancy). Lane 1a : le picker lead exclut déjà le tech assigné
+ garde 409 « déjà assigné dans F » → re-pick déjà empêché (aucun code risqué ajouté).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Priorité accessibilité (usage terrain, gants, luminosité variable — utilisable par une personne âgée / focus TDAH) :
- Type scale +, contraste renforcé (sous-textes #94a3b8→#aebccf), cibles tactiles ≥54-58px (action principale, boutons d'action, cartes).
- Liste : emphase du PROCHAIN arrêt (ruban vert + bordure/halo), compteur « X de Y terminé(s) », arrêts terminés atténués.
- Détail : NAV + action principale au-dessus de la ligne de flottaison — « 🧭 Démarrer l'itinéraire » (bouton bleu proéminent) puis « 📍 Je suis arrivé » (grand vert) + phrase d'aide selon l'état ; carte descendue en référence ; Street View en lien discret ; boutons Commenter/Photo/Appareil agrandis (icône+libellé empilés).
- focus-visible + micro-feedback tactile (reduced-motion respecté).
Vérifié live (Playwright mobile, token tech réel) : liste + détail rendus, carte OSM/MapLibre (0 Mapbox).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avant : on associait un GPS à un tech UNIQUEMENT depuis le popover du tech (vue Semaine). Ajout du sens inverse demandé :
partir de l'APPAREIL. Hub roster.js :
- GET /roster/traccar-devices : roster enrichi (appareil + tech(s) assigné(s) + unassigned + duplicate + dernier signal),
croise Traccar getDevices × Dispatch Technician.traccar_device_id. Vérifié live (45 appareils, ex. #4 → Benjamin Djanpou).
- POST /roster/traccar-device-assign {device_id,tech_id} : assigne un appareil→tech en LIBÉRANT d'abord tout autre tech qui
le détient (le champ vit sur le tech → sinon 2 techs pointeraient le même device ; le doublon est aussi signalé dans la vue).
SPA : GpsDevicesDialog.vue (q-table statut/appareil/dernier signal/technicien via TechSelect + filtre « non associés »),
ouvert par un bouton « Appareils GPS » dans la barre Tournées de Planification. api/roster : listTraccarDevicesRoster + assignTraccarDevice.
PlanificationPage (bouton + mount) = co-édité → déployé, non commité.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Précision coords (le levier amont unique : alimente géofence + matrice OSRM + solveur, tous lus à chaud) :
- fibreByDelivery(deliveryId) : AUTORITÉ #0 = le DROP d'installation exact via ticket.delivery_id → service → fibre.*_service_id
(placemark d'infra si lié, sinon coord fibre). Câblé dans buildJob (avant resolveDevCoords) ET geolocateJobs (refreshFibre,
seuil de réécriture 30 m car drop exact). Le tick horaire l'applique tout seul désormais.
- BACKFILL exécuté : 15 jobs actifs corrigés (14 fibre_service_placemarks) — dont LEG-247012 (7,5 km !) et LEG-254147 (14 km) qui
étaient grossièrement mal géocodés → snappés au drop exact. Idempotent (re-run 0).
Hardening auto-dispatch (audit) :
- osrmMatrix : un nœud SANS coords n'est plus à 0 min de tout (« adjacent gratuit » → insertion n'importe où) mais à 180 min
(« loin/inconnu ») → le solveur ne chaîne plus gratuitement les jobs sans coords au milieu des vrais arrêts.
Restent des jobs sans source coord (ex. lots camping Lac des Pins sans ligne fibre) = data gap F, pas résolvable ici.
Suivi recommandé (non fait, flag) : valider bornes QC sur les chemins de LECTURE (pas que l'ingestion) ; retirer les défauts
tech silencieux (Montréal/Ste-Clotilde) ; sortir les jobs sans coords du solveur vers le pool « à situer » ; invalider le cache
matrice SPA quand les coords changent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diagnostic (Nathan Morrisseau 2026-07-18) : 3 jobs où le tech était à 8-13m pendant 37-166 min sont restés « En route »,
jamais « Arrivé ». Cause : le scan live n'accroche « on_site » que si le hub tourne 3 min EN CONTINU avec le tech dans le
rayon, et le compteur de séjour (enterMs) était en mémoire, persisté SEULEMENT à un changement d'état → tout redémarrage du
hub le remettait à zéro. 18/07 = 0 arrivée pour TOUTE la flotte (seul jour), alors que « en route » (1 tick) s'accrochait.
Fixes (les 3 demandés) :
- FIX 1 dwell persisté : runScan marque le store 'dirty' quand enterMs change → survit aux redémarrages.
- FIX 2 passe TRACE rétrospective : reconcileFromTrack(date) rejoue l'historique GPS Traccar complet du jour (traccar.getHistory
ajouté) et DÉRIVE arrivée/départ → insensible aux redémarrages/ticks manqués/bursts. Planifiée /15 min sur le jour courant
(scan live conservé pour la réactivité UI) + route GET /roster/geofence-reconcile?date=&dry=1 (backfill/diag).
- FIX 3 rayon 150→250m (+ sortie 230→350m), aligné sur l'arrivée-auto de l'app terrain → couvre le géocodage rural imprécis.
Vérifié live : backfill 18/07 → LEG-254929/255009/255042 = Arrivé+Reparti (source track), idempotent (re-run 0/0), today OK.
Restent en_route : LEG-254770 (187m, séjour <3min) + LEG-255029 (579m, coords à corriger) = imprécision géocodage, pas géofence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- field-app.html (app tech) : carte Mapbox GL → MapLibre GL + tuiles raster OSM (aucun jeton). C'était le DERNIER Mapbox vivant.
- NOUVEAU « 🚀 Meilleur trajet du jour (trafic en direct) » : ordre optimal des arrêts via NOTRE OSRM /table + plus-proche-voisin (le plugin /trip n'est pas compilé), puis handoff à Google Maps multi-arrêts → trafic EN DIRECT gratuit (OSM seul n'a pas de trafic). Endpoint hub GET /field/route?t=&from=lat,lon.
- config/erpnext.js MAPBOX_TOKEN vidé + JobEditModal (module mort) : vignette statique api.mapbox.com → lien OSM. Bundle SPA désormais SANS jeton Mapbox ni api.mapbox.com (vérifié).
- serveFieldApp : retrait de l'injection du jeton Mapbox.
Vérifié live : field-app rendu = MapLibre (0 api.mapbox.com) ; OSRM /table Ok ; ordre A→C→B correct depuis origine ouest ; /field/route 200. Reste (co-édité, déployé non-commité) : tooltips PlanificationPage « Mapbox »→« OSRM/OSM » + retrait carte réglages Mapbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- extrait n8nRequest(verb,url,body,timeout) partagé par dostuff() et wifiClients() (fin de la double implémentation https-GET-with-body)
- effets déplacés dans ACTIONS[action].effect → supprime l'échelle de if dans plan()
Comportement préservé (vérifié live : plan tech-3 suspend identique, gardes run inchangées). Déployé prod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>