From a86aa4c5d3315a6cea59bd23d3dac9fca539bec1 Mon Sep 17 00:00:00 2001
From: The Dark <12370876+CheAle14@users.noreply.github.com>
Date: Mon, 8 May 2023 02:57:14 +0100
Subject: [PATCH] New: On Health Restored notification

(cherry picked from commit 5fdc8514da7c7ad98192f2ecb2415b3a7b5d0d05)
---
 .../Notifications/Notification.js             | 24 +++++++++++++----
 .../Notifications/NotificationEventItems.js   | 15 ++++++++++-
 .../NotificationBaseFixture.cs                |  7 +++++
 .../032_health_restored_notification.cs       | 14 ++++++++++
 src/NzbDrone.Core/Datastore/TableMapping.cs   |  1 +
 .../HealthCheck/HealthCheckRestoredEvent.cs   | 16 ++++++++++++
 .../HealthCheck/HealthCheckService.cs         |  7 +++++
 src/NzbDrone.Core/Localization/Core/en.json   |  2 ++
 .../Notifications/Apprise/Apprise.cs          |  5 ++++
 .../Notifications/Boxcar/Boxcar.cs            |  5 ++++
 .../CustomScript/CustomScript.cs              | 15 +++++++++++
 .../Notifications/Discord/Discord.cs          | 23 ++++++++++++++++
 .../Notifications/Email/Email.cs              |  5 ++++
 .../Notifications/Gotify/Gotify.cs            |  5 ++++
 .../Notifications/INotification.cs            |  2 ++
 src/NzbDrone.Core/Notifications/Join/Join.cs  |  5 ++++
 .../Notifications/Mailgun/Mailgun.cs          |  5 ++++
 .../Notifications/Notifiarr/Notifiarr.cs      |  5 ++++
 .../Notifications/NotificationBase.cs         |  7 +++++
 .../Notifications/NotificationDefinition.cs   |  4 ++-
 .../Notifications/NotificationFactory.cs      |  7 +++++
 .../Notifications/NotificationService.cs      | 26 +++++++++++++++++--
 src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs  |  5 ++++
 .../Notifications/Prowl/Prowl.cs              |  5 ++++
 .../Notifications/PushBullet/PushBullet.cs    |  5 ++++
 .../Notifications/Pushover/Pushover.cs        |  5 ++++
 .../Notifications/SendGrid/SendGrid.cs        |  5 ++++
 .../Notifications/Simplepush/Simplepush.cs    |  5 ++++
 .../Notifications/Slack/Slack.cs              | 17 ++++++++++++
 .../Notifications/Telegram/Telegram.cs        |  5 ++++
 .../Notifications/Twitter/Twitter.cs          |  5 ++++
 .../Notifications/Webhook/Webhook.cs          |  5 ++++
 .../Notifications/Webhook/WebhookBase.cs      | 13 ++++++++++
 .../Notifications/Webhook/WebhookEventType.cs |  3 ++-
 .../Notifications/NotificationResource.cs     |  6 +++++
 35 files changed, 279 insertions(+), 10 deletions(-)
 create mode 100644 src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs
 create mode 100644 src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs

diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js
index 1331a7c8b..66021fd0d 100644
--- a/frontend/src/Settings/Notifications/Notifications/Notification.js
+++ b/frontend/src/Settings/Notifications/Notifications/Notification.js
@@ -57,9 +57,11 @@ class Notification extends Component {
       name,
       onGrab,
       onHealthIssue,
+      onHealthRestored,
       onApplicationUpdate,
       supportsOnGrab,
       supportsOnHealthIssue,
+      supportsOnHealthRestored,
       supportsOnApplicationUpdate
     } = this.props;
 
