Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions css/weathermap.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
76 changes: 40 additions & 36 deletions js/map-cycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jQuery.fn.center = function () {
return this;
};

if (typeof WMcycler !== 'undefined' && WMcycler.stop) { WMcycler.stop(); }

var WMcycler = {

KEYCODE_ESCAPE : 27,
Expand All @@ -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--;
Expand Down Expand Up @@ -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();
Expand All @@ -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 () {
Expand All @@ -145,17 +154,21 @@ 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);
}
},

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();
}

Expand All @@ -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 () {
Expand All @@ -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) {
Expand Down
22 changes: 22 additions & 0 deletions tests/Browser/CycleCountdownTest.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const {JSDOM}=require('jsdom'),jquery=require('jquery'),fs=require('fs'),assert=require('node:assert/strict');
const dom=new JSDOM('<a id="cycle_next" href="#"></a><a id="cycle_prev" href="#"></a><a id="cycle_pause" href="#"></a><div id="wm_countdown"></div><div id="wm_progress"></div><span id="wm_current_map"></span><span id="wm_total_map"></span><div class="weathermapholder"><img src="a?time=0"></div><div class="weathermapholder"><img src="b?time=0"></div>',{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');
54 changes: 27 additions & 27 deletions weathermap-cacti-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.', '<span id="wm_current_map">', '</span>', '<span id="wm_total_map">', '</span>', 'weathermaps');
}

$controls = weathermap_cycle_controls($fullscreen, $limit_to_group);
if ($fullscreen == 0) {
$pagetitle .= ' <span class="linkOverDark"> [ ' . "
<a id='cycle_stop' class='pic fas fa-stop' style='font-size:11px' href='?action='></a>
<a id='cycle_prev' class='fas fa-backward' style='font-size:11px' href='#'></a>
<a id='cycle_pause' class='fas fa-pause' style='font-size:11px' href='#'></a>
<a id='cycle_next' class='fas fa-forward' style='font-size:11px' href='#'></a>
<a target='_new' class='fas fa-expand-arrows-alt' style='font-size:11px' id='cycle_fullscreen' href='" . $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=1&group=' . $limit_to_group . "'></a> ]
[ " . $html . ' ] </span>';
?>
<div class="cactiTable">
<div class="cactiTableTitleRow"><?php print $pagetitle; ?></div>
</div>
<?php
print '<div class="cactiTable"><div class="cactiTableTitleRow wm-cycle-toolbar">' . $pagetitle . ' [ ' . $controls . ' ] [ ' . $html . ' ]</div></div>';
} else {
?>
<div id='wmcyclecontrolbox' class='<?php print $class ?>'>
<div id='wm_progress'></div>
<div id='wm_cyclecontrols'>
<a id='cycle_stop' class='fas fa-stop' href='?action='></a>
<a id='cycle_prev' class='fas fa-backward' href='#'></a>
<a id='cycle_pause' class='fas fa-pause' href='#'></a>
<a id='cycle_next' class='fas fa-forward' href='#'></a>
<a target='_new' class='fas fa-expand-arrows-alt' id='cycle_fullscreen' href='<?php print $config['url_path']; ?>plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=1&group=<?php print $limit_to_group; ?>'></a>
<?php print $html; ?>
</div>
</div>
<?php
print '<div id="wmcyclecontrolbox" class="fullscreen">' . $controls . '<span class="wm-cycle-status">' . $html . '</span></div>';
}
}

$i = 0;

if (cacti_sizeof($maplist)) {
print "<div class='all_map_holder $class'>";

Expand Down Expand Up @@ -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 .= '<a id="' . $id . '" class="wm-cycle-control fas ' . $action[0] . '" href="' . html_escape($action[2]) . '" title="' . html_escape($action[1]) . '" aria-label="' . html_escape($action[1]) . '"></a>';
}
$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 .= '<a id="' . ($fullscreen ? 'cycle_exit_fullscreen' : 'cycle_fullscreen') . '" class="wm-cycle-control wm-fullscreen-link fas ' . ($fullscreen ? 'fa-compress-arrows-alt' : 'fa-expand-arrows-alt') . '" href="' . html_escape($url) . '" title="' . html_escape($label) . '" aria-label="' . html_escape($label) . '">' . ($fullscreen ? '<span class="wm-cycle-exit-label">' . html_escape($label) . '</span>' : '') . '</a>';
$controls .= '<span id="wm_countdown" data-paused-label="' . __esc('Paused', 'weathermap') . '" data-next-label="' . __esc('Next map in %ss', 'weathermap') . '"></span><span class="wm-progress-track"><span id="wm_progress"></span></span>';
return $controls;
}