Skip to content

RG-T129 Notification System bug fixes - #446

Open
ucswift wants to merge 2 commits into
masterfrom
develop
Open

RG-T129 Notification System bug fixes#446
ucswift wants to merge 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 4, 2026

Copy link
Copy Markdown
Member

PR Description: RG-T129 Notification System Bug Fixes

Summary

This PR fixes several bugs in the notification system that prevented notifications from firing correctly and caused runtime errors under certain conditions.

Changes

Bug Fix: Incorrect Event Type in Group Lookup

In GetGroupForEventAsync, the code block that looks up personnel staffing data was incorrectly matched against PersonnelStatusChanged instead of PersonnelStaffingChanged. This meant group-based notifications for staffing changes would never resolve the correct department group.

Bug Fix: Empty BeforeData/CurrentData Causing Notifications to Never Fire

The notification validation logic previously returned false whenever BeforeData or CurrentData was null or empty. Since the UI's "Any" option was posting an empty string, notifications saved with default "Any" settings would never trigger. The validation now treats empty/null values as "-1" (the system's "Any" sentinel), allowing these notifications to process as intended.

Bug Fix: NullReferenceException When No Previous State Exists

For UnitStatusChanged, PersonnelStaffingChanged, and PersonnelStatusChanged events, when a "before" state was required but no prior state existed (e.g., the very first state change), the code would throw a null reference exception. Null checks were added so the notification is safely skipped (returns false) instead of crashing. A missing null check on currentState was also added for PersonnelStatusChanged.

Bug Fix: UI Dropdown Posting Incorrect "Any" Value

The client-side dropdown initialization was changed to post "-1" for the "Any" option instead of an empty string, aligning with the notification engine's expected value format. The API calls for populating these dropdowns were updated to stop requesting an "Any" entry from the server (includeAny=False), since it is now provided client-side.

Test Coverage

New unit tests were added covering:

  • Notifications processing when BeforeData/CurrentData are empty (should succeed)
  • Graceful handling when no previous state exists for each event type (should return false without throwing)
  • Group resolution for PersonnelStaffingChanged and PersonnelStatusChanged events

Summary by CodeRabbit

  • New Features

    • Added moderation tools for reporting, reviewing, completing, and auditing flagged chat messages, dispatch content, and system messages.
    • Administrators can search moderation requests, review reports and evidence, and track moderation actions.
    • Added localized moderation messaging and administrator-only controls.
  • Bug Fixes

    • Improved notification matching for blank values and personnel or unit status changes.
    • Deleted or unavailable attachments now return appropriate not-found responses.
    • Improved validation for call editing and message length limits.
    • Fixed template editor initialization when its target element is unavailable.

@request-info

request-info Bot commented Aug 4, 2026

Copy link
Copy Markdown

Thanks for opening this, but we'd appreciate a little more information. Could you update it with more details?

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a moderation workflow across storage, services, APIs, controllers, and chat interfaces. It also updates notification matching, localization, validation, message metadata, database migrations, and several isolated controller and template behaviors.

Changes

Moderation platform

