Skip to content

Commit 3aba383

Browse files
committed
fix(stats): address review feedback on accessibility, races and edge cases
- Pin balance scrapes to the account that opened the driver: both the on-launch warmup refresh and the dashboard "Refresh balance" capture the account id + StatsManager up front and only persist if the active account is unchanged, so a mid-scrape account switch can't write one profile's balance into another's. - Serialize ad-hoc balance refreshes through a dedicated lock held across the whole driver lifecycle, so concurrent refreshes can't open two drivers on the same Edge profile (the prior busy check was non-atomic). - Accept a real balance of 0 in the scrape selector (was rejecting >= 0 as "not found"), so a brand-new account's zero balance is captured correctly. - Make the stats card keyboard-activatable (Enter / Space) in addition to click, since it exposes role="button". - Drop the quotes around the single-token Consolas font name in the dashboard diagnostic list to satisfy Stylelint. - Add bottom padding inside the dashboard content container so the "All accounts" table isn't flush against the window edge.
1 parent 5235254 commit 3aba383

5 files changed

Lines changed: 41 additions & 11 deletions

File tree

gui/dashboard.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ body {
161161
.diag-list {
162162
margin: 6px 0 0 0;
163163
padding-left: 18px;
164-
font-family: "JetBrains Mono", "Consolas", monospace;
164+
font-family: "JetBrains Mono", Consolas, monospace;
165165
font-size: 11px;
166166
line-height: 1.5;
167167
}

gui/dashboard.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ <h2 class="dash-title">
379379
setup_needed: 'Complete First Setup for this account first.',
380380
busy: 'Browser busy (a run or startup is using it) — try again in a moment.',
381381
not_found: "Couldn't read the balance from the rewards page.",
382-
driver: 'Could not open the browser to read the balance.'
382+
driver: 'Could not open the browser to read the balance.',
383+
account_changed: 'Account changed during refresh — try again.'
383384
};
384385

385386
function hide_diag() {

gui/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ <h1>AutoRewarder</h1>
114114

115115
<!-- ==================== Stats card ==================== -->
116116
<section class="card stats-card" id="stats_card" role="button" tabindex="0"
117-
title="Open the statistics dashboard" onclick="show_stats()">
117+
title="Open the statistics dashboard" onclick="show_stats()"
118+
onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); show_stats(); }">
118119
<div class="card-header">
119120
<div class="card-label">Stats</div>
120121
<span class="ghost-link" aria-hidden="true">

src/api.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def __init__(self):
9090
self._driver = None
9191
self.is_driver_loading = False
9292
self._run_lock = threading.Lock()
93+
# Serializes ad-hoc balance scrapes so concurrent refreshes can't open
94+
# two drivers on the same Edge profile at once.
95+
self._balance_lock = threading.Lock()
9396
# Set when the user clicks Stop. Long loops in search_engine and
9497
# daily_set poll this between iterations and bail out cleanly.
9598
self._stop_event = threading.Event()
@@ -1715,16 +1718,22 @@ def _refresh_balance_on_launch(self, driver):
17151718
up-to-date total. Runs every launch (not just the first); a failed read
17161719
silently keeps the previously stored balance.
17171720
"""
1718-
if self.stats is None or self.account_meta is None:
1721+
# Pin to the account that owns this warmup driver. The scrape runs on a
1722+
# background thread, so a mid-scrape account switch could otherwise save
1723+
# this profile's balance into another account's stats.json.
1724+
stats = self.stats
1725+
meta = self.account_meta
1726+
account_id = self.account_manager.current_id()
1727+
if stats is None or meta is None or account_id is None:
17191728
return
1720-
if not self.account_meta.is_first_setup_done():
1729+
if not meta.is_first_setup_done():
17211730
return
17221731

17231732
# Only animate the card on the first launch, when there's nothing to
17241733
# show yet. On later launches a value is already displayed, so refresh
17251734
# it silently in the background — no distracting shimmer on a number
17261735
# that's already there.
1727-
animate = self.stats.get_stats()["balance"]["current"] is None
1736+
animate = stats.get_stats()["balance"]["current"] is None
17281737

17291738
self.log("Refreshing your Rewards points balance…")
17301739
if animate:
@@ -1740,7 +1749,10 @@ def _refresh_balance_on_launch(self, driver):
17401749
"(keeping the last known total)."
17411750
)
17421751
return
1743-
self.stats.update_balance(balance)
1752+
# Skip if the user switched accounts while we were scraping.
1753+
if self.account_manager.current_id() != account_id:
1754+
return
1755+
stats.update_balance(balance)
17441756
self._notify_stats_refresh()
17451757

17461758
def refresh_balance(self):
@@ -1753,7 +1765,8 @@ def refresh_balance(self):
17531765
dict: {"ok": True, "balance": int} on success, else
17541766
{"ok": False, "error": <reason>}.
17551767
"""
1756-
if self.account_manager.current_id() is None:
1768+
account_id = self.account_manager.current_id()
1769+
if account_id is None:
17571770
return {"ok": False, "error": "no_account"}
17581771
if self.account_meta is None or not self.account_meta.is_first_setup_done():
17591772
return {"ok": False, "error": "setup_needed"}
@@ -1763,11 +1776,22 @@ def refresh_balance(self):
17631776
if self._run_lock.locked() or self.is_driver_loading:
17641777
return {"ok": False, "error": "busy"}
17651778

1779+
# Atomically claim the profile: the checks above are advisory, so a
1780+
# dedicated lock is what actually prevents two refreshes from opening a
1781+
# driver on the same Edge profile at once.
1782+
if not self._balance_lock.acquire(blocking=False):
1783+
return {"ok": False, "error": "busy"}
1784+
1785+
# Pin to the account that opened the driver so a concurrent account
1786+
# switch can't redirect this profile's balance to another account.
1787+
stats = self.stats
1788+
driver_manager = self.driver_manager
1789+
17661790
self.log("Refreshing points balance…")
17671791
self._set_stats_loading(True)
17681792
driver = None
17691793
try:
1770-
driver = self.driver_manager.setup_driver(headless=True)
1794+
driver = driver_manager.setup_driver(headless=True)
17711795
balance = self._fetch_balance_with_driver(driver)
17721796
except Exception as e:
17731797
self.log(f"[WARNING] Balance refresh failed: {e}")
@@ -1779,6 +1803,7 @@ def refresh_balance(self):
17791803
except Exception:
17801804
pass
17811805
self._set_stats_loading(False)
1806+
self._balance_lock.release()
17821807

17831808
if balance is None:
17841809
self.log("[INFO] Could not read the points balance from the rewards page.")
@@ -1792,7 +1817,10 @@ def refresh_balance(self):
17921817
"diag_error": info.get("error"),
17931818
}
17941819

1795-
self.stats.update_balance(balance)
1820+
# Only persist + refresh the UI if we're still on the same account.
1821+
if stats is None or self.account_manager.current_id() != account_id:
1822+
return {"ok": False, "error": "account_changed"}
1823+
stats.update_balance(balance)
17961824
self._notify_stats_refresh()
17971825
self.log(f"Points balance updated: {balance:,}")
17981826
return {"ok": True, "balance": balance}

src/stats/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
if (candidates.length < 14) {
8181
candidates.push(selectors[i] + ' => "' + (aria || txt).slice(0, 32) + '" [' + val + ']');
8282
}
83-
if (val != null && val > 0) {
83+
if (val != null && val >= 0) {
8484
return {
8585
value: val, via: selectors[i], candidates: candidates,
8686
url: location.href, title: document.title

0 commit comments

Comments
 (0)