checkmk-swissknife/user_script/checkmk_swissknife.user.js

1295 lines
46 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==UserScript==
// @name Checkmk SwissKnife
// @namespace https://luigidacunto.com/
// @version 2.13.1
// @checkmk 2.3.x
// @description Collection of UI improvements for Checkmk WATO. Each fix or enhancement is added here as an independent feature.
// @author Luigi D'Acunto
// @homepageURL https://git.luigidacunto.com/tools/checkmk-swissknife
// @updateURL https://luigidacunto.com/scripts/checkmk-swissknife/user_script/checkmk_swissknife.user.js
// @downloadURL https://luigidacunto.com/scripts/checkmk-swissknife/user_script/checkmk_swissknife.user.js
// @include /^https?:\/\/.+\/check_mk\/(index|wato|view)\.py/
// @grant none
// ==/UserScript==
(function () {
'use strict';
// =========================================================================
// COMMON INFRASTRUCTURE
// =========================================================================
const LOG_PREFIX = '[CMK-SK]';
const POLL_INTERVAL_MS = 500;
const MAX_ATTEMPTS = 60;
// Returns the document to operate on:
// - If there is an iframe (index.py with sidebar) → use iframe's contentDocument
// - If wato.py is opened directly → use document only if it contains the target select
function getWatoDoc(selectId) {
const iframe = document.querySelector('iframe[name="main"], iframe#main');
if (iframe) {
try { return iframe.contentDocument; } catch (e) { return null; }
}
if (document.getElementById(selectId) ||
document.querySelector('select[name*="folder_path"]') ||
document.getElementById('wato_folder')) {
return document;
}
return null;
}
// Reads the "mode" parameter from the target document URL without accessing the DOM
function getPageMode(iDoc) {
try { return new URLSearchParams(iDoc.location.search).get('mode') || ''; }
catch (e) { return ''; }
}
// Returns the document to operate on, handling both the iframe case (index.py with
// sidebar) and the direct case (wato.py opened without sidebar, no iframe present).
function getTargetDoc() {
const iframe = document.querySelector('iframe[name="main"], iframe#main');
if (iframe) { try { return iframe.contentDocument; } catch (e) { return null; } }
return document;
}
// Pages that support accordion badges (same form_edit_host + table.nform structure)
const ACCORDION_MODES = new Set(['edit_host', 'bulkedit', 'editfolder']);
// Injects CSS into the target document (once only, deduplicated by id)
function injectStyles(iDoc, id, css) {
if (iDoc.getElementById(id)) return;
const style = iDoc.createElement('style');
style.id = id;
style.textContent = css;
iDoc.head.appendChild(style);
}
// =========================================================================
// FEATURE: Folder Path Select Enhancement
//
// Improves the folder path <select> in WATO by showing the full path
// in "Root Level Leaf" style and enabling search on it.
// Only activates when the #explicit_conditions_p_folder_path select is present.
// =========================================================================
const FOLDER_SELECT_ID = 'explicit_conditions_p_folder_path';
const FOLDER_DIV_ID = 'explicit_conditions_d_folder_path';
function formatPath(value) {
if (!value) return 'Main';
const parts = value.split('/');
return parts.map((p, i) =>
i === parts.length - 1
? p.toUpperCase()
: p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()
).join(' ');
}
function enhanceFolderSelect(iDoc) {
const sel = iDoc.getElementById(FOLDER_SELECT_ID);
if (!sel) return false;
if (sel.dataset.cmkEnhanced === '1') return true;
const iWin = iDoc.defaultView;
const S2 = iWin.Select2 || (iWin.$ && iWin.$.fn && iWin.$.fn.select2 ? iWin.$ : null);
if (!S2) return false;
Array.from(sel.options).forEach(opt => {
if (!opt.dataset.fullPath) {
opt.dataset.fullPath = formatPath(opt.value);
}
});
try {
const existing = getSelect2Instance(iDoc, sel);
if (existing && typeof existing.destroy === 'function') existing.destroy();
} catch (e) {
console.warn(LOG_PREFIX, 'Failed to destroy Select2:', e);
}
initSelect2Enhanced(iDoc, sel);
sel.dataset.cmkEnhanced = '1';
return true;
}
function getSelect2Instance(iDoc, sel) {
const iWin = iDoc.defaultView;
if (iWin.$ && iWin.$.fn && iWin.$.fn.select2) {
try { return iWin.$(sel).data('select2'); } catch (e) {}
}
if (iWin.Select2) {
try { return iWin.Select2.getInstance(sel); } catch (e) {}
}
return null;
}
function initSelect2Enhanced(iDoc, sel) {
const iWin = iDoc.defaultView;
const templateResult = function (option) {
if (!option.id) return option.text;
const fullPath = option.element?.dataset?.fullPath || formatPath(option.id);
const span = iDoc.createElement('span');
span.title = fullPath;
span.style.cssText = 'font-family: monospace; font-size: 12px;';
span.textContent = fullPath;
return span;
};
const templateSelection = function (option) {
if (!option.id) return option.text;
return formatPath(option.id);
};
const matcher = function (params, option) {
if (!params.term || params.term.trim() === '') return option;
const term = params.term.trim().toLowerCase();
const fullPath = (option.element?.dataset?.fullPath || formatPath(option.id || '')).toLowerCase();
const leafName = (option.id || '').split('/').pop().toLowerCase();
if (fullPath.includes(term) || leafName.includes(term)) return option;
return null;
};
const config = {
width: 'resolve',
allowClear: false,
dropdownAutoWidth: true,
templateResult,
templateSelection,
matcher,
dropdownCssClass: 'cmk-sk-folder-dropdown',
};
if (iWin.$ && iWin.$.fn && iWin.$.fn.select2) {
try {
iWin.$(sel).select2(config);
injectFolderStyles(iDoc);
return;
} catch (e) {
console.warn(LOG_PREFIX, 'jQuery Select2 init failed:', e);
}
}
if (iWin.Select2) {
try {
new iWin.Select2(sel, config);
injectFolderStyles(iDoc);
return;
} catch (e) {
console.warn(LOG_PREFIX, 'Select2 standalone init failed:', e);
}
}
// Fallback: custom search overlay over the native select2
buildCustomSearchOverlay(iDoc, sel);
}
function buildCustomSearchOverlay(iDoc, sel, containerEl) {
const divContainer = containerEl || iDoc.getElementById(FOLDER_DIV_ID);
if (!divContainer) return;
if (divContainer.querySelector('.cmk-sk-folder-overlay')) return;
const options = Array.from(sel.options).map(opt => ({
value: opt.value,
label: opt.value ? formatPath(opt.value) : 'Main',
original: opt.text.trim()
}));
const existingContainer = divContainer.querySelector('.select2-container');
if (existingContainer) existingContainer.style.display = 'none';
const wrapper = iDoc.createElement('div');
wrapper.className = 'cmk-sk-folder-overlay';
wrapper.style.cssText = `
display: inline-block;
position: relative;
min-width: 300px;
max-width: 600px;
width: 100%;
font-family: var(--font-family, sans-serif);
`;
const searchInput = iDoc.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search folder by name or path... (e.g.: veeam, dc1/veeam)';
searchInput.autocomplete = 'off';
searchInput.spellcheck = false;
searchInput.style.cssText = `
width: 100%;
padding: 4px 8px;
border: 1px solid #666;
background: #1a1a2e;
color: #e0e0e0;
font-size: 12px;
font-family: monospace;
border-radius: 3px;
box-sizing: border-box;
`;
const currentVal = sel.value;
const currentOpt = options.find(o => o.value === currentVal);
if (currentOpt) searchInput.value = currentOpt.label;
const dropdown = iDoc.createElement('div');
dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 300px;
overflow-y: auto;
background: #1a1a2e;
border: 1px solid #555;
border-top: none;
z-index: 99999;
display: none;
font-size: 12px;
font-family: monospace;
min-width: 450px;
`;
const badge = iDoc.createElement('div');
badge.style.cssText = `
font-size: 10px;
color: #aaa;
margin-top: 2px;
padding-left: 2px;
font-family: monospace;
`;
updateBadge();
function updateBadge() {
const v = sel.value;
badge.textContent = v ? `Path: ${v}` : 'Path: / (Main)';
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderDropdown(filter) {
dropdown.innerHTML = '';
const term = filter.trim().toLowerCase();
// Normalize separators: >, , / are equivalent in search
const normTerm = term.replace(/\s*[>/]\s*/g, '/');
const filtered = term
? options.filter(o =>
o.label.toLowerCase().includes(term) ||
o.label.toLowerCase().replace(/\s*\s*/g, '/').includes(normTerm) ||
o.value.toLowerCase().includes(normTerm) ||
o.original.toLowerCase().includes(term)
)
: options;
if (filtered.length === 0) {
const noRes = iDoc.createElement('div');
noRes.textContent = 'No results';
noRes.style.cssText = 'padding: 6px 10px; color: #aaa;';
dropdown.appendChild(noRes);
}
filtered.slice(0, 200).forEach((opt) => {
const item = iDoc.createElement('div');
item.dataset.value = opt.value;
item.style.cssText = `
padding: 4px 10px;
cursor: pointer;
color: #e0e0e0;
border-bottom: 1px solid #333;
white-space: nowrap;
`;
if (term) {
const labelLow = opt.label.toLowerCase();
const idx = labelLow.indexOf(term);
if (idx >= 0) {
item.innerHTML =
escapeHtml(opt.label.substring(0, idx)) +
'<mark style="background:#f0a500;color:#000;border-radius:2px;">' +
escapeHtml(opt.label.substring(idx, idx + term.length)) +
'</mark>' +
escapeHtml(opt.label.substring(idx + term.length));
} else {
item.textContent = opt.label;
}
} else {
item.textContent = opt.label;
}
if (opt.value === sel.value) {
item.style.background = '#2a4a6e';
item.style.fontWeight = 'bold';
}
item.addEventListener('mouseenter', () => { item.style.background = '#3a3a5e'; });
item.addEventListener('mouseleave', () => {
item.style.background = opt.value === sel.value ? '#2a4a6e' : '';
});
item.addEventListener('mousedown', (e) => {
e.preventDefault();
selectOption(opt.value, opt.label);
});
dropdown.appendChild(item);
});
if (filtered.length > 200) {
const more = iDoc.createElement('div');
more.textContent = `... and ${filtered.length - 200} more results. Refine your search.`;
more.style.cssText = 'padding: 6px 10px; color: #aaa; font-style: italic;';
dropdown.appendChild(more);
}
dropdown.style.display = 'block';
}
function selectOption(value, label) {
sel.value = value;
sel.dispatchEvent(new Event('change', { bubbles: true }));
searchInput.value = label;
dropdown.style.display = 'none';
updateBadge();
}
function highlightItem(items, idx) {
items.forEach(i => i.classList.remove('highlighted'));
if (items[idx]) {
items[idx].classList.add('highlighted');
items[idx].style.background = '#4a6a9e';
items[idx].scrollIntoView({ block: 'nearest' });
}
}
searchInput.addEventListener('focus', () => {
searchInput.select();
renderDropdown(searchInput.value === currentOpt?.label ? '' : searchInput.value);
});
searchInput.addEventListener('input', () => { renderDropdown(searchInput.value); });
searchInput.addEventListener('blur', () => {
setTimeout(() => {
dropdown.style.display = 'none';
const o = options.find(x => x.value === sel.value);
if (o) searchInput.value = o.label;
}, 200);
});
searchInput.addEventListener('keydown', (e) => {
const items = dropdown.querySelectorAll('[data-value]');
const current = dropdown.querySelector('[data-value].highlighted');
const currentIdx = current ? Array.from(items).indexOf(current) : -1;
if (e.key === 'ArrowDown') {
e.preventDefault();
highlightItem(items, Math.min(currentIdx + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlightItem(items, Math.max(currentIdx - 1, 0));
} else if (e.key === 'Enter') {
e.preventDefault();
const highlighted = dropdown.querySelector('[data-value].highlighted');
if (highlighted) {
selectOption(
highlighted.dataset.value,
options.find(o => o.value === highlighted.dataset.value)?.label || highlighted.textContent
);
}
} else if (e.key === 'Escape') {
dropdown.style.display = 'none';
}
});
wrapper.appendChild(searchInput);
wrapper.appendChild(dropdown);
divContainer.appendChild(wrapper);
divContainer.appendChild(badge);
sel.dataset.cmkEnhanced = '1';
injectFolderStyles(iDoc);
}
function injectFolderStyles(iDoc) {
injectStyles(iDoc, 'cmk-sk-folder-styles', `
.cmk-sk-folder-dropdown .select2-results__option {
font-family: monospace !important;
font-size: 12px !important;
white-space: nowrap !important;
}
.cmk-sk-folder-overlay mark {
background: #f0a500;
color: #000;
border-radius: 2px;
padding: 0 1px;
}
.cmk-sk-folder-overlay [data-value].highlighted {
background: #4a6a9e !important;
}
`);
}
// =========================================================================
// FEATURE: Accordion Checked Count Badge
//
// Shows in each accordion title on the edit_host page the number
// of active checkboxes in the group. E.g.: "Services - DB (1)".
// The counter updates in real time when checkboxes change.
// =========================================================================
function updateAccordionBadge(td) {
const table = td.closest('table.nform');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
const checked = tbody.querySelectorAll('input[type=checkbox]:checked').length;
const badge = td.querySelector('.cmk-sk-acc-count');
if (!badge) return;
if (checked > 0) {
badge.textContent = `(${checked})`;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
function updateInheritedBadge(td, iWin) {
const table = td.closest('table.nform');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
const count = Array.from(tbody.querySelectorAll('div.inherited')).filter(el =>
iWin.getComputedStyle(el).display !== 'none' && el.textContent.includes('Inherited from')
).length;
const badge = td.querySelector('.cmk-sk-inh-count');
if (!badge) return;
if (count > 0) {
badge.textContent = `${count}`;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
function updateDiffBadge(td, iWin) {
const table = td.closest('table.nform');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
const count = Array.from(tbody.querySelectorAll('div.inherited')).filter(el =>
iWin.getComputedStyle(el).display !== 'none' && el.textContent.includes('This value differs')
).length;
const badge = td.querySelector('.cmk-sk-diff-count');
if (!badge) return;
if (count > 0) {
badge.textContent = `${count}`;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
function initAccordionCheckedCounts(iDoc) {
const form = iDoc.getElementById('form_edit_host');
if (!form) return false;
if (form.dataset.cmkAccBadge === '1') return true;
const iWin = iDoc.defaultView;
const isBulkEdit = getPageMode(iDoc) === 'bulkedit';
injectStyles(iDoc, 'cmk-sk-acc-badge-styles', `
.cmk-sk-acc-count {
margin-left: 6px;
padding: 1px 6px;
background: #f0a500;
color: #000;
border-radius: 9px;
font-size: 11px;
font-weight: bold;
vertical-align: middle;
}
.cmk-sk-inh-count {
margin-left: 4px;
padding: 1px 6px;
background: #5ba4e5;
color: #000;
border-radius: 9px;
font-size: 11px;
font-weight: bold;
vertical-align: middle;
}
.cmk-sk-diff-count {
margin-left: 4px;
padding: 1px 6px;
background: #e55b5b;
color: #fff;
border-radius: 9px;
font-size: 11px;
font-weight: bold;
vertical-align: middle;
}
`);
iDoc.querySelectorAll('table.nform thead tr.heading td').forEach(td => {
const table = td.closest('table.nform');
const tbody = table?.querySelector('tbody');
if (!tbody) return;
const badge = iDoc.createElement('span');
badge.className = 'cmk-sk-acc-count';
badge.style.display = 'none';
const badgeInh = iDoc.createElement('span');
badgeInh.className = 'cmk-sk-inh-count';
badgeInh.style.display = 'none';
const img = td.querySelector('img.treeangle');
const afterImg = img?.nextSibling;
if (afterImg) {
afterImg.after(badge);
badge.after(badgeInh);
} else {
td.appendChild(badge);
td.appendChild(badgeInh);
}
if (isBulkEdit) {
const badgeDiff = iDoc.createElement('span');
badgeDiff.className = 'cmk-sk-diff-count';
badgeDiff.style.display = 'none';
badgeInh.after(badgeDiff);
}
updateAccordionBadge(td);
updateInheritedBadge(td, iWin);
if (isBulkEdit) updateDiffBadge(td, iWin);
tbody.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
updateAccordionBadge(td);
updateInheritedBadge(td, iWin);
if (isBulkEdit) updateDiffBadge(td, iWin);
}
});
});
form.dataset.cmkAccBadge = '1';
return true;
}
// =========================================================================
// FEATURE: Ineffective Rule Highlight
//
// In mode=edit_ruleset pages, replaces the barely visible
// "icon_hyphen.svg" (title="Ineffective rule") icon with a colored badge
// and adds a left border to the row.
// Works on both direct wato.py (no iframe) and inside index.py (iframe).
// =========================================================================
function highlightIneffectiveRules(doc) {
if (doc.body.dataset.cmkIneffHighlight === '1') return;
const imgs = doc.querySelectorAll('img.icon[title="Ineffective rule"]');
if (!imgs.length) return;
injectStyles(doc, 'cmk-sk-ineff-styles', `
tr.cmk-sk-ineffective > td:first-child {
border-left: 4px solid #e5a500 !important;
}
tr.cmk-sk-ineffective {
background: rgba(229, 165, 0, 0.08) !important;
}
.cmk-sk-ineff-badge {
display: inline-block;
background: #e5a500;
color: #000;
font-size: 10px;
font-weight: bold;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
font-family: monospace;
vertical-align: middle;
cursor: default;
letter-spacing: 0.03em;
}
`);
imgs.forEach(img => {
const row = img.closest('tr');
if (!row || row.classList.contains('cmk-sk-ineffective')) return;
row.classList.add('cmk-sk-ineffective');
const badge = doc.createElement('span');
badge.className = 'cmk-sk-ineff-badge';
badge.title = 'Ineffective rule';
badge.textContent = '⚠ ineffective';
img.replaceWith(badge);
});
doc.body.dataset.cmkIneffHighlight = '1';
}
// =========================================================================
// FEATURE: Rule Match Status Highlight
//
// In mode=edit_ruleset pages opened with host/service context (host= and
// service= parameters in the URL), highlights matching rows (green) and
// dims non-matching ones (grey), replacing the barely visible icon_checkmark
// and icon_hyphen icons with colored badges.
// =========================================================================
function highlightRuleMatchStatus(doc) {
if (doc.body.dataset.cmkMatchHighlight === '1') return;
const matchImgs = doc.querySelectorAll('img.icon[title^="This rule matches"], img.icon[title="Matches"]');
const noMatchImgs = doc.querySelectorAll('img.icon[title^="This rule does not match"]');
if (!matchImgs.length && !noMatchImgs.length) return;
injectStyles(doc, 'cmk-sk-match-styles', `
tr.cmk-sk-rule-match > td:first-child {
border-left: 4px solid #4caf50 !important;
}
tr.cmk-sk-rule-match {
background: rgba(76, 175, 80, 0.10) !important;
}
tr.cmk-sk-rule-nomatch {
opacity: 0.45;
}
tr.cmk-sk-rule-nomatch > td:first-child {
border-left: 4px solid #555 !important;
}
.cmk-sk-match-badge {
display: inline-block;
background: #4caf50;
color: #fff;
font-size: 10px;
font-weight: bold;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
font-family: monospace;
vertical-align: middle;
cursor: default;
letter-spacing: 0.03em;
}
.cmk-sk-nomatch-badge {
display: inline-block;
background: #444;
color: #888;
font-size: 10px;
font-weight: bold;
padding: 2px 6px;
border-radius: 3px;
white-space: nowrap;
font-family: monospace;
vertical-align: middle;
cursor: default;
letter-spacing: 0.03em;
}
`);
matchImgs.forEach(img => {
const row = img.closest('tr');
if (!row) return;
row.classList.add('cmk-sk-rule-match');
const badge = doc.createElement('span');
badge.className = 'cmk-sk-match-badge';
badge.title = img.title;
badge.textContent = '✓ match';
img.replaceWith(badge);
});
noMatchImgs.forEach(img => {
const row = img.closest('tr');
if (!row) return;
row.classList.add('cmk-sk-rule-nomatch');
const badge = doc.createElement('span');
badge.className = 'cmk-sk-nomatch-badge';
badge.title = img.title;
badge.textContent = '✗ no match';
img.replaceWith(badge);
});
doc.body.dataset.cmkMatchHighlight = '1';
}
// =========================================================================
// FEATURE: Ruleset Filter Toggle
//
// After the highlight functions have marked the relevant rows
// (match, no-match, ineffective), adds a bar at the top with a button
// to hide rows and folders with no relevance. Useful on pages with
// hundreds of rules where only a few are relevant.
// =========================================================================
function addRulesetFilterToggle(doc) {
if (doc.body.dataset.cmkFilterToggle === '1') return;
doc.body.dataset.cmkFilterToggle = '1';
const RELEVANT_SEL = 'tr.cmk-sk-rule-match, tr.cmk-sk-rule-nomatch, tr.cmk-sk-ineffective';
// No highlighted rows = no active search, toggle is useless
const relevantCount = doc.querySelectorAll(RELEVANT_SEL).length;
if (!relevantCount) return;
doc.querySelectorAll('tr.data').forEach(row => {
if (!row.matches(RELEVANT_SEL)) row.classList.add('cmk-sk-irrelevant-row');
});
doc.querySelectorAll('div.foldable_wrapper').forEach(wrapper => {
if (!wrapper.querySelector(RELEVANT_SEL)) wrapper.classList.add('cmk-sk-irrelevant-folder');
});
const irrelevantRows = doc.querySelectorAll('tr.cmk-sk-irrelevant-row').length;
const irrelevantFolders = doc.querySelectorAll('div.foldable_wrapper.cmk-sk-irrelevant-folder').length;
if (!irrelevantRows && !irrelevantFolders) return;
injectStyles(doc, 'cmk-sk-filter-toggle-styles', `
#cmk-sk-filter-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 10px;
margin: 6px 0 4px 0;
background: rgba(0,0,0,0.25);
border: 1px solid #3a3a3a;
border-radius: 4px;
font-size: 11px;
color: #999;
font-family: monospace;
}
#cmk-sk-filter-toggle-btn {
cursor: pointer;
padding: 3px 10px;
border-radius: 3px;
border: 1px solid #555;
background: #2a2a2a;
color: #bbb;
font-size: 11px;
font-family: monospace;
font-weight: bold;
letter-spacing: 0.03em;
}
#cmk-sk-filter-toggle-btn:hover { background: #383838; }
#cmk-sk-filter-toggle-btn.active {
background: #1c3320;
border-color: #4caf50;
color: #4caf50;
}
body.cmk-sk-filter-active tr.cmk-sk-irrelevant-row { display: none !important; }
body.cmk-sk-filter-active div.foldable_wrapper.cmk-sk-irrelevant-folder { display: none !important; }
`);
const bar = doc.createElement('div');
bar.id = 'cmk-sk-filter-bar';
const btn = doc.createElement('button');
btn.id = 'cmk-sk-filter-toggle-btn';
btn.type = 'button';
btn.textContent = 'Relevant only';
const info = doc.createElement('span');
info.textContent = `${relevantCount} relevant · ${irrelevantRows} rows and ${irrelevantFolders} folders not relevant`;
btn.addEventListener('click', () => {
const isActive = doc.body.classList.toggle('cmk-sk-filter-active');
btn.classList.toggle('active', isActive);
btn.textContent = isActive ? 'Show all' : 'Relevant only';
});
bar.appendChild(btn);
bar.appendChild(info);
const anchor = doc.querySelector('div.foldable_wrapper') || doc.querySelector('div.wato');
if (anchor) anchor.before(bar);
}
// =========================================================================
// FEATURE: Inventory Button on view.py
//
// In view.py pages (monitoring views), adds a small button next to each
// hostname in the Host column to directly open the host's Service Discovery
// page in a new tab.
// =========================================================================
function addInventoryButtons(doc) {
if (doc.body.dataset.cmkInventoryBtns === '1') return;
const hostCells = doc.querySelectorAll('table.data td.nobr');
if (!hostCells.length) return;
injectStyles(doc, 'cmk-sk-inv-btn-styles', `
.cmk-sk-btn-group {
display: inline-flex;
align-items: center;
gap: 2px;
margin-right: 4px;
vertical-align: middle;
}
.cmk-sk-inv-btn, .cmk-sk-copy-btn, .cmk-sk-copy-short-btn {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 16px !important;
height: 16px !important;
border-radius: 3px !important;
cursor: pointer !important;
opacity: 0.8;
flex-shrink: 0;
padding: 0 !important;
margin: 1px !important;
box-sizing: border-box !important;
text-decoration: none !important;
font-size: 0 !important;
line-height: 0 !important;
vertical-align: middle !important;
}
.cmk-sk-inv-btn, .cmk-sk-inv-btn:visited {
background: rgba(229,165,0,0.08) !important;
color: #e5a500 !important;
border: 1px solid #e5a500 !important;
}
.cmk-sk-inv-btn:hover { opacity: 1 !important; background: rgba(229,165,0,0.18) !important; }
.cmk-sk-copy-btn {
background: rgba(90,180,214,0.08) !important;
color: #5ab4d6 !important;
border: 1px solid #5ab4d6 !important;
}
.cmk-sk-copy-btn:hover { opacity: 1; background: rgba(90,180,214,0.18) !important; }
.cmk-sk-copy-short-btn {
background: rgba(160,120,200,0.08) !important;
color: #a078c8 !important;
border: 1px solid #a078c8 !important;
}
.cmk-sk-copy-short-btn:hover { opacity: 1; background: rgba(160,120,200,0.18) !important; }
.cmk-sk-copy-btn.copied, .cmk-sk-copy-short-btn.copied {
border-color: #4caf50 !important;
color: #4caf50 !important;
background: rgba(76,175,80,0.12) !important;
opacity: 1;
}
.cmk-sk-btn-group svg { display: block; }
`);
const CLIP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
hostCells.forEach(td => {
let hostname = null;
for (const a of td.querySelectorAll('a[href*="host="]')) {
const h = new URLSearchParams(a.getAttribute('href').split('?')[1] || '').get('host');
if (h) { hostname = h; break; }
}
if (!hostname) return;
const shortname = hostname.split('.')[0];
const group = doc.createElement('span');
group.className = 'cmk-sk-btn-group';
const btn = doc.createElement('a');
btn.className = 'cmk-sk-inv-btn';
btn.href = `wato.py?host=${encodeURIComponent(hostname)}&mode=inventory`;
btn.target = '_blank';
btn.rel = 'noopener';
btn.title = `Service Discovery: ${hostname}`;
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><polyline points="6 9 9 12 14 7" stroke-width="2"/></svg>';
const copyBtn = doc.createElement('button');
copyBtn.className = 'cmk-sk-copy-btn';
copyBtn.type = 'button';
copyBtn.title = `Copy hostname: ${hostname}`;
copyBtn.innerHTML = CLIP_SVG;
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(hostname).then(() => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 900);
});
});
const copyShortBtn = doc.createElement('button');
copyShortBtn.className = 'cmk-sk-copy-short-btn';
copyShortBtn.type = 'button';
copyShortBtn.title = `Copy short hostname: ${shortname}`;
copyShortBtn.innerHTML = CLIP_SVG;
copyShortBtn.addEventListener('click', () => {
navigator.clipboard.writeText(shortname).then(() => {
copyShortBtn.classList.add('copied');
setTimeout(() => copyShortBtn.classList.remove('copied'), 900);
});
});
group.appendChild(btn);
group.appendChild(copyBtn);
group.appendChild(copyShortBtn);
td.prepend(group);
});
doc.body.dataset.cmkInventoryBtns = '1';
}
// =========================================================================
// FEATURE: Monitor Button on wato.py (mode=folder)
//
// In WATO folder pages, adds a small button next to the hostname of each
// active (non-disabled) host to open the monitoring view in a new tab.
// The button occupies the space where the X icon appears for disabled hosts,
// maintaining column alignment.
// =========================================================================
function addWatoFolderMonitorButtons(doc) {
if (doc.body.dataset.cmkFolderMonBtns === '1') return;
const base = doc.location.pathname.replace(/[^/]*$/, '');
injectStyles(doc, 'cmk-sk-folder-mon-btn-styles', `
.cmk-sk-mon-btn, .cmk-sk-mon-btn:visited {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 20px !important;
height: 20px !important;
border-radius: 3px !important;
cursor: pointer !important;
opacity: 0.85;
flex-shrink: 0;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box !important;
text-decoration: none !important;
font-size: 0 !important;
line-height: 0 !important;
vertical-align: middle !important;
background: rgba(76,175,80,0.1) !important;
color: #4caf50 !important;
border: 1px solid #4caf50 !important;
}
.cmk-sk-mon-btn:hover { opacity: 1 !important; background: rgba(76,175,80,0.22) !important; }
.cmk-sk-mon-btn svg { display: block; }
`);
const EYE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
let active = 0, disabled = 0;
doc.querySelectorAll('table.data td').forEach(td => {
// Hostname cell: contains exactly one link with mode=edit_host whose text is the hostname
const links = td.querySelectorAll('a[href*="mode=edit_host"]');
if (links.length !== 1) return;
const link = links[0];
const hostname = new URLSearchParams((link.getAttribute('href') || '').split('?')[1] || '').get('host');
if (!hostname || link.textContent.trim() !== hostname) return;
// Skip disabled hosts (they have an img with title="This host is disabled" in the same cell)
if (td.querySelector('img[title="This host is disabled"]')) { disabled++; return; }
// Prevent duplicate insertion
if (td.querySelector('.cmk-sk-mon-btn')) return;
const viewUrl = base + 'view.py?host=' + encodeURIComponent(hostname) + '&view_name=host';
const href = base + 'index.py?start_url=' + encodeURIComponent(viewUrl);
const btn = doc.createElement('a');
btn.className = 'cmk-sk-mon-btn';
btn.href = href;
btn.target = '_blank';
btn.rel = 'noopener';
btn.title = `Monitoring: ${hostname}`;
btn.innerHTML = EYE_SVG;
td.insertBefore(btn, link);
td.insertBefore(doc.createTextNode(' '), link);
active++;
});
const h3 = doc.querySelector('h3.table');
if (h3 && !h3.querySelector('.cmk-sk-folder-summary')) {
const summary = doc.createElement('span');
summary.className = 'cmk-sk-folder-summary';
summary.style.cssText = 'float:right;font-size:12px;font-weight:normal;opacity:0.75;letter-spacing:0.02em;';
summary.innerHTML =
'<span style="color:#4caf50">&#9679; ' + active + ' monitorati</span>' +
'<span style="margin:0 6px;opacity:0.4;">|</span>' +
'<span style="color:#e05050">&#9679; ' + disabled + ' disabilitati</span>';
h3.appendChild(summary);
}
if (active > 0 || disabled > 0) doc.body.dataset.cmkFolderMonBtns = '1';
}
// =========================================================================
// FEATURE: "Configure in WATO" menu on view.py
//
// Adds a paginated <select> to the menu bar (Commands/Hosts/Export/...) of
// live monitoring views. Each option opens WATO with a group of up to 50
// hosts, pre-filtered by exact hostname via regex ~^(h1|h2|...)$.
// =========================================================================
const WATO_PAGE_SIZE = 50;
// True if the current user has WATO access (admin/configuration role).
// With sidebar: the outer document has a wato.py link in the Setup navigation.
// Without sidebar: the "pending changes" shortcut only appears for WATO admins.
function hasWatoAccess(doc) {
if (document !== doc && document.querySelector('a[href*="wato.py"]')) return true;
if (doc.querySelector('a[href*="wato.py"][href*="mode=changelog"]')) return true;
return false;
}
function addViewWatoMenu(doc) {
if (doc.body.dataset.cmkViewWatoMenu === '1') return;
doc.body.dataset.cmkViewWatoMenu = '1';
if (!hasWatoAccess(doc)) return;
try { if (!/\/view\.py/.test(doc.location.pathname)) return; } catch (e) { return; }
const menues = doc.querySelector('#page_menu_bar td.menues');
if (!menues) return;
const hosts = [...new Set(
[...doc.querySelectorAll('tr.data td.nobr a[href*="host="]')]
.map(a => { const m = a.href.match(/[?&]host=([^&]+)/); return m ? decodeURIComponent(m[1]) : null; })
.filter(Boolean)
)];
if (!hosts.length) return;
const folder = new URLSearchParams(doc.location.search).get('wato_folder') || '';
const base = doc.location.pathname.replace(/[^/]*$/, '');
const pages = Math.ceil(hosts.length / WATO_PAGE_SIZE);
const wrapper = doc.createElement('div');
wrapper.className = 'cmk-sk-wato-menu';
wrapper.style.cssText = 'display:inline-flex;align-items:center;padding:0 8px;border-left:1px solid rgba(255,255,255,0.15);';
const sel = doc.createElement('select');
sel.style.cssText = 'background:#3b3b3b;color:#ccc;border:1px solid #555;border-radius:3px;padding:2px 5px;font-size:12px;cursor:pointer;vertical-align:middle;';
const placeholder = doc.createElement('option');
placeholder.value = '';
placeholder.disabled = true;
placeholder.selected = true;
placeholder.textContent = 'WATO (' + hosts.length + ')';
sel.appendChild(placeholder);
for (let i = 0; i < pages; i++) {
const start = i * WATO_PAGE_SIZE;
const end = Math.min(start + WATO_PAGE_SIZE, hosts.length);
const opt = doc.createElement('option');
opt.value = i;
opt.textContent = 'Host ' + (start + 1) + '-' + end;
sel.appendChild(opt);
}
const btn = doc.createElement('button');
btn.textContent = 'Apri';
btn.style.cssText = 'margin-left:4px;background:#555;color:#ccc;border:1px solid #777;border-radius:3px;padding:2px 7px;font-size:12px;cursor:pointer;vertical-align:middle;';
btn.addEventListener('mouseover', () => { btn.style.background = '#6a6a6a'; });
btn.addEventListener('mouseout', () => { btn.style.background = '#555'; });
btn.addEventListener('click', function () {
const page = parseInt(sel.value);
if (isNaN(page)) return;
const slice = hosts.slice(page * WATO_PAGE_SIZE, (page + 1) * WATO_PAGE_SIZE);
const regex = '~^(' + slice.join('|') + ')$';
const url = location.origin + base + 'wato.py?mode=folder'
+ (folder ? '&folder=' + encodeURIComponent(folder) : '')
+ '&host_search=1'
+ '&host_search_host=' + encodeURIComponent(regex)
+ '&filled_in=edit_host';
window.open(url, '_blank');
sel.value = '';
});
wrapper.appendChild(sel);
wrapper.appendChild(btn);
menues.appendChild(wrapper);
}
// =========================================================================
// BOOTSTRAP: polling for each feature, activated only if the select is present
// =========================================================================
let attemptsFolder = 0;
let attemptsAcc = 0;
let attemptsRuleset = 0;
let attemptsInventory = 0;
let attemptsFolderMon = 0;
let attemptsViewWato = 0;
function tryEnhanceFolderSelect() {
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
if (!iDoc || !iDoc.body) {
if (++attemptsFolder < MAX_ATTEMPTS) setTimeout(tryEnhanceFolderSelect, POLL_INTERVAL_MS);
return;
}
const sel = iDoc.getElementById(FOLDER_SELECT_ID);
const selVF = iDoc.getElementById('wato_folder');
if (!sel && !selVF) return;
if (sel && !sel.dataset.cmkEnhanced) {
if (!sel.classList.contains('select2-hidden-accessible')) {
if (++attemptsFolder < MAX_ATTEMPTS) setTimeout(tryEnhanceFolderSelect, POLL_INTERVAL_MS);
return;
}
buildCustomSearchOverlay(iDoc, sel);
}
if (selVF && !selVF.dataset.cmkEnhanced) {
if (!selVF.classList.contains('select2-hidden-accessible')) {
if (++attemptsFolder < MAX_ATTEMPTS) setTimeout(tryEnhanceFolderSelect, POLL_INTERVAL_MS);
return;
}
buildCustomSearchOverlay(iDoc, selVF, selVF.closest('.floatfilter') || selVF.parentElement);
}
}
function tryInitAccordionCounts() {
const iDoc = getWatoDoc('form_edit_host');
if (!iDoc || !iDoc.body) {
if (++attemptsAcc < MAX_ATTEMPTS) setTimeout(tryInitAccordionCounts, POLL_INTERVAL_MS);
return;
}
// URL guard: only activates on pages with supported accordions
if (!ACCORDION_MODES.has(getPageMode(iDoc))) return;
if (!initAccordionCheckedCounts(iDoc)) {
if (++attemptsAcc < MAX_ATTEMPTS) setTimeout(tryInitAccordionCounts, POLL_INTERVAL_MS);
}
}
function tryHighlightRuleset() {
const doc = getTargetDoc();
if (!doc || !doc.body) {
if (++attemptsRuleset < MAX_ATTEMPTS) setTimeout(tryHighlightRuleset, POLL_INTERVAL_MS);
return;
}
if (getPageMode(doc) !== 'edit_ruleset') return;
highlightIneffectiveRules(doc);
highlightRuleMatchStatus(doc);
addRulesetFilterToggle(doc);
}
function tryAddInventoryButtons() {
const doc = getTargetDoc();
if (!doc || !doc.body) {
if (++attemptsInventory < MAX_ATTEMPTS) setTimeout(tryAddInventoryButtons, POLL_INTERVAL_MS);
return;
}
try { if (!/\/view\.py/.test(doc.location.pathname)) return; } catch (e) { return; }
addInventoryButtons(doc);
}
function tryAddViewWatoMenu() {
const doc = getTargetDoc();
if (!doc || !doc.body) {
if (++attemptsViewWato < MAX_ATTEMPTS) setTimeout(tryAddViewWatoMenu, POLL_INTERVAL_MS);
return;
}
try { if (!/\/view\.py/.test(doc.location.pathname)) return; } catch (e) { return; }
// Wait until the data table is present
if (!doc.querySelector('tr.data td.nobr a[href*="host="]')) {
if (++attemptsViewWato < MAX_ATTEMPTS) setTimeout(tryAddViewWatoMenu, POLL_INTERVAL_MS);
return;
}
addViewWatoMenu(doc);
}
function tryAddWatoFolderMonitorButtons() {
const doc = getTargetDoc();
if (!doc || !doc.body) {
if (++attemptsFolderMon < MAX_ATTEMPTS) setTimeout(tryAddWatoFolderMonitorButtons, POLL_INTERVAL_MS);
return;
}
try { if (!/\/wato\.py/.test(doc.location.pathname)) return; } catch (e) { return; }
if (getPageMode(doc) !== 'folder') return;
addWatoFolderMonitorButtons(doc);
}
function init() {
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
const mode = getPageMode(iDoc);
const targetDoc = getTargetDoc();
const targetMode = getPageMode(targetDoc);
attemptsFolder = 0;
attemptsAcc = 0;
attemptsRuleset = 0;
attemptsInventory = 0;
attemptsFolderMon = 0;
attemptsViewWato = 0;
// Folder select: self-stops if element not found, always schedules.
setTimeout(tryEnhanceFolderSelect, 800);
// Accordion: only on pages in ACCORDION_MODES.
if (!mode || ACCORDION_MODES.has(mode)) {
setTimeout(tryInitAccordionCounts, 800);
}
// Ruleset enhancements (ineffective + match status): only on edit_ruleset.
if (!targetMode || targetMode === 'edit_ruleset') {
setTimeout(tryHighlightRuleset, 300);
}
// Inventory button: on view.py, self-stops if not applicable.
setTimeout(tryAddInventoryButtons, 500);
// Monitor button: on wato.py mode=folder, self-stops if not applicable.
setTimeout(tryAddWatoFolderMonitorButtons, 500);
// WATO menu: on view.py with host rows, self-stops if not applicable.
setTimeout(tryAddViewWatoMenu, 800);
}
if (document.readyState === 'complete') {
init();
} else {
window.addEventListener('load', init);
}
// Detect SPA navigation (page change without full reload)
new MutationObserver(() => {
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
if (!iDoc) return;
const mode = getPageMode(iDoc);
const sel = iDoc.getElementById(FOLDER_SELECT_ID);
if (sel && sel.classList.contains('select2-hidden-accessible') && !sel.dataset.cmkEnhanced) {
attemptsFolder = 0;
setTimeout(tryEnhanceFolderSelect, 300);
}
const selVF = iDoc.getElementById('wato_folder');
if (selVF && selVF.classList.contains('select2-hidden-accessible') && !selVF.dataset.cmkEnhanced) {
attemptsFolder = 0;
setTimeout(tryEnhanceFolderSelect, 300);
}
if (ACCORDION_MODES.has(mode)) {
const form = iDoc.getElementById('form_edit_host');
if (form && !form.dataset.cmkAccBadge) {
attemptsAcc = 0;
setTimeout(tryInitAccordionCounts, 300);
}
}
if (mode === 'edit_ruleset' && iDoc.body &&
(!iDoc.body.dataset.cmkIneffHighlight || !iDoc.body.dataset.cmkMatchHighlight)) {
attemptsRuleset = 0;
setTimeout(tryHighlightRuleset, 300);
}
if (mode === 'folder' && iDoc.body && !iDoc.body.dataset.cmkFolderMonBtns) {
attemptsFolderMon = 0;
setTimeout(tryAddWatoFolderMonitorButtons, 300);
}
const tDoc = getTargetDoc();
if (tDoc && tDoc.body && !tDoc.body.dataset.cmkViewWatoMenu) {
try {
if (/\/view\.py/.test(tDoc.location.pathname) && tDoc.querySelector('tr.data td.nobr a[href*="host="]')) {
attemptsViewWato = 0;
setTimeout(tryAddViewWatoMenu, 300);
}
} catch (e) {}
}
}).observe(document.body, { childList: true, subtree: true });
// Restart on iframe load (sidebar layout)
const mainIframe = document.querySelector('iframe[name="main"], iframe#main');
if (mainIframe) {
mainIframe.addEventListener('load', init);
}
})();