Replace old static folder with new static folder
@ -3,3 +3,4 @@ Dockerfile
|
|||||||
.github
|
.github
|
||||||
.idea
|
.idea
|
||||||
.git
|
.git
|
||||||
|
web/app
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -installsuffix cgo -o gatus
|
|||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /app/gatus .
|
COPY --from=builder /app/gatus .
|
||||||
COPY --from=builder /app/config.yaml ./config/config.yaml
|
COPY --from=builder /app/config.yaml ./config/config.yaml
|
||||||
COPY --from=builder /app/static static/
|
COPY --from=builder /app/web/static ./app/web/static/
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
![Gatus](static/logo-with-name.png)
|
![Gatus](assets/logo-with-name.png)
|
||||||
|
|
||||||
![build](https://github.com/TwinProduction/gatus/workflows/build/badge.svg?branch=master)
|
![build](https://github.com/TwinProduction/gatus/workflows/build/badge.svg?branch=master)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/TwinProduction/gatus?)](https://goreportcard.com/report/github.com/TwinProduction/gatus)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/TwinProduction/gatus?)](https://goreportcard.com/report/github.com/TwinProduction/gatus)
|
||||||
|
12
config.yaml
@ -1,14 +1,14 @@
|
|||||||
services:
|
services:
|
||||||
- name: frontend
|
- name: Front End
|
||||||
group: core
|
group: Excuse Me
|
||||||
url: "https://twinnation.org/health"
|
url: "https://twinnation.org/health"
|
||||||
interval: 1m
|
interval: 30s
|
||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
- "[BODY].status == UP"
|
- "[BODY].status == UP"
|
||||||
- "[RESPONSE_TIME] < 70"
|
- "[RESPONSE_TIME] < 23"
|
||||||
|
|
||||||
- name: backend
|
- name: back-end
|
||||||
group: core
|
group: core
|
||||||
url: "http://example.org/"
|
url: "http://example.org/"
|
||||||
interval: 5m
|
interval: 5m
|
||||||
@ -29,7 +29,7 @@ services:
|
|||||||
conditions:
|
conditions:
|
||||||
- "[STATUS] == 200"
|
- "[STATUS] == 200"
|
||||||
|
|
||||||
- name: cat-fact
|
- name: cat fact
|
||||||
url: "https://cat-fact.herokuapp.com/facts/random"
|
url: "https://cat-fact.herokuapp.com/facts/random"
|
||||||
interval: 5m
|
interval: 5m
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -55,11 +55,12 @@ func Handle() {
|
|||||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
|
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
|
||||||
|
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET")
|
||||||
router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./static")))))
|
router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./web/static")))))
|
||||||
if cfg.Metrics {
|
if cfg.Metrics {
|
||||||
router.Handle(cfg.Web.PrependWithContextRoot("/metrics"), promhttp.Handler()).Methods("GET")
|
router.Handle(cfg.Web.PrependWithContextRoot("/metrics"), promhttp.Handler()).Methods("GET")
|
||||||
}
|
}
|
||||||
@ -151,5 +152,10 @@ func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
// favIconHandler handles requests for /favicon.ico
|
// favIconHandler handles requests for /favicon.ico
|
||||||
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
http.ServeFile(writer, request, "./static/favicon.ico")
|
http.ServeFile(writer, request, "./web/static/favicon.ico")
|
||||||
|
}
|
||||||
|
|
||||||
|
// spaHandler handles requests for /favicon.ico
|
||||||
|
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.ServeFile(writer, request, "./web/static/index.html")
|
||||||
}
|
}
|
||||||
|
6
static/bootstrap.min.css
vendored
@ -1,438 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Health Dashboard</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link href="./tailwind.min.css" rel="stylesheet" />
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
background-color: #f7f9fb;
|
|
||||||
}
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
#global {
|
|
||||||
max-width: 1140px;
|
|
||||||
}
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
#results .service-group-content > div:nth-child(1) {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 500ms ease-in-out;
|
|
||||||
overflow-x: hidden;
|
|
||||||
color: white;
|
|
||||||
width: 5%;
|
|
||||||
font-size: 75%;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
.bg-success {
|
|
||||||
background-color: #28a745;
|
|
||||||
}
|
|
||||||
.text-monospace {
|
|
||||||
font-family: Consolas, monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container mx-auto rounded shadow-xl border my-3 p-5" id="global">
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex flex-wrap">
|
|
||||||
<div class="w-2/3 text-left my-auto">
|
|
||||||
<div class="title font-light">Health Status</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1/3 flex justify-end">
|
|
||||||
<img src="logo.png" alt="Gatus" style="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="flex bg-gray-200 rounded border border-gray-300 shadow">
|
|
||||||
<div class="rounded-xl py-1 px-2 text-gray-600 text-sm">
|
|
||||||
↻
|
|
||||||
</div>
|
|
||||||
<select class="text-center text-gray-500 text-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 maximumNumberOfResults = 20;
|
|
||||||
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.conditionResults) {
|
|
||||||
let conditionResult = serviceResult.conditionResults[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: "0", left: "0"}).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();
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 rounded bg-success' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>✓</span>";
|
|
||||||
} else {
|
|
||||||
return "<span class='status rounded bg-success' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>✓</span>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showStatusOnHover) {
|
|
||||||
return "<span class='status rounded bg-red-600' onmouseenter='showTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)' onmouseleave='fadeTooltip()' onclick='userClickedStatus = !userClickedStatus;'>X</span>";
|
|
||||||
} else {
|
|
||||||
return "<span class='status rounded bg-red-600' onclick='toggleTooltip(\"" + serviceStatusIndex + "\", " + index + ", this)'>X</span>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBlankStatusBadge() {
|
|
||||||
return "<span class='status rounded border border-dashed'> </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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = serviceStatus.results.length; i < maximumNumberOfResults; i++) {
|
|
||||||
serviceStatusOverTime += createBlankStatusBadge();
|
|
||||||
}
|
|
||||||
let output = ""
|
|
||||||
+ "<div class='container px-3 py-3 border-l border-r border-t rounded-none'>"
|
|
||||||
+ " <div class='flex flex-wrap mb-2'>"
|
|
||||||
+ " <div class='w-3/4'>"
|
|
||||||
+ " <span class='font-bold'>" + serviceStatus.name + "</span> <span class='text-gray-500 font-light'>- " + hostname + "</span>"
|
|
||||||
+ " </div>"
|
|
||||||
+ " <div class='w-1/4 text-right'>"
|
|
||||||
+ " <span class='font-light status-min-max-ms'>" + (minResponseTime === maxResponseTime ? minResponseTime : (minResponseTime + "-" + maxResponseTime)) + "ms</span>"
|
|
||||||
+ " </div>"
|
|
||||||
+ " </div>"
|
|
||||||
+ " <div class=''>"
|
|
||||||
+ " <div class='status-over-time flex flex-row-reverse'>"
|
|
||||||
+ " " + serviceStatusOverTime
|
|
||||||
+ " </div>"
|
|
||||||
+ " </div>"
|
|
||||||
+ " <div class='flex flex-wrap status-time-ago'>"
|
|
||||||
+ " <div class='w-1/2'>"
|
|
||||||
+ " " + generatePrettyTimeAgo(oldestTimestamp)
|
|
||||||
+ " </div>"
|
|
||||||
+ " <div class='w-1/2 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-green-600'>✓</span>";
|
|
||||||
if (outputByGroup[group].includes("bg-red-600")) {
|
|
||||||
groupStatus = "<span class='text-yellow-400'>~</span>";
|
|
||||||
}
|
|
||||||
output += ""
|
|
||||||
+ "<div class='" + (output.length ? 'mt-4' : 'mt-3') + "'>"
|
|
||||||
+ " <div class='container pt-2 border service-group' id='service-group-" + key + "' data-group='" + key + "' onclick='toggleGroup(this)'>"
|
|
||||||
+ " <h5 class='text-monospace text-gray-400 text-xl font-medium pb-2 px-3'>"
|
|
||||||
+ " " + 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='" + (output.length ? 'mt-4' : 'mt-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>
|
|
2
static/jquery.min.js
vendored
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 83 KiB |
1
static/tailwind.min.css
vendored
@ -85,7 +85,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
console.log("[Details][fetchData] Fetching data");
|
console.log("[Details][fetchData] Fetching data");
|
||||||
fetch(`${SERVER_URL}/api/v1/statuses/${this.$route.params.key}`)
|
fetch(`${this.serverUrl}/api/v1/statuses/${this.$route.params.key}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
||||||
@ -96,8 +96,10 @@ export default {
|
|||||||
if (i === data.events.length-1) {
|
if (i === data.events.length-1) {
|
||||||
if (event.type === "UNHEALTHY") {
|
if (event.type === "UNHEALTHY") {
|
||||||
event.fancyText = "Service is unhealthy";
|
event.fancyText = "Service is unhealthy";
|
||||||
} else {
|
} else if (event.type === "HEALTHY") {
|
||||||
event.fancyText = "Service is healthy";
|
event.fancyText = "Service is healthy";
|
||||||
|
} else if (event.type === "START") {
|
||||||
|
event.fancyText = "Monitoring started";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let nextEvent = data.events[i+1];
|
let nextEvent = data.events[i+1];
|
||||||
@ -121,7 +123,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
generateBadgeImageURL(duration) {
|
generateBadgeImageURL(duration) {
|
||||||
return `${SERVER_URL}/api/v1/badges/uptime/${duration}/${this.serviceStatus.key}`;
|
return `${this.serverUrl}/api/v1/badges/uptime/${duration}/${this.serviceStatus.key}`;
|
||||||
},
|
},
|
||||||
prettifyUptime(uptime) {
|
prettifyUptime(uptime) {
|
||||||
if (!uptime) {
|
if (!uptime) {
|
||||||
@ -140,7 +142,9 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceStatus: {},
|
serviceStatus: {},
|
||||||
events: []
|
events: [],
|
||||||
|
// Since this page isn't at the root, we need to modify the server URL a bit
|
||||||
|
serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
filenameHashing: false,
|
filenameHashing: false,
|
||||||
productionSourceMap: false
|
productionSourceMap: false,
|
||||||
|
outputDir: '../static'
|
||||||
}
|
}
|
3
web/static/css/app.css
Normal file
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
1
web/static/index.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>Health Dashboard | Gatus</title><link href="/css/app.css" rel="preload" as="style"><link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.js"></script><script src="/js/app.js"></script></body></html>
|