Une application mobile Android / iOS pour Gladys : approche et premiers résultats

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é… :star_struck:

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.js lisait process.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

  1. Contexte et objectifs
  2. État de l’existant
  3. Architecture cible
  4. Connexion et authentification
  5. Capacités natives
  6. Adaptations d’interface
  7. Sécurité
  8. Points durs identifiés
  9. Lots de livraison
  10. Build, CI et publication
  11. Tests et recette
  12. 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 :white_check_mark: Fait (lot 1). 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 :white_check_mark: Fait (lot 1). Le <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’un MulticastLock pendant la recherche.
  • iOS : NWBrowser (Network framework). Nécessite l’entitlement com.apple.developer.networking.multicast, à demander explicitement à Apple, et la déclaration NSBonjourServices dans Info.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 :

  1. Si un profil local est configuré, une requête GET /api/v1/ping est tentée avec un délai de garde de 1,5 seconde.
  2. En cas de succès, le mode local est retenu : latence minimale, aucune dépendance externe.
  3. En cas d’échec et si un compte Gladys Plus est lié, l’app bascule sur la passerelle.
  4. 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 + HttpClient pour le local, GatewaySession + GatewayHttpClient pour Gladys Plus. Le basculement doit vider les caches de requêtes en vol de HttpClient (la Map pendingRequests) 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 permission ACCESS_BACKGROUND_LOCATION se 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.js via MSE reste donc nécessaire. La compatibilité MSE en WKWebView est à vérifier tôt.
  • Le plein écran vidéo demande allowsInlineMediaPlayback et 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_SECURE optionnel 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 :

  1. Instance en HTTPS (reverse proxy TLS : Caddy, nginx, Traefik). Tout passe
    en https/wss, plus aucun blocage, contexte sécurisé conservé. C’est la
    seule solution qui vaudra aussi pour iOS, où le schéma capacitor:// impose
    les mêmes contraintes. Voie recommandée.
  2. Plugin WebSocket natif, ouvrant la connexion hors WebView comme
    CapacitorHttp le fait pour les requêtes. Permet de rester en https, mais
    demande de modifier Session.js, partagé avec le web.

Conséquence pratique : tant que le lot 1 est en androidScheme: "http",
crypto.subtle est indisponible dans l’app et toute tentative de connexion
Gladys Plus échouera. Le prototype de validation, lui, mesurait bien la crypto
en https — 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 :white_check_mark: 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.

  • :white_check_mark: Configuration résolue à l’exécution (setRuntimeConfig, liste blanche de clés)
  • :white_check_mark: Couche utils/native/ — éliminée du bundle web par tree-shaking
  • :white_check_mark: Stockage sécurisé (Keychain / Keystore, repli localStorage sur le web)
  • :white_check_mark: Profils multi-instances : liste, ajout, édition, suppression, bascule
  • :white_check_mark: Écran de première connexion, avec test de l’adresse avant enregistrement
  • :white_check_mark: Page de réglages « Instances », masquée sur le web (nativeOnly)
  • :white_check_mark: Indicateur de connexion perdue dans l’en-tête
  • :white_check_mark: Zones sûres : viewport-fit=cover + barre supérieure et tiroir de navigation
  • :white_check_mark: Non-régression web vérifiée à la main
  • :warning: Connexion Gladys Plus non fonctionnelle — voir l’arbitrage du schéma, section 8
  • :cross_mark: 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/ ou front/ : 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=UTC imposé.
  • 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 page http://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

  1. 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.
  2. 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.
  3. 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 ?
  4. 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.

Tu as tout lu ? :face_with_crossed_out_eyes:

Bon sang, j’ai décroché à " Trois choses que la spec n’avait pas prévues", j’en pouvais plus de l’IA, désolé @Will_71 :face_with_peeking_eye:

Pour résumé, à quoi va servir une app native ?
Je trouve le système de WPA vraiment top, ça fonctionne tout le temps, stable, sécurisé.
Avec les apps mobiles, c’est 2 plateformes de plus à gérer et maintenir, au niveau compatibilité de Gladys, des évolutions d’OS, de la sécurité (on va éviter de faire comme les impôts…), etc.
Donc oui, pourquoi ?

PS : je n’ai pas encore vu la vidéo, peut-être que je vais y trouver des réponses …

C’était juste un essai pour voir ce que Claude pouvait faire et en 15 min, j’ai eu la spec et une app fonctionnelle.

Je ne dis pas que l’app PWA ne fonctionne pas, au contraire, je l’utilise tous les jours.
Ici, Claude n’est pas parti pour faire 3 systèmes différents, mais elle partage le même front, donc si on modifie cela, c’est disponible pour les 3.

L’avantage de l’app, c’est de pouvoir gérer la géolocalisation d’une personne (pour la présence dans une zone), nativement par le smartphone, par exemple.
Et je vois bien, plus tard, se servir de l’app pour appairer les appareils Matter, avec le Bluetooth et la caméra du téléphone.