ROBUST PICKLEBALL
plano a-002026 · escala 1:1
Icono de la app: los dos arcos rojos de marca sobre negro #121212.

El historialdura años

El entrenador dicta 90 segundos al bajar de la cancha; el alumno recibe un historial de desarrollo que dura años.

App móvil deportiva · Plataforma de desarrollo de jugadores2026

rolDesarrollo integral: arquitectura Flutter, backend serverless en Firebase, canalización de IA con Gemini, reglas de seguridad, sistema de diseño y localización

estadoEn desarrollo activo con cliente real (Cliff, primer coach de la plataforma). Firebase en plan Blaze y funciones desplegadas; todavía sin publicar en tiendas.

24

pantallas

23

Cloud Functions

87

idiomas

28

habilidades en 4 categorías

a-01 / Guía y trazo

De la nota suelta a la línea medida

Un plano empieza con la guía punteada y termina con el trazo firme. El producto hace lo mismo con lo que el entrenador dice de viva voz.

línea de guía

Lo que no existía

Un entrenador de pickleball trabaja con decenas de alumnos y, al llegar a la cancha, no puede responder en diez segundos a la pregunta que importa: ¿qué dijimos que trabajaríamos hoy? Los apuntes viven en notas de voz sueltas, en papel o en la memoria; el alumno se va con la sensación de la clase pero sin ninguna prueba de que esté mejorando; y los porcentajes que el coach dicta de viva voz («sacó 80, 90, 75») no se convierten nunca en una serie de datos comparable. Escribir un reporte formal por alumno y por clase es un trabajo que nadie hace de forma sostenida, así que el historial —que es lo único que demuestra progreso— sencillamente no existe.

trazo definitivo

Lo que se construyó

Convertir el dictado en el único trabajo que el coach tiene que hacer, y poner toda la disciplina en el servidor. La app graba el audio, lo persiste en disco antes de nada y lo encola: si no hay señal en la cancha, se sube solo al recuperarla. Una Cloud Function se lo pasa a Gemini 2.5 Flash como URI de Storage y devuelve un borrador estructurado que el coach revisa y publica. Al publicar, el servidor construye con lista blanca una proyección para el alumno, recalcula su Player DNA a partir de conteos reales y programa el recordatorio de tareas. Todo lo que el modelo no pueda contrastar contra la transcripción literal se retira con aviso en lugar de guardarse: un hueco visible se arregla en diez segundos, un número inventado ya no se distingue de una medición.

a-02 / El ciclo

Cuatro pasos y ni uno más

El único trabajo del coach es hablar noventa segundos. Toda la disciplina vive en el servidor.

01

Dicta

30 a 90 segundos al bajar de la cancha. El audio se escribe en disco y se encola antes de tocar la red.

02

Estructura

Gemini 2.5 Flash recibe el URI de Storage y devuelve ejercicios, series contadas, tareas y claves.

03

Revisa y publica

El coach corrige sobre la transcripción literal y publica con un toque. La IA nunca cambia el estado.

04

El alumno recibe

El servidor arma con lista blanca su proyección, recalcula el Player DNA y programa el recordatorio.

a-03 / Piezas

Seis decisiones que sostienen el historial

Dictado que sobrevive a la cancha sin señal

El audio se escribe en disco y se encola en SharedPreferences antes de tocar la red, así que sobrevive a que se cierre la app y se vacía solo al recuperar conexión. Firebase se resuelve de forma perezosa: grabar y encolar no dependen de que su arranque haya funcionado.

Asistente conversacional con cuaderno acumulado

Cuatro callables (openAiSession, appendCoachTurn, finalizeAiSession, abandonAiSession) mantienen un cuaderno en JSON que viaja en cada turno junto al audio nuevo. No se reenvía el historial de audios: Gemini cuenta el audio a unos 32 tokens por segundo y reenviarlo crecería de forma cuadrática.

La omisión nunca borra

El modelo devuelve el cuaderno entero y una función pura lo reconcilia: solo puede añadir, corregir con justificación o eliminar con justificación. La justificación se contrasta con la transcripción del mismo turno y, ante la duda, se revierte y se anota en countDrift.

