Docs: Agregar documentación completa del sistema de notificaciones
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 9s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 9s
- Explicación de arquitectura y componentes - Guía de uso para desarrolladores - Ejemplos de código y casos de uso - Detalles de separación por usuario - Configuración de limpieza automática - Sincronización entre pestañas - FAQ y mejoras futuras
This commit is contained in:
557
docs/sistema-notificaciones.md
Normal file
557
docs/sistema-notificaciones.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# Sistema de Notificaciones
|
||||
|
||||
Sistema de notificaciones con historial persistente por usuario, implementado con localStorage y separación automática basada en el UID de Authentik.
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
- [Características Principales](#características-principales)
|
||||
- [Arquitectura del Sistema](#arquitectura-del-sistema)
|
||||
- [Componentes Principales](#componentes-principales)
|
||||
- [Uso Básico](#uso-básico)
|
||||
- [API del Backend](#api-del-backend)
|
||||
- [Estructura de Datos](#estructura-de-datos)
|
||||
- [Separación por Usuario](#separación-por-usuario)
|
||||
- [Limpieza Automática](#limpieza-automática)
|
||||
- [Sincronización entre Pestañas](#sincronización-entre-pestañas)
|
||||
|
||||
---
|
||||
|
||||
## Características Principales
|
||||
|
||||
✅ **Historial separado por usuario**: Cada usuario tiene su propio historial usando su UID de Authentik como clave
|
||||
|
||||
✅ **Persistencia local**: Las notificaciones se guardan en localStorage del navegador
|
||||
|
||||
✅ **Guardado automático de toasts**: Todos los toasts se guardan automáticamente en el historial
|
||||
|
||||
✅ **Auto-limpieza**: Notificaciones mayores a 30 días se eliminan automáticamente
|
||||
|
||||
✅ **Sincronización entre pestañas**: Los cambios se reflejan en tiempo real en todas las pestañas
|
||||
|
||||
✅ **Filtrado y búsqueda**: Filtrar por tipo (info, warning, success, error) y buscar por texto
|
||||
|
||||
✅ **Múltiples orígenes**: Soporta notificaciones desde toasts, backend y manuales
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura del Sistema
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Aplicación Nuxt │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ useNotifications() │ │
|
||||
│ │ - Gestión de notificaciones │ │
|
||||
│ │ - Filtrado y búsqueda │ │
|
||||
│ │ - Limpieza automática │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ useToast() │ │
|
||||
│ │ - Wrapper que intercepta toasts │ │
|
||||
│ │ - Guarda automáticamente en historial │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ API Endpoints (Backend) │ │
|
||||
│ │ - /api/notifications/send │ │
|
||||
│ │ - /api/notifications/list │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ localStorage │
|
||||
│ notifications-{uid} │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### 1. Composable `useNotifications()`
|
||||
|
||||
**Ubicación:** `app/composables/useNotifications.ts`
|
||||
|
||||
El composable principal que gestiona todo el sistema de notificaciones.
|
||||
|
||||
**Funciones disponibles:**
|
||||
|
||||
```typescript
|
||||
const {
|
||||
notifications, // Ref<Notification[]> - Array reactivo de todas las notificaciones
|
||||
unreadCount, // ComputedRef<number> - Contador de notificaciones no leídas
|
||||
addNotification, // Agregar una notificación al historial
|
||||
markAsRead, // Marcar una notificación como leída
|
||||
markAllAsRead, // Marcar todas como leídas
|
||||
removeNotification, // Eliminar una notificación específica
|
||||
clearAll, // Eliminar todas las notificaciones
|
||||
cleanupOldNotifications, // Eliminar notificaciones > 30 días
|
||||
getFilteredNotifications, // Obtener notificaciones filtradas
|
||||
initialize // Inicializar el sistema (se llama automáticamente)
|
||||
} = useNotifications()
|
||||
```
|
||||
|
||||
### 2. Wrapper `useToast()`
|
||||
|
||||
**Ubicación:** `app/composables/useToast.ts`
|
||||
|
||||
Wrapper transparente que intercepta todos los toasts de Nuxt UI y los guarda automáticamente en el historial.
|
||||
|
||||
**Uso (sin cambios en código existente):**
|
||||
|
||||
```typescript
|
||||
const toast = useToast()
|
||||
|
||||
// Funciona exactamente igual que antes, pero ahora se guarda en el historial
|
||||
toast.add({
|
||||
title: 'Operación exitosa',
|
||||
description: 'Los datos se guardaron correctamente',
|
||||
color: 'success'
|
||||
})
|
||||
```
|
||||
|
||||
### 3. API del Backend
|
||||
|
||||
**Ubicación:** `server/api/notifications/`
|
||||
|
||||
Endpoints para crear y obtener notificaciones desde el servidor.
|
||||
|
||||
---
|
||||
|
||||
## Uso Básico
|
||||
|
||||
### Agregar una notificación manualmente
|
||||
|
||||
```typescript
|
||||
const { addNotification } = useNotifications()
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Proceso completado',
|
||||
message: 'El reporte se generó correctamente',
|
||||
origin: 'manual',
|
||||
metadata: {
|
||||
reportId: 123,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Marcar notificación como leída
|
||||
|
||||
```typescript
|
||||
const { markAsRead } = useNotifications()
|
||||
|
||||
markAsRead('notification-id-123')
|
||||
```
|
||||
|
||||
### Obtener notificaciones filtradas
|
||||
|
||||
```typescript
|
||||
const { getFilteredNotifications } = useNotifications()
|
||||
|
||||
// Solo notificaciones de error no leídas
|
||||
const errorNotifications = getFilteredNotifications({
|
||||
type: ['error'],
|
||||
read: false
|
||||
})
|
||||
|
||||
// Buscar por texto
|
||||
const searchResults = getFilteredNotifications({
|
||||
search: 'reporte'
|
||||
})
|
||||
|
||||
// Limitar resultados
|
||||
const latest = getFilteredNotifications({
|
||||
limit: 10
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API del Backend
|
||||
|
||||
### Enviar notificación desde el servidor
|
||||
|
||||
**Endpoint:** `POST /api/notifications/send`
|
||||
|
||||
```typescript
|
||||
// Desde cualquier endpoint del servidor
|
||||
const response = await $fetch('/api/notifications/send', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
targetUserId: 'uid-del-usuario-destino',
|
||||
type: 'info',
|
||||
title: 'Nueva actualización',
|
||||
message: 'Hay una nueva versión disponible',
|
||||
metadata: {
|
||||
version: '2.0.0',
|
||||
changelog: 'https://...'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Parámetros:**
|
||||
|
||||
- `targetUserId` (string, requerido): UID del usuario destino
|
||||
- `type` (string, requerido): Tipo de notificación (`'info'`, `'warning'`, `'success'`, `'error'`)
|
||||
- `title` (string, requerido): Título de la notificación
|
||||
- `message` (string, opcional): Mensaje descriptivo
|
||||
- `metadata` (object, opcional): Datos adicionales
|
||||
|
||||
### Obtener notificaciones pendientes
|
||||
|
||||
**Endpoint:** `GET /api/notifications/list`
|
||||
|
||||
```typescript
|
||||
// Desde el cliente (automático)
|
||||
const { data } = await $fetch('/api/notifications/list')
|
||||
|
||||
console.log(data.notifications) // Array de notificaciones pendientes
|
||||
console.log(data.count) // Número de notificaciones
|
||||
```
|
||||
|
||||
> **Nota:** Este endpoint usa una cola en memoria. En producción, considera usar Redis o una base de datos para persistencia.
|
||||
|
||||
---
|
||||
|
||||
## Estructura de Datos
|
||||
|
||||
### Interfaz `Notification`
|
||||
|
||||
```typescript
|
||||
interface Notification {
|
||||
id: string // ID único generado automáticamente
|
||||
type: 'info' | 'warning' | 'success' | 'error'
|
||||
title: string // Título de la notificación
|
||||
message: string // Mensaje descriptivo
|
||||
timestamp: number // Timestamp en milisegundos
|
||||
read: boolean // Estado de lectura
|
||||
origin: 'toast' | 'backend' | 'manual' // Origen de la notificación
|
||||
metadata?: Record<string, any> // Datos adicionales opcionales
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo de notificación guardada
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1730328465123-a3f8d9e2b",
|
||||
"type": "success",
|
||||
"title": "Tema guardado",
|
||||
"message": "Los cambios se aplicaron correctamente",
|
||||
"timestamp": 1730328465123,
|
||||
"read": false,
|
||||
"origin": "toast",
|
||||
"metadata": {
|
||||
"icon": "i-lucide-check",
|
||||
"color": "green",
|
||||
"hasActions": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Separación por Usuario
|
||||
|
||||
El sistema utiliza el **UID de Authentik** como clave única para separar las notificaciones de cada usuario.
|
||||
|
||||
### Cómo funciona
|
||||
|
||||
1. **Autenticación con Authentik:**
|
||||
- El usuario se autentica mediante Authentik Proxy Outpost
|
||||
- El header `x-authentik-uid` contiene el UID único del usuario
|
||||
|
||||
2. **Clave de localStorage:**
|
||||
```typescript
|
||||
const storageKey = `notifications-${user.uid}`
|
||||
// Ejemplo: "notifications-a3f8d9e2b4c1a5d6"
|
||||
```
|
||||
|
||||
3. **Cambio de usuario:**
|
||||
- Al detectar un cambio en el UID, el sistema automáticamente:
|
||||
- Guarda las notificaciones del usuario anterior
|
||||
- Carga las notificaciones del nuevo usuario
|
||||
- Limpia el estado en memoria
|
||||
|
||||
### Ejemplo práctico
|
||||
|
||||
```
|
||||
Usuario A (UID: abc123):
|
||||
localStorage["notifications-abc123"] = [notif1, notif2, notif3]
|
||||
|
||||
Usuario B (UID: xyz789):
|
||||
localStorage["notifications-xyz789"] = [notif4, notif5]
|
||||
|
||||
// Cada usuario solo ve sus propias notificaciones
|
||||
// Incluso si usan el mismo navegador en la misma máquina
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limpieza Automática
|
||||
|
||||
El sistema elimina automáticamente las notificaciones antiguas para evitar acumulación excesiva.
|
||||
|
||||
### Configuración
|
||||
|
||||
```typescript
|
||||
// En useNotifications.ts
|
||||
const RETENTION_DAYS = 30 // Días de retención
|
||||
```
|
||||
|
||||
### Cuándo se ejecuta
|
||||
|
||||
1. **Al iniciar la aplicación:**
|
||||
- Se ejecuta `cleanupOldNotifications()` en `app.vue`
|
||||
- Elimina notificaciones mayores a 30 días
|
||||
|
||||
2. **Al cargar notificaciones:**
|
||||
- Las notificaciones antiguas se filtran automáticamente
|
||||
- Solo se cargan las recientes
|
||||
|
||||
### Modificar el período de retención
|
||||
|
||||
Si necesitas cambiar el período de retención, modifica la constante:
|
||||
|
||||
```typescript
|
||||
// app/composables/useNotifications.ts
|
||||
const RETENTION_DAYS = 60 // Cambiar a 60 días
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sincronización entre Pestañas
|
||||
|
||||
El sistema mantiene sincronizadas todas las pestañas del navegador usando el evento `storage`.
|
||||
|
||||
### Cómo funciona
|
||||
|
||||
```typescript
|
||||
// Cuando cambia el localStorage en una pestaña...
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === storageKey && event.newValue) {
|
||||
// ...todas las demás pestañas se actualizan automáticamente
|
||||
notifications.value = JSON.parse(event.newValue)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Casos de uso
|
||||
|
||||
1. **Usuario agrega notificación en Pestaña A:**
|
||||
- Se guarda en localStorage
|
||||
- Pestaña B y C reciben el evento `storage`
|
||||
- Todas las pestañas muestran la nueva notificación
|
||||
|
||||
2. **Usuario elimina notificación en Pestaña B:**
|
||||
- Se actualiza localStorage
|
||||
- Pestaña A y C se sincronizan automáticamente
|
||||
|
||||
---
|
||||
|
||||
## Página de Notificaciones
|
||||
|
||||
**Ubicación:** `app/pages/notifications.vue`
|
||||
|
||||
### Características
|
||||
|
||||
- ✅ Lista completa de notificaciones del usuario actual
|
||||
- ✅ Búsqueda por texto (título y mensaje)
|
||||
- ✅ Filtros por tipo (info, warning, success, error)
|
||||
- ✅ Marcar como leídas (individual o todas)
|
||||
- ✅ Eliminar (individual o todas)
|
||||
- ✅ Indicadores de tiempo relativo ("hace 2 horas", "hace 3 días")
|
||||
- ✅ Badges de origen (toast, backend, manual)
|
||||
- ✅ Estados vacíos informativos
|
||||
|
||||
### Badge en Sidebar
|
||||
|
||||
**Ubicación:** `app/components/app/AppSidebar.vue`
|
||||
|
||||
- Muestra el número de notificaciones no leídas
|
||||
- Se oculta automáticamente cuando no hay notificaciones
|
||||
- Indicador visual (punto rojo) cuando hay notificaciones pendientes
|
||||
|
||||
---
|
||||
|
||||
## Inicialización del Sistema
|
||||
|
||||
**Ubicación:** `app/app.vue`
|
||||
|
||||
```typescript
|
||||
// Se inicializa automáticamente al montar la aplicación
|
||||
const { initialize, cleanupOldNotifications } = useNotifications()
|
||||
|
||||
onMounted(() => {
|
||||
initialize() // Inicializar el sistema
|
||||
cleanupOldNotifications() // Limpiar notificaciones antiguas
|
||||
})
|
||||
```
|
||||
|
||||
### Flujo de inicialización
|
||||
|
||||
1. **App se monta** → `onMounted()` se ejecuta
|
||||
2. **Usuario autenticado** → Se obtiene el UID de Authentik
|
||||
3. **Carga desde localStorage** → Se cargan notificaciones del usuario
|
||||
4. **Limpieza automática** → Se eliminan notificaciones > 30 días
|
||||
5. **Configurar sincronización** → Se activa el listener de `storage`
|
||||
6. **Sistema listo** → El usuario puede ver su historial
|
||||
|
||||
---
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
### 1. Notificación de operación exitosa
|
||||
|
||||
```typescript
|
||||
// Desde cualquier componente
|
||||
const toast = useToast()
|
||||
|
||||
async function saveData() {
|
||||
try {
|
||||
await $fetch('/api/save', { method: 'POST', body: data })
|
||||
|
||||
// Se guarda automáticamente en el historial
|
||||
toast.add({
|
||||
title: 'Datos guardados',
|
||||
description: 'Los cambios se guardaron correctamente',
|
||||
color: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: 'Error al guardar',
|
||||
description: error.message,
|
||||
color: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Notificación desde el backend
|
||||
|
||||
```typescript
|
||||
// server/api/generate-report.post.ts
|
||||
export default defineEventHandler(async (event) => {
|
||||
const headers = getHeaders(event)
|
||||
const userId = headers['x-authentik-uid']
|
||||
|
||||
// Generar el reporte...
|
||||
const report = await generateReport()
|
||||
|
||||
// Notificar al usuario
|
||||
await $fetch('/api/notifications/send', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
targetUserId: userId,
|
||||
type: 'success',
|
||||
title: 'Reporte generado',
|
||||
message: `El reporte ${report.name} está listo para descargar`,
|
||||
metadata: {
|
||||
reportId: report.id,
|
||||
downloadUrl: report.url
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, report }
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Consultar historial programáticamente
|
||||
|
||||
```typescript
|
||||
const { getFilteredNotifications } = useNotifications()
|
||||
|
||||
// Obtener últimas 5 notificaciones de error
|
||||
const recentErrors = getFilteredNotifications({
|
||||
type: ['error'],
|
||||
limit: 5
|
||||
})
|
||||
|
||||
// Mostrar en consola
|
||||
recentErrors.forEach(notif => {
|
||||
console.error(`[${notif.timestamp}] ${notif.title}: ${notif.message}`)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mejoras Futuras
|
||||
|
||||
### Posibles extensiones del sistema:
|
||||
|
||||
1. **Persistencia en base de datos:**
|
||||
- Migrar de localStorage a Supabase
|
||||
- Acceso desde cualquier dispositivo
|
||||
- Historial ilimitado
|
||||
|
||||
2. **Notificaciones push:**
|
||||
- Integración con Web Push API
|
||||
- Notificaciones incluso cuando la app está cerrada
|
||||
|
||||
3. **Categorías personalizadas:**
|
||||
- Permitir al usuario crear categorías
|
||||
- Filtros más granulares
|
||||
|
||||
4. **Acciones en notificaciones:**
|
||||
- Botones de acción (Descargar, Ver más, Abrir)
|
||||
- Ejecutar código al hacer clic
|
||||
|
||||
5. **Notificaciones programadas:**
|
||||
- Enviar notificaciones en fecha/hora específica
|
||||
- Recordatorios recurrentes
|
||||
|
||||
6. **Sistema de preferencias:**
|
||||
- Configurar qué notificaciones recibir
|
||||
- Silenciar notificaciones por tipo
|
||||
- Modo "No molestar"
|
||||
|
||||
---
|
||||
|
||||
## Preguntas Frecuentes
|
||||
|
||||
### ¿Las notificaciones se pierden al cerrar el navegador?
|
||||
|
||||
No, las notificaciones se guardan en localStorage y persisten entre sesiones.
|
||||
|
||||
### ¿Qué pasa si dos usuarios usan el mismo navegador?
|
||||
|
||||
Cada usuario tiene su propio historial separado por su UID de Authentik. Al cambiar de usuario (logout/login), el sistema carga automáticamente las notificaciones del nuevo usuario.
|
||||
|
||||
### ¿Cuántas notificaciones se pueden guardar?
|
||||
|
||||
El límite lo impone localStorage (aproximadamente 5-10 MB dependiendo del navegador). Con la limpieza automática de 30 días, es poco probable alcanzar este límite.
|
||||
|
||||
### ¿Se pueden recuperar notificaciones eliminadas?
|
||||
|
||||
No, una vez eliminadas del localStorage, no se pueden recuperar. Considera implementar persistencia en base de datos si necesitas esta funcionalidad.
|
||||
|
||||
### ¿Las notificaciones son privadas?
|
||||
|
||||
Sí, están almacenadas localmente en el navegador del usuario. Solo son accesibles por el usuario que las recibió y en el dispositivo donde se guardaron.
|
||||
|
||||
---
|
||||
|
||||
## Soporte
|
||||
|
||||
Para reportar problemas o sugerencias sobre el sistema de notificaciones:
|
||||
|
||||
1. Revisar los logs en la consola del navegador
|
||||
2. Verificar que el UID de Authentik esté presente
|
||||
3. Comprobar el localStorage en DevTools: `localStorage.getItem('notifications-{uid}')`
|
||||
4. Crear un issue en el repositorio con los detalles
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2025-10-30
|
||||
**Versión:** 1.0.0
|
||||
Reference in New Issue
Block a user