Imported from JaggerLewis/jag-support-app (
AGENTS.md). Install upstream withnpx skills add JaggerLewis/jag-support-app. Copyright stays with the author.
AGENTS.md — POC architecture mobile (BLE réel + feature flags)
Ce que c'est
Un POC Flutter standalone, mais qui prend la forme d'une vraie app
opérationnelle/support — pas un écran jetable. L'idée : si l'équipe support
ou terrain peut s'en servir pour appairer un collier, suivre son état et
pousser une mise à jour firmware réelle, alors l'architecture a fait ses
preuves sur un cas d'usage complet, pas seulement sur un flow de démo.
DeviceTransport en abstraction du BLE, feature flags PostHog derrière une
façade, Riverpod pour l'état, DFU réel pour la mise à jour firmware. Le BLE
est réimplémenté proprement sur universal_ble — décision du 2026-09-18,
motivée dans FRICTIONS.md. L'app d'origine (jagger-mobile, basée sur
flutter_reactive_ble) reste une référence à lire, jamais une dépendance.
Definition of done du POC : un opérateur peut scanner, appairer, voir
l'état d'un collier réel, lancer une vraie mise à jour DFU et suivre sa
progression jusqu'au bout (succès ou échec géré proprement) — le tout piloté
en CI via Patrol sur le runner self-hosted — et chaque friction rencontrée a
une entrée dans FRICTIONS.md.
Périmètre fonctionnel de l'app support
- Mettre à jour un device (DFU réel, voir §DFU réel)
- Synchroniser les données (device ↔ backend, via
api_client) - Envoyer des commandes Bluetooth au collier (via
device_collar) - Faire un diagnostic du collier (lecture d'état, logs, erreurs protocole)
- Afficher les infos du collier (version firmware, batterie, identifiants, dernier sync)
Le backend n'expose aucune spec OpenAPI/Swagger de son côté. Ce dépôt en
contient une, packages/api_client/openapi.yaml, mais elle a été
reconstituée depuis les appels HTTP de jagger-mobile : c'est l'inventaire
de ce que l'ancien client appelle, pas un contrat serveur. Elle décrit 79
routes dont au moins une n'existe pas, et l'extraction ne documente aucune
réponse. Les routes réellement utilisées par l'app support sont donc vérifiées
à la main contre la production, puis leur réponse est écrite dans ce fichier
avec la mention « relevé sur la production le ».
C'est en soi une friction à observer : sans contrat versionné côté serveur, une
dérive de l'API ne casse rien automatiquement côté app. Le cas s'est déjà
produit — voir FRICTIONS.md (2026-09-21).
Une documentation du protocole Bluetooth existe et sera fournie, mais elle
n'est pas versionnée dans ce dépôt (voir docs/ble/ dans .gitignore).
Un agent qui travaille sur device_collar doit s'appuyer sur cette doc
quand elle est présente localement, sans jamais la recopier ni la committer.
Si elle est absente au moment d'implémenter une trame ou une commande, le
signaler plutôt que de deviner le protocole. Le code BLE de jagger-mobile
peut être lu pour comprendre une trame ou un comportement device, mais il
n'est ni importé ni recopié : c'est de la documentation, pas une dépendance.
Layout du repo
packages/
app_core/ # Result, erreurs, pas de dépendance Flutter
device_transport/ # interface DeviceTransport + DTOs. SEUL package qui importe universal_ble
device_collar/ # protocole, commandes BLE, diagnostic, infos device, DFU réel (nordic_dfu)
api_client/ # clients HTTP écrits à la main (pas d'OpenAPI/Swagger côté backend) — dio.
# ApiClient = API applicative (Bearer) ; BoxClient = service d'ingestion (sans auth)
feature_flags/ # interface FeatureFlags + adapter PostHog. SEUL package qui importe posthog_flutter
apps/
mobile/ # l'app opérationnelle. N'importe JAMAIS le SDK BLE, nordic_dfu, dio ni posthog_flutter directement
apps/mobile/integration_test/ # E2E Patrol — DOIT vivre dans le package de l'app,
# `patrol test` s'exécute depuis apps/mobile
FRICTIONS.md # journal des points de friction, mis à jour à chaque obstacle
Frontières (non négociables même pour un POC)
apps/mobilene connaît queDeviceTransport,FeatureFlagset les use cases exposés pardevice_collaretapi_client— jamais les SDK sous-jacents (BLE,nordic_dfu,dio,posthog_flutter) directement.universal_bleest importé pardevice_transportet par lui seul. Aucun type du SDK ne franchit l'interface : ce qui sort deDeviceTransportest du Dart neutre (DTOs + streams). C'est ce qui rend le SDK remplaçable — le passage deflutter_reactive_bleàuniversal_blen'a touché aucun autre package.- Le dépôt
jagger-mobilen'est une dépendance d'aucun package d'ici. Pas depath:vers lui, pas de copier-coller de son code BLE. - Deux implémentations de
DeviceTransportdès le départ :RealBleTransport(universal_ble) etFakeDeviceTransport(en mémoire, pour itérer sans device). Choix au lancement via--dart-define=DEVICE_MODE=fake|real. - Le DFU réel (
nordic_dfu) vit uniquement dansdevice_collar— l'app déclenche une mise à jour via un use case (updateFirmware(deviceId)), jamais via l'APInordic_dfudirectement. - Les commandes BLE (envoi de commande, diagnostic, lecture d'infos) sont
des use cases nommés de
device_collar(sendCommand,runDiagnostic,readDeviceInfo) — pas des appels bruts au protocole depuis l'UI. api_clientest le seul package qui parle au backend (dio). Chaque route utilisée par l'app support y est documentée au moment où elle est ajoutée (méthode, payload, réponse attendue) puisqu'il n'y a pas de contrat OpenAPI à générer.
Commandes
fvm install # une fois, lit .fvmrc
dart pub global activate melos # une fois
melos bootstrap # pub get sur tout le workspace
melos run analyze # dart analyze sur chaque package
melos run test # tests unitaires/widgets, packages qui ont un dossier test/
# depuis apps/mobile — `flutter run` n'a pas de flag pour pointer un autre package
cd apps/mobile
cp config/example.json config/local.json # une fois, puis y mettre le token
fvm flutter run --dart-define-from-file=config/local.json
fvm flutter run --dart-define-from-file=config/local.json --dart-define=DEVICE_MODE=real
patrol test --target integration_test/pairing_test.dart --device <id> \
--dart-define-from-file=config/local.json # --device obligatoire, voir FRICTIONS.md
La configuration passe par --dart-define-from-file, pas par des
--dart-define empilés : config/local.json porte DEVICE_MODE,
API_BASE_URL, API_TOKEN et BOX_BASE_URL (le service d'ingestion, qui
n'est pas le même hôte que l'API — voir FRICTIONS.md). Il est gitignoré
— il contient un Bearer — et config/example.json en est le gabarit suivi. Un --dart-define passé
explicitement l'emporte sur la valeur du fichier (vérifié), ce qui permet
de basculer en DEVICE_MODE=real sans éditer le fichier.
Version Flutter épinglée dans .fvmrc (3.47.4) — toujours passer par fvm,
jamais flutter en direct, pour que la version soit la même en local que
pour quiconque reprend le POC. Melos (configuré sous la clé melos: du
pubspec.yaml racine — en workspace pub, un melos.yaml est ignoré) évite
de répéter les mêmes commandes sur les packages du workspace ; c'est aussi
ce que le job CI appelle plutôt que des commandes flutter éparpillées.
Patrol + CI sur runner self-hosted
Le POC couvre aussi le pilotage E2E et son passage en CI, pas seulement le code applicatif :
- Patrol pour l'E2E (accès natif : permissions, Bluetooth système), avec
patrol_mcppour que l'agent puisse lancer un test, lister les devices, capturer l'écran et l'arbre natif. - Un runner self-hosted (pas les runners GitHub hébergés — ils n'ont pas accès à du vrai hardware BLE) qui exécute le job E2E contre le device réel branché en USB.
- Le workflow CI est volontairement minimal pour ce POC : un seul job qui lance Patrol sur le runner self-hosted. Pas de lint bloquant, pas de goldens, pas de build multi-plateforme — ce n'est pas encore l'objet du POC.
- Toute friction sur le pilotage du runner (device qui se déconnecte entre
deux jobs, permissions USB, état du device pas remis à zéro entre deux
runs) va dans
FRICTIONS.mdau même titre que les frictions applicatives.
DFU réel
Le DFU n'est pas simulé dans ce POC : il fait partie de ce qu'on évalue.
nordic_dfu(SoC Nordic confirmé) enveloppé dansdevice_collar, derrière un use case simple (updateFirmware) qui expose progression et succès/échec sans fuir l'API du package.- Un firmware de test suffit — pas besoin de la dernière version de production, juste d'un binaire valide pour vérifier tout le chemin.
- Frictions typiques à surveiller : la connexion BLE doit-elle être fermée avant d'entrer en mode DFU, le device redémarre-t-il en bootloader de façon fiable, la reprise après échec réseau/BLE en plein transfert.
- Le use case DFU est couvert par au moins un test Patrol E2E réel (voir ci-dessus), pas seulement par un test unitaire sur le wrapper.
Ce qui n'est PAS dans ce POC
Sentry, Widgetbook, goldens, tests de contrat, Bumble, Figma. Rien de tout ça n'aide à répondre à la question du POC : est-ce que cette architecture (transport, flags, DFU réel, et pilotage E2E en CI) tient avec du vrai hardware, jusqu'à devenir une app support utilisable.
Quand un obstacle apparaît
Ne pas contourner silencieusement. Ajouter une entrée dans FRICTIONS.md
(symptôme, pourquoi l'abstraction ne suffit pas telle quelle, contournement
choisi, effort estimé pour une vraie migration). C'est la sortie la plus
utile de ce POC, avant même l'écran qui fonctionne.
Glossaire
- DeviceTransport : interface unique par laquelle l'app parle à un device — connect/disconnect/stream d'état, quelle que soit l'implémentation dessous.
- FeatureFlags : façade au-dessus de PostHog (
isEnabled,getVariant) ; l'app n'appelle jamais le SDK PostHog directement. - DEVICE_MODE : bascule de lancement entre transport réel et factice.