Skip to content

feat(tasks): record a completion date and apply the reopen rule to it (#350) - #378

Merged
nevridge merged 4 commits into
mainfrom
feature/task-completed-at
Oct 31, 2026
Merged

nevridge merged 4 commits into
mainfrom
feature/task-completed-at

Conversation

@nevridge

Copy link
Copy Markdown
Owner

Summary

Records when a task was completed and applies the reopen rule to that day (#350, part A of the completion-date work in #366).

  • New server-owned, nullable UTC TaskItem.CompletedAt. It is set on any move to Completed (PUT, PATCH, Blocked to Completed, create as Completed, parent auto-complete) and cleared on reopen. It is written only in TaskRepository.UpdateAsync and AddAsync. Client-sent values are ignored (no request DTO has the field, and the Detached update path keeps the persisted value).
  • completedAt is returned on task responses, project tasks and journal todos (including child todos).
  • Reopen guard: "past day" now means the user's local completion day when CompletedAt exists, otherwise the linked journal day (unchanged). Tasks with neither are exempt. Code TASK_REOPEN_PAST_DAY_NOT_ALLOWED, message and 422 status are unchanged; details.assignedDate holds the completion day.
  • New 422 TASK_PLAN_COMPLETED_NOT_ALLOWED: PUT or PATCH with a journalDate that differs from the current link on a task with a CompletedAt is rejected before any write.
  • Additive migration AddTaskItemCompletedAt backfills CompletedAt from the latest genuine Completed event that is after the latest Reopened event (julianday ordering, so both SQLite timestamp formats compare correctly; synthetic "Marked complete by a data migration" events are ignored; Status = 2 guard). Tasks with no usable event stay null.
  • UserDateHelper.GetUserDate added; GetUserToday delegates to it.
  • [ProducesResponseType(422)] and XML docs on Update and Patch; client regenerated (never hand-edited).
  • Docs: docs/API.md, docs/FRONTEND.md, docs/ARCHITECTURE.md (stale IsComplete sample replaced).
  • Frontend (small): task-errors.ts maps the new code; the client-side reopen string is now "Tasks completed on a past day cannot be reopened."; ProjectTaskDto.completedAt. No UI, keyboard or request-body changes.

Closes #350

Decisions made during the build

  • AC15 amended: the auto-complete path sets CompletedAt through the central write and needs no offset parameter (it is a UTC instant).
  • Reopen and re-plan in one request on a task with a stored CompletedAt returns 422 (reopen first, plan second).
  • Legacy Completed tasks with a null CompletedAt stay plannable.
  • Create-as-Completed stamps CompletedAt but writes only the TaskCreated event.

Behavior change to note

A task with a CompletedAt, no journal link, and a past local completion day can no longer be reopened (previously a task with no link was always reopenable). After the backfill this applies to legacy tasks completed days ago that have a recoverable Completed event and no link. The migration runs on startup, so it runs in production on deploy.

Files changed

  • Backend: TaskItem, TaskRepository, TaskItemsController, JournalTodosController, ProjectsController, DTOs (new TaskRuleErrorResponseDto), UserDateHelper, UtcDateTimeModelBuilderExtensions, migration 20261031024516_AddTaskItemCompletedAt (+ Designer, snapshot), TaskFlow.Api.csproj.
  • Generated client: types.gen.ts, sdk.gen.ts, index.ts.
  • Frontend: task-errors.ts (+ test), TodosSection.tsx (+ test), TaskDetailPage.test.tsx, projects.ts.
  • Docs: API.md, FRONTEND.md, ARCHITECTURE.md.
  • Tests: new AddTaskItemCompletedAtMigrationTests, new TaskItemsControllerCompletedAtTests; extended repository, controller, journal, project, UTC-allowlist and UserDateHelper tests.

Test results

  • dotnet build TaskFlow.sln: 0 warnings, 0 errors.
  • dotnet test --project TaskFlow.Api.Tests: 921 passed, 0 failed. CI-style coverage about 95% (floor 75%).
  • dotnet format TaskFlow.sln --verify-no-changes: pass.
  • npm run type-check: pass.
  • npm run test -- --run --coverage: 88 files, 1568 tests passed; 97.31% line coverage (floor 80%).
  • npm run lint: 0 errors, 1 warning at TasksPage.tsx:424 (not touched here).

Residual findings (for awareness)

  • Needs a decision: completing and re-planning a not-yet-completed task in one request succeeds, because the plan check reads the stored CompletedAt. This matches the AC16 wording; if it should be blocked, that is a follow-up (relevant to the completion-day display in feat(journal): show completed tasks on the day they were completed (journal reads and UI) #367).
  • Backfill treats events in the same millisecond (across the 3- and 7-digit formats) as equal, so CompletedAt stays null. Conservative and pinned by tests.
  • OpenAPI declares only 200 and 422 on PUT and PATCH (not 400 or 404), so the generated error unions list only the 422 body.
  • Enabling GenerateDocumentationFile (so the 422 descriptions reach OpenAPI) adds JSDoc blocks, some with raw cref text, to existing DTOs in types.gen.ts. Cosmetic.
  • Left alone on purpose: TASK_PARENT_CYCLE_NOT_ALLOWED in FRONTEND.md (not a controller constant), the API.md title-max typo (200 vs 100), and the lowercase 'completed' the journal toggle sends (accepted by the case-insensitive enum converter).
  • Not recoverable by the backfill: completions before 2026-05-17, created-as-Completed tasks, and tasks with only synthetic events. They stay null, as the story specifies.

@nevridge nevridge left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: 💬 Comments

No issues found in this diff.


Reviewed at 1e92b75 · findings from pr-review + code-review, verified and triaged by @nevridge with Claude Code

@nevridge
nevridge merged commit c0c5ea1 into main Oct 31, 2026
15 checks passed
@nevridge
nevridge deleted the feature/task-completed-at branch October 31, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tasks): record a completion date and apply the reopen rule to it

1 participant