Tidy up homepage and navigation

This commit is contained in:
Dan Milne
2025-11-09 20:58:13 +11:00
parent c9e2992fe0
commit 1f4428348d
56 changed files with 2822 additions and 955 deletions

View File

@@ -0,0 +1,343 @@
<% content_for :title, "Analytics Dashboard - Baffle Hub" %>
<div class="space-y-6" data-controller="dashboard" data-dashboard-period-value="<%= @time_period %>">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">Analytics Dashboard</h1>
<p class="mt-2 text-gray-600">Overview of WAF events, rules, and network activity</p>
</div>
<div class="flex items-center space-x-4">
<!-- Auto-refresh indicator -->
<div class="flex items-center text-sm text-gray-500">
<svg class="animate-spin h-4 w-4 mr-2 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Auto-refreshing
</div>
<!-- Time Period Selector -->
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">Time Period:</span>
<div class="flex bg-white rounded-md shadow-sm border border-gray-300">
<%= link_to "1H", analytics_path(period: :hour),
class: time_period_class(:hour),
data: { action: "click->dashboard#periodChanged", period: "hour" } %>
<%= link_to "24H", analytics_path(period: :day),
class: time_period_class(:day),
data: { action: "click->dashboard#periodChanged", period: "day" } %>
<%= link_to "1W", analytics_path(period: :week),
class: time_period_class(:week),
data: { action: "click->dashboard#periodChanged", period: "week" } %>
<%= link_to "1M", analytics_path(period: :month),
class: time_period_class(:month),
data: { action: "click->dashboard#periodChanged", period: "month" } %>
</div>
</div>
</div>
</div>
<!-- Key Statistics Cards -->
<div class="space-y-6">
<div id="dashboard-stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Events -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.92 6-4.72 7.28l-.28.12V22l-5-5 5-5v2.4c2.21-.47 3.88-2.35 3.88-4.65 0-2.76-2.24-5-5-5-1.3 0-2.47.5-3.36 1.31L9 6.36C10.11 5.26 11.49 4.56 13 4.56V2.05c0-.45.54-.67.85-.35l8.78 8.78c.2.2.2.51 0 .71l-8.78 8.78c-.31.31-.85.1-.85-.35v-2.02c-5.05-.5-9-4.76-9-9.93 0-4.08 2.73-7.54 6.58-8.67L13 2.05z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_events) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">Last <%= @time_period.to_s.humanize %></span>
</div>
</div>
</div>
<!-- Active Rules -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_rules) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">Enabled</span>
<% if @system_health[:disabled_rules] > 0 %>
<span class="text-gray-500"> · <%= @system_health[:disabled_rules] %> disabled</span>
<% end %>
</div>
</div>
</div>
<!-- Network Ranges with Events -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Network Ranges</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@network_ranges_with_events) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-purple-600 font-medium">of <%= number_with_delimiter(@total_network_ranges) %> total</span>
</div>
</div>
</div>
<!-- System Health -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">System Health</dt>
<dd class="text-lg font-medium text-gray-900">Normal</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">All systems operational</span>
</div>
</div>
</div>
</div>
<!-- Charts and Detailed Analytics -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 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>
<div class="p-6">
<div class="space-y-4">
<% @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-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>
</div>
<div class="w-12 text-sm text-gray-900 text-right"><%= data[:total] %></div>
</div>
<% end %>
</div>
</div>
</div>
<!-- Event Actions Breakdown -->
<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">Event Actions</h3>
</div>
<div class="p-6">
<% if @chart_data[:actions].any? %>
<div class="space-y-4">
<% @chart_data[:actions].each do |action| %>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="w-4 h-4 rounded mr-3
<%= case action[:action]
when 'Allow' then 'bg-green-500'
when 'Deny', 'Block' then 'bg-red-500'
when 'Challenge' then 'bg-yellow-500'
else 'bg-gray-500'
end %>">
</div>
<span class="text-sm font-medium text-gray-900"><%= action[:action] %></span>
</div>
<div class="flex items-center">
<span class="text-sm text-gray-600 mr-2"><%= number_with_delimiter(action[:count]) %></span>
<span class="text-sm text-gray-500">(<%= action[:percentage] %>%)</span>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-center py-8">No events in the selected time period</p>
<% end %>
</div>
</div>
</div>
<!-- Secondary Information Rows -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Top Countries -->
<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">Top Countries</h3>
</div>
<div class="p-6">
<% if @top_countries.any? %>
<div class="space-y-3">
<% @top_countries.first(5).each do |country, count| %>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-900"><%= country %></span>
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(count) %></span>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-center py-4">No country data available</p>
<% end %>
</div>
</div>
<!-- Network Intelligence -->
<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">Network Intelligence</h3>
</div>
<div class="p-6">
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-900">🏢 Datacenter</span>
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:datacenter_ranges]) %></span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-900">🔒 VPN</span>
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:vpn_ranges]) %></span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-900">🛡️ Proxy</span>
<span class="text-sm font-medium text-gray-900"><%= number_with_delimiter(@network_intelligence[:proxy_ranges]) %></span>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div id="recent-activity" 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">Recent Activity</h3>
</div>
<div class="p-6">
<div class="space-y-3">
<% @recent_events.first(3).each do |event| %>
<div class="flex items-center justify-between text-sm">
<div class="flex items-center">
<div class="w-2 h-2 rounded-full mr-2
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
</div>
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Top Blocked IPs -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">Top Blocked IPs</h3>
<%= link_to "View All Events", events_path, class: "text-sm text-blue-600 hover:text-blue-800" %>
</div>
</div>
<div class="p-6">
<% if @top_blocked_ips.any? %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<% @top_blocked_ips.each do |ip, count| %>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-lg font-mono font-medium text-gray-900"><%= ip %></div>
<div class="text-sm text-red-600"><%= number_with_delimiter(count) %> blocks</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-center py-8">No blocked events in the selected time period</p>
<% end %>
</div>
</div>
<!-- Quick Actions -->
<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">Quick Actions</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<%= link_to new_rule_path, class: "flex items-center justify-center px-4 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" do %>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
Create Rule
<% end %>
<%= link_to new_network_range_path, class: "flex items-center justify-center px-4 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors" do %>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
Add Network Range
<% end %>
<%= link_to events_path, class: "flex items-center justify-center px-4 py-3 bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors" do %>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
View Events
<% end %>
<%= link_to rules_path, class: "flex items-center justify-center px-4 py-3 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors" do %>
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
Manage Rules
<% end %>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,132 @@
<%= turbo_stream.replace "dashboard-stats" do %>
<div id="dashboard-stats" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Events -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93 0 3.21-1.92 6-4.72 7.28l-.28.12V22l-5-5 5-5v2.4c2.21-.47 3.88-2.35 3.88-4.65 0-2.76-2.24-5-5-5-1.3 0-2.47.5-3.36 1.31L9 6.36C10.11 5.26 11.49 4.56 13 4.56V2.05c0-.45.54-.67.85-.35l8.78 8.78c.2.2.2.51 0 .71l-8.78 8.78c-.31.31-.85.1-.85-.35v-2.02c-5.05-.5-9-4.76-9-9.93 0-4.08 2.73-7.54 6.58-8.67L13 2.05z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_events) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">Last <%= @time_period.to_s.humanize %></span>
</div>
</div>
</div>
<!-- Active Rules -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@total_rules) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">Enabled</span>
<% if @system_health[:disabled_rules] > 0 %>
<span class="text-gray-500"> · <%= @system_health[:disabled_rules] %> disabled</span>
<% end %>
</div>
</div>
</div>
<!-- Network Ranges with Events -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Network Ranges</dt>
<dd class="text-lg font-medium text-gray-900"><%= number_with_delimiter(@network_ranges_with_events) %></dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-purple-600 font-medium">of <%= number_with_delimiter(@total_network_ranges) %> total</span>
</div>
</div>
</div>
<!-- System Health -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-orange-500 rounded-md flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</div>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">System Health</dt>
<dd class="text-lg font-medium text-gray-900">Normal</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="text-sm">
<span class="text-green-600 font-medium">All systems operational</span>
</div>
</div>
</div>
</div>
<% end %>
<%= turbo_stream.replace "recent-activity" do %>
<div id="recent-activity" 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">Recent Activity</h3>
</div>
<div class="p-6">
<div class="space-y-3">
<% @recent_events.first(3).each do |event| %>
<div class="flex items-center justify-between text-sm">
<div class="flex items-center">
<div class="w-2 h-2 rounded-full mr-2
<%= event.waf_action == 'allow' ? 'bg-green-500' : 'bg-red-500' %>"></div>
<span class="text-gray-900 truncate max-w-[120px]"><%= event.ip_address %></span>
</div>
<span class="text-gray-500"><%= time_ago_in_words(event.timestamp) %> ago</span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>