diff --git a/Core/Resgrid.Services/WeatherAlertService.cs b/Core/Resgrid.Services/WeatherAlertService.cs
index 8fa8814f9..f18dd7444 100644
--- a/Core/Resgrid.Services/WeatherAlertService.cs
+++ b/Core/Resgrid.Services/WeatherAlertService.cs
@@ -195,6 +195,8 @@ public async Task ProcessWeatherAlertSourceAsync(Guid sourceId, CancellationToke
var existing = await _weatherAlertRepository.GetByExternalIdAndSourceIdAsync(
alert.ExternalId, source.WeatherAlertSourceId);
+ TruncateAlertFields(alert);
+
if (existing == null)
{
// New alert
@@ -525,78 +527,31 @@ private static string FormatAlertMessageBody(WeatherAlert alert, Department depa
{
var sb = new System.Text.StringBuilder();
- // Header
- sb.AppendLine($"=== WEATHER ALERT: {alert.Event?.ToUpper()} ===");
- sb.AppendLine();
+ sb.AppendLine($"WEATHER ALERT: {alert.Event?.ToUpper()}");
+ sb.AppendLine($"Severity: {SeverityNames[Math.Min(alert.Severity, 4)]}");
- // Headline
- if (!string.IsNullOrEmpty(alert.Headline))
+ if (alert.ExpiresUtc.HasValue)
{
- sb.AppendLine(alert.Headline);
- sb.AppendLine();
+ if (department != null)
+ sb.AppendLine($"Expires: {alert.ExpiresUtc.Value.TimeConverter(department):MM/dd/yyyy h:mm tt}");
+ else
+ sb.AppendLine($"Expires: {alert.ExpiresUtc.Value:yyyy-MM-dd HH:mm} UTC");
}
- // Classification grid
- sb.AppendLine("--- Alert Details ---");
- sb.AppendLine($"Severity: {SeverityNames[Math.Min(alert.Severity, 4)]}");
- sb.AppendLine($"Urgency: {UrgencyNames[Math.Min(alert.Urgency, 4)]}");
- sb.AppendLine($"Certainty: {CertaintyNames[Math.Min(alert.Certainty, 4)]}");
- sb.AppendLine($"Category: {CategoryNames[Math.Min(alert.AlertCategory, 4)]}");
sb.AppendLine();
- // Timing
- sb.AppendLine("--- Timing ---");
- if (department != null)
- {
- sb.AppendLine($"Effective: {alert.EffectiveUtc.TimeConverter(department):MM/dd/yyyy h:mm tt}");
- if (alert.OnsetUtc.HasValue)
- sb.AppendLine($"Onset: {alert.OnsetUtc.Value.TimeConverter(department):MM/dd/yyyy h:mm tt}");
- if (alert.ExpiresUtc.HasValue)
- sb.AppendLine($"Expires: {alert.ExpiresUtc.Value.TimeConverter(department):MM/dd/yyyy h:mm tt}");
- }
- else
- {
- sb.AppendLine($"Effective: {alert.EffectiveUtc:yyyy-MM-dd HH:mm} UTC");
- if (alert.OnsetUtc.HasValue)
- sb.AppendLine($"Onset: {alert.OnsetUtc.Value:yyyy-MM-dd HH:mm} UTC");
- if (alert.ExpiresUtc.HasValue)
- sb.AppendLine($"Expires: {alert.ExpiresUtc.Value:yyyy-MM-dd HH:mm} UTC");
- }
- sb.AppendLine();
-
- // Affected area
- if (!string.IsNullOrEmpty(alert.AreaDescription))
- {
- sb.AppendLine("--- Affected Area ---");
- sb.AppendLine(alert.AreaDescription);
- sb.AppendLine();
- }
-
- // Description
- if (!string.IsNullOrEmpty(alert.Description))
- {
- sb.AppendLine("--- Description ---");
- sb.AppendLine(alert.Description);
- sb.AppendLine();
- }
+ if (!string.IsNullOrEmpty(alert.Headline))
+ sb.AppendLine(alert.Headline);
- // Instructions (critical for responders)
if (!string.IsNullOrEmpty(alert.Instruction))
{
- sb.AppendLine("--- INSTRUCTIONS ---");
- sb.AppendLine(alert.Instruction);
sb.AppendLine();
+ sb.AppendLine(alert.Instruction);
}
- // Source info
- if (!string.IsNullOrEmpty(alert.Sender))
- sb.AppendLine($"Source: {alert.Sender}");
-
- sb.AppendLine($"Alert ID: {alert.ExternalId}");
sb.AppendLine();
- sb.AppendLine("This is an automated weather alert from the Resgrid Weather Alert System.");
+ sb.AppendLine("View active weather alerts for full details.");
- // Respect the 4000 char body limit
var body = sb.ToString();
if (body.Length > 3950)
body = body.Substring(0, 3947) + "...";
@@ -619,5 +574,24 @@ private static double CalculateDistanceMiles(double lat1, double lng1, double la
private static double ToRadians(double degrees) => degrees * Math.PI / 180;
#endregion
+
+ private static void TruncateAlertFields(WeatherAlert alert)
+ {
+ alert.ExternalId = Truncate(alert.ExternalId, 500);
+ alert.Sender = Truncate(alert.Sender, 500);
+ alert.Event = Truncate(alert.Event, 500);
+ alert.Headline = Truncate(alert.Headline, 500);
+ alert.AreaDescription = Truncate(alert.AreaDescription, 500);
+ alert.CenterGeoLocation = Truncate(alert.CenterGeoLocation, 100);
+ alert.ReferencesExternalId = Truncate(alert.ReferencesExternalId, 500);
+ }
+
+ private static string Truncate(string value, int maxLength)
+ {
+ if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
+ return value;
+
+ return value.Substring(0, maxLength);
+ }
}
}
diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs
index 2aa1a0575..0018fb397 100644
--- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs
+++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitConnection.cs
@@ -13,6 +13,10 @@ internal class RabbitConnection
private static ConnectionFactory _factory { get; set; }
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
+ ///
+ /// Raised when the connection is reset so callers can clear cached declaration state.
+ ///
+ public static event Action ConnectionReset;
public static async Task VerifyAndCreateClients(string clientName)
{
@@ -21,6 +25,7 @@ public static async Task VerifyAndCreateClients(string clientName)
_connection.Dispose();
_connection = null;
_factory = null;
+ RaiseConnectionReset();
}
if (_connection == null)
@@ -77,7 +82,7 @@ public static async Task VerifyAndCreateClients(string clientName)
{
try
{
- var channel = await _connection.CreateChannelAsync();
+ using var channel = await _connection.CreateChannelAsync();
await channel.QueueDeclareAsync(queue: SetQueueNameForEnv(ServiceBusConfig.SystemQueueName),
durable: true,
@@ -180,6 +185,7 @@ public static async Task CreateConnection(string clientName)
_connection.Dispose();
_connection = null;
_factory = null;
+ RaiseConnectionReset();
await VerifyAndCreateClients(clientName);
}
@@ -187,6 +193,25 @@ public static async Task CreateConnection(string clientName)
return _connection;
}
+ private static void RaiseConnectionReset()
+ {
+ var handler = ConnectionReset;
+ if (handler == null)
+ return;
+
+ foreach (var subscriber in handler.GetInvocationList())
+ {
+ try
+ {
+ ((Action)subscriber).Invoke();
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+ }
+ }
+
public static string SetQueueNameForEnv(string cacheKey)
{
if (Config.SystemBehaviorConfig.Environment == SystemEnvironment.Dev)
diff --git a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs
index 29f3ecb84..373f48327 100644
--- a/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs
+++ b/Providers/Resgrid.Providers.Bus.Rabbit/RabbitTopicProvider.cs
@@ -14,6 +14,12 @@ namespace Resgrid.Providers.Bus.Rabbit
public class RabbitTopicProvider
{
private readonly string _clientName = "Resgrid-Topic";
+ private static volatile bool _exchangeDeclared;
+
+ static RabbitTopicProvider()
+ {
+ RabbitConnection.ConnectionReset += () => _exchangeDeclared = false;
+ }
public async Task PersonnelStatusChanged(UserStatusEvent message)
{
@@ -117,14 +123,20 @@ private static async Task VerifyAndCreateClients(string clientName)
{
try
{
+ // Validate/create connection first so a reconnect clears _exchangeDeclared
var connection = await RabbitConnection.CreateConnection(clientName);
+ if (_exchangeDeclared)
+ return true;
+
if (connection != null)
{
using (var channel = await connection.CreateChannelAsync())
{
await channel.ExchangeDeclareAsync(RabbitConnection.SetQueueNameForEnv(Topics.EventingTopic), "fanout");
}
+
+ _exchangeDeclared = true;
}
}
catch (Exception ex)
diff --git a/Web/Resgrid.Web.Services/Controllers/TwilioController.cs b/Web/Resgrid.Web.Services/Controllers/TwilioController.cs
index d2218ff32..eb0939856 100644
--- a/Web/Resgrid.Web.Services/Controllers/TwilioController.cs
+++ b/Web/Resgrid.Web.Services/Controllers/TwilioController.cs
@@ -591,14 +591,14 @@ public async Task VoiceCallAction(string userId, int callId, [From
response.Say("You have been marked responding to the scene, goodbye.").Hangup();
}
- else
+ else if (int.TryParse(twilioRequest.Digits, out var digit))
{
var call = await _callsService.GetCallByIdAsync(callId);
var stations = await _departmentGroupsService.GetAllStationGroupsForDepartmentAsync(call.DepartmentId);
- int index = int.Parse(twilioRequest.Digits) - 2;
+ int index = digit - 2;
- if (index >= 0 && index < 8)
+ if (index >= 0 && index < stations.Count)
{
var station = stations[index];
@@ -611,6 +611,10 @@ await _actionLogsService.SetUserActionAsync(userId, call.DepartmentId, (int)Acti
}
}
}
+ else
+ {
+ response.Say("Sorry, that was not a valid selection.").Redirect(new Uri(string.Format("{0}/api/Twilio/VoiceCall?userId={1}&callId={2}", Config.SystemBehaviorConfig.ResgridApiBaseUrl, userId, callId)), "GET");
+ }
return new ContentResult
{
diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CustomStatusesController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CustomStatusesController.cs
index 913dd73fb..4121e01af 100644
--- a/Web/Resgrid.Web/Areas/User/Controllers/CustomStatusesController.cs
+++ b/Web/Resgrid.Web/Areas/User/Controllers/CustomStatusesController.cs
@@ -102,8 +102,8 @@ where key.ToString().StartsWith("buttonText_")
if (gpsValue == "on")
gps = true;
- var noteType = int.Parse(form["noteType_" + i]);
- var detailType = int.Parse(form["detailType_" + i]);
+ int.TryParse(form["noteType_" + i], out var noteType);
+ int.TryParse(form["detailType_" + i], out var detailType);
var detail = new CustomStateDetail();
detail.ButtonText = text;
@@ -113,15 +113,8 @@ where key.ToString().StartsWith("buttonText_")
detail.DetailType = detailType;
detail.TextColor = textColor;
- if (!string.IsNullOrWhiteSpace(order))
- detail.Order = int.Parse(order);
- else
- detail.Order = 0;
-
- if (!string.IsNullOrWhiteSpace(baseType))
- detail.BaseType = int.Parse(baseType);
- else
- detail.BaseType = 0;
+ detail.Order = int.TryParse(order, out var parsedNewOrder) ? parsedNewOrder : 0;
+ detail.BaseType = int.TryParse(baseType, out var parsedNewBaseType) ? parsedNewBaseType : -1;
model.State.Details.Add(detail);
}
@@ -217,6 +210,8 @@ public async Task EditDetail(EditDetailView model, CancellationTo
model.DetailTypes = model.DetailType.ToSelectList();
model.NoteTypes = model.NoteType.ToSelectList();
+ model.BaseTypes = model.BaseType.ToSelectList();
+ model.Detail.CustomState = await _customStateService.GetCustomSateByIdAsync(model.Detail.CustomStateId);
if (ModelState.IsValid)
{
@@ -248,8 +243,6 @@ public async Task EditDetail(EditDetailView model, CancellationTo
return RedirectToAction("Edit", new { id = detail.CustomStateId });
}
- model.Detail.CustomState = await _customStateService.GetCustomSateByIdAsync(model.Detail.CustomStateId);
-
return View(model);
}
@@ -292,8 +285,8 @@ where key.ToString().StartsWith("buttonText_")
if (gpsValue == "on")
gps = true;
- var noteType = int.Parse(form["noteType_" + i]);
- var detailType = int.Parse(form["detailType_" + i]);
+ int.TryParse(form["noteType_" + i], out var noteType);
+ int.TryParse(form["detailType_" + i], out var detailType);
var detail = new CustomStateDetail();
detail.ButtonText = text;
@@ -302,8 +295,8 @@ where key.ToString().StartsWith("buttonText_")
detail.NoteType = noteType;
detail.DetailType = detailType;
detail.TextColor = textColor;
- detail.Order = int.Parse(order);
- detail.BaseType = int.Parse(baseType);
+ detail.Order = int.TryParse(order, out var parsedOrder) ? parsedOrder : 0;
+ detail.BaseType = int.TryParse(baseType, out var parsedBaseType) ? parsedBaseType : -1;
if (!string.IsNullOrWhiteSpace(customStateDetailId))
detail.CustomStateDetailId = int.Parse(customStateDetailId);
diff --git a/Web/Resgrid.Web/Areas/User/Controllers/GroupsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/GroupsController.cs
index fd4c8a327..1a36cf9d4 100644
--- a/Web/Resgrid.Web/Areas/User/Controllers/GroupsController.cs
+++ b/Web/Resgrid.Web/Areas/User/Controllers/GroupsController.cs
@@ -606,7 +606,8 @@ public async Task Geofence(int departmentGroupId)
if (model.Group.DepartmentId != DepartmentId)
return Unauthorized();
- model.Coordinates = await _departmentGroupsService.GetMapCenterCoordinatesForGroupAsync(departmentGroupId);
+ model.Coordinates = await _departmentGroupsService.GetMapCenterCoordinatesForGroupAsync(departmentGroupId)
+ ?? new Coordinates { Latitude = 39.8283, Longitude = -98.5795 };
return View(model);
}
diff --git a/Web/Resgrid.Web/Areas/User/Views/Shifts/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shifts/Index.cshtml
index 14018b117..d4ef89891 100644
--- a/Web/Resgrid.Web/Areas/User/Views/Shifts/Index.cshtml
+++ b/Web/Resgrid.Web/Areas/User/Views/Shifts/Index.cshtml
@@ -9,7 +9,6 @@
@section Styles
{
-