statsys/www/index.html

650 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>status - {{ .Title }}</title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/styles.css" />
<style>
* {
box-sizing: border-box;
}
main {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
min-height: 100%;
max-width: 40rem;
padding: 4rem 2rem 1.5rem;
background-color: white;
z-index: 1;
outline: 1px solid rgba(0, 0, 0, 0.04);
}
h1 {
font-weight: 800;
margin-top: 1rem;
margin-bottom: 1rem;
font-size: 1.5rem;
}
hr {
border: 0;
border-bottom: 2px dotted rgba(0, 0, 0, 0.1);
}
p {
margin-top: 4px;
line-height: 1.5rem;
}
#emoticon {
display: flex;
align-items: end;
height: 4.375rem;
}
#emoticon span {
font-size: 3rem;
font-weight: 700;
}
#overall-status {
background-color: black;
color: white;
padding: 0.5rem;
font-size: 0.875rem;
width: fit-content;
}
#pattern {
flex-grow: 1;
background-image: repeating-linear-gradient(-45deg, #000, #000 10px, transparent 0px, transparent 20px);
}
#caption {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.25rem;
margin-bottom: 1.25rem;
}
#last-updated {
color: rgba(0, 0, 0, 0.6);
font-size: 0.875rem;
margin: 0;
}
#legend {
display: flex;
justify-content: flex-end;
gap: 1rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.75rem;
pointer-events: none;
}
.legend-item .status-bar {
width: 1rem;
height: 1rem;
}
#services {
display: flex;
flex-direction: column;
gap: 2.5rem;
margin-bottom: 5rem;
}
.info {
display: flex;
align-items: end;
justify-content: space-between;
}
.service-name {
margin: 0;
font-size: 1.25rem;
text-decoration: none;
color: black;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.2rem;
}
.service-name svg {
height: 1rem;
color: rgba(0, 0, 0, 0.2);
}
.service-name:hover svg {
color: rgba(0, 0, 0, 0.8);
}
.status {
font-family: "JetBrains Mono", monospace;
font-weight: 600;
font-size: 0.75rem;
}
.bars-container {
display: flex;
flex-direction: column;
align-items: end;
}
.bars {
position: relative;
display: flex;
justify-content: end;
width: 100%;
}
.bars > div {
display: flex;
justify-content: center;
width: calc(100% / 30);
height: 2.5rem;
position: relative;
}
.status-bar {
background-color: black;
border: 1px solid black;
width: calc(100% - 0.188rem);
height: 100%;
transition: transform 100ms ease-in-out;
position: relative;
z-index: 0;
border-radius: 3px;
}
.bars > div:hover .status-bar {
transform: translateY(-3px);
filter: brightness(0.9);
}
.bars > div:hover .status-bar::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
margin-bottom: 0.5rem;
font-family: "JetBrains Mono", monospace;
font-weight: 600;
background-color: black;
color: white;
padding: 0.45rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
transform: translateX(-50%);
white-space: nowrap;
z-index: 10;
}
.status-bar.Unknown {
background-color: transparent;
background: radial-gradient(circle at center, rgb(235, 235, 235) 3px, transparent 3px);
border: 0;
}
.status-bar.Degraded {
background-color: white;
background-image: linear-gradient(45deg, black 25%, transparent 25%, transparent 50%, black 50%, black 75%, transparent 75%, transparent);
background-size: 8px 8px;
}
.status-bar.Offline {
background-color: white;
}
.status-bar.Offline::before {
content: "✕";
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: 700;
color: black;
pointer-events: none;
overflow: hidden;
}
.bars-footer {
position: relative;
display: flex;
justify-content: space-between;
font-size: 0.75rem;
font-family: "JetBrains Mono", monospace;
margin-top: 0.5rem;
color: rgba(0, 0, 0, 0.4);
width: 100%;
}
.bars-footer span {
padding: 0 0.5rem;
}
.bars-footer hr {
flex-grow: 1;
border-color: rgba(0, 0, 0, 0.1);
}
.uptime-percentage {
position: absolute;
left: 50%;
transform: translateX(-50%);
background-color: white;
color: rgba(0, 0, 0, 0.5);
}
footer {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
footer a {
color: rgba(0, 0, 0, 0.3);
font-size: 0.875rem;
text-decoration: none;
}
footer a:hover {
color: black;
}
.combobox {
position: relative;
width: 8rem;
}
.combobox-trigger {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 1.75rem;
font-family: "Inter", sans-serif;
font-size: 0.825rem;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.5);
border-radius: 4px;
outline: none;
padding: 0;
padding-left: 0.5rem;
cursor: pointer;
transition: background-color 100ms ease;
}
.combobox-trigger:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.combobox-trigger div {
display: flex;
justify-content: center;
align-items: center;
border-left: 1px solid rgba(0, 0, 0, 0.5);
}
.combobox-trigger.open div svg {
transform: rotate(180deg);
}
.combobox-dropdown {
position: absolute;
width: 100%;
background-color: white;
border: 1px solid black;
display: none;
flex-direction: column;
z-index: 100;
border-radius: 4px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.combobox-dropdown.open {
display: flex;
}
.combobox-option {
padding: 0.35rem 0.5rem;
cursor: pointer;
color: black;
font-size: 0.825rem;
text-decoration: none;
}
.combobox-option.selected {
background-color: rgba(0, 0, 0, 0.08);
}
.combobox-option:hover {
background-color: black;
color: white;
}
@media (max-width: 640px) {
main {
padding: 3rem 1rem 2rem;
}
#emoticon {
font-size: 2.5rem;
}
h1 {
font-size: 1.25rem;
}
h2 {
font-size: 1.125rem;
}
#overall-status {
font-size: 0.8125rem;
}
.status {
font-size: 0.6875rem;
}
.bars {
gap: 0.15rem;
}
.status-bar {
height: 2rem;
}
.legend-item {
font-size: 0.6875rem;
}
.legend-item .status-bar {
width: 0.875rem;
height: 0.875rem;
}
.bars-footer {
font-size: 0.6875rem;
}
#legend {
gap: 0.75rem;
}
}
@media (max-width: 480px) {
main {
padding: 2rem 0.875rem 1.5rem;
}
#emoticon {
font-size: 2rem;
}
h1 {
font-size: 1.125rem;
}
h2 {
font-size: 1rem;
}
.bars {
gap: 0.125rem;
}
.status-bar {
height: 1.75rem;
}
.status-bar:hover::after {
font-size: 0.6rem;
padding: 0.35rem 0.4rem;
}
#services {
gap: 2rem;
}
}
</style>
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
</head>
<body>
<main>
<div id="emoticon">
<span>{{ if .IsOperational }} ( • ⩊ • ) {{ else }} ( 𖦹 𖦹 ) {{ end }}</span>
</div>
<h1>status — {{ .Title }}</h1>
{{ if .EnableThemeSwitcher }}
<div class="combobox" style="position: absolute; right: 2rem; top: 2rem">
<button class="combobox-trigger">
<span
>{{ if eq .Theme "colored-dark" }} colored dark {{ else if eq .Theme "ctp-latte" }} ctp latte {{ else if eq .Theme "ctp-frappe" }} ctp
frappé {{ else if eq .Theme "ctp-macchiato" }} ctp macchiato {{ else if eq .Theme "ctp-mocha" }} ctp mocha {{ else }} {{ .Theme }} {{ end
}}</span
>
<!-- arrow icon -->
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m7 10l5 5m0 0l5-5" />
</svg>
</div>
</button>
<div class="combobox-dropdown" style="top: calc(100% + 0.25rem)">
<a href="/?theme=monochrome&view={{ .View }}" class='combobox-option {{ if eq .Theme "monochrome" }}selected{{ end }}'>monochrome</a>
<a href="/?theme=inverted&view={{ .View }}" class='combobox-option {{ if eq .Theme "inverted" }}selected{{ end }}'>inverted</a>
<a href="/?theme=color&view={{ .View }}" class='combobox-option {{ if eq .Theme "color" }}selected{{ end }}'>color</a>
<a href="/?theme=colored-dark&view={{ .View }}" class='combobox-option {{ if eq .Theme "colored-dark" }}selected{{ end }}'>colored dark</a>
<a href="/?theme=ctp-latte&view={{ .View }}" class='combobox-option {{ if eq .Theme "ctp-latte" }}selected{{ end }}'>ctp latte</a>
<a href="/?theme=ctp-frappe&view={{ .View }}" class='combobox-option {{ if eq .Theme "ctp-frappe" }}selected{{ end }}'>ctp frappé</a>
<a href="/?theme=ctp-macchiato&view={{ .View }}" class='combobox-option {{ if eq .Theme "ctp-macchiato" }}selected{{ end }}'
>ctp macchiato</a
>
<a href="/?theme=ctp-mocha&view={{ .View }}" class='combobox-option {{ if eq .Theme "ctp-mocha" }}selected{{ end }}'>ctp mocha</a>
</div>
</div>
{{ end }}
<div style="display: flex">
<div id="overall-status">
<span style="font-weight: 500; margin-right: 0.15rem">{{ if .IsOperational }} ✔ {{ else }} ✖ {{ end }}</span>
<span>{{ if .IsOperational }} All services are operational! {{ else }} We're experiencing some problems! {{ end }}</span>
</div>
<div id="pattern"></div>
</div>
<div id="caption">
<p id="last-updated">Last updated: ? ago</p>
<div id="legend">
<div class="legend-item">
<div class="status-bar Offline"></div>
<span>Offline</span>
</div>
<div class="legend-item">
<div class="status-bar Degraded" style="background-position: 3px 3px"></div>
<span>Degraded</span>
</div>
<div class="legend-item">
<div class="status-bar Online"></div>
<span>Online</span>
</div>
</div>
</div>
<div id="services">
{{ range $serviceIndex, $serviceStatus := .Services }}
<div>
<div class="info">
<a href="{{ $serviceStatus.Url }}" class="service-name">
{{ $serviceStatus.Name }}
<!-- link icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 256 256">
<path
fill="currentColor"
d="M228 104a12 12 0 0 1-24 0V69l-59.51 59.51a12 12 0 0 1-17-17L187 52h-35a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12Zm-44 24a12 12 0 0 0-12 12v64H52V84h64a12 12 0 0 0 0-24H48a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h128a20 20 0 0 0 20-20v-68a12 12 0 0 0-12-12"
/>
</svg>
</a>
<span class="status">{{ $serviceStatus.Status | ToUpper }}</span>
</div>
<hr />
<div class="bars-container">
{{ if eq $.View "minutes" }}
<div class="bars">
{{ range $timelineIndex, $timelineStatus := $serviceStatus.MinuteTimeline }}
<div>
<div
class="status-bar {{ $timelineStatus.Status }}"
data-tooltip="{{ $timelineStatus.FormattedTime }} • {{ $timelineStatus.Status }}"
></div>
</div>
{{ end }}
</div>
<div class="bars-footer">
<span>30 minutes ago</span>
<hr />
<span class="uptime-percentage">{{ $serviceStatus.MinuteUptime }}% uptime</span>
<span>now</span>
</div>
{{ else if eq $.View "hours" }}
<div class="bars">
{{ range $timelineIndex, $timelineStatus := $serviceStatus.HourTimeline }}
<div>
<div
class="status-bar {{ $timelineStatus.Status }}"
data-tooltip="{{ $timelineStatus.FormattedTime }} • {{ $timelineStatus.Status }}"
></div>
</div>
{{ end }}
</div>
<div class="bars-footer">
<span>24 hours ago</span>
<hr />
<span class="uptime-percentage">{{ $serviceStatus.HourUptime }}% uptime</span>
<span>now</span>
</div>
{{ else }}
<div class="bars">
{{ range $timelineIndex, $timelineStatus := $serviceStatus.DayTimeline }}
<div>
<div
class="status-bar {{ $timelineStatus.Status }}"
data-tooltip="{{ $timelineStatus.FormattedTime }} • {{ $timelineStatus.Status }}"
></div>
</div>
{{ end }}
</div>
<div class="bars-footer">
<span>30 days ago</span>
<hr />
<span class="uptime-percentage">{{ $serviceStatus.DayUptime }}% uptime</span>
<span>today</span>
</div>
{{ end }}
</div>
</div>
{{ else }}
<p>No services configured.</p>
{{ end }}
</div>
<footer>
<div class="combobox">
<button class="combobox-trigger">
<span>{{ if eq .View "minutes" }} 30 minutes {{ else if eq .View "hours" }} 24 hours {{ else }} 30 days {{ end }}</span>
<!-- arrow icon -->
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m7 10l5 5m0 0l5-5" />
</svg>
</div>
</button>
<div class="combobox-dropdown" style="bottom: calc(100% + 0.25rem)">
<a href="/?view=minutes&theme={{ .Theme }}" class='combobox-option {{ if eq .View "minutes" }}selected{{ end }}'>30 minutes</a>
<a href="/?view=hours&theme={{ .Theme }}" class='combobox-option {{ if eq .View "hours" }}selected{{ end }}'>24 hours</a>
<a href="/?view=days&theme={{ .Theme }}" class='combobox-option {{ if eq .View "days" }}selected{{ end }}'>30 days</a>
</div>
</div>
<a href="{{ .LinkUrl }}">{{ .LinkText }}</a>
</footer>
</main>
<script>
const lastUpdatedText = document.querySelector("#last-updated");
function update() {
const seconds = Math.floor((Date.now() - new Date(Number("{{ .LastUpdated }}"))) / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(seconds / 3600);
const days = Math.floor(seconds / 86400);
if (days >= 1) {
lastUpdatedText.textContent = `Last updated: ${days}d ago`;
} else if (hours >= 1) {
lastUpdatedText.textContent = `Last updated: ${hours}h ago`;
} else if (minutes >= 1) {
lastUpdatedText.textContent = `Last updated: ${minutes}m ago`;
} else {
lastUpdatedText.textContent = `Last updated: now`;
}
}
update();
setInterval(update, 60000);
// combobox
const triggers = document.querySelectorAll(".combobox-trigger");
triggers.forEach((trigger) => {
trigger.addEventListener("click", () => {
const parent = trigger.parentElement;
const dropdown = parent.querySelector(".combobox-dropdown");
trigger.classList.toggle("open");
dropdown.classList.toggle("open");
});
});
</script>
</body>
</html>