rails_error_dashboard 0.1.28 → 0.1.30

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -6
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
  5. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
  6. data/app/models/rails_error_dashboard/application.rb +1 -1
  7. data/app/models/rails_error_dashboard/error_log.rb +44 -16
  8. data/app/views/layouts/rails_error_dashboard.html.erb +71 -1237
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
  10. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
  11. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
  12. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +70 -0
  13. data/app/views/rails_error_dashboard/errors/analytics.html.erb +9 -37
  14. data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -37
  15. data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
  16. data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
  17. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
  18. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
  19. data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
  20. data/app/views/rails_error_dashboard/errors/show.html.erb +102 -76
  21. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
  22. data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
  23. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
  24. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
  25. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
  26. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
  27. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
  28. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
  29. data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
  30. data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
  31. data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
  32. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
  33. data/db/migrate/20251226020100_create_error_comments.rb +3 -0
  34. data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
  35. data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
  36. data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
  37. data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
  38. data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
  39. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
  40. data/lib/rails_error_dashboard/configuration.rb +160 -3
  41. data/lib/rails_error_dashboard/configuration_error.rb +24 -0
  42. data/lib/rails_error_dashboard/engine.rb +17 -0
  43. data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
  44. data/lib/rails_error_dashboard/queries/analytics_stats.rb +1 -2
  45. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
  46. data/lib/rails_error_dashboard/queries/errors_list.rb +27 -8
  47. data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
  48. data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
  49. data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
  50. data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
  51. data/lib/rails_error_dashboard/version.rb +1 -1
  52. data/lib/rails_error_dashboard.rb +6 -0
  53. metadata +14 -10
  54. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
  55. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
  56. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
  57. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
  58. data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
  59. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
  60. data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
  61. data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
@@ -75,9 +75,17 @@
75
75
  </td>
76
76
  <td onclick="window.location='<%= error_path(error) %>';">
77
77
  <% if error.resolved? %>
78
- <i class="bi bi-check-circle-fill text-success" title="Resolved"></i>
78
+ <i class="bi bi-check-circle-fill text-success"
79
+ data-bs-toggle="tooltip"
80
+ data-bs-placement="left"
81
+ data-bs-html="true"
82
+ title="<%= if error.resolution_comment.present?
83
+ "<strong>Resolved</strong><br><em>#{error.resolution_comment.truncate(100).gsub('"', '&quot;').gsub("'", '&#39;')}</em>"
84
+ else
85
+ 'Resolved'
86
+ end %>"></i>
79
87
  <% else %>
80
- <i class="bi bi-exclamation-circle-fill text-danger" title="Unresolved"></i>
88
+ <i class="bi bi-exclamation-circle-fill text-danger" data-bs-toggle="tooltip" title="Unresolved"></i>
81
89
  <% end %>
82
90
  </td>
83
91
  <td onclick="event.stopPropagation();">
