gigafibre-fsm/docs/architecture/module-interactions.md
louispaulb 30a867a326 fix(tech): restore Gemini-native scanner + port equipment UX into ops
The ops tech module at /ops/#/j/* had drifted from the field app in two ways:

1. Scanner — a prior "restoration" re-added html5-qrcode, but the
   design has always been native <input capture="environment"> → Gemini
   2.5 Flash via targo-hub /vision/barcodes (up to 3 codes) and
   /vision/equipment (structured labels, up to 5). Revert useScanner.js
   + ScanPage.vue + TechScanPage.vue to commit e50ea88 and drop
   html5-qrcode from both package.json + lockfiles. No JS barcode
   library, no camera stream, no polyfills.

2. Equipment UX — TechJobDetailPage.vue was a 186-line stub missing the
   Ajouter bottom-sheet (Scanner / Rechercher / Créer), the debounced
   SN-then-MAC search, the 5-field create dialog, Type + Priority
   selects on the info card, and the location-detail contact expansion.
   Port the full UX from apps/field/src/pages/JobDetailPage.vue (526
   lines) into the ops module (458 lines after consolidation).

Rebuilt and deployed both apps. Remote smoke test confirms 0 bundles
reference html5-qrcode and the new TechJobDetailPage.1075b3b8.js chunk
(16.7 KB vs ~5 KB stub) ships the equipment bottom-sheet strings.

Docs:

- docs/features/tech-mobile.md — new. Documents all three delivery
  surfaces (legacy SSR /t/{jwt}, transitional apps/field/, unified
  /ops/#/j/*), Gemini-native scanner pipeline, equipment UX, magic-link
  JWT, cutover plan. Replaces an earlier stub that incorrectly
  referenced html5-qrcode.
- docs/features/dispatch.md — new. Dispatch board, scheduling, tags,
  travel-time optimization, magic-link SMS, SSE updates.
- docs/features/customer-portal.md — new. Plan A passwordless magic-link
  at portal.gigafibre.ca, Stripe self-service, file inventory.
- docs/architecture/module-interactions.md — new. One-page call graph
  with sequence diagrams for the hot paths.
- docs/README.md — expanded module index (§2) now lists every deployed
  surface with URL + primary doc + primary code locations (was missing
  dispatch, tickets, équipe, rapports, telephony, network, agent-flows,
  OCR, every customer-portal page). New cross-module edge map in §4.
- docs/features/README.md + docs/architecture/README.md — cross-link
  all new docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:56:38 -04:00

330 lines
22 KiB
Markdown

# 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, Oktopus, Traccar) 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-mqtt.js` | (worker) | MQTT subscriber to the Oktopus broker — compensates for their broken events-controller hook. |
| `oktopus.js` | `/oktopus/*` | Oktopus CE REST API — TR-369/USP device preauth and provisioning. |
| `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 preauth. |
| `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 (legacy CPE fleet). |
| Oktopus CE | `lib/oktopus.js`, `lib/oktopus-mqtt.js` | REST + MQTT (TR-369/USP) | New CPE fleet. |
| 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`,
`MQTT`, `SMS`, `SMTP`, `TR-069`, `TR-369`, `Webhook`, `JWT`, `ForwardAuth`,
iCal). Empty = no direct interaction (use the hub as a relay).
| caller ↓ / callee → | ops | client | field | hub | ERPNext | GenieACS | Oktopus | Traccar | Twilio | Stripe | Gemini | Authentik | DocuSeal |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **ops** | — | — | — | REST + SSE | REST (nginx proxy w/ token) | — (via hub) | — (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 + MQTT | REST + WS | SMS REST + Webhook | REST + Webhook (`Stripe-Signature`) | REST | — | 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 / Oktopus | 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 |
| TR-369 device events | MQTT | Oktopus broker | `lib/oktopus-mqtt.js` worker | device-online, heartbeat |
| 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).