rails_error_dashboard 0.5.15 → 0.6.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.
- checksums.yaml +4 -4
- data/app/controllers/rails_error_dashboard/application_controller.rb +37 -12
- data/app/controllers/rails_error_dashboard/errors_controller.rb +48 -26
- data/app/helpers/rails_error_dashboard/application_helper.rb +12 -5
- data/app/views/layouts/rails_error_dashboard.html.erb +1219 -1927
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +4 -4
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +69 -79
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +4 -4
- data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +34 -50
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -11
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +114 -172
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/index.html.erb +311 -613
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/overview.html.erb +192 -363
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +11 -11
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/releases.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/settings.html.erb +53 -51
- data/app/views/rails_error_dashboard/errors/show.html.erb +200 -203
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +19 -19
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +6 -6
- data/config/routes.rb +1 -0
- data/lib/rails_error_dashboard/configuration.rb +6 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +1 -1
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +1 -1
- data/lib/rails_error_dashboard/test_error.rb +7 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +1 -0
- metadata +3 -2
|
@@ -5,175 +5,167 @@
|
|
|
5
5
|
<%= turbo_stream_from "error_list" %>
|
|
6
6
|
<% end %>
|
|
7
7
|
|
|
8
|
-
<div
|
|
9
|
-
<div
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<span
|
|
15
|
-
<i class="bi bi-broadcast"></i> Live
|
|
8
|
+
<div data-controller="loading">
|
|
9
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4);">
|
|
10
|
+
<h1 style="font-size: 20px; font-weight: 700; margin: 0;">Errors</h1>
|
|
11
|
+
<span style="font-size: 12px; color: var(--text-tertiary);">
|
|
12
|
+
Last updated: <%= local_time(Time.current, format: :full) %>
|
|
13
|
+
<% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) && RailsErrorDashboard::Services::ErrorBroadcaster.available? %>
|
|
14
|
+
<span style="display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; font-size: 10px; font-weight: 600; border-radius: var(--radius-full); background: var(--status-success-bg); color: var(--status-success); margin-left: 6px;">
|
|
15
|
+
<i class="bi bi-broadcast" style="font-size: 10px;"></i> Live
|
|
16
16
|
</span>
|
|
17
|
-
|
|
18
|
-
</
|
|
17
|
+
<% end %>
|
|
18
|
+
</span>
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
<!--
|
|
22
|
-
<div
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<div class="row g-4">
|
|
28
|
-
<% 4.times do %>
|
|
29
|
-
<div class="col-md-3">
|
|
30
|
-
<div class="card stat-card">
|
|
31
|
-
<div class="card-body">
|
|
32
|
-
<div class="skeleton skeleton-text skeleton-text-short mb-2"></div>
|
|
33
|
-
<div class="skeleton skeleton-card"></div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
<% end %>
|
|
38
|
-
</div>
|
|
21
|
+
<!-- Hidden skeleton placeholders for loading state management -->
|
|
22
|
+
<div class="loading-skeleton" data-loading-target="skeleton" style="display: none;">
|
|
23
|
+
<div style="display: flex; gap: var(--space-4); margin-bottom: var(--space-4);">
|
|
24
|
+
<div class="skeleton skeleton-card" style="flex: 1;"></div>
|
|
25
|
+
<div class="skeleton skeleton-card" style="flex: 1;"></div>
|
|
26
|
+
<div class="skeleton skeleton-card" style="flex: 1;"></div>
|
|
39
27
|
</div>
|
|
40
28
|
</div>
|
|
41
29
|
|
|
42
|
-
<!--
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<small class="text-muted text-truncate d-block" title="<%= error_type %>"><%= error_type.split('::').last %></small>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
<% end %>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
<% end %>
|
|
30
|
+
<!-- Summary line + batch actions -->
|
|
31
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4);">
|
|
32
|
+
<span style="font-size: 13px; color: var(--text-secondary);">
|
|
33
|
+
<strong style="color: var(--text-primary);"><%= @pagy.count %></strong> errors
|
|
34
|
+
<span style="margin: 0 6px; color: var(--border-secondary);">·</span>
|
|
35
|
+
<strong style="color: var(--status-critical);"><%= @stats[:unresolved] %></strong> unresolved
|
|
36
|
+
<span style="margin: 0 6px; color: var(--border-secondary);">·</span>
|
|
37
|
+
<% if params[:timeframe].present? %>
|
|
38
|
+
<%= params[:timeframe].humanize %>
|
|
39
|
+
<% else %>
|
|
40
|
+
All time
|
|
41
|
+
<% end %>
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
66
44
|
|
|
67
45
|
<!-- Spike Detection Alert -->
|
|
68
46
|
<% if @stats[:spike_detected] %>
|
|
69
|
-
<div class="alert alert-warning
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</p>
|
|
88
|
-
</div>
|
|
47
|
+
<div class="alert alert-warning" role="alert" style="display: flex; align-items: center; gap: 10px; padding: var(--space-3) var(--space-5); background: var(--status-caution-bg); border: 1px solid var(--status-caution); border-radius: var(--radius-md); margin-bottom: var(--space-4); font-size: 13px; color: var(--text-primary);">
|
|
48
|
+
<i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; color: var(--status-caution); flex-shrink: 0;"></i>
|
|
49
|
+
<div>
|
|
50
|
+
<strong>
|
|
51
|
+
<% case @stats[:spike_info][:severity] %>
|
|
52
|
+
<% when :critical %>
|
|
53
|
+
Critical Error Spike Detected!
|
|
54
|
+
<% when :high %>
|
|
55
|
+
High Error Spike Detected
|
|
56
|
+
<% when :elevated %>
|
|
57
|
+
Elevated Error Activity
|
|
58
|
+
<% end %>
|
|
59
|
+
</strong>
|
|
60
|
+
<span style="color: var(--text-secondary);">
|
|
61
|
+
Today: <strong><%= @stats[:spike_info][:today_count] %> errors</strong>
|
|
62
|
+
(7-day avg: <%= @stats[:spike_info][:avg_count] %>) —
|
|
63
|
+
<strong><%= @stats[:spike_info][:multiplier] %>x normal levels</strong>
|
|
64
|
+
</span>
|
|
89
65
|
</div>
|
|
90
66
|
</div>
|
|
91
67
|
<% end %>
|
|
92
68
|
|
|
93
|
-
<!--
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
</div>
|
|
104
|
-
<div class="card-body">
|
|
105
|
-
<%= line_chart @stats[:errors_trend_7d],
|
|
106
|
-
color: "#8B5CF6",
|
|
107
|
-
curve: false,
|
|
108
|
-
points: true,
|
|
109
|
-
height: "250px",
|
|
110
|
-
library: {
|
|
111
|
-
plugins: {
|
|
112
|
-
legend: { display: false }
|
|
113
|
-
},
|
|
114
|
-
scales: {
|
|
115
|
-
y: {
|
|
116
|
-
beginAtZero: true,
|
|
117
|
-
ticks: { precision: 0 }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} %>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
69
|
+
<!-- Filter bar -->
|
|
70
|
+
<%= form_with url: errors_path, method: :get, data: { turbo: false, action: "submit->loading#submit" }, id: "filter-form" do %>
|
|
71
|
+
<% if params[:reopened].present? %>
|
|
72
|
+
<%= hidden_field_tag :reopened, params[:reopened] %>
|
|
73
|
+
<% end %>
|
|
74
|
+
<div style="display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); background: var(--surface-primary); border-radius: var(--radius-md); border: 1px solid var(--border-primary); margin-bottom: var(--space-4); flex-wrap: wrap;">
|
|
75
|
+
<!-- Search -->
|
|
76
|
+
<div style="position: relative; flex: 1 1 200px; min-width: 160px;">
|
|
77
|
+
<i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
|
|
78
|
+
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", style: "width: 100%; padding: 6px 10px 6px 30px; font-size: 13px; border: 1px solid var(--border-primary); border-radius: var(--radius-sm); background: var(--surface-base); color: var(--text-primary); outline: none; font-family: var(--font-sans);" %>
|
|
123
79
|
</div>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
80
|
+
|
|
81
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
82
|
+
|
|
83
|
+
<!-- Status pills -->
|
|
84
|
+
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
|
|
85
|
+
<%
|
|
86
|
+
current_status = params[:status]
|
|
87
|
+
is_unresolved_only = params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
88
|
+
%>
|
|
89
|
+
<%= link_to errors_path(app_context), class: "btn filter-pill #{current_status.blank? && !is_unresolved_only ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>All<% end %>
|
|
90
|
+
<%= link_to errors_path(app_context.merge(unresolved: '1')), class: "btn filter-pill #{is_unresolved_only && current_status.blank? ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Unresolved <span style="font-size: 11px; opacity: 0.7;"><%= @stats[:unresolved] %></span><% end %>
|
|
91
|
+
<%= link_to errors_path(app_context.merge(status: 'resolved', unresolved: '0')), class: "btn filter-pill #{current_status == 'resolved' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Resolved<% end %>
|
|
92
|
+
<%= link_to errors_path(app_context.merge(assigned_to: '__assigned__')), class: "btn filter-pill #{params[:assigned_to] == '__assigned__' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Assigned<% end %>
|
|
93
|
+
<%= link_to errors_path(app_context.merge(reopened: 'true')), class: "btn filter-pill #{params[:reopened] == 'true' ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>Reopened<% end %>
|
|
137
94
|
</div>
|
|
95
|
+
|
|
96
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
97
|
+
|
|
98
|
+
<!-- Severity pills -->
|
|
99
|
+
<div style="display: flex; gap: 4px;">
|
|
100
|
+
<% current_severity = params[:severity] %>
|
|
101
|
+
<%= link_to errors_path(permitted_filter_params.except(:severity)), class: "filter-pill #{current_severity.blank? ? 'active' : ''}", style: "text-decoration: none;" do %>Any severity<% end %>
|
|
102
|
+
<% %w[critical high medium low].each do |sev| %>
|
|
103
|
+
<%= link_to errors_path(permitted_filter_params.merge(severity: sev)), class: "filter-pill #{current_severity == sev ? 'active' : ''}", style: "text-decoration: none;" do %><%= sev.capitalize %><% end %>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Advanced filters toggle -->
|
|
108
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
109
|
+
<button type="button" onclick="document.getElementById('advanced-filters').style.display = document.getElementById('advanced-filters').style.display === 'none' ? 'flex' : 'none'" class="filter-pill" style="border: none;">
|
|
110
|
+
<i class="bi bi-sliders"></i> More filters
|
|
111
|
+
</button>
|
|
138
112
|
</div>
|
|
139
113
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
114
|
+
<!-- Advanced filters -->
|
|
115
|
+
<div id="advanced-filters" style="display: none; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4); padding: var(--space-3) var(--space-4); background: var(--surface-primary); border-radius: var(--radius-md); border: 1px solid var(--border-primary);">
|
|
116
|
+
<% if @applications.size > 1 %>
|
|
117
|
+
<%= select_tag :application_id, options_for_select([['All Apps', '']] + @applications, params[:application_id]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
118
|
+
<% end %>
|
|
119
|
+
<% if @platforms.size > 1 %>
|
|
120
|
+
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
121
|
+
<% end %>
|
|
122
|
+
<%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
123
|
+
<%= select_tag :timeframe, options_for_select([['All Time', ''], ['Last Hour', 'last_hour'], ['Today', 'today'], ['Yesterday', 'yesterday'], ['Last 7 Days', 'last_7_days'], ['Last 30 Days', 'last_30_days'], ['Last 90 Days', 'last_90_days']], params[:timeframe]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
124
|
+
<%= select_tag :frequency, options_for_select([['All Frequencies', ''], ['Once', 'once'], ['2-9 Times', 'few'], ['10-99 Times', 'frequent'], ['100+ Times', 'very_frequent'], ['Recurring', 'recurring']], params[:frequency]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
125
|
+
<%= select_tag :priority_level, options_for_select([['All Priorities', '']] + RailsErrorDashboard::ErrorLog.priority_options(include_emoji: true), params[:priority_level]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
126
|
+
<%= select_tag :assigned_to, options_for_select([['All Assignments', ''], ['Unassigned', '__unassigned__'], ['Assigned', '__assigned__']], params[:assigned_to]), class: "form-select", id: "assigned_to_filter", style: "width: auto; min-width: 120px;" %>
|
|
127
|
+
<div id="assignee_name_filter" style="<%= params[:assigned_to] == '__assigned__' ? '' : 'display: none;' %>">
|
|
128
|
+
<% assignees = defined?(@assignees) ? @assignees : [] %>
|
|
129
|
+
<%= select_tag :assignee_name, options_for_select([['All Assignees', '']] + assignees.map { |name| [name, name] }, params[:assignee_name]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div style="display: flex; align-items: center; gap: var(--space-4); font-size: 12px; color: var(--text-secondary);">
|
|
133
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
134
|
+
<%
|
|
135
|
+
is_checked = if params[:unresolved].nil?
|
|
136
|
+
true
|
|
137
|
+
else
|
|
138
|
+
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
139
|
+
end
|
|
140
|
+
%>
|
|
141
|
+
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox", style: "width: 14px; height: 14px;" %>
|
|
142
|
+
Unresolved only
|
|
143
|
+
</label>
|
|
144
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
145
|
+
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input", style: "width: 14px; height: 14px;" %>
|
|
146
|
+
Hide snoozed
|
|
147
|
+
</label>
|
|
148
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
149
|
+
<%= check_box_tag :hide_muted, "1", params[:hide_muted] == "1", class: "form-check-input", style: "width: 14px; height: 14px;" %>
|
|
150
|
+
Hide muted
|
|
151
|
+
</label>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div style="display: flex; gap: 6px; margin-left: auto;">
|
|
155
|
+
<%= submit_tag "Apply Filters", class: "btn btn-primary btn-sm", data: { loading_target: "submitButton" } %>
|
|
156
|
+
<%= link_to "Clear", errors_path, class: "btn btn-sm", style: "font-size: 12px;" %>
|
|
162
157
|
</div>
|
|
163
158
|
</div>
|
|
164
159
|
<% end %>
|
|
165
160
|
|
|
166
|
-
<!-- Active
|
|
161
|
+
<!-- Active Filter Pills -->
|
|
167
162
|
<%
|
|
168
163
|
active_filters = []
|
|
169
164
|
active_filters << { label: "Search: #{params[:search]}", param: :search } if params[:search].present?
|
|
170
|
-
|
|
171
|
-
# Application filter pill
|
|
172
165
|
if params[:application_id].present? && defined?(@applications)
|
|
173
166
|
app_name = @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first
|
|
174
167
|
active_filters << { label: "App: #{app_name}", param: :application_id } if app_name
|
|
175
168
|
end
|
|
176
|
-
|
|
177
169
|
active_filters << { label: "Platform: #{params[:platform]}", param: :platform } if params[:platform].present?
|
|
178
170
|
active_filters << { label: "Type: #{params[:error_type]}", param: :error_type } if params[:error_type].present?
|
|
179
171
|
active_filters << { label: "Severity: #{params[:severity].titleize}", param: :severity } if params[:severity].present?
|
|
@@ -181,519 +173,225 @@
|
|
|
181
173
|
active_filters << { label: "Frequency: #{params[:frequency].humanize}", param: :frequency } if params[:frequency].present?
|
|
182
174
|
active_filters << { label: "Status: #{params[:status].humanize}", param: :status } if params[:status].present?
|
|
183
175
|
active_filters << { label: "Priority: P#{params[:priority_level]}", param: :priority_level } if params[:priority_level].present?
|
|
184
|
-
|
|
185
|
-
# Special handling for assigned_to
|
|
186
|
-
if params[:assigned_to].present? && params[:assigned_to] != '__unassigned__' && params[:assigned_to] != '__assigned__'
|
|
187
|
-
active_filters << { label: "Assigned to: #{params[:assigned_to]}", param: :assigned_to }
|
|
188
|
-
elsif params[:assigned_to] == '__unassigned__'
|
|
176
|
+
if params[:assigned_to] == '__unassigned__'
|
|
189
177
|
active_filters << { label: "Unassigned", param: :assigned_to }
|
|
190
178
|
elsif params[:assigned_to] == '__assigned__'
|
|
191
179
|
active_filters << { label: "Assigned", param: :assigned_to }
|
|
192
|
-
# Show assignee name filter if present
|
|
193
|
-
if params[:assignee_name].present?
|
|
194
|
-
active_filters << { label: "Assignee: #{params[:assignee_name]}", param: :assignee_name }
|
|
195
|
-
end
|
|
196
180
|
end
|
|
197
181
|
%>
|
|
198
|
-
|
|
199
182
|
<% if active_filters.any? %>
|
|
200
|
-
<div
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# Build URL without this specific filter
|
|
206
|
-
filter_params = permitted_filter_params.except(filter[:param])
|
|
207
|
-
%>
|
|
208
|
-
<%= link_to errors_path(filter_params), class: "badge bg-primary text-decoration-none filter-pill" do %>
|
|
209
|
-
<%= filter[:label] %>
|
|
210
|
-
<i class="bi bi-x ms-1"></i>
|
|
211
|
-
<% end %>
|
|
212
|
-
<% end %>
|
|
213
|
-
<%= link_to errors_path, class: "badge bg-secondary text-decoration-none filter-pill" do %>
|
|
214
|
-
<i class="bi bi-x-circle me-1"></i>
|
|
215
|
-
Clear All
|
|
216
|
-
<% end %>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
<% end %>
|
|
220
|
-
|
|
221
|
-
<!-- Filters -->
|
|
222
|
-
<div class="card mb-4" id="filters-section">
|
|
223
|
-
<div class="card-header bg-white">
|
|
224
|
-
<h5 class="mb-0">Filters & Search</h5>
|
|
225
|
-
</div>
|
|
226
|
-
<div class="card-body">
|
|
227
|
-
<!-- Quick Filter Buttons -->
|
|
228
|
-
<div class="d-flex gap-2 mb-3">
|
|
229
|
-
<%= link_to "All Errors", errors_path,
|
|
230
|
-
class: "btn btn-sm #{params[:assigned_to].blank? && params[:reopened].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
231
|
-
<%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
|
|
232
|
-
class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
233
|
-
<%= link_to "Reopened", errors_path(reopened: 'true'),
|
|
234
|
-
class: "btn btn-sm #{params[:reopened] == 'true' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false, action: "submit->loading#submit" } do %>
|
|
238
|
-
<% if params[:reopened].present? %>
|
|
239
|
-
<%= hidden_field_tag :reopened, params[:reopened] %>
|
|
240
|
-
<% end %>
|
|
241
|
-
<div class="col-md-4">
|
|
242
|
-
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<!-- Application Filter (only show if multiple apps) -->
|
|
246
|
-
<% if @applications.size > 1 %>
|
|
247
|
-
<div class="col-md-2">
|
|
248
|
-
<%= select_tag :application_id,
|
|
249
|
-
options_for_select(
|
|
250
|
-
[['All Apps', '']] + @applications,
|
|
251
|
-
params[:application_id]
|
|
252
|
-
),
|
|
253
|
-
class: "form-select" %>
|
|
254
|
-
</div>
|
|
183
|
+
<div style="display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: var(--space-4); font-size: 12px;">
|
|
184
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">Filters:</span>
|
|
185
|
+
<% active_filters.each do |filter| %>
|
|
186
|
+
<%= link_to errors_path(permitted_filter_params.except(filter[:param])), class: "filter-pill active", style: "text-decoration: none; font-size: 11px; padding: 3px 8px;" do %>
|
|
187
|
+
<%= filter[:label] %> <i class="bi bi-x" style="font-size: 12px;"></i>
|
|
255
188
|
<% end %>
|
|
256
|
-
|
|
257
|
-
<% if @platforms.size > 1 %>
|
|
258
|
-
<div class="col-md-2">
|
|
259
|
-
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
|
|
260
|
-
</div>
|
|
261
|
-
<% end %>
|
|
262
|
-
|
|
263
|
-
<div class="col-md-2">
|
|
264
|
-
<%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
|
|
265
|
-
</div>
|
|
266
|
-
|
|
267
|
-
<div class="col-md-2">
|
|
268
|
-
<%= select_tag :severity, options_for_select([
|
|
269
|
-
['All Severities', ''],
|
|
270
|
-
['🔴 Critical', 'critical'],
|
|
271
|
-
['🟠 High', 'high'],
|
|
272
|
-
['🟡 Medium', 'medium'],
|
|
273
|
-
['⚪ Low', 'low']
|
|
274
|
-
], params[:severity]), class: "form-select" %>
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
<!-- Time Range Filter -->
|
|
278
|
-
<div class="col-md-2">
|
|
279
|
-
<%= select_tag :timeframe, options_for_select([
|
|
280
|
-
['All Time', ''],
|
|
281
|
-
['Last Hour', 'last_hour'],
|
|
282
|
-
['Today', 'today'],
|
|
283
|
-
['Yesterday', 'yesterday'],
|
|
284
|
-
['Last 7 Days', 'last_7_days'],
|
|
285
|
-
['Last 30 Days', 'last_30_days'],
|
|
286
|
-
['Last 90 Days', 'last_90_days']
|
|
287
|
-
], params[:timeframe]), class: "form-select" %>
|
|
288
|
-
</div>
|
|
289
|
-
|
|
290
|
-
<!-- Frequency Filter -->
|
|
291
|
-
<div class="col-md-2">
|
|
292
|
-
<%= select_tag :frequency, options_for_select([
|
|
293
|
-
['All Frequencies', ''],
|
|
294
|
-
['Once', 'once'],
|
|
295
|
-
['2-9 Times', 'few'],
|
|
296
|
-
['10-99 Times', 'frequent'],
|
|
297
|
-
['100+ Times', 'very_frequent'],
|
|
298
|
-
['Recurring (Active)', 'recurring']
|
|
299
|
-
], params[:frequency]), class: "form-select" %>
|
|
300
|
-
</div>
|
|
301
|
-
|
|
302
|
-
<!-- Phase 3: Workflow Filters -->
|
|
303
|
-
<div class="col-md-2">
|
|
304
|
-
<%= select_tag :status, options_for_select([
|
|
305
|
-
['All Statuses', ''],
|
|
306
|
-
['🆕 New', 'new'],
|
|
307
|
-
['🔵 In Progress', 'in_progress'],
|
|
308
|
-
['🟡 Investigating', 'investigating'],
|
|
309
|
-
['✅ Resolved', 'resolved'],
|
|
310
|
-
['⚫ Won\'t Fix', 'wont_fix']
|
|
311
|
-
], params[:status]), class: "form-select" %>
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
|
-
<div class="col-md-2">
|
|
315
|
-
<%= select_tag :assigned_to, options_for_select([
|
|
316
|
-
['All Assignments', ''],
|
|
317
|
-
['Unassigned', '__unassigned__'],
|
|
318
|
-
['Assigned', '__assigned__']
|
|
319
|
-
], params[:assigned_to]), class: "form-select", id: "assigned_to_filter" %>
|
|
320
|
-
</div>
|
|
321
|
-
|
|
322
|
-
<!-- Assignee Name Filter - Only shown when "Assigned" is selected -->
|
|
323
|
-
<div class="col-md-2" id="assignee_name_filter" style="<%= params[:assigned_to] == '__assigned__' ? '' : 'display: none;' %>">
|
|
324
|
-
<%= select_tag :assignee_name, options_for_select(
|
|
325
|
-
[['All Assignees', '']] + @assignees.map { |name| [name, name] },
|
|
326
|
-
params[:assignee_name]
|
|
327
|
-
), class: "form-select", placeholder: "Filter by assignee..." %>
|
|
328
|
-
</div>
|
|
329
|
-
|
|
330
|
-
<div class="col-md-2">
|
|
331
|
-
<%= select_tag :priority_level, options_for_select(
|
|
332
|
-
[['All Priorities', '']] + RailsErrorDashboard::ErrorLog.priority_options(include_emoji: true),
|
|
333
|
-
params[:priority_level]
|
|
334
|
-
), class: "form-select" %>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<!-- Checkboxes on their own row -->
|
|
338
|
-
<div class="col-12 mt-2">
|
|
339
|
-
<div class="row g-3">
|
|
340
|
-
<div class="col-auto">
|
|
341
|
-
<div class="form-check">
|
|
342
|
-
<%
|
|
343
|
-
# Determine checkbox state based on params
|
|
344
|
-
# If unresolved param is missing or nil, default to checked (true)
|
|
345
|
-
# If unresolved param is explicitly "false" or "0", uncheck it
|
|
346
|
-
is_checked = if params[:unresolved].nil?
|
|
347
|
-
true # Default to checked when first loading
|
|
348
|
-
else
|
|
349
|
-
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
350
|
-
end
|
|
351
|
-
%>
|
|
352
|
-
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
|
|
353
|
-
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
<div class="col-auto">
|
|
358
|
-
<div class="form-check">
|
|
359
|
-
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
|
|
360
|
-
<%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<div class="col-auto">
|
|
365
|
-
<div class="form-check">
|
|
366
|
-
<%= check_box_tag :hide_muted, "1", params[:hide_muted] == "1", class: "form-check-input" %>
|
|
367
|
-
<%= label_tag :hide_muted, "Hide muted", class: "form-check-label" %>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
<div class="col-12 mt-3">
|
|
374
|
-
<%= submit_tag "Apply Filters", class: "btn btn-primary", data: { loading_target: "submitButton" } %>
|
|
375
|
-
<%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
|
|
376
|
-
</div>
|
|
377
|
-
<% end %>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
|
|
381
|
-
<!-- Error List Table -->
|
|
382
|
-
<div class="card">
|
|
383
|
-
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
384
|
-
<h5 class="mb-0">Recent Errors</h5>
|
|
385
|
-
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
386
|
-
</div>
|
|
387
|
-
|
|
388
|
-
<!-- Batch Actions Toolbar -->
|
|
389
|
-
<div class="card-body border-bottom" id="batch-actions-toolbar" style="display: none;">
|
|
390
|
-
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form" do |f| %>
|
|
391
|
-
<div class="row align-items-center">
|
|
392
|
-
<div class="col-auto">
|
|
393
|
-
<span id="selected-count" class="fw-bold">0 selected</span>
|
|
394
|
-
</div>
|
|
395
|
-
<div class="col-auto">
|
|
396
|
-
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm btn-success" do %>
|
|
397
|
-
<i class="bi bi-check-circle"></i> Resolve Selected
|
|
398
|
-
<% end %>
|
|
399
|
-
</div>
|
|
400
|
-
<div class="col-auto">
|
|
401
|
-
<%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm btn-danger", data: { confirm: "Are you sure you want to delete the selected errors?" } do %>
|
|
402
|
-
<i class="bi bi-trash"></i> Delete Selected
|
|
403
|
-
<% end %>
|
|
404
|
-
</div>
|
|
405
|
-
<div class="col-auto">
|
|
406
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selection">
|
|
407
|
-
Clear Selection
|
|
408
|
-
</button>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
189
|
<% end %>
|
|
190
|
+
<%= link_to errors_path, style: "color: var(--text-tertiary); font-size: 11px;" do %>Clear all<% end %>
|
|
412
191
|
</div>
|
|
192
|
+
<% end %>
|
|
413
193
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
194
|
+
<!-- Error Table -->
|
|
195
|
+
<div class="card" style="overflow: hidden;">
|
|
196
|
+
<% if @errors.any? %>
|
|
197
|
+
<div data-loading-target="content">
|
|
198
|
+
<div style="overflow-x: auto;">
|
|
199
|
+
<table class="table table-hover" style="margin-bottom: 0; width: 100%; table-layout: fixed;">
|
|
200
|
+
<thead>
|
|
201
|
+
<tr id="thead-columns" style="border-bottom: 1px solid var(--border-primary);">
|
|
202
|
+
<th style="width: 36px; padding: var(--space-3) var(--space-4); text-align: center;">
|
|
203
|
+
<input type="checkbox" id="select-all" class="form-check-input" style="accent-color: var(--accent);">
|
|
204
|
+
</th>
|
|
205
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: left;">Error</th>
|
|
206
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: left; width: 100px;">Status</th>
|
|
207
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 70px;">Events</th>
|
|
208
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 55px;">Users</th>
|
|
209
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 85px;">Last seen</th>
|
|
210
|
+
<% if @applications.size > 1 && params[:application_id].blank? %>
|
|
211
|
+
<th style="padding: var(--space-3) var(--space-4); width: 100px;">App</th>
|
|
212
|
+
<% end %>
|
|
213
|
+
<% if @platforms.size > 1 %>
|
|
214
|
+
<th style="padding: var(--space-3) var(--space-4); width: 100px;">Platform</th>
|
|
215
|
+
<% end %>
|
|
216
|
+
</tr>
|
|
217
|
+
<tr id="thead-batch" style="display: none; border-bottom: 1px solid var(--border-primary); background: var(--surface-secondary);">
|
|
218
|
+
<th style="width: 40px; padding: var(--space-3) var(--space-4); text-align: center;">
|
|
219
|
+
<input type="checkbox" id="select-all-batch" class="form-check-input" style="accent-color: var(--accent);" checked>
|
|
220
|
+
</th>
|
|
221
|
+
<th colspan="99" style="padding: var(--space-3) var(--space-4);">
|
|
222
|
+
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form", style: "display: flex; gap: 8px; align-items: center;" do |f| %>
|
|
223
|
+
<% if params[:application_id].present? %>
|
|
224
|
+
<%= hidden_field_tag :application_id, params[:application_id] %>
|
|
433
225
|
<% end %>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<
|
|
226
|
+
<span id="selected-count" style="font-size: 13px; font-weight: 600; color: var(--text-primary);"></span>
|
|
227
|
+
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
228
|
+
<i class="bi bi-check-circle"></i> Resolve
|
|
229
|
+
<% end %>
|
|
230
|
+
<%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px; color: var(--status-critical);", data: { confirm: "Are you sure you want to delete the selected errors?" } do %>
|
|
231
|
+
<i class="bi bi-trash"></i> Delete
|
|
437
232
|
<% end %>
|
|
438
|
-
<th>Status</th>
|
|
439
|
-
<th></th>
|
|
440
|
-
</tr>
|
|
441
|
-
</thead>
|
|
442
|
-
<tbody id="error_list">
|
|
443
|
-
<% @errors.each do |error| %>
|
|
444
|
-
<%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
|
|
445
233
|
<% end %>
|
|
446
|
-
</
|
|
447
|
-
</
|
|
448
|
-
</
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
</
|
|
234
|
+
</th>
|
|
235
|
+
</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody id="error_list">
|
|
238
|
+
<% @errors.each do |error| %>
|
|
239
|
+
<%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
|
|
240
|
+
<% end %>
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>
|
|
454
243
|
</div>
|
|
455
244
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
245
|
+
<!-- Pagination -->
|
|
246
|
+
<% if @pagy.pages > 1 %>
|
|
247
|
+
<div style="padding: var(--space-4); border-top: 1px solid var(--border-primary); display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-3);">
|
|
248
|
+
<span style="font-size: 12px; color: var(--text-tertiary);">Showing <%= @pagy.from %>–<%= @pagy.to %> of <%= @pagy.count %> errors</span>
|
|
249
|
+
<div><%== @pagy.series_nav(:bootstrap) %></div>
|
|
461
250
|
</div>
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
No errors match your current filters. Try adjusting your search criteria.
|
|
470
|
-
<% else %>
|
|
471
|
-
No errors have been logged yet. Your application is running smoothly!
|
|
472
|
-
<% end %>
|
|
473
|
-
</p>
|
|
474
|
-
<% unless params.values.compact.any? %>
|
|
475
|
-
<small class="text-muted d-block mt-3">
|
|
476
|
-
<i class="bi bi-lightbulb"></i> Errors will appear here automatically when they occur.
|
|
477
|
-
</small>
|
|
251
|
+
<% end %>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class="loading-skeleton" data-loading-target="skeleton">
|
|
255
|
+
<div style="padding: var(--space-4);">
|
|
256
|
+
<% 5.times do %>
|
|
257
|
+
<div class="skeleton skeleton-row" style="margin-bottom: 2px;"></div>
|
|
478
258
|
<% end %>
|
|
479
259
|
</div>
|
|
480
|
-
|
|
481
|
-
|
|
260
|
+
</div>
|
|
261
|
+
<% else %>
|
|
262
|
+
<!-- Empty State -->
|
|
263
|
+
<div class="red-empty-state">
|
|
264
|
+
<% if params[:search].present? || params[:error_type].present? || params[:severity].present? %>
|
|
265
|
+
<div class="red-empty-state-icon"><i class="bi bi-funnel"></i></div>
|
|
266
|
+
<div class="red-empty-state-title">No errors match your filters</div>
|
|
267
|
+
<div class="red-empty-state-message">Try adjusting your search or filter criteria to find what you're looking for.</div>
|
|
268
|
+
<%= link_to errors_path, class: "red-empty-state-cta" do %><i class="bi bi-x-circle"></i> Clear all filters<% end %>
|
|
269
|
+
<% else %>
|
|
270
|
+
<div class="red-empty-state-icon" style="background: var(--status-success-bg); color: var(--status-success);"><i class="bi bi-check-lg"></i></div>
|
|
271
|
+
<div class="red-empty-state-title">All clear!</div>
|
|
272
|
+
<div class="red-empty-state-message">No errors have been logged yet. Your application is running smoothly. Errors will appear here automatically when they occur.</div>
|
|
273
|
+
<% end %>
|
|
274
|
+
</div>
|
|
275
|
+
<% end %>
|
|
482
276
|
</div>
|
|
483
277
|
</div>
|
|
484
278
|
|
|
485
279
|
<script>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
280
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
281
|
+
var selectAllCheckbox = document.getElementById('select-all');
|
|
282
|
+
var selectAllBatchCheckbox = document.getElementById('select-all-batch');
|
|
283
|
+
var theadColumns = document.getElementById('thead-columns');
|
|
284
|
+
var theadBatch = document.getElementById('thead-batch');
|
|
285
|
+
var selectedCountSpan = document.getElementById('selected-count');
|
|
286
|
+
var batchForm = document.getElementById('batch-form');
|
|
287
|
+
|
|
288
|
+
function getErrorCheckboxes() {
|
|
289
|
+
return document.querySelectorAll('.error-checkbox');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function updateBatchToolbar() {
|
|
293
|
+
var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
294
|
+
var count = checkedBoxes.length;
|
|
295
|
+
var boxes = getErrorCheckboxes();
|
|
296
|
+
var allChecked = boxes.length > 0 && Array.from(boxes).every(function(cb) { return cb.checked; });
|
|
297
|
+
var someChecked = Array.from(boxes).some(function(cb) { return cb.checked; });
|
|
298
|
+
|
|
299
|
+
if (count > 0) {
|
|
300
|
+
theadColumns.style.display = 'none';
|
|
301
|
+
theadBatch.style.display = '';
|
|
302
|
+
selectedCountSpan.textContent = count + ' selected';
|
|
303
|
+
selectAllBatchCheckbox.checked = allChecked;
|
|
304
|
+
selectAllBatchCheckbox.indeterminate = someChecked && !allChecked;
|
|
305
|
+
} else {
|
|
306
|
+
theadColumns.style.display = '';
|
|
307
|
+
theadBatch.style.display = 'none';
|
|
504
308
|
}
|
|
505
309
|
|
|
506
|
-
// Select all checkbox
|
|
507
310
|
if (selectAllCheckbox) {
|
|
508
|
-
selectAllCheckbox.
|
|
509
|
-
|
|
510
|
-
checkbox.checked = this.checked;
|
|
511
|
-
});
|
|
512
|
-
updateBatchToolbar();
|
|
513
|
-
});
|
|
311
|
+
selectAllCheckbox.checked = allChecked;
|
|
312
|
+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
514
313
|
}
|
|
314
|
+
}
|
|
515
315
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// Update "select all" checkbox state
|
|
522
|
-
const allChecked = Array.from(errorCheckboxes).every(cb => cb.checked);
|
|
523
|
-
const someChecked = Array.from(errorCheckboxes).some(cb => cb.checked);
|
|
316
|
+
function handleSelectAll(checked) {
|
|
317
|
+
getErrorCheckboxes().forEach(function(cb) { cb.checked = checked; });
|
|
318
|
+
updateBatchToolbar();
|
|
319
|
+
}
|
|
524
320
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
});
|
|
321
|
+
if (selectAllCheckbox) {
|
|
322
|
+
selectAllCheckbox.addEventListener('change', function() {
|
|
323
|
+
handleSelectAll(selectAllCheckbox.checked);
|
|
530
324
|
});
|
|
325
|
+
}
|
|
531
326
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
});
|
|
538
|
-
if (selectAllCheckbox) {
|
|
539
|
-
selectAllCheckbox.checked = false;
|
|
540
|
-
selectAllCheckbox.indeterminate = false;
|
|
541
|
-
}
|
|
542
|
-
updateBatchToolbar();
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Form submission - add selected error IDs
|
|
547
|
-
if (batchForm) {
|
|
548
|
-
batchForm.addEventListener('submit', function(e) {
|
|
549
|
-
const checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
550
|
-
|
|
551
|
-
if (checkedBoxes.length === 0) {
|
|
552
|
-
e.preventDefault();
|
|
553
|
-
alert('Please select at least one error');
|
|
554
|
-
return false;
|
|
555
|
-
}
|
|
327
|
+
if (selectAllBatchCheckbox) {
|
|
328
|
+
selectAllBatchCheckbox.addEventListener('change', function() {
|
|
329
|
+
handleSelectAll(selectAllBatchCheckbox.checked);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
556
332
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
input.type = 'hidden';
|
|
561
|
-
input.name = 'error_ids[]';
|
|
562
|
-
input.value = checkbox.value;
|
|
563
|
-
this.appendChild(input);
|
|
564
|
-
});
|
|
565
|
-
});
|
|
333
|
+
document.addEventListener('change', function(e) {
|
|
334
|
+
if (e.target.classList.contains('error-checkbox')) {
|
|
335
|
+
updateBatchToolbar();
|
|
566
336
|
}
|
|
567
337
|
});
|
|
568
338
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
339
|
+
if (batchForm) {
|
|
340
|
+
batchForm.addEventListener('submit', function(e) {
|
|
341
|
+
var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
342
|
+
if (checkedBoxes.length === 0) { e.preventDefault(); return false; }
|
|
343
|
+
checkedBoxes.forEach(function(cb) {
|
|
344
|
+
var input = document.createElement('input');
|
|
345
|
+
input.type = 'hidden'; input.name = 'error_ids[]'; input.value = cb.value;
|
|
346
|
+
batchForm.appendChild(input);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
}
|
|
572
350
|
|
|
573
|
-
|
|
351
|
+
// Turbo Stream animations
|
|
352
|
+
document.addEventListener('turbo:before-stream-render', function(event) {
|
|
353
|
+
var target = event.detail.newStream.target;
|
|
354
|
+
var action = event.detail.newStream.action;
|
|
574
355
|
if (action === 'prepend' && target === 'error_list') {
|
|
575
|
-
setTimeout(()
|
|
576
|
-
|
|
356
|
+
setTimeout(function() {
|
|
357
|
+
var firstRow = document.querySelector('#error_list tr:first-child');
|
|
577
358
|
if (firstRow) {
|
|
578
|
-
firstRow.
|
|
579
|
-
|
|
580
|
-
// Remove class after animation completes
|
|
581
|
-
setTimeout(() => {
|
|
582
|
-
firstRow.classList.remove('new-error');
|
|
583
|
-
}, 3000);
|
|
359
|
+
firstRow.style.background = 'var(--status-success-bg)';
|
|
360
|
+
setTimeout(function() { firstRow.style.background = ''; }, 3000);
|
|
584
361
|
}
|
|
585
362
|
}, 10);
|
|
586
363
|
}
|
|
587
|
-
|
|
588
|
-
// Pulse stats cards when updated
|
|
589
|
-
if (action === 'replace' && target === 'dashboard_stats') {
|
|
590
|
-
setTimeout(() => {
|
|
591
|
-
const statCards = document.querySelectorAll('.stat-card');
|
|
592
|
-
statCards.forEach(card => {
|
|
593
|
-
card.classList.add('updated');
|
|
594
|
-
setTimeout(() => card.classList.remove('updated'), 500);
|
|
595
|
-
});
|
|
596
|
-
}, 10);
|
|
597
|
-
}
|
|
598
364
|
});
|
|
599
365
|
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
366
|
+
// Scroll position preservation
|
|
367
|
+
var scrollPos = sessionStorage.getItem('scrollPos');
|
|
368
|
+
if (scrollPos) { window.scrollTo(0, parseInt(scrollPos)); sessionStorage.removeItem('scrollPos'); }
|
|
369
|
+
|
|
370
|
+
var filterForm = document.getElementById('filter-form');
|
|
371
|
+
if (filterForm) {
|
|
372
|
+
filterForm.addEventListener('submit', function() {
|
|
373
|
+
sessionStorage.setItem('scrollPos', window.pageYOffset);
|
|
374
|
+
var unresolvedCb = document.getElementById('unresolved_checkbox');
|
|
375
|
+
if (unresolvedCb && !unresolvedCb.checked) {
|
|
376
|
+
var hidden = document.createElement('input');
|
|
377
|
+
hidden.type = 'hidden'; hidden.name = 'unresolved'; hidden.value = '0';
|
|
378
|
+
filterForm.appendChild(hidden);
|
|
379
|
+
unresolvedCb.disabled = true;
|
|
380
|
+
}
|
|
605
381
|
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const searchInput = document.querySelector('input[name="search"]');
|
|
622
|
-
if (searchInput) searchInput.focus();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// 'a' - Go to analytics
|
|
626
|
-
if (e.key === 'a') {
|
|
627
|
-
window.location.href = '<%= analytics_errors_path %>';
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// '?' - Show keyboard shortcuts help
|
|
631
|
-
if (e.key === '?') {
|
|
632
|
-
e.preventDefault();
|
|
633
|
-
const modal = new bootstrap.Modal(document.getElementById('keyboardShortcutsModal'));
|
|
634
|
-
modal.show();
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// Preserve scroll position on page load
|
|
639
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
640
|
-
const scrollPos = sessionStorage.getItem('scrollPos');
|
|
641
|
-
if (scrollPos) {
|
|
642
|
-
window.scrollTo(0, parseInt(scrollPos));
|
|
643
|
-
sessionStorage.removeItem('scrollPos');
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
// Handle filter form submission
|
|
648
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
649
|
-
const filterForm = document.querySelector('form[action="<%= errors_path %>"]');
|
|
650
|
-
if (filterForm) {
|
|
651
|
-
filterForm.addEventListener('submit', function(e) {
|
|
652
|
-
// Save scroll position before form submission
|
|
653
|
-
sessionStorage.setItem('scrollPos', window.pageYOffset);
|
|
654
|
-
|
|
655
|
-
// Handle unchecked checkbox - when unchecked, send "0" explicitly
|
|
656
|
-
const unresolvedCheckbox = document.getElementById('unresolved_checkbox');
|
|
657
|
-
if (unresolvedCheckbox && !unresolvedCheckbox.checked) {
|
|
658
|
-
// Create hidden input to send "0" when unchecked
|
|
659
|
-
const hiddenInput = document.createElement('input');
|
|
660
|
-
hiddenInput.type = 'hidden';
|
|
661
|
-
hiddenInput.name = 'unresolved';
|
|
662
|
-
hiddenInput.value = '0';
|
|
663
|
-
filterForm.appendChild(hiddenInput);
|
|
664
|
-
// Remove the checkbox from form submission to avoid sending empty value
|
|
665
|
-
unresolvedCheckbox.disabled = true;
|
|
666
|
-
}
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// Update browser tab title with unresolved error count
|
|
672
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
673
|
-
const unresolvedCount = <%= @stats[:unresolved] || 0 %>;
|
|
674
|
-
if (unresolvedCount > 0) {
|
|
675
|
-
document.title = `(${unresolvedCount}) ${document.title}`;
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
// Toggle assignee name filter visibility based on assigned_to selection
|
|
680
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
681
|
-
const assignedToFilter = document.getElementById('assigned_to_filter');
|
|
682
|
-
const assigneeNameFilter = document.getElementById('assignee_name_filter');
|
|
683
|
-
|
|
684
|
-
if (assignedToFilter && assigneeNameFilter) {
|
|
685
|
-
assignedToFilter.addEventListener('change', function() {
|
|
686
|
-
if (this.value === '__assigned__') {
|
|
687
|
-
assigneeNameFilter.style.display = 'block';
|
|
688
|
-
} else {
|
|
689
|
-
assigneeNameFilter.style.display = 'none';
|
|
690
|
-
// Reset assignee name filter when hiding
|
|
691
|
-
const assigneeNameSelect = assigneeNameFilter.querySelector('select');
|
|
692
|
-
if (assigneeNameSelect) {
|
|
693
|
-
assigneeNameSelect.value = '';
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Tab title with unresolved count
|
|
385
|
+
var unresolvedCount = <%= @stats[:unresolved] || 0 %>;
|
|
386
|
+
if (unresolvedCount > 0) { document.title = '(' + unresolvedCount + ') ' + document.title; }
|
|
387
|
+
|
|
388
|
+
// Toggle assignee name filter visibility
|
|
389
|
+
var assignedToFilter = document.getElementById('assigned_to_filter');
|
|
390
|
+
var assigneeNameFilter = document.getElementById('assignee_name_filter');
|
|
391
|
+
if (assignedToFilter && assigneeNameFilter) {
|
|
392
|
+
assignedToFilter.addEventListener('change', function() {
|
|
393
|
+
assigneeNameFilter.style.display = this.value === '__assigned__' ? '' : 'none';
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
});
|
|
699
397
|
</script>
|