feat: dashboard forecasts releases, tells what changed and weighs the load
The insights reading grows (GraphQL insights(since:), REST ?since=, MCP): - planned releases with a likely (p50) and safe (p85) finish date from a Monte Carlo over the last six weeks of finished work, one after another, and a verdict against their target; - with `since` (the viewer's last visit, kept in the browser): tickets created, finished and sent back from QA, and their mentions; - each person's open points against what they closed per sprint lately; - open tickets without estimate, area or assignee, or untouched for 90 days, each count opening the list filtered (new noEstimate, noArea and updatedBefore filters). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import {
|
||||
DEFAULT_PROJECT_SETTINGS,
|
||||
NotificationKind,
|
||||
ProjectInsightsReadModel,
|
||||
SprintStatus,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
import { computeInsights } from '../analytics/insights';
|
||||
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
||||
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
||||
import { FindReleasesRepository } from '../ports/find-releases.repository';
|
||||
import { NotificationsRepository } from '../ports/notifications.repository';
|
||||
import { FindSprintsRepository } from '../ports/find-sprints.repository';
|
||||
import { FindTimelineRepository } from '../ports/find-timeline.repository';
|
||||
import { GetProjectInsightsQuery } from './get-project-insights.query';
|
||||
@@ -21,24 +25,51 @@ export class GetProjectInsightsQueryHandler
|
||||
private readonly findIssues: FindIssuesRepository,
|
||||
private readonly findTimeline: FindTimelineRepository,
|
||||
private readonly findSprints: FindSprintsRepository,
|
||||
private readonly findReleases: FindReleasesRepository,
|
||||
private readonly notifications: NotificationsRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
query: GetProjectInsightsQuery,
|
||||
): Promise<ProjectInsightsReadModel> {
|
||||
await this.access.assertCanView(query.projectId, query.viewerId);
|
||||
const [project, issues, timeline, activeSprint] = await Promise.all([
|
||||
this.findProjects.findById(query.projectId),
|
||||
this.findIssues.findByProject(query.projectId),
|
||||
this.findTimeline.findByProject(query.projectId),
|
||||
this.findSprints.findActive(query.projectId),
|
||||
]);
|
||||
const [project, issues, timeline, sprints, releases, mentions] =
|
||||
await Promise.all([
|
||||
this.findProjects.findById(query.projectId),
|
||||
this.findIssues.findByProject(query.projectId),
|
||||
this.findTimeline.findByProject(query.projectId),
|
||||
this.findSprints.findByProject(query.projectId),
|
||||
this.findReleases.findByProject(query.projectId),
|
||||
this.mentionsSince(query),
|
||||
]);
|
||||
return computeInsights({
|
||||
issues,
|
||||
timeline,
|
||||
settings: project?.settings ?? DEFAULT_PROJECT_SETTINGS,
|
||||
activeSprint,
|
||||
activeSprint:
|
||||
sprints.find((s) => s.status === SprintStatus.ACTIVE) ?? null,
|
||||
sprints,
|
||||
releases,
|
||||
members: project?.members ?? [],
|
||||
since: query.since,
|
||||
mentions,
|
||||
now: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/** The viewer's mentions in this project after `since` (latest 200). */
|
||||
private async mentionsSince(query: GetProjectInsightsQuery): Promise<number> {
|
||||
if (!query.since || !query.viewerId) return 0;
|
||||
const since = query.since.toISOString();
|
||||
const latest = await this.notifications.findForUser(query.viewerId, {
|
||||
unreadOnly: false,
|
||||
limit: 200,
|
||||
});
|
||||
return latest.filter(
|
||||
(n) =>
|
||||
n.projectId === query.projectId &&
|
||||
n.kind === NotificationKind.MENTIONED &&
|
||||
n.createdAt > since,
|
||||
).length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import { ProjectInsightsReadModel } from '@task-stream/api/tracker/domain';
|
||||
|
||||
/** The dashboard's reading of a project: sprint, risks, aging, bottlenecks. */
|
||||
/**
|
||||
* The dashboard's reading of a project: sprint, risks, aging, bottlenecks,
|
||||
* release forecasts, team load, backlog health and, with `since`, what
|
||||
* changed after it.
|
||||
*/
|
||||
export class GetProjectInsightsQuery extends Query<ProjectInsightsReadModel> {
|
||||
constructor(
|
||||
/** Null: anonymous (public projects only). */
|
||||
public readonly viewerId: string | null,
|
||||
public readonly projectId: string,
|
||||
public readonly since: Date | null = null,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user