Display local time in the browser

This commit is contained in:
Dan Milne
2025-11-10 07:53:20 +11:00
parent ce2feb4180
commit 5851e04e06
9 changed files with 85 additions and 15 deletions

View File

@@ -99,15 +99,17 @@ class AnalyticsController < ApplicationController
.group("DATE_TRUNC('hour', timestamp)")
.count
# Convert to chart format
# Convert to chart format - keep everything in UTC for consistency
timeline_data = (0..23).map do |hour_ago|
hour_time = hour_ago.hours.ago
hour_key = hour_time.strftime("%Y-%m-%d %H:00:00")
hour_key = hour_time.utc.beginning_of_hour
{
time: hour_time.strftime("%H:00"),
# Store as ISO string for JavaScript to handle timezone conversion
time_iso: hour_time.iso8601,
total: events_by_hour[hour_key] || 0
}
end.reverse
end
# Action distribution for pie chart
action_distribution = @event_breakdown.map do |action, count|

View File

@@ -0,0 +1,54 @@
// Timeline controller for handling timezone conversion and animations
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["row", "time", "bar"]
connect() {
this.maxTotal = this.calculateMaxTotal()
this.updateTimeline()
}
calculateMaxTotal() {
const totals = this.rowTargets.map(row => parseInt(row.dataset.total))
return Math.max(...totals, 1)
}
updateTimeline() {
this.rowTargets.forEach((row, index) => {
this.updateRow(row, index)
})
}
updateRow(row, index) {
const timeIso = row.dataset.timeIso
const total = parseInt(row.dataset.total)
const timeElement = this.timeTargets.find(target => target.closest('[data-timeline-target="row"]') === row)
const barElement = this.barTargets.find(target => target.closest('[data-timeline-target="row"]') === row)
// Convert ISO time to local time
const date = new Date(timeIso)
const localTime = date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false
})
timeElement.textContent = localTime
timeElement.title = date.toLocaleString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
})
// Animate the bar width with a slight delay for each row
const barWidth = Math.max((total / this.maxTotal) * 100, 5)
setTimeout(() => {
barElement.style.width = `${barWidth}%`
}, 100 + (index * 50))
}
}

View File

@@ -63,6 +63,10 @@ class Rule < ApplicationRecord
end
# Network-specific methods
def network_range?
network_range.present?
end
def cidr
network_rule? ? network_range&.cidr : conditions&.dig("cidr")
end

View File

@@ -155,24 +155,26 @@
<!-- Events Timeline Chart -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">Events Timeline (Last 24 Hours)</h3>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">Events Timeline (Last 24 Hours)</h3>
<span class="text-sm text-gray-500">Times shown in your local timezone</span>
</div>
</div>
<div class="p-6">
<div class="space-y-4">
<div class="space-y-4" data-controller="timeline">
<% @chart_data[:timeline].each do |data| %>
<div class="flex items-center">
<div class="w-16 text-sm text-gray-500"><%= data[:time] %></div>
<div class="flex items-center" data-timeline-target="row" data-time-iso="<%= data[:time_iso] %>" data-total="<%= data[:total] %>">
<div class="w-20 text-sm text-gray-500" data-timeline-target="time">--:--</div>
<div class="flex-1 mx-4">
<div class="bg-gray-200 rounded-full h-4">
<div class="bg-blue-600 h-4 rounded-full"
style="width: <%= [((data[:total].to_f / [@chart_data[:timeline].map { |d| d[:total] }.max, 1].max) * 100), 5].max %>%">
</div>
<div class="bg-blue-600 h-4 rounded-full" data-timeline-target="bar" style="width: 0%"></div>
</div>
</div>
<div class="w-12 text-sm text-gray-900 text-right"><%= data[:total] %></div>
</div>
<% end %>
</div>
</div>
</div>
</div>

View File

@@ -24,7 +24,7 @@
<div>
<%= form.label :waf_action, "Action", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :waf_action,
options_for_select([['All', ''], ['Allow', 'allow'], ['Block', 'block'], ['Challenge', 'challenge']], params[:waf_action]),
options_for_select([['All', ''], ['Allow', 'allow'], ['Deny', 'deny'], ['Redirect', 'redirect'], ['Challenge', 'challenge']], params[:waf_action]),
{ }, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
</div>
<div>
@@ -94,7 +94,8 @@
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
<%= case event.waf_action
when 'allow' then 'bg-green-100 text-green-800'
when 'deny', 'block' then 'bg-red-100 text-red-800'
when 'deny' then 'bg-red-100 text-red-800'
when 'redirect' then 'bg-blue-100 text-blue-800'
when 'challenge' then 'bg-yellow-100 text-yellow-800'
else 'bg-gray-100 text-gray-800'
end %>">