feat: a day of the activity map opens what happened, ticket by ticket

On the project dashboard, a day with activity can be picked on the map:
a side panel lists that day's moves grouped by ticket (each key opens
it), with the time of each, and the project's own (sprints, releases,
members) apart.

The activity feed takes an `after` date (GraphQL, REST), so with
`before` it reads a window such as a day; the MCP's
get_project_activity takes a `day`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 20:42:30 +02:00
parent de9c8a7780
commit d66b1f3ece
25 changed files with 512 additions and 35 deletions

View File

@@ -432,6 +432,7 @@ export class IssuesService {
issueId?: string;
limit?: number;
before?: string;
after?: string;
},
): Promise<ActivityReadModel[]> {
return this.queryBus.execute(
@@ -441,6 +442,7 @@ export class IssuesService {
params.issueId,
params.limit,
params.before,
params.after,
),
);
}

View File

@@ -7,6 +7,8 @@ export abstract class FindActivityRepository {
limit: number;
/** ISO date cursor: only activity strictly older than this. */
before?: string;
/** ISO date: only activity at or after this. */
after?: string;
}): Promise<ActivityReadModel[]>;
/** The latest of several projects, newest first. */
abstract findAcross(

View File

@@ -35,6 +35,7 @@ export class GetActivityQueryHandler
issueId: query.issueId,
limit: Math.min(query.limit, 200),
before: query.before,
after: query.after,
});
}
}

View File

@@ -10,6 +10,8 @@ export class GetActivityQuery extends Query<ActivityReadModel[]> {
public readonly limit = 50,
/** ISO date cursor: only activity strictly older than this. */
public readonly before?: string,
/** ISO date: only activity at or after this (with `before`, a window). */
public readonly after?: string,
) {
super();
}

View File

@@ -603,10 +603,15 @@ export class MongoFindActivityRepository extends FindActivityRepository {
issueId?: string;
limit: number;
before?: string;
after?: string;
}): Promise<ActivityReadModel[]> {
const query: FilterQuery<ActivityDoc> = { projectId: params.projectId };
if (params.issueId) query.issueId = params.issueId;
if (params.before) query.occurredAt = { $lt: params.before };
if (params.before || params.after)
query.occurredAt = {
...(params.before && { $lt: params.before }),
...(params.after && { $gte: params.after }),
};
const docs = await this.activity
.find(query)
.sort({ occurredAt: -1 })

View File

@@ -193,12 +193,19 @@ export class IssueResolver {
@Args('limit', { type: () => Int, defaultValue: 50 }) limit = 50,
@Args('before', { nullable: true, description: 'ISO date cursor' })
before?: string,
@Args('after', {
nullable: true,
description:
'ISO date: only activity at or after it (with before, a window such as a day)',
})
after?: string,
) {
return this.issuesService.activity(viewerOf(me), {
projectId,
issueId,
limit,
before,
after,
});
}

View File

@@ -473,6 +473,15 @@ export class ActivityQueryDto {
@IsOptional()
@IsISO8601()
before?: string;
@ApiPropertyOptional({
format: 'date-time',
description:
'Entries at or after this date (with before, a window such as a day)',
})
@IsOptional()
@IsISO8601()
after?: string;
}
export class PageQueryDto {

View File

@@ -316,6 +316,7 @@ export class ProjectsController {
issueId: query.issueId,
limit: query.limit,
before: query.before,
after: query.after,
});
}