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

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