ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
App web para crear el hábito de la lectura mediante gamificación. El usuario elige qué días de la semana se compromete a leer y cuida un refugio de perritos. Cada día comprometido que no lee, un perrito se va. Cada sesión de lectura los trae de vuelta, con una curva de recompensa que crece cuanto más larga es la sesión. La idea es que el castigo cree urgencia y la curva ascendente cree el «un bloque más» que alarga la sesión — y que recuperarse siempre sea más rápido que caer.檢視完整 README ↗
App web para crear el hábito de la lectura mediante gamificación.
El usuario elige qué días de la semana se compromete a leer y cuida un refugio de perritos. Cada día comprometido que no lee, un perrito se va. Cada sesión de lectura los trae de vuelta, con una curva de recompensa que crece cuanto más larga es la sesión.
La idea es que el castigo cree urgencia y la curva ascendente cree el «un bloque más» que alarga la sesión — y que recuperarse siempre sea más rápido que caer.
Todos los números viven en src/lib/game/config.ts,
que es la única fuente de verdad del balance. Si una cifra aparece en otro
sitio, es un error.
Los objetivos de cronómetro (Lectura, Inglés, Estudio) se resuelven midiendo minutos. Salud se resuelve con un sí o un no desde su tarjeta: no tiene cronómetro, no tiene meta en minutos y no responder no cuesta nada.
| Cronómetro | Salud (binario) | |
|---|---|---|
| Cómo se cumple el día | minutos sobre el umbral | botón "No" |
| Qué suma | la curva del tipo | 1 perrito |
| Qué resta | día programado bajo el umbral | botón "Sí" |
| Si no haces nada | pierdes un perrito | nada |
Se calcula sobre los minutos del día, no por sesión: tres sesiones de 7 minutos no valen 0. Cada tipo de objetivo tiene su curva, porque no piden el mismo esfuerzo.
Lectura — umbral 10 min, bloques de 5, ascendente:
| Minutos en el día | 10 | 15 | 20 | 25 | 30 o más |
|---|---|---|---|---|---|
| Perritos | 1 | 2 | 4 | 7 | 11 |
| Gana el bloque | +1 | +1 | +2 | +3 | +4 |
Diez minutos son un hábito que hay que arrancar, así que la curva paga el "un bloque más". A partir de 30 minutos no crece.
Inglés y Estudio — umbral 30 min, bloques de 15, lineal y sin tope:
| Minutos en el día | 29 | 30 | 45 | 60 | 75 | 90 |
|---|---|---|---|---|---|---|
| Perritos | 0 | 1 | 2 | 3 | 4 | 5 |
Media hora ya es una sesión seria: aquí la dificultad está en llegar al umbral, no en estirarlo.
Si el refugio está lleno, los perritos extra pasan a adoptados (marcador histórico) y cada 10 adopciones amplían el aforo en 1, hasta 21.
El umbral de cada curva es también su mínimo de supervivencia: un día comprometido que cierra sin ganar perritos cuesta 1. Salud queda fuera de esto: sus perritos solo se mueven con un clic explícito. Son el mismo número a propósito, para que no puedan contradecirse — con 29 minutos de inglés se pierde un perrito y con 30 no.
Hay dos frenos deliberados:
ceil(7 × 0.6) = 5, así que fallar los siete
días deja el refugio en 2, no en 0.Una sola sesión de 20 minutos de lectura repone cuatro días fallados.
No hay cron. Los días cerrados se liquidan de forma perezosa la próxima vez
que el usuario entra a la app (syncOnVisit). El día en curso nunca se juzga:
siempre se puede salvar.
Cada objetivo tiene un interruptor en su acordeón de /ajustes. En pausa deja de
penalizar, y nada más: conserva sus días comprometidos, su refugio, su racha y su
historial, y lo que se practique sigue dando perritos.
| Pausar | Vaciar los días | Archivar | |
|---|---|---|---|
| ¿Penaliza? | no | no | no |
| ¿Conserva los días configurados? | sí | no, hay que volver a escribirlos | sí |
| ¿Sigue a la vista? | sí, marcado «En pausa» | sí | no, pasa a «Archivados» |
El motor resuelve un día pausado con la misma acción que un día libre, así que no
hubo que inventar reglas nuevas: reconcile deja de llamar a miss. Como
lastReconciledDay avanza igual durante la pausa, al reanudar no queda nada
pendiente que cobrar hacia atrás; /api/goals/[goalId] reconcilia antes de
cambiar la bandera para que eso se cumpla en los dos sentidos.
| Capa | Elección | Por qué |
|---|---|---|
| Framework | Astro 7 (output: 'server') | Islas: React solo donde hay interacción |
| UI | React 19 + Tailwind 4 | |
| Base de datos | MongoDB, driver nativo | Better Auth necesita un Db nativo; mongoose implicaría un segundo pool en serverless |
| Auth | Better Auth + @better-auth/mongo-adapter | Correo + contraseña, sin verificación ni 2FA |
| Gráficas | Recharts | 95 KB gzip, cargado solo en /progreso |
| Despliegue | Vercel |
Requiere Node 24 (engines.node: "24.x"), la misma versión con la que Vercel
construye y ejecuta las funciones. Con otra, npm install avisa EBADENGINE pero
el proyecto funciona.
cp .env.example .env # rellena los valores, SIN comillas
npm install
npm run db:init # crea los 7 índices de MongoDB
npm run dev # http://localhost:4321
⚠️ Los valores del
.envvan sin comillas. El panel de Vercel no limpia lo que pegas: unas comillas dentro del valor hacen quenew URL()lance «Invalid URL» (tumba el build y la autenticación) y que el driver no reconozca el esquemamongodb+srv://. La única excepción es un valor con#o espacios, que sí necesita comillas en el.envlocal, porque dotenv trataría el#como comentario; el código las quita al leer.
| Comando | Qué hace |
|---|---|
npm run dev | Servidor de desarrollo |
npm run build | Build de producción (salida en .vercel/output) |
npm run typecheck | tsc --noEmit sobre todo el proyecto |
npm test | Vitest: 85 tests — 48 del motor, 37 de los perfiles del seed |
npm run db:init | Crea los índices de MongoDB (idempotente) |
npm run db:dump | Vuelca todas las colecciones a backups/<marca>/ |
npm run db:restore -- <carpeta> | Restaura un volcado; exige ALLOW_DB_RESTORE=yes |
npm run db:migrate | Migración multi-objetivo; exige ALLOW_DB_MIGRATE=yes |
npm run db:reset | Vacía las colecciones; exige ALLOW_DB_RESET=yes |
npm run db:seed | Crea un usuario de demo con 8 semanas de historia |
npm run preview | No funciona con el adaptador de Vercel; usa vercel dev |
npm run db:seed
Simula 8 semanas día a día llamando a las mismas funciones que usa la app, así que
además sirve de test de integración del motor. Apunta SEED_USER_EMAIL a un correo
que no sea el tuyo y usa una contraseña de 8 caracteres o más: el script aborta
sin escribir nada si el correo ya está registrado, porque nunca borra cuentas.
src/
├── middleware.ts resuelve la sesión y protege /app, /progreso, /ajustes
├── lib/
│ ├── auth.ts instancia de Better Auth (perezosa y cacheada)
│ ├── env.ts lector de variables de entorno ← lee siempre por aquí
│ ├── name.ts reglas del nombre (cliente + servidor)
│ ├── time.ts dayKey / weekKey / día ISO ← única verdad temporal
│ ├── chart-theme.ts colores de gráfica leídos de los tokens en runtime
│ ├── api.ts helpers de respuesta JSON de los endpoints
│ ├── auth-client.ts cliente de Better Auth para React
│ ├── sessions-view.ts forma de la sesión que viaja al navegador
│ ├── shelter-events.ts puente por evento DOM entre cronómetro y refugio
│ ├── progress-summary.ts tipos del dashboard
│ ├── db/ conexión, tipos de documento, accesores
│ ├── game/
│ │ ├── config.ts constantes de balance ← única fuente de verdad
│ │ ├── rewards.ts curva de recompensa (puro)
│ │ ├── penalties.ts topes de penalización (puro)
│ │ ├── engine.ts reducer + reconciliación (puro)
│ │ ├── service.ts orquesta el motor con MongoDB (impuro)
│ │ └── __tests__/ 48 tests, incluida una prueba de fuzz
│ └── repos/ profile · sessions · progress · gameState · summary
├── components/ ui/ · auth/ · charts/ · icons/
│ today/ tarjeta de cada objetivo + semana de días
│ + respuesta sí/no de los binarios
│ progress/ pestañas, panel y archivados
│ settings/ acordeón y formulario por objetivo
│ goals/ archivar y reactivar un objetivo
│ SessionTimer · Shelter · ThemeToggle
├── layouts/ BaseLayout · AuthLayout · AppLayout
├── styles/global.css tokens de color de los dos temas
├── env.d.ts tipos de Astro.locals
└── pages/
├── index.astro Vista de Hoy: los objetivos que tocan (ruta protegida)
├── login · registro
├── sesion/[goalId] cronómetro + refugio de un objetivo
├── progreso.astro dashboard: una pestaña por objetivo + archivados
├── ajustes.astro la cuenta + un acordeón por objetivo
├── 404 · 500
└── api/ auth/[...all] · profile · goals/* · sessions/* · progress/summary
| Colección | Qué guarda | Índice |
|---|---|---|
user · account · session | Better Auth | — |
profiles | lo que es de la cuenta: zona horaria y onboarding | userId único |
goals | un objetivo: días comprometidos, meta, metadatos, pausa, archivado | userId+goalId único · userId+archivedAt+order |
sessions | cada sesión del cronómetro | userId+goalId+dayKey · userId único solo en las abiertas |
dailyProgress | agregado por objetivo y día | userId+goalId+dayKey único · userId+goalId+weekKey |
gameState | refugio, racha, semana en curso | userId+goalId único |
gameEvents | historial de movimientos | userId+goalId+createdAt desc |
Dos índices cargan con una regla de negocio cada uno:
dailyProgress es lo que hace seguro el upsert bajo
peticiones concurrentes. Lleva goalId desde la Tanda A: sin él, dos objetivos
del mismo día colisionarían y el segundo no podría registrarse.sessions limita a una sesión abierta por usuario aunque
tenga varios objetivos: nadie lee y estudia a la vez. Puesto en la base de
datos, es una regla y no una buena intención del código.Un gameState por objetivo es lo que hace los refugios independientes: perder
perritos en Inglés no toca los de Lectura.
El servidor manda en el tiempo. El cronómetro del navegador solo pinta. La
sesión guarda los tramos cerrados en accumulatedSeconds y el inicio del tramo en
curso en lastResumedAt; el tiempo real lo deriva el servidor. El cliente se
resincroniza cada 30 s y al volver a la pestaña, porque una pestaña dormida congela
setInterval.
Todo lo que mueve perritos es idempotente. settleSession paga solo la
diferencia contra lo ya otorgado ese día, así que liquidar dos veces la misma
sesión da 0. Entrar cien veces en un día no genera ni una escritura si no hay nada
que reconciliar.
Las variables de entorno solo por @/lib/env, nunca process.env.X directo:
astro dev carga el .env en import.meta.env y deja process.env vacío. La
única excepción es astro.config.mjs, que corre en Node puro antes de Vite.
Better Auth guarda userId como ObjectId en user, session y account;
nuestras colecciones lo guardan como string. Un deleteMany({ userId: "..." })
sobre las de Better Auth no borra nada y no avisa.
El nombre manda en la personalización. Se pide solo al registrarse (el login es
correo + contraseña). Se usa firstName() para saludos y el nombre completo para
títulos; el correo nunca aparece donde quepa el nombre.
Los colores se usan siempre por token (bg-surface, text-text-soft…), nunca
con hexadecimales ni con la paleta por defecto de Tailwind. Están en
src/styles/global.css; el tema oscuro redefine los
mismos nombres bajo .dark, así que ningún componente conoce dos paletas ni
escribe variantes dark:.
Cada color de marca tiene dos tokens, y confundirlos rompe WCAG AA:
text-primary, text-accent-text, text-alert-texttext-primary-bright, text-accent, text-alerttext-on-primary, nunca text-whitehover:bg-primary-hover, que oscurece en claro y aclara en oscuroLos 26 pares de contraste (13 × 2 temas) están verificados en AA. La rampa
secuencial del mapa de calor (--color-chart-1..4) está validada aparte
(monotonía de luminosidad, ΔL ≥ 0.06, extremo claro ≥ 2:1, un solo matiz) en los
dos temas: si cambias un paso, hay que revalidarla.
Más convenciones en AGENTS.md (CLAUDE.md es un enlace al mismo
archivo).
npm i -g vercel && vercel link && vercel --prod
Antes de publicar:
.env.example cargadas en Vercel (Production y
Preview), sin comillas.BETTER_AUTH_URL, PUBLIC_BETTER_AUTH_URL y PUBLIC_SITE_URL con el
dominio real y https://, no localhost.ALLOW_DB_RESET ausente o en no.0.0.0.0/0 (las funciones de Vercel no
tienen IP fija).npm run db:init ejecutado contra la base de producción.secure no
funcionan sobre http.Las diez tandas del plan están completas.
El plan técnico completo, con el detalle de cada tanda y las trampas encontradas
al ejecutarlas, está en planificacion.md.
La Parte II del plan (tandas A–E) convirtió la app de tracker de lectura en un sistema de objetivos independientes —Lectura, Inglés y Estudio—, cada uno con sus días comprometidos, sus metadatos y su propio refugio de perritos.
goals, goalId en todas
las colecciones de datos, migración y respaldo/api/goals y ajustes por objetivo/app y cronómetro en /sesion/[goalId]/progreso por pestañas y /ajustes por acordeonesLos dos criterios de la Tanda E que faltaban por comprobar con la app delante
—archivar un objetivo y npm run db:seed— se verificaron el 2026-10-07 contra una
base desechable. Al hacerlo apareció un defecto en el desarchivado, que cobra
las penalizaciones del período en que el objetivo estuvo archivado; el diagnóstico
y el arreglo propuesto están en el plan.
El motor de gamificación no cambia: es puro y opera sobre un GameState, así
que varios objetivos son varios estados. El Apéndice D lista
qué código se conserva y cuál se reescribe.
START_DOGS === BASE_CAPACITY, así que un usuario nuevo empieza con el refugio
lleno y sus primeros perritos van directos a «adoptados».public/dogs/ en lugar del emoji.