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:
@@ -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';
|
||||
|
||||
65
libs/web/tracker/domain/src/lib/models/insights.ts
Normal file
65
libs/web/tracker/domain/src/lib/models/insights.ts
Normal 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[];
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user