Skip to content

Fix #120: Stale Data Indicator & Network Connectivity Resilience#127

Open
eneshenderson wants to merge 2 commits into
baldbeardedbuilder:mainfrom
eneshenderson:feature/stale-data-120-clean
Open

Fix #120: Stale Data Indicator & Network Connectivity Resilience#127
eneshenderson wants to merge 2 commits into
baldbeardedbuilder:mainfrom
eneshenderson:feature/stale-data-120-clean

Conversation

@eneshenderson

Copy link
Copy Markdown
Contributor

PR Title: Fix #120: Stale Data Indicator & Network Connectivity Resilience

Fixes #120

🚀 What this PR does

This PR introduces a comprehensive and highly optimized solution to the silent stale data issue reported in #120, completely overhauling the network resilience of the PinnedWeatherBand.

1. Stale Data Indicator (⚠️)

  • When weather data becomes older than 15 minutes (due to long user-defined intervals, sleep, or network drops), the extension now automatically prepends a ⚠️ warning to the subtitle.
  • Optimized: Repurposed the existing _updateTimer to tick every 1 minute. The stale check happens locally in memory without triggering unnecessary API calls.

2. Instant OS-Level Network Recovery

  • Hooked directly into Windows OS events via System.Net.NetworkInformation.NetworkChange.
  • Subscribed to both NetworkAvailabilityChanged and NetworkAddressChanged (for guaranteed IP assignment detection).
  • Result: The exact millisecond the user's PC connects to Wi-Fi or Ethernet, the extension bypasses all timers and refreshes the weather instantly.

3. Smart 1-Minute Retry

  • If a data fetch fails (e.g., 503 Service Unavailable API error while internet is connected), the extension no longer waits for the user's full interval (e.g., 30-60 minutes).
  • It gracefully falls back to a 1-minute retry cycle until it successfully fetches data.

4. Hover Tooltip Sync Fix

  • Bug Fixed: Discovered an edge case where PowerToys CmdPal's hover tooltip (CommandItem.Name) would desync from the widget's actual text during error states.
  • Created a SyncDockItem() method to ensure the CmdPal tooltip and the Widget text are always perfectly identical, especially during ⚠️ -- error states.

5. Clearer Error States

  • Replaced the generic -- loading fallback with a prominent ⚠️ -- when the weather service is unavailable or network drops, providing much better UX.

6. Robust Unit Testing

  • Expanded DockBandCardSyncTests.cs to guarantee architectural integrity.
  • Used advanced IL (Intermediate Language) parsing to verify that the async OnTimerElapsed state machine successfully calls MarkAsStaleIfNeeded.
  • Verified all 273/273 MSUnit tests pass successfully.

🧪 How to test

  1. Test Instant Network Recovery:
    • Turn off Wi-Fi/Ethernet. Open CmdPal. The Weather widget should quickly display ⚠️ -- (and the hover tooltip should perfectly match).
    • Turn Wi-Fi/Ethernet back on. The exact second Windows establishes the connection, the widget should instantly refresh to the actual temperature without waiting for the timer.
  2. Test Smart Retry:
    • Simulate an API error. The widget will re-attempt to fetch every 1 minute instead of waiting for the full user-configured interval.
  3. Test Stale Indicator:
    • Set the Update Interval to 60 minutes in settings. Wait 15 minutes. The widget will prepend ⚠️ to the subtitle.

@michaeljolley michaeljolley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Squad review — mostly good, two issues to address

What this PR does well ✅

  • Stale indicator logic (MarkAsStaleIfNeeded) — clean, correct: only triggers after 15m since last success, guards against double-prefixing, skips error/loading states
  • Network resilienceNetworkChange.NetworkAvailabilityChanged + NetworkAddressChanged triggers immediate retry after connectivity restores. Smart: only fires if last attempt failed.
  • Timer rethink — 1-minute tick with interval gating is better than a long-interval timer for staleness detection
  • SyncDockItem() extraction — good refactor, DRY
  • Test coverage — structural tests verify the fields and method exist
  • Dispose cleanup — unsubscribes both network events ✅

Blocking Issues

1. Hardcoded 15-minute stale threshold ignores per-type cache TTLs

if (age.TotalMinutes >= 15)

