First commit!

This commit is contained in:
Dan Milne
2025-11-03 17:37:28 +11:00
commit 429d41eead
141 changed files with 5890 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><%= @project.name %> - Analytics</h1>
<div>
<%= link_to "← Back to Project", project_path(@project), class: "btn btn-secondary" %>
</div>
</div>
<!-- Time Range Selector -->
<div class="card mb-4">
<div class="card-header">
<h5>Time Range</h5>
</div>
<div class="card-body">
<%= form_with url: analytics_project_path(@project), method: :get, local: true do |form| %>
<div class="row align-items-end">
<div class="col-md-6">
<%= form.label :time_range, "Time Range", class: "form-label" %>
<%= form.select :time_range,
options_for_select([
["Last Hour", 1],
["Last 6 Hours", 6],
["Last 24 Hours", 24],
["Last 7 Days", 168],
["Last 30 Days", 720]
], @time_range),
{}, class: "form-select" %>
</div>
<div class="col-md-6">
<%= form.submit "Update", class: "btn btn-primary" %>
</div>
</div>
<% end %>
</div>
</div>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h3 class="text-primary"><%= number_with_delimiter(@total_events) %></h3>
<p class="card-text">Total Events</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h3 class="text-success"><%= number_with_delimiter(@allowed_events) %></h3>
<p class="card-text">Allowed</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h3 class="text-danger"><%= number_with_delimiter(@blocked_events) %></h3>
<p class="card-text">Blocked</p>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Top Blocked IPs -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Top Blocked IPs</h5>
</div>
<div class="card-body">
<% if @top_blocked_ips.any? %>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>IP Address</th>
<th>Blocked Count</th>
</tr>
</thead>
<tbody>
<% @top_blocked_ips.each do |stat| %>
<tr>
<td><code><%= stat.ip_address %></code></td>
<td><%= number_with_delimiter(stat.count) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% else %>
<p class="text-muted">No blocked events in this time range.</p>
<% end %>
</div>
</div>
</div>
<!-- Country Distribution -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Top Countries</h5>
</div>
<div class="card-body">
<% if @country_stats.any? %>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Country</th>
<th>Events</th>
</tr>
</thead>
<tbody>
<% @country_stats.each do |stat| %>
<tr>
<td><%= stat.country_code || 'Unknown' %></td>
<td><%= number_with_delimiter(stat.count) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% else %>
<p class="text-muted">No country data available.</p>
<% end %>
</div>
</div>
</div>
</div>
<!-- Action Distribution -->
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5>Action Distribution</h5>
</div>
<div class="card-body">
<% if @action_stats.any? %>
<div class="row">
<% @action_stats.each do |stat| %>
<div class="col-md-3 text-center mb-3">
<div class="card">
<div class="card-body">
<h4><%= stat.action.upcase %></h4>
<p class="card-text">
<span class="badge bg-<%=
case stat.action
when 'allow', 'pass' then 'success'
when 'block', 'deny' then 'danger'
when 'challenge' then 'warning'
when 'rate_limit' then 'info'
else 'secondary'
end %>">
<%= number_with_delimiter(stat.count) %>
</span>
</p>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-muted">No action data available.</p>
<% end %>
</div>
</div>
</div>
</div>
<% if @total_events > 0 %>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5>Block Rate</h5>
</div>
<div class="card-body">
<div class="progress" style="height: 30px;">
<% blocked_percentage = (@blocked_events.to_f / @total_events * 100).round(1) %>
<% allowed_percentage = (@allowed_events.to_f / @total_events * 100).round(1) %>
<div class="progress-bar bg-success" style="width: <%= allowed_percentage %>%">
<%= allowed_percentage %>% Allowed
</div>
<div class="progress-bar bg-danger" style="width: <%= blocked_percentage %>%">
<%= blocked_percentage %>% Blocked
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>
<div class="mt-4">
<%= link_to "View Events", events_project_path(@project), class: "btn btn-primary" %>
<%= link_to "Export Data", "#", class: "btn btn-secondary", onclick: "alert('Export feature coming soon!')" %>
</div>

View File

