Skip to content

RIC-T39 More IC work#435

Merged
ucswift merged 2 commits into
masterfrom
develop
Jul 22, 2026
Merged

RIC-T39 More IC work#435
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jul 22, 2026

Copy link
Copy Markdown
Member

PR Description

This PR delivers a substantial set of Incident Command (IC) feature enhancements, data model extensions, and auditability improvements across the service layer, data layer, and API controllers.

Key Changes

Command Lifecycle

  • Adds the ability to reopen a previously closed command with a caller-supplied reason, guarded against conflicts when another command is already active on the same call.
  • Introduces a read-only guard for ended/closed commands — all mutation operations (resource moves, note/attachment/role changes, objective completion, need updates, annotations, timers, etc.) are rejected when the parent command is not active.
  • Adds a command board-by-ID endpoint for viewing a specific closed command's history without pollution from a newer command on the same call.

Incident Information & Locations

  • Adds an incident-level display name and free-text + geocoded locations for Command Post (ICP/HQ), Staging, and Rehab. Location text without coordinates is automatically forward-geocoded on save.
  • Adds a new command info update endpoint consolidating name, corrected start time, estimated end, important information, ICS level, and all three locations into a single call.

Incident List Summaries

  • Adds a list-card command summaries endpoint returning lifecycle timing, resolved commander name, locations, and active unit/personnel counts for the department (active-only or including closed).

Entity-Category Needs

  • Introduces a new need category for requesting specific units, users, roles, or groups. Requested entities are individually dispatched onto the call (no full re-dispatch), tagged as "Requested by Incident Command." When those entities respond, their status is logged to the incident timeline.

Need Fulfillment Audit Trail

  • Adds an append-only audit table (IncidentNeedUpdates) capturing every status and fill-quantity change (including reductions) with an optional note, author, and timestamp. New endpoint exposes the audit history per need.

Tactical Objective Close-Out

  • Adds an outcome (Successful/Partial/Unsuccessful) and optional completion note recorded when objectives are completed. Reaching 100% via progress updates auto-closes as Successful.

Incident Maps

  • Adds a saved map view (center + zoom) on the incident command for consistent tactical-map framing.
  • Adds support for named tactical maps beyond the main map, with their own framing, description, optional expiry, and markup linkage. Lanes can attach to a specific named map.

Timeline Attribution

  • All timeline log entries now carry human-readable labels (full name + group, ICS standing) instead of raw user IDs/GUIDs, applied across role assignments, resource moves/releases, note additions, objective completions, need changes, and map edits.
  • PAR/accountability rows are enriched with resolved member names for display.

Database & Tests

  • Adds migrations M0094–M0099 (SQL Server and PostgreSQL) for new columns and tables (IncidentNeedUpdates, IncidentMaps, IncidentNeedEntities).
  • Adds and updates unit tests covering command reopen, info updates with geocoding, need audit/entity dispatch, objective outcomes, and map management.

Summary by CodeRabbit

  • New Features
    • Added incident command lists, call-based retrieval, board history, reopening, and detailed command information updates.
    • Added named tactical maps, saved map views, map annotations, and map management.
    • Added entity-based needs requests, dispatch tracking, fulfillment notes, and update history.
    • Added tactical objective outcomes and completion notes.
  • Bug Fixes
    • Restricted updates to active commands and improved audit records, labels, and location handling.
    • Improved incident need status tracking from personnel and unit status updates.
  • Documentation
    • Expanded API documentation for new incident command, needs, objectives, and map capabilities.

@Resgrid-Bot

This comment has been minimized.

@request-info

request-info Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

@coderabbitai

coderabbitai Bot commented Jul 22, 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

Run ID: 8ea4461b-b0a5-4aae-b515-90ab956776aa

📥 Commits

Reviewing files that changed from the base of the PR and between c3c1fed and 4a236ca.

📒 Files selected for processing (5)
  • Core/Resgrid.Services/IncidentCommandService.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0094_AddIncidentCommandNameAndLocationsPg.cs
  • Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/PersonnelStatusesController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/UnitStatusController.cs
🚧 Files skipped from review as they are similar to previous changes (5)
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0094_AddIncidentCommandNameAndLocationsPg.cs
  • Web/Resgrid.Web.Services/Controllers/v4/UnitStatusController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/PersonnelStatusesController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs
  • Core/Resgrid.Services/IncidentCommandService.cs

📝 Walkthrough

Walkthrough

Incident command support expands across reopening, metadata and location updates, objective close-out outcomes, entity-based needs, tactical maps, audit storage, synchronization, and v4 API endpoints.

Changes

Incident command expansion

