docs(fer-19) : plan d'implementation health-check pont-bascule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 13:56:22 +02:00
parent 0390f5f1a2
commit d6992aa549
2 changed files with 665 additions and 11 deletions

View File

@@ -0,0 +1,647 @@
# Health-check pont-bascule sur l'écran de pesée — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Brancher le vrai health-check du pont-bascule à l'arrivée sur l'écran de pesée, afficher l'état réel et désactiver le bouton « peser » tant que le pont n'est pas valide.
**Architecture:** Une URL de base unique (`PONT_BASCULE_BASE_URL`) ; le `PontBasculeService` construit `/send/dsd` et `/health`. Un nouvel endpoint générique `GET /pont_bascule/health` (carrier API Platform + DTO + provider) renvoie toujours `200 { healthy: bool, ... }`. Le front interroge cet endpoint une fois au montage de `WorkflowWeight`, affiche l'état et grise le bouton « peser » si non sain. Le bypass dev renvoie un état sain.
**Tech Stack:** Symfony 8 / API Platform 4 (PHP 8.4, PHPUnit, MockHttpClient) ; Nuxt 4 (Vue 3, composables).
**Spec:** `docs/superpowers/specs/2026-05-21-pont-bascule-healthcheck-design.md`
---
## File Structure
**Backend (créés) :**
- `src/Dto/PontBasculeHealth.php` — DTO domaine : `healthy` + champs informatifs, getters, Groups, factory `unhealthy()`.
- `src/ApiResource/PontBasculeHealthCheck.php` — carrier API Platform hébergeant `GET /pont_bascule/health`, `output: PontBasculeHealth::class`.
- `src/State/PontBasculeHealthProvider.php` — provider qui appelle `PontBasculeService::checkHealth()`.
**Backend (modifiés) :**
- `src/Service/PontBasculeService.php``$endpoint``$baseUrl` ; `fetch()` POST `{base}/send/dsd` ; nouvelle méthode `checkHealth()`.
- `config/services.yaml` — argument `$baseUrl``PONT_BASCULE_BASE_URL`.
- `.env`, `.env.local`, `.env.prod``PONT_BASCULE_URL``PONT_BASCULE_BASE_URL` (sans `/send/dsd`).
- `tests/Service/PontBasculeServiceTest.php` — adapter l'URL attendue + tests `checkHealth()`.
**Frontend (créés) :**
- `frontend/composables/usePontBascule.ts` — état `status` + `checkHealth()`.
**Frontend (modifiés) :**
- `frontend/components/workflow/workflow-weight.vue` — affichage dynamique de l'état + `disabled` sur « peser ».
---
## Task 1 : Refactor env vers une URL de base unique
**Files:**
- Modify: `src/Service/PontBasculeService.php:15-36`
- Modify: `config/services.yaml:25-28`
- Modify: `.env:21-22`, `.env.local:21-22`, `.env.prod:21-22`
- Test: `tests/Service/PontBasculeServiceTest.php`
- [ ] **Step 1 : Adapter le test existant à l'URL `/send/dsd`**
Dans `tests/Service/PontBasculeServiceTest.php`, méthode `testFetchUsesHttpClientWhenNotBypass`, remplacer l'attente d'URL :
```php
$httpClient
->expects(self::once())
->method('request')
->with('POST', 'http://example.test/send/dsd')
->willReturn($response)
;
```
(Les autres `'http://example.test'` passés au constructeur restent la **base** — inchangés.)
- [ ] **Step 2 : Lancer le test, vérifier l'échec**
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
Expected: FAIL sur `testFetchUsesHttpClientWhenNotBypass` (l'implémentation appelle encore `http://example.test`).
- [ ] **Step 3 : Renommer `$endpoint` → `$baseUrl` et construire `/send/dsd`**
Dans `src/Service/PontBasculeService.php`, constructeur et `fetch()` :
```php
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly PontBasculePayloadDecoder $payloadDecoder,
private readonly string $baseUrl,
private readonly bool $bypass,
) {}
```
et dans `fetch()` remplacer la ligne de requête :
```php
$response = $this->httpClient->request('POST', $this->baseUrl . '/send/dsd');
```
- [ ] **Step 4 : Lancer le test, vérifier le succès**
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
Expected: PASS (3 tests).
- [ ] **Step 5 : Mettre à jour `config/services.yaml`**
Remplacer le bloc du service (lignes 25-28) :
```yaml
App\Service\PontBasculeService:
arguments:
$baseUrl: '%env(PONT_BASCULE_BASE_URL)%'
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
```
- [ ] **Step 6 : Mettre à jour les fichiers `.env`**
Dans `.env` (lignes 21-22), remplacer :
```
PONT_BASCULE_BYPASS=
PONT_BASCULE_BASE_URL=
```
Dans `.env.local` (lignes 21-22), remplacer :
```
PONT_BASCULE_BYPASS=true
PONT_BASCULE_BASE_URL="http://100.122.43.54:8000"
```
Dans `.env.prod` (lignes 21-22), remplacer :
```
PONT_BASCULE_BYPASS=true
PONT_BASCULE_BASE_URL="http://100.122.43.54:8000"
```
- [ ] **Step 7 : Vider le cache et relancer toute la suite**
Run: `make cache-clear && make test`
Expected: PASS (9 tests, comme avant).
- [ ] **Step 8 : Commit**
```bash
git add src/Service/PontBasculeService.php config/services.yaml .env .env.local .env.prod tests/Service/PontBasculeServiceTest.php
git commit -m "refactor(fer-19) : url de base unique pour le pont-bascule"
```
---
## Task 2 : DTO `PontBasculeHealth`
**Files:**
- Create: `src/Dto/PontBasculeHealth.php`
- [ ] **Step 1 : Créer le DTO**
`src/Dto/PontBasculeHealth.php` — mêmes conventions que `PontBasculeReading` (readonly, Groups sur les props promues, getters) :
```php
<?php
declare(strict_types=1);
namespace App\Dto;
use Symfony\Component\Serializer\Attribute\Groups;
final readonly class PontBasculeHealth
{
public function __construct(
#[Groups(['pont_bascule:health:read'])]
private bool $healthy,
#[Groups(['pont_bascule:health:read'])]
private bool $ok = false,
#[Groups(['pont_bascule:health:read'])]
private bool $busy = false,
#[Groups(['pont_bascule:health:read'])]
private bool $portConnected = false,
#[Groups(['pont_bascule:health:read'])]
private ?string $portError = null,
#[Groups(['pont_bascule:health:read'])]
private ?string $hostname = null,
) {}
public static function unhealthy(): self
{
return new self(false);
}
public function isHealthy(): bool
{
return $this->healthy;
}
public function isOk(): bool
{
return $this->ok;
}
public function isBusy(): bool
{
return $this->busy;
}
public function isPortConnected(): bool
{
return $this->portConnected;
}
public function getPortError(): ?string
{
return $this->portError;
}
public function getHostname(): ?string
{
return $this->hostname;
}
}
```
- [ ] **Step 2 : Vérifier que PHP parse le fichier**
Run: `make shell` puis `php -l src/Dto/PontBasculeHealth.php` — ou directement :
`docker exec -u www-data php-ferme-fpm php -l src/Dto/PontBasculeHealth.php`
Expected: `No syntax errors detected`.
- [ ] **Step 3 : Commit**
```bash
git add src/Dto/PontBasculeHealth.php
git commit -m "feat(fer-19) : dto PontBasculeHealth"
```
---
## Task 3 : Méthode `PontBasculeService::checkHealth()`
**Files:**
- Modify: `src/Service/PontBasculeService.php`
- Test: `tests/Service/PontBasculeServiceTest.php`
- [ ] **Step 1 : Écrire les tests `checkHealth()`**
Ajouter dans `tests/Service/PontBasculeServiceTest.php` (le `use App\Dto\PontBasculeHealth;` n'est pas requis, on lit via getters). Ajouter ces méthodes à la classe :
```php
public function testCheckHealthBypassIsHealthyWithoutHttpCall(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->expects(self::never())->method('request');
$service = new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', true);
$health = $service->checkHealth();
self::assertTrue($health->isHealthy());
}
public function testCheckHealthHealthyPayload(): void
{
$service = $this->serviceForHealthBody(json_encode([
'ok' => true,
'busy' => false,
'port_connected' => true,
'port_error' => null,
'hostname' => 'liot-rasp-ferme-01',
], JSON_THROW_ON_ERROR));
$health = $service->checkHealth();
self::assertTrue($health->isHealthy());
self::assertSame('liot-rasp-ferme-01', $health->getHostname());
}
public function testCheckHealthUnhealthyWhenPortError(): void
{
$service = $this->serviceForHealthBody(json_encode([
'ok' => true,
'busy' => false,
'port_connected' => true,
'port_error' => 'device disconnected',
], JSON_THROW_ON_ERROR));
self::assertFalse($service->checkHealth()->isHealthy());
}
public function testCheckHealthUnhealthyWhenPortNotConnected(): void
{
$service = $this->serviceForHealthBody(json_encode([
'ok' => true,
'busy' => false,
'port_connected' => false,
'port_error' => null,
], JSON_THROW_ON_ERROR));
self::assertFalse($service->checkHealth()->isHealthy());
}
public function testCheckHealthUnhealthyWhenBusy(): void
{
$service = $this->serviceForHealthBody(json_encode([
'ok' => true,
'busy' => true,
'port_connected' => true,
'port_error' => null,
], JSON_THROW_ON_ERROR));
self::assertFalse($service->checkHealth()->isHealthy());
}
public function testCheckHealthUnhealthyOnTransportFailure(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient
->expects(self::once())
->method('request')
->willThrowException($this->createStub(TransportExceptionInterface::class))
;
$service = new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', false);
self::assertFalse($service->checkHealth()->isHealthy());
}
public function testCheckHealthUnhealthyOnInvalidJson(): void
{
self::assertFalse($this->serviceForHealthBody('not-json')->checkHealth()->isHealthy());
}
private function serviceForHealthBody(string $body): PontBasculeService
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getContent')->with(false)->willReturn($body);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient
->expects(self::once())
->method('request')
->with('GET', 'http://example.test/health')
->willReturn($response)
;
return new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', false);
}
```
- [ ] **Step 2 : Lancer les tests, vérifier l'échec**
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
Expected: FAIL (`checkHealth()` n'existe pas → Error).
- [ ] **Step 3 : Implémenter `checkHealth()`**
Dans `src/Service/PontBasculeService.php`, ajouter `use App\Dto\PontBasculeHealth;` en tête, puis ajouter la méthode après `fetch()` :
```php
public function checkHealth(): PontBasculeHealth
{
if ($this->bypass) {
return new PontBasculeHealth(
healthy: true,
ok: true,
busy: false,
portConnected: true,
portError: null,
hostname: 'bypass',
);
}
try {
$response = $this->httpClient->request('GET', $this->baseUrl . '/health');
$body = $response->getContent(false);
} catch (TransportExceptionInterface) {
return PontBasculeHealth::unhealthy();
}
$payload = json_decode($body, true);
if (!is_array($payload)) {
return PontBasculeHealth::unhealthy();
}
$ok = true === ($payload['ok'] ?? null);
$busy = true === ($payload['busy'] ?? null);
$portConnected = true === ($payload['port_connected'] ?? null);
$portError = $payload['port_error'] ?? null;
$hostname = $payload['hostname'] ?? null;
$healthy = $ok && $portConnected && !$busy && null === $portError;
return new PontBasculeHealth(
healthy: $healthy,
ok: $ok,
busy: $busy,
portConnected: $portConnected,
portError: is_string($portError) ? $portError : null,
hostname: is_string($hostname) ? $hostname : null,
);
}
```
- [ ] **Step 4 : Lancer les tests, vérifier le succès**
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
Expected: PASS (10 tests : 3 `fetch` + 7 `checkHealth`).
- [ ] **Step 5 : Commit**
```bash
git add src/Service/PontBasculeService.php tests/Service/PontBasculeServiceTest.php
git commit -m "feat(fer-19) : checkHealth sur le PontBasculeService"
```
---
## Task 4 : Endpoint `GET /pont_bascule/health`
**Files:**
- Create: `src/State/PontBasculeHealthProvider.php`
- Create: `src/ApiResource/PontBasculeHealthCheck.php`
- [ ] **Step 1 : Créer le provider**
`src/State/PontBasculeHealthProvider.php` (renvoie le DTO directement, comme `ReceptionWeighingProvider`, mais sans jamais lever d'exception) :
```php
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\PontBasculeHealth;
use App\Service\PontBasculeService;
final readonly class PontBasculeHealthProvider implements ProviderInterface
{
public function __construct(
private PontBasculeService $pontBasculeService,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): PontBasculeHealth
{
return $this->pontBasculeService->checkHealth();
}
}
```
- [ ] **Step 2 : Créer la ressource carrier**
`src/ApiResource/PontBasculeHealthCheck.php` (calque de `AppVersion` pour le style standalone, avec `output` séparé comme `/receptions/weigh`) :
```php
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use App\Dto\PontBasculeHealth;
use App\State\PontBasculeHealthProvider;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/pont_bascule/health',
openapi: new OpenApiOperation(
summary: 'Pont-bascule health check',
description: 'Returns the connection state of the pont-bascule. Always 200, even when unreachable.',
),
normalizationContext: ['groups' => ['pont_bascule:health:read']],
output: PontBasculeHealth::class,
provider: PontBasculeHealthProvider::class,
),
],
)]
final class PontBasculeHealthCheck {}
```
- [ ] **Step 3 : Vider le cache et vérifier que la route est déclarée**
Run: `make cache-clear && docker exec -u www-data php-ferme-fpm php bin/console debug:router | grep pont_bascule`
Expected: une ligne listant `GET /api/pont_bascule/health`.
- [ ] **Step 4 : Vérifier la réponse en bypass (sain)**
Le bypass est actif en dev (`.env.local`). Appeler l'endpoint authentifié n'étant pas trivial, vérifier d'abord sans auth que la route répond (401 attendu = route existe et sécurité globale `ROLE_USER` active) :
Run: `curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/api/pont_bascule/health`
Expected: `401` (route trouvée, auth requise) — **pas** `404`.
Si tu disposes d'un token JWT de dev, vérifier le corps :
Run: `curl -s http://localhost:8080/api/pont_bascule/health -H "Authorization: Bearer <token>"`
Expected: JSON contenant `"healthy":true` (bypass actif).
- [ ] **Step 5 : Commit**
```bash
git add src/State/PontBasculeHealthProvider.php src/ApiResource/PontBasculeHealthCheck.php
git commit -m "feat(fer-19) : endpoint GET /pont_bascule/health"
```
---
## Task 5 : Composable `usePontBascule`
**Files:**
- Create: `frontend/composables/usePontBascule.ts`
- [ ] **Step 1 : Créer le composable**
`frontend/composables/usePontBascule.ts` (mêmes conventions que `useAppVersion` : `useApi`, `toast: false` pour ne pas afficher d'erreur si le pont est down) :
```ts
import { ref } from 'vue'
import { useApi } from '~/composables/useApi'
export type PontBasculeStatus = 'checking' | 'connected' | 'disconnected'
export const usePontBascule = () => {
const api = useApi()
const status = ref<PontBasculeStatus>('checking')
const checkHealth = async () => {
status.value = 'checking'
try {
const res = await api.get<{ healthy: boolean }>('pont_bascule/health', {}, {
toast: false
})
status.value = res.healthy ? 'connected' : 'disconnected'
} catch {
status.value = 'disconnected'
}
}
return { status, checkHealth }
}
```
- [ ] **Step 2 : Commit**
```bash
git add frontend/composables/usePontBascule.ts
git commit -m "feat(fer-19) : composable usePontBascule"
```
---
## Task 6 : Affichage de l'état + désactivation du bouton dans `WorkflowWeight`
**Files:**
- Modify: `frontend/components/workflow/workflow-weight.vue:5` (texte) et `:19-23` (bouton peser) et `<script setup>`
- [ ] **Step 1 : Remplacer le texte hardcodé par l'affichage dynamique**
Dans `frontend/components/workflow/workflow-weight.vue`, remplacer la ligne 5 :
```html
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
```
par :
```html
<p
v-if="pontBasculeStatus === 'checking'"
class="uppercase text-2xl mt-2 text-primary-500">Vérification du pont-bascule…</p>
<p
v-else-if="pontBasculeStatus === 'connected'"
class="uppercase text-2xl mt-2 text-green-600">Pont-bascule connecté</p>
<p
v-else
class="uppercase text-2xl mt-2 text-red-600">Pont-bascule non connecté</p>
```
- [ ] **Step 2 : Désactiver le bouton « peser » tant que non connecté**
Toujours dans le template, ajouter `:disabled` sur le **premier** `UiButton` (celui qui déclenche `fetchWeight`) :
```html
<UiButton
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
:disabled="pontBasculeStatus !== 'connected'"
@click="fetchWeight"
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
```
(Ne pas toucher aux boutons « Valider la pesée » / « Générer le bon ».)
- [ ] **Step 3 : Brancher le composable dans `<script setup>`**
Dans le bloc `<script setup lang="ts">`, ajouter l'import en tête (à côté de `import { toRef } from 'vue'`) :
```ts
import { onMounted, toRef } from 'vue'
import { usePontBascule } from '~/composables/usePontBascule'
```
puis, après la ligne `const entityRef = toRef(props, 'entity')`, ajouter :
```ts
const { status: pontBasculeStatus, checkHealth } = usePontBascule()
onMounted(checkHealth)
```
- [ ] **Step 4 : Build front pour vérifier qu'il n'y a pas d'erreur de compilation/type**
Run: `cd frontend && npm run build:dist`
Expected: build OK, aucune erreur TypeScript ni de template.
- [ ] **Step 5 : Vérification manuelle (bypass on = sain)**
Avec `PONT_BASCULE_BYPASS=true` (dev), lancer le front (`cd frontend && npm run dev`), aller sur une réception à l'étape de pesée :
- Au chargement : bref « Vérification du pont-bascule… », puis « Pont-bascule connecté » (vert).
- Le bouton « peser » est actif et fonctionne comme avant.
- [ ] **Step 6 : Vérification manuelle (état non sain = bouton grisé)**
Simuler un pont down sans toucher au backend : dans l'onglet réseau / via un override temporaire, forcer la réponse de `pont_bascule/health` à `{ "healthy": false }` (ou couper `PONT_BASCULE_BYPASS` et pointer une base URL injoignable). Recharger l'écran de pesée :
- Texte « Pont-bascule non connecté » (rouge).
- Bouton « peser » grisé (`opacity-60`, `cursor-not-allowed`) et non cliquable.
Rétablir `PONT_BASCULE_BYPASS=true` après le test.
- [ ] **Step 7 : Commit**
```bash
git add frontend/components/workflow/workflow-weight.vue
git commit -m "feat(fer-19) : affichage etat pont-bascule et desactivation du bouton peser"
```
---
## Vérification finale
- [ ] **Suite backend complète**
Run: `make test`
Expected: PASS (16 tests : 9 existants + 7 nouveaux `checkHealth`).
- [ ] **Style PHP**
Run: `make php-cs-fixer-allow-risky FILES=src/Service/PontBasculeService.php src/Dto/PontBasculeHealth.php src/ApiResource/PontBasculeHealthCheck.php src/State/PontBasculeHealthProvider.php`
Expected: fichiers conformes (aucune correction ou corrections appliquées puis re-commit).
- [ ] **Route présente**
Run: `docker exec -u www-data php-ferme-fpm php bin/console debug:router | grep pont_bascule`
Expected: `GET /api/pont_bascule/health`.
---
## Notes de risque
- **Carrier + `output` sur ressource non-entité :** si API Platform refuse `output:` sur la classe carrier vide `PontBasculeHealthCheck`, repli sur le pattern `AppVersion` pur — déplacer les champs (props publiques + Groups) directement dans `PontBasculeHealthCheck`, supprimer `output:`, et faire que `PontBasculeHealthProvider` mappe le DTO `PontBasculeHealth` vers une instance de `PontBasculeHealthCheck`. Le comportement HTTP reste identique. (Le repli ne change que le câblage interne, pas l'API ni le front.)
- **Check unique au montage (choix produit) :** si le pont est down à l'arrivée, le bouton reste grisé jusqu'au rechargement de la page. Conforme à la spec ; pas de polling ni de bouton « réessayer » dans ce périmètre.

View File

@@ -88,15 +88,19 @@ Groupe de sérialisation `pont_bascule:health:read`. Champs :
- Champs informatifs (debug / affichage futur) : `ok: bool`, `busy: bool`,
`portConnected: bool`, `portError: ?string`, `hostname: ?string`.
### Ressource API `PontBasculeHealth` (`src/ApiResource/PontBasculeHealth.php`)
### Ressource API `PontBasculeHealthCheck` (`src/ApiResource/PontBasculeHealthCheck.php`)
- Opération `GET /pont_bascule/health`, sans état persistant, `output` = DTO,
`provider` = `PontBasculeHealthProvider`.
- Endpoint au pluriel : `pont_bascule` est déjà invariable, conserver `/pont_bascule/health`.
- Classe carrier fine (vide) hébergeant l'opération `GET /pont_bascule/health`,
sans état persistant, `output` = `PontBasculeHealth::class` (le DTO), `provider` =
`PontBasculeHealthProvider`. Même montage que `/receptions/weigh` (host déclare
l'opération, `output` = DTO, provider renvoie le DTO).
- Nommée `PontBasculeHealthCheck` pour éviter la collision de nom court avec le DTO
`Dto\PontBasculeHealth`.
- Route conservée `/pont_bascule/health` (`pont_bascule` invariable).
### `PontBasculeHealthProvider` (`src/State/`)
### `PontBasculeHealthProvider` (`src/State/PontBasculeHealthProvider.php`)
- Appelle `PontBasculeService::checkHealth()` et renvoie le DTO.
- Appelle `PontBasculeService::checkHealth()` et renvoie le DTO `PontBasculeHealth`.
- Toujours `200`, même pont down (pas de `HttpException`).
## Frontend
@@ -126,13 +130,16 @@ Groupe de sérialisation `pont_bascule:health:read`. Champs :
- Le bouton **« peser »** reçoit `disabled` tant que `pontBasculeStatus !== 'connected'`
(état grisé). Les boutons « Valider la pesée » et « Générer le bon » restent inchangés.
### i18n (`frontend/i18n/locales/fr.json`)
### Textes d'état (codés en dur)
Nouvelles clés :
Convention du projet : les textes d'UI dans les composants/pages sont **codés en dur en
français** (le « Pont-bascule connecté » actuel l'est déjà). `fr.json` n'est consommé que
par `useApi` pour les toasts. On reste cohérent : les trois libellés sont écrits directement
dans `workflow-weight.vue`, **pas** dans `fr.json`.
- `pontBascule.checking` → « Vérification du pont-bascule… »
- `pontBascule.connected` → « Pont-bascule connecté »
- `pontBascule.disconnected` → « Pont-bascule non connecté »
- `checking` → « Vérification du pont-bascule… » (couleur primary)
- `connected` → « Pont-bascule connecté » (vert)
- `disconnected` → « Pont-bascule non connecté » (rouge)
## Error handling