diff options
Diffstat (limited to 'internal/server/templates')
| -rw-r--r-- | internal/server/templates/index.html | 357 |
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> |