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