diff --git a/internal b/internal index 57a0138d..3f2687b5 160000 --- a/internal +++ b/internal @@ -1 +1 @@ -Subproject commit 57a0138dd501daf53753b17e7209b08a90b0edc6 +Subproject commit 3f2687b553102c7bc1dc3ad8271c9ad521c373b6 diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/Push/PushAlertProjectorTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/Push/PushAlertProjectorTests.cs index 1c99e1e4..d55cb011 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/Push/PushAlertProjectorTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/Push/PushAlertProjectorTests.cs @@ -188,4 +188,19 @@ public class PushAlertProjectorTests Assert.Null(PushAlertProjector.BuildTopic(Constants.PortalSessionUrl("sess-1"), null)); Assert.Null(PushAlertProjector.BuildTopic(Constants.PortalBaseUrl, Encoding.UTF8.GetBytes("key"))); } + + [Fact] + public void System_messages_never_share_a_tag_so_none_replaces_an_earlier_notification() + { + var scope = NotificationScope.Tenant("11111111-1111-1111-1111-111111111111"); + var now = new DateTime(2026, 10, 8, 18, 38, 40, DateTimeKind.Utc); + var first = PushAlertProjector.SystemMessage("session_watch", "Enrollment finished", "A finished.", "success", scope, now, Constants.PortalSessionUrl("sess-1")); + var second = PushAlertProjector.SystemMessage("session_watch", "Enrollment finished", "B finished.", "success", scope, now, Constants.PortalSessionUrl("sess-2")); + + Assert.Equal(first.Id, first.Tag); + Assert.Equal(second.Id, second.Tag); + Assert.NotEqual(first.Tag, second.Tag); + using var json = JsonDocument.Parse(PushAlertProjector.ToJsonBytes(first)); + Assert.Equal(first.Id, json.RootElement.GetProperty("notification").GetProperty("tag").GetString()); + } } diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Push/PushAlertProjector.cs b/src/Backend/AutopilotMonitor.Functions/Services/Push/PushAlertProjector.cs index 4871bf70..4dcbe0fb 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/Push/PushAlertProjector.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/Push/PushAlertProjector.cs @@ -133,20 +133,29 @@ public static class PushAlertProjector }; } - /// A system message to one device (test, paired, revoked, muted, paused, session watch). + /// + /// A system message to one device (test, paired, revoked, muted, paused, session watch). Its + /// tag is its id: a system message never replaces an earlier notification where the browser + /// honours tags, so two watched sessions finishing minutes apart stay two notifications + /// (alerts keep the per-session tag, so a later verdict for the same session replaces the + /// earlier one on the lock screen; the history keeps every push either way, keyed by id). + /// public static PushPayload SystemMessage(string type, string title, string body, string severity, NotificationScope scope, DateTime nowUtc, string? portalUrl = null) - => new() + { + var id = Guid.NewGuid().ToString("N"); + return new() { - Id = Guid.NewGuid().ToString("N"), + Id = id, Type = type, Title = Clean(title, MaxTitleLength), Body = Clean(body, MaxBodyLength), Severity = severity, PortalUrl = portalUrl, - Tag = type, + Tag = id, Scope = scope.IsPlatform ? "platform" : "tenant", TimestampUtc = nowUtc, }; + } /// /// Declarative Web Push JSON (web_push: 8030): Safari ≥ 18.4 shows it without the diff --git a/src/Web/autopilot-monitor-web/lib/push/pushClient.ts b/src/Web/autopilot-monitor-web/lib/push/pushClient.ts index 6914d229..307bfc6d 100644 --- a/src/Web/autopilot-monitor-web/lib/push/pushClient.ts +++ b/src/Web/autopilot-monitor-web/lib/push/pushClient.ts @@ -70,6 +70,16 @@ export async function getPushRegistration(): Promise {}); +} + export interface SubscriptionKeys { endpoint: string; p256dh: string; @@ -293,6 +303,7 @@ export async function reconcileDevice(): Promise { try { const registration = (await getPushRegistration()) ?? (isPushSupported() ? await registerPushWorker() : null); if (registration) { + checkForWorkerUpdate(registration); let subscription = await registration.pushManager.getSubscription(); const permission = notificationPermission(); const vapidPublicKey = meta[META_KEYS.vapidPublicKey];