@@ -0,0 +1,76 @@
1
+ <%# Displays source code for a backtrace frame with git blame and repository links %>
2
+ <%# Usage: render "source_code", frame: frame, error: @error, index: 0 %>
3
+
4
+ <% if RailsErrorDashboard.configuration.enable_source_code_integration %>
5
+ <% source_data = read_source_code(frame) %>
6
+ <% blame_data = read_git_blame(frame) if RailsErrorDashboard.configuration.enable_git_blame %>
7
+ <% repo_link = generate_repository_link(frame, error) %>
8
+
9
+ <% if source_data && source_data[:lines].present? %>
10
+ <div class="source-code-viewer mt-2 mb-3 border rounded shadow-sm">
11
+ <!-- Header with file info and actions -->
12
+ <div class="source-code-header bg-light border-bottom">
13
+ <!-- Top row: File path and View Source button -->
14
+ <div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
15
+ <div class="d-flex align-items-center gap-2">
16
+ <i class="bi bi-file-code text-primary"></i>
17
+ <span class="font-monospace source-file-path fw-medium" style="font-size: 0.9rem;">
18
+ <%= frame[:short_path] %>:<%= frame[:line_number] %>
19
+ </span>
20
+ </div>
21
+
22
+ <% if repo_link %>
23
+ <%= link_to repo_link, target: "_blank", rel: "noopener",
24
+ class: "btn btn-sm btn-primary",
25
+ title: "View on #{repo_link.include?('github') ? 'GitHub' : repo_link.include?('gitlab') ? 'GitLab' : 'Bitbucket'}" do %>
26
+ <i class="bi bi-box-arrow-up-right"></i> View Source
27
+ <% end %>
28
+ <% end %>
29
+ </div>
30
+
31
+ <!-- Bottom row: Git blame info (if available) -->
32
+ <% if blame_data %>
33
+ <div class="git-blame-info px-3 py-2 bg-white">
34
+ <div class="d-flex align-items-center gap-3 text-muted" style="font-size: 0.85rem;">
35
+ <div class="d-flex align-items-center gap-1">
36
+ <i class="bi bi-person-circle"></i>
37
+ <span><%= blame_data[:author] %></span>
38
+ </div>
39
+ <div class="d-flex align-items-center gap-1">
40
+ <i class="bi bi-clock"></i>
41
+ <span><%= time_ago_in_words(blame_data[:date]) %> ago</span>
42
+ </div>
43
+ <% if blame_data[:commit_message] %>
44
+ <div class="d-flex align-items-center gap-1 flex-grow-1">
45
+ <i class="bi bi-chat-left-text"></i>
46
+ <span class="text-truncate" style="max-width: 400px;" title="<%= blame_data[:commit_message] %>">
47
+ <%= blame_data[:commit_message] %>
48
+ </span>
49
+ </div>
50
+ <% end %>
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+ </div>
55
+
56
+ <!-- Source code lines with syntax highlighting -->
57
+ <div class="source-code-content bg-white">
58
+ <div class="code-block" style="max-height: 500px; overflow-y: auto; overflow-x: auto;">
59
+ <%
60
+ # Find the error line number for highlighting
61
+ error_line_number = source_data[:lines].find { |l| l[:highlight] }&.dig(:number)
62
+ # Get start line number (first line in context)
63
+ start_line = source_data[:lines].first[:number]
64
+ %>
65
+ <pre class="mb-0"><code class="language-<%= source_data[:language] || 'plaintext' %>" data-error-line="<%= error_line_number %>" data-start-line="<%= start_line %>"><%= source_data[:lines].map { |l| l[:content] }.join("\n") %></code></pre>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <% elsif source_data && source_data[:error] %>
70
+ <!-- Error message if source couldn't be read -->
71
+ <div class="alert alert-warning mt-2 mb-2 py-2">
72
+ <i class="bi bi-exclamation-triangle"></i>
73
+ <small>Could not read source: <%= source_data[:error] %></small>
74
+ </div>
75
+ <% end %>
76
+ <% end %>
@@ -74,96 +74,32 @@
74
74
  <% end %>
75
75
 
76
76
  <% if error.resolved? %>
77
- <span class="badge bg-success">
77
+ <span class="badge bg-success"
78
+ <% if error.resolution_comment.present? %>
79
+ data-bs-toggle="tooltip"
80
+ data-bs-placement="top"
81
+ data-bs-html="true"
82
+ title="<strong>Resolution Notes:</strong><br><%= error.resolution_comment.truncate(150).gsub('"', '&quot;') %>"
83
+ <% end %>>
78
84
  <i class="bi bi-check-circle"></i> Resolved
79
85
  </span>
80
86
  <% end %>
81
87
  </div>
88
+
89
+ <% if error.resolved? && error.resolution_comment.present? %>
90
+ <div class="mt-2 p-2 bg-success bg-opacity-10 border border-success rounded">
91
+ <small class="text-success fw-bold">
92
+ <i class="bi bi-journal-text"></i> Resolution Notes:
93
+ </small>
94
+ <div class="mb-0 small text-muted mt-1">
95
+ <%= auto_link_urls(truncate(error.resolution_comment, length: 200), error: error).html_safe %>
96
+ </div>
97
+ </div>
98
+ <% end %>
82
99
  </div>
83
100
  </div>
84
101
  <% end %>
85
102
  </div>
86
103
  </div>
87
104
  </div>
