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.
@@ -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 %>&times;</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 %>&#10;Hits: <%= ep.hit_count %>&#10;Avg: <%= ep.avg_duration.to_f.round(1) %>ms&#10;Max: <%= ep.max_duration.to_f.round(1) %>ms&#10;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-slate-800 rounded-lg p-4 border border-slate-700">
12
- <dt class="text-sm font-medium text-slate-400">Total Requests</dt>
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-slate-800 rounded-lg p-4 border border-slate-700">
16
- <dt class="text-sm font-medium text-slate-400">Today</dt>
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-slate-800 rounded-lg p-4 border border-slate-700">
20
- <dt class="text-sm font-medium text-slate-400">Avg Duration</dt>
21
- <dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:avg_duration] || 0 %><span class="text-lg text-slate-400">ms</span></dd>
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-slate-800 rounded-lg p-4 border border-slate-700">
24
- <dt class="text-sm font-medium text-slate-400">Error Rate</dt>
25
- <dd class="mt-1 text-3xl font-semibold <%= @stats[:error_rate].to_f > 5 ? 'text-red-400' : 'text-green-400' %>"><%= @stats[:error_rate] || 0 %><span class="text-lg text-slate-400">%</span></dd>
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-slate-800 rounded-lg p-4 border border-slate-700">
31
- <%= form_with url: binocs.requests_path, method: :get, local: true, class: "space-y-4" do |f| %>
32
- <div class="flex flex-wrap gap-4 items-center">
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-[200px]">
35
- <%= text_field_tag :search, params[:search], placeholder: "Search path, controller...", class: "w-full rounded-md bg-slate-700 border-slate-600 text-white placeholder-slate-400 focus:border-indigo-500 focus:ring-indigo-500 text-sm" %>
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
- <%= select_tag :method, options_for_select([["All Methods", ""], ["GET", "GET"], ["POST", "POST"], ["PUT", "PUT"], ["PATCH", "PATCH"], ["DELETE", "DELETE"]], params[:method]), class: "rounded-md bg-slate-700 border-slate-600 text-white text-sm focus:border-indigo-500 focus:ring-indigo-500" %>
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
- <%= 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: "rounded-md bg-slate-700 border-slate-600 text-white text-sm focus:border-indigo-500 focus:ring-indigo-500" %>
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
- <%= select_tag :controller_name, options_for_select([["All Controllers", ""]] + Binocs::Request.controllers_list.map { |c| [c, c] }, params[:controller_name]), class: "rounded-md bg-slate-700 border-slate-600 text-white text-sm focus:border-indigo-500 focus:ring-indigo-500" %>
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-slate-700 border-slate-600 text-indigo-500 focus:ring-indigo-500" %>
56
- <%= label_tag :has_exception, "Has Exception", class: "ml-2 text-sm text-slate-300" %>
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 - same height -->
60
- <div class="flex items-center gap-2">
61
- <%= submit_tag "Filter", class: "rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer h-[38px]" %>
62
- <%= link_to "Clear Filters", binocs.requests_path, class: "rounded-md bg-slate-600 px-4 py-2 text-sm font-medium text-white hover:bg-slate-500 h-[38px] inline-flex items-center" %>
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-slate-400">
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-green-600 hover:bg-green-500' : 'bg-slate-600 hover:bg-slate-500'"
79
- class="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-white transition-colors"
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-green-400 opacity-75"></span>
83
- <span :class="live ? 'bg-green-400' : 'bg-slate-400'" class="relative inline-flex rounded-full h-2 w-2"></span>
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-slate-700 px-3 py-2 text-sm font-medium text-white hover:bg-slate-600", data: { turbo_frame: "_top" } do %>
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-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500", data: { turbo_confirm: "Are you sure you want to clear all requests?" } %>
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-slate-800 rounded-lg border border-slate-700 overflow-hidden">
102
- <div id="requests-list" class="divide-y divide-slate-700">
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 %>