Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
51f71ef
docs: agregar tareas pendientes de SSL, UI/UX, seguridad, BD, CI/CD y…
Jun 18, 2026
a17b743
docs: agregar items de refresh token, auditoria, paginacion, PWA, API…
Jun 18, 2026
fe3ca60
docs: agregar seccion movil API + Play Store (MOB-001 a MOB-015)
Jun 18, 2026
4e48ba9
docs: actualizar README con estado actual, endpoints, CI/CD y referen…
Jun 18, 2026
f59cbe1
docs: agregar flujo de trabajo local->GitHub->VPS en README y PENDIENTES
Jun 18, 2026
06517e8
feat: UI/UX mejoras - perfil avatar, sidebar responsive, loading stat…
Jun 18, 2026
2f290a5
feat: imagen personaje en Ajustes/Progreso, responsive general, fix c…
Jun 18, 2026
e60182c
feat: login hero split layout, logo en sidebar, fix avatar position, …
Jun 18, 2026
93e185e
docs: auditoria responsividad agregada a PENDIENTES.md
Jun 18, 2026
b9850b9
fix(responsividad): corrección completa de layout responsive según au…
Jun 18, 2026
636090f
docs: actualiza README y PENDIENTES.md con estado de responsividad
Jun 18, 2026
08c4f63
feat(ux): rediseño completo UI/UX según auditoría
Jun 18, 2026
9248836
docs: agrega auditoría de responsividad móvil a PENDIENTES.md
Jun 18, 2026
cbd6656
fix(responsividad): corrige 20 issues de responsividad móvil
Jun 18, 2026
7db8fa9
Actualiza PENDIENTES.md: favicon, logo y login-hero marcados como com…
Jun 18, 2026
de01cdd
Agrega issues: login mojibake, Progreso/Ajustes/Inicio modo oscuro
Jun 18, 2026
ea0cd26
Corrige mojibake en ViewModels, dark mode (data-theme), personaje-gri…
Jun 18, 2026
019e0fa
auditoria UI/UX completa: 18 hallazgos nuevos (UX-017 a UX-034). Reab…
Jun 18, 2026
a003d34
Agrega auditoría completa ODS 3 (Bienestar/Salud Mental) a PENDIENTES.md
Jun 18, 2026
a9554ff
docs: agrega auditoría completa del módulo IA (EDY/Gemini) a PENDIENT…
Jun 18, 2026
eb63679
docs: agrega sección ARQ con 17 hallazgos de auditoría de arquitectura
Jun 18, 2026
dbbcfe1
fix(ui/ux): corrige 16 hallazgos del auditoría UI/UX (UX-017 a UX-034)
Jun 18, 2026
f74ab40
auditoria UI/UX: 12 nuevos hallazgos UX-035 a UX-046, reabiertos UX-0…
Jun 18, 2026
cca3790
fix(ux): corrige los 12 hallazgos UX-035 a UX-046 de la auditoria UI/UX
Jun 18, 2026
577609d
docs: tercera auditoria UI/UX - 8 nuevos hallazgos UX-047 a UX-054, r…
Jun 18, 2026
3714654
UX: corrige items UX-026 a UX-053 (notificaciones, skeletons, FOUC, b…
Jun 18, 2026
264c311
docs: cuarta auditoria UI/UX - 5 nuevos hallazgos UX-055 a UX-059
Jun 18, 2026
65549c0
fix(ui): corrige UX-055 a UX-059 pendientes de cuarta auditoria
Jun 18, 2026
8add820
docs: quinta auditoria UI/UX - corrige stale UX-055-057, 16 nuevos ha…
Jun 18, 2026
e5301ed
security: remove real credentials from appsettings.json, move API key…
Jun 18, 2026
829191e
feat: VPS monitoreo/rotacion/mantenimiento + ODS 3 auditoria completa
Jun 18, 2026
0dd1ffa
fix: agregar jQuery en _ValidationScriptsPartial
Jun 18, 2026
3da7ce5
fix: agregar jQuery en _ValidationScriptsPartial
Jun 18, 2026
54cbd4d
docs: actualizar README y PENDIENTES con fixes jQuery, Categoria y si…
Jun 18, 2026
866cc24
fix(ui): quitar icono de advertencia en validaciones y mejorar UI de …
Jun 18, 2026
6ecbfcf
fix: corregir encoding español en todo el sistema y unificar validaci…
Jun 18, 2026
7e29ccb
fix: agregar SSL/HTTPS con redirect HTTP→HTTPS y HSTS en Nginx
Jun 18, 2026
8cd268e
fix: deprecation http2 syntax en nginx
Jun 18, 2026
3c3d7ce
feat: flujo completo auth Google con registro y seleccion de carrera
Jun 18, 2026
ca082c8
fix: boton Google con colores originales, visible en modo claro
Jun 18, 2026
81e9aab
fix: cambiar SameSite a Lax en cookies JWT para que funcione OAuth de…
Jun 18, 2026
5265040
fix: unificar layout auth, remover logo hero, agregar icono stars
Jun 18, 2026
d02b055
fix: restaurar logo en hero, fijar altura 100vh para que imagen no se…
Jun 18, 2026
c6423b0
fix: reemplazar bi-stars por logo.webp en login
Jun 18, 2026
b300080
docs: actualizar PENDIENTES.md - CRITICO-006 Google OAuth resuelto
Jun 18, 2026
46cdf1b
fix: renombrar EDY a AI EDY en vistas, prompt y errores
Jun 18, 2026
8310eeb
fix: migracion vacia AddFeedbackToMensajesIA (columnas ya agregadas m…
Jun 18, 2026
b125fb2
fix: renombrar AI EDY a EDY AI, navbar a AI Edy, titulo a Soy EDY, mo…
Jun 18, 2026
91ff5fb
feat: migrar de Gemini a DeepSeek (deepseek-v4-flash)
Jun 18, 2026
4af79a2
fix: cambiar EnableRateLimiting de Gemini a DeepSeek en IaController
Jun 18, 2026
bc162d3
feat: dual provider AI - Gemini (gratis) o DeepSeek configurable via …
Jun 18, 2026
9eaf129
feat: modulo Diario de Animo ODS 3 - entidad, servicio, controlador, …
Jun 19, 2026
91bb5c3
docs: prompts de auditoria (12 especialidades) y roadmap de 15 nuevos…
Jun 19, 2026
660d494
feat: API endpoints completos para app movil Android
Jun 19, 2026
001ecb7
feat: rate limiting extendido y configurable para app movil
Jun 19, 2026
c405380
docs: prompts de auditoria (renombrado) + diagramas PlantUML
Jun 19, 2026
18528a4
fix(api): alinear endpoints con contrato Android app
Jun 19, 2026
0bf6dae
Fix rutas API y DTO crear habito para Android
Jun 20, 2026
c7c579e
Fix: ApiMisiones Crear - FechaLimite optional handling, CategoriaId n…
Jun 20, 2026
aa0d0e8
fix(pomodoro): auditoria completa - rutas API, IDOR, XP, validaciones…
Jun 21, 2026
bfd390f
docs: auditoria modulo Pomodoro - 19 hallazgos detallados para correc…
Jun 21, 2026
a0ebde2
fix(pomodoro): corregidos 17/19 hallazgos de auditoria - XP increment…
Jun 21, 2026
1670a8f
feat(pomodoro): configuracion avanzada, UX/UI completo y correcciones…
Jun 21, 2026
eb937b2
chore: limpiar PENDIENTES.md - eliminar todos los items corregidos (✅…
Jun 21, 2026
e914711
docs: agregar Documentacion.md con auditoria completa del proyecto
Jun 21, 2026
cf2f95a
fix(pomodoro): correccion completa de bugs, UX/UI y unificacion de no…
Jun 21, 2026
79b80ca
fix(pomodoro): sliders Configuracion, deseleccionar tareas y persiste…
Jun 21, 2026
71f8ae6
fix(pomodoro): sliders con IDs explicitos y modal para sesion activa
Jun 21, 2026
53885d2
fix(pomodoro): restaurar funcion guardarEstadoTimer que quedo huefrana
Jun 21, 2026
8f78bde
fix(pomodoro): tabs a la izquierda, fullscreen solo al card del pomodoro
Jun 21, 2026
a16c617
fix(pomodoro): desplazar contenedor tabs a la izquierda -0.75rem
Jun 21, 2026
e1078af
fix(diario-animo): redondear HorasSueno a 1 decimal al guardar y mostrar
Jun 21, 2026
c1036aa
fix(habitos): corregir URL /api/apihabitos -> /api/habitos en JS
Jun 21, 2026
607617d
fix(habitos): logging y mensajes detallados al completar habito
Jun 21, 2026
713e3d7
fix(habitos): ocultar boton 100% si ya fue completado hoy
Jun 21, 2026
6b48d88
chore: quitar async innecesario en NavegarMes (CS1998)
Jun 21, 2026
0d6adaa
docs: actualizar comandos deploy y agregar historial correcciones a P…
Jun 21, 2026
f2cc578
feat: tipar controladores con DTOs y añadir pruebas de contrato JSON …
Jun 21, 2026
4185820
Corregir estado de misión en ServicioIA: 'Completada' -> 'Completado'…
Jun 22, 2026
c49f4de
fix(pomodoro): corregir bugs críticos del temporizador
Jun 22, 2026
9fd3d02
fix(pomodoro): bonus XP, racha, logros, stats semanales 1 query, Inic…
Jun 22, 2026
119e0bc
fix(pomodoro): UX3, UX5, UX8, UX9, UX11, N2, N5, N6 - problemas UX/UI…
Jun 22, 2026
a139c18
fix(pomodoro): corrige 6 issues de auditoria - FinalizarSesion, Minut…
Jun 22, 2026
4b4be85
fix(pomodoro): CSRF, XP bonus con 0 ciclos, null display, validacion …
Jun 22, 2026
c19c22d
Pomodoro R3: corrige 13 bugs (C-8 a C-12, I-1 a I-8, M-1)
Jun 22, 2026
060fd95
Pomodoro M-2 M-3: minutos reales en stats + patron vibracion
Jun 22, 2026
5edae79
Pomodoro.md: actualiza estado M-2 M-3 corregidos, M-5 descartado
Jun 22, 2026
c43e376
Pomodoro R4: corrige 8 issues menores - historial reversal, sound pre…
Jun 22, 2026
6bb91c3
Pomodoro R5: seguridad S-2/S-3 + nombres N-2/N-4 + tests cobertura + …
Jun 22, 2026
3738d61
Pomodoro R6: NF-21 timer en segundo plano + visibilitychange + Date.n…
Jun 22, 2026
b5cedc7
Pomodoro R7: N-5 meta unificada + NF-20 BroadcastChannel + NF-22 filt…
Jun 22, 2026
103641b
fix: persiste descansos en BD, actualiza racha global, corrige dia de…
Jun 22, 2026
42d5f22
Agrega plan de integración de sub-tareas con tiempo de enfoque en Mis…
Jun 22, 2026
13f85c4
Implementa sistema de sub-tareas con integracion Pomodoro
Jun 22, 2026
9f68f21
Mejoras UI/UX sub-tareas, revertir mision, colores prioridad, histori…
Jun 22, 2026
2a99506
Actualiza PLAN_SUBTAREAS.md con mejoras post-auditoria
Jun 22, 2026
6057cd4
Corrige: quita texto tarea en timer, stats sin modificar historial, t…
Jun 22, 2026
98804ce
Corrige guardado config: TempData, validacion visible, JS siempre usa…
Jun 22, 2026
da19ba7
Corrige historial: usa tiempo real transcurrido, no config actual
Jun 22, 2026
0ca585f
fix: resolver SEC-003, SEC-005, ARQ-001-004, DB-002
Jun 24, 2026
8c99763
fix: resolver DEP-001/002, CI-001/003, UX-001/002, ARQ-006, DB-001, D…
Jun 24, 2026
94b8b4c
Bump Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.…
dependabot[bot] Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Build (warnings as errors)
run: dotnet build "EpycusApp.csproj" --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore -warnaserror

- name: Run unit tests
run: dotnet test "EpycusApp.Tests/EpycusApp.Tests.csproj" --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore --verbosity normal

# ── JOB 2: Build y publish ──────────────────────────────────────────
build:
name: Build & Publish
Expand Down Expand Up @@ -117,8 +120,21 @@ jobs:
target: ${{ env.VPS_APP_PATH }}
strip_components: 1

# Aplicar migraciones EF Core
- name: Apply EF Core migrations
uses: appleboy/ssh-action@v1.0.3
continue-on-error: true
with:
host: ${{ env.VPS_HOST }}
username: ${{ env.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ env.VPS_PORT }}
script: |
cd ${{ env.VPS_APP_PATH }}
sudo dotnet ef database update --no-build 2>&1 || echo "⚠️ Migraciones no aplicadas (pueden no requerirse)"
echo "✅ Migraciones ejecutadas"

# Reinicio seguro
- name: Restart service
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ env.VPS_HOST }}
Expand Down
3 changes: 1 addition & 2 deletions Ayudantes/ConstantesGamificacion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ public static class ConstantesGamificacion
public const int XP_MISION_BAJA = 30;
public const int XP_MISION_MEDIA = 50;
public const int XP_MISION_ALTA = 80;
public const int XP_BASE_POMODORO = 15;
public const int XP_POMODORO_POR_CICLO = 15;
public const int XpBasePomodoro = 15;
public const int NIVEL_MAXIMO = 20;
}
}
6 changes: 3 additions & 3 deletions Ayudantes/DebeSerVerdaderoAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
namespace EpycusApp.Ayudantes;

