From 56d283e86a598a22fa9fd4456b8ea9ee736f73df Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Sat, 3 Oct 2026 16:11:21 +0930
Subject: [PATCH 1/8] Restore cycle countdown and same-tab fullscreen controls
---
css/weathermap.css | 10 +++++
js/map-cycle.js | 76 +++++++++++++++++++------------------
weathermap-cacti-plugin.php | 54 +++++++++++++-------------
3 files changed, 77 insertions(+), 63 deletions(-)
diff --git a/css/weathermap.css b/css/weathermap.css
index 32caa7ab..6325a646 100644
--- a/css/weathermap.css
+++ b/css/weathermap.css
@@ -95,3 +95,13 @@ body#wm_fullscreen { font-family: sans-serif; margin: 0; border: 0; padding: 0;
.inplace .weathermapholder { display: none; }
#wm_cyclecontrols a img { width: 16px; height: 16px; border: 0; }
+
+/* Readable compact controls in the themed header and standalone cycle view. */
+.wm-cycle-control { display: inline-block; padding: 6px; margin: 0 5px; color: inherit !important; font-size: 14px; text-decoration: none; vertical-align: middle; }
+.wm-cycle-control:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
+.wm-cycle-toolbar { color: var(--text-color-highlight, #fff); }
+#wmcyclecontrolbox.fullscreen { opacity: 1; background: #26351c; color: #fff; width: auto; max-width: calc(100% - 40px); border: 1px solid #829673; border-radius: 4px; }
+#wm_countdown { margin-left: 12px; white-space: nowrap; }
+.wm-cycle-exit-label { font: 14px sans-serif; margin-left: 6px; }
+.wm-progress-track { display: inline-block; width: 140px; height: 6px; margin-left: 10px; vertical-align: middle; overflow: hidden; border: 1px solid currentColor; border-radius: 3px; background: rgba(255,255,255,.25); }
+.wm-progress-track #wm_progress { display: block; width: 100%; height: 100%; margin: 0; background: currentColor; }
diff --git a/js/map-cycle.js b/js/map-cycle.js
index 9ea988ba..eafe3b3c 100644
--- a/js/map-cycle.js
+++ b/js/map-cycle.js
@@ -11,6 +11,8 @@ jQuery.fn.center = function () {
return this;
};
+if (typeof WMcycler !== 'undefined' && WMcycler.stop) { WMcycler.stop(); }
+
var WMcycler = {
KEYCODE_ESCAPE : 27,
@@ -29,14 +31,17 @@ var WMcycler = {
timer_reloader : null,
updateProgress : function () {
- // update the countdown bar - 450 is the max width in pixels
- var progress = this.countdown / (this.period / 200) * 450;
- $("#wm_progress").css("width", progress);
+ // Update the countdown as a proportion of the selected cycle period.
+ var progress = this.period > 0 ? Math.max(0, Math.min(100, this.countdown / (this.period / 200) * 100)) : 100;
+ $("#wm_progress").css("width", progress + "%");
+ var label = $("#wm_countdown");
+ label.text(this.paused ? (label.attr('data-paused-label') || 'Paused') :
+ (label.attr('data-next-label') || 'Next map in %ss').replace('%s', Math.max(0, Math.ceil(this.countdown / 5))));
},
counterHandler : function () {
if (this.paused) {
- $("#wm_progress").toggleClass("paused");
+ this.updateProgress();
} else {
this.updateProgress();
this.countdown--;
@@ -107,7 +112,16 @@ var WMcycler = {
$("#wmcyclecontrolbox").fadeIn(100);
},
+ stop : function () {
+ clearInterval(this.timer_counter);
+ clearTimeout(this.timer_reloader);
+ $(document).off('.wmCycle');
+ $('#cycle_pause,#cycle_next,#cycle_prev,.wm-fullscreen-link').off('.wmCycle');
+ },
+
start : function (initialData) {
+ this.stop();
+ this.paused = false;
$('.weathermapholder').hide();
@@ -127,16 +141,11 @@ var WMcycler = {
// stop here if there were no maps
if (this.nmaps > 0) {
+ if (this.period === 0) { this.period = this.poller_cycle / this.nmaps; }
this.current = 0;
this.switchMap(0);
- // figure out how long the refresh is, so that we get
- // through all the maps in exactly one poller cycle
- if (this.period === 0) {
- this.period = this.poller_cycle / this.nmaps;
- }
- this.countdown = this.period / 200;
// a countdown timer in the top corner
this.timer_counter = setInterval(function () {
@@ -145,7 +154,11 @@ var WMcycler = {
// when to reload the whole page (with new map data)
this.timer_reloader = setTimeout(function () {
- loadPage(document.location.href);
+ if (typeof loadPage === 'function' && !that.fullscreen) {
+ loadPage(document.location.href);
+ } else {
+ window.location.reload();
+ }
}, this.poller_cycle);
this.initIdle(that);
@@ -153,9 +166,9 @@ var WMcycler = {
},
initKeys: function (that) {
- $(document).on('keyup', function(event) {
+ $(document).on('keyup.wmCycle', function(event) {
if (event.keyCode === that.KEYCODE_ESCAPE) {
- window.location.href = $('#cycle_stop').attr('href');
+ window.location.href = $(that.fullscreen ? '#cycle_exit_fullscreen' : '#cycle_stop').attr('href');
event.preventDefault();
}
@@ -177,38 +190,28 @@ var WMcycler = {
},
initEvents: function (that) {
+ $('.wm-fullscreen-link').off('click.wmCycle').on('click.wmCycle', function(event) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ window.location.assign(this.href);
+ });
- $("#cycle_pause").on('click', function() {
+ $("#cycle_pause").off('click.wmCycle').on('click.wmCycle', function(event) {
+ event.preventDefault();
that.pauseAction();
});
- $("#cycle_next").on('click', function() {
+ $("#cycle_next").off('click.wmCycle').on('click.wmCycle', function(event) {
+ event.preventDefault();
that.nextAction();
});
- $("#cycle_prev").on('click', function() {
+ $("#cycle_prev").off('click.wmCycle').on('click.wmCycle', function(event) {
+ event.preventDefault();
that.previousAction();
});
},
- initIdle: function (that) {
- // aim to get a video-player style OSD for fullscreen mode:
- // if the pointer is off the controls for more than 5 seconds, fade the
- // controls away
- // if the pointer moves after that, bring the controls back
- // if the pointer is over the controls, don't fade
- if (this.fullscreen) {
-
- $(document).idleTimer({
- timeout: 5000
- });
-
- $(document).on("idle.idleTimer", function () {
- that.hideControls();
- });
- $(document).on("active.idleTimer", function () {
- that.showControls();
- });
-
- }
+ initIdle: function () {
+ // Keep the full-screen exit and countdown visible.
},
nextAction : function () {
@@ -219,6 +222,7 @@ var WMcycler = {
},
pauseAction : function () {
this.paused = !this.paused;
+ this.updateProgress();
// remove the paused class on the progress bar, if we're mid-flash and
// no longer paused
if (!this.paused) {
diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php
index 1f722dec..d78ca3aa 100644
--- a/weathermap-cacti-plugin.php
+++ b/weathermap-cacti-plugin.php
@@ -674,38 +674,14 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group
$html = __('Showing %s %s of %s %s. Cycling all available maps.', '', ' ', '', ' ', 'weathermaps');
}
+ $controls = weathermap_cycle_controls($fullscreen, $limit_to_group);
if ($fullscreen == 0) {
- $pagetitle .= ' [ ' . "
-
-
-
-
- ]
- [ " . $html . ' ] ';
- ?>
-
- ' . $pagetitle . ' [ ' . $controls . ' ] [ ' . $html . ' ]
';
} else {
- ?>
-
- ' . $controls . '' . $html . ' ';
}
}
- $i = 0;
-
if (cacti_sizeof($maplist)) {
print "";
@@ -1058,3 +1034,27 @@ function weathermap_tabs($current_tab) {
return false;
}
}
+
+/** Render translated, keyboard-accessible cycle controls.
+ * @param int $fullscreen Whether Cacti chrome is hidden.
+ * @param int $group_id Group filter to retain when toggling full screen.
+ * @return string
+ */
+function weathermap_cycle_controls($fullscreen, $group_id) {
+ global $config;
+ $controls = '';
+ $actions = [
+ 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='],
+ 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'],
+ 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'],
+ 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#']
+ ];
+ foreach ($actions as $id => $action) {
+ $controls .= '
';
+ }
+ $label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap');
+ $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id;
+ $controls .= '
' . ($fullscreen ? '' . html_escape($label) . ' ' : '') . ' ';
+ $controls .= '
';
+ return $controls;
+}
From 5c733fd599fe34d50df760090d123ba6e02bf00f Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Sat, 3 Oct 2026 16:17:47 +0930
Subject: [PATCH 2/8] Check cycle timing, lifecycle and fullscreen reload
---
tests/Browser/CycleCountdownTest.cjs | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 tests/Browser/CycleCountdownTest.cjs
diff --git a/tests/Browser/CycleCountdownTest.cjs b/tests/Browser/CycleCountdownTest.cjs
new file mode 100644
index 00000000..42881cbd
--- /dev/null
+++ b/tests/Browser/CycleCountdownTest.cjs
@@ -0,0 +1,22 @@
+const {JSDOM}=require('jsdom'),jquery=require('jquery'),fs=require('fs'),assert=require('node:assert/strict');
+const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'});
+const w=dom.window,$=jquery(w);w.$=w.jQuery=$;$.fx.off=true;let reloadCallback;w.setInterval=()=>1;w.setTimeout=(fn)=>{reloadCallback=fn;return 2;};require('vm').runInContext(fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8'),dom.getInternalVMContext());
+const c=w.WMcycler;
+c.start({period:0,poller_cycle:300000,fullscreen:0});const pageReload=reloadCallback;
+assert.equal(c.period,150000);assert.equal(c.current,0);assert.equal($('#wm_countdown').text(),'Next map in 150s');assert.equal($('#wm_progress')[0].style.width,'100%');
+c.nextAction();assert.equal(c.current,1);c.nextAction();assert.equal(c.current,0);c.previousAction();assert.equal(c.current,1);
+c.countdown=500;c.updateProgress();assert.equal($('#wm_countdown').text(),'Next map in 100s');assert.ok(Math.abs(parseFloat($('#wm_progress')[0].style.width)-66.6667)<.01);
+c.pauseAction();const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();c.counterHandler();assert.equal(c.countdown,paused-1);
+c.countdown=0;c.counterHandler();assert.equal(c.current,0);assert.equal(c.countdown,750);
+let reloadUrl;w.loadPage=(url)=>reloadUrl=url;pageReload();assert.equal(reloadUrl,'https://example.test/');
+c.period=30000;c.switchMap(0);assert.equal($('#wm_countdown').text(),'Next map in 30s');
+c.countdown=-3;c.updateProgress();assert.equal($('#wm_progress')[0].style.width,'0%');
+const src=fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8');
+const body=src.match(/this.timer_reloader = setTimeout\(function \(\) \{([\s\S]*?)\}, this.poller_cycle\);/)[1];
+let fullReload=0;new Function('that','window','document',body)({fullscreen:1},{location:{reload(){fullReload++;}}},{location:{href:'x'}});assert.equal(fullReload,1);
+$('#wm_countdown').attr('data-next-label','Following map: %s seconds').attr('data-paused-label','Waiting');c.updateProgress();assert.equal($('#wm_countdown').text(),'Following map: 0 seconds');c.pauseAction();assert.equal($('#wm_countdown').text(),'Waiting');c.pauseAction();
+c.start({period:30000,poller_cycle:300000,fullscreen:0});c.start({period:30000,poller_cycle:300000,fullscreen:0});
+$('#cycle_next').trigger('click');assert.equal(c.current,1);$(w.document).trigger($.Event('keyup',{keyCode:39}));assert.equal(c.current,0);
+$('.weathermapholder').last().remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.period,300000);c.nextAction();assert.equal(c.current,0);
+$('.weathermapholder').remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.nmaps,0);c.stop();
+console.log('PASS: translated labels, repeated initialization, one/no-map cases, click/key navigation; automatic period, first countdown, next/previous wrap, pause/resume, automatic transition, progress bounds, configured period and fullscreen reload fallback');
From b9dda0e8ed389e9c09b5b1bc7a44fec9d14fe309 Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Sat, 3 Oct 2026 16:19:41 +0930
Subject: [PATCH 3/8] Document restore cycling countdown and same-tab
fullscreen controls
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dda727f0..8066b405 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++!
--- develop ---
+* bug: Restore cycle countdown, persistent fullscreen exit and reload fallback
+
* refactor: Relocate the plugin's database schema provisioning (`weathermap_setup_table()`) out of `setup.php` into a dedicated `includes/database.php` module (function move only, no behaviour change)
* feature: Add a root `manifest.json` file manifest plus an upgrade-time `weathermap_prune_files()` that removes tombstoned and dev-only paths while protecting whitelisted user data (`configs/`, `output/`) and `.git*`, backed by a `tests/bin/validate-manifest.php` CI gate that fails on manifest drift
* dev: Add xdebug coverage reporting to CI and enforce patch coverage of changed lines
From 002266ad7244b1f4f2332cb849820ebff80f6fe8 Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:01:41 +1030
Subject: [PATCH 4/8] Use numbered changelog entries for PR 282
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8066b405..db093b37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,7 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++!
--- develop ---
-* bug: Restore cycle countdown, persistent fullscreen exit and reload fallback
+-issue#282: Restore cycle countdown, persistent fullscreen exit and reload fallback
* refactor: Relocate the plugin's database schema provisioning (`weathermap_setup_table()`) out of `setup.php` into a dedicated `includes/database.php` module (function move only, no behaviour change)
* feature: Add a root `manifest.json` file manifest plus an upgrade-time `weathermap_prune_files()` that removes tombstoned and dev-only paths while protecting whitelisted user data (`configs/`, `output/`) and `.git*`, backed by a `tests/bin/validate-manifest.php` CI gate that fails on manifest drift
From 933fc20e93a228179e03ffc3adce388336aa982b Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Fri, 9 Oct 2026 10:12:52 +1030
Subject: [PATCH 5/8] Address cycle viewer review findings and refresh
translations
---
locales/po/cacti.pot | 35 ++++++++++++++++++++++++++++++++++-
weathermap-cacti-plugin.php | 21 ++++++++++++---------
2 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot
index 3481e6ad..728af904 100644
--- a/locales/po/cacti.pot
+++ b/locales/po/cacti.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti \n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
-"POT-Creation-Date: 2026-10-09 00:20+1030\n"
+"POT-Creation-Date: 2026-10-09 10:11+1030\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers
\n"
"Language-Team: Cacti Developers \n"
@@ -1802,3 +1802,36 @@ msgstr ""
#: weathermap-cacti-plugin.php
msgid "All Maps"
msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Stop cycling"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Previous"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Pause / resume"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Next"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Exit full screen"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Full screen"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+#, php-format
+msgid "Next map in %ss"
+msgstr ""
+
+#: weathermap-cacti-plugin.php
+msgid "Paused"
+msgstr ""
diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php
index d78ca3aa..9813ab66 100644
--- a/weathermap-cacti-plugin.php
+++ b/weathermap-cacti-plugin.php
@@ -689,8 +689,6 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group
$confdir = __DIR__ . '/configs/';
foreach ($maplist as $map) {
- $i++;
-
$htmlfile = $outdir . $map['filehash'] . '.html';
$maptitle = $map['titlecache'];
@@ -1035,26 +1033,31 @@ function weathermap_tabs($current_tab) {
}
}
-/** Render translated, keyboard-accessible cycle controls.
+/**
+ * Render translated, keyboard-accessible cycle controls.
+ *
* @param int $fullscreen Whether Cacti chrome is hidden.
- * @param int $group_id Group filter to retain when toggling full screen.
+ * @param int $group_id Group filter to retain when toggling full screen.
+ *
* @return string
*/
function weathermap_cycle_controls($fullscreen, $group_id) {
global $config;
$controls = '';
- $actions = [
- 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='],
- 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'],
+ $actions = [
+ 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='],
+ 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'],
'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'],
- 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#']
+ 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#']
];
+
foreach ($actions as $id => $action) {
$controls .= ' ';
}
$label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap');
- $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id;
+ $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id;
$controls .= '' . ($fullscreen ? '' . html_escape($label) . ' ' : '') . ' ';
$controls .= ' ';
+
return $controls;
}
From 0c5b0fc9581ea0b3fe3e304de61e1e163c9bbed1 Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Fri, 9 Oct 2026 10:21:40 +1030
Subject: [PATCH 6/8] Remove obsolete cycle idle handling and redundant group
cast
---
js/map-cycle.js | 14 --------------
weathermap-cacti-plugin.php | 4 +---
2 files changed, 1 insertion(+), 17 deletions(-)
diff --git a/js/map-cycle.js b/js/map-cycle.js
index eafe3b3c..2efbe623 100644
--- a/js/map-cycle.js
+++ b/js/map-cycle.js
@@ -104,14 +104,6 @@ var WMcycler = {
},
- hideControls: function () {
- $("#wmcyclecontrolbox").fadeOut(500);
- },
-
- showControls: function () {
- $("#wmcyclecontrolbox").fadeIn(100);
- },
-
stop : function () {
clearInterval(this.timer_counter);
clearTimeout(this.timer_reloader);
@@ -160,8 +152,6 @@ var WMcycler = {
window.location.reload();
}
}, this.poller_cycle);
-
- this.initIdle(that);
}
},
@@ -210,10 +200,6 @@ var WMcycler = {
});
},
- initIdle: function () {
- // Keep the full-screen exit and countdown visible.
- },
-
nextAction : function () {
this.switchMap(1);
},
diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php
index 9813ab66..d99537dd 100644
--- a/weathermap-cacti-plugin.php
+++ b/weathermap-cacti-plugin.php
@@ -666,8 +666,6 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group
print get_md5_include_js('include/js/jquery.js');
}
- print get_md5_include_js('plugins/weathermap/js/idle-timer.min.js');
-
if ($limit_to_group > 0) {
$html = __('Showing %s %s of %s %s. Cycling all available maps in this group.', '', ' ', '', ' ', 'weathermaps');
} else {
@@ -1055,7 +1053,7 @@ function weathermap_cycle_controls($fullscreen, $group_id) {
$controls .= ' ';
}
$label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap');
- $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id;
+ $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . $group_id;
$controls .= '' . ($fullscreen ? '' . html_escape($label) . ' ' : '') . ' ';
$controls .= ' ';
From 60ead841f68de65d4655f16a2c0a54b4d881a4ee Mon Sep 17 00:00:00 2001
From: Muris <27321443+alcatron@users.noreply.github.com>
Date: Fri, 9 Oct 2026 10:28:34 +1030
Subject: [PATCH 7/8] Use accessible cycle buttons and preserve focused
keyboard behavior
---
css/weathermap.css | 1 +
js/map-cycle.js | 6 ++++++
tests/Browser/CycleCountdownTest.cjs | 19 +++++++++++++++++--
weathermap-cacti-plugin.php | 18 ++++++++++++------
4 files changed, 36 insertions(+), 8 deletions(-)
diff --git a/css/weathermap.css b/css/weathermap.css
index 6325a646..b0a4a0e2 100644
--- a/css/weathermap.css
+++ b/css/weathermap.css
@@ -98,6 +98,7 @@ body#wm_fullscreen { font-family: sans-serif; margin: 0; border: 0; padding: 0;
/* Readable compact controls in the themed header and standalone cycle view. */
.wm-cycle-control { display: inline-block; padding: 6px; margin: 0 5px; color: inherit !important; font-size: 14px; text-decoration: none; vertical-align: middle; }
+button.wm-cycle-control { appearance: none; background: transparent; border: 0; box-shadow: none; cursor: pointer; line-height: inherit; }
.wm-cycle-control:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.wm-cycle-toolbar { color: var(--text-color-highlight, #fff); }
#wmcyclecontrolbox.fullscreen { opacity: 1; background: #26351c; color: #fff; width: auto; max-width: calc(100% - 40px); border: 1px solid #829673; border-radius: 4px; }
diff --git a/js/map-cycle.js b/js/map-cycle.js
index 2efbe623..7f50d534 100644
--- a/js/map-cycle.js
+++ b/js/map-cycle.js
@@ -31,6 +31,7 @@ var WMcycler = {
timer_reloader : null,
updateProgress : function () {
+ $("#cycle_pause").attr("aria-pressed", this.paused ? "true" : "false");
// Update the countdown as a proportion of the selected cycle period.
var progress = this.period > 0 ? Math.max(0, Math.min(100, this.countdown / (this.period / 200) * 100)) : 100;
$("#wm_progress").css("width", progress + "%");
@@ -114,6 +115,7 @@ var WMcycler = {
start : function (initialData) {
this.stop();
this.paused = false;
+ $('#cycle_pause').attr('aria-pressed', 'false');
$('.weathermapholder').hide();
@@ -157,6 +159,10 @@ var WMcycler = {
initKeys: function (that) {
$(document).on('keyup.wmCycle', function(event) {
+ if ($(event.target).closest('a, button, input, select, textarea, [role="button"], [contenteditable]:not([contenteditable="false"])').length) {
+ return;
+ }
+
if (event.keyCode === that.KEYCODE_ESCAPE) {
window.location.href = $(that.fullscreen ? '#cycle_exit_fullscreen' : '#cycle_stop').attr('href');
event.preventDefault();
diff --git a/tests/Browser/CycleCountdownTest.cjs b/tests/Browser/CycleCountdownTest.cjs
index 42881cbd..4ecc698f 100644
--- a/tests/Browser/CycleCountdownTest.cjs
+++ b/tests/Browser/CycleCountdownTest.cjs
@@ -1,12 +1,12 @@
const {JSDOM}=require('jsdom'),jquery=require('jquery'),fs=require('fs'),assert=require('node:assert/strict');
-const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'});
+const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'});
const w=dom.window,$=jquery(w);w.$=w.jQuery=$;$.fx.off=true;let reloadCallback;w.setInterval=()=>1;w.setTimeout=(fn)=>{reloadCallback=fn;return 2;};require('vm').runInContext(fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8'),dom.getInternalVMContext());
const c=w.WMcycler;
c.start({period:0,poller_cycle:300000,fullscreen:0});const pageReload=reloadCallback;
assert.equal(c.period,150000);assert.equal(c.current,0);assert.equal($('#wm_countdown').text(),'Next map in 150s');assert.equal($('#wm_progress')[0].style.width,'100%');
c.nextAction();assert.equal(c.current,1);c.nextAction();assert.equal(c.current,0);c.previousAction();assert.equal(c.current,1);
c.countdown=500;c.updateProgress();assert.equal($('#wm_countdown').text(),'Next map in 100s');assert.ok(Math.abs(parseFloat($('#wm_progress')[0].style.width)-66.6667)<.01);
-c.pauseAction();const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();c.counterHandler();assert.equal(c.countdown,paused-1);
+c.pauseAction();assert.equal($("#cycle_pause").attr("aria-pressed"),"true");const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();assert.equal($("#cycle_pause").attr("aria-pressed"),"false");c.counterHandler();assert.equal(c.countdown,paused-1);
c.countdown=0;c.counterHandler();assert.equal(c.current,0);assert.equal(c.countdown,750);
let reloadUrl;w.loadPage=(url)=>reloadUrl=url;pageReload();assert.equal(reloadUrl,'https://example.test/');
c.period=30000;c.switchMap(0);assert.equal($('#wm_countdown').text(),'Next map in 30s');
@@ -17,6 +17,21 @@ let fullReload=0;new Function('that','window','document',body)({fullscreen:1},{l
$('#wm_countdown').attr('data-next-label','Following map: %s seconds').attr('data-paused-label','Waiting');c.updateProgress();assert.equal($('#wm_countdown').text(),'Following map: 0 seconds');c.pauseAction();assert.equal($('#wm_countdown').text(),'Waiting');c.pauseAction();
c.start({period:30000,poller_cycle:300000,fullscreen:0});c.start({period:30000,poller_cycle:300000,fullscreen:0});
$('#cycle_next').trigger('click');assert.equal(c.current,1);$(w.document).trigger($.Event('keyup',{keyCode:39}));assert.equal(c.current,0);
+// Focused controls and editable content must retain their own keyboard behavior.
+const initialMap=c.current, initialPaused=c.paused;
+for (const selector of ['#cycle_next','#cycle_prev','#cycle_pause']) {
+ $(selector).trigger($.Event('keyup',{keyCode:32}));
+ $(selector).trigger($.Event('keyup',{keyCode:39}));
+ assert.equal(c.current,initialMap);assert.equal(c.paused,initialPaused);
+}
+$('body').append('Editing
Link ');
+for (const selector of ['#wm-input','#wm-editable','#wm-link']) {
+ $(selector).trigger($.Event('keyup',{keyCode:32}));
+ assert.equal(c.paused,initialPaused);
+}
+$(w.document).trigger($.Event('keyup',{keyCode:32}));assert.equal(c.paused,!initialPaused);
+assert.equal($('#cycle_pause').attr('aria-pressed'),'true');
+c.start({period:30000,poller_cycle:300000,fullscreen:0});assert.equal($('#cycle_pause').attr('aria-pressed'),'false');
$('.weathermapholder').last().remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.period,300000);c.nextAction();assert.equal(c.current,0);
$('.weathermapholder').remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.nmaps,0);c.stop();
console.log('PASS: translated labels, repeated initialization, one/no-map cases, click/key navigation; automatic period, first countdown, next/previous wrap, pause/resume, automatic transition, progress bounds, configured period and fullscreen reload fallback');
diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php
index d99537dd..8b9cef95 100644
--- a/weathermap-cacti-plugin.php
+++ b/weathermap-cacti-plugin.php
@@ -1034,8 +1034,8 @@ function weathermap_tabs($current_tab) {
/**
* Render translated, keyboard-accessible cycle controls.
*
- * @param int $fullscreen Whether Cacti chrome is hidden.
- * @param int $group_id Group filter to retain when toggling full screen.
+ * @param int|string $fullscreen Whether Cacti chrome is hidden.
+ * @param int|string $group_id Group filter to retain when toggling full screen.
*
* @return string
*/
@@ -1044,13 +1044,19 @@ function weathermap_cycle_controls($fullscreen, $group_id) {
$controls = '';
$actions = [
'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='],
- 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'],
- 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'],
- 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#']
+ 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap')],
+ 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap')],
+ 'cycle_next' => ['fa-forward', __('Next', 'weathermap')]
];
foreach ($actions as $id => $action) {
- $controls .= ' ';
+ $attributes = ' id="' . $id . '" class="wm-cycle-control fas ' . $action[0] . '" title="' . html_escape($action[1]) . '" aria-label="' . html_escape($action[1]) . '"';
+
+ if ($id === 'cycle_stop') {
+ $controls .= ' ';
+ } else {
+ $controls .= ' ';
+ }
}
$label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap');
$url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . $group_id;
From af870b2d8ae7092c8bfd3497a8b2815c01104fe3 Mon Sep 17 00:00:00 2001
From: TheWitness
Date: Thu, 8 Oct 2026 20:15:18 -0400
Subject: [PATCH 8/8] Allowlist weathermap-cacti-plugin.php for patch coverage
The patch-coverage gate fails because the changed production file weathermap-cacti-plugin.php is absent from Clover. It is a web UI entry point: it includes ../../include/auth.php and runs a switch (get_request_var('action')) dispatch at the top level, so it cannot be loaded into the isolated unit process to be measured. The new weathermap_cycle_controls() and the changed weathermap_fullview() lines live in that same un-loadable file. Add it to the unmeasured allowlist alongside the other weathermap entry points, matching the mechanism the gate documents for exactly this case.
---
tests/bin/patch-coverage.php | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/bin/patch-coverage.php b/tests/bin/patch-coverage.php
index bea57b42..57fcb3a9 100644
--- a/tests/bin/patch-coverage.php
+++ b/tests/bin/patch-coverage.php
@@ -163,6 +163,10 @@ function changed_lines($base_ref) {
* Empty by default; add entries per repository as the need arises.
*/
$unmeasured_allowlist = [
+ // Web UI entry point: top-level include of ../../include/auth.php plus a
+ // switch (get_request_var('action')) dispatch that runs at load, so it
+ // cannot load in the isolated unit process.
+ 'weathermap-cacti-plugin.php',
// Web UI entry point: top-level include of ../../include/auth.php plus
// request-var dispatch, so it cannot load in the isolated unit process.
'weathermap-cacti-plugin-mgmt.php',