binocs 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +528 -0
  4. data/Rakefile +7 -0
  5. data/app/assets/javascripts/binocs/application.js +105 -0
  6. data/app/assets/stylesheets/binocs/application.css +67 -0
  7. data/app/channels/binocs/application_cable/channel.rb +8 -0
  8. data/app/channels/binocs/application_cable/connection.rb +8 -0
  9. data/app/channels/binocs/requests_channel.rb +13 -0
  10. data/app/controllers/binocs/application_controller.rb +62 -0
  11. data/app/controllers/binocs/requests_controller.rb +69 -0
  12. data/app/helpers/binocs/application_helper.rb +61 -0
  13. data/app/models/binocs/application_record.rb +7 -0
  14. data/app/models/binocs/request.rb +198 -0
  15. data/app/views/binocs/requests/_empty_list.html.erb +9 -0
  16. data/app/views/binocs/requests/_request.html.erb +61 -0
  17. data/app/views/binocs/requests/index.html.erb +115 -0
  18. data/app/views/binocs/requests/show.html.erb +227 -0
  19. data/app/views/layouts/binocs/application.html.erb +109 -0
  20. data/config/importmap.rb +6 -0
  21. data/config/routes.rb +11 -0
  22. data/db/migrate/20240101000000_create_binocs_requests.rb +36 -0
  23. data/exe/binocs +86 -0
  24. data/lib/binocs/agent.rb +153 -0
  25. data/lib/binocs/agent_context.rb +165 -0
  26. data/lib/binocs/agent_manager.rb +302 -0
  27. data/lib/binocs/configuration.rb +65 -0
  28. data/lib/binocs/engine.rb +61 -0
  29. data/lib/binocs/log_subscriber.rb +56 -0
  30. data/lib/binocs/middleware/request_recorder.rb +264 -0
  31. data/lib/binocs/swagger/client.rb +100 -0
  32. data/lib/binocs/swagger/path_matcher.rb +118 -0
  33. data/lib/binocs/tui/agent_output.rb +163 -0
  34. data/lib/binocs/tui/agents_list.rb +195 -0
  35. data/lib/binocs/tui/app.rb +726 -0
  36. data/lib/binocs/tui/colors.rb +115 -0
  37. data/lib/binocs/tui/filter_menu.rb +162 -0
  38. data/lib/binocs/tui/help_screen.rb +93 -0
  39. data/lib/binocs/tui/request_detail.rb +899 -0
  40. data/lib/binocs/tui/request_list.rb +268 -0
  41. data/lib/binocs/tui/spirit_animal.rb +235 -0
  42. data/lib/binocs/tui/window.rb +98 -0
  43. data/lib/binocs/tui.rb +24 -0
  44. data/lib/binocs/version.rb +5 -0
  45. data/lib/binocs.rb +27 -0
  46. data/lib/generators/binocs/install/install_generator.rb +61 -0
  47. data/lib/generators/binocs/install/templates/create_binocs_requests.rb +36 -0
  48. data/lib/generators/binocs/install/templates/initializer.rb +25 -0
  49. data/lib/tasks/binocs_tasks.rake +38 -0
  50. metadata +149 -0
