Greasy Fork is available in English.
Adds local time and relative time beside UTC timestamps on githubstatus.com
// ==UserScript==
// @name GitHub Status - Local Time + Time Ago
// @namespace https://githubstatus.com/
// @version 1.3
// @description Adds local time and relative time beside UTC timestamps on githubstatus.com
// @match https://www.githubstatus.com/*
// @match https://githubstatus.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[GitHub Status Local Time]', ...args);
}
}
function warn(...args) {
if (DEBUG) {
console.warn('[GitHub Status Local Time]', ...args);
}
}
function formatLocalDate(date) {
const month = new Intl.DateTimeFormat('en-US', {
month: 'short'
}).format(date);
const day = date.getDate();
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const timezone = new Intl.DateTimeFormat('en-US', {
timeZoneName: 'short'
})
.formatToParts(date)
.find(part => part.type === 'timeZoneName')?.value ?? '';
return `${month} ${day}, ${year} - ${hours}:${minutes} ${timezone}`;
}
function formatTimeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 5) {
return 'just now';
}
if (seconds < 60) {
return `${seconds} seconds ago`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
}
const days = Math.floor(hours / 24);
if (days < 30) {
return `${days} day${days === 1 ? '' : 's'} ago`;
}
const months = Math.floor(days / 30);
if (months < 12) {
return `${months} month${months === 1 ? '' : 's'} ago`;
}
const years = Math.floor(days / 365);
return `${years} year${years === 1 ? '' : 's'} ago`;
}
function addLocalTimes() {
const start = performance.now();
const elements = document.querySelectorAll('.ago[data-datetime-unix]');
log(`Processing ${elements.length} timestamp(s)`);
let created = 0;
let updated = 0;
let skipped = 0;
elements.forEach(ago => {
const small = ago.closest('small');
if (!small) {
warn('Could not find <small> parent for:', ago);
return;
}
const unixMs = Number(ago.dataset.datetimeUnix);
if (!Number.isFinite(unixMs)) {
warn('Invalid data-datetime-unix:', ago.dataset.datetimeUnix);
return;
}
const date = new Date(unixMs);
let local = small.querySelector(':scope > .tm-local-time');
if (!local) {
local = document.createElement('span');
local.className = 'tm-local-time';
small.appendChild(local);
created++;
}
const newText = ` (${formatLocalDate(date)}, ${formatTimeAgo(date)})`;
// Important: don't mutate the DOM if nothing changed.
if (local.textContent !== newText) {
local.textContent = newText;
updated++;
} else {
skipped++;
}
});
log({
created,
updated,
skipped,
durationMs: (performance.now() - start).toFixed(2)
});
}
let observerTimer = null;
const observer = new MutationObserver(mutations => {
log(`MutationObserver fired with ${mutations.length} mutation(s)`);
// Debounce DOM mutations to avoid repeated processing.
clearTimeout(observerTimer);
observerTimer = setTimeout(() => {
log('Running update after DOM mutation');
addLocalTimes();
}, 100);
});
log('Script starting');
addLocalTimes();
observer.observe(document.body, {
childList: true,
subtree: true
});
log('MutationObserver started');
setInterval(() => {
log('Interval update');
addLocalTimes();
}, 30_000);
})();