Nuevo modulo backend/src/modules/vault/: contraseña y notas cifradas
en el servidor (AES-256-GCM, mismo mecanismo ya usado para la clave
SMTP). Acceso al menu protegido con un PIN de 4 digitos independiente
del login (token corto propio, header X-Vault-Token, nunca el JWT de
sesion). El PIN no participa del cifrado -- cambiarlo o restablecerlo
por correo nunca afecta las contraseñas ya guardadas. Privado por
usuario, sin excepcion. Cada accion sensible (crear/editar/eliminar/
revelar/desbloquear) queda en audit_log. Compartir con otro usuario
queda para una proxima version.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bug de seguridad real corregido: cualquier usuario autenticado veia
TODOS los proyectos (listado, detalle, actividades, tareas,
comentarios, adjuntos, KPIs del panel), sin importar si era miembro.
Ahora nadie ve un proyecto ajeno sin que su dueno se lo comparta --
los roles de administracion (superadmin/admin/it_manager) mantienen
visibilidad total para soporte.
Nuevo modelo de permisos por proyecto, simple y no granular:
- Solo el dueno original puede compartir un proyecto (agregar/quitar
miembros, cambiar su nivel) -- ni un admin del proyecto ni un rol
global pueden hacerlo por el.
- Dos niveles: Lectura (solo ver) y Escritura (crear/editar/eliminar
actividades y tareas -- nunca el proyecto en si: no lo borra, no
edita sus datos, no crea proyectos nuevos).
Actividades y tareas pasan a borrado logico (antes se borraban
fisico sin poder recuperar) -- nueva papelera por proyecto con
restaurar.
Incluye tambien el filtro "Usuario" en Proyectos (probado en dev en
un paso anterior), que va de la mano con este cambio de visibilidad.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nuevo menu "Notas" en el sidebar: crear, editar y eliminar apuntes rapidos, con fecha y hora de
creacion (y de ultima edicion si aplica). Siempre privadas de cada usuario -- una nota de otro
usuario responde 404, no 403, para no revelar que existe. Sin restriccion de rol.
Nuevo notes (tabla) + GET/POST/PUT/DELETE /api/notes. Probado de punta a punta en dev: crear,
editar, borrar y el formato de fecha/hora funcionan correctamente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Se asigna sola, sin etiquetar nada a mano: backend y errores de render de React (rompen toda la
pantalla) = alta; errores JS sueltos = media; promesas rechazadas sin manejar = baja (suelen ser
fallos de red puntuales, no criticos). Nuevo filtro en el panel ademas del de origen ya existente.
Migracion 014_error_logs_severity.sql (ALTER TABLE ADD COLUMN IF NOT EXISTS). Probado en dev: las
tres severidades se asignan correctamente segun el origen, un valor invalido del frontend cae en
media, y el filtro combinado con origen funciona.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Panel nuevo en Configuracion ("Errores del sistema", solo superadmin): errores de backend
(excepciones 500 no manejadas, con stack/ruta/metodo/usuario) y de frontend (errores JS y
promesas sin catch en el navegador del usuario, antes invisibles para cualquiera mas alla de esa
persona). Filtro por origen, boton para limpiar el historial.
Error Boundary de React: si algo rompe el render, ya no queda pantalla en blanco -- aviso simple
con boton de recargar, y el error queda reportado igual.
Nuevo error_logs (tabla, se recorta sola a 1000 filas) + GET/DELETE /api/error-logs (superadmin)
+ POST /api/error-logs (publico, rate-limited, para reportar incluso sin sesion).
Probado de punta a punta en dev: error real de backend (500) capturado con stack completo y
usuario correcto, error JS global y promesa rechazada capturados desde el navegador, panel
muestra ambos y el boton de limpiar funciona. Se encontro y corrigio un bug real en el camino: la
identificacion de usuario opcional leia jwt.payload.id en vez de .sub (como realmente lo firma
auth/service.js), asi que nunca identificaba a nadie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/install.sh automatiza el primer despliegue en un servidor Debian/Ubuntu nuevo: verifica
requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres), pregunta dominio,
puertos y base de datos (MariaDB local auto-instalada o un servidor existente), genera
backend/.env con secretos aleatorios, corre migraciones + catalogos, compila el frontend y
crea/arranca los servicios systemd. No crea usuarios: el primer registro publico se vuelve
superadmin solo (bootstrap ya existente).
Probando el instalador de punta a punta contra una base de datos recien creada goo un bug real:
la migracion 006_seed_it_baseline.sql (datos de ejemplo especificos de LinkedCorp) fallaba por
foreign key al no existir todavia catalogos ni usuario. Corregido para que detecte esa situacion
y quede inerte en instalaciones nuevas, sin tocar el comportamiento ya probado en dev/prod.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hook reutilizable useDragReorder (drag & drop nativo HTML5, sin
libreria nueva) usado en dos lugares: ListView (opt-in via onReorder,
deshabilitado mientras haya un ordenamiento por columna activo) para
el listado de actividades de un proyecto, y el listado de tareas de
una actividad. Orden persistido en sort_order (activities y tasks,
migracion 012, posiciones siempre 1..N).
Fix real encontrado durante las pruebas: sincronizar el estado interno
del hook por la REFERENCIA del arreglo de entrada entraba en loop
infinito de renders cuando el llamador pasa "algo || []" (un arreglo
nuevo en cada render mientras los datos cargan). Corregido para
sincronizar por el contenido (ids), no por referencia.
Probado en dev con curl (persistencia del orden) y Playwright
(arrastrar filas en el navegador, orden visible y guardado
correctamente, sin errores de consola tras el fix).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vacio = se repite sin fin (comportamiento previo). Al completar un
ciclo cuyo proximo vencimiento supera "hasta", la tarea se desactiva
sola. Probado en dev con curl (3 ciclos seguidos, se desactiva justo
al superar la fecha) y validacion de que "hasta" no puede ser anterior
al proximo vencimiento.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Menu propio en la barra lateral, independiente de Proyectos:
- Periodicidad configurable (cada N dias/meses/años), proximo
vencimiento, responsable, proyecto de origen opcional.
- "Completar ciclo" avanza el proximo vencimiento sumando la
periodicidad y limpia el recordatorio ya enviado.
- Recordatorio por correo automatico N dias antes del vencimiento
(default 7), a una hora del dia configurable. Un job en el propio
proceso del backend (setInterval cada 15 min) revisa y envia, sin
infraestructura nueva (cron del SO, cola, etc).
- Desde la pestaña Tareas de una actividad, cada tarea tiene un boton
"Convertir en recurrente" que la promueve (la tarea original no se
borra).
Probado en dev con curl (crear, completar ciclo avanza next_due_date,
promover desde una tarea real, disparo real del job de recordatorios
verificado forzando fecha/hora, segunda corrida no reenvia) y Playwright
(listado, popup de creacion, popup de promocion anidado sobre el detalle
de actividad) sin errores de consola.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
canWriteProject/ListView ya soportaba ordenar por cualquier columna con
clic en el encabezado; solo Prioridad ordenaba alfabetico por el texto
en vez de por severidad real. Se agrega pr.level a la consulta y
sortValue en la columna.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Migracion 006 de una sola vez (marcador en system_settings, no se repite
en deploys futuros): reemplaza el proyecto de prueba "Sistema ERP" por
los 6 proyectos reales de TI clasificados a partir del listado que
compartio el usuario, y borra la cuenta pflores@hotmail.es (autorregistro
nunca verificado). Probado en dev incluyendo la idempotencia (un proyecto
creado despues de correr la migracion sobrevive a un segundo "npm run
migrate").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Con 001_initial.sql creando `tasks` directamente para instalaciones
nuevas, el destino ya existe para cuando corre 005 en una instalacion
que aun tenia `subtasks` -- RENAME TABLE fallaba con "Table already
exists" (se vio en produccion, sin perdida de datos: subtasks estaba
vacia). Corregido para copiar los datos de subtasks a tasks (con id
explicito) y borrar subtasks, corrigiendo el AUTO_INCREMENT despues.
Probado con datos reales simulados en dev, idempotente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Actividad -> Tarea, sin nivel "sub" intermedio (no hay una entidad "Tarea"
de la que estas fueran sub-, asi que el prefijo sobraba). Tabla subtasks
-> tasks (RENAME TABLE idempotente en la migracion 005; 001_initial.sql
tambien actualizado para que una instalacion nueva cree la tabla ya con
el nombre final, sin pasar por el nombre viejo). Modulo backend, rutas,
servicio frontend, pestaña del detalle de actividad, i18n y documentacion
actualizados. Sin cambios de comportamiento, solo nombres.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Al agregar un miembro, el popup ahora permite elegir un usuario existente
o escribir un correo nuevo. Si ese correo no pertenece a nadie, se crea
una cuenta en estado 'invited' (nombre/clave placeholder) y se le manda
un correo con un enlace de un solo uso para que complete su registro
(elige su nombre y clave). La fila de project_members se crea de una vez
al invitar, asi que en cuanto acepta ya es miembro sin pasos adicionales.
A diferencia del autorregistro publico, no pasa por aprobacion de un
admin -- quien lo agrego a su proyecto ya lo vetó.
Nuevos endpoints publicos GET /api/auth/invite y POST /api/auth/accept-invite.
Nueva pagina AcceptInvitePage (/aceptar-invitacion). Migracion 004 agrega
el status 'invited' al enum de users.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- DELETE /api/projects/🆔 eliminacion logica (deleted_at/deleted_by), mismo
permiso que editar el proyecto. Popup de confirmacion antes de aplicarla.
- Papelera (GET /api/projects?trash=true, POST /:id/restore, DELETE
/:id/permanent) restringida al superadmin; el borrado fisico exige que el
proyecto ya este en la papelera y arrastra actividades/comentarios/adjuntos
por ON DELETE CASCADE.
- Dashboard (KPIs, actividades pendientes/calendario, proyectos por estado)
excluye proyectos eliminados logicamente.
- Columna "Creado" en el listado de proyectos, ordenado por defecto de mas
nuevo a mas antiguo (ListView admite initialSort ademas del orden por clic
en cabecera que ya tenia).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- POST /api/auth/register: primer usuario del sistema -> superadmin con sesion
directa (bootstrap, sin correo ni aprobacion); los siguientes quedan
pending_verification y reciben un correo con enlace de un solo uso (24h).
- GET /api/auth/verify-email: pasa a pending_approval y notifica in-app a los
admin/superadmin activos.
- admin/superadmin aprueban (POST /api/users/:id/approve, eligen rol y
departamento) o rechazan (POST /api/users/:id/reject, borra y audita el
email) desde Organizacion -> Usuarios (nuevo badge de estado + acciones).
- login() da mensaje especifico segun el status de la cuenta en vez del
generico "credenciales invalidas".
- Recuperacion de clave (forgot/reset-password): respuesta siempre generica,
token de 1 hora de un solo uso.
- Nuevo modulo settings: SMTP configurable SOLO por superadmin desde
Configuracion -> Correo (SMTP); clave cifrada AES-256-GCM (ENCRYPTION_KEY)
en la tabla system_settings, nunca expuesta por la API. sendMail() degrada
a modo dev (loguea en vez de enviar) si no hay SMTP configurado, y nunca
deja que un fallo de envio tumbe al que lo llama.
- Migracion 002_registration_and_smtp.sql: rol superadmin, columnas de ciclo
de vida en users (status, tokens), tabla system_settings, y promueve al
usuario mas antiguo del sistema a superadmin.
- Fix: React.StrictMode disparaba el efecto de VerifyEmailPage dos veces; la
segunda llamada (token ya consumido) mostraba "enlace invalido" pese a que
la verificacion ya habia funcionado. Se agrega una guarda por token.
- Fix (deuda tecnica v1.0.0): se reordena el montaje de rutas en app.js —
subtasksRoutes (mount generico en /api) va al final, para no interceptar
con su requireAuth interno una request sin token dirigida a un router mas
especifico registrado despues.
- Documentacion actualizada: AGENTS.md, SPEC.md, API.md, BASE_DE_DATOS.md,
CONFIGURACION.md, ESTADO.md, CHANGELOG.md. Version 1.1.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>