88
-
89
- <style>
90
- .timeline {
91
- position: relative;
92
- padding-left: 0;
93
- }
94
-
95
- .timeline-item {
96
- position: relative;
97
- padding-left: 40px;
98
- padding-bottom: 30px;
99
- }
100
-
101
- .timeline-item-last {
102
- padding-bottom: 0;
103
- }
104
-
105
- .timeline-item::before {
106
- content: '';
107
- position: absolute;
108
- left: 11px;
109
- top: 30px;
110
- bottom: 0;
111
- width: 2px;
112
- background: var(--bs-border-color);
113
- }
114
-
115
- .timeline-item-last::before {
116
- display: none;
117
- }
118
-
119
- .timeline-marker {
120
- position: absolute;
121
- left: 0;
122
- top: 0;
123
- width: 24px;
124
- height: 24px;
125
- display: flex;
126
- align-items: center;
127
- justify-content: center;
128
- background: white;
129
- border: 2px solid var(--bs-border-color);
130
- border-radius: 50%;
131
- z-index: 1;
132
- }
133
-
134
- .timeline-marker-current {
135
- border-color: var(--bs-danger);
136
- background: var(--bs-danger-bg-subtle);
137
- box-shadow: 0 0 0 4px var(--bs-danger-bg-subtle);
138
- }
139
-
140
- .timeline-marker i {
141
- font-size: 12px;
142
- }
143
-
144
- .timeline-content {
145
- background: var(--bs-body-bg);
146
- padding: 12px;
147
- border-radius: 8px;
148
- border: 1px solid var(--bs-border-color);
149
- }
150
-
151
- .timeline-item:hover .timeline-content {
152
- border-color: var(--bs-primary);
153
- background: var(--bs-primary-bg-subtle);
154
- }
155
-
156
- /* Dark mode support */
157
- body.dark-mode .timeline-marker,
158
- html[data-theme="dark"] body .timeline-marker {
159
- background: var(--card-bg);
160
- border-color: var(--border-color);
161
- }
162
-
163
- body.dark-mode .timeline-marker-current,
164
- html[data-theme="dark"] body .timeline-marker-current {
165
- border-color: var(--bs-danger);
166
- background: rgba(220, 53, 69, 0.2);
167
- }
168
- </style>
169
105
  <% end %>
@@ -0,0 +1,70 @@
1
+ <div class="table-responsive">
2
+ <table class="table table-hover mb-0">
3
+ <thead class="table-light">
4
+ <tr>
5
+ <% if show_rank %>
6
+ <th>Rank</th>
7
+ <% end %>
8
+ <th>User</th>
9
+ <% if show_error_type_count %>
10
+ <th>Different Error Types</th>
11
+ <% end %>
12
+ <th>Error Count</th>
13
+ <% if show_percentage %>
14
+ <th>Percentage</th>
15
+ <% end %>
16
+ <% if show_error_types %>
17
+ <th>Error Types</th>
18
+ <% end %>
19
+ <th>Actions</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% users.each_with_index do |user_data, index| %>
24
+ <tr>
25
+ <% if show_rank %>
26
+ <td><strong>#<%= index + 1 %></strong></td>
27
+ <% end %>
28
+ <td><%= user_data[:email] || user_data[:user_email] %></td>
29
+ <% if show_error_type_count %>
30
+ <td>
31
+ <span class="badge bg-warning text-dark">
32
+ <%= user_data[:error_type_count] %> types
33
+ </span>
34
+ </td>
35
+ <% end %>
36
+ <td>
37
+ <span class="badge bg-danger"><%= user_data[:count] || user_data[:total_errors] %></span>
38
+ </td>
39
+ <% if show_percentage && total_errors.present? %>
40
+ <td>
41
+ <div class="progress" style="height: 20px;">
42
+ <div class="progress-bar bg-danger"
43
+ role="progressbar"
44
+ style="width: <%= ((user_data[:count] || user_data[:total_errors]).to_f / total_errors * 100).round(1) %>%"
45
+ aria-valuenow="<%= user_data[:count] || user_data[:total_errors] %>"
46
+ aria-valuemin="0"
47
+ aria-valuemax="100">
48
+ <%= ((user_data[:count] || user_data[:total_errors]).to_f / total_errors * 100).round(1) %>%
49
+ </div>
50
+ </div>
51
+ </td>
52
+ <% end %>
53
+ <% if show_error_types && user_data[:error_types].present? %>
54
+ <td>
55
+ <% user_data[:error_types].first(3).each do |error_type| %>
56
+ <code class="small me-1"><%= error_type %></code>
57
+ <% end %>
58
+ <% if user_data[:error_types].count > 3 %>
59
+ <span class="text-muted small">+<%= user_data[:error_types].count - 3 %> more</span>
60
+ <% end %>
61
+ </td>
62
+ <% end %>
63
+ <td>
64
+ <%= link_to "View Errors", errors_path(user_id: user_data[:user_id]), class: "btn btn-sm btn-outline-primary" %>
65
+ </td>
66
+ </tr>
67
+ <% end %>
68
+ </tbody>
69
+ </table>
70
+ </div>
@@ -266,43 +266,15 @@
266
266
  <h5 class="mb-0"><i class="bi bi-people"></i> Top 10 Affected Users</h5>
267
267
  </div>
268
268
  <div class="card-body p-0">
