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];