feat: sprints with burndown and carry-over

Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:59:01 +02:00
parent d2e31789a7
commit 50d3ffd1dd
104 changed files with 7209 additions and 33 deletions

View File

@@ -28,6 +28,12 @@ import {
ReleaseCreatedEvent,
ReleaseShippedEvent,
ReleaseUpdatedEvent,
IssueSprintChangedEvent,
SprintClosedEvent,
SprintCreatedEvent,
SprintDeletedEvent,
SprintStartedEvent,
SprintUpdatedEvent,
TrackerEvent,
} from '@task-stream/api/tracker/domain';
@@ -37,6 +43,8 @@ export interface SummaryContext {
issueKey: string;
projectKey: string;
releaseName: string;
/** '' when unknown (e.g. the sprint was deleted). */
sprintName: string;
/** The project's workflow: names of statuses and types. */
workflow: ProjectWorkflow;
}
@@ -146,6 +154,12 @@ export function describeEvent(
if (event instanceof IssueAttachmentRemovedEvent)
return `${actor} quitó el adjunto ${event.fileName} de ${issueKey}`;
if (event instanceof IssueSprintChangedEvent) {
const sprint = ctx.sprintName ? `el sprint ${ctx.sprintName}` : 'un sprint';
return event.sprintId
? `${actor} metió ${issueKey} en ${sprint}`
: `${actor} sacó ${issueKey} de ${sprint}`;
}
if (event instanceof IssueParentChangedEvent) {
return event.parentKey
? `${actor} colocó ${issueKey} bajo ${event.parentKey}`
@@ -178,5 +192,20 @@ export function describeEvent(
return `${actor} publicó la release ${ctx.releaseName} 🚀`;
if (event instanceof ReleaseArchivedEvent)
return `${actor} archivó la release ${ctx.releaseName}`;
const sprint = ctx.sprintName || 'un sprint';
if (event instanceof SprintCreatedEvent)
return `${actor} planificó el sprint ${event.sprint.name}`;
if (event instanceof SprintUpdatedEvent)
return `${actor} actualizó el sprint ${sprint}`;
if (event instanceof SprintStartedEvent)
return `${actor} empezó el sprint ${sprint} 🏁`;
if (event instanceof SprintClosedEvent) {
const left = event.unfinishedIssues
? `; ${event.unfinishedIssues} ticket(s) sin terminar pasan ${event.carryOverTo ? 'al siguiente sprint' : 'al backlog'}`
: '';
return `${actor} cerró el sprint ${sprint}${left}`;
}
if (event instanceof SprintDeletedEvent)
return `${actor} borró el sprint ${sprint}`;
return `${actor}: ${event.constructor.name}`;
}