Remplace les 2 listes déroulantes de modèles (figées, non éditables) par un éditeur d'intervalle
compact début → fin, un pour la semaine (soir/nuit) + un optionnel fin de semaine. Le modèle on_call
est créé/retrouvé au save (ensureGardeTpl). Quart qui chevauche minuit : si fin ≤ début → badge
« 🌙 +1j » automatique, durée = wrap +24h (ex. 19:00 → 07:00 = 12h). La bande de garde de nuit
s'affiche du soir jusqu'à minuit sur la case (cellBands clampé). editGardeRule ré-hydrate les
intervalles depuis les modèles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quand on ouvre le menu contextuel d'édition de quart sur une cellule, la cellule reste maintenant
encadrée (contour bleu + fond léger) tant que le menu est ouvert → on voit clairement quelle cellule
on édite. Classe .cell-editing pilotée par menu.show + menu.tech/menu.day.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Un X sur une action de SUPPRESSION prête à confusion avec le X de fermeture d'un menu/fenêtre.
Toutes les commandes qui SUPPRIMENT/retirent un élément passent de icon="close" à icon="delete"
(corbeille) : WeeklyScheduleEditor (retirer quart), ConversationPanel (supprimer note),
PlanifCellMenu (retirer quart), FunctionCatalogDialog (supprimer rôle), TechScanPage (retirer
code-barres), EventsPage (retirer d'une liste/pool/programme/note), PlanificationPage (étape de
garde, deleteDayAsg ×2). Les 2 actions « retour au pool » (removeFromPush/removeFromDay) passent à
icon="inbox" (renvoi au pool — PAS une destruction). Les clears de filtre/champ libellés « Effacer »
restent inchangés (ce ne sont pas des suppressions d'élément).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Les inputs heure de la rangée de quart (.pcm-time) n'avaient pas de largeur → ils prenaient toute
la place et poussaient « total h » + ✕ hors du menu (scroll horizontal). Largeur fixe 58px + centré,
box-sizing border-box. La rangée début → fin + total + ✕ tient maintenant dans le menu 300px, comme
l'éditeur de préréglage. Override redondant .pcm-preset-editor .pcm-time retiré.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Un jour VIDE (ou une semaine future non planifiée) ne voyait aucun quart → le préréglage retombait
sur 8–16 générique. Désormais on APPREND la fenêtre la plus fréquente de chaque tech depuis ses vrais
quarts à chaque loadWeek et on la PERSISTE (localStorage roster-usual-win-v1). Sur un jour sans quart,
le préréglage propose donc le « quart normal le plus probable » du tech (ex. Houssam → 8–18), même
sur une semaine future. Priorité : perso enregistré > usuel appris > patron hebdo > 8–16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Le chip « Jour »/« Soir » et le champ ✎ prennent d'abord le quart RÉEL affiché ce jour
(ex. Houssam 8→18 s'affiche « Jour 8–18 »), sinon le préréglage parent (usuel/perso), sinon défaut.
- Éditeur de préréglage : label sur sa propre ligne + inputs bornés → début → fin ET les boutons ✓/✕
tiennent maintenant dans le menu 300px (avant : seule l'heure de début était visible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le préréglage « Jour » n'est plus le générique 8–16 : par défaut = la fenêtre la plus probable
du tech (la plus fréquente de son patron hebdo weekly_schedule, sinon de ses quarts réguliers
déjà assignés), ex. Houssam → 8–18. Toujours surchargeable via ✎ (perso enregistré prioritaire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vraie cause : les calques de cellule-jour (.ovl-warn triangle, .tl-fn-in rôle, .office-blk
« sans déplacement » = z-index 3 ; .drop-badge = 6) passaient PAR-DESSUS la colonne gelée
(.tech-col z-index 2) au défilement → ils recouvraient le nom du tech. Colonne gelée montée
à z-index 8 (en-tête 9) → elle recouvre désormais tout le contenu de cellule qui glisse dessous.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`.ovl-warn` (triangle surcharge) et `.offshift-warn` vivent hors de `.tl` (enfants directs
de `.cell`, sans overflow) → ils débordaient. `.cell { overflow:hidden }` les confine comme
les blocs de quart. `.drop-badge` repositionné à top:1px (au lieu de -8px) pour survivre au clip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
- 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>
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>
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>
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 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>
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>
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>
- 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>
Native detail sections (fin de la grille+desk pour les derniers doctypes cliquables de la fiche client) :
- PaymentMethodDetail / PaymentArrangementDetail / VoipLineDetail — LECTURE SEULE (aucune charge/paiement déclenché ; mot de passe SIP jamais affiché). Enregistrés dans SECTION_MAP.
Phase 2 G3 (parité écriture fibre tech-2 Raisecom, ~10 500 ONUs) :
- olt-ops.js : écritures SNMP natives (net-snmp) suspend/rétablir (VLAN 40↔666) / redémarrer / vitesse (line-profile), OIDs EXTRAITS DU DRIVER F raisecom_rcmg.php (vérifiés à la source) + SAVE 8886.1.2.1.1.0 après chaque set. Encodages olt_id (slot*1e7+port*1e5+ontid) et onu_id ((slot+32)*8388608+port*65536+ontid) — round-trip vérifié. Repli coords ONU depuis le poller SNMP live (olt-snmp.getOnuBySerial) car les coords Raisecom ne sont pas dans Service Equipment. Gaté : env OLT_RW_COMMUNITY requis (absent → run refuse proprement, 0 set), confirm+idempotencyKey obligatoires. Vérifié live : plan tech-2 refuse sans coords/RW ; run refuse ; 0 écriture réseau tirée.
- OnuActionsPanel : ajout « Redémarrer ».
Déployé prod (hub olt-ops + SPA). DetailModal SECTION_MAP (5 entrées ajoutées) = déployé mais co-édité → non commité.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- capacityByDay compte les jobs assignés SANS heure (untimed_used_h au prorata AM/PM/Soir)
- /roster/assign-job : garde 409 si le ticket F est déjà à un autre vrai tech (force pour écraser) ; SPA = dialog confirm
- mirrorAssign : start_time depuis F due_time (am/pm) + fix due_date (cur.→r.) + backfill fill-only dans watchLegacy (97% des tickets F = 'day' → le spread capacité est le fix porteur)
- pushAssignments : garde anti-clobber (conflits F skippés, force=1) + compteur conflicts
- ticket-collab : courriels de notification → deep link OPS /#/tickets?open= (fini le desk) ; TicketsPage lit ?open= ; Dashboard ouvre le ticket précis
- DetailModal + SupplierInvoices : lien desk gaté can('manage_settings')
- NOUVEAU JobMediaModule (photos tech géo-vérifiées + journal terrain) monté dans IssueDetail onsite + volet Planif ; hub /roster/job-media + /field/photo-file signé ; JobThread poll silencieux 45s
- legacy-sync emailList : strip caractères invisibles (soft hyphen U+00AD…) — fixait un 417 InvalidEmailAddress en boucle (compte 15988)
Déployé prod 2026-07-18 (hub restart + SPA /opt/ops-app), sondes live OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Suite retour Louis : plus de mode soft/hard (toggle du commit précédent retiré). Un
seul concept — un job/ticket interne qui OCCUPE la timeline (formation · réunion ·
projet · entretien) → dé-priorise le tech au dispatch. La PRIORITÉ est le curseur :
Moyenne/Basse = les urgences/réparations peuvent quand même prendre le tech ; Urgent
(high) = bloc dur, non traversé.
Front : dialogue unique « Bloquer du temps » (Intitulé · journée complète · date ·
début · durée · Priorité [défaut Moyenne] · ticket optionnel), info adaptée à la
priorité. Un seul submit `doBlockTime` → /roster/filler-job. Retiré : bascule de mode,
branche réserve, `roster.reserveTech`. Les 2 points d'entrée (bouton volet réglages +
menu de cellule) → même dialogue, relabellés « Bloquer du temps… ».
Hub (dispatch.js suggestSlots) : la traversée urgence passe du flag `job_type ===
'Réservation'` à la PRIORITÉ — `isSoftBlock` = bloc interne (Interne/Réservation) de
priorité ≤ moyenne → traversé en mode urgent ; high résiste ; les vrais jobs client ne
sont JAMAIS traversés (pas de double-booking). Ajout du champ `priority` au fetch.
(/roster/reserve laissé en place, inutilisé — retrait ultérieur.)
Vérifié : test unitaire pierce (medium/low traversé · high résiste · job client jamais)
+ aperçu (dialogue unique, priorité Moyenne, info souple, pas de toggle). Build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deux dialogues faisaient la même chose (rendre un tech indisponible pour un bloc)
avec des champs ~80% identiques, des points d'entrée différents (bouton volet
réglages vs menu de cellule) et aucune explication de leur différence → lus comme
un doublon. Seul vrai écart : bloc DUR (hors dispatch, ticket optionnel) vs réserve
SOUPLE (dé-priorise, urgences passent).
→ UN dialogue « Réserver / bloquer du temps » avec bascule de MODE (segmented
control : « Hors dispatch (bloc) » / « Urgences OK (souple) »). Champs communs
(tech · date · début · durée · journée complète · motif/intitulé) ; priorité/type/
ticket seulement en mode bloc. Soumission `doBlockOrReserve` → /roster/reserve OU
/roster/filler-job selon le mode (endpoints inchangés). Les 2 points d'entrée
(bouton « Réserver / bloquer… » du volet réglages [souple par défaut] · item de menu
de cellule [bloc par défaut, pré-rempli]) ouvrent le MÊME dialogue.
Supprimé : resvDlg + doReserve (le dialogue « Réserver » séparé). Aperçu vérifié :
bascule de mode montre/masque priorité/type/ticket + change libellé/info/bouton.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>