尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Libreta digital personal de entrenamiento. Local-first, sin cuentas, sin backend y sin sincronización: los datos viven en SQLite dentro del dispositivo y la app funciona sin conexión. Pruébala: gymtrack.luixcalderon12-cloudfare.workers.dev — se instala desde el navegador y, una vez instalada, abre sin conexión. Lo que registres se queda en tu dispositivo: no viaja a ningún servidor porque no hay ninguno.查看完整 README ↗
Libreta digital personal de entrenamiento. Local-first, sin cuentas, sin backend y sin sincronización: los datos viven en SQLite dentro del dispositivo y la app funciona sin conexión.
Pruébala: gymtrack.luixcalderon12-cloudfare.workers.dev — se instala desde el navegador y, una vez instalada, abre sin conexión. Lo que registres se queda en tu dispositivo: no viaja a ningún servidor porque no hay ninguno.
El alcance funcional está definido en Gym_Track_V2_Alcance_Simplificado.md.
La regla para decidir qué entra: ¿mejora directamente la experiencia de una libreta de
entrenamiento? Si no, queda fuera.
npm install
npm run dev # http://localhost:5173
npm run build # genera dist/ (PWA lista para publicar)
npm run preview # sirve el build
npm run lint # chequeo de tipos
Los iconos de la PWA se regeneran con node scripts/generate-icons.mjs (sin dependencias).
#/)La pantalla de inicio. Muestra qué toca entrenar hoy según el día de la semana, y debajo el agua, el cardio y el peso corporal del día. Un único botón grande abre o continúa el entrenamiento.
#/rutina, #/rutina/:weekday)Los siete días de la semana, siempre presentes: la rutina se configura, no se crea. Cada día
admite cualquier cantidad de ejercicios, con nombre, series y repeticiones objetivo y notas.
Se editan, se eliminan y se reordenan arrastrando (@dnd-kit, pensado para el dedo). Un día puede
marcarse como descanso.
#/entrenar/:sessionId)El corazón de la app. Cada ejercicio muestra sus series registradas y una fila de entrada rápida con peso, repeticiones y RIR:
+ sin escribir nada repite la serie anterior, que es el caso más común.Desde el menú ⋯ de cada ejercicio, con dos opciones que hacen cosas distintas:
En ambos casos la sesión conserva el ejercicio realmente realizado, con una etiqueta que recuerda a cuál sustituyó.
Arranca solo al registrar una serie y se queda visible en cualquier pantalla, encima de la navegación, para que puedas consultar el historial entre series sin perderlo de vista. Tiene −30 s, +30 s, pausa y saltar. La duración por defecto y el aviso se configuran en Ajustes.
La cuenta atrás se calcula contra una marca absoluta (Date.now()), nunca acumulando ticks, así
que no se desfasa cuando el navegador ralentiza los temporizadores en segundo plano. El pitido se
programa por adelantado en el reloj del AudioContext, que sigue corriendo con la pantalla
apagada; con setInterval habría sonado tarde justo en el escenario real de uso. Se genera con la
Web Audio API, sin archivos de audio.
Limitación conocida: iOS Safari suspende el
AudioContextal pasar la app a segundo plano, así que allí el pitido suena al volver a la app. En Android (el objetivo del APK) llega puntual. Evitarlo requeriría notificaciones locales, fuera del alcance de V2.
#/historial, #/historial/sesion/:id, #/ejercicio/:nombre)Las sesiones anteriores con sus totales, y un buscador por ejercicio. La vista de un ejercicio reúne todas sus series agrupadas por día — el "¿cómo se compara con lo que hice antes?" que justifica el producto.
#/progreso)Métricas de los últimos 7 días, mejores pesos por ejercicio y gráficas. El volumen es
Σ (peso × repeticiones) de cada serie.
#/cardio), Agua (#/agua), Peso (#/peso)Independientes del entrenamiento de fuerza y registrables cualquier día, incluidos los de descanso. Cardio admite seis tipos y sus métricas (tiempo, distancia, pasos, ritmo, velocidad, inclinación, frecuencia cardíaca) más intervalos con totales acumulados. Agua tiene meta diaria y accesos rápidos. Peso calcula el IMC como referencia.
#/ajustes)Empieza por Perfil: altura y meta de agua, las dos constantes personales que la app usa para calcular (el IMC y el progreso de hidratación), con el IMC actual recalculado a partir del último peso registrado. No es una cuenta —no las hay— sino los ajustes que antes estaban dispersos: la altura sólo se podía tocar al registrar un peso y la meta de agua vivía en la pantalla de Agua.
Debajo: tema claro/oscuro, duración del descanso, estado de protección de los datos, exportar e importar la copia de seguridad, instalar la app y relanzar la guía.
Todas hechas a mano en SVG, sin librerías de gráficas: mantienen el bundle pequeño y la app funcionando sin conexión.
| Dónde | Gráfica |
|---|---|
#/ejercicio/:nombre | Línea del mejor peso por sesión, con selector a barras de volumen |
#/progreso | Barras de volumen (o series) por semana, últimas 8 |
#/peso | Línea de la evolución del peso corporal |
#/agua | Barras de los últimos 14 días con la línea de meta |
Usan sólo variables del tema, así que funcionan en claro y oscuro. Los tooltips responden al
toque, no sólo al ratón, y cada gráfica lleva role="img" con un resumen de la tendencia y
una tabla equivalente oculta: el dato nunca depende únicamente del tooltip.
La primera vez que se abre la app se lanza una guía de 15 pasos que recorre cada sección y explica cómo funciona. Usa driver.js, empaquetado en el bundle (nunca desde un CDN) y cargado de forma perezosa: sólo se descarga al abrirla.
localStorage, tanto si se completa como si se salta.MutationObserver; si
un elemento no existe —lo normal en una instalación vacía, que es justo cuando se lanza— la
parada se salta con elegancia en lugar de romper la guía.#/entrenar: abrir esa ruta crearía una sesión vacía y la guía
ensuciaría el historial en el primer arranque.| Pieza | Elección | Por qué |
|---|---|---|
| UI | React 19 + Vite + TypeScript | Camino directo a Capacitor y tipado sobre el modelo de datos |
| Datos | SQLite real vía SQLocal (WASM + OPFS) | El mismo SQL se reutiliza con el plugin nativo al empaquetar el APK |
| PWA | vite-plugin-pwa (Workbox, autoUpdate) | Precarga el bundle y el .wasm, así que la app abre sin conexión |
| Reordenar | @dnd-kit | Arrastre que funciona con el dedo en el gimnasio |
| Guía | driver.js | Ligero y empaquetable; carga perezosa |
| Gráficas | SVG propio | Sin dependencias, sin peso extra, coherente con el tema |
| Estilos | CSS propio con tokens | Tema oscuro y claro sin framework |
No hay router externo: src/lib/router.tsx es un router sobre el hash. Además de ser diminuto, es
lo que funciona sin configurar reescrituras en el servidor y dentro del WebView del APK.
src/
db/ client.ts (conexión) · schema.ts (migraciones) · un repositorio por dominio
lib/ router, fechas, formato es-ES, useQuery, tema, restTimer, install
components/ UI compartida · charts/ (gráficas) · tour/ (guía)
screens/ una pantalla por ruta
scripts/ generador de iconos PNG sin dependencias
Las migraciones son incrementales y viven en src/db/schema.ts; una migración publicada no se
edita, se agrega otra al final.
Tablas: routine_days, routine_exercises, workout_sessions, session_exercises,
workout_sets, cardio_sessions, cardio_intervals, water_records, body_measurements,
settings.
session_exercises es la única adición al modelo del documento y existe para resolver la
sustitución: al iniciar una sesión se copia el plan del día. Sobre esa copia, Solo hoy cambia
el nombre únicamente ahí, y Actualizar rutina cambia además routine_exercises. En ambos casos
workout_sets.exercise_name guarda el ejercicio realmente realizado, así que el historial no se
reescribe cuando la rutina cambia más adelante.
El Service Worker precarga el shell y el runtime de SQLite (~1,9 MB en total), de modo que la app abre sin red desde la segunda visita.
La base vive en OPFS, y el VFS de OPFS de SQLite sólo existe si el documento es
crossOriginIsolated. Eso exige dos cabeceras:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Sin ellas, SQLocal cae en silencio a una base en memoria (sólo deja un console.warn): la app
arranca, los datos parecen guardarse y desaparecen enteros en la siguiente recarga. En
desarrollo no se nota porque el plugin sqlocal/vite pone esas cabeceras en el servidor de Vite;
el fallo aparece únicamente en producción.
public/_headers las incluye. Si tu hosting no lee ese archivo, configúralas a mano. La app no
carga ningún recurso de otro origen, así que require-corp no rompe nada.
Como el fallo es silencioso y catastrófico, la app se niega a arrancar si detecta que la base
quedó en memoria (getDatabaseInfo().storageType === 'memory') y explica qué falta, en vez de
dejarte anotar un entrenamiento que se va a perder.
Para comprobarlo en cualquier despliegue, desde la consola del navegador:
self.crossOriginIsolated // debe ser true
// y debe existir el archivo:
for await (const [n] of (await navigator.storage.getDirectory()).entries()) console.log(n)
// → gymtrack.sqlite3
El navegador puede desalojar el almacenamiento de un sitio cuando al dispositivo le falta
espacio, y con él se iría la libreta entera. Por eso la app pide navigator.storage.persist() al
arrancar y muestra el estado en Ajustes (Protegidos / Sin proteger), con un botón para
reintentarlo. La decisión es del navegador: suele concederla si la app está instalada en la
pantalla de inicio o tras usarla varios días, y puede denegarla sin explicación — de ahí que el
estado se muestre en vez de darse por hecho.
Como no hay nube, la copia de seguridad es responsabilidad del usuario: Ajustes → Exportar
copia descarga un .json con todas las tablas e Importar copia lo restaura. Si se desinstala la
app o se borran los datos del navegador sin haber exportado, se pierden.
La copia es JSON ({ app, format, schema, exportedAt, tables }) para que se pueda leer y
aprovechar fuera de la app. Las copias .sqlite3 de versiones anteriores se siguen importando.
Copia diaria. Una PWA no puede escribir archivos en el teléfono sin un toque del usuario, y lo que guarde por dentro se va con ella al desinstalarla, así que la copia no puede ser automática. En su lugar, Hoy la propone una vez al día, sólo si hay datos nuevos desde la última, con Descargar copia / Hoy no. Ajustes muestra la fecha de la última.
Compartir la copia. Donde el navegador permite compartir archivos (Web Share API), la copia se
puede mandar directamente a Drive, WhatsApp o el correo desde Hoy y desde Ajustes. Es lo que
protege la libreta si se pierde o se cambia el teléfono, no sólo si se desinstala la app. Si el
usuario cierra el menú sin elegir destino, no cuenta como copia. Chrome en Android no comparte
.json en algunas versiones: entonces la misma copia sale como .txt y se importa igual, porque
la importación mira el contenido y no la extensión.
Para quien vuelve tras reinstalar, la pantalla Hoy muestra ¿Ya usabas Gym Track? → Restaurar
copia mientras la libreta esté vacía, sin tener que ir a buscarlo a Ajustes. La importación
(src/lib/backup.ts) se defiende antes de dar el cambio por bueno:
.sqlite3 y lo
dejaría en gris.Los datos están atados al origen: si cambias de dominio, no viajan solos. Exporta en el viejo e importa en el nuevo.
El build es estático, así que sirve cualquier hosting. Recomendado: Cloudflare Pages o
Netlify — comando npm run build, carpeta dist.
public/_headers (que leen Netlify, Cloudflare Pages y Cloudflare Workers) hace dos cosas
imprescindibles: envía las cabeceras de aislamiento cross-origin sin las cuales no hay
persistencia (ver arriba), y evita que el Service Worker y el HTML se queden cacheados en una
versión vieja mientras los assets con hash se cachean para siempre.usuario.github.io/gymtrack/), ajusta base en
vite.config.ts y el start_url/scope del manifest.devOptions está activado en vite.config.ts para que el Service Worker exista también en
npm run dev; sin él el navegador no ofrece instalar la app y la opción no aparece por ningún
lado durante el desarrollo.
En Ajustes hay un botón Instalar app que captura beforeinstallprompt (registrado en
main.tsx, antes de montar React, porque el evento llega muy pronto) y lanza el diálogo nativo.
Si el navegador no lo ofrece, la sección explica cómo hacerlo a mano; si la app ya está instalada,
no se muestra.
La PWA está preparada para empaquetarse con Capacitor sin reescribir la capa de datos:
npm i -D @capacitor/cli && npm i @capacitor/core @capacitor/android
npx cap init "Gym Track" co.dominio.gymtrack --web-dir=dist
npx cap add android
Lo único que cambia es el driver de SQLite: @capacitor-community/sqlite en lugar de OPFS,
detrás de src/db/client.ts. El SQL de los repositorios se mantiene igual.
Sin login, backend, cloud, red social, nutrición, IA, GPS, wearables ni biblioteca multimedia.