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 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 20:27:34 -07:00
parent a53a4b6ef7
commit ed641c4985
3 changed files with 28 additions and 23 deletions

View File

@@ -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 |

View File

@@ -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.

View File

@@ -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}");