Bonjour à tous,
Suite au live d’hier et la demande d’application mobile, j’ai lancé Claude sur le sujet juste pour voir. En moins de 15 minutes, il m’a sorti une spec complète et une démo sur mon téléphone Android où je retrouve mes dashboards, scènes… tout Gladys en fait. Je suis bluffé… ![]()
Voilà son analyse:
Le je dans le message c’est Claude, j’aurais été incapable de sortir tout cela en 15 minutes.
J’ai commencé à travailler sur une application mobile native pour Gladys, et je voudrais partager la démarche avant d’aller plus loin — notamment le choix d’architecture, qui me semble le point le plus discutable et donc celui sur lequel vos retours seront les plus utiles.
Le socle est fonctionnel : l’app tourne sur mon téléphone, connectée à mon instance, avec tous mes dashboards et mes caméras.
Le problème
Aujourd’hui, Gladys sur mobile, c’est le navigateur ou l’ajout à l’écran d’accueil. Ça fonctionne, mais ça plafonne :
- pas de notifications fiables ;
- pas de suivi de présence en arrière-plan ;
- pas de présence sur les stores ;
- et le service worker actuel (
front/old-sw.js) se désinstalle volontairement à chaque activation — il n’y a donc pas de base PWA exploitable sur laquelle bâtir.
Le choix d’architecture : embarquer le front existant
C’est la décision structurante. Trois options étaient sur la table.
Option A — React Native, une app native distincte
Une app réécrite, consommant l’API REST et le WebSocket de Gladys. Meilleures performances, vraie sensation native.
Mais il faut réécrire toute l’interface : 23 routes, 22 types de widgets de dashboard, l’éditeur de scènes, la carte, le chat, les pages d’intégration. Et surtout, il faut ensuite maintenir deux interfaces en parallèle : chaque évolution du front web devrait être reportée à la main dans l’app mobile, avec le risque de divergence que ça implique. Pour un projet communautaire, ça me paraît intenable sur la durée.
Option B — Améliorer la PWA
Réparer le service worker, ajouter le cache hors ligne et le Web Push. Léger, pas de store à gérer.
Mais ça ne résout ni la géolocalisation en arrière-plan, ni les widgets d’écran d’accueil, ni la présence sur les stores. Et sur iOS, le Web Push reste limité. On resterait avec les mêmes plafonds.
Option C — Capacitor + le front existant ← retenu
Le front Gladys actuel (Preact / Vite) est compilé exactement comme aujourd’hui, puis embarqué dans une WebView native par Capacitor. Les capacités que le web ne sait pas offrir sont exposées via des plugins, derrière une couche d’abstraction.
front/ (Preact, inchangé)
└── build/ ──> WebView Capacitor
├── android/
└── ios/
+ plugins natifs : push, géoloc, biométrie,
mDNS, stockage sécurisé, widgets
Pourquoi ce choix :
- Un seul codebase. Toute évolution du front bénéficie au web, à Android et à iOS sans portage. C’est le point décisif pour un projet communautaire.
- Réutilisation quasi totale. Écrans, widgets, actions, traductions, thème sombre : tout est repris tel quel.
- Rien de perdu côté natif. Notifications push, géolocalisation par zones, biométrie, découverte mDNS, widgets d’écran d’accueil restent accessibles via les plugins.
- Réversible. Si l’approche montre ses limites, le front reste le front — on n’a pas créé de dette parallèle.
Le principe directeur : le shell natif n’embarque aucune logique métier. Il expose des capacités, le front les consomme. Toute fonctionnalité native est optionnelle, et son absence (permission refusée, plateforme non compatible) ne casse jamais l’application.
Ce qui a été fait : le socle de connexion
Le premier lot répond à une question que le front web ne se pose jamais : à quelle instance parler ?
Sur le web, la réponse est triviale — le front est servi par l’instance elle-même. Une app mobile, elle, est distribuée en un seul build pour tout le monde et doit être configurée par l’utilisateur.
Concrètement :
- Configuration résolue à l’exécution.
front/src/config.jslisaitprocess.env, que Vite remplace par des littéraux au build. L’objet exporté est désormais mutable, avec une liste blanche : seules les clés de connexion sont surchargeables, pour qu’un profil stocké malformé ne puisse pas activer le mode démo. - Écran de première connexion demandant l’adresse de l’instance, avec test de l’adresse avant enregistrement.
- Profils multi-instances (résidence principale, secondaire, instance de test), stockés dans le Keychain / Keystore.
- Bascule automatique réseau local ↔ Gladys Plus, avec une sonde sur
/api/v1/ping. - Page de réglages pour gérer ces instances — masquée sur le web, où elle n’aurait aucun sens.
Impact sur le front web : nul. Le code natif est entièrement éliminé du bundle web par le tree-shaking, et les valeurs de build restent celles d’avant. Vérifié à la main sur navigateur.
Trois choses que la spec n’avait pas prévues
C’est la partie la plus instructive, et celle qui peut servir à d’autres.
1. La règle mixed-content, et le compromis qu’elle impose
Pour que le chiffrement de bout en bout de Gladys Plus fonctionne, il faut crypto.subtle, qui n’est disponible que sur un contexte sécurisé. Capacitor l’obtient en servant la WebView en https://localhost.
Sauf qu’une page https:// ne peut appeler ni http:// ni ws:// — Chromium bloque. Or une instance Gladys locale est très souvent en HTTP simple. La configuration de sécurité réseau d’Android n’y change rien : c’est une politique du navigateur, pas de la plateforme.
Le plugin CapacitorHttp règle le cas des requêtes HTTP (elles sortent par la couche native), mais pas celui des WebSockets, qui restent bloqués. Sans WebSocket, pas de temps réel.
D’où l’arbitrage, qu’il faut connaître :
androidScheme |
Contexte sécurisé | Gladys Plus | HTTP local | WebSocket |
|---|---|---|---|---|
https |
oui | OK | via CapacitorHttp | bloqué |
http |
non | indisponible | OK | OK |
Pour ce premier lot, j’ai choisi http : l’objectif était la connexion à l’instance locale, qui exige le WebSocket. C’est explicitement temporaire. La sortie propre est de servir l’instance en HTTPS derrière un reverse proxy (Caddy, nginx, Traefik) : tout passe alors en https/wss, plus aucun blocage, contexte sécurisé conservé. C’est aussi la seule voie qui vaudra pour iOS.
Si vous avez un avis sur ce point, ça m’intéresse beaucoup : faut-il assumer d’exiger une instance en HTTPS pour l’usage de Gladys Plus depuis l’app mobile, ou vaut-il mieux investir dans un plugin WebSocket natif pour rester en https ?
2. Un client HTTP qui figeait son URL
HttpClient capturait config.localApiUrl dans son constructeur — exécuté au chargement du module, donc avant que le profil mobile soit résolu. Dans la WebView, window.location.origin vaut https://localhost, c’est-à-dire l’app elle-même : toutes les requêtes seraient parties dans le vide.
C’est devenu un getter qui lit la configuration à chaque requête. Sur le web la valeur ne change jamais, donc comportement identique.
3. viewport-fit=cover manquant
La barre supérieure de l’app passait sous l’horloge et les icônes de notification d’Android. La cause n’était pas l’en-tête, mais le <meta viewport> : sans viewport-fit=cover, toutes les variables env(safe-area-inset-*) valent zéro.
Détail intéressant : le front Gladys utilise déjà safe-area-inset-bottom à cinq endroits (chat, dashboard, liste d’appareils). Elles ne faisaient donc rien sur mobile. Le correctif les réactive toutes.
Validation sur appareil
Testé sur un Xiaomi sous Android 16 (WebView Chromium 151), contre une instance réelle :
| Point | Résultat |
|---|---|
| Web Crypto (RSA-OAEP 2048, ECDSA P-256, AES-GCM, PBKDF2) | 9/9 en contexte https |
| WebSocket vers une instance HTTP | Handshake en 44 ms |
| MSE / hls.js (caméras) | Disponible |
| Parcours complet : configuration, login, dashboards, caméras | Fonctionnel |
Une mesure contre-intuitive : PBKDF2 100 000 itérations en 15 ms sur le téléphone, contre 45 ms sur mon PC. L’implémentation Android est accélérée matériellement. Le risque d’un déverrouillage lent sur mobile, que j’avais anticipé, n’existe pas.
Une leçon aussi : mon prototype de validation initial donnait trois feux verts, et n’a pourtant détecté aucun des deux blocages réels. Il testait un WebSocket depuis une page http://localhost — donc sans mixed-content — et n’émettait aucune requête API. Il faut valider crypto, WebSocket et une vraie requête API dans la configuration de schéma définitive, pas isolément.
Ce qui reste à faire
Lot 2 — ergonomie tactile : barre d’onglets basse, geste de retour Android, glisser-déposer du dashboard au doigt, comportement du clavier.
Lot 3 — notifications et présence. C’est le principal apport de l’app par rapport au navigateur, et le seul lot qui demande du développement côté serveur : une route d’enregistrement des jetons push, et une action de scène user.send-push-notification.
Un point à trancher collectivement : une instance auto-hébergée ne peut pas parler directement à FCM ou APNs, faute de clés de service — qu’on ne peut évidemment pas distribuer dans une image publique. Le relais par Gladys Plus est la voie naturelle (contenu chiffré de bout en bout, la notification ne portant qu’un déclencheur), avec un repli sur les notifications locales sans abonnement. Est-ce un compromis acceptable pour la communauté ?
Lot 4 — intégration système : découverte mDNS (le serveur publie déjà le service), widgets d’écran d’accueil, App Intents iOS et raccourcis Android, assistant vocal.
Lot 5 — publication : CI de build et signature, fiches store, bêta.
Deux points ouverts sur lesquels j’ai besoin d’aide
iOS n’a pas pu être validé : je n’ai pas de Mac. Quatre inconnues subsistent — contexte sécurisé sur capacitor://, MSE en WKWebView, entitlement multicast pour le mDNS (accordé au cas par cas par Apple, avec des délais imprévisibles), et le comportement d’ATS face à une instance en HTTP. Si quelqu’un dispose d’un Mac et d’une demi-heure, faire tourner le prototype de validation lèverait ces quatre points d’un coup.
La question du HTTPS local. Elle conditionne l’usage de Gladys Plus depuis l’app. Exiger une instance en HTTPS est propre techniquement, mais ajoute une étape de configuration à des utilisateurs qui n’en ont pas besoin aujourd’hui. Vos avis m’intéressent.
La spécification complète est en pièce jointe ci-dessous (dépliez la section) : architecture, connexion, capacités natives, sécurité, points durs, lots de livraison, build et recette. Le code est sur une branche dédiée.
N’hésitez pas à challenger le choix d’architecture — c’est précisément le moment où il est encore facile d’en changer.
Merci de votre lecture.
[details=« 📄 Spécification technique complète (dépliez) »]
Spécification technique — v1
Porter le front Gladys existant (Preact / Vite) dans une application native Android et iOS via Capacitor, avec parité fonctionnelle complète et les capacités que seule une app native peut offrir : notifications push, géolocalisation en arrière-plan, déverrouillage biométrique, découverte de l’instance sur le réseau local.
| Base de code | Gladys 5.0.2 |
| Approche | Capacitor + front existant |
| Cibles | Android 8+ · iOS 15+ |
| Périmètre v1 | Parité complète |
| Date | 30 août 2026 |
Sommaire
- Contexte et objectifs
- État de l’existant
- Architecture cible
- Connexion et authentification
- Capacités natives
- Adaptations d’interface
- Sécurité
- Points durs identifiés
- Lots de livraison
- Build, CI et publication
- Tests et recette
- Hors périmètre de la v1
1. Contexte et objectifs
Gladys Assistant s’utilise aujourd’hui sur mobile via le navigateur ou l’ajout à l’écran d’accueil. Cette approche atteint ses limites : pas de notifications fiables, pas de suivi de présence en arrière-plan, pas de présence sur les stores, et un service worker qui, dans l’état actuel du dépôt, se désinstalle volontairement à chaque activation.
Objectifs
- Parité fonctionnelle avec le front web : dashboard, appareils, scènes, caméras, chat, calendrier, carte, réglages et intégrations.
- Notifications push natives déclenchées par les scènes Gladys, avec actions rapides depuis la notification.
- Présence automatique par géolocalisation en arrière-plan, alimentant la détection de zones de Gladys.
- Connexion sans friction : découverte automatique de l’instance sur le réseau local, bascule transparente local ↔ Gladys Plus.
- Une seule base de code pour le web, Android et iOS : toute évolution du front bénéficie aux trois cibles sans portage.
Principes directeurs
- Le respect de la vie privée reste la règle. Aucune télémétrie, aucun SDK analytique tiers. Le chiffrement de bout en bout de Gladys Plus est préservé sans exception.
- Le front reste la source de vérité. Le shell natif n’embarque aucune logique métier : il expose des capacités, le front les consomme.
- Dégradation propre. Toute fonctionnalité native est optionnelle ; son absence (permission refusée, plateforme non compatible) ne casse jamais l’app.
2. État de l’existant
Le front est une application Preact 10 construite par Vite 6, avec routage preact-router, état global unistore, et internationalisation preact-i18n en trois langues (fr, en, de). Elle est servie soit par le serveur Gladys local (server/static), soit par Gladys Plus.
Ce qui est directement réutilisable
| Élément | Emplacement | Statut |
|---|---|---|
| Écrans et routage | front/src/routes/ |
Tel quel |
| Widgets de dashboard (22 types) | front/src/components/boxs/ |
Tel quel |
Actions et store unistore |
front/src/actions/ |
Tel quel |
| Traductions fr / en / de | front/src/config/i18n/ |
Tel quel |
| Thème et mode sombre | front/src/style/ |
Tel quel |
Ce qui doit être adapté
| Élément | Emplacement | Nature du travail |
|---|---|---|
| Configuration d’URL | front/src/config.js |
Les URL sont figées au moment du vite build via process.env. Il faut les rendre dynamiques à l’exécution. Point dur |
| Stockage de session | front/src/utils/Session.js, front/src/utils/keyValueStore.js |
localStorage en clair. À remplacer par un stockage chiffré adossé au Keychain / Keystore. Point dur |
| Client HTTP | front/src/utils/HttpClient.js |
localApiUrl était capturé dans le constructeur, exécuté au chargement du module via getDefaultState() — donc avant la résolution du profil mobile. Devenu un getter lisant la config à chaque requête. Sur le web la valeur ne change jamais : comportement identique. |
| Zones sûres | front/index.html, front/src/template.html |
<meta viewport> n’avait pas viewport-fit=cover, ce qui mettait toutes les variables env(safe-area-inset-*) à zéro — y compris les cinq usages existants de safe-area-inset-bottom. |
| WebSocket | front/src/utils/Session.js |
Reconnexion à intervalle fixe de 1 s, sans conscience du cycle de vie applicatif. À doter d’un backoff et d’une réaction aux événements de mise en veille. |
| Service worker | front/old-sw.js |
Se désinstalle lui-même à l’activation. Non utilisé dans le shell natif ; à conserver tel quel pour le web afin de purger les anciens caches. |
| Caméras (HLS) | front/src/components/boxs/camera/ |
Chargeur hls.js personnalisé pour injecter le jeton. À valider en WebView, notamment le HLS natif sur iOS. |
| Glisser-déposer | front/src/utils/dragAndDropBackend.js |
Bascule déjà entre backends HTML5 et tactile ; à revalider dans la WebView. |
Côté serveur
Aucune modification n’est nécessaire pour les lots 1 et 2 : l’API REST (server/api/controllers/, 25 contrôleurs) et le WebSocket couvrent déjà les besoins. Deux ajouts serveur sont requis plus loin : l’enregistrement des jetons push (lot 3) et l’action de scène correspondante.
3. Architecture cible
Le front est compilé exactement comme aujourd’hui, puis empaqueté dans une WebView native par Capacitor. Les capacités que le web ne sait pas offrir sont exposées au front sous forme de plugins Capacitor, derrière une couche d’abstraction qui renvoie des valeurs neutres quand l’app tourne dans un navigateur.
┌─────────────────────────────────────┐
│ APPLICATION MOBILE │
│ │ ┌──────────────────────────────┐
│ ┌───────────────────────────────┐ │ │ Réseau local │
│ │ WebView — front Preact │ │───────>│ REST /api/v1 + WebSocket │
│ │ routes/ · components/ │ │ │ Jeton porteur, latence min. │
│ │ ───────────────────────────── │ │ │ Découverte mDNS │
│ │ utils/native/ — abstraction │ │ └──────────────┬───────────────┘
│ │ no-op sur le web │ │ │
│ └───────────────┬───────────────┘ │ ┌──────────────▼───────────────┐
│ ▼ │ │ Gladys Plus — à distance │
│ ┌───────────────────────────────┐ │───────>│ gladys-gateway-js │
│ │ Shell natif Capacitor │ │ │ RSA + ECDSA, bout en bout │
│ │ push · géoloc · biométrie │ │ │ Le serveur ne déchiffre rien │
│ │ mDNS · stockage · widgets │ │ └──────────────┬───────────────┘
│ └───────┬───────────────┬───────┘ │ │
└──────────┼───────────────┼──────────┘ │
▼ ▼ ┌──────────────▼───────────────┐
┌────────────┐ ┌────────────┐ │ Instance Gladys │
│ Android │ │ iOS │ │ Node 24 · SQLite │
│ Kotlin·FCM │ │ Swift·APNs │ │ 39 services │
└────────────┘ └────────────┘ └──────────────────────────────┘
La sélection du chemin réseau est automatique et réévaluée à chaque changement de réseau ; l’utilisateur peut la forcer depuis les réglages. Le front compilé est identique à celui servi sur le web : seule la couche utils/native/ est ajoutée, et elle renvoie des implémentations neutres hors mobile.
Arborescence
Un nouveau répertoire mobile/ à la racine, à côté de front/ et server/ :
mobile/
├── capacitor.config.ts configuration, pointe vers front/build
├── package.json dépendances Capacitor uniquement
├── android/ projet Android généré, versionné
├── ios/ projet Xcode généré, versionné
├── plugins/ plugins maison (mDNS, découverte)
└── resources/ icônes et écrans de démarrage sources
front/src/utils/native/ couche d'abstraction, dans le front
├── index.js détection de plateforme
├── push.js
├── geolocation.js
├── biometrics.js
├── secureStorage.js
└── discovery.js
Plugins retenus
| Besoin | Plugin | Origine |
|---|---|---|
| Notifications push | @capacitor/push-notifications |
Officiel |
| Notifications locales | @capacitor/local-notifications |
Officiel |
| Géolocalisation ponctuelle | @capacitor/geolocation |
Officiel |
| Géolocalisation en arrière-plan | @capacitor-community/background-geolocation |
Communauté |
| Stockage chiffré | capacitor-secure-storage-plugin |
Communauté |
| Biométrie | @aparajita/capacitor-biometric-auth |
Communauté |
| État du réseau | @capacitor/network |
Officiel |
| Cycle de vie de l’app | @capacitor/app |
Officiel |
| Barre d’état et encoches | @capacitor/status-bar |
Officiel |
| Découverte mDNS | gladys-discovery |
À écrire |
Choix de conception — Chaque plugin communautaire est un risque de maintenance. La couche
utils/native/existe précisément pour que le remplacement d’un plugin abandonné n’impacte qu’un seul fichier, jamais les écrans.
4. Connexion et authentification
C’est la partie la plus structurante de la spécification. Le front actuel ne connaît qu’un seul mode à la fois, décidé au moment du build par la variable GATEWAY_MODE. L’app mobile doit gérer les deux simultanément et basculer de l’un à l’autre sans intervention.
4.1 Configuration à l’exécution
front/src/config.js lit process.env, que Vite remplace par des littéraux au moment du build. Il faut introduire une résolution à l’exécution :
- Sur le web, le comportement actuel est conservé à l’identique : aucune régression.
- Sur mobile, les URL proviennent du profil de connexion actif, stocké dans le stockage chiffré.
Concrètement, config.js expose un objet mutable alimenté au démarrage par utils/native/, avant le premier rendu. Les modules qui importent la configuration aujourd’hui restent inchangés.
4.2 Profils de connexion
L’app gère plusieurs profils — un foyer principal, une résidence secondaire, une instance de test. Chaque profil enregistre :
| Champ | Contenu |
|---|---|
id |
UUID local |
name |
Nom affiché, saisi par l’utilisateur |
mode |
local, gateway ou auto |
localUrl |
URL de l’instance sur le réseau local |
localFingerprint |
Empreinte du certificat, si HTTPS auto-signé |
ssids |
Réseaux Wi-Fi où le mode local s’applique |
credentials |
Référence vers le stockage chiffré, jamais la valeur |
4.2 bis Les trois URL de Gladys Plus
À ne pas confondre — elles ont des rôles distincts :
| URL | Rôle | Utilisée par |
|---|---|---|
https://api.gladysgateway.com |
L’API de la passerelle | config.gladysGatewayApiUrl, appelée par gladys-gateway-js |
https://plus.gladysassistant.com |
Le front web de Gladys Plus (le même code, en mode gateway) | Liens sortants : abonnement, facturation |
https://gladysassistant.com/plus/ |
Page marketing | utils/gladysPlusUrl.js (liens d’inscription) |
L’app mobile parle à l’API, jamais au front hébergé : elle est le front.
La valeur par défaut de config.js est donc déjà correcte et n’a pas à être
modifiée.
En revanche, les parcours qui ne peuvent pas se dérouler dans l’app — gestion de
l’abonnement, facturation Stripe — doivent ouvrir plus.gladysassistant.com
dans le navigateur système, et non dans la WebView : un tunnel de paiement dans
une WebView applicative est refusé par les deux stores. Le plugin
@capacitor/browser (onglet système) est le bon véhicule.
4.3 Découverte de l’instance locale
Le serveur Gladys publie déjà un service mDNS (server/lib/mdns/). Le plugin gladys-discovery à écrire interroge ce service et présente les instances trouvées :
- Android :
NsdManager, avec acquisition d’unMulticastLockpendant la recherche. - iOS :
NWBrowser(Network framework). Nécessite l’entitlementcom.apple.developer.networking.multicast, à demander explicitement à Apple, et la déclarationNSBonjourServicesdansInfo.plist.
En cas d’échec de la découverte, la saisie manuelle d’une adresse reste toujours possible : elle n’est jamais un chemin de repli caché, mais une option visible dès le premier écran.
4.4 Bascule local ↔ distant
En mode auto, l’app choisit le chemin réseau à chaque démarrage, à chaque retour au premier plan et à chaque changement de connectivité signalé par @capacitor/network :
- Si un profil local est configuré, une requête
GET /api/v1/pingest tentée avec un délai de garde de 1,5 seconde. - En cas de succès, le mode local est retenu : latence minimale, aucune dépendance externe.
- En cas d’échec et si un compte Gladys Plus est lié, l’app bascule sur la passerelle.
- Si aucun chemin ne répond, un écran hors-ligne s’affiche avec les dernières données connues et un bouton de reprise.
La bascule reconstruit le client HTTP et la connexion WebSocket. Elle est signalée discrètement dans l’interface (un indicateur dans l’en-tête), jamais par une boîte de dialogue bloquante.
Attention — Les deux modes n’utilisent pas le même magasin de jetons ni le même client :
Session+HttpClientpour le local,GatewaySession+GatewayHttpClientpour Gladys Plus. Le basculement doit vider les caches de requêtes en vol deHttpClient(laMappendingRequests) pour éviter qu’une réponse issue de l’ancien chemin ne soit attribuée au nouveau.
4.5 Chiffrement de bout en bout
GatewaySession s’appuie sur @gladysassistant/gladys-gateway-js, qui reçoit window.crypto. Dans une WebView, la Web Crypto API n’est disponible que sur un contexte sécurisé : les schémas capacitor:// (iOS) et https:// (Android) le sont, contrairement à http://. Ce point est à valider dès le lot 1, car il conditionne tout l’accès distant.
Les clés sérialisées (gateway_serialized_keys) sont aujourd’hui dans localStorage. Sur mobile, elles vont dans le stockage chiffré adossé au Keychain (iOS) ou au Keystore avec chiffrement matériel (Android).
4.6 Double authentification
Le parcours 2FA existant (actions/login/loginGateway.js) est conservé sans modification, y compris la génération des codes de récupération et le collage depuis le presse-papiers. Un remplissage automatique du code depuis les suggestions du clavier est ajouté via l’attribut autocomplete="one-time-code".
4.7 Verrouillage biométrique
Optionnel, activable dans les réglages. Quand il est actif, un déverrouillage par empreinte ou reconnaissance faciale est demandé au lancement et après un délai d’inactivité configurable (par défaut 5 minutes en arrière-plan). Le repli est le code de l’appareil ; il n’y a jamais de code propre à Gladys à mémoriser en plus.
5. Capacités natives
5.1 Notifications push
C’est le principal apport de l’app par rapport au web. Il implique un développement côté serveur Gladys, et non seulement côté mobile.
Enregistrement. Au premier lancement après acceptation de la permission, l’app obtient un jeton FCM (Android) ou APNs (iOS) et l’envoie à l’instance. Nouvelle route à créer : POST /api/v1/user/push_token, avec le jeton, la plateforme et l’identifiant de session. Le jeton est lié à la session existante : révoquer une session révoque le push associé.
Envoi. Une instance Gladys auto-hébergée ne peut pas parler directement à FCM ou APNs sans clés de service — qui ne peuvent pas être distribuées dans une image publique. Deux chemins :
- Via Gladys Plus (recommandé) — L’instance transmet la notification à la passerelle, qui détient les clés et relaie vers FCM / APNs. Le contenu utile est chiffré de bout en bout ; la notification transportée ne contient qu’un déclencheur, l’app récupère le contenu réel auprès de l’instance à la réception.
- Sans Gladys Plus — Repli sur les notifications locales : tant que le WebSocket est vivant, l’app programme elle-même une notification. Fonctionne en arrière-plan récent, pas après une mise en veille prolongée. Cette limite doit être annoncée clairement dans les réglages, pas découverte à l’usage.
Déclenchement depuis une scène. Une nouvelle action de scène user.send-push-notification est ajoutée, avec destinataires, titre, message et éventuellement une image de caméra. Elle doit impérativement être déclarée dans le schéma Joi de server/models/scene.js, faute de quoi l’enregistrement de la scène échouera en 422 sans message explicite.
Actions rapides. Les notifications portent jusqu’à trois boutons d’action, définis dans la scène : exécuter une autre scène, allumer ou éteindre un appareil, ouvrir une caméra. Ces actions sont traitées sans ouvrir l’app quand le réseau le permet.
5.2 Géolocalisation et présence
Gladys dispose déjà de server/lib/location/ et de la gestion des zones. L’app alimente POST /api/v1/location :
- Suivi par zones plutôt que suivi continu : l’app s’abonne aux entrées et sorties des zones définies dans Gladys. Coût en batterie très inférieur à un relevé périodique.
- Précision réduite par défaut : la position n’est transmise qu’au franchissement d’une zone, pas en continu.
- File d’attente hors-ligne : les événements captés sans réseau sont conservés localement et envoyés à la reconnexion, horodatés à leur date réelle.
- Interrupteur global visible dans les réglages, et arrêt immédiat du suivi lorsqu’il est coupé.
Contrainte store — La géolocalisation en arrière-plan est le motif de rejet le plus fréquent sur l’App Store. Il faut : une explication en clair avant la demande de permission, une justification précise dans
NSLocationAlwaysAndWhenInUseUsageDescription, et une app pleinement fonctionnelle si la permission est refusée. Sur Android 13+, la permissionACCESS_BACKGROUND_LOCATIONse demande dans un second temps, après la permission de premier plan.
5.3 Widgets d’écran d’accueil
Écrits nativement : WidgetKit en SwiftUI sur iOS, Glance sur Android. Ils lisent un instantané de données écrit par l’app dans un espace partagé (App Group sur iOS, SharedPreferences sur Android), rafraîchi à chaque passage au premier plan et à chaque notification.
- Scènes : jusqu’à quatre scènes favorites, exécutables d’un appui.
- Température : température d’une pièce choisie.
- Appareils : état et bascule de deux à quatre appareils.
Les widgets n’ouvrent pas de session propre : ils délèguent à l’app, qui exécute l’action. Si la session a expiré, le widget affiche un état de reconnexion nécessaire plutôt qu’une erreur.
5.4 Raccourcis système
- iOS : exposition des scènes en App Intents, ce qui les rend disponibles dans Raccourcis, Siri et le bouton d’action.
- Android : raccourcis dynamiques sur l’icône de l’app pour les scènes favorites.
5.5 Assistant vocal
Le front possède déjà une chaîne vocale complète : speechCommandRecorder.js, recordUntilSilence.js, speechTtsPlayback.js, adossée à gateway.stt.js et gateway.processVoiceMessage.js côté serveur.
En WebView, l’accès micro exige la permission native (NSMicrophoneUsageDescription, RECORD_AUDIO) et l’autorisation au niveau de la WebView. Sur Android, cette dernière passe par onPermissionRequest, à traiter dans le shell natif. La lecture TTS doit configurer la catégorie de session audio sur iOS pour ne pas être coupée par le mode silencieux.
5.6 Caméras
Le flux HLS est lu via hls.js avec un chargeur personnalisé injectant le jeton d’authentification. En WebView :
- Sur iOS, le HLS natif de
<video>ne permet pas d’ajouter d’en-têtes ;hls.jsvia MSE reste donc nécessaire. La compatibilité MSE en WKWebView est à vérifier tôt. - Le plein écran vidéo demande
allowsInlineMediaPlaybacket une gestion explicite de la rotation. - La lecture doit s’interrompre au passage en arrière-plan et reprendre au retour, pour ne pas consommer batterie et données inutilement.
6. Adaptations d’interface
L’interface reste celle du front web. Les adaptations sont ciblées, et aucune ne doit dégrader l’expérience sur navigateur.
Zones sûres et encoches. Application de env(safe-area-inset-*) sur l’en-tête, la navigation basse et les modales. Le viewport passe en viewport-fit=cover dans front/src/template.html, sans effet sur le web.
Navigation. La barre latérale actuelle devient une barre d’onglets basse sur écran étroit : Dashboard, Appareils, Scènes, Chat, Réglages. Le geste de retour arrière Android est rattaché à preact-router via @capacitor/app, avec une confirmation de sortie uniquement à la racine.
Cibles tactiles et gestes.
- Toute cible interactive fait au minimum 44 × 44 points.
- Le glisser-déposer du dashboard utilise le backend tactile de
react-dnd, déjà présent, avec un appui long pour armer le déplacement afin de ne pas entrer en conflit avec le défilement. - Tirer pour rafraîchir sur les écrans de liste, désactivé pendant une édition en cours.
Clavier. Redimensionnement de la vue à l’ouverture du clavier plutôt que superposition, types de saisie adaptés (inputmode="numeric" pour les codes, type="email"), et défilement automatique vers le champ actif.
Mode sombre. Le thème sombre de Gladys repose sur une inversion CSS globale, avec la classe dark-mode-no-invert pour les éléments qui doivent garder leurs vraies couleurs. Ce mécanisme est conservé tel quel. Deux points nouveaux : la couleur de la barre d’état native doit suivre le thème, et l’inversion ne doit pas s’appliquer à l’écran de démarrage natif.
Mode tablette. Le mode tablette existant (routes/dashboard/SetTabletMode.jsx) prend tout son sens sur une tablette murale : on y ajoute le maintien de l’écran allumé, le verrouillage de l’orientation et un mode plein écran immersif. Le verrouillage par code existant est conservé.
7. Sécurité
Stockage des secrets
Aucun secret ne reste dans localStorage sur mobile. Migrent vers le stockage chiffré : jetons d’accès et de rafraîchissement, clés sérialisées Gladys Plus, empreintes de clés publiques, jeton de double authentification. Restent dans le stockage ordinaire les préférences non sensibles : langue, mode sombre, maison sélectionnée.
HTTP en clair sur le réseau local
Une instance Gladys locale est très souvent servie en http://. Or Android bloque le trafic en clair par défaut depuis la version 9, et iOS via ATS.
| Plateforme | Mécanisme | Portée |
|---|---|---|
| Android | network_security_config.xml |
Autorisation du trafic en clair restreinte aux plages privées : 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, plus .local |
| iOS | NSAllowsLocalNetworking |
Exception ATS limitée au réseau local, sans désactivation globale |
Il n’est jamais question de NSAllowsArbitraryLoads ni de cleartextTrafficPermitted global : ces deux réglages désarment la protection sur tout Internet et sont un motif de rejet à la revue Apple.
Certificats auto-signés
Pour une instance locale en HTTPS auto-signé, l’app propose l’épinglage de l’empreinte du certificat au moment de l’ajout du profil, avec affichage de l’empreinte pour vérification. Aucune acceptation aveugle ; un changement d’empreinte bloque la connexion et demande une confirmation explicite.
Protection du contenu
- Masquage du contenu dans le sélecteur d’apps (
FLAG_SECUREoptionnel sur Android, vue de recouvrement sur iOS), activable dans les réglages. - Aucune capture d’écran de flux caméra dans les aperçus système lorsque l’option est active.
- Effacement complet du stockage chiffré à la déconnexion.
Ce que l’app ne fait pas
- Aucun SDK d’analytique, de publicité ou de suivi.
- Aucun rapport de crash tiers automatique ; en cas de besoin, un envoi manuel, explicite et consenti.
- Aucune donnée de la maison ne transite par un serveur autre que l’instance de l’utilisateur ou Gladys Plus, dont le contenu est chiffré de bout en bout.
8. Points durs identifiés
Ces sept points sont ceux qui peuvent faire déraper le calendrier. Ils sont volontairement placés au plus tôt dans le phasage, pour que leur coût réel soit connu avant d’avoir investi dans le reste.
| Point | Risque | Traitement |
|---|---|---|
| Web Crypto en WebView | Si window.crypto.subtle est indisponible, tout l’accès distant Gladys Plus tombe. |
Prototype de validation dès la première semaine, sur les deux plateformes. Les schémas capacitor:// et https:// sont des contextes sécurisés : le risque est faible mais l’impact total. |
| Configuration figée au build | config.js est résolu par Vite au build : l’app ne peut pas changer d’instance. |
Refonte en configuration résolue à l’exécution, avec conservation stricte du comportement web actuel. |
| Push sans Gladys Plus | Une instance auto-hébergée n’a pas de clés FCM / APNs. | Relais par la passerelle pour les comptes Gladys Plus ; notifications locales en repli, avec leurs limites annoncées explicitement. |
| Multicast iOS | La découverte mDNS demande un entitlement accordé au cas par cas par Apple, avec des délais imprévisibles. | Demande déposée dès l’ouverture du compte développeur. La saisie manuelle d’adresse est un chemin de première classe, pas un repli. |
| WebSocket en arrière-plan | Les deux OS coupent les connexions en veille ; l’état affiché peut être périmé au réveil. | Reconnexion avec backoff au retour au premier plan, rechargement complet de l’état, et indicateur de fraîcheur des données. |
| HLS en WKWebView | La lecture MSE via hls.js peut se comporter différemment de Safari. |
Test sur appareil réel au lot 1, avant tout engagement sur le lot 4. |
| Mixed content (découvert en test) | La WebView servie en https://localhost ne peut appeler ni http:// ni ws:// sur l’instance locale : Chromium bloque. La configuration de sécurité réseau Android n’y change rien — c’est une politique du navigateur, pas de la plateforme. CapacitorHttp règle le cas des requêtes HTTP, mais pas celui des WebSockets, qui restent soumis à la règle. |
Voir l’arbitrage ci-dessous : le lot 1 passe en androidScheme: "http". |
Arbitrage : schéma de la WebView
Le choix du schéma oppose deux capacités qu’on ne peut pas obtenir ensemble tant
que l’instance est en HTTP :
androidScheme |
Contexte sécurisé | Crypto / Gladys Plus | HTTP local | WebSocket local |
|---|---|---|---|---|
https |
oui | OK | via CapacitorHttp | BLOQUÉ |
http |
non | indisponible | OK | OK |
Décision pour le lot 1 : http. Le lot vise la connexion à l’instance
locale, qui exige le WebSocket ; Gladys Plus n’y est pas encore branché. Ce
choix est explicitement temporaire.
À reprendre obligatoirement avant le lot Gladys Plus. Deux voies :
- Instance en HTTPS (reverse proxy TLS : Caddy, nginx, Traefik). Tout passe
enhttps/wss, plus aucun blocage, contexte sécurisé conservé. C’est la
seule solution qui vaudra aussi pour iOS, où le schémacapacitor://impose
les mêmes contraintes. Voie recommandée. - Plugin WebSocket natif, ouvrant la connexion hors WebView comme
CapacitorHttple fait pour les requêtes. Permet de rester enhttps, mais
demande de modifierSession.js, partagé avec le web.
Conséquence pratique : tant que le lot 1 est en
androidScheme: "http",
crypto.subtleest indisponible dans l’app et toute tentative de connexion
Gladys Plus échouera. Le prototype de validation, lui, mesurait bien la crypto
enhttps— la capacité de la plateforme est acquise, c’est sa cohabitation
avec un backend en clair qui ne l’est pas.
| Revue App Store | Géolocalisation en arrière-plan et app dite « client d’un service » sont deux motifs classiques de rejet. | Mode démo accessible aux relecteurs sans instance, justifications de permissions rédigées avec soin, capture d’écran de l’usage réel des zones. |
9. Lots de livraison
Les lots sont réellement séquentiels : chacun s’appuie sur le précédent et se termine par un livrable installable sur appareil réel. Le lot 1 concentre volontairement les risques techniques.
Lot 1 — Socle et authentification
LIVRÉ
Objectif : prouver que l’approche tient, et lever les points durs 1, 2 et 6.
Commit feat(mobile): resolve the instance to connect to at runtime —
22 fichiers, 1254 insertions, 14 suppressions.
Configuration résolue à l’exécution (setRuntimeConfig, liste blanche de clés)
Couche utils/native/— éliminée du bundle web par tree-shaking
Stockage sécurisé (Keychain / Keystore, repli localStoragesur le web)
Profils multi-instances : liste, ajout, édition, suppression, bascule
Écran de première connexion, avec test de l’adresse avant enregistrement
Page de réglages « Instances », masquée sur le web (nativeOnly)
Indicateur de connexion perdue dans l’en-tête
Zones sûres : viewport-fit=cover+ barre supérieure et tiroir de navigation
Non-régression web vérifiée à la main
Connexion Gladys Plus non fonctionnelle — voir l’arbitrage du schéma, section 8
Projet iOS non généré : pas de machine macOS disponible
Validé sur appareil (Xiaomi, Android 16, WebView Chromium 151) : parcours
complet de configuration, connexion à l’instance locale, login, dashboards,
caméras.
Contrôles : les trois checks du CI passent (Prettier, ESLint 0 erreur,
parité des traductions fr/en/de), plus 43 tests de logique exécutés sur le code
réel du dépôt.
Lot 2 — Parité fonctionnelle et ergonomie tactile
Objectif : une app utilisable au quotidien, sans encore les apports natifs.
- Zones sûres, barre d’onglets basse, geste de retour Android
- Glisser-déposer du dashboard validé au doigt
- Comportement du clavier, tirer pour rafraîchir, cibles tactiles
- Barre d’état et écran de démarrage accordés au mode sombre
- Revue écran par écran des 23 routes existantes sur téléphone et tablette
Lot 3 — Notifications et présence
Objectif : la première raison concrète d’installer l’app plutôt que d’ouvrir le navigateur.
- Serveur : route
POST /api/v1/user/push_token, liée aux sessions - Serveur : action de scène
user.send-push-notification, schéma Joi compris - Relais des notifications par Gladys Plus, contenu chiffré de bout en bout
- Notifications locales en repli sans Gladys Plus
- Actions rapides dans les notifications
- Géolocalisation par zones, file d’attente hors-ligne, interrupteur global
- Biométrie et verrouillage après inactivité
Lot 4 — Intégration au système
Objectif : rendre Gladys accessible sans même ouvrir l’app.
- Découverte mDNS : plugin
gladys-discovery, Android et iOS - Widgets d’écran d’accueil : scènes, température, appareils
- App Intents iOS et raccourcis dynamiques Android
- Assistant vocal : permission micro en WebView, session audio iOS
- Caméras : plein écran, rotation, arrêt en arrière-plan
- Mode tablette murale : écran maintenu allumé, orientation verrouillée, plein écran immersif
Lot 5 — Publication
Objectif : l’app est installable depuis les stores et se met à jour toute seule.
- Chaîne CI : build et signature Android et iOS
- Fiches Play Store et App Store : descriptions, captures, politique de confidentialité
- Mode démo pour les relecteurs, sans instance requise
- Test interne puis bêta ouverte : Play Console et TestFlight
- Documentation utilisateur et documentation de contribution
Ordonnancement — Les lots 1 et 2 produisent déjà une app distribuable en interne. Si les points durs du lot 1 se révèlent plus coûteux que prévu, l’arbitrage se fait à ce moment-là, avant d’avoir engagé le développement natif du lot 3.
10. Build, CI et publication
Chaîne de construction
Nouveaux scripts npm à la racine, dans le prolongement des scripts existants :
build-mobile vite build ciblé mobile, puis npx cap sync
mobile:android ouvre le projet dans Android Studio
mobile:ios ouvre le projet dans Xcode
mobile:live rechargement à chaud sur appareil connecté
Le build mobile réutilise front/vite.config.mjs avec un mode dédié : la configuration d’URL n’est plus injectée au build, et le service worker n’est pas copié.
Versionnage
La version de l’app suit celle de Gladys (package.json racine, aujourd’hui 5.0.2), avec un numéro de build incrémental propre au mobile. Les versionCode Android et CFBundleVersion iOS sont dérivés automatiquement en CI.
Compatibilité d’instance
Une app à jour peut se connecter à une instance plus ancienne. utils/instanceVersion.js gère déjà cette détection sur le web : on l’étend pour désactiver proprement les fonctionnalités qui exigent une version serveur minimale, en particulier le push, plutôt que de laisser un appel échouer sans explication.
Intégration continue
Un workflow GitHub Actions dédié, distinct de la CI existante :
- Sur chaque pull request touchant
mobile/oufront/: build Android en mode debug, sans signature. - Sur chaque tag de version : build signé Android et iOS, dépôt sur les canaux de test interne.
- Secrets de signature en secrets de dépôt ; aucune clé dans le dépôt lui-même.
- Les vérifications existantes restent applicables au front : Prettier puis ESLint, et couverture de patch à 100 % pour tout code serveur ajouté.
Comptes et coûts
| Poste | Nature | Coût |
|---|---|---|
| Compte développeur Apple | Organisation | 99 $ / an |
| Compte Google Play | Organisation | 25 $ une fois |
| Projet Firebase | FCM uniquement | Gratuit |
| Machine de build iOS | macOS, CI ou locale | Variable |
11. Tests et recette
Ce que la CI couvre
- Les tests Cypress existants continuent de tourner sur le front web : ils protègent contre les régressions introduites par la refonte de la configuration.
- Tests unitaires Mocha sur le code serveur ajouté (route de jeton push, action de scène), avec
TZ=UTCimposé. - Le build Android en mode debug sert de test de fumée sur l’intégration Capacitor.
Recette manuelle sur appareil
Une matrice de test est tenue à jour pour chaque lot. Les scénarios qui ne peuvent pas être automatisés et qui doivent être vérifiés à la main :
| Scénario | Ce qui est vérifié |
|---|---|
| Bascule Wi-Fi → données mobiles | Passage automatique du mode local à Gladys Plus, sans déconnexion visible |
| Retour au premier plan après une nuit | Reconnexion WebSocket, état rafraîchi, aucune donnée périmée affichée comme actuelle |
| Notification, app fermée | Réception, ouverture sur le bon écran, action rapide exécutée sans lancer l’app |
| Entrée et sortie de zone | Événement de présence remonté, y compris après un passage hors réseau |
| Permission refusée | L’app reste pleinement utilisable, avec un message qui explique ce qui est désactivé |
| Caméra en direct | Lecture, plein écran, rotation, arrêt propre au passage en arrière-plan |
| Mode sombre | Cohérence de la barre d’état, de l’écran de démarrage et des zones non inversées |
| Instance hors ligne | Écran hors-ligne, aucune boucle de reconnexion agressive, reprise manuelle possible |
Résultats mesurés — 30 août 2026
Sur Xiaomi 2412DPC0AG, Android 16 (API 36), WebView Chromium 151, contre une
instance réelle en HTTP sur le réseau local :
| Test | Résultat |
|---|---|
crypto.subtle en contexte https://localhost |
disponible |
| RSA-OAEP 2048 / SHA-256 — génération | 104 ms |
| ECDSA P-256 — génération, signature, vérification | < 1 ms |
exportKey('jwk') (stockage des clés) |
OK |
| PBKDF2 100 000 itérations | 15 ms |
| AES-GCM 256, tagLength 128 | < 1 ms |
| WebSocket vers instance HTTP (handshake) | 44 ms |
MSE — Hls.isSupported() |
disponible |
| Parcours complet : configuration → login → dashboards → caméras | fonctionnel |
Le PBKDF2 est trois fois plus rapide sur le téléphone que sur un PC de bureau
(15 ms contre 45 ms) : l’implémentation Android est accélérée matériellement. Le
risque d’un déverrouillage lent, envisagé en section 5.7, n’existe pas.
Leçon de méthode. Le prototype de validation initial donnait trois feux
verts et n’a pourtant détecté aucun des deux blocages réels rencontrés
ensuite (mixed-content sur les requêtes HTTP, puis sur le WebSocket). Il
testait un WebSocket depuis une pagehttp://localhost— donc hors
mixed-content — et n’émettait aucune requête API vers l’instance. Il faut
valider crypto, WebSocket et une requête API réelle dans la configuration
de schéma définitive, jamais isolément.
Parc de test minimal
- Un Android récent et un Android ancien (API 26 à 28), pour la configuration de sécurité réseau.
- Un iPhone avec encoche et un iPad, pour les zones sûres et le mode tablette.
- Une instance Gladys locale en
http://et une instance liée à Gladys Plus.
12. Hors périmètre de la v1
Ces éléments sont explicitement écartés. Les mentionner évite qu’ils s’invitent en cours de route.
- Mode hors-ligne complet. L’app affiche les dernières données connues, mais ne rejoue pas les commandes émises hors réseau. Seuls les événements de présence sont mis en file d’attente.
- Application Apple Watch et Wear OS. Les widgets et raccourcis système couvrent l’essentiel du besoin à moindre coût.
- Configuration des intégrations complexes depuis mobile : appairage Zigbee, Matter, Z-Wave. Consultables, mais configurés depuis le web.
- Refonte visuelle. L’app reprend l’interface actuelle. Une refonte mobile-first est un chantier distinct.
- Support d’Android 7 et antérieurs, et d’iOS 14 et antérieurs.
- Réparation du service worker web. Le comportement actuel est conservé ; l’amélioration de la PWA reste un sujet à part entière.
Questions à trancher avant le démarrage
- Le relais de notifications passe-t-il par l’infrastructure Gladys Plus existante, ou faut-il un service dédié ? Cela conditionne le lot 3.
- Les comptes développeur sont-ils ouverts au nom du projet Gladys ou à titre personnel ? Cela a des conséquences durables sur la propriété des fiches.
- L’app est-elle réservée aux abonnés Gladys Plus pour les notifications, ou le repli local suffit-il à la promesse faite aux utilisateurs ?
- Une machine macOS est-elle disponible pour la CI iOS, ou faut-il prévoir un service de build hébergé ?
Spécification établie à partir du dépôt Gladys 5.0.2 : front Preact / Vite (front/), serveur Node 24 (server/, 39 services, 25 contrôleurs API). Les chemins de fichiers cités correspondent à l’arborescence réelle du dépôt à la date de rédaction.
