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
2 changes: 1 addition & 1 deletion Core/Resgrid.Config/InfoConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static class InfoConfig
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 (GDPR) compliance requirements.",
IsDefault = false,
AppUrl = "https://app.eu-central.resgrid.com",
AppUrl = "https://app-eu-central.resgrid.com",
ApiUrl = "https://api-eu-central.resgrid.com",
AllowsFreeAccounts = false
}
Expand Down
1 change: 1 addition & 0 deletions Core/Resgrid.Model/DepartmentSettingTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ public enum DepartmentSettingTypes
WeatherAlertAutoMessageSeverity = 41,
WeatherAlertCallIntegration = 42,
WeatherAlertCacheMinutes = 43,
WeatherAlertAutoMessageSchedule = 44,
}
}
77 changes: 67 additions & 10 deletions Core/Resgrid.Services/WeatherAlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Resgrid.Framework;
using Resgrid.Model;
using Resgrid.Model.Helpers;
Expand Down Expand Up @@ -319,15 +320,27 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)
continue;
}

// Get the auto-message severity threshold setting
var thresholdSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
// Load per-severity schedule (if configured)
List<AutoMessageSeveritySchedule> schedule = null;
var scheduleSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
if (scheduleSetting != null && !string.IsNullOrWhiteSpace(scheduleSetting.Setting))
{
try { schedule = JsonConvert.DeserializeObject<List<AutoMessageSeveritySchedule>>(scheduleSetting.Setting); }
catch { }
}

int threshold = (int)WeatherAlertSeverity.Severe; // Default: Severe=1
if (thresholdSetting != null && int.TryParse(thresholdSetting.Setting, out var parsed))
threshold = parsed;
// Fall back to legacy threshold if no schedule configured
int legacyThreshold = (int)WeatherAlertSeverity.Severe;
if (schedule == null || schedule.Count == 0)
{
var thresholdSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
if (thresholdSetting != null && int.TryParse(thresholdSetting.Setting, out var parsed))
legacyThreshold = parsed;
}

// Load department for sender info
// Load department for sender info and time conversion
Department department = null;
try
{
Expand All @@ -340,9 +353,8 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)

