diff --git a/docs/01-vision-general.md b/docs/01-vision-general.md index 2aaf292..4eca512 100644 --- a/docs/01-vision-general.md +++ b/docs/01-vision-general.md @@ -25,7 +25,7 @@ | 🗑️ Papelera | Borrar un ticket lo manda a la papelera (quien lo abrió o un admin); se recupera durante 30 días y luego se borra del todo | | ☑️ En bloque | Selecciona varios tickets del backlog y cámbiales a la vez estado, persona, sprint, prioridad o etiqueta | | ⌨️ Atajos y búsqueda | Ctrl/⌘ K o / busca tickets y proyectos de todos tus proyectos; `g b`, `g k`, `g s`, `g r` navegan, `c` crea, `e` edita, `?` los lista | -| 🗺️ Hoja de ruta | Las épicas en el tiempo (fecha de inicio y límite), con los sprints y las releases como referencias | +| 🗺️ Hoja de ruta | Las épicas en el tiempo (fecha de inicio y límite), con los sprints y las releases como referencias y flechas entre las que dependen unas de otras | | 🗳️ Propuestas y votos | Un proyecto público puede abrir un formulario para que cualquiera, sin cuenta, reporte un error o proponga una idea; quien tenga sesión vota lo que quiere | | 🌐 Web del colectivo | La página de cada proyecto en pireon.dev enseña su hoja de ruta, versiones, sprint y tickets para recién llegados (`good-first-issue`) | | 🧩 Desglose | Épicas → tickets → subtareas (tres niveles), con su progreso; y una checklist dentro de cada ticket para los pasos pequeños | diff --git a/docs/04-cliente-web.md b/docs/04-cliente-web.md index 9dff7fc..cec6002 100644 --- a/docs/04-cliente-web.md +++ b/docs/04-cliente-web.md @@ -289,6 +289,7 @@ visualización de la plataforma: | `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) | | `/p/:key/issues/:issueKey` | Ticket | | `/p/:key/releases` | Releases | +| `/p/:key/roadmap` | Hoja de ruta: épicas en el tiempo, con sprints y releases; flechas «bloquea» / «depende de» entre épicas y aviso si una empieza antes de que acabe la que la bloquea | | `/p/:key/dashboard` | Seguimiento: cifras, flujo por estado, equipo (cada persona enlaza a su tablero) y tipos | | `/p/:key/analytics` | Estadísticas: creados vs. resueltos, flujo acumulado, burnup de una release, tiempos de ciclo y entrega, QA y resueltos por persona (30 días a 1 año) | | `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto): estados (orden, categoría, color, motivo), transiciones, tipos, áreas y campos personalizados; archivar en lugar de borrar | diff --git a/docs/10-futuro.md b/docs/10-futuro.md index bdded90..5c3a6b6 100644 --- a/docs/10-futuro.md +++ b/docs/10-futuro.md @@ -30,26 +30,48 @@ cuando hay pistas, por dónde empezar. ### En curso, en este orden -4. **Dependencias en la hoja de ruta**: las relaciones «bloquea» / «depende de» entre épicas, como flechas en la vista - de calendario, para ver qué retrasa a qué. -5. **Burndown y velocidad de sprint**: puntos pendientes día a día durante el sprint y puntos cerrados por sprint, para +1. **Burndown y velocidad de sprint**: puntos pendientes día a día durante el sprint y puntos cerrados por sprint, para planificar el siguiente con datos (el historial de estados ya está en `issue_timeline`). -6. **Plantillas de proyecto**: crear un proyecto copiando de otro los estados, transiciones, tipos, áreas, - automatizaciones y etiquetas. -7. **Sincronizar con issues de Gitea o GitHub**: hoy los commits y PR se enlazan a tickets; además, que un issue del - repositorio cree o actualice un ticket, y al revés. -8. **Vistas guardadas en «Mi trabajo»**: montar la portada con filtros guardados de varios proyectos («Bugs críticos de - todos mis proyectos», «Lo que revisa QA esta semana»…). -9. **Exportar e imprimir un sprint o una release**: un PDF con los tickets, su estado y las notas, para un cliente o +2. **Plantillas de proyecto**: crear un proyecto copiando de otro los estados, transiciones, tipos, áreas, campos + personalizados, automatizaciones y etiquetas. +3. **Exportar e imprimir un sprint o una release**: un PDF con los tickets, su estado y las notas, para un cliente o para archivar. -10. **Asistente en la web** (la base ya está en el MCP): resumir un ticket largo con sus comentarios; al crear un - ticket, sugerir tipo, prioridad y área y avisar si se parece a otro; redactar las notas de versión a partir del - borrador. Pendiente de decidir la clave de la API de Claude y su coste. -11. **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. -12. **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. + +Y en el panel del proyecto: + +4. **Sprint en curso**: días que quedan, puntos hechos y pendientes, y aviso si al ritmo actual no llega. +5. **Tickets en riesgo**: vencidos, que vencen esta semana, bloqueados y atascados (días sin moverse en un estado en + curso), cada grupo con enlace a la lista filtrada. +6. **Antigüedad de lo abierto**: cada ticket en curso por los días que lleva en su estado, con el umbral habitual del + proyecto (p85 del tiempo de ciclo). +7. **Cuellos de botella**: días medios en cada estado y columnas por encima de su límite WIP. +8. **Próximas releases con previsión**: progreso, fecha objetivo y fecha probable según el ritmo real (Monte Carlo), + con semáforo. +9. **Qué ha cambiado desde tu última visita**: nuevos, cerrados, devueltos por QA y menciones. +10. **Carga del equipo en puntos**: asignado frente a lo que cada persona suele cerrar por sprint, con aviso de + sobrecarga y sus bloqueados. +11. **Salud del backlog**: sin estimar, sin área, sin asignar y sin tocar en 90 días, con enlace a la lista filtrada. +12. **Calidad**: bugs abiertos por prioridad y su tendencia, devoluciones de QA por área y bugs abiertos en los 14 días + siguientes a cada release. +13. **Desglose por campo personalizado**: tickets abiertos según un campo de opciones a elegir («bugs por + navegador»). + +### Aceptadas, para después de esta tanda + +- **Sincronizar con issues de Gitea o GitHub**: que un issue del repositorio cree o actualice un ticket, y al revés. +- **Vistas guardadas en «Mi trabajo»**: filtros guardados de varios proyectos («Bugs críticos de todos mis + proyectos»). +- **Búsqueda con lenguaje de consulta**: `estado:abierto asignado:yo navegador:Safari prioridad>=alta`, guardable como + vista. +- **Definición de «Hecho»**: no pasar a un estado `DONE` con la lista de comprobación a medias (salvo un admin). +- **Entornos de despliegue**: en qué entorno está cada ticket (pruebas, producción), desde los avisos de la + integración continua. +- **Equipos dentro del proyecto**: asignar a un equipo y mencionarlo (`@frontend`). +- **Presencia**: ver quién está mirando o editando el mismo ticket. +- **Rol «invitado»**: solo ve los tickets en los que participa (clientes, externos). +- **Página pública de novedades**: las notas de versión publicadas en una página por proyecto, con RSS. +- **Vista de cartera**: progreso de épicas y releases de todos los proyectos en una página. +- **Archivar un proyecto**: queda en solo lectura en lugar de borrarlo. ### Otras ideas apuntadas, sin decidir @@ -57,8 +79,13 @@ cuando hay pistas, por dónde empezar. - **Tickets recurrentes**: «revisar copias cada lunes», creados solos con la plantilla del tipo. - **Crear tickets por correo**: una dirección por proyecto; lo que llega es un ticket (con adjuntos) y las respuestas, comentarios. Necesita un buzón de entrada. -- **Tiempo en cada estado**: cuánto pasa un ticket en «En revisión» o «En QA», y dónde se atascan. -- **Página pública de novedades**: las notas de versión publicadas en una página por proyecto, con RSS. +- **Posponer un ticket**: «recuérdamelo el jueves». +- **Reunión diaria**: qué cerró cada uno ayer, qué tiene hoy y qué le bloquea, sacado del historial. +- **Requisitos al cambiar de estado**: campos obligatorios para entrar en un estado (estimación para «En curso»…). +- **Plazos por prioridad**: «lo crítico, en 24 h», con aviso antes de vencer. +- **Matriz impacto/esfuerzo** para ordenar el backlog. +- **Planning poker** y **retrospectiva del sprint** (las acciones se convierten en tickets). +- **Avisar a quien propuso** cuando su propuesta se hace o se descarta. ## 10.4 Descartado por ahora @@ -67,6 +94,11 @@ 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. +- **Asistente en la web** (resumir tickets, sugerir tipo, prioridad y área, redactar notas de versión): el MCP ya deja + hacerlo desde un cliente de IA. +- **Avisos a Discord, Slack o Telegram** con mensajes legibles: los webhooks salientes siguen ahí para quien los + necesite. +- **Conector de claude.ai** (OAuth del protocolo en el MCP y registro dinámico de clientes en Pireon). ## 10.5 Hecho desde la primera versión de esta lista @@ -96,3 +128,5 @@ Mucho trabajo para lo que aportan hoy; se pueden recuperar si alguien los necesi - Campos personalizados por proyecto (texto, número, fecha o lista de opciones): se definen en Configuración, se editan en la ficha, se ven bajo el título en la lista y se filtran por los de opciones (también por GraphQL, REST y MCP). +- Dependencias en la hoja de ruta: flechas entre las épicas que se bloquean o dependen unas de otras, y aviso cuando + una empieza antes de que acabe la que la bloquea. diff --git a/docs/TaskStream-Documentacion.docx b/docs/TaskStream-Documentacion.docx index 1f7c728..184a28c 100644 Binary files a/docs/TaskStream-Documentacion.docx and b/docs/TaskStream-Documentacion.docx differ diff --git a/docs/TaskStream-Documentacion.pdf b/docs/TaskStream-Documentacion.pdf index 67395b9..51ae9ca 100644 Binary files a/docs/TaskStream-Documentacion.pdf and b/docs/TaskStream-Documentacion.pdf differ diff --git a/libs/web/tracker/application/src/lib/facades/facades.spec.ts b/libs/web/tracker/application/src/lib/facades/facades.spec.ts index 1bc2b2a..5113750 100644 --- a/libs/web/tracker/application/src/lib/facades/facades.spec.ts +++ b/libs/web/tracker/application/src/lib/facades/facades.spec.ts @@ -233,6 +233,7 @@ describe('ProjectFacade', () => { transitions: [], issueTypes: [], areas: [], + customFields: [], }; project.saveSettings(settings); diff --git a/libs/web/tracker/domain/src/lib/models/models.ts b/libs/web/tracker/domain/src/lib/models/models.ts index dd04971..20234f6 100644 --- a/libs/web/tracker/domain/src/lib/models/models.ts +++ b/libs/web/tracker/domain/src/lib/models/models.ts @@ -71,6 +71,8 @@ export interface ChildProgress { export interface Epic { issue: IssueSummary; progress: ChildProgress; + /** Its relations: «bloquea» / «depende de» are the roadmap's arrows. */ + links: IssueLink[]; } /** What a saved view narrows the board to; empty: everything. */ diff --git a/libs/web/tracker/domain/src/lib/models/roadmap.spec.ts b/libs/web/tracker/domain/src/lib/models/roadmap.spec.ts index 5b90ede..b91b042 100644 --- a/libs/web/tracker/domain/src/lib/models/roadmap.spec.ts +++ b/libs/web/tracker/domain/src/lib/models/roadmap.spec.ts @@ -1,5 +1,8 @@ +import { IssueLinkType } from './enums'; import { + dependencyClash, roadmapBar, + roadmapDependencies, roadmapMonths, roadmapOffset, roadmapRange, @@ -30,4 +33,44 @@ describe('roadmap', () => { expect(roadmapOffset('2020-01-01', range)).toBe(0); expect(roadmapOffset('2030-01-01', range)).toBe(100); }); + + it('draws each dependency between epics once, first the one that blocks', () => { + const link = ( + type: IssueLinkType, + direction: 'OUTWARD' | 'INWARD', + issueId: string, + ) => ({ type, direction, issueId, issueKey: issueId.toUpperCase() }); + const epics = [ + { + issue: { id: 'a' }, + links: [ + link(IssueLinkType.BLOCKS, 'OUTWARD', 'b'), + link(IssueLinkType.RELATES_TO, 'OUTWARD', 'c'), + link(IssueLinkType.BLOCKS, 'OUTWARD', 'elsewhere'), + ], + }, + { + issue: { id: 'b' }, + links: [ + link(IssueLinkType.BLOCKS, 'INWARD', 'a'), + // The same relation told the other way round. + link(IssueLinkType.DEPENDS_ON, 'OUTWARD', 'a'), + ], + }, + { + issue: { id: 'c' }, + links: [link(IssueLinkType.DEPENDS_ON, 'OUTWARD', 'b')], + }, + ]; + expect(roadmapDependencies(epics)).toEqual([ + { before: 'a', after: 'b' }, + { before: 'b', after: 'c' }, + ]); + }); + + it('flags an epic that starts before the one it waits for ends', () => { + expect(dependencyClash('2026-11-10', '2026-11-11')).toBe(false); + expect(dependencyClash('2026-11-10', '2026-11-10')).toBe(true); + expect(dependencyClash('2026-11-10', '2026-11-03')).toBe(true); + }); }); diff --git a/libs/web/tracker/domain/src/lib/models/roadmap.ts b/libs/web/tracker/domain/src/lib/models/roadmap.ts index 0bc03f5..013aa8e 100644 --- a/libs/web/tracker/domain/src/lib/models/roadmap.ts +++ b/libs/web/tracker/domain/src/lib/models/roadmap.ts @@ -1,3 +1,6 @@ +import { IssueLinkType } from './enums'; +import { IssueLink } from './models'; + const DAY = 86_400_000; /** Time span a roadmap shows (UTC midnights). */ @@ -70,3 +73,48 @@ export function roadmapMonths( } return months; } + +/** An epic that has to finish before another one starts. */ +export interface RoadmapDependency { + /** Id of the epic that goes first (it blocks the other). */ + before: string; + /** Id of the epic that waits for it. */ + after: string; +} + +/** + * The «bloquea» / «depende de» relations among these epics, once per pair: + * A blocks B and B depends on A are the same arrow, A → B. Only outward + * links are read (each relation is stored on both tickets). + */ +export function roadmapDependencies( + epics: { issue: { id: string }; links: IssueLink[] }[], +): RoadmapDependency[] { + const ids = new Set(epics.map((e) => e.issue.id)); + const seen = new Set(); + const out: RoadmapDependency[] = []; + for (const epic of epics) + for (const link of epic.links) { + if (link.direction !== 'OUTWARD' || !ids.has(link.issueId)) continue; + const pair = + link.type === IssueLinkType.BLOCKS + ? { before: epic.issue.id, after: link.issueId } + : link.type === IssueLinkType.DEPENDS_ON + ? { before: link.issueId, after: epic.issue.id } + : null; + if (!pair || pair.before === pair.after) continue; + const key = `${pair.before}>${pair.after}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(pair); + } + return out; +} + +/** The epic that waits starts on or before the day the other one ends. */ +export function dependencyClash( + beforeEnds: string, + afterStarts: string, +): boolean { + return at(afterStarts) <= at(beforeEnds); +} diff --git a/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts b/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts index 5f0370d..15fae45 100644 --- a/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts +++ b/libs/web/tracker/infrastructure/src/lib/adapters/graphql-issues.gateway.ts @@ -50,7 +50,12 @@ import { UpdateIssueDocument, } from '../graphql/generated'; import { GraphqlOperations } from './graphql-operations'; -import { toCommandResult, toIssue, toIssueSummary } from './mappers'; +import { + toCommandResult, + toIssue, + toIssueLink, + toIssueSummary, +} from './mappers'; @Injectable() export class GraphqlIssuesGateway extends IssuesGateway { @@ -214,6 +219,7 @@ export class GraphqlIssuesGateway extends IssuesGateway { d.epics.map((epic) => ({ issue: toIssueSummary(epic.issue), progress: { ...epic.progress }, + links: epic.issue.links.map((l) => toIssueLink(l)), })), ), ); diff --git a/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts b/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts index 4315d11..b819697 100644 --- a/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts +++ b/libs/web/tracker/infrastructure/src/lib/adapters/mappers.ts @@ -3,6 +3,7 @@ import type { Comment, CommandResult, Issue, + IssueLink, IssueSummary, Project, ProjectSettings, @@ -39,6 +40,12 @@ export const toProject = (p: ProjectFieldsFragment): Project => ({ export const toIssueSummary = (i: IssueSummaryFieldsFragment): IssueSummary => as(i); export const toIssue = (i: IssueFieldsFragment): Issue => as(i); +export const toIssueLink = (l: { + type: string; + direction: string; + issueId: string; + issueKey: string; +}): IssueLink => as({ ...l }); export const toComment = (c: CommentFieldsFragment): Comment => c; export const toActivity = (a: ActivityFieldsFragment): Activity => a; export const toCommandResult = ( diff --git a/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts b/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts index effdce7..05ea553 100644 --- a/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts +++ b/libs/web/tracker/infrastructure/src/lib/graphql/generated.ts @@ -1115,6 +1115,12 @@ export type EpicsQuery = { startDate: string | null; dueDate: string | null; trashedAt: string | null; + links: Array<{ + type: IssueLinkType; + direction: LinkDirection; + issueId: string; + issueKey: string; + }>; assignee: { id: string; name: string } | null; release: { id: string; name: string } | null; sprint: { id: string; name: string } | null; @@ -6783,6 +6789,31 @@ export const EpicsDocument = { kind: 'FragmentSpread', name: { kind: 'Name', value: 'IssueSummaryFields' }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'links' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'type' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'direction' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'issueId' }, + }, + { + kind: 'Field', + name: { kind: 'Name', value: 'issueKey' }, + }, + ], + }, + }, ], }, }, diff --git a/libs/web/tracker/infrastructure/src/lib/graphql/issues.graphql b/libs/web/tracker/infrastructure/src/lib/graphql/issues.graphql index aa9ea6f..b685cf8 100644 --- a/libs/web/tracker/infrastructure/src/lib/graphql/issues.graphql +++ b/libs/web/tracker/infrastructure/src/lib/graphql/issues.graphql @@ -163,6 +163,12 @@ query Epics($projectId: ID!) { epics(projectId: $projectId) { issue { ...IssueSummaryFields + links { + type + direction + issueId + issueKey + } } progress { total diff --git a/libs/web/tracker/presenters/src/lib/pages/roadmap.page.ts b/libs/web/tracker/presenters/src/lib/pages/roadmap.page.ts index 30dd5e1..7b741be 100644 --- a/libs/web/tracker/presenters/src/lib/pages/roadmap.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/roadmap.page.ts @@ -1,12 +1,17 @@ import { AuthFacade } from '@task-stream/web/identity/application'; import { VoteComponent } from '../ui/vote'; import { + afterRenderEffect, ChangeDetectionStrategy, Component, computed, effect, + ElementRef, + HostListener, inject, + signal, untracked, + viewChild, } from '@angular/core'; import { RouterLink } from '@angular/router'; import { PiNoteComponent, PiSkeletonComponent } from '@pi/ui'; @@ -16,9 +21,11 @@ import { provideEpicsFacade, } from '@task-stream/web/tracker/application'; import { + dependencyClash, Epic, ReleaseStatus, roadmapBar, + roadmapDependencies, roadmapMonths, roadmapOffset, roadmapRange, @@ -51,7 +58,8 @@ const shortDay = (day: string) =>

Las épicas en el tiempo, con los sprints y las releases como referencias. Las fechas de una épica se ponen en su ficha («Empieza» y - «Fecha límite»). + «Fecha límite»); las flechas van de una épica a las que la esperan + (relaciones «bloquea» y «depende de»).

@@ -67,7 +75,22 @@ const shortDay = (day: string) => } @else if (!epics.loaded()) { } @else { -
+ @if (clashes().length) { + + } +
@@ -141,6 +164,7 @@ const shortDay = (day: string) => } + @if (arrows().length) { + + + } +