GitLab Board Improvements

Always show both issues and tasks on GitLab boards, display parent information (issue/epic) for each card, add a show/hide tasks toggle and standup helper UI, and show per-issue task completion progress based on child tasks fetched on demand.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GitLab Board Improvements
// @namespace    https://iqnox.com
// @version      0.18
// @description  Always show both issues and tasks on GitLab boards, display parent information (issue/epic) for each card, add a show/hide tasks toggle and standup helper UI, and show per-issue task completion progress based on child tasks fetched on demand.
// @author       [email protected], ChatGPT
// @license      MIT
// @match        https://gitlab.com/*/-/boards*
// @run-at       document-start
// @grant        none
// ==/UserScript==
 
(() => {
  "use strict";
 
  // Card selector constant for reuse across queries.
  const CARD_SELECTOR = ".board-card, .gl-issue-board-card, li[data-id]";
  const processedCards = new WeakSet();
  const TASK_HIDDEN_CLASS = "iqnox--task-hidden";
  const STANDUP_HIDDEN_CLASS = "iqnox--standup-hidden";
  const QUICK_FILTER_HIDDEN_CLASS = "iqnox--quick-filter-hidden";
  const WORK_ITEM_PARENT_QUERY = `
    query($id: WorkItemID!) {
      workItem(id: $id) {
        widgets {
          ... on WorkItemWidgetHierarchy {
            parent {
              id
              title
            }
          }
        }
      }
    }
  `.trim();
  const ISSUE_EPIC_QUERY = `
    query($fullPath: ID!, $iid: String!) {
      project(fullPath: $fullPath) {
        issue(iid: $iid) {
          epic {
            id
            title
          }
        }
      }
    }
  `.trim();
  const WORK_ITEM_PARENT_BY_NAMESPACE_QUERY = `
    query($fullPath: ID!, $iid: String!) {
      namespace(fullPath: $fullPath) {
        workItem(iid: $iid) {
          widgets {
            ... on WorkItemWidgetHierarchy {
              parent {
                id
                title
              }
            }
          }
        }
      }
    }
  `.trim();
  const WORK_ITEM_STALE_QUERY = `
    query($fullPath: ID!, $iid: String!) {
      namespace(fullPath: $fullPath) {
        workItem(iid: $iid) {
          updatedAt
        }
      }
    }
  `.trim();
  function ensureWorkItemTasksFeature() {
    if (!window.gon?.features) {
      console.warn("GitLab Board Improvements: gon.features not found");
    }
    window.gon.features.workItemTasksOnBoards = true;
    window.gon.features.workItemsClientSideBoards = true;
  }
 
  ensureWorkItemTasksFeature();
 
  // Debounce flag for task count UI updates.
  let scheduledTaskUI = false;
  /**
   * Schedule an update to the issue task count badges on the next animation frame.
   * This prevents thrashing the DOM when many child tasks finish loading at once.
   */
  function scheduleTaskCountUpdate() {
    if (scheduledTaskUI) return;
    scheduledTaskUI = true;
    requestAnimationFrame(() => {
      updateIssueTaskCountUI();
      scheduledTaskUI = false;
    });
  }
 
  function templateFragment(html) {
    const template = document.createElement("template");
    template.innerHTML = html.trim();
    return template.content.cloneNode(true);
  }
 
  function createInfoPill({ label, text, variant } = {}) {
    const pill = document.createElement("span");
    pill.className = `iqnox--info-pill${variant ? ` iqnox--${variant}-pill` : ""}`;
    const strong = document.createElement("strong");
    strong.textContent = `${label}:`;
    pill.appendChild(strong);
    pill.appendChild(document.createTextNode(` ${text || ""}`));
    return pill;
  }
 
  function createLocateParentButton(targetTitle) {
    const button = document.createElement("button");
    button.type = "button";
    button.textContent = "Locate";
    button.title = "Highlight parent card";
    button.className = "iqnox--locate-parent-btn";
    button.addEventListener("click", (event) => {
      event.stopPropagation();
      event.preventDefault();
      const parentCard = findCardByTitle(targetTitle);
      flashCardHighlight(parentCard);
    });
    return button;
  }

  function ensureCardInfoRow(card) {
    if (!card) return null;
    let info = card.querySelector(".iqnox--parent-info");
    if (info) return info;

    info = document.createElement("div");
    info.className = "iqnox--parent-info";
    const taskCount = card.querySelector(".iqnox--tasks-count");
    if (taskCount) {
      taskCount.insertAdjacentElement("beforebegin", info);
    } else {
      card.appendChild(info);
    }
    return info;
  }

  function findCardMetaRow(card) {
    if (!card) return null;

    const boardInfoItems = card.querySelector(".board-info-items");
    if (boardInfoItems) return boardInfoItems;

    const containerSelectors = [
      '[data-testid="board-card-metadata"]',
      ".board-card-metadata",
      ".board-card-footer",
      ".gl-issue-board-card-footer",
      ".gl-issue-card-details",
      ".issuable-info",
    ];
    for (const selector of containerSelectors) {
      const match = card.querySelector(selector);
      if (match) {
        const row = match.querySelector(".gl-flex-wrap, .gl-flex") || match;
        return row;
      }
    }

    const infoButtons = card.querySelectorAll(".board-card-info");
    if (infoButtons.length > 0) {
      const row = infoButtons[0].parentElement;
      if (row && infoButtons.length > 1) return row;
    }

    const title = card.querySelector(".board-card-title, [data-testid='board-card-title']");
    const titleContainer = title?.closest("div");
    if (titleContainer?.nextElementSibling) {
      return titleContainer.nextElementSibling;
    }

    return null;
  }
 
  /* ------------------------------------------------------------------------
   *  Board types filter (always show issues + tasks)
   * ---------------------------------------------------------------------- */
 
  function enforceIssueAndTaskFilter() {
    try {
      const url = new URL(window.location.href);
      const params = url.searchParams;
      const types = params.getAll("types[]");
      const hasIssue = types.includes("ISSUE");
      const hasTask = types.includes("TASK");
      if (!hasIssue || !hasTask) {
        params.delete("types[]");
        params.append("types[]", "ISSUE");
        params.append("types[]", "TASK");
        window.location.replace(url.toString());
      }
    } catch (err) {
      console.warn("Failed to enforce issue/task filter:", err);
    }
  }
 
  /* ------------------------------------------------------------------------
   *  Per-board "show tasks" state (localStorage)
   * ---------------------------------------------------------------------- */
 
  function getBoardId() {
    const match = window.location.pathname.match(/\/boards\/(\d+)/);
    return match ? match[1] : "default";
  }
 
  function getBoardKey() {
    return `gitlab-board-show-tasks-${getBoardId()}`;
  }

  function getBoardMineFilterKey() {
    return `gitlab-board-mine-filter-${getBoardId()}`;
  }
 
  function getBoardShowTasks() {
    const value = localStorage.getItem(getBoardKey());
    // default: false (tasks hidden)
    return value === "true";
  }
 
  function setBoardShowTasks(value) {
    localStorage.setItem(getBoardKey(), value ? "true" : "false");
  }

  function getBoardMineFilter() {
    return localStorage.getItem(getBoardMineFilterKey()) === "true";
  }

  function setBoardMineFilter(value) {
    localStorage.setItem(getBoardMineFilterKey(), value ? "true" : "false");
  }
 
  let showTasks = getBoardShowTasks();
  let mineFilterActive = getBoardMineFilter();
  let mineFilterLoading = mineFilterActive;
 
  function setWorkItemVisibility(card) {
    if (!card) return;
    card.classList.toggle(TASK_HIDDEN_CLASS, !showTasks);
  }
 
  function updateTaskVisibility() {
    document.querySelectorAll(CARD_SELECTOR).forEach((card) => {
      const link = card.querySelector('a[href*="/-/"]');
      if (!link) return;
      if (getCardKind(card, link.href) === "task") {
        setWorkItemVisibility(card);
      }
    });
  }

  function normalizeIdentity(value) {
    return String(value || "")
      .replace(/^Avatar for\s+/i, "")
      .replace(/^@/, "")
      .trim()
      .toLowerCase();
  }

  function getAssigneeIdentityParts(value) {
    const raw = String(value || "").replace(/^Avatar for\s+/i, "").trim();
    if (!raw) {
      return { raw: "", normalized: "", key: "", isEmail: false };
    }

    const normalized = normalizeIdentity(raw);
    const atIndex = normalized.indexOf("@");
    const isEmail = atIndex > 0;
    return {
      raw,
      normalized,
      key: isEmail ? normalized.slice(0, atIndex) : normalized,
      isEmail,
    };
  }

  function normalizeAvatarIdentity(value) {
    const raw = String(value || "").trim();
    if (!raw) return "";
    try {
      const url = new URL(raw, window.location.origin);
      url.search = "";
      url.hash = "";
      return url.toString();
    } catch (_) {
      return raw.split("?")[0];
    }
  }

  function getAssigneeLabelScore(parts) {
    if (!parts?.raw) return 0;
    if (parts.isEmail) return 1;
    if (/\s/.test(parts.raw)) return 3;
    return 2;
  }

  function choosePreferredAssigneeLabel(currentLabel, nextParts) {
    if (!nextParts?.raw) return currentLabel || "";
    if (!currentLabel) return nextParts.raw;

    const currentParts = getAssigneeIdentityParts(currentLabel);
    return getAssigneeLabelScore(nextParts) > getAssigneeLabelScore(currentParts)
      ? nextParts.raw
      : currentLabel;
  }

  function getCurrentUserCandidates() {
    const candidates = new Set();
    [
      window.gon?.current_username,
      window.gon?.current_user_username,
      window.gon?.current_user_fullname,
      document.querySelector('meta[name="user-login"]')?.getAttribute("content"),
      document.querySelector('meta[name="user-name"]')?.getAttribute("content"),
    ]
      .map(normalizeIdentity)
      .filter(Boolean)
      .forEach((value) => candidates.add(value));
    return Array.from(candidates);
  }

  function cardMatchesMine(card) {
    const currentUser = getCurrentUserCandidates();
    if (currentUser.length === 0) return true;

    const cardAssignees = Array.from(card.querySelectorAll("img[alt]"))
      .map((img) => normalizeIdentity(img.getAttribute("alt")))
      .filter(Boolean);

    return currentUser.some((candidate) =>
      cardAssignees.some(
        (assignee) =>
          assignee === candidate ||
          assignee.includes(candidate) ||
          candidate.includes(assignee)
      )
    );
  }

  function applyQuickFilters() {
    document.querySelectorAll(CARD_SELECTOR).forEach((card) => {
      const hiddenByMine = mineFilterActive && !mineFilterLoading && !cardMatchesMine(card);
      card.classList.toggle(QUICK_FILTER_HIDDEN_CLASS, hiddenByMine);
    });
    const mineButton = document.querySelector(
      '[data-testid="iqnox-mine-filter-item"] .gl-toggle'
    );
    if (mineButton) {
      mineButton.classList.toggle("is-active", mineFilterActive);
      mineButton.setAttribute("aria-pressed", mineFilterActive ? "true" : "false");
      mineButton.setAttribute("aria-checked", mineFilterActive ? "true" : "false");
    }
  }

  function showBoardLoadingIndicator(text) {
    let indicator = document.querySelector(".iqnox--board-loading");
    if (!indicator) {
      indicator = document.createElement("div");
      indicator.className = "iqnox--board-loading";
      document.body.appendChild(indicator);
    }
    indicator.textContent = text || "Loading board…";
    indicator.hidden = false;
  }

  function hideBoardLoadingIndicator() {
    const indicator = document.querySelector(".iqnox--board-loading");
    if (indicator) indicator.hidden = true;
  }

  async function toggleMineFilter(nextValue) {
    mineFilterActive = !!nextValue;
    setBoardMineFilter(mineFilterActive);

    if (!mineFilterActive) {
      mineFilterLoading = false;
      hideBoardLoadingIndicator();
      applyQuickFilters();
      return;
    }

    mineFilterLoading = true;
    applyQuickFilters();
    showBoardLoadingIndicator("Loading full board for Mine filter…");
    try {
      await loadAllBoardColumns();
    } finally {
      mineFilterLoading = false;
      hideBoardLoadingIndicator();
      applyQuickFilters();
    }
  }

  /* ------------------------------------------------------------------------
   *  Helper: parse item type / id / namespace from card link
   * ---------------------------------------------------------------------- */
 
  function parseItemFromUrl(href) {
    try {
      const url = new URL(href);
      const segments = url.pathname.split("/").filter(Boolean);
      const dashIndex = segments.indexOf("-");
      if (dashIndex !== -1 && segments.length > dashIndex + 2) {
        const typeSegment = segments[dashIndex + 1];
        const idSegment = segments[dashIndex + 2];
        const namespaceSegments = segments.slice(0, dashIndex);
        return {
          namespace: (() => {
            const parts = [...namespaceSegments];
            if (parts[0] === "groups") parts.shift();
            return parts.join("/");
          })(),
          type: typeSegment,
          id: idSegment,
        };
      }
    } catch (_) {
      // ignore
    }
    return null;
  }

  function getCardKind(card, href) {
    const typeBadge = Array.from(card?.querySelectorAll("button, span, a") || [])
      .map((el) => el.textContent?.trim())
      .find((text) => text === "Issue" || text === "Task");
    if (typeBadge === "Issue") return "issue";
    if (typeBadge === "Task") return "task";

    const gid = card?.getAttribute("data-item-id") || "";
    if (gid.includes("/Issue/")) return "issue";
    if (gid.includes("/WorkItem/")) {
      const boardButton = card.querySelector('[data-testid="board-card-button"]');
      const ariaLabel = boardButton?.getAttribute("aria-label") || "";
      if (/^Issue number\b/i.test(ariaLabel)) return "issue";
      if (/^Task number\b/i.test(ariaLabel)) return "task";

      return "task";
    }

    const parsed = href ? parseItemFromUrl(href) : null;
    if (parsed?.type === "issues") return "issue";
    if (parsed?.type === "work_items") return "task";
    return null;
  }
 
  /* ------------------------------------------------------------------------
   *  GraphQL helpers (parents for tasks/issues, children for issues)
   * ---------------------------------------------------------------------- */
 
  async function fetchParent(globalId) {
    try {
      const response = await fetch("/api/graphql", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ query: WORK_ITEM_PARENT_QUERY, variables: { id: globalId } }),
      });
      const json = await response.json();
      const widgets = json?.data?.workItem?.widgets;
      if (Array.isArray(widgets)) {
        for (const widget of widgets) {
          if (widget && widget.parent) {
            return widget.parent;
          }
        }
      }
    } catch (err) {
      console.error("Error fetching parent for", globalId, err);
    }
    return null;
  }
 
  async function fetchIssueEpic(fullPath, iid) {
    try {
      const response = await fetch("/api/graphql", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          query: ISSUE_EPIC_QUERY,
          variables: { fullPath, iid: String(iid) },
        }),
      });
      const json = await response.json();
      return json?.data?.project?.issue?.epic || null;
    } catch (err) {
      console.error("Error fetching epic for issue", fullPath, iid, err);
      return null;
    }
  }
 
  // Tasks: parent via namespace(workItem(iid))
  async function fetchWorkItemParentByNamespace(fullPath, iid) {
    try {
      const response = await fetch("/api/graphql", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          query: WORK_ITEM_PARENT_BY_NAMESPACE_QUERY,
          variables: { fullPath, iid: String(iid) },
        }),
      });
      const json = await response.json();
      const widgets = json?.data?.namespace?.workItem?.widgets;
      if (Array.isArray(widgets)) {
        for (const widget of widgets) {
          if (widget && widget.parent) {
            return widget.parent;
          }
        }
      }
    } catch (err) {
      console.error("Error fetching parent for work item", fullPath, iid, err);
    }
    return null;
  }
 
  async function buildParentChainForTask(fullPath, iid) {
    const chain = [];
    const immediate = await fetchWorkItemParentByNamespace(fullPath, iid);
    if (!immediate) return chain;
    chain.push(immediate);
 
    // Fetch grandparent epic via global WorkItem ID of the parent
    const globalId = immediate.id;
    const grand = await fetchParent(globalId);
    if (grand) chain.push(grand);
    return chain;
  }

  const staleInfoCache = {}; // key: `${fullPath}::${iid}` -> { updatedAt, staleDays, tone }

  function getStaleTone(staleDays) {
    if (staleDays >= 14) return "stale";
    if (staleDays >= 7) return "aging";
    return "fresh";
  }

  function formatStaleLabel(staleDays) {
    return `${staleDays}d`;
  }

  function formatDateTime(value) {
    if (!value) return "";
    try {
      return new Date(value).toLocaleString();
    } catch (_) {
      return value;
    }
  }

  async function fetchWorkItemStaleInfo(fullPath, iid) {
    const key = issueKey(fullPath, iid);
    if (staleInfoCache[key]) return staleInfoCache[key];

    try {
      const response = await fetch("/api/graphql", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          query: WORK_ITEM_STALE_QUERY,
          variables: { fullPath, iid: String(iid) },
        }),
      });
      const json = await response.json();
      const updatedAt = json?.data?.namespace?.workItem?.updatedAt || "";
      if (!updatedAt) {
        staleInfoCache[key] = null;
        return null;
      }

      const ageMs = Date.now() - new Date(updatedAt).getTime();
      const staleDays = Math.max(0, Math.floor(ageMs / 86400000));
      const staleInfo = {
        updatedAt,
        staleDays,
        tone: getStaleTone(staleDays),
      };
      staleInfoCache[key] = staleInfo;
      return staleInfo;
    } catch (err) {
      console.error("Error fetching stale info for work item", fullPath, iid, err);
      staleInfoCache[key] = null;
      return null;
    }
  }

  function renderStaleBadge(card, staleInfo) {
    if (!card) return;

    let badge = card.querySelector(".iqnox--stale-badge");
    if (!staleInfo) {
      badge?.remove();
      return;
    }

    if (!badge) {
      badge = document.createElement("button");
      badge.type = "button";
      badge.className =
        "iqnox--stale-badge gl-flex gl-items-center gl-gap-2 gl-isolate gl-align-bottom board-card-info gl-text-sm gl-text-subtle !gl-cursor-help gl-bg-transparent gl-border-0 gl-p-0 focus-visible:gl-focus-inset";
      badge.innerHTML = `
        <span class="iqnox--stale-badge-dot" aria-hidden="true"></span>
        <span class="iqnox--stale-badge-text gl-truncate-component gl-min-w-0 board-card-info-text">
          <span class="gl-truncate-end"></span>
        </span>
      `;
    }

    const nativeMetaRow = findCardMetaRow(card);
    if (nativeMetaRow && badge.parentElement !== nativeMetaRow) {
      nativeMetaRow.appendChild(badge);
    } else if (!nativeMetaRow) {
      const info = ensureCardInfoRow(card);
      if (info && badge.parentElement !== info) {
        info.appendChild(badge);
      }
    }

    const text = badge.querySelector(".gl-truncate-end");
    if (text) {
      text.textContent = `Upd ${formatStaleLabel(staleInfo.staleDays)}`;
    } else {
      badge.textContent = `Upd ${formatStaleLabel(staleInfo.staleDays)}`;
    }
    badge.dataset.state = staleInfo.tone;
    badge.setAttribute(
      "aria-label",
      `Updated ${formatStaleLabel(staleInfo.staleDays)} ago`
    );
    badge.title =
      `Last updated ${formatDateTime(staleInfo.updatedAt)}\n` +
      `Shows how many days ago this card was updated.`;
  }
 
  /* ------------------------------------------------------------------------
   *  Issue children cache & progress rendering (fetch on demand)
   * ---------------------------------------------------------------------- */
 
  const issueChildrenCache = {}; // key: `${fullPath}::${iid}` -> array of children { id, title, state }
  const ISSUE_CHILDREN_QUERY = `
    query($fullPath: ID!, $iid: String!) {
      namespace(fullPath: $fullPath) {
        workItem(iid: $iid) {
          widgets {
            ... on WorkItemWidgetHierarchy {
              children(first: 100) {
                nodes {
                  id
                  iid
                  webUrl
                  title
                  state
                  widgets {
                    ... on WorkItemWidgetAssignees {
                      assignees {
                        nodes {
                          username
                          avatarUrl
                        }
                      }
                    }
                    ... on WorkItemWidgetStatus {
                      status {
                        name
                        color
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  `.trim();
 
  function issueKey(fullPath, iid) {
    return `${fullPath}::${iid}`;
  }
 
  async function fetchIssueChildren(fullPath, iid) {
    const key = issueKey(fullPath, iid);
    if (issueChildrenCache[key]) return issueChildrenCache[key];
 
    const query = ISSUE_CHILDREN_QUERY;
 
    try {
      const response = await fetch("/api/graphql", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          query,
          variables: { fullPath, iid: String(iid) },
        }),
      });
      const json = await response.json();
      const widgets = json?.data?.namespace?.workItem?.widgets || [];
      let children = [];
      for (const widget of widgets) {
        if (widget?.children?.nodes) {
          children = widget.children.nodes.map((node) => {
            const assignees = [];
            let statusName = "";
            let statusColor = "";
            (node.widgets || []).forEach((nested) => {
              if (nested?.assignees?.nodes) {
                nested.assignees.nodes.forEach((person) => {
                  const username = person?.username;
                  const avatarUrl = person?.avatarUrl;
                  if (username && !assignees.some((a) => a.username === username)) {
                    assignees.push({ username, avatarUrl });
                  }
                });
              }
              const name = nested?.status?.name;
              if (name) statusName = name;
              const color = nested?.status?.color;
              if (color) statusColor = color;
            });
            return {
              id: node.id,
              iid: node.iid,
              webUrl: node.webUrl,
              title: node.title,
              state: node.state,
              statusName,
              statusColor,
              assignees,
            };
          });
          break;
        }
      }
      issueChildrenCache[key] = children;
      // Defer updating task counts to avoid spamming the DOM.
      scheduleTaskCountUpdate();
      return children;
    } catch (err) {
      console.error("Error fetching children for issue", fullPath, iid, err);
      issueChildrenCache[key] = [];
      // Defer updating task counts to avoid spamming the DOM.
      scheduleTaskCountUpdate();
      return [];
    }
  }

  function collectChildAssignees(children) {
    const seen = new Set();
    const assignees = [];

    (children || []).forEach((child) => {
      (child.assignees || []).forEach((assignee) => {
        const username = assignee?.username;
        if (!username || seen.has(username)) return;
        seen.add(username);
        assignees.push({
          username,
          avatarUrl: assignee.avatarUrl || "",
        });
      });
    });

    return assignees;
  }

  function createChildRowElement(child, className) {
    const href = child?.webUrl || "";
    const item = document.createElement(href ? "a" : "div");
    item.className = className;
    if (href) {
      item.href = href;
      item.target = "_blank";
      item.rel = "noopener noreferrer";
      item.addEventListener("click", (event) => {
        event.stopPropagation();
      });
    }

    const content = document.createElement("span");
    content.className = "iqnox--child-content";

    const name = document.createElement("span");
    name.className = "iqnox--child-name";
    name.textContent = child?.title || "";

    const statusBadge = document.createElement("span");
    statusBadge.className = "iqnox--child-status-badge";
    statusBadge.style.setProperty(
      "--iqnox-status-color",
      child?.statusColor || "#0f172a"
    );

    const statusText = document.createElement("span");
    statusText.className = "iqnox--child-status-text";
    statusText.textContent = child?.statusName || "Status unknown";
    statusBadge.appendChild(statusText);

    content.appendChild(name);
    content.appendChild(statusBadge);

    const avatar = document.createElement("span");
    avatar.className = "iqnox--child-avatar";
    const assigneeLabel =
      child?.assignees?.length > 0
        ? child.assignees.map((assignee) => assignee.username).join(", ")
        : "Unassigned";
    avatar.title = assigneeLabel;

    const avatarSrc = child?.assignees?.[0]?.avatarUrl || "";
    if (avatarSrc) {
      const img = document.createElement("img");
      img.src = avatarSrc;
      img.alt = assigneeLabel;
      avatar.appendChild(img);
    }

    item.appendChild(content);
    item.appendChild(avatar);
    return item;
  }

  function renderTaskBadgeContent(badge, children, completed, total, pct) {
    badge.innerHTML = "";

    const header = document.createElement("div");
    header.className = "iqnox--tasks-count__header";

    const title = document.createElement("span");
    title.textContent = "Tasks";
    const summary = document.createElement("span");
    summary.textContent = `${completed}/${total} (${pct}%)`;
    header.appendChild(title);
    header.appendChild(summary);

    const progress = document.createElement("div");
    progress.className = "iqnox--tasks-count__progress";
    const progressBar = document.createElement("div");
    progressBar.className = "iqnox--tasks-count__progress-bar";
    progressBar.style.setProperty("width", `${pct}%`);
    progress.appendChild(progressBar);

    const assignees = collectChildAssignees(children);
    const avatarRow = document.createElement("div");
    avatarRow.className = "iqnox--tasks-count__avatars";
    assignees.slice(0, 4).forEach((assignee) => {
      const avatar = document.createElement("span");
      avatar.className = "iqnox--tasks-count__avatar";
      avatar.title = assignee.username;
      if (assignee.avatarUrl) {
        const img = document.createElement("img");
        img.src = assignee.avatarUrl;
        img.alt = assignee.username;
        avatar.appendChild(img);
      } else {
        avatar.textContent = assignee.username.slice(0, 1).toUpperCase();
      }
      avatarRow.appendChild(avatar);
    });
    if (assignees.length > 4) {
      const more = document.createElement("span");
      more.className = "iqnox--tasks-count__avatar-more";
      more.textContent = `+${assignees.length - 4}`;
      avatarRow.appendChild(more);
    }

    const tooltip = document.createElement("div");
    tooltip.className = "iqnox--tasks-count__tooltip";

    const tooltipHeader = document.createElement("div");
    tooltipHeader.className = "iqnox--tasks-count__tooltip-header";
    tooltipHeader.textContent = `${total} child task${total === 1 ? "" : "s"}`;
    tooltip.appendChild(tooltipHeader);

    children.slice(0, 6).forEach((child) => {
      tooltip.appendChild(createChildRowElement(child, "iqnox--tasks-count__tooltip-row"));
    });
    if (children.length > 6) {
      const more = document.createElement("div");
      more.className = "iqnox--tasks-count__tooltip-more";
      more.textContent = `+${children.length - 6} more`;
      tooltip.appendChild(more);
    }

    badge.appendChild(header);
    badge.appendChild(progress);
    if (assignees.length > 0) {
      badge.appendChild(avatarRow);
    }
    badge.appendChild(tooltip);
  }

  function updateIssueTaskCountUI() {
    const cards = document.querySelectorAll(CARD_SELECTOR);

    cards.forEach((card) => {
      const link = card.querySelector('a[href*="/-/"]');
      if (!link) return;
      const parsed = parseItemFromUrl(link.href);
      if (!parsed || getCardKind(card, link.href) !== "issue") return;

      const key = issueKey(parsed.namespace, parsed.id);
      const children = issueChildrenCache[key];
      let badge = card.querySelector(".iqnox--tasks-count");
 
      if (!children || children.length === 0) {
        if (badge) badge.remove();
        return;
      }
 
      const total = children.length;
      const completed = children.filter((c) => c.state === "CLOSED").length;
      const pct = Math.round((completed / total) * 100);
      const isDone = completed === total;
 
      if (!badge) {
        badge = document.createElement("div");
        badge.className = "iqnox--tasks-count";
        card.appendChild(badge);
      }

      renderTaskBadgeContent(badge, children, completed, total, pct);
 
      badge.dataset.state = isDone ? "done" : "pending";
      badge.title = `Show ${total} child task${total === 1 ? "" : "s"}`;

    });
  }

  /* ------------------------------------------------------------------------
   *  Card processing: parents/epics + children progress
   * ---------------------------------------------------------------------- */
 
  function processCard(card) {
    if (!card) return;
    // Avoid duplicate processing of the same card across mutation observer events
    if (processedCards.has(card)) return;
    processedCards.add(card);
    if (card.dataset.parentInjected) return;
 
    const link = card.querySelector('a[href*="/-/"]');
    if (!link) return;
    const parsed = parseItemFromUrl(link.href);
    if (!parsed || !parsed.id) return;
    const cardKind = getCardKind(card, link.href);
    if (!cardKind) {
      card.dataset.parentInjected = "done";
      return;
    }

    card.dataset.parentInjected = "pending";
    const stalePromise = fetchWorkItemStaleInfo(parsed.namespace, parsed.id).then((staleInfo) => {
      renderStaleBadge(card, staleInfo);
    });

    if (cardKind === "task") {
      // Task / work item
      setWorkItemVisibility(card);
 
      Promise.all([buildParentChainForTask(parsed.namespace, parsed.id), stalePromise])
        .then(([chain]) => {
          if (!Array.isArray(chain) || chain.length === 0) return;
          const info = ensureCardInfoRow(card);

          if (chain[0]) {
            const parentSpan = createInfoPill({
              label: "Parent",
              text: chain[0].title,
              variant: "parent",
            });
            parentSpan.appendChild(createLocateParentButton(chain[0].title));
            info.appendChild(parentSpan);
          }

          if (chain[1]) {
            const epicSpan = createInfoPill({
              label: "Epic",
              text: chain[1].title,
              variant: "epic",
            });
            info.appendChild(epicSpan);
          }
        })
        .catch((err) => {
          console.error("Error processing task card", parsed.namespace, parsed.id, err);
        })
        .finally(() => {
          card.dataset.parentInjected = "done";
          renderStaleBadge(card, staleInfoCache[issueKey(parsed.namespace, parsed.id)]);
          applyQuickFilters();
        });
    } else if (cardKind === "issue") {
      // Issues: epic + children
      Promise.all([
        fetchIssueEpic(parsed.namespace, parsed.id),
        fetchIssueChildren(parsed.namespace, parsed.id),
        stalePromise,
      ])
        .then(([epic]) => {
          if (!epic) return;
          const info = ensureCardInfoRow(card);

          const epicSpan = createInfoPill({
            label: "Epic",
            text: epic.title,
            variant: "epic",
          });
          info.appendChild(epicSpan);
        })
        .catch((err) => {
          console.error("Error processing issue card", parsed.namespace, parsed.id, err);
        })
        .finally(() => {
          card.dataset.parentInjected = "done";
          // Defer badge updates rather than applying immediately.
          scheduleTaskCountUpdate();
          renderStaleBadge(card, staleInfoCache[issueKey(parsed.namespace, parsed.id)]);
          applyQuickFilters();
        });
    } else {
      card.dataset.parentInjected = "done";
      applyQuickFilters();
    }
  }
 
  function scanBoard() {
    const cards = document.querySelectorAll(CARD_SELECTOR);
    cards.forEach((card) => processCard(card));
    // Batch update task badges.
    scheduleTaskCountUpdate();
    applyQuickFilters();
  }
 
  /* ------------------------------------------------------------------------
   *  Mutation observer (cards)
   * ---------------------------------------------------------------------- */
 
  function setupMutationObserver() {
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (!(node instanceof HTMLElement)) return;
 
          if (
            node.matches &&
            node.matches(CARD_SELECTOR)
          ) {
            processCard(node);
          } else {
            const cards = node.querySelectorAll?.(CARD_SELECTOR);
            cards?.forEach((card) => processCard(card));
          }
        });
      });
      applyQuickFilters();
    });
 
    const observeTarget = document.body || document.documentElement || document;
    observer.observe(observeTarget, { childList: true, subtree: true });
    scanBoard();
  }
 
  /* ------------------------------------------------------------------------
   *  Dropdown: Show tasks toggle + standup item
   * ---------------------------------------------------------------------- */
 
  function insertShowTasksToggle() {
    try {
      const lists = document.querySelectorAll(".gl-new-dropdown-contents");
      lists.forEach((ul) => {
        const showLabelsItem = ul.querySelector(
          '[data-testid="show-labels-toggle-item"]'
        );
        if (!showLabelsItem) return;
        insertOrUpdateToggleItem({
          ul,
          afterItem: showLabelsItem,
          templateItem: showLabelsItem,
          testId: "show-tasks-toggle-item",
          label: "Show tasks",
          isChecked: showTasks,
          listenerKey: "tasksListenerAttached",
          onToggle: (nextValue) => {
            showTasks = nextValue;
            setBoardShowTasks(nextValue);
            updateTaskVisibility();
          },
        });
        insertAdditionalDropdownItems(ul);
      });
    } catch (err) {
      console.warn("Error inserting show tasks toggle:", err);
    }
  }

  function insertOrUpdateToggleItem({
    ul,
    afterItem,
    templateItem,
    testId,
    label,
    isChecked,
    listenerKey,
    onToggle,
  }) {
    let item = ul.querySelector(`[data-testid="${testId}"]`);
    if (!item) {
      item = templateItem.cloneNode(true);
      item.setAttribute("data-testid", testId);
      afterItem.parentNode.insertBefore(item, afterItem.nextSibling);
    }

    const labelSpan = item.querySelector(".gl-toggle-label");
    if (labelSpan) {
      labelSpan.textContent = label;
      labelSpan.id = `${testId}-label`;
    }

    const toggleButton = item.querySelector(".gl-toggle");
    if (!toggleButton) return;

    const useEl = toggleButton.querySelector("use");
    let iconBase = "";
    if (useEl) {
      const href = useEl.getAttribute("href");
      const idx = href ? href.indexOf("#") : -1;
      if (idx > 0) iconBase = href.substring(0, idx);
    }

    const onIcon = "check-xs";
    const offIcon = "close-xs";
    const applyState = (state) => {
      toggleButton.setAttribute("aria-checked", state ? "true" : "false");
      toggleButton.classList.toggle("is-checked", state);
      toggleButton.classList.toggle("is-active", state);
      if (labelSpan) {
        toggleButton.setAttribute("aria-labelledby", labelSpan.id);
      }
      const iconName = state ? onIcon : offIcon;
      if (useEl && iconBase) {
        useEl.setAttribute("href", `${iconBase}#${iconName}`);
      }
    };

    applyState(isChecked);

    if (!toggleButton.dataset[listenerKey]) {
      toggleButton.addEventListener("click", (event) => {
        event.stopPropagation();
        const nextValue = toggleButton.getAttribute("aria-checked") !== "true";
        applyState(nextValue);
        onToggle(nextValue);
      });
      toggleButton.dataset[listenerKey] = "true";
    }
  }
 
  function insertAdditionalDropdownItems(ul) {
    const showTasksItem = ul.querySelector('[data-testid="show-tasks-toggle-item"]');
    const showLabelsItem = ul.querySelector('[data-testid="show-labels-toggle-item"]');
    const anchorItem = showTasksItem || showLabelsItem;
    if (anchorItem) {
      insertOrUpdateToggleItem({
        ul,
        afterItem: anchorItem,
        templateItem: showLabelsItem || showTasksItem,
        testId: "iqnox-mine-filter-item",
        label: "Mine only",
        isChecked: mineFilterActive,
        listenerKey: "mineListenerAttached",
        onToggle: (nextValue) => {
          toggleMineFilter(nextValue);
        },
      });
    }

    if (ul.querySelector('[data-testid="standup-item"]')) return;

    function createDropdownItem(label, testid, onClick) {
      const li = document.createElement("li");
      li.className = "gl-new-dropdown-item";
      li.setAttribute("tabindex", "0");
      li.setAttribute("data-testid", testid);

      const btn = document.createElement("button");
      btn.className = "gl-new-dropdown-item-content";
      btn.type = "button";
      btn.tabIndex = -1;

      const wrapper = document.createElement("span");
      wrapper.className = "gl-new-dropdown-item-text-wrapper";

      const textDiv = document.createElement("div");
      textDiv.className = "gl-new-dropdown-item-text";
      textDiv.textContent = label;

      wrapper.appendChild(textDiv);
      btn.appendChild(wrapper);
      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        onClick();
      });
      li.appendChild(btn);
      return li;
    }
 
    const standupItem = createDropdownItem(
      "Start standup",
      "standup-item",
      () => {
        startStandup();
      }
    );
 
    ul.appendChild(standupItem);
  }
 
  const dropdownObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      mutation.addedNodes.forEach((node) => {
        if (!(node instanceof HTMLElement)) return;
        if (node.matches && node.matches(".gl-new-dropdown-panel")) {
          insertShowTasksToggle();
        }
      });
    });
  });
  const dropdownTarget = document.body || document.documentElement || document;
  dropdownObserver.observe(dropdownTarget, { childList: true, subtree: true });
 
  insertShowTasksToggle();
 
  /* ------------------------------------------------------------------------
   *  Standup helper
   * ---------------------------------------------------------------------- */
 
  let standupState = null;
 
  function ensureStandupStyles() {
    if (document.getElementById("iqnox--standup-css")) return;
    const style = document.createElement("style");
    style.id = "iqnox--standup-css";
    style.textContent = `
      .iqnox--standup-overlay {
        position: fixed;
        bottom: 16px;
        right: 16px;
        width: 260px;
        max-width: 260px;
        border: 1px solid rgba(15, 23, 42, 0.08);
        border-radius: 12px;
        padding: 14px;
        box-shadow: 0 12px 24px rgba(15, 23, 42, 0.18);
        z-index: 10000;
        font-size: 0.85em;
        font-family: Inter, sans-serif;
        background: var(--gl-bg-color, #ffffff);
        color: var(--gl-text-color, #111);
      }
      html.gl-dark .iqnox--standup-overlay {
        background: var(--gl-dark-bg, #080d17);
        color: #f8fafc;
      }
      .iqnox--task-hidden {
        display: none !important;
      }
      .iqnox--quick-filter-hidden {
        display: none !important;
      }
      .iqnox--standup-hidden {
        display: none !important;
      }
      .iqnox--board-loading {
        position: fixed;
        top: 16px;
        right: 16px;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 12px;
        border-radius: 999px;
        border: 1px solid rgba(15, 23, 42, 0.1);
        background: rgba(255, 255, 255, 0.96);
        color: var(--gl-text-color, #111);
        font-size: 0.78rem;
        font-weight: 700;
        box-shadow: 0 8px 24px rgba(15, 23, 42, 0.14);
        z-index: 10001;
      }
      .iqnox--board-loading::before {
        content: "";
        width: 10px;
        height: 10px;
        border-radius: 999px;
        border: 2px solid rgba(59, 130, 246, 0.25);
        border-top-color: #2563eb;
        animation: iqnox-spin 0.8s linear infinite;
      }
      .iqnox--board-loading[hidden] {
        display: none !important;
      }
      html.gl-dark .iqnox--board-loading {
        border-color: rgba(255, 255, 255, 0.12);
        background: rgba(17, 24, 39, 0.96);
        color: #f8fafc;
      }
      @keyframes iqnox-spin {
        to {
          transform: rotate(360deg);
        }
      }
      .iqnox--standup-highlight {
        outline: 2px solid #fb923c;
        box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.45);
        background-color: rgba(251, 113, 25, 0.25) !important;
        transform: scale(1.02);
        transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
      }
      html.gl-dark .iqnox--standup-highlight {
        outline-color: #fbbf24;
        background-color: rgba(59, 130, 246, 0.24) !important;
      }
      .iqnox--parent-highlight {
        outline: 3px solid var(--gl-accent, #38bdf8);
        box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.8);
        transform: scale(1.01);
        transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
      }
      .iqnox--child-item {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: nowrap;
        color: inherit;
        text-decoration: none;
      }
      .iqnox--child-item:hover {
        text-decoration: none;
        opacity: 0.92;
      }
      .iqnox--child-content {
        flex: 1;
        min-width: 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 6px;
      }
      .iqnox--child-avatar {
        width: 24px;
        height: 24px;
        border-radius: 50%;
        overflow: hidden;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex-shrink: 0;
      }
      .iqnox--child-avatar img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      .iqnox--child-name {
        font-weight: 600;
        flex: 1;
        min-width: 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .iqnox--child-status-badge {
        display: inline-flex;
        align-items: center;
        gap: 4px;
        padding: 2px 6px;
        border-radius: 999px;
        font-size: 0.65rem;
        letter-spacing: 0.05em;
        border: 1px solid rgba(148, 163, 184, 0.35);
        background: rgba(255, 255, 255, 0.9);
        color: var(--iqnox-status-color, #0f172a);
      }
      html.gl-dark .iqnox--child-status-badge {
        border-color: rgba(255, 255, 255, 0.25);
        background: white;
      }
      .iqnox--child-status-text {
        display: inline-flex;
      }
      .iqnox--parent-info {
        font-size: 0.85em;
        margin-bottom: 8px;
        margin-left: 12px;
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
      }
      .iqnox--stale-badge {
        display: inline-flex;
        align-items: center;
        gap: 0.5rem;
        margin: 0;
        padding: 0;
        border: 0;
        background: transparent;
        color: var(--gl-text-color-subtle, var(--gl-text-color, #6b7280));
        font-size: 12px;
        font-weight: inherit;
        line-height: inherit;
        cursor: help;
        white-space: nowrap;
        font-family: inherit;
      }
      .iqnox--stale-badge-dot {
        width: 0.5rem;
        height: 0.5rem;
        border-radius: 999px;
        flex-shrink: 0;
        background: currentColor;
        opacity: 1;
      }
      .iqnox--stale-badge-text {
        display: inline-flex;
        min-width: 0;
        font-size: 12px;
        line-height: inherit;
        font-weight: inherit;
      }
      .iqnox--stale-badge[data-state="fresh"] {
        color: #047857;
      }
      .iqnox--stale-badge[data-state="aging"] {
        color: #c2410c;
      }
      .iqnox--stale-badge[data-state="stale"] {
        color: #b91c1c;
      }
      .iqnox--info-pill {
        padding: 2px 8px;
        border-radius: 999px;
        font-size: 0.75rem;
        font-weight: 500;
        display: inline-flex;
        align-items: center;
        gap: 6px;
      }
      .iqnox--parent-pill {
        background-color: var(--blue-50, #e0f2fe);
        color: var(--blue-700, #0369a1);
      }
      .iqnox--epic-pill {
        background-color: var(--purple-50, #ede9fe);
        color: var(--purple-700, #5b21b6);
      }
      .iqnox--locate-parent-btn {
        border: none;
        background: transparent;
        color: inherit;
        font-size: 0.65rem;
        cursor: pointer;
        text-decoration: underline;
        position: relative;
        z-index: 3;
        pointer-events: auto;
      }
      .iqnox--tasks-count {
        font-size: 0.75em;
        margin: 4px 12px 8px;
        padding: 4px 6px;
        border-radius: 6px;
        display: block;
        box-sizing: border-box;
        position: relative;
        z-index: 3;
        cursor: pointer;
        overflow: visible;
      }
      .iqnox--tasks-count[data-state="done"] {
        background-color: var(--green-50, #ecfdf3);
        color: var(--green-700, #047857);
      }
      .iqnox--tasks-count[data-state="pending"] {
        background-color: var(--yellow-50, #fef9c3);
        color: var(--yellow-800, #92400e);
      }
      .iqnox--tasks-count__header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 2px;
      }
      .iqnox--tasks-count__progress {
        position: relative;
        width: 100%;
        height: 4px;
        border-radius: 999px;
        background: rgba(0, 0, 0, 0.08);
        overflow: hidden;
      }
      .iqnox--tasks-count__progress-bar {
        width: 0;
        height: 100%;
        border-radius: 999px;
        transition: width 0.3s ease;
      }
      .iqnox--tasks-count__avatars {
        display: none;
        align-items: center;
        gap: 4px;
        margin-top: 6px;
      }
      .iqnox--tasks-count:hover .iqnox--tasks-count__avatars,
      .iqnox--tasks-count:focus-within .iqnox--tasks-count__avatars {
        display: flex;
      }
      .iqnox--tasks-count__avatar {
        width: 18px;
        height: 18px;
        border-radius: 999px;
        overflow: hidden;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 0.65rem;
        font-weight: 700;
        background: rgba(15, 23, 42, 0.12);
        color: inherit;
      }
      .iqnox--tasks-count__avatar img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      .iqnox--tasks-count__avatar-more {
        font-size: 0.65rem;
        font-weight: 700;
        opacity: 0.8;
      }
      .iqnox--tasks-count__tooltip {
        display: none;
        position: absolute;
        left: 0;
        top: calc(100% + 6px);
        min-width: 260px;
        max-width: 320px;
        padding: 8px;
        border-radius: 10px;
        border: 1px solid rgba(15, 23, 42, 0.12);
        background: var(--gl-bg-color, #ffffff);
        box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
        z-index: 20;
      }
      .iqnox--tasks-count:hover .iqnox--tasks-count__tooltip,
      .iqnox--tasks-count:focus-within .iqnox--tasks-count__tooltip {
        display: block;
      }
      html.gl-dark .iqnox--tasks-count__tooltip {
        background: #111827;
        border-color: rgba(255, 255, 255, 0.12);
        box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
      }
      .iqnox--tasks-count__tooltip-header {
        font-size: 0.68rem;
        font-weight: 700;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        margin-bottom: 6px;
        opacity: 0.8;
      }
      .iqnox--tasks-count__tooltip-row {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 4px 2px;
        border-radius: 6px;
        color: inherit;
        text-decoration: none;
      }
      .iqnox--tasks-count__tooltip-row:hover {
        background: rgba(15, 23, 42, 0.05);
        text-decoration: none;
      }
      html.gl-dark .iqnox--tasks-count__tooltip-row:hover {
        background: rgba(255, 255, 255, 0.06);
      }
      .iqnox--tasks-count__tooltip-more {
        margin-top: 4px;
        font-size: 0.68rem;
        opacity: 0.75;
      }
      .iqnox--tasks-count[data-state="done"] .iqnox--tasks-count__progress-bar {
        background: #22c55e;
      }
      .iqnox--tasks-count[data-state="pending"] .iqnox--tasks-count__progress-bar {
        background: #f97316;
      }
      .iqnox--standup-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 10px;
        margin-bottom: 6px;
      }
      .iqnox--standup-title-row {
        display: inline-flex;
        align-items: center;
        gap: 6px;
      }
      .iqnox--standup-title {
        font-weight: 700;
        font-size: 1rem;
      }
      .iqnox--standup-mode-toggle {
        display: inline-flex;
        align-items: center;
        gap: 1px;
        padding: 1px;
        border-radius: 999px;
        border: 1px solid rgba(148, 163, 184, 0.35);
        background: rgba(15, 23, 42, 0.04);
      }
      .iqnox--standup-mode-button {
        border: none;
        background: transparent;
        color: var(--gl-text-color, #111);
        font-size: 0.75rem;
        line-height: 1;
        padding: 3px 6px;
        border-radius: 999px;
        cursor: pointer;
        opacity: 0.7;
      }
      .iqnox--standup-mode-button.is-active {
        background: rgba(99, 102, 241, 0.18);
        opacity: 1;
      }
      html.gl-dark .iqnox--standup-mode-toggle {
        border-color: rgba(255, 255, 255, 0.2);
        background: rgba(255, 255, 255, 0.06);
      }
      html.gl-dark .iqnox--standup-mode-button.is-active {
        background: rgba(99, 102, 241, 0.3);
        color: #f8fafc;
      }
      .iqnox--standup-button-group {
        display: flex;
        align-items: center;
        gap: 6px;
      }
      .iqnox--standup-actions {
        display: flex;
        align-items: center;
        gap: 4px;
        margin-left: auto;
      }
      .iqnox--standup-icon-btn {
        width: 28px;
        height: 28px;
        padding: 0;
        border-radius: 999px;
        border: 1px solid rgba(15, 23, 42, 0.15);
        background: rgba(15, 23, 42, 0.02);
        color: var(--gl-text-color, #111);
        font-size: 0.9rem;
        line-height: 1;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        box-shadow: none;
      }
      .iqnox--standup-icon-btn:hover {
        background: rgba(15, 23, 42, 0.06);
      }
      html.gl-dark .iqnox--standup-icon-btn {
        border-color: rgba(255, 255, 255, 0.2);
        background: rgba(255, 255, 255, 0.06);
        color: #f8fafc;
      }
      html.gl-dark .iqnox--standup-icon-btn:hover {
        background: rgba(255, 255, 255, 0.12);
      }
      .iqnox--standup-current {
        font-size: 0.95rem;
        font-weight: 600;
        margin-bottom: 10px;
      }
      .iqnox--standup-loading {
        display: none;
        align-items: center;
        gap: 6px;
        font-size: 0.75rem;
        margin: 6px 0 10px;
        color: var(--gl-text-color, #6b7280);
      }
      html.gl-dark .iqnox--standup-loading {
        color: rgba(248, 250, 252, 0.7);
      }
      .iqnox--standup-list {
        list-style: none;
        margin: 0 0 10px 0;
        padding: 0;
        max-height: 300px;
        overflow-y: auto;
      }
      .iqnox--standup-assignee {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 4px 0;
        cursor: pointer;
        white-space: nowrap;
        transition: opacity 0.2s ease;
        opacity: 0.6;
      }
      .iqnox--standup-assignee.iqnox--standup-active {
        font-weight: 700;
        text-decoration: underline;
        opacity: 1;
      }
      .iqnox--standup-avatar {
        width: 22px;
        height: 22px;
        border-radius: 50%;
        flex-shrink: 0;
      }
      .iqnox--standup-nav {
        display: flex;
        gap: 8px;
        margin: 6px 0 0;
      }
      .iqnox--standup-button {
        flex: 1;
        height: 36px;
        background: #ffffff;
        color: var(--gl-text-color, #111);
        border: 1px solid rgba(15, 23, 42, 0.15);
        box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12);
        border-radius: 10px;
        font-size: 1.1rem;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: transform 0.12s ease, background 0.2s ease;
      }
      html.gl-dark .iqnox--standup-button {
        background: #111827;
        border-color: rgba(255, 255, 255, 0.1);
        color: #f8fafc;
        box-shadow: 0 6px 15px rgba(15, 23, 42, 0.4);
      }
      .iqnox--standup-button:hover {
        transform: translateY(-1px);
      }
      .iqnox--standup-button:active {
        transform: scale(0.96);
      }
      .iqnox--standup-control {
        width: 28px;
        height: 28px;
        padding: 0;
      }
      .iqnox--standup-close {
        font-size: 1rem;
      }
    `;
    document.head.appendChild(style);
  }
 
  function gatherAssigneesAndCards() {
    const mapping = {};
    const avatarCache = {}; // stores avatar URLs for each user
    const labelCache = {};
    const cards = document.querySelectorAll(CARD_SELECTOR);
 
    cards.forEach((card) => {
      if (card.classList.contains(TASK_HIDDEN_CLASS)) return;
 
      const avatars = card.querySelectorAll("img[alt]");
      const seenForCard = new Set();
 
      avatars.forEach((img) => {
        const assignee = getAssigneeIdentityParts(img.getAttribute("alt"));
        const avatarKey = normalizeAvatarIdentity(img.src);
        const key = avatarKey || assignee.key;
        if (!key) return;

        if (seenForCard.has(key)) return;
        seenForCard.add(key);

        if (!mapping[key]) mapping[key] = [];
        mapping[key].push(card);
        labelCache[key] = choosePreferredAssigneeLabel(
          labelCache[key],
          assignee
        );
 
        // Save avatar URL (only first avatar per assignee)
        if (!avatarCache[key]) {
          avatarCache[key] = img.src;
        }
      });
    });
 
    return { mapping, avatarCache, labelCache };
  }
 
  /**
   * Create an SVG progress ring to visualize the completion percentage of a user's tasks.
   * @param {number} open Number of open tasks
   * @param {number} total Total number of tasks
   * @returns {SVGElement}
   */
  function createProgressRing(open, total) {
    // percentage closed: completed = total - open
    const pct = total === 0 ? 0 : Math.round(((total - open) / total) * 100);
    const radius = 8;
    const circ = 2 * Math.PI * radius;
    const offset = circ - (pct / 100) * circ;
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("width", "20");
    svg.setAttribute("height", "20");
    const bg = document.createElementNS(svg.namespaceURI, "circle");
    bg.setAttribute("cx", "10");
    bg.setAttribute("cy", "10");
    bg.setAttribute("r", radius);
    bg.setAttribute("stroke", "var(--gl-border-color, #d1d5db)");
    bg.setAttribute("stroke-width", "3");
    bg.setAttribute("fill", "none");
    const fg = document.createElementNS(svg.namespaceURI, "circle");
    fg.setAttribute("cx", "10");
    fg.setAttribute("cy", "10");
    fg.setAttribute("r", radius);
    fg.setAttribute("stroke", "var(--green-600, #16a34a)");
    fg.setAttribute("stroke-width", "3");
    fg.setAttribute("fill", "none");
    fg.setAttribute("stroke-dasharray", circ);
    fg.setAttribute("stroke-dashoffset", offset);
    fg.setAttribute("transform", "rotate(-90 10 10)");
    svg.appendChild(bg);
    svg.appendChild(fg);
    return svg;
  }
 
  function highlightTasksForAssignee(assignee, mapping) {
    document
      .querySelectorAll(".iqnox--standup-highlight")
      .forEach((el) => el.classList.remove("iqnox--standup-highlight"));
 
    const cards = mapping[assignee] || [];
    cards.forEach((card) => {
      card.classList.add("iqnox--standup-highlight");
    });
 
    if (cards.length > 0) {
      cards[0].scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
    }
  }
 
  function flashCardHighlight(card) {
    if (!card) return;
    card.scrollIntoView({ behavior: "smooth", block: "center" });
    card.classList.add("iqnox--parent-highlight");
    setTimeout(() => card.classList.remove("iqnox--parent-highlight"), 2000);
  }
 
  function findCardByTitle(title) {
    if (!title) return null;
    const target = title.trim().toLowerCase();
    return (
      Array.from(document.querySelectorAll(".board-card-title"))
        .map((titleEl) => ({
          text: titleEl.textContent?.trim().toLowerCase(),
          card: titleEl.closest(".board-card"),
        }))
        .find((item) => item.text && item.text.includes(target))?.card || null
    );
  }
 
  /* ---- Standup order persistence ---- */
 
  function getStandupOrderKey() {
    return `iqnox--standup-order-${getBoardId()}`;
  }

  function getStandupModeKey() {
    return `iqnox--standup-mode-${getBoardId()}`;
  }

  function loadStandupMode() {
    const raw = localStorage.getItem(getStandupModeKey());
    return raw === "filter" ? "filter" : "highlight";
  }

  function saveStandupMode(mode) {
    localStorage.setItem(getStandupModeKey(), mode === "filter" ? "filter" : "highlight");
  }
 
  function saveStandupOrder(list) {
    try {
      localStorage.setItem(getStandupOrderKey(), JSON.stringify(list));
    } catch (_) {}
  }
 
  function loadStandupOrder(defaultList) {
    const raw = localStorage.getItem(getStandupOrderKey());
    if (!raw) return defaultList;
    try {
      const saved = JSON.parse(raw);
      // Filter out users no longer on board
      return saved.filter((x) => defaultList.includes(x));
    } catch (_) {
      return defaultList;
    }
  }

  function toggleFocusMode() {
    const button = document.querySelector('[data-testid="focus-mode-button"]');
    button?.click();
  }

  function getBoardLists() {
    const lists = new Set();
    document.querySelectorAll(CARD_SELECTOR).forEach((card) => {
      const list = card.closest("ul, ol");
      if (list) lists.add(list);
    });
    document.querySelectorAll('li.board-list-count[data-issue-id="-1"]').forEach((node) => {
      const list = node.closest("ul, ol");
      if (list) lists.add(list);
    });
    return Array.from(lists);
  }

  function findHorizontalScroller() {
    const candidates = [
      '[data-testid="issue-board"]',
      ".boards-app",
      ".boards-list",
      ".board-list",
      ".board",
    ];
    for (const selector of candidates) {
      const el = document.querySelector(selector);
      if (el && el.scrollWidth > el.clientWidth + 5) return el;
    }
    const all = Array.from(document.querySelectorAll("div"));
    return all.find((el) => el.scrollWidth > el.clientWidth + 20) || document.body;
  }

  async function waitForHorizontalScroller(shouldAbort) {
    let attempts = 0;
    while (attempts < 20) {
      if (shouldAbort?.()) return null;
      const scroller = findHorizontalScroller();
      if (scroller && scroller.scrollWidth > scroller.clientWidth + 5) return scroller;
      await new Promise((resolve) => setTimeout(resolve, 200));
      attempts += 1;
    }
    return findHorizontalScroller();
  }

  async function ensureColumnsRendered(shouldAbort) {
    const scroller = await waitForHorizontalScroller(shouldAbort);
    if (!scroller) return;
    const max = scroller.scrollWidth - scroller.clientWidth;
    if (max <= 0) return;

    const isWindowScroller =
      scroller === document.body || scroller === document.documentElement;
    const setScrollLeft = (value) => {
      if (isWindowScroller) {
        window.scrollTo(value, window.scrollY);
      } else {
        scroller.scrollLeft = value;
      }
    };

    const step = Math.max(240, Math.floor(scroller.clientWidth * 0.9));
    for (let x = 0; x <= max; x += step) {
      if (shouldAbort?.()) return;
      setScrollLeft(x);
      await new Promise((resolve) => setTimeout(resolve, 350));
    }
    setScrollLeft(max);
    await new Promise((resolve) => setTimeout(resolve, 500));
    setScrollLeft(0);
  }

  function findScrollContainer(element) {
    let current = element;
    while (current && current !== document.body) {
      if (current.scrollHeight > current.clientHeight + 5) return current;
      current = current.parentElement;
    }
    return null;
  }

  function isListFullyLoaded(list) {
    const countNode = list.querySelector('li.board-list-count[data-issue-id="-1"] span');
    if (!countNode) return true;
    const text = countNode.textContent?.toLowerCase() || "";
    return text.includes("showing all");
  }

  async function loadAllBoardColumns(shouldAbort) {
    await ensureColumnsRendered(shouldAbort);
    if (shouldAbort?.()) return;
    const lists = getBoardLists();
    if (lists.length === 0) return;

    const horizontalScroller = await waitForHorizontalScroller(shouldAbort);
    const isWindowScroller =
      !horizontalScroller ||
      horizontalScroller === document.body ||
      horizontalScroller === document.documentElement;

    const getColumnElement = (list) =>
      list.closest(
        '[data-testid="board-list"], .board-list, .gl-issue-board-list, .gl-board-list'
      ) || list;

    for (const list of lists) {
      if (shouldAbort?.()) return;
      const columnEl = getColumnElement(list);
      if (columnEl && columnEl.scrollIntoView) {
        columnEl.scrollIntoView({ behavior: "instant", inline: "center", block: "nearest" });
        await new Promise((resolve) => setTimeout(resolve, 250));
      } else if (horizontalScroller && !isWindowScroller && columnEl) {
        const parentRect = horizontalScroller.getBoundingClientRect();
        const colRect = columnEl.getBoundingClientRect();
        const offset = colRect.left - parentRect.left;
        horizontalScroller.scrollLeft += offset;
        await new Promise((resolve) => setTimeout(resolve, 250));
      }

      const scroller = findScrollContainer(list) || list;
      let attempts = 0;
      while (!isListFullyLoaded(list) && attempts < 60) {
        if (shouldAbort?.()) return;
        scroller.scrollTop = scroller.scrollHeight;
        await new Promise((resolve) => setTimeout(resolve, 250));
        attempts += 1;
      }
    }
  }
 
  async function startStandup() {
    if (standupState) return;

    let mapping = {};
    let avatarCache = {};
    let labelCache = {};
    const captureAssignees = () => {
      const result = gatherAssigneesAndCards();
      mapping = result.mapping;
      avatarCache = result.avatarCache;
      labelCache = result.labelCache;
      return Object.keys(mapping).filter(Boolean);
    };
    let assignees = [];
    let index = 0;

    // --- Overlay container ---
    const overlayFragment = templateFragment(`
      <div class="iqnox--standup-overlay">
        <div class="iqnox--standup-header">
          <div class="iqnox--standup-title-row">
            <div class="iqnox--standup-title">Standup</div>
            <div class="iqnox--standup-mode-toggle" data-standup-mode-toggle>
              <button class="iqnox--standup-mode-button is-active" data-standup-mode-highlight type="button" title="Highlight mode">
                ✨
              </button>
              <button class="iqnox--standup-mode-button" data-standup-mode-filter type="button" title="Filter mode">
                🔎
              </button>
            </div>
          </div>
          <div class="iqnox--standup-actions">
            <div class="iqnox--standup-button-group">
              <button class="iqnox--standup-icon-btn iqnox--standup-control"
                data-standup-refresh
                title="Refresh assignees"
                type="button"
              >⟳</button>
              <button class="iqnox--standup-icon-btn iqnox--standup-control"
                data-standup-randomize
                title="Randomize order"
                type="button"
              >⤮</button>
              <button class="iqnox--standup-icon-btn iqnox--standup-close"
                data-standup-close
                title="Close standup"
                type="button"
              >×</button>
            </div>
          </div>
        </div>
        <div class="iqnox--standup-loading" data-standup-loading>⏳ Loading all columns…</div>
        <div class="iqnox--standup-current" data-standup-current></div>
        <ul class="iqnox--standup-list" data-standup-list></ul>
        <div class="iqnox--standup-nav">
          <button class="iqnox--standup-button" data-standup-prev type="button">◀️</button>
          <button class="iqnox--standup-button" data-standup-next type="button">▶️</button>
        </div>
      </div>
    `);
    const overlay = overlayFragment.firstElementChild;
    if (!overlay) return;
    document.body.appendChild(overlay);
    standupState = { overlay, loading: true };
    toggleFocusMode();
    const nameElem = overlay.querySelector("[data-standup-current]");
    const modeHighlightBtn = overlay.querySelector("[data-standup-mode-highlight]");
    const modeFilterBtn = overlay.querySelector("[data-standup-mode-filter]");
    const assigneeList = overlay.querySelector("[data-standup-list]");
    const loadingEl = overlay.querySelector("[data-standup-loading]");
    const refreshBtn = overlay.querySelector("[data-standup-refresh]");
    const randBtn = overlay.querySelector("[data-standup-randomize]");
    const closeBtn = overlay.querySelector("[data-standup-close]");
    const prevBtn = overlay.querySelector("[data-standup-prev]");
    const nextBtn = overlay.querySelector("[data-standup-next]");
    if (
      !nameElem ||
      !modeHighlightBtn ||
      !modeFilterBtn ||
      !assigneeList ||
      !loadingEl ||
      !refreshBtn ||
      !randBtn ||
      !closeBtn ||
      !prevBtn ||
      !nextBtn
    )
      return;

    let standupMode = loadStandupMode();

    function clearStandupFilter() {
      document
        .querySelectorAll(`.${STANDUP_HIDDEN_CLASS}`)
        .forEach((el) => el.classList.remove(STANDUP_HIDDEN_CLASS));
    }

    function clearStandupHighlights() {
      document
        .querySelectorAll(".iqnox--standup-highlight")
        .forEach((el) => el.classList.remove("iqnox--standup-highlight"));
    }

    function applyStandupMode(user) {
      if (standupMode === "filter") {
        clearStandupHighlights();
        const visibleCards = new Set(mapping[user] || []);
        document.querySelectorAll(CARD_SELECTOR).forEach((card) => {
          if (visibleCards.has(card)) {
            card.classList.remove(STANDUP_HIDDEN_CLASS);
          } else {
            card.classList.add(STANDUP_HIDDEN_CLASS);
          }
        });
        return;
      }

      clearStandupFilter();
      highlightTasksForAssignee(user, mapping);
    }

    function setStandupMode(mode) {
      standupMode = mode === "filter" ? "filter" : "highlight";
      saveStandupMode(standupMode);
      modeHighlightBtn.classList.toggle("is-active", standupMode === "highlight");
      modeFilterBtn.classList.toggle("is-active", standupMode === "filter");
      const user = assignees[index];
      applyStandupMode(user);
    }

    modeHighlightBtn.addEventListener("click", () => setStandupMode("highlight"));
    modeFilterBtn.addEventListener("click", () => setStandupMode("filter"));

    closeBtn.addEventListener("click", () => {
      clearStandupFilter();
      clearStandupHighlights();
      overlay.remove();
      standupState = null;
    });

    loadingEl.style.display = "flex";
    await loadAllBoardColumns(() => !standupState || standupState.overlay !== overlay);
    if (!standupState || standupState.overlay !== overlay) return;
    loadingEl.style.display = "none";

    assignees = captureAssignees();
    if (assignees.length === 0) {
      alert("No assignees found on this board.");
      overlay.remove();
      standupState = null;
      return;
    }

    // Load saved order, or randomize & save if none
    const hadSavedOrder = !!localStorage.getItem(getStandupOrderKey());
    assignees = loadStandupOrder(assignees);
    if (!hadSavedOrder) {
      assignees = assignees.sort(() => Math.random() - 0.5);
      saveStandupOrder(assignees);
    }
 
    // Build list items with avatar, progress ring and counts
    function addAssigneeListItem(user) {
      const cards = mapping[user] || [];
      const displayName = labelCache[user] || user;
      const { open: openCount, closed: closedCount } = computeCounts(cards);
      const total = openCount + closedCount;
      const fragment = templateFragment(`
        <li class="iqnox--standup-assignee">
          <img class="iqnox--standup-avatar" />
          <span class="iqnox--standup-text"></span>
        </li>
      `);
      const li = fragment.querySelector("li");
      if (!li) return;
      li.dataset.assignee = user;
      const avatar = li.querySelector("img");
      if (avatar) {
        avatar.src = avatarCache[user] || "";
        avatar.alt = displayName;
      }
      const text = li.querySelector(".iqnox--standup-text");
      const ring = createProgressRing(openCount, total);
      if (text) {
        li.insertBefore(ring, text);
        text.textContent = `${displayName} (${openCount}/${total})`;
      } else {
        li.appendChild(ring);
      }
      li.addEventListener("click", () => {
        const idx = assignees.indexOf(user);
        if (idx !== -1) {
          index = idx;
          showCurrent();
        }
      });
      assigneeList.appendChild(li);
    }
 
    assignees.forEach(addAssigneeListItem);
 
    function rebuildAssigneeList() {
      assigneeList.innerHTML = "";
      assignees.forEach(addAssigneeListItem);
    }
 
    function reorderAssignees({ randomize } = {}) {
      const refreshedAssignees = captureAssignees();
      if (refreshedAssignees.length === 0) {
        alert("No assignees found on this board.");
        return false;
      }
 
      if (randomize) {
        assignees = refreshedAssignees.sort(() => Math.random() - 0.5);
        localStorage.removeItem(getStandupOrderKey());
      } else {
        const savedOrder = loadStandupOrder(refreshedAssignees);
        const missing = refreshedAssignees.filter(
          (name) => !savedOrder.includes(name)
        );
        assignees = [...savedOrder, ...missing];
      }
 
      saveStandupOrder(assignees);
      rebuildAssigneeList();
      index = 0;
      showCurrent();
 
      if (standupState) {
        standupState.mapping = mapping;
        standupState.avatarCache = avatarCache;
        standupState.labelCache = labelCache;
        standupState.assignees = assignees;
        standupState.index = index;
      }
      return true;
    }
 
    // Highlight current user in list
    function updateListHighlight(user) {
      Array.from(assigneeList.children).forEach((li) => {
        li.classList.toggle("iqnox--standup-active", li.dataset.assignee === user);
      });
    }
 
    // Compute open/closed tasks using the isCardClosed helper. Cards may not carry a data-board-type attribute, so rely on our helper.
    function computeCounts(cards) {
      let open = 0;
      let closed = 0;
      cards.forEach((card) => {
        const closedFlag = card.parentElement?.getAttribute("data-board-type") === "closed";
        if (closedFlag) {
          closed++;
        } else {
          open++;
        }
      });
      return { open, closed };
    }
 
    // Update the UI for the currently selected user
    function showCurrent() {
      const user = assignees[index];
      const cards = mapping[user];
      const displayName = labelCache[user] || user;
      const { open, closed } = computeCounts(cards);
 
      nameElem.textContent = `${displayName} — ${open} open / ${open + closed} total`;

      updateListHighlight(user);
      applyStandupMode(user);
    }
 
    // Button actions
    prevBtn.onclick = () => {
      index = (index - 1 + assignees.length) % assignees.length;
      showCurrent();
    };
    nextBtn.onclick = () => {
      index = (index + 1) % assignees.length;
      showCurrent();
    };
 
    // Randomize order
    randBtn.onclick = () => reorderAssignees({ randomize: true });
 
    refreshBtn.onclick = () => reorderAssignees();
 
 
    standupState = { mapping, avatarCache, labelCache, assignees, index, overlay };
    setStandupMode(standupMode);
    showCurrent();
  }
 
  /* ------------------------------------------------------------------------
   *  Entry point
   * ---------------------------------------------------------------------- */
 
  function runWhenDomReady(fn) {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", fn, { once: true });
    } else {
      fn();
    }
  }
 
  runWhenDomReady(() => {
    enforceIssueAndTaskFilter();
    setupMutationObserver();
    ensureStandupStyles();
    if (mineFilterActive) {
      toggleMineFilter(true);
    }
  });
})();