aboutsummaryrefslogtreecommitdiff
path: root/internal/server/templates
diff options
context:
space:
mode:
Diffstat (limited to 'internal/server/templates')
-rw-r--r--internal/server/templates/index.html357
1 files changed, 357 insertions, 0 deletions
diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html
new file mode 100644
index 0000000..c351c73
--- /dev/null
+++ b/internal/server/templates/index.html
@@ -0,0 +1,357 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{.Site.Name}}</title>
+ <meta name="description" content="{{.Site.Description}}">
+ {{if .Site.Favicon}}<link rel="icon" href="{{.Site.Favicon}}">{{end}}
+ <link rel="stylesheet" href="/static/style.css">
+ <script>
+ // Theme detection
+ if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
+ document.documentElement.classList.add('dark');
+ }
+ </script>
+</head>
+<body class="bg-neutral-50 dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 min-h-screen font-mono">
+ <div class="max-w-4xl mx-auto px-4 py-8 sm:py-12">
+ <!-- Header -->
+ <header class="mb-8 sm:mb-12">
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-3">
+ {{if .Site.Logo}}
+ <img src="{{.Site.Logo}}" alt="Logo" class="h-8 w-8">
+ {{end}}
+ <div>
+ <h1 class="text-xl sm:text-2xl font-bold tracking-tight">{{.Site.Name}}</h1>
+ <p class="text-sm text-neutral-500 dark:text-neutral-400">{{.Site.Description}}</p>
+ </div>
+ </div>
+ {{if .ShowThemeToggle}}
+ <button onclick="toggleTheme()" class="p-2 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors" aria-label="Toggle theme">
+ <svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
+ </svg>
+ <svg class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
+ </svg>
+ </button>
+ {{end}}
+ </div>
+ </header>
+
+ <!-- Overall Status Banner -->
+ <div class="mb-8 p-4 rounded-lg border {{if eq .OverallStatus "All Systems Operational"}}bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-900{{else if eq .OverallStatus "Partial Outage"}}bg-yellow-50 dark:bg-yellow-950/30 border-yellow-200 dark:border-yellow-900{{else}}bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-900{{end}}">
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-3">
+ <div class="flex-shrink-0">
+ {{if eq .OverallStatus "All Systems Operational"}}
+ <div class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div>
+ {{else if eq .OverallStatus "Partial Outage"}}
+ <div class="w-3 h-3 rounded-full bg-yellow-500 animate-pulse"></div>
+ {{else}}
+ <div class="w-3 h-3 rounded-full bg-red-500 animate-pulse"></div>
+ {{end}}
+ </div>
+ <span class="font-medium">{{.OverallStatus}}</span>
+ </div>
+ <span class="text-sm text-neutral-500 dark:text-neutral-400" data-tooltip='{{.TimezoneTooltip}}'>{{.CurrentTime}}</span>
+ </div>
+ </div>
+
+ <!-- Monitor Groups -->
+ <div class="space-y-6">
+ {{range $groupIndex, $group := .Groups}}
+ <section class="border border-neutral-200 dark:border-neutral-800 rounded-lg overflow-hidden" data-group="{{$group.Name}}">
+ <div class="px-4 py-3 bg-neutral-100 dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-800 cursor-pointer hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors" onclick="toggleGroup('{{$group.Name}}')">
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-3">
+ <svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400 transition-transform" data-group-icon="{{$group.Name}}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
+ </svg>
+ <h2 class="font-semibold text-sm uppercase tracking-wider text-neutral-600 dark:text-neutral-400">{{$group.Name}}</h2>
+ </div>
+ {{if $group.ShowGroupUptime}}
+ <span class="text-sm font-medium {{if ge $group.GroupUptime 99.0}}text-emerald-600 dark:text-emerald-400{{else if ge $group.GroupUptime 95.0}}text-yellow-600 dark:text-yellow-400{{else}}text-red-600 dark:text-red-400{{end}}">{{formatUptime $group.GroupUptime}}</span>
+ {{end}}
+ </div>
+ </div>
+ <div class="divide-y divide-neutral-200 dark:divide-neutral-800 group-content" data-group-content="{{$group.Name}}" data-default-collapsed="{{$group.DefaultCollapsed}}">
+ {{range .Monitors}}
+ <div class="p-4 hover:bg-neutral-100/50 dark:hover:bg-neutral-900/50 transition-colors">
+ <div class="flex items-start justify-between gap-4">
+ <div class="flex-1 min-w-0">
+ <div class="flex items-center gap-2 mb-2">
+ {{if eq .Status "up"}}
+ <div class="w-2 h-2 rounded-full bg-emerald-500 flex-shrink-0"></div>
+ {{else if eq .Status "degraded"}}
+ <div class="w-2 h-2 rounded-full bg-yellow-500 flex-shrink-0"></div>
+ {{else if eq .Status "down"}}
+ <div class="w-2 h-2 rounded-full bg-red-500 flex-shrink-0"></div>
+ {{else}}
+ <div class="w-2 h-2 rounded-full bg-neutral-400 flex-shrink-0"></div>
+ {{end}}
+ <span class="font-medium truncate">{{.Name}}</span>
+ <span class="text-xs px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 uppercase">{{.Type}}</span>
+ </div>
+ <div class="flex items-center gap-4 text-xs text-neutral-500 dark:text-neutral-400">
+ <span>{{formatDuration .ResponseTime}}</span>
+ {{if gt .SSLDaysLeft 0}}
+ <span class="{{if lt .SSLDaysLeft 14}}text-yellow-600 dark:text-yellow-400{{else if lt .SSLDaysLeft 7}}text-red-600 dark:text-red-400{{end}}" data-tooltip='{{.SSLTooltip}}'>SSL: {{.SSLDaysLeft}}d</span>
+ {{end}}
+ {{if .LastError}}
+ {{if .LastError}}<span class="text-red-600 dark:text-red-400 truncate max-w-[200px]" data-tooltip='{"header":"Last Error","error":"{{.LastError}}"}'>{{.LastError}}</span>{{end}}
+ {{end}}
+ </div>
+ </div>
+ <div class="flex items-center gap-2 flex-shrink-0">
+ <span class="text-sm font-medium {{if ge .UptimePercent 99.0}}text-emerald-600 dark:text-emerald-400{{else if ge .UptimePercent 95.0}}text-yellow-600 dark:text-yellow-400{{else}}text-red-600 dark:text-red-400{{end}}">{{formatUptime .UptimePercent}}</span>
+ </div>
+ </div>
+ <!-- History Bar -->
+ <div class="mt-3 flex gap-px">
+ {{range .Ticks}}
+ <div class="flex-1 h-6 rounded-sm {{tickColor .}}" data-tooltip='{{tickTooltipData . $.TickMode $.Timezone}}'></div>
+ {{else}}
+ {{range seq $.TickCount}}
+ <div class="flex-1 h-6 rounded-sm bg-neutral-200 dark:bg-neutral-800" data-tooltip='{"header":"No data"}'></div>
+ {{end}}
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ </div>
+ </section>
+ {{end}}
+ </div>
+
+ <!-- Incidents -->
+ {{if .Incidents}}
+ <section class="mt-8">
+ <h2 class="text-lg font-semibold mb-4">Incidents</h2>
+ <div class="space-y-4">
+ {{range .Incidents}}
+ <div class="border border-neutral-200 dark:border-neutral-800 rounded-lg overflow-hidden">
+ <div class="p-4 {{if .IsActive}}bg-yellow-50 dark:bg-yellow-950/20{{else}}bg-neutral-50 dark:bg-neutral-900/50{{end}}">
+ <div class="flex items-start justify-between gap-4">
+ <div>
+ <div class="flex items-center gap-2 mb-1">
+ {{if eq .Status "resolved"}}
+ <svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
+ </svg>
+ {{else if eq .Status "scheduled"}}
+ <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
+ </svg>
+ {{else}}
+ <svg class="w-4 h-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
+ </svg>
+ {{end}}
+ <span class="font-medium">{{.Title}}</span>
+ </div>
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">{{.Message}}</p>
+ {{if .IsScheduled}}
+ <p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
+ Scheduled: {{if .ScheduledStart}}{{formatTime .ScheduledStart}}{{end}} - {{if .ScheduledEnd}}{{formatTime .ScheduledEnd}}{{end}}
+ </p>
+ {{end}}
+ </div>
+ <div class="flex-shrink-0">
+ <span class="text-xs px-2 py-1 rounded-full {{if eq .Status "resolved"}}bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300{{else if eq .Status "scheduled"}}bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300{{else if eq .Status "investigating"}}bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300{{else if eq .Status "identified"}}bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300{{else}}bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300{{end}} capitalize">
+ {{.Status}}
+ </span>
+ </div>
+ </div>
+ </div>
+ {{if .Updates}}
+ <div class="border-t border-neutral-200 dark:border-neutral-800 p-4 space-y-3 bg-white dark:bg-neutral-950">
+ {{range .Updates}}
+ <div class="text-sm">
+ <div class="flex items-center gap-2 text-neutral-500 dark:text-neutral-400 mb-1">
+ <span class="capitalize font-medium">{{.Status}}</span>
+ <span class="text-xs">{{formatTime .Time}}</span>
+ </div>
+ <p class="text-neutral-700 dark:text-neutral-300">{{.Message}}</p>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+ </section>
+ {{end}}
+
+ <!-- Footer -->
+ <footer class="mt-12 pt-6 border-t border-neutral-200 dark:border-neutral-800">
+ <div class="flex items-center justify-between text-xs text-neutral-500 dark:text-neutral-400">
+ <span data-tooltip='{{.LastUpdatedTooltip}}'>Updated {{timeAgo .LastUpdated}}</span>
+ <span>Powered by <a href="https://github.com/Fuwn/kaze" class="hover:text-neutral-900 dark:hover:text-neutral-100 underline underline-offset-2">Kaze</a></span>
+ </div>
+ </footer>
+ </div>
+
+ <!-- Tooltip container -->
+ <div id="tooltip" class="tooltip"></div>
+
+ <script>
+ function toggleTheme() {
+ if (document.documentElement.classList.contains('dark')) {
+ document.documentElement.classList.remove('dark');
+ localStorage.theme = 'light';
+ } else {
+ document.documentElement.classList.add('dark');
+ localStorage.theme = 'dark';
+ }
+ }
+
+ // Group collapse/expand functionality
+ function toggleGroup(groupName) {
+ const content = document.querySelector('[data-group-content="' + groupName + '"]');
+ const icon = document.querySelector('[data-group-icon="' + groupName + '"]');
+
+ if (!content || !icon) return;
+
+ const isCollapsed = content.classList.contains('collapsed');
+
+ if (isCollapsed) {
+ content.classList.remove('collapsed');
+ icon.classList.remove('rotated');
+ localStorage.setItem('group-' + groupName, 'expanded');
+ } else {
+ content.classList.add('collapsed');
+ icon.classList.add('rotated');
+ localStorage.setItem('group-' + groupName, 'collapsed');
+ }
+ }
+
+ // Initialize group states on page load
+ (function initGroupStates() {
+ document.querySelectorAll('[data-group-content]').forEach(function(content) {
+ const groupName = content.getAttribute('data-group-content');
+ const defaultCollapsed = content.getAttribute('data-default-collapsed') === 'true';
+ const savedState = localStorage.getItem('group-' + groupName);
+ const icon = document.querySelector('[data-group-icon="' + groupName + '"]');
+
+ // Determine initial state: localStorage > config default > expanded
+ let shouldCollapse = false;
+ if (savedState !== null) {
+ shouldCollapse = savedState === 'collapsed';
+ } else {
+ shouldCollapse = defaultCollapsed;
+ }
+
+ if (shouldCollapse) {
+ content.classList.add('collapsed');
+ if (icon) icon.classList.add('rotated');
+ }
+ });
+ })();
+
+ // Custom tooltip handling
+ (function() {
+ const tooltip = document.getElementById('tooltip');
+ let currentTarget = null;
+ let hideTimeout = null;
+
+ function renderTooltip(data) {
+ let html = '<span class="tooltip-header">' + data.header + '</span>';
+ if (data.error) {
+ html += '<div style="white-space:normal;max-width:260px;margin-top:0.25rem">' + data.error + '</div>';
+ } else if (data.rows) {
+ data.rows.forEach(function(row) {
+ html += '<div class="tooltip-row">';
+ html += '<span class="tooltip-label">' + row.label + '</span>';
+ html += '<span class="tooltip-value ' + (row.class || '') + '">' + row.value + '</span>';
+ html += '</div>';
+ });
+ }
+ return html;
+ }
+
+ function showTooltip(e) {
+ const target = e.target.closest('[data-tooltip]');
+ if (!target) return;
+
+ clearTimeout(hideTimeout);
+ currentTarget = target;
+
+ // Parse and render tooltip content
+ try {
+ const data = JSON.parse(target.getAttribute('data-tooltip'));
+ tooltip.innerHTML = renderTooltip(data);
+ } catch (err) {
+ tooltip.innerHTML = target.getAttribute('data-tooltip');
+ }
+
+ // Make visible to calculate dimensions
+ tooltip.classList.add('visible');
+
+ // Position tooltip
+ const rect = target.getBoundingClientRect();
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ // Calculate horizontal position (center above the element)
+ let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
+
+ // Keep tooltip within viewport horizontally
+ const padding = 8;
+ if (left < padding) {
+ left = padding;
+ } else if (left + tooltipRect.width > window.innerWidth - padding) {
+ left = window.innerWidth - tooltipRect.width - padding;
+ }
+
+ // Calculate vertical position (above element by default)
+ let top = rect.top - tooltipRect.height - 8;
+
+ // If not enough space above, show below
+ if (top < padding) {
+ top = rect.bottom + 8;
+ tooltip.classList.add('tooltip-top');
+ } else {
+ tooltip.classList.remove('tooltip-top');
+ }
+
+ tooltip.style.left = left + 'px';
+ tooltip.style.top = top + 'px';
+ }
+
+ function hideTooltip() {
+ hideTimeout = setTimeout(() => {
+ tooltip.classList.remove('visible');
+ currentTarget = null;
+ }, 100);
+ }
+
+ // Event delegation for tooltip triggers
+ document.addEventListener('mouseenter', showTooltip, true);
+ document.addEventListener('mouseleave', function(e) {
+ if (e.target.closest('[data-tooltip]')) {
+ hideTooltip();
+ }
+ }, true);
+
+ // Handle touch devices
+ document.addEventListener('touchstart', function(e) {
+ const target = e.target.closest('[data-tooltip]');
+ if (target) {
+ if (currentTarget === target) {
+ hideTooltip();
+ } else {
+ showTooltip(e);
+ }
+ } else {
+ hideTooltip();
+ }
+ }, { passive: true });
+ })();
+
+ // Auto-refresh every 30 seconds
+ setTimeout(() => location.reload(), 30000);
+ </script>
+</body>
+</html>