/// <summary>
/// Valida que un checkbox booleano esté marcado como true.
/// Reemplaza [Range(typeof(bool), "true", "true")] que falla en validación
/// Valida que un checkbox booleano esté marcado como true.
/// Reemplaza [Range(typeof(bool), "true", "true")] que falla en validación
/// cliente por incompatibilidad entre "True" (.NET) y "true" (HTML/JS).
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DebeSerVerdaderoAttribute : ValidationAttribute, IClientModelValidator
{
public DebeSerVerdaderoAttribute()
{
ErrorMessage = "Debe aceptar los términos";
ErrorMessage = "Debe aceptar los términos";
}

public override bool IsValid(object? value)
Expand Down
14 changes: 14 additions & 0 deletions Ayudantes/FormateadorTiempo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace EpycusApp.Ayudantes
{
public static class FormateadorTiempo
{
public static string FormatearSegundos(int segundos)
{
if (segundos < 60) return $"{segundos}s";
if (segundos < 3600) return $"{segundos / 60}min";
int horas = segundos / 3600;
int mins = (segundos % 3600) / 60;
return mins > 0 ? $"{horas}h {mins}m" : $"{horas}h";
}
}
}
4 changes: 2 additions & 2 deletions Controllers/AdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public async Task<IActionResult> ActivarSuscripcion(int usuarioId)
{
var adminId = ObtenerUsuarioId();
await _servicioAdmin.ActivarSuscripcion(usuarioId, adminId);
TempData["Exito"] = "Suscripción activada correctamente.";
TempData["Exito"] = "Suscripción activada correctamente.";
return RedirectToAction(nameof(Usuarios));
}

Expand All @@ -130,7 +130,7 @@ public async Task<IActionResult> ActivarSuscripcion(int usuarioId)
public async Task<IActionResult> DesactivarSuscripcion(int usuarioId)
{
await _servicioAdmin.DesactivarSuscripcion(usuarioId);
TempData["Exito"] = "Suscripción desactivada correctamente.";
TempData["Exito"] = "Suscripción desactivada correctamente.";
return RedirectToAction(nameof(Usuarios));
}

Expand Down
36 changes: 13 additions & 23 deletions Controllers/AjustesController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using EpycusApp.Servicios.Interfaces;
using EpycusApp.Servicios.Interfaces;
using EpycusApp.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace EpycusApp.Controllers;
Expand All @@ -18,19 +18,10 @@ public AjustesController(IServicioPerfil servicioPerfil, IServicioAutenticacion
_servicioAutenticacion = servicioAutenticacion;
}

public async Task<IActionResult> Index()
[HttpGet]
public IActionResult Index()
{
var usuarioId = ObtenerUsuarioId();
var perfil = await _servicioPerfil.ObtenerPerfilCompletoAsync(usuarioId);

if (perfil == null)
{
return NotFound();
}

perfil.PersonajesDisponibles = await _servicioPerfil.ObtenerPersonajesDisponiblesAsync(usuarioId);
ViewBag.Carreras = await _servicioAutenticacion.ObtenerCarrerasActivas();
return View(perfil);
return RedirectToAction("Index", "Perfil");
}

[HttpPost]
Expand All @@ -41,22 +32,22 @@ public async Task<IActionResult> ActualizarPerfil(ActualizarPerfilViewModel mode
TempData["Error"] = string.Join("; ", ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage));
return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Perfil");
}

