Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9da803f
Add traffic colour presets and high-load link pulsing
alcatron Oct 11, 2026
5ba4133
Validate all scale users and omit unused pulse metadata
alcatron Oct 11, 2026
6ba3fc1
Fix high-traffic SVG glow rendering in Safari
alcatron Oct 11, 2026
7ad49b4
Preserve custom scales and clean up detached traffic glow maps
alcatron Oct 11, 2026
6122db9
Normalize scale ranges and render traffic glow in Live View
alcatron Oct 11, 2026
cdb2364
Validate serialized scales and exercise the colour editor
alcatron Oct 12, 2026
6618348
Refresh traffic snapshots and discard cancelled background edits
alcatron Oct 12, 2026
d02dd06
Refresh only the authorized current map during cycling
alcatron Oct 12, 2026
8be3389
Clean up persistent locks in combined colour-save fixtures
alcatron Oct 12, 2026
274f899
Merge updated develop and preserve editor changes
alcatron Oct 12, 2026
82d5690
Merge develop after gallery resize landed
alcatron Oct 12, 2026
a6b4155
Complete editor translation and PHP documentation cleanup
alcatron Oct 12, 2026
f4b769f
Refresh sibling hotspot maps together with traffic images
alcatron Oct 12, 2026
87da13a
Reload colour fixtures between independent revision-checked edits
alcatron Oct 12, 2026
10fa72b
Label colour controls accessibly and preserve editor revisions
alcatron Oct 12, 2026
a5d98e7
Reject stale colour dialogs before applying changes
alcatron Oct 12, 2026
06023d7
Load and require colour form revision tokens
alcatron Oct 12, 2026
2facae8
Refresh translations for registration failure reporting
alcatron Oct 12, 2026
11023da
Serialize colour revision checks and saves under the shared mutex
alcatron Oct 13, 2026
aded6e3
Cover atomic persistence of map templates
alcatron Oct 13, 2026
5f9e657
Reload complete editor state after saving colours
alcatron Oct 13, 2026
a4f0266
Report normal editor CAS conflicts and operational colour failures
alcatron Oct 13, 2026
1b1c0b5
Report management and CLI write failures and invalid backgrounds
alcatron Oct 13, 2026
25e7372
Run actual CLI failure paths and collect their coverage
alcatron Oct 13, 2026
1221abc
Expand CLI fixture setup into individual statements
alcatron Oct 13, 2026
c87ede4
Keep property dialog Save actions after cancelled placement
alcatron Oct 13, 2026
0fcc544
Report rejected saves and require complete configuration snapshots
alcatron Oct 13, 2026
e1eba10
Keep read-only dependency checks and creation retry guidance safe
alcatron Oct 13, 2026
a5a515e
Coordinate symlink alias writes and reject malformed colours
alcatron Oct 13, 2026
03b6dfd
Address repair, serialization and dialog submission review findings
alcatron Oct 13, 2026
609782f
Preserve every dialog submit action during placement and Enter saves
alcatron Oct 13, 2026
300e633
Merge develop and address save and rendering review findings
alcatron Oct 13, 2026
5f9389c
Document boolean map creation results
alcatron Oct 13, 2026
31df946
Align map creation error branches
alcatron Oct 13, 2026
6be014f
Require mutation revisions and bound colour scale inputs
alcatron Oct 14, 2026
0bef96b
Validate alias snapshots and colour dialog saves
alcatron Oct 14, 2026
ed1e9e5
Bind root symlink identity to editor snapshots
alcatron Oct 14, 2026
2d602f9
Return reload conflicts for retargeted colour roots
alcatron Oct 14, 2026
38c4369
Preserve directory aliases and label advanced colour inputs
alcatron Oct 14, 2026
5459fdc
Merge develop notes support into colour and pulsing changes
alcatron Oct 14, 2026
8bba3f2
Protect shared included traffic scales from ineffective edits
alcatron Oct 14, 2026
daafaa8
Preserve portable INCLUDE directives separately from snapshot paths
alcatron Oct 14, 2026
1ec5087
Preserve colour roundtrips and cloned node ownership
alcatron Oct 14, 2026
840cc97
Preserve included link styles and reject lossy scale bounds
alcatron Oct 14, 2026
a4996ab
Preserve read-only bound tokens and effective setting inheritance
alcatron Oct 14, 2026
f846d2e
Cover empty included pulse setting precedence
alcatron Oct 14, 2026
ad1f369
Correct glow masks, optional tooltips and unlocked range creation
alcatron Oct 14, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++!

