GitHub Actions: Timestamps NZ Time AM/PM Toggle (Top-Center)

Show GitHub Actions timestamps in NZ time (AM/PM) with seconds, top-center toggle, live updates, SPA friendly

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GitHub Actions: Timestamps NZ Time AM/PM Toggle (Top-Center)
// @namespace    https://github.com/chaoscreater
// @version      4.0
// @description  Show GitHub Actions timestamps in NZ time (AM/PM) with seconds, top-center toggle, live updates, SPA friendly
// @author       chaoscreater
// @match        https://github.com/*/actions/runs/*/job/*
// @match        https://github.*.co.nz/*/*/actions/runs/*/job/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'gh_actions_auto_timestamps_enabled';

    // --- Helpers ---
    const isEnabled = () => localStorage.getItem(STORAGE_KEY) !== 'false'; // default: true
    const setEnabled = (v) => localStorage.setItem(STORAGE_KEY, v ? 'true' : 'false');

    // Check if current URL matches @match patterns
    const isValidPage = () => {
      const url = location.href;
      // Match: https://github.com/*/actions/runs/*/job/*
      // Match: https://github.*.co.nz/*/*/actions/runs/*/job/*
      const patterns = [
        /^https:\/\/github\.com\/[^/]+\/[^/]+\/actions\/runs\/[^/]+\/job\/.+/,
        /^https:\/\/github\.[^/]+\.co\.nz\/[^/]+\/[^/]+\/actions\/runs\/[^/]+\/job\/.+/
      ];
      return patterns.some(pattern => pattern.test(url));
    };

    // --- UI: Add Toggle Button ---
    function addToggleButton() {
      if (document.getElementById('gh-ts-toggle')) return;
      if (!isValidPage()) return;

      const btn = document.createElement('button');
      btn.id = 'gh-ts-toggle';
      btn.textContent = isEnabled() ? '⏱️ Timestamps: ON' : '⏹️ Timestamps: OFF';
      btn.style.cssText = `
        position: fixed;
        top: 10px;
        left: 50%;
        transform: translateX(-50%);
        z-index: 99999;
        background: ${isEnabled() ? '#2ea043' : '#6e7681'};
        color: white;
        border: none;
        border-radius: 6px;
        padding: 4px 12px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        opacity: 0.9;
        box-shadow: 0 2px 5px rgba(0,0,0,0.2);
      `;
      btn.title = 'Toggle auto "Show timestamps"';
      btn.onclick = () => {
        const nowEnabled = !isEnabled();
        setEnabled(nowEnabled);
        btn.textContent = nowEnabled ? '⏱️ Timestamps: ON' : '⏹️ Timestamps: OFF';
        btn.style.background = nowEnabled ? '#2ea043' : '#6e7681';
        updateTimestampsLive(nowEnabled);
      };
      document.body.appendChild(btn);
    }

    // --- Convert GMT string to NZ time with AM/PM, preserve date ---
    function convertToNZTime(gmtString) {
      const date = new Date(gmtString);
      if (isNaN(date)) return gmtString;

      // Options for NZ time conversion
      const options = {
        timeZone: 'Pacific/Auckland',
        hour12: true,
        weekday: 'short',
        year: 'numeric',
        month: 'short',
        day: '2-digit',
        hour: 'numeric',
        minute: '2-digit',
        second: '2-digit',
      };

      const nzStr = date.toLocaleString('en-NZ', options);
      // Example: "Tue, 21 Oct 2025, 6:34:29 am"

      const match = nzStr.match(/^(\w{3}, \d{2} \w{3} \d{4}), (\d{1,2}):(\d{2}):(\d{2}) (\w{2})$/);
      if (!match) return gmtString;

      const [ , dayStr, hour, minute, second, ampm ] = match;

      return `${dayStr} ${hour}:${minute}${ampm.toLowerCase()} ${second}s NZDT`;
    }

    // --- Update all timestamp elements ---
    function updateAllTimestamps() {
      const tsElements = document.querySelectorAll('.CheckStep-line-timestamp');
      tsElements.forEach(el => {
        const raw = el.textContent.trim();
        const nz = convertToNZTime(raw);
        el.textContent = nz;
      });
    }

    // --- Enable/disable timestamps live ---
    function updateTimestampsLive(enable) {
      const inputs = Array.from(document.querySelectorAll('input.js-checks-log-timestamps'));
      if (!inputs.length) return;

      for (const inp of inputs) {
        try {
          const label = inp.closest('label');
          if (!label) continue;

          if (enable && !inp.checked) label.click();
          else if (!enable && inp.checked) label.click();
        } catch (err) {
          console.error('[gh-ts] failed updating timestamp live', err);
        }
      }

      if (enable) updateAllTimestamps();
    }

    // --- Watch for DOM changes ---
    const mo = new MutationObserver(() => {
      if (isValidPage() && isEnabled()) updateTimestampsLive(true);
    });

    function startObserving() {
      try {
        mo.observe(document.documentElement, { childList: true, subtree: true });
      } catch (e) { console.error('[gh-ts] MO start failed', e); }
    }

    // --- SPA navigation hook ---
    (function observeLocationChanges() {
      const _push = history.pushState;
      const _replace = history.replaceState;
      const emit = () => window.dispatchEvent(new Event('locationchange'));
      history.pushState = function () { _push.apply(this, arguments); emit(); };
      history.replaceState = function () { _replace.apply(this, arguments); emit(); };
      window.addEventListener('popstate', emit);
    })();

    let lastLocation = location.href;
    function onLocationChange() {
      if (location.href === lastLocation) return;
      lastLocation = location.href;

      // Remove button if navigating away from valid page
      const btn = document.getElementById('gh-ts-toggle');
      if (!isValidPage()) {
        if (btn) btn.remove();
        mo.disconnect();
        return;
      }

      // Re-initialize if navigating to valid page
      addToggleButton();
      startObserving();
      setTimeout(() => { if (isEnabled()) updateTimestampsLive(true); }, 800);
    }

    window.addEventListener('locationchange', onLocationChange);
    window.addEventListener('load', () => {
      if (!isValidPage()) return;
      addToggleButton();
      startObserving();
      setTimeout(() => { if (isEnabled()) updateTimestampsLive(true); }, 1000);
    });

  })();