foreach (var alert in group)
{
// Only send notifications for alerts meeting severity threshold
// Lower enum value = higher severity (Extreme=0, Severe=1, etc.)
if (alert.Severity <= threshold)
bool shouldSend = ShouldSendAutoMessage(alert.Severity, schedule, legacyThreshold, department);
if (shouldSend)
{
try
{
Expand Down Expand Up @@ -594,5 +606,50 @@ private static string Truncate(string value, int maxLength)

return value.Substring(0, maxLength);
}

private static bool ShouldSendAutoMessage(int severity, List<AutoMessageSeveritySchedule> schedule, int legacyThreshold, Department department)
{
if (schedule != null && schedule.Count > 0)
{
var entry = schedule.FirstOrDefault(s => s.Severity == severity);

// Severity not in schedule — don't send
if (entry == null || !entry.Enabled)
return false;

// Check time window (StartHour == 0 && EndHour == 0 means 24h/always)
if (entry.StartHour == 0 && entry.EndHour == 0)
return true;

// Get department local time
var now = DateTime.UtcNow;
if (department != null)
now = now.TimeConverter(department);

int currentHour = now.Hour;

if (entry.StartHour <= entry.EndHour)
{
// Same-day window: e.g. 6-18
return currentHour >= entry.StartHour && currentHour < entry.EndHour;
}
else
{
// Overnight window: e.g. 18-6 (6pm to 6am)
return currentHour >= entry.StartHour || currentHour < entry.EndHour;
}
}

// Legacy: simple severity threshold
return severity <= legacyThreshold;
}

private class AutoMessageSeveritySchedule
{
public int Severity { get; set; }
public bool Enabled { get; set; }
public int StartHour { get; set; }
public int EndHour { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,20 @@ public async Task<ActionResult<GetWeatherAlertSettingsResult>> SaveSettings([Fro
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, input.AutoMessageSeverity.ToString(), DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, input.CallIntegrationEnabled.ToString(), DepartmentSettingTypes.WeatherAlertCallIntegration);

if (input.AutoMessageSchedule != null)
{
var scheduleJson = Newtonsoft.Json.JsonConvert.SerializeObject(input.AutoMessageSchedule);
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, scheduleJson, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
}

var result = new GetWeatherAlertSettingsResult();
result.Data = new WeatherAlertSettingsData
{
WeatherAlertsEnabled = input.WeatherAlertsEnabled,
MinimumSeverity = input.MinimumSeverity,
AutoMessageSeverity = input.AutoMessageSeverity,
CallIntegrationEnabled = input.CallIntegrationEnabled
CallIntegrationEnabled = input.CallIntegrationEnabled,
AutoMessageSchedule = input.AutoMessageSchedule
};

ResponseHelper.PopulateV4ResponseData(result);
Expand All @@ -378,6 +385,7 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
var minSeveritySetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertMinimumSeverity);
var autoMsgSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
var callIntSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertCallIntegration);
var scheduleSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);

if (enabledSetting != null && !string.IsNullOrWhiteSpace(enabledSetting.Setting))
settings.WeatherAlertsEnabled = bool.TryParse(enabledSetting.Setting, out var enabled) && enabled;
Expand All @@ -391,6 +399,15 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
if (callIntSetting != null && !string.IsNullOrWhiteSpace(callIntSetting.Setting))
settings.CallIntegrationEnabled = bool.TryParse(callIntSetting.Setting, out var callInt) && callInt;

if (scheduleSetting != null && !string.IsNullOrWhiteSpace(scheduleSetting.Setting))
{
try
{
settings.AutoMessageSchedule = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.List<WeatherAlertSeverityScheduleData>>(scheduleSetting.Setting);
}
catch { }
}

return settings;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace Resgrid.Web.Services.Models.v4.WeatherAlerts
{
public class SaveWeatherAlertSettingsInput
Expand All @@ -6,5 +8,6 @@ public class SaveWeatherAlertSettingsInput
public int MinimumSeverity { get; set; }
public int AutoMessageSeverity { get; set; }
public bool CallIntegrationEnabled { get; set; }
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;

namespace Resgrid.Web.Services.Models.v4.WeatherAlerts
{
public class WeatherAlertSettingsData
Expand All @@ -6,5 +8,14 @@ public class WeatherAlertSettingsData
public int MinimumSeverity { get; set; }
public int AutoMessageSeverity { get; set; }
public bool CallIntegrationEnabled { get; set; }
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
}

public class WeatherAlertSeverityScheduleData
{
public int Severity { get; set; }
public bool Enabled { get; set; }
public int StartHour { get; set; }
public int EndHour { get; set; }
}
}
54 changes: 54 additions & 0 deletions Web/Resgrid.Web/Areas/User/Views/Shared/_MinimalLayout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@using Microsoft.AspNetCore.Http
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />

<link rel="stylesheet" href="/css/int-bundle.css" />

<link rel="shortcut icon" href="~/favicon.ico" />

@if (IsSectionDefined("Styles"))
{
@RenderSection("Styles", required: false)
}
</head>
<body class="gray-bg">
<div class="container" style="padding-top: 20px;">
@RenderBody()
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
Comment on lines +30 to +35
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.

⚠️ Potential issue | 🟠 Major

Fix the jQuery fallback URL.

asp-fallback-src="lib/jquery/jquery-1.12.3.min.js" is route-relative, so when the CDN fallback is needed on /User/... pages the browser will request the wrong path and all page JS will break.

🐛 Proposed fix
-            asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
+            asp-fallback-src="~/lib/jquery/jquery-1.12.3.min.js"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="~/lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Web/Resgrid.Web/Areas/User/Views/Shared/_MinimalLayout.cshtml` around lines
30 - 35, The asp-fallback-src value on the script tag is route-relative and
breaks on nested routes; update the asp-fallback-src on the <script> tag in
_MinimalLayout.cshtml (the attribute
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js") to be application-rooted
(e.g. prefix with "~/lib/..." or "/lib/...") so the browser requests the correct
file regardless of the current route; leave other attributes (asp-fallback-test,
integrity, crossorigin) unchanged.

<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-vhJnz1OVIdLktyixHY4Uk3OHEwdQqPppqYR8+5mjsauETgLOcEynD9oPHhhz18Nw">
</script>
<script src="~/lib/sweetalert/dist/sweetalert.min.js"></script>

<script>
var resgrid = resgrid || {};
resgrid.absoluteBaseUrl = "@Resgrid.Config.SystemBehaviorConfig.ResgridBaseUrl";
</script>

@if (IsSectionDefined("Scripts"))
{
@RenderSection("Scripts", required: false)
}
</body>
</html>
Loading
Loading