@@ -0,0 +1,115 @@
1
+ <div class="space-y-6" x-data="{ live: true, connected: false }" x-init="
2
+ // Check connection status
3
+ if (typeof Turbo !== 'undefined' && Turbo.cable) {
4
+ connected = Turbo.cable.connection.isOpen();
5
+ Turbo.cable.connection.events.connected = () => { connected = true; };
6
+ Turbo.cable.connection.events.disconnected = () => { connected = false; };
7
+ }
8
+ ">
9
+ <!-- Stats Cards -->
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>
13
+ <dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:total] %></dd>
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>
17
+ <dd class="mt-1 text-3xl font-semibold text-white"><%= @stats[:today] %></dd>
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>
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>
26
+ </div>
27
+ </div>
28
+
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">
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" %>
36
+ </div>
37
+
38
+ <!-- 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" %>
41
+ </div>
42
+
43
+ <!-- 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>
47
+
48
+ <!-- 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" %>
51
+ </div>
52
+
53
+ <!-- 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" %>
57
+ </div>
58
+
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
+ </div>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+
68
+ <!-- Actions Bar -->
69
+ <div class="flex justify-between items-center">
70
+ <div class="flex items-center gap-4">
71
+ <span class="text-sm text-slate-400">
72
+ Showing <%= @requests.size %> requests
73
+ </span>
74
+
75
+ <!-- Live Toggle -->
76
+ <button
77
+ @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"
80
+ >
81
+ <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>
84
+ </span>
85
+ <span x-text="live ? 'Live' : 'Paused'">Live</span>
86
+ </button>
87
+ </div>
88
+
89
+ <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 %>
91
+ <svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
92
+ <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
+ </svg>
94
+ Refresh
95
+ <% 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?" } %>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- 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">
103
+ <% if @requests.any? %>
104
+ <%= render partial: "binocs/requests/request", collection: @requests, as: :request %>
105
+ <% else %>
106
+ <%= render partial: "binocs/requests/empty_list" %>
107
+ <% end %>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Turbo Stream Subscription (controlled by live toggle) -->
112
+ <template x-if="live">
113
+ <%= turbo_stream_from "binocs_requests" %>
114
+ </template>
115
+ </div>
@@ -0,0 +1,227 @@
1
+ <div class="space-y-6">
2
+ <!-- Back Link -->
3
+ <div>
4
+ <%= link_to binocs.requests_path, class: "inline-flex items-center text-sm text-slate-400 hover:text-white" do %>
5
+ <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
+ </svg>
8
+ Back to Requests
9
+ <% end %>
10
+ </div>
11
+
12
+ <!-- Header -->
13
+ <div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
14
+ <div class="flex items-center justify-between">
15
+ <div class="flex items-center space-x-4">
16
+ <span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= method_badge_class(@request.method) %>">
17
+ <%= @request.method %>
18
+ </span>
19
+ <span class="inline-flex items-center rounded px-3 py-1.5 text-sm font-medium <%= status_badge_class(@request.status_code) %>">
20
+ <%= @request.status_code %>
21
+ </span>
22
+ <h1 class="text-lg font-semibold text-white break-all"><%= @request.path %></h1>
23
+ </div>
24
+ <div class="flex items-center space-x-2">
25
+ <%= link_to "Open in Swagger", @request.swagger_url, target: "_blank", class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500" %>
26
+ <%= button_to "Delete", binocs.request_path(@request.uuid), 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?" } %>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-4">
31
+ <div>
32
+ <dt class="text-sm text-slate-400">Duration</dt>
33
+ <dd class="text-lg font-medium text-white"><%= @request.formatted_duration %></dd>
34
+ </div>
35
+ <div>
36
+ <dt class="text-sm text-slate-400">Memory Delta</dt>
37
+ <dd class="text-lg font-medium text-white"><%= @request.formatted_memory_delta %></dd>
38
+ </div>
39
+ <div>
40
+ <dt class="text-sm text-slate-400">IP Address</dt>
41
+ <dd class="text-lg font-medium text-white"><%= @request.ip_address || "N/A" %></dd>
42
+ </div>
43
+ <div>
44
+ <dt class="text-sm text-slate-400">Time</dt>
45
+ <dd class="text-lg font-medium text-white"><%= @request.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
46
+ </div>
47
+ </div>
48
+
49
+ <% if @request.controller_action %>
50
+ <div class="mt-4 pt-4 border-t border-slate-700">
51
+ <span class="text-sm text-slate-400">Route:</span>
52
+ <span class="ml-2 text-sm text-white"><%= @request.controller_action %></span>
53
+ </div>
54
+ <% end %>
55
+ </div>
56
+
57
+ <!-- Exception (if present) -->
58
+ <% if @request.has_exception? %>
59
+ <div class="bg-red-900/20 rounded-lg p-6 border border-red-800">
60
+ <h2 class="text-lg font-semibold text-red-400 flex items-center">
61
+ <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
62
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
63
+ </svg>
64
+ Exception
65
+ </h2>
66
+ <div class="mt-4 space-y-3">
67
+ <div>
68
+ <span class="text-sm text-red-300 font-medium"><%= @request.exception["class"] %></span>
69
+ </div>
70
+ <div class="text-sm text-red-200">
71
+ <%= @request.exception["message"] %>
72
+ </div>
73
+ <% if @request.exception["backtrace"].present? %>
74
+ <div class="mt-4">
75
+ <h3 class="text-sm font-medium text-red-300 mb-2">Backtrace</h3>
76
+ <pre class="bg-slate-900 rounded p-4 text-xs text-red-200 overflow-x-auto max-h-96"><%= @request.exception["backtrace"].join("\n") %></pre>
77
+ </div>
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+ <% end %>
82
+
83
+ <!-- Tabs -->
84
+ <div x-data="{ activeTab: 'params' }" class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
85
+ <!-- Tab Headers -->
86
+ <div class="border-b border-slate-700">
87
+ <nav class="flex -mb-px">
88
+ <button @click="activeTab = 'params'" :class="activeTab === 'params' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
89
+ Params
90
+ </button>
91
+ <button @click="activeTab = 'headers'" :class="activeTab === 'headers' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
92
+ Headers
93
+ </button>
94
+ <button @click="activeTab = 'body'" :class="activeTab === 'body' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
95
+ Body
96
+ </button>
97
+ <button @click="activeTab = 'response'" :class="activeTab === 'response' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
98
+ Response
99
+ </button>
100
+ <button @click="activeTab = 'logs'" :class="activeTab === 'logs' ? 'border-indigo-500 text-indigo-400' : 'border-transparent text-slate-400 hover:text-white hover:border-slate-600'" class="px-6 py-3 border-b-2 text-sm font-medium">
101
+ Logs
102
+ <% if @request.logs.present? && @request.logs.any? %>
103
+ <span class="ml-2 inline-flex items-center rounded-full bg-slate-700 px-2 py-0.5 text-xs"><%= @request.logs.size %></span>
104
+ <% end %>
105
+ </button>
106
+ </nav>
107
+ </div>
108
+
109
+ <!-- Tab Content -->
110
+ <div class="p-6">
111
+ <!-- Params Tab -->
112
+ <div x-show="activeTab === 'params'">
113
+ <% if @request.params.present? && @request.params.any? %>
114
+ <div class="space-y-2">
115
+ <% @request.params.each do |key, value| %>
116
+ <div class="flex">
117
+ <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
118
+ <span class="text-sm text-white break-all"><%= format_value(value) %></span>
119
+ </div>
120
+ <% end %>
121
+ </div>
122
+ <% else %>
123
+ <p class="text-sm text-slate-400">No parameters</p>
124
+ <% end %>
125
+ </div>
126
+
127
+ <!-- Headers Tab -->
128
+ <div x-show="activeTab === 'headers'" x-cloak>
129
+ <div class="space-y-6">
130
+ <div>
131
+ <h3 class="text-sm font-medium text-slate-300 mb-3">Request Headers</h3>
132
+ <% if @request.request_headers.present? && @request.request_headers.any? %>
133
+ <div class="space-y-2">
134
+ <% @request.request_headers.each do |key, value| %>
135
+ <div class="flex">
136
+ <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
137
+ <span class="text-sm text-white break-all"><%= value %></span>
138
+ </div>
139
+ <% end %>
140
+ </div>
141
+ <% else %>
142
+ <p class="text-sm text-slate-400">No request headers</p>
143
+ <% end %>
144
+ </div>
145
+
146
+ <div>
147
+ <h3 class="text-sm font-medium text-slate-300 mb-3">Response Headers</h3>
148
+ <% if @request.response_headers.present? && @request.response_headers.any? %>
149
+ <div class="space-y-2">
150
+ <% @request.response_headers.each do |key, value| %>
151
+ <div class="flex">
152
+ <span class="text-sm font-medium text-indigo-400 w-48 flex-shrink-0"><%= key %></span>
153
+ <span class="text-sm text-white break-all"><%= value %></span>
154
+ </div>
155
+ <% end %>
156
+ </div>
157
+ <% else %>
158
+ <p class="text-sm text-slate-400">No response headers</p>
159
+ <% end %>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Body Tab -->
165
+ <div x-show="activeTab === 'body'" x-cloak>
166
+ <h3 class="text-sm font-medium text-slate-300 mb-3">Request Body</h3>
167
+ <% if @request.request_body.present? %>
168
+ <pre class="bg-slate-900 rounded p-4 text-sm text-white overflow-x-auto max-h-96"><%= format_body(@request.request_body) %></pre>
169
+ <% else %>
170
+ <p class="text-sm text-slate-400">No request body</p>
171
+ <% end %>
172
+ </div>
173
+
174
+ <!-- Response Tab -->
175
+ <div x-show="activeTab === 'response'" x-cloak>
176
+ <h3 class="text-sm font-medium text-slate-300 mb-3">Response Body</h3>
177
+ <% if @request.response_body.present? %>
178
+ <pre class="bg-slate-900 rounded p-4 text-sm text-white overflow-x-auto max-h-96"><%= format_body(@request.response_body) %></pre>
179
+ <% else %>
180
+ <p class="text-sm text-slate-400">No response body captured</p>
181
+ <% end %>
182
+ </div>
183
+
184
+ <!-- Logs Tab -->
185
+ <div x-show="activeTab === 'logs'" x-cloak>
186
+ <% if @request.logs.present? && @request.logs.any? %>
187
+ <div class="space-y-3">
188
+ <% @request.logs.each do |log| %>
189
+ <div class="bg-slate-900 rounded p-4">
190
+ <div class="flex items-center justify-between mb-2">
191
+ <span class="text-xs font-medium text-indigo-400 uppercase"><%= log["type"] %></span>
192
+ <span class="text-xs text-slate-500"><%= log["timestamp"] %></span>
193
+ </div>
194
+ <% if log["type"] == "controller" %>
195
+ <div class="text-sm text-white">
196
+ <span class="text-slate-400">Controller:</span> <%= log["controller"] %>#<%= log["action"] %>
197
+ <% if log["view_runtime"] %>
198
+ <span class="ml-4 text-slate-400">View:</span> <%= log["view_runtime"] %>ms
199
+ <% end %>
200
+ <% if log["db_runtime"] %>
201
+ <span class="ml-4 text-slate-400">DB:</span> <%= log["db_runtime"] %>ms
202
+ <% end %>
203
+ </div>
204
+ <% elsif log["type"] == "redirect" %>
205
+ <div class="text-sm text-white">
206
+ <span class="text-slate-400">Redirect to:</span> <%= log["location"] %>
207
+ <span class="ml-4 text-slate-400">Status:</span> <%= log["status"] %>
208
+ </div>
209
+ <% else %>
210
+ <pre class="text-sm text-white"><%= format_value(log) %></pre>
211
+ <% end %>
212
+ </div>
213
+ <% end %>
214
+ </div>
215
+ <% else %>
216
+ <p class="text-sm text-slate-400">No logs captured</p>
217
+ <% end %>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ <!-- Full URL -->
223
+ <div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
224
+ <h2 class="text-sm font-medium text-slate-400 mb-2">Full URL</h2>
225
+ <p class="text-sm text-white break-all font-mono"><%= @request.full_url %></p>
226
+ </div>
227
+ </div>
@@ -0,0 +1,109 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full bg-gray-900">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Binocs - Request Monitor</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+ <%= stylesheet_link_tag "binocs/application", media: "all" %>
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
12
+ <script>
13
+ tailwind.config = {
14
+ darkMode: 'class',
15
+ theme: {
16
+ extend: {
17
+ colors: {
18
+ binocs: {
19
+ bg: '#0f172a',
20
+ card: '#1e293b',
21
+ border: '#334155',
22
+ accent: '#6366f1'
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ </script>
29
+ <%= turbo_include_tags %>
30
+ <%= stimulus_include_tags rescue nil %>
31
+ <style>[x-cloak] { display: none !important; }</style>
32
+ </head>
33
+ <body class="h-full">
34
+ <div class="min-h-full">
35
+ <!-- Navigation -->
36
+ <nav class="bg-slate-800 border-b border-slate-700">
37
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
38
+ <div class="flex h-16 items-center justify-between">
39
+ <div class="flex items-center">
40
+ <div class="flex-shrink-0">
41
+ <span class="text-2xl font-bold text-indigo-400">Binocs</span>
42
+ <span class="ml-2 text-sm text-slate-400">Request Monitor</span>
43
+ </div>
44
+ <div class="ml-10 flex items-baseline space-x-4">
45
+ <%= link_to "Requests", binocs.requests_path, class: "rounded-md px-3 py-2 text-sm font-medium #{current_page?(binocs.requests_path) ? 'bg-slate-900 text-white' : 'text-slate-300 hover:bg-slate-700 hover:text-white'}" %>
46
+ </div>
47
+ </div>
48
+ <div class="flex items-center space-x-4">
49
+ <span class="text-xs text-slate-500">
50
+ Rails <%= Rails::VERSION::STRING %> |
51
+ Ruby <%= RUBY_VERSION %> |
52
+ <%= Rails.env %>
53
+ </span>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </nav>
58
+
59
+ <!-- Authentication Required Banner (hidden by default, shown by JS when WebSocket fails) -->
60
+ <div id="binocs-auth-banner" class="hidden bg-amber-900/50 border-b border-amber-700">
61
+ <div class="mx-auto max-w-7xl px-4 py-3 sm:px-6 lg:px-8">
62
+ <div class="flex items-center justify-between flex-wrap gap-2">
63
+ <div class="flex items-center">
64
+ <svg class="h-5 w-5 text-amber-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
66
+ </svg>
67
+ <p class="text-sm text-amber-200">
68
+ <strong>Real-time updates unavailable.</strong>
69
+ Authentication may be required for live streaming.
70
+ </p>
71
+ </div>
72
+ <div class="flex gap-2">
73
+ <% login_url = "#{Binocs.configuration.resolved_login_path}?redirect_to=#{ERB::Util.url_encode(request.fullpath)}" rescue Binocs.configuration.resolved_login_path %>
74
+ <a href="<%= login_url %>" class="rounded-md bg-amber-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-amber-500">
75
+ Sign in
76
+ </a>
77
+ <button onclick="document.getElementById('binocs-auth-banner').classList.add('hidden')" class="rounded-md bg-amber-800 px-3 py-1.5 text-sm font-medium text-amber-200 hover:bg-amber-700">
78
+ Dismiss
79
+ </button>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Flash Messages -->
86
+ <% if notice %>
87
+ <div class="bg-green-900/50 border-b border-green-800">
88
+ <div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
89
+ <p class="text-sm text-green-300"><%= notice %></p>
90
+ </div>
91
+ </div>
92
+ <% end %>
93
+ <% if alert %>
94
+ <div class="bg-red-900/50 border-b border-red-800">
95
+ <div class="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
96
+ <p class="text-sm text-red-300"><%= alert %></p>
97
+ </div>
98
+ </div>
99
+ <% end %>
100
+
101
+ <!-- Main Content -->
102
+ <main class="bg-slate-900 min-h-screen">
103
+ <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
104
+ <%= yield %>
105
+ </div>
106
+ </main>
107
+ </div>
108
+ </body>
109
+ </html>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Binocs Engine Importmap
4
+ pin "binocs/application", to: "binocs/application.js"
5
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
6
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Binocs::Engine.routes.draw do
4
+ root to: "requests#index"
5
+
6
+ resources :requests, only: [:index, :show, :destroy] do
7
+ collection do
8
+ delete :clear
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBinocsRequests < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :binocs_requests do |t|
6
+ t.string :uuid, null: false, index: { unique: true }
7
+ t.string :method, null: false
8
+ t.string :path, null: false
9
+ t.text :full_url
10
+ t.string :controller_name
11
+ t.string :action_name
12
+ t.string :route_name
13
+ t.text :params
14
+ t.text :request_headers
15
+ t.text :response_headers
16
+ t.text :request_body
17
+ t.text :response_body
18
+ t.integer :status_code
19
+ t.float :duration_ms
20
+ t.string :ip_address
21
+ t.string :session_id
22
+ t.text :logs
23
+ t.text :exception
24
+ t.integer :memory_delta
25
+ t.string :content_type
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ add_index :binocs_requests, :method
31
+ add_index :binocs_requests, :status_code
32
+ add_index :binocs_requests, :controller_name
33
+ add_index :binocs_requests, :created_at
34
+ add_index :binocs_requests, :duration_ms
35
+ end
36
+ end
data/exe/binocs ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+
6
+ options = {
7
+ environment: ENV['RAILS_ENV'] || 'development',
8
+ refresh_interval: 2
9
+ }
10
+
11
+ OptionParser.new do |opts|
12
+ opts.banner = "Usage: binocs [options]"
13
+
14
+ opts.on("-e", "--environment ENV", "Rails environment (default: development)") do |env|
15
+ options[:environment] = env
16
+ end
17
+
18
+ opts.on("-r", "--refresh SECONDS", Integer, "Refresh interval in seconds (default: 2)") do |r|
19
+ options[:refresh_interval] = r
20
+ end
21
+
22
+ opts.on("-h", "--help", "Show this help message") do
23
+ puts opts
24
+ puts <<~HELP
25
+
26
+ Vim-style keybindings:
27
+ j/↓ Move down
28
+ k/↑ Move up
29
+ g Go to top
30
+ G Go to bottom
31
+ Enter/l View request details
32
+ h/Esc Go back / Close detail view
33
+ / Search
34
+ f Filter menu
35
+ r Refresh
36
+ q Quit
37
+ ? Show help
38
+
39
+ HELP
40
+ exit
41
+ end
42
+ end.parse!
43
+
44
+ # Boot Rails environment
45
+ ENV['RAILS_ENV'] = options[:environment]
46
+
47
+ # Find and load the Rails application
48
+ def find_rails_root
49
+ dir = Dir.pwd
50
+ while dir != '/'
51
+ return dir if File.exist?(File.join(dir, 'config', 'environment.rb'))
52
+ dir = File.dirname(dir)
53
+ end
54
+ nil
55
+ end
56
+
57
+ rails_root = find_rails_root
58
+
59
+ if rails_root.nil?
60
+ puts "\e[31mError: Could not find Rails application.\e[0m"
61
+ puts "Make sure you're running this from within a Rails project directory."
62
+ exit 1
63
+ end
64
+
65
+ Dir.chdir(rails_root)
66
+
67
+ begin
68
+ require File.join(rails_root, 'config', 'environment')
69
+ rescue => e
70
+ puts "\e[31mError loading Rails environment:\e[0m #{e.message}"
71
+ exit 1
72
+ end
73
+
74
+ # Now load and run the TUI
75
+ require 'binocs/tui'
76
+
77
+ begin
78
+ Binocs::TUI::App.new(options).run
79
+ rescue Interrupt
80
+ # Clean exit on Ctrl+C
81
+ exit 0
82
+ rescue => e
83
+ puts "\e[31mError:\e[0m #{e.message}"
84
+ puts e.backtrace.first(5).join("\n") if ENV['DEBUG']
85
+ exit 1
86
+ end