01-overview.md
Visión general · Presencia
Presencia (repo assistance-tracker) es una Progressive Web App mobile-first para llevar la asistencia de uno o varios cursos: presencia, falta, impuntualidad y clase cancelada. Funciona offline por defecto y sincroniza a la nube si el usuario abre sesión.
A diferencia de Task Cloud, es una sola app Angular/Ionic (no monorepo Nest). La nube es Neon Auth + Data API, expuesta same-origin vía proxy de Netlify para que la cookie de sesión sea first-party (iOS Safari / PWA instalada).
Enlaces
- Web: https://asistenciacurso.netlify.app
- GitHub: Criscode2022/assistance-tracker
- Producto: control mensual de asistencia a cursos (
package.jsonnamepresencia)
Stack
| Capa | Tecnología |
|---|---|
| Web | Angular 21.2, Ionic 8, Capacitor 8, Tailwind 4, Signal Forms |
| Offline | localStorage (courses_v1, attendance_v3) |
| Nube | Neon Auth + Neon Data API (@neondatabase/neon-js) |
| i18n | ngx-translate ES / EN |
| Deploy | Netlify (www/ + SPA rewrite + proxy /__neon-*) |
02-product.md
Producto y features
Problema
Llevar faltas y retrasos en Excel, WhatsApp o la memoria falla cuando hay varios cursos, un mínimo de asistencia y tardanzas que restan minutos. Las apps de campus suelen exigir cuenta institucional. Hacía falta un registro preciso, usable en el móvil, sin red, y con sync opcional a una cuenta personal.
Solución
PWA instalable (Presencia) con dashboard de salud del curso, log diario, historial por periodo y CRUD de cursos. Offline-first en localStorage. Si el usuario activa el modo online, Neon Auth (email/password + verificación) y Data API sincronizan cursos y registros.
Características
Dashboard
Anillo de % de asistencia (verde / ámbar / rojo / gris), tarjetas de faltas y tardanzas frente al máximo, horas asistidas y minutos perdidos.
Log diario
Cada día laborable del periodo es una card. Tap → action sheet (presente, impuntual con horas, falta, cancelada, sin registrar). Swipe para toggle rápido presente/falta.
Historial
Resumen mes a mes o por módulos: %, ratio de días, horas.
Cursos
Alta/edición de horario, horas/día, máximos, % mínimo, modo mes o módulos. Import/export JSON.
Ajustes
Tema, idioma, notificaciones (recordatorio diario y alerta de %), modo online/offline, borrar todo.
Offline-first
Sin red, el registro diario no se bloquea. La nube es continuidad entre dispositivos, no el camino crítico de cada tap.
i18n
Inglés y español, fechas según locale.
03-architecture.md
Arquitectura
Diagrama lógico
Browser / PWA Ionic (Angular 21)
│ offline: localStorage
│ online + sesión: Neon JS client
▼
Same-origin /__neon-auth/* y /__neon-data/*
│ (Netlify reverse proxy)
▼
Neon Auth + Neon Data API ──► Postgres (proyecto wild-breeze-65639945)
Capas
- Cliente — Tabs Ionic: Dashboard, Log, Historial, Cursos. Settings y Auth fuera de la tab bar. Servicios:
AttendanceService,NeonService,CloudSyncService,AppModeService. - Offline —
courses_v1+attendance_v3enlocalStorage, con migraciones desdeattendance_v2. - Nube —
@neondatabase/neon-js: Auth (email/password) y Data API. No hay Nest propio: el BaaS de Neon es la API. - Deploy — Netlify sirve
www/y reescribe/__neon-auth/*y/__neon-data/*al host Neon para cookie first-party.
Principios
- Offline primero; sync opt-in.
- Sesión first-party (ITP / PWA iOS).
- Sin analytics ni tracking.
- Secretos de Neon no van al bundle más allá de las URLs públicas de Auth/Data (en prod, paths same-origin).
04-monorepo.md
Estructura (app única)
Presencia no es un monorepo Turborepo: un solo paquete Angular/Ionic, equivalente a apps/web de Task Cloud.
| Path | Rol |
|---|---|
src/app/dashboard | Tab: anillo y métricas |
src/app/log | Tab: días del periodo |
src/app/history | Tab: resumen por mes/módulo |
src/app/courses | Tab: CRUD e import/export |
src/app/auth | Sign-in / sign-up |
src/app/config | Ajustes (fuera de tabs) |
src/app/services | Datos, sync, tema, i18n, notificaciones |
src/app/models/attendance.model.ts | Course, DayRecord, MonthStats |
src/environments | URLs Neon (prod: /__neon-auth, /__neon-data) |
netlify.toml | Build, proxy Neon, SPA fallback |
capacitor.config.ts | com.presencia.app |
Scripts
| Script | Descripción |
|---|---|
npm start / ionic serve | Dev en :8100 |
npm run build | Producción → www/ |
npm run test:ci | Karma headless |
npm run e2e:ci | Playwright |
Package manager: npm. Node ≥ 20. Angular CLI 21.
05-api.md
Superficie API (Neon Auth + Data)
No hay Nest propio. El cliente habla con Neon a través de @neondatabase/neon-js.
En producción (same-origin)
| Prefijo | Destino (Netlify rewrite 200) |
|---|---|
/__neon-auth/* | Neon Auth (*.neonauth.*.aws.neon.tech/neondb/auth) |
/__neon-data/* | Neon Data API REST v1 (*.apirest.*.aws.neon.tech/neondb/rest/v1) |
Así la cookie de sesión es first-party en asistenciacurso.netlify.app y sobrevive en PWA iOS.
En desarrollo
environment.ts apunta a las URLs absolutas de Neon (sin proxy).
Operaciones de producto
El sync (CloudSyncService) sube y baja:
- Cursos del usuario autenticado
- Registros de asistencia (
DayRecordpor fecha)
Tras registro, los datos locales se suben. Tras login, la nube reemplaza lo local del dispositivo (documentado en el README).
Por qué no hay /api Nest
El dominio es un único cliente PWA. Auth + persistencia cubiertas por Neon Auth/Data API evitan un segundo servicio que desplegar. Task Cloud necesita Nest porque el contrato (PIN, bulk tasks, ownership) es custom.
06-auth.md
Modelo de autenticación
Flujo
- El usuario crea cuenta (nombre, email, contraseña) o inicia sesión.
- Neon Auth (
client.auth.signUp.email/signIn.email). - Verificación de email antes de poder entrar (requisito Neon Auth).
getSession()calienta la caché; en PWA iOS la cookie puede no leerse al instante — no se tratasession === nullinmediato como fallo de login.- Logout:
signOut(). Los datos locales se conservan en el dispositivo.
Seguridad
- Contraseña y tokens los gestiona Neon Auth; el bundle no guarda secretos de servidor.
- Proxy same-origin para no perder la sesión por ITP.
- Cloud sync es opt-in (modo online en Ajustes).
Por qué email/password y no PIN
El caso de uso es un estudiante/profesional con cuenta recuperable entre dispositivos. Neon Auth ya ofrece email + verificación. Un PIN local no cubría “el mismo curso en el portátil y en el móvil” sin un backend custom como el de Task Cloud.
07-data-model.md
Modelo de datos
Offline (localStorage)
| Clave | Contenido |
|---|---|
courses_v1 | Course[] |
attendance_v3 | Mapa curso → fecha → DayRecord |
selected_course_id | Curso activo |
(legado) attendance_v2 | Migrado al arrancar |
Tipos (attendance.model.ts)
Course
id, name, startDate / endDate, startTime, hoursPerDay, maxAbsences, maxTardiness, minAttendancePercent, periodMode (month | module), modules?.
DayRecord
status: present | absent | late | unlogged | cancelled.
Si late: entryTime / exitTime (HH:mm).
MonthStats (derivado)
Días laborables, presentes/faltas/tardanzas, % = horas asistidas / horas esperadas a fecha, minutos perdidos, overallStatus: ok | warning | failed.
Cálculo de asistencia
- Días laborables: lun–vie en el rango del curso (o del módulo).
- Impuntualidad resta minutos (entrada/salida vs horario).
- Clase cancelada no cuenta como falta ni como hora esperada.
Nube
Neon Data API persiste cursos y registros ligados al user.id de Neon Auth. El cliente no expone SQL; el contrato es el Data API REST.
08-deploy-vercel.md
Deploy en Netlify
Idea
- Build Angular →
www/ - Catch-all SPA:
/*→/index.html200 - Proxy Neon antes del catch-all para Auth y Data API
Config (netlify.toml)
[build]
publish = "www"
command = "npm run build -- --configuration production"
[[redirects]]
from = "/__neon-auth/*"
to = "https://<endpoint-neon-auth>/neondb/auth/:splat"
status = 200
force = true
[[redirects]]
from = "/__neon-data/*"
to = "https://<endpoint-neon-data>/neondb/rest/v1/:splat"
status = 200
force = true
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Variables
En producción las URLs de Auth/Data van same-origin (environment.prod.ts). El proyecto Neon (wild-breeze-65639945) se configura en el dashboard de Neon; no hay DATABASE_URL en el cliente.
Smoke
- Abrir https://asistenciacurso.netlify.app
- Crear un curso y marcar un día sin cuenta (offline).
- Activar modo online → registro/login → comprobar que el curso reaparece en otro navegador.
Local
npm install
ionic serve # :8100
Sin cuenta Neon, el modo offline cubre todo el producto excepto sync.
09-decisions.md
Decisiones
Neon Auth/Data en lugar de Nest propio
Un segundo servicio (como Task Cloud) no aportaba contrato custom: email, sesión y filas por usuario caben en Neon Auth + Data API. Menos ops, un solo deploy estático.
Proxy Netlify same-origin
Sin proxy, la cookie de Auth es third-party y iOS/PWA la tiran. El rewrite /__neon-* es una decisión de producto (sesión persistente), no cosmética.
Offline primero, sync opt-in
El registro del día no puede depender de cobertura en el aula. localStorage es la fuente en el dispositivo; la nube es continuidad.
localStorage en vez de Ionic Storage
El modelo cabe en JSON versionado (v1/v3). Ionic Storage habría añadido un adapter sin ganar queries. Se documentan migraciones explícitas.
Signal Forms en Angular 21, Zone por Ionic
Formularios de curso y auth usan @angular/forms/signals. Ionic 8 sigue pidiendo Zone; no se fuerza zoneless.
Netlify, no Vercel
SPA + redirects + proxy encajan en netlify.toml sin función serverless. Task Cloud necesita Vercel por Nest; aquí no.
10-challenges-results.md
Retos y aprendizajes
Cookie de sesión en PWA iOS
Safari ITP bloquea cookies third-party. El login “funcionaba” en desktop y se perdía al “Añadir a pantalla de inicio”. Solución: reverse proxy same-origin + no fallar si getSession() tarda un tick.
Convergencia local ↔ nube
Tras login, la nube sustituye lo local (documentado). Tras registro, se sube lo que ya había en el móvil. Evita merges silenciosos de dos calendarios distintos.
Cálculo de % vs sensación del usuario
El % usa horas (tardanza resta minutos), no solo días. Hay que explicarlo en el dashboard (chip de minutos perdidos) para que “estuve pero llegué tarde” no parezca un bug.
Periodos mes vs módulo
Cursos que no coinciden con el mes natural (módulos) obligaron a periodMode y a un selector de periodo compartido entre tabs.
Resultados
- PWA en asistenciacurso.netlify.app, instalable, usable 100% offline.
- Dashboard, log, historial y cursos con i18n ES/EN y tema claro/oscuro.
- Sync opt-in con Neon Auth + Data API y cookie first-party.
- e2e Playwright + tests de servicios; Capacitor preparado (
com.presencia.app). - Código abierto como referencia Angular/Ionic + Neon + Netlify (complemento de Task Cloud: Nest + Vercel).
11-angular-skill.md
Angular en Presencia
App Angular 21.2 + Ionic 8 + TypeScript 5.9 + Zone. Formularios con Signal Forms (@angular/forms/signals), como indica el README. No hay skill Angular publicada en este repo (a diferencia de Task Cloud, que versiona .claude/skills/angular).
Qué se aplica
| Práctica | Dónde |
|---|---|
| Signal Forms | Alta/edición de curso y validación (mín. %, fechas, horas) |
| Standalone / módulos Ionic | Tabs y páginas; shell Ionic |
| ngx-translate | ES / EN |
| Service worker | PWA + notificaciones |
| Tests | Karma unit + Playwright e2e |
Techo Ionic 8 (igual que Task Cloud)
| API moderna | Por qué no se fuerza | Qué hay |
|---|---|---|
| Zoneless | Ionic 8 pide zone.js | zone.js ~0.15 |
[formField] en ion-* | Los controles Ionic no implementan FormValueControl | Signal Forms en validación; ion-* escribe el modelo |
| View Transitions del Router Angular | Navegación por ion-tabs / ion-router-outlet | Transiciones Ionic |
| SSR | PWA + localStorage + Capacitor | SPA en Netlify |
La revisión de Task Cloud (Angular 22 + httpResource) no se copia aquí: Presencia no tiene Nest que leer con httpResource; el cliente Neon JS cubre Auth/Data.