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

@@ -1435,6 +1435,89 @@
]
}
},
"/api/projects/{project}/insights": {
"get": {
"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.",
"operationId": "ProjectsController_insights",
"parameters": [
{
"name": "project",
"required": true,
"in": "path",
"description": "Project id or key",
"schema": {
"example": "WEB",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectInsightsDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
},
{}
],
"summary": "The dashboard",
"tags": [
"Projects"
]
}
},
"/api/projects/{project}/activity": {
"get": {
"operationId": "ProjectsController_activity",
@@ -11804,6 +11887,264 @@
"qa"
]
},
"SprintPulseDto": {
"type": "object",
"properties": {
"sprintId": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"startDate": {
"type": "string",
"format": "date"
},
"endDate": {
"type": "string",
"format": "date"
},
"totalDays": {
"type": "number"
},
"daysLeft": {
"type": "number",
"description": "Days left after today"
},
"tickets": {
"type": "number"
},
"doneTickets": {
"type": "number"
},
"points": {
"type": "number"
},
"donePoints": {
"type": "number"
},
"pacePerDay": {
"type": "number",
"nullable": true
},
"neededPerDay": {
"type": "number",
"nullable": true
},
"behind": {
"type": "boolean",
"description": "At this pace, what is open would not be closed"
}
},
"required": [
"sprintId",
"name",
"startDate",
"endDate",
"totalDays",
"daysLeft",
"tickets",
"doneTickets",
"points",
"donePoints",
"pacePerDay",
"neededPerDay",
"behind"
]
},
"InsightIssueDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"key": {
"type": "string",
"example": "WEB-12"
},
"title": {
"type": "string"
},
"status": {
"type": "string",
"description": "Status id"
},
"assignee": {
"nullable": true,
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/UserRefDto"
}
]
},
"days": {
"type": "number",
"description": "Days in its status (aging, stuck) or past / to its due date"
},
"dueDate": {
"type": "string",
"nullable": true,
"format": "date"
}
},
"required": [
"id",
"key",
"title",
"status",
"assignee",
"days",
"dueDate"
]
},
"RisksDto": {
"type": "object",
"properties": {
"overdue": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InsightIssueDto"
}
},
"overdueCount": {
"type": "number"
},
"dueSoon": {
"description": "Due within 7 days",
"type": "array",
"items": {
"$ref": "#/components/schemas/InsightIssueDto"
}
},
"dueSoonCount": {
"type": "number"
},
"blocked": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InsightIssueDto"
}
},
"blockedCount": {
"type": "number"
},
"stuck": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InsightIssueDto"
}
},
"stuckCount": {
"type": "number"
},
"stuckDays": {
"type": "number"
}
},
"required": [
"overdue",
"overdueCount",
"dueSoon",
"dueSoonCount",
"blocked",
"blockedCount",
"stuck",
"stuckCount",
"stuckDays"
]
},
"AgingDto": {
"type": "object",
"properties": {
"issues": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InsightIssueDto"
}
},
"thresholdDays": {
"type": "number",
"nullable": true,
"description": "p85 cycle time of the last 90 days"
}
},
"required": [
"issues",
"thresholdDays"
]
},
"StatusTimeDto": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"averageDays": {
"type": "number",
"nullable": true
},
"samples": {
"type": "number"
},
"open": {
"type": "number"
},
"wipLimit": {
"type": "number",
"nullable": true
},
"overLimit": {
"type": "boolean"
}
},
"required": [
"status",
"averageDays",
"samples",
"open",
"wipLimit",
"overLimit"
]
},
"ProjectInsightsDto": {
"type": "object",
"properties": {
"generatedAt": {
"type": "string"
},
"sprint": {
"nullable": true,
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/SprintPulseDto"
}
]
},
"risks": {
"$ref": "#/components/schemas/RisksDto"
},
"aging": {
"$ref": "#/components/schemas/AgingDto"
},
"bottlenecks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/StatusTimeDto"
}
}
},
"required": [
"generatedAt",
"sprint",
"risks",
"aging",
"bottlenecks"
]
},
"ActivityDto": {
"type": "object",
"properties": {