We just merged PR #119 with per-type cache expiration: Current=15m, Hourly=45m, Forecast=180m. The stale indicator should use _settings.UpdateIntervalMinutes (the user's configured refresh interval) rather than a hardcoded 15. If the user sets a 3-hour update interval, data is expected to be 3 hours old — marking it stale at 15m would produce false positives.

Fix: Replace >= 15 with >= _settings.UpdateIntervalMinutes (or _settings.UpdateIntervalMinutes * 1.5 for a grace period).

2. Stale prefix accumulates over time with no reset

MarkAsStaleIfNeeded adds "⚠ " to Subtitle, but after a successful fetch the subtitle is overwritten entirely (good). However, if the success path only sets Title without touching Subtitle (the "location" DockBandSubtitle mode sets Subtitle = _location.DisplayName), then on the next stale tick the prefix gets checked correctly (!Subtitle.StartsWith("⚠ ")) — this is fine.

BUT: there's a NullReferenceException risk — Subtitle can be null in theory (it's set in the constructor, but ListItem.Subtitle is nullable). Add a null check:

if (Subtitle != null && !Subtitle.StartsWith("⚠ ", StringComparison.Ordinal))

Wait — you already have Subtitle != null. ✅ Ignore this one.

Revised: only blocking issue #1 (hardcoded threshold).

Non-blocking Notes

  1. DockItem icon removal from success path — Moving dockCommandItem.Icon = Icon into SyncDockItem() is correct BUT SyncDockItem() is only called at the end of UpdateWeatherAsync (success case) and in error paths. In the original code, the icon was set immediately after updating Title. Now it's set after the forecast fetch too. Fine in practice — just noting the shift.

  2. Timer interval change from configurable to 60s — The 1-minute tick is appropriate for stale detection, but the original code respected UpdateIntervalMinutes * 60 * 1000. Since the timer now gates on timeSinceLastUpdate.TotalMinutes >= intervalMinutes, this is functionally equivalent. Just worth a comment explaining why the timer is always 60s.

  3. ⚠️ vs — Error states use the emoji version ⚠️ (with variation selector) while stale uses plain . Consider making them consistent — either both emoji or both plain.


Once the hardcoded 15-minute threshold is replaced with _settings.UpdateIntervalMinutes, this is ready to approve.

@michaeljolley michaeljolley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Murdock QA review — requesting changes

Blocking Issue

Hardcoded 15-minute stale threshold (echoing team lead's block)

Confirmed in MarkAsStaleIfNeeded:

if (age.TotalMinutes >= 15)

This conflicts with the canonical TTL values from PR #119 (Current=15m, Hourly=45m, DailyForecast=180m) and ignores the user's own UpdateIntervalMinutes setting. A user on a 3-hour update interval would see false stale warnings every 15 minutes. Fix: use _settings.UpdateIntervalMinutes as the threshold (or apply a grace multiplier like * 1.5 to tolerate brief network hiccups).

Test Coverage Gaps (blocking)

The new structural tests confirm the fields and method exist — that's necessary but not sufficient. The staleness logic itself has no behavioral coverage:

  1. No test for the >= threshold boundary — a test with a mocked/injected clock showing data aged exactly at and just under UpdateIntervalMinutes would prevent future regressions (including the current hardcoded-15 bug, which a behavioral test would have caught).
  2. No test for the network recovery pathOnNetworkAvailabilityChanged and OnNetworkAddressChanged fire UpdateWeatherAsync conditionally on _lastUpdateAttempt > _lastSuccessfulFetch. No test covers the guard condition. At minimum, a structural test confirming both event subscriptions are wired and both unsubscriptions exist in Dispose() would guard against future regression.
  3. SyncDockItem() not covered — new method updating DockItem.Title/Subtitle/Icon has no test verifying the propagation.

A structural test for the dispose unsubscriptions is fast to add given the existing reflection pattern — please add those before this merges.

Non-blocking Notes

  • The IL-scan test PinnedWeatherBand_OnTimerElapsed_CallsMarkAsStaleIfNeeded is clever, but note it will pass even if MarkAsStaleIfNeeded is called unconditionally — it doesn't verify the conditional path. Fine for structural gating; just be aware of the coverage gap.
  • The ⚠️ (emoji) vs (plain) inconsistency flagged by the team lead is worth fixing for consistency in any follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stale-data indicator on dock band when cache exceeds TTL

2 participants