gigafibre-fsm/docs/features/dispatch.md
louispaulb 0f8d2b0565 docs: bring all docs in sync with the May 2026 reality
Mass refresh — the docs were last touched 2026-04-22, two weeks behind
shipped reality. This commit updates 9 files to reflect current truth.

WHAT CHANGED IN THE PRODUCT (since 22 Apr) THAT THE DOCS NOW REFLECT:

  • Oktopus CE / TR-369 stack decommissioned (containers + volumes +
    images all removed; broker had filled /dev/sdb with 75 GB of debug
    logs and took ERPNext down for 4 days). Hub gates the integration
    behind OKTOPUS_DISABLED=1 — modules retained, no-op'd at runtime.
  • dispatch.gigafibre.ca (legacy PHP SPA) replaced by an nginx 301
    redirect to /ops/#/dispatch.
  • Top toolbar of the dispatch module: collapsed to single-color
    Lucide icons + ⋯ overflow menu + "Vue principale ▾" + "[👥 N ▾]"
    resource type chip (defaults to techs, materials in the dropdown
    only when relevant).
  • Tech home base / departure point: editable per-tech via 📍 button,
    address geocode (Nominatim) or click-on-map picker, right-click
    on tech pin opens the same actions. Map defaults centered on
    Gigafibre HQ (1867 chemin de la Rivière, Sainte-Clotilde) instead
    of downtown Montreal.
  • POST /auth/users invite flow on the hub: creates the Authentik
    user, sets a temp password, mails it via Mailjet (Authentik's
    own recovery flow isn't configured), creates the matching ERPNext
    System User. Surfaced in ops Settings → Utilisateurs → Inviter.
  • Two Authentik instances clarified as parallel-and-permanent (not
    a migration): auth.targo.ca for staff, id.gigafibre.ca for clients.

FILES TOUCHED:

  README.md — service table refreshed, arch diagram redrawn (no
    Oktopus row), auth section explains the invite flow + two
    parallel instances.
  docs/architecture/overview.md — new "Decommissioned" section,
    correct retirement status for dispatch-app + apps/field, two
    Authentik instances explicitly distinguished, dev-gotchas list
    rewritten (drops MongoDB AVX, adds log-rotation hard-learned
    lesson, adds note about Authentik recovery flow).
  docs/architecture/data-model.md — Step 5 hardware provisioning
    now describes the GenieACS path (TR-069 Inform → preset push)
    instead of the dead TR-369 path.
  docs/architecture/module-interactions.md — oktopus.js and
    oktopus-mqtt.js entries marked as gated, provision.js note
    updated, GenieACS row in external-integrations updated, MQTT
    row removed from real-time channels, interaction matrix loses
    the Oktopus column and gains an Authentik admin REST cell.
  docs/features/dispatch.md — Top bar section completely rewritten
    to match the current chrome (left/center/right regions,
    single-color Lucide, dropdowns); new Tech home base section
    documenting the 📍 + map-pick + right-click flows; retirement
    note now reads as a status, not a plan.
  docs/features/cpe-management.md — full rewrite. Oktopus migration
    plan replaced by a "decommissioned" note + the existing GenieACS
    + modem-bridge architecture as the steady state. TP-Link XX230v
    deep-dive sections preserved (still accurate).
  docs/README.md, docs/features/README.md, docs/roadmap.md —
    intent-table descriptions and live-URLs table corrected.

The docs/archive/ snapshots (2026-04-18, 2026-04-19) are untouched —
they're historical and should remain that way.
2026-05-05 20:10:40 -04:00

438 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dispatch
> Real-time technician scheduling, work-order creation, and field publishing for the Ops PWA. Largest module in `apps/ops/` — the single page where CSRs turn incoming service requests into scheduled work on a technician's calendar.
## 1. Goals & Principles
Dispatch is the live control surface for every field intervention. It replaces the legacy PHP `dispatch-app` (slated for retirement per [../architecture/overview.md](../architecture/overview.md)) with a Vue 3 / Pinia timeline bound directly to ERPNext Dispatch Job and Dispatch Technician DocTypes.
Principles enforced in the code:
- **Single source of truth is ERPNext.** Every drag, resize, tag toggle issues a `PUT /api/resource/Dispatch Job/{name}` through `src/api/dispatch.js`. The Pinia store (`src/stores/dispatch.js`) only holds a mapped view; on reload it rehydrates from Frappe.
- **Optimistic UI + undo stack.** `useUndo` snapshots mutations so keyboard `Cmd+Z` can roll back a mistake before ERPNext ever replies.
- **Match skill, not cost.** Per `feedback_dispatch_tags.md` convention, tags carry a level 1-5 and auto-dispatch picks the lowest adequate match — experts are preserved for jobs that require them.
- **Push, don't poll.** Timeline stays in sync via SSE topics `dispatch` and `network` from `targo-hub` — tech-absence SMS, outage alerts, OLT up/down events all land as toasts without a refresh.
- **Human and material resources share one grid.** A `Dispatch Technician` row is either a human (with a phone / user) or a material asset (Véhicule, Nacelle, OTDR, Fusionneuse…) — the same drag-drop logic applies.
- **Work is not visible to techs until it is published.** The `published` flag on a Dispatch Job gates both the mobile page (`/t/{token}`) and the iCal feed.
## 2. File Inventory
### Front-end (Ops PWA)
| Path | Responsibility |
| --- | --- |
| `apps/ops/src/pages/DispatchPage.vue` | Top-level page, composes all composables, owns SSE connection, renders timeline / week / month switcher plus inline map panel. |
| `apps/ops/src/modules/dispatch/components/TimelineRow.vue` | One row per resource — renders shift, absence, travel, assist, ghost and job segments; emits drag / drop / resize events. |
| `apps/ops/src/modules/dispatch/components/WeekCalendar.vue` | 7-day grid view with absence chips, ghost recurrences and day-load bars; handles planning mode shifts and on-call bands. |
| `apps/ops/src/modules/dispatch/components/MonthCalendar.vue` | Month overview with per-day tech-count badges; shows selected tech's availability when in planning mode. |
| `apps/ops/src/modules/dispatch/components/BottomPanel.vue` | Unassigned jobs tray with lasso selection, batch assign and auto-distribute buttons. |
| `apps/ops/src/modules/dispatch/components/RightPanel.vue` | Details side-panel for a selected job — edit, move, geofix, unassign, set end date, remove assistant, assign pending, update tags. |
| `apps/ops/src/modules/dispatch/components/JobEditModal.vue` | Inline edit of title, address, note, duration, priority and tags. |
| `apps/ops/src/modules/dispatch/components/WoCreateModal.vue` | "Nouveau work order" modal — runs `rankTechs` + `refineWithDrivingTimes` on the top 3 candidates. |
| `apps/ops/src/modules/dispatch/components/CreateOfferModal.vue` | Creates a job offer in broadcast / targeted / pool mode with a pricing preset. |
| `apps/ops/src/modules/dispatch/components/OfferPoolPanel.vue` | Live offer feed with status chips (open, pending, accepted, expired, cancelled). |
| `apps/ops/src/modules/dispatch/components/PublishScheduleModal.vue` | "Publier & envoyer l'horaire par SMS" — publishes jobs, builds per-tech magic link + webcal iCal URL, sends via Twilio. |
| `apps/ops/src/modules/dispatch/components/SbModal.vue` | Generic overlay modal with header / body / footer slots. |
| `apps/ops/src/modules/dispatch/components/SbContextMenu.vue` | Positioned context-menu wrapper used for right-click menus on techs and jobs. |
| `apps/ops/src/modules/dispatch/components/MapPanel.vue` | Standalone map panel component — present but **not imported** by `DispatchPage.vue`, which uses an inline Mapbox block instead. |
| `apps/ops/src/modules/dispatch/components/SuggestSlotsDialog.vue` | Client for `/dispatch/suggest-slots` (hub endpoint). Present but **not imported** by `DispatchPage.vue` — kept for future "Find me a time" entry point. |
| `apps/ops/src/stores/dispatch.js` | Pinia store — maps ERPNext rows to UI-friendly shape, rebuilds tech queues, triggers GPS polling. |
| `apps/ops/src/config/dispatch.js` | Row height constant, resource icon map (Véhicule, Nacelle, Fusionneuse…), re-exports `HUB_SSE_URL`. |
| `apps/ops/src/api/dispatch.js` | All Frappe REST calls — `fetchTechnicians`, `fetchJobsFast`, `updateJob`, `publishJobs`, `createJob`, `createTech`. |
| `apps/ops/src/composables/useScheduler.js` | Drives period navigation, busy lanes, ghost materialization. |
| `apps/ops/src/composables/useDragDrop.js` | Drag sources / drop targets for jobs, techs and assistants. |
| `apps/ops/src/composables/useAutoDispatch.js` | Client-side candidate ranking mirroring the hub algorithm. |
| `apps/ops/src/composables/useJobOffers.js` | Offer-pool state + `PRICING_PRESETS`. |
| `apps/ops/src/composables/useAbsenceResize.js` | Drag-to-resize absence bands. |
| `apps/ops/src/composables/useTechManagement.js` | Create / delete / reassign technicians. |
| `apps/ops/src/composables/useTagManagement.js` | Tag editor bindings for techs and jobs. |
| `apps/ops/src/composables/useContextMenus.js` | Tech and job right-click menus. |
| `apps/ops/src/composables/useSelection.js` | Lasso selection in the bottom panel. |
| `apps/ops/src/composables/useBottomPanel.js` | Collapsed / pinned state of the unassigned tray. |
| `apps/ops/src/composables/usePeriodNavigation.js` | 3-period buffer and infinite horizontal scroll. |
| `apps/ops/src/composables/useResourceFilter.js` | Human / material / group filtering. |
| `apps/ops/src/composables/useMap.js` | Mapbox GL layer and marker lifecycle. |
| `apps/ops/src/composables/useUndo.js` | Per-mutation snapshots for `Cmd+Z`. |
| `apps/ops/src/composables/useAddressSearch.js` | Mapbox geocoder wrapper used by Wo and Job modals. |
| `apps/ops/src/components/dispatch/NlpInput.vue` | Natural-language "type-to-dispatch" shortcut. |
### Back-end (targo-hub)
| Path | Responsibility |
| --- | --- |
| `services/targo-hub/server.js` | Routes `/dispatch/*` to `dispatch.js`, `/dispatch/ical-token/:id` and `/dispatch/calendar/:id.ics` to `ical.js`, `/t/*` to `tech-mobile.js`, `/traccar/*` to `traccar.js`, `/magic-link/*` to `magic-link.js`. |
| `services/targo-hub/lib/dispatch.js` | `POST /dispatch/best-tech`, `POST /dispatch/suggest-slots`, `POST /dispatch/create-job`. Implements `rankTechs`, `getTechsWithLoad`, `enrichWithGps`, `suggestSlots`. |
| `services/targo-hub/lib/ical.js` | HMAC-SHA256 token signing, VCALENDAR builder with `America/Toronto` VTIMEZONE, status + priority mapping, RRULE passthrough. |
| `services/targo-hub/lib/traccar.js` | Bearer-auth proxy to the Traccar GPS server with a 60s device cache. |
| `services/targo-hub/lib/magic-link.js` | `POST /magic-link/generate`, `POST /magic-link/verify`, `POST /magic-link/refresh` — JWT tokens, default 72h TTL, job-level and all-jobs-for-tech variants. |
| `services/targo-hub/lib/tech-mobile.js` | Server-rendered mobile page at `/t/{token}` with status update, scan, vision, equipment and catalog endpoints. |
| `services/targo-hub/lib/tech-absence-sms.js` | Inbound Twilio SMS → Gemini Flash NLU (regex fallback) → `setTechAbsence` / `clearTechAbsence` → SSE `tech-absence` broadcast. |
| `services/targo-hub/lib/sse.js` | Topic fan-out used by the `dispatch` and `network` channels. |
### ERPNext (Frappe custom DocTypes)
| Path | Responsibility |
| --- | --- |
| `erpnext/setup_fsm_doctypes.py` | Creates / patches custom fields on Dispatch Job and Dispatch Technician. |
## 3. Data Model
### Dispatch Job
Core Frappe fields plus these custom fields (from `erpnext/setup_fsm_doctypes.py`):
| Field | Type | Purpose |
| --- | --- | --- |
| `customer` | Link → Customer | Billing-side link. |
| `service_location` | Link → Service Location | Physical address + geocoded coordinates. |
| `job_type` | Select | Installation / Réparation / Maintenance / Retrait / Dépannage / Autre. |
| `source_issue` | Link → Issue | Origin ticket, when the job came from Support. |
| `depends_on` | Link → Dispatch Job | Blocks scheduling until the dependency is done. |
| `parent_job` | Link → Dispatch Job | Multi-step workflow (with `step_order`). |
| `on_open_webhook`, `on_close_webhook` | Data | Flow-editor callbacks — see [flow-editor.md](flow-editor.md). |
| `equipment_items`, `materials_used`, `checklist`, `photos` | Table | Child tables fed by the mobile tech page. |
| `actual_start`, `actual_end`, `travel_time_min` | Datetime / Int | Field-recorded metrics. |
| `completion_notes`, `customer_signature` | Text / Attach | End-of-job capture. |
| `published` | Check | Gates visibility on the mobile page and iCal feed. |
| `is_recurring`, `recurrence_rule`, `recurrence_end`, `pause_periods`, `template_id` | Mixed | RRULE-based repeating jobs (materialized on demand). |
| `continuous` | Check | Emergency jobs that may span weekends / off-days. |
| `assistants` (child table) | Table | `tech_id`, `tech_name`, `duration_h`, `note`, `pinned`. |
| `tags` (child table) | Table | `tag`, `level`, `required`. |
### Dispatch Technician
| Field | Type | Purpose |
| --- | --- | --- |
| `resource_type` | Select | `human` or `material`. |
| `resource_category` | Select | Véhicule / Outil / Salle / Équipement / Nacelle / Grue / Fusionneuse / OTDR. |
| `weekly_schedule` | Text / JSON | Parsed by `parseWeeklySchedule`. |
| `extra_shifts` | JSON (hidden) | Per-date overrides. |
| `absence_reason`, `absence_from`, `absence_until`, `absence_start_time`, `absence_end_time` | Mixed | Current absence band. |
| `traccar_device_id` | Data | Links to a Traccar GPS device. |
| `tags` (child table) | Table | `tag`, `level`. |
Full field definitions live in [../architecture/data-model.md](../architecture/data-model.md).
## 4. UI Surfaces
All French strings below are pulled directly from `DispatchPage.vue`.
### Top bar
The header is split in three flex regions (left / center / right), all
icons are single-color Lucide-style strokes pulled from `ICON.*` in
`useHelpers.js` — no emojis or multi-color glyphs.
**Left region** (filters + view selector):
- **Search** — type a tech name or saved-preset chip to scope the grid.
- **Resource type chip** `[👥 N ▾]` — single dropdown anchored to the
current selection. Default is `human` (techs only). Materials are
secondary; the `Matériel` and `Tous` entries appear only if there's
at least one material resource. Persisted in localStorage.
- **Board view dropdown** `[Vue principale ▾]` — replaces the inline
tabs. Click → list of saved board views + a future "+ Nouvelle vue"
entry.
- **Filters/settings** (sliders icon) — opens the filter panel
(status / group / tags / hide-absent). The icon is `sliders`, *not*
`wrench` — wrench is reserved for the materials filter so the two
concerns don't share a glyph.
- **Projets** — visible only when there are team-jobs (`assistants[]`
populated); shows the project count badge.
**Center region** (period + view + planning):
- **Aujourd'hui** jump-to-today, ` ` prev/next period.
- **Jour / Semaine / Mois** view toggle.
- **Planning toggle** (calendar icon, label `Planning`) — switches the
grid to availability/shift editing mode.
**Right region** (signal + CTAs + overflow):
- **Surchargé** alert (triangle icon) — appears only when at least
one tech is over capacity for the selected day; tooltip lists the
techs and their %.
- **Jobs non assignées** (clipboard icon) with a badge for the
unscheduled count.
- **Carte** (map icon) — toggles the inline Mapbox panel (Day view
only). The map defaults to centered on Gigafibre HQ
`(lng=-73.6756, lat=45.1599, zoom=10)` covering Sainte-Clotilde +
Châteauguay + Napierville + Hemmingford. Clicking a tech in the
resource list flies the map to their position (live Traccar fix
if online, else saved home base).
- **Publier** (purple CTA) with a draft-count badge.
- **+ WO** (indigo CTA) — opens `WoCreateModal`.
- **⋯ overflow menu** — hosts the secondary actions: Actualiser,
Assistant IA (collapses the NLP input bar by default), Offres aux
techs (with green count badge), Ressources & GPS (opens the
Traccar / tech management modal — see §4.x), and "Ouvrir ERPNext"
(with inline status dot for the API connection).
Two clarifications worth knowing:
- The `.sb-header` container uses `overflow:visible` so dropdowns can
spill below it. Earlier `overflow:hidden` clipped the ⋯ menu — fixed
in commit `16343b6`.
- All dropdowns close on Escape, on click outside, and after picking
an item. The handler chain is shared with the existing `ctxMenu` /
`techCtx` / `assistCtx` close logic.
### Timeline view (`TimelineRow.vue`)
One row per resource. Each row renders stacked segments:
| Segment type | Source | Notes |
| --- | --- | --- |
| `shift` | `weeklySchedule` + `extraShifts` | Active working window; drop targets enabled. |
| `absence` | `absence_from..absence_until` or weekly "off" days | Resizable via `useAbsenceResize`. |
| `travel` | Computed between consecutive jobs | Mapbox Directions API; non-draggable. |
| `assist` | `assistants[]` on another tech's job | Pinned assists cannot be dragged away. |
| `ghost` | Unmaterialized recurrence | Click → materialize concrete Dispatch Job. |
| `job` | Assigned, scheduled Dispatch Job | Primary drag source. |
Drag sources: tech avatar (to split shifts), job blocks (to move / reassign), pinned assist blocks (to nudge helper). Emits `select-tech`, `ctx-tech`, `job-dragstart`, `timeline-drop`, `block-move`, `block-resize`, `absence-resize`, `ghost-click`, `ghost-materialize`.
### Week view (`WeekCalendar.vue`)
Seven-day grid per resource. Distinguishes `isExplicitAbsent` (a typed absence record) from `isScheduleOff` (non-working weekday). In planning mode it draws availability bands and on-call shift overlays on top.
### Month view (`MonthCalendar.vue`)
Month grid with per-day badges showing how many techs are available. In planning mode a sidebar surfaces the selected tech's weekly schedule and extra shifts.
### Bottom panel (`BottomPanel.vue`)
Unassigned jobs tray. Supports:
- Lasso selection (click-drag to select multiple)
- Batch assign to a tech
- Auto-distribute across available techs using `useAutoDispatch`
- Collapse / pin state via `useBottomPanel`
### Right panel (`RightPanel.vue`)
Details + pending-request slide-in. Emits `edit`, `move`, `geofix`, `unassign`, `set-end-date`, `remove-assistant`, `assign-pending`, `update-tags`.
### Inline map
DispatchPage renders its own Mapbox panel using `useMap` and `MAPBOX_TOKEN` from `config/erpnext.js`. `MapPanel.vue` exists in the module but is not currently imported — kept for a possible future extraction.
### Offer pool (`OfferPoolPanel.vue`, `CreateOfferModal.vue`)
Three offer modes surface as icon toggles in `CreateOfferModal`:
| Mode | Behaviour |
| --- | --- |
| `broadcast` | Blast to every matching tech; first accepter wins. |
| `targeted` | Single named recipient. |
| `pool` | A named candidate list; first to accept wins. |
Pricing is composed from `PRICING_PRESETS` (in `useJobOffers`) as `displacement$ + hourlyRate$/h × duration`. Offer status chips: `open`, `pending`, `accepted`, `expired`, `cancelled`.
### Context menu (`SbContextMenu.vue`)
Right-click on a tech row, a tech pin on the map, or a job block opens
a context menu wired through `useContextMenus` — quick reassign,
cancel, open in ERP, copy magic link, copy iCal URL.
Tech-specific menu (`techCtx`) entries:
- 🗺 Voir sur la carte
- 🔀 Optimiser la route
- 🏷 Skills / Tags
- 📅 Copier le lien iCal
- 📍 Adresse de départ… — opens the home-base dialog (see below)
- 🎯 Choisir sur la carte — enters the geoFixTech pick mode
- ↗ Ouvrir dans ERPNext
### Tech home base (departure point)
Each tech has a `latitude` / `longitude` on `Dispatch Technician` that
serves as their start-of-day coordinate when no live Traccar fix is
available. The dispatch route optimizer uses these as the origin
when computing the optimal job sequence.
**Editing it** — three paths converge on `saveTechHome(tech, lng, lat)`
in `useTechManagement.js`:
1. **📍 button next to a tech in the GPS sidebar** — opens a 2-option
chooser: "Saisir une adresse" (free-text geocoded via OpenStreetMap
Nominatim) or "Cliquer sur la carte" (entry into geoFixTech mode).
2. **Right-click on the tech's pin on the map** — same context menu
entries as above.
3. **Quick paste** — the address dialog also accepts a literal
`lat, lng` pair (e.g. `45.16, -73.68`) for power users.
Pick mode (`geoFixTech`) shows an indigo banner at the top of the
screen with the tech's name and an Annuler button. Cursor is set to
crosshair on the map. Next click captures lng/lat → PUTs to ERPNext →
recomputes routes. ESC cancels.
**Default fallback** for techs without saved coords is Gigafibre HQ —
1867 chemin de la Rivière, Sainte-Clotilde QC
(`lng=-73.6756177, lat=45.1599145`). Set in `apps/ops/src/stores/dispatch.js`.
### Drag-drop rules (enforced in `useDragDrop`)
- Drop on an active shift / within absence band = allowed (toggle absence).
- Drop on a non-working weekday = blocked unless `continuous=1`.
- Drop on a tech whose tags don't meet `tags.required` levels = warning toast but still allowed (CSR override).
- `Escape` cancels any in-flight drag.
- `Delete` while a job is selected opens the unassign confirmation.
- `Cmd+Z` walks back the `useUndo` snapshot stack.
## 5. Key Flows
### 5.1 Create a work order
1. CSR clicks `+ WO``WoCreateModal.vue` opens.
2. Address input auto-geocodes via `useAddressSearch` (Mapbox geocoder).
3. On submit, `WoCreateModal` calls `findBestTech()` which locally runs the ranking, then refines the top 3 with live driving times.
4. `createJob` (in `src/api/dispatch.js`) POSTs to ERPNext.
5. The store prepends the new job; SSE fan-out notifies other open Ops tabs.
### 5.2 Suggest the best tech (server side)
`POST /dispatch/best-tech` on `targo-hub` (`lib/dispatch.js`):
- `rankTechs` computes a score per candidate using SCORE_WEIGHTS — `proximityMultiplier=4`, `proximityMax=100` km, `loadMultiplier=30`, `overloadPenalty=500`, `gpsFreshnessBonus=20`.
- Proximity uses a Euclidean km approximation at Montreal latitude (fast enough for ranking).
- `getTechsWithLoad` tallies today's scheduled hours; anything above the configured cap triggers the overload penalty.
- `enrichWithGps` adds a freshness bonus when a recent Traccar fix exists.
### 5.3 Suggest time slots
`POST /dispatch/suggest-slots` (`lib/dispatch.js`) returns up to N best windows across all techs:
- 7-day horizon, capped at 2 slots per tech to diversify suggestions.
- Default shift 08:00-17:00 (overridable per tech).
- 15-minute travel buffer inserted before/after existing jobs.
- `SuggestSlotsDialog.vue` is the front-end client — present in the repo but not yet wired into `DispatchPage.vue`; the current path is tech selection via `WoCreateModal` or manual drag.
### 5.4 Offer pool
1. CSR opens `CreateOfferModal`, picks mode + pricing preset.
2. Offer is persisted through `useJobOffers`.
3. `OfferPoolPanel` subscribes to offer events and updates chip status.
4. Accepting techs fire a state transition that converts the offer into an assigned Dispatch Job.
### 5.5 Publish & SMS schedule
`PublishScheduleModal.vue` — title "Publier & envoyer l'horaire par SMS":
1. Operator selects a tech + date range.
2. `publishJobs(jobNames)` from `src/api/dispatch.js` PUTs `published: 1` on each job in parallel.
3. For each tech, the modal fetches:
- A magic link via `POST /magic-link/generate` (targo-hub, default 72h TTL) → produces the `/t/{token}` URL that opens the mobile tech page.
- An iCal token via `GET /dispatch/ical-token/{techId}` → builds a `webcal://` URL pointing at `/dispatch/calendar/{techId}.ics?token=...`.
4. Twilio sends two SMS:
- "Mes tâches: {link}" — the magic link.
- "Ajouter à mon calendrier: {webcal url}" — the iCal subscription URL.
5. `sendTestSms` is invoked with `reference_doctype = Dispatch Technician` so every send is logged against the tech in ERPNext.
### 5.6 Absence via SMS (inbound)
`services/targo-hub/lib/tech-absence-sms.js`:
1. Twilio inbound webhook hits `handleAbsenceSms`.
2. `lookupTechByPhone` matches the last 10 digits of the sender against Dispatch Technicians.
3. Gemini 2.5 Flash (OpenAI-compatible endpoint) parses intent + dates; a regex fallback catches "absent / malade / sick / formation / vacances" with simple date ranges if the LLM is down.
4. `setTechAbsence` or `clearTechAbsence` PUTs to Dispatch Technician and broadcasts SSE `tech-absence` on the `dispatch` topic.
5. Ops timelines update in-place; the operator sees a toast. A French confirmation SMS is returned to the tech.
### 5.7 Recurring jobs (ghosts → materialized)
- A template job carries `is_recurring=1` and an RFC 5545 `recurrence_rule` (e.g. `FREQ=WEEKLY;BYDAY=SA,SU`).
- `WeekCalendar` and `TimelineRow` render upcoming occurrences as ghost segments computed client-side.
- Clicking a ghost fires `ghost-materialize`, creating a concrete Dispatch Job with `template_id` set — subsequent edits diverge from the template.
- `pause_periods` carves out vacation windows without deleting the template.
## 6. External Integrations
### Traccar (GPS)
`services/targo-hub/lib/traccar.js` proxies `/traccar/devices`, `/traccar/positions` and a generic GET fallback with Traccar bearer-token auth preferred over Basic. Device metadata is cached for 60 seconds. The store (`useGpsTracking`) polls positions and feeds live `gpsCoords`, `gpsSpeed`, `gpsOnline` into each tech row — these in turn power the freshness bonus in the server-side ranker. Traccar limitation noted in [../architecture/overview.md](../architecture/overview.md) #6.4: only one `deviceId` per request — poll in parallel.
### Twilio
- **Outbound** — `PublishScheduleModal` calls `sendTestSms` to deliver the magic link and webcal URL per tech.
- **Inbound** — `tech-absence-sms.js` owns Twilio's incoming SMS webhook and drives the absence flow above.
- Number `+14382313838` per `reference_twilio.md`.
### iCal / webcal
`services/targo-hub/lib/ical.js`:
- Token is HMAC-SHA256 over `{techId}` with `ICAL_SECRET`, truncated to 16 hex chars.
- Fetches jobs from past 7 days to future 60 days for the tech.
- Emits RFC 5545 with a full `America/Toronto` VTIMEZONE (DST rules included).
- Status: `open → TENTATIVE`, `cancelled` / `completed → CANCELLED`.
- Priority: `urgent → 1`, `medium → 5`, everything else → `9`.
- Honors `recurrence_rule` — passes the RRULE through untouched.
Subscription URL handed to techs is `webcal://msg.gigafibre.ca/dispatch/calendar/{techId}.ics?token=...` — Apple Calendar and Google Calendar both handle this.
### Mobile tech page (`/t/{token}`)
`services/targo-hub/lib/tech-mobile.js` is a lightweight server-rendered page — **not** the Ops PWA. It authenticates the tech with a JWT magic link (`verifyJwt`), lists their published jobs for the day, and offers POST endpoints:
| Route | Purpose |
| --- | --- |
| `GET /t/{token}` | Day view for the tech. |
| `POST /t/{token}/status` | Flip status (In Progress, Completed). |
| `POST /t/{token}/scan` | Record a barcode scan. |
| `POST /t/{token}/vision` | Send a photo to `/vision/barcodes` for OCR. |
| `POST /t/{token}/equip` | Install a Service Equipment row. |
| `POST /t/{token}/equip-remove` | Decommission equipment. |
| `GET /t/{token}/catalog` | Fetch the equipment catalog. |
| `GET /t/{token}/equip-list` | Fetch equipment already on site. |
The Ops PWA never opens this page — it lives alongside Dispatch to replace the retiring `apps/field` app. See [../architecture/overview.md](../architecture/overview.md) §1.
## 7. Cross-module References
- [../architecture/overview.md](../architecture/overview.md) — ecosystem map, retirement plan for the legacy dispatch-app and field app.
- [../architecture/data-model.md](../architecture/data-model.md) — canonical Dispatch Job / Dispatch Technician field list.
- [flow-editor.md](flow-editor.md) — wires `on_open_webhook` / `on_close_webhook` on Dispatch Job into workflow runs.
- [vision-ocr.md](vision-ocr.md) — barcode / equipment recognition consumed by the tech mobile page during a dispatch.
- [cpe-management.md](cpe-management.md) — "Diagnostiquer" deep-dives started from a customer card flow into the same tech assignment surface.
- [billing-payments.md](billing-payments.md) — Sales Orders that trigger installation Dispatch Jobs (`order_source`, `sales_order` fields on the job).
## 8. Failure Modes
| Symptom | Likely cause | Where to look |
| --- | --- | --- |
| Timeline loads but all rows are empty | ERPNext token revoked or Administrator "Generate Keys" clicked | `src/api/auth.js` + note in [../architecture/overview.md](../architecture/overview.md) §6.3 |
| Drops appear to work then revert on refresh | ERPNext `exc` swallowed — check console for `[API] PUT Dispatch Job/... failed` | `apps/ops/src/api/dispatch.js` |
| iCal subscription returns 401 | Wrong token or `ICAL_SECRET` changed — ask operator to re-copy the webcal URL | `services/targo-hub/lib/ical.js` |
| Tech never receives magic link SMS | Twilio rejected the number or `published=0` on the selected jobs | `PublishScheduleModal.vue` + Twilio console |
| Absence SMS ignored | Phone number not matched in Dispatch Technician (last-10-digits LIKE) | `services/targo-hub/lib/tech-absence-sms.js``lookupTechByPhone` |
| GPS dot stale / missing | Traccar token expired, cache 60s, or no `traccar_device_id` on the tech | `services/targo-hub/lib/traccar.js` |
| Suggestion always picks same tech | Overload penalty not firing — check `getTechsWithLoad` result for load cap | `services/targo-hub/lib/dispatch.js` |
| SSE toasts stop arriving | ForwardAuth dropped the session; reload to re-auth with Authentik | `apps/ops/src/pages/DispatchPage.vue` SSE setup |
| Ghost occurrences missing | RRULE parse failed client-side — malformed `recurrence_rule` | `apps/ops/src/stores/dispatch.js` `_mapJob` |
| Tech mobile page shows "not found" | JWT expired (default 72h) — operator must re-publish to regenerate | `services/targo-hub/lib/magic-link.js` |
## 9. Retirement Status (May 2026)
Both legacy frontends are now decommissioned:
- **`dispatch-app` (PHP)** at `dispatch.gigafibre.ca`
the nginx serving `/opt/dispatch-app/` was repointed to a single
`return 301 https://erp.gigafibre.ca/ops/#/dispatch` rule. Anyone
hitting an old bookmarked URL bounces through the redirect into
the ops dispatch module without re-authentication noise (the
`authentik@file` middleware was removed from the dispatch router so
the redirect fires immediately).
- **`apps/field`** — the lightweight mobile tech page at `/t/{token}`
is the replacement. See §6 of this doc.
The replacement pairing is:
- **Ops SPA Dispatch module** — this document — for all scheduling,
ranking, publishing.
- **Tech mobile page** at `/t/{token}` for the field-side experience.
Do not add new features to `dispatch-app` or `apps/field`. Any feature
request should land in `apps/ops/src/modules/dispatch/` or the
tech-mobile handler in `services/targo-hub/lib/tech-mobile.js`.
---
Back to [docs/README.md](../README.md) · [roadmap.md](../roadmap.md)