Last active
November 10, 2024 13:48
-
-
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.2 | |
// @description Show an icon on Fastmail indicating current service status from the status API. | |
// @author Matthew Bafford | |
// @match https://app.fastmail.com/* | |
// @grant none | |
// ==/UserScript== | |
/** | |
* Modified version of the original script to always show a status icon | |
* Green checkmark for normal operation, warning symbol for active incidents | |
*/ | |
(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) { | |
showStatusIcon('UNKNOWN', null, 'Status check failed'); | |
} else if (status === "UP") { | |
showStatusIcon('UP', null, 'All systems operational'); | |
} else if (data.activeIncidents) { | |
showStatusIcon('DOWN', data.activeIncidents, 'Active incidents detected'); | |
} else { | |
showStatusIcon('UNKNOWN', null, 'Unknown state'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error fetching the status:', error); | |
showStatusIcon('ERROR', null, 'Failed to fetch status'); | |
}); | |
} | |
function showStatusIcon(status, activeIncidents, tooltipText) { | |
console.debug("showStatusIcon: " + status) | |
let icon = document.getElementById('statusIcon'); | |
if (icon == null) { | |
console.debug("creating icon") | |
icon = document.createElement('div'); | |
icon.id = 'statusIcon'; | |
icon.style.position = 'absolute'; | |
icon.style.right = '35px'; | |
icon.style.top = '4px'; | |
icon.style.cursor = 'pointer'; | |
const inboxElement = document.querySelector('.v-Sources-list'); | |
if (!inboxElement) { | |
alert("Could not find the inbox sources list to add the status icon to.") | |
return | |
} | |
inboxElement.appendChild(icon); | |
console.debug("inboxElement", inboxElement) | |
console.debug("icon", icon) | |
} | |
// Set icon based on status | |
switch(status) { | |
case 'UP': | |
icon.innerText = '🆗'; | |
icon.onclick = () => { | |
window.open('https://fastmailstatus.com'); | |
}; | |
break; | |
case 'DOWN': | |
icon.innerText = '⚠️'; | |
icon.onclick = () => { | |
if (activeIncidents && activeIncidents.length > 0) { | |
window.open(activeIncidents[0].url); | |
} else { | |
window.open('https://fastmailstatus.com'); | |
} | |
}; | |
break; | |
case 'ERROR': | |
icon.innerText = '❌'; | |
icon.onclick = () => { | |
window.open('https://fastmailstatus.com'); | |
}; | |
break; | |
default: | |
icon.innerText = '❔'; | |
icon.onclick = () => { | |
window.open('https://fastmailstatus.com'); | |
}; | |
} | |
createTooltip(status, activeIncidents, tooltipText); | |
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 createTooltip(status, incidents, defaultText) { | |
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); | |
} | |
tooltip.innerHTML = ''; | |
if (incidents && incidents.length > 0) { | |
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); | |
}); | |
} else { | |
const statusDiv = document.createElement('div'); | |
statusDiv.textContent = defaultText; | |
tooltip.appendChild(statusDiv); | |
} | |
// Add last update time | |
const updateTimeDiv = document.createElement('div'); | |
updateTimeDiv.style.marginTop = '10px'; | |
updateTimeDiv.style.borderTop = '1px solid #ccc'; | |
updateTimeDiv.style.paddingTop = '5px'; | |
updateTimeDiv.style.fontSize = '0.8em'; | |
updateTimeDiv.style.color = '#666'; | |
updateTimeDiv.textContent = `Last checked: ${new Date().toLocaleTimeString()}`; | |
tooltip.appendChild(updateTimeDiv); | |
} | |
/** | |
* 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); | |
}); | |
} | |
/** | |
* Sets up an observer waiting for the "is-refreshing" status to show up on any inbox. | |
*/ | |
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(); | |
}); | |
}); | |
} | |
console.debug("waiting for .v-Sources-list") | |
waitForElm(document, ".v-Sources-list", {subtree: true, attributes: true}).then((elm) => { | |
console.debug("got for .v-Sources-list") | |
// Initial status check | |
checkStatus(); | |
// Set up refresh monitoring | |
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.