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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GitLab Board Improvements
// @namespace    https://iqnox.com
// @version      0.13
// @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 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();
  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` : ""}`;
    pill.innerHTML = `<strong>${label}:</strong> ${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;
  }
 
  /* ------------------------------------------------------------------------
   *  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 getBoardShowTasks() {
    const value = localStorage.getItem(getBoardKey());
    // default: false (tasks hidden)
    return value === "true";
  }
 
  function setBoardShowTasks(value) {
    localStorage.setItem(getBoardKey(), value ? "true" : "false");
  }
 
  let showTasks = getBoardShowTasks();
 
  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;
      const parsed = parseItemFromUrl(link.href);
      if (parsed && parsed.type === "work_items") {
        setWorkItemVisibility(card);
      }
    });
  }
 
  /* ------------------------------------------------------------------------
   *  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;
  }
 
  /* ------------------------------------------------------------------------
   *  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;
  }
 
  /* ------------------------------------------------------------------------
   *  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
                  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,
              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 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 || parsed.type !== "issues") 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);
      }
 
      badge.innerHTML = "";
      const fragment = templateFragment(`
        <div class="iqnox--tasks-count__header">
          <span>Tasks</span>
          <span>${completed}/${total} (${pct}%)</span>
        </div>
        <div class="iqnox--tasks-count__progress">
          <div class="iqnox--tasks-count__progress-bar"></div>
        </div>
      `);
      badge.appendChild(fragment);
      const progressBar = badge.querySelector(".iqnox--tasks-count__progress-bar");
      if (progressBar) {
        progressBar.style.setProperty("width", `${pct}%`);
      }
 
      badge.dataset.state = isDone ? "done" : "pending";
      badge.title = `Show ${total} child task${total === 1 ? "" : "s"}`;
 
      if (!badge.dataset.detailListenerAttached) {
        badge.dataset.detailListenerAttached = "true";
        badge.addEventListener("click", (event) => {
          event.stopPropagation();
          event.preventDefault();
          if (!showTasks) {
            showTasks = true;
            setBoardShowTasks(true);
            updateTaskVisibility();
          }
          renderChildDetails(card, key, children, badge);
        });
      }
 
    });
  }
 
  /* ------------------------------------------------------------------------
   *  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;
 
    card.dataset.parentInjected = "pending";
 
    if (parsed.type === "work_items") {
      // Task / work item
      setWorkItemVisibility(card);
 
      buildParentChainForTask(parsed.namespace, parsed.id).then((chain) => {
        if (!Array.isArray(chain) || chain.length === 0) {
          card.dataset.parentInjected = "done";
          return;
        }
 
        const info = document.createElement("div");
        info.className = "iqnox--parent-info";
 
        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);
        }
 
        card.appendChild(info);
        card.dataset.parentInjected = "done";
      });
    } else if (parsed.type === "issues") {
      // Issues: epic + children
      Promise.all([
        fetchIssueEpic(parsed.namespace, parsed.id),
        fetchIssueChildren(parsed.namespace, parsed.id),
      ]).then(([epic]) => {
        const info = document.createElement("div");
        info.className = "iqnox--parent-info";
 
        if (epic) {
          const epicSpan = createInfoPill({
            label: "Epic",
            text: epic.title,
            variant: "epic",
          });
          info.appendChild(epicSpan);
        }
 
        if (info.childElementCount > 0) {
          card.appendChild(info);
        }
 
        card.dataset.parentInjected = "done";
        // Defer badge updates rather than applying immediately.
        scheduleTaskCountUpdate();
      });
    } else {
      card.dataset.parentInjected = "done";
    }
  }
 
  function scanBoard() {
    const cards = document.querySelectorAll(CARD_SELECTOR);
    cards.forEach((card) => processCard(card));
    // Batch update task badges.
    scheduleTaskCountUpdate();
  }
 
  /* ------------------------------------------------------------------------
   *  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));
          }
        });
      });
    });
 
    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 hasShowTasks = ul.querySelector(
          '[data-testid="show-tasks-toggle-item"]'
        );
        const showLabelsItem = ul.querySelector(
          '[data-testid="show-labels-toggle-item"]'
        );
        if (!showLabelsItem) return;
 
        if (hasShowTasks) {
          insertAdditionalDropdownItems(ul);
          return;
        }
 
        const newItem = showLabelsItem.cloneNode(true);
        newItem.setAttribute("data-testid", "show-tasks-toggle-item");
 
        const labelSpan = newItem.querySelector(".gl-toggle-label");
        if (labelSpan) {
          labelSpan.textContent = "Show tasks";
          labelSpan.id = `toggle-label-show-tasks-${Date.now()}`;
        }
 
        const toggleButton = newItem.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 current = showTasks;
        const onIcon = "check-xs";
        const offIcon = "close-xs";
 
        function applyState(state) {
          toggleButton.setAttribute("aria-checked", state ? "true" : "false");
          toggleButton.classList.toggle("is-checked", state);
          const iconName = state ? onIcon : offIcon;
          if (useEl && iconBase) {
            useEl.setAttribute("href", `${iconBase}#${iconName}`);
          }
        }
 
        applyState(current);
        if (labelSpan)
          toggleButton.setAttribute("aria-labelledby", labelSpan.id);
 
        if (!toggleButton.dataset.tasksListenerAttached) {
          toggleButton.addEventListener("click", (event) => {
            event.stopPropagation();
            const currentState =
              toggleButton.getAttribute("aria-checked") === "true";
            const newValue = !currentState;
            showTasks = newValue;
            setBoardShowTasks(newValue);
            applyState(newValue);
            updateTaskVisibility();
          });
          toggleButton.dataset.tasksListenerAttached = "true";
        }
 
        showLabelsItem.parentNode.insertBefore(
          newItem,
          showLabelsItem.nextSibling
        );
        insertAdditionalDropdownItems(ul);
      });
    } catch (err) {
      console.warn("Error inserting show tasks toggle:", err);
    }
  }
 
  function insertAdditionalDropdownItems(ul) {
    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--standup-hidden {
        display: none !important;
      }
      .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-details {
        margin: 0 12px 10px 12px;
        padding: 8px 10px;
        border-radius: 10px;
        border: 1px solid rgba(99, 102, 241, 0.35);
        background: rgba(99, 102, 241, 0.08);
        font-size: 0.75rem;
        color: var(--gl-text-color, #1f2937);
        display: flex;
        flex-direction: column;
        gap: 6px;
      }
      html.gl-dark .iqnox--child-details {
        border-color: rgba(148, 163, 184, 0.35);
        background: rgba(255, 255, 255, 0.04);
        color: var(--gl-text-color, #e3e8ff);
      }
      .iqnox--child-item {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: nowrap;
      }
      .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 {
        font-size: 0.65rem;
        text-transform: uppercase;
        font-weight: 700;
        letter-spacing: 0.05em;
        color: #0f172a;
        white-space: nowrap;
      }
      .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--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;
      }
      .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[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 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) => {
        let name = img.getAttribute("alt") || "";
 
        // Strip leading “Avatar for …”
        name = name.replace(/^Avatar for\s+/i, "").trim();
        if (!name) return;
 
        if (seenForCard.has(name)) return;
        seenForCard.add(name);
 
        if (!mapping[name]) mapping[name] = [];
        mapping[name].push(card);
 
        // Save avatar URL (only first avatar per assignee)
        if (!avatarCache[name]) {
          avatarCache[name] = img.src;
        }
      });
    });
 
    return { mapping, avatarCache };
  }
 
  /**
   * 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 renderChildDetails(card, sourceKey, children, badge) {
    if (!card || !badge) return;
    const existing = card.querySelector(".iqnox--child-details");
    if (existing && existing.dataset.source === sourceKey) {
      existing.remove();
      return;
    }
    if (existing) existing.remove();
    if (!children || children.length === 0) return;
 
    const container = document.createElement("div");
    container.className = "iqnox--child-details";
    container.dataset.source = sourceKey;
 
      container.innerHTML = children
        .map((child) => {
          const assigneeLabel =
            child.assignees?.length > 0
              ? child.assignees.map((assignee) => assignee.username).join(", ")
              : "Unassigned";
          const statusLabel = child.statusName || "";
          const avatarSrc = child.assignees?.[0]?.avatarUrl || "";
          const statusColor = child.statusColor || "#0f172a";
          return `
            <div class="iqnox--child-item">
              <span class="iqnox--child-content">
                <span class="iqnox--child-name">${child.title}</span>
                 <span class="iqnox--child-status-badge" style="--iqnox-status-color:${statusColor}">
                   <span class="iqnox--child-status-text">${statusLabel || "Status unknown"}</span>
                 </span>
              </span>
              <span class="iqnox--child-avatar" title="${assigneeLabel}">
                ${avatarSrc ? `<img src="${avatarSrc}" alt="${assigneeLabel}" />` : ""}
              </span>
          </div>
        `;
      })
      .join("");
 
    badge.insertAdjacentElement("afterend", container);
  }
 
  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 = {};
    const captureAssignees = () => {
      const result = gatherAssigneesAndCards();
      mapping = result.mapping;
      avatarCache = result.avatarCache;
      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 { 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 = user;
      }
      const text = li.querySelector(".iqnox--standup-text");
      const ring = createProgressRing(openCount, total);
      if (text) {
        li.insertBefore(ring, text);
        text.textContent = `${user} (${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.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 { open, closed } = computeCounts(cards);
 
      nameElem.textContent = `${user} — ${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, 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();
    scanBoard();
    updateTaskVisibility();
    insertShowTasksToggle();
    // Schedule task badge updates to avoid thrashing
    scheduleTaskCountUpdate();
  });
})();