binocs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -4
- data/app/controllers/binocs/requests_controller.rb +190 -1
- data/app/helpers/binocs/application_helper.rb +43 -11
- data/app/models/binocs/request.rb +31 -0
- data/app/views/binocs/requests/_empty_list.html.erb +3 -3
- data/app/views/binocs/requests/_log_entry.html.erb +104 -0
- data/app/views/binocs/requests/_request.html.erb +5 -5
- data/app/views/binocs/requests/analytics.html.erb +239 -0
- data/app/views/binocs/requests/heatmap.html.erb +179 -0
- data/app/views/binocs/requests/index.html.erb +42 -37
- data/app/views/binocs/requests/lifecycle.html.erb +886 -0
- data/app/views/binocs/requests/raw.html.erb +184 -0
- data/app/views/binocs/requests/sequence.html.erb +449 -0
- data/app/views/binocs/requests/show.html.erb +81 -74
- data/app/views/layouts/binocs/application.html.erb +36 -19
- data/config/routes.rb +7 -0
- data/db/migrate/20260314000000_add_client_identifier_to_binocs_requests.rb +8 -0
- data/lib/binocs/engine.rb +9 -0
- data/lib/binocs/log_subscriber.rb +164 -0
- data/lib/binocs/middleware/request_recorder.rb +35 -3
- data/lib/binocs/tui/app.rb +181 -2
- data/lib/binocs/tui/endpoints.rb +327 -0
- data/lib/binocs/tui/help_screen.rb +16 -0
- data/lib/binocs/tui/sequence_diagram.rb +266 -0
- data/lib/binocs/tui/window.rb +20 -0
- data/lib/binocs/tui.rb +2 -0
- data/lib/binocs/version.rb +1 -1
- metadata +21 -7
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<!-- Header -->
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-xl font-semibold text-zinc-200">Analytics</h1>
|
|
5
|
+
<p class="text-sm text-zinc-500 mt-1">Request patterns and performance insights</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Summary cards -->
|
|
9
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
10
|
+
<div class="bg-zinc-900 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition-all p-5">
|
|
11
|
+
<div class="text-xs text-zinc-500 uppercase tracking-wider font-semibold">Total Requests</div>
|
|
12
|
+
<div class="text-2xl font-bold text-white mt-1"><%= number_with_delimiter(@total_requests) %></div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="bg-zinc-900 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition-all p-5">
|
|
15
|
+
<div class="text-xs text-zinc-500 uppercase tracking-wider font-semibold">Today</div>
|
|
16
|
+
<div class="text-2xl font-bold text-white mt-1"><%= number_with_delimiter(@today_requests) %></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="bg-zinc-900 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition-all p-5">
|
|
19
|
+
<div class="text-xs text-zinc-500 uppercase tracking-wider font-semibold">Avg Duration</div>
|
|
20
|
+
<div class="text-2xl font-bold text-white mt-1"><%= @avg_duration ? "#{@avg_duration.round(1)}ms" : "N/A" %></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="bg-zinc-900 rounded-lg border border-yellow-500/20 hover:border-yellow-500/40 transition-all p-5">
|
|
23
|
+
<div class="text-xs text-zinc-500 uppercase tracking-wider font-semibold">Error Rate</div>
|
|
24
|
+
<div class="text-2xl font-bold <%= @error_rate.to_f > 5 ? 'text-rose-400' : @error_rate.to_f > 1 ? 'text-amber-400' : 'text-emerald-400' %> mt-1"><%= @error_rate %>%</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
29
|
+
|
|
30
|
+
<!-- Hourly Traffic (last 24h) -->
|
|
31
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
32
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Traffic (Last 24 Hours)</h2>
|
|
33
|
+
<% if @hourly_traffic.any? %>
|
|
34
|
+
<%
|
|
35
|
+
max_hourly = @hourly_traffic.values.max || 1
|
|
36
|
+
# Generate all 24 hours
|
|
37
|
+
hours = (0..23).map { |i| (Time.current - (23 - i).hours).strftime("%Y-%m-%d %H:00") }
|
|
38
|
+
%>
|
|
39
|
+
<div class="space-y-1">
|
|
40
|
+
<% hours.each do |hour| %>
|
|
41
|
+
<%
|
|
42
|
+
count = @hourly_traffic[hour] || 0
|
|
43
|
+
pct = max_hourly > 0 ? (count.to_f / max_hourly * 100) : 0
|
|
44
|
+
hour_label = Time.parse(hour).strftime("%H:00") rescue hour.split(" ").last
|
|
45
|
+
%>
|
|
46
|
+
<div class="flex items-center space-x-2 group">
|
|
47
|
+
<span class="text-xs text-zinc-600 w-12 text-right font-mono"><%= hour_label %></span>
|
|
48
|
+
<div class="flex-1 h-4 bg-zinc-800/50 rounded-sm overflow-hidden">
|
|
49
|
+
<div class="h-full rounded-sm transition-all duration-300 <%= count > 0 ? 'bg-yellow-500' : '' %>"
|
|
50
|
+
style="width: <%= [pct, 100].min %>%"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<span class="text-xs text-zinc-600 w-10 text-right font-mono"><%= count %></span>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
<% else %>
|
|
57
|
+
<p class="text-sm text-zinc-500">No traffic data in the last 24 hours.</p>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Response Time Distribution -->
|
|
62
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
63
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Response Time Distribution</h2>
|
|
64
|
+
<% if @duration_buckets.any? %>
|
|
65
|
+
<% max_bucket = @duration_buckets.values.max || 1 %>
|
|
66
|
+
<div class="space-y-3">
|
|
67
|
+
<% @duration_buckets.each do |label, count| %>
|
|
68
|
+
<%
|
|
69
|
+
pct = (count.to_f / max_bucket * 100)
|
|
70
|
+
color = case label
|
|
71
|
+
when "< 10ms", "10-50ms" then "bg-emerald-500"
|
|
72
|
+
when "50-100ms", "100-250ms" then "bg-amber-500"
|
|
73
|
+
when "250-500ms", "500ms-1s" then "bg-orange-500"
|
|
74
|
+
else "bg-rose-500"
|
|
75
|
+
end
|
|
76
|
+
%>
|
|
77
|
+
<div>
|
|
78
|
+
<div class="flex items-center justify-between mb-1">
|
|
79
|
+
<span class="text-xs text-zinc-500 font-mono"><%= label %></span>
|
|
80
|
+
<span class="text-xs text-zinc-500"><%= count %> (<%= @total_requests > 0 ? (count.to_f / @total_requests * 100).round(1) : 0 %>%)</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="h-5 bg-zinc-800/50 rounded overflow-hidden">
|
|
83
|
+
<div class="h-full rounded transition-all duration-300 <%= color %>" style="width: <%= [pct, 100].min %>%"></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
<% else %>
|
|
89
|
+
<p class="text-sm text-zinc-500">No duration data recorded.</p>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Status Code Distribution -->
|
|
94
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
95
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Status Codes</h2>
|
|
96
|
+
<% if @status_distribution.any? %>
|
|
97
|
+
<% max_status = @status_distribution.values.max || 1 %>
|
|
98
|
+
<div class="space-y-2">
|
|
99
|
+
<% @status_distribution.sort_by { |code, _| code.to_i }.each do |code, count| %>
|
|
100
|
+
<%
|
|
101
|
+
pct = (count.to_f / max_status * 100)
|
|
102
|
+
color = case code.to_i
|
|
103
|
+
when 200..299 then "bg-emerald-500"
|
|
104
|
+
when 300..399 then "bg-cyan-500"
|
|
105
|
+
when 400..499 then "bg-amber-500"
|
|
106
|
+
when 500..599 then "bg-rose-500"
|
|
107
|
+
else "bg-zinc-500"
|
|
108
|
+
end
|
|
109
|
+
%>
|
|
110
|
+
<div class="flex items-center space-x-3">
|
|
111
|
+
<span class="text-sm font-mono w-10 text-right <%= status_badge_class(code.to_i).gsub(/bg-\S+/, '').gsub(/ring-\S+/, '').strip %>"><%= code %></span>
|
|
112
|
+
<div class="flex-1 h-4 bg-zinc-800/50 rounded overflow-hidden">
|
|
113
|
+
<div class="h-full rounded transition-all duration-300 <%= color %>" style="width: <%= [pct, 100].min %>%"></div>
|
|
114
|
+
</div>
|
|
115
|
+
<span class="text-xs text-zinc-500 w-16 text-right"><%= count %> (<%= (@total_requests > 0 ? (count.to_f / @total_requests * 100).round(1) : 0) %>%)</span>
|
|
116
|
+
</div>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
119
|
+
<% else %>
|
|
120
|
+
<p class="text-sm text-zinc-500">No status data recorded.</p>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- HTTP Method Distribution -->
|
|
125
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
126
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">HTTP Methods</h2>
|
|
127
|
+
<% if @method_distribution.any? %>
|
|
128
|
+
<% max_method = @method_distribution.values.max || 1 %>
|
|
129
|
+
<div class="space-y-3">
|
|
130
|
+
<% @method_distribution.sort_by { |_, count| -count }.each do |method, count| %>
|
|
131
|
+
<% pct = (count.to_f / max_method * 100) %>
|
|
132
|
+
<div>
|
|
133
|
+
<div class="flex items-center justify-between mb-1">
|
|
134
|
+
<span class="text-xs font-bold <%= method_badge_class(method) %> rounded px-1.5 py-0.5"><%= method %></span>
|
|
135
|
+
<span class="text-xs text-zinc-500"><%= count %> (<%= @total_requests > 0 ? (count.to_f / @total_requests * 100).round(1) : 0 %>%)</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="h-4 bg-zinc-800/50 rounded overflow-hidden">
|
|
138
|
+
<% bar_color = case method.upcase
|
|
139
|
+
when "GET" then "bg-emerald-500"
|
|
140
|
+
when "POST" then "bg-cyan-500"
|
|
141
|
+
when "PUT", "PATCH" then "bg-amber-500"
|
|
142
|
+
when "DELETE" then "bg-rose-500"
|
|
143
|
+
else "bg-zinc-500"
|
|
144
|
+
end %>
|
|
145
|
+
<div class="h-full rounded transition-all duration-300 <%= bar_color %>" style="width: <%= [pct, 100].min %>%"></div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<% end %>
|
|
149
|
+
</div>
|
|
150
|
+
<% else %>
|
|
151
|
+
<p class="text-sm text-zinc-500">No method data recorded.</p>
|
|
152
|
+
<% end %>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<!-- Top Endpoints -->
|
|
157
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
158
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Top Endpoints by Volume</h2>
|
|
159
|
+
<% if @top_endpoints.any? %>
|
|
160
|
+
<% max_top = @top_endpoints.first.hit_count %>
|
|
161
|
+
<div class="space-y-2">
|
|
162
|
+
<% @top_endpoints.each_with_index do |ep, i| %>
|
|
163
|
+
<% pct = max_top > 0 ? (ep.hit_count.to_f / max_top * 100) : 0 %>
|
|
164
|
+
<div class="relative">
|
|
165
|
+
<!-- Background bar -->
|
|
166
|
+
<div class="absolute inset-0 rounded bg-yellow-500/10" style="width: <%= [pct, 100].min %>%"></div>
|
|
167
|
+
<!-- Content -->
|
|
168
|
+
<div class="relative flex items-center px-3 py-2">
|
|
169
|
+
<span class="text-xs text-zinc-600 w-6 font-mono"><%= i + 1 %>.</span>
|
|
170
|
+
<span class="text-xs font-bold <%= method_badge_class(ep.method) %> rounded px-1.5 py-0.5 mr-2 flex-shrink-0"><%= ep.method %></span>
|
|
171
|
+
<span class="text-sm text-zinc-300 font-mono truncate flex-1"><%= ep.path %></span>
|
|
172
|
+
<span class="text-sm text-white font-semibold ml-3 flex-shrink-0"><%= ep.hit_count %></span>
|
|
173
|
+
<span class="text-xs text-zinc-600 ml-3 w-20 text-right flex-shrink-0"><%= ep.avg_duration.to_f.round(1) %>ms avg</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
</div>
|
|
178
|
+
<% else %>
|
|
179
|
+
<p class="text-sm text-zinc-500">No endpoint data recorded.</p>
|
|
180
|
+
<% end %>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
184
|
+
<!-- Slowest Endpoints -->
|
|
185
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
186
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Slowest Endpoints (avg)</h2>
|
|
187
|
+
<% if @slowest_endpoints.any? %>
|
|
188
|
+
<div class="space-y-3">
|
|
189
|
+
<% @slowest_endpoints.each do |ep| %>
|
|
190
|
+
<%
|
|
191
|
+
avg_ms = ep.avg_duration.to_f
|
|
192
|
+
color = if avg_ms > 1000 then "text-rose-400"
|
|
193
|
+
elsif avg_ms > 500 then "text-orange-400"
|
|
194
|
+
elsif avg_ms > 200 then "text-amber-400"
|
|
195
|
+
else "text-emerald-400"
|
|
196
|
+
end
|
|
197
|
+
%>
|
|
198
|
+
<div class="flex items-center">
|
|
199
|
+
<span class="text-xs font-bold <%= method_badge_class(ep.method) %> rounded px-1.5 py-0.5 mr-2 flex-shrink-0"><%= ep.method %></span>
|
|
200
|
+
<span class="text-sm text-zinc-300 font-mono truncate flex-1"><%= ep.path %></span>
|
|
201
|
+
<div class="flex-shrink-0 ml-3 text-right">
|
|
202
|
+
<span class="text-sm font-semibold <%= color %>"><%= avg_ms.round(1) %>ms</span>
|
|
203
|
+
<span class="text-xs text-zinc-600 ml-2">max <%= ep.max_duration.to_f.round(0) %>ms</span>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<% end %>
|
|
207
|
+
</div>
|
|
208
|
+
<% else %>
|
|
209
|
+
<p class="text-sm text-zinc-500">Not enough data (need 2+ requests per endpoint).</p>
|
|
210
|
+
<% end %>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<!-- Error Hotspots -->
|
|
214
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-5">
|
|
215
|
+
<h2 class="text-sm font-semibold text-zinc-200 mb-4">Error Hotspots</h2>
|
|
216
|
+
<% if @error_endpoints.any? %>
|
|
217
|
+
<div class="space-y-3">
|
|
218
|
+
<% @error_endpoints.each do |ep| %>
|
|
219
|
+
<div class="flex items-center">
|
|
220
|
+
<span class="text-xs font-bold <%= method_badge_class(ep.method) %> rounded px-1.5 py-0.5 mr-2 flex-shrink-0"><%= ep.method %></span>
|
|
221
|
+
<span class="text-sm text-zinc-300 font-mono truncate flex-1"><%= ep.path %></span>
|
|
222
|
+
<div class="flex-shrink-0 ml-3 flex items-center space-x-2">
|
|
223
|
+
<span class="text-xs font-medium <%= status_badge_class(ep.status_code.to_i) %> rounded px-1.5 py-0.5"><%= ep.status_code %></span>
|
|
224
|
+
<span class="text-sm font-semibold text-rose-400"><%= ep.error_count %>×</span>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<% end %>
|
|
228
|
+
</div>
|
|
229
|
+
<% else %>
|
|
230
|
+
<div class="flex items-center space-x-2">
|
|
231
|
+
<svg class="w-5 h-5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
232
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
233
|
+
</svg>
|
|
234
|
+
<p class="text-sm text-emerald-400">No errors recorded. Looking clean!</p>
|
|
235
|
+
</div>
|
|
236
|
+
<% end %>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<div class="space-y-6" x-data="{ viewMode: '<%= @view_mode %>' }">
|
|
2
|
+
<!-- Header -->
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="text-xl font-semibold text-zinc-200">API Heatmap</h1>
|
|
6
|
+
<p class="text-sm text-zinc-500 mt-1"><%= @total_requests %> total requests across <%= @endpoint_groups.values.flatten.size %> endpoints</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- View mode toggle -->
|
|
10
|
+
<div class="inline-flex rounded-lg bg-zinc-800 p-1">
|
|
11
|
+
<button @click="viewMode = 'frequency'"
|
|
12
|
+
:class="viewMode === 'frequency' ? 'bg-yellow-500 text-black shadow' : 'text-zinc-400 hover:text-zinc-200'"
|
|
13
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200">
|
|
14
|
+
Frequency
|
|
15
|
+
</button>
|
|
16
|
+
<button @click="viewMode = 'latency'"
|
|
17
|
+
:class="viewMode === 'latency' ? 'bg-amber-500 text-black shadow' : 'text-zinc-400 hover:text-zinc-200'"
|
|
18
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200">
|
|
19
|
+
Latency
|
|
20
|
+
</button>
|
|
21
|
+
<button @click="viewMode = 'errors'"
|
|
22
|
+
:class="viewMode === 'errors' ? 'bg-rose-500 text-white shadow' : 'text-zinc-400 hover:text-zinc-200'"
|
|
23
|
+
class="rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200">
|
|
24
|
+
Errors
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Legend -->
|
|
30
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 px-5 py-3 flex items-center justify-between">
|
|
31
|
+
<div class="flex items-center space-x-6 text-xs text-zinc-500">
|
|
32
|
+
<!-- Frequency legend -->
|
|
33
|
+
<div x-show="viewMode === 'frequency'" class="flex items-center space-x-2">
|
|
34
|
+
<span>Low traffic</span>
|
|
35
|
+
<div class="flex space-x-0.5">
|
|
36
|
+
<div class="w-5 h-3 rounded-sm bg-yellow-950"></div>
|
|
37
|
+
<div class="w-5 h-3 rounded-sm bg-yellow-900"></div>
|
|
38
|
+
<div class="w-5 h-3 rounded-sm bg-yellow-700"></div>
|
|
39
|
+
<div class="w-5 h-3 rounded-sm bg-yellow-500"></div>
|
|
40
|
+
<div class="w-5 h-3 rounded-sm bg-yellow-400"></div>
|
|
41
|
+
</div>
|
|
42
|
+
<span>High traffic</span>
|
|
43
|
+
</div>
|
|
44
|
+
<!-- Latency legend -->
|
|
45
|
+
<div x-show="viewMode === 'latency'" x-cloak class="flex items-center space-x-2">
|
|
46
|
+
<span>Fast</span>
|
|
47
|
+
<div class="flex space-x-0.5">
|
|
48
|
+
<div class="w-5 h-3 rounded-sm bg-emerald-900"></div>
|
|
49
|
+
<div class="w-5 h-3 rounded-sm bg-emerald-700"></div>
|
|
50
|
+
<div class="w-5 h-3 rounded-sm bg-amber-700"></div>
|
|
51
|
+
<div class="w-5 h-3 rounded-sm bg-orange-600"></div>
|
|
52
|
+
<div class="w-5 h-3 rounded-sm bg-rose-500"></div>
|
|
53
|
+
</div>
|
|
54
|
+
<span>Slow</span>
|
|
55
|
+
</div>
|
|
56
|
+
<!-- Errors legend -->
|
|
57
|
+
<div x-show="viewMode === 'errors'" x-cloak class="flex items-center space-x-2">
|
|
58
|
+
<span>No errors</span>
|
|
59
|
+
<div class="flex space-x-0.5">
|
|
60
|
+
<div class="w-5 h-3 rounded-sm bg-zinc-800"></div>
|
|
61
|
+
<div class="w-5 h-3 rounded-sm bg-rose-950"></div>
|
|
62
|
+
<div class="w-5 h-3 rounded-sm bg-rose-800"></div>
|
|
63
|
+
<div class="w-5 h-3 rounded-sm bg-rose-600"></div>
|
|
64
|
+
<div class="w-5 h-3 rounded-sm bg-rose-400"></div>
|
|
65
|
+
</div>
|
|
66
|
+
<span>High errors</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="text-xs text-zinc-600">
|
|
70
|
+
Grouped by API tag / resource
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<% if @endpoint_groups.any? %>
|
|
75
|
+
<!-- Endpoint groups -->
|
|
76
|
+
<% @endpoint_groups.each do |tag, endpoints| %>
|
|
77
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
|
|
78
|
+
<!-- Group header -->
|
|
79
|
+
<div class="px-5 py-3 border-b border-zinc-800 flex items-center justify-between">
|
|
80
|
+
<div class="flex items-center space-x-3">
|
|
81
|
+
<h2 class="text-sm font-semibold text-zinc-200"><%= tag %></h2>
|
|
82
|
+
<span class="text-xs text-zinc-600"><%= endpoints.size %> endpoints</span>
|
|
83
|
+
</div>
|
|
84
|
+
<span class="text-xs text-zinc-600"><%= endpoints.sum(&:hit_count) %> total hits</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Endpoint grid -->
|
|
88
|
+
<div class="p-4">
|
|
89
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
|
90
|
+
<% endpoints.each do |ep| %>
|
|
91
|
+
<%
|
|
92
|
+
freq_pct = @max_hits > 0 ? (ep.hit_count.to_f / @max_hits) : 0
|
|
93
|
+
latency_pct = @max_avg_duration > 0 ? (ep.avg_duration.to_f / @max_avg_duration) : 0
|
|
94
|
+
total_errors = ep.error_count.to_i + ep.client_error_count.to_i
|
|
95
|
+
error_pct = ep.hit_count > 0 ? (total_errors.to_f / ep.hit_count) : 0
|
|
96
|
+
|
|
97
|
+
# Frequency colors (yellow/gold scale)
|
|
98
|
+
freq_bg = if freq_pct > 0.8 then "bg-yellow-400/20 border-yellow-400/40"
|
|
99
|
+
elsif freq_pct > 0.5 then "bg-yellow-500/15 border-yellow-500/30"
|
|
100
|
+
elsif freq_pct > 0.25 then "bg-yellow-700/15 border-yellow-600/25"
|
|
101
|
+
elsif freq_pct > 0.1 then "bg-yellow-900/20 border-yellow-700/20"
|
|
102
|
+
else "bg-yellow-950/20 border-yellow-800/15"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Latency colors (green -> red)
|
|
106
|
+
avg_ms = ep.avg_duration.to_f
|
|
107
|
+
latency_bg = if avg_ms > 1000 then "bg-rose-500/20 border-rose-400/40"
|
|
108
|
+
elsif avg_ms > 500 then "bg-orange-600/20 border-orange-500/30"
|
|
109
|
+
elsif avg_ms > 200 then "bg-amber-700/15 border-amber-600/25"
|
|
110
|
+
elsif avg_ms > 50 then "bg-emerald-700/15 border-emerald-600/20"
|
|
111
|
+
else "bg-emerald-900/15 border-emerald-700/15"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Error colors (gray -> red)
|
|
115
|
+
error_bg = if error_pct > 0.5 then "bg-rose-400/20 border-rose-400/40"
|
|
116
|
+
elsif error_pct > 0.2 then "bg-rose-600/20 border-rose-500/30"
|
|
117
|
+
elsif error_pct > 0.05 then "bg-rose-800/15 border-rose-700/25"
|
|
118
|
+
elsif total_errors > 0 then "bg-rose-950/15 border-rose-800/20"
|
|
119
|
+
else "bg-zinc-800/50 border-zinc-700/30"
|
|
120
|
+
end
|
|
121
|
+
%>
|
|
122
|
+
<div class="rounded-lg border p-3 transition-all duration-200 hover:scale-[1.02] hover:shadow-lg cursor-default group"
|
|
123
|
+
:class="viewMode === 'frequency' ? '<%= freq_bg %>' : viewMode === 'latency' ? '<%= latency_bg %>' : '<%= error_bg %>'"
|
|
124
|
+
title="<%= ep.method %> <%= ep.path %> Hits: <%= ep.hit_count %> Avg: <%= ep.avg_duration.to_f.round(1) %>ms Max: <%= ep.max_duration.to_f.round(1) %>ms Errors: <%= total_errors %>">
|
|
125
|
+
<!-- Method + Path -->
|
|
126
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
127
|
+
<span class="text-xs font-bold <%= method_badge_class(ep.method) %> rounded px-1.5 py-0.5 flex-shrink-0"><%= ep.method %></span>
|
|
128
|
+
<span class="text-xs text-zinc-300 truncate font-mono"><%= ep.path %></span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Stats row -->
|
|
132
|
+
<div class="flex items-center justify-between text-xs">
|
|
133
|
+
<div x-show="viewMode === 'frequency'">
|
|
134
|
+
<span class="text-yellow-300 font-semibold"><%= ep.hit_count %></span>
|
|
135
|
+
<span class="text-zinc-600 ml-1">hits</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div x-show="viewMode === 'latency'" x-cloak>
|
|
138
|
+
<span class="font-semibold <%= avg_ms > 500 ? 'text-rose-300' : avg_ms > 200 ? 'text-amber-300' : 'text-emerald-300' %>"><%= ep.avg_duration.to_f.round(1) %>ms</span>
|
|
139
|
+
<span class="text-zinc-600 ml-1">avg</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div x-show="viewMode === 'errors'" x-cloak>
|
|
142
|
+
<% if total_errors > 0 %>
|
|
143
|
+
<span class="text-rose-300 font-semibold"><%= total_errors %></span>
|
|
144
|
+
<span class="text-zinc-600 ml-1">errors (<%= (error_pct * 100).round(1) %>%)</span>
|
|
145
|
+
<% else %>
|
|
146
|
+
<span class="text-emerald-400 font-semibold">Clean</span>
|
|
147
|
+
<% end %>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Secondary stats (always visible) -->
|
|
151
|
+
<div class="flex items-center space-x-3 text-zinc-600">
|
|
152
|
+
<span x-show="viewMode !== 'frequency'"><%= ep.hit_count %> hits</span>
|
|
153
|
+
<span x-show="viewMode !== 'latency'" x-cloak><%= ep.avg_duration.to_f.round(0) %>ms</span>
|
|
154
|
+
<% if total_errors > 0 %>
|
|
155
|
+
<span x-show="viewMode !== 'errors'" x-cloak class="text-rose-400"><%= total_errors %> err</span>
|
|
156
|
+
<% end %>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Mini bar -->
|
|
161
|
+
<div class="mt-2 h-1 bg-zinc-800/50 rounded-full overflow-hidden">
|
|
162
|
+
<div class="h-full rounded-full transition-all duration-500"
|
|
163
|
+
:class="viewMode === 'frequency' ? 'bg-yellow-400' : viewMode === 'latency' ? '<%= avg_ms > 500 ? 'bg-rose-400' : avg_ms > 200 ? 'bg-amber-400' : 'bg-emerald-400' %>' : '<%= total_errors > 0 ? 'bg-rose-400' : 'bg-zinc-600' %>'"
|
|
164
|
+
:style="viewMode === 'frequency' ? 'width: <%= (freq_pct * 100).round(1) %>%' : viewMode === 'latency' ? 'width: <%= (latency_pct * 100).round(1) %>%' : 'width: <%= (error_pct * 100).round(1) %>%'">
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<% end %>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<% end %>
|
|
173
|
+
<% else %>
|
|
174
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 p-12 text-center">
|
|
175
|
+
<p class="text-zinc-500">No request data yet.</p>
|
|
176
|
+
<p class="text-sm text-zinc-600 mt-2">Endpoints will appear here as requests are recorded.</p>
|
|
177
|
+
</div>
|
|
178
|
+
<% end %>
|
|
179
|
+
</div>
|
|
@@ -8,58 +8,62 @@
|
|
|
8
8
|
">
|
|
9
9
|
<!-- Stats Cards -->
|
|
10
10
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
11
|
-
<div class="bg-
|
|
12
|
-
<dt class="text-sm font-medium text-
|
|
11
|
+
<div class="bg-zinc-900 rounded-lg p-4 border border-yellow-500/20 hover:border-yellow-500/40 transition-all">
|
|
12
|
+
<dt class="text-sm font-medium text-zinc-400">Total Requests</dt>
|
|
13
13
|
<dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:total] %></dd>
|
|
14
14
|
</div>
|
|
15
|
-
<div class="bg-
|
|
16
|
-
<dt class="text-sm font-medium text-
|
|
15
|
+
<div class="bg-zinc-900 rounded-lg p-4 border border-yellow-500/20 hover:border-yellow-500/40 transition-all">
|
|
16
|
+
<dt class="text-sm font-medium text-zinc-400">Today</dt>
|
|
17
17
|
<dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:today] %></dd>
|
|
18
18
|
</div>
|
|
19
|
-
<div class="bg-
|
|
20
|
-
<dt class="text-sm font-medium text-
|
|
21
|
-
<dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:avg_duration] || 0 %><span class="text-lg text-
|
|
19
|
+
<div class="bg-zinc-900 rounded-lg p-4 border border-yellow-500/20 hover:border-yellow-500/40 transition-all">
|
|
20
|
+
<dt class="text-sm font-medium text-zinc-400">Avg Duration</dt>
|
|
21
|
+
<dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:avg_duration] || 0 %><span class="text-lg text-zinc-500">ms</span></dd>
|
|
22
22
|
</div>
|
|
23
|
-
<div class="bg-
|
|
24
|
-
<dt class="text-sm font-medium text-
|
|
25
|
-
<dd class="mt-1 text-3xl font-semibold <%= @stats[:error_rate].to_f > 5 ? 'text-
|
|
23
|
+
<div class="bg-zinc-900 rounded-lg p-4 border border-yellow-500/20 hover:border-yellow-500/40 transition-all">
|
|
24
|
+
<dt class="text-sm font-medium text-zinc-400">Error Rate</dt>
|
|
25
|
+
<dd class="mt-1 text-3xl font-semibold <%= @stats[:error_rate].to_f > 5 ? 'text-rose-400' : 'text-emerald-400' %>"><%= @stats[:error_rate] || 0 %><span class="text-lg text-zinc-500">%</span></dd>
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
29
|
<!-- Filters -->
|
|
30
|
-
<div class="bg-
|
|
31
|
-
<%= form_with url: binocs.requests_path, method: :get, local: true
|
|
32
|
-
<div class="flex flex-wrap gap-
|
|
30
|
+
<div class="bg-zinc-900 rounded-lg p-5 border border-zinc-800">
|
|
31
|
+
<%= form_with url: binocs.requests_path, method: :get, local: true do |f| %>
|
|
32
|
+
<div class="flex flex-wrap gap-3 items-end">
|
|
33
33
|
<!-- Search -->
|
|
34
|
-
<div class="flex-1 min-w-[
|
|
35
|
-
|
|
34
|
+
<div class="flex-1 min-w-[240px]">
|
|
35
|
+
<label class="block text-xs font-medium text-zinc-400 mb-1.5">Search</label>
|
|
36
|
+
<%= text_field_tag :search, params[:search], placeholder: "Search path, controller...", class: "w-full rounded-lg bg-zinc-800 border border-zinc-700 text-white placeholder-zinc-500 focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500/30 focus:outline-none text-sm px-4 py-2.5 transition-colors" %>
|
|
36
37
|
</div>
|
|
37
38
|
|
|
38
39
|
<!-- Method Filter -->
|
|
39
|
-
<div>
|
|
40
|
-
|
|
40
|
+
<div class="min-w-[160px]">
|
|
41
|
+
<label class="block text-xs font-medium text-zinc-400 mb-1.5">Method</label>
|
|
42
|
+
<%= select_tag :method, options_for_select([["All Methods", ""], ["GET", "GET"], ["POST", "POST"], ["PUT", "PUT"], ["PATCH", "PATCH"], ["DELETE", "DELETE"]], params[:method]), class: "w-full rounded-lg bg-zinc-800 border border-zinc-700 text-white text-sm px-4 py-2.5 focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500/30 focus:outline-none appearance-none cursor-pointer transition-colors" %>
|
|
41
43
|
</div>
|
|
42
44
|
|
|
43
45
|
<!-- Status Filter -->
|
|
44
|
-
<div>
|
|
45
|
-
|
|
46
|
+
<div class="min-w-[170px]">
|
|
47
|
+
<label class="block text-xs font-medium text-zinc-400 mb-1.5">Status</label>
|
|
48
|
+
<%= select_tag :status, options_for_select([["All Status", ""], ["2xx Success", "2xx"], ["3xx Redirect", "3xx"], ["4xx Client Error", "4xx"], ["5xx Server Error", "5xx"]], params[:status]), class: "w-full rounded-lg bg-zinc-800 border border-zinc-700 text-white text-sm px-4 py-2.5 focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500/30 focus:outline-none appearance-none cursor-pointer transition-colors" %>
|
|
46
49
|
</div>
|
|
47
50
|
|
|
48
51
|
<!-- Controller Filter -->
|
|
49
|
-
<div>
|
|
50
|
-
|
|
52
|
+
<div class="min-w-[180px]">
|
|
53
|
+
<label class="block text-xs font-medium text-zinc-400 mb-1.5">Controller</label>
|
|
54
|
+
<%= select_tag :controller_name, options_for_select([["All Controllers", ""]] + Binocs::Request.controllers_list.map { |c| [c, c] }, params[:controller_name]), class: "w-full rounded-lg bg-zinc-800 border border-zinc-700 text-white text-sm px-4 py-2.5 focus:border-yellow-500 focus:ring-1 focus:ring-yellow-500/30 focus:outline-none appearance-none cursor-pointer transition-colors" %>
|
|
51
55
|
</div>
|
|
52
56
|
|
|
53
57
|
<!-- Has Exception -->
|
|
54
|
-
<div class="flex items-center">
|
|
55
|
-
<%= check_box_tag :has_exception, "1", params[:has_exception] == "1", class: "rounded bg-
|
|
56
|
-
<%= label_tag :has_exception, "Has Exception", class: "ml-2 text-sm text-
|
|
58
|
+
<div class="flex items-center pb-2.5">
|
|
59
|
+
<%= check_box_tag :has_exception, "1", params[:has_exception] == "1", class: "w-4 h-4 rounded bg-zinc-800 border-zinc-700 text-yellow-500 focus:ring-yellow-500/30 cursor-pointer" %>
|
|
60
|
+
<%= label_tag :has_exception, "Has Exception", class: "ml-2 text-sm text-zinc-300 cursor-pointer whitespace-nowrap" %>
|
|
57
61
|
</div>
|
|
58
62
|
|
|
59
|
-
<!-- Filter Buttons
|
|
60
|
-
<div class="flex items-
|
|
61
|
-
<%= submit_tag "Filter", class: "rounded-
|
|
62
|
-
<%= link_to "Clear Filters", binocs.requests_path, class: "rounded-
|
|
63
|
+
<!-- Filter Buttons — matched height to inputs -->
|
|
64
|
+
<div class="flex items-end gap-2">
|
|
65
|
+
<%= submit_tag "Filter", class: "rounded-lg bg-yellow-500 px-5 py-2.5 text-sm font-medium text-black hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-yellow-500/50 cursor-pointer transition-colors" %>
|
|
66
|
+
<%= link_to "Clear Filters", binocs.requests_path, class: "rounded-lg bg-zinc-800 border border-zinc-700 px-5 py-2.5 text-sm font-medium text-zinc-300 hover:bg-zinc-700 hover:border-zinc-600 inline-flex items-center transition-colors" %>
|
|
63
67
|
</div>
|
|
64
68
|
</div>
|
|
65
69
|
<% end %>
|
|
@@ -68,38 +72,39 @@
|
|
|
68
72
|
<!-- Actions Bar -->
|
|
69
73
|
<div class="flex justify-between items-center">
|
|
70
74
|
<div class="flex items-center gap-4">
|
|
71
|
-
<span class="text-sm text-
|
|
75
|
+
<span class="text-sm text-zinc-400">
|
|
72
76
|
Showing <%= @requests.size %> requests
|
|
73
77
|
</span>
|
|
74
78
|
|
|
75
79
|
<!-- Live Toggle -->
|
|
76
80
|
<button
|
|
77
81
|
@click="live = !live"
|
|
78
|
-
:class="live ? 'bg-
|
|
79
|
-
class="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium
|
|
82
|
+
:class="live ? 'bg-yellow-500/15 text-yellow-400 ring-1 ring-yellow-500/30 hover:bg-yellow-500/25' : 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'"
|
|
83
|
+
class="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-all"
|
|
84
|
+
data-testid="live-toggle"
|
|
80
85
|
>
|
|
81
86
|
<span class="relative flex h-2 w-2">
|
|
82
|
-
<span x-show="live" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-
|
|
83
|
-
<span :class="live ? 'bg-
|
|
87
|
+
<span x-show="live" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-400 opacity-75"></span>
|
|
88
|
+
<span :class="live ? 'bg-yellow-400' : 'bg-zinc-500'" class="relative inline-flex rounded-full h-2 w-2"></span>
|
|
84
89
|
</span>
|
|
85
90
|
<span x-text="live ? 'Live' : 'Paused'">Live</span>
|
|
86
91
|
</button>
|
|
87
92
|
</div>
|
|
88
93
|
|
|
89
94
|
<div class="flex gap-2">
|
|
90
|
-
<%= link_to binocs.requests_path, class: "rounded-md bg-
|
|
95
|
+
<%= link_to binocs.requests_path, class: "rounded-md bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-700 transition-colors", data: { turbo_frame: "_top" } do %>
|
|
91
96
|
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
92
97
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
93
98
|
</svg>
|
|
94
99
|
Refresh
|
|
95
100
|
<% end %>
|
|
96
|
-
<%= button_to "Clear All", binocs.clear_requests_path, method: :delete, class: "rounded-md bg-
|
|
101
|
+
<%= button_to "Clear All", binocs.clear_requests_path, method: :delete, class: "rounded-md bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 px-3 py-2 text-sm font-medium hover:bg-rose-500/25 transition-colors", data: { turbo_confirm: "Are you sure you want to clear all requests?" } %>
|
|
97
102
|
</div>
|
|
98
103
|
</div>
|
|
99
104
|
|
|
100
105
|
<!-- Requests List -->
|
|
101
|
-
<div class="bg-
|
|
102
|
-
<div id="requests-list" class="divide-y divide-
|
|
106
|
+
<div class="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
|
|
107
|
+
<div id="requests-list" class="divide-y divide-zinc-800">
|
|
103
108
|
<% if @requests.any? %>
|
|
104
109
|
<%= render partial: "binocs/requests/request", collection: @requests, as: :request %>
|
|
105
110
|
<% else %>
|