rails_error_dashboard 0.5.15 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -26
- data/app/helpers/rails_error_dashboard/application_helper.rb +12 -5
- data/app/views/layouts/rails_error_dashboard.html.erb +1217 -1935
- 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 +1 -1
- 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 +292 -620
- 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 +32 -52
- data/app/views/rails_error_dashboard/errors/show.html.erb +200 -203
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +6 -6
- data/lib/rails_error_dashboard/configuration.rb +6 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +2 -2
|
@@ -5,175 +5,181 @@
|
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 id="batch-actions-inline" style="display: none; gap: 6px;">
|
|
44
|
+
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form", style: "display: flex; gap: 6px; align-items: center;" do |f| %>
|
|
45
|
+
<% if params[:application_id].present? %>
|
|
46
|
+
<%= hidden_field_tag :application_id, params[:application_id] %>
|
|
47
|
+
<% end %>
|
|
48
|
+
<span id="selected-count" style="font-size: 12px; font-weight: 600; color: var(--text-secondary);"></span>
|
|
49
|
+
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
50
|
+
Resolve
|
|
51
|
+
<% end %>
|
|
52
|
+
<%= 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 %>
|
|
53
|
+
Delete
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
64
56
|
</div>
|
|
65
|
-
|
|
57
|
+
</div>
|
|
66
58
|
|
|
67
59
|
<!-- Spike Detection Alert -->
|
|
68
60
|
<% 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>
|
|
61
|
+
<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);">
|
|
62
|
+
<i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; color: var(--status-caution); flex-shrink: 0;"></i>
|
|
63
|
+
<div>
|
|
64
|
+
<strong>
|
|
65
|
+
<% case @stats[:spike_info][:severity] %>
|
|
66
|
+
<% when :critical %>
|
|
67
|
+
Critical Error Spike Detected!
|
|
68
|
+
<% when :high %>
|
|
69
|
+
High Error Spike Detected
|
|
70
|
+
<% when :elevated %>
|
|
71
|
+
Elevated Error Activity
|
|
72
|
+
<% end %>
|
|
73
|
+
</strong>
|
|
74
|
+
<span style="color: var(--text-secondary);">
|
|
75
|
+
Today: <strong><%= @stats[:spike_info][:today_count] %> errors</strong>
|
|
76
|
+
(7-day avg: <%= @stats[:spike_info][:avg_count] %>) —
|
|
77
|
+
<strong><%= @stats[:spike_info][:multiplier] %>x normal levels</strong>
|
|
78
|
+
</span>
|
|
89
79
|
</div>
|
|
90
80
|
</div>
|
|
91
81
|
<% end %>
|
|
92
82
|
|
|
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>
|
|
83
|
+
<!-- Filter bar -->
|
|
84
|
+
<%= form_with url: errors_path, method: :get, data: { turbo: false, action: "submit->loading#submit" }, id: "filter-form" do %>
|
|
85
|
+
<% if params[:reopened].present? %>
|
|
86
|
+
<%= hidden_field_tag :reopened, params[:reopened] %>
|
|
87
|
+
<% end %>
|
|
88
|
+
<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;">
|
|
89
|
+
<!-- Search -->
|
|
90
|
+
<div style="position: relative; flex: 1 1 200px; min-width: 160px;">
|
|
91
|
+
<i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
|
|
92
|
+
<%= 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
93
|
</div>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
94
|
+
|
|
95
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
96
|
+
|
|
97
|
+
<!-- Status pills -->
|
|
98
|
+
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
|
|
99
|
+
<%
|
|
100
|
+
current_status = params[:status]
|
|
101
|
+
is_unresolved_only = params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
102
|
+
%>
|
|
103
|
+
<%= 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 %>
|
|
104
|
+
<%= 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 %>
|
|
105
|
+
<%= 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 %>
|
|
106
|
+
<%= 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 %>
|
|
107
|
+
<%= 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 %>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
111
|
+
|
|
112
|
+
<!-- Severity pills -->
|
|
113
|
+
<div style="display: flex; gap: 4px;">
|
|
114
|
+
<% current_severity = params[:severity] %>
|
|
115
|
+
<%= link_to errors_path(permitted_filter_params.except(:severity)), class: "filter-pill #{current_severity.blank? ? 'active' : ''}", style: "text-decoration: none;" do %>Any severity<% end %>
|
|
116
|
+
<% %w[critical high medium low].each do |sev| %>
|
|
117
|
+
<%= 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 %>
|
|
118
|
+
<% end %>
|
|
137
119
|
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Advanced filters toggle -->
|
|
122
|
+
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
123
|
+
<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;">
|
|
124
|
+
<i class="bi bi-sliders"></i> More filters
|
|
125
|
+
</button>
|
|
138
126
|
</div>
|
|
139
127
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
128
|
+
<!-- Advanced filters -->
|
|
129
|
+
<div id="advanced-filters" style="display: flex; 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);">
|
|
130
|
+
<% if @applications.size > 1 %>
|
|
131
|
+
<%= select_tag :application_id, options_for_select([['All Apps', '']] + @applications, params[:application_id]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
132
|
+
<% end %>
|
|
133
|
+
<% if @platforms.size > 1 %>
|
|
134
|
+
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select", style: "width: auto; min-width: 120px;" %>
|
|
135
|
+
<% end %>
|
|
136
|
+
<%= 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;" %>
|
|
137
|
+
<%= 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;" %>
|
|
138
|
+
<%= 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;" %>
|
|
139
|
+
<%= 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;" %>
|
|
140
|
+
<%= 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;" %>
|
|
141
|
+
<div id="assignee_name_filter" style="<%= params[:assigned_to] == '__assigned__' ? '' : 'display: none;' %>">
|
|
142
|
+
<% assignees = defined?(@assignees) ? @assignees : [] %>
|
|
143
|
+
<%= 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;" %>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div style="display: flex; align-items: center; gap: var(--space-4); font-size: 12px; color: var(--text-secondary);">
|
|
147
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
148
|
+
<%
|
|
149
|
+
is_checked = if params[:unresolved].nil?
|
|
150
|
+
true
|
|
151
|
+
else
|
|
152
|
+
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
153
|
+
end
|
|
154
|
+
%>
|
|
155
|
+
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox", style: "width: 14px; height: 14px;" %>
|
|
156
|
+
Unresolved only
|
|
157
|
+
</label>
|
|
158
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
159
|
+
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input", style: "width: 14px; height: 14px;" %>
|
|
160
|
+
Hide snoozed
|
|
161
|
+
</label>
|
|
162
|
+
<label style="display: flex; align-items: center; gap: 4px; font-weight: 400;">
|
|
163
|
+
<%= check_box_tag :hide_muted, "1", params[:hide_muted] == "1", class: "form-check-input", style: "width: 14px; height: 14px;" %>
|
|
164
|
+
Hide muted
|
|
165
|
+
</label>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div style="display: flex; gap: 6px; margin-left: auto;">
|
|
169
|
+
<%= submit_tag "Apply Filters", class: "btn btn-primary btn-sm", data: { loading_target: "submitButton" } %>
|
|
170
|
+
<%= link_to "Clear", errors_path, class: "btn btn-sm", style: "font-size: 12px;" %>
|
|
162
171
|
</div>
|
|
163
172
|
</div>
|
|
164
173
|
<% end %>
|
|
165
174
|
|
|
166
|
-
<!-- Active
|
|
175
|
+
<!-- Active Filter Pills -->
|
|
167
176
|
<%
|
|
168
177
|
active_filters = []
|
|
169
178
|
active_filters << { label: "Search: #{params[:search]}", param: :search } if params[:search].present?
|
|
170
|
-
|
|
171
|
-
# Application filter pill
|
|
172
179
|
if params[:application_id].present? && defined?(@applications)
|
|
173
180
|
app_name = @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first
|
|
174
181
|
active_filters << { label: "App: #{app_name}", param: :application_id } if app_name
|
|
175
182
|
end
|
|
176
|
-
|
|
177
183
|
active_filters << { label: "Platform: #{params[:platform]}", param: :platform } if params[:platform].present?
|
|
178
184
|
active_filters << { label: "Type: #{params[:error_type]}", param: :error_type } if params[:error_type].present?
|
|
179
185
|
active_filters << { label: "Severity: #{params[:severity].titleize}", param: :severity } if params[:severity].present?
|
|
@@ -181,519 +187,185 @@
|
|
|
181
187
|
active_filters << { label: "Frequency: #{params[:frequency].humanize}", param: :frequency } if params[:frequency].present?
|
|
182
188
|
active_filters << { label: "Status: #{params[:status].humanize}", param: :status } if params[:status].present?
|
|
183
189
|
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__'
|
|
190
|
+
if params[:assigned_to] == '__unassigned__'
|
|
189
191
|
active_filters << { label: "Unassigned", param: :assigned_to }
|
|
190
192
|
elsif params[:assigned_to] == '__assigned__'
|
|
191
193
|
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
194
|
end
|
|
197
195
|
%>
|
|
198
|
-
|
|
199
196
|
<% 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 %>
|
|
197
|
+
<div style="display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: var(--space-4); font-size: 12px;">
|
|
198
|
+
<span style="color: var(--text-tertiary); font-weight: 500;">Filters:</span>
|
|
199
|
+
<% active_filters.each do |filter| %>
|
|
200
|
+
<%= 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 %>
|
|
201
|
+
<%= filter[:label] %> <i class="bi bi-x" style="font-size: 12px;"></i>
|
|
212
202
|
<% 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>
|
|
255
|
-
<% 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
203
|
<% end %>
|
|
204
|
+
<%= link_to errors_path, style: "color: var(--text-tertiary); font-size: 11px;" do %>Clear all<% end %>
|
|
378
205
|
</div>
|
|
379
|
-
|
|
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>
|
|
206
|
+
<% end %>
|
|
387
207
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<
|
|
208
|
+
<!-- Error Table -->
|
|
209
|
+
<div class="card" style="overflow: hidden;">
|
|
210
|
+
<% if @errors.any? %>
|
|
211
|
+
<div data-loading-target="content">
|
|
212
|
+
<table class="table table-hover" style="margin-bottom: 0;">
|
|
213
|
+
<thead>
|
|
214
|
+
<tr style="border-bottom: 1px solid var(--border-primary);">
|
|
215
|
+
<th style="width: 40px; padding: var(--space-3) var(--space-4); text-align: center;">
|
|
216
|
+
<input type="checkbox" id="select-all" class="form-check-input" style="accent-color: var(--accent);">
|
|
217
|
+
</th>
|
|
218
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: left;">Error</th>
|
|
219
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: left; width: 90px;">Status</th>
|
|
220
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 90px;">Events</th>
|
|
221
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 70px;">Users</th>
|
|
222
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 90px;">Last seen</th>
|
|
223
|
+
<% if @applications.size > 1 && params[:application_id].blank? %>
|
|
224
|
+
<th style="padding: var(--space-3) var(--space-4); width: 90px;">App</th>
|
|
225
|
+
<% end %>
|
|
226
|
+
<% if @platforms.size > 1 %>
|
|
227
|
+
<th style="padding: var(--space-3) var(--space-4); width: 80px;">Platform</th>
|
|
228
|
+
<% end %>
|
|
229
|
+
</tr>
|
|
230
|
+
</thead>
|
|
231
|
+
<tbody id="error_list">
|
|
232
|
+
<% @errors.each do |error| %>
|
|
233
|
+
<%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
|
|
403
234
|
<% end %>
|
|
404
|
-
</
|
|
405
|
-
|
|
406
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selection">
|
|
407
|
-
Clear Selection
|
|
408
|
-
</button>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
<% end %>
|
|
412
|
-
</div>
|
|
413
|
-
|
|
414
|
-
<div class="card-body p-0">
|
|
415
|
-
<% if @errors.any? %>
|
|
416
|
-
<div data-loading-target="content">
|
|
417
|
-
<div class="table-responsive">
|
|
418
|
-
<table class="table table-hover mb-0">
|
|
419
|
-
<thead class="table-light">
|
|
420
|
-
<tr>
|
|
421
|
-
<th style="width: 40px;">
|
|
422
|
-
<input type="checkbox" id="select-all" class="form-check-input">
|
|
423
|
-
</th>
|
|
424
|
-
<th><%= sortable_header("Severity", "severity") %></th>
|
|
425
|
-
<th><%= sortable_header("Error Type", "error_type") %></th>
|
|
426
|
-
<th>Message</th>
|
|
427
|
-
<th><%= sortable_header("Occurrences", "occurrence_count") %></th>
|
|
428
|
-
<th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
|
|
235
|
+
</tbody>
|
|
236
|
+
</table>
|
|
429
237
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
<% if @platforms.size > 1 %>
|
|
436
|
-
<th><%= sortable_header("Platform", "platform") %></th>
|
|
437
|
-
<% 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
|
-
<% end %>
|
|
446
|
-
</tbody>
|
|
447
|
-
</table>
|
|
448
|
-
</div>
|
|
449
|
-
|
|
450
|
-
<!-- Pagination -->
|
|
451
|
-
<div class="p-3">
|
|
452
|
-
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
238
|
+
<!-- Pagination -->
|
|
239
|
+
<% if @pagy.pages > 1 %>
|
|
240
|
+
<div style="padding: var(--space-4); border-top: 1px solid var(--border-primary);">
|
|
241
|
+
<%== @pagy.series_nav(:bootstrap) %>
|
|
453
242
|
</div>
|
|
454
|
-
|
|
243
|
+
<% end %>
|
|
244
|
+
</div>
|
|
455
245
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
<% end %>
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
<% else %>
|
|
464
|
-
<div class="text-center py-5">
|
|
465
|
-
<i class="bi bi-check-circle display-1 text-success mb-3"></i>
|
|
466
|
-
<h4 class="text-muted">All Clear!</h4>
|
|
467
|
-
<p class="text-muted">
|
|
468
|
-
<% if params[:search].present? || params[:error_type].present? || params[:platform].present? || params[:severity].present? %>
|
|
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>
|
|
246
|
+
<div class="loading-skeleton" data-loading-target="skeleton">
|
|
247
|
+
<div style="padding: var(--space-4);">
|
|
248
|
+
<% 5.times do %>
|
|
249
|
+
<div class="skeleton skeleton-row" style="margin-bottom: 2px;"></div>
|
|
478
250
|
<% end %>
|
|
479
251
|
</div>
|
|
480
|
-
|
|
481
|
-
|
|
252
|
+
</div>
|
|
253
|
+
<% else %>
|
|
254
|
+
<!-- Empty State -->
|
|
255
|
+
<div class="red-empty-state">
|
|
256
|
+
<% if params[:search].present? || params[:error_type].present? || params[:severity].present? %>
|
|
257
|
+
<div class="red-empty-state-icon"><i class="bi bi-funnel"></i></div>
|
|
258
|
+
<div class="red-empty-state-title">No errors match your filters</div>
|
|
259
|
+
<div class="red-empty-state-message">Try adjusting your search or filter criteria to find what you're looking for.</div>
|
|
260
|
+
<%= link_to errors_path, class: "red-empty-state-cta" do %><i class="bi bi-x-circle"></i> Clear all filters<% end %>
|
|
261
|
+
<% else %>
|
|
262
|
+
<div class="red-empty-state-icon" style="background: var(--status-success-bg); color: var(--status-success);"><i class="bi bi-check-lg"></i></div>
|
|
263
|
+
<div class="red-empty-state-title">All clear!</div>
|
|
264
|
+
<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>
|
|
265
|
+
<% end %>
|
|
266
|
+
</div>
|
|
267
|
+
<% end %>
|
|
482
268
|
</div>
|
|
483
269
|
</div>
|
|
484
270
|
|
|
485
271
|
<script>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Select all checkbox
|
|
507
|
-
if (selectAllCheckbox) {
|
|
508
|
-
selectAllCheckbox.addEventListener('change', function() {
|
|
509
|
-
errorCheckboxes.forEach(checkbox => {
|
|
510
|
-
checkbox.checked = this.checked;
|
|
511
|
-
});
|
|
512
|
-
updateBatchToolbar();
|
|
513
|
-
});
|
|
272
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
273
|
+
var selectAllCheckbox = document.getElementById('select-all');
|
|
274
|
+
var batchInline = document.getElementById('batch-actions-inline');
|
|
275
|
+
var selectedCountSpan = document.getElementById('selected-count');
|
|
276
|
+
var batchForm = document.getElementById('batch-form');
|
|
277
|
+
|
|
278
|
+
function getErrorCheckboxes() {
|
|
279
|
+
return document.querySelectorAll('.error-checkbox');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function updateBatchToolbar() {
|
|
283
|
+
var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
284
|
+
var count = checkedBoxes.length;
|
|
285
|
+
if (count > 0) {
|
|
286
|
+
batchInline.style.display = 'flex';
|
|
287
|
+
selectedCountSpan.textContent = count + ' selected';
|
|
288
|
+
} else {
|
|
289
|
+
batchInline.style.display = 'none';
|
|
514
290
|
}
|
|
291
|
+
}
|
|
515
292
|
|
|
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);
|
|
524
|
-
|
|
525
|
-
if (selectAllCheckbox) {
|
|
526
|
-
selectAllCheckbox.checked = allChecked;
|
|
527
|
-
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
528
|
-
}
|
|
529
|
-
});
|
|
293
|
+
if (selectAllCheckbox) {
|
|
294
|
+
selectAllCheckbox.addEventListener('change', function() {
|
|
295
|
+
getErrorCheckboxes().forEach(function(cb) { cb.checked = selectAllCheckbox.checked; });
|
|
296
|
+
updateBatchToolbar();
|
|
530
297
|
});
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
}
|
|
556
|
-
|
|
557
|
-
// Add hidden inputs for each selected error ID
|
|
558
|
-
checkedBoxes.forEach(checkbox => {
|
|
559
|
-
const input = document.createElement('input');
|
|
560
|
-
input.type = 'hidden';
|
|
561
|
-
input.name = 'error_ids[]';
|
|
562
|
-
input.value = checkbox.value;
|
|
563
|
-
this.appendChild(input);
|
|
564
|
-
});
|
|
565
|
-
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
document.addEventListener('change', function(e) {
|
|
301
|
+
if (e.target.classList.contains('error-checkbox')) {
|
|
302
|
+
updateBatchToolbar();
|
|
303
|
+
var boxes = getErrorCheckboxes();
|
|
304
|
+
var allChecked = Array.from(boxes).every(function(cb) { return cb.checked; });
|
|
305
|
+
var someChecked = Array.from(boxes).some(function(cb) { return cb.checked; });
|
|
306
|
+
if (selectAllCheckbox) {
|
|
307
|
+
selectAllCheckbox.checked = allChecked;
|
|
308
|
+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
309
|
+
}
|
|
566
310
|
}
|
|
567
311
|
});
|
|
568
312
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
313
|
+
if (batchForm) {
|
|
314
|
+
batchForm.addEventListener('submit', function(e) {
|
|
315
|
+
var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
316
|
+
if (checkedBoxes.length === 0) { e.preventDefault(); return false; }
|
|
317
|
+
checkedBoxes.forEach(function(cb) {
|
|
318
|
+
var input = document.createElement('input');
|
|
319
|
+
input.type = 'hidden'; input.name = 'error_ids[]'; input.value = cb.value;
|
|
320
|
+
batchForm.appendChild(input);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
572
324
|
|
|
573
|
-
|
|
325
|
+
// Turbo Stream animations
|
|
326
|
+
document.addEventListener('turbo:before-stream-render', function(event) {
|
|
327
|
+
var target = event.detail.newStream.target;
|
|
328
|
+
var action = event.detail.newStream.action;
|
|
574
329
|
if (action === 'prepend' && target === 'error_list') {
|
|
575
|
-
setTimeout(()
|
|
576
|
-
|
|
330
|
+
setTimeout(function() {
|
|
331
|
+
var firstRow = document.querySelector('#error_list tr:first-child');
|
|
577
332
|
if (firstRow) {
|
|
578
|
-
firstRow.
|
|
579
|
-
|
|
580
|
-
// Remove class after animation completes
|
|
581
|
-
setTimeout(() => {
|
|
582
|
-
firstRow.classList.remove('new-error');
|
|
583
|
-
}, 3000);
|
|
333
|
+
firstRow.style.background = 'var(--status-success-bg)';
|
|
334
|
+
setTimeout(function() { firstRow.style.background = ''; }, 3000);
|
|
584
335
|
}
|
|
585
336
|
}, 10);
|
|
586
337
|
}
|
|
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
338
|
});
|
|
599
339
|
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
340
|
+
// Scroll position preservation
|
|
341
|
+
var scrollPos = sessionStorage.getItem('scrollPos');
|
|
342
|
+
if (scrollPos) { window.scrollTo(0, parseInt(scrollPos)); sessionStorage.removeItem('scrollPos'); }
|
|
343
|
+
|
|
344
|
+
var filterForm = document.getElementById('filter-form');
|
|
345
|
+
if (filterForm) {
|
|
346
|
+
filterForm.addEventListener('submit', function() {
|
|
347
|
+
sessionStorage.setItem('scrollPos', window.pageYOffset);
|
|
348
|
+
var unresolvedCb = document.getElementById('unresolved_checkbox');
|
|
349
|
+
if (unresolvedCb && !unresolvedCb.checked) {
|
|
350
|
+
var hidden = document.createElement('input');
|
|
351
|
+
hidden.type = 'hidden'; hidden.name = 'unresolved'; hidden.value = '0';
|
|
352
|
+
filterForm.appendChild(hidden);
|
|
353
|
+
unresolvedCb.disabled = true;
|
|
354
|
+
}
|
|
605
355
|
});
|
|
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
|
-
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Tab title with unresolved count
|
|
359
|
+
var unresolvedCount = <%= @stats[:unresolved] || 0 %>;
|
|
360
|
+
if (unresolvedCount > 0) { document.title = '(' + unresolvedCount + ') ' + document.title; }
|
|
361
|
+
|
|
362
|
+
// Toggle assignee name filter visibility
|
|
363
|
+
var assignedToFilter = document.getElementById('assigned_to_filter');
|
|
364
|
+
var assigneeNameFilter = document.getElementById('assignee_name_filter');
|
|
365
|
+
if (assignedToFilter && assigneeNameFilter) {
|
|
366
|
+
assignedToFilter.addEventListener('change', function() {
|
|
367
|
+
assigneeNameFilter.style.display = this.value === '__assigned__' ? '' : 'none';
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
699
371
|
</script>
|