KEX Page Checker

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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