feat: dashboard reads the sprint, the risks, aging and bottlenecks

A new projectInsights reading (GraphQL Project.insights, REST
/projects/{project}/insights, MCP get_project_insights), computed by pure
functions over the read model and the timeline:
- the sprint under way: days left, points done, pace against what is
  needed and whether it gets there;
- tickets at risk: overdue, due within a week, blocked by an open ticket
  and stuck (7 days or more in an in-progress or QA status);
- aging of the work under way against the p85 cycle time;
- average days in each in-progress and QA status, with WIP limits.
The dashboard shows them in four new panels, reloaded on every change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 11:55:52 +02:00
parent 86e313efaa
commit fe03c5bda8
41 changed files with 2774 additions and 20 deletions

View File

@@ -285,6 +285,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/projects/{project}/insights": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* The dashboard
* @description Sprint under way (pace against time), overdue, due-soon, blocked and stuck tickets, aging of the work under way against the p85 cycle time, and average days in each in-progress and QA status.
*/
get: operations["ProjectsController_insights"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/projects/{project}/activity": {
parameters: {
query?: never;
@@ -2100,6 +2120,72 @@ export interface components {
throughput: components["schemas"]["ThroughputDto"][];
qa: components["schemas"]["QaStatsDto"];
};
SprintPulseDto: {
/** Format: uuid */
sprintId: string;
name: string;
/** Format: date */
startDate: string;
/** Format: date */
endDate: string;
totalDays: number;
/** @description Days left after today */
daysLeft: number;
tickets: number;
doneTickets: number;
points: number;
donePoints: number;
pacePerDay: number | null;
neededPerDay: number | null;
/** @description At this pace, what is open would not be closed */
behind: boolean;
};
InsightIssueDto: {
/** Format: uuid */
id: string;
/** @example WEB-12 */
key: string;
title: string;
/** @description Status id */
status: string;
assignee: components["schemas"]["UserRefDto"] | null;
/** @description Days in its status (aging, stuck) or past / to its due date */
days: number;
/** Format: date */
dueDate: string | null;
};
RisksDto: {
overdue: components["schemas"]["InsightIssueDto"][];
overdueCount: number;
/** @description Due within 7 days */
dueSoon: components["schemas"]["InsightIssueDto"][];
dueSoonCount: number;
blocked: components["schemas"]["InsightIssueDto"][];
blockedCount: number;
stuck: components["schemas"]["InsightIssueDto"][];
stuckCount: number;
stuckDays: number;
};
AgingDto: {
issues: components["schemas"]["InsightIssueDto"][];
/** @description p85 cycle time of the last 90 days */
thresholdDays: number | null;
};
StatusTimeDto: {
status: string;
averageDays: number | null;
samples: number;
open: number;
wipLimit: number | null;
overLimit: boolean;
};
ProjectInsightsDto: {
generatedAt: string;
sprint: components["schemas"]["SprintPulseDto"] | null;
risks: components["schemas"]["RisksDto"];
aging: components["schemas"]["AgingDto"];
bottlenecks: components["schemas"]["StatusTimeDto"][];
};
ActivityDto: {
id: string;
/** @example IssueStatusChanged */
@@ -3882,6 +3968,64 @@ export interface operations {
};
};
};
ProjectsController_insights: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProjectInsightsDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
ProjectsController_activity: {
parameters: {
query?: {

View File

@@ -204,6 +204,23 @@ export class ProjectsResolver {
);
}
@Tool({
name: 'get_project_insights',
description:
"The project's dashboard: the sprint under way (pace against time, behind or not), overdue, due-soon, blocked and stuck tickets, how long the work under way has been where it is (against the p85 cycle time) and the average days in each in-progress and QA status, with WIP limits.",
paramsSchema: z.object({ project: projectRef }),
annotations: READ,
})
insights({ project }: { project: string }, ctx: McpContext) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).GET('/api/projects/{project}/insights', {
params: { path: { project } },
}),
),
);
}
@Tool({
name: 'get_project_activity',
description: 'Latest activity of a project (newest first).',