BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2,或任何藍牙控制器搭配 MyWhoosh、Rouvy、Zwift 等。可於 iOS、Android、Windows 與 macOS 免費試用。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Cancha Total F5 — Sistema de reservas En vivo: · salud de la base: Sistema de reservas para las dos canchas techadas de fútbol 5 de Cancha Total F5. Permite ver la disponibilidad del día, registrar reservas y cancelarlas. Node + Express con las vistas renderizadas en el servidor, y los datos en SQLite: un archivo local en desarrollo, Turso —que es SQLite en la nube— en producción. El mismo driver para las dos, así que el camino que ejercitan las pruebas es el que corre desplegado.檢視完整 README ↗
En vivo: https://cancha-total-f5.vercel.app · salud de la base: https://cancha-total-f5.vercel.app/api/health
Sistema de reservas para las dos canchas techadas de fútbol 5 de Cancha Total F5. Permite ver la disponibilidad del día, registrar reservas y cancelarlas.
Node + Express con las vistas renderizadas en el servidor, y los datos en SQLite: un archivo local en desarrollo, Turso —que es SQLite en la nube— en producción. El mismo driver para las dos, así que el camino que ejercitan las pruebas es el que corre desplegado.
La grilla de disponibilidad cuenta por qué la tarifa cambia. A las 17:00 se encienden las luces y el bloque pasa de ₡15.000 a ₡20.000: por eso las horas con luz van sobre fondo cálido, llevan la marca de un foco, y una regla ámbar cruza la tabla exactamente donde se aprieta el interruptor. El precio deja de ser un número que sube sin explicación.
Y cada bloque es una acción: el libre abre la reserva con su cancha, su fecha y su hora ya puestas; el ocupado lleva a la reserva que lo ocupa, para verla o anularla. Nadie tiene que volver a elegir a mano el bloque que acaba de mirar.
HORA ESTADO TARIFA ACCIÓN
16:00 ⊘ Libre ₡15.000 ⊕ Reservar
═══════════════════════════════════════════════════ ← se encienden las luces
17:00 ⚲ ⊗ Ocupado ₡20.000 ✎ Administrar
18:00 ⚲ ⊘ Libre ₡20.000 ⊕ Reservar
Lo demás que hay que saber para usarlo:
Las capturas de todas las pantallas —claro y oscuro, a 375 px y a pantalla completa— están en
Week5/capturas/interfaz/ del repositorio del curso:
https://github.com/jcyanez/LEClaudeCode_JuanCa_Cenfotec
npm ci # o npm install
npm run datos # crea reservas.db con 10 reservas de ejemplo
npm start
El servidor queda escuchando en http://localhost:3000
npm run datos borra reservas.db si existe y la recrea desde cero, con fechas relativas al
día en que se corre. reservas.db no está en el repositorio: sin correr ese comando, el sistema
arranca igual, con cero reservas.
./verificar.sh
Un solo comando: corre las mismas etapas que el CI, en el mismo orden, y devuelve un código de salida. La idea es que un turno que cierra en verde no descubra en el pipeline algo que se podía ver acá.
| Etapa | Qué revisa |
|---|---|
| Lint | ESLint 9, reglas de defecto (no de estilo) |
| Pruebas | las 129 pruebas de node:test |
| Build | que el artefacto desplegable sea coherente (ver más abajo) |
| Humo | que el sistema arranque, responda y llegue a su base |
| Código | Qué significa |
|---|---|
| 0 | Todo pasa. Se puede cerrar el turno. |
| 2 | Algo falló, o faltan las dependencias. |
Ese código es el que usa el hook Stop de .claude/settings.json para
impedir que un turno del agente cierre con la verificación en rojo.
Para correr una prueba suelta, o un archivo, se usa el corredor de Node directamente:
node --test pruebas/tarifas.test.js
node --test --test-name-pattern "17:00" "pruebas/*.test.js"
Las pruebas no dependen del reloj, ni de la red, ni del orden, ni unas de otras: cada una levanta
el sistema en un puerto libre, sobre una base de datos propia que desecha al terminar, y con el
reloj puesto en un instante fijo. Correrlas no toca reservas.db.
Sin variables de entorno el sistema arranca como siempre, contra el archivo local. Existen para poder levantarlo en otro puerto, contra otra base o con el reloj fijo, sin tocar el código:
| Variable | Por omisión | Para qué |
|---|---|---|
TURSO_DATABASE_URL | — | Dirección de la base en Turso. Si está, el sistema habla con Turso. |
TURSO_AUTH_TOKEN | — | Credencial de Turso. Va junto con la anterior; una sin la otra falla al arrancar y lo dice. |
CANCHA_PUERTO | 3000 | Puerto. Con 0 pide uno libre al sistema operativo. |
CANCHA_BD | reservas.db | Ruta del archivo de la base de datos. |
CANCHA_AHORA | el reloj real | Fija el instante que el sistema considera «ahora». |
Los nombres están en .env.example, sin valores. Para trabajar en local se copia a
.env y se llena; npm start y los scripts de base de datos lo leen solos, con
--env-file-if-exists de Node. .env está en .gitignore y no se sube nunca.
Las mismas dos variables de Turso están cargadas en dos lugares distintos, que no se hablan entre sí, y confundirlos es la causa más común de "funciona el pipeline pero la app da error":
| GitHub Actions Secrets | Vercel Environment Variables | |
|---|---|---|
| Quién las lee | Los workflows, mientras corren en el runner | La aplicación desplegada, en tiempo de ejecución |
| Para qué acá | Aplicar migraciones a Turso, y autenticar la CLI de Vercel | Que la app conecte a Turso al atender un pedido |
| Dónde se cargan | Settings → Secrets and variables → Actions | Vercel → Project Settings → Environment Variables |
| Qué contiene | TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID, VERCEL_AUTOMATION_BYPASS_SECRET | TURSO_DATABASE_URL, TURSO_AUTH_TOKEN |
Cargar el token de Turso solo en GitHub deja el despliegue verde y la aplicación sirviendo errores: el workflow pudo migrar, pero la función desplegada no tiene con qué conectarse. Cargarlo solo en Vercel deja la app funcionando y el job de migraciones rojo. Hacen falta los dos.
Ninguno de esos valores aparece en el repositorio, en un log ni en este README: lo único versionado son los nombres, en .env.example. El detalle está en docs/CI-CD.md § 8.
El orden de precedencia de la base importa: CANCHA_BD gana sobre TURSO_DATABASE_URL. Quien
nombra un archivo concreto está pidiendo ese archivo, y quien lo hace siempre es el arnés de
pruebas. Si Turso ganara, bastaría con tener TURSO_DATABASE_URL exportada en la terminal para que
npm test escribiera —y borrara— en la base de producción.
El esquema vive en archivos .sql versionados, no en el código:
database/
migrations/
001-crear-tabla-reservas.sql
002-indices-de-consulta.sql
migrar.js
sembrar.js
npm run db:migrate # aplica lo que falte, contra la base que digan las variables
npm run db:seed # reservas de ejemplo
Las migraciones son idempotentes y no destructivas: todas usan IF NOT EXISTS y ninguna lleva
DROP. Correr db:migrate sobre la base de producción no toca un dato, y por eso el pipeline lo
hace en cada despliegue. El servidor también las corre al arrancar, así que una base vacía queda
usable sin pasos previos.
La siembra es deliberadamente arisca: por omisión no borra nada —si la tabla tiene filas, se
detiene y lo dice—, hace falta --reiniciar para reemplazarlas, y sobre una base remota hace falta
además --confirmar-remoto.
npm run datos sigue siendo el atajo de desarrollo de siempre: borra el archivo local y lo recrea
sembrado. Se niega a correr si las variables apuntan a una base remota.
Desarrollador → Git → GitHub → GitHub Actions → CI → Vercel → Turso
│
¿CI falló?
│
NO HAY DESPLIEGUE
Tres workflows en .github/workflows/:
| Workflow | Cuándo | Qué hace |
|---|---|---|
ci.yml | push a una rama · pull request | install → lint → typecheck (N/A) → pruebas → build → humo |
deploy-preview.yml | pull request | CI, y si pasa, un preview en Vercel con su propia URL |
deploy-production.yml | push a main | CI → migrar Turso → vercel deploy --prod → verificar /api/health |
El CI está escrito una sola vez: los dos workflows de despliegue lo invocan con
uses: ./.github/workflows/ci.yml. Así no puede pasar que la puerta del preview y la de producción
revisen cosas distintas.
La regla del despliegue está en el grafo de dependencias, no en un comentario: un job con
needs: ci no arranca si el CI falló. Y el despliegue lo dispara el pipeline en vez del auto-deploy
de Vercel justamente por eso — el auto-deploy publica antes de saber si las pruebas pasaron.
El CI corre contra un archivo temporal del runner y no recibe los secretos de Turso: no tiene con qué tocar la base de producción. La puerta funciona sin ningún servicio externo.
Nada del pipeline flota: las ocho referencias a acciones externas están fijadas al SHA completo
(actions/checkout@3d3c42e… # v7) en vez de a la etiqueta móvil @v7, y la CLI de Vercel es una
devDependency exacta (59.10.0, sin ^ ni ~) fijada en el package-lock.json, en vez de
vercel@latest.
mainmain está protegida. No se puede empujar a main, ni siquiera siendo el dueño del
repositorio:
| Regla | Estado |
|---|---|
| Pull request obligatorio | sí |
| Aprobaciones requeridas | 0 — la puerta la hace el CI, no una firma |
| Check obligatorio | Lint · Pruebas · Build · Humo |
| Rama al día antes de fusionar | sí (strict) |
| Aplicada a administradores | sí (enforce_admins) |
| Force-push · borrado de la rama | deshabilitados |
| Actores de bypass | ninguno |
Se comprueba leyendo la API, no la interfaz:
gh api repos/jcyanez/cancha-total-f5/branches/main/protection \
--jq '{admins:.enforce_admins.enabled, checks:.required_status_checks.contexts}'
La demostración de que la puerta cierra y abre está dentro de un mismo pull request, el #2:
| Commit | Corrida pull_request | Resultado | |
|---|---|---|---|
| Rojo | 65f5c90 | runs/33446508303 | 88 pruebas, 1 falla · PR BLOCKED · preview skipped |
| Verde | 6f4921e | runs/33446710929 | 87 pruebas, 87 pasan |
El preview del PR también quedó verde —runs/33451632482,
10/10 con backend=turso—, después de corregir dos defectos de la sonda de humo y de cargar las
variables de Turso en el entorno Preview de Vercel. La historia completa está en
docs/ENTREGA-CASO-6.md § 3.
main tiene además un commit directo histórico,
d4fa9f9,
anterior a la activación de la protección. No se oculta ni se reescribe: borrarlo eliminaría la
evidencia de cuándo empezó a regir la política. El detalle está en
docs/CI-CD.md § 10.
Todo el detalle está en docs/CI-CD.md: qué es CI, qué es CD y cuál se usa acá, el vocabulario de GitHub Actions, la explicación de cada YAML, la diferencia entre GitHub Secrets y las variables de entorno de Vercel, y la lista de evidencias para la entrega.
curl https://cancha-total-f5.vercel.app/api/health
{ "status": "ok", "database": "connected", "driver": "libsql", "backend": "turso", "reservas": 10 }
Hace una lectura real contra la base. No devuelve la URL de la base ni el token, y la prueba de humo verifica activamente que no los filtre.
node herramientas/humo.js https://cancha-total-f5.vercel.app
Interroga una aplicación ya desplegada: la salud, las pantallas, la cotización. Contra una URL
remota solo hace lecturas, y exige que el backend reportado sea turso — una app serverless
sirviendo desde un archivo estaría perdiendo cada reserva.
El sistema se recibió sin un solo documento. Estos se escribieron reconstruyéndolo:
server.js el sistema: rutas, reglas del negocio y vistas
bd.js acceso a datos: el único archivo que sabe con qué base habla
datos.js atajo de desarrollo: rehace la base local sembrada
verificar.sh la puerta de calidad
api/index.js punto de entrada serverless de Vercel
vercel.json manda todas las rutas a esa función única
database/ el esquema versionado y la siembra
herramientas/
construir.js verificación del artefacto desplegable
humo.js interroga la app —local o desplegada— y su base
pruebas/ la suite
soporte/ lo que las pruebas usan para hablar con el sistema
docs/CI-CD.md el pipeline explicado
.github/workflows/ CI y despliegue
El esquema no está en el código: vive en database/migrations/*.sql. Estuvo escrito dos veces
—en server.js y en datos.js— y eso fue el hallazgo E-9 del caso práctico; después pasó a un
esquema.js compartido, y con la llegada de las migraciones quedó donde corresponde, en SQL
versionado.
No hay build de compilación, ni bundler, ni carpeta de archivos estáticos: el HTML se arma en el
servidor y la
hoja de estilo viaja dentro de él, en la constante ESTILOS de server.js. Lo que sí verifica
npm run verificar-artefacto es que el artefacto desplegable sea coherente: que todo parsee, que la entrada
serverless cargue, y que ningún archivo de tiempo de ejecución dependa de una devDependency.
Esto último es lo que justifica el script: better-sqlite3 es un módulo nativo que quedó como
dependencia de desarrollo —lo usa el arnés de pruebas, no el sistema—, y si alguien vuelve a
requerirlo desde server.js o bd.js, en local funciona y en producción el despliegue arranca
roto.
La hoja de estilo está organizada en
dos pisos: primero los tokens —un nombre por cada decisión de color, espacio, tipografía y
forma, declarados una sola vez en :root— y después las reglas, que solo consumen tokens.
Ningún color literal aparece fuera de ese bloque, y el tema oscuro se limita a redefinir los
mismos nombres. Para cambiar el verde de la marca en todo el sistema se toca una línea.
Los cuatro iconos —check-circle, x-circle, warning y lightbulb, del vocabulario de
Phosphor— van dibujados como data: URI y se pintan con mask, así que heredan el color del
texto: una sola definición sirve para todos los estados y ninguno se pide a un servidor externo.
Quince pruebas leen el HTML con expresiones regulares que exigen etiquetas pegadas. En concreto:
| No tocar | Por qué |
|---|---|
<td>8:00</td><td …>Libre</td> | El estado tiene que ser texto directo del <td>, sin nada anidado adentro. |
<td>₡15.000</td> | La celda de la tarifa no admite atributos. |
<li>texto</li> | Cualquier etiqueta dentro del <li> deja el conteo de errores en cero. |
<h2> | Sin atributos: dos pruebas parten el HTML por esa cadena literal. |
<option value="8"> | Sin atributos extra. |
<select name="hora" | name va primero; poner id antes rompe el regex. |
Por eso las píldoras, los iconos y la regla de la luz están hechos con CSS sobre el markup que ya existía, y no envolviendo texto en etiquetas nuevas. Si algo hace falta cambiar ahí, el camino es hablarlo antes: aflojar un helper de pruebas es una decisión, no un descuido.