Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ jobs:
dockerfile: Web/Resgrid.Web.Tts/Dockerfile
- image: resgridllc/resgridworkersconsole
dockerfile: Workers/Resgrid.Workers.Console/Dockerfile
- image: resgridllc/resgridtrackergateway
dockerfile: Workers/Resgrid.TrackerGateway/Dockerfile

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -145,6 +147,7 @@ jobs:
.replace(/##\s*Summary by CodeRabbit[\s\S]*/i, '')
.replace(/^(#+\s*)(Pull Request Description|PR Description)\s*$/gim, '$1Summary')
.replace(/Pull Request Description|PR Description/gi, 'Summary')
.replace(/\bPR\b/g, 'Release')
.trim() || 'No release notes provided.';
fs.writeFileSync('release_notes.md', body);
} else {
Expand Down
2 changes: 2 additions & 0 deletions Core/Resgrid.Config/ChatConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public static class ChatConfig
public static string NovuUnitApnsProviderId = "unit-apns";
public static string NovuResponderFcmProviderId = "respond-firebase-cloud-messaging";
public static string NovuResponderApnsProviderId = "respond-apns";
public static string NovuICFcmProviderId = "ic-firebase-cloud-messaging";
public static string NovuICApnsProviderId = "ic-apns";
public static string NovuDispatchUnitWorkflowId = "unit-dispatch";
public static string NovuDispatchUserWorkflowId = "user-dispatch";
public static string NovuMessageUserWorkflowId = "user-message";
Expand Down
10 changes: 10 additions & 0 deletions Core/Resgrid.Config/UnitTrackingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public static class UnitTrackingConfig
public static int DefaultLocationRetentionDays = 90;
public static int MinimumLocationRetentionDays = 1;
public static int MaximumLocationRetentionDays = 3650;
public static bool LocationRetentionWorkerEnabled = false;
public static int LocationRetentionBatchSize = 1000;
public static int LocationRetentionMaxRowsPerRun = 100000;
public static int LocationRetentionHourUtc = 4;
public static int PerDeviceRequestsPerMinute = 120;
public static int PerDeviceRecordsPerMinute = 1200;
public static int UnknownEndpointRequestsPerMinute = 30;
Expand All @@ -39,8 +43,14 @@ public static class UnitTrackingConfig
public static int TeltonikaTcpPort = 5027;
public static int TeltonikaUdpPort = 5027;
public static bool EnableQueclink = false;
public static bool EnableQueclinkTcp = true;
public static bool EnableQueclinkUdp = false;
public static bool EnableGt06 = false;
public static bool EnableGt06Tcp = true;
public static bool EnableGt06Udp = false;
public static bool EnableTeltonika = false;
public static bool EnableTeltonikaTcp = true;
public static bool EnableTeltonikaUdp = false;
public static bool RawDiagnosticCaptureEnabled = false;
}
}
43 changes: 43 additions & 0 deletions Core/Resgrid.Model/Providers/Models/INovuProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ public interface INovuProvider
/// <returns>True if the subscriber was created successfully; otherwise, false.</returns>
Task<bool> CreateUserSubscriber(string userId, string code, int departmentId, string email, string firstName, string lastName);

/// <summary>
/// Creates a Novu subscriber for an IC (Incident Command) app user. The IC app uses a distinct subscriber
/// id ({code}_IC_User_{userId}) so its Novu inbox is separate from the Responder app's.
/// </summary>
/// <param name="userId">The unique identifier of the user.</param>
/// <param name="code">The department code prefix.</param>
/// <param name="departmentId">The department the user belongs to.</param>
/// <param name="email">The user's email address.</param>
/// <param name="firstName">The user's first name.</param>
/// <param name="lastName">The user's last name.</param>
/// <returns>True if the subscriber was created successfully; otherwise, false.</returns>
Task<bool> CreateICUserSubscriber(string userId, string code, int departmentId, string email, string firstName, string lastName);

/// <summary>
/// Creates a Novu subscriber for a unit (device or group).
/// </summary>
Expand Down Expand Up @@ -66,6 +79,24 @@ public interface INovuProvider
/// <returns>True if the token was updated successfully; otherwise, false.</returns>
Task<bool> UpdateUserSubscriberApns(string userId, string code, string token);

/// <summary>
/// Updates the Firebase Cloud Messaging (FCM) token for an IC app user subscriber.
/// </summary>
/// <param name="userId">The unique identifier of the user.</param>
/// <param name="code">The department code prefix.</param>
/// <param name="token">The FCM token to associate with the user.</param>
/// <returns>True if the token was updated successfully; otherwise, false.</returns>
Task<bool> UpdateICUserSubscriberFcm(string userId, string code, string token);

/// <summary>
/// Updates the Apple Push Notification Service (APNS) token for an IC app user subscriber.
/// </summary>
/// <param name="userId">The unique identifier of the user.</param>
/// <param name="code">The department code prefix.</param>
/// <param name="token">The APNS token to associate with the user.</param>
/// <returns>True if the token was updated successfully; otherwise, false.</returns>
Task<bool> UpdateICUserSubscriberApns(string userId, string code, string token);

/// <summary>
/// Sends a dispatch notification to a unit.
/// </summary>
Expand Down Expand Up @@ -94,4 +125,16 @@ Task<bool> SendUnitDispatch(string title, string body, int unitId, string depCod
Task<bool> SendUserMessage(string title, string body, string userId, string depCode, string eventCode, string type);

Task<bool> SendUserNotification(string title, string body, string userId, string depCode, string eventCode, string type);

/// <summary>
/// Sends a notification to an IC app user subscriber ({depCode}_IC_User_{userId}) via the user-notification workflow.
/// </summary>
/// <param name="title">The notification title.</param>
/// <param name="body">The notification body content.</param>
/// <param name="userId">The unique identifier of the user to notify.</param>
/// <param name="depCode">The department code.</param>
/// <param name="eventCode">The event code associated with the notification.</param>
/// <param name="type">The type of notification.</param>
/// <returns>True if the notification was sent successfully; otherwise, false.</returns>
Task<bool> SendICUserNotification(string title, string body, string userId, string depCode, string eventCode, string type);
}
8 changes: 8 additions & 0 deletions Core/Resgrid.Model/PushUri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public class PushUri : IEntity
[ProtoMember(7)]
public int DepartmentId { get; set; }

/// <summary>
/// Source app of the registration (e.g. "IC" for the Incident Command app). Routes the Novu push
/// credential update to the app-specific subscriber; null/empty means the default Responder app.
/// </summary>
[NotMapped]
[ProtoMember(10)]
public string Source { get; set; }
Comment on lines +54 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 '\bIgnoredProperties\b|\bPushUri\b' --glob '*.cs'

Repository: Resgrid/Core

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching PushUri.cs/IgnoredProperties:\n'
git ls-files | rg '(^|/)PushUri\.cs$|IgnoredProperties|Repository|Persistence|Migration|EntityConfiguration|DbContext|DbModelBuilder|MapIgnore|Ignore|NotMapped' || true

printf '\nAll files with Source in PushUri context:\n'
rg -n 'class PushUri|Source\s*\{|IgnoredProperties|NotMapped|ProtoMember|Uuid|DepartmentId' --glob '*.cs' || true

Repository: Resgrid/Core

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'PushUri.cs:\n'
cat -n Core/Resgrid.Model/PushUri.cs

printf '\nPushUriRepository.cs:\n'
cat -n Repositories/Resgrid.Repositories.DataRepository/PushUriRepository.cs

printf '\nPushUri repository interface:\n'
cat -n Core/Resgrid.Model/Repositories/IPushUriRepository.cs

printf '\nFocused IgnoredProperties matches:\n'
rg -n '\bIgnoredProperties\b' --glob '*.cs' || true

printf '\nRepository reflection helper around property handling:\n'
cat -n Repositories/Resgrid.Repositories.DataRepository/Extensions/ReflectionExtensions.cs

Repository: Resgrid/Core

Length of output: 4319


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'All IgnoredProperties matches excluding result log:\n'
rg -n '\bIgnoredProperties\b' --glob '*.cs' || true

printf '\nRelevant generic repository/mapping code searches:\n'
rg -n '\bNotMapped\b|\bDatabaseGenerated\b|\bMapIgnore\b|\bIgnore\b|\bDapper\b|\bQuery\*' --glob '*.cs' \
  | rg '(Repositories/Resgrid\.Repositories\.DataRepository/Core\.Resgrid\.Model/.*/.*\.cs$|Repositories/Resgrid\.Repositories\.DataRepository.*\.cs$)' \
  | head -n 200 || true

printf '\nRepository base implementation files:\n'
git ls-files 'Repositories/Resgrid.Repositories.DataRepository/*.cs' 'Core/Resgrid.Model/*.cs' \
  | rg 'Base|Generic|Repository|Dapper|Mapper|Reflection' \
  | head -n 100

Repository: Resgrid/Core

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo DataRepository file count:\n'
git ls-files 'Repositories/Resgrid.Repositories.DataRepository/*.cs' | wc -l

printf '\nGeneric repository/builder symbols:\n'
rg -n 'class .*Repository|interface .*Repository|Build|Ignore|NotMapped|Insert|Update|InsertDynamic|UpdateDynamic|Dapper|QueryMulti' \
  --glob '*.cs' Repositories/Resgrid.Repositories.DataRepository/Core/Resgrid.Model \
  | head -n 300 || true

printf '\nRepository-specific symbol search using tracked files only:\n'
for f in $(git ls-files 'Repositories/Resgrid.Repositories.DataRepository/*.cs' 'Repositories/Resgrid.Repositories.DataRepository/**/*.cs' | rg 'Base|Generic|Mapping|Reflection|Query|Insert|Update'); do
  echo "--- $f"
  rg -n 'IgnoredProperties|Mapping|NotMapped|Build|Ignore|Dapper|QueryMulti|InsertDynamic|UpdateDynamic|Parameters\(' "$f" | head -n 50 || true
done | head -n 300 || true

Repository: Resgrid/Core

Length of output: 29062


Add Source to PushUri.IgnoredProperties.

Source is [NotMapped] and transient, but the generic insert/update helpers use IgnoredProperties to drop IEntity transient fields. Add "Source" to keep generic persistence in sync while retaining its protobuf field.

🤖 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/PushUri.cs` around lines 54 - 60, Add the "Source"
property name to PushUri.IgnoredProperties while leaving the existing
[NotMapped] and [ProtoMember(10)] attributes unchanged, so generic persistence
excludes this transient field.


[Required]
[ProtoMember(6)]
public DateTime CreatedOn { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Resgrid.Model.Repositories
{
public interface IUnitLocationRetentionRepository
{
Task<int> DeleteHardwareLocationsBeforeAsync(
int departmentId,
DateTime cutoffUtc,
int batchSize,
CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using System.Threading;

namespace Resgrid.Model.Repositories
{
Expand All @@ -12,5 +14,10 @@ public interface IUnitLocationsDocRepository
Task<UnitsLocation> GetByOldIdAsync(string id);
Task<UnitLocationWriteResult> InsertAsync(UnitsLocation location);
Task<UnitLocationWriteResult> UpdateAsync(UnitsLocation location);
Task<int> DeleteHardwareLocationsBeforeAsync(
int departmentId,
DateTime cutoffUtc,
int batchSize,
CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.Threading;

namespace Resgrid.Model.Repositories
{
Expand All @@ -7,5 +9,10 @@ public interface IUnitLocationsMongoRepository
Task EnsureIndexesAsync();
Task<UnitLocationWriteResult> InsertAsync(UnitsLocation location);
Task<UnitLocationWriteResult> UpdateAsync(UnitsLocation location);
Task<int> DeleteHardwareLocationsBeforeAsync(
int departmentId,
DateTime cutoffUtc,
int batchSize,
CancellationToken cancellationToken = default);
}
}
3 changes: 2 additions & 1 deletion Core/Resgrid.Model/Resgrid.Model.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<Configurations>Debug;Release;Docker</Configurations>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Tracking\Catalog\unit-tracking-catalog.json" />
<EmbeddedResource Include="..\..\Web\Resgrid.Web\wwwroot\audio\caf\Emergency_call.caf">
<Link>Audio\Emergency_call.caf</Link>
</EmbeddedResource>
Expand Down Expand Up @@ -92,4 +93,4 @@
<ProjectReference Include="..\Resgrid.Config\Resgrid.Config.csproj" />
<ProjectReference Include="..\Resgrid.Framework\Resgrid.Framework.csproj" />
</ItemGroup>
</Project>
</Project>
5 changes: 3 additions & 2 deletions Core/Resgrid.Model/Services/ICommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ Task<bool> SendCancelUnitCallAsync(Call call, CallDispatchUnit dispatch, string
/// <param name="departmentNumber">The department number.</param>
/// <param name="title">The title.</param>
/// <param name="profile">The profile.</param>
/// <param name="sendToICApp">When true, the push goes to the user's IC app subscriber (separate Novu inbox) instead of the Responder one.</param>
/// <returns>Task&lt;System.Boolean&gt;.</returns>
Task<bool> SendNotificationAsync(string userId, int departmentId, string message, string departmentNumber, Department department,
string title = "Notification", UserProfile profile = null);
Task<bool> SendNotificationAsync(string userId, int departmentId, string message, string departmentNumber, Department department,
string title = "Notification", UserProfile profile = null, bool sendToICApp = false);

/// <summary>
/// Sends the chat.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -30,5 +31,12 @@ Task NotifyLaneLeadChangedAsync(int departmentId, int callId, string laneName, b

/// <summary>Notifies every active user and unit on the incident (plus both commanders) that command was transferred.</summary>
Task NotifyCommandTransferredAsync(IncidentCommand command, string fromUserId, string toUserId, CancellationToken cancellationToken = default(CancellationToken));

/// <summary>
/// Sends a free-form message from a department member directly to specific command users (e.g. the
/// incident commander and deputies). The sender's display name is prepended to the body so recipients
/// know who it came from on every channel.
/// </summary>
Task SendMessageToCommandAsync(int departmentId, string senderUserId, string title, string body, IEnumerable<string> recipientUserIds, CancellationToken cancellationToken = default(CancellationToken));
Comment on lines +34 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Honor the new cancellation token.

The implementation in Core/Resgrid.Services/IncidentCommandNotificationService.cs Lines 117-137 never checks or forwards cancellationToken, so callers cannot cancel a potentially long recipient fan-out. Check cancellation before each send and pass the token to cancellable downstream 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 `@Core/Resgrid.Model/Services/IIncidentCommandNotificationService.cs` around
lines 34 - 40, Update SendMessageToCommandAsync in
IncidentCommandNotificationService to check cancellationToken before each
recipient send and forward it to every cancellable downstream notification
operation. Preserve the existing recipient fan-out and message behavior while
ensuring cancellation interrupts further sends.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep sender resolution inside the best-effort error boundary.

In Core/Resgrid.Services/IncidentCommandNotificationService.cs Lines 117-137, ResolveDisplayNameAsync runs before the per-recipient try/catch. If it throws, SendMessageToCommandAsync propagates the exception and IncidentCommandController.SendMessageToCommand can fail, contradicting this interface’s best-effort contract. Catch/log that failure and use the fallback sender name.

🤖 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/IIncidentCommandNotificationService.cs` around
lines 34 - 40, Update SendMessageToCommandAsync so ResolveDisplayNameAsync runs
within the best-effort error boundary, catching and logging resolution failures
before falling back to the sender’s default name; ensure the method continues
processing recipients and does not propagate the exception to
IncidentCommandController.SendMessageToCommand.

}
}
1 change: 1 addition & 0 deletions Core/Resgrid.Model/Services/IPushService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface IPushService
Task<bool> UnRegister(PushUri pushUri);
void UnRegisterNotificationOnly(PushUri pushUri);
Task<bool> PushNotification(StandardPushMessage message, string userId, UserProfile profile = null);
Task<bool> PushICNotification(StandardPushMessage message, string userId, UserProfile profile = null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Use the required Async suffix for the new asynchronous push API.

  • Core/Resgrid.Model/Services/IPushService.cs#L15-L15: rename PushICNotification to PushICNotificationAsync.
  • Core/Resgrid.Services/PushService.cs#L169-L192: rename the implementation accordingly.
  • Core/Resgrid.Services/CommunicationService.cs#L618-L621: update the IC routing call to the renamed method.

As per coding guidelines, service methods must be async and follow the {Verb}{Entity}{Filter}Async naming pattern.

📍 Affects 3 files
  • Core/Resgrid.Model/Services/IPushService.cs#L15-L15 (this comment)
  • Core/Resgrid.Services/PushService.cs#L169-L192
  • Core/Resgrid.Services/CommunicationService.cs#L618-L621
🤖 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/IPushService.cs` at line 15, Rename the
IPushService.PushICNotification contract to PushICNotificationAsync, rename the
corresponding PushService implementation, and update the IC routing call in
CommunicationService to use the new method name. Apply these changes at
Core/Resgrid.Model/Services/IPushService.cs:15,
Core/Resgrid.Services/PushService.cs:169-192, and
Core/Resgrid.Services/CommunicationService.cs:618-621.

Source: Coding guidelines

Task<bool> RegisterUnit(PushUri pushUri);
Task<bool> UnRegisterUnit(PushUri pushUri);
Task<bool> PushChat(StandardPushMessage message, string userId, UserProfile profile = null);
Expand Down
6 changes: 6 additions & 0 deletions Core/Resgrid.Model/Services/IUnitTrackingIngressService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -11,5 +12,10 @@ Task<TrackingIngressResult> AcceptAsync(
AuthenticatedTrackingSource source,
IReadOnlyCollection<CanonicalTrackingPosition> positions,
CancellationToken cancellationToken = default);

Task<TrackingIngressResult> AcceptHeartbeatAsync(
AuthenticatedTrackingSource source,
DateTime receivedOnUtc,
CancellationToken cancellationToken = default);
}
}
Loading
Loading