rails_error_dashboard 0.6.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa8243a13e9eccf5cedcffea0600d19a904e7dbba1b372dd3c633fece3ab6674
4
- data.tar.gz: 0aa65c2bfd6038ebd7d35901d8889a4e2ffc07bf653746a43650d7a9b6685257
3
+ metadata.gz: 8730984999d2ab508eeefe889bfd320e54cb9395c6baacbe87fef1a3aa20de7a
4
+ data.tar.gz: 6a301193d907d990418f6a74b83ee36a54caeac1b05dc57a75ea24213e9b7f7c
5
5
  SHA512:
6
- metadata.gz: c85d90b5c8e36466306e9bda5466b8cce46e8dae335c218d946cfde374d1243835d5ec3eae9457fb75f80dfe48a339e71cef7f1b0439d2ccc1419a123e8fab94
7
- data.tar.gz: 016bdd5a313427c5b07c72a1808262514e74159fb60761ff6903c6d64dc7c1127032df1f8c6ea5da64f858322be6ed734e249a3026a9d4b7dd5a0b8e50239628
6
+ metadata.gz: af05b90ebe19d1b90adb82b7e8d3a4cd57faf695327c3a51c2d36ced221d1951265e4d88d77e4ddb843c465582fe14d6911141e3f081b2de76ca3037a4e637f0
7
+ data.tar.gz: add93453ac6ebee6a3f320fc336fe30c81a952f58fc30b3b8554c410071586da7d9c25da1d886c5976b4ade28cc6504c5b355509a29dcf5025aa8b502d1fff65
@@ -23,23 +23,27 @@ module RailsErrorDashboard
23
23
  Rails.logger.error("Params: #{params.inspect}")
24
24
  Rails.logger.error(exception.backtrace&.first(10)&.join("\n")) if exception.backtrace
25
25
 
26
- # Render user-friendly error page
27
- render plain: "The Error Dashboard encountered an issue displaying this page.\n\n" \
28
- "Your application is unaffected - this is only a dashboard display error.\n\n" \
29
- "Error: #{exception.message}\n\n" \
30
- "Check Rails logs for details: [RailsErrorDashboard]",
31
- status: :internal_server_error,
32
- layout: false
26
+ render_dashboard_error(
27
+ icon: "bi-exclamation-triangle",
28
+ icon_style: "background: var(--status-warning-bg); color: var(--status-warning);",
29
+ title: "Something went wrong",
30
+ message: "The Error Dashboard encountered an issue displaying this page. Your application is unaffected.",
31
+ detail: exception.message,
32
+ status: :internal_server_error
33
+ )
33
34
  end
34
35
 
35
36
  # Handle record not found — return 404 instead of 500
36
37
  rescue_from ActiveRecord::RecordNotFound do |exception|
37
38
  Rails.logger.warn("[RailsErrorDashboard] Record not found: #{exception.message}")
38
- render plain: "The requested error was not found.\n\n" \
39
- "It may have been deleted or the ID is invalid.\n\n" \
40
- "Error: #{exception.message}",
41
- status: :not_found,
42
- layout: false
39
+
40
+ render_dashboard_error(
41
+ icon: "bi-search",
42
+ title: "The requested error was not found",
43
+ message: "It may have been deleted or the ID is invalid.",
44
+ detail: exception.message,
45
+ status: :not_found
46
+ )
43
47
  end
44
48
 
45
49
  # Handle Pagy pagination errors — redirect to page 1
@@ -47,5 +51,26 @@ module RailsErrorDashboard
47
51
  Rails.logger.warn("[RailsErrorDashboard] Pagination error: #{exception.message}")
48
52
  redirect_to request.path, status: :moved_permanently
49
53
  end
