Skip to content

Instantly share code, notes, and snippets.

@mbafford
Last active July 18, 2024 12:09
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.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();
})();
{
"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.

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