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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +528 -0
- data/Rakefile +7 -0
- data/app/assets/javascripts/binocs/application.js +105 -0
- data/app/assets/stylesheets/binocs/application.css +67 -0
- data/app/channels/binocs/application_cable/channel.rb +8 -0
- data/app/channels/binocs/application_cable/connection.rb +8 -0
- data/app/channels/binocs/requests_channel.rb +13 -0
- data/app/controllers/binocs/application_controller.rb +62 -0
- data/app/controllers/binocs/requests_controller.rb +69 -0
- data/app/helpers/binocs/application_helper.rb +61 -0
- data/app/models/binocs/application_record.rb +7 -0
- data/app/models/binocs/request.rb +198 -0
- data/app/views/binocs/requests/_empty_list.html.erb +9 -0
- data/app/views/binocs/requests/_request.html.erb +61 -0
- data/app/views/binocs/requests/index.html.erb +115 -0
- data/app/views/binocs/requests/show.html.erb +227 -0
- data/app/views/layouts/binocs/application.html.erb +109 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20240101000000_create_binocs_requests.rb +36 -0
- data/exe/binocs +86 -0
- data/lib/binocs/agent.rb +153 -0
- data/lib/binocs/agent_context.rb +165 -0
- data/lib/binocs/agent_manager.rb +302 -0
- data/lib/binocs/configuration.rb +65 -0
- data/lib/binocs/engine.rb +61 -0
- data/lib/binocs/log_subscriber.rb +56 -0
- data/lib/binocs/middleware/request_recorder.rb +264 -0
- data/lib/binocs/swagger/client.rb +100 -0
- data/lib/binocs/swagger/path_matcher.rb +118 -0
- data/lib/binocs/tui/agent_output.rb +163 -0
- data/lib/binocs/tui/agents_list.rb +195 -0
- data/lib/binocs/tui/app.rb +726 -0
- data/lib/binocs/tui/colors.rb +115 -0
- data/lib/binocs/tui/filter_menu.rb +162 -0
- data/lib/binocs/tui/help_screen.rb +93 -0
- data/lib/binocs/tui/request_detail.rb +899 -0
- data/lib/binocs/tui/request_list.rb +268 -0
- data/lib/binocs/tui/spirit_animal.rb +235 -0
- data/lib/binocs/tui/window.rb +98 -0
- data/lib/binocs/tui.rb +24 -0
- data/lib/binocs/version.rb +5 -0
- data/lib/binocs.rb +27 -0
- data/lib/generators/binocs/install/install_generator.rb +61 -0
- data/lib/generators/binocs/install/templates/create_binocs_requests.rb +36 -0
- data/lib/generators/binocs/install/templates/initializer.rb +25 -0
- data/lib/tasks/binocs_tasks.rake +38 -0
- 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>
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
|
@@ -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
|