Hola a todos,
Tras el directo de ayer y la solicitud de aplicación móvil, lancé a Claude sobre el tema solo para ver. En menos de 15 minutos, me sacó una especificación completa y una demo en mi teléfono Android donde encuentro mis dashboards, escenas… todo Gladys en realidad. Estoy impresionado… ![]()
Aquí está su análisis:
El « yo » en el mensaje es Claude, yo no habría sido capaz de sacar todo esto en 15 minutos.
He comenzado a trabajar en una aplicación móvil nativa para Gladys, y me gustaría compartir el enfoque antes de seguir adelante — especialmente la elección de arquitectura, que me parece el punto más discutible y, por lo tanto, aquel en el que sus comentarios serán más útiles.
La base es funcional: la aplicación funciona en mi teléfono, conectada a mi instancia, con todos mis dashboards y mis cámaras.
El problema
Hoy en día, Gladys en móvil es el navegador o la adición a la pantalla de inicio. Funciona, pero tiene un límite:
- no hay notificaciones confiables;
- no hay seguimiento de presencia en segundo plano;
- no hay presencia en las tiendas;
- y el service worker actual (
front/old-sw.js) se desinstala voluntariamente en cada activación — por lo tanto, no hay base PWA utilizable sobre la cual construir.
La elección de arquitectura: integrar el front existente
Esta es la decisión estructurante. Tres opciones estaban sobre la mesa.
Opción A — React Native, una aplicación nativa distinta
Una aplicación reescrita, consumiendo la API REST y el WebSocket de Gladys. Mejor rendimiento, sensación nativa real.
Pero hay que reescribir toda la interfaz: 23 rutas, 22 tipos de widgets de dashboard, el editor de escenas, el mapa, el chat, las páginas de integración. Y sobre todo, hay que mantener dos interfaces en paralelo: cada evolución del front web debería ser reportada manualmente a la aplicación móvil, con el riesgo de divergencia que esto implica. Para un proyecto comunitario, me parece insostenible a largo plazo.
Opción B — Mejorar la PWA
Reparar el service worker, agregar el caché sin conexión y el Web Push. Ligero, sin necesidad de gestionar tiendas.
Pero esto no resuelve ni la geolocalización en segundo plano, ni los widgets de pantalla de inicio, ni la presencia en las tiendas. Y en iOS, el Web Push sigue limitado. Seguiríamos con los mismos límites.
Opción C — Capacitor + el front existente ← seleccionado
El front de Gladys actual (Preact / Vite) se compila exactamente como hoy, y luego se integra en una WebView nativa mediante Capacitor. Las capacidades que el web no puede ofrecer se exponen a través de plugins, detrás de una capa de abstracción.
front/ (Preact, sin cambios)
└── build/ ──> WebView Capacitor
├── android/
└── ios/
+ plugins nativos: push, geoloc, biométrica,
mDNS, almacenamiento seguro, widgets
¿Por qué esta elección?:
- Un solo código base. Toda evolución del front beneficia al web, a Android y a iOS sin necesidad de portabilidad. Este es el punto decisivo para un proyecto comunitario.
- Reutilización casi total. Pantallas, widgets, acciones, traducciones, tema oscuro: todo se mantiene tal cual.
- Nada perdido en lo nativo. Notificaciones push, geolocalización por zonas, biométrica, descubrimiento mDNS, widgets de pantalla de inicio siguen accesibles a través de los plugins.
- Reversible. Si el enfoque muestra sus límites, el front sigue siendo el front — no se ha creado una deuda paralela.
El principio director: el shell nativo no incluye ninguna lógica de negocio. Expone capacidades, el front las consume. Toda funcionalidad nativa es opcional, y su ausencia (permiso denegado, plataforma no compatible) nunca rompe la aplicación.
Lo que se ha hecho: la base de conexión
El primer lote responde a una pregunta que el front web nunca se plantea: ¿a qué instancia hablar?
En el web, la respuesta es trivial — el front es servido por la instancia misma. Una aplicación móvil, en cambio, se distribuye en un solo build para todos y debe ser configurada por el usuario.
Concretamente:
- Configuración resuelta en tiempo de ejecución.
front/src/config.jsleíaprocess.env, que Vite reemplaza por literales en el build. El objeto exportado ahora es mutable, con una lista blanca: solo las claves de conexión son sobrescribibles, para que un perfil almacenado malformado no pueda activar el modo demo. - Pantalla de primera conexión pidiendo la dirección de la instancia, con prueba de la dirección antes del registro.
- Perfiles multi-instancia (residencia principal, secundaria, instancia de prueba), almacenados en el Keychain / Keystore.
- Conmutación automática red local ↔ Gladys Plus, con una sonda en
/api/v1/ping. - Página de ajustes para gestionar estas instancias — oculta en el web, donde no tendría sentido.
Impacto en el front web: nulo. El código nativo se elimina por completo del bundle web mediante el tree-shaking, y los valores de build siguen siendo los mismos. Verificado manualmente en el navegador.
Tres cosas que la especificación no había previsto
Esta es la parte más instructiva, y la que puede servir a otros.
1. La regla mixed-content, y el compromiso que impone
Para que el cifrado de extremo a extremo de Gladys Plus funcione, se necesita crypto.subtle, que solo está disponible en un contexto seguro. Capacitor lo obtiene sirviendo la WebView en https://localhost.
Excepto que una página https:// no puede llamar ni a http:// ni a ws:// — Chromium lo bloquea. Sin embargo, una instancia Gladys local suele estar en HTTP simple. La configuración de seguridad de red de Android no cambia nada: es una política del navegador, no de la plataforma.
El plugin CapacitorHttp resuelve el caso de las solicitudes HTTP (salen por la capa nativa), pero no el de los WebSockets, que siguen bloqueados. Sin WebSocket, no hay tiempo real.
De ahí el arbitraje, que hay que conocer:
androidScheme |
Contexto seguro | Gladys Plus | HTTP local | WebSocket |
|---|---|---|---|---|
https |
sí | OK | a través de CapacitorHttp | bloqueado |
http |
no | no disponible | OK | OK |
Para este primer lote, he elegido http: el objetivo era la conexión a la instancia local, que exige el WebSocket. Es explícitamente temporal. La salida limpia es servir la instancia en HTTPS detrás de un reverse proxy (Caddy, nginx, Traefik): todo pasa entonces en https/wss, sin bloqueos, contexto seguro conservado. Es también la única vía que valdrá para iOS.
Si tienes una opinión sobre este punto, me interesa mucho: ¿deberíamos asumir exigir una instancia en HTTPS para el uso de Gladys Plus desde la aplicación móvil, o es mejor invertir en un plugin WebSocket nativo para permanecer en https?
2. Un cliente HTTP que congelaba su URL
HttpClient capturaba config.localApiUrl en su constructor — ejecutado al cargar el módulo, por lo tanto antes de que el perfil móvil se resolviera. En la WebView, window.location.origin vale https://localhost, es decir, la aplicación misma: todas las solicitudes habrían ido en vano.
Se ha convertido en un getter que lee la configuración en cada solicitud. En el web, el valor nunca cambia, por lo tanto, comportamiento idéntico.
3. viewport-fit=cover faltante
La barra superior de la aplicación pasaba por debajo del reloj y los iconos de notificación de Android. La causa no era el encabezado, sino el <meta viewport>: suCon viewport-fit=cover, todas las variables env(safe-area-inset-*) valen cero.
Detalle interesante: el front de Gladys ya utiliza safe-area-inset-bottom en cinco lugares (chat, dashboard, lista de dispositivos). Por lo tanto, no hacían nada en móvil. La corrección las reactiva todas.
Validación en dispositivo
Probado en un Xiaomi con Android 16 (WebView Chromium 151), contra una instancia real:
| Punto | Resultado |
|---|---|
| Web Crypto (RSA-OAEP 2048, ECDSA P-256, AES-GCM, PBKDF2) | 9/9 en contexto https |
| WebSocket hacia una instancia HTTP | Handshake en 44 ms |
| MSE / hls.js (cámaras) | Disponible |
| Recorrido completo: configuración, inicio de sesión, dashboards, cámaras | Funcional |
Una medida contraintuitiva: PBKDF2 100,000 iteraciones en 15 ms en el teléfono, frente a 45 ms en mi PC. La implementación de Android está acelerada por hardware. El riesgo de un desbloqueo lento en móvil, que había anticipado, no existe.
Una lección también: mi prototipo de validación inicial daba tres luces verdes, y sin embargo no detectó ninguno de los dos bloqueos reales. Probaba un WebSocket desde una página http://localhost — por lo tanto sin mixed-content — y no emitía ninguna solicitud API. Hay que validar crypto, WebSocket y una solicitud API real en la configuración de esquema definitiva, no aisladamente.
Lo que queda por hacer
Lote 2 — ergonomía táctil: barra de pestañas baja, gesto de retroceso de Android, arrastrar y soltar del dashboard con el dedo, comportamiento del teclado.
Lote 3 — notificaciones y presencia. Es la principal aportación de la app en comparación con el navegador, y el único lote que requiere desarrollo en el lado del servidor: una ruta de registro de tokens push, y una acción de escena user.send-push-notification.
Un punto a decidir colectivamente: una instancia autoalojada no puede hablar directamente con FCM o APNs, por falta de claves de servicio — que no se pueden distribuir en una imagen pública. El relay por Gladys Plus es la vía natural (contenido cifrado de extremo a extremo, la notificación solo lleva un desencadenante), con una caída a las notificaciones locales sin suscripción. ¿Es un compromiso aceptable para la comunidad?
Lote 4 — integración del sistema: descubrimiento mDNS (el servidor ya publica el servicio), widgets de pantalla de inicio, App Intents iOS y accesos directos de Android, asistente de voz.
Lote 5 — publicación: CI de compilación y firma, fichas de la tienda, beta.
Dos puntos abiertos en los que necesito ayuda
iOS no ha podido ser validado: no tengo un Mac. Cuatro incógnitas persisten — contexto seguro en capacitor://, MSE en WKWebView, entitlement multicast para el mDNS (concedido caso por caso por Apple, con plazos impredecibles), y el comportamiento de ATS frente a una instancia en HTTP. Si alguien tiene un Mac y media hora, ejecutar el prototipo de validación resolvería estos cuatro puntos de una vez.
La cuestión del HTTPS local. Condiciona el uso de Gladys Plus desde la app. Exigir una instancia en HTTPS es correcto técnicamente, pero añade un paso de configuración a usuarios que no lo necesitan hoy. Sus opiniones me interesan.
La especificación completa está adjunta a continuación (despliegue la sección): arquitectura, conexión, capacidades nativas, seguridad, puntos duros, lotes de entrega, compilación y aceptación. El código está en una rama dedicada.
No duden en cuestionar la elección de arquitectura — es precisamente el momento en que aún es fácil cambiarla.
Gracias por su lectura.
[details=« 📄 Especificación técnica completa (despliegue) »]
Especificación técnica — v1
Portear el front de Gladys existente (Preact / Vite) a una aplicación nativa de Android e iOS a través de Capacitor, con paridad funcional completa y las capacidades que solo una app nativa puede ofrecer: notificaciones push, geolocalización en segundo plano, desbloqueo biométrico, descubrimiento de la instancia en la red local.
| Base de código | Gladys 5.0.2 |
| Enfoque | Capacitor + front existente |
| Objetivos | Android 8+ · iOS 15+ |
| Ámbito v1 | Paridad completa |
| Fecha | 30 de agosto de 2026 |
Índice
- Contexto y objetivos
- Estado del existente
- Arquitectura objetivo
- Conexión y autenticación
- Capacidades nativas
- Adaptaciones de interfaz
- Seguridad
- Puntos duros identificados
- Lotes de entrega
- Compilación, CI y publicación
- Pruebas y aceptación
- Fuera del ámbito de la v1
1. Contexto y objetivos
Gladys Assistant se usa hoy en día en móvil a través del navegador o la adición a la pantalla de inicio. Este enfoque tiene sus límites: no hay notificaciones fiables, no hay seguimiento de presencia en segundo plano, no hay presencia en las tiendas, y un service worker que, en el estado actual del repositorio, se desinstala voluntariamente en cada activación.
Objetivos
- Paridad funcional con el front web: dashboard, dispositivos, escenas, cámaras, chat, calendario, mapa, ajustes e integraciones.
- Notificaciones push nativas desencadenadas por las escenas de Gladys, con acciones rápidas desde la notificación.
- Presencia automática mediante geolocalización en segundo plano, alimentando la detección de zonas de Gladys.
- Conexión sin fricción: descubrimiento automático de la instancia en la red local, cambio transparente local ↔ Gladys Plus.
- Una sola base de código para el web, Android e iOS: toda evolución del front beneficia a los tres objetivos sin portabilidad.
Principios rectores
- El respeto a la vida privada sigue siendo la regla. Sin telemetría, sin SDK analítico de terceros. El cifrado de extremo a extremo de Gladys Plus se preserva sin excepción.
- El front sigue siendo la fuente de verdad. El shell nativo no incorpora ninguna lógica de negocio: expone capacidades, el front las consume.
- Degradación limpia. Toda funcionalidad nativa es opcional; su ausencia (permiso denegado, plataforma no compatible) nunca rompe la app.
2. Estado del existente
El front es una aplicación Preact 10 construida por Vite 6, con enrutamiento preact-router, estado global unistore, e internacionalización preact-i18n en tres idiomas (fr, en, de). Se sirve ya sea por el servidor Gladys local (server/static), ya sea por Gladys Plus.
Lo que es directamente reutilizable
| Elemento | Ubicación | Estado |
|---|---|---|
| Pantallas y enrutamiento | front/src/routes/ |
Tal cual |
| Widgets de dashboard (22 tipos) | front/src/components/boxs/ |
Tal cual |
Acciones y store unistore |
front/src/actions/ |
Tal cual |
| Traducciones fr / en / de | front/src/config/i18n/ |
Tal cual |
| Tema y modo oscuro | front/src/style/ |
Tal cual |
Lo que debe ser adaptado
| Elemento | Ubicación | Naturaleza del trabajo |
|---|---|---|
| Configuración de URL | front/src/config.js |
Las URL están fijas en el momento del vite build a través de process.env. Hay que hacerlas dinámicas en tiempo de ejecución. Punto duro |
| Almacenamiento de sesión | front/src/utils/Session.js, front/src/utils/keyValueStore.js |
localStorage en claro. Reemplazar por un almacenamiento cifrado apoyado en el Keychain / Keystore. Punto duro |
| Cliente HTTP | front/src/utils/HttpClient.js |
localApiUrl estaba capturado en el constructor, ejecutado al cargar el módulo a través de getDefaultState() — por lo tanto antes de la resolución del perfil móvil. Se ha convertido en un getter que lee la configuración en cada solicitud. En la web el valor nunca cambia: comportamiento idéntico. |
| Zonas seguras | front/index.html, front/src/template.html |
<meta viewport>No tenía viewport-fit=cover, lo que ponía todas las variables env(safe-area-inset-*) a cero —incluyendo los cinco usos existentes de safe-area-inset-bottom. |
| WebSocket | front/src/utils/Session.js |
Reconexión a intervalos fijos de 1 s, sin conciencia del ciclo de vida de la aplicación. Debe implementarse un backoff y una reacción a los eventos de suspensión. |
| Service worker | front/old-sw.js |
Se desinstala a sí mismo al activarse. No se utiliza en el shell nativo; debe conservarse tal cual para la web a fin de purgar las antiguas caches. |
| Cámaras (HLS) | front/src/components/boxs/camera/ |
Cargador hls.js personalizado para inyectar el token. Debe validarse en WebView, especialmente el HLS nativo en iOS. |
| Arrastre y suelta | front/src/utils/dragAndDropBackend.js |
Ya cambia entre backends HTML5 y táctiles; debe revalidarse en la WebView. |
Del lado del servidor
No se requieren modificaciones para los lotes 1 y 2: la API REST (server/api/controllers/, 25 controladores) y el WebSocket ya cubren las necesidades. Dos adiciones al servidor son necesarias más adelante: el registro de los tokens push (lote 3) y la acción de escena correspondiente.
3. Arquitectura objetivo
El front se compila exactamente como hoy, luego se empaqueta en una WebView nativa por Capacitor. Las capacidades que la web no puede ofrecer se exponen al front en forma de plugins Capacitor, detrás de una capa de abstracción que devuelve valores neutros cuando la app se ejecuta en un navegador.
┌─────────────────────────────────────┐
│ APLICACIÓN MÓVIL │
│ │ ┌──────────────────────────────┐
│ ┌───────────────────────────────┐ │ │ Red local │
│ │ WebView — front Preact │ │───────>│ REST /api/v1 + WebSocket │
│ │ routes/ · components/ │ │ │ Token portador, latencia min. │
│ │ ───────────────────────────── │ │ │ Descubrimiento mDNS │
│ │ utils/native/ — abstracción │ │ └──────────────┬───────────────┘
│ │ no-op en la web │ │ │
│ └───────────────┬───────────────┘ │ ┌──────────────▼───────────────┐
│ ▼ │ │ Gladys Plus — a distancia │
│ ┌───────────────────────────────┐ │───────>│ gladys-gateway-js │
│ │ Shell nativo Capacitor │ │ │ RSA + ECDSA, extremo a extremo │
│ │ push · geoloc · biométrica │ │ │ El servidor no descifra nada │
│ │ mDNS · almacenamiento · widgets │ │ └──────────────┬───────────────┘
│ └───────┬───────────────┬───────┘ │ │
└──────────┼───────────────┼──────────┘ │
▼ ▼ ┌──────────────▼───────────────┐
┌────────────┐ ┌────────────┐ │ Instancia Gladys │
│ Android │ │ iOS │ │ Node 24 · SQLite │
│ Kotlin·FCM │ │ Swift·APNs │ │ 39 servicios │
└────────────┘ └────────────┘ └──────────────────────────────┘
La selección de la ruta de red es automática y se reevaluada cada vez que cambia la red; el usuario puede forzarla desde la configuración. El front compilado es idéntico al servido en la web: solo se añade la capa utils/native/, y esta devuelve implementaciones neutras fuera de móvil.
Estructura de directorios
Un nuevo directorio mobile/ en la raíz, junto a front/ y server/:
mobile/
├── capacitor.config.ts configuración, apunta a front/build
├── package.json dependencias de Capacitor únicamente
├── android/ proyecto Android generado, versionado
├── ios/ proyecto Xcode generado, versionado
├── plugins/ plugins caseros (mDNS, descubrimiento)
└── resources/ iconos y pantallas de inicio fuentes
front/src/utils/native/ capa de abstracción, en el front
├── index.js detección de plataforma
├── push.js
├── geolocation.js
├── biometrics.js
├── secureStorage.js
└── discovery.js
Plugins seleccionados
| Necesidad | Plugin | Origen |
|---|---|---|
| Notificaciones push | @capacitor/push-notifications |
Oficial |
| Notificaciones locales | @capacitor/local-notifications |
Oficial |
| Geolocalización puntual | @capacitor/geolocation |
Oficial |
| Geolocalización en segundo plano | @capacitor-community/background-geolocation |
Comunidad |
| Almacenamiento cifrado | capacitor-secure-storage-plugin |
Comunidad |
| Biometría | @aparajita/capacitor-biometric-auth |
Comunidad |
| Estado de la red | @capacitor/network |
Oficial |
| Ciclo de vida de la app | @capacitor/app |
Oficial |
| Barra de estado y muescas | @capacitor/status-bar |
Oficial |
| Descubrimiento mDNS | gladys-discovery |
Por escribir |
Elección de diseño — Cada plugin de la comunidad es un riesgo de mantenimiento. La capa
utils/native/existe precisamente para que el reemplazo de un plugin abandonado solo afecte a un archivo, nunca a las pantallas.
4. Conexión y autenticación
Esta es la parte más estructurante de la especificación. El front actual solo conoce un modo a la vez, decidido en el momento de la compilación por la variable GATEWAY_MODE. La app móvil debe gestionar los dos simultáneamente y cambiar de uno a otro sin intervención.
4.1 Configuración en tiempo de ejecución
front/src/config.js lee process.env, que Vite reemplaza por literales en el momento de la compilación. Es necesario introducir una resolución en tiempo de ejecución:
- En la web, el comportamiento actual se conserva exactamente igual: ninguna regresión.
- En móvil, las URL provienen del perfil de conexión activo, almacenado en el almacenamiento cifrado.
En concreto, config.js expone un objeto mutable alimentado al inicio por utils/native/, antes del primer renderizado. Los módulos que importan la configuración hoy en día permanecen inalterados.
4.2 Perfiles de conexión
La app gestiona varios perfiles — un hogar principal, una residencia secundaria, una instancia de prueba. Cada perfil registra:
| Campo | Contenido |
|---|---|
id |
UUID local |
name |
Nombre mostrado, introducido por el usuario |
mode |
local, gateway o auto |
localUrl |
URL de la instancia en la red local |
localFingerprint |
Huella del certificado, si HTTPS auto-firmado |
ssids |
Redes Wi-Fi donde se aplica el modo local |
credentials |
Referencia al almacenamiento cifrado, nunca el valor |
4.2 bis Las tres URL de Gladys Plus
No confundir — tienen roles distintos:
| URL | Rol | Utilizada por |
|---|---|---|
https://api.gladysgateway.com |
La API de la pasarela | config.gladysGatewayApiUrl, llamada por gladys-gateway-js |
https://plus.gladysassistant.com |
El front web de Gladys Plus (el mismo código, en modo gateway) | Enlaces salientes: suscripción, facturación |
https://gladysassistant.com/plus/ |
Página de marketing | utils/gladysPlusUrl.js (enlaces de inscripción) |
La app móvil habla con la API, nunca con el front alojado: ella es el front.
El valor por defecto de config.js es por lo tanto ya correcto y no tiene que ser
modificado.
Por otro lado, los flujos que no pueden desarrollarse en la app — gestión de
la suscripción, facturación Stripe — deben abrir plus.gladysassistant.com
en el navegador del sistema, y no en la WebView: un túnel de pago en una WebView
aplicativa es rechazado por las dos tiendas. El plugin
@capacitor/browser (pestaña del sistema) es el vehículo adecuado.
4.3 Descubrimiento de la instancia local
El servidor Gladys ya publica un servicio mDNS (server/lib/mdns/). El plugin gladys-discovery a escribir consulta este servicio y presenta las instancias encontradas:
- Android:
NsdManager, con adquisición de unMulticastLockdurante la búsqueda. - iOS:
NWBrowser(Network framework). Requiere el entitlementcom.apple.developer.networking.multicast, que hay que solicitar explícitamente a Apple, y la declaraciónNSBonjourServicesenInfo.plist.
En caso de fallo en el descubrimiento, la introducción manual de una dirección sigue siendo siempre posible: nunca es un camino de repuesto oculto, sino una opción visible desde la primera pantalla.
4.4 Conmutación local ↔ remoto
En modo auto, la app elige la ruta de red en cada inicio, en cada retorno al primer plano y en cada cambio de conectividad señalado por @capacitor/network:
- Si un perfil local está configurado, se intenta una solicitud
GET /api/v1/pingcon un tiempo de espera de 1,5 segundos. - En caso de éxito, se mantiene el modo local: latencia mínima, ninguna dependencia externa.
- En caso de fallo y si una cuenta Gladys Plus está vinculada, la app cambia a la pasarela.
- Si ninguna ruta responde, se muestra una pantalla fuera de línea con los últimos datos conocidos y un botón de reanudación.
La conmutación reconstruye el cliente HTTP y la conexión WebSocket. Se indica discretamente en la interfaz (un indicador en el encabezado), nunca mediante una ventana emergente bloqueante.
Atención — Los dos modos no utilizan el mismo almacén de tokens ni el mismo cliente:
Session+HttpClientpara el local,GatewaySession+GatewayHttpClientpara Gladys Plus. El cambio debe vaciar las cachés de solicitudes en vuelo deHttpClient(laMappendingRequests) para evitar que una respuesta del antiguo camino se asigne al nuevo.
4.5 Cifrado de extremo a extremo
GatewaySession se basa en @gladysassistant/gladys-gateway-js, que recibe window.crypto. En una WebView, la Web Crypto API solo está disponible en un contexto seguro: los esquemas capacitor:// (iOS) y https:// (Android) lo son, a diferencia de http://. Este punto debe validarse desde el lote 1, ya que condiciona todo el acceso remoto.
Las claves serializadas (gateway_serialized_keys) están hoy en localStorage. En móvil, van al almacenamiento cifrado adosado al Keychain (iOS) o al Keystore con cifrado de hardware (Android).
4.6 Autenticación de dos factores
El flujo 2FA existente (actions/login/loginGateway.js) se conserva sin modificación, incluyendo la generación de los códigos de recuperación y el pegado desde el portapapeles. Se añade un relleno automático del código desde las sugerencias del teclado a través del atributo autocomplete="one-time-code".
4.7 Bloqueo biométrico
Opcional, activable en la configuración. Cuando está activo, se solicita un desbloqueo por huella o reconocimiento facial al inicio y después de un tiempo de inactividad configurable (por defecto 5 minutos en segundo plano). La caída es el código del dispositivo; nunca hay un código propio de Gladys que memorizar además.
5. Capacidades nativas
5.1 Notificaciones push
Esta es la principal aportación de la app en comparación con la web. Implica un desarrollo en el lado del servidor Gladys, y no solo en el lado móvil.
Registro. En el primer lanzamiento después de aceptar el permiso, la app obtiene un token FCM (Android) o APNs (iOS) y lo envía a la instancia. Nueva ruta a crear: POST /api/v1/user/push_token, con el token, la plataforma y el identificador de sesión. El token está vinculado a la sesión existente: revocar una sesión revoca el push asociado.
Envío. Una instancia Gladys autoalojada no puede hablar directamente con FCM o APNs sin claves de servicio — que no pueden distribuirse en una imagen pública. Dos caminos:
- A través de Gladys Plus (recomendado) — La instancia transmite la notificación a la pasarela, que posee las claves y reenvía a FCM / APNs. El contenido útil está cifrado de extremo a extremo; la notificación transportada solo contiene un desencadenante, la app recupera el contenido real de la instancia al recibirla.
- Sin Gladys Plus — Recurso a las notificaciones locales: mientras el WebSocket esté vivo, la app programa ella misma una notificación. Funciona en segundo plano reciente, no después de una suspensión prolongada. Este límite debe anunciarse claramente en la configuración, no descubierto en el uso.
Disparador desde una escena. Se agrega una nueva acción de escena user.send-push-notification, con destinatarios, título, mensaje y, opcionalmente, una imagen de cámara. Debe declararse obligatoriamente en el esquema Joi de server/models/scene.js, de lo contrario, el registro de la escena fallará con un código 422 sin mensaje explícito.
Acciones rápidas. Las notificaciones pueden incluir hasta tres botones de acción, definidos en la escena: ejecutar otra escena, encender o apagar un dispositivo, abrir una cámara. Estas acciones se procesan sin abrir la aplicación cuando la red lo permite.
5.2 Geolocalización y presencia
Gladys ya tiene server/lib/location/ y la gestión de zonas. La aplicación alimenta POST /api/v1/location:
- Seguimiento por zonas en lugar de seguimiento continuo: la aplicación se suscribe a las entradas y salidas de las zonas definidas en Gladys. El costo en batería es mucho menor que un registro periódico.
- Precisión reducida por defecto: la posición solo se transmite al cruzar una zona, no de manera continua.
- Cola de espera sin conexión: los eventos capturados sin red se guardan localmente y se envían al reconectarse, con la fecha y hora reales.
- Interruptor global visible en la configuración, y detención inmediata del seguimiento cuando se desactiva.
Restricción de la tienda — La geolocalización en segundo plano es el motivo de rechazo más frecuente en la App Store. Se requiere: una explicación clara antes de solicitar el permiso, una justificación precisa en
NSLocationAlwaysAndWhenInUseUsageDescription, y una aplicación completamente funcional si se deniega el permiso. En Android 13+, el permisoACCESS_BACKGROUND_LOCATIONse solicita en un segundo paso, después del permiso de primer plano.
5.3 Widgets de pantalla de inicio
Escritos nativamente: WidgetKit en SwiftUI en iOS, Glance en Android. Leen una instantánea de datos escrita por la aplicación en un espacio compartido (App Group en iOS, SharedPreferences en Android), actualizada cada vez que la aplicación pasa a primer plano y cada vez que se recibe una notificación.
- Escenas: hasta cuatro escenas favoritas, ejecutables con un toque.
- Temperatura: temperatura de una habitación elegida.
- Dispositivos: estado y conmutación de dos a cuatro dispositivos.
Los widgets no inician una sesión propia: delegan en la aplicación, que ejecuta la acción. Si la sesión ha expirado, el widget muestra un estado de reconexión necesaria en lugar de un error.
5.4 Accesos directos del sistema
- iOS: exposición de escenas en App Intents, lo que las hace disponibles en Accesos directos, Siri y el botón de acción.
- Android: accesos directos dinámicos en el icono de la aplicación para las escenas favoritas.
5.5 Asistente de voz
El front ya tiene una cadena de voz completa: speechCommandRecorder.js, recordUntilSilence.js, speechTtsPlayback.js, respaldada por gateway.stt.js y gateway.processVoiceMessage.js en el lado del servidor.
En WebView, el acceso al micrófono requiere el permiso nativo (NSMicrophoneUsageDescription, RECORD_AUDIO) y la autorización a nivel de WebView. En Android, esta última pasa por onPermissionRequest, que debe manejarse en el shell nativo. La reproducción TTS debe configurar la categoría de sesión de audio en iOS para no ser cortada por el modo silencioso.
5.6 Cámaras
El flujo HLS se lee a través de hls.js con un cargador personalizado que inyecta el token de autenticación. En WebView:
- En iOS, el HLS nativo de
<video>no permite agregar encabezados;hls.js` a través de MSE sigue siendo necesario. La compatibilidad con MSE en WKWebView debe verificarse pronto. - El modo de pantalla completa de video requiere
allowsInlineMediaPlaybacky una gestión explícita de la rotación. - La reproducción debe pausarse al pasar a segundo plano y reanudarse al volver, para no consumir batería y datos innecesariamente.
6. Adaptaciones de interfaz
La interfaz sigue siendo la del front web. Las adaptaciones son específicas, y ninguna debe degradar la experiencia en el navegador.
Zonas seguras y muescas. Aplicación de env(safe-area-inset-*) en el encabezado, la navegación inferior y los modales. El viewport pasa a viewport-fit=cover en front/src/template.html, sin efecto en la web.
Navegación. La barra lateral actual se convierte en una barra de pestañas inferior en pantalla estrecha: Dashboard, Dispositivos, Escenas, Chat, Ajustes. El gesto de retroceso de Android se asocia a preact-router a través de @capacitor/app, con una confirmación de salida solo en la raíz.
Objetivos táctiles y gestos.
- Todo objetivo interactivo tiene al menos 44 × 44 puntos.
- El arrastrar y soltar del dashboard utiliza el backend táctil de
react-dnd, ya presente, con una pulsación larga para armar el movimiento y evitar conflictos con el desplazamiento. - Tirar para actualizar en las pantallas de lista, desactivado durante una edición en curso.
Teclado. Redimensionamiento de la vista al abrir el teclado en lugar de superposición, tipos de entrada adaptados (inputmode="numeric" para los códigos, type="email"), y desplazamiento automático hacia el campo activo.
Modo oscuro. El tema oscuro de Gladys se basa en una inversión CSS global, con la clase dark-mode-no-invert para los elementos que deben mantener sus colores reales. Este mecanismo se conserva tal cual. Dos puntos nuevos: el color de la barra de estado nativa debe seguir el tema, y la inversión no debe aplicarse a la pantalla de inicio nativa.
Modo tableta. El modo tableta existente (routes/dashboard/SetTabletMode.jsx) cobra todo su sentido en una tableta mural: se añade el mantenimiento de la pantalla encendida, el bloqueo de la orientación y un modo de pantalla completa inmersivo. El bloqueo por código existente se conserva.
7. Seguridad
Almacenamiento de secretos
Ningún secreto permanece en localStorage en móvil. Migran al almacenamiento cifrado: tokens de acceso y actualización, claves serializadas Gladys Plus, huellas de claves públicas, token de autenticación doble. Permanecen en el almacenamiento ordinario las preferencias no sensibles: idioma, modo oscuro, casa seleccionada.
HTTP en claro en la red local
Una instancia Gladys local se sirve muy a menudo en http://. Sin embargo, Android bloquea el tráfico en claro por defecto desde la versión 9, e iOS a través de ATS.
| Plataforma | Mecanismo | Alcance |
|---|---|---|
| Android | network_security_config.xml |
Autorización del tráfico en claro restringido a los rangos privados: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, más .local |
| iOS | NSAllowsLocalNetworking |
Excepción ATS limitada a la red local, sin desactivación global |
Nunca se trata de NSAllowsArbitraryLoads ni de cleartextTrafficPermitted global: estos dos ajustes desarman la protección en todo Internet y son un motivo de rechazo en la revisión de Apple.
Certificados auto-firmados
Para una instancia local en HTTPS auto-firmado, la app propone el anclaje de la huella del certificado en el momento de añadir el perfil, con visualización de la huella para verificación. Ninguna aceptación ciega; un cambio de huella bloquea la conexión y exige una confirmación explícita.
Protección del contenido
- Ocultación del contenido en el selector de apps (
FLAG_SECUREopcional en Android, vista de superposición en iOS), activable en los ajustes. - Ninguna captura de pantalla de flujo de cámara en las vistas del sistema cuando la opción está activa.
- Borrado completo del almacenamiento cifrado al desconectarse.
Lo que la app no hace
- Ningún SDK de analítica, publicidad o seguimiento.
- Ningún informe de errores automático de terceros; en caso de necesidad, un envío manual, explícito y consentido.
- Ningún dato de la casa transita por un servidor distinto a la instancia del usuario o Gladys Plus, cuyo contenido está cifrado de extremo a extremo.
8. Puntos duros identificados
Estos siete puntos son aquellos que pueden hacer desbordar el calendario. Se han colocado voluntariamente lo más pronto posible en el planificación, para que su coste real sea conocido antes de haber invertido en el resto.
| Punto | Riesgo | Tratamiento |
|---|---|---|
| Web Crypto en WebView | Si window.crypto.subtle está indisponible, todo el acceso remoto Gladys Plus cae. |
Prototipo de validación desde la primera semana, en las dos plataformas. Los esquemas capacitor:// y https:// son contextos seguros: el riesgo es bajo pero el impacto total. |
| Configuración congelada en el build | config.js se resuelve por Vite en el build: la app no puede cambiar de instancia. |
Reestructuración en configuración resuelta en ejecución, con conservación estricta del comportamiento web actual. |
| Push sin Gladys Plus | Una instancia auto-alojada no tiene claves FCM / APNs. | Relé por la pasarela para las cuentas Gladys Plus; notificaciones locales en caso de fallo, con sus límites anunciados explícitamente. |
| Multicast iOS | El descubrimiento mDNS requiere un permiso concedido caso por caso por Apple, con plazos imprevisibles. | Solicitud presentada tan pronto como se abra la cuenta de desarrollador. La introducción manual de la dirección es un camino de primera clase, no un repliegue. |
| WebSocket en segundo plano | Los dos sistemas operativos cortan las conexiones en espera; el estado mostrado puede estar desactualizado al despertar. | Reconexión con backoff al volver al primer plano, recarga completa del estado, e indicador de frescura de los datos. |
| HLS en WKWebView | La reproducción MSE a través de hls.js puede comportarse de manera diferente a Safari. |
Prueba en dispositivo real en el lote 1, antes de cualquier compromiso en el lote 4. |
| Contenido mixto (descubierto en pruebas) | La WebView servida en https://localhost no puede llamar ni a http:// ni a ws:// en la instancia local: Chromium bloquea. La configuración de seguridad de red de Android no cambia nada — es una política del navegador, no de la plataforma. CapacitorHttp resuelve el caso de las solicitudes HTTP, pero no el de los WebSockets, que siguen sometidos a la regla. |
Ver el arbitraje a continuación: el lote 1 pasa a androidScheme: "http". |
Arbitraje: esquema de la WebView
La elección del esquema opone dos capacidades que no se pueden obtener juntas
mientras la instancia esté en HTTP:
androidScheme |
Contexto seguro | Crypto / Gladys Plus | HTTP local | WebSocket local |
|---|---|---|---|---|
https |
sí | OK | a través de CapacitorHttp | BLOQUEADO |
http |
no | indisponible | OK | OK |
Decisión para el lote 1: http. El lote tiene como objetivo la conexión a la
instancia
local, que exige el WebSocket; Gladys Plus aún no está conectado. Esta
elección es explícitamente temporal.
Debe revisarse obligatoriamente antes del lote Gladys Plus. Dos vías:
- Instancia en HTTPS (proxy inverso TLS: Caddy, nginx, Traefik). Todo pasa
ahttps/wss, sin bloqueos, contexto seguro conservado. Es la
única solución que también valdrá para iOS, donde el esquemacapacitor://impone
las mismas restricciones. Vía recomendada. - Plugin WebSocket nativo, abriendo la conexión fuera de WebView como
CapacitorHttplo hace para las solicitudes. Permite permanecer enhttps, pero
exige modificarSession.js, compartido con la web.
Consecuencia práctica: mientras el lote 1 esté en
androidScheme: "http",
crypto.subtleno está disponible en la app y cualquier intento de conexión
Gladys Plus fallará. El prototipo de validación, en cambio, medía bien la criptografía
enhttps— la capacidad de la plataforma está adquirida, pero no su coexistencia
con un backend en claro.
| Revisión App Store | Geolocalización en segundo plano y app llamada « cliente de un servicio » son dos motivos clásicos de rechazo. | Modo demo accesible a los revisores sin instancia, justificaciones de permisos redactadas con cuidado, captura de pantalla del uso real de las zonas. |
9. Lotes de entrega
Los lotes son realmente secuenciales: cada uno se basa en el anterior y termina con un entregable instalable en un dispositivo real. El lote 1 concentra intencionalmente los riesgos técnicos.
Lote 1 — Base y autenticación
ENTREGADO
Objetivo: demostrar que el enfoque funciona, y resolver los puntos difíciles 1, 2 y 6.
Commit feat(mobile): resolver la instancia a la que conectarse en tiempo de ejecución —
22 archivos, 1254 inserciones, 14 eliminaciones.
Configuración resuelta en tiempo de ejecución (setRuntimeConfig, lista blanca de claves)
Capa utils/native/— eliminada del bundle web por tree-shaking
Almacenamiento seguro (Keychain / Keystore, repliegue localStorageen la web)
Perfiles multiinstancia: lista, añadir, edición, eliminación, conmutación
Pantalla de primera conexión, con prueba de la dirección antes del registro
Página de ajustes « Instancias », oculta en la web (nativeOnly)
Indicador de conexión perdida en el encabezado
Zonas seguras: viewport-fit=cover+ barra superior y cajón de navegación
No regresión web verificada manualmente
Conexión Gladys Plus no funcional — ver el arbitraje del esquema, sección 8
Proyecto iOS no generado: no hay máquina macOS disponible
Validado en dispositivo (Xiaomi, Android 16, WebView Chromium 151): recorrido
completo de configuración, conexión a la instancia local, inicio de sesión, dashboards,
cámaras.
Controles: los tres checks del CI pasan (Prettier, ESLint 0 errores,
paridad de traducciones fr/en/de), más 43 pruebas de lógica ejecutadas en el código
real del repositorio.
Lote 2 — Paridad funcional y ergonomía táctil
Objetivo: una app utilizable a diario, sin aún las aportaciones nativas.
- Zonas seguras, barra de pestañas baja, gesto de retorno Android
- Arrastre y suelta del dashboard validado con el dedo
- Comportamiento del teclado, tirar para actualizar, objetivos táctiles
- Barra de estado y pantalla de inicio acordes al modo oscuro
- Revisión pantalla por pantalla de las 23 rutas existentes en teléfono y tableta
Lote 3 — Notificaciones y presencia
Objetivo: la primera razón concreta para instalar la app en lugar de abrir el navegador.
- Servidor: ruta
POST /api/v1/user/push_token, vinculada a las sesiones - Servidor: acción de escena
user.send-push-notification, esquema Joi incluido - Relé de notificaciones por Gladys Plus, contenido cifrado de extremo a extremo
- Notificaciones locales en caso de fallo sin Gladys Plus
- Acciones rápidas en las notificaciones
- Geolocalización por zonas, cola fuera de línea, interruptor global
- Biometría y bloqueo después de inactividad
Lote 4 — Integración al sistema
Objetivo: hacer que Gladys sea accesible sin siquiera abrir la app.
- Descubrimiento mDNS: plugin
gladys-discovery, Android e iOS - Widgets de pantalla de inicio: escenas, temperatura, dispositivos
- App Intents iOS y accesos directos dinámicos Android
- Asistente de voz: permiso de micrófono en WebView, sesión de audio iOS
- Cámaras: pantalla completa, rotación, parada en segundo plano
- Modo tableta mural: pantalla mantenida encendida, orientación bloqueada, pantalla inmersiva completa
Lote 5 — Publicación
Objetivo: la app es instalable desde las tiendas y se actualiza sola.
- Cadena CI: construcción y firma Android e iOS
- Fichas Play Store y App Store: descripciones, capturas, política de privacidad
- Modo demo para los revisores, sin instancia requerida
- Prueba interna luego beta abierta: Play Console y TestFlight
- Documentación de usuario y documentación de contribución
Ordenación — Los lotes 1 y 2 ya producen una app distribuible internamente. Si los puntos difíciles del lote 1 resultan más costosos de lo previsto, el arbitraje se hace en ese momento, antes de haber iniciado el desarrollo nativo del lote 3.
10. Construcción, CI y publicación
Cadena de construcción
Nuevos scripts npm en la raíz, en continuación de los scripts existentes:
build-mobile vite build dirigido a móvil, luego npx cap sync
mobile:android abre el proyecto en Android Studio
mobile:ios abre el proyecto en Xcode
mobile:live recarga en caliente en dispositivo conectado
La construcción móvil reutiliza front/vite.config.mjs con un modo dedicado: la configuración de URL ya no se inyecta en la construcción, y el service worker no se copia.
Versionado
La versión de la app sigue la de Gladys (package.json raíz, hoy 5.0.2), con un número de construcción incremental propio del móvil. Los versionCode Android y CFBundleVersion iOS se derivan automáticamente en CI.
Compatibilidad de instancia
Una app actualizada puede conectarse a una instancia más antigua. utils/instanceVersion.js ya gestiona esta detección en la web: se extiende para desactivar correctamente las funcionalidades que requieren una versión mínima del servidor, en particular el push, en lugar de dejar que una llamada falle sin explicación.
Integración continua
Un flujo de trabajo GitHub Actions dedicado, distinto de la CI existente:
- En cada pull request que afecta a
mobile/ofront/: construcción Android en modo debug, sin firma. - En cada etiqueta de versión: construcción firmada Android e iOS, depósito en los canales de prueba interna.
- Secretos de firma en secretos de repositorio; ninguna clave en el repositorio mismo.
- Las verificaciones existentes siguen aplicándose al front: Prettier luego ESLint, y cobertura de parche al 100% para cualquier código servidor añadido.
Cuentas y costos
| Poste | Naturaleza | Costo |
|---|---|---|
| Cuenta desarrollador Apple | Organización | 99 $ / año |
| Cuenta Google Play | Organización | 25 $ una vez |
| Proyecto Firebase | FCM únicamente | Gratis |
| Máquina de construcción iOS | macOS, CI o local | Variable |
11. Pruebas y aceptación
Lo que cubre la CI
- Las pruebas Cypress existentes siguen ejecutándose en el front web: protegen contra regresiones introducidas por la refactorización de la configuración.
- Pruebas unitarias Mocha en el código servidor añadido (ruta de token push, acción de escena), con
TZ=UTCimpuesto. - La construcción Android en modo debug sirve de prueba de humo en la integración Capacitor.
Prueba manual en dispositivo
Una matriz de prueba se mantiene actualizada para cada lote. Los escenarios que no pueden ser automatizados y que deben ser verificados manualmente:
| Escenario | Lo que se verifica |
|---|---|
| Conmutación Wi-Fi → datos móviles | Paso automático del modo local a Gladys Plus, sin desconexión visible |
| Retorno al primer plano después de una noche | Reconexión WebSocket, estado actualizado, ninguna datos caducados mostrados como actuales |
| Notificación, app cerrada | Recepción, apertura en la pantalla correcta, acción rápida ejecutada sin lanzar la app |
| Entrada y salida de zona | Evento de presencia reportado, incluso después de un paso fuera de red |
| Permiso denegado | La app sigue siendo completamente utilizable, con un mensaje que explica lo que está desactivado |
| Cámara en directo | Lectura, pantalla completa, rotación, parada limpia al pasar a segundo plano |
| Modo oscuro | Cohesión de la barra de estado, de la pantalla de inicio y de las zonas no invertidas |
| Instancia fuera de línea | Pantalla fuera de línea, ninguna reconexión agresiva, reanudación manual posible |
Resultados medidos — 30 de agosto de 2026
En Xiaomi 2412DPC0AG, Android 16 (API 36), WebView Chromium 151, frente a una
instancia real en HTTP en la red local:
| Prueba | Resultado |
|---|---|
crypto.subtle en contexto https://localhost |
disponible |
| RSA-OAEP 2048 / SHA-256 — generación | 104 ms |
| ECDSA P-256 — generación, firma, verificación | < 1 ms |
exportKey('jwk') (almacenamiento de claves) |
OK |
| PBKDF2 100 000 iteraciones | 15 ms |
| AES-GCM 256, tagLength 128 | < 1 ms |
| WebSocket a instancia HTTP (handshake) | 44 ms |
MSE — Hls.isSupported() |
disponible |
| Recorrido completo: configuración → inicio de sesión → paneles → cámaras | funcional |
El PBKDF2 es tres veces más rápido en el teléfono que en un PC de escritorio
(15 ms frente a 45 ms): la implementación de Android está acelerada por hardware. El
riesgo de un desbloqueo lento, considerado en la sección 5.7, no existe.
Lección de método. El prototipo de validación inicial dio tres luces
verdes y, sin embargo, no detectó ninguno de los dos bloqueos reales
encontrados posteriormente (contenido mixto en las solicitudes HTTP, luego en el WebSocket).
Probó un WebSocket desde una páginahttp://localhost— por lo tanto, sin
contenido mixto — y no emitió ninguna solicitud API a la instancia. Hay que
validar crypto, WebSocket y una solicitud API real en la configuración de esquema definitiva,
nunca por separado.
Parque de pruebas mínimo
- Un Android reciente y un Android antiguo (API 26 a 28), para la configuración de seguridad de red.
- Un iPhone con notch y una iPad, para las zonas seguras y el modo tableta.
- Una instancia Gladys local en
http://y una instancia vinculada a Gladys Plus.
12. Fuera del alcance de la v1
Estos elementos se descartan explícitamente. Mencionarlos evita que aparezcan en el camino.
- Modo fuera de línea completo. La aplicación muestra los últimos datos conocidos, pero no reproduce los comandos emitidos fuera de la red. Solo los eventos de presencia se ponen en cola.
- Aplicación Apple Watch y Wear OS. Los widgets y accesos directos del sistema cubren la mayor parte de las necesidades a menor costo.
- Configuración de integraciones complejas desde móvil: emparejamiento Zigbee, Matter, Z-Wave. Consultables, pero configurados desde la web.
- Rediseño visual. La aplicación retoma la interfaz actual. Un rediseño mobile-first es un proyecto aparte.
- Soporte para Android 7 y anteriores, y para iOS 14 y anteriores.
- Reparación del service worker web. El comportamiento actual se mantiene; la mejora de la PWA sigue siendo un tema aparte.
Preguntas a resolver antes del inicio
- ¿El relay de notificaciones pasa por la infraestructura Gladys Plus existente, o se necesita un servicio dedicado? Esto condiciona el lote 3.
- ¿Las cuentas de desarrollador están abiertas a nombre del proyecto Gladys o a título personal? Esto tiene consecuencias duraderas en la propiedad de las fichas.
- ¿La aplicación está reservada para los suscriptores de Gladys Plus para las notificaciones, o el repliegue local es suficiente para la promesa hecha a los usuarios?
- ¿Hay una máquina macOS disponible para la CI de iOS, o hay que prever un servicio de compilación alojado?
Especificación establecida a partir del repositorio Gladys 5.0.2: front Preact / Vite (front/), servidor Node 24 (server/, 39 servicios, 25 controladores API). Las rutas de archivos citadas corresponden a la estructura real del repositorio en la fecha de redacción.
