Files
task-stream/docs/03-api.md
Josep Piguillem b88296c22b feat: «Recordármelo», a reminder of a ticket only for me
On a ticket, «Recordármelo» offers in an hour, this evening, tomorrow or
next Monday at 9, in a week, or any date and time, with an optional note.
The ticket shows the waiting one («Te lo recordaré el mié 21 a las 9:00»)
to change or drop. Every minute a scheduler tells the ones whose time
came as a REMINDER notification (bell, push, e-mail), once, and forgets
those whose ticket went to the trash or out of reach. One per person and
ticket, private, no history (Mongo `issue_reminders`).

API: setReminder / clearReminder, Issue.myReminder, myReminders;
PUT/DELETE /api/issues/{issue}/reminder, GET /api/me/reminders. MCP:
remind_me.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:08:50 +02:00

79 KiB

3. API

API GraphQL en /graphql (HTTP para queries y mutations, WebSocket graphql-ws para subscriptions) más unos pocos endpoints REST bajo /api. El esquema completo se genera en apps/api/schema.gql.

3.1 Modelo de dominio (contexto tracker)

classDiagram
  class Project {
    key: "TS"
    name
    members: (userId, role)[]
    settings: estados · transiciones · tipos · áreas
    addMember() changeMemberRole() removeMember() updateDetails() updateSettings()
  }
  class Issue {
    key: "TS-42"
    type · area · priority · status
    assigneeId · releaseId · labels · figmaUrl
    links: (type, target)[]
    changeStatus() assign() linkTo() unlink() setRelease() updateDetails()
  }
  class Comment {
    body · authorId
    edit() delete()
  }
  class Release {
    name · status · targetDate
    update() ship() archive()
  }
  Project "1" --> "*" Issue
  Issue "1" --> "*" Comment
  Project "1" --> "*" Release
  Release "0..1" <-- "*" Issue
  Issue "*" --> "*" Issue : links

Roles por proyecto

Rol Ver Crear/modificar tickets, comentar Gestionar miembros y releases
OWNER (quien crea el proyecto) ✅ ✅ ✅ (no se le puede quitar ni cambiar el rol)
ADMIN ✅ ✅ ✅
MEMBER ✅ ✅ ❌
VIEWER ✅ ❌ ❌
GUEST Solo sus tickets Abrir tickets, comentar y adjuntar en los suyos ❌

Un invitado (GUEST, para clientes o gente de fuera) de un proyecto privado solo ve los tickets en los que participa: los que abrió, los que tiene asignados y los que sigue. El resto no existen para él (404, también en «Mi trabajo», en las suscripciones en tiempo real, en «bloqueado por» y en los enlazados), y las vistas de todo el proyecto (actividad, estadísticas, panel, releases, sprints, épicas, papelera) le responden 403. Abre tickets sin asignarlos ni planificarlos, comenta y adjunta en los suyos; no cambia su estado ni sus campos. En un proyecto público ve lo mismo que cualquiera.

Cualquiera puede salir de un proyecto (salvo el propietario). Los no miembros reciben not found, para no revelar que el proyecto existe.

Proyectos públicos

Con isPublic (al crear, o después con updateProject / PATCH /api/projects/{project}, propietario o admin), cualquiera puede leer el proyecto, también sin token: el proyecto y sus miembros, tablero, tickets, comentarios, historial, releases (progreso y burnup), actividad, seguimiento y estadísticas. Escribir sigue siendo cosa de los miembros según su rol (sin token: 401; con token pero sin ser miembro: 403). Un usuario con sesión que no es miembro lee igual que un anónimo. Las subscriptions exigen sesión; con ella, también se pueden seguir proyectos públicos.

Propuestas del público: un proyecto público puede aceptar errores e ideas de cualquiera, sin cuenta (acceptsProposals, en updateProject / PATCH). POST /api/explore/projects/{project}/proposals (kind: BUG o 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 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}/propose, 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 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 leer esos proyectos.

Borrar un proyecto

Solo el propietario, escribiendo la clave del proyecto para confirmar (deleteProject(projectId, confirmKey) o DELETE /api/projects/{project}?confirm=CLAVE). El proyecto desaparece al momento y, a continuación, una saga borra sus tickets (y sus comentarios), sus releases y los enlaces que tickets de otros proyectos tenían hacia los suyos (ver Arquitectura). La clave queda libre para un proyecto nuevo. Los eventos se quedan en el event store como auditoría, pero desde la API no se puede deshacer.

Archivar un proyecto

El propietario archiva un proyecto terminado (archiveProject / POST /api/projects/{project}/archive) en lugar de borrarlo: se sigue viendo todo (tablero, tickets, historia, estadísticas, y quien lo administra sigue viendo sus reglas y webhooks), pero ninguna orden lo cambia (422, «está archivado»): ni tickets, ni comentarios, ni votos, ni ajustes, ni miembros; las automatizaciones no se ejecutan y el formulario público no acepta propuestas. Seguir un ticket sí se puede, porque solo atañe a quien lo sigue. unarchiveProject / POST …/unarchive lo devuelve. Project.archivedAt dice desde cuándo.

Configuración por proyecto

Cada proyecto tiene sus estados, transiciones, tipos de ticket, áreas y campos personalizados (Project.settings). Todos empiezan con los valores por defecto de abajo, y el propietario o un admin los cambia con updateProjectSettings (o PUT /api/projects/{project}/settings); el cambio es un evento más (ProjectSettingsChangedEvent).

Valor Campos Por defecto
Estado id, name, category, color, requiresReason, archived (el orden es el del tablero) BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, READY_FOR_QA, IN_QA, QA_FAILED, DONE, CANCELLED
Tipo id, name, icon, color, archived BUG, FEATURE, UI_CHANGE, TASK, QA
Área id, name, archived FRONTEND, BACKEND, MOBILE, QA, DESIGN, DEVOPS

La categoría de un estado dice qué significa, se llame como se llame: TODO (pendiente), IN_PROGRESS, QA, DONE y CANCELLED. Releases («todo terminado» = DONE o CANCELLED), «Mi trabajo», el seguimiento y las estadísticas trabajan con categorías, así que siguen funcionando con estados propios. Los tickets guardan el id del estado y, en el read model, su categoría (Issue.statusCategory).

