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.
27 KiB
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) 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}throughsrc/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.
useUndosnapshots mutations so keyboardCmd+Zcan roll back a mistake before ERPNext ever replies. - Match skill, not cost. Per
feedback_dispatch_tags.mdconvention, 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
dispatchandnetworkfromtargo-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 Technicianrow 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
publishedflag 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. |
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.
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 ishuman(techs only). Materials are secondary; theMatérielandTousentries 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, notwrench— 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-headercontainer usesoverflow:visibleso dropdowns can spill below it. Earlieroverflow:hiddenclipped the ⋯ menu — fixed in commit16343b6. - All dropdowns close on Escape, on click outside, and after picking
an item. The handler chain is shared with the existing
ctxMenu/techCtx/assistCtxclose 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:
- 📍 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).
- Right-click on the tech's pin on the map — same context menu entries as above.
- Quick paste — the address dialog also accepts a literal
lat, lngpair (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.requiredlevels = warning toast but still allowed (CSR override). Escapecancels any in-flight drag.Deletewhile a job is selected opens the unassign confirmation.Cmd+Zwalks back theuseUndosnapshot stack.
5. Key Flows
5.1 Create a work order
- CSR clicks
+ WO→WoCreateModal.vueopens. - Address input auto-geocodes via
useAddressSearch(Mapbox geocoder). - On submit,
WoCreateModalcallsfindBestTech()which locally runs the ranking, then refines the top 3 with live driving times. createJob(insrc/api/dispatch.js) POSTs to ERPNext.- 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):
rankTechscomputes a score per candidate using SCORE_WEIGHTS —proximityMultiplier=4,proximityMax=100km,loadMultiplier=30,overloadPenalty=500,gpsFreshnessBonus=20.- Proximity uses a Euclidean km approximation at Montreal latitude (fast enough for ranking).
getTechsWithLoadtallies today's scheduled hours; anything above the configured cap triggers the overload penalty.enrichWithGpsadds 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.vueis the front-end client — present in the repo but not yet wired intoDispatchPage.vue; the current path is tech selection viaWoCreateModalor manual drag.
5.4 Offer pool
- CSR opens
CreateOfferModal, picks mode + pricing preset. - Offer is persisted through
useJobOffers. OfferPoolPanelsubscribes to offer events and updates chip status.- 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":
- Operator selects a tech + date range.
publishJobs(jobNames)fromsrc/api/dispatch.jsPUTspublished: 1on each job in parallel.- 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 awebcal://URL pointing at/dispatch/calendar/{techId}.ics?token=....
- A magic link via
- Twilio sends two SMS:
- "Mes tâches: {link}" — the magic link.
- "Ajouter à mon calendrier: {webcal url}" — the iCal subscription URL.
sendTestSmsis invoked withreference_doctype = Dispatch Technicianso every send is logged against the tech in ERPNext.
5.6 Absence via SMS (inbound)
services/targo-hub/lib/tech-absence-sms.js:
- Twilio inbound webhook hits
handleAbsenceSms. lookupTechByPhonematches the last 10 digits of the sender against Dispatch Technicians.- 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.
setTechAbsenceorclearTechAbsencePUTs to Dispatch Technician and broadcasts SSEtech-absenceon thedispatchtopic.- 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=1and an RFC 5545recurrence_rule(e.g.FREQ=WEEKLY;BYDAY=SA,SU). WeekCalendarandTimelineRowrender upcoming occurrences as ghost segments computed client-side.- Clicking a ghost fires
ghost-materialize, creating a concrete Dispatch Job withtemplate_idset — subsequent edits diverge from the template. pause_periodscarves 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 #6.4: only one deviceId per request — poll in parallel.
Twilio
- Outbound —
PublishScheduleModalcallssendTestSmsto deliver the magic link and webcal URL per tech. - Inbound —
tech-absence-sms.jsowns Twilio's incoming SMS webhook and drives the absence flow above. - Number
+14382313838perreference_twilio.md.
iCal / webcal
services/targo-hub/lib/ical.js:
- Token is HMAC-SHA256 over
{techId}withICAL_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/TorontoVTIMEZONE (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 §1.
7. Cross-module References
- ../architecture/overview.md — ecosystem map, retirement plan for the legacy dispatch-app and field app.
- ../architecture/data-model.md — canonical Dispatch Job / Dispatch Technician field list.
- flow-editor.md — wires
on_open_webhook/on_close_webhookon Dispatch Job into workflow runs. - vision-ocr.md — barcode / equipment recognition consumed by the tech mobile page during a dispatch.
- cpe-management.md — "Diagnostiquer" deep-dives started from a customer card flow into the same tech assignment surface.
- billing-payments.md — Sales Orders that trigger installation Dispatch Jobs (
order_source,sales_orderfields 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 §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) atdispatch.gigafibre.ca— the nginx serving/opt/dispatch-app/was repointed to a singlereturn 301 https://erp.gigafibre.ca/ops/#/dispatchrule. Anyone hitting an old bookmarked URL bounces through the redirect into the ops dispatch module without re-authentication noise (theauthentik@filemiddleware 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 · roadmap.md