Last active
July 18, 2024 12:09
-
-
Save mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2 to your computer and use it in GitHub Desktop.
Add Fastmail outages/incidents indicator to fastmail webUI (tampermonkey)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Fastmail Status Checker | |
// @namespace https://gist.github.com/mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2 | |
// @version 1.1 | |
// @description Show an icon on Fastmail if there's any active issue from the status API. | |
// @author Matthew Bafford | |
// @match https://app.fastmail.com/* | |
// @grant none | |
// ==/UserScript== | |
/** | |
* Shamelessly thrown together with the help of ChatGPT | |
* Tampermonkey script to add an icon beside the Inbox link on the fastmail webUI | |
* if there's an active incident reported on fastmailstatus.com | |
*/ | |
(function() { | |
'use strict'; | |
const statusApiUrl = 'https://fastmailstatus.com/summary.json'; | |
// for testing purposes, uncomment alternate statuses | |
// OK status | |
// const statusApiUrl = "https://gist.githubusercontent.com/mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2/raw/dfd7f8a771c84ef9cfb6e6d59f64e55ec928dad3/example.summary.ok.json"; | |
// FAILURE status | |
// const statusApiUrl = "https://gist.githubusercontent.com/mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2/raw/dfd7f8a771c84ef9cfb6e6d59f64e55ec928dad3/example.summary.issues.json"; | |
let lastUpdate = 0; | |
function checkStatus() { | |
// skip updating if it's been less than 10 seconds since the last update | |
if ( Date.now() - lastUpdate < 10 * 1000 ) { | |
return; | |
} | |
console.debug("Fetching latest FastMail status from https://fastmailstatus.com/"); | |
fetch(statusApiUrl) | |
.then(response => response.json()) | |
.then(data => { | |
lastUpdate = Date.now(); | |
const status = data?.page?.status | |
if ( !status || status === "UP" ) { | |
removeStatusIcon(); | |
} else if ( data.activeIncidents ) { | |
showStatusIcon(data.activeIncidents); | |
} else { | |
console.error("unknown state"); | |
} | |
}) | |
.catch(error => console.error('Error fetching the status:', error)); | |
} | |
function showStatusIcon(activeIncidents) { | |
let icon = document.getElementById('statusIcon'); | |
if ( icon == null ) { | |
icon = document.createElement('div'); | |
icon.id = 'statusIcon'; | |
icon.style.position = 'absolute'; | |
icon.style.right = '35px'; | |
icon.style.top = '4px'; | |
icon.innerText = '⚠️'; | |
icon.style.cursor = 'pointer'; | |
icon.onclick = () => { | |
window.open(activeIncidents[0].url); | |
}; | |
const inboxElement = document.querySelector('.v-Sources-list'); | |
if (inboxElement) { | |
inboxElement.appendChild(icon); | |
} | |
createTooltip(activeIncidents); | |
} | |
icon.addEventListener('mouseover', function() { | |
const tooltip = document.getElementById('statusTooltip'); | |
tooltip.style.display = 'block'; | |
const rect = icon.getBoundingClientRect(); | |
tooltip.style.top = `${rect.bottom + window.scrollY}px`; | |
tooltip.style.left = `${rect.left + window.scrollX}px`; | |
}); | |
icon.addEventListener('mouseout', function() { | |
const tooltip = document.getElementById('statusTooltip'); | |
tooltip.style.display = 'none'; | |
}); | |
} | |
function removeStatusIcon() { | |
const existingIcon = document.getElementById('statusIcon'); | |
if (existingIcon) { | |
existingIcon.remove(); | |
} | |
document.getElementById('statusTooltip')?.remove(); | |
} | |
/** | |
* Waits for an element matching a given query selector to appear (or disappear) | |
* in the dom. If negate is true, then this waits for the element to be removed. | |
* If the specified condition is already true when this is called, it will resolve | |
* the promise immediately. | |
* | |
* Inspired by https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists | |
*/ | |
function waitForElm(parent, selector, options, negate) { | |
return new Promise(resolve => { | |
let el = document.querySelector(selector); | |
if ( el && !negate ) { | |
return resolve(el); | |
} else if ( !el && negate ) { | |
return resolve(null); | |
} | |
const observer = new MutationObserver(mutations => { | |
let el = document.querySelector(selector); | |
let match = false; | |
if ( el && !negate ) { | |
match = true; | |
} else if ( !el && negate ) { | |
match = true; | |
} | |
if ( match ) { | |
observer.disconnect(); | |
resolve(el); | |
} | |
}); | |
observer.observe(document.body, options); | |
}); | |
} | |
function createTooltip(incidents) { | |
let tooltip = document.getElementById('statusTooltip'); | |
if (!tooltip) { | |
tooltip = document.createElement('div'); | |
tooltip.id = 'statusTooltip'; | |
tooltip.style.position = 'absolute'; | |
tooltip.style.backgroundColor = '#fff'; | |
tooltip.style.border = '1px solid #000'; | |
tooltip.style.padding = '10px'; | |
tooltip.style.zIndex = '10000'; | |
tooltip.style.display = 'none'; | |
tooltip.style.maxWidth = '300px'; | |
tooltip.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; | |
tooltip.style.borderRadius = '5px'; | |
document.body.appendChild(tooltip); | |
} else { | |
tooltip.innerHTML = ''; | |
} | |
incidents.forEach(incident => { | |
const incidentDiv = document.createElement('div'); | |
incidentDiv.style.marginBottom = '10px'; | |
const incidentTitle = document.createElement('div'); | |
incidentTitle.textContent = incident.name; | |
incidentTitle.style.fontWeight = 'bold'; | |
incidentTitle.style.marginBottom = '5px'; | |
const incidentStarted = document.createElement('div'); | |
incidentStarted.textContent = `Since: ${incident.started}`; | |
const incidentStatus = document.createElement('div'); | |
incidentStatus.textContent = `Status: ${incident.status}`; | |
const incidentImpact = document.createElement('div'); | |
incidentImpact.textContent = `Impact: ${incident.impact}`; | |
incidentDiv.appendChild(incidentTitle); | |
incidentDiv.appendChild(incidentStarted); | |
incidentDiv.appendChild(incidentStatus); | |
incidentDiv.appendChild(incidentImpact); | |
tooltip.appendChild(incidentDiv); | |
}); | |
} | |
/** | |
* Sets up an observer waiting for the "is-refreshing" status to show up on any inbox. This happens every time the inbox | |
* refreshes through manual intervention (aka when you're clicking repeatedly to get an email you're expecting). | |
* | |
* The goal is to piggy-back on that intent of wanting an updated status, and then kick off a refresh of the status of the | |
* fastmail service. | |
* | |
* Adds a delay before re-installing the trigger just to prevent infinite | |
*/ | |
function waitForRefresh() { | |
// wait for any of the inbox elements to have the "is-refreshing" class | |
waitForElm(document.querySelector("div.v-Sources"), ".app-source.is-refreshing", { subtree: true, attributes: true }).then((elm) => { | |
checkStatus(); | |
// now wait for the refreshing status to go away before installing a new observer | |
waitForElm(document.querySelector("div.v-Sources"), ".app-source.is-refreshing", { subtree: true, attributes: true }, true).then((elm) => { | |
waitForRefresh(); | |
}); | |
}); | |
} | |
waitForRefresh(); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"page": { | |
"name": "Fastmail", | |
"url": "https://fastmailstatus.com", | |
"status": "HASISSUES" | |
}, | |
"activeIncidents": [ | |
{ | |
"id": "clypu668y362151han131th0gqi", | |
"name": "Interrupted access to Fastmail services", | |
"started": "2024-07-17T12:30:41.598Z", | |
"status": "MONITORING", | |
"impact": "DEGRADEDPERFORMANCE", | |
"url": "https://fastmailstatus.com/clypu668y362151han131th0gqi" | |
} | |
], | |
"activeMaintenances": [ | |
{ | |
"id": "clypnezjt204177gwn1krsnlrp7", | |
"name": "Planned server migrations scheduled for the next two days", | |
"start": "2024-07-16T18:30:00.000Z", | |
"status": "NOTSTARTEDYET", | |
"duration": 2880, | |
"url": "https://fastmailstatus.com/clypnezjt204177gwn1krsnlrp7" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"page": { | |
"name": "Fastmail", | |
"url": "https://fastmailstatus.com", | |
"status": "UP" | |
}, | |
"activeMaintenances": [ | |
{ | |
"id": "clypnezjt204177gwn1krsnlrp7", | |
"name": "Planned server migrations scheduled for the next two days", | |
"start": "2024-07-16T18:30:00.000Z", | |
"status": "NOTSTARTEDYET", | |
"duration": 2880, | |
"url": "https://fastmailstatus.com/clypnezjt204177gwn1krsnlrp7" | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's what it looks like:
And on mouse over:
It auto-refreshes when the inbox is being refreshed (when the spinning icon is showing beside the mailbox name). This is triggered automatically periodically by the FastMail code, and also when you click on a mailbox to force a refresh. This means if you're frustrated and trying to get an urgent email, you will keep refreshing the status as you click on the inbox.
This will only check every 10 seconds at most, no matter how much you hammer the refresh button.