# Module Interactions

> The cross-cutting "who talks to whom" reference for the Gigafibre FSM
> codebase. Every feature doc links here for dependency questions instead of
> duplicating a matrix of its own.
>
> Sibling docs: [overview.md](overview.md) (containers, networks, Traefik) ·
> [data-model.md](data-model.md) (ERPNext doctypes).

---

## 1. Purpose

The codebase is split into a handful of frontends (`apps/`), a Node.js API
gateway (`services/targo-hub/`), a couple of side-car services, and a long tail
of external integrations (ERPNext, Stripe, Twilio, GenieACS, etc.). Any given
feature crosses three or four of these layers, which means the interesting
question is rarely "what does module X do" but "when module X does its job,
who does it call, and who reacts to the side-effect?".

This document is the single authoritative answer to that question. Feature
docs in [../features/](../features/README.md) describe a capability end-to-end
and link back here for the cross-module picture. If you are adding a new
module or moving an existing call, update this file first.

---

## 2. Module catalog

### 2.1 Apps (frontends)

| Module | Host / Route | Owns | Feature doc |
|---|---|---|---|
| `apps/ops` | `erp.gigafibre.ca/ops/#/*` | Staff PWA — Dispatch, Clients, Tickets, Équipe, Rapports, OCR, Téléphonie, Agent Flows, Réseau, Settings. Also serves the tech mobile tree `/j/*` (same bundle, different layout). Routes registered in [`apps/ops/src/router/index.js`](../../apps/ops/src/router/index.js). | [billing-payments.md](../features/billing-payments.md) · [cpe-management.md](../features/cpe-management.md) · [vision-ocr.md](../features/vision-ocr.md) · [flow-editor.md](../features/flow-editor.md) |
| `apps/client` | `portal.gigafibre.ca/#/*` | Customer portal PWA — Dashboard, Invoices, Tickets, Messages, Account, Catalog/Cart, payment return landings. Hash router, routes in [`apps/client/src/router/index.js`](../../apps/client/src/router/index.js). Plan A: passwordless magic-link JWT, no Authentik. | [billing-payments.md §4](../features/billing-payments.md) |
| `apps/field` | `/field/` (legacy) | Original Quasar tech app. Being retired in favour of `apps/ops` `/j/*` + SMS magic-link. See §8. | — |
| `apps/website` | `www.gigafibre.ca` | React + Vite marketing site. Lead-capture form posts to targo-hub `/api/checkout` / `/api/order`. Built from [`apps/website/`](../../apps/website/). | — |
| `apps/portal` | (Traefik only) | Not a runtime. Holds [`traefik-client-portal.yml`](../../apps/portal/traefik-client-portal.yml) which permanently redirects the legacy host `client.gigafibre.ca` → `portal.gigafibre.ca`, so stale SMS and bookmarks keep working. | — |

### 2.2 Services (backends we own)

| Module | Host / Port | Owns | Notes |
|---|---|---|---|
| `services/targo-hub` | `msg.gigafibre.ca:3300` | API gateway / monolith. Routes registered in [`services/targo-hub/server.js`](../../services/targo-hub/server.js). Every external integration (Twilio, Stripe, Mailjet, Gemini, GenieACS, Traccar, DocuSeal, Authentik admin) is reached *through* the hub — frontends never hit those directly. | Lib catalog below. |
| `services/modem-bridge` | `:3301` (internal) | Playwright + headless Chromium driving TP-Link XX230v web UI to read encrypted TR-181 parameters the vendor exposes only via client-side JS. Token auth, internal network only. | [`services/modem-bridge/server.js`](../../services/modem-bridge/server.js) |
| `services/docuseal` | `sign.gigafibre.ca` | DocuSeal container for commercial-contract e-signature. Residential contracts use a JWT-based acceptance flow (see `lib/acceptance.js` + `lib/contracts.js`) — DocuSeal is the commercial track. | — |
| `services/legacy-db` | `10.100.80.100:3307` | Read-only MariaDB bridge into the old PHP `gestionclient` database. Used by migration scripts and a handful of lookup queries in `lib/auth.js`. | [`services/legacy-db/docker-compose.yml`](../../services/legacy-db/docker-compose.yml) |

