aboutsummaryrefslogtreecommitdiff
path: root/internal/server
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-19 04:13:03 -0800
committerFuwn <[email protected]>2026-01-19 04:13:03 -0800
commit412fb771235a03cca73f59c5e249b057bdb06ecb (patch)
tree5503311f9b8b48b934398a405a4252b986fe151d /internal/server
parentfix: Implement response buffering to prevent broken pipe errors (diff)
downloadkaze-412fb771235a03cca73f59c5e249b057bdb06ecb.tar.xz
kaze-412fb771235a03cca73f59c5e249b057bdb06ecb.zip
feat: Add browser timezone option for client-side time display
Diffstat (limited to 'internal/server')
-rw-r--r--internal/server/server.go25
-rw-r--r--internal/server/templates/index.html173
2 files changed, 177 insertions, 21 deletions
diff --git a/internal/server/server.go b/internal/server/server.go
index abd18cb..d0f6fa5 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -127,6 +127,7 @@ type PageData struct {
TickCount int
ShowThemeToggle bool
Timezone string // Timezone for display
+ UseBrowserTimezone bool // Use client-side timezone conversion
}
// GroupData contains data for a monitor group
@@ -190,11 +191,12 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
// Build page data
data := PageData{
- Site: s.config.Site,
- TickMode: s.config.Display.TickMode,
- TickCount: s.config.Display.TickCount,
- ShowThemeToggle: s.config.Display.ShowThemeToggle != nil && *s.config.Display.ShowThemeToggle,
- Timezone: s.config.Display.Timezone,
+ Site: s.config.Site,
+ TickMode: s.config.Display.TickMode,
+ TickCount: s.config.Display.TickCount,
+ ShowThemeToggle: s.config.Display.ShowThemeToggle != nil && *s.config.Display.ShowThemeToggle,
+ Timezone: s.config.Display.Timezone,
+ UseBrowserTimezone: s.config.Display.Timezone == "Browser",
}
overallUp := true
@@ -522,9 +524,12 @@ func templateFuncs() template.FuncMap {
return string(b)
}
- // Convert timestamp to configured timezone
+ // If using browser timezone, include raw timestamp for client-side conversion
+ useBrowserTz := timezone == "Browser"
+
+ // Convert timestamp to configured timezone (fallback for non-JS users)
loc := time.Local
- if timezone != "" && timezone != "Local" {
+ if timezone != "" && timezone != "Local" && timezone != "Browser" {
if l, err := time.LoadLocation(timezone); err == nil {
loc = l
}
@@ -547,6 +552,12 @@ func templateFuncs() template.FuncMap {
data := make(map[string]interface{})
rows := []map[string]string{}
+ // Include raw timestamp for browser timezone conversion
+ if useBrowserTz {
+ data["timestamp"] = tick.Timestamp.Format(time.RFC3339)
+ data["mode"] = mode
+ }
+
switch mode {
case "ping":
header = t.Format("Jan 2, 15:04:05")
diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html
index c351c73..c6e2a81 100644
--- a/internal/server/templates/index.html
+++ b/internal/server/templates/index.html
@@ -56,7 +56,7 @@
</div>
<span class="font-medium">{{.OverallStatus}}</span>
</div>
- <span class="text-sm text-neutral-500 dark:text-neutral-400" data-tooltip='{{.TimezoneTooltip}}'>{{.CurrentTime}}</span>
+ <span class="text-sm text-neutral-500 dark:text-neutral-400" data-tooltip='{{.TimezoneTooltip}}'{{if .UseBrowserTimezone}} data-timestamp="{{.LastUpdated.Format "2006-01-02T15:04:05Z07:00"}}" data-format="datetime"{{end}}>{{.CurrentTime}}</span>
</div>
</div>
@@ -153,11 +153,11 @@
<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}}
+ {{if .IsScheduled}}
+ <p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2"{{if $.UseBrowserTimezone}}{{if .ScheduledStart}} data-timestamp-start="{{.ScheduledStart.Format "2006-01-02T15:04:05Z07:00"}}"{{end}}{{if .ScheduledEnd}} data-timestamp-end="{{.ScheduledEnd.Format "2006-01-02T15:04:05Z07:00"}}"{{end}} data-format="scheduled"{{end}}>
+ 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">
@@ -169,13 +169,13 @@
{{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>
+ <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"{{if $.UseBrowserTimezone}} data-timestamp="{{.Time.Format "2006-01-02T15:04:05Z07:00"}}" data-format="datetime-short"{{end}}>{{formatTime .Time}}</span>
+ </div>
+ <p class="text-neutral-700 dark:text-neutral-300">{{.Message}}</p>
+ </div>
{{end}}
</div>
{{end}}
@@ -188,7 +188,7 @@
<!-- 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 data-tooltip='{{.LastUpdatedTooltip}}'{{if $.UseBrowserTimezone}} data-timestamp="{{$.LastUpdated.Format "2006-01-02T15:04:05Z07:00"}}" data-format="timeago"{{end}}>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>
@@ -258,6 +258,43 @@
let hideTimeout = null;
function renderTooltip(data) {
+ {{if .UseBrowserTimezone}}
+ // Convert tick tooltip timestamps to browser timezone
+ if (data.timestamp && data.mode) {
+ const date = new Date(data.timestamp);
+ const tzInfo = {
+ tzName: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Local',
+ offset: -date.getTimezoneOffset()
+ };
+ const hours = Math.floor(tzInfo.offset / 60);
+ const minutes = Math.abs(tzInfo.offset % 60);
+ const utcOffset = 'UTC' + (hours >= 0 ? '+' : '') + hours + (minutes ? ':' + String(minutes).padStart(2, '0') : '');
+
+ // Update header based on mode
+ if (data.mode === 'ping') {
+ data.header = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
+ date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
+ } else if (data.mode === 'minute') {
+ data.header = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' +
+ date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
+ } else if (data.mode === 'hour') {
+ const hourStr = String(date.getHours()).padStart(2, '0') + ':00';
+ data.header = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + hourStr;
+ } else if (data.mode === 'day') {
+ data.header = date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
+ }
+
+ // Update timezone info in rows
+ if (data.rows) {
+ data.rows.forEach(function(row) {
+ if (row.label === 'Timezone') {
+ row.value = tzInfo.tzName + ' (' + utcOffset + ')';
+ }
+ });
+ }
+ }
+ {{end}}
+
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>';
@@ -352,6 +389,114 @@
// Auto-refresh every 30 seconds
setTimeout(() => location.reload(), 30000);
+
+ // Client-side timezone conversion
+ {{if .UseBrowserTimezone}}
+ (function() {
+ function formatDateTime(date) {
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ const month = months[date.getMonth()];
+ const day = date.getDate();
+ const year = date.getFullYear();
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ return month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes;
+ }
+
+ function formatDateTimeShort(date) {
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ const month = months[date.getMonth()];
+ const day = date.getDate();
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ return month + ' ' + day + ', ' + hours + ':' + minutes;
+ }
+
+ function timeAgo(date) {
+ const seconds = Math.floor((new Date() - date) / 1000);
+ if (seconds < 60) return seconds + ' seconds ago';
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return minutes + ' minute' + (minutes === 1 ? '' : 's') + ' ago';
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return hours + ' hour' + (hours === 1 ? '' : 's') + ' ago';
+ const days = Math.floor(hours / 24);
+ return days + ' day' + (days === 1 ? '' : 's') + ' ago';
+ }
+
+ function getTimezoneInfo() {
+ const date = new Date();
+ const tzName = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Local';
+ const offset = -date.getTimezoneOffset();
+ const hours = Math.floor(offset / 60);
+ const minutes = Math.abs(offset % 60);
+ const utcOffset = 'UTC' + (hours >= 0 ? '+' : '') + hours + (minutes ? ':' + String(minutes).padStart(2, '0') : '');
+ const gmtOffset = 'GMT' + (hours >= 0 ? '+' : '') + hours + (minutes ? ':' + String(minutes).padStart(2, '0') : '');
+ return { tzName: tzName, utcOffset: utcOffset, gmtOffset: gmtOffset };
+ }
+
+ // Convert all timestamps
+ document.querySelectorAll('[data-timestamp]').forEach(function(el) {
+ const timestamp = el.getAttribute('data-timestamp');
+ const format = el.getAttribute('data-format');
+ const date = new Date(timestamp);
+
+ if (format === 'datetime') {
+ el.textContent = formatDateTime(date);
+ } else if (format === 'datetime-short') {
+ el.textContent = formatDateTimeShort(date);
+ } else if (format === 'timeago') {
+ el.textContent = 'Updated ' + timeAgo(date);
+ } else if (format === 'scheduled') {
+ const startTimestamp = el.getAttribute('data-timestamp-start');
+ const endTimestamp = el.getAttribute('data-timestamp-end');
+ let text = 'Scheduled: ';
+ if (startTimestamp) {
+ text += formatDateTimeShort(new Date(startTimestamp));
+ }
+ text += ' - ';
+ if (endTimestamp) {
+ text += formatDateTimeShort(new Date(endTimestamp));
+ }
+ el.textContent = text;
+ }
+
+ // Update tooltips to show browser timezone
+ const tooltipData = el.getAttribute('data-tooltip');
+ if (tooltipData) {
+ try {
+ const data = JSON.parse(tooltipData);
+ if (data.rows) {
+ const tzInfo = getTimezoneInfo();
+ data.rows.forEach(function(row) {
+ if (row.label === 'Timezone') {
+ row.value = tzInfo.tzName;
+ } else if (row.label === 'UTC Offset') {
+ row.value = tzInfo.utcOffset;
+ } else if (row.label === 'GMT Offset') {
+ row.value = tzInfo.gmtOffset;
+ } else if (row.label === 'Date & Time' && format === 'timeago') {
+ // Update "Last Check" tooltip with browser timezone
+ const dtFormat = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+ });
+ row.value = dtFormat.format(date);
+ }
+ });
+ el.setAttribute('data-tooltip', JSON.stringify(data));
+ }
+ } catch (e) {
+ // Ignore JSON parse errors
+ }
+ }
+ });
+ })();
+ {{end}}
</script>
</body>
</html>