269
- <div class="table-responsive">
270
- <table class="table table-hover mb-0">
271
- <thead class="table-light">
272
- <tr>
273
- <th>Rank</th>
274
- <th>User</th>
275
- <th>Error Count</th>
276
- <th>Percentage</th>
277
- <th>Actions</th>
278
- </tr>
279
- </thead>
280
- <tbody>
281
- <% @top_users.each_with_index do |(email, count), index| %>
282
- <tr>
283
- <td><strong>#<%= index + 1 %></strong></td>
284
- <td><%= email %></td>
285
- <td><span class="badge bg-danger"><%= count %></span></td>
286
- <td>
287
- <div class="progress" style="height: 20px;">
288
- <div class="progress-bar bg-danger"
289
- role="progressbar"
290
- style="width: <%= (count.to_f / @error_stats[:total] * 100).round(1) %>%"
291
- aria-valuenow="<%= count %>"
292
- aria-valuemin="0"
293
- aria-valuemax="<%= @error_stats[:total] %>">
294
- <%= (count.to_f / @error_stats[:total] * 100).round(1) %>%
295
- </div>
296
- </div>
297
- </td>
298
- <td>
299
- <%= link_to "View Errors", errors_path(search: email), class: "btn btn-sm btn-outline-primary" %>
300
- </td>
301
- </tr>
302
- <% end %>
303
- </tbody>
304
- </table>
305
- </div>
269
+ <%= render partial: 'user_errors_table',
270
+ locals: {
271
+ users: @top_users,
272
+ show_rank: true,
273
+ show_error_type_count: false,
274
+ show_percentage: true,
275
+ show_error_types: false,
276
+ total_errors: @error_stats[:total]
277
+ } %>
306
278
  </div>
307
279
  </div>
308
280
  </div>
@@ -216,43 +216,17 @@
216
216
  <small class="text-muted">Users experiencing 2+ different error types</small>
217
217
  </div>
218
218
  <div class="card-body">
219
- <div class="table-responsive">
220
- <table class="table table-hover">
221
- <thead>
222
- <tr>
223
- <th>User</th>
224
- <th>Different Error Types</th>
225
- <th>Total Errors</th>
226
- <th>Error Types</th>
227
- <th>Actions</th>
228
- </tr>
229
- </thead>
230
- <tbody>
231
- <% @multi_error_users.first(20).each do |user_data| %>
232
- <tr>
233
- <td><%= user_data[:user_email] %></td>
234
- <td>
235
- <span class="badge bg-warning text-dark">
236
- <%= user_data[:error_type_count] %> types
237
- </span>
238
- </td>
239
- <td><%= user_data[:total_errors] %></td>
240
- <td>
241
- <% user_data[:error_types].first(3).each do |error_type| %>
242
- <code class="small me-1"><%= error_type %></code>
243
- <% end %>
244
- <% if user_data[:error_types].count > 3 %>
245
- <span class="text-muted small">+<%= user_data[:error_types].count - 3 %> more</span>
246
- <% end %>
247
- </td>
248
- <td>
249
- <%= link_to "View", errors_path(search: user_data[:user_email]), class: "btn btn-sm btn-outline-primary" %>
250
- </td>
251
- </tr>
252
- <% end %>
253
- </tbody>
254
- </table>
255
- </div>
219
+ <%= render partial: 'user_errors_table',
220
+ locals: {
221
+ users: @multi_error_users.first(20),
222
+ show_rank: false,
223
+ show_error_type_count: true,
224
+ show_percentage: false,
225
+ show_error_types: true,
226
+ total_errors: nil
227
+ } %>
228
+ </div>
229
+ <div class="card-body pt-0 border-top">
256
230
  <% if @multi_error_users.count > 20 %>
257
231
  <p class="text-muted small mb-0">
258
232
  Showing 20 of <%= @multi_error_users.count %> users
@@ -148,6 +148,10 @@
148
148
  active_filters << { label: "Unassigned", param: :assigned_to }
149
149
  elsif params[:assigned_to] == '__assigned__'
150
150
  active_filters << { label: "Assigned", param: :assigned_to }
151
+ # Show assignee name filter if present
152
+ if params[:assignee_name].present?
153
+ active_filters << { label: "Assignee: #{params[:assignee_name]}", param: :assignee_name }
154
+ end
151
155
  end
152
156
  %>
153
157
 
@@ -185,8 +189,6 @@
185
189
  class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
186
190
  <%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
187
191
  class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
188
- <%= link_to "My Errors", errors_path(assigned_to: current_user_name),
189
- class: "btn btn-sm #{params[:assigned_to] == current_user_name ? 'btn-primary' : 'btn-outline-primary'}" %>
190
192
  </div>