#### targo-hub library map (`services/targo-hub/lib/`)

One file per capability; the router in `server.js` dispatches by path prefix.

| File | Mount point | Responsibility |
|---|---|---|
| `acceptance.js` | `/accept*` | Residential contract acceptance tokens — JWT sign + signed-blob storage on Quotation. |
| `address-search.js` | (lib) | Calls the Supabase `rddrjzptzhypltuzmere` RQA address database for autocomplete. |
| `agent.js` | `/agent/*` | LLM agent runtime + tool-use dispatch (reads `agent-tools.json`). |
| `ai.js` | `/ai/*` | Internal AI decision-making powered by Gemini Flash (classification, summaries, suggestions). |
| `auth.js` | `/auth/*` | Staff auth bridge — reads Authentik headers, optionally checks legacy `gestionclient` MySQL for migration. |
| `checkout.js` | `/api/catalog`, `/api/checkout`, `/api/accept-for-client`, `/api/order`, `/api/address`, `/api/otp` | Onboarding wizard: catalog → cart → OTP → order. Creates ERPNext Lead/Quotation/Customer. |
| `config.js` | (lib) | Environment variable accessor with typed defaults. |
| `contracts.js` | `/contract*` | Signed-contract lifecycle — residential JWT and commercial DocuSeal. |
| `conversation.js` | `/conversations*` | Threaded inbox: SMS, email, web chat → ERPNext Communication / HD Ticket. |
| `device-extractors.js` | (lib) | Pure functions to extract WAN/LAN/WiFi/optical facts from GenieACS device JSON. |
| `device-hosts.js` | (lib) | `Device.Hosts.Host.{n}.*` TR-181 host list parser. |
| `devices.js` | `/devices`, `/acs/*` | GenieACS NBI proxy — device lookup, presets, tasks, diagnostic summary. |
| `dispatch.js` | `/dispatch*` | Dispatch Job assignment scoring, tech-to-job matching. |
| `email-templates.js` | (lib) | HTML templates for OTP, magic-link, invoice email. |
| `email.js` | (lib) | SMTP transport (Mailjet `in-v3.mailjet.com` per `config.js`). |
| `flow-api.js` | `/flow/start`, `/flow/advance`, `/flow/complete`, `/flow/event`, `/flow/runs/*` | HTTP endpoints for the flow runtime. |
| `flow-runtime.js` | (lib) | Execution engine for Flow Templates — `dispatchEvent()` is how modules fire triggers. |
| `flow-templates.js` | `/flow/templates*` | CRUD for Flow Template doctype. |
| `helpers.js` | (lib) | HTTP client, ERPNext REST wrapper (`erpFetch`, `erpRequest`), JSON response helper. |
| `ical.js` | `/dispatch/ical-token/:tech`, `/dispatch/calendar/:tech.ics` | Signed-token iCal feed — lets techs subscribe their jobs in Apple/Google Calendar. |
| `magic-link.js` | `/magic-link*` | JWT sign/verify + one-time magic-link issuance. |
| `modem-bridge.js` | `/modem*` | Thin client for the modem-bridge sidecar. |
| `network-intel.js` | `/network/*` | InfluxDB + Grafana log analysis → Gemini summarization. |
| `oktopus.js` / `oktopus-mqtt.js` | gated | TR-369/USP integration — disabled May 2026 (`OKTOPUS_DISABLED=1` in hub env). Files retained behind the env gate; if the gate is flipped, registers at `/oktopus/*` and starts the MQTT subscriber worker. |
| `olt-snmp.js` | `/olt*` | SNMP poller for ONU status / optical power on the OLTs. |
| `otp.js` | (lib) | One-time-password issuance + email/SMS delivery (used by checkout + portal). |
| `outage-monitor.js` | `/webhook/kuma` | Uptime-Kuma webhook ingest; cross-references OLT poller to synthesize mass-outage alerts. |
| `payments.js` | `/payments*`, `/webhook/stripe` | Stripe Checkout session creation, customer portal links, webhook verification, Payment Entry reconciliation, PPA cron. Fires `on_payment_received` flow trigger. |
| `pbx.js` | `/webhook/3cx/call-event` | 3CX call-event webhook → ERPNext Communication. |
| `portal-auth.js` | `/portal/*` | Passwordless magic-link endpoint `POST /portal/request-link` — rate-limited, anti-enumeration. |
| `project-templates.js` | (lib) | Hard-coded job-template catalogue (fibre_install, etc.) for dispatch. |
| `provision.js` | `/provision/*` | Device provisioning orchestrator — GenieACS preset push (Oktopus pre-auth call retained but no-ops while `OKTOPUS_DISABLED=1`). |
| `referral.js` | `/api/referral/*` | Referral credit validation + application, backing the wizard. |
| `reports.js` | `/reports*` | GL / revenue / tax / A/R reports, Frappe PostgreSQL queries. |
| `sse.js` | `/sse`, `/broadcast` | Server-Sent Events fan-out. Topics: `customer:*`, `conversations`, `dispatch`, etc. |
| `tech-absence-sms.js` | (worker) | Auto-notify customers when their tech is absent. |
| `tech-mobile.js` | `/t/*` | Lightweight mobile tech page (predecessor of `/j/*`). |
| `telephony.js` | `/telephony/*` | Routr / identity SIP pool management. |
| `traccar.js` | `/traccar*` | Traccar REST API proxy (GPS fleet tracker). |
| `twilio.js` | `/send/sms`, `/webhook/twilio/sms-*`, `/voice/*` | Twilio SMS send + inbound webhook + Voice tokens + TwiML. |
| `vision.js` | `/vision/barcodes`, `/vision/equipment`, `/vision/invoice` | Gemini 2.5 Flash vision OCR endpoints. |
| `voice-agent.js` | `/voice/inbound`, `/voice/gather`, `/voice/connect-agent`, `/voice/ws` | Twilio IVR + agent runtime with Media Streams WebSocket. |

