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

@@ -129,6 +129,15 @@ type AutomationTriggerSpec {
status: String
}
type BacklogHealth {
noArea: Int!
open: Int!
stale: Int!
staleDays: Int!
unassigned: Int!
unestimated: Int!
}
type BoardColumn {
issues: [Issue!]!
status: String!
@@ -184,6 +193,22 @@ input ChangeIssueStatusInput {
status: String!
}
"""What happened after a moment"""
type ChangesSince {
created: [InsightIssue!]!
createdCount: Int!
finished: [InsightIssue!]!
finishedCount: Int!
"""Times I was mentioned here"""
mentions: Int!
"""Sent back from QA"""
returned: [InsightIssue!]!
returnedCount: Int!
since: String!
}
"""A step inside a ticket that does not deserve a ticket of its own"""
type ChecklistItem {
done: Boolean!
@@ -598,6 +623,12 @@ input IssueFilterInput {
dueTo: String
labels: [String!]
"""Only tickets with no area"""
noArea: Boolean
"""Only tickets nobody estimated"""
noEstimate: Boolean
"""Only tickets in no sprint"""
noSprint: Boolean
@@ -615,6 +646,9 @@ input IssueFilterInput {
types: [String!]
unassigned: Boolean
"""Only tickets not updated since this instant (ISO)"""
updatedBefore: String
"""Only the tickets I follow (none when anonymous)"""
watching: Boolean
}
@@ -978,7 +1012,10 @@ type Project {
"""
The dashboard: sprint under way, overdue / blocked / stuck tickets, aging and bottlenecks
"""
insights: ProjectInsights!
insights(
"""ISO instant (e.g. my last visit): also tell what changed after it"""
since: String
): ProjectInsights!
"""Anyone can read it, also without signing in"""
isPublic: Boolean!
@@ -1021,13 +1058,21 @@ type ProjectChange {
version: Int!
}
"""The dashboard: sprint under way, risks, aging and bottlenecks"""
"""
The dashboard: sprint under way, risks, aging, bottlenecks, release forecasts, team load, backlog health and changes
"""
type ProjectInsights {
aging: Aging!
backlog: BacklogHealth!
bottlenecks: [StatusTime!]!
"""Only when asked with `since`"""
changes: ChangesSince
generatedAt: String!
releases: [ReleaseForecast!]!
risks: Risks!
sprint: SprintPulse
team: [TeamLoad!]!
}
type ProjectMember {
@@ -1237,6 +1282,24 @@ type ReleaseChange {
version: Int!
}
"""When a planned release would be finished, at the real pace"""
type ReleaseForecast {
finished: Int!
"""Half of the simulated futures finish by then"""
likelyDate: String
name: String!
releaseId: ID!
"""85% finish by then"""
safeDate: String
targetDate: String
total: Int!
"""DONE, NO_DATE, UNKNOWN (no pace yet), ON_TRACK, AT_RISK or LATE"""
verdict: String!
}
type ReleaseProgress {
byStatus: [CountByStatus!]!
finished: Int!
@@ -1450,6 +1513,17 @@ type Subscription {
sprintChanged(projectId: ID!): SprintChange!
}
type TeamLoad {
blocked: Int!
openPoints: Int!
openTickets: Int!
overloaded: Boolean!
"""Points closed per sprint, last three closed sprints"""
pointsPerSprint: Float
user: UserRef!
}
type Throughput {
count: Int!
user: UserRef