From ed641c4985ed2196c316040e06f07039b4cbc668 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Sun, 25 Oct 2026 20:27:34 -0700 Subject: [PATCH] case 4654 - every direct notification message to a user is delivered Direct messages are general notifications with no object, so the 12-hour repeat check in CoreJobNotify (case 3917, keyed on subscription and object) dropped every message to a user after the first. For general notifications the check now applies only to the job's own delivery error messages (DELIVERY_ERROR_NAME). Changelog. Co-Authored-By: Claude Opus 5.5 --- devdocs/modernization/NET10-PORT.md | 2 +- docs/8.0/ayanova/docs/changelog.md | 1 + server/AyaNova/generator/CoreJobNotify.cs | 48 ++++++++++++----------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index b03165ab..9ae4c6be 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -205,7 +205,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4735 | Roles audit: every documented role right against `BizRoles.cs` (found in 4719, see Deferred) | 00 | | 4692 | SuperUser can be deleted | 00. **Done** (`UserBiz.ValidateCanDelete` refuses user 1 with 2004 before any other check; the integration test `UserWithActivityShouldNotBeDeleteable`, which deleted SuperUser on a database where it had no related records and so was excluded from every run, is now `SuperUserShouldNotBeDeleteable` and runs with the rest: full suite = baseline, SuperUser still logs in afterwards; changelog) | | 4693 | FormFieldReference start-up race | 00. **Done** (both lazily built lists, `FormFieldReferenceList` and `FormFieldKeys`, are built in a local and published when complete, fields volatile; three restarts each followed at once by the full suite: baseline every time, no "is not valid" in the log) | -| 4654 | General notification delivers at most once per 12 hours (circuit breaker) | 00 | +| 4654 | General notification delivers at most once per 12 hours (circuit breaker) | 00. **Done** (direct messages are general notifications with no object, so the 12-hour repeat check in `CoreJobNotify` (case 3917, keyed on subscription and object) dropped every message to a user after the first; for general notifications the check now applies only to the job's own delivery error messages (`DELIVERY_ERROR_NAME`, "Error"); integration test `DirectMessage_SendAndDelete` un-skipped and now sends two messages: before, only the first arrived; after, both; full suite = baseline; changelog) | | 4694 | Standalone launcher not shutting down PostgreSQL (raven-launcher) | 1 | | 4695 | Shipping the Visual C++ runtime DLLs with the standalone install | 1 | | 4696 | JWT secret missing from the standalone config.json | 1. Resolved by 4711's server change; confirm on a fresh standalone install | diff --git a/docs/8.0/ayanova/docs/changelog.md b/docs/8.0/ayanova/docs/changelog.md index c1965523..81376c2a 100644 --- a/docs/8.0/ayanova/docs/changelog.md +++ b/docs/8.0/ayanova/docs/changelog.md @@ -32,6 +32,7 @@ IN PROGRESS - Server: while a report renders, its template no longer holds the User's actual access token or download key; the AyaNova server adds the User's access only to the report's requests to the `ayApiUrl` address. Templates that call the AyaNova API through any other address need to be changed. See [API usage in reports](ay-report-edit.md#api-usage). - Server: users with the "Business administration - restricted" role can view [report templates](adm-report-templates.md) but no longer create, edit or import them, as described for that [role](ay-biz-admin-roles.md). Give the full "Business administration" role to users who edit report templates. - Server: the SuperUser account can no longer be deleted. +- Server: fixed direct notification messages to a user: only the first one within 12 hours was delivered, now every message is. - Server: the server information page at `/api/v8/` no longer shows license details. - Server: business logo uploads must be PNG, JPEG, GIF, WebP or SVG images, and need full rights to global settings. See [business logos](adm-global-logo.md). - Server: when a user's password is reset or set by an administrator, or their login is disabled, they are signed out and must sign in again. diff --git a/server/AyaNova/generator/CoreJobNotify.cs b/server/AyaNova/generator/CoreJobNotify.cs index 627a8290..decd5f0c 100644 --- a/server/AyaNova/generator/CoreJobNotify.cs +++ b/server/AyaNova/generator/CoreJobNotify.cs @@ -19,6 +19,8 @@ namespace AyaNova.Biz { private static bool NotifyIsRunning = false; private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("CoreJobNotify"); + //case 4654: name of this job's own delivery error messages to a user, the only general notifications limited to one per 12 hours + private const string DELIVERY_ERROR_NAME = "Error"; private static DateTime lastRun = DateTime.MinValue; private static DateTime lastNotifyHealthCheckSentLocal = DateTime.MinValue; private static TimeSpan TS_24_HOURS = new TimeSpan(24, 0, 0);//used to ensure daily ops happen no more than that @@ -103,29 +105,31 @@ namespace AyaNova.Biz //Don't check for ones that are regular object based //which can and will properly send out the same notification regularly //(e.g. workorder status change into out of and back into the same staus) - switch (notifyevent.EventType) + //case 4654: a general notification is either a message a user sent (never repetitive by itself, and with no + //object, so the check below took every message to a user after the first within 12 hours as a repeat) or + //this job's own delivery error message (which can repeat); only the latter is checked + var checkRepeat = notifyevent.EventType switch { - case NotifyEventType.BackupStatus: - case NotifyEventType.GeneralNotification: - case NotifyEventType.ServerOperationsProblem: - case NotifyEventType.PMGenerationFailed: - { - //check if we've just delivered this same thing in the last 12 hours which is the hard limit (case 3917) - var twelvehoursago = DateTime.UtcNow - new TimeSpan(12, 0, 0); + NotifyEventType.GeneralNotification => notifyevent.Name == DELIVERY_ERROR_NAME, + NotifyEventType.BackupStatus or NotifyEventType.ServerOperationsProblem or NotifyEventType.PMGenerationFailed => true, + _ => false + }; + if (checkRepeat) + { + //check if we've just delivered this same thing in the last 12 hours which is the hard limit (case 3917) + var twelvehoursago = DateTime.UtcNow - new TimeSpan(12, 0, 0); - //look for same delivery less than last12hours ago - if (await ct.NotifyDeliveryLog.AnyAsync(z => z.Processed > twelvehoursago && z.NotifySubscriptionId == notifyevent.NotifySubscriptionId && z.ObjectId == notifyevent.ObjectId)) - { - log.LogDebug($"Notification event will not be delivered: repetitive (server system event type and delivered at least once in the last 12 hours to this subscriber: {notifyevent})"); - ct.NotifyEvent.Remove(notifyevent); - await ct.SaveChangesAsync(); + //look for same delivery less than last12hours ago + if (await ct.NotifyDeliveryLog.AnyAsync(z => z.Processed > twelvehoursago && z.NotifySubscriptionId == notifyevent.NotifySubscriptionId && z.ObjectId == notifyevent.ObjectId)) + { + log.LogDebug($"Notification event will not be delivered: repetitive (server system event type and delivered at least once in the last 12 hours to this subscriber: {notifyevent})"); + ct.NotifyEvent.Remove(notifyevent); + await ct.SaveChangesAsync(); #if (DEBUG) - log.LogInformation($"DeliverInApp event will not be delivered: repetitive (server system event type and delivered at least once in the last 12 hours to this subscriber: {notifyevent})"); + log.LogInformation($"DeliverInApp event will not be delivered: repetitive (server system event type and delivered at least once in the last 12 hours to this subscriber: {notifyevent})"); #endif - continue; - } - } - break; + continue; + } } //Do the delivery, it's kosher @@ -201,7 +205,7 @@ namespace AyaNova.Biz log.LogDebug($"DeliverSMTP delivering notify event: {ne}"); if (string.IsNullOrWhiteSpace(deliveryAddress)) { - await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"No email address is set in subscription to deliver email notification. This event will be removed from the delivery queue as undeliverable: {ne}", "Error", null, ne.UserId); + await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"No email address is set in subscription to deliver email notification. This event will be removed from the delivery queue as undeliverable: {ne}", DELIVERY_ERROR_NAME, null, ne.UserId); DeliveryLogItem.Fail = true; DeliveryLogItem.Error = $"No email address provided for smtp delivery; event: {ne}"; } @@ -301,7 +305,7 @@ namespace AyaNova.Biz if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive) { - await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"Email notifications are set to OFF at server, unable to send email notification for this event:{ne}", "Error", null, ne.UserId); + await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"Email notifications are set to OFF at server, unable to send email notification for this event:{ne}", DELIVERY_ERROR_NAME, null, ne.UserId); log.LogInformation($"** WARNING: SMTP notification is currently set to Active=False; unable to deliver email notification, re-routed to in-app notification instead [UserId={ne.UserId}, Notify subscription={ne.NotifySubscriptionId}]. Change this setting or have users remove email delivery notifications if this is permanent **"); DeliveryLogItem.Fail = true; DeliveryLogItem.Error = $"Email notifications are set to OFF at server, unable to send email notification for this event: {ne}"; @@ -314,7 +318,7 @@ namespace AyaNova.Biz catch (Exception ex) { await NotifyEventHelper.AddOpsProblemEvent("SMTP Notification failed", ex); - await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"An error prevented delivering the following notification via email. System operator users have been notified:{ne}", "Error", null, ne.UserId); + await NotifyEventHelper.AddGeneralNotifyEvent(NotifyEventType.GeneralNotification, $"An error prevented delivering the following notification via email. System operator users have been notified:{ne}", DELIVERY_ERROR_NAME, null, ne.UserId); DeliveryLogItem.Fail = true; DeliveryLogItem.Error = $"SMTP Notification failed to deliver for this event: {ne}, message: {ex.Message}"; log.LogDebug(ex, $"DeliverSMTP Failure delivering notify event: {ne}");