diff --git a/CHANGELOG.md b/CHANGELOG.md
index c57c7481..31f0a4af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
also marked with an orange warning on its header instead of the grey info glyph that plain
limitations use; press Refresh to sign in.
+- Windows: **expired activations no longer linger after the PC wakes.** As on macOS, a background
+ refresh that could not renew a sign-in silently kept the tenant's last-known rows, including
+ activations that had ended meanwhile. Elevate now drops an activation a minute after its end
+ time, whether or not the tenant could be read. A tenant waiting for a sign-in to refresh is also
+ marked with an orange warning on its header instead of the grey info glyph that plain
+ limitations use; press Refresh to sign in.
+
- CLI: **`elevate watch` no longer keeps listing activations that have ended.** When a re-read
failed — a sign-in it could not renew silently, a dropped network — the table kept the last rows
it had, including activations whose end time had since passed. `watch` now drops a row as soon as
diff --git a/windows/src/Elevate.App.Model/ViewModels/AppModel.cs b/windows/src/Elevate.App.Model/ViewModels/AppModel.cs
index 05b51984..ccd23edf 100644
--- a/windows/src/Elevate.App.Model/ViewModels/AppModel.cs
+++ b/windows/src/Elevate.App.Model/ViewModels/AppModel.cs
@@ -604,6 +604,7 @@ private async Task RunClockAsync(CancellationToken ct)
{
Clock = DateTimeOffset.UtcNow;
Touch();
+ await DropLapsedAssignmentsAsync(Clock);
}
}
catch (OperationCanceledException)
@@ -611,6 +612,36 @@ private async Task RunClockAsync(CancellationToken ct)
}
}
+ ///
+ /// How long past its end an activation is kept, so the "expired" toast — due a few seconds
+ /// after the end — fires before the reschedule below would withdraw it.
+ ///
+ private static readonly TimeSpan LapseGrace = TimeSpan.FromMinutes(1);
+
+ ///
+ /// A refresh that cannot read a tenant (a sign-in it cannot renew silently, a failed request)
+ /// keeps that tenant's known rows. The end times are known, though: once one has passed, the
+ /// row is gone whatever the service would say, and must not linger with a Deactivate button.
+ ///
+ internal async Task DropLapsedAssignmentsAsync(DateTimeOffset now)
+ {
+ var cutoff = now - LapseGrace;
+ var lapsed = Active.Values.Where(a => a.HasLapsed(cutoff)).Select(a => a.RoleKey).ToList();
+ if (lapsed.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var key in lapsed)
+ {
+ Active.Remove(key);
+ StopWatchingPropagation(key);
+ }
+
+ Touch();
+ await RescheduleNotificationsAsync();
+ }
+
private async Task RunRefreshTimerAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(60));
diff --git a/windows/src/Elevate.App/App.xaml b/windows/src/Elevate.App/App.xaml
index d067f1bc..9848fc4d 100644
--- a/windows/src/Elevate.App/App.xaml
+++ b/windows/src/Elevate.App/App.xaml
@@ -8,6 +8,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+