### 2.3 External integrations

| Provider | Used from | Channel | Purpose |
|---|---|---|---|
| ERPNext v16 / Frappe | hub (`helpers.erpFetch`), ops (nginx proxy), client (via hub) | REST `/api/resource`, `/api/method` | Source of truth for Customer, Subscription, Invoice, Ticket, Dispatch Job, etc. |
| Authentik SSO | Traefik ForwardAuth middleware | Header injection | Guards staff surfaces (`/ops/`, `n8n.`, `hub.`). |
| Twilio | `lib/twilio.js`, `lib/voice-agent.js` | SMS / Voice (REST + webhook + Media Streams WS) | SMS magic-links, OTP, IVR, Voice tokens. |
| Stripe | `lib/payments.js` | REST + webhook (`Stripe-Signature`) | Checkout sessions, Billing Portal, card-on-file, Klarna BNPL. |
| Mailjet | `lib/email.js` (SMTP `in-v3.mailjet.com`) | SMTP | Transactional email (OTP, invoices, magic-links). |
| GenieACS | `lib/devices.js`, `helpers.nbiRequest` | NBI REST / TR-069 | Modem provisioning + diagnostics for the entire CPE fleet (TR-369/USP via Oktopus was decommissioned May 2026). |
| Traccar | `lib/traccar.js` | REST + WebSocket | GPS breadcrumbs for field techs. |
| Gemini 2.5 Flash | `lib/vision.js`, `lib/ai.js` | REST (`generativelanguage.googleapis.com`) | Vision OCR + internal AI. |
| n8n | (via Authentik header proxy) | REST | Workflow automation — long-running batch jobs. |
| DocuSeal | `sign.gigafibre.ca` | REST + webhook | Commercial contract e-signature. |

---

## 3. Interaction matrix

Row = caller, column = callee. Cell = primary channel (`REST`, `SSE`, `WS`,
`SMS`, `SMTP`, `TR-069`, `Webhook`, `JWT`, `ForwardAuth`, iCal). Empty =
no direct interaction (use the hub as a relay).

