mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-26 08:39:02 +01:00
414 lines
14 KiB
HTML
414 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Health Dashboard</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="stylesheet" href="./bootstrap.min.css" />
|
|
<style>
|
|
html, body {
|
|
background-color: #f7f9fb;
|
|
}
|
|
html {
|
|
height: 100%;
|
|
}
|
|
#results div.container:first-child {
|
|
border-top-left-radius: 3px;
|
|
border-top-right-radius: 3px;
|
|
}
|
|
#results div.container:last-child {
|
|
border-bottom-left-radius: 3px;
|
|
border-bottom-right-radius: 3px;
|
|
border-bottom-width: 1px;
|
|
border-color: #dee2e6;
|
|
border-style: solid;
|
|
}
|
|
.status {
|
|
cursor: pointer;
|
|
transition: all 500ms ease-in-out;
|
|
overflow-x: hidden;
|
|
padding: .25em 0;
|
|
color: white;
|
|
}
|
|
.title {
|
|
font-size: 2.5rem;
|
|
}
|
|
.status:hover {
|
|
opacity: 0.7;
|
|
transition: opacity 100ms ease-in-out;
|
|
color: black;
|
|
}
|
|
.status-over-time {
|
|
overflow: auto;
|
|
}
|
|
.status-over-time>span:not(:last-child) {
|
|
margin-left: 2px;
|
|
}
|
|
.status-time-ago {
|
|
color: #6a737d;
|
|
opacity: 0.5;
|
|
margin-top: 5px;
|
|
}
|
|
.status-min-max-ms {
|
|
overflow-x: hidden;
|
|
}
|
|
#tooltip {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
background-color: white;
|
|
border: 1px solid lightgray;
|
|
border-radius: 4px;
|
|
padding: 6px;
|
|
font-size: 13px;
|
|
}
|
|
#tooltip code {
|
|
color: #212529;
|
|
line-height: 1;
|
|
}
|
|
#tooltip .tooltip-title {
|
|
font-weight: bold;
|
|
margin-bottom: 0;
|
|
display: block;
|
|
}
|
|
#tooltip .tooltip-title {
|
|
margin-top: 8px;
|
|
}
|
|
#tooltip>.tooltip-title:first-child {
|
|
margin-top: 0;
|
|
}
|
|
#social {
|
|
position: fixed;
|
|
right: 5px;
|
|
bottom: 5px;
|
|
padding: 5px;
|
|
margin: 0;
|
|
z-index: 100;
|
|
}
|
|
#social img {
|
|
opacity: 0.3;
|
|
}
|
|
#social img:hover {
|
|
opacity: 1;
|
|
}
|
|
#settings {
|
|
position: fixed;
|
|
left: 5px;
|
|
bottom: 5px;
|
|
padding: 5px;
|
|
}
|
|
#settings select:focus {
|
|
box-shadow: none;
|
|
}
|
|
.service-group {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.service-group h5:hover {
|
|
color: #1b1e21 !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container my-3 rounded p-3 border shadow">
|
|
<div class="mb-2">
|
|
<div class="row">
|
|
<div class="col-8 text-left my-auto">
|
|
<div class="title display-4">Health Status</div>
|
|
</div>
|
|
<div class="col-4 text-right">
|
|
<img src="logo.png" alt="Gatus" style="position: relative; min-width: 50px; max-width: 200px; width: 20%;"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="results"></div>
|
|
</div>
|
|
|
|
<div id="tooltip" style="display: none">
|
|
<div class="tooltip-title">Timestamp:</div>
|
|
<code id="tooltip-timestamp">...</code>
|
|
<div class="tooltip-title">Response time:</div>
|
|
<code id="tooltip-response-time">...</code>
|
|
<div class="tooltip-title">Conditions:</div>
|
|
<code id="tooltip-conditions">...</code>
|
|
<div id="tooltip-errors-container">
|
|
<div class="tooltip-title">Errors:</div>
|
|
<code id="tooltip-errors">...</code>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./jquery.min.js"></script>
|
|
|
|
<div id="social">
|
|
<a href="https://github.com/TwinProduction/gatus" target="_blank" title="Gatus on GitHub">
|
|
<img src="./github.png" alt="GitHub" width="32" height="auto" />
|
|
</a>
|
|
</div>
|
|
|
|
<div id="settings">
|
|
<div class="input-group input-group-sm">
|
|
<div class="input-group-prepend">
|
|
<div class="input-group-text">↻</div>
|
|
</div>
|
|
<select class="form-control form-control-sm" id="refresh-rate">
|
|
<option value="10">10s</option>
|
|
<option value="30" selected>30s</option>
|
|
<option value="60">1m</option>
|
|
<option value="120">2m</option>
|
|
<option value="300">5m</option>
|
|
<option value="600">10m</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let serviceStatuses = {};
|
|
let timerHandler = 0;
|
|
let refreshIntervalHandler = 0;
|
|
let userClickedStatus = false;
|
|
|
|
// TODO: make this variable configurable and persist the choice in localStorage
|
|
let showStatusOnHover = true;
|
|
|
|
function showTooltip(serviceName, index, element) {
|
|
//userClickedStatus = false;
|
|
clearTimeout(timerHandler);
|
|
let serviceResult = serviceStatuses[serviceName].results[index];
|
|
$("#tooltip-timestamp").text(prettifyTimestamp(serviceResult.timestamp));
|
|
$("#tooltip-response-time").text(parseInt(serviceResult.duration/1000000) + "ms");
|
|
// Populate the condition section
|
|
let conditions = "";
|
|
for (let i in serviceResult['condition-results']) {
|
|
let conditionResult = serviceResult['condition-results'][i];
|
|
conditions += (conditionResult.success ? "✓" : "X") + " ~ " + htmlEntities(conditionResult.condition) + "<br />";
|
|
}
|
|
$("#tooltip-conditions").html(conditions);
|
|
// Populate the error section only if there are errors
|
|
if (serviceResult.errors && serviceResult.errors.length > 0) {
|
|
let errors = "";
|
|
for (let i in serviceResult.errors) {
|
|
errors += "- " + htmlEntities(serviceResult.errors[i]) + "<br />";
|
|
}
|
|
$("#tooltip-errors").html(errors);
|
|
$("#tooltip-errors-container").show();
|
|
} else {
|
|
$("#tooltip-errors-container").hide();
|
|
}
|
|
// Position tooltip
|
|
$("#tooltip").css({top: "0px", left: "0px"}).show();
|
|
let targetTopPosition = element.getBoundingClientRect().y + 30;
|
|
let targetLeftPosition = element.getBoundingClientRect().x;
|
|
// Make adjustments if necessary
|
|
let tooltipBoundingClientRect = document.querySelector('#tooltip').getBoundingClientRect();
|
|
if (targetLeftPosition + window.scrollX + tooltipBoundingClientRect.width + 50 > document.body.getBoundingClientRect().width) {
|
|
targetLeftPosition = element.getBoundingClientRect().x - tooltipBoundingClientRect.width + element.getBoundingClientRect().width;
|
|
if (targetLeftPosition < 0) {
|
|
targetLeftPosition += -targetLeftPosition;
|
|
}
|
|
}
|
|
if (targetTopPosition + window.scrollY + tooltipBoundingClientRect.height + 50 > document.body.getBoundingClientRect().height && targetTopPosition >= 0) {
|
|
targetTopPosition = element.getBoundingClientRect().y - (tooltipBoundingClientRect.height + 10);
|
|
if (targetTopPosition < 0) {
|
|
targetTopPosition = element.getBoundingClientRect().y + 30;
|
|
}
|
|
}
|
|
$("#tooltip").css({top: targetTopPosition + "px", left: targetLeftPosition + "px"});
|
|
}
|
|
|
|
function fadeTooltip() {
|
|
if (!userClickedStatus) {
|
|
timerHandler = setTimeout(function () {
|
|
$("#tooltip").hide();
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
function toggleTooltip(serviceName, index, element) {
|
|
console.log("userClickedStatus="+userClickedStatus);
|
|
if (!userClickedStatus) {
|
|
showTooltip(serviceName, index, element);
|
|
userClickedStatus = true;
|
|
} else {
|
|
$("#tooltip").hide();
|
|
userClickedStatus = false;
|
|
}
|
|
}
|
|
|
|
function createStatusBadge(serviceStatusIndex, index, success) {
|
|
if (success) {
|
|
if (showStatusOnHover) {
|
|
return "<span class='status badge badge-success' style='width: 5%' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>✓</span>";
|
|
} else {
|
|
return "<span class='status badge badge-success' style='width: 5%' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>✓</span>";
|
|
}
|
|
}
|
|
if (showStatusOnHover) {
|
|
return "<span class='status badge badge-danger' style='width: 5%' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>X</span>";
|
|
} else {
|
|
return "<span class='status badge badge-danger' style='width: 5%' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>X</span>";
|
|
}
|
|
}
|
|
|
|
function refreshStatuses() {
|
|
$.getJSON("./api/v1/statuses", function (data) {
|
|
// Update the table only if there's a change
|
|
if (JSON.stringify(serviceStatuses) !== JSON.stringify(data)) {
|
|
serviceStatuses = data;
|
|
buildTable();
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildTable() {
|
|
let outputByGroup = {};
|
|
for (let serviceStatusIndex in serviceStatuses) {
|
|
let serviceStatusOverTime = "";
|
|
let serviceStatus = serviceStatuses[serviceStatusIndex];
|
|
let hostname = serviceStatus.results[serviceStatus.results.length-1].hostname;
|
|
let minResponseTime = null;
|
|
let maxResponseTime = null;
|
|
let newestTimestamp = null;
|
|
let oldestTimestamp = null;
|
|
for (let resultIndex in serviceStatus.results) {
|
|
let serviceResult = serviceStatus.results[resultIndex];
|
|
serviceStatusOverTime = createStatusBadge(serviceStatusIndex, resultIndex, serviceResult.success) + serviceStatusOverTime;
|
|
const responseTime = parseInt(serviceResult.duration/1000000);
|
|
if (minResponseTime == null || minResponseTime > responseTime) {
|
|
minResponseTime = responseTime;
|
|
}
|
|
if (maxResponseTime == null || maxResponseTime < responseTime) {
|
|
maxResponseTime = responseTime;
|
|
}
|
|
const timestamp = new Date(serviceResult.timestamp);
|
|
if (newestTimestamp == null || newestTimestamp < timestamp) {
|
|
newestTimestamp = timestamp;
|
|
}
|
|
if (oldestTimestamp == null || oldestTimestamp > timestamp) {
|
|
oldestTimestamp = timestamp;
|
|
}
|
|
}
|
|
let output = ""
|
|
+ "<div class='container py-3 border-left border-right border-top border-black rounded-0'>"
|
|
+ " <div class='row mb-2'>"
|
|
+ " <div class='col-md-10'>"
|
|
+ " <span class='font-weight-bold'>" + serviceStatus.name + "</span> <span class='text-secondary font-weight-lighter'>- " + hostname + "</span>"
|
|
+ " </div>"
|
|
+ " <div class='col-md-2 text-right'>"
|
|
+ " <span class='font-weight-lighter status-min-max-ms'>" + (minResponseTime === maxResponseTime ? minResponseTime : (minResponseTime + "-" + maxResponseTime)) + "ms</span>"
|
|
+ " </div>"
|
|
+ " </div>"
|
|
+ " <div class='row'>"
|
|
+ " <div class='col-12 d-flex flex-row-reverse status-over-time'>"
|
|
+ " " + serviceStatusOverTime
|
|
+ " </div>"
|
|
+ " </div>"
|
|
+ " <div class='row status-time-ago'>"
|
|
+ " <div class='col-6'>"
|
|
+ " " + generatePrettyTimeAgo(oldestTimestamp)
|
|
+ " </div>"
|
|
+ " <div class='col-6 text-right'>"
|
|
+ " " + generatePrettyTimeAgo(newestTimestamp)
|
|
+ " </div>"
|
|
+ " </div>"
|
|
+ "</div>";
|
|
// create an empty entry if this group is new
|
|
if (!outputByGroup[serviceStatus.group]) {
|
|
outputByGroup[serviceStatus.group] = "";
|
|
}
|
|
outputByGroup[serviceStatus.group] += output;
|
|
}
|
|
let output = "";
|
|
for (let group in outputByGroup) {
|
|
// Services that don't have a group should be skipped and left for last
|
|
if (group === 'undefined') {
|
|
continue
|
|
}
|
|
let key = group.replace(/[^a-zA-Z0-9]/g, '');
|
|
let existingGroupContentSelector = $("#service-group-" + key + "-content");
|
|
let isCurrentlyHidden = existingGroupContentSelector.length && existingGroupContentSelector[0].style.display === 'none';
|
|
let groupStatus = "<span class='text-success'>✓</span>";
|
|
if (outputByGroup[group].includes("badge badge-danger")) {
|
|
groupStatus = "<span class='text-warning'>~</span>";
|
|
}
|
|
output += ""
|
|
+ "<div class='mt-" + (output.length ? '4' : '3') + "'>"
|
|
+ " <div class='container pt-2 border-left border-right border-top border-black border-bottom service-group' id='service-group-" + key + "' data-group='" + key + "' onclick='toggleGroup(this)'>"
|
|
+ " <h5 class='text-secondary text-monospace pb-0'>"
|
|
+ " " + groupStatus + " " + group
|
|
+ " <span class='float-right service-group-arrow' id='service-group-" + key + "-arrow'>" + (isCurrentlyHidden ? "▼" : "▲") + "</span>"
|
|
+ " </h5>"
|
|
+ " </div>"
|
|
+ " <div class='service-group-content' id='service-group-" + key + "-content' style='" + (isCurrentlyHidden ? "display: none;" : "") + "'>"
|
|
+ " " + outputByGroup[group]
|
|
+ " </div>"
|
|
+ "</div>";
|
|
}
|
|
// Add all services that don't have a group at the end
|
|
if (outputByGroup['undefined']) {
|
|
output += ""
|
|
+ "<div class='mt-" + (output.length ? '4' : '3') + "'>"
|
|
+ " " + outputByGroup['undefined']
|
|
+ "</div>"
|
|
}
|
|
$("#results").html(output);
|
|
}
|
|
|
|
function toggleGroup(element) {
|
|
let selector = $("#service-group-" + element.dataset.group + "-content");
|
|
selector.toggle("fast", function() {
|
|
if (selector.length && selector[0].style.display === 'none') {
|
|
$("#service-group-" + element.dataset.group + "-arrow").html("▼");
|
|
} else {
|
|
$("#service-group-" + element.dataset.group + "-arrow").html("▲");
|
|
}
|
|
});
|
|
}
|
|
|
|
function prettifyTimestamp(timestamp) {
|
|
let date = new Date(timestamp);
|
|
let YYYY = date.getFullYear();
|
|
let MM = ((date.getMonth()+1)<10?"0":"")+""+(date.getMonth()+1);
|
|
let DD = ((date.getDate())<10?"0":"")+""+(date.getDate());
|
|
let hh = ((date.getHours())<10?"0":"")+""+(date.getHours());
|
|
let mm = ((date.getMinutes())<10?"0":"")+""+(date.getMinutes());
|
|
let ss = ((date.getSeconds())<10?"0":"")+""+(date.getSeconds());
|
|
return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss;
|
|
}
|
|
|
|
function generatePrettyTimeAgo(t) {
|
|
let differenceInMs = new Date().getTime() - new Date(t).getTime();
|
|
if (differenceInMs > 3600000) {
|
|
let hours = (differenceInMs/3600000).toFixed(0);
|
|
return hours + " hour" + (hours !== "1" ? "s" : "") + " ago";
|
|
}
|
|
if (differenceInMs > 60000) {
|
|
let minutes = (differenceInMs/60000).toFixed(0);
|
|
return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago";
|
|
}
|
|
return (differenceInMs/1000).toFixed(0) + " seconds ago";
|
|
}
|
|
|
|
function htmlEntities(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function setRefreshInterval(seconds) {
|
|
refreshStatuses();
|
|
refreshIntervalHandler = setInterval(function() {
|
|
refreshStatuses();
|
|
}, seconds * 1000);
|
|
}
|
|
|
|
$("#refresh-rate").change(function() {
|
|
clearInterval(refreshIntervalHandler);
|
|
setRefreshInterval($(this).val());
|
|
});
|
|
setRefreshInterval(30);
|
|
$("#refresh-rate").val(30);
|
|
</script>
|
|
</body>
|
|
</html> |