Commit Graph

571 Commits

Author SHA1 Message Date
louispaulb
d988becfba feat(planif): préréglage Jour pré-rempli avec la fenêtre USUELLE du tech (P8)
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>
2026-07-23 11:55:30 -04:00
louispaulb
9e91ba6791 fix(planif): colonne Ressource gelée AU-DESSUS des calques de cellule (A6, z-index)
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>
2026-07-23 11:38:57 -04:00
louispaulb
1ebf182c29 fix(planif): clippe le triangle de surcharge + l'alerte hors-quart dans la cellule (A6)
`.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>
2026-07-23 11:32:23 -04:00
louispaulb
94da547b1c fix(planif): l'œil n'est plus poussé/clippé par un long tag groupe (A6)
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>
2026-07-23 11:19:09 -04:00
louispaulb
71285e4ef8 feat(dispatch): lot d'améliorations Planification (doc « Remarques » + captures)
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>
2026-07-23 11:03:24 -04:00
louispaulb
80a059f50c feat(ops): wireless on the same status cards + DHCP leases (F parity)
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>
2026-07-21 15:17:34 -04:00
louispaulb
883546c85b feat(ops): dynamic status cards for device view (dumb-proof)
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>
2026-07-21 14:48:09 -04:00
louispaulb
93588975ad feat(ops): sync fiber ONU from F on swap (reconcile-on-load + daily)
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>
2026-07-21 14:36:33 -04:00
louispaulb
81701724a2 fix(ops): modern fiber status hero when GenieACS has no data (tech-3 ONUs)
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>
2026-07-21 14:22:08 -04:00
louispaulb
db03b0cdab feat(ops): page-load wireless status + daily 03h RADIUS MAC reconcile
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>
2026-07-20 19:12:06 -04:00
louispaulb
3f416fe2ec feat(ops): auto-sync CPE MAC from RADIUS into OPS (no manual step)
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>
2026-07-20 18:54:01 -04:00
louispaulb
22593a06c3 feat(ops): RADIUS-authoritative CPE MAC + live client-ethernet throughput
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>
2026-07-20 18:49:15 -04:00
louispaulb
60d3f73623 feat(ops): wireless backfill + signal interval coloring + device/AP links
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>
2026-07-20 18:38:33 -04:00
louispaulb
0ec7a8cbba feat(assistant): check_signal — live signal parity with F ("Do Stuff get")
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>
2026-07-20 18:18:47 -04:00
louispaulb
9ce66eec9e feat(ops): wireless Do Stuff LIVE via F bridge (ops_airos.php)
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>
2026-07-20 15:02:58 -04:00
louispaulb
cd8b0b8528 docs: n8n get_signal_airos webhook spec (turnkey wireless Do Stuff enablement)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:23:44 -04:00
louispaulb
d10bb521f7 feat(ops): wireless (airOS/Cambium) Do Stuff parity — n8n bridge + panel
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>
2026-07-20 14:22:37 -04:00
louispaulb
80fdb91591 feat(ops): standardize "Do Stuff" + fiber parity with F device_view
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>
2026-07-20 13:59:28 -04:00
louispaulb
77e4411ba2 feat(geofence): réconciliation véhicule — assistant / mauvaise affectation (suggestions)
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>
2026-07-20 13:28:40 -04:00
louispaulb
f793eb4257 feat(service): bouton « Do Stuff » sur le service → ouvre la vue appareil
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>
2026-07-20 13:14:10 -04:00
louispaulb
877dfa5c44 feat(geofence): fige le véhicule Traccar sur chaque événement de passage
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>
2026-07-20 12:57:50 -04:00
louispaulb
3547f4a680 feat(map): véhicules Traccar libellés par initiales du TECH, repli sur le device
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>
2026-07-20 11:44:04 -04:00
louispaulb
08a68016ec feat(job-map): itinéraire routier OSRM tracé DANS la carte MapLibre
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>
2026-07-20 11:25:52 -04:00
louispaulb
12b9da3e36 fix(legacy-sync): adresse/pin du job = adresse de service du billet (déménagement/multi-adresses)
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>
2026-07-20 11:17:49 -04:00
louispaulb
b40cf7d059 feat(assistant): capacités CLIENT 360 (solde, factures, abonnements, équipement, historique)
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>
2026-07-20 09:17:39 -04:00
louispaulb
69290d7b07 feat(assistant): désambiguïsation client via options cliquables (un seul champ)
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>
2026-07-20 08:54:53 -04:00
louispaulb
a580a49eed fix(assistant): reboot = downtime honnête (≥3 min) + sévérité haute
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>
2026-07-20 07:53:03 -04:00
louispaulb
4f24179f4f feat(assistant): reboot via TR-069 pour les ONU tech-3 (repli GenieACS)
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>
2026-07-20 07:47:25 -04:00
louispaulb
6d07f070d7 fix(roster): créneaux OFFERTS au client réservent le trajet autour des jobs
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>
2026-07-20 07:41:31 -04:00
louispaulb
0542e525ea fix(dispatch): créneaux/occupation TIENNENT COMPTE DU TRAJET entre jobs
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>
2026-07-20 07:38:30 -04:00
louispaulb
473b360c4c feat(assistant): Do Stuff — outil device_action (OLT) dans le copilote STAFF
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>
2026-07-20 07:26:13 -04:00
louispaulb
19e2c1fea0 feat(assistant): chat moderne — saisie en bas, fil qui défile en bulles
- 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>
2026-07-19 21:33:53 -04:00
louispaulb
17e85d88a9 feat(dispatch-agent): lecteur check_service — diagnostic à distance (modem/signal) avant un déplacement
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>
2026-07-19 21:17:49 -04:00
louispaulb
0372176fc8 feat(assistant): vrai CHAT multi-tours — on peut répondre aux questions de l'assistant
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>
2026-07-19 21:07:55 -04:00
louispaulb
54fdc22c97 feat(dispatch-agent): lecteur resource_availability — « a-t-on la ressource ? » avant de proposer
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>
2026-07-19 20:59:49 -04:00
louispaulb
2b5c1c6d07 fix(skill-resolver): diagnostic/panne → réparation (plus « fibre » forcé en installation)
Suite au diagnostic « voir signal fibre » → installation (faux). deptToSkill traitait « fibre » comme une intention
d'installation et ne reconnaissait pas « panne ». Corrigé (SOURCE UNIQUE → impacte diagnostic agent, AvailabilityByReason,
couleur des tags) :
- Nouvelle règle réparation AVANT installation : panne, signal, lent, coupé, hors service, ne fonctionne pas, sans
  internet, déconnecté, intermittent, vérifier, diagnostic, dépannage, problème, bris, défectueux.
- Installation = intention de POSER : install, raccordement, activation, nouveau, mise en service, branchement (« fibre »
  seul = techno, plus un déclencheur d'installation).
- Bonus : « tv » en texte libre (\btv\b) → tv.
Vérifié : voir signal fibre/panne/internet lent/coupé/ne fonctionne pas/vérifier le signal → réparation ; installation
fibre/raccordement/activation → installation ; ajout tv → tv. Déployé, hub sain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:57:16 -04:00
louispaulb
f97152c831 feat(dispatch-agent): diagnostic interactif (raison→compétence→dispo) + adresse libre avant de créer
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>
2026-07-19 20:41:42 -04:00
louispaulb
6022cdf5fb fix(dispatch): nextJobRef lisait r.data (objet) au lieu de r.data.data → collisions ticket_id → « Failed to create dispatch job »
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>
2026-07-19 20:36:50 -04:00
louispaulb
c4c4cc6575 feat(ops): « Allouer un technicien » = chat IA (Gemini) avec typeahead client/adresse
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>
2026-07-19 20:30:48 -04:00
louispaulb
788d969090 chore(reconcile): commit MES hunks déployés dans les fichiers co-édités (split via git apply --cached)
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>
2026-07-19 20:06:47 -04:00
louispaulb
60f6d71615 chore(ops): commit ThemeEditor.vue (extrait de l'ex-page /theme, consolidé dans Paramètres)
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>
2026-07-19 20:00:39 -04:00
louispaulb
4718d025f3 improve(ops): accueil = tableau VIVANT — rafraîchit les compteurs (60 s + retour d'onglet)
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>
2026-07-19 19:56:16 -04:00
louispaulb
7371c1b446 improve(booking): /book libre-service — modifier ou annuler un RDV confirmé
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>
2026-07-19 17:04:14 -04:00
louispaulb
81af69e68c improve(booking): SMS de confirmation transactionnel quand le client réserve via /book
À 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>
2026-07-19 16:41:12 -04:00
louispaulb
6b9f7b41fa improve(booking): confirmation /book → « Ajouter à mon calendrier » (.ics universel)
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>
2026-07-19 16:38:08 -04:00
louispaulb
f8af4235fb improve(ops): accueil — détecte les ARRIVÉES EN RETARD sur les interventions du jour
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>
2026-07-19 16:34:28 -04:00
louispaulb
0d80c323f0 improve(ops): accueil — chiffres HONNÊTES depuis les vraies sources (pas de proxy trompeur)
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>
2026-07-19 16:04:57 -04:00
louispaulb
23f0f72731 feat(ops): « / » = accueil universel adapté au rôle (step 3/3) + consolidation Thème/Équipe→Paramètres
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>
2026-07-19 15:35:33 -04:00
louispaulb
99f44daf31 feat(booking): page /book « jamais bloquée » — repli disponibilités libres quand aucun créneau
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>
2026-07-19 15:29:18 -04:00
louispaulb
e7b94947cf feat(dispatch): « Proposer au client » directement dans le flux Dispo par motif (in-ticket/conversation)
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>
2026-07-19 15:25:37 -04:00