Player DNA sobre conteos, no sobre opiniones

Cuatro categorías y 28 habilidades. Una habilidad que ningún ejercicio mide se marca como no medida en vez de rellenarse con un cero, cada categoría declara su cobertura («1 de 5 medidas») y con menos de tres mediciones no se declara tendencia.

El alumno y el coach leen colecciones distintas

El jugador nunca toca /orgs/**: lee su propio player_space, una proyección construida con lista blanca que solo escriben las Cloud Functions. Las notas privadas del coach viven en una subcolección a la que ninguna regla puede evaluarse a favor del alumno, y hay pruebas de reglas contra el emulador que lo verifican.

87 idiomas, incluidos los avisos por push y correo

Las cadenas se generan desde los catálogos ARB hacia un JSON que consumen las funciones, con los plurales resueltos por Intl.PluralRules. El glosario de pickleball (dink, third shot drop, reset) no se traduce nunca, y las fuentes van empaquetadas para que la app no aparezca en Roboto al abrirla sin cobertura.

a-04 / Dos lados de la red

Nada cruza salvo una flecha

El alumno nunca toca /orgs/**: lee su propio player_space. El puente entre los dos lados lo cruzan únicamente las Cloud Functions.

lado a

Área del entrenador

/orgs/{orgId}/**

  • Panel con avisos, cola de trabajo y quién entra hoy
  • Alumnos, fichas y notas privadas del entrenador
  • Clases, borradores y publicación manual
  • Estudio de dictado y asistente conversacional
  • Court ID: la batería versionada de diez ejercicios

lado b

Área del alumno

/player_space/{uid}/**

  • Panel propio, sin nada de la organización
  • Player DNA con cobertura declarada
  • Progreso sobre bolas contadas
  • Historial de clases que se acumula
  • Tareas con id estable y su respuesta al coach
Cloud Functions

Proyección con lista blanca, feedback espejado y notas privadas que ninguna regla puede evaluar a favor del alumno.

a-05 / Cotas

Lo que no se inventa

Un hueco visible se arregla en diez segundos; un número inventado ya no se distingue de una medición.

01

La IA nunca publica

La publicación del reporte es manual. El modelo propone un borrador; el estado de la clase sólo lo cambia una persona.

02

La omisión nunca borra

El servidor recibe el cuaderno entero y sólo acepta añadir, corregir con justificación o eliminar con justificación. Lo omitido se restituye y se anota donde el coach lo ve.

03

Dos unidades que no se convierten

Bolas contadas (made/attempted) y puntuación 0-100 por serie viven separadas. Ningún denominador se deduce: o lo dijo el coach o es el tamaño de serie del catálogo.

04

El dominio es consistencia

Una serie hundida deja el ejercicio en ámbar aunque el promedio sea alto. Y con menos de tres mediciones no se declara tendencia.

a-06 / Pantallas

Tres pantallas del mismo plano

La app prioriza el tema claro: lienzo #F7F7F8, tarjetas blancas y el rojo de marca como único acento. Aquí van recreadas.

s-02móvil
Sin conexión. Los apuntes se guardan y se suben solos.

Asistente

Clase 12 · Lillian

Terminar
Esto no publica nada. Al cerrar, los apuntes pasan al borrador.
Cuaderno3 turnos

Resumen

Dinks cruzados y tercer golpe. Lillian sube la altura del reset.

Ejercicios

Dink cruzado sostenido8/109/107/10
Third shot drop6/107/10
Se restituyó «Reset desde el fondo»: countDrift. La omisión no borra.

Tareas

50 dinks cruzados antes del jueves
Grabar 3 saques desde la derecha

Claves

· Pala arriba antes del bote

«sacó 80, 90, 75, 48 y 90»

0:41

¿De cuántas bolas era cada serie? No la supongo.

0:41Dictar en su lugar

Estudio · asistente de IA conversacional

anillo del micrófono · bucle de 1600 ms

s-03móvil

Tu ADN de juego

Un mapa de lo que hemos medido en tus clases. Lo que no se ha medido no se rellena con un cero.

77%
Técnica
54%
Táctica
41%
Movimiento
Mental

Mental: todavía no lo hemos medido

Técnica1 de 5 medidas
Dink cruzadofuerte

empezaste en 60%, vas por 77%

Third shot dropen desarrollo

empezaste en 48%, vas por 54%

Reset desde el fondoprioridad

empezaste en 45%, vas por 41%

Volea de revéstodavía no lo hemos medido
Saque profundotodavía no lo hemos medido

Player DNA del alumno

cuatro arcos · cobertura declarada

s-04móvil

Court ID · Lillian

6/10
01Dink cruzado sostenidobolas contadas
89798
02Third shot dropbolas contadas
89389

Una serie hundida deja el ejercicio en ámbar: el criterio es consistencia, no promedio.

03Saque profundopuntuación

Sin teclado: 21 casillas de cinco en cinco.

05101520253035404550556065707580859095100

Compuesto objetivo

72/100

Nivel 3.5 · provisional

La traducción a nivel es provisional. Autoguardado a los 3 s del último conteo.

Court ID · ejecución de la evaluación

rejilla de conteo · 21 casillas, sin teclado

s-01

Panel del entrenador

Panel ordenado por urgencia y no por estética: aviso, cola de trabajo, quién entra hoy, marcador en vivo y seis gráficas servidas desde un único documento agregado.

s-05

Reporte del alumno

El destino de la notificación push: mensaje del entrenador, lo que salió bien, en qué seguimos, ejercicios contados, tareas marcables y el bloque donde el alumno responde.

a-07 / Hoja de marca

Dos arcos y un solo rojo

El símbolo es la trayectoria de una bola y su rebote. Cada capa del icono está cortada para su destino: adaptativo de Android, monocromo tematizado, oscuro y teñido de iOS 18.

Icono de la app: los dos arcos rojos de marca sobre negro #121212.
Icono de la app: los dos arcos rojos de marca sobre negro #121212.
Variante clara del icono, para fondos blancos.
Variante clara del icono, para fondos blancos.
Símbolo vectorial de ROBUST: dos arcos en movimiento, con currentColor para teñirlo desde la app.
Símbolo vectorial de ROBUST: dos arcos en movimiento, con currentColor para teñirlo desde la app.
Símbolo en rojo de marca sobre fondo transparente.
Símbolo en rojo de marca sobre fondo transparente.
Capa monocroma del icono para los iconos tematizados de Android 13+.
Capa monocroma del icono para los iconos tematizados de Android 13+.
Capa de primer plano del icono adaptativo de Android.
Capa de primer plano del icono adaptativo de Android.
Variante oscura del icono para iOS 18.
Variante oscura del icono para iOS 18.
Variante teñida en escala de grises del icono para iOS 18.
Variante teñida en escala de grises del icono para iOS 18.
#F5361Bbrand · relleno y acento
#B31F0CbrandDeep · texto sobre claro
#FF7A1AbrandEmber · sólo extremo del gradiente
#0D0D0Fcanvas · lienzo oscuro

Atlético y contundente. Negro casi absoluto, un rojo-naranja de alta energía y tipografía display condensada (Saira) sobre cuerpo neutro (Inter). El símbolo son dos arcos en movimiento, como la trayectoria de una bola y su rebote. El gradiente empieza en el rojo hundido —la esquina donde va el titular blanco— y termina en el ámbar, que nunca lleva texto encima. La identidad es nativa de fondo oscuro, pero el producto prioriza el tema claro, así que el rojo tiene variantes medidas para cada fondo.

a-08 / Especificación

Índice de lo que ya funciona

22 partidashoja de especificación
01Dictado de voz de 30-90 segundos con cola durable en disco y subida automática al recuperar señal
02Asistente de IA conversacional por turnos con cuaderno acumulado, tope duro de tamaño y reapertura sobre conversaciones cerradas
03Estructuración automática del reporte con Gemini 2.5 Flash: ejercicios, series, tareas, claves y hallazgos
04Transcripción literal mostrada al coach para que un error de transcripción no se vuelva un dato falso
05Publicación manual del reporte: la IA nunca cambia el estado de la clase
06Panel del entrenador con avisos, cola de trabajo, quién entra hoy, marcador en vivo y seis gráficas servidas desde un único documento agregado
07Briefing pre-clase que responde en diez segundos a qué se trabajó la vez anterior, servido desde caché y sin consultas nuevas
08Court ID: batería de evaluación versionada de diez ejercicios fijos, con compuesto objetivo y bandas marcadas como provisionales en pantalla
09Player DNA con cuatro arcos de categoría, cobertura declarada y línea de progreso sobre bolas contadas
10Dos unidades de medida que nunca se convierten entre sí: bolas contadas (made/attempted) y puntuación 0-100 por serie
11Criterio de dominio por consistencia y no por promedio
12Área del alumno con panel, progreso, historial de clases y tareas con id estable
13Feedback del alumno espejado al lado del coach por Cloud Function, sin darle acceso a la organización
14Reporte en PDF generado en el cliente, compartible, con fuentes Noto para árabe y hebreo incrustadas
15Notificaciones push y correo de respaldo en el idioma del destinatario, sin que el contenido del reporte viaje nunca en el aviso
16Recordatorio programado de tareas pendientes a las 48 horas, con índice de un solo campo que solo existe mientras hay aviso pendiente
17Acceso sin contraseña: código de seis dígitos por correo, Google, Apple y código de invitación
18Vinculación y desvinculación de fichas de alumno con plan puro y probado, más borrado de cuenta irreversible con traspaso o cierre de organización
19Modo activo (entrenador / alumno) que decide la navegación y admite que una misma persona sea las dos cosas
20Modo sin conexión con persistencia de Firestore sin límite, banner no bloqueante y escrituras que se reintentan solas
21Tema claro y oscuro con contraste verificado por pruebas automáticas
22Control de gasto: cuota diaria por organización contada en servidor, cuota de tokens, idempotencia, maxInstances e interruptor de apagado de la IA
a-09 / Materiales

Con qué está levantado

01Cliente
Flutter 3.35+Dart SDK 3.10Material 3flutter_localizationsintl
02Estado y navegación
flutter_riverpodgo_router (StatefulShellRoute)collectionuuid
03Firebase
firebase_corefirebase_authcloud_firestorefirebase_storagefirebase_messagingcloud_functionsfirebase_app_checkfirebase_analyticsfirebase_crashlyticsfirebase_remote_config
04Backend serverless
Cloud Functions gen2 sobre Node 22TypeScript 5.9firebase-adminfirebase-functionsgoogle-auth-libraryFirestore (nam5)Cloud Storage con reglas de ciclo de vida
05IA
Vertex AIGemini 2.5 Flash (endpoint global)Salida con esquema JSONContabilidad de thoughtsTokenCount
06Autenticación
google_sign_insign_in_with_appleCódigo de seis dígitos por correo con custom tokens
07Medios y dispositivo
recordjust_audioimage_pickerflutter_image_compresspermission_handlerconnectivity_pluspath_providershared_preferencespackage_info_plus
08UI, marca y datos
flutter_svgflutter_animatecached_network_imagefl_chartFuentes variables Saira e Inter empaquetadas
09Reportes
pdfprintingshare_plusNoto Sans Arabic y Noto Sans Hebrew incrustadas en el PDF
10Calidad y herramientas
flutter_testflutter_lintsnode:testFirebase Emulator Suite (Auth, Firestore, Storage, Functions)Pruebas de firestore.rules contra el emuladorflutter_launcher_iconsflutter_native_splashScripts Python para catálogos, marca y traducciones
a-10 / Esquema general

Cómo está levantado por dentro

e-01clean architecture · 11 features · 23 functions

Clean Architecture por feature: cada uno de los 11 módulos de lib/features/ se abre en domain (entidades y contratos de repositorio), infrastructure (implementaciones sobre Firebase) y presentation (providers, controllers, screens y widgets). Los imports son siempre por paquete y no hay barrel files; tampoco freezed ni json_serializable, porque copyWith y fromJson se escriben a mano para que el modelo de datos se pueda leer entero sin generación. La configuración transversal vive en lib/config (tema y router) y lib/core (i18n, catálogo de habilidades, preferencias, analítica, errores). El router es un go_router con dos StatefulShellRoute —una por cada área, entrenador y alumno— más las rutas neutras de acceso, y su redirect se decide por el modo activo y no por el rol, esperando a que los vínculos del alumno estén resueltos antes de mandar a nadie a ninguna parte. En el servidor, 23 funciones organizadas por dominio (auth, orgs, sessions, ai, dna, feedback, notifications, stats, account) mezclan callables, triggers de Firestore, un trigger de Storage y dos tareas programadas; el catálogo de habilidades, ejercicios y batería vive en shared/ como fuente única y se copia hacia las funciones en el prebuild. Las decisiones delicadas —el plan de borrado de cuenta, el de desvinculación, la reconciliación del cuaderno de la IA, el plegado de estadísticas y la regla del recordatorio— están extraídas a funciones puras y probadas aparte de la función que ejecuta.

incidencias7
incidencia 01

El modelo inventaba denominadores. Como el coach dicta porcentajes con naturalidad («sacó 80, 90, 75, 48 y 90»), la suposición de series de diez bolas era imposible con un 80, así que la IA elegía el denominador que cuadraba —100— y guardaba quinientas bolas que nadie golpeó, con la marca de descarte vacía. El dato era coherente; el problema no era la coherencia.

corrección

Dos unidades separadas y un contraste obligatorio. La suposición de las diez bolas solo vale si todos los números de la serie caben en diez, y el servidor coteja cada denominador contra la transcripción literal: o lo dijo el coach en ese mismo turno, o es el tamaño de serie del catálogo. Lo demás se retira y se anota en countDrift. En la conversación el modelo pregunta por el tamaño de serie en vez de adivinarlo; en el dictado, que es una sola pasada, el ejercicio se nombra sin conteos. El caso real y sus regresiones viven en functions/test/invented_counts.test.ts.

incidencia 02

En el asistente conversacional, que el modelo dejara fuera del cuaderno algo que había apuntado en un turno anterior equivalía a borrarlo. Con cuarenta turnos, además, un modelo desbocado producía 6,3 MB y la evaluación entera dejaba de poder escribirse en un documento de 1 MiB.

corrección

La reconciliación se sacó a una función pura (ai/merge.ts): el servidor recibe el cuaderno completo y solo acepta añadir, corregir con justificación o eliminar con justificación, restituyendo lo omitido y anotándolo donde el coach lo ve. Se añadieron topes duros por campo, y al recortar se descarta siempre lo nuevo: si el tope pudiera tirar lo anterior, sería una vía por la que la omisión sí borra.

incidencia 03

El destino de navegación se decidía por el rol, y eso expulsaba a un entrenador de su propia área de alumno. Peor: los vínculos del alumno llegan por un stream de Firestore, así que entre resolver la sesión y recibir el espacio había un hueco en el que un alumno de dos años parecía no tener área, y se le mandaba a la pantalla de conectar con su entrenador con su panel al otro lado y ningún botón que llevara.

corrección

Un modo activo explícito, porque una misma persona puede entrenar a unos y ser alumna de otro. Y una bandera de resolución en el estado del guard: sin modo y sin resolver todavía, el splash se queda un momento más en vez de decidir. Al entrenador no le afecta, porque su rol viaja en los custom claims.

incidencia 04

Las fichas de alumno se volvían invisibles para la búsqueda de invitaciones pendientes y el alumno entraba a una app vacía sin ningún error que lo explicara.

corrección

Firestore no distingue campo ausente de campo null a favor de quien consulta: una consulta por igualdad a null solo encuentra documentos donde el campo existe. Se convirtió en regla del proyecto escribir siempre los campos que se consultan así, aunque valgan null, con una prueba que hace la consulta de verdad contra el emulador. La regla inversa se aplica al recordatorio de tareas, donde la consulta es por rango y el campo se borra en lugar de anularse.

incidencia 05

Vincular una ficha a una cuenta era un viaje de ida: un correo mal escrito o un alumno que entró con otra cuenta de Google dejaban la ficha atada para siempre, y la única salida era que el alumno borrara su cuenta entera. Quien se vinculaba por error a la ficha de otro se quedaba además con el historial de un tercero.

corrección

Una operación de desvinculación con su decisión en una función pura y probada aparte. Retira del espacio del alumno los reportes ya entregados de ese vínculo —retirar no destruye, porque el reporte del alumno es una copia y se repone al volver a vincular—, borra los códigos de invitación vivos, retira el correo solo si era el de esa cuenta, y no toca nada del historial del entrenador. Todo idempotente y con registro de quién, cuándo y sobre qué ficha.

incidencia 06

En tema oscuro, la elevación clásica —aclarar la tarjeta— hundía el rojo de marca por debajo del mínimo de contraste de WCAG AA, y el color de error se pintaba como texto en seis pantallas justo en el rojo menos legible del tema. Ninguna prueba lo veía porque ese token no se miraba.

corrección

La elevación se consigue hundiendo el lienzo por debajo de la tarjeta en lugar de aclararla, lo que además mejora todos los pares de contraste a la vez. La paleta se partió en cuatro rojos con papeles distintos —marca para relleno, uno oscurecido para texto sobre claro, uno encendido para texto sobre oscuro y uno hundido para estados presionados— y un test de contraste vigila cada par.

incidencia 07

El proyecto está en plan Blaze, que no tiene techo de gasto, y cada turno del asistente puede llevar nueve minutos de audio. Contar llamadas no es contar gasto.

corrección

El guardián real es la cuota de tokens por organización y día, contada en servidor e incluyendo los tokens de razonamiento que Gemini reporta aparte de la salida. Encima: cuota diaria de dictados, idempotencia por turno y por generación, maxInstances por función, borrado de audios a los siete días por ciclo de vida de Storage, interruptores de apagado que cortan la IA al instante sin desplegar nada, y alerta de presupuesto con avisos escalonados.

a-11mediciones del repositorio

24

pantallas

11

módulos de features

23

Cloud Functions

87

idiomas

17

colecciones Firestore

28

habilidades en 4 categorías

16

ejercicios en el catálogo

10

pruebas de la batería Court ID

93

archivos de prueba

~40k

líneas de Dart

memoria del proyecto

ROBUST PICKLEBALL es una plataforma de desarrollo de jugadores construida alrededor de una idea que el propio repositorio repite como norma: el producto no es el reporte de una clase, es el historial del jugador. Una clase dura una hora; el historial debe durar años.

El ciclo completo cabe en cuatro pasos. El coach termina la clase y dicta un resumen de 30 a 90 segundos (o conversa por turnos con un asistente que va llenando un cuaderno). Gemini 2.5 Flash lo estructura en ejercicios, series contadas, tareas y claves de entrenamiento. El coach revisa y publica con un toque. El alumno recibe un reporte que se acumula en su propio espacio, con su Player DNA, su línea de progreso y sus tareas.

La app es una sola aplicación Flutter con dos productos dentro: el área del entrenador (panel, alumnos, clases, estudio de dictado, evaluaciones Court ID) y el área del alumno (panel, progreso, historial, tareas). Cada lado lee colecciones de Firestore distintas y el puente entre ambos lo cruzan únicamente las Cloud Functions.

Es multi-tenant desde el primer día: ROBUST es la plataforma y el primer entrenador es solo la primera organización. Se traduce a 87 idiomas, funciona sin cobertura en la cancha y genera el PDF del reporte en el propio teléfono.

Multi-tenant desde el día unoSin cobertura en la canchaPDF en el propio teléfono
dinkthird shot dropresetkitchenplayer DNAcourt IDcount driftel glosario no se traducedinkthird shot dropresetkitchenplayer DNAcourt IDcount driftel glosario no se traduce

ROBUST PICKLEBALL

¿Quieres algo así para tu producto?

Una app Flutter con dos productos dentro, veintitrés Cloud Functions y una IA a la que no se le deja inventar un solo número. Si necesitas algo así —voz que se vuelve dato, permisos que de verdad separan, gasto acotado— es exactamente el terreno que conozco.

fin del plano · ROBUST PICKLEBALL