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:
@@ -63,10 +63,13 @@ export class GraphqlProjectsGateway extends ProjectsGateway {
|
||||
.pipe(map((d) => toStats(d.project.stats)));
|
||||
}
|
||||
|
||||
insights(projectId: string): Observable<ProjectInsights> {
|
||||
insights(
|
||||
projectId: string,
|
||||
since: string | null,
|
||||
): Observable<ProjectInsights> {
|
||||
return this.gql
|
||||
.query(ProjectInsightsDocument, { idOrKey: projectId })
|
||||
.pipe(map((d) => d.project.insights));
|
||||
.query(ProjectInsightsDocument, { idOrKey: projectId, since })
|
||||
.pipe(map((d) => d.project.insights as ProjectInsights));
|
||||
}
|
||||
|
||||
analytics(projectId: string, days: number): Observable<ProjectAnalytics> {
|
||||
|
||||
@@ -178,6 +178,10 @@ export type IssueFilterInput = {
|
||||
/** Due on or before this day (YYYY-MM-DD) */
|
||||
dueTo?: string | null | undefined;
|
||||
labels?: Array<string> | null | undefined;
|
||||
/** Only tickets with no area */
|
||||
noArea?: boolean | null | undefined;
|
||||
/** Only tickets nobody estimated */
|
||||
noEstimate?: boolean | null | undefined;
|
||||
/** Only tickets in no sprint */
|
||||
noSprint?: boolean | null | undefined;
|
||||
/** Only the tickets hanging from this one */
|
||||
@@ -192,6 +196,8 @@ export type IssueFilterInput = {
|
||||
statuses?: Array<string> | null | undefined;
|
||||
types?: Array<string> | null | undefined;
|
||||
unassigned?: boolean | null | undefined;
|
||||
/** Only tickets not updated since this instant (ISO) */
|
||||
updatedBefore?: string | null | undefined;
|
||||
/** Only the tickets I follow (none when anonymous) */
|
||||
watching?: boolean | null | undefined;
|
||||
};
|
||||
@@ -1769,6 +1775,7 @@ export type InsightIssueFieldsFragment = {
|
||||
|
||||
export type ProjectInsightsQueryVariables = Exact<{
|
||||
idOrKey: string;
|
||||
since?: string | null | undefined;
|
||||
}>;
|
||||
|
||||
export type ProjectInsightsQuery = {
|
||||
@@ -1854,6 +1861,66 @@ export type ProjectInsightsQuery = {
|
||||
wipLimit: number | null;
|
||||
overLimit: boolean;
|
||||
}>;
|
||||
releases: Array<{
|
||||
releaseId: string;
|
||||
name: string;
|
||||
targetDate: string | null;
|
||||
total: number;
|
||||
finished: number;
|
||||
likelyDate: string | null;
|
||||
safeDate: string | null;
|
||||
verdict: string;
|
||||
}>;
|
||||
changes: {
|
||||
since: string;
|
||||
createdCount: number;
|
||||
finishedCount: number;
|
||||
returnedCount: number;
|
||||
mentions: number;
|
||||
created: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
status: string;
|
||||
days: number;
|
||||
dueDate: string | null;
|
||||
assignee: { id: string; name: string } | null;
|
||||
}>;
|
||||
finished: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
status: string;
|
||||
days: number;
|
||||
dueDate: string | null;
|
||||
assignee: { id: string; name: string } | null;
|
||||
}>;
|
||||
returned: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
status: string;
|
||||
days: number;
|
||||
dueDate: string | null;
|
||||
assignee: { id: string; name: string } | null;
|
||||
}>;
|
||||
} | null;
|
||||
team: Array<{
|
||||
openTickets: number;
|
||||
openPoints: number;
|
||||
blocked: number;
|
||||
pointsPerSprint: number | null;
|
||||
overloaded: boolean;
|
||||
user: { id: string; name: string };
|
||||
}>;
|
||||
backlog: {
|
||||
open: number;
|
||||
unestimated: number;
|
||||
noArea: number;
|
||||
unassigned: number;
|
||||
stale: number;
|
||||
staleDays: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -10928,6 +10995,14 @@ export const ProjectInsightsDocument = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'since' },
|
||||
},
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
@@ -10952,6 +11027,16 @@ export const ProjectInsightsDocument = {
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'insights' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'since' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'since' },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
@@ -11175,6 +11260,202 @@ export const ProjectInsightsDocument = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'releases' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'releaseId' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'name' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'targetDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'total' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'finished' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'likelyDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'safeDate' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'verdict' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'changes' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'since' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'created' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'InsightIssueFields',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'createdCount' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'finished' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'InsightIssueFields',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'finishedCount' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'returned' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'InsightIssueFields',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'returnedCount' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'mentions' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'team' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'user' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'FragmentSpread',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'UserRefFields',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'openTickets' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'openPoints' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'blocked' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'pointsPerSprint' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'overloaded' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'backlog' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'open' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'unestimated' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'noArea' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'unassigned' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'stale' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'staleDays' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -157,10 +157,10 @@ fragment InsightIssueFields on InsightIssue {
|
||||
dueDate
|
||||
}
|
||||
|
||||
query ProjectInsights($idOrKey: String!) {
|
||||
query ProjectInsights($idOrKey: String!, $since: String) {
|
||||
project(idOrKey: $idOrKey) {
|
||||
id
|
||||
insights {
|
||||
insights(since: $since) {
|
||||
generatedAt
|
||||
sprint {
|
||||
sprintId
|
||||
@@ -210,6 +210,50 @@ query ProjectInsights($idOrKey: String!) {
|
||||
wipLimit
|
||||
overLimit
|
||||
}
|
||||
releases {
|
||||
releaseId
|
||||
name
|
||||
targetDate
|
||||
total
|
||||
finished
|
||||
likelyDate
|
||||
safeDate
|
||||
verdict
|
||||
}
|
||||
changes {
|
||||
since
|
||||
created {
|
||||
...InsightIssueFields
|
||||
}
|
||||
createdCount
|
||||
finished {
|
||||
...InsightIssueFields
|
||||
}
|
||||
finishedCount
|
||||
returned {
|
||||
...InsightIssueFields
|
||||
}
|
||||
returnedCount
|
||||
mentions
|
||||
}
|
||||
team {
|
||||
user {
|
||||
...UserRefFields
|
||||
}
|
||||
openTickets
|
||||
openPoints
|
||||
blocked
|
||||
pointsPerSprint
|
||||
overloaded
|
||||
}
|
||||
backlog {
|
||||
open
|
||||
unestimated
|
||||
noArea
|
||||
unassigned
|
||||
stale
|
||||
staleDays
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user