54
+
55
+ private
56
+
57
+ def render_dashboard_error(icon:, title:, message:, detail: nil, icon_style: nil, status: :internal_server_error)
58
+ set_common_view_variables
59
+ error_html = <<~ERB
60
+ <div class="red-empty-state" style="margin-top: var(--space-6);">
61
+ <div class="red-empty-state-icon"#{icon_style ? " style=\"#{icon_style}\"" : ""}><i class="bi #{icon}"></i></div>
62
+ <div class="red-empty-state-title">#{ERB::Util.html_escape(title)}</div>
63
+ <div class="red-empty-state-message">#{ERB::Util.html_escape(message)}</div>
64
+ #{"<div style=\"font-size: 12px; color: var(--text-tertiary); margin-top: var(--space-2); font-family: var(--font-mono);\">" + ERB::Util.html_escape(detail) + "</div>" if detail}
65
+ <a href="#{errors_path}" class="red-empty-state-cta" style="margin-top: var(--space-4);"><i class="bi bi-arrow-left"></i> Back to errors</a>
66
+ </div>
67
+ ERB
68
+ render html: error_html.html_safe, status: status, layout: "rails_error_dashboard"
69
+ end
70
+
71
+ def set_common_view_variables
72
+ @applications = Application.ordered_by_name.pluck(:name, :id) rescue []
73
+ @default_credentials_warning = RailsErrorDashboard.configuration.default_credentials? rescue false
74
+ end
50
75
  end
51
76
  end
@@ -560,6 +560,23 @@ module RailsErrorDashboard
560
560
  redirect_back fallback_location: errors_path
561
561
  end
562
562
 
563
+ def test_error
564
+ exception = RailsErrorDashboard::TestError.new(
565
+ "[RED Test] This is a test error sent from the dashboard to verify " \
566
+ "that error capture and notification delivery are working correctly. " \
567
+ "It is safe to resolve or delete this error."
568
+ )
569
+ exception.set_backtrace(caller)
570
+
571
+ Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
572
+
573
+ flash[:notice] = "Test error logged successfully. Check your notification channels (Slack, Discord, email, etc.) to confirm delivery."
574
+ redirect_to errors_path(**app_context_params)
575
+ rescue => e
576
+ flash[:alert] = "Failed to log test error: #{e.message}"
577
+ redirect_to settings_path(**app_context_params)
578
+ end
579
+
563
580
  def settings
564
581
  @config = RailsErrorDashboard.configuration
565
582
  end
@@ -135,9 +135,9 @@
135
135
  --surface-secondary: #313244;
136
136
  --surface-tertiary: #45475a;
137
137
  --surface-hover: #313244;
138
- --text-primary: #cdd6f4;
139
- --text-secondary: #a6adc8;
140
- --text-tertiary: #6c7086;
138
+ --text-primary: #ffffff;
139
+ --text-secondary: #cdd6f4;
140
+ --text-tertiary: #a6adc8;
141
141
  --border-primary: #313244;
142
142
  --border-secondary: #45475a;
143
143
  --accent: <%= ac[:dark] %>;
@@ -279,17 +279,21 @@ dd { color: var(--text-primary); }
279
279
  .ps-3 { padding-left: var(--space-4); }
280
280
  .pe-2 { padding-right: var(--space-2); }
281
281
 
282
- /* Display */
283
- .d-none { display: none; }
284
- .d-block { display: block; }
285
- .d-inline { display: inline; }
286
- .d-inline-block { display: inline-block; }
287
- @media (min-width: 576px) { .d-sm-inline { display: inline; } .d-sm-block { display: block; } }
288
- @media (min-width: 768px) { .d-md-block { display: block; } .d-md-none { display: none; } .d-md-flex { display: flex; } .d-md-inline { display: inline; } .ms-sm-auto { margin-left: auto; } .px-md-4 { padding-left: var(--space-6); padding-right: var(--space-6); } }
282
+ /* Display (use !important to match Bootstrap utility behavior) */
283
+ .d-none { display: none !important; }
284
+ .d-block { display: block !important; }
285
+ .d-inline { display: inline !important; }
286
+ .d-inline-block { display: inline-block !important; }
287
+ @media (min-width: 576px) { .d-sm-inline { display: inline !important; } .d-sm-block { display: block !important; } }
288
+ @media (min-width: 768px) { .d-md-block { display: block !important; } .d-md-none { display: none !important; } .d-md-flex { display: flex !important; } .d-md-inline { display: inline !important; } .ms-sm-auto { margin-left: auto; } .px-md-4 { padding-left: var(--space-6); padding-right: var(--space-6); } }
289
289
 
290
290
  /* Text */
