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

@@ -70,6 +70,64 @@ export class StatusTimeReadModel {
overLimit!: boolean;
}
/** When a planned release would be finished, at the team's real pace. */
export class ReleaseForecastReadModel {
releaseId!: string;
name!: string;
targetDate!: string | null;
/** Tickets in it (cancelled ones aside). */
total!: number;
finished!: number;
/** Half of the simulated futures finish by then (null: no pace yet). */
likelyDate!: string | null;
/** 85% of them do. */
safeDate!: string | null;
/**
* DONE: nothing left. NO_DATE: no target to compare. UNKNOWN: nothing
* finished lately to measure a pace. ON_TRACK: 85% in time. AT_RISK: half
* in time. LATE: less than half.
*/
verdict!: 'DONE' | 'NO_DATE' | 'UNKNOWN' | 'ON_TRACK' | 'AT_RISK' | 'LATE';
}
/** What has happened since a moment (the viewer's last visit). */
export class ChangesSinceReadModel {
since!: string;
created!: InsightIssueReadModel[];
createdCount!: number;
finished!: InsightIssueReadModel[];
finishedCount!: number;
/** Sent back from QA. */
returned!: InsightIssueReadModel[];
returnedCount!: number;
/** Times the viewer was mentioned in this project. */
mentions!: number;
}
/** One person's open work against what they usually close. */
export class TeamLoadReadModel {
user!: UserRefReadModel;
openTickets!: number;
openPoints!: number;
/** Their open tickets waiting for another open one. */
blocked!: number;
/** Points they closed per sprint, over the last three closed sprints. */
pointsPerSprint!: number | null;
/** More open points than they usually close in a sprint. */
overloaded!: boolean;
}
/** Open tickets missing what planning needs. */
export class BacklogHealthReadModel {
open!: number;
unestimated!: number;
noArea!: number;
unassigned!: number;
/** Not updated in `staleDays` days. */
stale!: number;
staleDays!: number;
}
/** What the project dashboard reads beyond the counts. */
export class ProjectInsightsReadModel {
generatedAt!: string;
@@ -78,4 +136,11 @@ export class ProjectInsightsReadModel {
aging!: AgingReadModel;
/** In-progress and QA statuses, in board order. */
bottlenecks!: StatusTimeReadModel[];
/** Planned releases, by target date. */
releases!: ReleaseForecastReadModel[];
/** Null unless asked with a `since`. */
changes!: ChangesSinceReadModel | null;
/** Members (and others with open tickets), busiest first. */
team!: TeamLoadReadModel[];
backlog!: BacklogHealthReadModel;
}