Layer / File(s) Summary
Moderation contracts and persistence
Core/Resgrid.Model/..., Providers/..., Repositories/...
Adds moderation entities, repositories, audit types, database tables, legacy-flag migration, evidence storage, and moderation state for chat messages.
Moderation service workflow
Core/Resgrid.Services/ModerationService.cs
Implements reporting, authorization, evidence capture, content removal, completion, notifications, evidence access, and audit actions.
Moderation API contracts and endpoints
Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs, Web/Resgrid.Web.Services/Models/v4/Moderation/*
Adds v4 endpoints and models for reporting, status lookup, request search, completion, and evidence downloads.
User-area moderation flows
Web/Resgrid.Web/Areas/User/Controllers/*, Web/Resgrid.Web/Areas/User/Views/*
Routes chat, message, call-note, and call-image reports through moderation requests and displays request status and administrator notes.
Chat moderation interface
Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/*
Adds request and report tabs, filters, evidence downloads, completion actions, audit trails, localized text, and existing-request status handling.

Supporting behavior updates

Layer / File(s) Summary
Notification matching and form values
Core/Resgrid.Services/NotificationService.cs, Web/Resgrid.Web/wwwroot/js/app/internal/notifications/resgrid.notifications.addNotification.js
Personnel staffing events now resolve groups through user-state data. Blank status values map to “Any,” and the form submits -1.
Localization and model metadata
Core/Resgrid.Localization/..., Core/Resgrid.Model/..., Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml
Adds moderation localization lookup and client exposure. Adds protobuf metadata and model constants or metadata updates.
Validation and isolated fixes
Web/Resgrid.Web.Services/Controllers/v4/*, Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs, Web/Resgrid.Web/wwwroot/js/app/internal/templates/resgrid.templates.newtemplate.js
Uses invariant identifier parsing, validates required input, normalizes expiry checks, and avoids template editor initialization when the target element is absent.
Communication test delete migrations
Providers/Resgrid.Providers.Migrations/Migrations/M0111_CascadeCommunicationTestDeletes.cs, Providers/Resgrid.Providers.MigrationsPg/Migrations/M0111_CascadeCommunicationTestDeletesPg.cs
Adds cascading-delete foreign keys with reversible rollback logic.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reporter
  participant ModerationController
  participant ModerationService
  participant ModerationRepository
  participant Moderator
  Reporter->>ModerationController: submit report
  ModerationController->>ModerationService: flag content
  ModerationService->>ModerationRepository: store request and report
  ModerationRepository-->>ModerationService: return moderation data
  ModerationService-->>Reporter: return report status
  Moderator->>ModerationController: search or complete request
  ModerationController->>ModerationService: authorize and complete
  ModerationService->>ModerationRepository: store action and status
  ModerationService-->>Moderator: return completed request
Loading

Possibly related PRs

  • Resgrid/Core#255: Both modify role or recipient identifier handling in MessagesController.
  • Resgrid/Core#300: Both update notification validation and “Any” handling.
  • Resgrid/Core#312: Both extend audit event types and audit display mappings.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the notification fixes included in the pull request, although it does not mention the additional moderation system expansion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Stylelint (17.14.0)
Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chat.css

ConfigurationError: Could not find "stylelint-config-sass-guidelines". Do you need to install the package or use the "configBasedir" option?
at getModulePath (file:///usr/local/lib/node_modules/stylelint/lib/utils/getModulePath.mjs:38:9)
at loadExtendedConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:285:21)
at extendConfig (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:252:25)
at async augmentConfigBasic (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:85:20)
at async augmentConfigFull (file:///usr/local/lib/node_modules/stylelint/lib/augmentConfig.mjs:138:24)
at async getConfigForFile (file:///usr/local/lib/node_modules/stylelint/lib/getConfigForFile.mjs:102:26)
at async resolveOptionValue (file:///usr/local/lib/node_modules/stylelint/lib/utils/resolveOptionValue.mjs:27:24)
at async standalone (file:///usr/local/lib/node_modules/stylelint/lib/standalone.mjs:127:22)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Services/NotificationService.cs`:
- Around line 507-511: Update the single-select comparisons in the notification
matching logic around the existing before/current data branches to test
normalized values for exact equality with "-1", not substring containment. Apply
this consistently to the early wildcard match and both beforeAny/currentAny
assignments, including the corresponding branches around the additional affected
locations, so values such as "-10" are not treated as the wildcard.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c388c78-29fd-42a9-9a14-01652d4b0775

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0f0e3 and ea6eef3.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Services/NotificationServiceTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (2)
  • Core/Resgrid.Services/NotificationService.cs
  • Web/Resgrid.Web/wwwroot/js/app/internal/notifications/resgrid.notifications.addNotification.js

Comment thread Core/Resgrid.Services/NotificationService.cs Outdated

if ((currentAny || currentState.State == int.Parse(setting.CurrentData)) &&
(beforeAny || beforeState.State == int.Parse(setting.BeforeData)))
if ((currentAny || currentState.State == int.Parse(currentData)) &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unsafe string conversion: int.Parse(currentData) is used without TryParse validation for user/IO input. Prefer int.TryParse and validate culture/format where applicable.

Kody rule violation: Use TryParse for string conversions

Prompt for LLM

File Core/Resgrid.Services/NotificationService.cs:

Line 521:

Unsafe string conversion: `int.Parse(currentData)` is used without `TryParse` validation for user/IO input. Prefer `int.TryParse` and validate culture/format where applicable.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (setting.BeforeData.Contains("-1") && setting.CurrentData.Contains("-1"))
// Empty Before/Current data means "Any": the post-Telerik UI posts "" for the
// default Any option, so settings saved that way must still match every change.
var beforeData = String.IsNullOrWhiteSpace(setting.BeforeData) ? "-1" : setting.BeforeData;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string literal "-1" is scattered across NotificationService.cs (lines 505–565) and resgrid.notifications.addNotification.js (lines 132–146) as a sentinel meaning "Any" without a named constant. Define a class-level constant private const string AnySelection = "-1"; and replace all inline occurrences.

Kody rule violation: Centralize string constants

Prompt for LLM

File Core/Resgrid.Services/NotificationService.cs:

Line 504:

Magic string literal `"-1"` is scattered across `NotificationService.cs` (lines 505–565) and `resgrid.notifications.addNotification.js` (lines 132–146) as a sentinel meaning "Any" without a named constant. Define a class-level constant `private const string AnySelection = "-1";` and replace all inline occurrences.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (setting.BeforeData.Contains("-1") && setting.CurrentData.Contains("-1"))
// Empty Before/Current data means "Any": the post-Telerik UI posts "" for the
// default Any option, so settings saved that way must still match every change.
var beforeData = String.IsNullOrWhiteSpace(setting.BeforeData) ? "-1" : setting.BeforeData;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Triplicated logic: the normalization-plus-state-comparison sequence is duplicated verbatim across the UnitStatusChanged, PersonnelStaffingChanged, and PersonnelStatusChanged cases. Extract a single generic helper such as ValidateStateChangeAsync<TState>(setting, Func<int, Task<TState>> getCurrent, Func<TState, Task<TState>> getBefore, Func<TState,int> stateSelector) and call it from each case.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File Core/Resgrid.Services/NotificationService.cs:

Line 504:

Triplicated logic: the normalization-plus-state-comparison sequence is duplicated verbatim across the `UnitStatusChanged`, `PersonnelStaffingChanged`, and `PersonnelStatusChanged` cases. Extract a single generic helper such as `ValidateStateChangeAsync<TState>(setting, Func<int, Task<TState>> getCurrent, Func<TState, Task<TState>> getBefore, Func<TState,int> stateSelector)` and call it from each case.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
DepartmentId = 1,
MessageId = "123456",
Data = new NotificationItem() { StateId = 3, DepartmentId = 1, PreviousStateId = 2 }.SerializeProto(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic numbers StateId=3 and PreviousStateId=2 lack self-documenting domain meaning. Use enum casts such as StateId=(int)UnitStateTypes.Responding, consistent with existing BeforeData casts in the same test.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File Tests/Resgrid.Tests/Services/NotificationServiceTests.cs:

Line 771:

Magic numbers `StateId=3` and `PreviousStateId=2` lack self-documenting domain meaning. Use enum casts such as `StateId=(int)UnitStateTypes.Responding`, consistent with existing `BeforeData` casts in the same test.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

$('#beforeStateControl').empty().append('<select id="Notification_BeforeData" name="Notification.BeforeData" style="width:100%"></select>');
$('#currentStateControl').empty().append('<select id="Notification_CurrentData" name="Notification.CurrentData" style="width:100%"></select>');
var url = resgrid.absoluteBaseUrl + '/User/CustomStatuses/GetUnitStatusesLevelsForDepartmentCombined?includeAny=True';
var url = resgrid.absoluteBaseUrl + '/User/CustomStatuses/GetUnitStatusesLevelsForDepartmentCombined?includeAny=False';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

String concatenation using + violates the team template literals rule. Replace with a template literal to improve readability and reduce error-proneness.

Kody rule violation: Use Template Literals Instead of String Concatenation

Prompt for LLM

File Web/Resgrid.Web/wwwroot/js/app/internal/notifications/resgrid.notifications.addNotification.js:

Line 132:

String concatenation using `+` violates the team template literals rule. Replace with a template literal to improve readability and reduce error-proneness.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

var $sel = $(selector).empty().append('<option value="">-- Any --</option>');
// "Any" must post "-1", not "" — the notification engine treats the value as a
// state id and an empty string used to make the setting never match.
var $sel = $(selector).empty().append('<option value="-1">-- Any --</option>');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

var declaration of $sel violates Rule [37] and risks function-scoping pitfalls. Use const since $sel is never reassigned.

Kody rule violation: Always use const and let

Prompt for LLM

File Web/Resgrid.Web/wwwroot/js/app/internal/notifications/resgrid.notifications.addNotification.js:

Line 119:

`var` declaration of `$sel` violates Rule [37] and risks function-scoping pitfalls. Use `const` since `$sel` is never reassigned.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

Resgrid-Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

public async Task<ActionResult<GetCallResult>> GetCall(string callId, [FromQuery] string departmentId = null)
{
if (String.IsNullOrWhiteSpace(callId))
if (!int.TryParse(callId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedCallId))
var result = new EditCallResult();

var canDoOperation = await _authorizationService.CanUserEditCallAsync(UserId, int.Parse(editCallInput.Id));
if (editCallInput == null || !ModelState.IsValid ||

var canDoOperation = await _authorizationService.CanUserEditCallAsync(UserId, int.Parse(editCallInput.Id));
if (editCallInput == null || !ModelState.IsValid ||
!int.TryParse(editCallInput.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int callId))
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<ModerationActionResult>> Flag([FromBody] FlagModerationInput input,
public async Task<ActionResult<ModerationActionResult>> Flag([FromBody] FlagModerationInput input,
CancellationToken cancellationToken)
{
if (!ModelState.IsValid || input == null)

/// <summary>Completes a scoped request with no action or by removing the live content.</summary>
[HttpPost("Complete")]
public async Task<ActionResult<ModerationActionResult>> Complete(string requestId,
public async Task<ActionResult<ModerationActionResult>> Complete(string requestId,
[FromBody] CompleteModerationInput input, CancellationToken cancellationToken)
{
if (!ModelState.IsValid || input == null || string.IsNullOrWhiteSpace(requestId))

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs (1)

1461-1486: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

FlagMessage does not handle the exceptions FlagAsync throws.

ModerationService.FlagAsync throws in cases this endpoint can reach:

  • InvalidOperationException when the chat message is deleted. CheckMessageChannelAccessAsync does not inspect DeletedOn, so flagging a tombstoned message reaches LoadEvidenceAsync and throws.
  • ArgumentOutOfRangeException when input.Reason is outside the ModerationReason range. FlagMessageInput.Reason is a plain int.
  • UnauthorizedAccessException from LoadEvidenceAsync.

None are caught, so each returns 500. ModerationController.Flag catches all three and maps them to 400 or 401. Mirror that handling here.

The cast (ModerationReason)input.Reason also couples two independently declared enums. The values align today. Map them explicitly so a future change to either enum fails at compile time instead of silently mislabelling a report.

🛠️ Proposed fix
 			var result = new ChatActionResult();
-			var flag = await _moderationService.FlagAsync(DepartmentId, UserId,
-				ModerationItemType.ChatMessage, messageId, (ModerationReason)input.Reason, input.Note,
-				BuildModerationContext("Reporter"), cancellationToken);
+			ModerationReport flag;
+
+			try
+			{
+				flag = await _moderationService.FlagAsync(DepartmentId, UserId,
+					ModerationItemType.ChatMessage, messageId, (ModerationReason)input.Reason, input.Note,
+					BuildModerationContext("Reporter"), cancellationToken);
+			}
+			catch (UnauthorizedAccessException)
+			{
+				return Unauthorized();
+			}
+			catch (ArgumentException ex)
+			{
+				return BadRequest(ex.Message);
+			}
+			catch (InvalidOperationException ex)
+			{
+				return BadRequest(ex.Message);
+			}
 
 			result.Success = flag != null;

ArgumentOutOfRangeException derives from ArgumentException, so the ArgumentException catch covers the invalid-reason case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs` around lines 1461
- 1486, Update FlagMessage to explicitly map input.Reason to the corresponding
ModerationReason value instead of directly casting between enums, and wrap
FlagAsync in exception handling matching ModerationController.Flag: map
ArgumentException and InvalidOperationException to BadRequest,
UnauthorizedAccessException to Unauthorized, and preserve the existing success
response for successful flags.
Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatStore.ts (1)

341-345: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Copy IsModerated for deleted thread replies.

The channel-message branch copies moderation state from HubDeletedPayload, but the thread-reply branch does not. A moderator-deleted reply remains IsModerated: false in local state. Apply the same payload.IsModerated ?? payload.DeletedByModerator value in this branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatStore.ts` around
lines 341 - 345, Update the thread-reply handling in the loop over
state.threadMessagesByRoot to include IsModerated from payload.IsModerated ??
payload.DeletedByModerator when calling upsertThreadMessage, alongside DeletedOn
and Body. Preserve the existing reply lookup and early return behavior.
Web/Resgrid.Web/Areas/User/Controllers/MessagesController.cs (1)

37-56: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the required dependency-resolution pattern.

These changes add constructor injection for new dependencies. Resolve the dependencies with Bootstrapper.GetKernel().Resolve<T>() in each constructor instead.

  • Web/Resgrid.Web/Areas/User/Controllers/MessagesController.cs#L37-L56: Resolve IModerationService and the moderation localizer through the required service locator.
  • Web/Resgrid.Web/Areas/User/Controllers/ModerationController.cs#L13-L18: Resolve IDepartmentGroupsService through the required service locator.

As per coding guidelines, use Bootstrapper.GetKernel().Resolve<T>() to resolve dependencies explicitly in constructors rather than constructor injection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/MessagesController.cs` around lines 37
- 56, Replace constructor injection with explicit service-locator resolution in
MessagesController: remove the IModerationService and moderation localizer
parameters and initialize both fields via Bootstrapper.GetKernel().Resolve<T>().
In Web/Resgrid.Web/Areas/User/Controllers/ModerationController.cs lines 13-18,
resolve IDepartmentGroupsService through Bootstrapper.GetKernel().Resolve<T>()
instead of injecting it; update each constructor accordingly.

Source: Coding guidelines

🧹 Nitpick comments (8)
Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs (1)

45-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused IChatModerationService dependency from ChatController.

_chatModerationService and its constructor parameter are no longer referenced by ChatController; removing them reduces unnecessary injected dependencies and the unused registration can be cleaned up separately if this is its only remaining direct dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs` around lines 45 -
46, Remove the unused IChatModerationService field and its constructor parameter
from ChatController, and update constructor assignments and calls accordingly
while preserving the existing IModerationService dependency.

Source: Coding guidelines

Web/Resgrid.Web.Services/Models/v4/Moderation/ModerationApiModels.cs (1)

78-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive moderation ranges from the enums.

FlagAsync and CompleteRequestAsync use Enum.IsDefined/explicit checks for ModerationItemType, ModerationReason, and the accepted ModerationDisposition values, while the API input uses literal Range constraints. If a future enum value is added, validation can block it at the controller before the service rejects it. Bind these properties to their enum types or validate with Enum.IsDefined so the enum remains the source of truth.

Note limits are not an issue here: both migration definitions use text / int.MaxValue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web.Services/Models/v4/Moderation/ModerationApiModels.cs` around
lines 78 - 101, Replace the literal Range constraints on
FlagModerationInput.ItemType, FlagModerationInput.Reason, and
CompleteModerationInput.Disposition with enum-based validation using their
corresponding moderation enums, preferably by changing the properties to those
enum types or applying Enum.IsDefined validation. Preserve the existing note and
ItemId validation.
Core/Resgrid.Services/ChatMessageService.cs (1)

316-324: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Remove the duplicate moderator flag from the deletion event payload.

DeletedByModerator now carries the same raw asModerator value as IsModerated, so moderators deleting their own messages publish both fields as true. The chat client derives the display state from IsModerated, so keep that field and remove DeletedByModerator from the event and type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/ChatMessageService.cs` around lines 316 - 324, Update
the deletion event payload in the message deletion flow around PublishEvent to
remove DeletedByModerator = asModerator while retaining message.IsModerated.
Remove the corresponding DeletedByModerator property from the event payload type
and update any affected consumers to use IsModerated only.
Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx (2)

246-246: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Confirm the destructive action before it runs.

The RemoveContent button calls complete(request, 2) on the first click. The moderation service removes the live content for that disposition, and the table offers no undo. Add a confirmation step, so a mis-click does not delete a message, a call note, or a call image.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`
at line 246, Update the RemoveContent button in ModerationRequestsTable so it
asks the user for confirmation before invoking complete(request, 2). Only call
complete after confirmation is accepted; preserve the existing isBusy disabled
state and behavior for other moderation actions.

71-75: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Index the personnel list once.

personName runs a linear people.find for every content author, every report, and every audit action. With a page of 100 requests and a large personnel roster, the render performs thousands of scans. Build a Map once and read from it.

♻️ Proposed refactor
+  const peopleById = useMemo(
+    () => new Map(people.map((item) => [item.userId, item.name])),
+    [people],
+  );
+
   const personName = useCallback((userId?: string | null) => {
     if (!userId) return moderationText('SystemOrUnknown');
-    const person = people.find((item) => item.userId === userId);
-    return person ? `${person.name} (${userId})` : userId;
-  }, [people]);
+    const name = peopleById.get(userId);
+    return name ? `${name} (${userId})` : userId;
+  }, [peopleById]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`
around lines 71 - 75, Update the personnel lookup used by personName to build a
Map keyed by userId once per people change, then read entries from that Map
instead of calling people.find for each author, report, or audit action.
Preserve the existing SystemOrUnknown fallback and display formatting for found
and missing users.
Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs (2)

69-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the no-op ternary.

Both branches return "r", and every generated statement already hard-codes the r alias. Replace the variable with the literal at line 153.

♻️ Proposed cleanup
-				var requestAlias = postgres ? "r" : "r";
 				var filters = new List<string>();

Then use the literal alias in the PostgreSQL statement:

-FROM {_sqlConfiguration.SchemaName}.moderationrequests {requestAlias}
+FROM {_sqlConfiguration.SchemaName}.moderationrequests r
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs`
at line 69, Remove the no-op requestAlias assignment in the moderation
repository and replace its usage in the PostgreSQL statement around the
generated query with the literal “r” alias. Preserve the existing SQL behavior
and remove the now-unused variable.

180-193: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the _unitOfWork null handling consistent.

Line 185 tests _unitOfWork?.Connection, which states that _unitOfWork can be null. Line 183 then reads _unitOfWork.Transaction inside the delegate, and line 192 calls _unitOfWork.CreateOrGetConnection(). If _unitOfWork were ever null, the delegate throws a NullReferenceException when it runs, not at line 185. The same mix exists at lines 244/246 and 298/300.

The constructors require the dependency, so remove the null-conditional operator, or guard the whole method.

Also applies to: 243-253, 297-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs`
around lines 180 - 193, Make null handling consistent in QueryAsync and the
corresponding methods around the later query blocks: since constructors require
_unitOfWork, remove the null-conditional checks and use _unitOfWork.Connection
directly, or consistently guard the entire method before accessing
_unitOfWork.Transaction and CreateOrGetConnection(). Apply the same correction
to all three affected query methods.
Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatFormat.ts (1)

192-192: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the formatter out of the function.

formatRelativeDay runs once per rendered message and once per moderation or export table row. Each call with a one-day-old date constructs a new Intl.RelativeTimeFormat. Create the formatter once at module scope and reuse it.

Note that the output is now lowercase in English, for example "yesterday" instead of the previous "Yesterday". Confirm that reads correctly in the cells that show it.

♻️ Proposed change
+const relativeDayFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
+
 export function formatRelativeDay(iso: string | null | undefined): string {
   if (diffDays === 1) {
-    return new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }).format(-1, 'day');
+    return relativeDayFormatter.format(-1, 'day');
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatFormat.ts` at line
192, Hoist the Intl.RelativeTimeFormat instance used by formatRelativeDay to
module scope and reuse it for each one-day-old date instead of constructing it
per call. Preserve the relative-day output, and verify the resulting lowercase
English text such as “yesterday” reads correctly in message, moderation, and
export table cells.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Core/Resgrid.Model/Repositories/IChatRepositories.cs`:
- Around line 147-148: Update the delete flow that calls TombstoneAsync so it
derives one effective moderator/sender flag and reuses it for the TombstoneAsync
isModerated argument, message.IsModerated, and delete-event audit type;
alternatively block moderator self-deletion consistently. Ensure persisted
tombstone state and audit history reflect the same actor classification.

In `@Core/Resgrid.Services/ChatMessageService.cs`:
- Around line 303-313: Make the edit-history type and IsModerated assignment in
DeleteMessageAsync use the same moderator-deletion condition, including the
isSender case consistently. Derive both from one shared condition so
moderator-authored deletions cannot produce SenderDelete while setting
IsModerated to true.

In `@Core/Resgrid.Services/ModerationService.cs`:
- Around line 519-542: The HydrateAsync loop performs per-request report and
action queries, causing excessive sequential database round trips. Add batch
repository lookup methods accepting the collected ModerationRequestId values,
call each once, group the returned reports and actions by request ID in memory,
and use those groups while preserving ApplyGroupScope and null-scope behavior.
- Around line 338-343: Update LoadEvidenceAsync to retain and load every
attachment returned by GetMetadataByMessageIdsAsync instead of selecting only
FirstOrDefault, and pass the complete attachment collection through the evidence
model so RemoveLiveContentAsync preserves all attachments in the audit trail. If
the surrounding API cannot support multiple attachments, document the enforced
single-attachment limitation in LoadEvidenceAsync instead.
- Around line 272-282: Make CompleteRequestAsync persist content removal and the
moderation request status transition atomically, so RemoveLiveContentAsync
cannot leave destructive changes committed when
_moderationRequestRepository.UpdateAsync fails. Use the existing
transaction/unit-of-work mechanism around both operations; preserve the current
failure behavior and only finalize the transaction after the removal and request
update succeed.
- Around line 110-124: Update both insert exception handlers in the moderation
request flow, including the blocks around InsertAsync and the reporter lookup,
to catch OperationCanceledException separately and rethrow it so cancellation
propagates. Restrict the general handler to non-cancellation exceptions, log
each swallowed insert failure with Resgrid.Framework.Logging.LogException, then
retain the existing concurrent-row recovery and rethrow behavior. Add the
required Resgrid.Framework import.
- Around line 654-689: Update NotifyReportersAsync to verify the result returned
by SaveMessageAsync before passing it to SendMessageAsync; skip sending when the
saved message is null so notification failure cannot throw. Move the
per-recipient profile lookup, message persistence, and send/enqueue work out of
CompleteRequestAsync’s synchronous completion path by dispatching the
notification fan-out through the existing background queue mechanism, while
preserving recipient filtering and cancellation behavior.

In `@Providers/Resgrid.Providers.Migrations/Migrations/M0112_AddModeration.cs`:
- Around line 177-179: Replace manual metadata JSON string concatenation in the
migration’s notification metadata construction, including the logic around
n.Source, n.Latitude, n.Longitude, and the lines handling names, with SQL
Server’s JSON API such as FOR JSON or JSON_OBJECT. Ensure all values are
serialized with valid JSON escaping and numeric formatting, while preserving the
existing metadata fields and fallback values.
- Around line 118-127: Update M0112_AddModeration.cs at
Providers/Resgrid.Providers.Migrations/Migrations/M0112_AddModeration.cs:118-127
and M0112_AddModerationPg.cs at
Providers/Resgrid.Providers.MigrationsPg/Migrations/M0112_AddModerationPg.cs:118-127
by guarding each foreign-key creation with a constraint-existence check, and
invoke ImportLegacyFlags() only when ModerationRequests/moderationrequests
contains no rows. Apply the equivalent checks and condition in both migration
implementations.

In `@Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs`:
- Around line 41-45: Update the SQL built by GetByItemAsync to replace SELECT *
with the same explicit non-blob moderation request column list used by
SearchAsync and ModerationActionRepository.GetByRequestAsync. Exclude
OriginalContent and any other evidence blob columns while preserving the
existing PostgreSQL and non-PostgreSQL table and filter syntax.

In `@Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs`:
- Around line 190-206: Update DownloadEvidence to accept a CancellationToken and
pass it through to the moderation service calls, matching Flag and Complete.
Handle UnauthorizedAccessException from RecordEvidenceAccessAsync by returning
Unauthorized instead of allowing a 500, while preserving the existing
no-disclosure behavior. Also revise the method summary to describe general
evidence or retained content rather than only image evidence.

In `@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatPageElement.tsx`:
- Around line 59-65: Update openFlag to ignore stale getMyModerationRequest
responses when the selected ChatMessageId changes, using a request token or
matching the response to the current target before calling setFlagStatus. Apply
the same guard to success and failure handlers so an earlier lookup cannot alter
the second message’s dialog state.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`:
- Line 208: Update the conditional rendering for request.CallId and
report.ReporterGroupId to explicitly check that each value is neither null nor
undefined, preventing a numeric zero from rendering as text while preserving
rendering for valid zero and nonzero identifiers.
- Around line 77-86: Update ModerationRequestsTable’s pagination flow so
moderators can access results beyond the hard-coded first 100 records: add page
state and previous/next controls that update search.page, while keeping pageSize
within the repository’s 200-row cap. Reuse the existing ActionsTab pagination
behavior and ensure controls reflect whether another page is available based on
the returned result count.
- Line 270: Replace the dynamic moderationText key construction for
action.ActorRole in ModerationRequestsTable with an explicit
role-to-localization-key map and a defined fallback for unmapped roles,
following the existing ITEM_LABEL_KEYS and ACTION_LABEL_KEYS pattern. Preserve
the UnknownRole behavior when ActorRole is absent and ensure persisted values
such as LegacyImport do not render as raw localization keys.

In `@Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs`:
- Line 74: Remove the IModerationService parameter from the DispatchController
constructor and initialize the existing _moderationService field by resolving
IModerationService through
Bootstrapper.GetKernel().Resolve<IModerationService>() inside the constructor.
Update all affected constructor call sites while preserving the controller’s
existing behavior.
- Around line 1792-1793: Update the call-note handling in DispatchController to
load the current reporter’s moderation requests once before the note loop,
instead of awaiting GetReporterRequestAsync for each note. Build a set of
flagged CallNote item IDs from that result, then assign note.IsFlagged by
checking callNote.CallNoteId against the set while preserving the existing
department, user, and moderation item-type filters.

---

Outside diff comments:
In `@Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs`:
- Around line 1461-1486: Update FlagMessage to explicitly map input.Reason to
the corresponding ModerationReason value instead of directly casting between
enums, and wrap FlagAsync in exception handling matching
ModerationController.Flag: map ArgumentException and InvalidOperationException
to BadRequest, UnauthorizedAccessException to Unauthorized, and preserve the
existing success response for successful flags.

In `@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatStore.ts`:
- Around line 341-345: Update the thread-reply handling in the loop over
state.threadMessagesByRoot to include IsModerated from payload.IsModerated ??
payload.DeletedByModerator when calling upsertThreadMessage, alongside DeletedOn
and Body. Preserve the existing reply lookup and early return behavior.

In `@Web/Resgrid.Web/Areas/User/Controllers/MessagesController.cs`:
- Around line 37-56: Replace constructor injection with explicit service-locator
resolution in MessagesController: remove the IModerationService and moderation
localizer parameters and initialize both fields via
Bootstrapper.GetKernel().Resolve<T>(). In
Web/Resgrid.Web/Areas/User/Controllers/ModerationController.cs lines 13-18,
resolve IDepartmentGroupsService through Bootstrapper.GetKernel().Resolve<T>()
instead of injecting it; update each constructor accordingly.

---

Nitpick comments:
In `@Core/Resgrid.Services/ChatMessageService.cs`:
- Around line 316-324: Update the deletion event payload in the message deletion
flow around PublishEvent to remove DeletedByModerator = asModerator while
retaining message.IsModerated. Remove the corresponding DeletedByModerator
property from the event payload type and update any affected consumers to use
IsModerated only.

In `@Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs`:
- Line 69: Remove the no-op requestAlias assignment in the moderation repository
and replace its usage in the PostgreSQL statement around the generated query
with the literal “r” alias. Preserve the existing SQL behavior and remove the
now-unused variable.
- Around line 180-193: Make null handling consistent in QueryAsync and the
corresponding methods around the later query blocks: since constructors require
_unitOfWork, remove the null-conditional checks and use _unitOfWork.Connection
directly, or consistently guard the entire method before accessing
_unitOfWork.Transaction and CreateOrGetConnection(). Apply the same correction
to all three affected query methods.

In `@Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs`:
- Around line 45-46: Remove the unused IChatModerationService field and its
constructor parameter from ChatController, and update constructor assignments
and calls accordingly while preserving the existing IModerationService
dependency.

In `@Web/Resgrid.Web.Services/Models/v4/Moderation/ModerationApiModels.cs`:
- Around line 78-101: Replace the literal Range constraints on
FlagModerationInput.ItemType, FlagModerationInput.Reason, and
CompleteModerationInput.Disposition with enum-based validation using their
corresponding moderation enums, preferably by changing the properties to those
enum types or applying Enum.IsDefined validation. Preserve the existing note and
ItemId validation.

In `@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatFormat.ts`:
- Line 192: Hoist the Intl.RelativeTimeFormat instance used by formatRelativeDay
to module scope and reuse it for each one-day-old date instead of constructing
it per call. Preserve the relative-day output, and verify the resulting
lowercase English text such as “yesterday” reads correctly in message,
moderation, and export table cells.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`:
- Line 246: Update the RemoveContent button in ModerationRequestsTable so it
asks the user for confirmation before invoking complete(request, 2). Only call
complete after confirmation is accepted; preserve the existing isBusy disabled
state and behavior for other moderation actions.
- Around line 71-75: Update the personnel lookup used by personName to build a
Map keyed by userId once per people change, then read entries from that Map
instead of calling people.find for each author, report, or audit action.
Preserve the existing SystemOrUnknown fallback and display formatting for found
and missing users.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f4c2e9b-e2eb-408b-8004-393cdc24a872

📥 Commits

Reviewing files that changed from the base of the PR and between ea6eef3 and 4bfbfd3.

⛔ Files ignored due to path filters (36)
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.ar.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.de.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.en.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.es.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.fr.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.it.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.pl.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.sv.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.uk.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.ar.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.de.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.en.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.es.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.fr.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.it.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.pl.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.sv.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Common.uk.resx is excluded by !**/*.resx
  • Tests/Resgrid.Tests/Chatbot/ChatbotDeptConfigAndSessionTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Localization/ModerationLocalizationTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Models/FormAutomationTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/MessageServiceInboxTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/ModerationServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/NotificationServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Web/Services/CallsControllerTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Web/Services/MessagesControllerTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Web/User/SubscriptionControllerTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (69)
  • Core/Resgrid.Localization/Areas/User/Moderation/Moderation.cs
  • Core/Resgrid.Model/AuditLogTypes.cs
  • Core/Resgrid.Model/Chat/ChatMessage.cs
  • Core/Resgrid.Model/ChatbotDepartmentConfig.cs
  • Core/Resgrid.Model/FormAutomation.cs
  • Core/Resgrid.Model/Message.cs
  • Core/Resgrid.Model/Moderation/Moderation.cs
  • Core/Resgrid.Model/Repositories/IChatRepositories.cs
  • Core/Resgrid.Model/Repositories/IModerationRepositories.cs
  • Core/Resgrid.Model/Services/IModerationService.cs
  • Core/Resgrid.Services/AuditService.cs
  • Core/Resgrid.Services/ChatMessageService.cs
  • Core/Resgrid.Services/MessageService.cs
  • Core/Resgrid.Services/ModerationService.cs
  • Core/Resgrid.Services/NotificationService.cs
  • Core/Resgrid.Services/Resgrid.Services.csproj
  • Core/Resgrid.Services/ServicesModule.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0111_CascadeCommunicationTestDeletes.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0112_AddModeration.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0111_CascadeCommunicationTestDeletesPg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0112_AddModerationPg.cs
  • Repositories/Resgrid.Repositories.DataRepository/ChatRepositories.cs
  • Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
  • Web/Resgrid.Web.Services/Controllers/v4/CallsController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/MessagesController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs
  • Web/Resgrid.Web.Services/Models/v4/Calls/EditCallInput.cs
  • Web/Resgrid.Web.Services/Models/v4/Chat/ChatApiModels.cs
  • Web/Resgrid.Web.Services/Models/v4/Moderation/ModerationApiModels.cs
  • Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatModerationElement.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatPageElement.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/FlagDialog.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/atoms/MessageBubble.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chat.css
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatFormat.ts
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/chatStore.ts
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ActionsTab.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ExportsTab.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/FlagsTab.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ReportsTab.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/SettingsTab.tsx
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderationApi.ts
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderationI18n.ts
  • Web/Resgrid.Web/Areas/User/Apps/src/components/chat/types.ts
  • Web/Resgrid.Web/Areas/User/Apps/src/elements.ts
  • Web/Resgrid.Web/Areas/User/Controllers/ChatController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/MessagesController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/ModerationController.cs
  • Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs
  • Web/Resgrid.Web/Areas/User/Models/Dispatch/FlagCallImageView.cs
  • Web/Resgrid.Web/Areas/User/Models/Dispatch/FlagCallNoteView.cs
  • Web/Resgrid.Web/Areas/User/Models/Messages/ViewMessageView.cs
  • Web/Resgrid.Web/Areas/User/Views/Chat/Moderation.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Dispatch/FlagCallImage.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Dispatch/FlagCallNote.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Messages/ViewMessage.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Moderation/Index.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Shared/_Navigation.cshtml
  • Web/Resgrid.Web/Areas/User/Views/Shared/_UserLayout.cshtml
  • Web/Resgrid.Web/wwwroot/js/app/internal/templates/resgrid.templates.newtemplate.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • Core/Resgrid.Services/NotificationService.cs

