Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Core/Resgrid.Config/ChatConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ public static class ChatConfig
public static string NovuApplicationId = "";
public static string NovuSecretKey = "";

public static string NovuUnitFcmProviderId = "";
public static string NovuUnitApnsProviderId = "";
public static string NovuResponderFcmProviderId = "";
public static string NovuResponderApnsProviderId = "";
public static string NovuUnitFcmProviderId = "firebase-cloud-messaging-7Z5wHFPpQ";
public static string NovuUnitApnsProviderId = "unit-apns";
public static string NovuResponderFcmProviderId = "respond-firebase-cloud-messaging";
public static string NovuResponderApnsProviderId = "respond-apns";
public static string NovuDispatchUnitWorkflowId = "unit-dispatch";
public static string NovuDispatchUserWorkflowId = "user-dispatch";
public static string NovuMessageUserWorkflowId = "user-message";
Expand Down
38 changes: 37 additions & 1 deletion Core/Resgrid.Config/InfoConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Resgrid.Config
using System.Collections.Generic;

namespace Resgrid.Config
{
public static class InfoConfig
{
Expand All @@ -21,5 +23,39 @@ public static class InfoConfig
public static string RelayAppKey = "RelayAppKey";

public static string EmailProcessorKey = "EmailProcessorKey";

public static List<ResgridSystemLocation> Locations = new List<ResgridSystemLocation>()
{
new ResgridSystemLocation()
{
Name = "US-West",
DisplayName = "Resgrid North America (Global)",
LocationInfo =
"This is the Resgrid system hosted in the Western United States (private datacenter). This system services most Resgrid customers.",
IsDefault = true,
ApiUrl = "https://api.resgrid.com",
AllowsFreeAccounts = true
},
new ResgridSystemLocation()
{
Name = "EU-Central",
DisplayName = "Resgrid Europe",
LocationInfo =
"This is the Resgrid system hosted in Central Europe (on OVH). This system services Resgrid customers in the European Union to help with data compliance requirements.",
IsDefault = false,
ApiUrl = "https://api.eu.resgrid.com",
AllowsFreeAccounts = false
}
};
Comment on lines +27 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Expose Locations as read-only to avoid global mutation; initialize immutably.

The config list is public and mutable, which is risky. Prefer IReadOnlyList and immutable initialization.

-		public static List<ResgridSystemLocation> Locations = new List<ResgridSystemLocation>()
-		{
-			new ResgridSystemLocation()
-			{
-				Name = "US-West",
-				DisplayName = "Resgrid North America (Global)",
-				LocationInfo =
-					"This is the Resgrid system hosted in the Western United States (private datacenter). This system services most Resgrid customers.",
-				IsDefault = true,
-				ApiUrl = "https://api.resgrid.com",
-				AllowsFreeAccounts = true
-			},
-			new ResgridSystemLocation()
-			{
-				Name = "EU-Central",
-				DisplayName = "Resgrid Europe",
-				LocationInfo =
-					"This is the Resgrid system hosted in Central Europe (on OVH). This system services Resgrid customers in the European Union to help with data compliance requirements.",
-				IsDefault = false,
-				ApiUrl = "https://api.eu.resgrid.com",
-				AllowsFreeAccounts = false
-			}
-		};
+		public static IReadOnlyList<ResgridSystemLocation> Locations { get; } = new[]
+		{
+			new ResgridSystemLocation(
+				Name: "US-West",
+				DisplayName: "Resgrid North America (Global)",
+				LocationInfo: "This is the Resgrid system hosted in the Western United States (private datacenter). This system services most Resgrid customers.",
+				IsDefault: true,
+				ApiUrl: "https://api.resgrid.com",
+				AllowsFreeAccounts: true
+			),
+			new ResgridSystemLocation(
+				Name: "EU-Central",
+				DisplayName: "Resgrid Europe",
+				LocationInfo: "This is the Resgrid system hosted in Central Europe (on OVH). This system services Resgrid customers in the European Union to help with data compliance requirements.",
+				IsDefault: false,
+				ApiUrl: "https://api.eu.resgrid.com",
+				AllowsFreeAccounts: false
+			)
+		};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Core/Resgrid.Config/InfoConfig.cs around lines 27 to 49, the public mutable
List<ResgridSystemLocation> Locations allows global mutation; change the API to
expose a read-only collection and initialize it immutably. Replace the public
mutable field with a private static readonly List<ResgridSystemLocation> backing
list populated with the two locations and expose it via a public static
IReadOnlyList<ResgridSystemLocation> (or ReadOnlyCollection/ImmutableList)
property that returns the immutable/read-only view so callers cannot add/remove
items.

}

public class ResgridSystemLocation
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string LocationInfo { get; set; }
public bool IsDefault { get; set; }
public string ApiUrl { get; set; }
public bool AllowsFreeAccounts { get; set; }
Comment on lines +52 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use a record with init-only semantics for config state.

Records with positional parameters make the configuration immutable and concise, matching the guideline to separate state from behavior.

-	public class ResgridSystemLocation
-	{
-		public string Name { get; set; }
-		public string DisplayName { get; set; }
-		public string LocationInfo { get; set; }
-		public bool IsDefault { get; set; }
-		public string ApiUrl { get; set; }
-		public bool AllowsFreeAccounts { get; set; }
-	}
+	public sealed record ResgridSystemLocation(
+		string Name,
+		string DisplayName,
+		string LocationInfo,
+		bool IsDefault,
+		string ApiUrl,
+		bool AllowsFreeAccounts
+	);

If other code uses object initializers, convert those call sites accordingly. Also consider adding a helper to fetch the default location:

public static ResgridSystemLocation DefaultLocation => Locations.First(l => l.IsDefault);
🤖 Prompt for AI Agents
In Core/Resgrid.Config/InfoConfig.cs around lines 52 to 59, the mutable class
ResgridSystemLocation should be converted to an immutable record with init-only
semantics (preferably a positional record) so configuration state is represented
immutably; replace the class declaration with a record declaration, update
properties to be init-only (or use positional parameters), and adjust any call
sites that use object initializers to use the new positional/with syntax;
optionally add a static helper property (e.g., DefaultLocation) that returns
Locations.First(l => l.IsDefault) to fetch the configured default location.

}
}
5 changes: 5 additions & 0 deletions Core/Resgrid.Config/SystemBehaviorConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public static class SystemBehaviorConfig
/// </summary>
public static string BlogUrl = "https://blog.resgrid.com";

