Commit Graph

13 Commits

Author SHA1 Message Date
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
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
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
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
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
cd25fa2504 fix(dispatch): double-assign F + médias terrain dans le ticket + deep links natifs
- 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>
2026-07-18 18:02:37 -04:00
louispaulb
e950bd3ac6 hub: reverse geocoding infra-first (fibre field-pins -> RQA fallback); address resolution + geotagged field photos
- /address/reverse: reverseNearestFibre() (fibre field-pins, source of truth) then RQA
- searchCustomers: civic address resolves to the resident (not fuzzy homonyms)
- field-app + /field/photo: geotagged photos confirm the correct job

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:52:14 -04:00
louispaulb
512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00
louispaulb
6628eaaa5b feat(ops+hub): sender picker, per-user notifs, editable rating emails, planif & tickets UX
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:41:51 -04:00
louispaulb
0f65c02d83 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
Accumulated work on the dispatch/legacy-writeback branch:
- Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard,
  PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs;
  hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js.
- Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js,
  sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental
  migration scripts; LegacySyncPage, SupplierInvoices + negative-billing /
  terminated-active reports.
- Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit,
  olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers,
  legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets,
  ClientDetail updates.
- docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:12:12 -04:00