--- develop ---

* Add traffic colour presets and optional high-load link pulsing

* Fix Safari rendering of high-traffic link glows using native SVG filters
* Make configuration saves and repairs atomic; coordinate writes, registration and deletion with destination-directory locks and report failed editor saves

* Add editable map notes with alignment and sharp text at logical map dimensions
Expand Down
5 changes: 4 additions & 1 deletion cli/cacti-integrate.php
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,10 @@
}
}

$map->WriteConfig($outputmapfile);
if (!$map->WriteConfig($outputmapfile)) {
fwrite(STDERR, "Could not write configuration to " . $outputmapfile . PHP_EOL);
exit(1);
}

print 'Wrote config to ' . $outputmapfile . PHP_EOL;

Expand Down
5 changes: 4 additions & 1 deletion cli/convert-to-dsstats.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@
}
}

$map->WriteConfig($outputfile);
if (!$map->WriteConfig($outputfile)) {
fwrite(STDERR, "Could not write configuration to " . $outputfile . PHP_EOL);
exit(1);
}

print "Wrote new config to $outputfile" . PHP_EOL;

Expand Down
5 changes: 4 additions & 1 deletion cli/map-tidyup.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@
// adjust node offsets so that links come from correct side of nodes, and ideally still
// from underneath them (e.g. NE80 not NE)

$map->WriteConfig($outputfile);
if (!$map->WriteConfig($outputfile)) {
fwrite(STDERR, "Could not write configuration to " . $outputfile . PHP_EOL);
exit(1);
}

print 'Wrote new config to ' . $outputfile . PHP_EOL;

Expand Down
23 changes: 23 additions & 0 deletions css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,29 @@ body#mainView .ui-dialog textarea.ui-state-default {
color: var(--text-color-lighter, #333) !important;
}

