-
-
Save nhalstead/6067e542f2a815e49c3d08eb217b361f to your computer and use it in GitHub Desktop.
Simple Status Page that has auto Reload and fully controlled by a Meta Tag.
This file contains hidden or 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Server Status Page</title> | |
<meta property="status" content='{"api":"//example.com/server_status.php?format=json","interval":60,"expand":true,"servers":[{"id":12,"short":"Strawberry","online":true,"error":true,"hostname":"nhalstead.me","statusPageLink":false,"serialNumber":"0000","location":{"building":"X","rack":13,"unit":40}}]}'> | |
<style> | |
a:hover, a:focus, a:active { | |
text-decoration: none; | |
color: inherit; | |
} | |
body { | |
margin: 50px; | |
} | |
h3 { | |
margin: 0; | |
} | |
dt { | |
float: left; | |
clear: left; | |
font-weight: bold; | |
margin-left: 1rem; | |
} | |
dt:after { | |
content: ':\00a0'; | |
} | |
dd:before { | |
content: '\00a0'; | |
} | |
.server { | |
position: relative; | |
box-sizing: border-box; | |
width: 100%; | |
margin: 0 0 5px 0; | |
border: 0; | |
padding: 20px; | |
background-color: green; | |
color: #fff; | |
font-family: helvetica; | |
font-size: 18px; | |
line-height: 1.7; | |
text-align: left; | |
} | |
.server_down .server{ | |
background-color: red; | |
} | |
.server_error .server { | |
background-color: #FFEB3B; | |
color: black; | |
} | |
.server:after { | |
content: 'details \25B8'; | |
speak: none; | |
position: absolute; | |
top: 2em; | |
right: 20px; | |
display: inline-block; | |
border-radius: 4px; | |
border: 0px none; | |
padding: 5px 11px; | |
box-shadow: 0px -2px rgba(0,0,0, 0.5) inset; | |
background-color: #fff; | |
color: #4D4E53; | |
font-weight: normal; | |
font-size: 14px; | |
line-height: 1; | |
text-transform: uppercase; | |
} | |
.server[aria-expanded="true"]:after { | |
content: 'details \25BE'; | |
} | |
.server:hover, .server:focus { | |
cursor: pointer; | |
} | |
.server:hover:after, .server:focus:after { | |
padding: 8px 14px; | |
top: calc(2em - 3px); | |
cursor: pointer; | |
} | |
.server:before { | |
content: '\2713'; | |
speak: none; | |
} | |
.server_down .server:before { | |
content: '\2715'; | |
speak: none; | |
} | |
.server_error .server:before { | |
content: '\25EC'; | |
speak: none; | |
} | |
.server_details { | |
margin-bottom: 5px; | |
margin-top: -8px; | |
padding: 0; | |
border: 3px solid green; | |
font-family: helvetica; | |
line-height:2; | |
list-style-type: none; | |
transition: height 0.5s ease-in-out, visibility 0.5s; | |
height: 8rem; | |
overflow: hidden; | |
} | |
.server_down + .server_details { | |
border-color: red; | |
} | |
.server_error + .server_details { | |
border-color: #FFEB3B; | |
} | |
.server_details.hidden { | |
height: 0; | |
visibility: hidden; | |
} | |
.offscreen { | |
position: absolute; | |
height: 1px; | |
width: 1px; | |
overflow: hidden; | |
clip: rect(1px 1px 1px 1px); | |
clip: rect(1px, 1px, 1px, 1px); | |
} | |
</style> | |
</head> | |
<body> | |
<div> | |
<h3> | |
<button class="server" aria-controls="blueberry" aria-expanded="false" aria-label="Server up: Blueberry"> | |
Blueberry | |
</button> | |
</h3> | |
<dl class="server_details hidden" id="blueberry"> | |
<dt>Hostname</dt><dd>web02.onr.example.com</dd> | |
<dt>Service Tag</dt><dd>JCQFZK1</dd> | |
<dt>Datacenter</dt><dd>ONR</dd> | |
<dt>Rack Location</dt><dd>104.6, Unit #24</dd> | |
</dl> | |
</div> | |
<div> | |
<h3 class="server_down"> | |
<button class="server" aria-controls="raspberry" aria-expanded="false" aria-label="Server down: Raspberry"> | |
Raspberry | |
</button> | |
</h3> | |
<dl class="server_details hidden" id="raspberry"> | |
<dt>Hostname</dt><dd>web02.onr.example.com</dd> | |
<dt>Service Tag</dt><dd>JCQFZK1</dd> | |
<dt>Datacenter</dt><dd>ONR</dd> | |
<dt>Rack Location</dt><dd>104.6, Unit #24</dd> | |
</dl> | |
</div> | |
<div> | |
<h3> | |
<button class="server" aria-controls="strawberry" aria-expanded="false" aria-label="Server up: Raspberry"> | |
Strawberry | |
</button> | |
</h3> | |
<dl class="server_details hidden" id="12"> | |
<dt>Hostname</dt><dd>web02.onr.example.com</dd> | |
<dt>Service Tag</dt><dd>JCQFZK1</dd> | |
<dt>Datacenter</dt><dd>ONR</dd> | |
<dt>Rack Location</dt><dd>104.6, Unit #24</dd> | |
</dl> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> | |
<script> | |
/** | |
* Adds some merge Capibility to objects like arrayMerge in php but for Objects | |
* This is the Same thing as Object.assign BUT it does not overrite the | |
* object you are calling it on | |
* | |
* @param {Object} Another Object to Add to the Current Object | |
* @return {Object} The self + the Object to merge. | |
*/ | |
Object.prototype.merge = function (objectToMerge) { | |
return Object.assign({}, this, objectToMerge); | |
}; | |
/** | |
* Adds some Capacity to run a foreach on an object | |
* | |
* @param {Function} Callback that will execute on every element | |
* @param {Mixed} Data to send to Every Call of the data | |
*/ | |
Object.prototype.forEach = function(callback, extraData) { | |
Object.keys(this).forEach(function(key) { | |
var value = this[key]; | |
callback(key, value, extraData); | |
}); | |
} | |
/** | |
* | |
* @param {String} name Meta Tag to get value of. | |
* @param {String} defaultValue Default value to return if the Meta Tag DOES NOT Exist. | |
* @return {String} Value from the Meta Tag. | |
*/ | |
function getHeadMeta(name, defaultValue = "") { | |
if(typeof defaultValue !== "string") defaultValue = "{}"; | |
return (document.head.querySelector("meta[property~='"+name+"'][content]") || {content: defaultValue}).content; | |
} | |
/** | |
* url Function, Get URL Data. | |
* | |
* @param {string} URL to Parse and Process | |
* @return {Object} URL Data | |
*/ | |
function url(urlIn = "/"){ | |
if(typeof urlIn !== "string") urlIn = ""; | |
let parser = document.createElement('a'); | |
parser.href = urlIn; | |
return { | |
href: parser.href, | |
protocol: parser.protocol, | |
host: parser.host, | |
hostname: parser.hostname, | |
port: parser.port, | |
pathname: parser.pathname, | |
hash: parser.hash, | |
serach: parser.search | |
}; | |
} | |
const dataContainer = { | |
api: "", // Update API URL | |
interval: 60, // Update Interval of the Data, 0 is disabled | |
expand: true, // Expand all of the Elements on Load. | |
first: false, // Tells the Init that it needs to pull data on load of the page. | |
servers: [] // Server List | |
}; | |
const dataServer = { | |
id: "", | |
short: "", | |
online: true, | |
error: false, | |
hostname: "", | |
statusPageLink: false, // Is the Hostname Link a FQDN that has a Status Page, Should it be Clickable? | |
serialNumber: "", | |
location: { | |
building: "", | |
rack: "", | |
unit: "" | |
} | |
}; | |
// Process all of the Init Config with the Server data. | |
function parse(data = "{}"){ | |
try { | |
var x = {}; | |
if(typeof data == "string"){ | |
x = JSON.parse(data); | |
} | |
else if (typeof data == "object"){ | |
x = data; | |
} | |
x = dataContainer.merge(x); | |
x.servers = parseServer(x.servers); | |
return x; | |
} | |
catch(e){ | |
console.warn(e); | |
return {}; | |
} | |
} | |
// Process Just Server Data | |
function parseServer(data = "{}"){ | |
try { | |
var x = []; | |
if(typeof data == "string"){ | |
x = JSON.parse(data); | |
} | |
else if (typeof data == "object"){ | |
x = data; | |
} | |
x.forEach((e,i)=>{ | |
e = dataServer.merge(e); | |
}); | |
return x; | |
} | |
catch(e){ | |
console.warn(e); | |
return []; | |
} | |
} | |
/** | |
* Build HTML for the UI from the Server Info being provided. | |
* | |
* @return {HtmlCollection} HTML Collection from JS | |
*/ | |
function buildItem (serverInfo){ | |
serverInfo = dataServer.merge(serverInfo); | |
return ((si, d, c, pH, pHb, pD, dt1, dt2, dt3, dt4, dd1, dd2, dd3, dd4)=>{ | |
c = d.createElement("div"); | |
pH = d.createElement("h3"); | |
if(si.online !== true){ | |
pH.classList.add("server_down"); | |
} | |
else if(si.error == true){ | |
pH.classList.add("server_error"); | |
} | |
pHb = d.createElement("button"); | |
pHb.classList.add("server"); | |
pHb.onclick = function(){ | |
$(this).toggleExpand(); | |
}; | |
pHb.setAttribute("aria-controls", si.id); | |
pHb.setAttribute("aria-expanded", "false"); | |
pHb.setAttribute("aria-label", "Server Details for " + si.id); | |
pHb.innerText = " " + ((si.short)?si.short:si.id); | |
pH.appendChild(pHb); | |
pD = d.createElement("dl"); | |
pD.id = si.id; | |
pD.classList.add("server_details"); | |
pD.classList.add("hidden"); | |
dt1 = d.createElement("dt"); | |
dd1 = d.createElement("dd"); | |
dt1.innerText = "Hostname"; | |
dd1.innerText = si.hostname; | |
pD.appendChild(dt1); | |
pD.appendChild(dd1); | |
dt2 = d.createElement("dt"); | |
dd2 = d.createElement("dd"); | |
dt2.innerText = "Service Tag"; | |
dd2.innerText = si.serialNumber; | |
pD.appendChild(dt2); | |
pD.appendChild(dd2); | |
dd3 = d.createElement("dd"); | |
dt3 = d.createElement("dt"); | |
dt3.innerText = "Datacenter"; | |
dd3.innerText = si.location.building; | |
pD.appendChild(dt3); | |
pD.appendChild(dd3); | |
dt4 = d.createElement("dt"); | |
dd4 = d.createElement("dd"); | |
dt4.innerText = "Rack Location"; | |
dd4.innerText = "Rack " + si.location.rack + ", Unit " + si.location.unit; | |
pD.appendChild(dt4); | |
pD.appendChild(dd4); | |
c.appendChild(pH); | |
c.appendChild(pD); | |
return c; | |
})(serverInfo, document); | |
} | |
/** | |
* Update Document Elements with the given data. | |
* | |
*/ | |
function process(data = "[]"){ | |
let list = []; | |
if(typeof data == "string"){ | |
list = parseServer(data); | |
} | |
else { | |
list = data; | |
} | |
list.forEach((e) => { | |
// Process Each Entry of the Server List. | |
let h = buildItem(e); | |
if(document.getElementById(e.id) == undefined){ | |
document.body.appendChild(h); | |
} | |
else { | |
let r = document.getElementById(e.id); | |
r.parentElement.replaceWith(h); | |
} | |
}); | |
} | |
function updateUI(){ | |
if(!window.RuntimeConfig) return; | |
if(!window.RuntimeConfig.api) return; | |
console.log("%cUpdating UI Component from API Address%s", "display:block;text-align:center;background-color:pink;padding:2px 4px;", url(window.RuntimeConfig.api).href); | |
var x = []; // Server List | |
x = '[{"id": "s2f-master"}]'; | |
process(x); | |
} | |
/** | |
* This INIT function is good for a few things. | |
* When you call it with a string or object it will process it, | |
* also sets up the needed var storage. | |
* | |
* If you give it an Object and the env is setup then it will add to the | |
* current config and reset the init environment. | |
* | |
* @param {Object} Config Function | |
*/ | |
function init(data){ | |
let conf = {}; | |
if(typeof window.RuntimeConfig !== "undefined" && typeof data == "object"){ | |
conf = data; | |
} | |
else { | |
window.RuntimeConfig = window.RuntimeConfig || {}; | |
conf = parse(data); | |
} | |
window.RuntimeConfig = Object.assign({}, window.RuntimeConfig, conf); | |
delete window.RuntimeConfig.servers; | |
console.log("Loaded Config."); | |
process(conf.servers); // Init Setup of the UI | |
if(conf.first == true){ | |
updateUI(); | |
} | |
if(conf.interval !== 0 && conf.api !== "") { | |
if(typeof window.RuntimeConfigInterval !== "undefined"){ | |
clearInterval(window.RuntimeConfigInterval); | |
} | |
console.log("Data Reload set for %c%s %cseconds", "color:lightseagreen;font-weight:bold;", conf.interval, ""); | |
conf.interval = conf.interval * 1000; | |
window.RuntimeConfigInterval = setInterval(() => { | |
updateUI(); | |
}, conf.interval); | |
} | |
} | |
(function ($) { | |
$.fn.toggleExpand = function(){ | |
// this == control element | |
var $controller = $(this); | |
// get controlled element | |
var controlledId = $controller.attr('aria-controls'); | |
var $controlled = $('#' + controlledId); | |
// Toggle values on both | |
$controller.ariaToggle('aria-expanded'); | |
$controlled.toggleClass('hidden'); | |
// Make it so we can chain this function, like any good jQuery function | |
return this; | |
}; | |
$.fn.ariaToggle = function(attr) { | |
var $this = $(this); | |
var currentValue = $this.attr(attr); | |
if(currentValue === 'true'){ | |
$this.attr(attr, false); | |
} else { | |
$this.attr(attr, true); | |
} | |
// Make it so we can chain this function, like any good jQuery function | |
return this; | |
}; | |
$('.server').on('click', function(){ | |
$(this).toggleExpand(); | |
}); | |
}(jQuery)); | |
</script> | |
<script> | |
// Start Setup of the page by loading the config from the Meta Tag. | |
init(getHeadMeta("status", "{}")); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment