Repository: go-gitea/gitea
File: web_src/js/components/RepoActionView.vue (Vue)

Code before:

  watch: {
    optionAlwaysAutoScroll() {
      this.saveLocaleStorageOptions();
    },
    optionAlwaysExpandRunning() {
      this.saveLocaleStorageOptions();
    },
  },

  async mounted() {
    // load job data and then auto-reload periodically
    // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
    await this.loadJob();
    this.intervalID = setInterval(() => this.loadJob(), 1000);
    document.body.addEventListener('click', this.closeDropdown);
    this.hashChangeListener();
    window.addEventListener('hashchange', this.hashChangeListener);

    // === Auto Expand + Auto Scroll Fix (for Issue #35570) ===
    // Ensure Vue has updated DOM for steps after initial load
    await nextTick();

    // Set up observer on the steps container (safer and more efficient than document.body)
    const stepsContainer = (this.$refs.steps as HTMLElement);
    if (stepsContainer && typeof MutationObserver !== 'undefined') {
      this.mutationObserver = new MutationObserver((mutations) => {
        for (const m of mutations) {
          // Auto-scroll new log lines as they appear
          if (m.type === 'childList') {
            for (const n of m.addedNodes) {
              if (n.nodeType === 1 && (n as Element).classList.contains('job-log-line')) {
                if (this.optionAlwaysAutoScroll) {
                  try { (n as Element).scrollIntoView({ behavior: 'smooth', block: 'end' }); }
                  catch { (n as Element).scrollIntoView(); }
                }
              }
            }
          }

          // Auto-expand running steps when their class changes
          if (m.type === 'attributes' && m.attributeName === 'class') {
            const t = m.target as Element;
            if (t.classList && t.classList.contains('job-step-summary')) {
              const stepAttr = t.getAttribute('data-step');
              if (!stepAttr) continue;
              const stepIndex = Number(stepAttr);
              // If expand-running option is on and step is expandable but not selected, open it via state
              if (this.optionAlwaysExpandRunning &&
                  t.classList.contains('step-expandable') &&
                  !t.classList.contains('selected')) {

Review comment (suggestion):
```suggestion
    if (stepsContainer && 'MutationObserver' in window) {
```

matches other `MutationObserver` usages.

Code after:
          //   status: '',
          // }
        ] as Array<Step>,
      },
    };
  },

  watch: {
    optionAlwaysAutoScroll() {
      this.saveLocaleStorageOptions();
    },
    optionAlwaysExpandRunning() {
      this.saveLocaleStorageOptions();
    },
  },

  async mounted() {
    // load job data and then auto-reload periodically
    // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener
    await this.loadJob();

    // auto-scroll to the bottom of the log group when it is opened
    // "toggle" event doesn't bubble, so we need to use 'click' event delegation to handle it
    addDelegatedEventListener(this.elStepsContainer(), 'click', 'summary.job-log-group-summary', (el, _) => {
      if (!this.optionAlwaysAutoScroll) return;
      const elJobLogGroup = el.closest('details.job-log-group') as HTMLDetailsElement;
      setTimeout(() => {
        if (elJobLogGroup.open && !isLogElementInViewport(elJobLogGroup)) {
          elJobLogGroup.scrollIntoView({behavior: 'smooth', block: 'end'});
        }
      }, 0);
    });

    this.intervalID = setInterval(() => this.loadJob(), 1000);
    document.body.addEventListener('click', this.closeDropdown);
    this.hashChangeListener();
    window.addEventListener('hashchange', this.hashChangeListener);
  },

  beforeUnmount() {
    document.body.removeEventListener('click', this.closeDropdown);
    window.removeEventListener('hashchange', this.hashChangeListener);
  },

  unmounted() {
    // clear the interval timer when the component is unmounted
    // even our page is rendered once, not spa style
    if (this.intervalID) {
      clearInterval(this.intervalID);
      this.intervalID = null;
    }