Conversation
Resolves #139 - Charts were crashing in older browsers (Firefox <121, older Chrome/Safari/Edge) because the :has() pseudo-class is not supported. Using querySelector with :has() throws errors in unsupported browsers, causing chart rendering failures. Changes: - Replace querySelector(".inside:has(#chart)") with closest(".inside") - Replace querySelector(".chart-wrap:has(#chart)") with closest() - Add null checks before DOM manipulations to prevent TypeErrors
Resolves #140 - Consent banner was showing even when GDPR Compliance Mode was disabled because the check only validated use_slimstat_banner without checking gdpr_enabled. Now the banner only renders when BOTH conditions are met: - gdpr_enabled is 'on' - use_slimstat_banner is 'on'
…or-crash Fix: Replace CSS :has() with closest() for browser compatibility
…-regression Fix: Gate consent banner on gdpr_enabled setting
#145) - Strip anchor-only/empty-href links from custom banner message to prevent duplicate Accept/Deny controls alongside rendered buttons. Real URL links (e.g. privacy policy) are preserved. - Register custom GDPR banner strings (message, accept/decline labels) with WPML (wpml_register_single_string) and Polylang (pll_register_string) on settings save, and apply translations at render time via wpml_translate_single_string with native Polylang fallback. - Update banner message field description to clarify link requirements.
The previous preg_replace pattern only handled href="#" and href="" but missed named anchors (href="#accept", href="#deny") and whitespace variants (href=" # "). Replace with preg_replace_callback that whitelists real URLs (http(s)://, /path) and strips everything else, preserving inner text content. Tested against 15 edge cases covering all known variants.
Fix: GDPR banner duplicate controls & WPML/Polylang i18n
Added version-gated migration for 5.4.1 to ensure 'use_separate_menu' setting is properly set to 'on' for users upgrading from older versions where the empty() check failed for the string 'no'. Fixes #146
…ation Fix admin bar migration bug for upgrading users
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds GDPR consent banner translation and registration hooks (WPML/Polylang), strips non‑URL anchors from banner messages, fixes duplicate banner buttons and chart-loading edge cases, adds a 5.4.1 upgrade migration for an admin menu setting, and updates plugin versioning, changelog, POT, and readme metadata. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant AdminConfig as Admin Config (save)
participant GDPRSvc as GDPRService
participant WPML as WPML/Polylang
participant Frontend as Browser (Banner)
AdminConfig->>GDPRSvc: save settings (opt_out_message, accept/deny texts)
GDPRSvc->>WPML: wpml_register_single_string (if available)
GDPRSvc->>WPML: pll_register_string (if available)
Frontend->>GDPRSvc: request banner HTML
GDPRSvc->>WPML: fetch translated strings (translateString)
GDPRSvc-->>Frontend: return banner HTML (anchors stripped, labels translated)
Frontend->>Frontend: render banner
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| } | ||
|
|
||
| // WPML registration | ||
| do_action('wpml_register_single_string', 'wp-slimstat', $name, $value); |
There was a problem hiding this comment.
This only registers strings when the settings form is saved, but GDPRService::translateString() now runs on every banner render. On upgrade from 5.4.0, sites that already have custom opt_out_message / button labels configured will never hit this save path, so WPML/Polylang will keep returning the original strings until an admin re-saves Slimstat settings. That leaves #145 only partially fixed for existing multilingual installs. Consider registering these strings during plugin init / banner bootstrap as well, or adding a version-gated migration that registers any already-stored values once after upgrade.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
admin/assets/js/slimstat-chart.js (1)
100-152:⚠️ Potential issue | 🟠 MajorAlways restore the chart UI after failed refreshes.
Line 100 hides the wrapper before the request, but only the happy path at Line 152 shows it again. Any non-200 response, bad JSON, or
result.success === falseleaves the chart hidden and the loading state stuck until a full reload.Suggested fix
inside.appendChild(loadingIndicator); - chartWrap.style.display = "none"; + var previousDisplay = chartWrap.style.display; + chartWrap.style.display = "none"; + + function resetLoadingState() { + if (loadingIndicator.parentNode === inside) { + inside.removeChild(loadingIndicator); + } + chartWrap.style.display = previousDisplay; + } var xhr = new XMLHttpRequest(); xhr.open("POST", slimstat_chart_vars.ajax_url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { - var result = JSON.parse(xhr.responseText); + var result; + try { + result = JSON.parse(xhr.responseText); + } catch (e) { + resetLoadingState(); + return; + } if (!result.success) { + resetLoadingState(); console.error("AJAX error:", result.data.message); return; } element.dataset.granularity = granularity; @@ - inside.removeChild(loadingIndicator); - chartWrap.style.display = "block"; + resetLoadingState(); } else { console.error("XHR error:", xhr.statusText); + resetLoadingState(); } } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@admin/assets/js/slimstat-chart.js` around lines 100 - 152, The AJAX handler hides chartWrap before sending but only restores it on the successful branch; update xhr.onreadystatechange (and the JSON parse) to ensure the loadingIndicator is removed and chartWrap.style.display is set back to "block" on all failure paths (non-200 status, JSON.parse errors, and when result.success === false). Specifically, wrap JSON.parse in try/catch inside xhr.onreadystatechange, handle xhr.status !== 200 and result.success === false by cleaning up (inside.removeChild(loadingIndicator) and chartWrap.style.display = "block"), and consolidate final cleanup so both the happy path and any early-return error cases restore the UI; reference chartWrap, loadingIndicator, inside, xhr.onreadystatechange, and result in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@admin/assets/js/slimstat-chart.js`:
- Around line 87-89: The guard clause that checks element, chartCanvas, inside,
and chartWrap currently emits a console.warn with chartId; remove that
console.warn so the function simply returns when the required DOM pieces are
missing (i.e., keep the if (!element || !chartCanvas || !inside || !chartWrap) {
return; } behavior) to avoid introducing production console output in
slimstat-chart.js.
In `@admin/config/index.php`:
- Around line 941-960: The GDPR banner string registration currently done in the
settings save flow (the $gdpr_translatable loop that calls
do_action('wpml_register_single_string', ...) and pll_register_string(...)) must
be made idempotent and invoked during plugin init/upgrade as well so
pre-existing custom banner text is registered for WPML/Polylang; extract that
loop into a reusable function (e.g., register_gdpr_translatable_strings) and
call it from your upgrade/init sequence (or hook into admin_init/plugin_loaded)
in addition to the save handler, ensuring it skips empty values and safely
re-registers the same keys without side effects.
In `@admin/index.php`:
- Around line 735-740: The migration currently forces
wp_slimstat::$settings['use_separate_menu'] = 'on' for all sites < 5.4.1; change
it to only modify legacy/unset values: read
wp_slimstat::$settings['use_separate_menu'] and if the key is not set, is empty,
or equals the legacy 'no' value, then set it to 'on'; otherwise leave the
existing value untouched so existing user preferences are preserved (keep the
version_compare check and update behavior confined to the migration block).
In `@languages/wp-slimstat.pot`:
- Around line 5489-5495: Unify the negative consent label to a single key (use
"Decline") so translations aren’t split; update the POT entries in
languages/wp-slimstat.pot to remove msgid "Deny" and keep only msgid "Decline",
and then update all code references that emit the button label (e.g., in
src/Services/GDPRService.php and wp-slimstat.php where __('Deny') or similar is
used) to use __('Decline') (or the chosen translation helper) so every banner
path uses the same translation key.
In `@src/Services/GDPRService.php`:
- Around line 210-223: The callback used in preg_replace_callback is too
restrictive: inside the anonymous function (the one handling $matches and
setting $href) replace the current preg_match('#^(https?://|/)#i', $href) check
with a pattern that accepts any valid URL scheme plus protocol-relative and
root-relative URLs so mailto:, tel:, ftp:, etc. are preserved; e.g. test for a
scheme per RFC (like '^[a-z][a-z0-9+.\-]*:'), protocol-relative ('//') or
root-relative ('/'), and return $matches[0] when that test passes, otherwise
return $matches[2].
---
Outside diff comments:
In `@admin/assets/js/slimstat-chart.js`:
- Around line 100-152: The AJAX handler hides chartWrap before sending but only
restores it on the successful branch; update xhr.onreadystatechange (and the
JSON parse) to ensure the loadingIndicator is removed and
chartWrap.style.display is set back to "block" on all failure paths (non-200
status, JSON.parse errors, and when result.success === false). Specifically,
wrap JSON.parse in try/catch inside xhr.onreadystatechange, handle xhr.status
!== 200 and result.success === false by cleaning up
(inside.removeChild(loadingIndicator) and chartWrap.style.display = "block"),
and consolidate final cleanup so both the happy path and any early-return error
cases restore the UI; reference chartWrap, loadingIndicator, inside,
xhr.onreadystatechange, and result in your changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: db1c7172-8ff2-494a-bab4-0f0f7263f145
📒 Files selected for processing (8)
CHANGELOG.mdadmin/assets/js/slimstat-chart.jsadmin/config/index.phpadmin/index.phplanguages/wp-slimstat.potreadme.txtsrc/Services/GDPRService.phpwp-slimstat.php
| if (!element || !chartCanvas || !inside || !chartWrap) { | ||
| console.warn("SlimStat: Could not find chart elements for chart " + chartId); | ||
| return; |
There was a problem hiding this comment.
Please don't add new console output here.
Line 88 adds a production console.warn() for a guard path that already fails safely by returning early.
As per coding guidelines "Do not include var_dump(), console.log(), or debug code in shipped JavaScript; remove before release."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/assets/js/slimstat-chart.js` around lines 87 - 89, The guard clause
that checks element, chartCanvas, inside, and chartWrap currently emits a
console.warn with chartId; remove that console.warn so the function simply
returns when the required DOM pieces are missing (i.e., keep the if (!element ||
!chartCanvas || !inside || !chartWrap) { return; } behavior) to avoid
introducing production console output in slimstat-chart.js.
| // Register GDPR banner strings for WPML/Polylang translation | ||
| $gdpr_translatable = [ | ||
| 'opt_out_message' => wp_slimstat::$settings['opt_out_message'] ?? '', | ||
| 'gdpr_accept_button_text' => wp_slimstat::$settings['gdpr_accept_button_text'] ?? '', | ||
| 'gdpr_decline_button_text' => wp_slimstat::$settings['gdpr_decline_button_text'] ?? '', | ||
| ]; | ||
|
|
||
| foreach ($gdpr_translatable as $name => $value) { | ||
| if (empty($value)) { | ||
| continue; | ||
| } | ||
|
|
||
| // WPML registration | ||
| do_action('wpml_register_single_string', 'wp-slimstat', $name, $value); | ||
|
|
||
| // Native Polylang registration | ||
| if (function_exists('pll_register_string')) { | ||
| pll_register_string($name, $value, 'wp-slimstat', ($name === 'opt_out_message')); | ||
| } | ||
| } |
There was a problem hiding this comment.
Register existing banner strings outside the save flow.
This loop only runs after a settings save. On upgraded sites that already have custom banner text, GDPRService will try to translate these names before WPML/Polylang has ever seen them, so the new translation feature looks broken until someone manually re-saves the settings page. Please make this registration idempotent and call it during upgrade/init as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/config/index.php` around lines 941 - 960, The GDPR banner string
registration currently done in the settings save flow (the $gdpr_translatable
loop that calls do_action('wpml_register_single_string', ...) and
pll_register_string(...)) must be made idempotent and invoked during plugin
init/upgrade as well so pre-existing custom banner text is registered for
WPML/Polylang; extract that loop into a reusable function (e.g.,
register_gdpr_translatable_strings) and call it from your upgrade/init sequence
(or hook into admin_init/plugin_loaded) in addition to the save handler,
ensuring it skips empty values and safely re-registers the same keys without
side effects.
| // --- Updates for version 5.4.1 --- | ||
| // Fix admin bar migration: empty('no') returned false in 5.4.0, missing users with legacy 'no' value | ||
| // Safe because this runs once (version bumps to 5.4.1 after), users who disable later are already on 5.4.1+ | ||
| if (version_compare(wp_slimstat::$settings['version'], '5.4.1', '<')) { | ||
| wp_slimstat::$settings['use_separate_menu'] = 'on'; | ||
| } |
There was a problem hiding this comment.
This migration overwrites existing menu preferences.
Line 739 forces use_separate_menu to 'on' for every site upgrading from < 5.4.1, including installs where the admin already chose the other menu mode. That makes the upgrade change behavior even when no migration is needed. Please gate this to unset/legacy values only.
Suggested guard
if (version_compare(wp_slimstat::$settings['version'], '5.4.1', '<')) {
- wp_slimstat::$settings['use_separate_menu'] = 'on';
+ if (
+ ! array_key_exists('use_separate_menu', wp_slimstat::$settings) ||
+ '' === wp_slimstat::$settings['use_separate_menu']
+ ) {
+ wp_slimstat::$settings['use_separate_menu'] = 'on';
+ }
}Based on learnings: Applies to **/*.php : Migrate options/globals on upgrade; do not rename or remove without a migration path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/index.php` around lines 735 - 740, The migration currently forces
wp_slimstat::$settings['use_separate_menu'] = 'on' for all sites < 5.4.1; change
it to only modify legacy/unset values: read
wp_slimstat::$settings['use_separate_menu'] and if the key is not set, is empty,
or equals the legacy 'no' value, then set it to 'on'; otherwise leave the
existing value untouched so existing user preferences are preserved (keep the
version_compare check and update behavior confined to the migration block).
| #: src/Services/GDPRService.php:227 | ||
| #: wp-slimstat.php:821 | ||
| msgid "Accept" | ||
| msgstr "" | ||
|
|
||
| #: src/Services/GDPRService.php:187 | ||
| #: src/Services/GDPRService.php:230 | ||
| msgid "Deny" |
There was a problem hiding this comment.
Use one default decline label across all banner paths.
These entries now expose both Deny and Decline for the same negative consent action. That splits translations into two keys, so the new WPML/Polylang support can still miss one render path and show inconsistent button text.
Also applies to: 5892-5894
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@languages/wp-slimstat.pot` around lines 5489 - 5495, Unify the negative
consent label to a single key (use "Decline") so translations aren’t split;
update the POT entries in languages/wp-slimstat.pot to remove msgid "Deny" and
keep only msgid "Decline", and then update all code references that emit the
button label (e.g., in src/Services/GDPRService.php and wp-slimstat.php where
__('Deny') or similar is used) to use __('Decline') (or the chosen translation
helper) so every banner path uses the same translation key.
| // Strip links that don't point to real URLs (legacy accept/deny controls) | ||
| // Keeps: https://..., http://..., /path, //protocol-relative | ||
| // Strips: #, #fragment, empty, whitespace-only — preserves inner text | ||
| $message = preg_replace_callback( | ||
| '/<a\s[^>]*href\s*=\s*["\']([^"\']*)["\'][^>]*>(.*?)<\/a>/is', | ||
| function ($matches) { | ||
| $href = trim($matches[1]); | ||
| if (preg_match('#^(https?://|/)#i', $href)) { | ||
| return $matches[0]; // Real URL — keep the link | ||
| } | ||
| return $matches[2]; // Not a real URL — keep text, strip tag | ||
| }, | ||
| $message | ||
| ); |
There was a problem hiding this comment.
Don't strip valid non-HTTP links from the banner.
After wp_kses() has already sanitized the anchor, the callback on Line 217 still removes safe schemes like mailto: and tel: because it only whitelists http(s) and slash-prefixed URLs. That means legitimate consent/privacy/contact links can still disappear.
Suggested fix
$message = preg_replace_callback(
'/<a\s[^>]*href\s*=\s*["\']([^"\']*)["\'][^>]*>(.*?)<\/a>/is',
function ($matches) {
$href = trim($matches[1]);
- if (preg_match('#^(https?://|/)#i', $href)) {
+ $scheme = wp_parse_url($href, PHP_URL_SCHEME);
+
+ if (
+ 0 === strpos($href, '/') ||
+ (is_string($scheme) && in_array(strtolower($scheme), wp_allowed_protocols(), true))
+ ) {
return $matches[0]; // Real URL — keep the link
}
return $matches[2]; // Not a real URL — keep text, strip tag
},
$message🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Services/GDPRService.php` around lines 210 - 223, The callback used in
preg_replace_callback is too restrictive: inside the anonymous function (the one
handling $matches and setting $href) replace the current
preg_match('#^(https?://|/)#i', $href) check with a pattern that accepts any
valid URL scheme plus protocol-relative and root-relative URLs so mailto:, tel:,
ftp:, etc. are preserved; e.g. test for a scheme per RFC (like
'^[a-z][a-z0-9+.\-]*:'), protocol-relative ('//') or root-relative ('/'), and
return $matches[0] when that test passes, otherwise return $matches[2].
- Fix clearTimeout to clearInterval for interval handle - Add _lastManualRefreshTime to track manual refresh events - Show 0:00 for 2 seconds after manual refresh to provide visual feedback - Replace unused _refresh_timer with functional _lastManualRefreshTime Fixes #153 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix refresh button not resetting countdown timer (#153)
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@admin/assets/js/admin.js`:
- Around line 1470-1477: The early return when timeSinceManualRefresh < 2000
prevents dispatching the minute pulse event and delays real-time updates; before
returning in that conditional (inside the block that checks
SlimStatAdmin._lastManualRefreshTime) ensure you still dispatch the minute pulse
(the same event emitted elsewhere, e.g. 'slimstat:minute_pulse' or by calling
the existing minute pulse emitter) and then reset lastTriggerMinute = -1 and
return; reference SlimStatAdmin._lastManualRefreshTime and lastTriggerMinute to
locate the block and add the dispatch so the minute pulse always fires even
after a manual refresh.
- Around line 1449-1451: Currently the code sets
SlimStatAdmin._lastManualRefreshTime inside the refresh path when id ==
"slim_p7_02", which makes auto-refreshes appear manual; remove that assignment
from refresh_report() and instead set SlimStatAdmin._lastManualRefreshTime at
the actual manual trigger site(s). Concretely, either (a) delete the block that
sets _lastManualRefreshTime in refresh_report()/where id=="slim_p7_02", and add
a line that sets SlimStatAdmin._lastManualRefreshTime = Date.now() in the manual
refresh handler(s) that call refresh_report("slim_p7_02"), or (b) change
refresh_report(id, {manual: true}) to accept a manual flag and only set
_lastManualRefreshTime when manual===true, updating manual callers to pass that
flag; reference: id "slim_p7_02", function refresh_report, and property
SlimStatAdmin._lastManualRefreshTime.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dcaedb89-4ef8-4542-9432-7a3ddd774e38
📒 Files selected for processing (1)
admin/assets/js/admin.js
| if (id == "slim_p7_02") { | ||
| SlimStatAdmin._refresh_timer = SlimStatAdminParams.refresh_interval; | ||
| SlimStatAdmin._lastManualRefreshTime = Date.now(); | ||
| } |
There was a problem hiding this comment.
Track manual refreshes at the trigger site, not in refresh_report().
refresh_report("slim_p7_02") is also used by the minute-pulse auto-refresh path, so setting _lastManualRefreshTime here makes automatic refreshes look manual and shows the new 0:00 state after non-manual reloads too.
Suggested direction
jQuery(document).on("click", "[id^=slim_] .refresh", function (e) {
e.preventDefault();
var id = jQuery(this).parents(".postbox").attr("id");
+ if (id == "slim_p7_02") {
+ SlimStatAdmin._lastManualRefreshTime = Date.now();
+ }
// Is this a pagination link?
if (typeof jQuery(this).attr("href").split("?")[1] == "string") {
...
}
refresh = SlimStatAdmin.refresh_report(id);
refresh();
});- if (id == "slim_p7_02") {
- SlimStatAdmin._lastManualRefreshTime = Date.now();
- }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/assets/js/admin.js` around lines 1449 - 1451, Currently the code sets
SlimStatAdmin._lastManualRefreshTime inside the refresh path when id ==
"slim_p7_02", which makes auto-refreshes appear manual; remove that assignment
from refresh_report() and instead set SlimStatAdmin._lastManualRefreshTime at
the actual manual trigger site(s). Concretely, either (a) delete the block that
sets _lastManualRefreshTime in refresh_report()/where id=="slim_p7_02", and add
a line that sets SlimStatAdmin._lastManualRefreshTime = Date.now() in the manual
refresh handler(s) that call refresh_report("slim_p7_02"), or (b) change
refresh_report(id, {manual: true}) to accept a manual flag and only set
_lastManualRefreshTime when manual===true, updating manual callers to pass that
flag; reference: id "slim_p7_02", function refresh_report, and property
SlimStatAdmin._lastManualRefreshTime.
| // Check if a manual refresh happened recently (within 2 seconds) | ||
| var timeSinceManualRefresh = Date.now() - SlimStatAdmin._lastManualRefreshTime; | ||
| if (timeSinceManualRefresh < 2000) { | ||
| jQuery(".refresh-timer").html("0:00"); | ||
| // Reset the trigger minute to sync with the wall clock after manual refresh | ||
| lastTriggerMinute = -1; | ||
| return; | ||
| } |
There was a problem hiding this comment.
Don't return before dispatching the minute pulse.
If a refresh finishes during second 0, this early return skips slimstat:minute_pulse for the entire minute. That delays both the real-time auto-refresh and the online-visitors update until the next minute.
Suggested fix
function slimstat_sync_and_countdown() {
var now = new Date();
var currentSeconds = now.getSeconds();
var currentMinute = now.getMinutes();
+ if (currentSeconds === 0 && lastTriggerMinute !== currentMinute) {
+ lastTriggerMinute = currentMinute;
+ window.dispatchEvent(new CustomEvent("slimstat:minute_pulse"));
+ }
+
var timeSinceManualRefresh = Date.now() - SlimStatAdmin._lastManualRefreshTime;
if (timeSinceManualRefresh < 2000) {
jQuery(".refresh-timer").html("0:00");
- lastTriggerMinute = -1;
return;
}
-
- if (currentSeconds === 0 && lastTriggerMinute !== currentMinute) {
- lastTriggerMinute = currentMinute;
- window.dispatchEvent(new CustomEvent("slimstat:minute_pulse"));
- }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@admin/assets/js/admin.js` around lines 1470 - 1477, The early return when
timeSinceManualRefresh < 2000 prevents dispatching the minute pulse event and
delays real-time updates; before returning in that conditional (inside the block
that checks SlimStatAdmin._lastManualRefreshTime) ensure you still dispatch the
minute pulse (the same event emitted elsewhere, e.g. 'slimstat:minute_pulse' or
by calling the existing minute pulse emitter) and then reset lastTriggerMinute =
-1 and return; reference SlimStatAdmin._lastManualRefreshTime and
lastTriggerMinute to locate the block and add the dispatch so the minute pulse
always fires even after a manual refresh.
Release v5.4.1
Changelog
Summary by CodeRabbit
New Features
Bug Fixes
Chores