rails_error_dashboard 0.2.4 → 0.3.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -14
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +139 -1
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
  5. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
  6. data/app/views/layouts/rails_error_dashboard.html.erb +157 -1
  7. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
  8. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
  9. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
  10. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
  11. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
  12. data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
  13. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
  14. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +108 -0
  15. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
  16. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
  17. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
  18. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
  19. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
  20. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +450 -0
  21. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
  22. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +152 -0
  23. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
  24. data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
  25. data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
  26. data/config/routes.rb +5 -0
  27. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +6 -0
  28. data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
  29. data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
  30. data/lib/generators/rails_error_dashboard/install/install_generator.rb +31 -3
  31. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
  32. data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
  33. data/lib/rails_error_dashboard/configuration.rb +45 -3
  34. data/lib/rails_error_dashboard/engine.rb +6 -1
  35. data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
  36. data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
  37. data/lib/rails_error_dashboard/queries/database_health_summary.rb +82 -0
  38. data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
  39. data/lib/rails_error_dashboard/queries/job_health_summary.rb +101 -0
  40. data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
  41. data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
  42. data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
  43. data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
  44. data/lib/rails_error_dashboard/services/database_health_inspector.rb +168 -0
  45. data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
  46. data/lib/rails_error_dashboard/services/rspec_generator.rb +145 -0
  47. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
  48. data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
  49. data/lib/rails_error_dashboard/version.rb +1 -1
  50. data/lib/rails_error_dashboard.rb +24 -0
  51. data/lib/tasks/error_dashboard.rake +68 -2
  52. metadata +33 -2