191
193
 
192
194
  <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
@@ -268,44 +270,54 @@
268
270
  ['All Assignments', ''],
269
271
  ['Unassigned', '__unassigned__'],
270
272
  ['Assigned', '__assigned__']
271
- ], params[:assigned_to]), class: "form-select" %>
273
+ ], params[:assigned_to]), class: "form-select", id: "assigned_to_filter" %>
272
274
  </div>
273
275
 
274
- <div class="col-md-2">
275
- <%= select_tag :priority_level, options_for_select([
276
- ['All Priorities', ''],
277
- ['🔴 Critical (P3)', 3],
278
- ['🟠 High (P2)', 2],
279
- ['🟡 Medium (P1)', 1],
280
- ['⚪ Low (P0)', 0]
281
- ], params[:priority_level]), class: "form-select" %>
276
+ <!-- Assignee Name Filter - Only shown when "Assigned" is selected -->
277
+ <div class="col-md-2" id="assignee_name_filter" style="<%= params[:assigned_to] == '__assigned__' ? '' : 'display: none;' %>">
278
+ <%= select_tag :assignee_name, options_for_select(
279
+ [['All Assignees', '']] + @assignees.map { |name| [name, name] },
280
+ params[:assignee_name]
281
+ ), class: "form-select", placeholder: "Filter by assignee..." %>
282
282
  </div>
283
283
 
284
- <div class="col-auto">
285
- <div class="form-check mt-2">
286
- <%
287
- # Determine checkbox state based on params
288
- # If unresolved param is missing or nil, default to checked (true)
289
- # If unresolved param is explicitly "false" or "0", uncheck it
290
- is_checked = if params[:unresolved].nil?
291
- true # Default to checked when first loading
292
- else
293
- params[:unresolved] != "false" && params[:unresolved] != "0"
294
- end
295
- %>
296
- <%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
297
- <%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
298
- </div>
284
+ <div class="col-md-2">
285
+ <%= select_tag :priority_level, options_for_select(
286
+ [['All Priorities', '']] + RailsErrorDashboard::ErrorLog.priority_options(include_emoji: true),
287
+ params[:priority_level]
288
+ ), class: "form-select" %>
299
289
  </div>
300
290
 
301
- <div class="col-auto">
302
- <div class="form-check mt-2">
303
- <%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
304
- <%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
291
+ <!-- Checkboxes on their own row -->
292
+ <div class="col-12 mt-2">
293
+ <div class="row g-3">
294
+ <div class="col-auto">
295
+ <div class="form-check">
296
+ <%
297
+ # Determine checkbox state based on params
298
+ # If unresolved param is missing or nil, default to checked (true)
299
+ # If unresolved param is explicitly "false" or "0", uncheck it
300
+ is_checked = if params[:unresolved].nil?
301
+ true # Default to checked when first loading
302
+ else
303
+ params[:unresolved] != "false" && params[:unresolved] != "0"
304
+ end
305
+ %>
306
+ <%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
307
+ <%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
308
+ </div>
309
+ </div>
310
+
311
+ <div class="col-auto">
312
+ <div class="form-check">
313
+ <%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
314
+ <%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
315
+ </div>
316
+ </div>
305
317
  </div>
306
318
  </div>
307
319
 
308
- <div class="col-12">
320
+ <div class="col-12 mt-3">
309
321
  <%= submit_tag "Apply Filters", class: "btn btn-primary" %>
310
322
  <%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
311
323
  </div>
@@ -600,4 +612,25 @@
600
612
  document.title = `(${unresolvedCount}) ${document.title}`;
601
613
  }
602
614
  });
615
+
616
+ // Toggle assignee name filter visibility based on assigned_to selection
617
+ document.addEventListener('DOMContentLoaded', function() {
618
+ const assignedToFilter = document.getElementById('assigned_to_filter');
619
+ const assigneeNameFilter = document.getElementById('assignee_name_filter');
620
+
621
+ if (assignedToFilter && assigneeNameFilter) {
622
+ assignedToFilter.addEventListener('change', function() {
623
+ if (this.value === '__assigned__') {
624
+ assigneeNameFilter.style.display = 'block';
625
+ } else {
626
+ assigneeNameFilter.style.display = 'none';
627
+ // Reset assignee name filter when hiding
628
+ const assigneeNameSelect = assigneeNameFilter.querySelector('select');
629
+ if (assigneeNameSelect) {
630
+ assigneeNameSelect.value = '';
631
+ }
632
+ }
633
+ });
634
+ }
635
+ });
603
636
  </script>