Reglas:

  • No se borra lo que ya existe: se archiva. Tickets, historial y estadísticas siguen apuntando a ese id. Un valor archivado no se ofrece para tickets nuevos ni recibe transiciones; los tickets que ya lo tienen lo conservan.
  • Los ids son estables (A-Z, 0-9, _, hasta 30, empezando por letra); los nombres, de 1 a 40 caracteres, se pueden cambiar cuando se quiera.
  • Hace falta al menos un estado activo de categoría TODO (los tickets nuevos entran en el primero), uno DONE y un tipo activo.
  • Las transiciones solo pueden apuntar a estados que existen y nunca de un estado a sí mismo.
  • Colores: uno de la paleta (slate, stone, violet, blue, sky, teal, green, amber, orange, red, pink), que tiene su versión para modo oscuro, o cualquier #rrggbb. Iconos: bug, feature, palette, task, qa, flag, star-fill, rocket, document, light-bulb, key, support.

Un proyecto nuevo puede empezar con la configuración de otro que administres: templateProjectId en createProject o template (id o clave) en POST /api/projects. Se copian una vez sus estados, transiciones, tipos, áreas, campos personalizados y automatizaciones (las pausadas, pausadas; las que asignan a alguien que no está en el nuevo proyecto se quedan fuera). Las etiquetas no tienen catálogo, así que no hay nada que copiar.

Los proyectos creados antes de que existiera la configuración usan los valores por defecto (sus eventos no la llevan). Al arrancar, la API rellena la categoría de los tickets del read model que no la tienen.

Equipos

settings.teams: grupos de miembros (id, name, memberIds, archived), como mucho 30. Al guardar, quien no es miembro del proyecto se quita del equipo; un equipo no se borra, se archiva; y si teams no viene, se quedan como estaban. Un ticket va a un equipo con team (en createIssue / updateIssue y por REST), y la lista se filtra con teams (?team=FRONT). En un comentario, @[Frontend](team-FRONT) avisa a cada miembro del equipo («mencionó a tu equipo…»).

Presencia

Quién más está en un ticket ahora. La página de un ticket late cada 10 s con touchPresence(issueId, editing) (o PUT /api/issues/{issue}/presence con { editing }), y la respuesta es quién más está, primero los que editan. Se guarda en Redis (un hash por ticket, task-stream:presence:<id>) y una entrada sin latido en 30 s desaparece sola; leavePresence (DELETE …/presence) la quita al salir. No es historia: no pasa por el event store. Solo para quien puede leer el ticket.

Definición de «Hecho»

settings.checklistBeforeDone (desactivada por defecto): con ella, un ticket no pasa a un estado de categoría DONE mientras le quede algún paso de la checklist sin marcar (422, «quedan N pasos…»). El propietario y los admins sí pueden cerrarlo; las automatizaciones, no. Al guardar la configuración, si no viene, se queda como estaba.

Campos personalizados

Datos propios del proyecto en cada ticket («Navegador», «Cliente», «Coste»…), en settings.customFields: id, name, type y archived, más options si es una lista. Un proyecto tiene como mucho 30, y empieza sin ninguno.

Tipo Valor
TEXT Hasta 500 caracteres
NUMBER Un número (se guarda en forma canónica: 1,5 → 1.5)
DATE YYYY-MM-DD
SELECT Una de sus options (de 1 a 50, de hasta 60 caracteres, únicas)
  • Como el resto de la configuración, un campo no se borra: se archiva. Su tipo no cambia una vez guardado, para que los valores que ya hay sigan teniendo sentido. Un campo archivado se puede vaciar en un ticket, pero no rellenar.
  • Al guardar la configuración, si customFields no viene se quedan como estaban (los clientes que no los conocen no los pierden).
  • En el ticket: GraphQL Issue.customFields es una lista { fieldId, value } (solo los que tienen valor) y se cambia con customFields: [{ fieldId, value }] en createIssue / updateIssue; por REST es un objeto { "BROWSER": "Safari" }. En ambos se mandan solo los campos que cambian, y null o "" vacía uno.
  • Filtrar: issues(filter: { customFields: [{ fieldId, value }] }) o GET /api/projects/{project}/issues?field=BROWSER:Safari (se puede repetir).
  • IssueUpdatedEvent lleva el mapa entero de valores ya validado, así la proyección solo lo copia.

Workflow por defecto

stateDiagram-v2
  [*] --> BACKLOG
  BACKLOG --> TODO
  BACKLOG --> IN_PROGRESS
  BACKLOG --> CANCELLED
  TODO --> BACKLOG
  TODO --> IN_PROGRESS
  TODO --> CANCELLED
  IN_PROGRESS --> TODO
  IN_PROGRESS --> IN_REVIEW
  IN_PROGRESS --> READY_FOR_QA
  IN_PROGRESS --> CANCELLED
  IN_REVIEW --> IN_PROGRESS
  IN_REVIEW --> READY_FOR_QA
  IN_REVIEW --> DONE
  READY_FOR_QA --> IN_QA
  READY_FOR_QA --> IN_PROGRESS
  IN_QA --> DONE
  IN_QA --> QA_FAILED
  QA_FAILED --> IN_PROGRESS
  QA_FAILED --> TODO
  DONE --> TODO
  DONE --> IN_PROGRESS
  CANCELLED --> BACKLOG
  • QA_FAILED y CANCELLED exigen un motivo (requiresReason), que queda en el historial.
  • No se puede pasar a DONE saltándose QA salvo desde IN_REVIEW (cambios que no requieren QA).
  • Issue.allowedTransitions devuelve los estados activos a los que puede pasar el ticket según el workflow de su proyecto.

