Add batch triage and suggestion editing for Inbox artifacts#607
Add batch triage and suggestion editing for Inbox artifacts#607Chris0Jeky merged 8 commits intomainfrom
Conversation
Add BatchTriageAsync and UpdateSuggestionAsync to ICaptureService/CaptureService.
Add POST /api/capture/items/batch-triage with 207 partial-success semantics.
Add PUT /api/capture/items/{id}/suggestion for editing capture text before triage.
Batch operations process each item independently with per-item error reporting.
Closes #220 (backend portion).
Add multi-select checkboxes with select-all toggle to inbox item list. Add batch action bar for triage/ignore/cancel selected items. Add inline suggestion text editing before triage with save/cancel. Add batchTriage and updateSuggestion to captureStore and captureApi. Add BatchTriageResult and UpdateCaptureSuggestionDto types.
Unit tests: batch validation (empty, duplicates, invalid actions, oversized), partial failure handling, ignore action, suggestion text editing with authorization and state-transition guards. API tests: unauth checks for new endpoints, batch triage success/partial/empty, suggestion editing with ownership and conflict guards.
InboxView: checkbox rendering, select-all toggle, batch action bar visibility, batch triage/ignore dispatch, selection clearing after batch, edit button for new items, suggestion editing save/cancel flows. captureStore: batch API dispatch, partial failure toasts, batchBusy tracking, suggestion update caching, actionBusyItemId tracking, error handling. captureApi: batch triage POST, suggestion PUT endpoint tests.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Adversarial Self-ReviewFindings1. Sequential batch processing (no parallelism risk but N+1 DB concern) 2. Missing text length validation on UpdateSuggestionAsync 3. Batch actions share a single 4. Frontend selectedIds reactivity with Set 5. 6. No CSRF concern Actions Taken
|
Add MaxRawTextLength and MaxTitleHintLength validation to UpdateSuggestionAsync to prevent arbitrarily long text submissions. Allow Triaged items to show the Edit Text button in InboxView, matching the backend's allowed edit statuses (New, Failed, Triaged).
Self-review fixes applied (197c7c8)
All backend (31 CaptureService + 25 CaptureApi) and frontend (1377) tests pass after these fixes. |
There was a problem hiding this comment.
Code Review
This pull request introduces batch triage capabilities and the ability to edit capture suggestions before triage. Key changes include new API endpoints for batch operations and suggestion updates, corresponding service layer logic in CaptureService, and a new batch selection UI in the frontend InboxView. I have identified two areas for improvement: the batch processing logic in the backend currently performs sequential database saves which could be optimized to a single transaction or bulk update, and the suggestion editing UI should pre-populate the title hint from the existing item data to improve the user experience.
| foreach (var itemAction in request.Items) | ||
| { | ||
| try | ||
| { | ||
| var actionResult = itemAction.Action.ToLowerInvariant() switch | ||
| { | ||
| "triage" => await ExecuteBatchItemTriageAsync(userId, itemAction.ItemId, cancellationToken), | ||
| "ignore" => await CancelInternalAsync(userId, itemAction.ItemId, cancellationToken), | ||
| "cancel" => await CancelInternalAsync(userId, itemAction.ItemId, cancellationToken), | ||
| _ => Result.Failure(ErrorCodes.ValidationError, $"Unknown action: {itemAction.Action}") | ||
| }; | ||
|
|
||
| results.Add(new BatchTriageItemResultDto( | ||
| itemAction.ItemId, | ||
| actionResult.IsSuccess, | ||
| actionResult.IsSuccess ? null : actionResult.ErrorCode, | ||
| actionResult.IsSuccess ? null : actionResult.ErrorMessage)); | ||
| } | ||
| catch (Exception) | ||
| { | ||
| results.Add(new BatchTriageItemResultDto( | ||
| itemAction.ItemId, | ||
| false, | ||
| ErrorCodes.UnexpectedError, | ||
| "An unexpected error occurred while processing this item")); | ||
| } | ||
| } |
There was a problem hiding this comment.
The BatchTriageAsync method processes items sequentially and performs a database save (SaveChangesAsync) for every item in the batch. For a batch size of up to 50, this results in significant overhead due to multiple database roundtrips and sequential execution. It is recommended to refactor the logic to perform updates in memory and call SaveChangesAsync once at the end of the batch, or at least use a single transaction to batch the operations.
| function startEditSuggestion() { | ||
| if (!selectedItem.value) return | ||
| editedText.value = selectedItem.value.rawText | ||
| editedTitleHint.value = '' |
There was a problem hiding this comment.
startEditSuggestion now pre-fills the title hint field with the item's existing titleHint instead of empty string.
Adversarial Code Review -- PR #607 (Batch Triage Inbox)1. DATA LOSS BUG: UpdateSuggestionAsync drops ClientCreatedAt [Severity: HIGH]File: The updated payload constructor passes var updatedPayload = new CapturePayloadV1(
currentPayload.Version,
currentPayload.Source,
dto.Text,
null, // BUG: drops currentPayload.ClientCreatedAt
dto.TitleHint ?? currentPayload.TitleHint,
currentPayload.ExternalRef,
currentPayload.Provenance);CapturePayloadV1 has ClientCreatedAt as an optional parameter with default null, so this compiles fine but loses data. Should be 2. NO TRANSACTION AROUND BATCH -- PARTIAL MUTATIONS ARE SILENTLY COMMITTED [Severity: MEDIUM-HIGH]File: Each item in the batch calls CancelInternalAsync or ExecuteBatchItemTriageAsync, and each of those calls SaveChangesAsync individually. There is no wrapping transaction. If item 3 of 5 fails mid-batch:
This is acceptable if documented as intended behavior, but it means the batch endpoint is not atomic. If a transient DB error occurs partway through, the user is left in a half-processed state with no built-in retry mechanism. Additionally, each sub-operation does its own GetByIdAsync + SaveChangesAsync round-trip -- for a batch of 50, that is 50-100 DB round-trips. Consider loading all items in a single query upfront. 3. STALE SELECTION STATE AFTER BOARD/FILTER CHANGE [Severity: MEDIUM]File: selectedIds is never cleared when:
This means: select 3 items in Board A, switch to Board B, and the batch action bar still shows "3 selected" with stale IDs that no longer appear in the list. Clicking "Triage" would send IDs that belong to Board A, which will either fail (if authorization is board-scoped) or succeed unexpectedly (if it is user-scoped only), triaging items the user did not intend to act on in the current context. Fix: Clear selectedIds in the watch(activeBoardId) handler and prune stale IDs in the watch(items) handler. 4. ignore AND cancel ARE IDENTICAL -- CONFUSING API SURFACE [Severity: LOW-MEDIUM]File: Both "ignore" and "cancel" map to the exact same CancelInternalAsync call. This is a confusing API contract -- callers have two names for the same operation with no behavioral difference. The frontend exposes both as separate buttons in the batch action bar ("Ignore" and "Cancel"). Either:
5. USER INPUT REFLECTED IN ERROR MESSAGES [Severity: LOW-MEDIUM]File: return Result.Failure<BatchTriageResultDto>(ErrorCodes.ValidationError,
$"Invalid action(s): {string.Join(", ", invalidActions.Select(i => i.Action))}. Valid actions: triage, ignore, cancel");The raw user-supplied Action string is reflected directly into the error message. While this is a validation error (not an HTML page), if these error messages are ever logged or displayed in a context that interprets markup, this could be an injection vector. Consider sanitizing or truncating the reflected values. 6. NO AUTHORIZATION TEST FOR CROSS-USER BATCH TRIAGE [Severity: MEDIUM]Files: There is no test verifying that a batch containing another user's item IDs returns per-item Forbidden errors. The authorization check exists in CancelInternalAsync and EnqueueTriageAsync (they check item.UserId != userId), but there is no test proving:
The single-item UpdateSuggestion has a cross-user forbidden test, but the batch endpoint does not. 7. MISSING VALIDATION: Guid.Empty ITEM IDs IN BATCH [Severity: LOW]File: The batch validates for duplicate IDs but does not check for Guid.Empty item IDs. Sending 8. MISSING NULL CHECK ON Action STRING [Severity: LOW]File: public record BatchTriageItemActionDto(
Guid ItemId,
string Action);Action is a non-nullable string, but JSON deserialization can still produce null if the caller sends 9. TEST GAP: 207 STATUS CODE NOT TESTED AT API INTEGRATION LEVEL FOR ALL-FAIL CASE [Severity: LOW]File: BatchTriage_ShouldReturnPartialSuccess_WhenSomeItemsFail tests the 207 case. But there is no integration test for the all-fail case (expected: 422 UnprocessableEntity). The unit test covers it conceptually but the API-level status code mapping is not verified. Summary
Items 1, 3, and 6 should be addressed before merge. Item 2 is an architectural decision worth documenting. Items 4-5, 7-8, 9 are improvements that could be follow-ups. |
UpdateSuggestionAsync was passing null for ClientCreatedAt instead of currentPayload.ClientCreatedAt, silently dropping the original capture timestamp on every suggestion edit.
The frontend CaptureItem detail type does not expose titleHint, so initializing from it causes a TS2339 compile error. Revert to empty string initialization.
Summary
POST /api/capture/items/batch-triageendpoint accepting multiple item IDs with per-item actions (triage, ignore, cancel). Returns 200/207/422 based on success/partial/total failure. AddPUT /api/capture/items/{id}/suggestionfor editing capture text before triage with state-transition guards.batchTriage,updateSuggestion.Closes #220
Test plan