@@ -0,0 +1,450 @@
1
+ <% content_for :page_title, "DB Health" %>
2
+
3
+ <div class="container-fluid py-4">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 class="h3 mb-0">
6
+ <i class="bi bi-database me-2"></i>
7
+ Database Health
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to database_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ 7 Days
13
+ <% end %>
14
+ <%= link_to database_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ 30 Days
16
+ <% end %>
17
+ <%= link_to database_health_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ 90 Days
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <%# ===== SECTION A: Live Database Health ===== %>
24
+
25
+ <h4 class="mb-3"><i class="bi bi-activity text-primary me-2"></i>Live Database Health</h4>
26
+
27
+ <% unless @live_health[:postgresql] %>
28
+ <div class="alert alert-info mb-3">
29
+ <i class="bi bi-info-circle me-2"></i>
30
+ <strong>Non-PostgreSQL adapter detected (<%= @live_health[:adapter] || "unknown" %>).</strong>
31
+ Live table, index, and activity stats require PostgreSQL. Connection pool and historical data are still available below.
32
+ </div>
33
+ <% end %>
34
+
35
+ <%# Connection Pool (always works — all adapters) %>
36
+ <% if @live_health[:connection_pool] %>
37
+ <% pool = @live_health[:connection_pool] %>
38
+ <div class="row mb-4">
39
+ <div class="col-md-2">
40
+ <div class="card text-center">
41
+ <div class="card-body py-2">
42
+ <div class="h4 mb-0 text-primary"><%= pool[:size] %></div>
43
+ <small class="text-muted">Pool Size</small>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <div class="col-md-2">
48
+ <div class="card text-center">
49
+ <div class="card-body py-2">
50
+ <% busy_color = pool[:size] > 0 && (pool[:busy].to_f / pool[:size]) >= 0.8 ? "danger" : "info" %>
51
+ <div class="h4 mb-0 text-<%= busy_color %>"><%= pool[:busy] %></div>
52
+ <small class="text-muted">Busy</small>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="col-md-2">
57
+ <div class="card text-center">
58
+ <div class="card-body py-2">
59
+ <div class="h4 mb-0 text-success"><%= pool[:idle] %></div>
60
+ <small class="text-muted">Idle</small>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <div class="col-md-2">
65
+ <div class="card text-center">
66
+ <div class="card-body py-2">
67
+ <% dead_color = pool[:dead] > 0 ? "danger" : "success" %>
68
+ <div class="h4 mb-0 text-<%= dead_color %>"><%= pool[:dead] %></div>
69
+ <small class="text-muted">Dead</small>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <div class="col-md-2">
74
+ <div class="card text-center">
75
+ <div class="card-body py-2">
76
+ <% waiting_color = pool[:waiting] > 0 ? "warning" : "success" %>
77
+ <div class="h4 mb-0 text-<%= waiting_color %>"><%= pool[:waiting] %></div>
78
+ <small class="text-muted">Waiting</small>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ <% end %>
84
+
85
+ <% if @live_health[:postgresql] %>
86
+ <%# Summary cards for PostgreSQL %>
87
+ <% total_db_size = (@host_tables + @gem_tables).sum { |t| t[:total_bytes] } %>
88
+ <div class="row mb-4">
89
+ <div class="col-md-3">
90
+ <div class="card text-center">
91
+ <div class="card-body py-2">
92
+ <div class="h4 mb-0 text-primary"><%= number_to_human_size(total_db_size) %></div>
93
+ <small class="text-muted">Total DB Size</small>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ <div class="col-md-3">
98
+ <div class="card text-center">
99
+ <div class="card-body py-2">
100
+ <div class="h4 mb-0 text-info"><%= @host_tables.size + @gem_tables.size %></div>
101
+ <small class="text-muted">Tables</small>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ <div class="col-md-3">
106
+ <div class="card text-center">
107
+ <div class="card-body py-2">
108
+ <% unused_color = @live_health[:unused_indexes]&.any? ? "warning" : "success" %>
109
+ <div class="h4 mb-0 text-<%= unused_color %>"><%= @live_health[:unused_indexes]&.size || 0 %></div>
110
+ <small class="text-muted">Unused Indexes</small>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ <div class="col-md-3">
115
+ <div class="card text-center">
116
+ <div class="card-body py-2">
117
+ <div class="h4 mb-0 text-secondary"><%= @live_health[:activity] ? @live_health[:activity][:total] : "N/A" %></div>
118
+ <small class="text-muted">Active Connections</small>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <%# Host App Tables %>
125
+ <% if @host_tables.any? %>
126
+ <div class="card mb-4">
127
+ <div class="card-header bg-white">
128
+ <h5 class="mb-0">
129
+ <i class="bi bi-table text-primary me-2"></i>
130
+ Host App Tables
131
+ <span class="badge bg-primary"><%= @host_tables.size %></span>
132
+ </h5>
133
+ </div>
134
+ <div class="card-body p-0">
135
+ <div class="table-responsive">
136
+ <table class="table table-hover mb-0">
137
+ <thead class="table-light">
138
+ <tr>
139
+ <th>Table</th>
140
+ <th width="100">Est. Rows</th>
141
+ <th width="100">Size</th>
142
+ <th width="80">Seq Scans</th>
143
+ <th width="80">Idx Scans</th>
144
+ <th width="100">Dead Tuples</th>
145
+ <th width="140">Last Vacuum</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ <% @host_tables.each do |table| %>
150
+ <tr>
151
+ <td><code><%= table[:name] %></code></td>
152
+ <td><%= number_with_delimiter(table[:estimated_rows]) %></td>
153
+ <td><%= number_to_human_size(table[:total_bytes]) %></td>
154
+ <td><%= number_with_delimiter(table[:seq_scan]) %></td>
155
+ <td><%= number_with_delimiter(table[:idx_scan]) %></td>
156
+ <td>
157
+ <% if table[:dead_tuples] > 1000 %>
158
+ <span class="badge bg-warning text-dark"><%= number_with_delimiter(table[:dead_tuples]) %></span>
159
+ <% else %>
160
+ <%= number_with_delimiter(table[:dead_tuples]) %>
161
+ <% end %>
162
+ </td>
163
+ <td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
164
+ </tr>
165
+ <% end %>
166
+ </tbody>
167
+ </table>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ <% end %>
172
+
173
+ <%# Gem Tables (collapsible) %>
174
+ <% if @gem_tables.any? %>
175
+ <div class="card mb-4">
176
+ <div class="card-header bg-white">
177
+ <a class="text-decoration-none" data-bs-toggle="collapse" href="#gemTablesCollapse" role="button" aria-expanded="false" aria-controls="gemTablesCollapse">
178
+ <h5 class="mb-0">
179
+ <i class="bi bi-table text-secondary me-2"></i>
180
+ Error Dashboard Tables
181
+ <span class="badge bg-secondary"><%= @gem_tables.size %></span>
182
+ <i class="bi bi-chevron-down ms-2" style="font-size: 0.8em;"></i>
183
+ </h5>
184
+ </a>
185
+ </div>
186
+ <div class="collapse" id="gemTablesCollapse">
187
+ <div class="card-body p-0">
188
+ <div class="table-responsive">
189
+ <table class="table table-hover mb-0">
190
+ <thead class="table-light">
191
+ <tr>
192
+ <th>Table</th>
193
+ <th width="100">Est. Rows</th>
194
+ <th width="100">Size</th>
195
+ <th width="80">Seq Scans</th>
196
+ <th width="80">Idx Scans</th>
197
+ <th width="100">Dead Tuples</th>
198
+ <th width="140">Last Vacuum</th>
199
+ </tr>
200
+ </thead>
201
+ <tbody>
202
+ <% @gem_tables.each do |table| %>
203
+ <tr>
204
+ <td><code><%= table[:name] %></code></td>
205
+ <td><%= number_with_delimiter(table[:estimated_rows]) %></td>
206
+ <td><%= number_to_human_size(table[:total_bytes]) %></td>
207
+ <td><%= number_with_delimiter(table[:seq_scan]) %></td>
208
+ <td><%= number_with_delimiter(table[:idx_scan]) %></td>
209
+ <td>
210
+ <% if table[:dead_tuples] > 1000 %>
211
+ <span class="badge bg-warning text-dark"><%= number_with_delimiter(table[:dead_tuples]) %></span>
212
+ <% else %>
213
+ <%= number_with_delimiter(table[:dead_tuples]) %>
214
+ <% end %>
215
+ </td>
216
+ <td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
217
+ </tr>
218
+ <% end %>
219
+ </tbody>
220
+ </table>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ <% end %>
226
+
227
+ <%# Unused Indexes %>
228
+ <% if @live_health[:unused_indexes]&.any? %>
229
+ <div class="card mb-4">
230
+ <div class="card-header bg-white">
231
+ <h5 class="mb-0">
232
+ <i class="bi bi-exclamation-triangle text-warning me-2"></i>
233
+ Unused Indexes
234
+ <span class="badge bg-warning text-dark"><%= @live_health[:unused_indexes].size %></span>
235
+ </h5>
236
+ </div>
237
+ <div class="card-body p-0">
238
+ <div class="table-responsive">
239
+ <table class="table table-hover mb-0">
240
+ <thead class="table-light">
241
+ <tr>
242
+ <th>Index</th>
243
+ <th>Table</th>
244
+ <th width="100">Size</th>
245
+ </tr>
246
+ </thead>
247
+ <tbody>
248
+ <% @live_health[:unused_indexes].each do |idx| %>
249
+ <tr>
250
+ <td><code><%= idx[:name] %></code></td>
251
+ <td><code><%= idx[:table_name] %></code></td>
252
+ <td><span class="badge bg-warning text-dark"><%= number_to_human_size(idx[:size_bytes]) %></span></td>
253
+ </tr>
254
+ <% end %>
255
+ </tbody>
256
+ </table>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ <% end %>
261
+
262
+ <%# Activity by State %>
263
+ <% if @live_health[:activity] %>
264
+ <div class="card mb-4">
265
+ <div class="card-header bg-white">
266
+ <h5 class="mb-0">
267
+ <i class="bi bi-people text-info me-2"></i>
268
+ Connection Activity
269
+ <span class="badge bg-info"><%= @live_health[:activity][:total] %> total</span>
270
+ <% if @live_health[:activity][:total_waiting] > 0 %>
271
+ <span class="badge bg-warning text-dark"><%= @live_health[:activity][:total_waiting] %> waiting</span>
272
+ <% end %>
273
+ </h5>
274
+ </div>
275
+ <div class="card-body p-0">
276
+ <div class="table-responsive">
277
+ <table class="table table-hover mb-0">
278
+ <thead class="table-light">
279
+ <tr>
280
+ <th>State</th>
281
+ <th width="100">Count</th>
282
+ <th width="100">Waiting</th>
283
+ </tr>
284
+ </thead>
285
+ <tbody>
286
+ <% @live_health[:activity][:by_state].each do |row| %>
287
+ <tr>
288
+ <td><span class="badge bg-secondary"><%= row[:state] %></span></td>
289
+ <td><%= row[:count] %></td>
290
+ <td>
291
+ <% if row[:waiting] > 0 %>
292
+ <span class="badge bg-warning text-dark"><%= row[:waiting] %></span>
293
+ <% else %>
294
+ 0
295
+ <% end %>
296
+ </td>
297
+ </tr>
298
+ <% end %>
299
+ </tbody>
300
+ </table>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ <% end %>
305
+ <% end %>
306
+
307
+ <%# ===== SECTION B: Historical Connection Pool at Error Time ===== %>
308
+
309
+ <hr class="my-4">
310
+ <h4 class="mb-3"><i class="bi bi-clock-history text-secondary me-2"></i>Historical Connection Pool at Error Time</h4>
311
+
312
+ <% if @errors_with_pool == 0 %>
313
+ <div class="text-center py-5">
314
+ <i class="bi bi-check-circle display-1 text-success mb-3"></i>
315
+ <h4 class="text-muted">No Connection Pool Data Found</h4>
316
+ <p class="text-muted">
317
+ No connection pool stats were detected in system health snapshots over the last <%= @days %> days.
318
+ </p>
319
+ <div class="card mx-auto" style="max-width: 500px;">
320
+ <div class="card-body text-start">
321
+ <h6>How connection pool tracking works:</h6>
322
+ <ul class="mb-0">
323
+ <li>System health must be enabled (<code>enable_system_health = true</code>)</li>
324
+ <li>Connection pool stats are captured automatically at error time</li>
325
+ <li>Stats include pool size, busy, idle, dead, and waiting connections</li>
326
+ <li>This section shows pool health per-error, sorted by stress score</li>
327
+ </ul>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ <% else %>
332
+ <div class="row mb-4">
333
+ <div class="col-md-3">
334
+ <div class="card text-center">
335
+ <div class="card-body">
336
+ <div class="display-6 text-info"><%= @errors_with_pool %></div>
337
+ <small class="text-muted">Errors with Pool Data</small>
338
+ </div>
339
+ </div>
340
+ </div>
341
+ <div class="col-md-3">
342
+ <div class="card text-center">
343
+ <div class="card-body">
344
+ <% util_color = @max_utilization >= 80 ? "danger" : (@max_utilization >= 60 ? "warning" : "success") %>
345
+ <div class="display-6 text-<%= util_color %>"><%= @max_utilization %>%</div>
346
+ <small class="text-muted">Peak Utilization</small>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ <div class="col-md-3">
351
+ <div class="card text-center">
352
+ <div class="card-body">
353
+ <% dead_color = @total_dead > 0 ? "danger" : "success" %>
354
+ <div class="display-6 text-<%= dead_color %>"><%= @total_dead %></div>
355
+ <small class="text-muted">Total Dead</small>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ <div class="col-md-3">
360
+ <div class="card text-center">
361
+ <div class="card-body">
362
+ <% waiting_color = @total_waiting > 0 ? "warning" : "success" %>
363
+ <div class="display-6 text-<%= waiting_color %>"><%= @total_waiting %></div>
364
+ <small class="text-muted">Total Waiting</small>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <div class="card mb-4">
371
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
372
+ <h5 class="mb-0">
373
+ <i class="bi bi-database text-info me-2"></i>
374
+ Connection Pool by Error
375
+ <span class="badge bg-info"><%= @errors_with_pool %></span>
376
+ </h5>
377
+ <small class="text-muted"><%== @pagy.info_tag %></small>
378
+ </div>
379
+ <div class="card-body p-0">
380
+ <div class="table-responsive">
381
+ <table class="table table-hover mb-0">
382
+ <thead class="table-light">
383
+ <tr>
384
+ <th width="80">Error</th>
385
+ <th>Error Type</th>
386
+ <th width="110">Utilization</th>
387
+ <th width="60">Busy</th>
388
+ <th width="60">Idle</th>
389
+ <th width="60">Dead</th>
390
+ <th width="70">Waiting</th>
391
+ <th width="70">Pool Size</th>
392
+ <th width="140">Last Seen</th>
393
+ </tr>
394
+ </thead>
395
+ <tbody>
396
+ <% @entries.each do |entry| %>
397
+ <tr>
398
+ <td><%= link_to "##{entry[:error_id]}", error_path(entry[:error_id]), class: "text-decoration-none" %></td>
399
+ <td><code><%= truncate(entry[:error_type].to_s, length: 40) %></code></td>
400
+ <td>
401
+ <% util = entry[:utilization] %>
402
+ <% util_badge = util >= 80 ? "danger" : (util >= 60 ? "warning" : "success") %>
403
+ <div class="progress" style="height: 20px;">
404
+ <div class="progress-bar bg-<%= util_badge %>" role="progressbar" style="width: <%= [ util, 100 ].min %>%;" aria-valuenow="<%= util %>" aria-valuemin="0" aria-valuemax="100">
405
+ <%= util %>%
406
+ </div>
407
+ </div>
408
+ </td>
409
+ <td><%= entry[:busy] %></td>
410
+ <td><%= entry[:idle] %></td>
411
+ <td>
412
+ <% if entry[:dead] > 0 %>
413
+ <span class="badge bg-danger"><%= entry[:dead] %></span>
414
+ <% else %>
415
+ 0
416
+ <% end %>
417
+ </td>
418
+ <td>
419
+ <% if entry[:waiting] > 0 %>
420
+ <span class="badge bg-warning text-dark"><%= entry[:waiting] %></span>
421
+ <% else %>
422
+ 0
423
+ <% end %>
424
+ </td>
425
+ <td><%= entry[:size] %></td>
426
+ <td><%= local_time_ago(entry[:occurred_at]) %></td>
427
+ </tr>
428
+ <% end %>
429
+ </tbody>
430
+ </table>
431
+ </div>
432
+ </div>
433
+ <div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
434
+ <div>
435
+ <small class="text-muted">
436
+ <i class="bi bi-lightbulb text-warning"></i> High utilization during errors may indicate connection pool exhaustion or long-running queries.
437
+ </small>
438
+ <small class="ms-3">
439
+ <a href="https://guides.rubyonrails.org/configuring.html#configuring-a-database" target="_blank" rel="noopener" class="text-decoration-none">
440
+ <i class="bi bi-book"></i> Database Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
441
+ </a>
442
+ </small>
443
+ </div>
444
+ <div>
445
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ <% end %>
450
+ </div>
@@ -0,0 +1,129 @@
1
+ <% content_for :page_title, "Deprecation Warnings" %>
2
+
3
+ <div class="container-fluid py-4">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 class="h3 mb-0">
6
+ <i class="bi bi-exclamation-triangle me-2"></i>
7
+ Deprecation Warnings
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to deprecations_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ 7 Days
13
+ <% end %>
14
+ <%= link_to deprecations_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ 30 Days
16
+ <% end %>
17
+ <%= link_to deprecations_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ 90 Days
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @unique_count == 0 %>
24
+ <div class="text-center py-5">
25
+ <i class="bi bi-check-circle display-1 text-success mb-3"></i>
26
+ <h4 class="text-muted">No Deprecation Warnings Found</h4>
27
+ <p class="text-muted">
28
+ No deprecation warnings were detected in error breadcrumbs over the last <%= @days %> days.
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 500px;">
31
+ <div class="card-body text-start">
32
+ <h6>How deprecations are captured:</h6>
33
+ <ul class="mb-0">
34
+ <li>Breadcrumbs must be enabled (<code>enable_breadcrumbs = true</code>)</li>
35
+ <li>Rails deprecation warnings are automatically captured as breadcrumbs</li>
36
+ <li>Deprecations appear here when an error occurs during a request with deprecation warnings</li>
37
+ </ul>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <% else %>
42
+ <div class="row mb-4">
43
+ <div class="col-md-4">
44
+ <div class="card text-center">
45
+ <div class="card-body">
46
+ <div class="display-6 text-danger"><%= @unique_count %></div>
47
+ <small class="text-muted">Unique Warnings</small>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <div class="col-md-4">
52
+ <div class="card text-center">
53
+ <div class="card-body">
54
+ <div class="display-6 text-warning"><%= @total_count %></div>
55
+ <small class="text-muted">Total Occurrences</small>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <div class="col-md-4">
60
+ <div class="card text-center">
61
+ <div class="card-body">
62
+ <div class="display-6 text-info"><%= @affected_count %></div>
63
+ <small class="text-muted">Affected Errors</small>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="card mb-4">
70
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
71
+ <h5 class="mb-0">
72
+ <i class="bi bi-exclamation-triangle text-danger me-2"></i>
73
+ All Deprecation Warnings
74
+ <span class="badge bg-danger"><%= @unique_count %></span>
75
+ </h5>
76
+ <small class="text-muted"><%== @pagy.info_tag %></small>
77
+ </div>
78
+ <div class="card-body p-0">
79
+ <div class="table-responsive">
80
+ <table class="table table-hover mb-0">
81
+ <thead class="table-light">
82
+ <tr>
83
+ <th>Warning</th>
84
+ <th width="250">Source</th>
85
+ <th width="120">Occurrences</th>
86
+ <th width="250">Errors</th>
87
+ <th width="140">Last Seen</th>
88
+ </tr>
89
+ </thead>
90
+ <tbody>
91
+ <% @deprecations.each do |dep| %>
92
+ <tr>
93
+ <td><code style="font-size: 0.85em;"><%= dep[:message] %></code></td>
94
+ <td><small class="text-muted"><%= dep[:source] %></small></td>
95
+ <td><span class="badge bg-warning text-dark"><%= dep[:count] %></span></td>
96
+ <td>
97
+ <% shown_ids = dep[:error_ids].first(5) %>
98
+ <% shown_ids.each_with_index do |eid, i| %>
99
+ <%= link_to "##{eid}", error_path(eid), class: "text-decoration-none" %><%= "," unless i == shown_ids.size - 1 %>
100
+ <% end %>
101
+ <% if dep[:error_ids].size > 5 %>
102
+ <small class="text-muted">+<%= dep[:error_ids].size - 5 %> more</small>
103
+ <% end %>
104
+ </td>
105
+ <td><%= local_time_ago(dep[:last_seen]) %></td>
106
+ </tr>
107
+ <% end %>
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ </div>
112
+ <div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
113
+ <div>
114
+ <small class="text-muted">
115
+ <i class="bi bi-lightbulb text-warning"></i> Fix deprecations before upgrading Rails to avoid breaking changes.
116
+ </small>
117
+ <small class="ms-3">
118
+ <a href="https://guides.rubyonrails.org/upgrading_ruby_on_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
119
+ <i class="bi bi-book"></i> Rails Upgrade Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
120
+ </a>
121
+ </small>
122
+ </div>
123
+ <div>
124
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ <% end %>
129
+ </div>