/// <summary>
/// Sets the name of the location this Resgrid system is running in
/// </summary>
public static string LocationName = "US-West";

public static string GetEnvPrefix()
{
switch (Environment)
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Model/Services/ICommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface ICommunicationService
/// <param name="profile">The profile.</param>
/// <returns>Task&lt;System.Boolean&gt;.</returns>
Task<bool> SendMessageAsync(Message message, string sendersName, string departmentNumber, int departmentId,
UserProfile profile = null);
UserProfile profile = null, Department department = null);

/// <summary>
/// Sends the call asynchronous.
Expand Down Expand Up @@ -110,6 +110,6 @@ Task<bool> SendTextMessageAsync(string userId, string title, string message, int
/// <param name="profile">The profile.</param>
/// <returns>Task&lt;System.Boolean&gt;.</returns>
Task<bool> SendCalendarAsync(string userId, int departmentId, string message, string departmentNumber,
string title = "Notification", UserProfile profile = null);
string title = "Notification", UserProfile profile = null, Department department = null);
}
}
6 changes: 3 additions & 3 deletions Core/Resgrid.Services/CalendarService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ public async Task<bool> NotifyNewCalendarItemAsync(CalendarItem calendarItem)
// Notify the entire department
foreach (var profile in profiles)
{
await _communicationService.SendCalendarAsync(profile.Key, calendarItem.DepartmentId, message, departmentNumber, title, profile.Value);
await _communicationService.SendCalendarAsync(profile.Key, calendarItem.DepartmentId, message, departmentNumber, title, profile.Value, department);
}
}
else
Expand All @@ -487,9 +487,9 @@ public async Task<bool> NotifyNewCalendarItemAsync(CalendarItem calendarItem)
foreach (var member in group.Members)
{
if (profiles.ContainsKey(member.UserId))
await _communicationService.SendNotificationAsync(member.UserId, calendarItem.DepartmentId, message, departmentNumber, department, title, profiles[member.UserId]);
await _communicationService.SendCalendarAsync(member.UserId, calendarItem.DepartmentId, message, departmentNumber, title, profiles[member.UserId], department);
else
await _communicationService.SendNotificationAsync(member.UserId, calendarItem.DepartmentId, message, departmentNumber, department, title, null);
await _communicationService.SendCalendarAsync(member.UserId, calendarItem.DepartmentId, message, departmentNumber, title, null, department);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,48 +49,84 @@ public async Task<Call> GenerateCall(CallEmail email, string managingUser, List<
if (String.IsNullOrEmpty(email.Subject))
return null;

string[] sections = email.TextBody.Split(new[] {" ALPHA 512 "}, StringSplitOptions.None);
string[] sectionOneParts = sections[0].Split(new[] {" "}, StringSplitOptions.None);

Call c = new Call();
c.Notes = email.TextBody;
c.Name = sections[1].Trim();
c.LoggedOn = DateTime.UtcNow;
c.Priority = priority;
c.ReportingUserId = managingUser;
c.Dispatches = new Collection<CallDispatch>();
c.CallSource = (int)CallSources.EmailImport;
c.SourceIdentifier = email.MessageId;
c.NatureOfCall = sections[1].Trim();
c.IncidentNumber = sectionOneParts[0].Trim();
c.ExternalIdentifier = sectionOneParts[0].Trim();

if (users != null && users.Any())
try
{
foreach (var u in users)
string[] sections = email.TextBody.Split(new[] { " ALPHA 512 " }, StringSplitOptions.None);
string[] sectionOneParts = sections[0].Split(new[] { " " }, StringSplitOptions.None);

Call c = new Call();
c.Notes = email.TextBody;
c.Name = sections[1].Trim();
c.LoggedOn = DateTime.UtcNow;
c.Priority = priority;
c.ReportingUserId = managingUser;
c.Dispatches = new Collection<CallDispatch>();
c.CallSource = (int)CallSources.EmailImport;
c.SourceIdentifier = email.MessageId;
c.NatureOfCall = sections[1].Trim();
c.IncidentNumber = sectionOneParts[0].Trim();
c.ExternalIdentifier = sectionOneParts[0].Trim();

if (users != null && users.Any())
{
CallDispatch cd = new CallDispatch();
cd.UserId = u.UserId;
foreach (var u in users)
{
CallDispatch cd = new CallDispatch();
cd.UserId = u.UserId;

c.Dispatches.Add(cd);
c.Dispatches.Add(cd);
}
}
}

// Search for an active call
if (activeCalls != null && activeCalls.Any())
// Search for an active call
if (activeCalls != null && activeCalls.Any())
{
var activeCall = activeCalls.FirstOrDefault(x => x.IncidentNumber == c.IncidentNumber);

if (activeCall != null)
{
activeCall.Notes = c.Notes;
activeCall.LastDispatchedOn = DateTime.UtcNow;

return activeCall;
}
}

return c;
}
catch (Exception ex)
{
var activeCall = activeCalls.FirstOrDefault(x => x.IncidentNumber == c.IncidentNumber);
Call c = new Call();
c.Name = email.Subject;
c.NatureOfCall = $"ERROR PROCESSING DISPATCH EMAIL, Unprocessed email body: {email.TextBody}";

if (activeCall != null)
if (users != null && users.Any())
{
activeCall.Notes = c.Notes;
activeCall.LastDispatchedOn = DateTime.UtcNow;
foreach (var u in users)
{
CallDispatch cd = new CallDispatch();
cd.UserId = u.UserId;

return activeCall;
c.Dispatches.Add(cd);
}
}
Comment on lines +99 to 112
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix NullReference and complete fallback Call construction in catch block

c.Dispatches isn't initialized before Add, causing a NullReferenceException inside catch. Also key fields (Notes, LoggedOn, Priority, ReportingUserId, CallSource, SourceIdentifier) aren't set, and active-call lookup uses a null IncidentNumber.

Apply this diff:

-			catch (Exception ex)
+			catch (Exception ex)
 			{
-				Call c = new Call();
-				c.Name = email.Subject;
-				c.NatureOfCall = $"ERROR PROCESSING DISPATCH EMAIL, Unprocessed email body: {email.TextBody}";
+				Call c = new Call();
+				c.Name = email.Subject ?? "Email-to-Call Error";
+				c.Notes = email.TextBody;
+				c.NatureOfCall = $"ERROR PROCESSING DISPATCH EMAIL, Unprocessed email body: {email.TextBody}";
+				c.LoggedOn = DateTime.UtcNow;
+				c.Priority = priority;
+				c.ReportingUserId = managingUser;
+				c.Dispatches = new Collection<CallDispatch>();
+				c.CallSource = (int)CallSources.EmailImport;
+				c.SourceIdentifier = email.MessageId;
 
 				if (users != null && users.Any())
 				{
 					foreach (var u in users)
 					{
 						CallDispatch cd = new CallDispatch();
 						cd.UserId = u.UserId;
 
 						c.Dispatches.Add(cd);
 					}
 				}
 
-				// Search for an active call
-				if (activeCalls != null && activeCalls.Any())
+				// Search for an active call only if we have an incident number
+				if (!string.IsNullOrWhiteSpace(c.IncidentNumber) && activeCalls != null && activeCalls.Any())
 				{
 					var activeCall = activeCalls.FirstOrDefault(x => x.IncidentNumber == c.IncidentNumber);
 
 					if (activeCall != null)
 					{
 						activeCall.Notes = c.Notes;
 						activeCall.LastDispatchedOn = DateTime.UtcNow;
 
 						return activeCall;
 					}
 				}
 
 				return c;
 			}

Also applies to: 114-129

🤖 Prompt for AI Agents
In Core/Resgrid.Services/CallEmailTemplates/OttawaKingstonTorontoTemplate.cs
around lines 99-112 (and similarly for 114-129), the fallback Call created in
the catch block leaves Call.Dispatches null and omits key properties causing
NREs and incorrect active-call lookup. Initialize c.Dispatches to an empty
collection before adding CallDispatch items; set the missing properties (c.Notes
= email.TextBody or appropriate message, c.LoggedOn = DateTime.UtcNow,
c.Priority = default priority, c.ReportingUserId = a valid reporter id or null
handling, c.CallSource = CallSource.Email (or appropriate enum),
c.SourceIdentifier = email.MessageId or a unique identifier) and ensure
IncidentNumber is set to a non-null value (e.g., generate or use
email.MessageId) before doing the active-call lookup; also keep the users
null/empty check and only add dispatches when users exist.

}

return c;
// Search for an active call
if (activeCalls != null && activeCalls.Any())
{
var activeCall = activeCalls.FirstOrDefault(x => x.IncidentNumber == c.IncidentNumber);

if (activeCall != null)
{
activeCall.Notes = c.Notes;
activeCall.LastDispatchedOn = DateTime.UtcNow;

return activeCall;
}
}

return c;
}
}
}
}
8 changes: 6 additions & 2 deletions Core/Resgrid.Services/CommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public CommunicationService(ISmsService smsService, IEmailService emailService,
_userStateService = userStateService;
}