291
- .text-muted { color: var(--text-tertiary); }
291
+ .text-muted { color: var(--text-secondary); }
292
292
  .text-secondary { color: var(--text-secondary); }
293
+
294
+ /* Bootstrap compat: table-light thead */
295
+ .table-light, thead.table-light { background: var(--surface-secondary); }
296
+ .table-light th { color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
293
297
  .text-center { text-align: center; }
294
298
  .text-end { text-align: right; }
295
299
  .text-start { text-align: left; }
@@ -632,6 +636,10 @@ dd { color: var(--text-primary); }
632
636
  .red-sidebar.collapsed { display: none; }
633
637
  }
634
638
 
639
+ /* Error detail responsive grid */
640
+ .red-detail-grid { display: grid; grid-template-columns: 1fr 260px; gap: var(--space-4); }
641
+ @media (max-width: 1023.98px) { .red-detail-grid { grid-template-columns: 1fr; } }
642
+
635
643
  /* Env badge */
636
644
  .red-env-badge {
637
645
  padding: 3px 10px; font-size: 11px; font-weight: 600;
@@ -884,7 +892,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
884
892
  <!-- Sidebar -->
885
893
  <nav class="red-sidebar d-none d-md-flex" id="sidebar">
886
894
  <!-- Logo -->
887
- <div class="red-sidebar-logo">
895
+ <a href="/" class="red-sidebar-logo" title="Back to <%= Rails.application.class.module_parent_name %>" style="text-decoration: none; color: inherit;">
888
896
  <div class="red-sidebar-logo-icon">R</div>
889
897
  <div>
890
898
  <div style="font-size: 14px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em;">RED</div>
@@ -893,7 +901,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
893
901
  end %>
894
902
  <div style="font-size: 10px; color: var(--text-tertiary); margin-top: -2px;"><%= sidebar_app_name || Rails.application.class.module_parent_name %> &middot; <%= Rails.env %></div>
895
903
  </div>
896
- </div>
904
+ </a>
897
905
 
898
906
  <!-- Nav groups -->
899
907
  <div style="flex: 1; overflow: auto; padding: var(--space-3) 0;">
@@ -1062,7 +1070,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1062
1070
  <% if params[:application_id].present? %>
1063
1071
  <%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
1064
1072
  <% else %>
1065
- All Apps
1073
+ All Apps (<%= @applications.size %>)
1066
1074
  <% end %>
1067
1075
  </button>
1068
1076
  <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
@@ -1094,7 +1102,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1094
1102
  </ul>
1095
1103
  </div>
1096
1104
  <% end %>
1097
- <kbd>?</kbd>
1105
+ <button type="button" title="Keyboard shortcuts (?)" data-bs-toggle="modal" data-bs-target="#keyboardShortcutsModal" style="background: none; border: none; padding: 0; cursor: pointer; line-height: 1;"><kbd>?</kbd></button>
1098
1106
  <button class="red-theme-toggle" id="themeToggle" title="Toggle theme">
1099
1107
  <i class="bi bi-moon" id="themeIcon"></i>
1100
1108
  </button>
@@ -1105,14 +1113,16 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1105
1113
  <!-- Main content -->
1106
1114
  <main style="flex: 1; padding: var(--space-6) var(--space-8); max-width: 1200px; width: 100%; margin: 0 auto;">
1107
1115
  <% if @default_credentials_warning %>
1108
- <div class="alert alert-warning" style="display: flex; align-items: center; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid var(--status-warning);">
1109
- <i class="bi bi-exclamation-triangle-fill" style="font-size: 18px;"></i>
1110
- <div>
1116
+ <div id="security-warning" class="alert alert-warning" style="display: flex; align-items: center; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid var(--status-warning);">
1117
+ <i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; flex-shrink: 0;"></i>
1118
+ <div style="flex: 1;">
1111
1119
  <strong>Security Warning:</strong> You are using default credentials (gandalf/youshallnotpass).
1112
1120
  Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
1113
1121
  or configure <code>authenticate_with</code> in your initializer.
1114
1122
  </div>
1123
+ <button type="button" onclick="this.parentElement.style.display='none'; try { sessionStorage.setItem('red_dismiss_creds_warning','1') } catch(e) {}" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0 4px; flex-shrink: 0; line-height: 1;" title="Dismiss for this session">&times;</button>
1115
1124
  </div>
1125
+ <script>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
1116
1126
  <% end %>
1117
1127
  <%= yield %>
1118
1128
  </main>
@@ -1124,7 +1134,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1124
1134
  Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank"><strong>RED</strong> &mdash; Rails Error Dashboard</a>
1125
1135
  </p>
1126
1136
  <p style="font-size: 12px; color: var(--text-tertiary); margin: 0;">
1127
- Created by <a href="https://www.anjan.dev/" target="_blank">Anjan Jagirdar</a>
1137
+ v<%= RailsErrorDashboard::VERSION %> &middot; Built with ❤️ by <a href="https://anjan.dev" target="_blank">Anjan Jagirdar</a>
1128
1138
  </p>
1129
1139
  </footer>
1130
1140
  </div>
@@ -1133,10 +1143,10 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1133
1143
  <!-- Mobile Sidebar (Offcanvas) -->
1134
1144
  <div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
1135
1145
  <div class="offcanvas-header">
1136
- <div style="display: flex; align-items: center; gap: 10px;">
1146
+ <a href="/" style="display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit;" title="Back to app home">
1137
1147
  <div class="red-sidebar-logo-icon">R</div>
1138
1148
  <strong>RED</strong>
1139
- </div>
1149
+ </a>
1140
1150
  <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1141
1151
  </div>
1142
1152
  <div class="offcanvas-body">
@@ -20,7 +20,7 @@
20
20
  <span style="display: inline-block; padding: 1px 6px; font-size: 9px; font-weight: 600; border-radius: var(--radius-full); background: var(--status-success-bg); color: var(--status-success); margin-left: 4px; text-transform: uppercase;">NEW</span>
21
21
  <% end %>
22
22
  </div>
23
- <div style="color: var(--text-tertiary); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 400px;" title="<%= error.message %>"><%= error.message %></div>
23
+ <div style="color: var(--text-tertiary); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<%= error.message %>"><%= error.message %></div>
24
24
  </div>
25
25
  </div>
26
26
  </td>
@@ -72,12 +72,12 @@
72
72
  <%= local_time_ago(error.last_seen_at) %>
73
73
  </td>
74
74
  <% if local_assigns[:show_application] %>
75
- <td style="padding: var(--space-3) var(--space-4); font-size: 12px;" onclick="window.location='<%= error_path(error, **app_context) %>'">
75
+ <td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.application&.name || 'Unknown' %>" onclick="window.location='<%= error_path(error, **app_context) %>'">
76
76
  <span class="badge bg-info"><%= error.application&.name || 'Unknown' %></span>
77
77
  </td>
78
78
  <% end %>
79
79
  <% if local_assigns[:show_platform] %>
80
- <td style="padding: var(--space-3) var(--space-4); font-size: 12px;" onclick="window.location='<%= error_path(error, **app_context) %>'">
80
+ <td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.platform || 'API' %>" onclick="window.location='<%= error_path(error, **app_context) %>'">
81
81
  <% if error.platform == 'iOS' %>
82
82
  <span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
83
83
  <% elsif error.platform == 'Android' %>
@@ -87,11 +87,11 @@
87
87
  </div>
88
88
 
89
89
  <% if error.resolved? && error.resolution_comment.present? %>
90
- <div class="mt-2 p-2 bg-success bg-opacity-10 border border-success rounded">
91
- <small class="text-success fw-bold">
90
+ <div style="margin-top: var(--space-2); padding: var(--space-2) var(--space-3); background: var(--status-success-bg); border: 1px solid var(--status-success); border-radius: var(--radius-sm);">
91
+ <small style="color: var(--status-success); font-weight: 600;">
92
92
  <i class="bi bi-journal-text"></i> Resolution Notes:
93
93
  </small>
94
- <div class="mb-0 small text-muted mt-1">
94
+ <div style="font-size: 13px; color: var(--text-primary); margin-top: 4px;">
95
95
  <%= auto_link_urls(truncate(error.resolution_comment, length: 200), error: error).html_safe %>
96
96
  </div>
97
97
  </div>
@@ -40,20 +40,6 @@
40
40
  All time
41
41
  <% end %>
42
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 %>
56
- </div>
57
43
  </div>
58
44
 
59
45
  <!-- Spike Detection Alert -->
@@ -126,7 +112,7 @@
126
112
  </div>
127
113
 
128
114
  <!-- 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);">
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);">
130
116
  <% if @applications.size > 1 %>
