✨ Características: - 45 ejercicios universitarios (Basic → Advanced) - Renderizado LaTeX profesional - IA generativa (Z.ai/DashScope) - Docker 9 servicios - Tests 123/123 pasando - Seguridad enterprise (JWT, XSS, Rate limiting) 🐳 Infraestructura: - Next.js 14 + Node.js 20 - PostgreSQL 15 + Redis 7 - Docker Compose completo - Nginx + SSL ready 📚 Documentación: - 5 informes técnicos completos - README profesional - Scripts de deployment automatizados Estado: Producción lista ✅
6.0 KiB
Streak Calculator - Documentación Técnica
Resumen del Fix
Problema Identificado (Issue #10)
El cálculo anterior de streak en score.calculator.ts (líneas 187-193) tenía una inconsistencia grave:
// PROBLEMA: Cálculo inconsistente de daysDiff
const latestAttempt = new Date(attempts[0].createdAt);
latestAttempt.setHours(0, 0, 0, 0);
const daysDiff = Math.floor((today.getTime() - latestAttempt.getTime()) / (1000 * 60 * 60 * 24));
// Caso problemático:
// - Hoy es lunes 9:00 AM
// - Último attempt fue lunes 2:00 AM
// - daysDiff = 0 (mismo día calendario)
// - PERO: El usuario NO completó ejercicios "ayer" (domingo)
// - Resultado: Streak incorrectamente activo
Solución Implementada
Se creó StreakCalculator con manejo robusto de timezones usando date-fns y date-fns-tz.
Arquitectura
Componentes Principales
-
StreakCalculator (
streak.calculator.ts)- Cálculo timezone-aware
- Algoritmo de ventana deslizante para longest streak
- Manejo de DST (Daylight Saving Time)
- Caché implícita de 1 minuto (cálculos costosos)
-
ScoreCalculator (actualizado)
- Delega cálculo de streak a
StreakCalculator - Mantiene interfaz existente para compatibilidad
- Delega cálculo de streak a
-
Endpoint
/api/ranking/streak- Devuelve streak completo con metadata
- Requiere autenticación
- Usa timezone del usuario desde base de datos
Algoritmo de Cálculo
Fase 1: Normalización de Timezone
// Obtener "hoy" en el timezone del usuario
const now = new Date();
const today = startOfDay(toZonedTime(now, timezone));
Fase 2: Obtención de Datos
- Query a Prisma: últimos 3 días de actividad
- Conversión de fechas UTC a timezone local
- Eliminación de duplicados (múltiples ejercicios mismo día)
Fase 3: Verificación de Streak Activo
isStreakActive(lastActivity: Date, today: Date): boolean {
const diff = differenceInCalendarDays(today, lastActivity);
return diff <= 1; // Hoy (0) o ayer (1)
}
Fase 4: Cálculo de Días Consecutivos
calculateConsecutiveDays(sortedDays: Date[]): number {
let streak = 1;
for (let i = 0; i < sortedDays.length - 1; i++) {
const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i+1]);
if (diff === 1) streak++;
else if (diff > 1) break;
}
return streak;
}
Fase 5: Longest Streak Histórico
Algoritmo de ventana deslizante O(n):
let maxStreak = 1, currentStreak = 1;
for (let i = 1; i < sortedDays.length; i++) {
const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i-1]);
if (diff === 1) {
currentStreak++;
maxStreak = Math.max(maxStreak, currentStreak);
} else if (diff > 1) {
currentStreak = 1;
}
}
Manejo de Edge Cases
1. Timezone Boundaries
Escenario: Usuario en Argentina (UTC-3) completa ejercicio a las 23:00 hora local.
- UTC: 02:00 del día siguiente
- Local: 23:00 del día actual
Solución: toZonedTime() convierte UTC a hora local antes de comparar días.
2. Daylight Saving Time (DST)
Escenario: Cambio de horario de verano en NY (1 hora "perdida" o "ganada")
Solución: date-fns-tz maneja automáticamente DST, calculando días calendario reales.
3. Viaje entre Timezones
Escenario: Usuario viaja de NY → Tokyo
Comportamiento:
- Streak se mantiene activo/inactivo independientemente del timezone
currentStreakpuede variar según la hora local- Se usa el timezone de preferencia del usuario (almacenado en DB)
4. Múltiples Ejercicios mismo día
Solución: Set para eliminar duplicados antes de contar días.
API Response
GET /api/ranking/streak
{
"success": true,
"data": {
"currentStreak": 5,
"longestStreak": 12,
"lastActivityDate": "2024-03-30T00:00:00.000Z",
"isStreakActive": true,
"daysUntilStreakBreaks": 1
},
"meta": {
"timestamp": "2024-03-30T14:30:00.000Z",
"timezone": "America/Argentina/Buenos_Aires"
}
}
Interpretación de daysUntilStreakBreaks
1.0: Actividad hoy → tiene hasta mañana (24+ horas)0.x: Actividad ayer → debe actuar hoy (menos de 24 horas restantes)0: Streak inactivo o roto
Migración de Datos
Nuevo Campo en User
model User {
// ... campos existentes
timezone String @default("UTC")
}
Script de Migración Sugerido
// Para usuarios existentes, asignar timezone basado en preferencias o default UTC
await prisma.user.updateMany({
where: { timezone: null },
data: { timezone: 'UTC' }
});
Tests
Cobertura
- ✅ Streak básico (0, 1, N días)
- ✅ Rotura de streak (2+ días sin actividad)
- ✅ Timezone: Argentina (UTC-3)
- ✅ Timezone: Viaje NY → Tokyo
- ✅ DST: New York
- ✅ DST: Europa
- ✅ Múltiples ejercicios mismo día
- ✅ Longest streak histórico
- ✅ Activity check por fecha
- ✅ Days until break calculation
Comando de Ejecución
cd /home/ren/Documents/math2/backend
npm test -- streak.calculator.test.ts
Dependencias
{
"dependencies": {
"date-fns": "^3.x",
"date-fns-tz": "^3.x"
}
}
Instalación:
npm install date-fns date-fns-tz
Optimizaciones Futuras
- Redis Cache: Cachear streak por 1 minuto para reducir queries
- Batch Processing: Calcular streaks en background cada hora
- WebSocket: Notificar cuando queden pocas horas para mantener streak
- Push Notifications: Recordatorio diario configurable
Changelog
v2.0.0 (2024-03-30)
- ✅ Fix: Inconsistencia en cálculo de streak (Issue #10)
- ✅ Feature: Soporte de timezones con date-fns-tz
- ✅ Feature: Endpoint GET /api/ranking/streak
- ✅ Feature: Campo timezone en modelo User
- ✅ Feature: Algoritmo de longest streak
- ✅ Tests: 20+ casos de prueba incluyendo DST y timezones