public async Task<bool> SendMessageAsync(Message message, string sendersName, string departmentNumber, int departmentId, UserProfile profile = null)
public async Task<bool> SendMessageAsync(Message message, string sendersName, string departmentNumber, int departmentId, UserProfile profile = null, Department department = null)
{
if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
return false;
Expand Down Expand Up @@ -79,6 +79,7 @@ public async Task<bool> SendMessageAsync(Message message, string sendersName, st
{
var spm = new StandardPushMessage();
spm.MessageId = message.MessageId;
spm.DepartmentCode = department?.Code;

if (message.SystemGenerated)
spm.SubTitle = "Msg from System";
Expand Down Expand Up @@ -124,6 +125,7 @@ public async Task<bool> SendCallAsync(Call call, CallDispatch dispatch, string d
spc.Priority = call.Priority;
spc.ActiveCallCount = 1;
spc.DepartmentId = departmentId;
spc.DepartmentCode = call.Department?.Code;

if (call.CallPriority != null && !String.IsNullOrWhiteSpace(call.CallPriority.Color))
{
Expand Down Expand Up @@ -335,7 +337,7 @@ public async Task<bool> SendNotificationAsync(string userId, int departmentId, s
return true;
}

public async Task<bool> SendCalendarAsync(string userId, int departmentId, string message, string departmentNumber, string title = "Notification", UserProfile profile = null)
public async Task<bool> SendCalendarAsync(string userId, int departmentId, string message, string departmentNumber, string title = "Notification", UserProfile profile = null, Department department = null)
{
if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
return false;
Expand Down Expand Up @@ -364,6 +366,7 @@ public async Task<bool> SendCalendarAsync(string userId, int departmentId, strin
var spm = new StandardPushMessage();
spm.Title = "Calendar";
spm.SubTitle = $"{title} {message}";
spm.DepartmentCode = null;

try
{
Expand Down Expand Up @@ -467,6 +470,7 @@ public async Task<bool> SendTroubleAlertAsync(TroubleAlertEvent troubleAlertEven
spc.Priority = (int)CallPriority.Emergency;
spc.ActiveCallCount = 1;
spc.DepartmentId = departmentId;
spc.DepartmentCode = call.Department?.Code;

string subTitle = String.Empty;
if (!String.IsNullOrWhiteSpace(unitAddress))
Expand Down
5 changes: 3 additions & 2 deletions Core/Resgrid.Services/PushService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ public async Task<bool> PushMessage(StandardPushMessage message, string userId,

try
{
await _novuProvider.SendUserMessage(message.Title, message.SubTitle, userId, message.DepartmentCode, string.Format("M{0}", message.MessageId), null);
if (!string.IsNullOrWhiteSpace(message.DepartmentCode))
await _novuProvider.SendUserMessage(message.Title, message.SubTitle, userId, message.DepartmentCode, string.Format("M{0}", message.MessageId), null);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -143,7 +144,7 @@ public async Task<bool> PushNotification(StandardPushMessage message, string use
}
try
{
await _novuProvider.SendUserMessage(message.Title, message.SubTitle, userId, message.DepartmentCode, string.Format("N{0}", message.MessageId), null);
await _novuProvider.SendUserNotification(message.Title, message.SubTitle, userId, message.DepartmentCode, string.Format("N{0}", message.MessageId), null);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard DepartmentCode before sending; prevent malformed subscriber IDs

Without a non-empty DepartmentCode, recipientId becomes "User{userId}", which won't match a subscriber. Mirror the message path guard and, optionally, pass a concrete notification type.

-					await _novuProvider.SendUserNotification(message.Title, message.SubTitle, userId, message.DepartmentCode, string.Format("N{0}", message.MessageId), null);
+					if (!string.IsNullOrWhiteSpace(message.DepartmentCode))
+						await _novuProvider.SendUserNotification(
+							message.Title,
+							message.SubTitle,
+							userId,
+							message.DepartmentCode,
+							string.Format("N{0}", message.MessageId),
+							((int)PushSoundTypes.Notifiation).ToString());
🤖 Prompt for AI Agents
In Core/Resgrid.Services/PushService.cs around line 147, guard the
DepartmentCode before calling _novuProvider.SendUserNotification: mirror the
same message-path guard used elsewhere by checking
string.IsNullOrWhiteSpace(message.DepartmentCode) and only pass the department
code when it is non-empty (otherwise pass null/empty so the recipientId isn’t
built as "_User_{userId}"); also consider replacing the null notification-type
argument with a concrete type string (eg. "notification") when calling
SendUserNotification.

}
catch (Exception ex)
{
Expand Down
1 change: 1 addition & 0 deletions Providers/Resgrid.Providers.Messaging/NovuProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ private async Task<bool> SendNotification(string title, string body, string reci
{
subject = title,
body = body,
id = eventCode
},
overrides = new
{
Expand Down
17 changes: 17 additions & 0 deletions Web/Resgrid.Web.Services/Controllers/v4/ConfigController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ public ConfigController()
}
#endregion Members and Constructors

/// <summary>
/// Gets the system config
/// </summary>
/// <returns></returns>
[HttpGet("GetSystemConfig")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<GetSystemConfigResult>> GetSystemConfig()
{
var result = new GetSystemConfigResult();

result.PageSize = 1;
result.Status = ResponseHelper.Success;
ResponseHelper.PopulateV4ResponseData(result);

return result;
}

/// <summary>
/// Gets the config values for a key
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Resgrid.Config;

namespace Resgrid.Web.Services.Models.v4.Configs
{
/// <summary>
/// Gets Configuration Information for the Resgrid System
/// </summary>
public class GetSystemConfigResult : StandardApiResponseV4Base
{
/// <summary>
/// Response Data
/// </summary>
public GetSystemConfigResultData Data { get; set; }

/// <summary>
/// Default constructor
/// </summary>
public GetSystemConfigResult()
{
Data = new GetSystemConfigResultData();
}
}

/// <summary>
/// Information about the Resgrid System
/// </summary>
public class GetSystemConfigResultData
{
/// <summary>
/// Resgrid Datacenter Locations
/// </summary>
public List<ResgridSystemLocation> Locations { get; set; }

public GetSystemConfigResultData()
{
Locations = InfoConfig.Locations;
}
Comment on lines +35 to +38
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t return a reference to global mutable state (deep-copy Locations).

Assigning InfoConfig.Locations directly exposes and couples response instances to a shared static list. Any downstream mutation (even accidental) would alter global config for all requests. Deep-copy the entries before returning.

 using System.Collections.Generic;
+using System.Linq;
 using Resgrid.Config;
@@
   public GetSystemConfigResultData()
   {
-    Locations = InfoConfig.Locations;
+    Locations = InfoConfig.Locations
+      .Select(l => new ResgridSystemLocation
+      {
+        Name = l.Name,
+        DisplayName = l.DisplayName,
+        LocationInfo = l.LocationInfo,
+        IsDefault = l.IsDefault,
+        ApiUrl = l.ApiUrl,
+        AllowsFreeAccounts = l.AllowsFreeAccounts
+      })
+      .ToList();
   }

Also applies to: 1-1

🤖 Prompt for AI Agents
In Web/Resgrid.Web.Services/Models/v4/Configs/GetSystemConfigResult.cs around
lines 35-38, the constructor assigns Locations = InfoConfig.Locations which
exposes a reference to global mutable state; replace this with a deep-copy of
the list and its entries (e.g. null-check InfoConfig.Locations, create a new
list and map/clone each Location item into a new DTO/instance copying all
relevant properties) so the returned Locations cannot mutate the shared static
list.

}
}
Loading
Loading