131
117
  <%= select_tag :application_id, options_for_select([['All Apps', '']] + @applications, params[:application_id]), class: "form-select", style: "width: auto; min-width: 120px;" %>
132
118
  <% end %>
@@ -209,24 +195,44 @@
209
195
  <div class="card" style="overflow: hidden;">
210
196
  <% if @errors.any? %>
211
197
  <div data-loading-target="content">
212
- <table class="table table-hover" style="margin-bottom: 0;">
198
+ <div style="overflow-x: auto;">
199
+ <table class="table table-hover" style="margin-bottom: 0; width: 100%; table-layout: fixed;">
213
200
  <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;">
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;">
216
203
  <input type="checkbox" id="select-all" class="form-check-input" style="accent-color: var(--accent);">
217
204
  </th>
218
205
  <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>
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>
223
210
  <% if @applications.size > 1 && params[:application_id].blank? %>
224
- <th style="padding: var(--space-3) var(--space-4); width: 90px;">App</th>
211
+ <th style="padding: var(--space-3) var(--space-4); width: 100px;">App</th>
225
212
  <% end %>
226
213
  <% if @platforms.size > 1 %>
227
- <th style="padding: var(--space-3) var(--space-4); width: 80px;">Platform</th>
214
+ <th style="padding: var(--space-3) var(--space-4); width: 100px;">Platform</th>
228
215
  <% end %>
