KEX Page Checker

Scans KEX SOP pages for post-publication issues (broken links, missing images, leftover Watson refs, encoding problems)

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         KEX Page Checker
// @namespace    https://knowledge.sps.amazon.dev
// @version      1.0.0
// @description  Scans KEX SOP pages for post-publication issues (broken links, missing images, leftover Watson refs, encoding problems)
// @match        https://knowledge.sps.amazon.dev/k?id=*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  // Wait for page content to load
  function waitForContent() {
    var check = function () {
      if (document.querySelector(".WatsonSOPBody") || document.querySelector("h1") || document.querySelector("h2")) {
        setTimeout(createUI, 1500); // extra delay for images to load
      } else {
        setTimeout(check, 1000);
      }
    };
    check();
  }

  function createUI() {
    // Create floating button
    var btn = document.createElement("button");
    btn.id = "kex-checker-btn";
    btn.textContent = "Check Page";
    btn.style.cssText = "position:fixed;bottom:20px;right:20px;z-index:999998;background:#232F3E;color:#FF6200;border:none;border-radius:8px;padding:10px 18px;font-size:14px;font-weight:bold;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,0.3);font-family:Arial,sans-serif;";
    btn.addEventListener("click", runChecker);
    document.body.appendChild(btn);
  }

  function runChecker() {
    var results = [];
    var highlights = [];

    // Clean up previous run
    var oldStyle = document.getElementById("kex-checker-style");
    if (oldStyle) oldStyle.remove();
    var oldPanel = document.getElementById("kex-checker-panel");
    if (oldPanel) oldPanel.remove();
    document.querySelectorAll(".kex-check-highlight").forEach(function (el) {
      el.classList.remove("kex-check-highlight");
      el.removeAttribute("data-kex-issue");
    });

    // Inject styles
    var style = document.createElement("style");
    style.id = "kex-checker-style";
    style.textContent = [
      ".kex-check-highlight{outline:3px solid red !important;outline-offset:2px;position:relative;}",
      ".kex-check-highlight::after{content:attr(data-kex-issue);position:absolute;top:-20px;left:0;background:red;color:#fff;font-size:11px;padding:2px 6px;border-radius:3px;white-space:nowrap;z-index:99999;font-family:Arial,sans-serif;}",
      "#kex-checker-panel{position:fixed;top:10px;right:10px;width:440px;max-height:80vh;overflow-y:auto;background:#fff;border:2px solid #232F3E;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.3);z-index:999999;font-family:Arial,sans-serif;font-size:13px;}",
      "#kex-checker-panel .chk-header{background:#232F3E;color:#fff;padding:12px 16px;border-radius:8px 8px 0 0;display:flex;justify-content:space-between;align-items:center;}",
      "#kex-checker-panel .chk-header h3{margin:0;font-size:15px;color:#FF6200;}",
      "#kex-checker-panel .close-btn{cursor:pointer;font-size:18px;color:#fff;font-weight:bold;}",
      "#kex-checker-panel .chk-body{padding:12px 16px;}",
      "#kex-checker-panel .section{margin-bottom:12px;}",
      "#kex-checker-panel .section h4{margin:0 0 6px;font-size:13px;color:#232F3E;}",
      "#kex-checker-panel .pass{color:#27ae60;font-weight:bold;}",
      "#kex-checker-panel .fail{color:#c0392b;font-weight:bold;}",
      "#kex-checker-panel .issue{padding:4px 0;border-bottom:1px solid #f0f0f0;cursor:pointer;font-size:12px;}",
      "#kex-checker-panel .issue:hover{background:#f8f8f8;}",
      "#kex-checker-panel .summary{padding:10px;background:#f8f9fa;border-radius:6px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;}",
      "#kex-checker-panel .export-btn{padding:6px 14px;background:#232F3E;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:bold;cursor:pointer;font-family:Arial,sans-serif;}",
      "#kex-checker-panel .export-btn:hover{background:#37475a;}"
    ].join("");
    document.head.appendChild(style);

    function flag(el, msg, cat) {
      if (el && el.classList) {
        el.classList.add("kex-check-highlight");
        el.setAttribute("data-kex-issue", msg);
      }
      highlights.push(el);
      results.push({
        cat: cat,
        msg: msg,
        el: el,
        detail: el ? (el.getAttribute("href") || el.getAttribute("src") || el.tagName) : "n/a"
      });
    }

    // ── LINKS ──
    var links = document.querySelectorAll("a[href]");
    links.forEach(function (a) {
      var href = a.getAttribute("href") || "";
      var text = (a.textContent || "").trim();

      if (href.indexOf("share.amazon.com/sites/amazonwatson") !== -1) {
        flag(a, "Watson link still present", "links");
      }
      if (href.indexOf("REPLACE_WITH_GUID") !== -1) {
        flag(a, "GUID placeholder not replaced", "links");
      }
      if (href.match(/[?&]id=(#|$|&)/) || href.match(/[?&]id=$/)) {
        flag(a, "Paragon link with empty GUID", "links");
      }
      if (!text && !a.querySelector("img")) {
        flag(a, "Empty link (no text or image)", "links");
      }
      if (href === "about:blank" || href === "javascript:void(0)" || href === "javascript:void") {
        flag(a, "Dead link (about:blank/void)", "links");
      }
      if (href.startsWith("#") && href.length > 1) {
        var anchor = href.substring(1);
        if (anchor.startsWith("help-page-header-")) return;
        if (!document.getElementById(anchor) && !document.querySelector('[name="' + CSS.escape(anchor) + '"]')) {
          flag(a, "Broken anchor: " + href, "links");
        }
      }
    });

    // ── IMAGES ──
    var imgs = document.querySelectorAll("img");
    imgs.forEach(function (img) {
      if (img.complete && img.naturalWidth === 0 && img.src && img.src.indexOf("data:") !== 0) {
        flag(img, "Broken image (failed to load)", "images");
      }
      var src = img.getAttribute("src") || "";
      if (src.indexOf("Resources/Images/") !== -1 || src.indexOf("Resources\\Images\\") !== -1) {
        flag(img, "Local image path (not Media Central)", "images");
      }
      if (!img.hasAttribute("alt")) {
        flag(img, "Missing alt attribute", "images");
      }
    });

    // ── CSS / JS ──
    var inlineStyles = document.querySelectorAll("style");
    var leftoverLinks = document.querySelectorAll('link[rel="stylesheet"]');

    if (inlineStyles.length > 0) {
      results.push({ cat: "css_js", msg: inlineStyles.length + ' inline <style> block(s) found (old embed approach?)', el: null, detail: "" });
    }
    if (leftoverLinks.length > 0) {
      leftoverLinks.forEach(function (l) {
        flag(l, "Leftover <link stylesheet> tag", "css_js");
      });
    }

    // ── CONTENT QUALITY ──
    var allText = document.body.innerText || "";
    var mojibakePatterns = [/Ã[\x80-\xBF]/g, /Â[\x80-\xBF]/g, /â€[^\s]/g, /é/g, /ö/g, /ü/g];
    var hasMojibake = false;
    mojibakePatterns.forEach(function (p) { if (p.test(allText)) hasMojibake = true; });
    if (hasMojibake) {
      results.push({ cat: "content", msg: "Possible encoding issues (mojibake characters detected)", el: null, detail: "" });
    }

    var watsonSource = document.getElementById("WatsonSOPSource");
    if (watsonSource && watsonSource.offsetHeight > 0) {
      flag(watsonSource, "WatsonSOPSource element visible", "content");
    }

    var siteBanner = document.getElementById("SiteMovedBanner_MessageBar_Site");
    if (siteBanner && siteBanner.offsetHeight > 0) {
      flag(siteBanner, "SiteMovedBanner still visible", "content");
    }

    var madcapConds = document.querySelectorAll("[MadCap\\:conditions]");
    if (madcapConds.length > 0) {
      madcapConds.forEach(function (el) {
        flag(el, "MadCap:conditions attribute in rendered HTML", "content");
      });
    }

    var allIds = document.querySelectorAll("[id]");
    var idMap = {};
    allIds.forEach(function (el) {
      var id = el.id;
      if (id) {
        if (idMap[id]) { flag(el, "Duplicate ID: " + id, "content"); }
        else { idMap[id] = true; }
      }
    });

    var emptyCollapsibles = document.querySelectorAll(".collapsible");
    emptyCollapsibles.forEach(function (btn) {
      var next = btn.nextElementSibling;
      if (next && next.classList.contains("collapsed") && (next.textContent || "").trim().length === 0) {
        flag(btn, "Empty collapsible section", "content");
      }
    });

    // ── LAYOUT ──
    var bodyWidth = document.body.scrollWidth;
    var viewWidth = window.innerWidth;
    if (bodyWidth > viewWidth + 5) {
      results.push({ cat: "layout", msg: "Content wider than viewport (horizontal scroll: " + bodyWidth + "px vs " + viewWidth + "px)", el: null, detail: "" });
    }
    imgs.forEach(function (img) {
      if (img.naturalWidth > 0 && img.offsetWidth > img.parentElement.offsetWidth + 10) {
        flag(img, "Image exceeds container width", "layout");
      }
    });

    // ── BUILD PANEL ──
    var cats = { links: "Links", images: "Images", css_js: "CSS / JS", content: "Content Quality", layout: "Layout" };
    var catResults = {};
    results.forEach(function (r) {
      if (!catResults[r.cat]) catResults[r.cat] = [];
      catResults[r.cat].push(r);
    });
    var totalIssues = results.length;

    var panel = document.createElement("div");
    panel.id = "kex-checker-panel";

    var html = '<div class="chk-header"><h3>KEX Page Checker</h3><span class="close-btn" id="kex-checker-close">X</span></div>';
    html += '<div class="chk-body">';
    html += '<div class="summary">';
    if (totalIssues === 0) {
      html += '<span class="pass">All checks passed! No issues found.</span>';
    } else {
      html += '<span class="fail">' + totalIssues + ' issue' + (totalIssues > 1 ? 's' : '') + ' found</span>';
    }
    html += '</div>';
    html += '<button class="export-btn" id="kex-export-csv">Export Issues as CSV</button>';

    Object.keys(cats).forEach(function (catKey) {
      var catName = cats[catKey];
      var items = catResults[catKey] || [];
      html += '<div class="section"><h4>' + catName + ' ';
      if (items.length === 0) {
        html += '<span class="pass">PASS</span>';
      } else {
        html += '<span class="fail">(' + items.length + ')</span>';
      }
      html += '</h4>';
      items.forEach(function (item, idx) {
        var elIdx = highlights.indexOf(item.el);
        html += '<div class="issue" data-highlight-idx="' + elIdx + '">' + item.msg + '</div>';
      });
      html += '</div>';
    });

    html += '</div>';

    panel.innerHTML = html;
    document.body.appendChild(panel);

    // ── Event: close panel ──
    document.getElementById("kex-checker-close").addEventListener("click", function () {
      document.getElementById("kex-checker-panel").remove();
      document.querySelectorAll(".kex-check-highlight").forEach(function (e) {
        e.classList.remove("kex-check-highlight");
        e.removeAttribute("data-kex-issue");
      });
    });

    // ── Event: click issue to scroll ──
    panel.querySelectorAll(".issue").forEach(function (issueEl) {
      issueEl.addEventListener("click", function () {
        var idx = parseInt(this.getAttribute("data-highlight-idx"));
        if (highlights[idx]) {
          highlights[idx].scrollIntoView({ behavior: "smooth", block: "center" });
        }
      });
    });

    // ── Event: export CSV ──
    document.getElementById("kex-export-csv").addEventListener("click", function () {
      var csv = "Category,Issue,Detail,Page URL\n";
      results.forEach(function (r) {
        var cat = cats[r.cat] || r.cat;
        var msg = r.msg.replace(/"/g, '""');
        var detail = (r.detail || "").replace(/"/g, '""');
        csv += '"' + cat + '","' + msg + '","' + detail + '","' + window.location.href + '"\n';
      });
      var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
      var url = URL.createObjectURL(blob);
      var a = document.createElement("a");
      a.href = url;
      a.download = "kex_page_check_" + document.title.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 40) + ".csv";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    });
  }

  waitForContent();
})();