# Gigafibre FSM — Architecture ## Service Map ``` ┌──────────────────┐ │ Authentik SSO │ │ id.gigafibre.ca │ └────────┬─────────┘ │ OIDC / Proxy Auth ▼ ┌──────────────────┐ │ Traefik │ │ Reverse Proxy │ │ + Let's Encrypt │ └──┬───┬───┬───┬───┘ │ │ │ │ ┌───────────────────┘ │ │ └──────────────────┐ ▼ ▼ ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Ops App │ │ ERPNext v16 │ │ Targo-Hub │ │ erp.../ops/ │ │ erp.gigafibre.ca│ │ msg.gigafibre.ca │ │ (nginx+Quasar) │ │ (Frappe/Python) │ │ (Node.js) │ └───────┬─────────┘ └───────┬──────────┘ └──┬──────┬───────┘ │ │ │ │ │ /api/* proxy │ │ │ │ (token injected) │ │ │ └─────────────────────┘ │ │ │ │ ┌──────────────────────────────────────┘ │ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ GenieACS NBI │ │ Twilio API │ │ 10.5.2.115:7557 │ │ SMS + Voice │ │ (TR-069 ACS) │ └──────────────────┘ └───────┬──────────┘ │ CWMP (TR-069) ▼ ┌──────────────────┐ ┌──────────────────┐ │ CPE / ONT │ │ Oktopus (USP) │ │ TP-Link XX230v │ │ oss.gigafibre.ca│ │ Raisecom HT803G │ │ TR-369 (future) │ └──────────────────┘ └──────────────────┘ ``` ## Host: 96.125.196.67 (hubdocker) All services run on a single Docker host. DNS records `erp.gigafibre.ca`, `oss.gigafibre.ca`, `msg.gigafibre.ca` all resolve to this IP. ### Docker Containers | Container | Image | Port | Network | Purpose | |-----------|-------|------|---------|---------| | ops-frontend | nginx:alpine | 80 | proxy | Ops SPA + ERPNext API proxy | | targo-hub | node:20-alpine | 3300 | proxy, erpnext | SSE relay, SMS, GenieACS proxy | | erpnext-frontend | frappe/erpnext | 8080 | erpnext | ERPNext web + API | | erpnext-backend | frappe/erpnext | 8000 | erpnext | Frappe worker | | erpnext-db-1 | postgres:16 | 5432 | erpnext | ERPNext database | | oktopus-acs-1 | oktopusp/acs | 9292 | oktopus | USP/TR-369 controller | | oktopus-mongo-1 | mongo:7 | 27017 | oktopus | Oktopus datastore | | fn-routr | fonoster/routr-one | — | fonoster | VoIP SIP routing | | fn-asterisk | fonoster/asterisk | — | fonoster | PBX media server | | fn-postgres | postgres:16 | — | fonoster | Fonoster DB | | apps-targo-db-1 | postgres | — | apps | Targo-hub database | | authentik-* | goauthentik | — | authentik | SSO provider | --- ## Ops App (Quasar v2 + Vite) **Served from:** `/opt/ops-app/` via `ops-frontend` (nginx) **URL:** `https://erp.gigafibre.ca/ops/` ### Request Flow ``` Browser Traefik ops-frontend (nginx) ERPNext │ │ │ │ │── GET /ops/... ──────▶ strip /ops ────────────▶ try_files ──▶ SPA │ │ │ │ │ │── GET /ops/api/... ──▶ strip /ops ────────────▶ /api/* proxy ─────▶ │ │ │ + Auth token │ │ │ │ injected │ ``` ### Directory Structure ``` apps/ops/src/ ├── api/ # API clients │ ├── auth.js # Token auth + session check │ ├── erp.js # ERPNext CRUD (listDocs, getDoc, updateDoc...) │ ├── dispatch.js # Dispatch jobs/techs/tags CRUD │ ├── sms.js # SMS via n8n webhook │ ├── traccar.js # GPS tracking │ ├── ocr.js # Ollama Vision OCR │ └── service-request.js # Service booking (future) ├── components/ │ ├── shared/ │ │ ├── DetailModal.vue # Right-panel detail viewer │ │ ├── InlineField.vue # Odoo-style inline editing │ │ ├── TagEditor.vue # Tag selector with levels │ │ └── detail-sections/ # Per-doctype detail views │ │ ├── EquipmentDetail # ONT diagnostics + mesh topology │ │ ├── IssueDetail # Ticket view │ │ ├── InvoiceDetail # Invoice + payment status │ │ ├── PaymentDetail # Payment entry │ │ └── SubscriptionDetail# Subscription management │ └── customer/ │ ├── CustomerHeader.vue # Name, status, actions │ ├── ContactCard.vue # Phone/email │ ├── CustomerInfoCard.vue # Flags, notes, tax category │ ├── ChatterPanel.vue # SMS/call thread │ ├── ComposeBar.vue # Message input │ ├── SmsThread.vue # SMS history │ └── PhoneModal.vue # Twilio voice/SIP ├── composables/ # 24 Vue composables │ ├── useDeviceStatus.js # GenieACS device lookup + cache │ ├── useSSE.js # Server-sent events (targo-hub) │ ├── useInlineEdit.js # Inline field save logic │ ├── usePhone.js # 3CX/Twilio voice config │ └── ... # Dispatch, map, formatting, etc. ├── modules/ │ └── dispatch/components/ # Dispatch-specific UI (timeline, calendar, map) ├── pages/ # 10 routed pages │ ├── DashboardPage # KPI overview │ ├── ClientsPage # Search-first customer list │ ├── ClientDetailPage # Customer detail (subs, invoices, tickets) │ ├── TicketsPage # Issue/ticket list │ ├── DispatchPage # Scheduling + timeline (~1600 LOC) │ ├── EquipePage # Equipment fleet │ ├── OcrPage # Invoice OCR │ ├── TelephonyPage # VoIP management │ ├── RapportsPage # Reports (stub) │ └── SettingsPage # Config: API, SMS, 3CX ├── stores/ # Pinia: dispatch, auth ├── config/ # erpnext.js, nav.js, ticket-config.js └── router/index.js ``` --- ## Targo-Hub (Node.js) **Container:** `targo-hub` | **URL:** `msg.gigafibre.ca` | **Port:** 3300 ### Module Structure ``` services/targo-hub/ ├── server.js # 100 LOC — HTTP router, CORS, SSE setup, server.listen ├── lib/ │ ├── config.js # 42 LOC — All env var config (ERP, Twilio, 3CX, GenieACS, SIP) │ ├── helpers.js # 129 LOC — log, json, parseBody, httpRequest, erpFetch, nbiRequest │ ├── sse.js # 57 LOC — SSE client registry, broadcast, broadcastAll │ ├── twilio.js # 191 LOC — SMS in/out/status, voice token/TwiML/status, SIP config │ ├── pbx.js # 159 LOC — 3CX webhook handler + call log poller │ ├── telephony.js # 118 LOC — Fonoster/Routr PostgreSQL CRUD (/telephony/*) │ ├── devices.js # 417 LOC — GenieACS proxy, summarizeDevice, hosts, ACS config export │ └── provision.js # 163 LOC — OLT pre-auth, on-scan, equipment swap ├── package.json └── docker-compose.yml ``` ### Endpoints | Method | Path | Purpose | |--------|------|---------| | GET | `/sse?topics=customer:X` | Server-Sent Events stream | | POST | `/broadcast` | Push event to SSE clients | | POST | `/send/sms` | Send SMS via Twilio | | POST | `/webhook/twilio/sms-incoming` | Receive inbound SMS | | POST | `/webhook/twilio/sms-status` | SMS delivery status | | GET | `/voice/token` | Twilio voice JWT | | GET | `/devices/lookup?serial=X` | Find CPE in GenieACS | | GET | `/devices/summary` | Fleet statistics | | GET | `/devices/:id/hosts` | Connected clients + mesh mapping | | POST | `/devices/:id/tasks` | Send task (reboot, refresh) | | GET | `/health` | Health check | ### GenieACS Device Lookup (3 fallbacks) ``` serial = "TPLGC4160688" 1. Exact: DeviceID.SerialNumber._value == serial → match? 2. GPON: Device.Optical...GponSn._value =~ /C4160688$/ → match? 3. ID: _id =~ /TPLGC4160688/ → match? ``` ### Hosts Endpoint Flow ``` GET /devices/:id/hosts?refresh │ ├── Task 1: getParameterValues → Device.Hosts.Host.{1-20}.* │ (connection_request, timeout=15s) │ ├── Task 2: getParameterValues → Device.WiFi.MultiAP.APDevice.{1-3} │ .Radio.{1-2}.AP.{1-4}.AssociatedDevice.{1-8}.* │ (timeout=10s) │ ├── Read cached data from GenieACS MongoDB │ ├── Build clientNodeMap: MAC → {nodeName, band, signal, speed} │ └── Return { total, hosts[{name, ip, mac, band, signal, attachedNode, lease}] } ``` --- ## GenieACS Provision (XX230v) **Provision:** `xx230v_inform` / `XX230v_inform_TpLink_fix` TP-Link requires `clear()` + `commit()` before re-reading to avoid error 9805/9806: ```javascript clear("Device.Hosts.Host", Date.now()); clear("Device.WiFi.MultiAP.APDevice.*.Radio.*.AP.*.AssociatedDevice", Date.now()); commit(); // Re-read in same provision cycle declare("Device.Hosts.Host.*.HostName", {value: now}); declare("Device.WiFi.MultiAP.APDevice.*.Radio.*.AP.*.AssociatedDevice.*.MACAddress", {value: now}); ``` Configures: TR-069 credentials, remote access (HTTPS on 443), VoIP digit map, NTP, superadmin password. Reads ~100 diagnostic parameters per inform. --- ## Data Model (ERPNext Doctypes) ``` Customer (ERPNext native) └─ Service Location (LOC-#####) ├─ Address + GPS coordinates ├─ Connection type (FTTH/FTTB/Cable/DSL) ├─ OLT port, VLAN, network config │ ├─ Service Equipment (EQP-#####) │ ├─ Type: ONT / Router / Switch / AP / Decodeur │ ├─ Serial number + MAC address │ ├─ Status: Active / Inactive / En stock / Defectueux / Retourne │ ├─ Network config (IP, firmware, credentials) │ └─ Move history (Equipment Move Log) │ └─ Service Subscription (SUB-#####) ├─ Plan: Internet / IPTV / VoIP / Bundle ├─ Billing: price, frequency, Stripe integration └─ Status: pending → active → suspended → cancelled Dispatch Job ├─ Customer + Service Location ├─ Assigned tech + assistants + tags ├─ Schedule: date, time, duration ├─ Equipment Items / Materials Used └─ GPS position (Traccar) ``` --- ## External Services | Service | URL | Used By | Purpose | |---------|-----|---------|---------| | ERPNext | erp.gigafibre.ca | Ops App, targo-hub | Business data | | GenieACS | 10.5.2.115:7557 | targo-hub | CPE management (TR-069) | | Twilio | api.twilio.com | targo-hub | SMS + Voice | | Traccar | tracker.targointernet.com:8082 | Ops App | GPS fleet tracking | | n8n | n8n.gigafibre.ca | Ops App | SMS workflow | | Authentik | id.gigafibre.ca | Traefik | SSO (staff) | | Authentik Client | auth.targo.ca | Portal | SSO (customers) | | Mapbox | api.mapbox.com | Ops App | Maps + routing | | 3CX PBX | targopbx.3cx.ca | targo-hub | Call logging | --- ## Data Flow: Customer → Device Diagnostics ``` User clicks equipment chip in ClientDetailPage │ ▼ EquipmentDetail.vue │ ├── fetchStatus([{serial_number: "TPLGC4160688"}]) │ → GET msg.gigafibre.ca/devices/lookup?serial=TPLGC4160688 │ → targo-hub → GenieACS NBI → summarizeDevice() │ → {interfaces[], mesh[], wifi{}, opticalStatus, ethernet{}} │ ├── fetchHosts(serial, refresh=true) │ → GET msg.gigafibre.ca/devices/:id/hosts?refresh │ → 2 inline tasks to CPE → read cache → clientNodeMap │ → {total, hosts[{name, ip, mac, band, signal, attachedNode}]} │ ▼ UI renders: ┌─ Fibre [Up] Rx dBm (when available) ├─ IP Addresses (Internet, Gestion [clickable → /superadmin/], Service, LAN) ├─ WiFi (per-radio + total clients: direct + mesh) ├─ Ethernet ports ├─ General (firmware, SSID, uptime) └─ Connected Clients (collapsible, grouped by mesh node) ├─ basement (5) — signal %, name, IP, MAC, band, lease ├─ hallway (2) ├─ living_room (4) └─ Filaire / Autre (wired clients) ```