1(async ()=>{ 2 // Simple, self-contained IT Status bar script 3 // for v3 and v4 main templates 4 5 let status = false; 6 7 // Define the colors for the status indicators 8 const colors = { 9 none: "#72BF44", 10 maintenance: "#2DCCD3", 11 minor: "#FDD923", 12 major: "#E77724", 13 critical: "#AD2327" 14 }; 15 16 // Define the status messages 17 const messages = { 18 none: "Operational", 19 maintenance: "Maintenance", 20 minor: "Degraded", 21 major: "Partial outage", 22 critical: "Major outage" 23 }; 24 25 // Define the icons 26 const icons = { 27 none: "fa-check", 28 maintenance: "fa-wrench", 29 minor: "fa-exclamation-triangle", 30 major: "fa-circle-xmark", 31 critical: "fa-square-minus" 32 }; 33 34 /** 35 * Fetch the status from StatusPage 36 * @async 37 * @returns {object|boolean} The status object, or false if fetch failed, invalid response, or unhandled status code 38 */ 39 async function fetchStatus() { 40 let status = await fetch("https://status.mst.edu/api/v2/status.json"); 41 42 // If the fetch failed, return false 43 if (!status) { 44 console.log("IT Status Bar: Failed to fetch from StatusPage."); 45 return false; 46 } 47 48 // If the fetch succeeded, but the response was invalid, return false 49 try { 50 status = await status.json(); 51 } catch (e) { 52 console.log("IT Status Bar: Received invalid response from StatusPage."); 53 return false; 54 } 55 56 // If the status code isn't handled by this script, return false 57 if (!status.status.indicator.match(/none|maintenance|minor|major|critical/)) { 58 console.log("IT Status Bar: " + status.status.indicator + " is not a handled status code."); 59 return false; 60 } 61 62 return status; 63 } 64 65 /** 66 * Retrieves the firstChild DOM node of a container DOM node, skipping whitespace 67 * @param {Element} el the container element 68 * @returns {Element} the firstChild 69 */ 70 function getFirstChild(el) { 71 let firstChild = el.firstChild; 72 while(firstChild != null && firstChild.nodeType === 3) { 73 firstChild = firstChild.nextSibling; 74 } 75 return firstChild; 76 } 77 78 /** 79 * Determine via cookies if this particular status update has been dismissed 80 * @returns {boolean} true if this status update has been dismissed; false if new update or not yet dismissed 81 */ 82 function isBarDismissed() { 83 let cookies = document.cookie.split("; "); 84 let lastUpdate = new Date(status.page.updated_at); 85 let dismissedUpdate = false; 86 for (let cookie of cookies) { 87 cookie = cookie.split("="); 88 if (cookie[0] === "it-status-hide-update") dismissedUpdate = new Date(parseInt(cookie[1])); 89 } 90 91 // If the it-status-hide-update cookie is set, then the user has dismissed an update 92 // Is it this update? 93 if (dismissedUpdate) { 94 if (dismissedUpdate.getTime() === lastUpdate.getTime()) { 95 // The user did dismiss this particular update. Don't continue to bar creation. 96 return true; 97 } else { 98 // This is a new message the user hasn't seen yet. Delete the cookie. 99 document.cookie = "it-status-hide-update=; Max-Age=0; domain=mst.edu"; 100 return false; 101 } 102 } 103 104 return false; 105 } 106 107 /** 108 * Determine if the status bar is required 109 * @uses isBarDismissed() 110 * @returns {boolean} true if the status bar is required; false if dismissed or status is "none" or "minor" 111 */ 112 function isBarRequired() { 113 // If the bar has been dismissed, don't continue 114 if (isBarDismissed()) return false; 115 116 // If the status is "none", "minor", or "maintenance" don't continue 117 if (!!status.status.indicator.match(/none|minor|maintenance/)) return false; 118 119 return true; 120 } 121 122 /** 123 * Builds the footer link DOM element 124 */ 125 function buildLink() { 126 // Is there already a link? If so, delete it 127 let existingLink = document.querySelector("#it-status-link"); 128 if (existingLink) existingLink.parentNode.removeChild(existingLink); 129 130 // Determine where to inject the status link 131 let v3LinkAfterSelector = document.querySelector("footer .footer_legal_row > .column:nth-child(2) .column:nth-child(2) ul li:last-child"); 132 let v4LinkAfterSelector = document.querySelector("#footer #footer__quick-links-4 ul li:first-child"); 133 let linkAfter = v3LinkAfterSelector ? v3LinkAfterSelector : v4LinkAfterSelector; 134 if (!linkAfter) { 135 console.log("IT Status Bar: Incompatible template. Cannot insert footer link."); 136 return; 137 }; 138 139 // Create the link 140 let link = document.createElement("li"); 141 link.id = "it-status-link"; 142 link.innerHTML = `<a href='https://status.mst.edu' target='_blank'>IT Systems: ${ messages[status.status.indicator] }</a> <i class="fa-solid ${ icons[status.status.indicator] }" style="color: ${ colors[status.status.indicator] };"></i>`; 143 144 // Inject the link 145 linkAfter.parentNode.insertBefore(link, linkAfter.nextSibling); 146 } 147 148 /** 149 * Builds the status bar DOM element 150 */ 151 function buildBar() { 152 // Is there already a bar? If so, delete it 153 let existingBar = document.querySelector("#it-status-bar"); 154 if (existingBar) existingBar.parentNode.removeChild(existingBar); 155 156 // Parse the time values 157 let lastUpdate = new Date(status.page.updated_at); 158 let lastUpdatePretty = new Intl.DateTimeFormat("en-US", { weekday: "short", month: "short", day: "
158numeric", hour: "numeric", minute: "numeric", hour12: true, timeZone: "America/Chicago" }).format(lastUpdate); 159 160 // Create the bar 161 let barTemplate = document.createElement("div"); 162 barTemplate.innerHTML = ` 163 <div 164 id="it-status-bar" 165 aria-label="IT status bar" 166 role="status" 167 style=" 168 position: fixed; 169 box-sizing: border-box; 170 bottom: 0px; 171 left: 0px; 172 width: 100%; 173 background-color: rgb(255, 255, 255); 174 border-top: 5px solid rgb(0, 122, 51); 175 z-index: 99; 176 display: flex; 177 align-items: center; 178 justify-content: center; 179 font-family: "Libre Franklin", sans-serif; 180 font-size: 1rem; 181 color: rgb(0, 0, 0); 182 text-align: left; 183 line-height: 1.5rem; 184 padding: 1.5rem 3.5rem 2rem 2rem; 185 "> 186 <!-- Grid holder --> 187 <div style=" 188 display: grid; 189 grid-template-areas: 190 "date date date" 191 "icon label description"; 192 font-size: 1.2rem; 193 gap: 0.25rem 0; 194 "> 195 196 <!-- Incident icon --> 197 <i 198 class="fa-solid ${ icons[status.status.indicator] }" 199 style=" 200 color: ${ colors[status.status.indicator] }; 201 margin-right: 0.5rem; 202 grid-area: icon; 203 margin-top: 0.125em; 204 "></i> 205 206 <!-- Incident text --> 207 <span style=" 208 font-weight: 700; 209 margin-right: 0.5rem; 210 grid-area: label; 211 "> 212 ${ messages[status.status.indicator] }: 213 </span> 214 215 <!-- StatusPage redirect message --> 216 <span style=" 217 color: #444; 218 grid-area: description; 219 "> 220 One or more IT services may be experiencing issues. For more information please <a href="https://status.mst.edu" target="_blank">see the IT Status Page</a>. 221 </span> 222 223 <!-- Incident timestamp --> 224 <span style=" 225 font-size: 0.925rem; 226 display: block; 227 grid-area: date; 228 color: #63666A; 229 "> 230 ${ lastUpdatePretty } 231 </span> 232 </div> 233 234 <!-- Close button --> 235 <span 236 role="button" 237 tabindex="0" 238 title="Dismiss" 239 style=" 240 position: absolute; 241 top: 50%; 242 transform: translateY(-50%); 243 right: 1rem; 244 width: 2rem; 245 height: 2rem; 246 line-height: 2rem; 247 text-align: center; 248 cursor: pointer; 249 font-size: 3rem; 250 color: rgb(0 0 0 / 79%); 251 "> 252 Ã 253 </span> 254 </div> 255 `; 256 let bar = getFirstChild(barTemplate); 257 258 // Inject the bar 259 document.body.appendChild(bar); 260 261 // Attach a click listener to the span with title Dismiss 262 document.querySelector("#it-status-bar span[title='Dismiss']").addEventListener("click", ()=>{ 263 document.cookie = `it-status-hide-update=${ lastUpdate.getTime() }; Max-Age=31536000; domain=mst.edu; path=/`; 264 document.querySelector("#it-status-bar").parentNode.removeChild(document.querySelector("#it-status-bar")); 265 }); 266 } 267 268 /** 269 * Trigger a fake IT status update for testing purposes 270 * @param {string} severity The severity of the fake status update (none, minor, major, or critical) 271 */ 272 function triggerFakeITStatus(severity) { 273 if (!severity.match(/none|maintenance|minor|major|critical/)) return; 274 275 // create a fake status object 276 status = { 277 page: { 278 updated_at: "2023-08-15T13:27:40.360-05:00" 279 }, 280 status: { 281 indicator: severity 282 } 283 }; 284 285 // clear any cookies 286 document.cookie = "it-status-hide-update=; Max-Age=0; domain=mst.edu; path=/"; 287 288 // delete any bars 289 let existingBar = document.querySelector("#it-status-bar"); 290 if (existingBar) existingBar.parentNode.removeChild(existingBar); 291 292 // cancel the update interval 293 clearInterval(itStatusUpdateInterval); 294 295 // build the link and bar 296 buildLink(); 297 if (isBarRequired()) buildBar(); 298 } 299 window.triggerFakeITStatus = triggerFakeITStatus; 300 301 /** 302 * Main function, updates status, runs on interval 303 * @async 304 */ 305 async function update() { 306 // Fetch the status 307 status = await fetchStatus(); 308 if (!status) return; 309 310 // Build the link and bar 311 buildLink(); 312 if (isBarRequired()) buildBar(); 313 } 314 315 // Update the status every 5 minutes 316 await update(); 317 window.itStatusUpdateInterval = setInterval(async ()=>{ await update(); }, 1000 * 60 * 5); 318})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.