Comment on lines +147 to +148
/// <summary>Targeted tombstone (body/metadata cleared, deletion and moderation state stamped) guarded by DeletedOn IS NULL.</summary>
Task<bool> TombstoneAsync(string chatMessageId, DateTime deletedOn, string deletedByUserId, bool isModerated, CancellationToken cancellationToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 '\bDeleteMessageAsync\s*\(|\bTombstoneAsync\s*\(' --glob '*.cs'

Repository: Resgrid/Core

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git ls-files matching relevant names =="
git ls-files | rg '(IChatRepositories\.cs|ChatMessageService\.cs|ChatMessage\.cs|ChatMessageRepository.*\.cs)' || true

echo "== all TombstoneAsync/DeleteMessageAsync occurrences across repo =="
rg -n -C 6 '\b(TombstoneAsync|DeleteMessageAsync)\s*\(' --glob '*.cs' . || true

echo "== file sizes =="
while IFS= read -r f; do
  [ -n "$f" ] && wc -l "$f"
done < <(git ls-files | rg '(IChatRepositories\.cs|ChatMessageService\.cs|ChatMessage\.cs|ChatMessageRepository.*\.cs)' || true)

Repository: Resgrid/Core

Length of output: 12019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ChatMessage model =="
sed -n '1,220p' Core/Resgrid.Model/Chat/ChatMessage.cs

echo "== DeleteMessageAsync implementation =="
sed -n '260,325p' Core/Resgrid.Services/ChatMessageService.cs

echo "== ModeratorDeleteMessageAsync implementation =="
sed -n '90,130p' Core/Resgrid.Services/ChatModerationService.cs

echo "== repository tombstone implementation =="
sed -n '1475,1535p' Repositories/Resgrid.Repositories.DataRepository/ChatRepositories.cs

echo "== ChatController DeleteMessage context =="
sed -n '840,885p' Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs

Repository: Resgrid/Core

Length of output: 14773


Keep the tombstone actor state consistent with audit history.

ChatMessage.IsModerated means a moderator applied the tombstone, but DeleteMessageAsync records one audit type from asModerator && !isSender and a different persisted state from raw asModerator. Use the same effective sender/moderator flag for TombstoneAsync(), message.IsModerated, and delete-events, or prevent this code path from running when a moderator deletes their own message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Model/Repositories/IChatRepositories.cs` around lines 147 - 148,
Update the delete flow that calls TombstoneAsync so it derives one effective
moderator/sender flag and reuses it for the TombstoneAsync isModerated argument,
message.IsModerated, and delete-event audit type; alternatively block moderator
self-deletion consistently. Ensure persisted tombstone state and audit history
reflect the same actor classification.

Comment on lines 303 to +313
await SaveEditHistoryAsync(message, asModerator && !isSender ? ChatMessageEditType.ModeratorDelete : ChatMessageEditType.SenderDelete, byUserId, cancellationToken);

var deletedOn = DateTime.UtcNow;
if (!await _chatMessageRepository.TombstoneAsync(chatMessageId, deletedOn, byUserId, cancellationToken))
if (!await _chatMessageRepository.TombstoneAsync(chatMessageId, deletedOn, byUserId, asModerator, cancellationToken))
return false;

message.Body = null;
message.MetadataJson = null;
message.DeletedOn = deletedOn;
message.DeletedByUserId = byUserId;
message.IsModerated = asModerator;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

IsModerated and the edit-history type disagree for a moderator's own message.

Line 303 records ChatMessageEditType.ModeratorDelete only when asModerator && !isSender. Line 313 sets IsModerated = asModerator with no isSender term.

ModerationService.RemoveLiveContentAsync always calls DeleteMessageAsync(..., asModerator: true, ...). If the moderator authored the reported message, isSender is true. The edit history then records SenderDelete while the message is stored with IsModerated = true. A report that counts moderator deletions from edit history will not match the IsModerated flag.

Pick one derivation and use it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/ChatMessageService.cs` around lines 303 - 313, Make the
edit-history type and IsModerated assignment in DeleteMessageAsync use the same
moderator-deletion condition, including the isSender case consistently. Derive
both from one shared condition so moderator-authored deletions cannot produce
SenderDelete while setting IsModerated to true.

Comment on lines +110 to +124
try
{
request = await _moderationRequestRepository.InsertAsync(request, cancellationToken);
}
catch
{
// The unique department/type/item index is the race backstop. If another reporter won
// the insert, join that request; otherwise preserve the original failure.
var concurrent = await _moderationRequestRepository.GetByItemAsync(departmentId, (int)itemType, itemId);
if (concurrent == null)
throw;

request = concurrent;
createdRequest = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Log the swallowed exception and do not catch cancellation.

Both catch blocks discard the original exception without logging it. If GetByItemAsync or GetByRequestAndReporterAsync then returns a row, the real insert failure is lost, so a schema error, a timeout, or a permission error looks like a normal concurrent-insert race. The coding guidelines require Resgrid.Framework.Logging.LogException when catching exceptions.

A bare catch also catches OperationCanceledException from cancellationToken. A cancelled request then follows the race-recovery path instead of propagating cancellation.

Restrict the catch and log the swallowed failure.

🛠️ Proposed fix for both catch blocks
 				try
 				{
 					request = await _moderationRequestRepository.InsertAsync(request, cancellationToken);
 				}
-				catch
+				catch (Exception ex) when (ex is not OperationCanceledException)
 				{
 					// The unique department/type/item index is the race backstop. If another reporter won
 					// the insert, join that request; otherwise preserve the original failure.
 					var concurrent = await _moderationRequestRepository.GetByItemAsync(departmentId, (int)itemType, itemId);
 					if (concurrent == null)
 						throw;
 
+					Logging.LogException(ex, "Moderation request insert lost the unique-index race; joining the existing request.");
 					request = concurrent;
 					createdRequest = false;
 				}
 			try
 			{
 				report = await _moderationReportRepository.InsertAsync(report, cancellationToken);
 			}
-			catch
+			catch (Exception ex) when (ex is not OperationCanceledException)
 			{
 				// A unique request/reporter index prevents duplicate reports under concurrent submissions.
 				var concurrent = await _moderationReportRepository.GetByRequestAndReporterAsync(
 					request.ModerationRequestId, reportedByUserId);
 				if (concurrent == null)
 					throw;
 
+				Logging.LogException(ex, "Moderation report insert lost the unique-index race; returning the existing report.");
 				return concurrent;
 			}

Add using Resgrid.Framework; for the Logging static class.

Also applies to: 163-176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/ModerationService.cs` around lines 110 - 124, Update
both insert exception handlers in the moderation request flow, including the
blocks around InsertAsync and the reporter lookup, to catch
OperationCanceledException separately and rethrow it so cancellation propagates.
Restrict the general handler to non-cancellation exceptions, log each swallowed
insert failure with Resgrid.Framework.Logging.LogException, then retain the
existing concurrent-row recovery and rethrow behavior. Add the required
Resgrid.Framework import.

Source: Coding guidelines

Comment on lines +272 to +282
if (disposition == ModerationDisposition.ContentRemoved && !await RemoveLiveContentAsync(request, completedByUserId, cancellationToken))
throw new InvalidOperationException(ModerationResources.GetCurrent("ContentCouldNotBeRemoved"));

var previousStatus = request.Status;
request.Status = (int)ModerationRequestStatus.Completed;
request.Disposition = (int)disposition;
request.CompletedByUserId = completedByUserId;
request.CompletedOn = DateTime.UtcNow;
request.ModifiedOn = request.CompletedOn.Value;
request.AdminNote = adminNote;
request = await _moderationRequestRepository.UpdateAsync(request, cancellationToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Content removal is destructive and runs before the status update.

RemoveLiveContentAsync performs irreversible writes. For CallImage it sets attachment.Data = null and attachment.Size = 0. For Message it overwrites Subject and Body. For ChatMessage it tombstones the message.

The request status update on Line 282 is a separate write. If UpdateAsync fails, the live content is already destroyed while the request stays Pending. A moderator can then retry CompleteRequestAsync with NoAction, and the request records NoAction for content that no longer exists.

Wrap the removal and the status update in one unit of work, or record the removal before mutating live content so a failed status update is recoverable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/ModerationService.cs` around lines 272 - 282, Make
CompleteRequestAsync persist content removal and the moderation request status
transition atomically, so RemoveLiveContentAsync cannot leave destructive
changes committed when _moderationRequestRepository.UpdateAsync fails. Use the
existing transaction/unit-of-work mechanism around both operations; preserve the
current failure behavior and only finalize the transaction after the removal and
request update succeed.

Comment on lines +338 to +343
ChatAttachment attachment = null;
var attachmentMetadata = await _chatAttachmentRepository.GetMetadataByMessageIdsAsync(new[] { itemId });
var firstAttachment = attachmentMetadata?.FirstOrDefault();
if (firstAttachment != null)
attachment = await _chatAttachmentRepository.GetByIdAsync(firstAttachment.ChatAttachmentId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Only the first attachment is captured as evidence.

LoadEvidenceAsync reads the attachment metadata list for the chat message and keeps FirstOrDefault(). If the reported message carries more than one attachment, the remaining attachments are never captured. RemoveLiveContentAsync still tombstones the whole message, so the uncaptured attachments are lost from the audit trail.

Capture every attachment, or document the single-attachment limit in the method comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Core/Resgrid.Services/ModerationService.cs` around lines 338 - 343, Update
LoadEvidenceAsync to retain and load every attachment returned by
GetMetadataByMessageIdsAsync instead of selecting only FirstOrDefault, and pass
the complete attachment collection through the evidence model so
RemoveLiveContentAsync preserves all attachments in the audit trail. If the
surrounding API cannot support multiple attachments, document the enforced
single-attachment limitation in LoadEvidenceAsync instead.

Comment on lines +77 to +86
const search = useMemo<ModerationSearch>(() => ({
status: status < 0 ? undefined : status,
itemType: itemType < 0 ? undefined : itemType,
contentAuthorUserId: contentAuthorUserId.trim() || undefined,
reportedByUserId: reportedByUserId.trim() || undefined,
from: from ? new Date(`${from}T00:00:00`).toISOString() : undefined,
to: to ? new Date(`${to}T23:59:59.999`).toISOString() : undefined,
page: 1,
pageSize: 100,
}), [contentAuthorUserId, from, itemType, reportedByUserId, status, to]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add pagination, or show that results are truncated.

search hard-codes page: 1 and pageSize: 100, and the interface has no control that changes either value. The repository caps a page at 200 rows and applies LIMIT/OFFSET. A department with more than 100 matching requests therefore has moderation items that no moderator can reach through this table. ActionsTab already implements previous and next controls for the same reason.

Add page controls, or display a message when the returned count reaches the page size.

Also applies to: 172-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`
around lines 77 - 86, Update ModerationRequestsTable’s pagination flow so
moderators can access results beyond the hard-coded first 100 records: add page
state and previous/next controls that update search.page, while keeping pageSize
within the repository’s 200-row cap. Reuse the existing ActionsTab pagination
behavior and ensure controls reflect whether another page is available based on
the returned result count.

<td>
<strong>{moderationText(ITEM_LABEL_KEYS[request.ItemType] ?? 'UnknownContentType')}</strong>
<div className="rgchat-convo__sub">{moderationText('IdFormat', request.ItemId)}</div>
{request.CallId && <div className="rgchat-convo__sub">{moderationText('CallFormat', request.CallId)}</div>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against the falsy zero render.

{request.CallId && ...} and {report.ReporterGroupId && ...} render the literal 0 when the value is 0, because React renders the number 0 as a text node. Compare against null and undefined instead.

🐛 Proposed fix
-                    {request.CallId && <div className="rgchat-convo__sub">{moderationText('CallFormat', request.CallId)}</div>}
+                    {request.CallId != null && <div className="rgchat-convo__sub">{moderationText('CallFormat', request.CallId)}</div>}
-                        {report.ReporterGroupId && <span className="rgchat-convo__sub"> · {moderationText('GroupFormat', report.ReporterGroupId)}</span>}
+                        {report.ReporterGroupId != null && <span className="rgchat-convo__sub"> · {moderationText('GroupFormat', report.ReporterGroupId)}</span>}

Also applies to: 228-228

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`
at line 208, Update the conditional rendering for request.CallId and
report.ReporterGroupId to explicitly check that each value is neither null nor
undefined, preventing a numeric zero from rendering as text while preserving
rendering for valid zero and nonzero identifiers.

<strong>{moderationText(ACTION_LABEL_KEYS[action.ActionType] ?? 'Action')}</strong> {moderationText('By')} {personName(action.PerformedByUserId)}
<div>{action.Note || moderationText('NoNote')}</div>
<div className="rgchat-convo__sub">
{formatTimestamp(action.PerformedOn)} · {action.ActorRole ? moderationText(`Actor${action.ActorRole}`) : moderationText('UnknownRole')} · {action.IpAddress || moderationText('NoIp')} · {moderationText('TraceFormat', action.TraceId || moderationText('NotAvailable'))}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not build a localization key from stored server data.

moderationText(Actor${action.ActorRole}) derives the key from the persisted ActorRole string. moderationText returns the key itself when the entry is missing, so an unmapped role renders a raw identifier. The SQL Server and PostgreSQL migrations both write ActorRole = 'LegacyImport' for every imported action, which produces the key ActorLegacyImport.

Use an explicit map with a fallback, like ITEM_LABEL_KEYS and ACTION_LABEL_KEYS do for the numeric enums.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx`
at line 270, Replace the dynamic moderationText key construction for
action.ActorRole in ModerationRequestsTable with an explicit
role-to-localization-key map and a defined fallback for unmapped roles,
following the existing ITEM_LABEL_KEYS and ACTION_LABEL_KEYS pattern. Preserve
the UnknownRole behavior when ActorRole is absent and ensure persisted values
such as LegacyImport do not render as raw localization keys.

private readonly ICheckInTimerService _checkInTimerService;
private readonly IWeatherAlertService _weatherAlertService;
private readonly ICallDispatchStatusService _callDispatchStatusService;
private readonly IModerationService _moderationService;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve IModerationService through the required service locator.

Line 87 adds constructor injection for IModerationService. Remove this parameter. Resolve the service in the constructor with Bootstrapper.GetKernel().Resolve<IModerationService>().

As per coding guidelines, use Service Locator pattern via Bootstrapper.GetKernel().Resolve<T>() to resolve dependencies explicitly in constructors, rather than constructor injection.

Also applies to: 87-87, 118-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs` at line 74,
Remove the IModerationService parameter from the DispatchController constructor
and initialize the existing _moderationService field by resolving
IModerationService through
Bootstrapper.GetKernel().Resolve<IModerationService>() inside the constructor.
Update all affected constructor call sites while preserving the controller’s
existing behavior.

Source: Coding guidelines

Comment on lines +1792 to +1793
note.IsFlagged = await _moderationService.GetReporterRequestAsync(DepartmentId, UserId,
ModerationItemType.CallNote, callNote.CallNoteId.ToString(CultureInfo.InvariantCulture)) != null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid one moderation lookup per call note.

The loop awaits GetReporterRequestAsync once for each call note. A call with N notes now causes N sequential moderation lookups. Load the current reporter’s requests in one service call before the loop. Build an item-ID set for IsFlagged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs` around lines
1792 - 1793, Update the call-note handling in DispatchController to load the
current reporter’s moderation requests once before the note loop, instead of
awaiting GetReporterRequestAsync for each note. Build a set of flagged CallNote
item IDs from that result, then assign note.IsFlagged by checking
callNote.CallNoteId against the set while preserving the existing department,
user, and moderation item-type filters.


return resourceSet.Cast<DictionaryEntry>()
.Where(x => x.Key is string && x.Value is string)
.ToDictionary(x => (string)x.Key, x => (string)x.Value!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Unsafe type casting violates team rule. Use the as operator or pattern matching for safe casts and guard null results before usage.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File Core/Resgrid.Localization/Areas/User/Moderation/Moderation.cs:

Line 49:

Unsafe type casting violates team rule. Use the `as` operator or pattern matching for safe casts and guard null results before usage.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
var cultureInfo = GetSupportedCulture(culture);
var value = ResourceManager.GetString(key, cultureInfo)
?? ResourceManager.GetString(key, CultureInfo.GetCultureInfo("en"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string "en" for the default culture is repeated across multiple locations, risking inconsistency during changes. Define a private constant like private const string DefaultCulture = "en"; in ModerationResources and reference it.

Kody rule violation: Centralize string constants

Prompt for LLM

File Core/Resgrid.Localization/Areas/User/Moderation/Moderation.cs:

Line 28:

Magic string `"en"` for the default culture is repeated across multiple locations, risking inconsistency during changes. Define a private constant like `private const string DefaultCulture = "en";` in `ModerationResources` and reference it.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}
}

private async Task<List<ModerationRequest>> HydrateAsync(IEnumerable<ModerationRequest> requests,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance high

Query amplification in HydrateAsync issues two sequential database round-trips (GetByRequestAsync for reports and actions) per request, causing up to 400 queries on a 200-item page. Batch-load reports and actions using WHERE ModerationRequestId IN (...) and group them in memory.

var requestIds = result.Select(x => x.ModerationRequestId).ToList();
var allReports = await _moderationReportRepository.GetByRequestIdsAsync(requestIds);
var allActions = await _moderationActionRepository.GetByRequestIdsAsync(requestIds);
var reportsByRequest = allReports.ToLookup(x => x.ModerationRequestId);
var actionsByRequest = allActions.ToLookup(x => x.ModerationRequestId);

foreach (var request in result)
{
	var reports = reportsByRequest[request.ModerationRequestId].ToList();
	var actions = actionsByRequest[request.ModerationRequestId].ToList();

	if (visibleGroupIds == null)
	{
		request.Reports = reports;
		request.Actions = actions;
	}
	else
	{
		ApplyGroupScope(request, reports, actions, visibleGroupIds, viewerUserId);
	}
}
Prompt for LLM

File Core/Resgrid.Services/ModerationService.cs:

Line 519:

Query amplification in `HydrateAsync` issues two sequential database round-trips (`GetByRequestAsync` for reports and actions) per request, causing up to 400 queries on a 200-item page. Batch-load reports and actions using `WHERE ModerationRequestId IN (...)` and group them in memory.

Suggested Code:

var requestIds = result.Select(x => x.ModerationRequestId).ToList();
var allReports = await _moderationReportRepository.GetByRequestIdsAsync(requestIds);
var allActions = await _moderationActionRepository.GetByRequestIdsAsync(requestIds);
var reportsByRequest = allReports.ToLookup(x => x.ModerationRequestId);
var actionsByRequest = allActions.ToLookup(x => x.ModerationRequestId);

foreach (var request in result)
{
	var reports = reportsByRequest[request.ModerationRequestId].ToList();
	var actions = actionsByRequest[request.ModerationRequestId].ToList();

	if (visibleGroupIds == null)
	{
		request.Reports = reports;
		request.Actions = actions;
	}
	else
	{
		ApplyGroupScope(request, reports, actions, visibleGroupIds, viewerUserId);
	}
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}
}

private async Task<List<ModerationRequest>> HydrateAsync(IEnumerable<ModerationRequest> requests,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance high

O(N) query amplification in HydrateAsync issues two database round-trips (GetByRequestAsync for reports and actions) per ModerationRequest, causing up to 400 sequential queries on a 200-item page. Replace the loop with two batch queries keyed on request IDs (WHERE ModerationRequestId IN @ids) and group the results client-side.

// Batch-load all reports and actions for the page in two queries, then group client-side:
// var ids = result.Select(r => r.ModerationRequestId).ToList();
// var allReports = (await _moderationReportRepository.GetByRequestIdsAsync(ids))
//     .GroupBy(r => r.ModerationRequestId).ToDictionary(g => g.Key, g => g.ToList());
// var allActions = (await _moderationActionRepository.GetByRequestIdsAsync(ids))
//     .GroupBy(a => a.ModerationRequestId).ToDictionary(g => g.Key, g => g.ToList());
// then iterate result and pull reports/actions from the dictionaries (falling back to empty lists).
Prompt for LLM

File Core/Resgrid.Services/ModerationService.cs:

Line 519:

O(N) query amplification in `HydrateAsync` issues two database round-trips (`GetByRequestAsync` for reports and actions) per `ModerationRequest`, causing up to 400 sequential queries on a 200-item page. Replace the loop with two batch queries keyed on request IDs (`WHERE ModerationRequestId IN @ids`) and group the results client-side.

Suggested Code:

// Batch-load all reports and actions for the page in two queries, then group client-side:
// var ids = result.Select(r => r.ModerationRequestId).ToList();
// var allReports = (await _moderationReportRepository.GetByRequestIdsAsync(ids))
//     .GroupBy(r => r.ModerationRequestId).ToDictionary(g => g.Key, g => g.ToList());
// var allActions = (await _moderationActionRepository.GetByRequestIdsAsync(ids))
//     .GroupBy(a => a.ModerationRequestId).ToDictionary(g => g.Key, g => g.ToList());
// then iterate result and pull reports/actions from the dictionaries (falling back to empty lists).

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

request.CompletedOn = null;
request.AdminNote = null;
request.ModifiedOn = DateTime.UtcNow;
await _moderationRequestRepository.UpdateAsync(request, cancellationToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Database inconsistency risk arises when the request-reopen block performs three separate writes (UpdateAsync, InsertAsync, SaveAuditLogAsync) without a wrapping transaction. Wrap the entire sequence in a transaction or unit-of-work to ensure atomic commits.

Kody rule violation: Handle transaction rollbacks properly

Prompt for LLM

File Core/Resgrid.Services/ModerationService.cs:

Line 141:

Database inconsistency risk arises when the request-reopen block performs three separate writes (`UpdateAsync`, `InsertAsync`, `SaveAuditLogAsync`) without a wrapping transaction. Wrap the entire sequence in a transaction or unit-of-work to ensure atomic commits.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

.WithColumn("ContentAuthorUserId").AsString(450).Nullable()
.WithColumn("ContentAuthorUnitId").AsInt32().Nullable()
.WithColumn("ContentCreatedOn").AsDateTime2().Nullable()
.WithColumn("OriginalSubject").AsString(int.MaxValue).Nullable()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Inefficient row storage occurs because OriginalSubject uses NVARCHAR(MAX), preventing SQL Server optimization for bounded subjects. Use a bounded length like AsString(512).

Kody rule violation: Optimize string column types

Prompt for LLM

File Providers/Resgrid.Providers.Migrations/Migrations/M0112_AddModeration.cs:

Line 28:

Inefficient row storage occurs because `OriginalSubject` uses `NVARCHAR(MAX)`, preventing SQL Server optimization for bounded subjects. Use a bounded length like `AsString(512)`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +16 to +19
Create.ForeignKey("fk_communicationtestruns_communicationtests")
.FromTable("communicationtestruns").ForeignColumn("communicationtestid")
.ToTable("communicationtests").PrimaryColumn("communicationtestid")
.OnDelete(Rule.Cascade);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules critical

Locking and downtime risk occurs when adding a new FK constraint, as it takes ACCESS EXCLUSIVE locks while validating existing rows. Use the PostgreSQL online pattern by adding the constraint NOT VALID first, then executing VALIDATE CONSTRAINT in a later transaction.

Kody rule violation: Block risky database migrations (locking ops, downtime risk)

Prompt for LLM

File Providers/Resgrid.Providers.MigrationsPg/Migrations/M0111_CascadeCommunicationTestDeletesPg.cs:

Line 16 to 19:

Locking and downtime risk occurs when adding a new FK constraint, as it takes `ACCESS EXCLUSIVE` locks while validating existing rows. Use the PostgreSQL online pattern by adding the constraint `NOT VALID` first, then executing `VALIDATE CONSTRAINT` in a later transaction.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

MIN(m.senderuserid::text), MIN(m.senderunitid), MIN(m.senton),
COALESCE(MIN(m.body), (SELECT e.priorbody FROM chatmessageedits e
WHERE e.chatmessageid = f.chatmessageid ORDER BY e.editedon DESC LIMIT 1)),
(SELECT ca.filename FROM chatattachments ca

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Tripled per-row I/O occurs due to three separate correlated subqueries to chatattachments for the same chatmessageid. Replace them with a single LEFT JOIN LATERAL to select the required fields in one pass.

Kody rule violation: Optimize database queries with JOINs

Prompt for LLM

File Providers/Resgrid.Providers.MigrationsPg/Migrations/M0112_AddModerationPg.cs:

Line 141:

Tripled per-row I/O occurs due to three separate correlated subqueries to `chatattachments` for the same `chatmessageid`. Replace them with a single `LEFT JOIN LATERAL` to select the required fields in one pass.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}
}

var extra = filters.Count > 0 ? " AND " + string.Join(" AND ", filters) : string.Empty;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Error-prone string concatenation using + violates team rule. Use template literals to improve readability.

Kody rule violation: Use Template Literals Instead of String Concatenation

Prompt for LLM

File Repositories/Resgrid.Repositories.DataRepository/ModerationRepositories.cs:

Line 144:

Error-prone string concatenation using `+` violates team rule. Use template literals to improve readability.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
foreach (var pair in english)
{
var expected = Regex.Matches(pair.Value, @"\{\d+\}").Select(x => x.Value).OrderBy(x => x);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Regular expression denial of service (ReDoS) vulnerability violates team rule. Define a timeout when using regex on untrusted input to prevent DoS attacks.

Kody rule violation: Specify Timeout for Regular Expressions

Prompt for LLM

File Tests/Resgrid.Tests/Localization/ModerationLocalizationTests.cs:

Line 118:

Regular expression denial of service (ReDoS) vulnerability violates team rule. Define a timeout when using regex on untrusted input to prevent DoS attacks.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
foreach (var pair in english)
{
var expected = Regex.Matches(pair.Value, @"\{\d+\}").Select(x => x.Value).OrderBy(x => x);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Wasted CPU cycles occur because Regex.Matches is called with a constant pattern inside a foreach loop, forcing recompilation on every iteration. Declare a private static readonly Regex with RegexOptions.Compiled at the class level and reuse it.

Kody rule violation: Cache expensive operations outside loops

Prompt for LLM

File Tests/Resgrid.Tests/Localization/ModerationLocalizationTests.cs:

Line 118:

Wasted CPU cycles occur because `Regex.Matches` is called with a constant pattern inside a `foreach` loop, forcing recompilation on every iteration. Declare a `private static readonly Regex` with `RegexOptions.Compiled` at the class level and reuse it.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

.GetColumns(new SqlServerConfiguration(), ignoreProperties: automation.IgnoredProperties)
.ToList();

automation.IdType.Should().Be(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Opaque magic number 1 represents an IdType value without a named constant, making the test brittle and hard to read. Reference the named enum or constant, such as automation.IdType.Should().Be((int)IdType.String), for self-documenting assertions.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File Tests/Resgrid.Tests/Models/FormAutomationTests.cs:

Line 28:

Opaque magic number `1` represents an `IdType` value without a named constant, making the test brittle and hard to read. Reference the named enum or constant, such as `automation.IdType.Should().Be((int)IdType.String)`, for self-documenting assertions.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
var response = await _controller.GetCall(callId);

response.Result.Should().BeOfType<BadRequestResult>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules critical

Blocking async methods with .Result or .Wait() can cause deadlocks and violates team rule. Use await instead for proper asynchronous execution.

Kody rule violation: Avoid Blocking Calls to Async Methods

Prompt for LLM

File Tests/Resgrid.Tests/Web/Services/CallsControllerTests.cs:

Line 63:

Blocking async methods with `.Result` or `.Wait()` can cause deadlocks and violates team rule. Use `await` instead for proper asynchronous execution.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{
var response = await _controller.GetCall(callId);

response.Result.Should().BeOfType<BadRequestResult>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Blocking async operation violates team rule. Await Tasks instead of blocking with .Result or .Wait(), and prefer async/await end-to-end.

Kody rule violation: Await async operations properly

Prompt for LLM

File Tests/Resgrid.Tests/Web/Services/CallsControllerTests.cs:

Line 63:

Blocking async operation violates team rule. Await Tasks instead of blocking with `.Result` or `.Wait()`, and prefer `async/await` end-to-end.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +1282 to +1284
var message = await _chatMessageService.GetMessageByIdAsync(attachment.ChatMessageId);
if (message == null || message.DeletedOn.HasValue)
return NotFound();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Duplicated domain logic for the deleted-message check violates the DRY principle across GetAttachment and GetAttachmentThumbnail. Extract a helper method like EnsureMessageNotDeletedAsync(ChatMessageId) to handle the rule in one location.

Kody rule violation: Extract duplicated business logic

Prompt for LLM

File Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs:

Line 1282 to 1284:

Duplicated domain logic for the deleted-message check violates the DRY principle across `GetAttachment` and `GetAttachmentThumbnail`. Extract a helper method like `EnsureMessageNotDeletedAsync(ChatMessageId)` to handle the rule in one location.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (!Enum.IsDefined(typeof(ModerationItemType), itemType) || string.IsNullOrWhiteSpace(itemId))
return BadRequest();

var request = await _moderationService.GetReporterRequestAsync(DepartmentId, UserId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled domain exceptions from GetReporterRequestAsync return generic 500 errors instead of appropriate 401/400 responses. Wrap the external service call in a try/catch block to map UnauthorizedAccessException or ArgumentException to the correct HTTP responses.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs:

Line 78:

Unhandled domain exceptions from `GetReporterRequestAsync` return generic 500 errors instead of appropriate 401/400 responses. Wrap the external service call in a `try/catch` block to map `UnauthorizedAccessException` or `ArgumentException` to the correct HTTP responses.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

int? itemType = null, string contentAuthorUserId = null, string reportedByUserId = null,
DateTime? from = null, DateTime? to = null, int page = 1, int pageSize = 50)
{
if (!await _moderationService.CanModerateAsync(DepartmentId, UserId))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unnecessary database round-trips occur when CanModerateAsync executes before validating enum inputs, allowing invalid query parameters to hit the database. Move the Enum.IsDefined checks above the CanModerateAsync call to return BadRequest first.

Kody rule violation: Order validations before database queries

Prompt for LLM

File Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs:

Line 98:

Unnecessary database round-trips occur when `CanModerateAsync` executes before validating enum inputs, allowing invalid query parameters to hit the database. Move the `Enum.IsDefined` checks above the `CanModerateAsync` call to return `BadRequest` first.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +122 to +124
Page = Math.Max(page, 1),
PageSize = requests.Count,
Status = requests.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug medium

Incorrect pagination metadata occurs when GetRequests assigns PageSize = requests.Count instead of the requested page size, returning partial counts on the last page. Set PageSize = Math.Max(pageSize, 1) to report the correct requested page size.

Page = Math.Max(page, 1),
PageSize = Math.Max(pageSize, 1),
Status = requests.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound
Prompt for LLM

File Web/Resgrid.Web.Services/Controllers/v4/ModerationController.cs:

Line 122 to 124:

Incorrect pagination metadata occurs when `GetRequests` assigns `PageSize = requests.Count` instead of the requested page size, returning partial counts on the last page. Set `PageSize = Math.Max(pageSize, 1)` to report the correct requested page size.

Suggested Code:

Page = Math.Max(page, 1),
PageSize = Math.Max(pageSize, 1),
Status = requests.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

/// <summary>
/// Whether the tombstone represents a moderation action
/// </summary>
public bool IsModerated { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Uninitialized auto-property IsModerated relies on implicit language defaults, violating rule requirements. Initialize the property explicitly with a sensible default value, such as public bool IsModerated { get; set; } = false;.

Kody rule violation: Initialize properties with default values

Prompt for LLM

File Web/Resgrid.Web.Services/Models/v4/Chat/ChatApiModels.cs:

Line 540:

Uninitialized auto-property `IsModerated` relies on implicit language defaults, violating rule requirements. Initialize the property explicitly with a sensible default value, such as `public bool IsModerated { get; set; } = false;`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

export default function ChatModerationElement({ departmentAdmin = false }: ChatModerationElementProps) {
const [tab, setTab] = useState<ModTab>('requests');
const sharedTabs: { key: ModTab; label: string }[] = [
{ key: 'requests', label: moderationText('TabRequests') },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Repeated raw string literal 'requests' for the finite ModTab set introduces typo risks and hides intent. Introduce a const object like const ModTabKey = { Requests: 'requests', ... } as const; to reference these values safely.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatModerationElement.tsx:

Line 20:

Repeated raw string literal `'requests'` for the finite `ModTab` set introduces typo risks and hides intent. Introduce a const object like `const ModTabKey = { Requests: 'requests', ... } as const;` to reference these values safely.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

{ key: 'settings', label: 'Settings' },
{ key: 'exports', label: 'Exports' },
];
type ModTab = 'requests' | 'reports' | 'actions' | 'settings' | 'exports';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Maintenance drift risk arises because the ModTab union type duplicates string literals used in the sharedTabs and departmentTabs arrays. Declare a const tuple to derive both the type and runtime arrays from a single source of truth.

Kody rule violation: Derive TypeScript types from validation schemas

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatModerationElement.tsx:

Line 10:

Maintenance drift risk arises because the `ModTab` union type duplicates string literals used in the `sharedTabs` and `departmentTabs` arrays. Declare a `const` tuple to derive both the type and runtime arrays from a single source of truth.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


export default function ChatModerationElement(_props: ChatModerationElementProps) {
const [tab, setTab] = useState<ModTab>('flags');
export default function ChatModerationElement({ departmentAdmin = false }: ChatModerationElementProps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Reduced refactor safety and discoverability occur because ChatModerationElement uses a default export. Use a named export instead (export function ChatModerationElement(...)) and update importers accordingly.

Kody rule violation: Avoid default exports

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatModerationElement.tsx:

Line 17:

Reduced refactor safety and discoverability occur because `ChatModerationElement` uses a default export. Use a named export instead (`export function ChatModerationElement(...)`) and update importers accordingly.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

<FlagDialog
existingRequest={flagStatus}
statusLoading={flagStatus === undefined}
onClose={() => setFlagTarget(null)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Performance degradation caused by using .bind() or inline arrow functions in JSX props violates team rule. Move function definitions outside the render method to prevent creating new functions on every render.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatPageElement.tsx:

Line 205:

Performance degradation caused by using `.bind()` or inline arrow functions in JSX props violates team rule. Move function definitions outside the render method to prevent creating new functions on every render.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

setFlagStatus(undefined);
getMyModerationRequest(0, message.ChatMessageId)
.then(setFlagStatus)
.catch(() => setFlagStatus(null));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Silent error swallowing violates team rules when the .catch handler discards rejections without logging context. Log the error with identifying details using logger.error inside the .catch handler to ensure failures are visible during debugging.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/ChatPageElement.tsx:

Line 64:

Silent error swallowing violates team rules when the `.catch` handler discards rejections without logging context. Log the error with identifying details using `logger.error` inside the `.catch` handler to ensure failures are visible during debugging.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

<div>{summarize(request.OriginalText)}</div>
{request.OriginalFileName && <div className="rgchat-convo__sub">{request.OriginalFileName}</div>}
{request.HasOriginalContent && (
<button type="button" className="rgchat-thread-link" onClick={() => void downloadModerationEvidence(request)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled promise rejection occurs when downloadModerationEvidence(request) is invoked with void and no error guard. Wrap the async operation in a try/catch block or chain a .catch() handler.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx:

Line 215:

Unhandled promise rejection occurs when `downloadModerationEvidence(request)` is invoked with `void` and no error guard. Wrap the async operation in a `try/catch` block or chain a `.catch()` handler.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

<>
<label>
<span>{moderationText('AddedByUserId')}</span>
<input className="rgchat-input" list="rg-moderation-authors" value={contentAuthorUserId} onChange={(event) => setContentAuthorUserId(event.target.value)} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Excessive network requests occur because the onChange handler fires getModerationRequests on every keystroke without debouncing. Debounce the state update or the load effect to ensure a burst of keystrokes issues only one request.

Kody rule violation: Debounce or throttle user input that triggers work

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/ModerationRequestsTable.tsx:

Line 150:

Excessive network requests occur because the `onChange` handler fires `getModerationRequests` on every keystroke without debouncing. Debounce the state update or the `load` effect to ensure a burst of keystrokes issues only one request.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}
} catch (error) {
console.error('Failed to save chat settings.', error);
console.error(moderationText('FailedSaveSettings'), error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unstructured error logging violates team rules by using plain strings instead of structured fields. Replace console.error with a structured logger call that includes the operation name and relevant identifiers.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderation/SettingsTab.tsx:

Line 57:

Unstructured error logging violates team rules by using plain strings instead of structured fields. Replace `console.error` with a structured logger call that includes the operation name and relevant identifiers.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 4000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Memory leak risk occurs when a setTimeout ID is not stored for cleanup, allowing the callback to fire even if the component unmounts. Store the timer ID in a variable and clear it via clearTimeout(timerId) in the component's teardown path.

Kody rule violation: Clear timers on teardown/unmount

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Apps/src/components/chat/moderationApi.ts:

Line 133:

Memory leak risk occurs when a `setTimeout` ID is not stored for cleanup, allowing the callback to fire even if the component unmounts. Store the timer ID in a variable and clear it via `clearTimeout(timerId)` in the component's teardown path.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +1792 to +1793
note.IsFlagged = await _moderationService.GetReporterRequestAsync(DepartmentId, UserId,
ModerationItemType.CallNote, callNote.CallNoteId.ToString(CultureInfo.InvariantCulture)) != null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

N+1 query pattern degrades performance as GetReporterRequestAsync is called per note inside the GetCallNotes loop. Add a batch method to IModerationService and call it once before the loop to retrieve all moderation statuses in a single query.

Kody rule violation: Detect N+1 style queries and suggest batching

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs:

Line 1792 to 1793:

N+1 query pattern degrades performance as `GetReporterRequestAsync` is called per note inside the `GetCallNotes` loop. Add a batch method to `IModerationService` and call it once before the loop to retrieve all moderation statuses in a single query.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +1792 to +1793
note.IsFlagged = await _moderationService.GetReporterRequestAsync(DepartmentId, UserId,
ModerationItemType.CallNote, callNote.CallNoteId.ToString(CultureInfo.InvariantCulture)) != null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance medium

N+1 query pattern in the foreach (var callNote in call.CallNotes) loop calls GetReporterRequestAsync per note, executing 2N database round-trips per request. Add a batch method to IModerationService and call it once before the loop to retrieve all flagged note IDs.

// Resolve all flagged note IDs in one query before the loop:
var flaggedNoteIds = await _moderationService.GetReporterItemIdsAsync(DepartmentId, UserId,
    ModerationItemType.CallNote, call.CallNotes.Select(n => n.CallNoteId.ToString(CultureInfo.InvariantCulture)));
// Inside the loop:
note.IsFlagged = flaggedNoteIds.Contains(callNote.CallNoteId.ToString(CultureInfo.InvariantCulture));
Prompt for LLM

File Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs:

Line 1792 to 1793:

N+1 query pattern in the `foreach (var callNote in call.CallNotes)` loop calls `GetReporterRequestAsync` per note, executing 2N database round-trips per request. Add a batch method to `IModerationService` and call it once before the loop to retrieve all flagged note IDs.

Suggested Code:

// Resolve all flagged note IDs in one query before the loop:
var flaggedNoteIds = await _moderationService.GetReporterItemIdsAsync(DepartmentId, UserId,
    ModerationItemType.CallNote, call.CallNotes.Select(n => n.CallNoteId.ToString(CultureInfo.InvariantCulture)));
// Inside the loop:
note.IsFlagged = flaggedNoteIds.Contains(callNote.CallNoteId.ToString(CultureInfo.InvariantCulture));

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +1582 to +1588
var moderationRequest = await _moderationService.GetReporterRequestAsync(DepartmentId, UserId,
ModerationItemType.CallImage, attachment.CallAttachmentId.ToString(CultureInfo.InvariantCulture));
var ownReport = moderationRequest?.Reports?.FirstOrDefault();
model.IsFlagged = moderationRequest != null;
model.FlagNote = ownReport?.Note;
model.ModerationStatus = moderationRequest?.Status;
model.ModerationAdminNote = moderationRequest?.AdminNote;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Duplicated mapping logic for CallImage and CallNote increases maintenance burden and risks inconsistency. Extract a helper method like MapModerationFieldsAsync(IFlagViewModel model, ModerationItemType itemType, string itemId) to handle the fetch and property mapping for both actions.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Controllers/DispatchController.cs:

Line 1582 to 1588:

Duplicated mapping logic for CallImage and CallNote increases maintenance burden and risks inconsistency. Extract a helper method like `MapModerationFieldsAsync(IFlagViewModel model, ModerationItemType itemType, string itemId)` to handle the fetch and property mapping for both actions.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

<div class="wrapper wrapper-content">
<div class="ibox float-e-margins">
<div class="ibox-content">
<rg-chat-moderation departmentadmin="@ClaimsAuthorizationHelper.IsUserDepartmentAdmin().ToString().ToLowerInvariant()"></rg-chat-moderation>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Thick UI violation occurs because authorization logic (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) is invoked directly in the Razor view. Move this logic to the controller, compute the flag, and pass it via ViewBag.IsDepartmentAdmin to keep the view thin.

Kody rule violation: Separate UI logic from business logic

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Views/Moderation/Index.cshtml:

Line 25:

Thick UI violation occurs because authorization logic (`ClaimsAuthorizationHelper.IsUserDepartmentAdmin()`) is invoked directly in the Razor view. Move this logic to the controller, compute the flag, and pass it via `ViewBag.IsDepartmentAdmin` to keep the view thin.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

<div class="wrapper wrapper-content">
<div class="ibox float-e-margins">
<div class="ibox-content">
<rg-chat-moderation departmentadmin="@ClaimsAuthorizationHelper.IsUserDepartmentAdmin().ToString().ToLowerInvariant()"></rg-chat-moderation>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Unclear naming convention violates team rules because the custom element rg-chat-moderation uses an abbreviation. Rename it to a full-word format like resgrid-chat-moderation to improve clarity and maintainability.

Kody rule violation: Full-word component names

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Views/Moderation/Index.cshtml:

Line 25:

Unclear naming convention violates team rules because the custom element `rg-chat-moderation` uses an abbreviation. Rename it to a full-word format like `resgrid-chat-moderation` to improve clarity and maintainability.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

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