feat: steadier live updates and notices for public proposals

- The web pings the live connection, reconnects when it goes quiet and
  reloads what each screen shows after reconnecting.
- Dropping a card on a column it cannot go to puts it back.
- Owners and admins get a notice when a proposal arrives.
- The proposal limit lives in Redis, so it holds across instances.
- REST tells whether I voted; pasted images leave the attachment list.
- Deleting a project removes its Git integration; no lint warnings left.
- Reactions and the blocked badge are components of their own.
- Docs: what is done and what is left (chapter 10), Word and PDF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 19:57:21 +02:00
parent 74903f7842
commit c91df110f8
50 changed files with 615 additions and 321 deletions

View File

@@ -61,12 +61,12 @@ lee igual que un anónimo. Las subscriptions exigen sesión; con ella, también
`IDEA`, `title`, `description`, `author` opcional) crea un ticket con la etiqueta `propuesta`, a nombre de TaskStream,
en el primer estado, de tipo Bug o Feature (o el primer tipo activo). No se pide correo, porque quedaría a la vista. Contra
el spam, el campo `website` es una trampa (si llega relleno se responde como si nada y no se guarda) y cada dirección
envía como mucho 5 cada 15 minutos (429); el límite vive en memoria de cada instancia, así que con varias es
aproximado. La web tiene el formulario en `/p/{CLAVE}/proponer`, enlazado en la cabecera del proyecto.
envía como mucho 5 cada 15 minutos (429); el contador vive en Redis (`RateLimitStore`), así que vale igual con
varias instancias. Los propietarios y admins del proyecto reciben un aviso (`PROPOSED`). La web tiene el formulario en `/p/{CLAVE}/proponer`, enlazado en la cabecera del proyecto.
**Votos**: quien tenga sesión vota los tickets de un proyecto público, y los miembros los de uno privado
(`voteIssue(issueId, on)`, `PUT/DELETE /api/issues/{issue}/vote`). Cada ticket da `votes` y, en GraphQL, `voted` (si he
votado yo); la lista de quién votó no se publica.
(`voteIssue(issueId, on)`, `PUT/DELETE /api/issues/{issue}/vote`). Cada ticket da `votes` y `voted` (si he votado
yo; en REST, solo con token); la lista de quién votó no se publica.
`publicProjects` (GraphQL) y `GET /api/explore/projects` (REST, sin token) los listan. Al volver a privado, quien no es
miembro vuelve a recibir _not found_. Los enlaces a tickets de otros proyectos solo se muestran si el visitante puede
@@ -265,6 +265,7 @@ ningún agregado, solo cada uno lee y descarta las suyas.
| Alguien comenta | Quien lo tiene asignado y quien lo abrió | `COMMENTED` |
| Alguien responde a un comentario | Quien escribió el comentario | `REPLIED` |
| Te añaden a un proyecto | La persona añadida | `ADDED_TO_PROJECT` |
| Llega una propuesta del público | Propietarios y admins del proyecto | `PROPOSED` |
**Recordatorios** (`REMINDER`): cada hora la API mira los tickets sin terminar con fecha límite y avisa a quien lo
tiene asignado y a quien lo sigue el día antes, el mismo día y una vez cuando ya ha vencido (el día cambia en

View File

@@ -141,6 +141,10 @@ de identidad (`SessionTokenSource`) le pide el token para el cliente GraphQL.
fuente de verdad.
- Las subscriptions van por `graphql-ws` con el token en `connectionParams`; si la API responde `UNAUTHENTICATED`, la
sesión se cierra.
- El cliente manda un _ping_ cada 10 s y, si no llega el _pong_ en 5 s, cierra el socket y reconecta (una conexión
medio muerta, p. ej. tras suspender el portátil o pasar por un proxy, no se nota de otro modo). Al reconectar,
`RealtimeConnection.reconnected$` avisa y cada store vuelve a pedir lo que muestra, porque lo que pasó mientras
tanto no llega por las subscriptions.
- Las operaciones están en `libs/web/*/infrastructure/src/lib/graphql/*.graphql`. Tras cambiar la API o una operación:
```sh

View File

@@ -6,34 +6,26 @@ cuando hay pistas, por dónde empezar.
## 10.1 Para dar el producto por terminado
| Qué | Por qué | Por dónde empezar |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Averiguar por qué se pierde el tiempo real en producción | La ficha del ticket ya no depende de él (se recarga tras cada cambio propio), pero el tablero, la actividad y la campana sí | Sospecha: el WebSocket manda el token solo al conectar (`connectionParams`); cuando caduca o se renueva, la conexión sigue viva pero puede dejar de recibir. Dejar una pestaña abierta más que la vida del token |
| Pruebas de navegador en la integración continua | Los fallos del tablero (no se podía arrastrar entre columnas) y de la ficha (no se refrescaba) solo se vieron usándolo | Ya existe la receta: API real con usuarios simulados (`IdentityProvider` sustituido), `nx serve web` y Playwright con la sesión en `localStorage` (`task-stream.session`). Convertirla en `apps/web-e2e` |
| Copias de seguridad de Postgres y Mongo | Postgres guarda el event store, la fuente de verdad; Mongo se puede reconstruir desde él, Postgres no | Copias programadas en Dokploy y **una restauración de prueba** |
| Limpiar los datos de prueba de producción | DEMO, TEST y los tickets de las pruebas del MCP | Borrar los proyectos desde Configuración (borrado definitivo con su clave) |
| Registrar el webhook del cliente de contraseña en Pireon | Sin él, revocar un token de quien entra con contraseña no se nota hasta que caduca la caché | Solo si `PIREON_PASSWORD_CLIENT_ID` tiene valor: webhook en ese cliente y su secret en `PIREON_PASSWORD_WEBHOOK_SECRET` (ver [Autenticación](05-autenticacion.md)) |
| Una versión 1.0 | Un punto fijo al que volver | Desplegar todo, etiquetar `v1.0.0` y redactar la lista de cambios (las notas de versión de TaskStream dan el borrador) |
| Revisión de seguridad | Hay rutas sin sesión (proyectos públicos, escaparate, propuestas) y un MCP abierto a Internet | Permisos por rol, qué ve un anónimo, límites de peticiones (solo las propuestas tienen), CORS, cabeceras |
| Registro de errores en producción | Hoy un fallo solo se conoce si alguien avisa | Sentry (o similar) en la API, la web y el MCP |
| Qué | Por qué | Por dónde empezar |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Comprobar en producción que el tiempo real ya no se pierde | Ya hay _ping_ cada 10 s, reconexión si no hay respuesta y recarga de cada pantalla al reconectar, pero no se ha probado allí | Dejar una pestaña abierta más que la vida del token y tras suspender el portátil, y mover tickets desde otra. Si aún falla, mirar el token de `connectionParams`, que solo se manda al conectar |
| Pruebas de navegador en la integración continua | Los fallos del tablero (no se podía arrastrar entre columnas) y de la ficha (no se refrescaba) solo se vieron usándolo | Ya existe la receta: API real con usuarios simulados (`IdentityProvider` sustituido), `nx serve web` y Playwright con la sesión en `localStorage` (`task-stream.session`). Convertirla en `apps/web-e2e` |
| Copias de seguridad de Postgres y Mongo | Postgres guarda el event store, la fuente de verdad; Mongo se puede reconstruir desde él, Postgres no | Copias programadas en Dokploy y **una restauración de prueba** |
| Limpiar los datos de prueba de producción | DEMO, TEST y los tickets de las pruebas del MCP | Borrar los proyectos desde Configuración (borrado definitivo con su clave) |
| Registrar el webhook del cliente de contraseña en Pireon | Sin él, revocar un token de quien entra con contraseña no se nota hasta que caduca la caché | Solo si `PIREON_PASSWORD_CLIENT_ID` tiene valor: webhook en ese cliente y su secret en `PIREON_PASSWORD_WEBHOOK_SECRET` (ver [Autenticación](05-autenticacion.md)) |
| Una versión 1.0 | Un punto fijo al que volver | Desplegar todo, etiquetar `v1.0.0` y redactar la lista de cambios (las notas de versión de TaskStream dan el borrador) |
| Revisión de seguridad | Hay rutas sin sesión (proyectos públicos, escaparate, propuestas) y un MCP abierto a Internet | Permisos por rol, qué ve un anónimo, límites de peticiones (solo las propuestas tienen), CORS, cabeceras |
| Registro de errores en producción | Hoy un fallo solo se conoce si alguien avisa | Sentry (o similar) en la API, la web y el MCP |
## 10.2 Fallos y deuda conocidos
- **Error al crear subtareas en producción**: lo reportó el usuario y no se reprodujo en local; con el arreglo de la
ficha la lista ya se refresca, pero falta el mensaje exacto si vuelve a salir.
- **Arrastrar a una columna prohibida**: la tarjeta se queda en la última columna permitida por la que pasó el cursor
(comportamiento de `@angular/cdk/drag-drop`). Debería volver a su sitio.
- **Tamaño de la web**: el paquete inicial supera en ~130 kB el aviso de 2 MB, y los estilos de la ficha del ticket
(`issue.page.scss`) pasan de los 8 kB. Mirar qué entra en el paquete inicial (`marked` y `dompurify` deberían ir solo
con la ficha) y sacar las reacciones a su componente.
- **Imágenes pegadas**: también aparecen en la lista de adjuntos del ticket, y si se borran del texto el adjunto sigue
ahí.
- **Límite de propuestas por instancia**: vive en memoria, así que con varias instancias de la API es aproximado.
Pasarlo a Redis si hay más de una.
- **Votos por REST**: dan el total (`votes`) pero no si he votado yo (`voted` solo está en GraphQL).
- **Tamaño de la web**: el paquete inicial supera en ~130 kB el aviso de 2 MB. La causa es `@pi/ui`: su paquete
(un solo `fesm`) importa `chart.js` al cargar, aunque solo lo usan las gráficas de analítica. Se arregla en la
librería, con un punto de entrada aparte para las gráficas (`@pi/ui/charts`) o cargando `chart.js` bajo demanda.
- **Imágenes pegadas**: ya no salen en la lista de adjuntos, pero si se borran del texto el adjunto sigue guardado.
- **Seguimiento**: los tickets que ya existían no se apuntan a quien los creó o los tiene asignados (solo los nuevos).
- **Borrar un proyecto** no borra su documento de integración con Git (`git_integrations`); no molesta, pero queda.
- **Aviso de lint** en `automation-rules.ts` (una aserción `!`), anterior a estos cambios.
- **El MCP no hace el flujo OAuth del protocolo**: el cliente tiene que llevar un token de Pireon en la cabecera y
renovarlo a mano cuando caduca.
@@ -45,16 +37,14 @@ En orden de lo que más aportaría:
- resumir un ticket largo con sus comentarios;
- al crear un ticket, sugerir tipo, prioridad y área, y avisar si se parece a otro que ya existe;
- redactar las notas de versión en lenguaje natural a partir del borrador.
2. **Avisos de propuestas**: hoy una propuesta llega como ticket con la etiqueta «propuesta», pero nadie recibe aviso.
Avisar a propietarios y admins, y quizá una bandeja para revisarlas.
3. **Propuestas y votos en la web pública** (la landing): el escaparate ya trae `proposals` (si acepta, dónde está el
2. **Propuestas y votos en la web pública** (la landing): el escaparate ya trae `proposals` (si acepta, dónde está el
formulario y las más votadas) y los votos de la hoja de ruta; falta mostrarlos.
4. **Avisos a Discord, Slack o Telegram**: los webhooks salientes mandan JSON en bruto, que esos chats no muestran
3. **Avisos a Discord, Slack o Telegram**: los webhooks salientes mandan JSON en bruto, que esos chats no muestran
bien. Una integración por canal (o un formato por destino en los webhooks) mandaría mensajes ya legibles: «Ana
movió WEB-12 a En revisión», con enlace.
5. **Conector de claude.ai**: el MCP necesitaría el flujo OAuth del protocolo, y Pireon publicar su metadata de
4. **Conector de claude.ai**: el MCP necesitaría el flujo OAuth del protocolo, y Pireon publicar su metadata de
autorización y admitir el registro dinámico de clientes.
6. **Proyectos de la landing que ya existen**: hoy el tablero en TaskStream solo se abre al crear el proyecto; poder
5. **Proyectos de la landing que ya existen**: hoy el tablero en TaskStream solo se abre al crear el proyecto; poder
abrirlo después desde el panel.
## 10.4 Descartado por ahora
@@ -64,3 +54,14 @@ Mucho trabajo para lo que aportan hoy; se pueden recuperar si alguien los necesi
- **Registro de horas** dedicadas a cada ticket.
- **Objetivos u OKR** por encima de las épicas.
- **App móvil**: la web ya se adapta a pantallas estrechas.
## 10.5 Hecho desde la primera versión de esta lista
- Tiempo real: _ping_ y reconexión del WebSocket, y recarga de lo que se ve al reconectar.
- Arrastrar a una columna prohibida devuelve la tarjeta a su sitio.
- Reacciones y «Bloqueado» en componentes propios (los estilos de la ficha vuelven a caber en 8 kB).
- Límite de propuestas en Redis, válido con varias instancias.
- Aviso `PROPOSED` a propietarios y admins cuando llega una propuesta.
- `voted` también por REST.
- Borrar un proyecto borra su integración con Git.
- Sin avisos de lint.

Binary file not shown.

Binary file not shown.