Files
redux-scraper/app/views/log_entries/stats.html.erb
2025-02-26 00:34:40 +00:00

217 lines
8.1 KiB
Plaintext

<% content_for :head do %>
<style type="text/css" data-turbolinks-track>
.grid-cell {
padding: 0.25rem 1rem;
border-right: 1px solid #e2e8f0;
}
.grid-cell:last-child {
border-right: none;
}
.grid-row:hover .grid-cell {
background-color: #f1f5f9;
transition: background-color 150ms ease-in-out;
}
.stats-card {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
background-color: white;
padding: 1rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.stats-grid {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
background-color: white;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
overflow: hidden;
}
.stats-grid-header {
background-color: #f8fafc;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #334155;
cursor: pointer;
user-select: none;
}
.stats-grid-header:hover {
background-color: #f1f5f9;
}
.sort-indicator::after {
content: "▼";
font-size: 0.75rem;
margin-left: 0.25rem;
opacity: 0.5;
}
.sort-indicator.asc::after {
content: "▲";
}
</style>
<% end %>
<div class="mx-auto max-w-5xl px-4 mt-6 sm:mt-8">
<h1 class="text-2xl font-bold text-slate-900 text-center">HTTP Request Log Stats</h1>
<div class="mt-2 text-center">
<%= link_to "Back to Index",
log_entries_path,
class: "text-blue-600 hover:text-blue-800 transition-colors" %>
</div>
<div class="mt-6 text-center">
<div class="space-x-2">
<% [10.seconds, 30.seconds, 1.minute, 5.minutes, 30.minutes].each do |time_window| %>
<% if @time_window == time_window %>
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800">
<%= if time_window < 1.minute
pluralize(time_window.seconds, "second")
else
time_ago_in_words(time_window.ago)
end %>
</span>
<% else %>
<%= link_to(
(
if time_window < 1.minute
pluralize(time_window.seconds, "second")
else
time_ago_in_words(time_window.ago)
end
),
stats_log_entries_path(seconds: time_window.in_seconds),
class: "rounded-full px-3 py-1 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 transition-colors",
) %>
<% end %>
<% end %>
</div>
<div class="stats-card mt-6">
<div class="text-xl font-bold text-slate-900">
<%= @last_window_count %> requests
<span class="text-base font-normal text-slate-600">
in last
<%= if @time_window < 1.minute
pluralize(@time_window.seconds, "second")
else
time_ago_in_words(@time_window.ago)
end %>
</span>
</div>
<div class="mt-1 text-slate-600">
<span class="font-medium"><%= (@last_window_count.to_f / @time_window.in_seconds).round(1) %></span> requests/sec
</div>
<div class="mt-1 text-sm text-slate-600">
<span class="font-medium"><%= HexUtil.humansize(@last_window_bytes) %></span> transferred •
<span class="font-medium"><%= HexUtil.humansize(@last_window_bytes / @time_window.in_seconds) %></span>/sec
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold text-slate-900 mb-3">By Content Type</h2>
<div class="stats-grid">
<div class="grid grid-cols-[1fr_auto_auto] sortable-table" id="content-type-table">
<div class="grid-row contents">
<div class="stats-grid-header sort-indicator" data-column="content_type">Content Type</div>
<div class="stats-grid-header text-right sort-indicator" data-column="count">Requests</div>
<div class="stats-grid-header text-right sort-indicator" data-column="bytes">Transferred</div>
</div>
<% @content_type_counts
.sort_by { |_ignore, stats| -stats[:count] }
.each do |content_type, stats| %>
<div class="grid-row contents" data-content-type="<%= content_type %>" data-count="<%= stats[:count] %>" data-bytes="<%= stats[:bytes] %>">
<div class="grid-cell font-medium text-slate-900 text-sm"><%= content_type %></div>
<div class="grid-cell text-right text-slate-600 text-sm"><%= stats[:count] %></div>
<div class="grid-cell text-right text-slate-600 font-medium text-sm">
<%= HexUtil.humansize(stats[:bytes]) %>
</div>
</div>
<% end %>
</div>
</div>
</div>
<div class="mt-8 mb-8">
<h2 class="text-xl font-bold text-slate-900 mb-3">By Domain</h2>
<div class="stats-grid">
<div class="grid grid-cols-[1fr_auto_auto] sortable-table" id="domain-table">
<div class="grid-row contents">
<div class="stats-grid-header sort-indicator" data-column="domain">Domain</div>
<div class="stats-grid-header text-right sort-indicator" data-column="count">Requests</div>
<div class="stats-grid-header text-right sort-indicator" data-column="bytes">Transferred</div>
</div>
<% @by_domain_counts
.sort_by { |_ignore, stats| -stats[:bytes] }
.each do |domain, stats| %>
<div class="grid-row contents" data-domain="<%= domain %>" data-count="<%= stats[:count] %>" data-bytes="<%= stats[:bytes] %>">
<div class="grid-cell font-medium text-slate-900 text-sm"><%= domain %></div>
<div class="grid-cell text-right text-slate-600 text-sm"><%= stats[:count] %></div>
<div class="grid-cell text-right text-slate-600 font-medium text-sm">
<%= HexUtil.humansize(stats[:bytes]) %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize sortable tables
document.querySelectorAll('.sortable-table').forEach(table => {
initSortableTable(table);
});
function initSortableTable(table) {
const headers = table.querySelectorAll('.stats-grid-header');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-column');
const isAsc = !this.classList.contains('asc');
// Reset all headers
headers.forEach(h => h.classList.remove('asc'));
// Set current header state
if (isAsc) {
this.classList.add('asc');
}
// Get all rows except the header row
const rows = Array.from(table.querySelectorAll('.grid-row:not(:first-child)'));
// Sort the rows
rows.sort((a, b) => {
let aValue, bValue;
if (table.id === 'content-type-table') {
if (column === 'content_type') {
aValue = a.getAttribute('data-content-type');
bValue = b.getAttribute('data-content-type');
return isAsc ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
} else {
aValue = parseInt(a.getAttribute(`data-${column}`), 10);
bValue = parseInt(b.getAttribute(`data-${column}`), 10);
}
} else if (table.id === 'domain-table') {
if (column === 'domain') {
aValue = a.getAttribute('data-domain');
bValue = b.getAttribute('data-domain');
return isAsc ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
} else {
aValue = parseInt(a.getAttribute(`data-${column}`), 10);
bValue = parseInt(b.getAttribute(`data-${column}`), 10);
}
}
return isAsc ? aValue - bValue : bValue - aValue;
});
// Remove existing rows
rows.forEach(row => row.remove());
// Append sorted rows
rows.forEach(row => {
table.appendChild(row);
});
});
});
}
});
</script>