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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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);
    });

  })();