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 { -