Module: boards¶
Purpose¶
Provides Kanban (active sprint board) and Scrum backlog views. Handles column ordering and drag-and-drop issue reordering by delegating status changes to IssueService, which validates against the workflow. Publishes WebSocket notifications for real-time board updates.
Entities Owned¶
The boards module owns no database tables. All data is derived from workflow statuses, sprint assignments, and issues owned by other modules.
DB Schema¶
No owned tables. Board views are derived from workflow_statuses (column definitions ordered by position), sprints (active sprint selection), and issues (issue data and status assignments).
API Endpoints¶
BoardController — /api/v1/projects/{key}¶
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/projects/{key}/board |
USER | Returns the active sprint board grouped by workflow status columns; returns 204 No Content when no active sprint exists |
| PATCH | /api/v1/projects/{key}/board/move |
USER | Moves an issue to a new status column; delegates to IssueService.update() which validates the transition against the workflow |
| GET | /api/v1/projects/{key}/backlog |
USER | Returns PLANNED sprints with their issues plus unassigned backlog issues |
Events Emitted¶
The boards module emits no domain events. BoardEventPublisher subscribes to domain events from other modules and forwards them as WebSocket messages on a per-project topic.
Events Consumed¶
BoardEventPublisher — WebSocket forwarding via SimpMessagingTemplate¶
| Event | Source module | WebSocket topic | Message payload |
|---|---|---|---|
IssueStatusChangedEvent |
issues | /topic/projects/{projectKey} |
{ type: "ISSUE_MOVED", issueId, newStatusId, projectKey } |
SprintStartedEvent |
sprints | /topic/projects/{projectKey} |
{ type: "SPRINT_UPDATED", sprintId, projectKey } |
SprintCompletedEvent |
sprints | /topic/projects/{projectKey} |
{ type: "SPRINT_UPDATED", sprintId, projectKey } |
Key Files¶
| File | Responsibility |
|---|---|
backend/src/main/kotlin/com/taskowolf/boards/application/BoardService.kt |
getBoard(): assembles board columns from active sprint and workflow statuses; getBacklog(): returns PLANNED sprint entries plus unassigned issues |
backend/src/main/kotlin/com/taskowolf/boards/api/BoardController.kt |
REST endpoints; move delegates directly to IssueService.update() — no transition logic in this class |
backend/src/main/kotlin/com/taskowolf/boards/events/BoardEventPublisher.kt |
@EventListener on IssueStatusChangedEvent, SprintStartedEvent, SprintCompletedEvent; forwards to WebSocket |
backend/src/main/kotlin/com/taskowolf/boards/api/dto/BoardResponse.kt |
BoardResponse(sprint: BoardSprintSummary, columns: List<BoardColumnResponse>); BoardSprintSummary includes daysRemaining: Long? |
backend/src/main/kotlin/com/taskowolf/boards/api/dto/BacklogResponse.kt |
BacklogResponse(sprints: List<BacklogSprintEntry>, backlogIssues: List<IssueResponse>); BacklogSprintEntry includes totalPoints: Int |
backend/src/main/kotlin/com/taskowolf/boards/api/dto/BoardMoveRequest.kt |
BoardMoveRequest(issueId: UUID, newStatusId: UUID) |
Extension Points¶
To add a new board column type (e.g. swimlane grouping by assignee):
- Add a second grouping pass after the status grouping in
BoardService.getBoard(). - Add the new fields to
BoardColumnResponseor introduce a separate response DTO. - Add a new endpoint in
BoardControllerif the view warrants a distinct path.
No DB migration is required — boards derive all data from other modules.
Common Pitfalls¶
- Boards do not own status transitions.
PATCH /board/movecallsIssueService.update(key, issueId, UpdateIssueRequest(statusId = newStatusId), user). Transition validation is performed byWorkflowService.validateTransition()insideIssueService. Do not add transition logic toBoardControllerorBoardService. - DO NOT add business logic to board endpoints. Board endpoints are read-heavy views. Avoid per-issue queries inside loops; always load issues in batch.
getBoard()returnsnull(HTTP 204) when no active sprint exists. Callers must handle the 204 case explicitly.daysRemaininginBoardSprintSummaryis computed asmax(0, ChronoUnit.DAYS.between(LocalDate.now(), endDate)). It is null whenendDateis not set on the sprint.completedPointsinBoardSprintSummaryis computed live from current DONE issues in the sprint, not fromsprint.completedPoints(which is only set at close time). The board reflects current state, not the final snapshot.
Example¶
Board column assembly in BoardService.getBoard() — columns ordered by position from the workflow, each containing only sprint issues with the matching status:
val sprintIssues = issueRepository.findBySprintId(sprint.id)
val issuesByStatus: Map<UUID, List<Issue>> = sprintIssues.groupBy { it.status.id }
val columns = workflow.statuses.map { status ->
BoardColumnResponse(
status = StatusSummary(
id = status.id,
name = status.name,
category = status.category.name,
color = status.color
),
issues = (issuesByStatus[status.id] ?: emptyList()).map { IssueResponse.from(it) }
)
}
workflow.statuses is ordered by position ASC via @OrderBy("position ASC") on Workflow.statuses. Column order follows the workflow status order; there is no separate board column configuration.
Drag-and-drop move in BoardController.move() — fully delegated, no local logic:
@PatchMapping("/board/move")
fun move(
@PathVariable key: String,
@RequestBody request: BoardMoveRequest,
@AuthenticationPrincipal user: User
) {
issueService.update(key, request.issueId, UpdateIssueRequest(statusId = request.newStatusId), user)
}
Test Patterns¶
Unit tests (MockK, no Spring context)¶
| File | What is tested |
|---|---|
BoardServiceTest |
getBoard() returns null when no active sprint exists |
BoardServiceTest |
getBoard() groups issues into columns by status ID |
BoardServiceTest |
getBacklog() returns PLANNED sprints and unassigned backlog issues |
No dedicated board integration tests exist in isolation. Board endpoints are covered by SprintLifecycleIntegrationTest (backend/src/test/kotlin/com/taskowolf/sprints/SprintLifecycleIntegrationTest.kt), which exercises GET /board and PATCH /board/move as part of the full sprint lifecycle.