Layer / File(s) Summary
Domain contracts and data shapes
Core/Resgrid.Model/IncidentCommand/*, Core/Resgrid.Model/Services/*, Core/Resgrid.Model/Events/*, Web/Resgrid.Web.Services/Models/v4/IncidentCommand/*
Adds models, enums, events, repository contracts, service signatures, request DTOs, and response wrappers for reopening, objective outcomes, entity needs, audits, and tactical maps.
Persistence and dependency wiring
Providers/Resgrid.Providers.Migrations*/Migrations/*, Repositories/Resgrid.Repositories.DataRepository/*
Adds database schemas, indexes, rollback logic, repositories, and dependency registrations for maps, need updates, and requested entities.
Incident command service workflows
Core/Resgrid.Services/IncidentCommandService.cs
Implements lifecycle checks, reopening, enriched timeline entries, objective close-out data, need auditing and dispatch, map CRUD/view updates, synchronization, and location geocoding.
v4 API and status integrations
Web/Resgrid.Web.Services/Controllers/v4/*, Web/Resgrid.Web.Services/Resgrid.Web.Services.xml
Adds command, need, objective, and map endpoints; status controllers record entity responses; API documentation is updated.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.81% 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 is vague and generic, and it doesn’t clearly describe the incident command changes in the PR. Use a specific title that names the main change, such as incident command reopen, maps, needs, and objective updates.
✅ 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
📝 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.


public async Task<IncidentMap> SaveIncidentMapAsync(IncidentMap map, string userId, CancellationToken cancellationToken = default(CancellationToken))
{
if (map == null || string.IsNullOrWhiteSpace(map.IncidentCommandId) || string.IsNullOrWhiteSpace(map.Name))
try
{
var entities = input.Entities.Select(e => new IncidentNeedEntity { EntityKind = e.EntityKind, EntityId = e.EntityId }).ToList();
var need = await _incidentCommandService.RequestNeedEntitiesAsync(DepartmentId, input.IncidentCommandId, input.Name, input.Description, entities, UserId, CancellationToken.None);
map.DepartmentId = DepartmentId;

try
{

@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: 2

Caution

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

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

850-858: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep CompleteObjective accepting no body as intended.

The endpoint defaults input to null, but the current ASP.NET Core configuration still requires a non-empty body for [FromBody] parameters, so clients sending no body can now get 400 "A non-empty request body is required." Use an optional binding strategy or the configured empty-body opt-in so this backward-compatible call path still works.

🤖 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 850 - 858, Update CompleteObjective’s [FromBody] binding so an omitted
request body is accepted, using the project’s configured empty-body opt-in or
established optional binding approach. Preserve the existing null handling and
TacticalObjectiveOutcome.NotSet behavior when input is absent.
🧹 Nitpick comments (1)
Core/Resgrid.Services/IncidentCommandService.cs (1)

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

Avoid the redundant second write on map create. On the create path UpsertOwnedAsync inserts the row with CreatedByUserId = null and CreatedOn = default(DateTime), then this method backfills those fields and issues a second SaveOrUpdateAsync. Stamping the audit fields on map before the upsert (letting the preserve callback keep the stored values on the update path) removes the extra round-trip and the transient min-value CreatedOn.

♻️ Proposed refactor
 			map.CallId = command.CallId;
 			map.Name = TrimToLength(map.Name, 500);
 			map.Description = TrimToLength(map.Description, 2000);
+			map.CreatedByUserId = userId;
+			map.CreatedOn = DateTime.UtcNow;

 			var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.DepartmentId,
 				e => e.DepartmentId, (stored, incoming) =>
 				{
 					incoming.CreatedByUserId = stored.CreatedByUserId;
 					incoming.CreatedOn = stored.CreatedOn;
 					incoming.DeletedOn = stored.DeletedOn;
 					incoming.UpdatedByUserId = userId;
 					incoming.UpdatedOn = DateTime.UtcNow;
 				}, cancellationToken);
 			if (rejected)
 				return null;
 			map = saved;
-
-			if (isNew)
-			{
-				map.CreatedByUserId = userId;
-				map.CreatedOn = DateTime.UtcNow;
-				map = await _incidentMapRepository.SaveOrUpdateAsync(Touch(map), cancellationToken);
-			}
🤖 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` around lines 2446 - 2489,
Update SaveIncidentMapAsync to assign CreatedByUserId, CreatedOn,
UpdatedByUserId, and UpdatedOn on map before calling UpsertOwnedAsync, using
userId and DateTime.UtcNow. Remove the isNew-only SaveOrUpdateAsync block, while
preserving the existing preserve callback so updates retain stored creation
fields.
🤖 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 `@Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs`:
- Around line 946-976: In RequestNeedEntities, validate every input EntityKind
with Enum.IsDefined(typeof(NeedEntityKind), e.EntityKind) before constructing
IncidentNeedEntity objects. Reject the request with BadRequest when any kind is
undefined, while preserving the existing entity mapping and service call for
valid values.

In `@Web/Resgrid.Web.Services/Controllers/v4/UnitStatusController.cs`:
- Around line 285-299: Update the RecordNeedEntityStatusAsync call in
ProcessSetUnitState to pass the existing cancellationToken argument, matching
the equivalent SavePersonStatus call while preserving the current best-effort
exception handling.

---

Outside diff comments:
In `@Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs`:
- Around line 850-858: Update CompleteObjective’s [FromBody] binding so an
omitted request body is accepted, using the project’s configured empty-body
opt-in or established optional binding approach. Preserve the existing null
handling and TacticalObjectiveOutcome.NotSet behavior when input is absent.

---

Nitpick comments:
In `@Core/Resgrid.Services/IncidentCommandService.cs`:
- Around line 2446-2489: Update SaveIncidentMapAsync to assign CreatedByUserId,
CreatedOn, UpdatedByUserId, and UpdatedOn on map before calling
UpsertOwnedAsync, using userId and DateTime.UtcNow. Remove the isNew-only
SaveOrUpdateAsync block, while preserving the existing preserve callback so
updates retain stored creation fields.
🪄 Autofix (Beta)

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

Run ID: 4c290382-decc-405b-9891-12740e8fda5b

📥 Commits

Reviewing files that changed from the base of the PR and between 60db890 and c3c1fed.

⛔ Files ignored due to path filters (4)
  • Tests/Resgrid.Tests/Services/IncidentCommandContentServiceTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/IncidentCommandInfoAndReopenTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/IncidentCommandNeedsAndLeadsTests.cs is excluded by !**/Tests/**
  • Tests/Resgrid.Tests/Services/IncidentCommandServiceParTests.cs is excluded by !**/Tests/**
📒 Files selected for processing (33)
  • Core/Resgrid.Model/Events/IncidentCommandEvents.cs
  • Core/Resgrid.Model/IncidentCommand/CommandStructureNode.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentCommand.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentCommandBoard.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentCommandChanges.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentCommandEnums.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentCommandSummary.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentMap.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentNeed.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentNeedEntity.cs
  • Core/Resgrid.Model/IncidentCommand/IncidentTacticals.cs
  • Core/Resgrid.Model/Repositories/IIncidentCommandRepositories.cs
  • Core/Resgrid.Model/Services/IIncidentCommandService.cs
  • Core/Resgrid.Services/IncidentCommandService.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0094_AddIncidentCommandNameAndLocations.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0095_AddIncidentNeedUpdates.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0096_AddObjectiveOutcome.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0097_AddIncidentMapView.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0098_AddIncidentMaps.cs
  • Providers/Resgrid.Providers.Migrations/Migrations/M0099_AddIncidentNeedEntities.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0094_AddIncidentCommandNameAndLocationsPg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0095_AddIncidentNeedUpdatesPg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0096_AddObjectiveOutcomePg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0097_AddIncidentMapViewPg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0098_AddIncidentMapsPg.cs
  • Providers/Resgrid.Providers.MigrationsPg/Migrations/M0099_AddIncidentNeedEntitiesPg.cs
  • Repositories/Resgrid.Repositories.DataRepository/IncidentCommandRepositories.cs
  • Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
  • Web/Resgrid.Web.Services/Controllers/v4/IncidentCommandController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/PersonnelStatusesController.cs
  • Web/Resgrid.Web.Services/Controllers/v4/UnitStatusController.cs
  • Web/Resgrid.Web.Services/Models/v4/IncidentCommand/IncidentCommandModels.cs
  • Web/Resgrid.Web.Services/Resgrid.Web.Services.xml

Comment thread Web/Resgrid.Web.Services/Controllers/v4/UnitStatusController.cs
public List<IncidentMapAnnotation> Annotations { get; set; } = new List<IncidentMapAnnotation>();

/// <summary>Named tactical maps for the incident (the main map lives on the Command itself).</summary>
public List<IncidentMap> Maps { get; set; } = new List<IncidentMap>();

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: the Maps property publicly exposes a mutable List<IncidentMap>, allowing callers to alter the underlying collection unexpectedly. Expose the collection as IReadOnlyList<IncidentMap> while keeping the internal mutable list for initialization.

Kody rule violation: Use IReadOnlyList for immutable collections

Prompt for LLM

File Core/Resgrid.Model/IncidentCommand/IncidentCommandBoard.cs:

Line 26:

Mutable collection exposure: the `Maps` property publicly exposes a mutable `List<IncidentMap>`, allowing callers to alter the underlying collection unexpectedly. Expose the collection as `IReadOnlyList<IncidentMap>` while keeping the internal mutable list for initialization.

Talk to Kody by mentioning @kody

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

public int AnnotationType { get; set; }

/// <summary>The named <see cref="IncidentMap"/> this markup belongs to; null = the incident's main map.</summary>
public string IncidentMapId { 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

Null-initialized string property: IncidentMapId is declared without an initializer, defaulting to null. Null strings invite null-reference exceptions and ambiguous state. Initialize the property with string.Empty or make it explicitly nullable (string?) with documented sentinel semantics.

Kody rule violation: Initialize properties with default values

Prompt for LLM

File Core/Resgrid.Model/IncidentCommand/IncidentTacticals.cs:

Line 152:

Null-initialized string property: `IncidentMapId` is declared without an initializer, defaulting to null. Null strings invite null-reference exceptions and ambiguous state. Initialize the property with `string.Empty` or make it explicitly nullable (`string?`) with documented sentinel semantics.

Talk to Kody by mentioning @kody

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

Comment on lines +802 to +812
foreach (var command in selected)
{
Call call = null;
try
{
call = await _callsService.GetCallByIdAsync(command.CallId);
}
catch (Exception ex)
{
Resgrid.Framework.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 Performance high

N+1 query: GetCallByIdAsync is called individually inside a foreach loop over every selected command, triggering one database round-trip per incident. Batch-load the calls once before the loop via GetCallsByIdsAsync or a preloaded dictionary to eliminate the N+1 pattern on every GetCommandList request.

var callIds = selected.Select(c => c.CallId).Distinct().ToList();
var callsById = new Dictionary<int, Call>();
foreach (var callId in callIds)
{
    try { var c = await _callsService.GetCallByIdAsync(callId); if (c != null) callsById[callId] = c; }
    catch (Exception ex) { Resgrid.Framework.Logging.LogException(ex); }
}
// then inside the loop: callsById.TryGetValue(command.CallId, out var call);
Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 802 to 812:

N+1 query: `GetCallByIdAsync` is called individually inside a `foreach` loop over every selected command, triggering one database round-trip per incident. Batch-load the calls once before the loop via `GetCallsByIdsAsync` or a preloaded dictionary to eliminate the N+1 pattern on every `GetCommandList` request.

Suggested Code:

var callIds = selected.Select(c => c.CallId).Distinct().ToList();
var callsById = new Dictionary<int, Call>();
foreach (var callId in callIds)
{
    try { var c = await _callsService.GetCallByIdAsync(callId); if (c != null) callsById[callId] = c; }
    catch (Exception ex) { Resgrid.Framework.Logging.LogException(ex); }
}
// then inside the loop: callsById.TryGetValue(command.CallId, out var call);

Talk to Kody by mentioning @kody

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

Comment on lines +2106 to +2113
var command = await GetActiveCommandForCallAsync(departmentId, callId);
if (command == null)
return;

var all = await _incidentNeedEntityRepository.GetAllByDepartmentIdAsync(departmentId);
var callEntities = (all ?? Enumerable.Empty<IncidentNeedEntity>()).Where(x => x.CallId == callId).ToList();
if (callEntities.Count == 0)
return;

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

Hot-path overhead: RecordNeedEntityStatusAsync unconditionally issues two database round-trips (GetActiveCommandForCallAsync + GetAllByDepartmentIdAsync) on every personnel and unit status save before the early return when no entity needs exist. Add a fast-exit guard that checks whether the department has any IncidentNeedEntity rows (or cache a per-call flag) before the full entity-load and match logic.

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 2106 to 2113:

Hot-path overhead: `RecordNeedEntityStatusAsync` unconditionally issues two database round-trips (`GetActiveCommandForCallAsync` + `GetAllByDepartmentIdAsync`) on every personnel and unit status save before the early return when no entity needs exist. Add a fast-exit guard that checks whether the department has any `IncidentNeedEntity` rows (or cache a per-call flag) before the full entity-load and match logic.

Talk to Kody by mentioning @kody

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

Comment on lines +802 to +812
foreach (var command in selected)
{
Call call = null;
try
{
call = await _callsService.GetCallByIdAsync(command.CallId);
}
catch (Exception ex)
{
Resgrid.Framework.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 Performance high

N+1 query: GetCallByIdAsync is called individually inside a foreach loop over every selected command, causing one database round-trip per command. With includeClosed=true, a department with many historical commands triggers O(N) sequential network round-trips. Batch-fetch all needed calls via GetCallsByIdsAsync before the loop and look them up from a dictionary.

var callIds = selected.Select(c => c.CallId).Distinct().ToList();
var callsById = new Dictionary<int, Call>();
foreach (var callId in callIds)
{
    try { var c = await _callsService.GetCallByIdAsync(callId); if (c != null) callsById[callId] = c; }
    catch (Exception ex) { Resgrid.Framework.Logging.LogException(ex); }
}
foreach (var command in selected)
{
    callsById.TryGetValue(command.CallId, out var call);
Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 802 to 812:

N+1 query: `GetCallByIdAsync` is called individually inside a `foreach` loop over every selected command, causing one database round-trip per command. With `includeClosed=true`, a department with many historical commands triggers O(N) sequential network round-trips. Batch-fetch all needed calls via `GetCallsByIdsAsync` before the loop and look them up from a dictionary.

Suggested Code:

var callIds = selected.Select(c => c.CallId).Distinct().ToList();
var callsById = new Dictionary<int, Call>();
foreach (var callId in callIds)
{
    try { var c = await _callsService.GetCallByIdAsync(callId); if (c != null) callsById[callId] = c; }
    catch (Exception ex) { Resgrid.Framework.Logging.LogException(ex); }
}
foreach (var command in selected)
{
    callsById.TryGetValue(command.CallId, out var call);

Talk to Kody by mentioning @kody

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

Comment on lines +2110 to +2113
var all = await _incidentNeedEntityRepository.GetAllByDepartmentIdAsync(departmentId);
var callEntities = (all ?? Enumerable.Empty<IncidentNeedEntity>()).Where(x => x.CallId == callId).ToList();
if (callEntities.Count == 0)
return;

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

Unbounded hot-path overhead: RecordNeedEntityStatusAsync loads ALL department need-entity rows via GetAllByDepartmentIdAsync before filtering to one call in memory on every personnel and unit status save (PersonnelStatusesController line 174, UnitStatusController line 292). Loading an entire department table on each status save adds overhead proportional to the department's total historical entity-need count. Filter at the data layer by callId before materializing results.

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 2110 to 2113:

Unbounded hot-path overhead: `RecordNeedEntityStatusAsync` loads ALL department need-entity rows via `GetAllByDepartmentIdAsync` before filtering to one call in memory on every personnel and unit status save (`PersonnelStatusesController` line 174, `UnitStatusController` line 292). Loading an entire department table on each status save adds overhead proportional to the department's total historical entity-need count. Filter at the data layer by `callId` before materializing results.

Talk to Kody by mentioning @kody

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

Call call = null;
try
{
call = await _callsService.GetCallByIdAsync(command.CallId);

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: GetCallByIdAsync is called per command inside a foreach loop, producing N database round-trips for N commands. Batch-load all call IDs before the loop via GetCallsByIdsAsync(selected.Select(c => c.CallId).Distinct()) and build a dictionary for lookup inside the loop.

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

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 807:

N+1 query: `GetCallByIdAsync` is called per command inside a `foreach` loop, producing N database round-trips for N commands. Batch-load all call IDs before the loop via `GetCallsByIdsAsync(selected.Select(c => c.CallId).Distinct())` and build a dictionary for lookup inside the loop.

Talk to Kody by mentioning @kody

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

Call call = null;
try
{
call = await _callsService.GetCallByIdAsync(command.CallId);

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: each command's call is fetched individually inside the summary loop instead of via a single JOIN or batch fetch. Eager-load calls via a batch query or a join between commands and calls before iterating.

Kody rule violation: Optimize database queries with JOINs

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 807:

N+1 query: each command's call is fetched individually inside the summary loop instead of via a single JOIN or batch fetch. Eager-load calls via a batch query or a join between commands and calls before iterating.

Talk to Kody by mentioning @kody

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

map.Name = TrimToLength(map.Name, 500);
map.Description = TrimToLength(map.Description, 2000);

var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.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 Bug medium

Audit corruption: SaveIncidentMapAsync inserts the map via UpsertOwnedAsync before setting CreatedByUserId and CreatedOn, persisting a row with untrusted/default audit values that a second SaveOrUpdateAsync immediately overwrites. Set CreatedByUserId and CreatedOn on the map object before calling UpsertOwnedAsync, eliminating the redundant double-write.

if (string.IsNullOrWhiteSpace(map.IncidentMapId))
				map.IncidentMapId = Guid.NewGuid().ToString();
				map.CreatedByUserId = userId;
				map.CreatedOn = DateTime.UtcNow;

				var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.DepartmentId, ...
Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 2463:

Audit corruption: `SaveIncidentMapAsync` inserts the map via `UpsertOwnedAsync` before setting `CreatedByUserId` and `CreatedOn`, persisting a row with untrusted/default audit values that a second `SaveOrUpdateAsync` immediately overwrites. Set `CreatedByUserId` and `CreatedOn` on the map object before calling `UpsertOwnedAsync`, eliminating the redundant double-write.

Suggested Code:

if (string.IsNullOrWhiteSpace(map.IncidentMapId))
				map.IncidentMapId = Guid.NewGuid().ToString();
				map.CreatedByUserId = userId;
				map.CreatedOn = DateTime.UtcNow;

				var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.DepartmentId, ...

Talk to Kody by mentioning @kody

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

map.Name = TrimToLength(map.Name, 500);
map.Description = TrimToLength(map.Description, 2000);

var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.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 Bug medium

Audit corruption: SaveIncidentMapAsync calls UpsertOwnedAsync (which inserts via InsertAsync) before setting CreatedByUserId and CreatedOn, then immediately calls SaveOrUpdateAsync a second time to stamp them. If the second save fails due to a network error or timeout, the audit trail is permanently corrupted with null CreatedByUserId and default(DateTime) CreatedOn. Set map.CreatedByUserId and map.CreatedOn before calling UpsertOwnedAsync, matching how annotation and node saves set audit fields before upsert.

if (string.IsNullOrWhiteSpace(map.IncidentMapId))
{
    map.CreatedByUserId = userId;
    map.CreatedOn = DateTime.UtcNow;
}

var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.DepartmentId,
    e => e.DepartmentId, (stored, incoming) =>
    {
        incoming.CreatedByUserId = stored.CreatedByUserId;
        incoming.CreatedOn = stored.CreatedOn;
        incoming.DeletedOn = stored.DeletedOn;
        incoming.UpdatedByUserId = userId;
        incoming.UpdatedOn = DateTime.UtcNow;
    }, cancellationToken);
if (rejected)
    return null;
map = saved;
Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 2463:

Audit corruption: `SaveIncidentMapAsync` calls `UpsertOwnedAsync` (which inserts via `InsertAsync`) before setting `CreatedByUserId` and `CreatedOn`, then immediately calls `SaveOrUpdateAsync` a second time to stamp them. If the second save fails due to a network error or timeout, the audit trail is permanently corrupted with null `CreatedByUserId` and `default(DateTime)` `CreatedOn`. Set `map.CreatedByUserId` and `map.CreatedOn` before calling `UpsertOwnedAsync`, matching how annotation and node saves set audit fields before upsert.

Suggested Code:

if (string.IsNullOrWhiteSpace(map.IncidentMapId))
{
    map.CreatedByUserId = userId;
    map.CreatedOn = DateTime.UtcNow;
}

var (saved, isNew, rejected) = await UpsertOwnedAsync(_incidentMapRepository, map, map.DepartmentId,
    e => e.DepartmentId, (stored, incoming) =>
    {
        incoming.CreatedByUserId = stored.CreatedByUserId;
        incoming.CreatedOn = stored.CreatedOn;
        incoming.DeletedOn = stored.DeletedOn;
        incoming.UpdatedByUserId = userId;
        incoming.UpdatedOn = DateTime.UtcNow;
    }, cancellationToken);
if (rejected)
    return null;
map = saved;

Talk to Kody by mentioning @kody

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

foreach (var column in new[] { "StagingLatitude", "StagingLongitude", "RehabLatitude", "RehabLongitude" })
{
if (!Schema.Table("IncidentCommands").Column(column).Exists())
Alter.Table("IncidentCommands").AddColumn(column).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

Oversized column type: four coordinate columns (StagingLatitude, StagingLongitude, RehabLatitude, RehabLongitude) are created with AsString(int.MaxValue) (NVARCHAR(MAX)), but latitude and longitude values are short bounded strings at most ~20 characters. NVARCHAR(MAX) prevents efficient page-row storage, harms index/seek performance, and bloats storage. Use a bounded length such as AsString(50) or store coordinates in a numeric type like AsDouble with appropriate precision.

Kody rule violation: Optimize string column types

Prompt for LLM

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

Line 33:

Oversized column type: four coordinate columns (`StagingLatitude`, `StagingLongitude`, `RehabLatitude`, `RehabLongitude`) are created with `AsString(int.MaxValue)` (NVARCHAR(MAX)), but latitude and longitude values are short bounded strings at most ~20 characters. NVARCHAR(MAX) prevents efficient page-row storage, harms index/seek performance, and bloats storage. Use a bounded length such as `AsString(50)` or store coordinates in a numeric type like `AsDouble` with appropriate precision.

Talk to Kody by mentioning @kody

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

Comment on lines +35 to +40
foreach (var column in new[]
{
"name", "commandpostlocationtext",
"staginglocationtext", "staginglatitude", "staginglongitude",
"rehablocationtext", "rehablatitude", "rehablongitude"
})

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 array: the column-names array is defined identically in both Up() (lines 18–23) and Down() (lines 35–40). Extract the array to a private static readonly field such as private static readonly string[] IncidentCommandColumns = { … }; and reference it in both methods.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

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

Line 35 to 40:

Duplicated array: the column-names array is defined identically in both `Up()` (lines 18–23) and `Down()` (lines 35–40). Extract the array to a `private static readonly` field such as `private static readonly string[] IncidentCommandColumns = { … };` and reference it in both methods.

Talk to Kody by mentioning @kody

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

{
public override void Up()
{
if (!Schema.Table("tacticalobjectives").Exists())

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 string literal: the table name "tacticalobjectives" is repeated 8 times as an inline string across the migration. A typo in any instance would cause the existence check to silently evaluate false and skip a migration step. Define a class-level constant such as private const string TableName = "tacticalobjectives"; and reference it everywhere.

Kody rule violation: Centralize string constants

Prompt for LLM

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

Line 14:

Duplicated string literal: the table name `"tacticalobjectives"` is repeated 8 times as an inline string across the migration. A typo in any instance would cause the existence check to silently evaluate false and skip a migration step. Define a class-level constant such as `private const string TableName = "tacticalobjectives";` and reference it everywhere.

Talk to Kody by mentioning @kody

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

.WithColumn("incidentcommandid").AsCustom("citext").NotNullable()
.WithColumn("departmentid").AsInt32().NotNullable()
.WithColumn("callid").AsInt32().NotNullable()
.WithColumn("entitykind").AsInt32().NotNullable().WithDefaultValue(0)

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

Unexplained magic number: the default value 0 for the entitykind column does not document which EntityKind it represents. Define a named constant or cast the domain enum, e.g. const int DefaultEntityKind = (int)EntityKind.Unknown;, so the migration's intent is self-documenting and stays in sync with the domain enum.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

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

Line 23:

Unexplained magic number: the default value `0` for the `entitykind` column does not document which `EntityKind` it represents. Define a named constant or cast the domain enum, e.g. `const int DefaultEntityKind = (int)EntityKind.Unknown;`, so the migration's intent is self-documenting and stays in sync with the domain enum.

Talk to Kody by mentioning @kody

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

Comment on lines +1142 to +1145
[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = ResgridResources.Command_Update)]
public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)

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 Security high

Missing authorization attribute: DeleteIncidentMap omits [RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)] that its sibling endpoints SaveIncidentMap and UpdateMapView both carry. A user holding the department-level Command_Update claim but lacking the ManageAnnotations capability can delete incident maps, bypassing the incident-scoped authorization check enforced on the create/update path. Add the attribute to DeleteIncidentMap.

[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
		[ProducesResponseType(StatusCodes.Status200OK)]
		[Authorize(Policy = ResgridResources.Command_Update)]
		[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]
		public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)
Prompt for LLM

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

Line 1142 to 1145:

Missing authorization attribute: `DeleteIncidentMap` omits `[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]` that its sibling endpoints `SaveIncidentMap` and `UpdateMapView` both carry. A user holding the department-level `Command_Update` claim but lacking the `ManageAnnotations` capability can delete incident maps, bypassing the incident-scoped authorization check enforced on the create/update path. Add the attribute to `DeleteIncidentMap`.

Suggested Code:

[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
		[ProducesResponseType(StatusCodes.Status200OK)]
		[Authorize(Policy = ResgridResources.Command_Update)]
		[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]
		public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)

Talk to Kody by mentioning @kody

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

Comment on lines +1142 to +1145
[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = ResgridResources.Command_Update)]
public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)

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 Security high

Authorization bypass: DeleteIncidentMap lacks the [RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)] attribute that its sibling endpoints SaveIncidentMap (line 1115) and UpdateMapView (line 1172) both enforce. Any department member with the coarse Command_Update permission can delete incident maps without the fine-grained ManageAnnotations capability check. Add the attribute to the DeleteIncidentMap action.

[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = ResgridResources.Command_Update)]
[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]
public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)
Prompt for LLM

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

Line 1142 to 1145:

Authorization bypass: `DeleteIncidentMap` lacks the `[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]` attribute that its sibling endpoints `SaveIncidentMap` (line 1115) and `UpdateMapView` (line 1172) both enforce. Any department member with the coarse `Command_Update` permission can delete incident maps without the fine-grained `ManageAnnotations` capability check. Add the attribute to the `DeleteIncidentMap` action.

Suggested Code:

[HttpDelete("DeleteIncidentMap/{incidentMapId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = ResgridResources.Command_Update)]
[RequiresIncidentCapability(IncidentCapabilities.ManageAnnotations)]
public async Task<ActionResult<ICModels.IncidentCommandActionResult>> DeleteIncidentMap(string incidentMapId)

Talk to Kody by mentioning @kody

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

public async Task<ActionResult<ICModels.IncidentCommandBoardResult>> GetCommandBoardById(string incidentCommandId)
{
var result = new ICModels.IncidentCommandBoardResult();
var board = await _incidentCommandService.GetCommandBoardByIdAsync(DepartmentId, 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 high

Missing input validation: GetCommandBoardById issues the service/DB query without first validating the incidentCommandId route value. Empty or whitespace IDs should be rejected up-front to avoid pointless data-layer calls and ambiguous null results. Add if (string.IsNullOrWhiteSpace(incidentCommandId)) return BadRequest(); before the await, matching the validation pattern used in the mutating actions.

Kody rule violation: Order validations before database queries

Prompt for LLM

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

Line 164:

Missing input validation: `GetCommandBoardById` issues the service/DB query without first validating the `incidentCommandId` route value. Empty or whitespace IDs should be rejected up-front to avoid pointless data-layer calls and ambiguous null results. Add `if (string.IsNullOrWhiteSpace(incidentCommandId)) return BadRequest();` before the await, matching the validation pattern used in the mutating actions.

Talk to Kody by mentioning @kody

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


var result = new ICModels.IncidentNeedResult();
var need = await _incidentCommandService.SetNeedStatusAsync(DepartmentId, input.IncidentNeedId, (IncidentNeedStatus)input.Status, input.QuantityFulfilled, UserId, CancellationToken.None);
var need = await _incidentCommandService.SetNeedStatusAsync(DepartmentId, input.IncidentNeedId, (IncidentNeedStatus)input.Status, input.QuantityFulfilled, UserId, input.Note, CancellationToken.None);

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

Unvalidated enum cast: (IncidentNeedStatus)input.Status directly casts user-supplied input without verification. An out-of-range integer silently becomes an invalid enum value, risking downstream errors. Validate before casting, e.g. guard with Enum.IsDefined(typeof(IncidentNeedStatus), input.Status) or return BadRequest when the value is undefined, matching the pattern used at line 857.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

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

Line 918:

Unvalidated enum cast: `(IncidentNeedStatus)input.Status` directly casts user-supplied input without verification. An out-of-range integer silently becomes an invalid enum value, risking downstream errors. Validate before casting, e.g. guard with `Enum.IsDefined(typeof(IncidentNeedStatus), input.Status)` or return `BadRequest` when the value is undefined, matching the pattern used at line 857.

Talk to Kody by mentioning @kody

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

}
catch (Exception ex)
{
Resgrid.Framework.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

Contextless error logging: the catch block logs only the raw exception without any operation name or relevant identifiers such as department id, destination id, or user id. Pass structured context into the log call, e.g. Resgrid.Framework.Logging.LogException(ex, $"RecordNeedEntityStatusAsync failed for Department {DepartmentId}, Destination {destinationId}, User {input.UserId}").

Kody rule violation: Include error context in structured logs

Prompt for LLM

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

Line 178:

Contextless error logging: the catch block logs only the raw exception without any operation name or relevant identifiers such as department id, destination id, or user id. Pass structured context into the log call, e.g. `Resgrid.Framework.Logging.LogException(ex, $"RecordNeedEntityStatusAsync failed for Department {DepartmentId}, Destination {destinationId}, User {input.UserId}")`.

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 Jul 22, 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.

Comment on lines +803 to +807
foreach (var callId in selected.Select(x => x.CallId).Distinct())
{
try
{
var loadedCall = await _callsService.GetCallByIdAsync(callId);

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 occurs as the foreach loop issues a separate database round-trip via GetCallByIdAsync for each distinct CallId. Add a batched repository method such as GetCallsByIdsAsync(IEnumerable<int> ids) using WHERE Id IN (...) to load all calls in a single query.

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

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 803 to 807:

N+1 query pattern occurs as the `foreach` loop issues a separate database round-trip via `GetCallByIdAsync` for each distinct `CallId`. Add a batched repository method such as `GetCallsByIdsAsync(IEnumerable<int> ids)` using `WHERE Id IN (...)` to load all calls 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 +803 to +807
foreach (var callId in selected.Select(x => x.CallId).Distinct())
{
try
{
var loadedCall = await _callsService.GetCallByIdAsync(callId);

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 inefficiency results from loading calls individually via GetCallByIdAsync inside the loop, creating excessive database round-trips. Implement a batched API like GetCallsByIdsAsync backed by WHERE Id IN @ids or join the Call rows up front when querying selected.

Kody rule violation: Optimize database queries with JOINs

Prompt for LLM

File Core/Resgrid.Services/IncidentCommandService.cs:

Line 803 to 807:

N+1 query inefficiency results from loading calls individually via `GetCallByIdAsync` inside the loop, creating excessive database round-trips. Implement a batched API like `GetCallsByIdsAsync` backed by `WHERE Id IN @ids` or join the `Call` rows up front when querying `selected`.

Talk to Kody by mentioning @kody

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

try
{
var stateLabel = Enum.IsDefined(typeof(UnitStateTypes), state.State) ? ((UnitStateTypes)state.State).ToString() : $"status {state.State}";
await _incidentCommandService.RecordNeedEntityStatusAsync(DepartmentId, state.DestinationId.Value, NeedEntityKind.Unit, state.UnitId.ToString(), stateLabel, UserId, 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

InvalidOperationException risk arises as state.DestinationId.Value is accessed without verifying the underlying nullable type has a value. Guard with a HasValue check, such as if (!state.DestinationId.HasValue) return BadRequest(...);, before calling RecordNeedEntityStatusAsync.

Kody rule violation: Add null checks to prevent NullReferenceException

Prompt for LLM

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

Line 292:

InvalidOperationException risk arises as `state.DestinationId.Value` is accessed without verifying the underlying nullable type has a value. Guard with a `HasValue` check, such as `if (!state.DestinationId.HasValue) return BadRequest(...);`, before calling `RecordNeedEntityStatusAsync`.

Talk to Kody by mentioning @kody

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

@ucswift

ucswift commented Jul 22, 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 8d2e6c2 into master Jul 22, 2026
17 of 19 checks passed
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