229
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] %>
225
+ <% end %>
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
232
+ <% end %>
233
+ <% end %>
234
+ </th>
235
+ </tr>
230
236
  </thead>
231
237
  <tbody id="error_list">
232
238
  <% @errors.each do |error| %>
@@ -234,11 +240,13 @@
234
240
  <% end %>
235
241
  </tbody>
236
242
  </table>
243
+ </div>
237
244
 
238
245
  <!-- Pagination -->
239
246
  <% if @pagy.pages > 1 %>
240
- <div style="padding: var(--space-4); border-top: 1px solid var(--border-primary);">
241
- <%== @pagy.series_nav(:bootstrap) %>
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>
242
250
  </div>
243
251
  <% end %>
244
252
  </div>
@@ -271,7 +279,9 @@
271
279
  <script>
272
280
  document.addEventListener('DOMContentLoaded', function() {
273
281
  var selectAllCheckbox = document.getElementById('select-all');
274
- var batchInline = document.getElementById('batch-actions-inline');
282
+ var selectAllBatchCheckbox = document.getElementById('select-all-batch');
283
+ var theadColumns = document.getElementById('thead-columns');
284
+ var theadBatch = document.getElementById('thead-batch');
275
285
  var selectedCountSpan = document.getElementById('selected-count');
276
286
  var batchForm = document.getElementById('batch-form');
277
287
 
@@ -282,31 +292,47 @@ document.addEventListener('DOMContentLoaded', function() {
282
292
  function updateBatchToolbar() {
283
293
  var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
284
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
+
285
299
  if (count > 0) {
286
- batchInline.style.display = 'flex';
300
+ theadColumns.style.display = 'none';
301
+ theadBatch.style.display = '';
287
302
  selectedCountSpan.textContent = count + ' selected';
303
+ selectAllBatchCheckbox.checked = allChecked;
304
+ selectAllBatchCheckbox.indeterminate = someChecked && !allChecked;
288
305
  } else {
289
- batchInline.style.display = 'none';
306
+ theadColumns.style.display = '';
307
+ theadBatch.style.display = 'none';
308
+ }
309
+
310
+ if (selectAllCheckbox) {
311
+ selectAllCheckbox.checked = allChecked;
312
+ selectAllCheckbox.indeterminate = someChecked && !allChecked;
290
313
  }
291
314
  }
292
315
 
316
+ function handleSelectAll(checked) {
317
+ getErrorCheckboxes().forEach(function(cb) { cb.checked = checked; });
318
+ updateBatchToolbar();
319
+ }
320
+
293
321
  if (selectAllCheckbox) {
294
322
  selectAllCheckbox.addEventListener('change', function() {
295
- getErrorCheckboxes().forEach(function(cb) { cb.checked = selectAllCheckbox.checked; });
296
- updateBatchToolbar();
323
+ handleSelectAll(selectAllCheckbox.checked);
324
+ });
325
+ }
326
+
327
+ if (selectAllBatchCheckbox) {
328
+ selectAllBatchCheckbox.addEventListener('change', function() {
329
+ handleSelectAll(selectAllBatchCheckbox.checked);
297
330
  });
298
331
  }
299
332
 
300
333
  document.addEventListener('change', function(e) {
301
334
  if (e.target.classList.contains('error-checkbox')) {
302
335
  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
- }
310
336
  }
311
337
  });
312
338
 
@@ -169,7 +169,10 @@
169
169
  %>
170
170
 
171
171
  <div>
172
- <h1 style="font-size: 20px; font-weight: 700; margin-bottom: var(--space-4);">Settings</h1>
172
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4);">
173
+ <h1 style="font-size: 20px; font-weight: 700; margin: 0;">Settings</h1>
174
+ <span style="font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--text-tertiary); padding: 2px 10px; border: 1px solid var(--border-primary); border-radius: var(--radius-full);">v<%= RailsErrorDashboard::VERSION %></span>
175
+ </div>
173
176
 
174
177
  <p style="font-size: 13px; color: var(--text-tertiary); margin-bottom: var(--space-6);">
175
178
  Read-only view of current configuration. Edit via <code>config/initializers/rails_error_dashboard.rb</code> or environment variables.
@@ -255,6 +258,25 @@
255
258
  </div>
256
259
  </div>
257
260
 
261
+ <!-- Test Error -->
262
+ <div class="card" style="margin-bottom: var(--space-4);">
263
+ <div style="padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
264
+ <span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="bi bi-send-check" style="margin-right: 6px;"></i> Test Notifications</span>
265
+ </div>
266
+ <div style="padding: var(--space-5) var(--space-6);">
267
+ <p style="font-size: 13px; color: var(--text-secondary); margin-bottom: var(--space-4);">
268
+ Send a test error through the full capture and notification pipeline. This creates a <code>RailsErrorDashboard::TestError</code> in the
269
+ error log and dispatches notifications to all configured channels (Slack, Discord, PagerDuty, email, webhooks).
270
+ The test error is clearly marked and safe to resolve or delete.
271
+ </p>
272
+ <%= form_tag test_error_errors_path(application_id: @current_application_id.presence), method: :post, style: "display: inline;" do %>
273
+ <button type="submit" class="btn btn-outline" onclick="return confirm('This will create a test error and send notifications to all configured channels. Continue?')">
274
+ <i class="bi bi-send" style="margin-right: 4px;"></i> Send Test Error
275
+ </button>
276
+ <% end %>
277
+ </div>
278
+ </div>
279
+
258
280
  <!-- Help Text -->
259
281
  <div class="alert alert-light border">
260
282
  <h6 class="alert-heading">
@@ -87,7 +87,7 @@
87
87
  </div>
88
88
 
89
89
  <!-- Main content: tabs + sidebar -->
90
- <div style="display: grid; grid-template-columns: 1fr 280px; gap: var(--space-4);">
90
+ <div class="red-detail-grid">
91
91
  <!-- Left: tabbed content -->
92
92
  <div>
93
93
  <!-- Tab bar -->
@@ -162,7 +162,7 @@
162
162
  </div>
163
163
 
164
164
  <!-- Right: sidebar -->
165
- <div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0;">
165
+ <div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; overflow-wrap: break-word;">
166
166
  <%= render "sidebar_metadata", error: @error, related_errors: @related_errors %>
167
167
 
168
168
  <!-- Baseline Statistics -->
@@ -79,23 +79,23 @@
79
79
  <div class="card-body p-0">
80
80
  <div class="table-responsive">
81
81
  <table class="table table-hover mb-0">
82
- <thead class="table-light">
83
- <tr>
84
- <th>Exception Class</th>
85
- <th width="250">Raise Location</th>
86
- <th width="250">Rescue Location</th>
87
- <th width="100">Raises</th>
88
- <th width="100">Rescues</th>
89
- <th width="100">Ratio</th>
90
- <th width="140">Last Seen</th>
82
+ <thead>
83
+ <tr style="border-bottom: 1px solid var(--border-primary);">
84
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;">Exception Class</th>
85
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="250">Raise Location</th>
86
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="250">Rescue Location</th>
87
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="100">Raises</th>
88
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="100">Rescues</th>
89
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="100">Ratio</th>
90
+ <th style="padding: var(--space-3) var(--space-4); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;" width="140">Last Seen</th>
91
91
  </tr>
92
92
  </thead>
93
93
  <tbody>
94
94
  <% @entries.each do |entry| %>
95
95
  <tr>
96
96
  <td><code style="font-size: 0.85em;"><%= entry[:exception_class] %></code></td>
97
- <td><small class="text-muted text-break"><%= entry[:raise_location] %></small></td>
98
- <td><small class="text-muted text-break"><%= entry[:rescue_location] || "Unknown" %></small></td>
97
+ <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:raise_location] %></td>
98
+ <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:rescue_location] || "Unknown" %></td>
99
99
  <td><span class="badge bg-info text-dark"><%= number_with_delimiter(entry[:raise_count]) %></span></td>
