336 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
		
			10 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;
 | 
						|
		}
 | 
						|
	</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;
 | 
						|
 | 
						|
		function showTooltip(serviceName, index, element) {
 | 
						|
			userClickedStatus = false;
 | 
						|
			clearTimeout(timerHandler);
 | 
						|
			let serviceResult = serviceStatuses[serviceName][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 createStatusBadge(serviceName, index, success) {
 | 
						|
			if (success) {
 | 
						|
				return "<span class='status badge badge-success' style='width: 5%' onmouseenter='showTooltip(\""+serviceName+"\", "+index+", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>✓</span>";
 | 
						|
			}
 | 
						|
			return "<span class='status badge badge-danger' style='width: 5%' onmouseenter='showTooltip(\""+serviceName+"\", "+index+", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>X</span>";
 | 
						|
		}
 | 
						|
 | 
						|
		function refreshResults() {
 | 
						|
			$.getJSON("/api/v1/results", function (data) {
 | 
						|
				// Update the table only if there's a change
 | 
						|
				if (JSON.stringify(serviceStatuses) !== JSON.stringify(data)) {
 | 
						|
					serviceStatuses = data;
 | 
						|
					buildTable();
 | 
						|
				}
 | 
						|
			});
 | 
						|
		}
 | 
						|
 | 
						|
		function buildTable() {
 | 
						|
			let output = "";
 | 
						|
			for (let serviceName in serviceStatuses) {
 | 
						|
				let serviceStatusOverTime = "";
 | 
						|
				let hostname = serviceStatuses[serviceName][serviceStatuses[serviceName].length-1].hostname
 | 
						|
				let minResponseTime = null;
 | 
						|
				let maxResponseTime = null;
 | 
						|
				let newestTimestamp = null;
 | 
						|
				let oldestTimestamp = null;
 | 
						|
				for (let key in serviceStatuses[serviceName]) {
 | 
						|
					let serviceResult = serviceStatuses[serviceName][key];
 | 
						|
					serviceStatusOverTime = createStatusBadge(serviceName, key, 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;
 | 
						|
					}
 | 
						|
				}
 | 
						|
				output += ""
 | 
						|
					+ "<div class='container py-3 border-left border-right border-top border-black'>"
 | 
						|
					+ "  <div class='row mb-2'>"
 | 
						|
					+ "    <div class='col-md-10'>"
 | 
						|
					+ "      <span class='font-weight-bold'>" + serviceName + "</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>";
 | 
						|
			}
 | 
						|
			$("#results").html(output);
 | 
						|
		}
 | 
						|
 | 
						|
		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) {
 | 
						|
			refreshResults();
 | 
						|
			refreshIntervalHandler = setInterval(function() {
 | 
						|
				refreshResults();
 | 
						|
			}, seconds * 1000)
 | 
						|
		}
 | 
						|
 | 
						|
		$("#refresh-rate").change(function() {
 | 
						|
			clearInterval(refreshIntervalHandler);
 | 
						|
			setRefreshInterval($(this).val())
 | 
						|
		});
 | 
						|
		setRefreshInterval(30);
 | 
						|
		$("#refresh-rate").val(30);
 | 
						|
	</script>
 | 
						|
</body>
 | 
						|
</html>
 |