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:
@@ -1366,4 +1366,53 @@ describe('task-stream (e2e)', () => {
|
||||
);
|
||||
expect(automations).toEqual([{ name: 'Etiquetar', active: false }]);
|
||||
});
|
||||
|
||||
it('reads the dashboard: sprint under way, risks, aging and bottlenecks', async () => {
|
||||
const key = 'D' + Date.now().toString(36).slice(-5).toUpperCase();
|
||||
await rest('token-ana', 'POST', '/projects', { key, name: key });
|
||||
await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}`),
|
||||
(r) => r.status === 200,
|
||||
);
|
||||
const late = (
|
||||
await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'TASK',
|
||||
title: 'Late',
|
||||
dueDate: '2020-01-01',
|
||||
})
|
||||
).body['key'];
|
||||
const waits = (
|
||||
await rest('token-ana', 'POST', `/projects/${key}/issues`, {
|
||||
type: 'TASK',
|
||||
title: 'Waits',
|
||||
})
|
||||
).body['key'];
|
||||
await eventually(
|
||||
() => rest('token-ana', 'GET', `/issues/${waits}`),
|
||||
(r) => r.status === 200,
|
||||
);
|
||||
await rest('token-ana', 'POST', `/issues/${late}/links`, {
|
||||
type: 'BLOCKS',
|
||||
targetIssue: waits,
|
||||
});
|
||||
const insights = await eventually(
|
||||
() => rest('token-ana', 'GET', `/projects/${key}/insights`),
|
||||
(r) => r.status === 200 && r.body['risks'].blockedCount === 1,
|
||||
);
|
||||
expect(insights.body['sprint']).toBeNull();
|
||||
expect(insights.body['risks'].overdue[0]).toMatchObject({
|
||||
key: late,
|
||||
dueDate: '2020-01-01',
|
||||
});
|
||||
expect(insights.body['risks'].blocked[0].key).toBe(waits);
|
||||
expect(
|
||||
insights.body['bottlenecks'].map((b: { status: string }) => b.status),
|
||||
).toContain('IN_PROGRESS');
|
||||
const { project } = await gql(
|
||||
'token-ana',
|
||||
`query($k: String!) { project(idOrKey: $k) { insights { risks { overdueCount } aging { thresholdDays } } } }`,
|
||||
{ k: key },
|
||||
);
|
||||
expect(project.insights.risks.overdueCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user