Skip to content

Develop - #457

Merged
ucswift merged 6 commits into
masterfrom
develop
Aug 11, 2026
Merged

Develop#457
ucswift merged 6 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 10, 2026

Copy link
Copy Markdown
Member

Incident Chat Channels, App-Level Access Gates, and Automated Notification Suppression

This PR introduces several major features across the Resgrid Core platform:

New Incident Chat Channels

Two new chat channel types have been added to improve incident communication:

  • IncidentLeads — a channel for the Incident Commander and all lane primary/secondary leads, with membership derived live from the board so leads gain/lose access automatically as assignments change.
  • IncidentDispatch — the incident's direct line to authorized dispatchers, accessible to everyone on the incident plus dispatch-authorized users.

A lazy backfill mechanism provisions any missing channels (incident, command, leads, dispatch, and lane channels) the first time someone opens an incident board or responder view, eliminating the need for a data migration. Channel provisioning is idempotent and cache-guarded.

App-Level Access Control

Two new permissions gate access to the specialized apps:

  • DispatchAppLogin (type 29) — controls who can sign in to the Dispatch app and participate in dispatch chat channels.
  • CommandAppLogin (type 30) — controls who can act as a commander, establish incident command, and read command boards.

Both default to "Everyone" so existing departments are unaffected on upgrade. New PermissionGateServiceBase and corresponding service implementations provide cached, fail-closed evaluation. When a department narrows the command permission, those authorized users also gain a CommandAssistCapabilities set allowing them to assist on boards (assign/move resources, manage timers/accountability) without holding an ICS role.

Frozen/Archived Channel Enforcement

