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:
2026-10-15 12:50:24 +02:00
parent fe03c5bda8
commit 0d158a2ed7
41 changed files with 2050 additions and 118 deletions

View File

@@ -2179,12 +2179,67 @@ export interface components {
wipLimit: number | null;
overLimit: boolean;
};
ReleaseForecastDto: {
/** Format: uuid */
releaseId: string;
name: string;
/** Format: date */
targetDate: string | null;
total: number;
finished: number;
/**
* Format: date
* @description Half of the simulated futures finish by then
*/
likelyDate: string | null;
/**
* Format: date
* @description 85% finish by then
*/
safeDate: string | null;
/** @enum {string} */
verdict: "DONE" | "NO_DATE" | "UNKNOWN" | "ON_TRACK" | "AT_RISK" | "LATE";
};
ChangesSinceDto: {
since: string;
created: components["schemas"]["InsightIssueDto"][];
createdCount: number;
finished: components["schemas"]["InsightIssueDto"][];
finishedCount: number;
/** @description Sent back from QA */
returned: components["schemas"]["InsightIssueDto"][];
returnedCount: number;
/** @description Times you were mentioned here */
mentions: number;
};
TeamLoadDto: {
user: components["schemas"]["UserRefDto"];
openTickets: number;
openPoints: number;
blocked: number;
/** @description Points closed per sprint, last three closed sprints */
pointsPerSprint: number | null;
overloaded: boolean;
};
BacklogHealthDto: {
open: number;
unestimated: number;
noArea: number;
unassigned: number;
stale: number;
staleDays: number;
};
ProjectInsightsDto: {
generatedAt: string;
sprint: components["schemas"]["SprintPulseDto"] | null;
risks: components["schemas"]["RisksDto"];
aging: components["schemas"]["AgingDto"];
bottlenecks: components["schemas"]["StatusTimeDto"][];
releases: components["schemas"]["ReleaseForecastDto"][];
/** @description Only when asked with `since` */
changes: components["schemas"]["ChangesSinceDto"] | null;
team: components["schemas"]["TeamLoadDto"][];
backlog: components["schemas"]["BacklogHealthDto"];
};
ActivityDto: {
id: string;
@@ -3970,7 +4025,10 @@ export interface operations {
};
ProjectsController_insights: {
parameters: {
query?: never;
query?: {
/** @description ISO instant (e.g. your last visit): the answer also tells what changed after it */
since?: string;
};
header?: never;
path: {
/** @description Project id or key */
@@ -4579,6 +4637,12 @@ export interface operations {
dueFrom?: string;
/** @description Due on or before this day */
dueTo?: string;
/** @description Only tickets nobody estimated */
noEstimate?: boolean;
/** @description Only tickets with no area */
noArea?: boolean;
/** @description Only tickets not updated since this instant (ISO) */
updatedBefore?: string;
/** @description Custom values the tickets must hold, as FIELD:value. Repeat the parameter for several */
field?: string[];
/** @description RECENT (default): last updated first. RANK: backlog order. */

View File

@@ -207,15 +207,26 @@ 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 }),
"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), average days in each in-progress and QA status with WIP limits, when each planned release would be finished at the real pace (Monte Carlo), each person's open points against what they close per sprint, and open tickets missing an estimate, an area or an assignee. With since, also what changed after it.",
paramsSchema: z.object({
project: projectRef,
since: z
.string()
.optional()
.describe(
'ISO instant: also tell what was created, finished or sent back from QA after it',
),
}),
annotations: READ,
})
insights({ project }: { project: string }, ctx: McpContext) {
insights(
{ project, since }: { project: string; since?: string },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).GET('/api/projects/{project}/insights', {
params: { path: { project } },
params: { path: { project }, query: { since } },
}),
),
);