Régression du lot précédent : `.tech-col { width:360px; overflow:hidden }` coupait l'œil
quand le contenu dépassait 360px (ex. groupe « Vente porte-à-porte », flex-shrink:0 sans max-width).
- retire la largeur fixe + overflow:hidden sur .tech-col (la colonne s'ajuste, l'œil reste visible)
- .grp tronque (max-width 90px + ellipsis, rétrécissable) → ne pousse plus l'œil
- .hide-eye épinglé (flex-shrink:0) au bout de la rangée
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug Gigafibre.ca — corrigés/ajoutés :
- P1 réassignation « Non assignés » (le menu placeholder n'exige plus l'union des compétences → assignable à la main, ⚠ par job)
- P2 tech en congé/vacances exclu du pool de dispatch (garde suggestUnavailable + pastille congé)
- P3 cadence MASQUÉE par défaut (tous 100 %), révélée seulement si nettement plus lent/rapide
- P5 l'auto-save (draft) ne supprime plus un quart PUBLIÉ (cause du « publié puis à republier »)
- P6 récap jobs simplifié (type·ville·rue·AM/PM·#) dans la modale Techniciens disponibles
- P8 plages perso Jour/Soir PAR TECH (presets personnalisables, ex. 7:30–17:30)
- A1 re-clic sur un tech (q-menu no-parent-event) · A4 type de job au survol · A5 « Vider » visible
- A3 modem/ONU/remplacement → réparation (skill-resolver = source unique ; copie legacy périmée supprimée)
- A6 clip colonne Ressource + label de rôle (barre de défilement horizontale proportionnelle)
- A7 techs par compétence (survol) · A8 « Donner à » trié par compétences en commun
Note : ces fichiers portaient déjà des changements déployés-non-commités ; ce commit les RÉCONCILIE
avec le lot ci-dessus (co-édition — cf. reference_coedit_uncommitted_audit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unify the wireless (airOS) device view onto the shared statCards grid — signal
(colored + meter), signal côté borne, CCQ, débit lien TX/RX, capacité airMAX,
SSID, fréquence, uptime, gestion, point d'accès (link) — same glanceable cards as
fiber, replacing the old chip row.
"See what's happening" — the three things F shows, now in OPS:
- actual traffic: live eth0 sparkline (already present)
- DHCP leases = the devices behind the CPE: ops_airos.php mode=leases SSHes the CPE
(port 2194) -> cat /tmp/dhcpd.leases -> {mac, ip, host, vendor(OUI), remaining};
hub airosLeases + GET /collab/airos-leases; UI renders an on-demand "Appareils du
client (DHCP)" table with refresh.
- wifi/wired clients = those leases.
Verified: leases for Bryson CPE -> Cisco-Linksys 172.31.1.19 (OUI vendor resolved),
through the hub too. Build clean, leak 0; deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redesign the device hero into a glanceable card grid — one card per key metric,
plain-language hints, color = health. Driven by a merged statCards model (live OLT
dostuff > GenieACS), so it works for both ACS-covered and tech-3 (no-ACS) ONUs;
each card renders only when its data exists.
Cards: Signal fibre (colored value + healthy-range meter + word: Bon/Faible/…),
Uptime, IP publique, Appareils (Wi-Fi/filaire breakdown, click → device list),
Réseau maillé (satellites), Gestion (GUI link), Téléphonie/VoIP, Télé/IPTV,
Distance, Profil de ligne, Firmware, Dern. coupure. Replaces the old dense tiles
in both the ACS topo card and the no-ACS fiber hero.
Build clean, leak 0; deployed. Design previewed with the Imagine widget first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fiber twin of the wireless-MAC issue: when an ONT is replaced, F's `fibre` table
holds the current serial + OLT coords, but OPS keeps the old serial → dostuff
queries a serial no longer on the OLT → shows "offline" for an online customer
(C-LPB4: OPS had TPLGC4160688/ontid 0; F fibre id 13890 = TPLG55684520/ontid 2).
- hub: fibreById() reads F `fibre` by the SE's legacy_fibre_id (hub reaches
gestionclient); reconcileFibreOnu() aligns serial_number/gpon_serial/olt_ip/
slot/port/ontid; fibreLive({equipment}) reconciles THEN dostuffs with the current
serial. GET /collab/fibre-live, POST /collab/fibre-sync, and syncFibreOnus()
(park-wide, batched) folded into the daily 03h tick alongside wireless.
- ops: the fiber hero + "Do Stuff en direct" now call /collab/fibre-live (auto
reconcile on load), reflect the corrected serial locally, and show "ONU
synchronisée depuis F (remplacement)".
Verified: fibreLive(EQP-0000011366) auto-corrected TPLGC4160688 → TPLG55684520
(logged), and dostuff on the current serial = online (Rx -14.39 / Tx -13.78, bon,
uptime 1h17). Build clean, leak 0; deployed index.d652a898.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fibre ONUs on unpolled OLTs (tech-3 XGS-PON, e.g. olt3/172.17.192.6) have no
GenieACS `device`, so the modern topo card AND the "Do Stuff en direct" block —
both gated on `device` — rendered nothing, leaving a stuck "Chargement ACS…", a
stale "Unauthorized" modem-diag, and static titles. But dostuff gives full live
status for these ONUs.
- New modern fiber hero (v-if="isFibre && !device"): topology strip (Internet→OLT→
ONT) + online/offline pill + colored Rx/Tx signal + distance/uptime/mgmt(link)/
WAN/VoIP/IPTV/firmware tiles + copy-for-ticket + refresh — driven by the live OLT
read (dostuff), auto-loaded on mount (2.5s grace for ACS to fill `device` first,
so ACS-covered ONUs keep their native card; no double fetch).
- Suppressed the misleading stuck "Chargement diagnostic ACS…" for fibre (hero owns
the state) and confirmed the heavy Playwright modem-diag stays v-if="false".
Verified: build clean, leak 0; dostuff on the shown ONU (TPLGC4160688 @ 172.17.192.6)
returns live state (Hors ligne, uptime 2d12h, mgmt/WAN/VoIP/IPTV) → hero populates.
Deployed index.42eea809.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fast page-load status (no slow status.cgi): ops_airos.php gains a `mode=status`
path that returns RADIUS-only state (online, since, current MAC, AP IP) in ~ms.
Hub airosStatus() + GET /collab/airos-status; EquipmentDetail loads it on mount for
wireless → "● En ligne depuis 9h · IP …" badge (with MAC auto-sync on this call too).
Daily park-wide reconcile (03h ET): ops_airos.php `bulk=1` returns every wireless
service's current radacct MAC in one call (2 queries + merge); hub syncWirelessMacs()
indexes Service Equipment by mgmt IP and updates only changed MACs. Scheduled via a
/30-min tick that fires once in the 03h ET window (WIRELESS_MAC_SYNC=off to disable),
started lazily on first request (not at require → no timer in CLI/tests). Manual
trigger: POST /collab/airos-mac-sync.
Fixed erp.update success detection (returns {ok:false}, never throws) in autoSyncMac
+ syncWirelessMacs — counts are now accurate. Verified: reconcile bulk 351 / checked
333 / updated / errs=18 = pre-existing broken service_location links (flagged
separately, non-blocking). Deployed hub+F+SPA (index.5444d59b); leak 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MAC updates now flow into OPS automatically. On any signal read (wireless panel
OR the assistant's check_signal), the hub's airosSignal resolves the Service
Equipment record and, if the RADIUS-authoritative MAC (radacct.callingstationid,
auto-updated on WPA2 re-auth) differs from the stored mac_address, it updates
Service Equipment server-side — idempotent, fill/correct, no click.
UI shows a green "MAC synchronisée automatiquement depuis RADIUS (était …)" note
and reflects it in the Identity field; the manual button remains only as a fallback
if the auto-update can't run. Manual serial/MAC edit (InlineField) still available.
Verified: corrupted EQP-0000100001 mac to 00:00:00:00:00:00 → one airosSignal read
self-healed it to RADIUS 24:5A:4C:30:CE:AA (logged). SPA deployed; leak-check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RADIUS MAC (user's ask — auto-follows equipment swaps): ops_airos.php resolves the
service's radius_user (device→service) and reads the latest radacct session →
callingstationid = current CPE MAC (auto-updated on WPA2-RADIUS re-auth) and
nasipaddress = the AP IP (reliable — replaces the flaky radio-MAC guess). Verified
on Bryson: RADIUS shows the current MAC and a *different* MAC 2 days prior (proving
the swap was auto-tracked). EquipmentDetail flags a RADIUS≠stored MAC mismatch with
a 1-click "Mettre à jour" (updateDoc) — manual serial/MAC edit (InlineField) stays.
The RADIUS DB isn't reachable from the hub (no GRANT) → done via the F bridge.
Live client-ethernet throughput (F parity): ops_airos returns eth0 rx/tx byte
counters + ts; EquipmentDetail's wireless panel gets a "Débit en direct" toggle that
polls every ~6s (self-rescheduling, no overlap) and draws ▼download/▲upload
sparklines (eth0 tx=download, rx=upload). Verified live: ↓8.0/↑1.9 kbps on Bryson.
Also: AP link now uses the reliable nasipaddress; signal tiles keep interval coloring.
Deployed (hub + F bridge + SPA index.27375132); leak-check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backfill (scripts/backfill-wireless-equip.js): mirror F wireless CPEs
(airos_ac/airosm/cambium) into OPS Service Equipment, resolving customer +
service_location via device→service→delivery_id→Service Location(legacy_delivery_id),
dedup by MAC-or-IP, serial=MAC when F sn is null, undo manifest. Park was already
~99% synced — created the 2 genuinely-missing (incl. James A. Bryson) + flagged
devices without service/MAC. Verified: Bryson EQP-0000100001 → live LiteBeam 5AC
signal -74/-72, airMAX 93/67.
Signal interval coloring (F parity): wifiSigColor (airOS: >=-60 green … <-78 red)
and fibreRxColor (Rx -8..-25 green, -25..-28 orange, else red) color the live
signal tiles (wireless + fibre). Assistant analysis (check_signal advice) kept.
Device + AP links in service details: CPE management GUI link (Interface →
https://ip) in the wireless panel; AP link when its mgmt IP resolves (ops_airos.php
resolves the AP by radio MAC in F's device table, best-effort → apIp/ap_ip), else
shows the AP MAC. Hub normalizeAiros passes apIp through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The staff Assistant reported "signal non mesurable à distance" for fibre modems
because check_service only reads passive status (TR-069 + local SNMP poller),
which has no signal for tech-3 XGS-PON ONUs (olt3/172.17.x). F gets it live via
the n8n dostuff GET — the assistant just never called it.
New read tool check_signal (parity with F's live "get"):
- fibre: dostuffStatus (n8n, OLT live → Rx/Tx dBm, distance, uptime, VoIP/IPTV/
WAN/firmware); tech-2 fallback to olt-snmp getOnuBySerial.
- sans-fil (airOS): airosSignal via the F bridge (signal/signal-AP/CCQ/TX-RX
rate/airMAX capacity).
- resolves the customer's equipment from Service Equipment (prefers ONU w/ olt_ip,
else CPE w/ ip_address). Quality advice (fibre Rx thresholds, airOS dBm).
- check_service now points at check_signal when signal is null; system prompt
forbids concluding "non mesurable" without calling check_signal first.
Verified live on the reported case (Frédérick Séguin C-710816574555903 →
TPLGA1E7FA90 @ OLT 172.17.0.7): Rx -16.74 / Tx -11.77 dBm, "bon", distance 2024 m.
check_signal registered in staff tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hub can't reach the CPE network, so wireless signal now flows through a
new read-only F endpoint that CAN (F sits on that network + already has the
airOS code + device creds):
- services/legacy-bridge/ops_airos.php — token-gated (X-Ops-Token, same secret
as ops_reassign.php), SSRF-safe (only curls a mgmt IP present in F's `device`
table). Resolves device by serial/mac/ip, logs into the CPE (airOS >=8.5
/api/auth or <8.5 /login.cgi), reads status.cgi, returns normalized signal /
signal-AP / CCQ / TX-RX rate / airMAX capacity / model / uptime. v8 gets full
data; v6 falls back to top-level wireless.signal; CCQ clamped to 0-100.
- hub: airosSignal now calls the F bridge FIRST (OPS_LEGACY_URL host +
/app/data/ops_legacy.token), n8n webhook demoted to post-migration fallback.
Shared normalizeAiros() for both paths.
Verified end-to-end (hub -> F -> CPE): NanoBeam 5AC signal -53 / airMAX
140/143 Mbps; v6 NanoStation signal -47; offline CPE -> clean {ok:false}.
Auth reject + SSRF guard verified. docs updated (F bridge primary).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>