Futbol Veritabanı
8대 리그 클럽·선수·통계—Transfermarkt 데이터
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Garmin — HOY • Pasos, calorías, distancia, minutos activos, pisos • FC reposo, mínima, máxima + timeline por hora (248+ mediciones) • Sueño: fases (profundo, ligero, REM, despierto), score, horarios • Estrés: nivel medio, máximo + timeline por hora • Body Battery: máximo, mínimo + curva del día • HRV: media semanal, última noche, estado • SpO₂: media, mínimo (si el reloj lo soporta) • Respiración: media, máxima, mínimaREADME 전문 보기 ↗
mi-salud-final/
├── api/ ← Backend (despliega en Vercel)
│ ├── _lib/
│ │ ├── garmin_client.py ← Garmin Connect (CORREGIDO con test real)
│ │ ├── strava_client.py ← Strava API (actividades, streams, zonas)
│ │ ├── metrics.py ← CTL/ATL/TSB, ACWR, predicciones, readiness
│ │ ├── analytics.py ← Volumen semanal, calendario, récords, tendencias
│ │ ├── coach_context.py ← Dossier del atleta para el entrenador IA
│ │ ├── archive.py ← Ficha diaria: guarda el día en el histórico
│ │ ├── races.py ← Carreras de Carreiras Galegas, apuntados y novedades
│ │ └── cache.py ← Upstash Redis
│ ├── sync-garmin.py ← Endpoint: descargar datos Garmin
│ ├── sync-strava.py ← Endpoint: descargar datos Strava
│ ├── sync-all.py ← Endpoint: los tres pasos seguidos (lo llama el cron)
│ ├── calculate.py ← Endpoint: calcular métricas
│ ├── history.py ← Endpoint: leer el archivo diario
│ ├── dashboard.py ← Endpoint: leer todo (instantáneo)
│ ├── widget.py ← Endpoint: resumen mínimo para el widget de Android
│ ├── activity.py ← Endpoint: detalle + streams de una actividad
│ ├── day.py ← Endpoint: datos de Garmin de un día concreto
│ ├── plan.py ← Endpoint: próximos entrenos de Garmin Coach / calendario
│ │ (también sirve /api/compliance: pedido vs. hecho,
│ │ y /api/races: carreras de Carreiras Galegas)
│ ├── login.py ← Endpoint: inicio de sesión de la app (cookie)
│ └── coach.py ← Endpoint: entrenador IA (Claude)
├── public/index.html ← La app (React sin compilar, se sirve tal cual)
├── tests/ ← Tests (pytest), se pasan en GitHub Actions
├── vercel.json
├── requirements.txt
└── README.md
Fitness/Fatiga/Forma (CTL/ATL/TSB)
Medias exponenciales del Relative Effort de Strava: CTL a 42 días, ATL a 7, TSB = CTL − ATL. La simulación arranca en la actividad más antigua que haya (tope 400 días) y siembra ambas con la carga media de las dos primeras semanas. Con menos de 42 días de historial el CTL no ha convergido y la app lo avisa bajo la gráfica: el Fitness aún se está asentando.
Forma negativa = la última semana ha cargado más que tu media de 6 semanas. Es lo normal entrenando; se busca que suba a positivo antes de competir.
Ratio Agudo:Crónico (riesgo lesión)
Las tres cargas no son la misma cosa y nunca deben compararse entre sí:
| Cifra | Ventana | Escala |
|---|---|---|
| Carga aguda / crónica (ACWR) | Móvil: hoy-6→hoy y media de 28 días | Relative Effort de Strava |
| Barra de «Volumen por semana» | Semana natural lun→dom, la última EN CURSO | Relative Effort de Strava |
| «Carga» del Estado de entreno | Últimos 7 días | Escala propia de Garmin (EPOC) |
Un viernes con 202 en la barra de la semana y 400 de carga aguda es correcto: la barra lleva 5 días (lun→vie) y la aguda lleva 7 (incluye el sábado y el domingo anteriores). La app ahora etiqueta ambas con su ventana y marca la semana en curso con los días que lleva.
La media de 4 semanas se calcula solo con semanas completas: antes incluía la semana a medias, así que la referencia bajaba sola según avanzaba la semana.
Predicción carreras (5K, 10K, media, maratón)
El VO₂max que se muestra es el que mide Garmin corriendo. Solo si Garmin no lo da se recurre a la estimación desde el FTP, que es una fórmula de ciclismo y se queda muy corta para correr. Los tiempos en sí se calibran con tu mejor ritmo real de 5 km+, así que no dependen del VO₂max.
Training Readiness (compuesto)
Volumen por semana y por mes (16 semanas / 12 meses)
Calendario de días entrenados (12 semanas), racha y días de descanso
Reparto por deporte, por día de la semana y por franja horaria
Tendencias de ritmo (carrera) y velocidad (bici)
Récords: más larga, más desnivel, sesión más dura, ritmo más rápido
Antes los datos solo se refrescaban cuando abrías la app y pulsabas Sync. Eso dejaba dos agujeros:
garmin,
strava, metrics, meta) y cada sync las machacaba. Todo lo que la app
enseña «del pasado» sale de las ventanas móviles que devuelven Garmin y
Strava en ese momento: 7 días de sueño, HRV y estrés, 8 semanas de FC en
reposo, 200 actividades. En cuanto un dato salía de esa ventana,
desaparecía para siempre.vercel.json define un cron diario que llama a /api/sync-all:
"crons": [{ "path": "/api/sync-all", "schedule": "10 5 * * *" }]
/api/sync-all encadena los tres pasos (Garmin → Strava → cálculo) en una
sola llamada, porque un cron de Vercel apunta a una URL y no puede encadenar
tres. El botón Sync de la app sigue llamando a los tres endpoints por separado:
así enseña el progreso paso a paso y cada petición tiene su propio presupuesto
de tiempo.
status (ok / partial / error) y failed.?steps=garmin,strava limita qué pasos corren. Los tres caben de sobra en
los 60 s del plan Hobby, pero si algún día no cupieran se puede partir en
varios crons sin tocar código.Ojo con el plan de Vercel. En Hobby cada cron dispara una vez al día y
maxDuration no puede pasar de 60 s (por eso está en 60 y no más). En Pro
puedes subir la frecuencia a varias veces al día — 0 */6 * * * para cada seis
horas — y es lo recomendable para que el widget esté siempre al día.
/api/sync-all dispara un login de Garmin y la descarga de 200 actividades, y
la URL de Vercel es pública. Define CRON_SECRET y exigirá
Authorization: Bearer <secreto>, que es justo la cabecera que manda Vercel
Cron por su cuenta. Sin esa variable el endpoint queda abierto.
Cada cálculo guarda una ficha compacta del día en day:YYYY-MM-DD, más un
índice en day:index con las fechas que hay. Lleva los pasos, calorías,
distancia, FC en reposo/mín/máx, estrés, Body Battery, sueño con sus fases,
HRV, SpO₂, respiración, y además el CTL/ATL/TSB, la readiness con su desglose
y el ACWR de ese día.
Es compacta a propósito: la foto completa lleva la curva de FC del día (248+ mediciones) y no tiene sentido multiplicarla por 365.
Dos cosas que hace y que no son obvias:
steps_week, hrv_week, …) traen los 7 últimos días con su fecha, así que
se aprovechan para completar hacia atrás. De paso tapa los huecos de los días
que no sincronizaste.La readiness y el ACWR solo se guardan del día en que se calcularon — no se inventan hacia atrás. El CTL/ATL/TSB de días pasados sí, porque sale del timeline que Strava permite recalcular entero.
GET /api/history?days=90 # fichas completas
GET /api/history?days=180&field=readiness.score # una sola serie, aplanada
GET /api/history?days=90&field=daily.resting_hr
Devuelve también total_archived, first y last, para saber cuánto archivo
hay acumulado. Cuanto más tiempo lleve el cron corriendo, más largo es — y a
diferencia del resto de la app, esto no se puede reconstruir: si no se
guardó en su día, no está.
| Pestaña | Qué tiene |
|---|---|
| Resumen | Anillos del día, sueño, FC, estrés, Body Battery, tendencias de la semana |
| Forma | CTL/ATL/TSB, carga aguda/crónica con su ventana, predicción de carreras, training readiness, volumen |
| Entrenos | Gráficas de volumen, desnivel, reparto por deporte, cuándo entrenas, tendencia de ritmo/velocidad, récords, calendario de entrenos e historial |
| Coach | Copia tu informe para pegarlo en la app de Claude, o chatea aquí mismo si activas la API |
Al tocar una actividad se abre el detalle con el mapa, la evolución durante la sesión (FC, ritmo, altitud, cadencia, potencia), el tiempo en cada zona de FC, los parciales por km y los mejores esfuerzos. Desde ahí puedes pulsar «Preguntar al entrenador sobre esta sesión» y el chat arranca con esa actividad como contexto (parciales y zonas incluidos).
Hay dos formas de usarlo, y no son excluyentes.
La pestaña Coach tiene un botón «Copiar informe»: genera un texto con las instrucciones de entrenador + todo tu dossier y lo deja en el portapapeles. Lo pegas en una conversación nueva de la app de Claude (la de tu suscripción) y ya te responde como entrenador con tus datos delante.
GET /api/coach?dossier=1 es lo que devuelve ese texto. No necesita
ANTHROPIC_API_KEY, así que funciona sin gastar nada de API.Si defines ANTHROPIC_API_KEY, la pestaña Coach añade un chat completo.
POST /api/coach monta un dossier con todo lo que hay en caché — perfil, datos
de Garmin de hoy, tendencias de la semana, CTL/ATL/TSB, ACWR, readiness,
volumen, calendario, récords y las últimas 40 actividades — y se lo pasa a
Claude junto con la conversación.
cache_control, así que las preguntas siguientes de
la misma conversación reutilizan el contexto cacheado (más rápido y barato).localStorage), no en el servidor.COACH_MODEL (por defecto claude-sonnet-5,
que va sobrado para esto; pon claude-opus-5 si quieres análisis más finos).La app es privada pero la URL de Vercel es pública. Si defines APP_SECRET,
/api/coach exige la cabecera X-App-Secret tanto para el chat como para el
informe; la app te pedirá esa clave la primera vez y la recordará en el
navegador. Protege tu cuota de API y, sobre todo, tus datos de salud.
# Crear app en https://www.strava.com/settings/api
# Autorizar:
open "https://www.strava.com/oauth/authorize?client_id=TU_ID&response_type=code&redirect_uri=http://localhost&scope=read_all,activity:read_all,profile:read_all"
# Cambiar code por tokens:
curl -X POST https://www.strava.com/oauth/token \
-d client_id=TU_ID -d client_secret=TU_SECRET \
-d code=EL_CODE -d grant_type=authorization_code
pip install garminconnect
python test_garmin.py # (el script de test que ya tienes)
cd mi-salud-final
git init && git add . && git commit -m "Mi Salud v1"
git remote add origin https://github.com/TU_USER/mi-salud.git
git push -u origin main
GARMIN_EMAIL / GARMIN_PASSWORDSTRAVA_CLIENT_ID / STRAVA_CLIENT_SECRET / STRAVA_REFRESH_TOKENUPSTASH_REDIS_URL / UPSTASH_REDIS_TOKENANTHROPIC_API_KEY ← solo si quieres el chat dentro de la app (console.anthropic.com → API Keys). El botón «Copiar informe» funciona sin ella.APP_SECRET (opcional) ← contraseña de la app. Si no existe, la
contraseña es la de Garmin (GARMIN_PASSWORD). Todos los endpoints con
datos piden sesión; ver api/_lib/auth.py.CRON_SECRET (opcional) ← Vercel Cron la manda sola como
Authorization: Bearer …. Sin ella, /api/sync-all acepta llamadas sin
sesión pero como mucho una cada 30 minutos y sin devolver cifras.COACH_MODEL (opcional) ← por defecto claude-sonnet-5public/index.html es la app entera (React por CDN, sin compilar): Vercel la
sirve en la raíz del despliegue. La primera vez pide la contraseña (la de
Garmin, o APP_SECRET si la definiste) y guarda la sesión en una cookie.
Dos opciones:
A) PWA (sin compilar nada)
B) APK de Android — ver la sección siguiente.
En android/ hay una app nativa mínima: una WebView a pantalla completa que
abre el despliegue de Vercel. No duplica el frontend, así que cualquier cambio
en public/index.html aparece en el móvil sin recompilar la APK.
Qué añade sobre la PWA:
La APK trae un widget redimensionable (4x2 por defecto) que enseña, sin abrir la app: pasos del día con barra de progreso sobre tu objetivo, Body Battery, FC en reposo, sueño, readiness y una línea con la forma (fitness y TSB).
Lee de GET /api/widget, un endpoint nuevo que devuelve solo esos números
(un par de kB). El widget no puede usar /api/dashboard: ese devuelve el
volcado completo con 200 actividades y sus polylines, y no tiene sentido
bajárselo cada media hora desde la pantalla de inicio.
/api/widget no pide APP_SECRET, igual que /api/dashboard — el widget es
código nativo y no tiene acceso a la clave que guarda el navegador. Si algún
día proteges la API entera, este endpoint hay que tenerlo en cuenta.
La compila GitHub Actions — no hace falta Android Studio.
android/, y también a mano
con Run workflow (ahí puedes escribir otra URL de despliegue)..apk queda en dos sitios: como artifact del run y como
release (apk-v1.0.N), que es la cómoda para el móvil..apk.Por defecto apunta a https://mi-salud-nine.vercel.app. Para otra:
appUrl en android/app/build.gradle;Con el SDK de Android instalado:
cd android
./gradlew assembleRelease -PappUrl=https://mi-salud-nine.vercel.app
# app/build/outputs/apk/release/app-release.apk
La pestaña Carreras enseña el calendario por fines de semana o como calendario mensual. Marcas con el check las que te apuntas: la semana de la carrera sale un aviso en Resumen y en el widget, y el entrenador las tiene en cuenta. Las pruebas que aparecen entre una descarga y la siguiente salen como «Nueva» y la app de Android avisa con una notificación; el filtro Nuevas deja solo esas.
Cada carrera lleva su distancia en línea recta desde casa (Noia por defecto) y se puede filtrar por radio («a menos de 50 km de Noia»). La lista enseña también hasta cuándo hay inscripción (de CCNorte y, para las carreras de los próximos 60 días, de la ficha de Carreiras Galegas).
Cómo llegan los datos:
.github/workflows/carreras.yml ejecuta cada mañana scripts/carreras/scrape.py,
que lee www.carreirasgalegas.com/events (con un navegador sin ventana, es
una app Angular) y ccnorte.com (HTML normal, con distancias, hora, precio y
plazo de inscripción). Sitúa cada localidad en el mapa con Nominatim
(OpenStreetMap) y publica el resultado en carreras.json en la rama
datos, junto con geo.json, la caché de coordenadas (un solo commit, que
se reemplaza cada día; Vercel no la despliega).races del cron sync-all (o con
«Actualizar carreras» en la app), junta las carreras repetidas en las dos
webs y marca las nuevas.Para descargar en el momento: GitHub → Actions → Carreras → Run workflow.
No hace falta configurar ninguna clave. CARRERAS_DATA_URL permite leer el
JSON de otro sitio, y CASA_LAT, CASA_LON y CASA_NOMBRE cambian el punto
desde el que se miden las distancias.