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

@@ -12,3 +12,4 @@ export * from './lib/models/roadmap';
export * from './lib/models/shortcuts';
export * from './lib/models/due';
export * from './lib/models/calendar';
export * from './lib/models/insights';

View File

@@ -0,0 +1,65 @@
import { UserRef } from './models';
/** A ticket the dashboard names, with the number that put it there. */
export interface InsightIssue {
id: string;
key: string;
title: string;
/** Status id of the project's settings. */
status: string;
assignee: UserRef | null;
/** Days in its status (aging, stuck) or past / to its due date. */
days: number;
dueDate: string | null;
}
/** The sprint under way: time gone against work done. */
export interface SprintPulse {
sprintId: string;
name: string;
startDate: string;
endDate: string;
totalDays: number;
/** Days left after today (negative once it is over). */
daysLeft: number;
tickets: number;
doneTickets: number;
points: number;
donePoints: number;
pacePerDay: number | null;
neededPerDay: number | null;
/** At this pace, what is open would not be closed in time. */
behind: boolean;
}
/** Tickets to look at now (worst first, a few of each). */
export interface Risks {
overdue: InsightIssue[];
overdueCount: number;
dueSoon: InsightIssue[];
dueSoonCount: number;
blocked: InsightIssue[];
blockedCount: number;
stuck: InsightIssue[];
stuckCount: number;
stuckDays: number;
}
/** Time tickets spend in a status of the flow, and how many are there. */
export interface StatusTime {
status: string;
averageDays: number | null;
samples: number;
open: number;
wipLimit: number | null;
overLimit: boolean;
}
/** The dashboard's reading of a project beyond the counts. */
export interface ProjectInsights {
generatedAt: string;
sprint: SprintPulse | null;
risks: Risks;
aging: { issues: InsightIssue[]; thresholdDays: number | null };
bottlenecks: StatusTime[];
}

View File

@@ -1,3 +1,4 @@
import { ProjectInsights } from '../models/insights';
import { Observable } from 'rxjs';
import {
Activity,
@@ -63,6 +64,8 @@ export abstract class ProjectsGateway {
abstract publicProjects(): Observable<Project[]>;
abstract project(idOrKey: string): Observable<Project>;
abstract stats(projectId: string): Observable<ProjectStats>;
/** Sprint under way, risks, aging and bottlenecks (the dashboard). */
abstract insights(projectId: string): Observable<ProjectInsights>;
/** Statistics of the last `days` days. */
abstract analytics(
projectId: string,