100
100
  <td><span class="badge bg-warning text-dark"><%= number_with_delimiter(entry[:rescue_count]) %></span></td>
101
101
  <td>
@@ -104,7 +104,7 @@
104
104
  <%= ratio_pct %>%
105
105
  </span>
106
106
  </td>
107
- <td><%= local_time_ago(entry[:last_seen]) %></td>
107
+ <td style="color: var(--text-secondary); font-size: 12px;"><%= local_time_ago(entry[:last_seen]) %></td>
108
108
  </tr>
109
109
  <% end %>
110
110
  </tbody>
data/config/routes.rb CHANGED
@@ -44,6 +44,7 @@ RailsErrorDashboard::Engine.routes.draw do
44
44
  post :enable_coverage
45
45
  post :disable_coverage
46
46
  post :batch_action
47
+ post :test_error
47
48
  end
48
49
  end
49
50
  end
@@ -113,7 +113,7 @@ module RailsErrorDashboard
113
113
  server.pubsub
114
114
  @broadcast_unavailable_until = nil
115
115
  true
116
- rescue => e
116
+ rescue LoadError, StandardError => e
117
117
  @broadcast_unavailable_until = Time.current + 60
118
118
  RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Broadcast not available (pausing 60s): #{e.message}")
119
119
  false
@@ -195,7 +195,7 @@ module RailsErrorDashboard
195
195
  connections: server.connections.count,
196
196
  adapter: server.pubsub&.class&.name&.demodulize
197
197
  }
198
- rescue => e
198
+ rescue LoadError, StandardError => e
199
199
  nil
200
200
  end
201
201
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Custom exception class for test errors triggered from the dashboard.
5
+ # Clearly identifiable in error lists so users know it's not a real issue.
6
+ class TestError < StandardError; end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "rails_error_dashboard/version"
2
2
  require "rails_error_dashboard/engine"
3
3
  require "rails_error_dashboard/configuration_error"
4
+ require "rails_error_dashboard/test_error"
4
5
  require "rails_error_dashboard/configuration"
5
6
  require "rails_error_dashboard/logger"
6
7
  require "rails_error_dashboard/manual_error_reporter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -480,6 +480,7 @@ files:
480
480
  - lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb
481
481
  - lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb
482
482
  - lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb
483
+ - lib/rails_error_dashboard/test_error.rb
483
484
  - lib/rails_error_dashboard/value_objects/error_context.rb
484
485
  - lib/rails_error_dashboard/version.rb
485
486
  - lib/tasks/error_dashboard.rake
@@ -496,7 +497,7 @@ metadata:
496
497
  funding_uri: https://github.com/sponsors/AnjanJ
497
498
  post_install_message: |
498
499
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
499
- RED (Rails Error Dashboard) v0.6.0
500
+ RED (Rails Error Dashboard) v0.6.1
500
501
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
501
502
 
502
503
  First install: