Auditoría de /portal/dashboard. Cuatro problemas, todos en el mismo bucle:
1. El avance se veía un render atrasado. ActualizarProgresoProyecto corría
DESPUÉS de cargar los proyectos: escribía el valor nuevo en la base pero
los structs ya cargados seguían con el viejo, que es lo que se renderiza.
El usuario veía el cálculo de la visita anterior.
2. N+1 con escrituras en un GET: dos COUNT y un UPDATE por proyecto. Con 10
proyectos, 30 consultas y 10 escrituras por cada carga del dashboard —
incluyendo las de cualquier bot que pase. Ahora es UNA consulta agrupada
y ninguna escritura; los caminos que tocan una fase ya mantienen la
columna al día, así que recalcular en el GET no aportaba nada.
3. Orden aleatorio de los grupos: se recorría un map de Go, así que un
partner veía sus clientes en distinto orden en cada recarga.
4. isPartner se decidía con u.Rol mientras el alcance se decidía con
Role.EsPortalPartner. Desincronizados, un usuario veía proyectos de
varios clientes sin agrupar, o la vista agrupada vacía. Ahora hay una
sola definición (PortalUser.EsPartner) con test de los dos sentidos.
Además, el error de carga se descartaba con `_` y el usuario terminaba
viendo "no tenés proyectos", indistinguible de una caída de la base.
Sin hallazgos de seguridad: el chequeo de acceso por cliente está en todas
las rutas, la sesión revalida Activo en cada request, y las plantillas no
usan x-html ni template.HTML, así que Go escapa todo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
El cliente ya veía el roadmap en pantalla pero no tenía cómo pasárselo a
alguien que no entra al portal.
El .xlsx se genera con archive/zip + encoding/xml de la stdlib en vez de
sumar una dependencia de Excel para una sola pantalla. Un CSV renombrado
no servía: en Excel en español el separador y los acentos se rompen, y
esto es un archivo que el cliente le entrega a un tercero.
Detalles que hacen que Excel lo acepte y se lea bien: inline strings (sin
tabla sharedStrings, así ningún índice puede apuntar a la cadena
equivocada), encabezado en negrita y congelado, anchos de columna según
el contenido, y nombre de hoja saneado (Excel rechaza el archivo si pasa
de 31 caracteres o trae : \ / ? * [ ]).
El endpoint repite el chequeo de acceso de PortalProyecto: sin eso,
cualquiera con sesión de portal se bajaba el cronograma de otro cliente
adivinando el slug.
Verificado abriendo el archivo generado con openpyxl: 6 partes, CRC OK,
XML bien formado en todas, acentos y CJK intactos, negrita y panel
congelado donde corresponde.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Nuevo modelo PortalPasswordResetToken con token seguro (32 bytes, 1h vigencia)
- AutoMigrate del nuevo modelo en main.go
- SendPortalPasswordResetEmail con diseño consistente al app
- Handlers: PortalForgotPasswordPage/Post y PortalResetPasswordPost/Page
- Rutas públicas GET/POST /portal/forgot-password y /portal/reset-password
- Vistas forgot_password.html y reset_password.html con validación JS
- Enlace ¿Olvidaste tu contraseña? en login.html + soporte mensaje success
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>