var usuarioId = ObtenerUsuarioId();
var resultado = await _servicioPerfil.ActualizarPerfilAsync(usuarioId, modelo);

if (resultado.EsExitoso)
{
TempData["Mensaje"] = "Perfil actualizado correctamente.";
TempData["Exito"] = "Perfil actualizado correctamente.";
}
else
{
TempData["Error"] = resultado.Mensaje ?? "No se pudo actualizar el perfil.";
}

return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Perfil");
}

[HttpPost]
Expand All @@ -65,10 +56,9 @@ public async Task<IActionResult> CambiarContrasena(CambiarContrasenaViewModel mo
if (!ModelState.IsValid)
{
TempData["Error"] = "Por favor verifica los datos ingresados.";
return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Perfil");
}

var usuarioId = ObtenerUsuarioId();
var correo = User.FindFirstValue(ClaimTypes.Email);

var resultado = await _servicioAutenticacion.CambiarContrasenaAsync(
Expand All @@ -79,14 +69,14 @@ public async Task<IActionResult> CambiarContrasena(CambiarContrasenaViewModel mo

if (resultado.EsExitoso)
{
TempData["Mensaje"] = "Contraseña cambiada correctamente.";
TempData["Exito"] = "Contraseña cambiada correctamente.";
}
else
{
TempData["Error"] = resultado.Mensaje ?? "No se pudo cambiar la contraseña.";
TempData["Error"] = resultado.Mensaje ?? "No se pudo cambiar la contraseña.";
}

return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Perfil");
}

[HttpPost]
Expand Down
101 changes: 101 additions & 0 deletions Controllers/Api/ApiAdminController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using EpycusApp.Ayudantes;
using EpycusApp.DTOs;
using EpycusApp.Servicios.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace EpycusApp.Controllers.Api
{
[ApiController]
[Route("api/v1/admin")]
[Authorize]
[EnableRateLimiting("Api")]
public class ApiAdminController : BaseApiController
{
private readonly IServicioAdmin _servicioAdmin;
private readonly IServicioAutenticacion _servicioAutenticacion;

public ApiAdminController(IServicioAdmin servicioAdmin, IServicioAutenticacion servicioAutenticacion)
{
_servicioAdmin = servicioAdmin;
_servicioAutenticacion = servicioAutenticacion;
}

[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] AdminLoginRequest request)
{
var (exito, mensaje, token, refreshToken) = await _servicioAutenticacion.Login(request.Correo, request.Contrasena);

if (!exito)
return Ok(RespuestaApi<MensajeResponseDto>.Fallida(mensaje));

return Ok(RespuestaApi<AdminLoginResponseDto>.Exitosa(new AdminLoginResponseDto { Token = token!, RefreshToken = refreshToken!, Mensaje = mensaje }));
}

[HttpGet("usuarios")]
public async Task<IActionResult> Usuarios()
{
var usuarios = await _servicioAdmin.ObtenerTodosUsuarios();
return Ok(RespuestaApi<object>.Exitosa(usuarios));
}

[HttpGet("usuarios/{id}")]
public async Task<IActionResult> UsuarioPorId(int id)
{
var usuario = await _servicioAdmin.ObtenerUsuarioPorId(id);
if (usuario == null)
return NotFound(RespuestaApi<MensajeResponseDto>.Fallida("Usuario no encontrado"));
return Ok(RespuestaApi<object>.Exitosa(usuario));
}

[HttpPost("usuarios/{usuarioId}/suscripcion/activar")]
public async Task<IActionResult> ActivarSuscripcion(int usuarioId)
{
var adminId = ObtenerUsuarioId()!.Value;
await _servicioAdmin.ActivarSuscripcion(usuarioId, adminId);
return Ok(RespuestaApi<SuccessResponseDto>.Exitosa(new SuccessResponseDto()));
}

[HttpPost("usuarios/{usuarioId}/suscripcion/desactivar")]
public async Task<IActionResult> DesactivarSuscripcion(int usuarioId)
{
await _servicioAdmin.DesactivarSuscripcion(usuarioId);
return Ok(RespuestaApi<SuccessResponseDto>.Exitosa(new SuccessResponseDto()));
}

[HttpGet("frases")]
public async Task<IActionResult> Frases()
{
var frases = await _servicioAdmin.ObtenerFrases();
return Ok(RespuestaApi<object>.Exitosa(frases));
}

[HttpPost("frases")]
public async Task<IActionResult> CrearFrase([FromBody] CrearFraseRequest request)
{
await _servicioAdmin.CrearFrase(request.Frase, request.Autor ?? "Anonimo");
return Created(string.Empty, RespuestaApi<SuccessResponseDto>.Exitosa(new SuccessResponseDto()));
}

[HttpDelete("frases/{id}")]
public async Task<IActionResult> EliminarFrase(int id)
{
await _servicioAdmin.EliminarFrase(id);
return Ok(RespuestaApi<SuccessResponseDto>.Exitosa(new SuccessResponseDto()));
}
}

public class AdminLoginRequest
{
public string Correo { get; set; } = string.Empty;
public string Contrasena { get; set; } = string.Empty;
}

public class CrearFraseRequest
{
public string Frase { get; set; } = string.Empty;
public string? Autor { get; set; }
}
}
Loading