KEX Page Checker

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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