@@ -0,0 +1,49 @@
<h1>Projects</h1>
<%= link_to "New Project", new_project_path, class: "btn btn-primary mb-3" %>
<div class="row">
<% @projects.each do |project| %>
<div class="col-md-6 col-lg-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><%= project.name %></h5>
<span class="badge <%= project.enabled? ? 'bg-success' : 'bg-secondary' %>">
<%= project.enabled? ? 'Active' : 'Disabled' %>
</span>
</div>
<div class="card-body">
<p class="card-text">
<strong>Status:</strong>
<span class="badge bg-<%= project.waf_status == 'active' ? 'success' : project.waf_status == 'idle' ? 'warning' : 'danger' %>">
<%= project.waf_status %>
</span>
</p>
<p class="card-text">
<strong>Events (24h):</strong> <%= project.event_count(24.hours.ago) %>
</p>
<p class="card-text">
<strong>Blocked (24h):</strong> <%= project.blocked_count(24.hours.ago) %>
</p>
<small class="text-muted">
<strong>DSN:</strong><br>
<code><%= project.dsn %></code>
</small>
</div>
<div class="card-footer">
<%= link_to "View", project_path(project), class: "btn btn-primary btn-sm" %>
<%= link_to "Events", events_project_path(project), class: "btn btn-secondary btn-sm" %>
<%= link_to "Analytics", analytics_project_path(project), class: "btn btn-info btn-sm" %>
</div>
</div>
</div>
<% end %>
</div>
<% if @projects.empty? %>
<div class="text-center my-5">
<h3>No projects yet</h3>
<p>Create your first project to start monitoring WAF events.</p>
<%= link_to "Create Project", new_project_path, class: "btn btn-primary" %>
</div>
<% end %>

View File

@@ -0,0 +1,32 @@
<h1>New Project</h1>
<%= form_with(model: @project, local: true) do |form| %>
<% if @project.errors.any? %>
<div class="alert alert-danger">
<h4><%= pluralize(@project.errors.count, "error") %> prohibited this project from being saved:</h4>
<ul>
<% @project.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="mb-3">
<%= form.label :name, class: "form-label" %>
<%= form.text_field :name, class: "form-control" %>
</div>
<div class="mb-3">
<%= form.label :enabled, class: "form-label" %>
<div class="form-check">
<%= form.check_box :enabled, class: "form-check-input" %>
<%= form.label :enabled, "Enable this project", class: "form-check-label" %>
</div>
</div>
<div class="mb-3">
<%= form.submit "Create Project", class: "btn btn-primary" %>
<%= link_to "Cancel", projects_path, class: "btn btn-secondary" %>
</div>
<% end %>

View File

@@ -0,0 +1,118 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><%= @project.name %></h1>
<div>
<%= link_to "Edit", edit_project_path(@project), class: "btn btn-secondary" %>
<%= link_to "Events", events_project_path(@project), class: "btn btn-primary" %>
<%= link_to "Analytics", analytics_project_path(@project), class: "btn btn-info" %>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Project Status</h5>
</div>
<div class="card-body">
<p><strong>Status:</strong>
<span class="badge bg-<%= @waf_status == 'active' ? 'success' : @waf_status == 'idle' ? 'warning' : 'danger' %>">
<%= @waf_status %>
</span>
</p>
<p><strong>Enabled:</strong>
<span class="badge bg-<%= @project.enabled? ? 'success' : 'secondary' %>">
<%= @project.enabled? ? 'Yes' : 'No' %>
</span>
</p>
<p><strong>Events (24h):</strong> <%= @event_count %></p>
<p><strong>Blocked (24h):</strong> <%= @blocked_count %></p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>DSN Configuration</h5>
</div>
<div class="card-body">
<p><strong>DSN:</strong></p>
<code><%= @project.dsn %></code>
<button class="btn btn-sm btn-outline-primary ms-2" onclick="copyDSN()">Copy</button>
<% if @project.internal_dsn.present? %>
<hr>
<p><strong>Internal DSN:</strong></p>
<code><%= @project.internal_dsn %></code>
<% end %>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="card">
<div class="card-header">
<h5>Recent Events</h5>
</div>
<div class="card-body">
<% if @recent_events.any? %>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Time</th>
<th>IP</th>
<th>Action</th>
<th>Path</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% @recent_events.limit(5).each do |event| %>
<tr>
<td><%= event.timestamp.strftime("%H:%M:%S") %></td>
<td><%= event.ip_address %></td>
<td>
<span class="badge bg-<%= event.blocked? ? 'danger' : event.allowed? ? 'success' : 'warning' %>">
<%= event.action %>
</span>
</td>
<td><code><%= event.request_path %></code></td>
<td><%= event.response_status %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="text-end">
<%= link_to "View All Events", events_project_path(@project), class: "btn btn-primary btn-sm" %>
</div>
<% else %>
<p class="text-muted">No events received yet.</p>
<% end %>
</div>
</div>
</div>
<script>
function copyDSN() {
const dsnElement = document.querySelector('code');
const textArea = document.createElement('textarea');
textArea.value = dsnElement.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('btn-success');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('btn-success');
}, 2000);
}
</script>