Otras reglas

  • Proyecto: clave de 2 a 10 caracteres alfanuméricos que empieza por letra y es única (los tickets se numeran CLAVE-1, CLAVE-2…).
  • Asignación: solo a miembros del proyecto. Para añadir miembros, el usuario debe existir y estar activo en Pireon.
  • Relaciones: nunca consigo mismo ni duplicadas; pueden cruzar proyectos si el actor puede ver el destino. Se muestran en los dos tickets (OUTWARD en el origen, INWARD en el destino).
  • Releases: solo las PLANNED aceptan o pierden tickets; publicar con tickets sin terminar exige force: true. Las notas de versión (notes, Markdown) se escriben también después de publicar (no una vez archivada); notesDraft / GET /api/releases/{id}/notes/draft propone un borrador con los tickets hechos agrupados por tipo (los de QA quedan fuera).
  • Comentarios: solo el autor edita; borra el autor o un admin del proyecto. Se puede responder a un comentario (replyTo), con un solo nivel de respuestas: responder a una respuesta la suma al mismo hilo, y la respuesta tiene que ser del mismo ticket. Quien escribió el comentario recibe un aviso (REPLIED). Un comentario borrado que tiene respuestas se queda, sin texto (deleted: true), para que el hilo se siga entendiendo, y desaparece con su última respuesta; no cuenta en commentCount.
  • Formato: descripciones, comentarios y notas de versión se guardan en Markdown; la API los guarda tal cual y la web los muestra limpiando el HTML (DOMPurify). En la web se escriben con un editor visual que lee y escribe ese mismo Markdown, así que un cliente de la API (o el MCP) sigue mandando Markdown. Una imagen pegada se sube como adjunto y queda en el texto como ![nombre](attachment:<id>): la subida responde attachmentId, y la web la descarga con la sesión de quien lee, así que sirve también en proyectos privados.
  • Reacciones: 👍 👎 😄 🎉 😕 ❤️ 🚀 👀 en los comentarios (reactToComment(commentId, emoji, on), POST /api/comments/{id}/reactions); los miembros, también los lectores. Cada comentario da reactions con quién eligió cada una. No generan actividad ni avisos.
  • Conversaciones: un comentario raíz y sus respuestas se resuelven (resolveThread(commentId, resolved, conclusion), PUT/DELETE /api/comments/{id}/resolution; evento CommentThreadResolvedEvent) por quien puede cambiar el ticket; una respuesta no se resuelve sola. Si trae conclusion, antes de cerrarla se añade como respuesta marcada como decisión. El comentario da resolved, resolvedBy y resolvedAt. Escribiendo: typing(issueId, threadId) (mutation, cada pocos segundos mientras se escribe) avisa al momento por la subscription typing(issueId) a quien tenga el ticket abierto; no se guarda (canal de Redis tracker.typing.<ticket>), y cada cliente descarta sus propios avisos.
  • Decisiones: quien puede cambiar el ticket marca un comentario como decisión (markDecision(commentId, on), PUT/DELETE /api/comments/{id}/decision; evento CommentDecisionMarkedEvent). El comentario da decision, decidedBy y decidedAt; el ticket, decisions (más antiguas primero, GET /api/issues/{issue}/decisions), que la web muestra encima de la descripción. Se buscan por su texto: decisions(projectId?, search, limit), GET /api/projects/{project}/decisions y, de todos mis proyectos, GET /api/me/decisions (más recientes primero; sin las de la papelera y, para un invitado, solo las de sus tickets).
  • Bloqueos: un ticket espera a otro si ese otro lo bloquea (BLOCKS) o si él depende de ese otro (DEPENDS_ON), mientras el otro no esté hecho ni cancelado. blockedBy lo calcula al leer (los enlaces no guardan el estado del otro ticket), con una sola consulta por tablero o página.
  • Duplicar: duplicateIssue / POST /api/issues/{issue}/duplicate copia lo que dice el ticket (tipo, título con «(copia)», descripción, área, prioridad, etiquetas, Figma, estimación, fechas, épica y pasos de la checklist sin marcar), no dónde está: la copia empieza en el primer estado, sin asignar, sin sprint, release, comentarios ni adjuntos.
  • Cambios de UI: figmaUrl debe ser un enlace https://…figma.com/….
  • Adjuntos: los suben los miembros (no los lectores), hasta ATTACHMENTS_MAX_MB (25 MB) por archivo y 50 por ticket; pueden ir en el ticket o bajo uno de sus comentarios (commentId). Quita uno quien lo subió o un admin. Los bytes se guardan primero y el evento después, así que nunca queda un adjunto apuntando a un archivo que no existe; si el evento no se guarda, el archivo se borra. Al borrar un ticket se borran sus archivos.
  • Épicas y subtareas: un ticket puede colgar de otro del mismo proyecto (parent), hasta tres niveles (épica → ticket → subtarea) y sin bucles; se comprueba sobre el event store, no sobre el read model. Una «épica» es cualquier ticket de primer nivel del que cuelgan otros (epics(projectId) da su progreso: terminados = hechos o cancelados, y puntos). Si se borra el padre, sus hijos vuelven al primer nivel.
  • Orden del backlog: cada ticket tiene un rank (texto que se compara carácter a carácter). Mover uno (rankIssue, PUT /api/issues/{issue}/rank) solo reescribe ese ticket: recibe un rango entre el de quien queda encima y el de quien queda debajo, leídos del event store; si entretanto el orden cambió, la API lo rechaza y la web recarga. Un ticket que nunca se movió ocupa su orden de creación (IssueRankedEvent no existe para él). Las columnas del tablero y issues(filter: { sort: RANK }) siguen este orden.
  • Alertas → bugs: un owner o admin activa en Integraciones una URL secreta por proyecto, POST /api/webhooks/alerts/{proyecto}/{token} (el token es el secreto: Sentry o Uptime Kuma solo dejan pegar una URL). Entiende Sentry (integración interna, cabecera sentry-hook-resource, o el plugin de webhooks), Uptime Kuma, Prometheus Alertmanager y Grafana; cualquier otra herramienta manda JSON con title y, si quiere, fingerprint, description, url y resolved. Cada aviso tiene una huella (el id del problema en Sentry, el monitor en Uptime Kuma, el groupKey en Alertmanager o fingerprint): la primera vez TaskStream abre un bug (actor system, etiqueta alerta, el tipo y la prioridad elegidos; sin tipo, el primero con icono de bug); mientras ese bug siga abierto, las repeticiones van a él (un comentario como mucho cada 30 minutos, con cuántas van) y «resuelta» se dice en él; si ya está cerrado, abre otro que nombra el anterior. Responde { action: created | repeated | resolved | ignored, issueKey }; proyecto desconocido, alertas apagadas o token malo responden igual: 403. Las alertas de un proyecto se atienden de una en una, así que dos iguales a la vez abren un solo bug. Configuración: alertIntegration, saveAlertIntegration(input: { typeId, priority }), rotateAlertToken, disableAlertIntegration; GET/PUT/DELETE /api/projects/{project}/alert-integration y POST …/token. Vive en MongoDB (alert_integrations, y alert_traces con qué bug abrió cada huella), no en el event store.
  • Integración con Git: un owner o admin añade en la configuración del proyecto sus repositorios (api, web, admin…), cada uno con su propio secreto. Todos llaman a la misma URL, POST /api/webhooks/git/{proyecto}, con los eventos push y pull/merge request; GitHub y Gitea firman el cuerpo con el secreto (HMAC-SHA256) y GitLab lo envía como X-Gitlab-Token, y el secreto que valida la firma dice de qué repositorio viene. Cada commit, rama o PR que nombre una clave del proyecto (WEB-12, también en minúsculas) queda en devLinks del ticket con el nombre de su repositorio (IssueDevLinkRecordedEvent, actor system); el mismo enlace solo vuelve a contar si cambia (una PR fusionada). Proyecto desconocido, sin repositorios o firma mala responden igual: 403. Cambiar el secreto de un repositorio o quitarlo no afecta a los demás, y lo ya enlazado se queda. Los repositorios viven en MongoDB (git_integrations, un documento por proyecto), no en el event store; un proyecto que activó el webhook único de antes conserva su secreto como repositorio «principal».
  • Escaparate público: GET /api/explore/projects/{project}/showcase resume un proyecto público para webs de fuera (la del colectivo lo enseña en la página de cada proyecto): épicas con fechas y progreso, las últimas versiones (con sus notas si ya salieron), el sprint en curso y los tickets abiertos con la etiqueta good-first-issue. Sin token, con Cache-Control: public, max-age=300; un proyecto privado responde 404.
  • Papelera: trashIssue / DELETE /api/issues/{issue} manda un ticket a la papelera (quien lo abrió o un owner/ admin; IssueTrashedEvent): desaparece del tablero, las búsquedas, las estadísticas y el progreso, y no se puede cambiar hasta recuperarlo (restoreIssue / POST …/restore, IssueRestoredEvent). trash(projectId) / GET /api/projects/{project}/trash los lista. A los TRASH_DAYS días (30) se borra del todo, con la cascada de siempre: comentarios, enlaces y subtareas de vuelta al primer nivel.
  • Edición en bloque: bulkUpdateIssues / POST /api/issues/bulk aplica el mismo cambio (estado, persona o unassign, sprint o noSprint, release o noRelease, prioridad, etiquetas que añadir) a hasta 100 tickets. Cada uno pasa por los comandos normales (permisos, flujo, sprints y releases abiertos) y puede negarse sin parar al resto; la respuesta dice cuáles. La web parte selecciones mayores (la Lista enseña hasta 200) en tandas de 100, una tras otra, y suma el resultado.
  • Archivar una release: archiveRelease(releaseId, moveTo) / POST /api/releases/{id}/archive ({ moveTo }). Sus tickets sin terminar pasan a moveTo (otra release abierta del mismo proyecto) o, sin él, se quedan sin release; los terminados se quedan, como registro de lo que salió. Igual que al cerrar un sprint con carryOverTo.
  • Plantillas por tipo: en la configuración, cada tipo de ticket puede llevar template con una descripción (hasta 5000 caracteres) y una checklist (hasta 20 pasos). Al crear un ticket de ese tipo, la descripción se usa si llega vacía y los pasos se añaden siempre (las importaciones no aplican plantillas).
  • Automatizaciones: owners y admins crean reglas (hasta 50 por proyecto, por GraphQL: automations, createAutomation, updateAutomation, deleteAutomation). Disparador: ticket creado, cambio de estado (opcionalmente a uno concreto), asignación, comentario o PR fusionada (integración Git). Condiciones: tipo, prioridad, área o etiqueta. Acción: mover a un estado, asignar (a alguien o a quien lo abrió), añadir una etiqueta, poner una prioridad, comentar o meter en el sprint en curso. Actúan como system («TaskStream») y respetando el flujo: si la transición no está permitida, la regla no hace nada y lo deja en lastError. Lo que hace una regla no dispara otras (no hay cadenas ni bucles) y cada regla corre una sola vez por evento aunque se reentregue (automation_runs). Viven en MongoDB (automations), fuera del event store.
  • Webhooks salientes: owners y admins dan de alta URLs (hasta 20 por proyecto) suscritas a eventos (issue.created, issue.status_changed, comment.added, release.shipped, sprint.closed… — la lista en webhookEvents / GET /api/webhook-events), a familias (issue.*) o a todo (*). Cada envío es un POST JSON con event, deliveryId, occurredAt, actorId, project, issue (si va de un ticket) y data (los campos del evento), y las cabeceras X-TaskStream-Event, X-TaskStream-Delivery y X-TaskStream-Signature: sha256=<HMAC del cuerpo con el secreto>. El secreto solo se muestra al crearlo. Se reintenta a los 2 y 10 s (no ante un 4xx), con 5 s de espera máxima; se guarda el último envío. Salen en segundo plano y no son duraderos: si la API se reinicia a mitad, ese envío se pierde. No llaman a direcciones privadas salvo con WEBHOOKS_ALLOW_PRIVATE=true.
  • Importar y exportar: GET /api/projects/{project}/export.csv da todos los tickets (UTF-8 con BOM; las celdas que una hoja de cálculo ejecutaría como fórmula empiezan por '). POST /api/projects/{project}/import (owners y admins, multipart file, hasta 5 MB y 2000 filas) lee ese mismo CSV o una exportación CSV de Jira: tipos, estados y personas se buscan por id o por nombre (los de Jira por su significado: Story → Feature, Highest → crítica, categoría In Progress → primer estado en curso…), lo que no encaja se queda con un valor por defecto y un aviso, y los padres se reconstruyen al final. Con dryRun=true solo se cuenta lo que se crearía. Cada ticket importado es una creación normal (con sus eventos), en el estado que traía si está activo.
  • Límite WIP: cada estado puede tener wipLimit (1-999) en la configuración del proyecto. Es un aviso, no un bloqueo: la API acepta pasarse y el tablero marca la columna.
  • Sprints: los planifica y gestiona quien administra el proyecto (owner o admin); mete tickets cualquier miembro. Un sprint va de PLANNED a ACTIVE (uno solo en curso por proyecto) y a CLOSED; dura como mucho 90 días y un sprint en curso conserva su fecha de inicio. Un sprint cerrado ya no acepta ni pierde tickets. Al cerrarlo se elige adónde van los tickets sin terminar: a otro sprint abierto del proyecto o al backlog; los terminados se quedan como registro. Solo se borran sprints que no han empezado (sus tickets vuelven al backlog).
  • Vistas guardadas: filtros del tablero con nombre. Las guardan los miembros del proyecto (los lectores, solo para sí); compartir una la hace visible a todo el proyecto. Borra una su autor, o un admin si es compartida; como mucho 50 por persona y proyecto. Son preferencias, no historia: viven en MongoDB (saved_filters), fuera del event store, y se borran con el proyecto.
  • Checklist: hasta 100 pasos de hasta 300 caracteres por ticket; los cambia quien puede editar el ticket.

Calendario (iCalendar)

Cada persona puede suscribir sus fechas en Google Calendar, Outlook o Apple Calendar con una URL secreta: GET /api/calendar/{token}.ics (sin sesión: las aplicaciones de calendario no pueden iniciarla). Trae, como eventos de día completo:

  • sus tickets sin terminar con fecha límite, asignados o que sigue;
  • los sprints planificados o en curso de sus proyectos;
  • las releases planificadas con fecha objetivo.

Cada evento enlaza a la web (WEB_URL). El token (32 bytes aleatorios) se crea, renueva o retira con GET, POST y DELETE /api/me/calendar (GraphQL: myCalendarFeed, createCalendarFeed, revokeCalendarFeed); renovarlo deja sin servicio la URL anterior. Se guarda tal cual en calendar_feeds para poder volver a copiar la URL: solo da lectura de fechas y títulos que ya están en el read model.

Panel para clientes

Cada proyecto puede enseñar su estado a sus clientes con un enlace de solo lectura, sin cuenta: /share/{token} en la web, que lee GET /api/shared/dashboards/{token} (GraphQL: sharedDashboard(token)), ambos sin sesión. Solo trae los bloques elegidos, y los demás llegan a null:

Bloque Qué da
flow Tickets en cada estado activo, con su nombre y color
sprint El sprint en curso, como en el panel (¿llega?)
releases Las releases planificadas con su previsión al ritmo real
epics Las épicas abiertas (8, por fecha): título, fecha, terminados y porcentaje
activity Movimientos por día de las últimas 12 semanas

No enseña personas ni tickets sueltos (salvo las épicas). Propietarios y admins lo activan, cambian sus bloques o lo renuevan con GET, PUT y DELETE /api/projects/{project}/share ({ blocks, renew }; GraphQL: dashboardShare, shareDashboard, unshareDashboard). Renovarlo deja sin servicio el enlace anterior. El token (32 bytes aleatorios) se guarda tal cual en dashboard_shares para poder volver a copiar el enlace, y se borra con el proyecto.

Notificaciones

Se derivan de los eventos (NotificationsEventHandler) y se guardan por persona en MongoDB: no son estado de ningún agregado, solo cada uno lee y descarta las suyas.

Cuándo A quién Tipo
Se crea un ticket asignado o se asigna La persona asignada ASSIGNED
Un ticket cambia de estado Quien lo tiene asignado y quien lo abrió STATUS_CHANGED
Un ticket vuelve de QA sin pasar a hecho Los mismos, con el motivo QA_FAILED
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

Push (Web Push): además de en la campana y, si se pidió, por correo, cada aviso llega a los dispositivos donde la persona activó «Avisos en este dispositivo», con TaskStream cerrado. El navegador se suscribe con la clave pública VAPID (notificationSettings.pushPublicKey, null si no hay claves) y la API guarda la suscripción (subscribePush / POST /api/me/push-subscriptions; se retira con unsubscribePush / DELETE). El mensaje va cifrado (aes128gcm) y firmado con VAPID, con { title, body, url, tag }; si el servicio push responde 404 o 410, la suscripción se olvida. Como el correo, un fallo de envío no frena los eventos.

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 REMINDERS_TIMEZONE). Cada aviso tiene id reminder:<ticket>:<fecha>:<fase>:<persona>, así que no se repite aunque varias instancias lo comprueben a la vez.

