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

@@ -18,6 +18,13 @@ input AddChecklistItemInput {
text: String!
}
type Aging {
issues: [InsightIssue!]!
"""p85 cycle time of the last 90 days"""
thresholdDays: Float
}
type AreaOption {
archived: Boolean!
id: String!
@@ -475,6 +482,19 @@ type GitRepository {
secret: String
}
"""A ticket the dashboard names, with the number that put it there"""
type InsightIssue {
assignee: UserRef
"""Days in its status (aging, stuck) or past / to its due date"""
days: Float!
dueDate: String
id: ID!
key: String!
status: String!
title: String!
}
type Issue {
"""Statuses this ticket can move to next (project's workflow)"""
allowedTransitions: [String!]!
@@ -955,6 +975,11 @@ type Project {
description: String
id: ID!
"""
The dashboard: sprint under way, overdue / blocked / stuck tickets, aging and bottlenecks
"""
insights: ProjectInsights!
"""Anyone can read it, also without signing in"""
isPublic: Boolean!
key: String!
@@ -996,6 +1021,15 @@ type ProjectChange {
version: Int!
}
"""The dashboard: sprint under way, risks, aging and bottlenecks"""
type ProjectInsights {
aging: Aging!
bottlenecks: [StatusTime!]!
generatedAt: String!
risks: Risks!
sprint: SprintPulse
}
type ProjectMember {
role: ProjectRole!
user: UserRef!
@@ -1221,6 +1255,21 @@ enum ReleaseStatus {
RELEASED
}
"""Tickets to look at now (worst first)"""
type Risks {
blocked: [InsightIssue!]!
blockedCount: Int!
"""Due within 7 days"""
dueSoon: [InsightIssue!]!
dueSoonCount: Int!
overdue: [InsightIssue!]!
overdueCount: Int!
stuck: [InsightIssue!]!
stuckCount: Int!
stuckDays: Int!
}
input SaveFilterInput {
criteria: SavedFilterCriteriaInput!
name: String!
@@ -1321,6 +1370,26 @@ type SprintProgress {
total: Int!
}
"""The sprint under way: time gone against work done"""
type SprintPulse {
"""At this pace, what is open would not be closed"""
behind: Boolean!
"""Days left after today"""
daysLeft: Int!
donePoints: Int!
doneTickets: Int!
endDate: String!
name: String!
neededPerDay: Float
pacePerDay: Float
points: Int!
sprintId: ID!
startDate: String!
tickets: Int!
totalDays: Int!
}
type SprintRef {
id: ID!
name: String!
@@ -1344,6 +1413,15 @@ enum StatusCategory {
TODO
}
type StatusTime {
averageDays: Float
open: Int!
overLimit: Boolean!
samples: Int!
status: String!
wipLimit: Int
}
type StatusTransitions {
from: String!
to: [String!]!