feat: a public showcase of each project

A public endpoint sums up a public project for websites: epics with their
dates and progress, the latest releases with the notes of shipped ones,
the sprint under way and open tickets labelled good-first-issue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 19:13:56 +02:00
parent bec39da46d
commit feea58d0e8
11 changed files with 721 additions and 3 deletions

View File

@@ -234,6 +234,72 @@ export class BulkResultDto {
@ApiProperty({ type: [BulkIssueResultDto] }) results!: BulkIssueResultDto[];
}
class ShowcaseProjectDto {
@ApiProperty({ format: 'uuid' }) id!: string;
@ApiProperty({ example: 'WEB' }) key!: string;
@ApiProperty() name!: string;
@ApiProperty({ type: String, nullable: true }) description!: string | null;
}
class ShowcaseSprintDto {
@ApiProperty() name!: string;
@ApiProperty({ type: String, nullable: true }) goal!: string | null;
@ApiProperty({ format: 'date' }) startDate!: string;
@ApiProperty({ format: 'date' }) endDate!: string;
@ApiProperty() percent!: number;
}
class ShowcaseEpicDto {
@ApiProperty() key!: string;
@ApiProperty() title!: string;
@ApiProperty({ type: String, nullable: true, format: 'date' })
startDate!: string | null;
@ApiProperty({ type: String, nullable: true, format: 'date' })
dueDate!: string | null;
@ApiProperty() percent!: number;
@ApiProperty() done!: boolean;
@ApiProperty({
example: '/p/WEB/issues/WEB-3',
description: 'In the web app',
})
path!: string;
}
class ShowcaseReleaseDto {
@ApiProperty() name!: string;
@ApiProperty({ enum: ReleaseStatus, enumName: 'ReleaseStatus' })
status!: ReleaseStatus;
@ApiProperty({ type: String, nullable: true }) targetDate!: string | null;
@ApiProperty({ type: String, nullable: true }) releasedAt!: string | null;
@ApiProperty() percent!: number;
@ApiProperty({
type: String,
nullable: true,
description: 'Shipped ones only',
})
notes!: string | null;
}
class ShowcaseIssueDto {
@ApiProperty() key!: string;
@ApiProperty() title!: string;
@ApiProperty({ description: 'A ticket type id' }) type!: string;
@ApiProperty({ example: '/p/WEB/issues/WEB-9' }) path!: string;
}
export class ShowcaseDto {
@ApiProperty({ type: ShowcaseProjectDto }) project!: ShowcaseProjectDto;
@ApiProperty({ type: ShowcaseSprintDto, nullable: true })
activeSprint!: ShowcaseSprintDto | null;
@ApiProperty({ type: [ShowcaseEpicDto] }) roadmap!: ShowcaseEpicDto[];
@ApiProperty({ type: [ShowcaseReleaseDto] }) releases!: ShowcaseReleaseDto[];
@ApiProperty({
type: [ShowcaseIssueDto],
description: 'Open tickets labelled good-first-issue',
})
goodFirst!: ShowcaseIssueDto[];
}
export class GitDeliveryResultDto {
@ApiProperty({ description: 'Tickets that got something new' })
recorded!: number;

View File

@@ -1,8 +1,14 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Header, Param } from '@nestjs/common';
import {
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { Public } from '@task-stream/api/identity/presenters';
import { ProjectsService } from '@task-stream/api/tracker/application';
import { ProjectDto } from './dto/responses.dto';
import { ProjectDto, ShowcaseDto } from './dto/responses.dto';
import { toProjectDto } from './refs';
/** What anyone can read without signing in. */
@@ -22,4 +28,23 @@ export class ExploreController {
async publicProjects() {
return (await this.projects.findPublic()).map(toProjectDto);
}
@Get('projects/:project/showcase')
@Public()
@Header('Cache-Control', 'public, max-age=300')
@ApiParam({
name: 'project',
description: 'Project id or key',
example: 'WEB',
})
@ApiOperation({
summary: "A public project's showcase (no token needed)",
description:
'For public websites: epics with their dates and progress, the latest releases (with the notes of shipped ones), the sprint under way and open tickets labelled good-first-issue. Paths point into the web app.',
})
@ApiOkResponse({ type: ShowcaseDto })
@ApiNotFoundResponse({ description: 'Not a public project' })
showcase(@Param('project') project: string) {
return this.projects.showcase(project);
}
}