KEX Page Checker

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         KEX Page Checker
// @namespace    https://knowledge.sps.amazon.dev
// @version      1.1.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.indexOf("GKZCCMW7TJCAF6KF") !== -1) {
        flag(a, "Placeholder GUID (GKZCCMW7TJCAF6KF) - not yet migrated", "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();
})();