Skip to content

feat: extract task comment timeline into the Collaboration microservice#84

Merged
4Keyy merged 15 commits into
developfrom
claude/collaboration-comments-microservice
May 30, 2026
Merged

feat: extract task comment timeline into the Collaboration microservice#84
4Keyy merged 15 commits into
developfrom
claude/collaboration-comments-microservice

Conversation

@4Keyy

@4Keyy 4Keyy commented May 30, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

Extracts the task comment timeline ("ветки") — user, genesis, and system comments — out of TodoApi into a dedicated Collaboration microservice. This gives the timeline its own bounded context, database, and lifecycle, so comments can evolve (and scale) independently of the task aggregate while keeping TodoApi focused purely on tasks.

The new service follows the platform template exactly: clean architecture, BuildingBlocks wiring, Serilog + OpenTelemetry, shared global exception middleware, JWT + security-stamp validation, rate limiting, response compression, health endpoints, the Outbox pattern, and a non-root Dockerfile.

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Documentation
  • CI / infrastructure
  • Other:

Responsibility split

  • TodoApi no longer contains any comment code. It publishes task-lifecycle integration events through its own outbox — TaskCreatedIntegrationEvent, TaskActivityIntegrationEvent (completed/started/left), TaskDeletedIntegrationEvent — and exposes TodoService.CheckTaskCommentAccess over gRPC. The EF migration RemoveCommentsAddOutbox drops todo_item_comments and adds todo.OutboxMessages.
  • Collaboration owns collaboration.comments (database planora_collaboration, gateway prefix /collaboration/api/v1/comments). It authorises every read/write via the Todo gRPC access check (owner / shared / public + friendship — never reading Todo's DB, INV-OWN-1), materialises system/genesis comments from the Todo events through idempotent Inbox consumers (INV-COMM-4), and fans out a NotificationEvent per participant on each new comment (Outbox → RabbitMQ → Realtime/SignalR).

Errors, validation & security

  • gRPC faults from the Todo access check surface as HTTP 503 via a DomainException (ExternalServiceUnavailableException); the shared middleware also maps RpcException natively.
  • FluentValidation validators reject malformed input as 400 through the shared ValidationBehavior.
  • [Authorize] on the controller; every operation re-checks task access server-side. gRPC clients carry the x-service-key (INV-COMM-2); JWT + security-stamp revocation wired (INV-AUTH-4).

Data migration

Planora.Migrator --backfill-collaboration idempotently copies todo.todo_item_commentscollaboration.comments (INSERT ... ON CONFLICT (Id) DO NOTHING). Run it before applying RemoveCommentsAddOutbox in production, and again at cutover to capture the window.

Frontend

Comment API calls repoint to /collaboration/api/v1/comments/*; the CommentDto JSON shape is unchanged, so the timeline UI is untouched. Existing develop frontend (CSRF retry, trace-id propagation, request cancellation) preserved through the merge.

Testing

  • Backend unit tests written (dotnet test Planora.sln) — Collaboration domain, handler (access matrix + notification fan-out), and integration-event consumer (replay-safe materialisation, cascade, user-deletion) suites, plus WorkerLifecycleEventTests pinning the new event-based worker lifecycle in TodoApi.
  • Frontend lint/type-check pass — to be confirmed by CI
  • Frontend tests pass — to be confirmed by CI
  • Manually tested end-to-end — note: no .NET SDK in the authoring environment; build/test verified statically and delegated to CI (see note below)

⚠️ Author's note: this branch was prepared in an environment without a .NET SDK, so dotnet build/dotnet test/dotnet ef could not be run locally. Everything was verified statically (signatures, usings, snapshot↔migration↔model consistency, layering, invariant compliance). Please rely on the CI backend, migrations, e2e, and security workflows for the authoritative green check.

Checklist

  • No secrets, personal data, or local-only config committed
  • .env.example updated if new env vars were added (reuses existing shared vars; added GrpcServices__TodoApi usage + Collaboration entry in deploy/fly/set-secrets.ps1)
  • Documentation updated (architecture, database, API, codebase-map, features, testing, security-idor-coverage, overview, index, glossary, INVARIANTS)
  • CHANGELOG.md updated under ## Unreleased

https://claude.ai/code/session_019ETjrb3oYgitUtT6ABM7hF


Generated by Claude Code

claude added 11 commits May 29, 2026 21:47
…ment timeline

Extract the task comment timeline ('ветки') — user, genesis and system
comments — into a dedicated Collaboration microservice, fully consistent
with the existing service template (clean architecture, BuildingBlocks,
gRPC+ServiceKey, Outbox, Serilog/OTEL, health checks, Docker, Ocelot).

- New Planora.Collaboration.{Domain,Application,Infrastructure,Api} projects
- Comment aggregate + repository + EF config (schema 'collaboration')
- CQRS handlers: add/genesis/update/delete/get comments
- Inbox consumers materialise system/genesis comments and cascade-delete
  from Todo lifecycle integration events; clean up on user deletion
- Comment notifications published to Outbox -> NotificationEvent -> Realtime
- Task access authorised via Todo gRPC (CheckTaskCommentAccess) so no
  cross-service DB reads (INV-OWN-1); avatars via Auth gRPC (cached)
- Shared integration-event contracts (TaskCreated/TaskActivity/TaskDeleted)
- Wiring: solution, docker-compose, Ocelot routes, Migrator, Todo gRPC port
- Frontend comment API calls repointed to /collaboration/api/v1/comments

Todo-side removal of comment code follows in a subsequent commit.
…cycle events

TodoApi no longer owns any 'ветки' code. The comment entity, repository,
DTO, 5 CQRS handlers, avatar gRPC client and REST endpoints are deleted.
Task lifecycle now drives the Collaboration timeline via integration events
published through a new Todo outbox (INV-COMM-3):

- CreateTodo  -> TaskCreatedIntegrationEvent (system 'created' + genesis)
- Update/Join/Leave -> TaskActivityIntegrationEvent (completed/started/left)
- DeleteTodo  -> TaskDeletedIntegrationEvent (cascade soft-delete)

- TodoGrpcService.CheckTaskCommentAccess exposes the task access decision
  (owner/shared/public + friendship) + participants for Collaboration
- TodoDbContext gains OutboxMessages; OutboxProcessor registered; Todo gRPC
  endpoint published on :81 in Docker
- EF migration drops todo.todo_item_comments and creates todo.OutboxMessages
- Tests: comment-only suites removed, handler fixtures switched to the outbox
  ctor, Comment domain tests ported to Collaboration, arch tests cover it
…variants

- Planora.Migrator --backfill-collaboration: idempotent copy of
  todo.todo_item_comments -> collaboration.comments (run before route cutover)
- docs/INVARIANTS.md: INV-OWN-1 adds Collaboration DB; INV-AZ-4 documents that
  comment-thread friendship authorisation is delegated to TodoApi via the
  CheckTaskCommentAccess gRPC contract
…trap

- TaskAccessGrpcClient wraps gRPC faults in ExternalServiceUnavailableException
  (DomainException -> HTTP 503 via shared middleware), matching TodoApi semantics
- Honour OperationCanceledException without remapping
- deploy/fly/collaboration-service.fly.toml added (mirrors todo-service)
- deploy/fly/postgres-init.sql creates planora_collaboration
…t matrix

- ExternalServiceUnavailableException now mirrors TodoApi exactly (DomainException
  with ErrorCode.Infrastructure.ExternalServiceUnavailable + ServiceUnavailable
  category + inner exception) — the previous ctor signature did not exist
- Rename deploy/fly manifest to collaboration.fly.toml matching the repo naming
  and structure (internal gRPC over :443, /health/live + /health/ready checks)
- set-secrets.ps1: planora-collaboration joins the secret matrix (shared + DB +
  Auth/Todo gRPC addresses); migrator gains CollaborationDatabase
Duplicate 'using ...Application.Context;' would fail the -warnaserror build
(CS0105). Verified no duplicate usings remain across Todo/Collaboration.
…on-comments-microservice

# Conflicts:
#	Services/CollaborationApi/Planora.Collaboration.Infrastructure/Persistence/Configurations/CommentConfiguration.cs
#	docker-compose.yml
#	tests/Planora.UnitTests/Architecture/ArchitectureTests.cs
#	tools/Planora.Migrator/Planora.Migrator.csproj
#	tools/Planora.Migrator/Program.cs
…ypass)

The repo .gitignore lists **/Migrations/**; existing Todo migrations were
force-added historically. 'git add -A' silently skipped the new migration, so
the DROP of todo_item_comments + creation of todo.OutboxMessages was absent
from history while the model snapshot already reflected them — a snapshot/DB
drift that would make MigrateAsync fail or leave the schema wrong. Force-add
both the migration and its designer so the schema change ships.
…/API/architecture docs

Tests (Collaboration):
- CommentCommandHandlerTests: access matrix for add/genesis/update/delete
  (grant/deny/not-found, owner-only genesis, dup-genesis guard, author-vs-owner
  delete rules, notification fan-out count)
- IntegrationEventConsumerTests: TaskCreated (system+genesis, replay-safe),
  TaskActivity (completed/started/left + unknown-type skip), TaskDeleted cascade,
  UserDeleted authored-comment cleanup + no-op

Docs:
- database.md: Collaboration DB section, ownership row, Todo OutboxMessages,
  RemoveCommentsAddOutbox migration, corrected gitignore/force-add note, backfill
- API.md: Collaboration endpoint section + gateway routes; Todo comment routes removed
- architecture.md: service list, boundaries, gRPC access delegation, event flow
…le coverage

Docs (all service-facing pages now reflect the Collaboration extraction):
- codebase-map.md: Collaboration Service section; Todo critical-files updated
- features.md: Task Comments rewritten around the Collaboration service + event flow
- security-idor-coverage.md: comment rows repointed to /collaboration + new coverage,
  worker rows repointed off the deleted test file
- testing.md: Collaboration test inventory; worker lifecycle now event-based
- overview.md / index.md / glossary.md: Collaboration listed in services, ownership, terms

Tests:
- WorkerLifecycleEventTests.cs: pins that Join/Leave publish the correct
  TaskActivityIntegrationEvent via outbox (restores coverage lost when the
  comment-coupled WorkersAndComments test suite was removed), owner-cannot-leave guard
WORKDIR /app
COPY --from=publish /app/publish .

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
claude added 4 commits May 30, 2026 11:46
- CollaborationApi/Program.cs: add missing using for
  Planora.BuildingBlocks.Infrastructure.Configuration so AddPlanoraSwaggerGen /
  UsePlanoraSwagger resolve (CI 'backend' -warnaserror build break, CS1061)
- CHANGELOG.md: use '*' list markers in the new section to match the file's
  established bullet style (CI 'docs' MD004/ul-style consistency)
…Tests

TodoItemDto has 10 required init members; the mapper mock only set 4, failing
the -warnaserror backend build (CS9035). Populate every required member.
Badges, architecture diagram + service/data-store table, tech-stack matrix,
engineering-principles (invariants) section, Docker-first quickstart, config
table, testing/CI summary, project structure, and a documentation index.
Adds the Collaboration service that the previous README omitted.
…xUnit2013)

xUnit2013 (do not use Assert.Equal for collection size) is warning-as-error
in this repo (-warnaserror); the codebase uses zero such patterns. The new
TaskCreated consumer test tripped it, failing the test-project build. Switch
to Assert.Collection, which also pins the [system, genesis] ordering.
@4Keyy 4Keyy merged commit 4af1b2b into develop May 30, 2026
8 of 22 checks passed
@4Keyy 4Keyy deleted the claude/collaboration-comments-microservice branch May 30, 2026 19:17
4Keyy pushed a commit that referenced this pull request May 30, 2026
The merged PR #84 left the backend build red on develop: WorkerLifecycleEventTests
referenced IUnitOfWork without importing Planora.BuildingBlocks.Domain.Interfaces
(CS0246). Add the using so 'dotnet build -warnaserror' passes.
4Keyy added a commit that referenced this pull request May 31, 2026
The #84 Collaboration microservice extraction left CI and the security
scan red on develop. Four independent breakages are fixed here so every
gate is green again.

WHAT changed and WHY:
- TodoGrpcServiceTests: the gRPC service constructor gained
  ITodoRepository and IFriendshipService (for CheckTaskCommentAccess),
  but the test factory still passed the old two-arg shape (CS7036).
  Updated CreateService to supply default mocks for both dependencies.
- WorkerLifecycleEventTests: replaced Assert.True(.Any(...)) with
  Assert.Contains to satisfy the xUnit2012 analyzer promoted to error
  under -warnaserror.
- docs/database.md: a wrapped bullet line began with '+ shared-with',
  which markdownlint parsed as a plus-style list marker (MD004).
  Rewrapped so the continuation no longer starts with a list glyph.
- Collaboration Dockerfile: added --no-install-recommends to the
  apt-get install, matching every other service Dockerfile and clearing
  the Trivy IaC HIGH that blocked the security scan.

HOW verified: full backend build with -warnaserror, dotnet test (788
passed), markdownlint with the CI globs (0 errors), and the frontend
lint/type-check/test/build pipeline all pass locally. The CodeQL csharp
job failed only because autobuild hit the same CS7036 compile error, so
it recovers with the test fix.

Security: restores the Trivy IaC HIGH/CRITICAL gate to green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4Keyy added a commit that referenced this pull request May 31, 2026
…cher

The #84 Collaboration microservice extraction added a new service that the
local launcher never started, and the launcher carried four hand-maintained
port arrays that had already drifted from reality. This brings the launcher
in line with the current topology and makes its port handling impossible to
desync.

WHAT changed and WHY:
- Start collaboration-api: added it to $ServiceDefs at port 5060, placed
  after auth-api and todo-api because it is a gRPC client of both (Auth 5031,
  Todo 5101). It now builds, starts, health-checks, appears in the summary
  table, and is torn down with the rest.
- Single source of truth for ports: $ServiceRestPorts, $ServiceGrpcPorts,
  $AllPlanoraPorts, and $ShutdownOrder are now derived from $ServiceDefs.
  Stop-PlanoraProcesses and Invoke-GracefulShutdown use them, replacing four
  divergent literal arrays (which omitted 5060 and disagreed on gRPC ports).
- New -Stop switch: stops every process the launcher started and frees their
  ports without touching infrastructure containers or data volumes, reusing
  the existing graceful-shutdown path.
- New -Help switch: prints a concise usage block and exits before any side
  effect (no transcript/log file created).

HOW verified: the script parses cleanly via the PowerShell AST parser, and
both -Help and -Stop were run end-to-end (Stop exercises the derived port
lists and shutdown order and exits 0). README and getting-started.md document
the new flags; CHANGELOG records the change.

Security: -Stop deliberately skips loading .env, so no secrets enter the
process environment for a teardown-only invocation
4Keyy added a commit that referenced this pull request May 31, 2026
UpdateCommentCommandValidator capped every comment edit at 2000 characters,
but a genesis comment (the task description) is allowed up to 5000 - both on
create and in the domain (Comment.UpdateGenesisContent). Because the validator
runs in the ValidationBehavior pipeline before the handler, editing a task
description to 2001-5000 characters was rejected with a 400, contradicting the
domain, docs/features.md, and the documented PUT behavior in docs/API.md.

The validator only receives the ids and content - it cannot tell a genesis
comment from a regular one - so it now enforces only the shared upper bound
(5000). The domain applies the exact per-kind limit (2000 regular via
Comment.UpdateContent, 5000 genesis via UpdateGenesisContent) and a violation
surfaces as a 400 (InvalidValueObjectException is ErrorCategory.Validation).

Added UpdateCommentCommandValidatorTests pinning the boundaries (accepts 3000
and 5000, rejects empty and 5001, requires TaskId and CommentId). The full
Collaboration suite passes (40 tests). docs/API.md and docs/features.md already
described the intended 5000 limit, so the code was the defect, not the docs.

Refs: #84
4Keyy added a commit that referenced this pull request May 31, 2026
… live author identity

Removes a cross-service data-duplication bug class. The task description was
stored twice — TodoItem.Description (the card) and a 'genesis' comment in the
Collaboration DB (the branch) — synced only at creation via an async event. So
pre-Collaboration tasks had an empty branch, new tasks' descriptions lagged the
outbox cycle, and the two edit paths could diverge.

P1 — description = single source of truth (Todo):
- CheckTaskCommentAccess gRPC now returns the live description + task_created_at.
- GetCommentsQueryHandler synthesises the pinned 'Author's Note' from it on read
  (page 1 only; id = task id, author = owner) instead of reading a stored genesis.
  Instant, always matches the card, present for old tasks.
- TaskCreatedEventConsumer no longer materialises a genesis (only the 'created
  the task' system comment). Removed the POST /genesis endpoint, the
  AddGenesisComment command/handler/validator, and Comment.CreateGenesis /
  UpdateGenesisContent. The read query excludes any legacy genesis rows.
- Frontend edits the description on the task (PUT /todos) via a new
  onSaveDescription path in the edit modal; branch-feed no longer calls genesis
  endpoints. Removed the dead addGenesisComment API client function.

P2 — author identity resolved live:
- Comment.AuthorName was a stored copy of the Auth-owned name that went stale on
  rename. Added AuthService.GetUserProfilesBatch (name + avatar); Collaboration
  resolves comment + genesis author identity live (60 s cache via CachingUserService),
  keeping the stored name only as an offline fallback. IUserService is now
  profile-based (GetUserProfilesAsync) instead of avatar-only.

HOW verified: end-to-end on a live local stack — a task created with a description
returns the Author's Note on an immediate (0s) fetch with the author name resolved
live, and editing the description on the task reflects in the branch. Backend builds
under -warnaserror; 784 backend + 370 frontend tests pass on net10.0. Docs (API,
features, database, architecture) updated to the new model.

Refs: #84
4Keyy added a commit that referenced this pull request Jun 1, 2026
Audit follow-ups (P1 + P2).

P1 - consumer idempotency (INV-COMM-4) was documented but not implemented.
RabbitMqEventBus delivers at-least-once (nack+requeue on failure), but nothing
deduped - the IdempotentMessageHandler/Inbox machinery was dead code, so a
redelivered or restart-replayed event produced duplicate system comments
(Collaboration) / notifications. The bus now dedups centrally on the stable
@event.Id via IInboxRepository: skip the handler when the id is already recorded,
record it after success. Graceful + defensive - a service with no inbox (or an
inbox error) falls back to the previous behaviour, never worse. Added an
InboxMessages table + repository to Collaboration (PK = event id); Realtime is a
follow-up. Also fixed the pre-existing bug where IdempotentMessageHandler checked
ExistsAsync against the PK while storing the id in MessageId (never matched) - the
new InboxMessage(Guid eventId,...) ctor makes the PK the event id.

P2 - AsNoTracking on CategoryApi reads. BaseRepository already does this for its
generic reads, but the custom CategoryRepository did not. Added it to the
read-only methods (list/get/paged); kept tracking on GetByIdAsync (load-then-update)
and FindAsync (also used for fetch-then-RemoveRange).

HOW verified: build clean under -warnaserror; 784 tests pass on net10.0; live
stack confirmed the inbox records the processed TaskCreatedIntegrationEvent and
exactly one 'created the task' system comment is produced. Docs (database.md,
features.md) updated; the Collaboration inbox table is created by EnsureCreated on
a fresh DB.

Refs: #84
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.

3 participants