feat: dashboard maps the activity, follows a ticket's way and the proposals

Three more blocks on the project dashboard, also by REST, GraphQL and
MCP:
- Activity: what moved each day of the last 12 weeks, in a heatmap,
  with the busiest weekday and the longest lull.
- A ticket's way: for the tickets done in the last 90 days, where the
  time from created to done went, status by status (waiting included),
  and how much of it was work (flow efficiency).
- The public's proposals: received, waiting, under way, done and
  discarded, and the most voted still unanswered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 09:29:38 +02:00
parent 9ccc3b5967
commit 1101b6e926
24 changed files with 1417 additions and 73 deletions

View File

@@ -1415,6 +1415,16 @@ describe('task-stream (e2e)', () => {
);
expect(project.insights.risks.overdueCount).toBe(1);
// Activity per day (today's creations among it), the journey, no proposals.
const today = new Date().toISOString().slice(0, 10);
expect(insights.body['heatmap']).toHaveLength(84);
expect(
insights.body['heatmap'].find((d: { day: string }) => d.day === today)
?.count,
).toBeGreaterThan(0);
expect(insights.body['journey']).toMatchObject({ samples: 0, steps: [] });
expect(insights.body['proposals']).toBeNull();
// What changed since a moment, and what the backlog lacks.
const since = new Date(Date.now() - 60_000).toISOString();
const later = await rest(

View File

@@ -13403,6 +13403,142 @@
"none"
]
},
"ActivityDayDto": {
"type": "object",
"properties": {
"day": {
"type": "string",
"example": "2026-10-15"
},
"count": {
"type": "number"
}
},
"required": [
"day",
"count"
]
},
"JourneyStepDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Status id of the project settings"
},
"days": {
"type": "number",
"description": "Average days a closed ticket spent in it"
},
"share": {
"type": "number",
"description": "Its part of the whole lead time, 0-100"
}
},
"required": [
"status",
"days",
"share"
]
},
"JourneyDto": {
"type": "object",
"properties": {
"samples": {
"type": "number",
"description": "Tickets done in the last 90 days"
},
"leadDays": {
"type": "number",
"nullable": true
},
"flowEfficiency": {
"type": "number",
"nullable": true,
"description": "Part of the lead time in an in-progress status, 0-100"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/JourneyStepDto"
}
}
},
"required": [
"samples",
"leadDays",
"flowEfficiency",
"steps"
]
},
"WaitingProposalDto": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"votes": {
"type": "number"
},
"days": {
"type": "number",
"description": "Days since it came in"
}
},
"required": [
"id",
"key",
"title",
"votes",
"days"
]
},
"ProposalsFunnelDto": {
"type": "object",
"properties": {
"received": {
"type": "number"
},
"receivedLately": {
"type": "number",
"description": "In the last 30 days"
},
"waiting": {
"type": "number",
"description": "Open, not started, in no sprint nor release"
},
"underWay": {
"type": "number"
},
"done": {
"type": "number"
},
"discarded": {
"type": "number"
},
"topWaiting": {
"type": "array",
"items": {
"$ref": "#/components/schemas/WaitingProposalDto"
}
}
},
"required": [
"received",
"receivedLately",
"waiting",
"underWay",
"done",
"discarded",
"topWaiting"
]
},
"ProjectInsightsDto": {
"type": "object",
"properties": {
@@ -13464,6 +13600,26 @@
"items": {
"$ref": "#/components/schemas/FieldBreakdownDto"
}
},
"heatmap": {
"description": "The last 12 weeks from a Monday, oldest first (84 days)",
"type": "array",
"items": {
"$ref": "#/components/schemas/ActivityDayDto"
}
},
"journey": {
"$ref": "#/components/schemas/JourneyDto"
},
"proposals": {
"nullable": true,
"description": "Null while the project has never had a proposal",
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ProposalsFunnelDto"
}
]
}
},
"required": [
@@ -13477,7 +13633,10 @@
"team",
"backlog",
"quality",
"fields"
"fields",
"heatmap",
"journey",
"proposals"
]
},
"ActivityDto": {

View File

@@ -13,6 +13,14 @@ type Activity {
type: String!
}
"""How much happened on one day"""
type ActivityDay {
count: Int!
"""YYYY-MM-DD"""
day: String!
}
input AddChecklistItemInput {
issueId: ID!
text: String!
@@ -815,6 +823,28 @@ input IssueTypeOptionInput {
template: IssueTemplateInput
}
"""
Where the time of the tickets done in the last 90 days went, created to done
"""
type Journey {
"""Part of the lead time in an in-progress status, 0-100"""
flowEfficiency: Int
leadDays: Float
samples: Int!
steps: [JourneyStep!]!
}
type JourneyStep {
"""Average days a closed ticket spent in it"""
days: Float!
"""Its part of the whole lead time, 0-100"""
share: Int!
"""Status id of the project settings"""
status: String!
}
enum LinkDirection {
INWARD
OUTWARD
@@ -1283,6 +1313,13 @@ type ProjectInsights {
"""One per active list custom field"""
fields: [FieldBreakdown!]!
generatedAt: String!
"""The last 12 weeks from a Monday, oldest first (84 days)"""
heatmap: [ActivityDay!]!
journey: Journey!
"""Null while the project has never had a proposal"""
proposals: ProposalsFunnel
quality: Quality!
releases: [ReleaseForecast!]!
risks: Risks!
@@ -1361,6 +1398,23 @@ type ProjectStats {
total: Int!
}
"""The public's proposals, from received to done or discarded"""
type ProposalsFunnel {
discarded: Int!
done: Int!
received: Int!
"""In the last 30 days"""
receivedLately: Int!
"""Most voted waiting, at most 5"""
topWaiting: [WaitingProposal!]!
underWay: Int!
"""Open, not started, in no sprint nor release"""
waiting: Int!
}
"""A browser's Web Push subscription (PushSubscription.toJSON())"""
input PushSubscriptionInput {
auth: String!
@@ -1914,6 +1968,15 @@ type UserRef {
name: String!
}
type WaitingProposal {
"""Days since it came in"""
days: Int!
id: ID!
key: String!
title: String!
votes: Int!
}
"""
Where the project tells another system what happened (signed POST, HMAC-SHA256)
"""

View File

@@ -2441,6 +2441,46 @@ export interface components {
/** @description Open tickets with no value */
none: number;
};
ActivityDayDto: {
/** @example 2026-10-15 */
day: string;
count: number;
};
JourneyStepDto: {
/** @description Status id of the project settings */
status: string;
/** @description Average days a closed ticket spent in it */
days: number;
/** @description Its part of the whole lead time, 0-100 */
share: number;
};
JourneyDto: {
/** @description Tickets done in the last 90 days */
samples: number;
leadDays: number | null;
/** @description Part of the lead time in an in-progress status, 0-100 */
flowEfficiency: number | null;
steps: components["schemas"]["JourneyStepDto"][];
};
WaitingProposalDto: {
id: string;
key: string;
title: string;
votes: number;
/** @description Days since it came in */
days: number;
};
ProposalsFunnelDto: {
received: number;
/** @description In the last 30 days */
receivedLately: number;
/** @description Open, not started, in no sprint nor release */
waiting: number;
underWay: number;
done: number;
discarded: number;
topWaiting: components["schemas"]["WaitingProposalDto"][];
};
ProjectInsightsDto: {
generatedAt: string;
sprint: components["schemas"]["SprintPulseDto"] | null;
@@ -2455,6 +2495,11 @@ export interface components {
quality: components["schemas"]["QualityDto"];
/** @description One per active list custom field */
fields: components["schemas"]["FieldBreakdownDto"][];
/** @description The last 12 weeks from a Monday, oldest first (84 days) */
heatmap: components["schemas"]["ActivityDayDto"][];
journey: components["schemas"]["JourneyDto"];
/** @description Null while the project has never had a proposal */
proposals: components["schemas"]["ProposalsFunnelDto"] | null;
};
ActivityDto: {
id: string;

View File

@@ -253,7 +253,7 @@ export class ProjectsResolver {
@Tool({
name: 'get_project_insights',
description:
"The project's dashboard: the sprint under way (pace against time, behind or not), overdue, due-soon, blocked and stuck tickets, how long the work under way has been where it is (against the p85 cycle time), average days in each in-progress and QA status with WIP limits, when each planned release would be finished at the real pace (Monte Carlo), each person's open points against what they close per sprint, open tickets missing an estimate, an area or an assignee, quality (open bugs per priority, bugs created and closed per week, QA returns per area, bugs reported after each release) and open tickets per option of each list custom field. With since, also what changed after it.",
"The project's dashboard: the sprint under way (pace against time, behind or not), overdue, due-soon, blocked and stuck tickets, how long the work under way has been where it is (against the p85 cycle time), average days in each in-progress and QA status with WIP limits, when each planned release would be finished at the real pace (Monte Carlo), each person's open points against what they close per sprint, open tickets missing an estimate, an area or an assignee, quality (open bugs per priority, bugs created and closed per week, QA returns per area, bugs reported after each release) open tickets per option of each list custom field, activity per day over the last 12 weeks, where the time of the tickets done lately went from created to done (days and share per status, and the part spent at work: flow efficiency) and, if the public has proposed anything, the proposals funnel (received, waiting, under way, done, discarded, most voted waiting). With since, also what changed after it.",
paramsSchema: z.object({
project: projectRef,
since: z