Skip to content

Instantly share code, notes, and snippets.

@mbafford
Last active November 10, 2024 13:48
Show Gist options
  • Save mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2 to your computer and use it in GitHub Desktop.
Save mbafford/9fbfd4a4bb0c7f67b0910a9556bb65e2 to your computer and use it in GitHub Desktop.
Add Fastmail outages/incidents indicator to fastmail webUI (tampermonkey)
// ==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();
});
})();
{
"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"
}
]
}
{
"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"
}
]
}
@mbafford
Copy link
Author

mbafford commented Jul 18, 2024

Here's what it looks like:

2024_07_17_20_46_39

And on mouse over:

2024_07_17_20_46_56

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.

@mbafford
Copy link
Author

Updated to fix a race condition on elements not being initialized when loading. Added an icon even when status is ok.

Screenshot 2024-11-10 at 08 47 58

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment