rails_vitals 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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +340 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/rails_vitals/application.css +180 -0
  6. data/app/controllers/rails_vitals/application_controller.rb +30 -0
  7. data/app/controllers/rails_vitals/associations_controller.rb +8 -0
  8. data/app/controllers/rails_vitals/dashboard_controller.rb +59 -0
  9. data/app/controllers/rails_vitals/heatmap_controller.rb +39 -0
  10. data/app/controllers/rails_vitals/models_controller.rb +65 -0
  11. data/app/controllers/rails_vitals/n_plus_ones_controller.rb +43 -0
  12. data/app/controllers/rails_vitals/requests_controller.rb +44 -0
  13. data/app/helpers/rails_vitals/application_helper.rb +63 -0
  14. data/app/jobs/rails_vitals/application_job.rb +4 -0
  15. data/app/mailers/rails_vitals/application_mailer.rb +6 -0
  16. data/app/models/rails_vitals/application_record.rb +5 -0
  17. data/app/views/layouts/rails_vitals/application.html.erb +27 -0
  18. data/app/views/rails_vitals/associations/index.html.erb +370 -0
  19. data/app/views/rails_vitals/dashboard/index.html.erb +158 -0
  20. data/app/views/rails_vitals/heatmap/index.html.erb +66 -0
  21. data/app/views/rails_vitals/models/index.html.erb +117 -0
  22. data/app/views/rails_vitals/n_plus_ones/index.html.erb +49 -0
  23. data/app/views/rails_vitals/n_plus_ones/show.html.erb +139 -0
  24. data/app/views/rails_vitals/requests/index.html.erb +60 -0
  25. data/app/views/rails_vitals/requests/show.html.erb +396 -0
  26. data/config/routes.rb +9 -0
  27. data/lib/rails_vitals/analyzers/association_mapper.rb +121 -0
  28. data/lib/rails_vitals/analyzers/n_plus_one_aggregator.rb +116 -0
  29. data/lib/rails_vitals/analyzers/sql_tokenizer.rb +240 -0
  30. data/lib/rails_vitals/collector.rb +78 -0
  31. data/lib/rails_vitals/configuration.rb +27 -0
  32. data/lib/rails_vitals/engine.rb +25 -0
  33. data/lib/rails_vitals/instrumentation/callback_instrumentation.rb +30 -0
  34. data/lib/rails_vitals/middleware/panel_injector.rb +75 -0
  35. data/lib/rails_vitals/notifications/subscriber.rb +59 -0
  36. data/lib/rails_vitals/panel_renderer.rb +233 -0
  37. data/lib/rails_vitals/request_record.rb +51 -0
  38. data/lib/rails_vitals/scorers/base_scorer.rb +25 -0
  39. data/lib/rails_vitals/scorers/composite_scorer.rb +36 -0
  40. data/lib/rails_vitals/scorers/n_plus_one_scorer.rb +43 -0
  41. data/lib/rails_vitals/scorers/query_scorer.rb +42 -0
  42. data/lib/rails_vitals/store.rb +34 -0
  43. data/lib/rails_vitals/version.rb +3 -0
  44. data/lib/rails_vitals.rb +33 -0
  45. data/lib/tasks/rails_vitals_tasks.rake +4 -0
  46. metadata +113 -0
