mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-07 14:00:56 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bf777965d |
+5
-1
@@ -16,6 +16,11 @@ and this project adheres to
|
|||||||
- ✨(frontend) let signed-out visitors start a meeting
|
- ✨(frontend) let signed-out visitors start a meeting
|
||||||
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
|
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ✨(frontend) warn users when the connection falls back to TURN
|
||||||
|
- 🔧(backend) configure the technical documentation url
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(frontend) enforce recording-mode permissions on the checkboxes
|
- 🐛(frontend) enforce recording-mode permissions on the checkboxes
|
||||||
@@ -156,7 +161,6 @@ and this project adheres to
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ These are the environmental options available on meet backend.
|
|||||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||||
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
|
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
|
||||||
|
| FRONTEND_TECHNICAL_DOCUMENTATION_URL | URL of the technical documentation (network prerequisites) linked from the footer and the connection test. If unset, both links are hidden | |
|
||||||
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
||||||
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
||||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||||
|
|||||||
@@ -466,6 +466,11 @@ class Base(Configuration):
|
|||||||
"documentation_url": values.Value(
|
"documentation_url": values.Value(
|
||||||
None, environ_name="FRONTEND_DOCUMENTATION_URL", environ_prefix=None
|
None, environ_name="FRONTEND_DOCUMENTATION_URL", environ_prefix=None
|
||||||
),
|
),
|
||||||
|
"technical_documentation_url": values.Value(
|
||||||
|
None,
|
||||||
|
environ_name="FRONTEND_TECHNICAL_DOCUMENTATION_URL",
|
||||||
|
environ_prefix=None,
|
||||||
|
),
|
||||||
"external_home_url": values.Value(
|
"external_home_url": values.Value(
|
||||||
None, environ_name="FRONTEND_EXTERNAL_HOME_URL", environ_prefix=None
|
None, environ_name="FRONTEND_EXTERNAL_HOME_URL", environ_prefix=None
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface ApiConfig {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
documentation_url?: string
|
documentation_url?: string
|
||||||
|
technical_documentation_url?: string
|
||||||
external_home_url?: string
|
external_home_url?: string
|
||||||
silence_livekit_debug_logs?: boolean
|
silence_livekit_debug_logs?: boolean
|
||||||
is_silent_login_enabled?: boolean
|
is_silent_login_enabled?: boolean
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export type IceCandidateInfo = {
|
|||||||
port?: number
|
port?: number
|
||||||
/** Local candidates only, and not reported by every browser. */
|
/** Local candidates only, and not reported by every browser. */
|
||||||
networkType?: string
|
networkType?: string
|
||||||
|
/**
|
||||||
|
* For a local relay candidate, the TURN URL it was gathered from
|
||||||
|
* (e.g. `turns:turn.example.com:443?transport=tcp`). Used as a fallback
|
||||||
|
* when the browser does not report `relayProtocol`.
|
||||||
|
*/
|
||||||
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IceCandidatePair = {
|
export type IceCandidatePair = {
|
||||||
@@ -42,6 +48,57 @@ export type IceCandidateReport = {
|
|||||||
working: IceCandidatePair[]
|
working: IceCandidatePair[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
typeof value === 'object' && value !== null
|
||||||
|
|
||||||
|
/** Narrows the loosely typed `data` stored on a step result. */
|
||||||
|
export const isIceCandidateReport = (
|
||||||
|
data: unknown
|
||||||
|
): data is IceCandidateReport =>
|
||||||
|
isObject(data) &&
|
||||||
|
Array.isArray(data.working) &&
|
||||||
|
(data.selected === null ||
|
||||||
|
(isObject(data.selected) && isObject(data.selected.local)))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport between the browser and the TURN server for a local relay
|
||||||
|
* candidate: udp, tcp or tls, or undefined when it cannot be determined.
|
||||||
|
*
|
||||||
|
* `protocol` is deliberately not used here: on a relay candidate it describes
|
||||||
|
* the TURN allocation (server to peer), which is UDP even when the client
|
||||||
|
* reaches the TURN server over TLS.
|
||||||
|
*/
|
||||||
|
export const getRelayTransport = (
|
||||||
|
candidate: IceCandidateInfo
|
||||||
|
): string | undefined => {
|
||||||
|
if (candidate.relayProtocol) return candidate.relayProtocol.toLowerCase()
|
||||||
|
if (!candidate.url) return undefined
|
||||||
|
|
||||||
|
const url = candidate.url.toLowerCase()
|
||||||
|
if (url.startsWith('turns:')) return 'tls'
|
||||||
|
if (!url.startsWith('turn:')) return undefined
|
||||||
|
const transport = /[?&]transport=(udp|tcp)\b/.exec(url)?.[1]
|
||||||
|
// RFC 7065: a turn: URI without a transport parameter defaults to UDP.
|
||||||
|
return transport ?? 'udp'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the selected pair goes through a TURN relay reached over TCP or
|
||||||
|
* TLS. Media still flows, but TCP head-of-line blocking usually degrades
|
||||||
|
* audio and video under packet loss.
|
||||||
|
*
|
||||||
|
* Direct routes (host, srflx, prflx), including ICE-TCP to the SFU, are out of
|
||||||
|
* scope: the warning and its documentation are about TURN fallbacks.
|
||||||
|
* An undetermined transport is not evidence of a bad route.
|
||||||
|
*/
|
||||||
|
export const isRelayedOverTcp = (data: unknown): boolean => {
|
||||||
|
if (!isIceCandidateReport(data) || !data.selected) return false
|
||||||
|
const { local } = data.selected
|
||||||
|
if (local.type !== 'relay') return false
|
||||||
|
const transport = getRelayTransport(local)
|
||||||
|
return transport === 'tcp' || transport === 'tls'
|
||||||
|
}
|
||||||
|
|
||||||
const PROBE_WIDTH = 320
|
const PROBE_WIDTH = 320
|
||||||
const PROBE_HEIGHT = 180
|
const PROBE_HEIGHT = 180
|
||||||
const PROBE_FPS = 15
|
const PROBE_FPS = 15
|
||||||
@@ -57,6 +114,7 @@ const readCandidate = (stats?: Stats): IceCandidateInfo => {
|
|||||||
protocol: stats.protocol as string | undefined,
|
protocol: stats.protocol as string | undefined,
|
||||||
relayProtocol: stats.relayProtocol as string | undefined,
|
relayProtocol: stats.relayProtocol as string | undefined,
|
||||||
networkType: stats.networkType as string | undefined,
|
networkType: stats.networkType as string | undefined,
|
||||||
|
url: stats.url as string | undefined,
|
||||||
...(INCLUDE_CANDIDATE_ADDRESSES
|
...(INCLUDE_CANDIDATE_ADDRESSES
|
||||||
? {
|
? {
|
||||||
address: stats.address as string | undefined,
|
address: stats.address as string | undefined,
|
||||||
@@ -67,7 +125,10 @@ const readCandidate = (stats?: Stats): IceCandidateInfo => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const describeCandidate = (candidate: IceCandidateInfo) => {
|
const describeCandidate = (candidate: IceCandidateInfo) => {
|
||||||
const transport = candidate.relayProtocol ?? candidate.protocol ?? 'unknown'
|
const transport =
|
||||||
|
(candidate.type === 'relay' ? getRelayTransport(candidate) : undefined) ??
|
||||||
|
candidate.protocol ??
|
||||||
|
'unknown'
|
||||||
const endpoint =
|
const endpoint =
|
||||||
candidate.address === undefined
|
candidate.address === undefined
|
||||||
? ''
|
? ''
|
||||||
|
|||||||
@@ -2,18 +2,44 @@ import type { ReactNode } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ProgressBar } from 'react-aria-components'
|
import { ProgressBar } from 'react-aria-components'
|
||||||
import { css, cx } from '@/styled-system/css'
|
import { css, cx } from '@/styled-system/css'
|
||||||
|
import { A } from '@/primitives'
|
||||||
|
import { useConfig } from '@/api/useConfig'
|
||||||
import type { ConnectionTestStats } from '../types'
|
import type { ConnectionTestStats } from '../types'
|
||||||
import { statusSquareClass } from './stepAppearance'
|
import { statusSquareClass } from './stepAppearance'
|
||||||
|
|
||||||
type SummaryState = 'idle' | 'running' | 'passed' | 'partial' | 'failed'
|
type SummaryState =
|
||||||
|
| 'idle'
|
||||||
|
| 'running'
|
||||||
|
| 'passed'
|
||||||
|
| 'partial'
|
||||||
|
| 'failed'
|
||||||
|
| 'warning'
|
||||||
|
|
||||||
/** Only a failure earns a colour: everything else stays near-black. */
|
/** Only a failure or a degraded route earns a colour: everything else stays near-black. */
|
||||||
const stateColorClass: Record<SummaryState, string> = {
|
const stateColorClass: Record<SummaryState, string> = {
|
||||||
idle: css({ color: 'greyscale.1000' }),
|
idle: css({ color: 'greyscale.1000' }),
|
||||||
running: css({ color: 'greyscale.1000' }),
|
running: css({ color: 'greyscale.1000' }),
|
||||||
passed: css({ color: 'greyscale.1000' }),
|
passed: css({ color: 'greyscale.1000' }),
|
||||||
partial: css({ color: 'greyscale.1000' }),
|
partial: css({ color: 'greyscale.1000' }),
|
||||||
failed: css({ color: 'danger.600' }),
|
failed: css({ color: 'danger.600' }),
|
||||||
|
warning: css({ color: 'warning' }),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hard failure still outranks a warning step; a warning outranks 'partial'
|
||||||
|
* because a measured degraded route matters more than skipped camera or
|
||||||
|
* microphone checks.
|
||||||
|
*/
|
||||||
|
const getSummaryState = (
|
||||||
|
stats: ConnectionTestStats,
|
||||||
|
isRunning: boolean
|
||||||
|
): SummaryState => {
|
||||||
|
if (isRunning) return 'running'
|
||||||
|
if (!stats.hasStarted) return 'idle'
|
||||||
|
if (stats.failed > 0) return 'failed'
|
||||||
|
if (stats.warnings > 0) return 'warning'
|
||||||
|
if (stats.skipped > 0) return 'partial'
|
||||||
|
return 'passed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardClass = css({
|
const cardClass = css({
|
||||||
@@ -183,16 +209,15 @@ export const ConnectionTestSummary = ({
|
|||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('connectionTest')
|
const { t } = useTranslation('connectionTest')
|
||||||
|
const { data: config } = useConfig()
|
||||||
|
|
||||||
const state: SummaryState = isRunning
|
// Network prerequisites for the reader's IT department. Instance specific,
|
||||||
? 'running'
|
// so it comes from the backend; without it the warning shows no link.
|
||||||
: !stats.hasStarted
|
const networkDocUrl = config?.technical_documentation_url
|
||||||
? 'idle'
|
|
||||||
: stats.failed > 0
|
const state = getSummaryState(stats, isRunning)
|
||||||
? 'failed'
|
// Skipped device checks still deserve their hint under a route warning.
|
||||||
: stats.skipped > 0
|
const showPartialHint = state === 'warning' && stats.skipped > 0
|
||||||
? 'partial'
|
|
||||||
: 'passed'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={cardClass}>
|
<section className={cardClass}>
|
||||||
@@ -206,7 +231,27 @@ export const ConnectionTestSummary = ({
|
|||||||
: t(`summary.${state}`)}
|
: t(`summary.${state}`)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className={hintClass}>{t(`summary.${state}Hint`)}</p>
|
<p className={hintClass}>
|
||||||
|
{t(`summary.${state}Hint`)}
|
||||||
|
{state === 'warning' && networkDocUrl && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<A
|
||||||
|
href={networkDocUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="sm"
|
||||||
|
externalIcon
|
||||||
|
aria-label={t('summary.warningDocLinkAriaLabel')}
|
||||||
|
>
|
||||||
|
{t('summary.warningDocLink')}
|
||||||
|
</A>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{showPartialHint && (
|
||||||
|
<p className={hintClass}>{t('summary.partialHint')}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.hasStarted && (
|
{stats.hasStarted && (
|
||||||
@@ -237,6 +282,13 @@ export const ConnectionTestSummary = ({
|
|||||||
value={stats.passed}
|
value={stats.passed}
|
||||||
label={t('counts.passed')}
|
label={t('counts.passed')}
|
||||||
/>
|
/>
|
||||||
|
{stats.warnings > 0 && (
|
||||||
|
<Counter
|
||||||
|
squareClass={statusSquareClass.warning}
|
||||||
|
value={stats.warnings}
|
||||||
|
label={t('counts.warnings')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Counter
|
<Counter
|
||||||
squareClass={statusSquareClass.skipped}
|
squareClass={statusSquareClass.skipped}
|
||||||
value={stats.skipped}
|
value={stats.skipped}
|
||||||
|
|||||||
@@ -15,15 +15,20 @@ export const statusSquareClass: Record<ConnectionTestStepStatus, string> = {
|
|||||||
animation: 'pulse_background 1.2s ease-in-out infinite',
|
animation: 'pulse_background 1.2s ease-in-out infinite',
|
||||||
}),
|
}),
|
||||||
success: css({ backgroundColor: 'success.600' }),
|
success: css({ backgroundColor: 'success.600' }),
|
||||||
|
warning: css({ backgroundColor: 'warning' }),
|
||||||
failed: css({ backgroundColor: 'danger.600' }),
|
failed: css({ backgroundColor: 'danger.600' }),
|
||||||
skipped: css({ backgroundColor: 'greyscale.300' }),
|
skipped: css({ backgroundColor: 'greyscale.300' }),
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Colour is carried by the square; the label stays near-black except on failure. */
|
/**
|
||||||
|
* Colour is carried by the square; the label stays near-black except on
|
||||||
|
* failure and warning.
|
||||||
|
*/
|
||||||
export const statusTextClass: Record<ConnectionTestStepStatus, string> = {
|
export const statusTextClass: Record<ConnectionTestStepStatus, string> = {
|
||||||
pending: css({ color: 'greyscale.500' }),
|
pending: css({ color: 'greyscale.500' }),
|
||||||
running: css({ color: 'greyscale.700' }),
|
running: css({ color: 'greyscale.700' }),
|
||||||
success: css({ color: 'greyscale.1000' }),
|
success: css({ color: 'greyscale.1000' }),
|
||||||
|
warning: css({ color: 'warning', fontWeight: 'medium' }),
|
||||||
failed: css({ color: 'danger.600', fontWeight: 'medium' }),
|
failed: css({ color: 'danger.600', fontWeight: 'medium' }),
|
||||||
skipped: css({ color: 'greyscale.500' }),
|
skipped: css({ color: 'greyscale.500' }),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
type CheckInfo,
|
type CheckInfo,
|
||||||
} from 'livekit-client'
|
} from 'livekit-client'
|
||||||
import { fetchConnectionTestDetails } from '../api/fetchConnectionTestDetails'
|
import { fetchConnectionTestDetails } from '../api/fetchConnectionTestDetails'
|
||||||
import { SelectedCandidateCheck } from '../checks/selectedCandidate'
|
import {
|
||||||
|
isRelayedOverTcp,
|
||||||
|
SelectedCandidateCheck,
|
||||||
|
} from '../checks/selectedCandidate'
|
||||||
import {
|
import {
|
||||||
createInitialSteps,
|
createInitialSteps,
|
||||||
type ConnectionTestLog,
|
type ConnectionTestLog,
|
||||||
@@ -49,10 +52,23 @@ const getErrorMessage = (error: unknown, fallback = 'Unknown error') =>
|
|||||||
const isPermissionError = (error: unknown) =>
|
const isPermissionError = (error: unknown) =>
|
||||||
error instanceof Error && PERMISSION_ERROR_NAMES.has(error.name)
|
error instanceof Error && PERMISSION_ERROR_NAMES.has(error.name)
|
||||||
|
|
||||||
|
const toStepStatus = (info: CheckInfo): ConnectionTestStepStatus => {
|
||||||
|
const status = CHECK_STATUS_TO_STEP[info.status] ?? 'failed'
|
||||||
|
return status === 'success' && isRelayedOverTcp(info.data)
|
||||||
|
? 'warning'
|
||||||
|
: status
|
||||||
|
}
|
||||||
|
|
||||||
const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({
|
const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({
|
||||||
status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed',
|
status: toStepStatus(info),
|
||||||
summary: info.description,
|
summary: info.description,
|
||||||
logs: info.logs,
|
logs: info.logs,
|
||||||
|
// Only SelectedCandidateCheck sets `data` (the ICE candidate report).
|
||||||
|
// Consumers narrow it with a type guard (see isIceCandidateReport).
|
||||||
|
data:
|
||||||
|
typeof info.data === 'object' && info.data !== null
|
||||||
|
? (info.data as Record<string, unknown>)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
|
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type ConnectionTestStepStatus =
|
|||||||
| 'pending'
|
| 'pending'
|
||||||
| 'running'
|
| 'running'
|
||||||
| 'success'
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
| 'failed'
|
| 'failed'
|
||||||
| 'skipped'
|
| 'skipped'
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ export type ConnectionTestStats = {
|
|||||||
total: number
|
total: number
|
||||||
settled: number
|
settled: number
|
||||||
passed: number
|
passed: number
|
||||||
|
warnings: number
|
||||||
failed: number
|
failed: number
|
||||||
skipped: number
|
skipped: number
|
||||||
hasStarted: boolean
|
hasStarted: boolean
|
||||||
@@ -77,24 +79,27 @@ export const summarizeSteps = (
|
|||||||
steps: ConnectionTestStepResult[]
|
steps: ConnectionTestStepResult[]
|
||||||
): ConnectionTestStats => {
|
): ConnectionTestStats => {
|
||||||
let passed = 0
|
let passed = 0
|
||||||
|
let warnings = 0
|
||||||
let failed = 0
|
let failed = 0
|
||||||
let skipped = 0
|
let skipped = 0
|
||||||
let pending = 0
|
let pending = 0
|
||||||
|
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
if (step.status === 'success') passed += 1
|
if (step.status === 'success') passed += 1
|
||||||
|
else if (step.status === 'warning') warnings += 1
|
||||||
else if (step.status === 'failed') failed += 1
|
else if (step.status === 'failed') failed += 1
|
||||||
else if (step.status === 'skipped') skipped += 1
|
else if (step.status === 'skipped') skipped += 1
|
||||||
else if (step.status === 'pending') pending += 1
|
else if (step.status === 'pending') pending += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = steps.length
|
const total = steps.length
|
||||||
const settled = passed + failed + skipped
|
const settled = passed + warnings + failed + skipped
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
settled,
|
settled,
|
||||||
passed,
|
passed,
|
||||||
|
warnings,
|
||||||
failed,
|
failed,
|
||||||
skipped,
|
skipped,
|
||||||
hasStarted: pending < total,
|
hasStarted: pending < total,
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ export const Footer = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isConnectionTestEnabled = !!data.diagnostics?.connection_test_enabled
|
||||||
|
const technicalDocumentationUrl = data.technical_documentation_url
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className={css({
|
className={css({
|
||||||
@@ -256,7 +259,9 @@ export const Footer = () => {
|
|||||||
{t('links.data')}
|
{t('links.data')}
|
||||||
</A>
|
</A>
|
||||||
</StyledLi>
|
</StyledLi>
|
||||||
<StyledLi divider>
|
<StyledLi
|
||||||
|
divider={isConnectionTestEnabled || !!technicalDocumentationUrl}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
underline={false}
|
underline={false}
|
||||||
footer="minor"
|
footer="minor"
|
||||||
@@ -266,8 +271,8 @@ export const Footer = () => {
|
|||||||
{t('links.accessibility')}
|
{t('links.accessibility')}
|
||||||
</Link>
|
</Link>
|
||||||
</StyledLi>
|
</StyledLi>
|
||||||
{data?.diagnostics?.connection_test_enabled && (
|
{isConnectionTestEnabled && (
|
||||||
<StyledLi divider>
|
<StyledLi divider={!!technicalDocumentationUrl}>
|
||||||
<Link
|
<Link
|
||||||
underline={false}
|
underline={false}
|
||||||
footer="minor"
|
footer="minor"
|
||||||
@@ -278,19 +283,21 @@ export const Footer = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</StyledLi>
|
</StyledLi>
|
||||||
)}
|
)}
|
||||||
<StyledLi>
|
{technicalDocumentationUrl && (
|
||||||
<A
|
<StyledLi>
|
||||||
externalIcon
|
<A
|
||||||
underline={false}
|
externalIcon
|
||||||
footer="minor"
|
underline={false}
|
||||||
href="https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/"
|
footer="minor"
|
||||||
aria-label={
|
href={technicalDocumentationUrl}
|
||||||
t('links.technicalDetails') + ' - ' + t('links.ariaLabel')
|
aria-label={
|
||||||
}
|
t('links.technicalDetails') + ' - ' + t('links.ariaLabel')
|
||||||
>
|
}
|
||||||
{t('links.technicalDetails')}
|
>
|
||||||
</A>
|
{t('links.technicalDetails')}
|
||||||
</StyledLi>
|
</A>
|
||||||
|
</StyledLi>
|
||||||
|
)}
|
||||||
</SecondRow>
|
</SecondRow>
|
||||||
<ThirdRow>
|
<ThirdRow>
|
||||||
{t('mentions')}{' '}
|
{t('mentions')}{' '}
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"running": "Läuft…",
|
"running": "Läuft…",
|
||||||
"success": "Erfolgreich",
|
"success": "Erfolgreich",
|
||||||
|
"warning": "Nicht optimal",
|
||||||
"failed": "Fehlgeschlagen",
|
"failed": "Fehlgeschlagen",
|
||||||
"skipped": "Übersprungen"
|
"skipped": "Übersprungen"
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"passed": "erfolgreich",
|
"passed": "erfolgreich",
|
||||||
|
"warnings": "nicht optimal",
|
||||||
"failed": "fehlgeschlagen",
|
"failed": "fehlgeschlagen",
|
||||||
"skipped": "übersprungen"
|
"skipped": "übersprungen"
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
|
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
|
||||||
"partial": "Teilweiser Test",
|
"partial": "Teilweiser Test",
|
||||||
"partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.",
|
"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": "Sie können an Ihren Besprechungen teilnehmen, aber die Bild- und Tonqualität kann aufgrund Ihrer Netzwerkeinstellungen beeinträchtigt sein. Ihre IT-Abteilung kann hier Abhilfe schaffen.",
|
||||||
|
"warningDocLink": "Netzwerkanforderungen für Ihre IT-Abteilung",
|
||||||
|
"warningDocLinkAriaLabel": "Netzwerkanforderungen für Ihre IT-Abteilung öffnen – öffnet in neuem Tab",
|
||||||
"failed_one": "{{count}} Prüfung fehlgeschlagen",
|
"failed_one": "{{count}} Prüfung fehlgeschlagen",
|
||||||
"failed_other": "{{count}} Prüfungen 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."
|
"failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung."
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"running": "Running…",
|
"running": "Running…",
|
||||||
"success": "Passed",
|
"success": "Passed",
|
||||||
|
"warning": "Not optimal",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"skipped": "Skipped"
|
"skipped": "Skipped"
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"passed": "passed",
|
"passed": "passed",
|
||||||
|
"warnings": "not optimal",
|
||||||
"failed": "failed",
|
"failed": "failed",
|
||||||
"skipped": "skipped"
|
"skipped": "skipped"
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
"passedHint": "Your browser, your devices and your network are ready for a meeting.",
|
"passedHint": "Your browser, your devices and your network are ready for a meeting.",
|
||||||
"partial": "Partially tested",
|
"partial": "Partially tested",
|
||||||
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
|
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
|
||||||
|
"warning": "Suboptimal connection",
|
||||||
|
"warningHint": "You can join your meetings, but video and audio quality may be reduced because of your network settings. Your IT department can improve this.",
|
||||||
|
"warningDocLink": "Network requirements for your IT department",
|
||||||
|
"warningDocLinkAriaLabel": "Open the network requirements for your IT department - opens in new window",
|
||||||
"failed_one": "{{count}} check failed",
|
"failed_one": "{{count}} check failed",
|
||||||
"failed_other": "{{count}} checks failed",
|
"failed_other": "{{count}} checks failed",
|
||||||
"failedHint": "Open the failed checks below for details, then send the report to your IT department."
|
"failedHint": "Open the failed checks below for details, then send the report to your IT department."
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
"pending": "En espera",
|
"pending": "En espera",
|
||||||
"running": "En curso…",
|
"running": "En curso…",
|
||||||
"success": "Correcto",
|
"success": "Correcto",
|
||||||
|
"warning": "No óptimo",
|
||||||
"failed": "Error",
|
"failed": "Error",
|
||||||
"skipped": "Omitido"
|
"skipped": "Omitido"
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"passed": "correctas",
|
"passed": "correctas",
|
||||||
|
"warnings": "no óptimas",
|
||||||
"failed": "con errores",
|
"failed": "con errores",
|
||||||
"skipped": "omitidas"
|
"skipped": "omitidas"
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
"passedHint": "Tu navegador, tus dispositivos y tu red están listos para una reunión.",
|
"passedHint": "Tu navegador, tus dispositivos y tu red están listos para una reunión.",
|
||||||
"partial": "Prueba parcial",
|
"partial": "Prueba parcial",
|
||||||
"partialHint": "Se han omitido algunas comprobaciones. Autoriza el acceso a tu cámara y a tu micrófono para probarlos.",
|
"partialHint": "Se han omitido algunas comprobaciones. Autoriza el acceso a tu cámara y a tu micrófono para probarlos.",
|
||||||
|
"warning": "Conexión no óptima",
|
||||||
|
"warningHint": "Puedes participar en tus reuniones, pero la calidad de la imagen y del sonido puede verse reducida por la configuración de tu red. Tu servicio informático puede mejorar la situación.",
|
||||||
|
"warningDocLink": "Requisitos de red para tu servicio informático",
|
||||||
|
"warningDocLinkAriaLabel": "Abrir los requisitos de red para tu servicio informático - se abre en una nueva ventana",
|
||||||
"failed_one": "{{count}} verificación en error",
|
"failed_one": "{{count}} verificación en error",
|
||||||
"failed_other": "{{count}} verificaciones en error",
|
"failed_other": "{{count}} verificaciones en error",
|
||||||
"failedHint": "Abre las verificaciones en error para ver el detalle y transmite después el informe a tu servicio informático."
|
"failedHint": "Abre las verificaciones en error para ver el detalle y transmite después el informe a tu servicio informático."
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"running": "En cours…",
|
"running": "En cours…",
|
||||||
"success": "Réussi",
|
"success": "Réussi",
|
||||||
|
"warning": "Non optimal",
|
||||||
"failed": "Échec",
|
"failed": "Échec",
|
||||||
"skipped": "Ignoré"
|
"skipped": "Ignoré"
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"passed": "réussis",
|
"passed": "réussis",
|
||||||
|
"warnings": "non optimaux",
|
||||||
"failed": "en échec",
|
"failed": "en échec",
|
||||||
"skipped": "ignorés"
|
"skipped": "ignorés"
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
|
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
|
||||||
"partial": "Test partiel",
|
"partial": "Test partiel",
|
||||||
"partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.",
|
"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": "Vous pouvez participer à vos réunions, mais la qualité de l'image et du son risque d'être réduite à cause des réglages de votre réseau. Votre service informatique peut améliorer la situation.",
|
||||||
|
"warningDocLink": "Prérequis réseau à transmettre à votre service informatique",
|
||||||
|
"warningDocLinkAriaLabel": "Ouvrir les prérequis réseau à transmettre à votre service informatique - ouvre dans une nouvelle fenêtre",
|
||||||
"failed_one": "{{count}} vérification en échec",
|
"failed_one": "{{count}} vérification en échec",
|
||||||
"failed_other": "{{count}} vérifications 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."
|
"failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique."
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
"pending": "In afwachting",
|
"pending": "In afwachting",
|
||||||
"running": "Bezig…",
|
"running": "Bezig…",
|
||||||
"success": "Geslaagd",
|
"success": "Geslaagd",
|
||||||
|
"warning": "Niet optimaal",
|
||||||
"failed": "Mislukt",
|
"failed": "Mislukt",
|
||||||
"skipped": "Overgeslagen"
|
"skipped": "Overgeslagen"
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"passed": "geslaagd",
|
"passed": "geslaagd",
|
||||||
|
"warnings": "niet optimaal",
|
||||||
"failed": "mislukt",
|
"failed": "mislukt",
|
||||||
"skipped": "overgeslagen"
|
"skipped": "overgeslagen"
|
||||||
},
|
},
|
||||||
@@ -46,6 +48,10 @@
|
|||||||
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
|
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
|
||||||
"partial": "Gedeeltelijke test",
|
"partial": "Gedeeltelijke test",
|
||||||
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
|
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
|
||||||
|
"warning": "Verbinding niet optimaal",
|
||||||
|
"warningHint": "Je kunt deelnemen aan je vergaderingen, maar de beeld- en geluidskwaliteit kan minder zijn door de instellingen van je netwerk. Je IT-afdeling kan dit verbeteren.",
|
||||||
|
"warningDocLink": "Netwerkvereisten voor je IT-afdeling",
|
||||||
|
"warningDocLinkAriaLabel": "Netwerkvereisten voor je IT-afdeling openen - opent in nieuw venster",
|
||||||
"failed_one": "{{count}} controle mislukt",
|
"failed_one": "{{count}} controle mislukt",
|
||||||
"failed_other": "{{count}} controles mislukt",
|
"failed_other": "{{count}} controles mislukt",
|
||||||
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
|
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ backend:
|
|||||||
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
|
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
|
||||||
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
|
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
|
||||||
FRONTEND_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/7c5bd65d-3c21-486f-bce1-26e0a921d642/"
|
FRONTEND_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/7c5bd65d-3c21-486f-bce1-26e0a921d642/"
|
||||||
|
FRONTEND_TECHNICAL_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/"
|
||||||
FRONTEND_MANIFEST_LINK: "https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/"
|
FRONTEND_MANIFEST_LINK: "https://docs.numerique.gouv.fr/docs/1ef86abf-f7e0-46ce-b6c7-8be8b8af4c3d/"
|
||||||
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
|
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
|
||||||
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
|
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
|
||||||
|
|||||||
Reference in New Issue
Block a user