/* Colour and image libraries. */
#wm-scale-rows input { width: 85px; box-sizing: border-box; }
#wm-scale-rows .wm-range-tag { width: 110px; }
.wm-asset-colour { display: inline-flex; flex-direction: column; gap: 4px; }
.wm-asset-colour select { max-width: 180px; }
.wm-asset-colour summary { font-size: 0.9em; cursor: pointer; }
.wm-asset-delete { color: #c62828 !important; background: #fff !important; border: 1px solid #c62828 !important; font-size: 20px; }

.wm-asset-swatches { display: flex; gap: 4px; }
.wm-asset-swatches button { width: 20px; height: 20px; border: 1px solid currentColor; cursor: pointer; padding: 0; }

#wm-traffic-preview { display: flex; flex-wrap: wrap; gap: 10px; margin: 12px 0; }
.wm-traffic-band { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; }
.wm-traffic-swatch { width: 55px; height: 22px; border: 1px solid #777; }
#wm-traffic-advanced > summary { cursor: pointer; margin: 14px 0; }

/* Use Cacti's own theme heading colours in every grouped editor dialog. */
.dlgProperties .wm-editor-section { display: table; width: 100%; box-sizing: border-box; margin: 10px 0 6px; }
.dlgProperties table .wm-editor-section { margin: 0; }
#wm-colours-error:not(:empty) { color: inherit; border-left: 3px solid #c62828; padding-left: 8px; font-weight: bold; }

#dlgColours > .dlgBody { display: block; }

/* Searchable node icon gallery; keep backgrounds on their existing selector. */
body#mainView .wm-icon-source { display: none; }
body#mainView .wm-icon-picker { width: 320px; max-width: 100%; }
Expand Down
6 changes: 6 additions & 0 deletions css/weathermap.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,9 @@ button.wm-cycle-control.fas { font-family: "Font Awesome 5 Free"; font-weight: 9

/* Keep gallery headings readable against the theme title bar. */
.cactiTableTitleRow.wm-gallery-heading { color: var(--text-color-highlight, #fff); }

.wm-traffic-glow { position: absolute; pointer-events: none; overflow: visible; }
.wm-traffic-pulse { opacity: .25; animation: wm-traffic-pulse 2.5s ease-in-out infinite; }
@keyframes wm-traffic-pulse { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }
.wm-glow-paused .wm-traffic-pulse { animation: none; opacity: .65; }
@media (prefers-reduced-motion: reduce) { .wm-traffic-pulse { animation: none; opacity: .65; } }
137 changes: 137 additions & 0 deletions js/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,8 @@ function manage_colours() {
$('#action').val('set_map_colours');

show_dialog('dlgColours');
$('#dlgColours').dialog('option', 'width', Math.min(1100, $(window).width() - 40));
wmOpenColours();
}

function manage_images() {
Expand Down Expand Up @@ -962,6 +964,9 @@ function form_submit(event) {
mapmode('existing');
}

if ($('#action').val() === 'set_map_properties') {
$('#map_background_colour').val($('#wm-map-background-colour .wm-colour-value').val());
}
if ($('#action').val() === 'place_note') {
show_note('', $('#x').val(), $('#y').val());
return;
Expand Down Expand Up @@ -1001,6 +1006,8 @@ function map_properties() {

hide_all_dialogs();

var background = wmAssetColour($('#map_background_colour').val(), false, true, true, wmEditorText.backgroundColour);
$('#wm-map-background-colour').empty().append(background);
$('#action').val('set_map_properties');

show_dialog('dlgMapProperties');
Expand Down Expand Up @@ -1826,6 +1833,136 @@ function wmFriendlyLinkNames() {
function wmDecodeEditorUrl(value) {
return String(value || '').replace(/&/g, '&');
}
var wmAssetPalette = {Black: '#000000', White: '#ffffff', Blue: '#0000ff', Red: '#cc0000', Green: '#008000', Purple: '#800080', Orange: '#b35a00', Grey: '#666666', None: 'none'};
function wmAssetColour(value, allowEmpty, swatches, solid, label) {
var box = $('<div class="wm-asset-colour">'), select = $('<select class="wm-colour-value">').attr('aria-label', label).appendTo(box);
if (allowEmpty) select.append($('<option>').val('').text(wmEditorText.colourText0));
Object.keys(wmAssetPalette).forEach(function(name) { if (solid && name === 'None') return; select.append($('<option>').val(wmAssetPalette[name]).text(wmEditorText.palette[name])); });
if (value && Object.values(wmAssetPalette).indexOf(value.toLowerCase()) < 0) select.append($('<option>').val(value).text(wmEditorText.colourText8.replace('%s', value)));
select.val(value);
if (swatches) {
var palette = $('<div class="wm-asset-swatches">').appendTo(box);
Object.keys(wmAssetPalette).forEach(function(name) {
if (name === 'None') return;
$('<button type="button">').attr({'title': wmEditorText.palette[name], 'aria-label': wmEditorText.palette[name]}).css('background-color', wmAssetPalette[name]).appendTo(palette).on('click', function() { select.val(wmAssetPalette[name]).trigger('change'); });
});
}
var advanced = $('<details>').append($('<summary>').text(wmEditorText.colourText1)).appendTo(box);
var picker = $('<input type="color">').attr('aria-label', label).val(/^#[0-9a-f]{6}$/i.test(value) ? value : '#000000').appendTo(advanced);
picker.on('input', function() {
var colour = this.value;
if (!select.find('option').filter(function() { return this.value === colour; }).length) select.append($('<option>').val(colour).text(wmEditorText.colourText8.replace('%s', colour)));
select.val(colour).trigger('change');
});
select.on('change selectmenuchange', function() { if (/^#/.test(this.value)) picker.val(this.value); if (select.selectmenu('instance')) select.selectmenu('refresh'); });
return box;
}
var wmColourRevision = null;
var wmLockedScales = [];
function wmColourRequest(action, extra, callback, errorId) {
var values = {action: action, mapname: $('#mapname').val()};
if (action === 'set_map_colours') values.config_revision = wmColourRevision;
if (action === 'set_map_colours' && typeof csrfMagicToken !== 'undefined') values.__csrf_magic = csrfMagicToken;
$.ajax({url: editor_url, type: action === 'colour_data' ? 'GET' : 'POST', dataType: 'json', data: $.extend(values, extra),
success: function(data) { if (data.revision) wmColourRevision = data.revision; callback(data); }, error: function(xhr) { $('#' + errorId).text(xhr.status === 409 && wmEditorText.staleError || xhr.responseJSON && xhr.responseJSON.error || wmEditorText.colourText4); }});
}
function wmAssetScaleRow(row) {
var tr = $('<tr>').appendTo('#wm-scale-rows');
['scale', 'bottom', 'top'].forEach(function(key) {
$('<td>').append($('<input>').attr('type', key === 'scale' ? 'text' : 'number').attr('step', 'any').attr('maxlength', key === 'scale' ? 1024 : null).attr('aria-label', {scale: wmEditorText.rangeScale, bottom: wmEditorText.rangeFrom, top: wmEditorText.rangeTo}[key]).addClass('wm-range-' + key).val(row[key])).appendTo(tr);
});
$('<td>').append(wmAssetColour(row.colour, false, false, false, wmEditorText.rangeColour)).appendTo(tr);
$('<td>').append(wmAssetColour(row.end || '', true, false, true, wmEditorText.gradientColour)).appendTo(tr);
$('<td>').append($('<input type="text" class="wm-range-tag">').attr('aria-label', wmEditorText.rangeLabel).val(row.tag || '')).appendTo(tr);
$('<td>').append($('<button type="button" class="ui-button wm-asset-delete">').text(wmEditorText.colourText2).attr('aria-label', wmEditorText.colourText7).on('click', function() { tr.remove(); })).appendTo(tr);
if (wmLockedScales.indexOf(row.scale) !== -1) tr.find('input, select, button').prop('disabled', true);
}
function wmColourRefreshMap() {
wmReloadEditor();
}
// Presets affect only DEFAULT; other named scales remain available in Advanced.
var wmTrafficPresets = {
classic: [[0, 0, '#c0c0c0'], [0, 1, '#ffffff'], [1, 10, '#8c00ff'], [10, 25, '#2020ff'], [25, 40, '#00c0ff'], [40, 55, '#00f000'], [55, 70, '#f0f000'], [70, 85, '#ffc000'], [85, 100, '#ff0000']],
signals: [[0, 0, '#c0c0c0'], [0, 50, '#008000'], [50, 75, '#d18b00'], [75, 90, '#e65c00'], [90, 100, '#cc0000']],
ocean: [[0, 0, '#c0c0c0'], [0, 20, '#b3e5fc'], [20, 40, '#4fc3f7'], [40, 60, '#0288d1'], [60, 80, '#01579b'], [80, 100, '#002b55']],
heat: [[0, 0, '#c0c0c0'], [0, 25, '#6a1b9a'], [25, 50, '#ab47bc'], [50, 75, '#ef6c00'], [75, 90, '#e53935'], [90, 100, '#9b0000']],
modern: [[0, 0, '#c0c0c0'], [0, 20, '#d9edc3'], [20, 40, '#a4ca79'], [40, 60, '#6e9e46'], [60, 80, '#427322'], [80, 100, '#1f3b08']],
sunrise: [[0, 0, '#c0c0c0'], [0, 20, '#fff0b3'], [20, 40, '#ffd166'], [40, 60, '#ffaa00'], [60, 80, '#e87500'], [80, 100, '#a63d00']],
midwinter: [[0, 0, '#c0c0c0'], [0, 20, '#e2e8ff'], [20, 40, '#a8b9ee'], [40, 60, '#7585cc'], [60, 80, '#4e5697'], [80, 100, '#252637']]
};
function wmAssetScaleRows() {
var rows = [];
$('#wm-scale-rows tr').each(function() {
var row = $(this);
rows.push({scale: row.find('.wm-range-scale').val(), bottom: row.find('.wm-range-bottom').val(), top: row.find('.wm-range-top').val(), colour: row.find('.wm-colour-value').eq(0).val(), end: row.find('.wm-colour-value').eq(1).val(), tag: row.find('.wm-range-tag').val()});
});
return rows;
}
function wmTrafficPresetRows(key, rows) {
if (!wmTrafficPresets[key]) return rows;
return rows.filter(function(row) { return row.scale !== 'DEFAULT'; }).concat(wmTrafficPresets[key].map(function(band) {
return {scale: 'DEFAULT', bottom: band[0], top: band[1], colour: band[2], end: '', tag: ''};
}));
}
function wmTrafficPresetMatch(rows) {
var current = rows.filter(function(row) { return row.scale === 'DEFAULT'; });
return Object.keys(wmTrafficPresets).find(function(key) {
var preset = wmTrafficPresets[key];
return current.length === preset.length && preset.every(function(band) {
return current.some(function(row) { return Number(row.bottom) === band[0] && Number(row.top) === band[1] && row.colour.toLowerCase() === band[2] && !row.end && !row.tag; });
});
}) || 'custom';
}
function wmTrafficPreview() {
var preview = $('#wm-traffic-preview').empty();
wmAssetScaleRows().filter(function(row) { return row.scale === 'DEFAULT'; }).forEach(function(row) {
var band = $('<span class="wm-traffic-band">').appendTo(preview);
var swatch = $('<span class="wm-traffic-swatch">').appendTo(band);
if (/^#[0-9a-f]{6}$/i.test(row.colour)) {
swatch.css('background-color', row.colour);
if (/^#[0-9a-f]{6}$/i.test(row.end)) swatch.css('background-image', 'linear-gradient(to right, ' + row.colour + ', ' + row.end + ')');
}
$('<span>').text(row.bottom === row.top ? row.bottom : row.bottom + '–' + row.top).appendTo(band);
});
if (!preview.children().length) preview.text(wmEditorText.colourText3);
}
function wmOpenColours() {
$('#wm-colours-error').empty(); $('#wm-colours-save').prop('disabled', true);
wmColourRequest('colour_data', {}, function(data) {
$('#wm-traffic-pulse').prop('checked', data.colours.pulse === true);
$('#wm-outline-colour').empty().append(wmAssetColour(data.colours.outline, false, true, false, wmEditorText.borderColour));
wmLockedScales = data.colours.locked_scales || [];
$('#wm-scale-rows').empty(); data.colours.rows.forEach(wmAssetScaleRow);
$('#wm-included-scale-help').remove();
if (wmLockedScales.length) $('<p id="wm-included-scale-help">').text(wmEditorText.includedScaleHelp).insertBefore('#wm-traffic-advanced');
$('#wm-traffic-preset').prop('disabled', wmLockedScales.indexOf('DEFAULT') !== -1);
$('#wm-traffic-advanced').prop('open', false);
$('#wm-traffic-preset').val(wmTrafficPresetMatch(data.colours.rows)).off('change selectmenuchange').on('change selectmenuchange', function() {
if (wmLockedScales.indexOf('DEFAULT') !== -1) return;
$('#wm-traffic-advanced').prop('open', this.value === 'custom');
if (this.value !== 'custom') {
var rows = wmTrafficPresetRows(this.value, wmAssetScaleRows());
$('#wm-scale-rows').empty(); rows.forEach(wmAssetScaleRow);
Comment thread
alcatron marked this conversation as resolved.
}
wmTrafficPreview();
});
$('#wm-scale-rows').off('input.wmPreset change.wmPreset click.wmPreset').on('input.wmPreset change.wmPreset click.wmPreset', function() {
$('#wm-traffic-preset').val(wmTrafficPresetMatch(wmAssetScaleRows())).selectmenu('refresh'); wmTrafficPreview();
});
$('#wm-traffic-preset').selectmenu('refresh');
wmTrafficPreview();
$('#dlgColours').dialog('option', {maxHeight: $(window).height() - 80, position: {my: 'center', at: 'center', of: window}});
$('#dlgColours').off('keydown.wmColourSave').on('keydown.wmColourSave', 'input', function(event) {
if (event.key === 'Enter' && (this.type === 'text' || this.type === 'number')) { event.preventDefault(); event.stopPropagation(); $('#wm-colours-save').trigger('click'); }
});
$('#wm-colours-save').prop('disabled', false).off('click').on('click', function() {
var rows = wmAssetScaleRows();
wmColourRequest('set_map_colours', {colours_json: JSON.stringify({outline: $('#wm-outline-colour .wm-colour-value').val(), pulse: $('#wm-traffic-pulse').prop('checked'), rows: rows})}, function() { hide_all_dialogs(); wmColourRefreshMap(); }, 'wm-colours-error');
});
}, 'wm-colours-error');
$('#wm-scale-add').off('click').on('click', function() { var scale = 'DEFAULT'; if (wmLockedScales.indexOf(scale) !== -1) { scale = 'Custom'; var suffix = 2; while (wmLockedScales.indexOf(scale) !== -1) scale = 'Custom' + suffix++; } wmAssetScaleRow({scale: scale, bottom: 0, top: 100, colour: '#0000ff'}); $('#wm-traffic-preset').val('custom').selectmenu('refresh'); wmTrafficPreview(); });
}

// Notes are ordinary label-only nodes, with a small editor-specific style form.
function add_note() {
hide_all_dialogs();
Expand Down
37 changes: 31 additions & 6 deletions js/map-cycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,37 @@ var WMcycler = {
}
},
forceReload: function (that) {
var d = new Date(),
newurl = $(that).find('img').attr("src");

newurl = newurl.replace(/time=\d+/, "time=" + d.getTime());

$(that).find('img').attr("src", newurl);
var holder = $(that);
if (holder.find('.weathermapimage[data-wm-traffic]').length) {
// Refresh the generated image, hotspots and glow metadata as one snapshot.
if (holder.data('wm-refreshing')) return;
holder.data('wm-refreshing', true);
$.ajax({
url: 'weathermap-cacti-plugin.php',
data: {action: 'viewmap', id: holder.attr('id').replace(/^mapholder_/, ''), header: 'false'},
dataType: 'html',
cache: false,
Comment thread
Copilot marked this conversation as resolved.
success: function(html) {
if (!holder[0].isConnected) return;
var page = $('<div>').append($.parseHTML(html, document, false));
var replacement = page.find('.weathermapimage');
var current = holder.find('.weathermapimage').first();
var oldName = current.find('img[usemap]').attr('usemap');
var newName = replacement.find('img[usemap]').attr('usemap');
var oldMap = holder.find('map').filter(function() { return '#' + this.name === oldName; });
var newMap = page.find('map').filter(function() { return '#' + this.name === newName; });
if (replacement.length === 1 && oldMap.length === 1 && newMap.length === 1) {
oldMap.replaceWith(newMap);
current.replaceWith(replacement);
}
},
complete: function() { holder.removeData('wm-refreshing'); }
});
return;
}
var d = new Date(), newurl = holder.find('img').attr('src');
newurl = newurl.replace(/time=\d+/, 'time=' + d.getTime());
holder.find('img').attr('src', newurl);
},

// change to the next (or previous) map, reset the countdown, update the bar
Expand Down
Loading
Loading