Skip to content

Commit ef42fa9

Browse files
Feature: (Ping - Host up/down) Notifications (BornToBeRoot#3471)
* Feature: NotificationManager + Ping notifications * Feature: Notifications * Docs: BornToBeRoot#3471 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent c89fe5f commit ef42fa9

14 files changed

Lines changed: 892 additions & 7 deletions

File tree

Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Lines changed: 91 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Source/NETworkManager.Localization/Resources/Strings.resx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,6 +2367,36 @@ $$hostname$$ --&gt; Hostname</value>
23672367
<data name="CouldNotResolveIPAddressFor" xml:space="preserve">
23682368
<value>Could not resolve ip address for: "{0}"</value>
23692369
</data>
2370+
<data name="PingMonitorNotification" xml:space="preserve">
2371+
<value>Notification</value>
2372+
</data>
2373+
<data name="ShowNotificationPopupOnStatusChange" xml:space="preserve">
2374+
<value>Show notification popup on status change</value>
2375+
</data>
2376+
<data name="PlaySoundOnStatusChange" xml:space="preserve">
2377+
<value>Play sound on status change</value>
2378+
</data>
2379+
<data name="NotificationSuccessThreshold" xml:space="preserve">
2380+
<value>Success threshold</value>
2381+
</data>
2382+
<data name="HelpMessage_NotificationSuccessThreshold" xml:space="preserve">
2383+
<value>Number of consecutive successful pings required before a "Host is up" notification is shown. Higher values reduce noise from flapping hosts.</value>
2384+
</data>
2385+
<data name="NotificationFailureThreshold" xml:space="preserve">
2386+
<value>Failure threshold</value>
2387+
</data>
2388+
<data name="HelpMessage_NotificationFailureThreshold" xml:space="preserve">
2389+
<value>Number of consecutive failed pings (timeouts) required before a "Host is down" notification is shown. Higher values reduce noise from flapping hosts.</value>
2390+
</data>
2391+
<data name="NotificationDurationInSeconds" xml:space="preserve">
2392+
<value>Display duration (seconds)</value>
2393+
</data>
2394+
<data name="HostIsUp" xml:space="preserve">
2395+
<value>Host is up</value>
2396+
</data>
2397+
<data name="HostIsDown" xml:space="preserve">
2398+
<value>Host is down</value>
2399+
</data>
23702400
<data name="TheApplicationWillBeRestarted" xml:space="preserve">
23712401
<value>The application will be restarted...</value>
23722402
</data>

Source/NETworkManager.Settings/GlobalStaticConfiguration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public static class GlobalStaticConfiguration
3030
// Network config
3131
public static int NetworkChangeDetectionDelay => 5000;
3232

33+
// Notification config
34+
// Minimum interval (ms) between two notification sounds, so a burst of near-simultaneous
35+
// status changes collapses into a single sound instead of an overlapping cacophony.
36+
public static int NotificationSoundThrottle => 3000;
37+
3338
// Profile config
3439
public static bool Profile_TagsMatchAny => true;
3540
public static bool Profile_ExpandProfileView => true;
@@ -147,6 +152,13 @@ public static class GlobalStaticConfiguration
147152
public static int PingMonitor_ChartTime => 120;
148153
public static ExportFileType PingMonitor_ExportFileType => ExportFileType.Csv;
149154

155+
// Application: Ping Monitor (notifications)
156+
public static bool PingMonitor_ShowNotificationPopup => true;
157+
public static bool PingMonitor_NotificationSound => true;
158+
public static int PingMonitor_NotificationSuccessThreshold => 1;
159+
public static int PingMonitor_NotificationFailureThreshold => 3;
160+
public static int PingMonitor_NotificationCloseTime => 10;
161+
150162
// Application: Traceroute
151163
public static int Traceroute_MaximumHops => 30;
152164
public static int Traceroute_Timeout => 4000;

Source/NETworkManager.Settings/SettingsInfo.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,71 @@ public int PingMonitor_ChartTime
15371537
}
15381538
} = GlobalStaticConfiguration.PingMonitor_ChartTime;
15391539