| caller ↓ / callee → | ops | client | field | hub | ERPNext | GenieACS | Traccar | Twilio | Stripe | Gemini | Authentik | DocuSeal |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **ops**     | —   | —   | —   | REST + SSE | REST (nginx proxy w/ token) | — (via hub) | WS (via hub proxy) | — (via hub) | — (via hub) | — (via hub `/vision`) | ForwardAuth | — |
| **client**  | —   | —   | —   | REST + SSE | — (via hub) | — | — | — (via hub) | — (via hub `checkout-link`) | — (via hub) | JWT (magic-link, not Authentik) | — |
| **field**   | —   | —   | —   | REST | REST (legacy, token) | — | — | — | — | — | — | — |
| **website** | —   | —   | —   | REST (`/api/checkout`, `/api/order`) | — (via hub) | — | — | — | — | — | — | — |
| **hub**     | SSE | SSE | —   | —   | REST (`erpFetch`, `Authorization: token ...`) | REST (NBI `nbiRequest`) | REST + WS | SMS REST + Webhook | REST + Webhook (`Stripe-Signature`) | REST | REST (`/auth/users` admin) | REST + Webhook |
| **modem-bridge** | — | — | — | — (hub calls it, `:3301`) | — | — | — | — | — | — | — | — |
| **ERPNext** | —   | —   | —   | — (fire-and-forget HTTP to hub `/flow/*` from server scripts) | — | — | — | — | — | — | — | — |
| **Traefik** | Forwards | Forwards | Forwards | Forwards | Forwards | — | — | — | — | — | ForwardAuth | — |
| **Twilio**  | —   | —   | —   | Webhook (`/webhook/twilio/*`) | — | — | — | — | — | — | — | — |
| **Stripe**  | —   | —   | —   | Webhook (`/webhook/stripe`) | — | — | — | — | — | — | — | — |
| **DocuSeal**| —   | —   | —   | Webhook (`/contract/*`) | — | — | — | — | — | — | — | — |

---

## 4. Canonical flows

Six flows cover 90 % of the interactions. Each is grounded in the hub lib
file that drives it.

### 4.1 New customer onboarding

```text
apps/website (React lead form)
    │ POST /api/order  (lib/checkout.js)
    ▼
targo-hub  ──► address-search.js  ──► Supabase RQA
           ──► otp.js              ──► Twilio SMS + Mailjet SMTP
           ──► erpFetch            ──► ERPNext (Lead → Quotation → Customer)
           ──► payments.js         ──► Stripe Checkout session
    │
    │ user pays
    ▼
Stripe ──webhook──► hub /webhook/stripe  (lib/payments.js, Stripe-Signature verify)
                                         ├─► ERPNext Payment Entry
                                         ├─► flow-runtime.dispatchEvent('on_payment_received')
                                         │      ├─► contracts.js  (DocuSeal or JWT accept)
                                         │      └─► dispatch.js    (create Dispatch Job)
                                         └─► sse.broadcast('customer:<name>', ...)
```

Feature doc: [billing-payments.md](../features/billing-payments.md).

### 4.2 Tech dispatches a job

```text
apps/ops /dispatch  (DispatchPage.vue)
    │ drag-drop assignment, POST /dispatch/assign (lib/dispatch.js)
    ▼
hub  ──► ERPNext Dispatch Job (scoring weights in dispatch.js)
     ──► twilio.sendSms       →  customer cell:  "/j/<JWT>"
    │
    ▼
Tech opens /j/<JWT> on phone   (apps/ops/src/modules/tech/*)
    │ JWT verified by magic-link.js
    │ GPS check-in → traccar.js → Traccar REST → WS breadcrumbs back to ops
    │ Scan device barcode → useScanner → /vision/barcodes → Gemini
    │ Mark complete → flow-runtime.dispatchEvent('on_job_complete')
    ▼
ops SSE topic `dispatch` refreshes the Kanban
```

