IEM

Assistent Strava/Garmin

Codi privat
  • Portada pública de la consola, sense cap dada de l'atleta.
    Portada pública, deliberadament sense dades
  • Tauler amb volum, TRIMP setmanal, CTL, ATL, TSB, ACWR, monotonia i dies actius, cadascun amb la seva miniatura de tendència.
    Tauler de càrrega i forma, amb la tendència de cada mètrica
  • Pestanya de rendiment: preparació per a la cursa objectiu, prediccions i millors marques.
    Rendiment: preparació, prediccions i millors marques
  • Pestanya de càrrega amb l'evolució de la càrrega aguda i crònica al llarg de l'any.
    Evolució de la càrrega aguda i crònica
  • Calendari d'entrenament per setmanes, amb la sessió de cada dia, el planificat davant el fet i l'activitat enllaçada des de Strava o Garmin.
    Calendari d'entrenaments: el que estava planificat i el que s'ha fet
  • Vista d'operacions amb la cua de tasques, els errors, el registre, les execucions i els webhooks rebuts.
    Operacions: cua, errors, registre i webhooks

Worker de Cloudflare que rep els webhooks de Strava, compara l'activitat principal del dia amb el pla d'entrenament i hi afegeix el temps i les dades de cursa. Després genera amb IA el títol i el resum que publica a Strava.

  • Cues amb reintents i cua de missatges fallits, més cron horari i purga diària.
  • Tokens de Strava xifrats amb AES-GCM abans de desar-los a D1.
  • Tauler d'administració en React servit pel mateix Worker.
  • Importació d'arxius FIT de Garmin i streams d'activitat desats a R2.

El problema

Strava titula cada entrenament «Cursa de la tarda» i la descripció es queda buida, així que l'historial d'un any no diu res de com va anar cap d'aquells dies. Les dades per explicar-ho sí que hi són: el pla d'entrenament, la meteorologia d'aquella hora i, si l'activitat era una cursa, el seu web oficial. El que faltava era alguna cosa que els ajuntés sola, cada dia, sense recordar-se'n.

El meu paper

Tot: el Worker, l'esquema de D1, les cues, el tauler d'administració en React i les eines locals que importen els fitxers de Garmin.

Decisions

  1. El webhook encua i contesta; la feina va a part

    Strava dona dos segons per respondre a un webhook, i el que cal fer —llegir l'activitat, creuar-la amb el pla, demanar el clima, generar el text i tornar-lo a publicar— no hi cap. El webhook deixa una feina en una cua de Cloudflare i respon; la cua la processa amb reintents i el que esgota els reintents cau en una cua morta en lloc de desaparèixer. Sense aquesta segona cua, una fallada de l'API de tercers s'empassa el dia sense deixar rastre.

  2. Al model no li arriba on vius

    El punt d'inici d'una tirada que surt de casa és el domicili, i aquesta coordenada anava sencera al prompt d'un tercer i es quedava desada en clar a la taula de curses. Ara s'arrodoneix a dos decimals, que són uns 1,1 km: suficient per distingir dues curses amb el mateix nom en ciutats diferents, que és l'única cosa per a la qual hi era, i insuficient per assenyalar un portal. Els tokens de Strava, a més, van xifrats amb AES-GCM a la base.

  3. El model escriu la prosa; els números els posa el codi

    El títol i l'entradeta els genera un model, però el bloc de mètriques és determinista i es calcula a part. Un model que redacta bé també arrodoneix ritmes, inventa un desnivell plausible i ningú ho nota fins que algú ho compara amb el rellotge. Separar-los deixa que cada part faci el que sap, i que un canvi de model no mogui ni una xifra.

  4. Garmin s'importa des de fora, no des del Worker

    Garmin Connect no té una API oberta com la de Strava: cal iniciar sessió, passar el segon factor i descodificar fitxers FIT. Això viu en eines locals que llegeixen i envien al Worker, en lloc de ficar credencials de Garmin i un descodificador binari dins d'alguna cosa que corre sempre i de cara a internet. La superfície exposada es queda en el que de debò ha d'estar exposat.

Resultat

  • 197 commits entre abril i setembre de 2026, 387 fitxers de TypeScript i 36 migracions de D1: un servei que porta mesos publicant sol.
  • 40 fitxers de proves amb gairebé 11 000 línies, corrent dins del mateix runtime de Workers i no en un Node simulat.
  • Dues fonts de dades —Strava per webhook i Garmin per fitxer FIT— acaben al mateix historial, amb clima i cursa oficial quan n'hi ha.

Què canviaria

La coordenada del domicili va arribar a viatjar a un tercer abans que ningú es preguntés què s'estava enviant, i es va arreglar després, llegint el codi amb altres ulls. Aquesta pregunta —què surt d'aquí i on va— no es pot deixar per a una auditoria: va en el moment d'escriure la primera crida. I el projecte va créixer fins a les 62 000 línies per a una cosa que fa servir una persona; la meitat de les funcions del tauler les he obert dues vegades.

  • Cloudflare Workers
  • D1
  • Queues
  • R2
  • React
  • TypeScript
  • Hono
  • Vitest