Nadie recibe aviso de lo que hizo él mismo, y cada persona recibe uno por evento: la primera razón de la tabla gana (una mención antes que «comentó en tu ticket», y eso antes que «un ticket que sigues»).

Menciones: se escriben @[Nombre](id-de-usuario) (la web las inserta al escribir @); el id dice a quién avisar y el nombre es lo que se ve, así que renombrar a alguien no rompe comentarios antiguos. Solo avisan a miembros del proyecto. Seguir: cualquier miembro, también los lectores, puede seguir un ticket (watchIssue/unwatchIssue, eventos IssueWatchedEvent/IssueUnwatchedEvent), y quien lo abre, quien lo tiene asignado y quien comenta pasan a seguirlo (PeopleFollowSaga, CommentersFollowSaga). Quien sigue un ticket (WATCHED) sabe cuándo cambia de estado, se asigna, se comenta, cambia su prioridad o su fecha límite, o entra o sale de un sprint; las ediciones menores (título, etiquetas…) no avisan. «Los que sigo»: watching en myIssues / GET /api/me/issues y en la búsqueda de un proyecto (issues(filter: { watching: true }), ?watching=true; sin sesión, ninguno). Recordármelo: cada persona pone en un ticket un recordatorio con fecha y hora y nota opcional (setReminder(issueId, at, note), clearReminder, Issue.myReminder, myReminders; PUT/DELETE /api/issues/{issue}/reminder, GET /api/me/reminders): uno por ticket, en el futuro y como mucho a un año. Cada minuto IssueReminderScheduler avisa de los que tocan (REMINDER, por la campana, el navegador y el correo; una vez, por su id) y olvida los de tickets en la papelera o fuera de alcance. Privados y sin historial (issue_reminders); REMINDERS_ENABLED=false los apaga junto con los de fecha límite. Seguir todo: seguir un ticket con todo lo que cuelga de él (una épica, un ticket con subtareas): followTree/unfollowTree, Issue.followingTree; PUT/DELETE /api/issues/{issue}/follow-tree. Quien lo hace oye lo mismo que un seguidor de cada ticket de debajo, a cualquier profundidad, también de los que se añadan, y además cuándo uno entra (IssueParentChangedEvent). Personal y sin historial (colección tree_follows); los invitados de un proyecto privado no oyen los tickets que no son suyos. Fijar: cada persona fija hasta 20 tickets para tenerlos a mano en el inicio y en la búsqueda (pinIssue/unpinIssue, myPins, Issue.pinned; GET /api/me/pins, PUT/DELETE /api/issues/{issue}/pin). Son privados y sin historial (colección pinned_issues, como las vistas de «Mi trabajo»); los que están en la papelera o fuera de su alcance no se listan, y al llegar a 20 se olvidan primero los que ya no puede leer. El id es <agregado>:<versión>:<persona>, así que reentregar un evento no duplica avisos. Llegan en vivo por la subscription notificationAdded y, si la instalación tiene SMTP_URL, también por correo a quien lo active (email, desactivado por defecto); un correo que falla se registra y no bloquea el stream.