Feature docs: [cpe-management.md](../features/cpe-management.md) (device scan) ·
[vision-ocr.md](../features/vision-ocr.md) (OCR pipeline) ·
[flow-editor.md](../features/flow-editor.md) (post-job trigger).

### 4.3 Customer pays an invoice from the portal

```text
apps/client /#/invoices/INV-0000123   (InvoiceDetailPage.vue)
    │ click "Payer maintenant"
    │ POST /billing/checkout-link (lib/payments.js)
    ▼
hub  ──► Stripe Checkout session (mode=payment, client_reference_id=INV-…)
    │ browser redirects to Stripe, user pays
    ▼
Stripe ──webhook──► hub /webhook/stripe
                    ├─► ERPNext Payment Entry
                    ├─► flow-runtime.dispatchEvent('on_payment_received')
                    └─► sse.broadcast('customer:<name>', 'invoice.paid', …)
    ▼
Both apps/ops (cust detail) and apps/client (dashboard) update live over SSE.
```

### 4.4 Customer requests support

```text
apps/client /#/messages  (MessagesPage.vue)
    │ POST /conversations  (lib/conversation.js)
    ▼
hub  ──► ERPNext HD Ticket  (erpFetch)
     ──► sse.broadcast('conversations', …)
    ▼
apps/ops /tickets  (TicketsPage.vue)  — live refresh
    │ staff replies → conversation.handle → ERPNext Communication
    ▼
hub ──► twilio.sendSms (if customer channel = SMS) or email.js (if email)
```

### 4.5 Modem diagnostic from ops

```text
apps/ops /network  (NetworkPage.vue)
    │ GET /devices/lookup?serial=…  (lib/devices.js)
    ▼
hub  ──► GenieACS NBI  (helpers.nbiRequest)
    │ if TP-Link XX230v → ask modem-bridge :3301 for encrypted params
    ▼
modem-bridge  ──► Playwright headless Chromium  ──► modem web UI (172.17.x.x:443)
              ──► decrypted TR-181 JSON back to hub
    ▼
hub  ──► device-extractors.summarizeDevice  → consolidated JSON
    ▼
ops UI renders WAN / LAN / WiFi / Optical; SSE topic `customer:<name>`
updates any other ops tab watching the same client.
```

Feature doc: [cpe-management.md](../features/cpe-management.md).

### 4.6 Passwordless login (customer)

```text
apps/client /#/login  (LoginPage.vue)
    │ POST /portal/request-link  (lib/portal-auth.js)
    │   rate-limited, anti-enumeration (constant-time response)
    ▼
hub  ──► otp.js / magic-link.js  (JWT HS256, 24h exp)
     ──► twilio.sendSms    ("Votre lien: https://portal.gigafibre.ca/#/?token=…")
     ──► email.js (SMTP Mailjet)
    ▼
customer clicks link → apps/client router guard hydrates store from token
                      → store.customerId set → dashboard loads
```

---

## 5. Authentication matrix

| Surface | Mechanism | Source of trust | Notes |
|---|---|---|---|
| Staff apps (ops, n8n, hub UI) | Authentik ForwardAuth via Traefik `authentik-client@file` middleware | `id.gigafibre.ca` session cookie | Headers `X-Authentik-Email`, `X-Authentik-Groups` injected into upstream. |
| Customer portal (`apps/client`) | 24 h HS256 JWT (magic-link) | `lib/magic-link.js` secret | No Authentik; no password form reachable (Traefik redirects legacy `/login`). |
| Tech mobile (`/j/*` inside `apps/ops`) | 24 h HS256 JWT sent by SMS | Same secret | Same JWT primitive as portal. |
| hub → ERPNext | `Authorization: token <key>:<secret>` (Frappe API token) | ERPNext Administrator token — never regenerate ("Generate Keys" in ERPNext UI breaks the hub) | Used by `helpers.erpFetch`. |
| ops → ERPNext (frontend) | nginx same-origin reverse proxy injects the same token server-side | Token stays server-side; browser never sees it | See `apps/ops/infra/`. |
| Webhooks | Per-provider signature verification | `Stripe-Signature`, `X-Twilio-Signature`, DocuSeal shared secret | Rejected synchronously before any side-effect. |
| `modem-bridge` | Shared bearer token | env var on both hub + bridge containers | Internal network only, not exposed via Traefik. |
| Traccar | Bearer token preferred (fall-back Basic) | env var on hub | See `lib/traccar.js` header note. |

