// ==UserScript== // @name Checkmk SwissKnife // @namespace https://luigidacunto.com/ // @version 2.13.2 // @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 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:#444;color:#ddd;border:1px solid #888;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 = 'Select hosts (' + 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_STYLE_OFF = 'margin-left:4px;background:#555;color:#999;border:1px solid #777;border-radius:3px;padding:2px 7px;font-size:12px;cursor:not-allowed;vertical-align:middle;'; const BTN_STYLE_ON = 'margin-left:4px;background:#1a73e8;color:#fff;border:1px solid #1a73e8;border-radius:3px;padding:2px 7px;font-size:12px;cursor:pointer;vertical-align:middle;'; const btn = doc.createElement('button'); btn.textContent = 'Apri'; btn.disabled = true; btn.style.cssText = BTN_STYLE_OFF; sel.addEventListener('change', function () { const valid = sel.value !== ''; btn.disabled = !valid; btn.style.cssText = valid ? BTN_STYLE_ON : BTN_STYLE_OFF; }); 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 = ''; btn.disabled = true; btn.style.cssText = BTN_STYLE_OFF; }); 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); } })();