Eventos de dominio

Todo cambio es un evento guardado en el event store (stream = id del agregado). Llevan actorId, occurredAt, aggregateId, projectId y aggregateVersion (la posición en el stream).

Agregado Eventos
Project ProjectCreatedEvent, ProjectUpdatedEvent, ProjectSettingsChangedEvent, ProjectMemberAddedEvent, ProjectMemberRoleChangedEvent, ProjectMemberRemovedEvent, ProjectDeletedEvent
Issue IssueCreatedEvent, IssueUpdatedEvent, IssueStatusChangedEvent, IssueAssignedEvent, IssueLinkedEvent, IssueUnlinkedEvent, IssueReleaseChangedEvent, IssueAttachmentAddedEvent, IssueAttachmentRemovedEvent, IssueWatchedEvent, IssueUnwatchedEvent, IssueParentChangedEvent, IssueRankedEvent, IssueSprintChangedEvent, IssueDevLinkRecordedEvent, IssueChecklistItemAddedEvent, IssueChecklistItemUpdatedEvent, IssueChecklistItemRemovedEvent, IssueDeletedEvent
Comment CommentAddedEvent, CommentEditedEvent, CommentDeletedEvent
Sprint SprintCreatedEvent, SprintUpdatedEvent, SprintStartedEvent, SprintClosedEvent, SprintDeletedEvent
Release ReleaseCreatedEvent, ReleaseUpdatedEvent, ReleaseShippedEvent, ReleaseNotesWrittenEvent, ReleaseArchivedEvent, ReleaseDeletedEvent

