gigafibre-fsm/apps/field/src/pages/DiagnosticPage.vue
louispaulb 07365d3b71 fix(tech-diag): warm-up fetch + no-redirect host so ping matches reality
Techs reported cloudflare.com showing 300+ms on the diagnostic page
while OS-level ICMP ping returned 5ms. The gap is entirely protocol
overhead:

- fetch() ≠ ICMP. Every call pays DNS + TCP + TLS + HTTP on top of
  the real RTT, which is easily 150–300ms cold on mobile LTE when the
  radio has to wake the RRC connection.
- Bare cloudflare.com redirects 301 → www.cloudflare.com, forcing a
  second DNS + TCP + TLS handshake for every "ping" and doubling
  the measured latency.
- TechDiagnosticPage.vue was also labeling the full 10MB download
  time as "Latence", so the number on the speed-test card was never
  a latency measurement at all.

Fixes, applied to both surfaces (Ops /j/diagnostic + Field /diagnostic):

- Swap cloudflare.com → 1.1.1.1/cdn-cgi/trace. 88-byte response, no
  redirect, no keepalive games — canonical "internet is up" endpoint.
- Warm-up fetch before every measurement. First call absorbs DNS +
  TCP + TLS + LTE wake; second call reports steady-state RTT. This
  applies to checkHosts() (ops) and resolveHost() (field composable).
- Split runSpeed() into separate ping + throughput measurements. Ping
  hits speed.cloudflare.com/cdn-cgi/trace (88 bytes on a warm
  connection); throughput hits /__down on the same origin so the TLS
  session is reused.

Deployed to production; smoke-verified:
- ops bundle TechDiagnosticPage.b925e02c.js contains
  '1.1.1.1/cdn-cgi/trace'
- field bundle DiagnosticPage.38a45f65.js contains the same
- zero bare 'cloudflare.com' hostname in either hosts array

Files:
- apps/ops/src/modules/tech/pages/TechDiagnosticPage.vue
- apps/field/src/composables/useSpeedTest.js
- apps/field/src/pages/DiagnosticPage.vue

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:08:24 -04:00

127 lines
4.6 KiB
Vue

<template>
<q-page padding>
<div class="text-h6 q-mb-md">Diagnostic réseau</div>
<!-- Speed test -->
<q-card class="q-mb-md">
<q-card-section>
<div class="text-subtitle1 q-mb-sm">Test de vitesse</div>
<div class="row q-gutter-md q-mb-sm">
<div class="col text-center">
<div class="text-h4 text-primary">{{ speedTest.downloadSpeed.value ?? '—' }}</div>
<div class="text-caption">Mbps (API)</div>
</div>
<div class="col text-center">
<div class="text-h4 text-secondary">{{ speedTest.latency.value ?? '—' }}</div>
<div class="text-caption">ms latence</div>
</div>
</div>
<q-btn color="primary" icon="speed" label="Lancer le test" :loading="speedTest.running.value"
@click="speedTest.runSpeedTest()" class="full-width" />
<div v-if="speedTest.error.value" class="text-negative text-caption q-mt-xs">{{ speedTest.error.value }}</div>
</q-card-section>
</q-card>
<!-- HTTP Resolve -->
<q-card class="q-mb-md">
<q-card-section>
<div class="text-subtitle1 q-mb-sm">Résolution HTTP</div>
<q-input v-model="resolveHost" label="Hostname (ex: google.ca)" outlined dense class="q-mb-sm"
@keyup.enter="doResolve">
<template v-slot:append>
<q-btn flat dense icon="dns" @click="doResolve" />
</template>
</q-input>
<!-- Quick hosts -->
<div class="row q-gutter-xs q-mb-sm">
<q-chip v-for="h in quickHosts" :key="h" dense clickable @click="resolveHost = h; doResolve()">{{ h }}</q-chip>
</div>
<!-- Results -->
<div v-if="resolveResults.length > 0">
<q-list dense separator>
<q-item v-for="r in resolveResults" :key="r.host">
<q-item-section avatar>
<q-icon :name="r.status === 'ok' ? 'check_circle' : 'error'" :color="r.status === 'ok' ? 'positive' : 'negative'" />
</q-item-section>
<q-item-section>
<q-item-label>{{ r.host }}</q-item-label>
<q-item-label caption>
{{ r.status === 'ok' ? r.time + 'ms' : r.error }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</q-card-section>
</q-card>
<!-- Batch check -->
<q-card>
<q-card-section>
<div class="text-subtitle1 q-mb-sm">Check complet</div>
<q-btn color="secondary" icon="fact_check" label="Tester tous les services" :loading="batchRunning"
@click="runBatchCheck" class="full-width" />
<div v-if="batchResults.length > 0" class="q-mt-sm">
<q-list dense separator>
<q-item v-for="r in batchResults" :key="r.host">
<q-item-section avatar>
<q-icon :name="r.status === 'ok' ? 'check_circle' : 'error'" :color="r.status === 'ok' ? 'positive' : 'negative'" />
</q-item-section>
<q-item-section>
<q-item-label>{{ r.host }}</q-item-label>
<q-item-label caption>{{ r.status === 'ok' ? r.time + 'ms' : r.error }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</q-card-section>
</q-card>
</q-page>
</template>
<script setup>
import { ref } from 'vue'
import { useSpeedTest } from 'src/composables/useSpeedTest'
const speedTest = useSpeedTest()
const resolveHost = ref('')
const resolveResults = ref([])
const batchRunning = ref(false)
const batchResults = ref([])
// 1.1.1.1/cdn-cgi/trace instead of bare cloudflare.com — the bare host
// redirects 301 → www.cloudflare.com, doubling the handshake cost. The
// trace endpoint is 88 bytes, no redirect, canonical "internet is up" probe.
const quickHosts = ['google.ca', 'erp.gigafibre.ca', '1.1.1.1/cdn-cgi/trace', '8.8.8.8']
const batchHosts = [
'erp.gigafibre.ca',
'dispatch.gigafibre.ca',
'auth.targo.ca',
'id.gigafibre.ca',
'oss.gigafibre.ca',
'n8n.gigafibre.ca',
'www.gigafibre.ca',
'google.ca',
]
async function doResolve () {
if (!resolveHost.value.trim()) return
await speedTest.resolveHost(resolveHost.value.trim())
if (speedTest.resolveResult.value) {
// Prepend to results, avoid duplicates
resolveResults.value = [
speedTest.resolveResult.value,
...resolveResults.value.filter(r => r.host !== speedTest.resolveResult.value.host),
]
}
}
async function runBatchCheck () {
batchRunning.value = true
batchResults.value = await speedTest.checkHosts(batchHosts)
batchRunning.value = false
}
</script>