---

## 6. Data ownership

Source of truth per entity. "Hub cache" means the hub keeps a read-through
copy; the authoritative write goes to the listed owner.

| Entity | Source of truth | Hub role | Frontend read path |
|---|---|---|---|
| Customer | ERPNext `Customer` | thin pass-through (`erpFetch`) | ops: direct via nginx proxy · client: via hub |
| Address (Service Location) | ERPNext `Service Location` | pass-through; `address-search.js` also calls Supabase RQA for autocomplete | — |
| Subscription | ERPNext `Subscription` | pass-through | — |
| Invoice | ERPNext `Sales Invoice` | pass-through + `/vision/invoice` OCR | — |
| Payment | ERPNext `Payment Entry` | written from Stripe webhook in `lib/payments.js` | — |
| Ticket | ERPNext `HD Ticket` | written from `lib/conversation.js` | — |
| Dispatch Job | ERPNext `Dispatch Job` (custom doctype) | written from `lib/dispatch.js` | ops SSE topic `dispatch` |
| Technician | ERPNext `Technician` (custom) | pass-through | — |
| Service Equipment | ERPNext `Service Equipment` (custom) | pass-through | — |
| Tag | ERPNext `Tag` | pass-through | — |
| Flow Template | ERPNext `Flow Template` (custom) | CRUD in `lib/flow-templates.js` | ops `/agent-flows` |
| Flow Run | ERPNext `Flow Run` (custom) | written from `lib/flow-runtime.js` | ops `/agent-flows` |
| **Exceptions** |  |  |  |
| Live modem state (WAN IP, SNR, host list) | GenieACS | hub never persists — always fetched live | — |
| GPS breadcrumbs | Traccar | hub proxies WS + REST | ops `/dispatch` map |
| JWT sessions | hub process memory + browser `localStorage` | — | no server-side session store |

---

## 7. Real-time channels

| Channel | Transport | Producer | Consumer | Topic / payload |
|---|---|---|---|---|
| Ops / portal live updates | SSE | `lib/sse.js` | `apps/ops`, `apps/client` | `customer:<name>`, `conversations`, `dispatch`, ad-hoc |
| GPS breadcrumbs | WebSocket | Traccar | `lib/traccar.js` proxy → ops dispatch | `deviceId`, `latitude`, `longitude`, `speed` |
| Twilio Media Streams | WebSocket upgrade on hub `/voice/ws` | Twilio | `lib/voice-agent.js` | raw audio frames for IVR agent |
| SSE keep-alive | server-pushed `: ping` | `lib/sse.js` 25 s interval | browsers | heartbeat |

---

## 8. Retired / in-retirement

Do not build new features on these. Linked target is where the replacement
lives.

| Retiring | Replacement | Status | Notes |
|---|---|---|---|
| `auth.targo.ca` | `id.gigafibre.ca` (Authentik) | active migration | ForwardAuth middleware already on the new host. |
| `apps/field` | `apps/ops` `/j/*` tree | in retirement | Same bundle, different layout — removes the separate deploy surface. |
| `dispatch-app` (legacy standalone) | `apps/ops` `/dispatch` | retired | Feature parity reached; decommission pending DNS cleanup. |
| `client.gigafibre.ca` | `portal.gigafibre.ca` | retired | Traefik permanent-redirect via [`apps/portal/traefik-client-portal.yml`](../../apps/portal/traefik-client-portal.yml). |
| Frappe `/login` form for customers | `/portal/request-link` magic-link | retired | Legacy MD5 hashes not migrated — customers forced through the new flow. |
| Ollama local `llama3.2-vision` | `lib/vision.js` → Gemini 2.5 Flash | retired | Ops VM has no GPU. |

---

Back to [docs/README.md](../README.md).