IssueStatusChangedEvent lleva además meta con el nombre y la categoría de los dos estados en el momento del cambio, para que el historial y las estadísticas no cambien si luego se renombra o recategoriza un estado.

Estadísticas

Una proyección más (issue_timeline en MongoDB) guarda cada paso de cada ticket: a qué estado pasó, con qué categoría, quién lo tenía asignado, y cuándo entró o salió de una release. Las estadísticas se calculan sobre ella con funciones puras (application/src/lib/analytics):

Métrica Definición
Creados vs. resueltos Por semana (lunes, UTC): tickets creados y tickets que entraron en una categoría DONE
Flujo acumulado Tickets en cada estado al final de cada día
Tiempo de ciclo Del primer paso a IN_PROGRESS o QA hasta DONE; media y percentil 85
Tiempo de entrega De la creación hasta DONE; media y percentil 85
Throughput Tickets resueltos por semana y por la persona que los tenía asignados
QA Salidas de un estado QA: a DONE cuenta como superado; a cualquier otro sitio salvo CANCELLED, como fallido
Burndown de un sprint Por día del sprint: tickets en él (sin cancelados), cuántos siguen abiertos y sus puntos; línea ideal hasta cero
Burnup de una release Por día, desde que se creó hasta que se publica (o hoy, máx. 180 días): tickets en ella (sin cancelados) y hechos

Los tickets anteriores a esta proyección solo tienen su creación y su estado actual; para tener su historia completa, reconstruye el read model (ver Despliegue).

La cartera (portfolio en GraphQL, GET /api/me/portfolio) junta, para cada proyecto activo de quien pregunta (no archivado y en el que no es invitado), los tickets abiertos, vencidos, bloqueados y atascados, el sprint en curso, la previsión de sus releases y sus épicas abiertas (las 6 con fecha límite más cercana). Se calcula con lo mismo que el panel.

La portada (myHome en GraphQL, GET /api/me/home) junta lo demás que se ve en la página de inicio (analytics/home.ts):

Bloque Qué da
agenda Los próximos 7 días: mis tickets abiertos (asignados o que sigo) que vencen, fin de sprints en curso y releases planificadas
activity Lo último en mis proyectos (30 entradas; sin los proyectos en los que soy invitado)
team / me Tickets terminados por día en los últimos 14 días; esta semana y la anterior, de mis equipos y míos (por quien lo tenía asignado), y mi racha
sprints Los sprints en curso, con el mismo pulso que el panel (¿llega?)
projects Por proyecto: abiertos, vencidos, actividad por día (14 días) y el sprint en curso con su porcentaje
suggestions Para cuando no tengo nada: sin asignar, esperando QA y propuestas por revisar (estas, solo a quien administra); las 5 mayores
firstSteps Si ya creé un proyecto, invité a alguien, abrí un ticket y conecté el calendario
online Quién está ahora en un ticket de mis proyectos (por la presencia, una entrada por persona)

El panel del proyecto (Project.insights en GraphQL, GET /api/projects/{project}/insights) lee lo mismo para decir qué mirar ahora (analytics/insights.ts; acepta since, un instante ISO):

Bloque Qué da
Sprint en curso Días que quedan, tickets y puntos hechos, ritmo (puntos al día) y el que haría falta; behind si no llega
Riesgo Abiertos vencidos, que vencen en 7 días, bloqueados (por un ticket abierto) y atascados (7 días o más sin moverse)
Antigüedad Lo que está en curso o en QA, por días en su estado, con el percentil 85 del tiempo de ciclo (90 días) como umbral
Cuellos de botella Días de media en cada estado en curso o de QA (estancias que terminaron en 90 días), cuántos hay y su límite WIP
Próximas releases Para cada release planificada (por fecha objetivo): fecha probable (p50) y segura (p85) simulando 500 futuros con lo que se cerró cada día de las últimas 6 semanas (Monte Carlo); cada release cuenta lo que queda de las anteriores. ON_TRACK, AT_RISK, LATE, NO_DATE, UNKNOWN (sin ritmo aún) o DONE
Cambios Con since (por ejemplo, la última visita): tickets creados, terminados y vueltos de QA desde entonces, y mis menciones en el proyecto
Equipo Por persona: tickets y puntos abiertos, bloqueados, puntos cerrados por sprint (últimos 3 cerrados) y overloaded si tiene abiertos más de los que suele cerrar
Salud del backlog Abiertos sin estimar, sin área, sin asignar y sin tocar en 90 días
Calidad Bugs (tipos con icono bug): abiertos por prioridad, nuevos y cerrados por semana (8 semanas), lo que QA devolvió y dejó pasar por área (90 días) y bugs abiertos en los 14 días tras cada una de las 3 últimas releases
Campos Para cada campo personalizado de lista: tickets abiertos por opción y sin valor
Actividad Movimientos por día (todo lo de la actividad) de las últimas 12 semanas, desde un lunes: 84 días, en heatmap
Recorrido De los tickets terminados en 90 días: días de media de creado a hecho, días y parte del total en cada estado por el que pasaron (también la espera en Backlog o Por hacer) y flowEfficiency, la parte en un estado «en curso»
Propuestas Si el público ha propuesto algo (tickets con la etiqueta «propuesta»): recibidas (y en 30 días), esperando (sin empezar ni planificar), en marcha, hechas y descartadas, y las 5 más votadas que esperan; si no, null

3.2 GraphQL

Las operaciones requieren Authorization: Bearer <token de Pireon> (en subscriptions, en el connection_init), salvo las lecturas de proyectos públicos (project, issue, issues, release, activity, publicProjects y sus campos), que funcionan también sin token.

Queries