1540+
public bool PingMonitor_ShowNotificationPopup
1541+
{
1542+
get;
1543+
set
1544+
{
1545+
if (value == field)
1546+
return;
1547+
1548+
field = value;
1549+
OnPropertyChanged();
1550+
}
1551+
} = GlobalStaticConfiguration.PingMonitor_ShowNotificationPopup;
1552+
1553+
public bool PingMonitor_NotificationSound
1554+
{
1555+
get;
1556+
set
1557+
{
1558+
if (value == field)
1559+
return;
1560+
1561+
field = value;
1562+
OnPropertyChanged();
1563+
}
1564+
} = GlobalStaticConfiguration.PingMonitor_NotificationSound;
1565+
1566+
public int PingMonitor_NotificationSuccessThreshold
1567+
{
1568+
get;
1569+
set
1570+
{
1571+
if (value == field)
1572+
return;
1573+
1574+
field = value;
1575+
OnPropertyChanged();
1576+
}
1577+
} = GlobalStaticConfiguration.PingMonitor_NotificationSuccessThreshold;
1578+
1579+
public int PingMonitor_NotificationFailureThreshold
1580+
{
1581+
get;
1582+
set
1583+
{
1584+
if (value == field)
1585+
return;
1586+
1587+
field = value;
1588+
OnPropertyChanged();
1589+
}
1590+
} = GlobalStaticConfiguration.PingMonitor_NotificationFailureThreshold;
1591+
1592+
public int PingMonitor_NotificationCloseTime
1593+
{
1594+
get;
1595+
set
1596+
{
1597+
if (value == field)
1598+
return;
1599+
1600+
field = value;
1601+
OnPropertyChanged();
1602+
}
1603+
} = GlobalStaticConfiguration.PingMonitor_NotificationCloseTime;
1604+
15401605
public string PingMonitor_ExportFilePath
15411606
{
15421607
get;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using MahApps.Metro.IconPacks;
2+
using NETworkManager.Settings;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Media;
7+
using System.Threading;
8+
using System.Windows;
9+
10+
namespace NETworkManager;
11+
12+
/// <summary>
13+
/// Shows and manages stackable notification popups. Follows the same pattern as
14+
/// <see cref="DialogHelper"/> — callers pass display parameters; internals are hidden.
15+
/// </summary>
16+
public static class NotificationManager
17+
{
18+
private static readonly List<NotificationWindow> ActiveWindows = [];
19+
20+
/// <summary>
21+
/// Margin (in DIP) between the screen edges and between stacked notification windows.
22+
/// </summary>
23+
internal const double WindowMargin = 10.0;
24+
25+
// Throttles the notification sound (see GlobalStaticConfiguration.NotificationSoundThrottle)
26+
// so a burst of near-simultaneous status changes collapses into a single sound.
27+
private static readonly Lock SoundLock = new();
28+
private static DateTime _lastSoundPlayed = DateTime.MinValue;
29+
30+
/// <summary>
31+
/// Shows a notification popup in the bottom-right corner of the primary screen.
32+
/// Safe to call from any thread.
33+
/// </summary>
34+
/// <param name="iconKind">The Material icon shown on the left of the popup.</param>
35+
/// <param name="iconColor">The icon fill color (e.g. "Red" or "#badc58").</param>
36+
/// <param name="title">The bold header text (e.g. the host title).</param>
37+
/// <param name="message">The gray subtext below the header (e.g. "Host is up").</param>
38+
/// <param name="closeTimeSeconds">Seconds before the popup closes automatically.</param>
39+
public static void Show(PackIconMaterialKind iconKind, string iconColor, string title, string message, int closeTimeSeconds)
40+
{
41+
// A late ping callback during application shutdown may find no Application instance.
42+
if (Application.Current is null)
43+
return;
44+
45+
Application.Current.Dispatcher.BeginInvoke(() =>
46+
{
47+
var window = new NotificationWindow(iconKind, iconColor, title, message, closeTimeSeconds);
48+
49+
window.Closed += (_, _) =>
50+
{
51+
ActiveWindows.Remove(window);
52+
RepositionAll();
53+
};
54+
55+
ActiveWindows.Add(window);
56+
window.Show(); // triggers OnSourceInitialized → window positions itself
57+
});
58+
}
59+
60+
/// <summary>
61+
/// Returns the combined height (including margins) of all windows stacked below the given
62+
/// window, i.e. the vertical offset from the bottom edge at which it should be placed.
63+
/// </summary>
64+
internal static double GetStackOffset(NotificationWindow window)
65+
{
66+
return ActiveWindows.TakeWhile(w => !ReferenceEquals(w, window)).Sum(w => w.ActualHeight + WindowMargin);
67+
}
68+
69+
/// <summary>
70+
/// Plays a notification sound, throttled so that a burst of near-simultaneous status changes
71+
/// (e.g. many hosts going down at once) results in a single sound. Safe to call from any thread.
72+
/// </summary>
73+
/// <param name="sound">The system sound to play.</param>
74+
public static void PlaySound(SystemSound sound)
75+
{
76+
lock (SoundLock)
77+
{
78+
var now = DateTime.UtcNow;
79+
80+
if ((now - _lastSoundPlayed).TotalMilliseconds < GlobalStaticConfiguration.NotificationSoundThrottle)
81+
return;
82+
83+
_lastSoundPlayed = now;
84+
}
85+
86+
sound.Play();
87+
}
88+
89+
/// <summary>
90+
/// Repositions all active windows, e.g. after one closes or its height changes so the stack
91+
/// stays bottom-anchored without gaps or overlap.
92+
/// </summary>
93+
internal static void RepositionAll()
94+
{
95+
foreach (var window in ActiveWindows)
96+
window.Reposition();
97+
}
98+
}

0 commit comments

Comments
 (0)