From f4c315bdbfe44755b77af8fdd2403ad70ea5211b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 25 Aug 2026 23:14:05 +0200 Subject: [PATCH] wip connection tester updated --- .../diagnostics/checks/selectedCandidate.ts | 17 +++++++ .../components/ConnectionTestSummary.tsx | 50 ++++++++++++++++--- .../hooks/useConnectionTestRunner.ts | 3 ++ .../diagnostics/routes/ConnectionTest.tsx | 14 +++++- .../src/locales/de/connectionTest.json | 3 ++ .../src/locales/en/connectionTest.json | 3 ++ .../src/locales/fr/connectionTest.json | 3 ++ .../src/locales/nl/connectionTest.json | 3 ++ 8 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/frontend/src/features/diagnostics/checks/selectedCandidate.ts b/src/frontend/src/features/diagnostics/checks/selectedCandidate.ts index 7ca26699..e41113ea 100644 --- a/src/frontend/src/features/diagnostics/checks/selectedCandidate.ts +++ b/src/frontend/src/features/diagnostics/checks/selectedCandidate.ts @@ -42,6 +42,23 @@ export type IceCandidateReport = { working: IceCandidatePair[] } +/** + * True when the selected pair does not carry media over plain UDP: the browser + * fell back to a TURN relay over TCP or TLS, or even the direct route is not + * UDP. Media still flows, but quality usually degrades under load. + * + * Accepts the loosely typed `data` stored on the step result; anything that is + * not an IceCandidateReport simply yields false. + */ +export const isSuboptimalRoute = (data: unknown): boolean => { + const selected = (data as IceCandidateReport | null | undefined)?.selected + if (!selected) return false + const transport = selected.local.relayProtocol ?? selected.local.protocol + // An unreported transport is not evidence of a bad route. + if (!transport) return false + return transport.toLowerCase() !== 'udp' +} + const PROBE_WIDTH = 320 const PROBE_HEIGHT = 180 const PROBE_FPS = 15 diff --git a/src/frontend/src/features/diagnostics/components/ConnectionTestSummary.tsx b/src/frontend/src/features/diagnostics/components/ConnectionTestSummary.tsx index 7552670d..153225d5 100644 --- a/src/frontend/src/features/diagnostics/components/ConnectionTestSummary.tsx +++ b/src/frontend/src/features/diagnostics/components/ConnectionTestSummary.tsx @@ -2,18 +2,33 @@ import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { ProgressBar } from 'react-aria-components' import { css, cx } from '@/styled-system/css' +import { A } from '@/primitives' import type { ConnectionTestStats } from '../types' import { statusSquareClass } from './stepAppearance' -type SummaryState = 'idle' | 'running' | 'passed' | 'partial' | 'failed' +/** + * Network prerequisites (protocols, ports, TURN fallbacks), written for the + * reader's IT department rather than for the end user. + */ +const NETWORK_REQUIREMENTS_DOC_URL = + 'https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/' -/** Only a failure earns a colour: everything else stays near-black. */ +type SummaryState = + | 'idle' + | 'running' + | 'passed' + | 'partial' + | 'failed' + | 'warning' + +/** Only a failure or a degraded route earns a colour: everything else stays near-black. */ const stateColorClass: Record = { idle: css({ color: 'greyscale.1000' }), running: css({ color: 'greyscale.1000' }), passed: css({ color: 'greyscale.1000' }), partial: css({ color: 'greyscale.1000' }), failed: css({ color: 'danger.600' }), + warning: css({ color: 'warning' }), } const cardClass = css({ @@ -176,23 +191,31 @@ const Counter = ({ export const ConnectionTestSummary = ({ stats, isRunning, + routeWarning = false, children, }: { stats: ConnectionTestStats isRunning: boolean + /** The selected ICE route is usable but not UDP (e.g. TURN over TCP/TLS). */ + routeWarning?: boolean children?: ReactNode }) => { const { t } = useTranslation('connectionTest') + // A hard failure still outranks the route warning; the warning outranks + // 'partial' because a measured degraded route matters more than skipped + // camera or microphone checks. const state: SummaryState = isRunning ? 'running' : !stats.hasStarted ? 'idle' : stats.failed > 0 ? 'failed' - : stats.skipped > 0 - ? 'partial' - : 'passed' + : routeWarning + ? 'warning' + : stats.skipped > 0 + ? 'partial' + : 'passed' return (
@@ -206,7 +229,22 @@ export const ConnectionTestSummary = ({ : t(`summary.${state}`)}

-

{t(`summary.${state}Hint`)}

+

+ {t(`summary.${state}Hint`)} + {state === 'warning' && ( + <> + {' '} + + {t('summary.warningDocLink')} + + + )} +

{stats.hasStarted && ( diff --git a/src/frontend/src/features/diagnostics/hooks/useConnectionTestRunner.ts b/src/frontend/src/features/diagnostics/hooks/useConnectionTestRunner.ts index e0c736ed..467ee345 100644 --- a/src/frontend/src/features/diagnostics/hooks/useConnectionTestRunner.ts +++ b/src/frontend/src/features/diagnostics/hooks/useConnectionTestRunner.ts @@ -53,6 +53,9 @@ const fromCheckInfo = (info: CheckInfo): Partial => ({ status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed', summary: info.description, logs: info.logs, + // Only SelectedCandidateCheck sets `data` (the ICE candidate report); for + // the stock LiveKit checks it is undefined and this is a no-op. + data: info.data as Record | undefined, }) const groupDevicesByKind = (devices: MediaDeviceInfo[]) => { diff --git a/src/frontend/src/features/diagnostics/routes/ConnectionTest.tsx b/src/frontend/src/features/diagnostics/routes/ConnectionTest.tsx index d4eb3134..de1ab5dd 100644 --- a/src/frontend/src/features/diagnostics/routes/ConnectionTest.tsx +++ b/src/frontend/src/features/diagnostics/routes/ConnectionTest.tsx @@ -13,6 +13,7 @@ import { css } from '@/styled-system/css' import { Center, VStack } from '@/styled-system/jsx' import { Permissions } from '@/features/rooms/components/Permissions' import { useConnectionTestRunner } from '../hooks/useConnectionTestRunner' +import { isSuboptimalRoute } from '../checks/selectedCandidate' import { ConnectionTestStepRow } from '../components/ConnectionTestStepRow' import { ConnectionTestSummary } from '../components/ConnectionTestSummary' import { CONNECTION_TEST_GROUPS, summarizeSteps } from '../types' @@ -70,6 +71,13 @@ const ConnectionTest = () => { () => new Map(steps.map((step) => [step.id, step] as const)), [steps] ) + // The summary headline downgrades to a warning when media flows but over a + // fallback route: TURN over TCP/TLS, or anything else that is not UDP. + const routeWarning = useMemo(() => { + const step = stepsById.get('selectedCandidate') + return step?.status === 'success' && isSuboptimalRoute(step.data) + }, [stepsById]) + const isPublishVideoRunning = stepsById.get('publishVideo')?.status === 'running' @@ -99,7 +107,11 @@ const ConnectionTest = () => {
- + {isRunning ? ( // A disabled "run" button while the test runs is dead weight: // cancelling is the only thing left to do. diff --git a/src/frontend/src/locales/de/connectionTest.json b/src/frontend/src/locales/de/connectionTest.json index 226940f1..b235d64e 100644 --- a/src/frontend/src/locales/de/connectionTest.json +++ b/src/frontend/src/locales/de/connectionTest.json @@ -46,6 +46,9 @@ "passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.", "partial": "Teilweiser Test", "partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.", + "warning": "Verbindung nicht optimal", + "warningHint": "Achtung: Sie befinden sich nicht in optimalen Bedingungen für die Nutzung des Tools. Ihre Medien werden nicht über UDP übertragen (Rückgriff auf ein TURN-Relay über TCP oder TLS), was die Audio- und Videoqualität beeinträchtigen kann.", + "warningDocLink": "Technische Dokumentation für Ihre IT-Abteilung", "failed_one": "{{count}} Prüfung fehlgeschlagen", "failed_other": "{{count}} Prüfungen fehlgeschlagen", "failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung." diff --git a/src/frontend/src/locales/en/connectionTest.json b/src/frontend/src/locales/en/connectionTest.json index 8c6868a5..d58e9158 100644 --- a/src/frontend/src/locales/en/connectionTest.json +++ b/src/frontend/src/locales/en/connectionTest.json @@ -46,6 +46,9 @@ "passedHint": "Your browser, your devices and your network are ready for a meeting.", "partial": "Partially tested", "partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.", + "warning": "Suboptimal connection", + "warningHint": "Warning: you are not in optimal conditions to use the tool. Your media is not carried over UDP (it falls back to a TURN relay over TCP or TLS), which can degrade audio and video quality.", + "warningDocLink": "Technical documentation for your IT department", "failed_one": "{{count}} check failed", "failed_other": "{{count}} checks failed", "failedHint": "Open the failed checks below for details, then send the report to your IT department." diff --git a/src/frontend/src/locales/fr/connectionTest.json b/src/frontend/src/locales/fr/connectionTest.json index 6390122a..ccf00716 100644 --- a/src/frontend/src/locales/fr/connectionTest.json +++ b/src/frontend/src/locales/fr/connectionTest.json @@ -46,6 +46,9 @@ "passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.", "partial": "Test partiel", "partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.", + "warning": "Connexion non optimale", + "warningHint": "Attention : vous n'êtes pas dans les conditions optimales pour accéder à l'outil. Vos flux ne transitent pas en UDP (repli sur un relais TURN en TCP ou TLS), ce qui peut dégrader la qualité audio et vidéo.", + "warningDocLink": "Documentation technique à destination de votre service informatique", "failed_one": "{{count}} vérification en échec", "failed_other": "{{count}} vérifications en échec", "failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique." diff --git a/src/frontend/src/locales/nl/connectionTest.json b/src/frontend/src/locales/nl/connectionTest.json index 785c629b..5ba53983 100644 --- a/src/frontend/src/locales/nl/connectionTest.json +++ b/src/frontend/src/locales/nl/connectionTest.json @@ -46,6 +46,9 @@ "passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.", "partial": "Gedeeltelijke test", "partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.", + "warning": "Verbinding niet optimaal", + "warningHint": "Let op: je bevindt je niet in optimale omstandigheden om de tool te gebruiken. Je media loopt niet via UDP (terugval op een TURN-relay via TCP of TLS), wat de audio- en videokwaliteit kan verminderen.", + "warningDocLink": "Technische documentatie voor je IT-afdeling", "failed_one": "{{count}} controle mislukt", "failed_other": "{{count}} controles mislukt", "failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."