Query Devuelve
me El usuario autenticado, con sus roles en Pireon
users(search, limit) Usuarios de Pireon (para asignar o añadir miembros)
user(id) Un usuario
myProjects Proyectos de los que soy miembro
myIssues(assignedToMe, statuses, categories, search, limit, offset) «Mi trabajo»: tickets de todos mis proyectos, asignados a mí (por defecto) o de cualquiera (p. ej. categories: [QA])
upcomingReleases(limit) Releases planificadas de mis proyectos, por fecha objetivo
publicProjects Proyectos públicos (sin token)
project(idOrKey) Un proyecto (mío, o público) con su settings. Campos calculados: board(releaseId, assigneeId, unassigned, area, types, priorities, labels, search, perColumn), stats, analytics(days), releases
issues(filter, limit, offset) Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, ticket padre, fecha límite entre dueFrom y dueTo, texto); sort: RANK para el orden del backlog
webhooks(projectId), webhookEvents Webhooks salientes del proyecto (owners y admins) y los eventos a los que se pueden suscribir
gitIntegration(projectId) Si el webhook de Git está activo, su ruta y (para owners y admins) el secreto
sprints(projectId), sprint(id) Sprints del proyecto (en curso, planificados, cerrados). Campos calculados: progress, burndown, issues
savedFilters(projectId) Mis vistas guardadas del tablero y las compartidas con el proyecto
epics(projectId) Épicas del proyecto (tickets de primer nivel de los que cuelgan otros) con el progreso de sus hijos
issue(idOrKey) Un ticket. Campos calculados: allowedTransitions, linkedIssues, comments, history
release(id) Una release. Campos calculados: progress, burnup, issues
activity(projectId, issueId?, limit, before?, after?) Feed de actividad, del más reciente al más antiguo; con after y before, una ventana (un día, por ejemplo). Por REST, los mismos parámetros en …/activity

Mutations

Todas devuelven CommandResult { id, key, version }.

Ámbito Mutations
Proyectos createProject, updateProject, updateProjectSettings, deleteProject(confirmKey), addProjectMember, changeProjectMemberRole, removeProjectMember
Tickets createIssue, updateIssue, changeIssueStatus, assignIssue, linkIssues, unlinkIssues, setIssueRelease, setIssueParent, rankIssue, addChecklistItem, updateChecklistItem, removeChecklistItem, watchIssue, unwatchIssue, pinIssue, unpinIssue, followTree, unfollowTree, clearReminder (devuelven true), setReminder, removeAttachment (subir: por REST), duplicateIssue, voteIssue
Comentarios addComment(replyTo), editComment, deleteComment, reactToComment, markDecision(commentId, on), resolveThread(commentId, resolved, conclusion), typing(issueId, threadId); consulta decisions(projectId, search)
Webhooks createWebhook (devuelve el secreto), updateWebhook, deleteWebhook, testWebhook
Git addGitRepository (devuelve su secreto), rotateGitRepositorySecret, removeGitRepository
Sprints createSprint, updateSprint, startSprint, closeSprint(carryOverToSprintId), deleteSprint, setIssueSprint
Vistas saveFilter(input) (devuelve el id), deleteSavedFilter(id), GET/POST /api/me/views, DELETE /api/me/views/{id} (mis vistas de «Mi trabajo»)
Avisos markNotificationsRead(ids), updateNotificationSettings(email)
Releases createRelease, updateRelease, publishRelease(force), writeReleaseNotes, archiveRelease

Subscriptions

Subscription Se emite cuando
issueChanged(projectId) Se crea o cambia cualquier ticket del proyecto (incluye el ticket completo y action = nombre del evento)
commentChanged(issueId) Se añade, edita o borra un comentario (comment es null al borrar, o el que queda sin texto si tenía respuestas)
typing(issueId) Alguien escribe en el ticket (threadId: la conversación que responde; null, un comentario nuevo). Llegan también los propios
activityAdded(projectId) Hay una nueva entrada en el feed
releaseChanged(projectId) Cambia una release
sprintChanged(projectId) Se planifica, cambia, empieza, cierra o borra un sprint (sprint es null al borrar)
projectChanged(projectId) Cambian los datos, la configuración o los miembros del proyecto
myProjectsChanged Me añaden a un proyecto o me quitan de uno

Solo los miembros pueden suscribirse a un proyecto; las conexiones sin token válido se cierran con el código 4403.

Ejemplos

Estados, tipos y áreas son texto (ids de la configuración del proyecto), no enums: van entre comillas.

mutation ($settings: ProjectSettingsInput!) {
  # La configuración entera (la que devuelve project.settings, cambiada).
  updateProjectSettings(projectId: "…", settings: $settings) {
    version
  }
}

mutation {
  createProject(input: { key: "SHOP", name: "Checkout revamp" }) {
    id
    key
    version
  }
}

mutation {
  createIssue(input: { projectId: "…", type: "BUG", title: "El botón de pagar no responde en Safari", area: "FRONTEND", priority: CRITICAL, assigneeId: "102", labels: ["safari"] }) {
    id
    key
    version
  }
}

mutation {
  changeIssueStatus(input: { issueId: "…", status: "QA_FAILED", reason: "Sigue fallando en Safari 17" }) {
    version
  }
}

mutation {
  linkIssues(input: { issueId: "<FE>", type: DEPENDS_ON, targetIssueId: "<BE>" }) {
    version
  }
}

query {
  project(idOrKey: "SHOP") {
    stats {
      total
      open
      byStatus {
        key
        count
      }
      byAssignee {
        user {
          name
        }
        count
      }
    }
    board {
      status
      total
      issues {
        key
        title
        assignee {
          name
        }
      }
    }
    releases {
      name
      status
      progress {
        percent
      }
    }
    analytics(days: 90) {
      createdVsResolved {
        week
        created
        resolved
      }
      cycleTime {
        averageDays
        p85Days
      }
      qa {
        failureRate
      }
    }
  }
  release(id: "…") {
    burnup {
      dates
      scope
      done
    }
  }
  issue(idOrKey: "SHOP-1") {
    status
    allowedTransitions
    links {
      type
      direction
      issueKey
    }
    comments {
      items {
        author {
          name
        }
        body
      }
    }
    history {
      summary
      occurredAt
    }
  }
}
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'ws://localhost:3000/graphql',
  connectionParams: { authorization: `Bearer ${token}` },
});

client.subscribe(
  {
    query: 'subscription($p: ID!) { issueChanged(projectId: $p) { action version issue { key status } } }',
    variables: { p: projectId },
  },
  { next: console.log, error: console.error, complete: () => {} },
);

Errores

Los errores de dominio llegan como errores GraphQL con extensions.code:

Código Significado
UNAUTHENTICATED Falta el token o Pireon lo rechaza
NOT_FOUND No existe o no eres miembro
FORBIDDEN No tienes permiso (p. ej. un VIEWER modificando)
VALIDATION Datos inválidos (clave de proyecto, URL de Figma…)
BUSINESS_RULE Regla de negocio (transición no permitida, release bloqueada…)
CONFLICT Otro cambio llegó antes (concurrencia optimista): reintenta

