Accumulated work on the dispatch/legacy-writeback branch: - Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard, PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs; hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js. - Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js, sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental migration scripts; LegacySyncPage, SupplierInvoices + negative-billing / terminated-active reports. - Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit, olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers, legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets, ClientDetail updates. - docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
29 KiB
TARGO / Gigafibre — OPS Platform Guide
What this is. A single reference to the logic behind every major feature of the OPS platform, the operating conditions we deliberately built into each one to fit how a Québec fibre ISP actually runs, and the gotchas that cost us real time. If you were rebuilding this from scratch, this is the document that would save you months.
Audience. Us (operators + future maintainers) and anyone evaluating "could we build our own ISP ops stack."
Last synthesized: 2026-06-13. Treat file:line references as directional — verify against current code.
0. TL;DR — the five things that explain everything
- F is the source of truth for billing. ERPNext is an operational mirror. Almost every design decision flows from this. We never write billing to F; we sync F → ERPNext one-way; we never auto-act on a divergence that is just "the mirror hasn't caught up."
- The legacy box (F) is fragile and must be treated like unexploded ordnance. One SSH at a time,
read-only, query only indexed columns, never the
account_idjoin. A bad query freezes live billing. - Two apps, one hub. A Quasar SPA (
apps/ops, staff desktop) + a field-tech mobile view, both talking to a single hand-rolled Node service (services/targo-hub) that fans out to F, ERPNext, Gmail, Twilio, SNMP, n8n, Stripe, GenieACS, OR-Tools, etc. The hub is the integration brain. - Status is derived, never imported. A customer is never flipped "inactive" from F. Cancellation
happens at the service level. Active/inactive truth = F
account.status, read on demand — not a synced flag. - Everything real-time is SSE. No websockets for app state (only Twilio media). The hub publishes topics; the SPA subscribes. If a badge/panel is "dead," suspect the single mount point, not the data.
1. The core model: F → ERPNext one-way mirror
┌─────────────────────┐ one-way, preview-first ┌──────────────────────┐
│ F (gestionclient)│ ───────────────────────────────▶ │ ERPNext v16 (PG) │
│ PHP + MariaDB │ clients · adresses · services · │ operational mirror │
│ 10.100.80.100 │ factures · paiements · tickets │ + OPS-only doctypes │
│ AUTHORITATIVE │ │ scheduler PAUSED │
│ for billing │ writes back ONLY via PHP bridge │ mute_emails ON │
└─────────────────────┘ (X-Ops-Token), never direct SQL └──────────────────────┘
- Why a mirror at all. F bills correctly but is a closed legacy PHP app with no API, no modern UX, no dispatch/CRM/automation surface. ERPNext gives us a queryable data model + a place to bolt on dispatch, inbox, campaigns, portals, AI — without touching the billing engine.
- Identifiers that glue the two worlds (memorize these):
Customer.legacy_account_id= Faccount.idService Subscription.legacy_service_id= Fservice.id— and the SUB doc name embeds it:SUB-0000056285→ F service56285. (We almost lost this correlation; keep it.)Service Location.legacy_delivery_id= Fdelivery.id
- F
account.statusis the authority for "is this client active":1=Actif, 2=Suspendu, 3/4/5=Résilié.mapAccountderivesdisabled = [1,2].includes(status) ? 0 : 1. - Sync direction & safety.
lib/legacy-sync.jsis preview/read-only by default; applying a change requires an explicit confirm string (SAFE-ADD); payment mirroring requiresF-WINS. The heavy invoice/service creation runs in host cron/opt/targo-sync/run.sh(hourly), not in the hub. The hub's DAG (lib/sync-orchestrator.js) documents/validates order (clients→adresses→services; factures→paiements; tickets separate) and exposes/sync/status.
Operating condition (the big one): A divergence is not an action. "F inactive / ERPNext active" for ~2,500 rows is noise (the mirror lag + derived flags), not churn to execute. Reconciliation dashboards must split actionable (email/phone/name) from derived/non-actionable (
disabled,is_suspended). See §3.7 and §4.
2. System topology
| Layer | What | Where / how |
|---|---|---|
| Staff SPA | apps/ops — Vue 3 + Quasar, hash-router. Desktop ops console. |
Built quasar build → scp dist/spa/* → 96.125.196.67:/opt/ops-app/. Behind Authentik SSO. |
| Field-tech view | apps/ops routes under /j (TechLayout) + a hub-served SPA /t/<token>. |
Delivered to techs by SMS magic-link (JWT). PWA-ish, offline store + camera scan. |
| Integration hub | services/targo-hub — Node 20, hand-rolled http.createServer (no Express). ~70 lib/*.js modules, ~28k lines. |
Container targo-hub, port 3300 not published (test via docker exec -i targo-hub node). Lib bind-mounted /opt/targo-hub/lib; deploy = scp lib + docker restart. |
| Billing truth | F = gestionclient PHP/MariaDB |
10.100.80.100, reached via a local legacy-db mirror + the PHP write bridge ops_reassign.php. |
| Operational store | ERPNext v16 on PostgreSQL | erp.gigafibre.ca (96.125.196.67). REST via token auth + Host header. Scheduler paused, mute_emails on. |
| Address DB | Québec rqa_addresses (5.24M) + fiber_availability |
Local Postgres (the ERPNext PG), shared pool owned by lib/address-db.js. |
| Realtime | SSE pub/sub (lib/sse.js) |
SPA subscribes /sse?topics=.... Only Twilio Media Streams use WebSocket. |
Auth tiers (enforced by one gate at the top of server.js):
- Public allowlist (
/health,/g/,/c/,/book,/field,/diag/,/webhook/*, checkout/portal/magic-link,/conversations/email-ingest|channel-ingest…). - Customer self-service — the token in the URL is the secret (signed JWT diag links, web-chat client routes, payment links).
- Staff/admin —
Authorization: Bearer HUB_SERVICE_TOKEN, injected server-side by the ops nginx/hub/proxy (same-origin). Cross-origin apps with no token can only reach public routes.HUB_GATEmodes:enforce(401) |report(log-only) | off.ALWAYS_ENFORCElist (/devices,/email-queue,/campaigns,/giftbit) is locked regardless.- Per-handler extra secrets:
X-Mail-Token(n8n ingest),HUB_INTERNAL_TOKEN(/broadcast), signed query token (iCal),X-Ops-Token(PHP write bridge).
Gotcha (auth): use a same-origin proxy that injects the Bearer token, NOT Traefik forward-auth alone — cross-origin + SSE breaks under forward-auth. Several routes were historically left open (
/devices,/conversations,/olt,/admin/pollers); the gate +ALWAYS_ENFORCEclosed the worst.
3. Feature catalog
Each entry: What · Logic · Operating conditions (the invariants we built to fit ops) · Gotchas.
3.1 Clients — the 360° file (/clients, /clients/:id)
- What. Searchable customer list + a single page that aggregates subscriptions, services, equipment, tickets, invoices, payments, notes, and live modem status for one customer.
- Logic.
ClientDetailPage.vue(1,725 lines) orchestrates ~8 composables (useClientData,useSubscriptionActions,useEquipmentActions,usePaymentActions,useDeviceStatus,useSubscriptionGroups, …). Data is ERPNext doctypes (Customer,Service Location,Service Subscription,Subscription Plan,Item) joined by thelegacy_*keys. - Operating conditions.
- Effective service price is not a column. F's
servicetable has no price → price =hijack ? hijack_price : product.price(+hijack_desc).status=1= active;date_suspendedwhenstatus=1is just history — ignore it. - A customer is never set inactive. Out-of-zone movers stay visible (cross-sell candidates).
disabled/is_suspendedare derived from subscriptions, never synced from F. - Recurring monthly total can never be negative. The page shows a red guard if it is, and each
subscription row carries an
F#id/ "added in OPS" badge so we can trace a bad rebate to its origin.
- Effective service price is not a column. F's
- Gotchas.
erp.listblocks some fields on ERPNext v16 ("Field not permitted in query");lib/erp.jsauto-retries by dropping them. The real per-service price field ismonthly_price, notactual_price.- This page is the #4 largest file — a refactor candidate, but it's load-bearing; split by section, don't rewrite.
3.2 Communications — unified inbox (/communications)
- What. One place for all customer conversations (SMS + email + web-chat) and open tickets, organized two ways: a department board ("Par département") and a sales pipeline ("Pipeline ventes").
- Logic.
useConversations.jsis a module-level singleton holdingconversations,discussions,panelOpen,activeCount, queue/claim/assign, SSE wiring. Backend:lib/conversation.js(+lib/ticket-collab.jsfor the ERPNext-Issue side).- Inbound mail/SMS/3CX/Facebook are ingested via
/conversations/email-ingest&/channel-ingest(n8n,X-Mail-Token). Replies go out via Gmail (gmail.js) or Twilio. DepartmentBoard.vuegroupsdiscussionsbyqueue(= department) with drag-to-reassign (assignQueue), click-to-open, and per-column compose.PipelineBoard.vueis the leads kanban.- AI triage (
inbox-triage.js): classifies inbound mail (known customer? type? suggest a ticket?). - Outbox / undo-send (
outbox.js): multi-recipient queue notifications are held N seconds and cancellable from an ops panel; single-recipient agent replies bypass the buffer. - Category taxonomy is single-source (
lib/categories.js): the 4 queues, the requestTYPES,type→queue(QUEUE_OF) andtype→ticket-category(CAT) live in ONE module, required by bothconversation.jsandinbox-triage.js. They had drifted (telephonie/televisionexisted in the queue map but not the triage types). Contract: the AI prompt enumeratesTYPESby hand — extend both. - Sender identity = the real
From(display name), then a customer matched by email, then the raw address — never the AI's content-guessed name. And a thread is never grouped by one of our own domains (support@targo.ca,*@targointernet.com): re-ingested outbound would otherwise collapse unrelated threads into one. Per-agent reply stats come from stampingmsg.agenton outbound replies.
- Operating conditions.
- Departments = the four queues
Facturations,Service à la clientèle,Supports,Technicien(+ "Non assigné"). The board reuses the existing queue labels — departments are queues. - Queue/notification lists must stay small. A queue notification once emailed the entire team and
re-ingested our own notifications into the inbox (a feedback loop). Fix: empty
queue_members+ aningestGuardthat drops own-notifications / internal mail / ticket-replies. - AI auto-reply only answers customer-initiated threads — never our own "planned maintenance"/dunning
notifications, never a bare "ok/merci" ack (
isAck), and yields if a human replied today. - Sales pipeline coexists with the department board (2 tabs) — we did not replace it, because the Sales-sync project still needs the leads stages.
- Departments = the four queues
- Gotchas.
ConversationPanel.vueis the single mount point for the whole comms subsystem. ItsonMountedruns the global init (fetchList+fetchTickets+SSE); the drawer (panelOpen), the badge (activeCount), and the "Nouveau texto/courriel" dialog all live inside it. If itssetup()throws, everything comms dies (no badge, panel won't open, compose won't open) while the rest of the app keeps working. That exact symptom pattern = a ConversationPanel mount failure — not the cache.- The throw we actually hit: a top-level
watch(coordToken, …)placed beforeconst coordToken = computed()→ TDZReferenceError. Rule: in<script setup>, any top-levelwatch()/eager call must come after theconstit references. The Quasar build does not catch this (runtime error). /email-queue(EmailQueuePage) is the ERPNext muted outbound queue (a technical inspector), not the Gmail conversation email. Don't confuse them.
3.3 Tickets & collaboration (/tickets, /collab/*)
- What. ERPNext
Issuelist + a "collab" layer: customer search (phone/civic/name fuzzy), service+modem status, live modem checks, create-ticket-from-conversation, comments/@mentions. - Logic.
ticket-collab.jsexposes/collab/service-status(service + modem in one call),/collab/dostuff(live modem status by hitting F's own n8n webhookn8napi.targo.ca/webhook/dostuff), the negative-billing & terminated-active reports, WAN-rate, and/collab/diag-link. - Operating conditions.
- A ticket created from a conversation keeps the conversation (≠ archive).
- "Do Stuff" live check is
tech=3only (tech=2is our own poller); ~27 s latency.
- Gotchas.
service-statuskeys ONUs by the same TPLG serial format as ERPNext (MAC fallback);rxPowerOnuis RAW (not dBm),rxPowerOltis dBm×10. ERPNext TPLG serials ≠ GenieACS real serials.
3.4 Workforce — Dispatch + Planification + RDV + Copilot
This is the largest, most interconnected domain. The strategic move was convergence: Dispatch and Planification share a single write path and a single resource rail.
- Dispatch (
/dispatch,DispatchPage.vue2,162 lines): drag-and-drop tech scheduling on a Mapbox-backed board — routing, auto-distribute, Uber-style job offers (useJobOffers: broadcast/ accept/decline, rush pricing), GPS, undo. Backendlib/dispatch.jsscores techs by proximity/cost/skills. - Planification (
/planification, 3,524 lines — the largest file): roster/shift grid driven by an OR-Tools solver (roster-solver:8090) vialib/roster.js, over ERPNext doctypes (Dispatch Technician, Shift Template/Requirement/Assignment, Tech Availability). - RDV (
/rdv): customer-appointment booking — roster-aware slots, 3 ranked offers + "fit", hold, confirm, reschedule-notify. Public booking served at/book. - Copilot (
/copilote): conversational ops assistant (Gemini function-calling) with write actions (reassign/SMS/escalate) + voice. - Historique (
/historique,HistoriquePage.vue): 3 tabs — Tournées par technicien (a day's jobs per tech, all statuses incl. Completed/Cancelled), Classement techniciens (jobs completed), and Classement Inbox (replies + threads handled per agent). It exists because the Dispatch board filters toopen/assigned/in_progress, so once a job is completed/cancelled (or its ticket closed/reassigned) it vanished — "where did the tech go?". Read-only hub endpoints/dispatch/history,/dispatch/leaderboard,/conversations/leaderboard; agent reply stats rely onmsg.agentstamping (accrue from 2026-06-14). - Operating conditions.
- One write path. Job→tech assignment goes through
features/workforce/useAssignment(the "hub"), shared by both the Dispatch board and the Planification grid — so the two views can't drift. - One resource rail.
features/workforce/useResourcesis the single techs+subgroups+search/score source. - Tags = skill levels; jobs carry a minimum required. Dispatch logic cost-optimizes: cheapest tech that still clears the job's minimum skill tags.
- A tech on pause turns their dispatch tickets red (roster ↔ dispatch coupling).
- Booking offers obey a policy (min lead time, hour window, max/day, buffer, blackouts).
- One write path. Job→tech assignment goes through
- Gotchas.
- In
lib/roster.js,erp.listmust be sequential, notPromise.all(ERPNext chokes), usehttp.request(not a globalfetch), and watch PydanticNoneon the solver side + anti-over-staffing. - The legacy osTicket → Dispatch Job bridge is opt-in (
LEGACY_DISPATCH_SYNC=on), idempotent vialegacy_ticket_id, and recreating the container ≠ restarting it for env to take effect.
- In
3.5 Field-tech mobile app (/j/*, TechLayout; hub /t/<token>, /field)
- What. A phone-first app techs reach via SMS magic-link: today's jobs, job detail + status updates, camera serial scan, device lookup, modem/Wi-Fi diagnostics, offline queue.
- Logic. Routes under
/jwith a magic-link catch-all/j/:token.useScanner(camera → Gemini Vision OCR),useOfflineStore(Pinia, queue writes when offline),useModemDiagnostic/useWifiDiagnostic(signal thresholds → verdicts). The hub also serves a server-rendered SPA at/t/<token>. - Operating conditions. Auth = the JWT in the link is the credential (no SSO on a tech's phone). Devices are linked to addresses, not customers — preserve manage-IP/credentials/parent hierarchy.
- Gotchas. This is the part most people get wrong on mobile elsewhere in the app — see §5: the tech view is mobile-designed; the staff console pages are not (that's the redesign work).
3.6 Network, devices, OLT, diagnostics (/network, /olt, /devices)
- What. Topology/dependency map, health, OLT signal stats, poller admin, AI log RCA.
- Logic.
devices.jscaches GenieACS devices (poller sweeps ~6,000 ONTs / 5 min).olt-snmp.jspolls TP-Link (ent. 11863) and Raisecom (ent. 8886) GPON OLTs over SNMP every 5 min — ONU table walk, signal/power/serial. WAN throughput (Mbps) is live SNMP, not Elasticsearch.network-intel.jsfeeds InfluxDB + Grafana logs to Gemini for root-cause analysis.outage-monitor.jsturns Uptime-Kuma webhooks into synthesized alerts.
- Operating conditions. WAN live-rate graph works on Raisecom OLTs (tech-2) only; tech-3 shows "indisponible". The OLT keys ONUs by the same TPLG format as ERPNext (direct match).
- Gotchas. SNMP Counter64 arrives as an 8-byte Buffer — read it with
readBigUInt64BE, or you get ~0/null. WAN octet OIDs (Raisecom):rx=…2.4.1.5.{oltId}.40,tx=…3.{oltId}.40,oltId = slot*1e7 + port*1e5 + ontid,.40= the internet VLAN.
3.7 Billing sync & financial reports (/sync-legacy, /rapports/*)
- What. The F→ERPNext sync dashboard + reports: revenue by account, detailed sales, TPS/TVQ taxes, AR aging, "Internet cher" (overpaying addresses + competition), negative recurring bills, and terminated accounts with active services.
- Logic. Reports hit ERPNext (
lib/reports.js) or the legacy MariaDB (lib/legacy-reports.js). Payment reconciliation (lib/legacy-payments.js) mirrors Payment Entries into ERPNext idempotently (PE-{id}), guarded byconfirm:'F-WINS', and computes a billing-coherence status. - Operating conditions.
- Recurring monthly bill is never negative. When it was, the root cause was a status-mapping bug
(
status=1 + date_suspended>0wrongly mapped to "Suspended"), corrected tostatus=1 ⇒ Actifin both the Python cron and the JS. (177 subscriptions corrected.) Report:/rapports/factures-negatives. - Terminating a customer does NOT cascade to deactivate their services. Some terminated accounts keep
legitimately-active services (employees with comped internet). Cancellation is per-service. The
account "Résilié" status is informational (a cleanup-report column), never an automatic action.
Report:
/rapports/resilies-actifs(~2,724 terminated accounts / 3,966 active services). - Before ERPNext ever emits real billing, validate balance against F first. (Scheduler stays paused until cutover.)
- Recurring monthly bill is never negative. When it was, the root cause was a status-mapping bug
(
- Gotchas.
- Never join
delivery.account_idin F — it's unindexed → full scan → freezes live billing. Query bydelivery_id(indexed). Drive delivery_ids from ERPNext PG, then query F by those. - ERPNext v16 on Postgres has real bugs (GROUP BY/HAVING/boolean/double-quote/ORDER BY NULL) — patches
are baked into the image; custom-field columns sometimes need an
ALTERvia the pool.
- Never join
3.8 Address intelligence (/conformite-adresses, /serviceability)
- What. Address search/autocomplete, conformity/normalization review (incl. camping sites), and per-address competitor lookup.
- Logic.
address-db.jsowns the shared PG pool overrqa_addresses(5.24M) +fiber_availability, with a phased trigram query.serviceability.jshits Québec IHV ArcGIS open data (providers by address, incl. Cogeco) with aReferer: quebec.caheader — this replaced a reCAPTCHA-blocked checker. - Operating conditions. Future targeting is by service availability at an address / a specific active service, at the service/address level — not the customer level.
- Gotchas. Trigram
word_similaritywith<%has a perf cliff — phase the query. The whole address DB is local now (no Supabase cloud).
3.9 Campaigns — gift cards (/campaigns/*)
- What. Email gift-card campaigns: 2 CSVs → match to ERPNext
Customer→ Mailjet blast of personalized FR/EN emails carrying a Giftbit shortlink → live SSE progress; reminder campaign for non-clickers; Unlayer template editor + AI FR↔EN translate. - Logic.
campaigns.js(2,389 lines): CSV parse/match, MJML compile, Mailjet send + Event webhook, the gift redirect wrapper/g/(own expiry + reuse).giftbit.jsis the gift-card API client. - Operating conditions. Email routing is FR/EN by
Customer.language. The gift/g/wrapper gives us our own expiry/reuse control on top of Giftbit. - Gotchas. A real incident: a
/campaignsendpoint was public without auth → "card already used" / "$0.03 balance" (cards drained). Locked by token. Giftbit defaults to TESTBED (no real cards) until a separate prod token is wired. Map-CSV customer matching only resolves ~25% — still a production blocker.
3.10 Customer-facing (portal, checkout, contracts, diag, store)
- What. Passwordless customer portal, public checkout/catalog/order, contract acceptance, a self-service live diagnostic page, a hardware store (staging), self-signup (staging).
- Logic.
portal-auth.js(email/phone → 24h JWT),checkout.js,contracts.js(residential JWT + commercial DocuSeal),client-diag.js(/diag/<jwt>shows what works, no internal data),payments.js(Stripe + webhook + daily PPA cron 06:00 EST). - Operating conditions.
- Legacy passwords are unsalted MD5 → force reset via email/SMS OTP, never trust them.
- The
/diagpage must never leak internal data. - Contract acceptance is two-track: residential = JWT (promotion framing), commercial = DocuSeal.
- CRTC 2026-43 (in force 2026-06-12): install is billable (exception), termination fees forbidden without a subsidized device; the residential "promotion spread/clawback" framing is now non-compliant.
- Gotchas. Invoice PDFs carry a QR code to the Stripe/portal payment link.
3.11 Telephony & voice agent (/telephony, /voice/*)
- What. 3CX telephony admin + a WebRTC softphone in the SPA (
usePhone, SIP.js over WSS) + an AI voice agent over Twilio Media Streams ↔ Gemini Live. - Logic.
pbx.jspolls the 3CX call log + handles call-event webhooks;voice-agent.jsdoes mulaw↔PCM transcoding and bridges Twilio audio to Gemini Live. - Gotchas. Voice agent is the only WebSocket consumer (
/voice/ws). Inbound/voice/*is public.
3.12 Admin / RBAC / settings (/settings, /equipe, /agent-flows)
- What. Users, permission matrix, user groups, legacy-sync controls, AI agent flow/prompt editor.
- Logic. RBAC in
lib/auth.js(capabilities likeview_clients,assign_jobs, …) over Authentik; the SPA gates nav items and features by capability (usePermissions). Nav is data-driven fromsrc/config/nav.js(each item has arequires). - Operating conditions. SSO is Authentik (
auth.targo.ca) — which is also why the staff app cannot be loaded headlessly for verification (see §4).
4. Cross-cutting gotchas — "if you build another software, know these"
These are the lessons that generalize beyond TARGO.
Legacy / data integrity
- Treat the authoritative legacy system as read-only and fragile. One connection at a time, only indexed queries, never the expensive join. A reporting query must never be able to freeze billing.
- Keep the foreign keys. Embedding
legacy_service_idin the mirror's record name saved us; losing a correlation key is expensive to rebuild. - A mirror lag is not a business event. Separate "the mirror disagrees" from "the customer changed." Auto-acting on the former manufactures fake churn.
- Derive status, don't sync it.
disabled/is_suspendedshould be computed from services, never flipped from the source — and cancellation belongs at the service level, not the account level (employees with comped service prove the account-level cascade wrong). - An invariant deserves a guard and a report. "Recurring bill ≥ 0" got both a fiche-level red warning
and a
/rapports/factures-negativessweep. The report found the systemic mapping bug the guard couldn't.
ERPNext v16 on PostgreSQL specifically
- It ships SQL that assumes MySQL (GROUP BY/HAVING/boolean/double-quotes/ORDER BY NULL). Patch in-image and
make patches idempotent.
get_listsilently rejects some fields → wrap with a field-drop-retry. - Keep the scheduler paused and
mute_emailson until you actually own billing.
Realtime / frontend architecture
- Find your single points of failure. One component (
ConversationPanel) owning the init + the drawer + the badge + the compose dialog means onesetup()throw blacks out an entire subsystem while the rest of the app looks fine. Either spread the init or know the symptom signature cold. - TDZ in
<script setup>— a top-levelwatch(x)/eager call beforeconst x = …throws at runtime and the build won't catch it. Order matters; computeds/functions are lazy, watches are not. - You can't verify an SSO-gated SPA headlessly. Plan for it: deterministic fixes + asking for the browser console error are faster than fighting the auth wall. Don't blame the cache before ruling out a mount throw.
- Cache after deploy. Hashed assets persist; serve
index.htmlno-cacheso redeploys take effect without a manual hard-refresh.
Integration plumbing
- SNMP Counter64 = 8-byte Buffer →
readBigUInt64BE. Vendor enterprise OIDs differ (TP-Link vs Raisecom). - Auth via same-origin token-injecting proxy, not forward-auth alone (cross-origin + SSE break).
- Sequential, not parallel for fragile upstreams (
erp.listin the roster);Promise.allcan DoS them. - Opt-in schedulers + idempotency keys for anything that writes (the osTicket bridge); and remember recreate ≠ restart for env changes.
- Lock every public endpoint that can spend money (the Giftbit incident). Default-deny + an
ALWAYS_ENFORCElist for the routes that should never see anonymous traffic.
Ops/SSH hygiene
- macOS has no
timeout/sudoto the legacy box; usesu -s /bin/bash www-data -c …andssh -o BatchMode=yes -o ServerAliveInterval=…. Back up before editing (*.bak-YYYYMMDD).
5. Mobile & code health (pointer)
The current state, the clean mobile-first design system, the per-page responsive strategy, and the
prioritized code-optimization backlog (helper consolidation, <StatusBadge>/<StatCard>, token
unification --sb-*→--ops-*, mega-component splits) live in a companion doc:
→ docs/UI_AND_OPTIMIZATION.md
One-line summary: the shell is responsive (drawer breakpoint 1024 + mobile header), but no content page
is (zero q-table grid mode, fixed-width dialogs/columns, $q.screen used in 2 files). The fix is a small
set of responsive primitives applied page-by-page, plus finishing the stalled helper consolidation
(useFormatters reaches 17 files, useStatusClasses only 4, while ~25 pages carry private copies).
6. Glossary
| Term | Meaning |
|---|---|
| F | gestionclient — the legacy PHP/MariaDB billing system. Authoritative for billing. |
| hub | services/targo-hub — the Node integration service (port 3300). |
| OPS | the Quasar staff SPA (apps/ops, /opt/ops-app). |
| SUB-000… | Service Subscription doc; the trailing number = F service.id. |
| hijack | per-service price override in F (hijack_price, hijack_desc). |
| queue / file | inbox label = department (Facturations, Service à la clientèle, Supports, Technicien). |
| dostuff | F's n8n webhook for live modem status (tech=3). |
| RQA | the local Québec address database (rqa_addresses). |
| PPA | pre-authorized payment (daily Stripe cron). |
| TPLG serial | ERPNext-side ONU serial format; ≠ GenieACS real serial (MAC fallback). |