From 9b1689e5915030c0f4dcd35885e37b144b8e1f94 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 6 May 2026 18:03:26 -0700 Subject: [PATCH 1/2] RE1-T115 Bug fixes --- .../Areas/User/CustomMaps/CustomMaps.ar.resx | 1 + .../Areas/User/CustomMaps/CustomMaps.en.resx | 3 +++ .../Resgrid.Model/Helpers/SerializerHelper.cs | 2 ++ Core/Resgrid.Model/PlanAddon.cs | 9 +++++++ Core/Resgrid.Services/CustomMapService.cs | 6 +++++ Core/Resgrid.Services/UsersService.cs | 3 +++ .../NwsWeatherAlertProvider.cs | 25 ++++++++++++++++--- .../User/Controllers/CustomMapsController.cs | 10 ++++++++ .../User/Controllers/PersonnelController.cs | 6 +++++ .../User/Controllers/TemplatesController.cs | 15 +++++++++++ .../Areas/User/Views/CustomMaps/Import.cshtml | 18 +++++++------ 11 files changed, 88 insertions(+), 10 deletions(-) diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx index 926203916..95b7a20ac 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx @@ -118,4 +118,5 @@ الحالة التاريخ خطأ + لا توجد طبقات متاحة. يرجى إنشاء طبقة أولاً قبل الاستيراد. diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx index d9fdbed56..878e54e55 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx @@ -365,4 +365,7 @@ Error + + No layers available. Please create a layer first before importing. + diff --git a/Core/Resgrid.Model/Helpers/SerializerHelper.cs b/Core/Resgrid.Model/Helpers/SerializerHelper.cs index 74d7a3ecf..50bb57043 100644 --- a/Core/Resgrid.Model/Helpers/SerializerHelper.cs +++ b/Core/Resgrid.Model/Helpers/SerializerHelper.cs @@ -11,7 +11,9 @@ public static void WarmUpProtobufSerializer() Serializer.PrepareSerializer
(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); + Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); + Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); Serializer.PrepareSerializer(); diff --git a/Core/Resgrid.Model/PlanAddon.cs b/Core/Resgrid.Model/PlanAddon.cs index 18b822191..60c9ae586 100644 --- a/Core/Resgrid.Model/PlanAddon.cs +++ b/Core/Resgrid.Model/PlanAddon.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using ProtoBuf; using Resgrid.Framework; using System; using System.Collections.Generic; @@ -6,20 +7,28 @@ namespace Resgrid.Model { + [ProtoContract] public class PlanAddon : IEntity { + [ProtoMember(1)] public string PlanAddonId { get; set; } + [ProtoMember(2)] public int? PlanId { get; set; } + [ProtoMember(3)] public virtual Plan Plan { get; set; } + [ProtoMember(4)] public int AddonType { get; set; } + [ProtoMember(5)] public double Cost { get; set; } + [ProtoMember(6)] public string ExternalId { get; set; } + [ProtoMember(7)] public string TestExternalId { get; set; } [NotMapped] diff --git a/Core/Resgrid.Services/CustomMapService.cs b/Core/Resgrid.Services/CustomMapService.cs index e0d024f1c..aa5135428 100644 --- a/Core/Resgrid.Services/CustomMapService.cs +++ b/Core/Resgrid.Services/CustomMapService.cs @@ -311,6 +311,9 @@ public async Task GetTileAsync(string layerId, int z, int x, int public async Task ImportGeoJsonAsync(string mapId, string layerId, string geoJsonString, string userId, CancellationToken cancellationToken = default(CancellationToken)) { + if (string.IsNullOrWhiteSpace(layerId)) + throw new ArgumentException("A target layer must be specified for import.", nameof(layerId)); + var import = new CustomMapImport { CustomMapId = mapId, @@ -353,6 +356,9 @@ public async Task GetTileAsync(string layerId, int z, int x, int public async Task ImportKmlAsync(string mapId, string layerId, Stream kmlStream, bool isKmz, string userId, CancellationToken cancellationToken = default(CancellationToken)) { + if (string.IsNullOrWhiteSpace(layerId)) + throw new ArgumentException("A target layer must be specified for import.", nameof(layerId)); + var import = new CustomMapImport { CustomMapId = mapId, diff --git a/Core/Resgrid.Services/UsersService.cs b/Core/Resgrid.Services/UsersService.cs index 78b26b2fa..10d051266 100644 --- a/Core/Resgrid.Services/UsersService.cs +++ b/Core/Resgrid.Services/UsersService.cs @@ -105,6 +105,9 @@ public async Task DoesUserHaveAnyActiveDepartments(string userName) public IdentityUser GetUserById(string userId, bool bypassCache = true) { + if (string.IsNullOrWhiteSpace(userId)) + return null; + if (!bypassCache && Config.SystemBehaviorConfig.CacheEnabled) { Func getUser = delegate () diff --git a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs index aee2efe34..242216a02 100644 --- a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs +++ b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs @@ -79,14 +79,33 @@ public async Task> FetchAlertsAsync(WeatherAlertSource source if (response.StatusCode == System.Net.HttpStatusCode.NotModified) return alerts; // No changes since last poll - response.EnsureSuccessStatusCode(); + // Read response body before checking status for diagnostic context + var json = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + // For client errors (4xx), the request is malformed — retrying won't help. + // Log the full diagnostic context and return empty rather than crashing the poller. + var snippet = json.Length > 500 ? json.Substring(0, 500) : json; + var errorMsg = $"NWS API returned {(int)response.StatusCode} ({response.ReasonPhrase}) " + + $"for URL '{url}', departmentId='{source.DepartmentId}', areaFilter='{source.AreaFilter}'. " + + $"Response body: {snippet}"; + + if ((int)response.StatusCode >= 500) + { + // Server errors are transient — throw so the poller can retry + throw new HttpRequestException(errorMsg); + } + + // Client errors (400, etc.) are permanent — log and return empty + System.Diagnostics.Debug.WriteLine(errorMsg); + return alerts; + } // Update ETag on source if (response.Headers.ETag != null) source.LastETag = response.Headers.ETag.Tag; - var json = await response.Content.ReadAsStringAsync(); - // Validate response content-type is JSON before parsing var contentType = response.Content.Headers.ContentType?.MediaType ?? ""; if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase)) diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs index 1b93ba535..53034d1a6 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs @@ -277,6 +277,16 @@ public async Task ImportUpload(string mapId, string layerId, IFor if (map == null || map.DepartmentId != DepartmentId) return RedirectToAction("Index"); + if (string.IsNullOrWhiteSpace(layerId)) + { + var model = new CustomMapImportView(); + model.Map = map; + model.Layers = await _customMapService.GetLayersForMapAsync(mapId); + model.Imports = await _customMapService.GetImportsForMapAsync(mapId); + model.Message = "Please select a target layer before importing. Create a layer first if none exist."; + return View("Import", model); + } + if (importFile == null || importFile.Length == 0) return RedirectToAction("Import", new { id = mapId }); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs b/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs index b1100005e..74a9ca34b 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/PersonnelController.cs @@ -649,6 +649,9 @@ public async Task AddPerson(AddPersonModel model, IFormCollection [Authorize(Policy = ResgridResources.Personnel_Delete)] public async Task DeletePerson(string userId) { + if (string.IsNullOrWhiteSpace(userId)) + return RedirectToAction("Index", "Personnel", new { area = "User" }); + if (!await _authorizationService.CanUserDeleteUserAsync(DepartmentId, UserId, userId)) return Unauthorized(); @@ -666,6 +669,9 @@ public async Task DeletePerson(string userId) [RequiresRecentTwoFactor] public async Task DeletePerson(DeletePersonModel model, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(model?.UserId)) + return RedirectToAction("Index", "Personnel", new { area = "User" }); + if (!await _authorizationService.CanUserDeleteUserAsync(DepartmentId, UserId, model.UserId)) return Unauthorized(); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs index 18d05758a..8a7fefd30 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs @@ -70,6 +70,17 @@ public async Task New() return View(model); } + private async Task PopulateDropdowns(NewTemplateModel model) + { + var priorites = await _callsService.GetActiveCallPrioritiesForDepartmentAsync(DepartmentId); + model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)); + + List types = new List(); + types.Add(new CallType { CallTypeId = 0, Type = "No Type" }); + types.AddRange(await _callsService.GetCallTypesForDepartmentAsync(DepartmentId)); + model.CallTypes = new SelectList(types, "Type", "Type"); + } + [HttpPost] [Authorize(Policy = ResgridResources.Department_Update)] public async Task New(NewTemplateModel model, CancellationToken cancellationToken) @@ -77,6 +88,7 @@ public async Task New(NewTemplateModel model, CancellationToken c if (String.IsNullOrWhiteSpace(model.Template.CallName) && String.IsNullOrWhiteSpace(model.Template.CallNature)) { + await PopulateDropdowns(model); model.Message = "You must specify a call name and/or call nature to set to save the template"; return View(model); } @@ -91,6 +103,7 @@ public async Task New(NewTemplateModel model, CancellationToken c return RedirectToAction("Index"); } + await PopulateDropdowns(model); return View(model); } @@ -125,6 +138,7 @@ public async Task Edit(NewTemplateModel model, CancellationToken if (String.IsNullOrWhiteSpace(model.Template.CallName) && String.IsNullOrWhiteSpace(model.Template.CallNature)) { + await PopulateDropdowns(model); model.Message = "You must specify a call name and/or call nature to set to save the template"; return View(model); } @@ -139,6 +153,7 @@ public async Task Edit(NewTemplateModel model, CancellationToken return RedirectToAction("Index"); } + await PopulateDropdowns(model); return View(model); } diff --git a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml index cecdcb094..43448ad44 100644 --- a/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/CustomMaps/Import.cshtml @@ -34,15 +34,19 @@
- + @foreach (var layer in Model.Layers) { } - } - + + }
@@ -52,7 +56,7 @@ @localizer["ImportFileHelp"]
- +
From 10ac0209267881dc2e3a9ad7c727508688c3a698 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 6 May 2026 18:24:44 -0700 Subject: [PATCH 2/2] RE1-T115 PR#372 fixes --- .../Areas/User/CustomMaps/CustomMaps.ar.resx | 1 + .../Areas/User/CustomMaps/CustomMaps.en.resx | 3 +++ Core/Resgrid.Model/PlanAddon.cs | 9 ++++++--- .../Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs | 7 ++++--- .../Areas/User/Controllers/CustomMapsController.cs | 8 ++++++-- .../Areas/User/Controllers/TemplatesController.cs | 4 ++-- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx index 95b7a20ac..15025adfa 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.ar.resx @@ -119,4 +119,5 @@ التاريخ خطأ لا توجد طبقات متاحة. يرجى إنشاء طبقة أولاً قبل الاستيراد. + يرجى تحديد طبقة الهدف قبل الاستيراد. أنشئ طبقة أولاً إذا لم تكن موجودة. diff --git a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx index 878e54e55..471295e30 100644 --- a/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx +++ b/Core/Resgrid.Localization/Areas/User/CustomMaps/CustomMaps.en.resx @@ -368,4 +368,7 @@ No layers available. Please create a layer first before importing. + + Please select a target layer before importing. Create a layer first if none exist. + diff --git a/Core/Resgrid.Model/PlanAddon.cs b/Core/Resgrid.Model/PlanAddon.cs index 60c9ae586..fca7d9d35 100644 --- a/Core/Resgrid.Model/PlanAddon.cs +++ b/Core/Resgrid.Model/PlanAddon.cs @@ -42,10 +42,13 @@ public class PlanAddon : IEntity public string GetExternalKey() { - if (Config.PaymentProviderConfig.IsTestMode) - return TestExternalId; - else + if (!string.IsNullOrEmpty(ExternalId)) return ExternalId; + + if (Config.PaymentProviderConfig.IsTestMode && !string.IsNullOrEmpty(TestExternalId)) + return TestExternalId; + + return null; } [NotMapped] diff --git a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs index 242216a02..fc849dbe4 100644 --- a/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs +++ b/Providers/Resgrid.Providers.Weather/NwsWeatherAlertProvider.cs @@ -1,4 +1,5 @@ using System; +using Resgrid.Framework; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -80,7 +81,7 @@ public async Task> FetchAlertsAsync(WeatherAlertSource source return alerts; // No changes since last poll // Read response body before checking status for diagnostic context - var json = await response.Content.ReadAsStringAsync(); + var json = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; if (!response.IsSuccessStatusCode) { @@ -98,7 +99,7 @@ public async Task> FetchAlertsAsync(WeatherAlertSource source } // Client errors (400, etc.) are permanent — log and return empty - System.Diagnostics.Debug.WriteLine(errorMsg); + Logging.LogError(errorMsg); return alerts; } @@ -107,7 +108,7 @@ public async Task> FetchAlertsAsync(WeatherAlertSource source source.LastETag = response.Headers.ETag.Tag; // Validate response content-type is JSON before parsing - var contentType = response.Content.Headers.ContentType?.MediaType ?? ""; + var contentType = response.Content?.Headers.ContentType?.MediaType ?? ""; if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase)) { var snippet = json.Length > 200 ? json.Substring(0, 200) : json; diff --git a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs index 53034d1a6..12afbb3c5 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/CustomMapsController.cs @@ -9,6 +9,8 @@ using Resgrid.Model; using Resgrid.Model.Services; using Resgrid.Web.Areas.User.Models.CustomMaps; +using Microsoft.Extensions.Localization; +using Resgrid.Localization.Areas.User.CustomMaps; using Resgrid.Web.Helpers; namespace Resgrid.Web.Areas.User.Controllers @@ -17,10 +19,12 @@ namespace Resgrid.Web.Areas.User.Controllers public class CustomMapsController : SecureBaseController { private readonly ICustomMapService _customMapService; + private readonly IStringLocalizer _localizer; - public CustomMapsController(ICustomMapService customMapService) + public CustomMapsController(ICustomMapService customMapService, IStringLocalizer localizer) { _customMapService = customMapService; + _localizer = localizer; } #region Map CRUD @@ -283,7 +287,7 @@ public async Task ImportUpload(string mapId, string layerId, IFor model.Map = map; model.Layers = await _customMapService.GetLayersForMapAsync(mapId); model.Imports = await _customMapService.GetImportsForMapAsync(mapId); - model.Message = "Please select a target layer before importing. Create a layer first if none exist."; + model.Message = _localizer["PleaseSelectTargetLayer"]; return View("Import", model); } diff --git a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs index 8a7fefd30..01b03fe77 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/TemplatesController.cs @@ -59,7 +59,7 @@ public async Task New() model.Template = new CallQuickTemplate(); var priorites = await _callsService.GetActiveCallPrioritiesForDepartmentAsync(DepartmentId); - model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)); + model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)?.DepartmentCallPriorityId); List types = new List(); @@ -73,7 +73,7 @@ public async Task New() private async Task PopulateDropdowns(NewTemplateModel model) { var priorites = await _callsService.GetActiveCallPrioritiesForDepartmentAsync(DepartmentId); - model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)); + model.CallPriorities = new SelectList(priorites, "DepartmentCallPriorityId", "Name", priorites.FirstOrDefault(x => x.IsDefault)?.DepartmentCallPriorityId); List types = new List(); types.Add(new CallType { CallTypeId = 0, Type = "No Type" });