Archived channels (from closed incidents or commands) are now truly immutable. Message edits, author deletions, and reactions are blocked on frozen channels, while moderator deletes still function for content moderation. This applies at both the command-close level (leaving the call's own channel live) and the call-close level.

Automated Change Notification Suppression

Status and staffing events now carry an AutoGenerated flag. Automated changes — such as scheduled department resets, group resets, and call dispatch auto-status — no longer trigger user notifications while still firing SignalR updates to keep clients in sync.

Additional Changes

  • ResourceIncidentView now includes ICS role contact info and caller-specific chat channel IDs resolved server-side.
  • System audit querying by type across all departments (platform-wide reporting).
  • GetChannels API supports an includeArchived parameter for viewing closed incident chat history.
  • Comprehensive unit test coverage for frozen channel behavior, incident backfill, dispatch/command access gates, and chat permission evaluations.

Summary by CodeRabbit

  • New Features

    • Added separate Leads and Dispatch channels for incident communications.
    • Added Dispatch and Command app access permissions with role-based controls.
    • Incident command boards now show role contacts and accessible chat channels.
    • Added command-assist access and configurable CORS origins, including approved subdomains.
    • Chat channel listings can optionally include archived channels.
  • Improvements

    • Incident channels are provisioned, archived, and reopened automatically as incidents change.
    • Archived channels prevent edits and reactions while preserving moderator controls.
    • Automated status and staffing updates no longer generate user notifications.
    • Improved incident access enforcement and system audit pagination.

@request-info

request-info Bot commented Aug 10, 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 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07754538-b252-457f-9f45-b3a7f2cdec7e

📥 Commits

Reviewing files that changed from the base of the PR and between c6c7511 and cc03635.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Config/CorsHelperTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (1)
  • Core/Resgrid.Config/CorsHelper.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Core/Resgrid.Config/CorsHelper.cs

📝 Walkthrough

Walkthrough

This change adds command and dispatch access controls, incident chat provisioning and lifecycle handling, automatic-event notification suppression, caller-specific incident view data, paged system-audit retrieval, and shared CORS origin validation.

Changes

Incident command and chat access

Layer / File(s) Summary
Command and dispatch access foundation
Core/Resgrid.Model/..., Core/Resgrid.Services/PermissionGateServiceBase.cs, Core/Resgrid.Services/*AccessService.cs
Adds command and dispatch permission types, shared permission evaluation, access services, command-assist capabilities, and dependency injection registration.
Command gates and permission administration
Web/Resgrid.Web.Services/Controllers/v4/..., Web/Resgrid.Web.Services/Filters/..., Web/Resgrid.Web/Areas/User/..., Web/Resgrid.Web/wwwroot/js/...
Enforces command access on incident endpoints and exposes Dispatch App Login and Command App Login settings and rights.
Incident chat provisioning and lifecycle
Core/Resgrid.Model/Chat/..., Core/Resgrid.Model/IncidentCommand/..., Core/Resgrid.Services/Chat*Service.cs, Core/Resgrid.Services/IncidentCommandService.cs, Repositories/.../ChatRepositories.cs
Adds leads and dispatch channels, active-command backfill, command-scoped archive handling, audience rules, resource-view chat metadata, and frozen-channel mutation checks.

Automatic event notification handling

Layer / File(s) Summary
Automatic event contracts and propagation
Core/Resgrid.Model/Events/..., Core/Resgrid.Model/Services/..., Core/Resgrid.Services/..., Workers/.../StaffingScheduleLogic.cs
Propagates AutoGenerated through status and staffing events. Bulk operations and scheduled staffing changes set the flag.
Automatic notification filtering
Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs
Suppresses user notifications for automatic changes while retaining live SignalR updates.

Paged system audit retrieval

Layer / File(s) Summary
Paged audit query and repository
Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs, Repositories/.../SystemAudits/..., Repositories/.../SystemAuditRepository.cs
Adds type-filtered, date-ranged, paged audit retrieval for PostgreSQL and SQL Server. Pagination is bounded and uses deterministic secondary ordering.

Shared CORS origin validation

Layer / File(s) Summary
CORS configuration and middleware integration
Core/Resgrid.Config/ApiConfig.cs, Core/Resgrid.Config/CorsHelper.cs, Web/Resgrid.Web.*/Startup.cs
Adds configured extra origins and centralizes origin validation for the web services and eventing middleware.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant IncidentCommandController
  participant CommandAccessService
  participant IncidentCommandService
  participant ChatChannelService
  Client->>IncidentCommandController: request command or board operation
  IncidentCommandController->>CommandAccessService: check department access
  CommandAccessService-->>IncidentCommandController: authorization result
  IncidentCommandController->>IncidentCommandService: process authorized request
  IncidentCommandService->>ChatChannelService: backfill incident channels
  ChatChannelService-->>IncidentCommandService: channel state
Loading

Possibly related PRs

  • Resgrid/Core#418: Shares incident-command authorization and controller and capability filtering.
  • Resgrid/Core#430: Shares service registration and related service wiring.
  • Resgrid/Core#435: Shares incident-command lifecycle and chat provisioning behavior.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Develop" is generic and does not identify the pull request's main changes, such as incident chat channels, access gates, or CORS updates. Replace "Develop" with a concise title that describes the primary change, such as "Add incident chat channels and app access controls".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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: 15

🧹 Nitpick comments (2)
Core/Resgrid.Services/ChatMessageService.cs (1)

264-291: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant channel re-fetch across four chat-mutation paths. IsChannelFrozenAsync fetches the channel by chatChannelId to check IsArchived. Each of its four callers then re-fetches the same channel later, purely to build the PublishEvent payload — one extra avoidable DB round trip per edit, delete, add-reaction, and remove-reaction call.

  • Core/Resgrid.Services/ChatMessageService.cs#L264-L291: change IsChannelFrozenAsync to return the fetched ChatChannel (or null when missing/archived) instead of a bool, so EditMessageAsync can reuse it instead of calling _chatChannelRepository.GetByIdAsync(message.ChatChannelId) again at line 309.
  • Core/Resgrid.Services/ChatMessageService.cs#L326-L331: reuse the channel returned by the updated frozen-check call instead of re-fetching it at line 344 for PublishEvent.
  • Core/Resgrid.Services/ChatMessageService.cs#L367-L369: reuse the channel returned by the updated frozen-check call instead of re-fetching it at line 411 for PublishEvent.
  • Core/Resgrid.Services/ChatMessageService.cs#L423-L425: reuse the channel returned by the updated frozen-check call instead of re-fetching it at line 431 for PublishEvent.
🤖 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 264 - 291,
Eliminate redundant channel lookups in ChatMessageService.cs at lines 264-291,
326-331, 367-369, and 423-425 by changing IsChannelFrozenAsync to return the
fetched ChatChannel only when present and not archived, otherwise null. Update
EditMessageAsync, the delete path, add-reaction path, and remove-reaction path
to retain and reuse that returned channel for PublishEvent instead of calling
GetByIdAsync again; preserve fail-closed behavior for missing, blank, or
archived channels.
Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs (1)

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

Use the required dependency resolution pattern.

Resolve ICommandAccessService with Bootstrapper.GetKernel().Resolve<ICommandAccessService>() in the constructor. Do not add constructor injection for this dependency.

As per coding guidelines, use Service Locator pattern via 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.Services/Controllers/v4/IncidentCommandController.cs` around
lines 35 - 42, Update IncidentCommandController’s constructor to remove the
ICommandAccessService parameter and resolve it through
Bootstrapper.GetKernel().Resolve<ICommandAccessService>() when assigning
_commandAccessService; retain constructor injection for the other dependencies.

Source: Coding guidelines

🤖 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/Services/IActionLogsService.cs`:
- Line 88: Propagate the autoGenerated flag through the single-log path by
adding it to the relevant SetUserActionAsync and
ActionLogsService.SaveActionLogAsync overloads, then assign it to
UserStatusEvent.AutoGenerated. Pass true from
CallDispatchStatusService.ApplyPersonnelStatusesAsync and add a regression test
verifying automated personnel status changes preserve the flag and avoid
outbound notifications.
- Line 88: Propagate the autoGenerated value from SaveActionLogAsync through
both SetUserActionAsync overloads into UserStatusEvent.AutoGenerated, preserving
false only when the argument is omitted. Update SaveAllActionLogsAsync to
document the autoGenerated parameter and ensure its calls pass that value
through the action-log save flow.

In `@Core/Resgrid.Services/ActionLogsService.cs`:
- Around line 348-350: Update the bulk status reset flow around
SaveAllActionLogsAsync so manual department-wide reset operations do not pass
autoGenerated: true and continue producing per-user notifications. Restrict the
autoGenerated flag to scheduled resets, or introduce a separate
notification-suppression parameter while preserving suppression for scheduled
resets.

In `@Core/Resgrid.Services/ChatChannelService.cs`:
- Around line 751-812: The channel provisioning flow in
EnsureIncidentChannelsAsync must rebind reused command, leads, and dispatch
channels to the current command. Update EnsureCommandChannelAsync,
EnsureLeadsChannelAsync, and EnsureDispatchChannelAsync so reused channels set
IncidentCommandId to the new command, clear IsArchived and ArchivedOn, and
invalidate the associated permission cache before returning.

In `@Core/Resgrid.Services/CommandAccessService.cs`:
- Around line 12-18: Update CommandAccessService’s constructor to remove its
injected service parameters and resolve IPermissionsService,
IDepartmentsService, IDepartmentGroupsService, IPersonnelRolesService, and
ICacheProvider via Bootstrapper.GetKernel().Resolve<T>(), passing the resolved
instances to the base constructor.

In `@Core/Resgrid.Services/IncidentCommandService.cs`:
- Line 482: Replace the new ServiceLocator.Current.GetInstance calls in
Core/Resgrid.Services/IncidentCommandService.cs at lines 482, 1075, and
1116-1117 with Bootstrapper.GetKernel().Resolve<T>() for ICommandAccessService,
IChatChannelService, and IChatChannelRepository respectively; preserve the
surrounding command logic.
- Around line 1090-1108: Scope incident nodes and roles to
command.IncidentCommandId throughout IncidentCommandService.cs: in lines
1090-1108 filter before building contacts and calculating isCommandStaff or
isLaneLead; in lines 528-531 and 1058-1059 pass only matching nodes to the
backfill; in lines 1074-1075 filter fallback nodes before
EnsureIncidentChannelsAsync. Preserve the existing command-specific channel and
authorization behavior.

In `@Core/Resgrid.Services/PermissionGateServiceBase.cs`:
- Around line 147-153: Update the permission flow in the relevant method of
PermissionGateServiceBase so GetDepartmentMemberAsync validates department
membership before handling a null permission. Return false when membership is
absent, and only return the default true for a missing permission after
membership has been confirmed; preserve the existing permission evaluation for
non-null permissions.
- Around line 59-75: Update EvaluateAsync and the permission paths used by
CanUseDispatchAsync and CanUseCommandAsync so a missing permission row returns
true only after GetDepartmentMemberAsync confirms department membership. Replace
the direct _cacheProvider GetStringAsync/SetStringAsync calls with
ICacheProvider.RetrieveAsync<string>() and a local fallback function that
evaluates and caches the permission result, preserving cache-outage fallback
behavior.

In
`@Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByTypePagedQuery.cs`:
- Line 16: The paged audit queries need a deterministic secondary sort. In
SelectSystemAuditsByTypePagedQuery.cs at lines 16-16 and 18-18, update each
ORDER BY clause to sort by the unique system-audit key after loggedon DESC,
preserving the existing pagination parameters and primary ordering.

In `@Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs`:
- Around line 136-139: Update the pagination parameter handling around safePage
and safePageSize to enforce a defined maximum page-size constant, clamping valid
pageSize values to that bound while retaining the minimum of 1. Compute the
Offset multiplication using long arithmetic after normalization, and pass the
bounded safePageSize to the database.

In `@Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs`:
- Around line 49-56: Apply CanCommandAsync as an authorization guard in
ReopenCommand, GetCommandForCall, and SendMessageToCommand before their existing
operation logic, matching EstablishCommand and the board-read routes. Preserve
the current capability-filter checks and return the established unauthorized
response when the command gate denies access.

In `@Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs`:
- Around line 62-70: The RequiresIncidentCapabilityAttribute filter must fail
closed when ICommandAccessService cannot be resolved. Update the commandAccess
handling before CanUseCommandAsync so a null service returns a server error or
denies the request, while preserving the existing unauthorized response when
CanUseCommandAsync returns false.

In `@Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml`:
- Around line 326-345: Replace the hard-coded labels and descriptions in the
Dispatch App Login and Command App Login rows with appropriate localizer
lookups, and add matching resource keys for each label and description in the
localization resources. Preserve the existing permission controls and localized
PermissionNA/PermissionNoRoles content.

In
`@Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js`:
- Around line 787-801: Change the permission updates in the DispatchAppLogin and
corresponding Command access handlers to use POST requests, and update the
SetPermission and SetPermissionData actions to require anti-forgery validation.
Include the page’s verification token in both AJAX request payloads while
preserving the existing permission type and value submission.

---

Nitpick comments:
In `@Core/Resgrid.Services/ChatMessageService.cs`:
- Around line 264-291: Eliminate redundant channel lookups in
ChatMessageService.cs at lines 264-291, 326-331, 367-369, and 423-425 by
changing IsChannelFrozenAsync to return the fetched ChatChannel only when
present and not archived, otherwise null. Update EditMessageAsync, the delete
path, add-reaction path, and remove-reaction path to retain and reuse that
returned channel for PublishEvent instead of calling GetByIdAsync again;
preserve fail-closed behavior for missing, blank, or archived channels.

In `@Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs`:
- Around line 35-42: Update IncidentCommandController’s constructor to remove
the ICommandAccessService parameter and resolve it through
Bootstrapper.GetKernel().Resolve<ICommandAccessService>() when assigning
_commandAccessService; retain constructor injection for the other dependencies.
🪄 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: a6221ec6-0c4a-4c0d-94bb-cf7ee93d5343

📥 Commits

Reviewing files that changed from the base of the PR and between a7e7984 and 946e92e.

⛔ Files ignored due to path filters (7)
  • .claude/settings.local.json is excluded by !**/.claude/**
  • Tests/Resgrid.Tests/Services/CallDispatchStatusServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/ChatFrozenChannelTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/ChatIncidentBackfillTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/ChatPermissionServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/DispatchAccessServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Web/Services/RequiresIncidentCapabilityFilterTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (43)
  • Core/Resgrid.Model/Chat/ChatEnums.cs
  • Core/Resgrid.Model/Events/UnitStatusEvent.cs
  • Core/Resgrid.Model/Events/UserStaffingEvent.cs
  • Core/Resgrid.Model/Events/UserStatusEvent.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentRole.cs
  • Core/Resgrid.Model/IncidentCommand/ResourceIncidentView.cs
  • Core/Resgrid.Model/PermissionTypes.cs
  • Core/Resgrid.Model/Repositories/IChatRepositories.cs
  • Core/Resgrid.Model/Repositories/ISystemAuditsRepository.cs
  • Core/Resgrid.Model/Services/IActionLogsService.cs
  • Core/Resgrid.Model/Services/IChatServices.cs
  • Core/Resgrid.Model/Services/ICommandAccessService.cs
  • Core/Resgrid.Model/Services/IDispatchAccessService.cs
  • Core/Resgrid.Model/Services/IUnitsService.cs
  • Core/Resgrid.Model/Services/IUserStateService.cs
  • Core/Resgrid.Services/ActionLogsService.cs
  • Core/Resgrid.Services/CallDispatchStatusService.cs
  • Core/Resgrid.Services/ChatChannelService.cs
  • Core/Resgrid.Services/ChatMessageService.cs
  • Core/Resgrid.Services/ChatPermissionService.cs
  • Core/Resgrid.Services/ChatProvisioningEventService.cs
  • Core/Resgrid.Services/CommandAccessService.cs
  • Core/Resgrid.Services/DispatchAccessService.cs
  • Core/Resgrid.Services/IncidentCommandService.cs
  • Core/Resgrid.Services/PermissionGateServiceBase.cs
  • Core/Resgrid.Services/ServicesModule.cs
  • Core/Resgrid.Services/UnitsService.cs
  • Core/Resgrid.Services/UserStateService.cs
  • Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs
  • Repositories/Resgrid.Repositories.DataRepository/ChatRepositories.cs
  • Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByTypePagedQuery.cs
  • Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs
  • Web/Resgrid.Web.Services/Controllers/v4/ChatController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/SecurityController.cs
  • Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs
  • Web/Resgrid.Web.Services/Models/v4/Security/DepartmentRightsResult.cs
  • Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
  • Web/Resgrid.Web/Areas/User/Controllers/SecurityController.cs
  • Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs
  • Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml
  • Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js
  • Workers/Resgrid.Workers.Framework/Logic/StaffingScheduleLogic.cs

/// <param name="cancellationToken">The cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Task&lt;System.Boolean&gt;.</returns>
Task<bool> SaveAllActionLogsAsync(List<ActionLog> actionLogs, CancellationToken cancellationToken = default(CancellationToken));
Task<bool> SaveAllActionLogsAsync(List<ActionLog> actionLogs, CancellationToken cancellationToken = default(CancellationToken), bool autoGenerated = 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.

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

Propagate autoGenerated through single action-log updates.

The new flag is exposed only by SaveAllActionLogsAsync. CallDispatchStatusService.ApplyPersonnelStatusesAsync uses SetUserActionAsync, which reaches ActionLogsService.SaveActionLogAsync. That event does not set UserStatusEvent.AutoGenerated, so the value remains false. OutboundEventProvider.personnelStatusChangedHandler then enqueues notifications for automated personnel status changes.

Propagate the flag through the relevant SetUserActionAsync and SaveActionLogAsync overloads. Pass true from the call-dispatch personnel path. Add a regression test for this flow.

🤖 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/Services/IActionLogsService.cs` at line 88, Propagate the
autoGenerated flag through the single-log path by adding it to the relevant
SetUserActionAsync and ActionLogsService.SaveActionLogAsync overloads, then
assign it to UserStatusEvent.AutoGenerated. Pass true from
CallDispatchStatusService.ApplyPersonnelStatusesAsync and add a regression test
verifying automated personnel status changes preserve the flag and avoid
outbound notifications.

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
file="Core/Resgrid.Model/Services/IActionLogsService.cs"
printf '%s\n' '--- file outline ---'
ast-grep outline "$file" || true
printf '%s\n' '--- interface section ---'
sed -n '60,105p' "$file"
printf '%s\n' '--- declarations and usages ---'
rg -n --glob '*.cs' 'SaveAllActionLogsAsync|SaveActionLogAsync|SetUserActionAsync|autoGenerated|AutoGenerated' Core

Repository: Resgrid/Core

Length of output: 11132


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- ActionLogsService implementation ---'
sed -n '175,340p' Core/Resgrid.Services/ActionLogsService.cs
printf '%s\n' '--- call-dispatch implementation ---'
sed -n '65,135p' Core/Resgrid.Services/CallDispatchStatusService.cs
printf '%s\n' '--- action-log event and notification references ---'
rg -n --glob '*.cs' 'ActionLogEvent|UserStatusEvent|ActionLog.*Event|AutoGenerated' Core/Resgrid.Model Core/Resgrid.Services Core/Resgrid.Messaging Core/Resgrid.Notifications 2>/dev/null || true

Repository: Resgrid/Core

Length of output: 11073


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

service = Path("Core/Resgrid.Services/ActionLogsService.cs").read_text()
dispatch = Path("Core/Resgrid.Services/CallDispatchStatusService.cs").read_text()

checks = {
    "SaveAllActionLogsAsync writes AutoGenerated": "AutoGenerated  = autoGenerated" in service,
    "SaveActionLogAsync sends UserStatusEvent without AutoGenerated": (
        "SaveActionLogAsync" in service
        and "Status         = saved,\n\t\t\t\tPreviousStatus = previousStatus\n\t\t\t});" in service
    ),
    "SetUserActionAsync delegates to SaveActionLogAsync": "return await SaveActionLogAsync(al, cancellationToken);" in service,
    "call dispatch personnel uses SetUserActionAsync": "_actionLogsService.SetUserActionAsync(userId, call.DepartmentId, statusToSet, null, call.CallId, cancellationToken)" in dispatch,
    "call dispatch unit path passes autoGenerated": "_unitsService.SetUnitStateAsync(state, call.DepartmentId, cancellationToken, autoGenerated: true)" in dispatch,
}

for name, result in checks.items():
    print(f"{name}: {'yes' if result else 'no'}")
PY
printf '%s\n' '--- all UserStatusEvent consumers ---'
rg -n --glob '*.cs' 'UserStatusEvent|AutoGenerated' .

Repository: Resgrid/Core

Length of output: 3705


🏁 Script executed:

#!/bin/bash
set -e
sed -n '70,95p' Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs
sed -n '225,245p' Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs

Repository: Resgrid/Core

Length of output: 1902


Propagate autoGenerated to personnel status updates.

SaveActionLogAsync always publishes UserStatusEvent.AutoGenerated = false. Call-dispatch personnel updates use SetUserActionAsync, so they still enqueue user notifications. Propagate autoGenerated through the SetUserActionAsync overloads and document the new parameter on SaveAllActionLogsAsync.

🤖 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/Services/IActionLogsService.cs` at line 88, Propagate the
autoGenerated value from SaveActionLogAsync through both SetUserActionAsync
overloads into UserStatusEvent.AutoGenerated, preserving false only when the
argument is omitted. Update SaveAllActionLogsAsync to document the autoGenerated
parameter and ensure its calls pass that value through the action-log save flow.

Comment on lines +348 to +350
// Bulk status operations (scheduled resets, manual department-wide resets) never generate
// per-user notifications, otherwise every member change fans out to every subscriber.
return await SaveAllActionLogsAsync(logs, autoGenerated: true);

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 \
  'SetActionForEntireDepartmentAsync|SetActionForDepartmentGroupAsync' \
  --glob '*.cs'

Repository: Resgrid/Core

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd -t f 'ActionLogsService\.cs$' . | head -n 1)"
printf '%s\n' "FILE=$file"
test -n "$file"
ast-grep outline "$file" --match 'SetActionForEntireDepartmentAsync' --view expanded || true
ast-grep outline "$file" --match 'SetActionForDepartmentGroupAsync' --view expanded || true
rg -n -C 8 'SetActionFor|autoGenerated|UserStatusEvent|SaveAllActionLogsAsync' "$file"
printf '\nCALL SITES\n'
rg -n -C 4 'SetActionForEntireDepartmentAsync|SetActionForDepartmentGroupAsync' --glob '*.cs' . || true

Repository: Resgrid/Core

Length of output: 10662


🏁 Script executed:

#!/bin/bash
set -euo pipefail

service="./Core/Resgrid.Services/ActionLogsService.cs"
cat -n "$service" | sed -n '180,240p;315,385p'

printf '\nAUTO-GENERATED DEFINITIONS AND CONSUMERS\n'
rg -n -C 6 'AutoGenerated|UserStatusEvent|Status.*Notification|Notification.*Status' \
  --glob '*.cs' Core Web Workers Tests | head -n 500

printf '\nCONTROLLER ACTIONS\n'
cat -n ./Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs | sed -n '1035,1078p'

printf '\nSCHEDULED CALLER\n'
cat -n ./Workers/Resgrid.Workers.Framework/Logic/StatusScheduleLogic.cs | sed -n '20,55p'

Repository: Resgrid/Core

Length of output: 25622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' 'USER STATUS EVENT REFERENCES'
rg -n -C 10 'UserStatusEvent' --glob '*.cs' . 

printf '\nAUTOGENERATED CONDITION REFERENCES\n'
rg -n -C 8 'AutoGenerated\s*(==|!=|&&|\|\||\)|\.)|\.AutoGenerated' --glob '*.cs' Core Web Workers Tests | head -n 800

printf '\nRELEVANT ACTIONS AND ROUTES\n'
rg -n -C 5 'ResetAllToStandingBy|ResetGroupToStandingBy|DepartmentStatusReset|SetActionForDepartmentGroupAsync' \
  --glob '*.cs' Web Workers Core Tests

Repository: Resgrid/Core

Length of output: 39788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' 'USER STATUS EVENT REFERENCES'
rg -n -C 10 'UserStatusEvent' --glob '*.cs' .

printf '\nAUTOGENERATED CONDITION REFERENCES\n'
rg -n -C 8 'AutoGenerated\s*(==|!=|&&|\|\||\)|\.)|\.AutoGenerated' \
  --glob '*.cs' Core Web Workers Tests | head -n 800

printf '\nRELEVANT ACTIONS AND ROUTES\n'
rg -n -C 5 'ResetAllToStandingBy|ResetGroupToStandingBy|DepartmentStatusReset|SetActionForDepartmentGroupAsync' \
  --glob '*.cs' Web Workers Core Tests

Repository: Resgrid/Core

Length of output: 39788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

provider="./Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs"
cat -n "$provider" | sed -n '218,270p'

printf '\nLISTENER REGISTRATION\n'
rg -n -C 6 'personnelStatusChangedHandler|PersonnelStatusChanged|AddListener<UserStatusEvent>|UserStatusEvent' \
  "$provider" ./Providers/Resgrid.Providers.Bus.Rabbit ./Core/Resgrid.Model

printf '\nALL AUTO-GENERATED STATUS HANDLING\n'
rg -n -C 5 'message\.AutoGenerated|AutoGenerated.*message|AutoGenerated.*UserStatusEvent|UserStatusEvent.*AutoGenerated' \
  --glob '*.cs' . || true

Repository: Resgrid/Core

Length of output: 30926


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

service = Path("Core/Resgrid.Services/ActionLogsService.cs").read_text()
controller = Path("Web/Resgrid.Web/Areas/User/Controllers/HomeController.cs").read_text()
worker = Path("Workers/Resgrid.Workers.Framework/Logic/StatusScheduleLogic.cs").read_text()
provider = Path("Providers/Resgrid.Providers.Bus/OutboundEventProvider.cs").read_text()

checks = {
    "department bulk method suppresses": re.search(
        r"SetActionForEntireDepartmentAsync.*?SaveAllActionLogsAsync\(logs,\s*autoGenerated:\s*true\)",
        service, re.S),
    "group bulk method suppresses": re.search(
        r"SetActionForDepartmentGroupAsync.*?SaveAllActionLogsAsync\(logs,\s*autoGenerated:\s*true\)",
        service, re.S),
    "manual department endpoint calls bulk method": re.search(
        r"ResetAllToStandingBy\s*\(\).*?SetActionForEntireDepartmentAsync",
        controller, re.S),
    "manual group endpoint calls bulk method": re.search(
        r"ResetGroupToStandingBy\s*\(int groupId\).*?SetActionForDepartmentGroupAsync",
        controller, re.S),
    "scheduled worker calls department method": re.search(
        r"DepartmentStatusReset.*?SetActionForEntireDepartmentAsync",
        worker, re.S),
    "outbound handler suppresses auto-generated notifications": re.search(
        r"personnelStatusChangedHandler.*?if\s*\(!message\.AutoGenerated\)\s*"
        r"\s*await\s+_outboundQueueProvider\.EnqueueNotification",
        provider, re.S),
}

for name, matched in checks.items():
    print(f"{name}: {'YES' if matched else 'NO'}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: Resgrid/Core

Length of output: 426


Preserve notifications for manual bulk status resets. Both manual reset endpoints pass autoGenerated: true, so OutboundEventProvider skips their user notifications. Pass this flag only for scheduled resets, or add a separate suppression parameter.

🤖 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/ActionLogsService.cs` around lines 348 - 350, Update
the bulk status reset flow around SaveAllActionLogsAsync so manual
department-wide reset operations do not pass autoGenerated: true and continue
producing per-user notifications. Restrict the autoGenerated flag to scheduled
resets, or introduce a separate notification-suppression parameter while
preserving suppression for scheduled resets.

Comment thread Core/Resgrid.Services/ChatChannelService.cs
Comment on lines +12 to +18
public CommandAccessService(
IPermissionsService permissionsService,
IDepartmentsService departmentsService,
IDepartmentGroupsService departmentGroupsService,
IPersonnelRolesService personnelRolesService,
ICacheProvider cacheProvider)
: base(permissionsService, departmentsService, departmentGroupsService, personnelRolesService, cacheProvider)

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 | 🏗️ Heavy lift

Resolve dependencies in the constructor through the configured service locator.

This constructor uses dependency injection for all service dependencies. Resolve these dependencies with Bootstrapper.GetKernel().Resolve<T>() and pass them to the base constructor.

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 `@Core/Resgrid.Services/CommandAccessService.cs` around lines 12 - 18, Update
CommandAccessService’s constructor to remove its injected service parameters and
resolve IPermissionsService, IDepartmentsService, IDepartmentGroupsService,
IPersonnelRolesService, and ICacheProvider via
Bootstrapper.GetKernel().Resolve<T>(), passing the resolved instances to the
base constructor.

Source: Coding guidelines

// permission side, which has no dependency on this service, does not close a DI cycle.
try
{
if (await ServiceLocator.Current.GetInstance<ICommandAccessService>().CanAssistWithCommandAsync(departmentId, userId))

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

Use the required service resolver.

Replace ServiceLocator.Current.GetInstance<T>() with Bootstrapper.GetKernel().Resolve<T>() at each new dependency-resolution site.

  • Core/Resgrid.Services/IncidentCommandService.cs#L482-L482: Resolve ICommandAccessService through Bootstrapper.GetKernel().Resolve<T>().
  • Core/Resgrid.Services/IncidentCommandService.cs#L1075-L1075: Resolve IChatChannelService through Bootstrapper.GetKernel().Resolve<T>().
  • Core/Resgrid.Services/IncidentCommandService.cs#L1116-L1117: Resolve IChatChannelRepository through Bootstrapper.GetKernel().Resolve<T>().

As per coding guidelines, use the Service Locator pattern via Bootstrapper.GetKernel().Resolve<T>().

📍 Affects 1 file
  • Core/Resgrid.Services/IncidentCommandService.cs#L482-L482 (this comment)
  • Core/Resgrid.Services/IncidentCommandService.cs#L1075-L1075
  • Core/Resgrid.Services/IncidentCommandService.cs#L1116-L1117
🤖 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/IncidentCommandService.cs` at line 482, Replace the new
ServiceLocator.Current.GetInstance calls in
Core/Resgrid.Services/IncidentCommandService.cs at lines 482, 1075, and
1116-1117 with Bootstrapper.GetKernel().Resolve<T>() for ICommandAccessService,
IChatChannelService, and IChatChannelRepository respectively; preserve the
surrounding command logic.

Source: Coding guidelines

Comment thread Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs Outdated
Comment on lines +49 to +56
private Task<bool> CanCommandAsync() => _commandAccessService.CanUseCommandAsync(DepartmentId, UserId);

/// <summary>
/// Whether the caller may READ command boards — the same commander gate as everything else on this
/// surface. A dispatcher who needs to work boards is given the command permission too (it grants
/// the assist capability set); dispatch authorization on its own is not a way in.
/// </summary>
private Task<bool> CanReadBoardsAsync() => CanCommandAsync();

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply the command gate to every command route without a capability filter.

CanCommandAsync() is only called by EstablishCommand and three board-read routes. A user denied CommandAppLogin can still call ReopenCommand, GetCommandForCall, and SendMessageToCommand when they hold the broad plan-level policy. Apply the department command gate consistently before these operations.

🤖 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/IncidentCommandController.cs` around
lines 49 - 56, Apply CanCommandAsync as an authorization guard in ReopenCommand,
GetCommandForCall, and SendMessageToCommand before their existing operation
logic, matching EstablishCommand and the board-read routes. Preserve the current
capability-filter checks and return the established unauthorized response when
the command gate denies access.

Comment thread Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml
Comment on lines +787 to +801
$('#DispatchAppLogin').change(function () {
var val = this.value;
$.ajax({
url: resgrid.absoluteBaseUrl + '/User/Security/SetPermission?type=29&perm=' + val,
type: 'GET'
}).done(function (results) {
});
if ($("#DispatchAppLogin").val() === "2") {
$('#dispatchAppLoginNoRolesSpan').hide();
$('#dispatchAppLoginRolesDiv').show();
} else {
$('#dispatchAppLoginNoRolesSpan').show();
$('#dispatchAppLoginRolesDiv').hide();
}
});

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not change access permissions with GET requests.

SetPermission changes department authorization through a query-string GET request. A malicious site can cause an authenticated administrator to request type=29 or type=30 and weaken Dispatch or Command access. Change SetPermission and SetPermissionData to anti-forgery-protected POST actions. Send the verification token from these AJAX calls.

Also applies to: 814-828

🤖 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/wwwroot/js/app/internal/security/resgrid.security.permissions.js`
around lines 787 - 801, Change the permission updates in the DispatchAppLogin
and corresponding Command access handlers to use POST requests, and update the
SetPermission and SetPermissionData actions to require anti-forgery validation.
Include the page’s verification token in both AJAX request payloads while
preserving the existing permission type and value submission.

Task<bool> CanUseCommandAsync(int departmentId, string userId);

/// <summary>Every user in the department who may act as a commander.</summary>
Task<List<string>> GetCommandUserIdsAsync(int departmentId);

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

Mutable collection exposure in ICommandAccessService.cs allows callers to alter the returned List<string>, violating encapsulation. Change the return type of GetCommandUserIdsAsync to Task<IReadOnlyList<string>> to enforce read-only results.

Kody rule violation: Use IReadOnlyList for immutable collections

Prompt for LLM

File Core/Resgrid.Model/Services/ICommandAccessService.cs:

Line 23:

Mutable collection exposure in ICommandAccessService.cs allows callers to alter the returned `List<string>`, violating encapsulation. Change the return type of GetCommandUserIdsAsync to `Task<IReadOnlyList<string>>` to enforce read-only results.

Talk to Kody by mentioning @kody

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

catch (Exception ex)
{
// A cache outage must not stop the backfill — worst case it runs again on the next read.
Logging.LogException(ex);

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

Insufficient error logging in ChatChannelService.cs omits operation names and identifiers during cache-read failures. Include structured context, such as command.IncidentCommandId and markerKey, within the Logging.LogException call to satisfy Rule [3] and improve diagnosability.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 771:

Insufficient error logging in ChatChannelService.cs omits operation names and identifiers during cache-read failures. Include structured context, such as `command.IncidentCommandId` and `markerKey`, within the `Logging.LogException` call to satisfy Rule [3] and improve diagnosability.

Talk to Kody by mentioning @kody

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

if (string.IsNullOrWhiteSpace(incidentCommandId))
return false;

var affected = await _chatChannelRepository.SetArchivedByIncidentCommandIdAsync(incidentCommandId, archived, archived ? DateTime.UtcNow : (DateTime?)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 low

Unsafe type casting violates the 'Use safe type casting with as operator' team rule. Implement the as operator or pattern matching to guard against null results before usage.

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 862:

Unsafe type casting violates the 'Use safe type casting with as operator' team rule. Implement the `as` operator or pattern matching to guard against null results before usage.

Talk to Kody by mentioning @kody

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

if (command.Status != (int)IncidentCommandStatus.Active)
return;

var markerKey = $"chat:incidentbackfill:{command.IncidentCommandId}";

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

Hardcoded cache key prefix in ChatChannelService.cs introduces typo risks and violates Rule [6]. Extract 'chat:incidentbackfill:' into a private constant string to ensure consistency across cache operations.

Kody rule violation: Centralize string constants

Prompt for LLM

File Core/Resgrid.Services/ChatChannelService.cs:

Line 761:

Hardcoded cache key prefix in ChatChannelService.cs introduces typo risks and violates Rule [6]. Extract `'chat:incidentbackfill:'` into a private constant string to ensure consistency across cache operations.

Talk to Kody by mentioning @kody

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

Comment on lines +199 to +200
foreach (var dispatcherId in await _dispatchAccessService.GetDispatchUserIdsAsync(channel.DepartmentId))
AddIfSet(userIds, dispatcherId);

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

Performance bottleneck in ResolveChannelAudienceUserIdsAsync triggers a full department member load with N+1 user lookups via GetDispatchUserIdsAsync on every message send. This hot path scales with department size and requires caching or batching to avoid database exhaustion.

Prompt for LLM

File Core/Resgrid.Services/ChatPermissionService.cs:

Line 199 to 200:

Performance bottleneck in ResolveChannelAudienceUserIdsAsync triggers a full department member load with N+1 user lookups via GetDispatchUserIdsAsync on every message send. This hot path scales with department size and requires caching or batching to avoid database exhaustion.

Talk to Kody by mentioning @kody

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

/// Deliberately derived from the lanes on each check rather than stored as membership — a lead who
/// is replaced on the board loses the channel without anyone having to remember to remove them.
/// </summary>
private async Task<bool> IsLaneLeadOrCommanderAsync(int departmentId, int callId, string 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 low

DRY violation in ChatPermissionService.cs duplicates the fetch-command and node-filtering pattern across IsLaneLeadOrCommanderAsync and AddLaneLeadsAsync. Extract a helper method, such as GetLaneLeadUserIdsAsync, to centralize the enumeration of commander and lead user IDs.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File Core/Resgrid.Services/ChatPermissionService.cs:

Line 541:

DRY violation in ChatPermissionService.cs duplicates the fetch-command and node-filtering pattern across IsLaneLeadOrCommanderAsync and AddLaneLeadsAsync. Extract a helper method, such as GetLaneLeadUserIdsAsync, to centralize the enumeration of commander and lead user IDs.

Talk to Kody by mentioning @kody

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

Comment on lines +556 to +558
return nodes.Any(n => !n.DeletedOn.HasValue &&
(string.Equals(n.PrimaryLeadUserId, userId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(n.SecondaryLeadUserId, userId, StringComparison.OrdinalIgnoreCase)));

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 LINQ predicates in ChatPermissionService.cs scatter the definition of an active node across multiple calls. Factor the !n.DeletedOn.HasValue check into a reusable helper or expression to maintain a single source of truth.

Kody rule violation: Extract common query logic

Prompt for LLM

File Core/Resgrid.Services/ChatPermissionService.cs:

Line 556 to 558:

Repeated LINQ predicates in ChatPermissionService.cs scatter the definition of an active node across multiple calls. Factor the `!n.DeletedOn.HasValue` check into a reusable helper or expression to maintain 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.

Comment on lines +129 to +133
foreach (var member in active)
{
if (await IsAllowedAsync(departmentId, member.UserId))
allowed.Add(member.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

N+1 query bottleneck in PermissionGateServiceBase.cs issues 5 separate database queries per active member during IsAllowedAsync evaluation. Implement a bulk evaluation method or Task.WhenAll batching to fetch all permissions, memberships, groups, and roles simultaneously.

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

Prompt for LLM

File Core/Resgrid.Services/PermissionGateServiceBase.cs:

Line 129 to 133:

N+1 query bottleneck in PermissionGateServiceBase.cs issues 5 separate database queries per active member during IsAllowedAsync evaluation. Implement a bulk evaluation method or `Task.WhenAll` batching to fetch all permissions, memberships, groups, and roles simultaneously.

Talk to Kody by mentioning @kody

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

Comment on lines +129 to +133
foreach (var member in active)
{
if (await IsAllowedAsync(departmentId, member.UserId))
allowed.Add(member.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

Excessive database round-trips in PermissionGateServiceBase.cs stem from per-member loops triggering individual lookups for permissions, memberships, groups, and roles. Eager-load the department data in bulk queries and evaluate eligibility in-memory to eliminate the N+1 query issue.

Kody rule violation: Optimize database queries with JOINs

Prompt for LLM

File Core/Resgrid.Services/PermissionGateServiceBase.cs:

Line 129 to 133:

Excessive database round-trips in PermissionGateServiceBase.cs stem from per-member loops triggering individual lookups for permissions, memberships, groups, and roles. Eager-load the department data in bulk queries and evaluate eligibility in-memory to eliminate the N+1 query issue.

Talk to Kody by mentioning @kody

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


return await x.QueryAsync<SystemAudit>(sql: query,
param: dynamicParameters,
transaction: _unitOfWork.Transaction);

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

NullReferenceException risk in SystemAuditRepository.cs arises when line 149 evaluates _unitOfWork as null, causing the selectFunction lambda to dereference _unitOfWork.Transaction unsafely. Apply a null-conditional operator to the transaction access or ensure the _unitOfWork instance is guaranteed non-null.

Kody rule violation: Add null checks to prevent NullReferenceException

Prompt for LLM

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

Line 145:

NullReferenceException risk in SystemAuditRepository.cs arises when line 149 evaluates `_unitOfWork` as null, causing the `selectFunction` lambda to dereference `_unitOfWork.Transaction` unsafely. Apply a null-conditional operator to the transaction access or ensure the `_unitOfWork` instance is guaranteed non-null.

Talk to Kody by mentioning @kody

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

=> _channelRepository.Setup(x => x.GetByCallIdAsync(CallId)).ReturnsAsync(new List<ChatChannel>(channels));

[Test]
public async Task an_incident_with_no_channels_gets_the_full_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

Naming convention violation in ChatIncidentBackfillTests.cs uses snake_case for test methods. Rename methods like an_incident_with_no_channels_gets_the_full_set to PascalCase to comply with Rule [7].

Kody rule violation: Use proper naming conventions

Prompt for LLM

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

Line 92:

Naming convention violation in ChatIncidentBackfillTests.cs uses snake_case for test methods. Rename methods like `an_incident_with_no_channels_gets_the_full_set` to PascalCase to comply with Rule [7].

Talk to Kody by mentioning @kody

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

var nextCalled = await Invoke(filter, context);

nextCalled.Should().BeFalse();
context.Result.Should().BeOfType<ObjectResult>()

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

Async method blocking violates the 'Avoid Blocking Calls to Async Methods' team rule. Replace .Result or .Wait() with await to prevent deadlocks and ensure proper asynchronous execution.

Prompt for LLM

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

Line 294:

Async method blocking violates the 'Avoid Blocking Calls to Async Methods' team rule. Replace `.Result` or `.Wait()` with `await` to prevent deadlocks and ensure proper asynchronous execution.

Talk to Kody by mentioning @kody

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

var nextCalled = await Invoke(filter, context);

nextCalled.Should().BeFalse();
context.Result.Should().BeOfType<ObjectResult>()

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

Async operation blocking violates the 'Await async operations properly' team rule. Await Tasks end-to-end instead of using .Result or .Wait() to prevent deadlocks and ensure efficient asynchronous execution.

Prompt for LLM

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

Line 294:

Async operation blocking violates the 'Await async operations properly' team rule. Await Tasks end-to-end instead of using `.Result` or `.Wait()` to prevent deadlocks and ensure efficient asynchronous execution.

Talk to Kody by mentioning @kody

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

if (input == null || input.CallId <= 0)
return BadRequest();

if (!await CanCommandAsync())

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 exception risk in IncidentCommandController.cs occurs when the external CanCommandAsync() database call rejects without a try/catch block. Wrap the authorization check to log contextual identifiers like DepartmentId and return a structured error response instead of an opaque 500.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

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

Line 74:

Unhandled exception risk in IncidentCommandController.cs occurs when the external CanCommandAsync() database call rejects without a try/catch block. Wrap the authorization check to log contextual identifiers like `DepartmentId` and return a structured error response instead of an opaque 500.

Talk to Kody by mentioning @kody

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

Comment on lines +74 to +75
if (!await CanCommandAsync())
return Unauthorized();

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

Conflicting HTTP status codes occur when EstablishCommand returns 401 Unauthorized for an authenticated user lacking commander permissions, whereas RequiresIncidentCapabilityAttribute correctly returns 403 Forbidden. Return Forbid() to align with the capability filter and accurately indicate an authorization failure.

if (!await CanCommandAsync())
    return Forbid();
Prompt for LLM

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

Line 74 to 75:

Conflicting HTTP status codes occur when EstablishCommand returns 401 Unauthorized for an authenticated user lacking commander permissions, whereas RequiresIncidentCapabilityAttribute correctly returns 403 Forbidden. Return Forbid() to align with the capability filter and accurately indicate an authorization failure.

Suggested Code:

if (!await CanCommandAsync())
    return Forbid();

Talk to Kody by mentioning @kody

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

// because a member the department hasn't authorized as a commander shouldn't reach the board
// surface whatever ICS role happens to be recorded against them.
var commandAccess = context.HttpContext.RequestServices?.GetService(typeof(ICommandAccessService)) as ICommandAccessService;
if (commandAccess != null && !await commandAccess.CanUseCommandAsync(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 external call in RequiresIncidentCapabilityAttribute.cs bypasses Rule [27] by omitting a try/catch around CanUseCommandAsync. Wrap the database call to map failures to application-level errors and log structured context such as departmentId and userId.

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

Prompt for LLM

File Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs:

Line 63:

Unhandled external call in RequiresIncidentCapabilityAttribute.cs bypasses Rule [27] by omitting a try/catch around CanUseCommandAsync. Wrap the database call to map failures to application-level errors and log structured context such as `departmentId` and `userId`.

Talk to Kody by mentioning @kody

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

if (permissions.Any(x => x.PermissionType == (int)PermissionTypes.DispatchAppLogin))
model.DispatchAppLogin = permissions.First(x => x.PermissionType == (int)PermissionTypes.DispatchAppLogin).Action;
else
model.DispatchAppLogin = 3;

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 number duplication in SecurityController.cs uses unexplained literal 3 for default 'Everyone' permissions. Extract a named constant or reuse the mapped enum value to prevent error-prone changes.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

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

Line 401:

Magic number duplication in SecurityController.cs uses unexplained literal `3` for default 'Everyone' permissions. Extract a named constant or reuse the mapped enum value to prevent error-prone changes.

Talk to Kody by mentioning @kody

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

model.DispatchAppLogin = 3;

var dispatchAppLoginPermissions = new List<dynamic>();
dispatchAppLoginPermissions.Add(new { Id = 3, Name = "Everyone" });

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 repetition in SecurityController.cs hardcodes the 'Everyone' permission level across multiple code blocks. Define a constant or enum for permission level names to eliminate duplication and reduce error risks.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

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

Line 404:

Magic string repetition in SecurityController.cs hardcodes the `'Everyone'` permission level across multiple code blocks. Define a constant or enum for permission level names to eliminate duplication and reduce error risks.

Talk to Kody by mentioning @kody

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

public SelectList UseCalendarSyncPermissions { get; set; }

public int DispatchAppLogin { get; set; }
public SelectList DispatchAppLoginPermissions { 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 SelectList property in PermissionsView.cs risks null reference exceptions if used before population. Initialize DispatchAppLoginPermissions with an empty SelectList or assign it within the constructor to comply with Rule [31].

Kody rule violation: Initialize properties with default values

Prompt for LLM

File Web/Resgrid.Web/Areas/User/Models/Security/PermissionsView.cs:

Line 98:

Uninitialized `SelectList` property in PermissionsView.cs risks null reference exceptions if used before population. Initialize DispatchAppLoginPermissions with an empty `SelectList` or assign it within the constructor to comply with Rule [31].

Talk to Kody by mentioning @kody

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

</td>
</tr>
<tr>
<td>Dispatch App Login</td>

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

Internationalization bypass in Index.cshtml hardcodes the "Dispatch App Login" label instead of utilizing the localization pipeline. Apply a resource key using @localizer to comply with Rule [112] and match existing localized strings like PermissionNA.

Kody rule violation: Internationalize user-facing text with next-intl or next-i18next

Prompt for LLM

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

Line 327:

Internationalization bypass in Index.cshtml hardcodes the "Dispatch App Login" label instead of utilizing the localization pipeline. Apply a resource key using `@localizer` to comply with Rule [112] and match existing localized strings like `PermissionNA`.

Talk to Kody by mentioning @kody

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

$('#DispatchAppLogin').change(function () {
var val = this.value;
$.ajax({
url: resgrid.absoluteBaseUrl + '/User/Security/SetPermission?type=29&perm=' + val,

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 violates the 'Use Template Literals Instead of String Concatenation' team rule. Implement template literals to construct the url string, improving readability and reducing error-prone syntax.

Prompt for LLM

File Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js:

Line 790:

String concatenation violates the 'Use Template Literals Instead of String Concatenation' team rule. Implement template literals to construct the `url` string, improving readability and reducing error-prone syntax.

Talk to Kody by mentioning @kody

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

// Dispatch App Login
////////////////////////////////////////////////////////
$('#DispatchAppLogin').change(function () {
var val = this.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 variable scoping in resgrid.security.permissions.js uses var instead of block-scoped declarations. Replace var with const for unassigned variables to adhere to Rule [37] and prevent subtle scoping bugs.

Kody rule violation: Always use const and let

Prompt for LLM

File Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js:

Line 788:

Unsafe variable scoping in resgrid.security.permissions.js uses `var` instead of block-scoped declarations. Replace `var` with `const` for unassigned variables to adhere to Rule [37] and prevent subtle scoping bugs.

Talk to Kody by mentioning @kody

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

if (item.ScheduledTask.TaskType == (int)TaskTypes.UserStaffingLevel)
{
await _userStateService.CreateUserState(item.ScheduledTask.UserId, item.ScheduledTask.DepartmentId, int.Parse(item.ScheduledTask.Data), item.ScheduledTask.Note);
await _userStateService.CreateUserState(item.ScheduledTask.UserId, item.ScheduledTask.DepartmentId, int.Parse(item.ScheduledTask.Data), item.ScheduledTask.Note, autoGenerated: true);

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 violates the 'Use TryParse for string conversions' team rule. Utilize int.TryParse for item.ScheduledTask.Data to handle invalid user or IO input safely and validate the format.

Prompt for LLM

File Workers/Resgrid.Workers.Framework/Logic/StaffingScheduleLogic.cs:

Line 36:

Unsafe string conversion violates the 'Use TryParse for string conversions' team rule. Utilize `int.TryParse` for `item.ScheduledTask.Data` to handle invalid user or IO input safely and validate the format.

Talk to Kody by mentioning @kody

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

@Resgrid-Bot

This comment has been minimized.

@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.

Caution

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

⚠️ Outside diff range comments (3)
Core/Resgrid.Services/ChatChannelService.cs (2)

709-748: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Include the new channel types in implicit-audience channel discovery.

GetChannelsForUserAsync only evaluates Incident, IncidentLane, and IncidentCommand channel types. It skips IncidentLeads and IncidentDispatch. These provisioning methods do not add explicit members, so eligible leads and dispatch users will not receive the new channels in their normal channel list.

Proposed fix
- if (type != ChatChannelType.CustomLocked && type != ChatChannelType.Incident &&
-     type != ChatChannelType.IncidentLane && type != ChatChannelType.IncidentCommand)
+ if (type != ChatChannelType.CustomLocked && type != ChatChannelType.Incident &&
+     type != ChatChannelType.IncidentLane && type != ChatChannelType.IncidentCommand &&
+     type != ChatChannelType.IncidentLeads && type != ChatChannelType.IncidentDispatch)
    continue;
🤖 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/ChatChannelService.cs` around lines 709 - 748, Update
GetChannelsForUserAsync to include ChatChannelType.IncidentLeads and
ChatChannelType.IncidentDispatch in its implicit-audience channel discovery
alongside the existing Incident, IncidentLane, and IncidentCommand types, so
eligible users receive channels provisioned by EnsureLeadsChannelAsync and
EnsureDispatchChannelAsync.

781-846: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use cache-aside retrieval and support cache bypass for incident backfill.

GetStringAsync and SetStringAsync bypass the required RetrieveAsync<T>() cache-aside flow. The method also has no bypassCache parameter and still accesses the cache when caching is disabled. Move provisioning into a local fallback function, call it through RetrieveAsync, and execute the fallback directly when bypassCache is true or caching is disabled.

As per coding guidelines, “All caching operations must go through ICacheProvider.Retrieve<T>() or ICacheProvider.RetrieveAsync<T>() using the cache-aside pattern with fallback functions,” and service methods should include bypassCache with default false.

🤖 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/ChatChannelService.cs` around lines 781 - 846, Update
EnsureIncidentChannelsAsync to accept a bypassCache parameter defaulting to
false, move the channel provisioning work into a local fallback function, and
invoke it through ICacheProvider.RetrieveAsync<T>() when caching is enabled and
bypassCache is false. Execute the fallback directly when bypassCache is true or
caching is disabled, and remove the direct GetStringAsync/SetStringAsync marker
operations while preserving the existing provisioning behavior.

Source: Coding guidelines

Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs (1)

128-174: ⚠️ Potential issue | 🟠 Major

Remove the remaining null dereference.

GetByTypePagedAsync enters the standalone-connection path when _unitOfWork is null, but QueryAsync still reads _unitOfWork.Transaction. This throws NullReferenceException before the query runs. Use _unitOfWork?.Transaction, or reject null unitOfWork in the constructor and remove the nullable path. Apply the same contract to all paged methods.

🤖 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/SystemAuditRepository.cs`
around lines 128 - 174, Update GetByTypePagedAsync and every other paged
repository method to pass _unitOfWork?.Transaction to QueryAsync when supporting
standalone connections, preventing dereferences when _unitOfWork is null.
Alternatively, enforce a non-null unit-of-work contract in the constructor and
remove the nullable connection path consistently across all paged methods.
🧹 Nitpick comments (1)
Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs (1)

35-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve ICommandAccessService through the required service locator.

Remove ICommandAccessService from the constructor parameters. Resolve it with Bootstrapper.GetKernel().Resolve<ICommandAccessService>() in the constructor.

As per coding guidelines, use “Service Locator pattern via 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.Services/Controllers/v4/IncidentCommandController.cs` around
lines 35 - 41, Update IncidentCommandController by removing
ICommandAccessService from the constructor parameters and resolving it inside
the constructor via Bootstrapper.GetKernel().Resolve<ICommandAccessService>(),
assigning the result to _commandAccessService while leaving the other injected
services unchanged.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@Core/Resgrid.Services/ChatChannelService.cs`:
- Around line 709-748: Update GetChannelsForUserAsync to include
ChatChannelType.IncidentLeads and ChatChannelType.IncidentDispatch in its
implicit-audience channel discovery alongside the existing Incident,
IncidentLane, and IncidentCommand types, so eligible users receive channels
provisioned by EnsureLeadsChannelAsync and EnsureDispatchChannelAsync.
- Around line 781-846: Update EnsureIncidentChannelsAsync to accept a
bypassCache parameter defaulting to false, move the channel provisioning work
into a local fallback function, and invoke it through
ICacheProvider.RetrieveAsync<T>() when caching is enabled and bypassCache is
false. Execute the fallback directly when bypassCache is true or caching is
disabled, and remove the direct GetStringAsync/SetStringAsync marker operations
while preserving the existing provisioning behavior.

In `@Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs`:
- Around line 128-174: Update GetByTypePagedAsync and every other paged
repository method to pass _unitOfWork?.Transaction to QueryAsync when supporting
standalone connections, preventing dereferences when _unitOfWork is null.
Alternatively, enforce a non-null unit-of-work contract in the constructor and
remove the nullable connection path consistently across all paged methods.

---

Nitpick comments:
In `@Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs`:
- Around line 35-41: Update IncidentCommandController by removing
ICommandAccessService from the constructor parameters and resolving it inside
the constructor via Bootstrapper.GetKernel().Resolve<ICommandAccessService>(),
assigning the result to _commandAccessService while leaving the other injected
services unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d58d7bd-d969-421a-9ecb-e3d39f9b1716

📥 Commits

Reviewing files that changed from the base of the PR and between 946e92e and 3654b20.

⛔ Files ignored due to path filters (12)
  • Core/Resgrid.Localization/Areas/User/Security/Security.ar.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.de.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.en.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.es.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.fr.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.it.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.pl.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.sv.resx is excluded by !**/*.resx
  • Core/Resgrid.Localization/Areas/User/Security/Security.uk.resx is excluded by !**/*.resx
  • Tests/Resgrid.Tests/Services/ChatIncidentBackfillTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/DispatchAccessServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Web/Services/RequiresIncidentCapabilityFilterTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (12)
  • Core/Resgrid.Model/Repositories/IChatRepositories.cs
  • Core/Resgrid.Model/Services/IActionLogsService.cs
  • Core/Resgrid.Services/ChatChannelService.cs
  • Core/Resgrid.Services/PermissionGateServiceBase.cs
  • Repositories/Resgrid.Repositories.DataRepository/ChatRepositories.cs
  • Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByDepartmentIdPagedQuery.cs
  • Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByTypePagedQuery.cs
  • Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByUserIdPagedQuery.cs
  • Repositories/Resgrid.Repositories.DataRepository/SystemAuditRepository.cs
  • Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs
  • Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs
  • Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml
🚧 Files skipped from review as they are similar to previous changes (5)
  • Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs
  • Core/Resgrid.Services/PermissionGateServiceBase.cs
  • Repositories/Resgrid.Repositories.DataRepository/Queries/SystemAudits/SelectSystemAuditsByTypePagedQuery.cs
  • Core/Resgrid.Model/Services/IActionLogsService.cs
  • Web/Resgrid.Web/Areas/User/Views/Security/Index.cshtml

@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.Config/CorsHelper.cs`:
- Around line 127-134: Replace the partial _publicRegistrySuffixes check in the
parent-domain widening loop with a maintained Public Suffix List implementation
that includes private suffixes, or remove automatic parent widening and require
sibling origins through ApiConfig.CorsAllowedOrigins. Ensure shared-hosting
domains such as github.io cannot widen to an attacker-controlled sibling, and
add regression coverage for these suffixes.
🪄 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: 1b280f7c-5e8c-40e1-8b22-d3ac8e845f67

📥 Commits

Reviewing files that changed from the base of the PR and between 3654b20 and c6c7511.

⛔ Files ignored due to path filters (1)
  • Tests/Resgrid.Tests/Config/CorsHelperTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (4)
  • Core/Resgrid.Config/ApiConfig.cs
  • Core/Resgrid.Config/CorsHelper.cs
  • Web/Resgrid.Web.Eventing/Startup.cs
  • Web/Resgrid.Web.Services/Startup.cs

Comment thread Core/Resgrid.Config/CorsHelper.cs
}
catch (Exception ex)
{
Logging.LogException(ex);

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

Insufficient error logging in the catch block. The call to Logging.LogException(ex) lacks the operation name and relevant identifiers (chatChannelId, incidentCommandId) as structured fields, making failures untraceable. Pass structured context, e.g. Logging.LogException(ex, new { op = "RebindToIncidentCommand", chatChannelId, incidentCommandId }).

Kody rule violation: Include error context in structured logs

Prompt for LLM

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

Line 563:

Insufficient error logging in the catch block. The call to `Logging.LogException(ex)` lacks the operation name and relevant identifiers (`chatChannelId`, `incidentCommandId`) as structured fields, making failures untraceable. Pass structured context, e.g. `Logging.LogException(ex, new { op = "RebindToIncidentCommand", chatChannelId, incidentCommandId })`.

Talk to Kody by mentioning @kody

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

// this gate is asked about a channel's department, not necessarily the caller's own.
GivenPermission(null);
_departmentsService.Setup(x => x.GetDepartmentMemberAsync("stranger", DepartmentId, It.IsAny<bool>()))
.ReturnsAsync((DepartmentMember)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 low

Unsafe type casting detected. 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 Tests/Resgrid.Tests/Services/DispatchAccessServiceTests.cs:

Line 120:

Unsafe type casting detected. 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 nextCalled = await Invoke(filter, context);

nextCalled.Should().BeFalse();
context.Result.Should().BeOfType<StatusCodeResult>()

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 call to async method identified. Blocking async methods with .Result or .Wait() can cause deadlocks and prevent efficient asynchronous execution; use await instead.

Kody rule violation: Avoid Blocking Calls to Async Methods

Prompt for LLM

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

Line 293:

Blocking call to async method identified. Blocking async methods with `.Result` or `.Wait()` can cause deadlocks and prevent efficient asynchronous execution; use `await` instead.

Talk to Kody by mentioning @kody

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

var nextCalled = await Invoke(filter, context);

nextCalled.Should().BeFalse();
context.Result.Should().BeOfType<StatusCodeResult>()

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 found. Await Tasks instead of blocking with .Result or .Wait(), and prefer async/await end-to-end with appropriate await configuration.

Kody rule violation: Await async operations properly

Prompt for LLM

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

Line 293:

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

Talk to Kody by mentioning @kody

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

return;
}

if (!await commandAccess.CanUseCommandAsync(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 exception risk in the awaited call commandAccess.CanUseCommandAsync(departmentId, userId). If the service throws a database timeout or connection failure, the exception propagates unhandled and surfaces a raw 500 without context. Wrap the await in try/catch, log with structured context (departmentId, userId), and set an appropriate error result.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs:

Line 72:

Unhandled exception risk in the awaited call `commandAccess.CanUseCommandAsync(departmentId, userId)`. If the service throws a database timeout or connection failure, the exception propagates unhandled and surfaces a raw 500 without context. Wrap the await in try/catch, log with structured context (`departmentId`, `userId`), and set an appropriate error result.

Talk to Kody by mentioning @kody

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

return;
}

if (!await commandAccess.CanUseCommandAsync(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

Missing error boundary around the external service call CanUseCommandAsync. Transient database failures surface as unstructured exceptions rather than mapped application-level errors. Wrap the call in try/catch, add operation context (departmentId, userId) to the log, and return a mapped error result on failure.

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

Prompt for LLM

File Web/Resgrid.Web.Services/Filters/RequiresIncidentCapabilityAttribute.cs:

Line 72:

Missing error boundary around the external service call `CanUseCommandAsync`. Transient database failures surface as unstructured exceptions rather than mapped application-level errors. Wrap the call in try/catch, add operation context (`departmentId`, `userId`) to the log, and return a mapped error result on failure.

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 11, 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.

}

[Test]
public void should_not_widen_the_parent_domain_past_a_shared_hosting_suffix()

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

Naming convention violation in Tests/Resgrid.Tests/Config/CorsHelperTests.cs where the test method uses snake_case instead of standard .NET PascalCase. Rename the method to ShouldNotWidenTheParentDomainPastASharedHostingSuffix.

Kody rule violation: Use proper naming conventions

Prompt for LLM

File Tests/Resgrid.Tests/Config/CorsHelperTests.cs:

Line 87:

Naming convention violation in `Tests/Resgrid.Tests/Config/CorsHelperTests.cs` where the test method uses snake_case instead of standard .NET PascalCase. Rename the method to `ShouldNotWidenTheParentDomainPastASharedHostingSuffix`.

Talk to Kody by mentioning @kody

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

@ucswift

ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift
ucswift merged commit 93a86a0 into master Aug 11, 2026
16 of 18 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 11, 2026
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.

2 participants