@@ -74,17 +76,27 @@ class Notification extends Component {
         </div>
 
         {
-          supportsOnGrab && onGrab &&
+          supportsOnGrab && onGrab ?
             <Label kind={kinds.SUCCESS}>
               {translate('OnGrab')}
-            </Label>
+            </Label> :
+            null
         }
 
         {
-          supportsOnHealthIssue && onHealthIssue &&
+          supportsOnHealthIssue && onHealthIssue ?
             <Label kind={kinds.SUCCESS}>
               {translate('OnHealthIssue')}
-            </Label>
+            </Label> :
+            null
+        }
+
+        {
+          supportsOnHealthRestored && onHealthRestored ?
+            <Label kind={kinds.SUCCESS}>
+              {translate('OnHealthRestored')}
+            </Label> :
+            null
         }
 
         {
@@ -96,7 +108,7 @@ class Notification extends Component {
         }
 
         {
-          !onGrab && !onHealthIssue && !onApplicationUpdate ?
+          !onGrab && !onHealthIssue && !onHealthRestored && !onApplicationUpdate ?
             <Label
               kind={kinds.DISABLED}
               outline={true}
@@ -132,9 +144,11 @@ Notification.propTypes = {
   name: PropTypes.string.isRequired,
   onGrab: PropTypes.bool.isRequired,
   onHealthIssue: PropTypes.bool.isRequired,
+  onHealthRestored: PropTypes.bool.isRequired,
   onApplicationUpdate: PropTypes.bool.isRequired,
   supportsOnGrab: PropTypes.bool.isRequired,
   supportsOnHealthIssue: PropTypes.bool.isRequired,
+  supportsOnHealthRestored: PropTypes.bool.isRequired,
   supportsOnApplicationUpdate: PropTypes.bool.isRequired,
   onConfirmDeleteNotification: PropTypes.func.isRequired
 };
diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js
index 153842692..1508b029f 100644
--- a/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js
+++ b/frontend/src/Settings/Notifications/Notifications/NotificationEventItems.js
@@ -17,10 +17,12 @@ function NotificationEventItems(props) {
   const {
     onGrab,
     onHealthIssue,
+    onHealthRestored,
     onApplicationUpdate,
     supportsOnGrab,
     includeManualGrabs,
     supportsOnHealthIssue,
+    supportsOnHealthRestored,
     includeHealthWarnings,
     supportsOnApplicationUpdate
   } = item;
@@ -70,8 +72,19 @@ function NotificationEventItems(props) {
             />
           </div>
 
+          <div>
+            <FormInputGroup
+              type={inputTypes.CHECK}
+              name="onHealthRestored"
+              helpText={translate('OnHealthRestoredHelpText')}
+              isDisabled={!supportsOnHealthRestored.value}
+              {...onHealthRestored}
+              onChange={onInputChange}
+            />
+          </div>
+
           {
-            onHealthIssue.value &&
+            (onHealthIssue.value || onHealthRestored.value) &&
               <div>
                 <FormInputGroup
                   type={inputTypes.CHECK}
diff --git a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
index 366687860..42c92c909 100644
--- a/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
+++ b/src/NzbDrone.Core.Test/NotificationTests/NotificationBaseFixture.cs
@@ -51,6 +51,11 @@ namespace NzbDrone.Core.Test.NotificationTests
                 TestLogger.Info("OnHealthIssue was called");
             }
 
+            public override void OnHealthRestored(Core.HealthCheck.HealthCheck healthCheck)
+            {
+                TestLogger.Info("OnHealthRestored was called");
+            }
+
             public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
             {
                 TestLogger.Info("OnApplicationUpdate was called");
@@ -79,6 +84,7 @@ namespace NzbDrone.Core.Test.NotificationTests
             var notification = new TestNotificationWithAllEvents();
 
             notification.SupportsOnHealthIssue.Should().BeTrue();
+            notification.SupportsOnHealthRestored.Should().BeTrue();
             notification.SupportsOnApplicationUpdate.Should().BeTrue();
             notification.SupportsOnGrab.Should().BeTrue();
         }
@@ -89,6 +95,7 @@ namespace NzbDrone.Core.Test.NotificationTests
             var notification = new TestNotificationWithNoEvents();
 
             notification.SupportsOnHealthIssue.Should().BeFalse();
+            notification.SupportsOnHealthRestored.Should().BeFalse();
             notification.SupportsOnApplicationUpdate.Should().BeFalse();
             notification.SupportsOnGrab.Should().BeFalse();
         }
diff --git a/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs b/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs
new file mode 100644
index 000000000..81ed4a209
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/032_health_restored_notification.cs
@@ -0,0 +1,14 @@
+using FluentMigrator;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+    [Migration(032)]
+    public class health_restored_notification : NzbDroneMigrationBase
+    {
+        protected override void MainDbUpgrade()
+        {
+            Alter.Table("Notifications").AddColumn("OnHealthRestored").AsBoolean().WithDefaultValue(0);
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs
index 93f4616d4..aa87b40f2 100644
--- a/src/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/src/NzbDrone.Core/Datastore/TableMapping.cs
@@ -69,6 +69,7 @@ namespace NzbDrone.Core.Datastore
                   .Ignore(x => x.ImplementationName)
                   .Ignore(i => i.SupportsOnGrab)
                   .Ignore(i => i.SupportsOnHealthIssue)
+                  .Ignore(i => i.SupportsOnHealthRestored)
                   .Ignore(i => i.SupportsOnApplicationUpdate);
 
             Mapper.Entity<IndexerProxyDefinition>("IndexerProxies").RegisterModel()
diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs
new file mode 100644
index 000000000..a31b63cc4
--- /dev/null
+++ b/src/NzbDrone.Core/HealthCheck/HealthCheckRestoredEvent.cs
@@ -0,0 +1,16 @@
+using NzbDrone.Common.Messaging;
+
+namespace NzbDrone.Core.HealthCheck
+{
+    public class HealthCheckRestoredEvent : IEvent
+    {
+        public HealthCheck PreviousCheck { get; private set; }
+        public bool IsInStartupGracePeriod { get; private set; }
+
+        public HealthCheckRestoredEvent(HealthCheck previousCheck, bool isInStartupGracePeriod)
+        {
+            PreviousCheck = previousCheck;
+            IsInStartupGracePeriod = isInStartupGracePeriod;
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs
index 1538674ab..11ae42719 100644
--- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs
+++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs
@@ -91,6 +91,13 @@ namespace NzbDrone.Core.HealthCheck
             {
                 if (result.Type == HealthCheckResult.Ok)
                 {
+                    var previous = _healthCheckResults.Find(result.Source.Name);
+
+                    if (previous != null)
+                    {
+                        _eventAggregator.PublishEvent(new HealthCheckRestoredEvent(previous, !_hasRunHealthChecksAfterGracePeriod));
+                    }
+
                     _healthCheckResults.Remove(result.Source.Name);
                 }
                 else
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 77db15c79..48d631a9a 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -294,6 +294,8 @@
   "OnGrabHelpText": "On Release Grab",
   "OnHealthIssue": "On Health Issue",
   "OnHealthIssueHelpText": "On Health Issue",
+  "OnHealthRestored": "On Health Restored",
+  "OnHealthRestoredHelpText": "On Health Restored",
   "OpenBrowserOnStart": "Open browser on start",
   "OpenThisModal": "Open This Modal",
   "Options": "Options",
diff --git a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
index d2b896b3f..ef84b743c 100644
--- a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
+++ b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs
@@ -27,6 +27,11 @@ namespace NzbDrone.Core.Notifications.Apprise
             _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
index c4caa0435..c1f616d5a 100644
--- a/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
+++ b/src/NzbDrone.Core/Notifications/Boxcar/Boxcar.cs
@@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Boxcar
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, message.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage message)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, message.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs
index 13dfa8478..f87917468 100755
--- a/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs
+++ b/src/NzbDrone.Core/Notifications/CustomScript/CustomScript.cs
@@ -55,6 +55,21 @@ namespace NzbDrone.Core.Notifications.CustomScript
             ExecuteScript(environmentVariables);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            var environmentVariables = new StringDictionary();
+
+            environmentVariables.Add("Prowlarr_EventType", "HealthRestored");
+            environmentVariables.Add("Prowlarr_InstanceName", _configFileProvider.InstanceName);
+            environmentVariables.Add("Prowlarr_ApplicationUrl", _configService.ApplicationUrl);
+            environmentVariables.Add("Prowlarr_Health_Restored_Level", Enum.GetName(typeof(HealthCheckResult), previousCheck.Type));
+            environmentVariables.Add("Prowlarr_Health_Restored_Message", previousCheck.Message);
+            environmentVariables.Add("Prowlarr_Health_Restored_Type", previousCheck.Source.Name);
+            environmentVariables.Add("Prowlarr_Health_Restored_Wiki", previousCheck.WikiUrl.ToString() ?? string.Empty);
+
+            ExecuteScript(environmentVariables);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             var environmentVariables = new StringDictionary();
diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs
index ca69464a4..9eb41e989 100644
--- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs
+++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs
@@ -101,6 +101,29 @@ namespace NzbDrone.Core.Notifications.Discord
             _proxy.SendPayload(payload, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            var attachments = new List<Embed>
+                              {
+                                  new Embed
+                                  {
+                                      Author = new DiscordAuthor
+                                      {
+                                          Name = Settings.Author.IsNullOrWhiteSpace() ? Environment.MachineName : Settings.Author,
+                                          IconUrl = "https://raw.githubusercontent.com/Prowlarr/Prowlarr/develop/Logo/256.png"
+                                      },
+                                      Title = "Health Issue Resolved: " + previousCheck.Source.Name,
+                                      Description = $"The following issue is now resolved: {previousCheck.Message}",
+                                      Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
+                                      Color = (int)DiscordColors.Success
+                                  }
+                              };
+
+            var payload = CreatePayload(null, attachments);
+
+            _proxy.SendPayload(payload, Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             var attachments = new List<Embed>
diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs
index 5bbb76867..6f09eec92 100644
--- a/src/NzbDrone.Core/Notifications/Email/Email.cs
+++ b/src/NzbDrone.Core/Notifications/Email/Email.cs
@@ -36,6 +36,11 @@ namespace NzbDrone.Core.Notifications.Email
             SendEmail(Settings, HEALTH_ISSUE_TITLE_BRANDED, message.Message);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousMessage)
+        {
+            SendEmail(Settings, HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousMessage.Message}");
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             var body = $"{updateMessage.Message}";
diff --git a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs
index 58276c6fe..5ee380e8b 100644
--- a/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs
+++ b/src/NzbDrone.Core/Notifications/Gotify/Gotify.cs
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Notifications.Gotify
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", null);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/INotification.cs b/src/NzbDrone.Core/Notifications/INotification.cs
index 1483c1b6f..8431661a6 100644
--- a/src/NzbDrone.Core/Notifications/INotification.cs
+++ b/src/NzbDrone.Core/Notifications/INotification.cs
@@ -8,10 +8,12 @@ namespace NzbDrone.Core.Notifications
 
         void OnGrab(GrabMessage grabMessage);
         void OnHealthIssue(HealthCheck.HealthCheck healthCheck);
+        void OnHealthRestored(HealthCheck.HealthCheck previousCheck);
         void OnApplicationUpdate(ApplicationUpdateMessage updateMessage);
         void ProcessQueue();
         bool SupportsOnGrab { get; }
         bool SupportsOnHealthIssue { get; }
+        bool SupportsOnHealthRestored { get; }
         bool SupportsOnApplicationUpdate { get; }
     }
 }
diff --git a/src/NzbDrone.Core/Notifications/Join/Join.cs b/src/NzbDrone.Core/Notifications/Join/Join.cs
index b8f9320b0..51069d568 100644
--- a/src/NzbDrone.Core/Notifications/Join/Join.cs
+++ b/src/NzbDrone.Core/Notifications/Join/Join.cs
@@ -27,6 +27,11 @@ namespace NzbDrone.Core.Notifications.Join
             _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, message.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousMessage)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousMessage.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs b/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs
index 30c5297b4..7eb33c80e 100644
--- a/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs
+++ b/src/NzbDrone.Core/Notifications/Mailgun/Mailgun.cs
@@ -24,6 +24,11 @@ namespace NzbDrone.Core.Notifications.Mailgun
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheckMessage.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheckMessage)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheckMessage.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs
index ba6f3996b..bdf19825e 100644
--- a/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs
+++ b/src/NzbDrone.Core/Notifications/Notifiarr/Notifiarr.cs
@@ -30,6 +30,11 @@ namespace NzbDrone.Core.Notifications.Notifiarr
             _proxy.SendNotification(BuildHealthPayload(healthCheck), Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(BuildHealthRestoredPayload(previousCheck), Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(BuildApplicationUploadPayload(updateMessage), Settings);
diff --git a/src/NzbDrone.Core/Notifications/NotificationBase.cs b/src/NzbDrone.Core/Notifications/NotificationBase.cs
index 2235b4d6d..61af1d21e 100644
--- a/src/NzbDrone.Core/Notifications/NotificationBase.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationBase.cs
@@ -10,10 +10,12 @@ namespace NzbDrone.Core.Notifications
     {
         protected const string RELEASE_GRABBED_TITLE = "Release Grabbed";
         protected const string HEALTH_ISSUE_TITLE = "Health Check Failure";
+        protected const string HEALTH_RESTORED_TITLE = "Health Check Restored";
         protected const string APPLICATION_UPDATE_TITLE = "Application Updated";
 
         protected const string RELEASE_GRABBED_TITLE_BRANDED = "Prowlarr - " + RELEASE_GRABBED_TITLE;
         protected const string HEALTH_ISSUE_TITLE_BRANDED = "Prowlarr - " + HEALTH_ISSUE_TITLE;
+        protected const string HEALTH_RESTORED_TITLE_BRANDED = "Prowlarr - " + HEALTH_RESTORED_TITLE;
         protected const string APPLICATION_UPDATE_TITLE_BRANDED = "Prowlarr - " + APPLICATION_UPDATE_TITLE;
 
         public abstract string Name { get; }
@@ -37,6 +39,10 @@ namespace NzbDrone.Core.Notifications
         {
         }
 
+        public virtual void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+        }
+
         public virtual void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
         }
@@ -47,6 +53,7 @@ namespace NzbDrone.Core.Notifications
 
         public bool SupportsOnGrab => HasConcreteImplementation("OnGrab");
         public bool SupportsOnHealthIssue => HasConcreteImplementation("OnHealthIssue");
+        public bool SupportsOnHealthRestored => HasConcreteImplementation("OnHealthRestored");
         public bool SupportsOnApplicationUpdate => HasConcreteImplementation("OnApplicationUpdate");
 
         protected TSettings Settings => (TSettings)Definition.Settings;
diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs
index f77536f08..67dcefe11 100644
--- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs
@@ -5,14 +5,16 @@ namespace NzbDrone.Core.Notifications
     public class NotificationDefinition : ProviderDefinition
     {
         public bool OnHealthIssue { get; set; }
+        public bool OnHealthRestored { get; set; }
         public bool OnApplicationUpdate { get; set; }
         public bool OnGrab { get; set; }
         public bool SupportsOnGrab { get; set; }
         public bool IncludeManualGrabs { get; set; }
         public bool SupportsOnHealthIssue { get; set; }
+        public bool SupportsOnHealthRestored { get; set; }
         public bool IncludeHealthWarnings { get; set; }
         public bool SupportsOnApplicationUpdate { get; set; }
 
-        public override bool Enable => OnHealthIssue || OnApplicationUpdate || OnGrab;
+        public override bool Enable => OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnGrab;
     }
 }
diff --git a/src/NzbDrone.Core/Notifications/NotificationFactory.cs b/src/NzbDrone.Core/Notifications/NotificationFactory.cs
index 9fc614811..36dc2145e 100644
--- a/src/NzbDrone.Core/Notifications/NotificationFactory.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationFactory.cs
@@ -11,6 +11,7 @@ namespace NzbDrone.Core.Notifications
     {
         List<INotification> OnGrabEnabled();
         List<INotification> OnHealthIssueEnabled();
+        List<INotification> OnHealthRestoredEnabled();
         List<INotification> OnApplicationUpdateEnabled();
     }
 
@@ -31,6 +32,11 @@ namespace NzbDrone.Core.Notifications
             return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthIssue).ToList();
         }
 
+        public List<INotification> OnHealthRestoredEnabled()
+        {
+            return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnHealthRestored).ToList();
+        }
+
         public List<INotification> OnApplicationUpdateEnabled()
         {
             return GetAvailableProviders().Where(n => ((NotificationDefinition)n.Definition).OnApplicationUpdate).ToList();
@@ -42,6 +48,7 @@ namespace NzbDrone.Core.Notifications
 
             definition.SupportsOnGrab = provider.SupportsOnGrab;
             definition.SupportsOnHealthIssue = provider.SupportsOnHealthIssue;
+            definition.SupportsOnHealthRestored = provider.SupportsOnHealthRestored;
             definition.SupportsOnApplicationUpdate = provider.SupportsOnApplicationUpdate;
         }
     }
diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs
index 052f5a69d..33f6e8089 100644
--- a/src/NzbDrone.Core/Notifications/NotificationService.cs
+++ b/src/NzbDrone.Core/Notifications/NotificationService.cs
@@ -1,10 +1,8 @@
 using System;
-using System.Drawing.Drawing2D;
 using NLog;
 using NzbDrone.Common.Extensions;
 using NzbDrone.Core.HealthCheck;
 using NzbDrone.Core.Indexers.Events;
-using NzbDrone.Core.Indexers.PassThePopcorn;
 using NzbDrone.Core.Messaging.Events;
 using NzbDrone.Core.Parser.Model;
 using NzbDrone.Core.Update.History.Events;
@@ -13,6 +11,7 @@ namespace NzbDrone.Core.Notifications
 {
     public class NotificationService
         : IHandle<HealthCheckFailedEvent>,
+          IHandle<HealthCheckRestoredEvent>,
           IHandleAsync<HealthCheckCompleteEvent>,
           IHandle<UpdateInstalledEvent>,
           IHandle<IndexerDownloadEvent>
@@ -103,6 +102,29 @@ namespace NzbDrone.Core.Notifications
             }
         }
 
+        public void Handle(HealthCheckRestoredEvent message)
+        {
+            if (message.IsInStartupGracePeriod)
+            {
+                return;
+            }
+
+            foreach (var notification in _notificationFactory.OnHealthRestoredEnabled())
+            {
+                try
+                {
+                    if (ShouldHandleHealthFailure(message.PreviousCheck, ((NotificationDefinition)notification.Definition).IncludeHealthWarnings))
+                    {
+                        notification.OnHealthRestored(message.PreviousCheck);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _logger.Warn(ex, "Unable to send OnHealthRestored notification to: " + notification.Definition.Name);
+                }
+            }
+        }
+
         public void HandleAsync(HealthCheckCompleteEvent message)
         {
             ProcessQueue();
diff --git a/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs b/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs
index df81e8140..2c642fd1e 100644
--- a/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs
+++ b/src/NzbDrone.Core/Notifications/Ntfy/Ntfy.cs
@@ -27,6 +27,11 @@ namespace NzbDrone.Core.Notifications.Ntfy
             _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, message.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs
index 417c0fd0b..aa0217548 100644
--- a/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs
+++ b/src/NzbDrone.Core/Notifications/Prowl/Prowl.cs
@@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Prowl
             _prowlProxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousMessage)
+        {
+            _prowlProxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousMessage.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _prowlProxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs
index c4fcb46d0..8b4eae918 100644
--- a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs
+++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Notifications.PushBullet
             _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs
index 9d824a35d..28556b77e 100644
--- a/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs
+++ b/src/NzbDrone.Core/Notifications/Pushover/Pushover.cs
@@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Pushover
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs b/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs
index daf44d5e8..98eba8e3f 100644
--- a/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs
+++ b/src/NzbDrone.Core/Notifications/SendGrid/SendGrid.cs
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Notifications.SendGrid
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs b/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs
index 53b691e72..eaec87f93 100644
--- a/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs
+++ b/src/NzbDrone.Core/Notifications/Simplepush/Simplepush.cs
@@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Simplepush
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs
index 6737443f5..efb3487d4 100644
--- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs
+++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs
@@ -36,6 +36,23 @@ namespace NzbDrone.Core.Notifications.Slack
             _proxy.SendPayload(payload, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            var attachments = new List<Attachment>
+                              {
+                                  new Attachment
+                                  {
+                                      Title = previousCheck.Source.Name,
+                                      Text = $"The following issue is now resolved: {previousCheck.Message}",
+                                      Color = "good"
+                                  }
+                              };
+
+            var payload = CreatePayload("Health Issue Resolved", attachments);
+
+            _proxy.SendPayload(payload, Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             var attachments = new List<Attachment>
diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs
index 43a0ca283..6cf9b97e0 100644
--- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs
+++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs
@@ -26,6 +26,11 @@ namespace NzbDrone.Core.Notifications.Telegram
             _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
diff --git a/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs b/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs
index 44c13c626..181d1163c 100644
--- a/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs
+++ b/src/NzbDrone.Core/Notifications/Twitter/Twitter.cs
@@ -29,6 +29,11 @@ namespace NzbDrone.Core.Notifications.Twitter
             _twitterService.SendNotification($"Health Issue: {healthCheck.Message}", Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _twitterService.SendNotification($"Health Issue Resolved: {previousCheck.Message}", Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _twitterService.SendNotification($"Application Updated: {updateMessage.Message}", Settings);
diff --git a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs
index c85d9d74d..47d50eb38 100755
--- a/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs
+++ b/src/NzbDrone.Core/Notifications/Webhook/Webhook.cs
@@ -28,6 +28,11 @@ namespace NzbDrone.Core.Notifications.Webhook
             _proxy.SendWebhook(BuildHealthPayload(healthCheck), Settings);
         }
 
+        public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
+        {
+            _proxy.SendWebhook(BuildHealthRestoredPayload(previousCheck), Settings);
+        }
+
         public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
         {
             _proxy.SendWebhook(BuildApplicationUploadPayload(updateMessage), Settings);
diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs
index 9fc0341df..fabb59e3b 100644
--- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs
+++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs
@@ -46,6 +46,19 @@ namespace NzbDrone.Core.Notifications.Webhook
             };
         }
 
+        protected WebhookHealthPayload BuildHealthRestoredPayload(HealthCheck.HealthCheck healthCheck)
+        {
+            return new WebhookHealthPayload
+            {
+                EventType = WebhookEventType.HealthRestored,
+                InstanceName = _configFileProvider.InstanceName,
+                Level = healthCheck.Type,
+                Message = healthCheck.Message,
+                Type = healthCheck.Source.Name,
+                WikiUrl = healthCheck.WikiUrl?.ToString()
+            };
+        }
+
         protected WebhookApplicationUpdatePayload BuildApplicationUploadPayload(ApplicationUpdateMessage updateMessage)
         {
             return new WebhookApplicationUpdatePayload
diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs
index fdc765877..bb6401912 100644
--- a/src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs
+++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookEventType.cs
@@ -13,6 +13,7 @@ namespace NzbDrone.Core.Notifications.Webhook
         Download,
         Rename,
         Health,
-        ApplicationUpdate
+        ApplicationUpdate,
+        HealthRestored
     }
 }
diff --git a/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs b/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs
index 098b9bcdd..1951866ac 100644
--- a/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs
+++ b/src/Prowlarr.Api.V1/Notifications/NotificationResource.cs
@@ -7,10 +7,12 @@ namespace Prowlarr.Api.V1.Notifications
         public string Link { get; set; }
         public bool OnGrab { get; set; }
         public bool OnHealthIssue { get; set; }
+        public bool OnHealthRestored { get; set; }
         public bool OnApplicationUpdate { get; set; }
         public bool SupportsOnGrab { get; set; }
         public bool IncludeManualGrabs { get; set; }
         public bool SupportsOnHealthIssue { get; set; }
+        public bool SupportsOnHealthRestored { get; set; }
         public bool IncludeHealthWarnings { get; set; }
         public bool SupportsOnApplicationUpdate { get; set; }
         public string TestCommand { get; set; }
@@ -31,7 +33,9 @@ namespace Prowlarr.Api.V1.Notifications
             resource.SupportsOnGrab = definition.SupportsOnGrab;
             resource.IncludeManualGrabs = definition.IncludeManualGrabs;
             resource.OnHealthIssue = definition.OnHealthIssue;
+            resource.OnHealthRestored = definition.OnHealthRestored;
             resource.SupportsOnHealthIssue = definition.SupportsOnHealthIssue;
+            resource.SupportsOnHealthRestored = definition.SupportsOnHealthRestored;
             resource.IncludeHealthWarnings = definition.IncludeHealthWarnings;
             resource.OnApplicationUpdate = definition.OnApplicationUpdate;
             resource.SupportsOnApplicationUpdate = definition.SupportsOnApplicationUpdate;
@@ -52,7 +56,9 @@ namespace Prowlarr.Api.V1.Notifications
             definition.SupportsOnGrab = resource.SupportsOnGrab;
             definition.IncludeManualGrabs = resource.IncludeManualGrabs;
             definition.OnHealthIssue = resource.OnHealthIssue;
+            definition.OnHealthRestored = resource.OnHealthRestored;
             definition.SupportsOnHealthIssue = resource.SupportsOnHealthIssue;
+            definition.SupportsOnHealthRestored = resource.SupportsOnHealthRestored;
             definition.IncludeHealthWarnings = resource.IncludeHealthWarnings;
             definition.OnApplicationUpdate = resource.OnApplicationUpdate;
             definition.SupportsOnApplicationUpdate = resource.SupportsOnApplicationUpdate;