3.3 REST con OpenAPI (para otras apps)

Los mismos casos de uso que GraphQL, como API REST documentada con OpenAPI 3.0, para que otras apps de la plataforma la consuman o generen un cliente. Son otro adaptador de entrada (presenters/http) que llama a los mismos *.service.ts: misma validación, permisos y errores.

Swagger UI /api/docs (con «Authorize» contra Pireon)
Documento OpenAPI /api/docs-json (y /api/docs-yaml)
Fichero exportado apps/api/openapi.json: lo reescribe nx serve api al arrancar, como schema.gql

Autenticación: siempre a través de Pireon

Cualquier app o usuario que la consuma pasa por el OAuth de Pireon: todas las rutas, salvo health, auth, el webhook, explore y las lecturas de proyectos públicos, exigen Authorization: Bearer <token de Pireon> (el AuthGuard global, igual que en GraphQL). En el documento OpenAPI, las rutas donde el token es opcional lo indican con un requisito de seguridad vacío ({}). Declara dos esquemas:

  • pireon-oauth (OAuth2): Authorization Code + PKCE contra oauth.pireon.dev/oauth/authorize y, si PIREON_PASSWORD_GRANT=true, password. El intercambio de tokens va por POST /api/auth/token (Pireon no tiene CORS en /oauth/token y el client_secret se queda en el servidor). Para usar «Authorize» en Swagger UI, registra <api>/api/docs/oauth2-redirect.html como redirect URI del cliente en Pireon.
  • pireon (Bearer): para una app que ya tiene un token de Pireon, por ejemplo con su propio cliente OAuth.

Lo que cada usuario puede ver o cambiar depende de su rol en cada proyecto, como en la web.

Recursos

Proyectos y tickets se pueden indicar por id o por clave (WEB, WEB-12).

Recurso Rutas
Usuarios GET /api/users/me, GET /api/users?search=, GET /api/users/{id}
Proyectos GET/POST /api/projects, GET/PATCH/DELETE /api/projects/{project} (DELETE …?confirm=CLAVE), POST …/archive, POST …/unarchive, PUT …/settings, …/board, …/epics, …/stats, …/analytics?days=, …/insights, …/activity, …/members (POST, PATCH, DELETE)
Tickets GET /api/me/home (portada), GET /api/me/portfolio (cartera), GET /api/me/issues (mi trabajo; filtros project, priority, bugsOnly, overdue), GET/POST /api/projects/{project}/issues, GET/PATCH /api/issues/{issue}, PUT …/status, PUT …/assignee, PUT …/release, PUT/DELETE …/watch, PUT/DELETE …/follow-tree, PUT/DELETE …/reminder, GET /api/me/reminders, GET /api/me/pins y PUT/DELETE …/pin (fijados), PUT/DELETE …/presence, PUT …/parent, PUT …/rank, POST …/checklist, PATCH/DELETE …/checklist/{item}, POST/DELETE …/links, GET/POST …/comments, GET …/history, POST /api/issues/{issue}/duplicate, PUT/DELETE …/vote
Comentarios PATCH/DELETE /api/comments/{commentId}, POST …/reactions, PUT/DELETE …/decision, PUT/DELETE …/resolution; GET /api/issues/{issue}/decisions, GET /api/projects/{project}/decisions, GET /api/me/decisions
Sprints GET/POST /api/projects/{project}/sprints, GET/PATCH/DELETE /api/sprints/{sprint}, POST …/start, POST …/close, GET …/progress, …/burndown, …/issues, PUT /api/issues/{issue}/sprint
Vistas GET/POST /api/projects/{project}/filters, DELETE /api/filters/{id}
Avisos GET /api/me/notifications, POST /api/me/notifications/read, GET/PUT /api/me/notification-settings
Adjuntos POST /api/issues/{issue}/attachments (multipart: file y opcional commentId), GET/DELETE /api/issues/{issue}/attachments/{id}
Releases GET /api/me/releases (próximas), GET/POST /api/projects/{project}/releases, GET/PATCH /api/releases/{id}, PUT …/notes, GET …/notes/draft, …/progress, …/burnup, …/issues, POST …/publish, POST …/archive
Explorar GET /api/explore/projects (proyectos públicos, sin token)
Compartir GET/PUT/DELETE /api/projects/{project}/share (enlace para clientes), GET /api/shared/dashboards/{token} (lo que enseña, sin token)
Importar GET /api/projects/{project}/export.csv, POST /api/projects/{project}/import?dryRun= (multipart file)
Webhooks GET /api/webhook-events, GET/POST /api/projects/{project}/webhooks, PATCH/DELETE /api/outgoing-webhooks/{id}, POST …/test
Git GET /api/projects/{project}/git-integration, POST …/git-integration/repositories, POST …/repositories/{id}/secret, DELETE …/repositories/{id}, POST /api/webhooks/git/{project} (lo llama el host de Git)
Alertas GET/PUT/DELETE /api/projects/{project}/alert-integration, POST …/alert-integration/token, POST /api/webhooks/alerts/{project}/{token}
Explorar GET /api/explore/projects/{project}/showcase (sin token, proyectos públicos): hoja de ruta con votos, versiones, sprint en curso, tickets good-first-issue y propuestas abiertas; POST …/proposals (sin token)
Sistema GET /api/health, GET /api/auth/config, POST /api/auth/token, POST /api/webhooks/pireon

Los archivos van por REST porque GraphQL no está hecho para bytes; la lista de adjuntos de un ticket llega con el ticket (attachments, en GraphQL y en REST). La descarga sirve en línea solo imágenes, vídeo, audio, PDF y texto plano, y todo con X-Content-Type-Options: nosniff y una CSP sandbox: un HTML o un SVG subido se descarga, nunca se ejecuta en el origen de la API.

Los filtros de tickets aceptan status (ids del proyecto) y category (?category=QA), repetidos o separados por comas. Las escrituras responden { id, key?, version } en cuanto el evento se guarda; las lecturas salen del read model, que se pone al día en milisegundos (un GET justo después puede devolver la versión anterior, o 404 tras crear). Para tiempo real, las subscriptions de GraphQL.

Errores

{ statusCode, code?, message }: 400 entrada no válida (message es una lista), 401 sin token o token rechazado por Pireon, 403/404 sin acceso, 409 cambio concurrente (la API ya lo ha reintentado 5 veces; raro) y 422 regla de negocio (p. ej. una transición que el workflow no permite).

Generar un cliente en otra app

npx openapi-typescript apps/api/openapi.json -o src/task-stream-api.d.ts      # tipos
# o un cliente completo: openapi-generator, orval, ng-openapi-gen…

Ver también Autenticación.