@@ -0,0 +1,158 @@
1
+ <div class="page-title">Dashboard</div>
2
+
3
+ <div class="grid-3">
4
+ <div class="stat-card">
5
+ <div class="stat-value"><%= @total %></div>
6
+ <div class="stat-label">Requests Recorded</div>
7
+ </div>
8
+ <div class="stat-card">
9
+ <div class="stat-value"><%= @avg_score %></div>
10
+ <div class="stat-label">Avg Health Score</div>
11
+ </div>
12
+ <div class="stat-card">
13
+ <div class="stat-value"><%= @avg_queries %></div>
14
+ <div class="stat-label">Avg Query Count</div>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="card">
19
+ <div class="card-title">Recent Requests</div>
20
+ <table>
21
+ <thead>
22
+ <tr>
23
+ <th>Endpoint</th>
24
+ <th>Score</th>
25
+ <th>Queries</th>
26
+ <th>DB Time</th>
27
+ <th>N+1</th>
28
+ <th>Duration</th>
29
+ <th></th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% @records.first(10).each do |r| %>
34
+ <tr>
35
+ <td><%= r.endpoint %></td>
36
+ <td>
37
+ <span class="badge badge-<%= r.color %>">
38
+ <%= r.score %> <%= r.label %>
39
+ </span>
40
+ </td>
41
+ <td><%= r.total_query_count %></td>
42
+ <td><%= r.total_db_time_ms.round(1) %>ms</td>
43
+ <td>
44
+ <% if r.n_plus_one_patterns.any? %>
45
+ <span class="n1-badge"><%= r.n_plus_one_patterns.size %></span>
46
+ <% else %>
47
+ <span style="color:#68d391;">None</span>
48
+ <% end %>
49
+ </td>
50
+ <td><%= r.duration_ms&.round(1) %>ms</td>
51
+ <td><%= link_to "→", rails_vitals.request_path(r.id) %></td>
52
+ </tr>
53
+ <% end %>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+
58
+ <div class="card">
59
+ <div class="card-title">Top Offenders by Avg Score</div>
60
+ <table>
61
+ <thead>
62
+ <tr>
63
+ <th>Endpoint</th>
64
+ <th>Hits</th>
65
+ <th>Avg Score</th>
66
+ <th>Avg Queries</th>
67
+ <th>Avg DB Time</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ <% @top_offenders.each do |endpoint, stats| %>
72
+ <tr>
73
+ <td><%= endpoint %></td>
74
+ <td><%= stats[:count] %></td>
75
+ <td><%= stats[:avg_score] %></td>
76
+ <td><%= stats[:avg_queries] %></td>
77
+ <td><%= stats[:avg_db_time_ms].round(1) %>ms</td>
78
+ </tr>
79
+ <% end %>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+
84
+ <% if @records.any? %>
85
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:16px;">
86
+
87
+ <div class="card">
88
+ <div class="card-title">Score Distribution</div>
89
+ <table>
90
+ <thead>
91
+ <tr>
92
+ <th>Bracket</th>
93
+ <th>Count</th>
94
+ <th>%</th>
95
+ </tr>
96
+ </thead>
97
+ <tbody>
98
+ <% @score_distribution.each do |label, count| %>
99
+ <tr>
100
+ <td><%= label %></td>
101
+ <td><%= count %></td>
102
+ <td style="color:#a0aec0;">
103
+ <%= @total > 0 ? ((count.to_f / @total) * 100).round(1) : 0 %>%
104
+ </td>
105
+ </tr>
106
+ <% end %>
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+
111
+ <div class="card">
112
+ <div class="card-title">Health Trend (last 10 requests)</div>
113
+ <table>
114
+ <thead>
115
+ <tr>
116
+ <th>Endpoint</th>
117
+ <th>Score</th>
118
+ </tr>
119
+ </thead>
120
+ <tbody>
121
+ <% @health_trend.each do |endpoint, score| %>
122
+ <tr>
123
+ <td style="color:#a0aec0;font-size:11px;"><%= endpoint %></td>
124
+ <td>
125
+ <span class="badge badge-<%= score_label_to_color(score) %>">
126
+ <%= score %>
127
+ </span>
128
+ </td>
129
+ </tr>
130
+ <% end %>
131
+ </tbody>
132
+ </table>
133
+ </div>
134
+
135
+ <div class="card">
136
+ <div class="card-title">Query Volume (last 10 requests)</div>
137
+ <table>
138
+ <thead>
139
+ <tr>
140
+ <th>Request</th>
141
+ <th>Queries</th>
142
+ <th>DB Time</th>
143
+ </tr>
144
+ </thead>
145
+ <tbody>
146
+ <% @query_volume.each do |label, stats| %>
147
+ <tr>
148
+ <td style="color:#a0aec0;font-size:11px;"><%= label %></td>
149
+ <td><%= stats[:queries] %></td>
150
+ <td style="color:#a0aec0;"><%= stats[:db_time] %>ms</td>
151
+ </tr>
152
+ <% end %>
153
+ </tbody>
154
+ </table>
155
+ </div>
156
+
157
+ </div>
158
+ <% end %>
@@ -0,0 +1,66 @@
1
+ <div style="margin-bottom:24px;">
2
+ <h1 style="font-size:20px;font-weight:bold;color:#e2e8f0;">Endpoint Heatmap</h1>
3
+ <p style="color:#a0aec0;font-size:13px;margin-top:4px;">
4
+ <%= @total %> requests recorded across <%= @heatmap.size %> endpoints
5
+ </p>
6
+ </div>
7
+
8
+ <% if @heatmap.any? %>
9
+ <div class="card">
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th>Endpoint</th>
14
+ <th>Avg Score</th>
15
+ <th>Hits</th>
16
+ <th>Avg Queries</th>
17
+ <th>Avg DB Time</th>
18
+ <th>Avg Callback Time</th>
19
+ <th>N+1 Frequency</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% @heatmap.each do |row| %>
24
+ <% color = score_label_to_color(row[:avg_score]) %>
25
+ <tr>
26
+ <td style="font-weight:bold;">
27
+ <%= link_to row[:endpoint],
28
+ requests_path(endpoint: row[:endpoint]),
29
+ style: "color:#90cdf4;" %>
30
+ </td>
31
+ <td>
32
+ <span class="badge badge-<%= color %>">
33
+ <%= row[:avg_score] %>
34
+ </span>
35
+ </td>
36
+ <td style="color:#a0aec0;"><%= row[:hits] %></td>
37
+ <td>
38
+ <span style="color:<%= query_heat_color(row[:avg_queries]) %>">
39
+ <%= row[:avg_queries] %>
40
+ </span>
41
+ </td>
42
+ <td>
43
+ <span style="color:<%= time_heat_color(row[:avg_db_time]) %>">
44
+ <%= row[:avg_db_time] %>ms
45
+ </span>
46
+ </td>
47
+ <td>
48
+ <span style="color:<%= time_heat_color(row[:avg_callback_time]) %>">
49
+ <%= row[:avg_callback_time] %>ms
50
+ </span>
51
+ </td>
52
+ <td>
53
+ <span style="color:<%= n1_heat_color(row[:n_plus_one_freq]) %>">
54
+ <%= row[:n_plus_one_freq] %>%
55
+ </span>
56
+ </td>
57
+ </tr>
58
+ <% end %>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ <% else %>
63
+ <div class="card" style="color:#a0aec0;text-align:center;padding:48px;">
64
+ No requests recorded yet. Visit your app to generate data.
65
+ </div>
66
+ <% end %>
@@ -0,0 +1,117 @@
1
+ <div style="margin-bottom:24px;">
2
+ <h1 style="font-size:20px;font-weight:bold;color:#e2e8f0;">Per-Model Breakdown</h1>
3
+ <p style="color:#a0aec0;font-size:13px;margin-top:4px;">
4
+ Query activity aggregated across <%= @total_requests %> recorded requests
5
+ </p>
6
+ </div>
7
+
8
+ <% if @breakdown.any? %>
9
+ <% @breakdown.each do |row| %>
10
+ <div class="card" style="margin-bottom:16px;">
11
+
12
+ <%# Model header %>
13
+ <div style="
14
+ display:flex;
15
+ justify-content:space-between;
16
+ align-items:center;
17
+ margin-bottom:16px;
18
+ padding-bottom:12px;
19
+ border-bottom:1px solid #2d3748;
20
+ ">
21
+ <span style="font-size:16px;font-weight:bold;color:#90cdf4;">
22
+ <%= row[:model] %>
23
+ </span>
24
+ <div style="display:flex;gap:24px;font-size:13px;">
25
+ <span>
26
+ <span style="color:#a0aec0;">Queries</span>
27
+ <span style="color:#e2e8f0;margin-left:6px;font-weight:bold;">
28
+ <%= row[:query_count] %>
29
+ </span>
30
+ </span>
31
+ <span>
32
+ <span style="color:#a0aec0;">Total DB Time</span>
33
+ <span style="color:<%= time_heat_color(row[:total_time_ms]) %>;margin-left:6px;font-weight:bold;">
34
+ <%= row[:total_time_ms] %>ms
35
+ </span>
36
+ </span>
37
+ <span>
38
+ <span style="color:#a0aec0;">Avg Query Time</span>
39
+ <span style="color:<%= time_heat_color(row[:avg_time_ms]) %>;margin-left:6px;font-weight:bold;">
40
+ <%= row[:avg_time_ms] %>ms
41
+ </span>
42
+ </span>
43
+ </div>
44
+ </div>
45
+
46
+ <div style="display:grid;grid-template-columns:<%= row[:callbacks].any? ? '1fr 1fr 1fr' : '1fr 1fr' %>;gap:16px;">
47
+
48
+ <%# Endpoints that triggered this model %>
49
+ <div>
50
+ <div style="color:#a0aec0;font-size:11px;font-weight:bold;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em;">
51
+ Triggered By
52
+ </div>
53
+ <table>
54
+ <thead>
55
+ <tr>
56
+ <th>Endpoint</th>
57
+ <th>Queries</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ <% row[:endpoints].each do |endpoint, count| %>
62
+ <tr>
63
+ <td>
64
+ <%= link_to endpoint,
65
+ requests_path(endpoint: endpoint),
66
+ style: "color:#90cdf4;font-size:12px;" %>
67
+ </td>
68
+ <td style="color:#a0aec0;"><%= count %></td>
69
+ </tr>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
73
+ </div>
74
+
75
+ <%# Callback types — only if present %>
76
+ <% if row[:callbacks].any? %>
77
+ <div>
78
+ <div style="color:#a0aec0;font-size:11px;font-weight:bold;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em;">
79
+ Callbacks
80
+ </div>
81
+ <table>
82
+ <thead>
83
+ <tr>
84
+ <th>Type</th>
85
+ <th>Count</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody>
89
+ <% row[:callbacks].each do |type, count| %>
90
+ <tr>
91
+ <td>
92
+ <span style="
93
+ background:<%= callback_color(type) %>;
94
+ color:#fff;
95
+ padding:1px 6px;
96
+ border-radius:3px;
97
+ font-size:11px;
98
+ ">
99
+ <%= type %>
100
+ </span>
101
+ </td>
102
+ <td><%= count %></td>
103
+ </tr>
104
+ <% end %>
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ <% end %>
109
+
110
+ </div>
111
+ </div>
112
+ <% end %>
113
+ <% else %>
114
+ <div class="card" style="color:#a0aec0;text-align:center;padding:48px;">
115
+ No data recorded yet. Visit your app to generate data.
116
+ </div>
117
+ <% end %>
@@ -0,0 +1,49 @@
1
+ <div style="margin-bottom:24px;">
2
+ <h1 style="font-size:20px;font-weight:bold;color:#e2e8f0;">N+1 Patterns</h1>
3
+ <p style="color:#a0aec0;font-size:13px;margin-top:4px;">
4
+ Aggregated across <%= @total_requests %> recorded requests
5
+ </p>
6
+ </div>
7
+
8
+ <% if @patterns.any? %>
9
+ <div class="card">
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th>Pattern</th>
14
+ <th>Occurrences</th>
15
+ <th>Affected Endpoints</th>
16
+ <th>Fix</th>
17
+ <th></th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @patterns.each do |p| %>
22
+ <tr>
23
+ <td style="font-family:monospace;font-size:11px;color:#a0aec0;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
24
+ <%= p[:pattern] %>
25
+ </td>
26
+ <td>
27
+ <span style="color:#fc8181;font-weight:bold;"><%= p[:occurrences] %>x</span>
28
+ </td>
29
+ <td style="color:#a0aec0;font-size:12px;">
30
+ <%= p[:endpoints].keys.join(", ") %>
31
+ </td>
32
+ <td style="font-family:monospace;font-size:11px;color:#68d391;">
33
+ <%= p[:fix_suggestion][:code] %>
34
+ </td>
35
+ <td>
36
+ <%= link_to "Simulate →",
37
+ n_plus_one_path(pattern_id(p)),
38
+ style: "color:#90cdf4;font-size:12px;" %>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ <% else %>
46
+ <div class="card" style="color:#a0aec0;text-align:center;padding:48px;">
47
+ No N+1 patterns detected yet.
48
+ </div>
49
+ <% end %>
@@ -0,0 +1,139 @@
1
+ <%# app/views/rails_vitals/n_plus_one/show.html.erb %>
2
+ <div style="margin-bottom:24px;">
3
+ <div style="margin-bottom:8px;">
4
+ <%= link_to "← N+1 Patterns", n_plus_ones_path,
5
+ style: "color:#a0aec0;font-size:13px;" %>
6
+ </div>
7
+ <h1 style="font-size:20px;font-weight:bold;color:#e2e8f0;">Impact Simulator</h1>
8
+ </div>
9
+
10
+ <%# Pattern card %>
11
+ <div class="card" style="margin-bottom:16px;">
12
+ <div class="card-title">Detected Pattern</div>
13
+ <pre style="
14
+ background:#1a202c;
15
+ padding:12px;
16
+ border-radius:6px;
17
+ font-size:12px;
18
+ color:#a0aec0;
19
+ white-space:pre-wrap;
20
+ word-break:break-all;
21
+ margin-bottom:16px;
22
+ "><%= @pattern[:pattern] %></pre>
23
+
24
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;font-size:13px;">
25
+ <div>
26
+ <div style="color:#a0aec0;margin-bottom:4px;">Total Occurrences</div>
27
+ <div style="color:#fc8181;font-size:20px;font-weight:bold;"><%= @pattern[:occurrences] %>x</div>
28
+ </div>
29
+ <div>
30
+ <div style="color:#a0aec0;margin-bottom:4px;">Estimated Time Wasted</div>
31
+ <div style="color:#fc8181;font-size:20px;font-weight:bold;"><%= @estimated_saving_ms %>ms</div>
32
+ </div>
33
+ <div>
34
+ <div style="color:#a0aec0;margin-bottom:4px;">Affected Requests</div>
35
+ <div style="color:#e2e8f0;font-size:20px;font-weight:bold;"><%= @affected_requests.size %></div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <%# Fix suggestion card %>
41
+ <div class="card" style="margin-bottom:16px;">
42
+ <div class="card-title">Suggested Fix</div>
43
+ <div style="margin-bottom:12px;color:#a0aec0;font-size:13px;">
44
+ <%= @pattern[:fix_suggestion][:description] %>
45
+ </div>
46
+ <pre style="
47
+ background:#1a202c;
48
+ padding:12px;
49
+ border-radius:6px;
50
+ font-size:13px;
51
+ color:#68d391;
52
+ "><%= @pattern[:fix_suggestion][:code] %></pre>
53
+ </div>
54
+
55
+ <%# Impact simulator card %>
56
+ <div class="card" style="margin-bottom:16px;">
57
+ <div class="card-title">
58
+ Impact Simulator
59
+ <span style="color:#a0aec0;font-weight:normal;font-size:12px;margin-left:8px;">
60
+ if you fix this N+1
61
+ </span>
62
+ </div>
63
+
64
+ <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px;">
65
+ <div style="
66
+ background:#1a202c;
67
+ border-radius:8px;
68
+ padding:16px;
69
+ text-align:center;
70
+ ">
71
+ <div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
72
+ Est. Saving Per Request
73
+ </div>
74
+ <div style="color:#68d391;font-size:28px;font-weight:bold;">
75
+ <%= @avg_saving_per_request %>ms
76
+ </div>
77
+ </div>
78
+ <div style="
79
+ background:#1a202c;
80
+ border-radius:8px;
81
+ padding:16px;
82
+ text-align:center;
83
+ ">
84
+ <div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
85
+ Total Time Recovered
86
+ </div>
87
+ <div style="color:#68d391;font-size:28px;font-weight:bold;">
88
+ <%= @estimated_saving_ms %>ms
89
+ </div>
90
+ </div>
91
+ <div style="
92
+ background:#1a202c;
93
+ border-radius:8px;
94
+ padding:16px;
95
+ text-align:center;
96
+ ">
97
+ <div style="color:#a0aec0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">
98
+ Requests Affected
99
+ </div>
100
+ <div style="color:#e2e8f0;font-size:28px;font-weight:bold;">
101
+ <%= @affected_requests.size %>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <%# Affected requests list %>
107
+ <div style="color:#a0aec0;font-size:11px;font-weight:bold;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em;">
108
+ Affected Requests
109
+ </div>
110
+ <table>
111
+ <thead>
112
+ <tr>
113
+ <th>Endpoint</th>
114
+ <th>Score</th>
115
+ <th>Queries</th>
116
+ <th>Duration</th>
117
+ <th></th>
118
+ </tr>
119
+ </thead>
120
+ <tbody>
121
+ <% @affected_requests.each do |r| %>
122
+ <tr>
123
+ <td><%= r.endpoint %></td>
124
+ <td>
125
+ <span class="badge badge-<%= score_label_to_color(r.score) %>">
126
+ <%= r.score %>
127
+ </span>
128
+ </td>
129
+ <td><%= r.total_query_count %></td>
130
+ <td style="color:#a0aec0;"><%= r.duration_ms.round(1) %>ms</td>
131
+ <td>
132
+ <%= link_to "→", request_path(r.id),
133
+ style: "color:#90cdf4;" %>
134
+ </td>
135
+ </tr>
136
+ <% end %>
137
+ </tbody>
138
+ </table>
139
+ </div>
@@ -0,0 +1,60 @@
1
+ <div class="page-title">Request History</div>
2
+
3
+ <div class="filter-bar">
4
+ <%= link_to "All", rails_vitals.requests_path, class: params[:score].blank? && params[:n_plus_one].blank? ? "active" : "" %>
5
+ <%= link_to "Critical", rails_vitals.requests_path(score: "critical"), class: params[:score] == "critical" ? "active" : "" %>
6
+ <%= link_to "Warning", rails_vitals.requests_path(score: "warning"), class: params[:score] == "warning" ? "active" : "" %>
7
+ <%= link_to "Acceptable", rails_vitals.requests_path(score: "acceptable"), class: params[:score] == "acceptable" ? "active" : "" %>
8
+ <%= link_to "Healthy", rails_vitals.requests_path(score: "healthy"), class: params[:score] == "healthy" ? "active" : "" %>
9
+ <%= link_to "N+1 Only", rails_vitals.requests_path(n_plus_one: "1"), class: params[:n_plus_one].present? ? "active" : "" %>
10
+ </div>
11
+
12
+ <% if params[:endpoint].present? %>
13
+ <div style="margin:20px 0;font-size:13px;color:#a0aec0;">
14
+ Filtering by endpoint:
15
+ <span style="color:#90cdf4;"><%= params[:endpoint] %></span>
16
+ &nbsp;
17
+ <%= link_to "✕ Clear", requests_path, style: "color:#fc8181;text-decoration:none;" %>
18
+ </div>
19
+ <% end %>
20
+
21
+ <div class="card">
22
+ <table>
23
+ <thead>
24
+ <tr>
25
+ <th>Time</th>
26
+ <th>Endpoint</th>
27
+ <th>Score</th>
28
+ <th>Queries</th>
29
+ <th>DB Time</th>
30
+ <th>N+1</th>
31
+ <th>Duration</th>
32
+ <th></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% @records.each do |r| %>
37
+ <tr>
38
+ <td style="color:#a0aec0;font-size:11px;"><%= r.recorded_at.strftime("%H:%M:%S") %></td>
39
+ <td><%= r.endpoint %></td>
40
+ <td>
41
+ <span class="badge badge-<%= r.color %>">
42
+ <%= r.score %>
43
+ </span>
44
+ </td>
45
+ <td><%= r.total_query_count %></td>
46
+ <td><%= r.total_db_time_ms.round(1) %>ms</td>
47
+ <td>
48
+ <% if r.n_plus_one_patterns.any? %>
49
+ <span class="n1-badge"><%= r.n_plus_one_patterns.size %></span>
50
+ <% else %>
51
+ <span style="color:#68d391;">—</span>
52
+ <% end %>
53
+ </td>
54
+ <td><%= r.duration_ms&.round(1) %>ms</td>
55
+ <td><%= link_to "→", rails_vitals.request_path(r.id) %></td>
56
+ </tr>
57
+ <% end %>
58
+ </tbody>
59
+ </table>
60
+ </div>