rails_error_dashboard 0.6.0 → 0.6.2
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 +22 -2
- data/app/helpers/rails_error_dashboard/application_helper.rb +27 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +152 -33
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +13 -15
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +6 -6
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +3 -3
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +8 -8
- data/app/views/rails_error_dashboard/errors/index.html.erb +66 -43
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/settings.html.erb +24 -2
- data/app/views/rails_error_dashboard/errors/show.html.erb +12 -12
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +12 -12
- data/config/routes.rb +1 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d065c1edba3f66205914ba82dbfc5aed936e0df62fd96df8f1dcd1667bc119af
|
|
4
|
+
data.tar.gz: dff67e2453ec7d33429b5a4c07754b1e7d9c3103946cd5fdcb6ed282a3a14bce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14b55055cb0a3c765824778c82cb7d8f33cd2d4ad5c445ea2e568fc10c51aec611271e0610fa295c8318a4701bba6a051db5f313ee6f12b4d6ac04be331df85f
|
|
7
|
+
data.tar.gz: 0c154f205b6622ad747e3e037864b0e2bc2078f72098f71c2b26fb42d717a1b09a275e87ec29f2e34864684505b66b169f96efe8cc13910dc6a9f4c58fca5a4c
|
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
@@ -60,8 +60,11 @@ module RailsErrorDashboard
|
|
|
60
60
|
# Use Query to get filtered errors
|
|
61
61
|
errors_query = Queries::ErrorsList.call(filter_params)
|
|
62
62
|
|
|
63
|
-
# Paginate with Pagy
|
|
64
|
-
|
|
63
|
+
# Paginate with Pagy. raise_range_error makes pagy 43.x raise Pagy::RangeError
|
|
64
|
+
# on out-of-range pages (e.g. ?page=999999) so application_controller.rb's
|
|
65
|
+
# rescue_from can redirect to a valid page. Without this, pagy 43.x silently
|
|
66
|
+
# returns an empty result and users see "All clear!" instead of their data.
|
|
67
|
+
@pagy, @errors = pagy(:offset, errors_query, limit: params[:per_page] || 25, raise_range_error: true)
|
|
65
68
|
|
|
66
69
|
# Get dashboard stats using Query (pass application filter)
|
|
67
70
|
@stats = Queries::DashboardStats.call(application_id: @current_application_id)
|
|
@@ -560,6 +563,23 @@ module RailsErrorDashboard
|
|
|
560
563
|
redirect_back fallback_location: errors_path
|
|
561
564
|
end
|
|
562
565
|
|
|
566
|
+
def test_error
|
|
567
|
+
exception = RailsErrorDashboard::TestError.new(
|
|
568
|
+
"[RED Test] This is a test error sent from the dashboard to verify " \
|
|
569
|
+
"that error capture and notification delivery are working correctly. " \
|
|
570
|
+
"It is safe to resolve or delete this error."
|
|
571
|
+
)
|
|
572
|
+
exception.set_backtrace(caller)
|
|
573
|
+
|
|
574
|
+
Commands::LogError.call(exception, { request: request, source: "dashboard.test_error" })
|
|
575
|
+
|
|
576
|
+
flash[:notice] = "Test error logged successfully. Check your notification channels (Slack, Discord, email, etc.) to confirm delivery."
|
|
577
|
+
redirect_to errors_path(**app_context_params)
|
|
578
|
+
rescue => e
|
|
579
|
+
flash[:alert] = "Failed to log test error: #{e.message}"
|
|
580
|
+
redirect_to settings_path(**app_context_params)
|
|
581
|
+
end
|
|
582
|
+
|
|
563
583
|
def settings
|
|
564
584
|
@config = RailsErrorDashboard.configuration
|
|
565
585
|
end
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
module RailsErrorDashboard
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
# Returns the host app's CSP nonce (if any) so inline <script> tags pass strict CSP.
|
|
4
|
+
# Falls back to nil when the host has no CSP configured — in that case the script tag
|
|
5
|
+
# works without a nonce attribute. Strict CSPs (script-src 'self' 'nonce-...') require
|
|
6
|
+
# this; without it the script is blocked.
|
|
7
|
+
def red_csp_nonce
|
|
8
|
+
return nil unless respond_to?(:content_security_policy_nonce)
|
|
9
|
+
content_security_policy_nonce
|
|
10
|
+
rescue StandardError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Wraps an inline <script> block with the host app's CSP nonce when available.
|
|
15
|
+
# Use everywhere we have <script>...</script> in our views so they pass strict CSP.
|
|
16
|
+
#
|
|
17
|
+
# <%= red_javascript_tag do %>
|
|
18
|
+
# console.log('hi');
|
|
19
|
+
# <% end %>
|
|
20
|
+
def red_javascript_tag(&block)
|
|
21
|
+
nonce = red_csp_nonce
|
|
22
|
+
content = capture(&block)
|
|
23
|
+
if nonce
|
|
24
|
+
content_tag(:script, content.html_safe, nonce: nonce)
|
|
25
|
+
else
|
|
26
|
+
content_tag(:script, content.html_safe)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
3
30
|
# Returns Bootstrap color class for error severity
|
|
4
31
|
# Uses Catppuccin Mocha colors in dark theme via CSS variables
|
|
5
32
|
# @param severity [Symbol] The severity level (:critical, :high, :medium, :low, :info)
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<% end %>
|
|
40
40
|
|
|
41
41
|
<!-- Early theme detection (prevents flash of wrong theme) -->
|
|
42
|
-
<script
|
|
42
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
43
43
|
(function(){
|
|
44
44
|
var t = localStorage.getItem('red-theme');
|
|
45
45
|
if (t === 'dark' || (!t && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
@@ -135,9 +135,9 @@
|
|
|
135
135
|
--surface-secondary: #313244;
|
|
136
136
|
--surface-tertiary: #45475a;
|
|
137
137
|
--surface-hover: #313244;
|
|
138
|
-
--text-primary: #
|
|
139
|
-
--text-secondary: #
|
|
140
|
-
--text-tertiary: #
|
|
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-
|
|
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;
|
|
@@ -872,6 +880,12 @@ main { animation: contentFadeIn 0.15s ease; }
|
|
|
872
880
|
details { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); }
|
|
873
881
|
summary { padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: 500; color: var(--text-primary); background: var(--surface-hover); }
|
|
874
882
|
details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
883
|
+
|
|
884
|
+
/* Error row hover (replaces inline onmouseenter/onmouseleave) */
|
|
885
|
+
tr[data-red-row-href] { transition: background 0.1s ease; }
|
|
886
|
+
tr[data-red-row-href]:hover { background: var(--surface-hover); }
|
|
887
|
+
tr[data-red-row-href] .sev-bar { transition: opacity 0.1s ease; }
|
|
888
|
+
tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
875
889
|
</style>
|
|
876
890
|
</head>
|
|
877
891
|
|
|
@@ -884,7 +898,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
884
898
|
<!-- Sidebar -->
|
|
885
899
|
<nav class="red-sidebar d-none d-md-flex" id="sidebar">
|
|
886
900
|
<!-- Logo -->
|
|
887
|
-
<
|
|
901
|
+
<a href="/" class="red-sidebar-logo" title="Back to <%= Rails.application.class.module_parent_name %>" style="text-decoration: none; color: inherit;">
|
|
888
902
|
<div class="red-sidebar-logo-icon">R</div>
|
|
889
903
|
<div>
|
|
890
904
|
<div style="font-size: 14px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em;">RED</div>
|
|
@@ -893,7 +907,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
893
907
|
end %>
|
|
894
908
|
<div style="font-size: 10px; color: var(--text-tertiary); margin-top: -2px;"><%= sidebar_app_name || Rails.application.class.module_parent_name %> · <%= Rails.env %></div>
|
|
895
909
|
</div>
|
|
896
|
-
</
|
|
910
|
+
</a>
|
|
897
911
|
|
|
898
912
|
<!-- Nav groups -->
|
|
899
913
|
<div style="flex: 1; overflow: auto; padding: var(--space-3) 0;">
|
|
@@ -949,7 +963,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
949
963
|
|
|
950
964
|
<% if health_items.any? %>
|
|
951
965
|
<div style="margin-bottom: var(--space-2);" id="navHealthSection">
|
|
952
|
-
<button class="red-sidebar-section-label"
|
|
966
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navHealthItems">
|
|
953
967
|
Health
|
|
954
968
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navHealthChevron"></i>
|
|
955
969
|
</button>
|
|
@@ -980,7 +994,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
980
994
|
|
|
981
995
|
<% if diag_items.any? %>
|
|
982
996
|
<div style="margin-bottom: var(--space-2);" id="navDiagSection">
|
|
983
|
-
<button class="red-sidebar-section-label"
|
|
997
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navDiagItems">
|
|
984
998
|
Diagnostics
|
|
985
999
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navDiagChevron"></i>
|
|
986
1000
|
</button>
|
|
@@ -998,7 +1012,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
998
1012
|
|
|
999
1013
|
<!-- INSIGHTS section -->
|
|
1000
1014
|
<div style="margin-bottom: var(--space-2);" id="navInsightsSection">
|
|
1001
|
-
<button class="red-sidebar-section-label"
|
|
1015
|
+
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navInsightsItems">
|
|
1002
1016
|
Insights
|
|
1003
1017
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navInsightsChevron"></i>
|
|
1004
1018
|
</button>
|
|
@@ -1062,7 +1076,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1062
1076
|
<% if params[:application_id].present? %>
|
|
1063
1077
|
<%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
|
|
1064
1078
|
<% else %>
|
|
1065
|
-
All Apps
|
|
1079
|
+
All Apps (<%= @applications.size %>)
|
|
1066
1080
|
<% end %>
|
|
1067
1081
|
</button>
|
|
1068
1082
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
|
|
@@ -1094,7 +1108,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1094
1108
|
</ul>
|
|
1095
1109
|
</div>
|
|
1096
1110
|
<% end %>
|
|
1097
|
-
<kbd>?</kbd>
|
|
1111
|
+
<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
1112
|
<button class="red-theme-toggle" id="themeToggle" title="Toggle theme">
|
|
1099
1113
|
<i class="bi bi-moon" id="themeIcon"></i>
|
|
1100
1114
|
</button>
|
|
@@ -1105,14 +1119,16 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1105
1119
|
<!-- Main content -->
|
|
1106
1120
|
<main style="flex: 1; padding: var(--space-6) var(--space-8); max-width: 1200px; width: 100%; margin: 0 auto;">
|
|
1107
1121
|
<% 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>
|
|
1122
|
+
<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);">
|
|
1123
|
+
<i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; flex-shrink: 0;"></i>
|
|
1124
|
+
<div style="flex: 1;">
|
|
1111
1125
|
<strong>Security Warning:</strong> You are using default credentials (gandalf/youshallnotpass).
|
|
1112
1126
|
Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
|
|
1113
1127
|
or configure <code>authenticate_with</code> in your initializer.
|
|
1114
1128
|
</div>
|
|
1129
|
+
<button type="button" data-red-action="dismiss-creds-warning" 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">×</button>
|
|
1115
1130
|
</div>
|
|
1131
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
|
|
1116
1132
|
<% end %>
|
|
1117
1133
|
<%= yield %>
|
|
1118
1134
|
</main>
|
|
@@ -1124,7 +1140,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1124
1140
|
Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank"><strong>RED</strong> — Rails Error Dashboard</a>
|
|
1125
1141
|
</p>
|
|
1126
1142
|
<p style="font-size: 12px; color: var(--text-tertiary); margin: 0;">
|
|
1127
|
-
|
|
1143
|
+
v<%= RailsErrorDashboard::VERSION %> · Built with ❤️ by <a href="https://anjan.dev" target="_blank">Anjan Jagirdar</a>
|
|
1128
1144
|
</p>
|
|
1129
1145
|
</footer>
|
|
1130
1146
|
</div>
|
|
@@ -1133,10 +1149,10 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1133
1149
|
<!-- Mobile Sidebar (Offcanvas) -->
|
|
1134
1150
|
<div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
|
|
1135
1151
|
<div class="offcanvas-header">
|
|
1136
|
-
<
|
|
1152
|
+
<a href="/" style="display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit;" title="Back to app home">
|
|
1137
1153
|
<div class="red-sidebar-logo-icon">R</div>
|
|
1138
1154
|
<strong>RED</strong>
|
|
1139
|
-
</
|
|
1155
|
+
</a>
|
|
1140
1156
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
1141
1157
|
</div>
|
|
1142
1158
|
<div class="offcanvas-body">
|
|
@@ -1201,7 +1217,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1201
1217
|
<script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js"></script>
|
|
1202
1218
|
|
|
1203
1219
|
<!-- Loading State Controller -->
|
|
1204
|
-
<script
|
|
1220
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1205
1221
|
(function() {
|
|
1206
1222
|
'use strict';
|
|
1207
1223
|
if (typeof Stimulus === 'undefined') return;
|
|
@@ -1280,7 +1296,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1280
1296
|
</script>
|
|
1281
1297
|
|
|
1282
1298
|
<!-- Syntax Highlighting -->
|
|
1283
|
-
<script
|
|
1299
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1284
1300
|
(function() {
|
|
1285
1301
|
'use strict';
|
|
1286
1302
|
if (typeof hljs === 'undefined') return;
|
|
@@ -1324,7 +1340,7 @@ details[open] summary { border-bottom: 1px solid var(--border-primary); }
|
|
|
1324
1340
|
</script>
|
|
1325
1341
|
|
|
1326
1342
|
<!-- Theme Toggle + Chart Theme -->
|
|
1327
|
-
<script
|
|
1343
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1328
1344
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1329
1345
|
var themeToggle = document.getElementById('themeToggle');
|
|
1330
1346
|
var themeIcon = document.getElementById('themeIcon');
|
|
@@ -1411,7 +1427,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1411
1427
|
</script>
|
|
1412
1428
|
|
|
1413
1429
|
<!-- Utilities (tooltips, clipboard, time conversion, keyboard shortcuts) -->
|
|
1414
|
-
<script
|
|
1430
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1415
1431
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1416
1432
|
|
|
1417
1433
|
// Tooltips
|
|
@@ -1581,11 +1597,114 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1581
1597
|
window.location.href = '<%= analytics_errors_path %>';
|
|
1582
1598
|
}
|
|
1583
1599
|
});
|
|
1600
|
+
|
|
1601
|
+
// ==========================================================================
|
|
1602
|
+
// Delegated event handlers for [data-red-action]
|
|
1603
|
+
// Replaces inline event handlers (which strict CSP blocks).
|
|
1604
|
+
// ==========================================================================
|
|
1605
|
+
function findActionEl(target) {
|
|
1606
|
+
var el = target;
|
|
1607
|
+
while (el && el !== document.body) {
|
|
1608
|
+
if (el.dataset && el.dataset.redAction) return el;
|
|
1609
|
+
el = el.parentElement;
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
document.addEventListener('click', function(e) {
|
|
1615
|
+
// Row navigation: <tr data-red-row-href="/path"> — clicking anywhere on the row
|
|
1616
|
+
// navigates, except when clicking inside a checkbox cell or interactive elements.
|
|
1617
|
+
var rowEl = e.target.closest && e.target.closest('[data-red-row-href]');
|
|
1618
|
+
if (rowEl && !e.defaultPrevented) {
|
|
1619
|
+
var inIgnore = e.target.closest('input, button, a, label, [data-red-stop]');
|
|
1620
|
+
if (!inIgnore) {
|
|
1621
|
+
window.location = rowEl.dataset.redRowHref;
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
var actionEl = findActionEl(e.target);
|
|
1627
|
+
if (!actionEl) return;
|
|
1628
|
+
var action = actionEl.dataset.redAction;
|
|
1629
|
+
|
|
1630
|
+
switch (action) {
|
|
1631
|
+
case 'navigate':
|
|
1632
|
+
if (actionEl.dataset.redHref) {
|
|
1633
|
+
window.location = actionEl.dataset.redHref;
|
|
1634
|
+
}
|
|
1635
|
+
break;
|
|
1636
|
+
case 'copy':
|
|
1637
|
+
if (typeof window.copyToClipboard === 'function') {
|
|
1638
|
+
var text = actionEl.dataset.redCopyText;
|
|
1639
|
+
if (text == null && actionEl.dataset.redCopyFrom) {
|
|
1640
|
+
text = actionEl.dataset[actionEl.dataset.redCopyFrom];
|
|
1641
|
+
}
|
|
1642
|
+
window.copyToClipboard(text || '', actionEl);
|
|
1643
|
+
}
|
|
1644
|
+
break;
|
|
1645
|
+
case 'copy-markdown':
|
|
1646
|
+
if (typeof window.copyToClipboard === 'function') {
|
|
1647
|
+
var md = (actionEl.dataset.markdown || '').replace(/\\(.)/g, function(m, c) {
|
|
1648
|
+
return c === 'n' ? '\n' : c;
|
|
1649
|
+
});
|
|
1650
|
+
window.copyToClipboard(md, actionEl);
|
|
1651
|
+
}
|
|
1652
|
+
break;
|
|
1653
|
+
case 'download-json':
|
|
1654
|
+
if (typeof window.downloadErrorJSON === 'function') {
|
|
1655
|
+
// Provide currentTarget pointing at the button so the handler can show feedback.
|
|
1656
|
+
window.downloadErrorJSON({ currentTarget: actionEl });
|
|
1657
|
+
}
|
|
1658
|
+
break;
|
|
1659
|
+
case 'switch-tab':
|
|
1660
|
+
if (typeof window.switchTab === 'function' && actionEl.dataset.redTab) {
|
|
1661
|
+
window.switchTab(actionEl.dataset.redTab);
|
|
1662
|
+
}
|
|
1663
|
+
break;
|
|
1664
|
+
case 'toggle-display':
|
|
1665
|
+
var tgtSel = actionEl.dataset.redTarget;
|
|
1666
|
+
if (tgtSel) {
|
|
1667
|
+
var tgt = document.querySelector(tgtSel);
|
|
1668
|
+
if (tgt) {
|
|
1669
|
+
var openDisplay = actionEl.dataset.redDisplay || 'block';
|
|
1670
|
+
tgt.style.display = (tgt.style.display === 'none' || tgt.style.display === '') ? openDisplay : 'none';
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
break;
|
|
1674
|
+
case 'toggle-next':
|
|
1675
|
+
var nextEl = actionEl.nextElementSibling;
|
|
1676
|
+
if (nextEl) {
|
|
1677
|
+
var isHidden = nextEl.style.display === 'none' || nextEl.style.display === '';
|
|
1678
|
+
nextEl.style.display = isHidden ? (actionEl.dataset.redDisplay || 'block') : 'none';
|
|
1679
|
+
var chev = actionEl.querySelector('.chev');
|
|
1680
|
+
if (chev) {
|
|
1681
|
+
chev.className = isHidden ? 'bi bi-chevron-up chev' : 'bi bi-chevron-down chev';
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
break;
|
|
1685
|
+
case 'dismiss-creds-warning':
|
|
1686
|
+
if (actionEl.parentElement) actionEl.parentElement.style.display = 'none';
|
|
1687
|
+
try { sessionStorage.setItem('red_dismiss_creds_warning', '1'); } catch (err) {}
|
|
1688
|
+
break;
|
|
1689
|
+
case 'toggle-nav-section':
|
|
1690
|
+
if (typeof window.toggleNavSection === 'function' && actionEl.dataset.redTarget) {
|
|
1691
|
+
window.toggleNavSection(actionEl.dataset.redTarget);
|
|
1692
|
+
}
|
|
1693
|
+
break;
|
|
1694
|
+
case 'confirm-submit':
|
|
1695
|
+
var msg = actionEl.dataset.redConfirmMessage || 'Are you sure?';
|
|
1696
|
+
if (!window.confirm(msg)) {
|
|
1697
|
+
e.preventDefault();
|
|
1698
|
+
e.stopPropagation();
|
|
1699
|
+
}
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1584
1703
|
});
|
|
1585
1704
|
</script>
|
|
1586
1705
|
|
|
1587
1706
|
<!-- Sidebar Toggle + Nav Section Toggle -->
|
|
1588
|
-
<script
|
|
1707
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1589
1708
|
document.addEventListener('DOMContentLoaded', function() {
|
|
1590
1709
|
var sidebarToggle = document.getElementById('sidebarToggle');
|
|
1591
1710
|
var sidebar = document.getElementById('sidebar');
|
|
@@ -1612,7 +1731,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1612
1731
|
});
|
|
1613
1732
|
|
|
1614
1733
|
// Nav section collapse/expand
|
|
1615
|
-
function
|
|
1734
|
+
window.toggleNavSection = function(itemsId) {
|
|
1616
1735
|
var items = document.getElementById(itemsId);
|
|
1617
1736
|
var chevron = document.getElementById(itemsId.replace('Items', 'Chevron'));
|
|
1618
1737
|
if (items) {
|
|
@@ -1621,7 +1740,7 @@ function toggleNavSection(itemsId) {
|
|
|
1621
1740
|
if (chevron) chevron.className = hidden ? 'bi bi-chevron-down' : 'bi bi-chevron-right';
|
|
1622
1741
|
localStorage.setItem('red_nav_' + itemsId, hidden ? 'open' : 'closed');
|
|
1623
1742
|
}
|
|
1624
|
-
}
|
|
1743
|
+
};
|
|
1625
1744
|
|
|
1626
1745
|
// Restore nav section states
|
|
1627
1746
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -1638,7 +1757,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1638
1757
|
</script>
|
|
1639
1758
|
|
|
1640
1759
|
<!-- Flash Messages -->
|
|
1641
|
-
<script
|
|
1760
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
1642
1761
|
<% if defined?(flash) && flash.present? %>
|
|
1643
1762
|
<% if flash[:notice] %>
|
|
1644
1763
|
showToast('<%= j flash[:notice] %>', 'success');
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</span>
|
|
14
14
|
<% end %>
|
|
15
15
|
</h5>
|
|
16
|
-
<button class="btn btn-sm btn-outline-light"
|
|
16
|
+
<button type="button" class="btn btn-sm btn-outline-light" data-red-action="copy" data-red-copy-text="<%= j error.error_type %>" title="Copy error type">
|
|
17
17
|
<i class="bi bi-clipboard"></i>
|
|
18
18
|
</button>
|
|
19
19
|
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<div class="card-body">
|
|
22
22
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
23
23
|
<h6 class="text-muted mb-0">Error Message:</h6>
|
|
24
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
24
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.message %>" title="Copy error message">
|
|
25
25
|
<i class="bi bi-clipboard"></i> Copy
|
|
26
26
|
</button>
|
|
27
27
|
</div>
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
<% end %>
|
|
96
96
|
</h6>
|
|
97
97
|
<% if error.backtrace.present? %>
|
|
98
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
98
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= j error.backtrace %>" title="Copy full backtrace">
|
|
99
99
|
<i class="bi bi-clipboard"></i> Copy Full Backtrace
|
|
100
100
|
</button>
|
|
101
101
|
<% end %>
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
<tr id="error_<%= error.id %>"
|
|
2
|
-
|
|
3
|
-
style="border-bottom: 1px solid var(--border-primary); cursor: pointer;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<span class="sev-bar" style="position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: <%= severity_color_var(error.severity) %>; opacity: 0; transition: opacity 0.1s ease; border-radius: 0 2px 2px 0;"></span>
|
|
8
|
-
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>" onclick="event.stopPropagation();" style="accent-color: var(--accent);">
|
|
2
|
+
data-red-row-href="<%= error_path(error, **app_context) %>"
|
|
3
|
+
style="border-bottom: 1px solid var(--border-primary); cursor: pointer;">
|
|
4
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: center; position: relative;" data-red-stop>
|
|
5
|
+
<span class="sev-bar" style="position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: <%= severity_color_var(error.severity) %>; opacity: 0; border-radius: 0 2px 2px 0;"></span>
|
|
6
|
+
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>" data-red-stop style="accent-color: var(--accent);">
|
|
9
7
|
</td>
|
|
10
|
-
<td style="padding: var(--space-3) var(--space-4);"
|
|
8
|
+
<td style="padding: var(--space-3) var(--space-4);">
|
|
11
9
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
12
10
|
<span class="badge bg-<%= severity_color(error.severity) %>" style="display: inline-flex; align-items: center; gap: 4px;">
|
|
13
11
|
<span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
|
|
@@ -20,11 +18,11 @@
|
|
|
20
18
|
<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
19
|
<% end %>
|
|
22
20
|
</div>
|
|
23
|
-
<div style="color: var(--text-tertiary); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
21
|
+
<div style="color: var(--text-tertiary); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<%= error.message %>"><%= error.message %></div>
|
|
24
22
|
</div>
|
|
25
23
|
</div>
|
|
26
24
|
</td>
|
|
27
|
-
<td style="padding: var(--space-3) var(--space-4);"
|
|
25
|
+
<td style="padding: var(--space-3) var(--space-4);">
|
|
28
26
|
<%
|
|
29
27
|
# Use the model's status field if it has a meaningful workflow status
|
|
30
28
|
raw_status = error.respond_to?(:status) ? error.status : nil
|
|
@@ -58,26 +56,26 @@
|
|
|
58
56
|
<i class="bi bi-arrow-counterclockwise" style="color: var(--status-warning); margin-left: 4px; font-size: 12px;" title="Reopened"></i>
|
|
59
57
|
<% end %>
|
|
60
58
|
</td>
|
|
61
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);"
|
|
59
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);">
|
|
62
60
|
<%= error.occurrence_count.to_s(:delimited) rescue error.occurrence_count %>
|
|
63
61
|
</td>
|
|
64
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);"
|
|
62
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);">
|
|
65
63
|
<% if error.user_id %>
|
|
66
64
|
<%= error.user_id %>
|
|
67
65
|
<% else %>
|
|
68
66
|
<span style="color: var(--text-tertiary);">—</span>
|
|
69
67
|
<% end %>
|
|
70
68
|
</td>
|
|
71
|
-
<td style="padding: var(--space-3) var(--space-4); text-align: right; color: var(--text-tertiary); font-size: 12px;"
|
|
69
|
+
<td style="padding: var(--space-3) var(--space-4); text-align: right; color: var(--text-tertiary); font-size: 12px;">
|
|
72
70
|
<%= local_time_ago(error.last_seen_at) %>
|
|
73
71
|
</td>
|
|
74
72
|
<% if local_assigns[:show_application] %>
|
|
75
|
-
<td style="padding: var(--space-3) var(--space-4); font-size: 12px;"
|
|
73
|
+
<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' %>">
|
|
76
74
|
<span class="badge bg-info"><%= error.application&.name || 'Unknown' %></span>
|
|
77
75
|
</td>
|
|
78
76
|
<% end %>
|
|
79
77
|
<% if local_assigns[:show_platform] %>
|
|
80
|
-
<td style="padding: var(--space-3) var(--space-4); font-size: 12px;"
|
|
78
|
+
<td style="padding: var(--space-3) var(--space-4); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= error.platform || 'API' %>">
|
|
81
79
|
<% if error.platform == 'iOS' %>
|
|
82
80
|
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
83
81
|
<% elsif error.platform == 'Android' %>
|
|
@@ -84,17 +84,17 @@
|
|
|
84
84
|
<th>Replay Request:</th>
|
|
85
85
|
<td>
|
|
86
86
|
<% if curl_cmd.present? %>
|
|
87
|
-
<button class="btn btn-sm btn-outline-primary me-2"
|
|
88
|
-
|
|
89
|
-
data-
|
|
87
|
+
<button type="button" class="btn btn-sm btn-outline-primary me-2"
|
|
88
|
+
data-red-action="copy"
|
|
89
|
+
data-red-copy-text="<%= h curl_cmd %>"
|
|
90
90
|
title="Copy curl command to clipboard">
|
|
91
91
|
<i class="bi bi-terminal"></i> Copy as curl
|
|
92
92
|
</button>
|
|
93
93
|
<% end %>
|
|
94
94
|
<% if rspec_cmd.present? %>
|
|
95
|
-
<button class="btn btn-sm btn-outline-success"
|
|
96
|
-
|
|
97
|
-
data-
|
|
95
|
+
<button type="button" class="btn btn-sm btn-outline-success"
|
|
96
|
+
data-red-action="copy"
|
|
97
|
+
data-red-copy-text="<%= h rspec_cmd %>"
|
|
98
98
|
title="Copy RSpec request spec to clipboard">
|
|
99
99
|
<i class="bi bi-code-slash"></i> Copy as RSpec
|
|
100
100
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<script
|
|
2
|
-
function
|
|
1
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
2
|
+
window.downloadErrorJSON = function(event) {
|
|
3
3
|
const errorData = {
|
|
4
4
|
id: <%= raw error.id.to_json %>,
|
|
5
5
|
error_type: <%= raw error.error_type.to_json %>,
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
button.classList.remove('btn-success');
|
|
53
53
|
button.classList.add('btn-outline-secondary');
|
|
54
54
|
}, 2000);
|
|
55
|
-
}
|
|
55
|
+
};
|
|
56
56
|
|
|
57
57
|
// Section Navigation — pill bar with scroll spy
|
|
58
58
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -526,7 +526,7 @@
|
|
|
526
526
|
<small class="metadata-label d-block mb-1">Error ID</small>
|
|
527
527
|
<div class="d-flex align-items-center gap-2">
|
|
528
528
|
<code><%= error.id %></code>
|
|
529
|
-
<button class="btn btn-sm btn-outline-secondary"
|
|
529
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-red-action="copy" data-red-copy-text="<%= error.id %>" title="Copy error ID">
|
|
530
530
|
<i class="bi bi-clipboard"></i>
|
|
531
531
|
</button>
|
|
532
532
|
</div>
|
|
@@ -87,11 +87,11 @@
|
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
89
|
<% if error.resolved? && error.resolution_comment.present? %>
|
|
90
|
-
<div
|
|
91
|
-
<small
|
|
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
|
|
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% content_for :page_title, "Analytics" %>
|
|
2
2
|
|
|
3
|
-
<script
|
|
3
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
4
4
|
// Dynamic chart colors based on theme
|
|
5
5
|
window.getChartColors = function() {
|
|
6
6
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
</div>
|
|
68
68
|
<div class="card-body">
|
|
69
69
|
<div id="errors-over-time-chart"></div>
|
|
70
|
-
<script
|
|
70
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
71
71
|
document.addEventListener('DOMContentLoaded', function() {
|
|
72
72
|
const colors = window.getChartColors();
|
|
73
73
|
new Chartkick.LineChart("errors-over-time-chart", <%= raw @errors_over_time.to_json %>, {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
</div>
|
|
113
113
|
<div class="card-body">
|
|
114
114
|
<div id="errors-by-platform-chart"></div>
|
|
115
|
-
<script
|
|
115
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
116
116
|
document.addEventListener('DOMContentLoaded', function() {
|
|
117
117
|
const colors = window.getChartColors();
|
|
118
118
|
// Distinct, accessible colors for 5 platforms with good contrast
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
</div>
|
|
167
167
|
<div class="card-body">
|
|
168
168
|
<div id="errors-by-type-chart"></div>
|
|
169
|
-
<script
|
|
169
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
170
170
|
document.addEventListener('DOMContentLoaded', function() {
|
|
171
171
|
const colors = window.getChartColors();
|
|
172
172
|
new Chartkick.BarChart("errors-by-type-chart", <%= raw @errors_by_type.to_json %>, {
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
</div>
|
|
209
209
|
<div class="card-body">
|
|
210
210
|
<div id="errors-by-hour-chart"></div>
|
|
211
|
-
<script
|
|
211
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
212
212
|
document.addEventListener('DOMContentLoaded', function() {
|
|
213
213
|
const colors = window.getChartColors();
|
|
214
214
|
new Chartkick.ColumnChart("errors-by-hour-chart", <%= raw @errors_by_hour.to_json %>, {
|
|
@@ -410,7 +410,7 @@
|
|
|
410
410
|
</div>
|
|
411
411
|
<div class="card-body">
|
|
412
412
|
<div id="errors-by-version-chart"></div>
|
|
413
|
-
<script
|
|
413
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
414
414
|
document.addEventListener('DOMContentLoaded', function() {
|
|
415
415
|
const versionData = <%= raw @errors_by_version.transform_values { |v| v[:count] }.to_json %>;
|
|
416
416
|
const colors = window.getChartColors();
|
|
@@ -549,7 +549,7 @@
|
|
|
549
549
|
<div class="mt-4">
|
|
550
550
|
<h6 class="text-muted mb-3">MTTR by Platform</h6>
|
|
551
551
|
<div id="mttr-by-platform-chart"></div>
|
|
552
|
-
<script
|
|
552
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
553
553
|
document.addEventListener('DOMContentLoaded', function() {
|
|
554
554
|
const colors = window.getChartColors();
|
|
555
555
|
new Chartkick.BarChart("mttr-by-platform-chart",
|
|
@@ -602,7 +602,7 @@
|
|
|
602
602
|
<div class="mt-4">
|
|
603
603
|
<h6 class="text-muted mb-3">MTTR Trend (Weekly)</h6>
|
|
604
604
|
<div id="mttr-trend-chart"></div>
|
|
605
|
-
<script
|
|
605
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
606
606
|
document.addEventListener('DOMContentLoaded', function() {
|
|
607
607
|
const colors = window.getChartColors();
|
|
608
608
|
new Chartkick.LineChart("mttr-trend-chart",
|
|
@@ -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 -->
|
|
@@ -100,7 +86,7 @@
|
|
|
100
86
|
current_status = params[:status]
|
|
101
87
|
is_unresolved_only = params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
102
88
|
%>
|
|
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 %>
|
|
89
|
+
<%= link_to errors_path(app_context.merge(unresolved: '0')), class: "btn filter-pill #{current_status.blank? && !is_unresolved_only ? 'active btn-primary' : 'btn-outline-primary'}", style: "text-decoration: none;" do %>All<% end %>
|
|
104
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 %>
|
|
105
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 %>
|
|
106
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 %>
|
|
@@ -120,13 +106,13 @@
|
|
|
120
106
|
|
|
121
107
|
<!-- Advanced filters toggle -->
|
|
122
108
|
<div style="width: 1px; height: 24px; background: var(--border-primary);"></div>
|
|
123
|
-
<button type="button"
|
|
109
|
+
<button type="button" data-red-action="toggle-display" data-red-target="#advanced-filters" data-red-display="flex" class="filter-pill" style="border: none;">
|
|
124
110
|
<i class="bi bi-sliders"></i> More filters
|
|
125
111
|
</button>
|
|
126
112
|
</div>
|
|
127
113
|
|
|
128
114
|
<!-- Advanced filters -->
|
|
129
|
-
<div id="advanced-filters" style="display:
|
|
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 %>
|
|
@@ -208,23 +194,41 @@
|
|
|
208
194
|
<!-- Error Table -->
|
|
209
195
|
<div class="card" style="overflow: hidden;">
|
|
210
196
|
<% if @errors.any? %>
|
|
197
|
+
<!-- Batch action toolbar (shown when one or more errors are selected). Sits above the table
|
|
198
|
+
so it doesn't disrupt the table's column widths the way a colspan thead row does. -->
|
|
199
|
+
<div id="batch-toolbar" style="display: none; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border-primary); background: var(--surface-secondary);">
|
|
200
|
+
<%= form_with url: batch_action_errors_path, method: :post, id: "batch-form", style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;" do |f| %>
|
|
201
|
+
<% if params[:application_id].present? %>
|
|
202
|
+
<%= hidden_field_tag :application_id, params[:application_id] %>
|
|
203
|
+
<% end %>
|
|
204
|
+
<input type="checkbox" id="select-all-batch" class="form-check-input" style="accent-color: var(--accent); margin: 0;" checked>
|
|
205
|
+
<span id="selected-count" style="font-size: 13px; font-weight: 600; color: var(--text-primary); margin-right: var(--space-2);"></span>
|
|
206
|
+
<%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
|
|
207
|
+
<i class="bi bi-check-circle"></i> Resolve
|
|
208
|
+
<% end %>
|
|
209
|
+
<%= 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 %>
|
|
210
|
+
<i class="bi bi-trash"></i> Delete
|
|
211
|
+
<% end %>
|
|
212
|
+
<% end %>
|
|
213
|
+
</div>
|
|
211
214
|
<div data-loading-target="content">
|
|
212
|
-
<
|
|
215
|
+
<div style="overflow-x: auto;">
|
|
216
|
+
<table class="table table-hover" style="margin-bottom: 0; width: 100%; table-layout: fixed;">
|
|
213
217
|
<thead>
|
|
214
|
-
<tr style="border-bottom: 1px solid var(--border-primary);">
|
|
215
|
-
<th style="width:
|
|
218
|
+
<tr id="thead-columns" style="border-bottom: 1px solid var(--border-primary);">
|
|
219
|
+
<th style="width: 36px; padding: var(--space-3) var(--space-4); text-align: center;">
|
|
216
220
|
<input type="checkbox" id="select-all" class="form-check-input" style="accent-color: var(--accent);">
|
|
217
221
|
</th>
|
|
218
222
|
<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:
|
|
220
|
-
<th style="padding: var(--space-3) var(--space-4); text-align: right; width:
|
|
221
|
-
<th style="padding: var(--space-3) var(--space-4); text-align: right; width:
|
|
222
|
-
<th style="padding: var(--space-3) var(--space-4); text-align: right; width:
|
|
223
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: left; width: 100px;">Status</th>
|
|
224
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 70px;">Events</th>
|
|
225
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 55px;">Users</th>
|
|
226
|
+
<th style="padding: var(--space-3) var(--space-4); text-align: right; width: 85px;">Last seen</th>
|
|
223
227
|
<% if @applications.size > 1 && params[:application_id].blank? %>
|
|
224
|
-
<th style="padding: var(--space-3) var(--space-4); width:
|
|
228
|
+
<th style="padding: var(--space-3) var(--space-4); width: 100px;">App</th>
|
|
225
229
|
<% end %>
|
|
226
230
|
<% if @platforms.size > 1 %>
|
|
227
|
-
<th style="padding: var(--space-3) var(--space-4); width:
|
|
231
|
+
<th style="padding: var(--space-3) var(--space-4); width: 100px;">Platform</th>
|
|
228
232
|
<% end %>
|
|
229
233
|
</tr>
|
|
230
234
|
</thead>
|
|
@@ -234,11 +238,13 @@
|
|
|
234
238
|
<% end %>
|
|
235
239
|
</tbody>
|
|
236
240
|
</table>
|
|
241
|
+
</div>
|
|
237
242
|
|
|
238
243
|
<!-- Pagination -->
|
|
239
244
|
<% if @pagy.pages > 1 %>
|
|
240
|
-
<div style="padding: var(--space-4); border-top: 1px solid var(--border-primary);">
|
|
241
|
-
|
|
245
|
+
<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);">
|
|
246
|
+
<span style="font-size: 12px; color: var(--text-tertiary);">Showing <%= @pagy.from %>–<%= @pagy.to %> of <%= @pagy.count %> errors</span>
|
|
247
|
+
<div><%== @pagy.series_nav(:bootstrap) %></div>
|
|
242
248
|
</div>
|
|
243
249
|
<% end %>
|
|
244
250
|
</div>
|
|
@@ -268,10 +274,11 @@
|
|
|
268
274
|
</div>
|
|
269
275
|
</div>
|
|
270
276
|
|
|
271
|
-
|
|
277
|
+
<%= red_javascript_tag do %>
|
|
272
278
|
document.addEventListener('DOMContentLoaded', function() {
|
|
273
279
|
var selectAllCheckbox = document.getElementById('select-all');
|
|
274
|
-
var
|
|
280
|
+
var selectAllBatchCheckbox = document.getElementById('select-all-batch');
|
|
281
|
+
var batchToolbar = document.getElementById('batch-toolbar');
|
|
275
282
|
var selectedCountSpan = document.getElementById('selected-count');
|
|
276
283
|
var batchForm = document.getElementById('batch-form');
|
|
277
284
|
|
|
@@ -282,31 +289,47 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
282
289
|
function updateBatchToolbar() {
|
|
283
290
|
var checkedBoxes = document.querySelectorAll('.error-checkbox:checked');
|
|
284
291
|
var count = checkedBoxes.length;
|
|
292
|
+
var boxes = getErrorCheckboxes();
|
|
293
|
+
var allChecked = boxes.length > 0 && Array.from(boxes).every(function(cb) { return cb.checked; });
|
|
294
|
+
var someChecked = Array.from(boxes).some(function(cb) { return cb.checked; });
|
|
295
|
+
|
|
285
296
|
if (count > 0) {
|
|
286
|
-
|
|
287
|
-
selectedCountSpan.textContent = count + ' selected';
|
|
297
|
+
if (batchToolbar) batchToolbar.style.display = '';
|
|
298
|
+
if (selectedCountSpan) selectedCountSpan.textContent = count + ' selected';
|
|
299
|
+
if (selectAllBatchCheckbox) {
|
|
300
|
+
selectAllBatchCheckbox.checked = allChecked;
|
|
301
|
+
selectAllBatchCheckbox.indeterminate = someChecked && !allChecked;
|
|
302
|
+
}
|
|
288
303
|
} else {
|
|
289
|
-
|
|
304
|
+
if (batchToolbar) batchToolbar.style.display = 'none';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (selectAllCheckbox) {
|
|
308
|
+
selectAllCheckbox.checked = allChecked;
|
|
309
|
+
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
290
310
|
}
|
|
291
311
|
}
|
|
292
312
|
|
|
313
|
+
function handleSelectAll(checked) {
|
|
314
|
+
getErrorCheckboxes().forEach(function(cb) { cb.checked = checked; });
|
|
315
|
+
updateBatchToolbar();
|
|
316
|
+
}
|
|
317
|
+
|
|
293
318
|
if (selectAllCheckbox) {
|
|
294
319
|
selectAllCheckbox.addEventListener('change', function() {
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
handleSelectAll(selectAllCheckbox.checked);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (selectAllBatchCheckbox) {
|
|
325
|
+
selectAllBatchCheckbox.addEventListener('change', function() {
|
|
326
|
+
handleSelectAll(selectAllBatchCheckbox.checked);
|
|
297
327
|
});
|
|
298
328
|
}
|
|
299
329
|
|
|
300
330
|
document.addEventListener('change', function(e) {
|
|
301
331
|
if (e.target.classList.contains('error-checkbox')) {
|
|
302
332
|
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
333
|
}
|
|
311
334
|
});
|
|
312
335
|
|
|
@@ -368,4 +391,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
368
391
|
});
|
|
369
392
|
}
|
|
370
393
|
});
|
|
371
|
-
|
|
394
|
+
<% end %>
|
|
@@ -169,7 +169,10 @@
|
|
|
169
169
|
%>
|
|
170
170
|
|
|
171
171
|
<div>
|
|
172
|
-
<
|
|
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.
|
|
@@ -178,7 +181,7 @@
|
|
|
178
181
|
<!-- Dynamically render setting groups -->
|
|
179
182
|
<% setting_groups.each do |group_name, group_config| %>
|
|
180
183
|
<div class="card" style="margin-bottom: var(--space-4);">
|
|
181
|
-
<button
|
|
184
|
+
<button type="button" data-red-action="toggle-next" data-red-display="block" style="display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-4) var(--space-6); background: none; border: none; cursor: pointer; border-bottom: 1px solid var(--border-primary);">
|
|
182
185
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="<%= group_config[:icon] %>" style="margin-right: 6px;"></i> <%= group_name %></span>
|
|
183
186
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
184
187
|
<span style="font-size: 11px; color: var(--text-tertiary);"><%= group_config[:settings].size %> options</span>
|
|
@@ -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" data-red-action="confirm-submit" data-red-confirm-message="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">
|
|
@@ -77,32 +77,32 @@
|
|
|
77
77
|
<button type="button" class="btn" data-bs-toggle="modal" data-bs-target="#assignModal" id="hero-assign-btn">
|
|
78
78
|
Assign
|
|
79
79
|
</button>
|
|
80
|
-
<button type="button" class="btn"
|
|
80
|
+
<button type="button" class="btn" data-red-action="copy-markdown" data-markdown="<%= j @error_markdown %>">
|
|
81
81
|
<i class="bi bi-clipboard" style="margin-right: 4px;"></i>Copy for LLM
|
|
82
82
|
</button>
|
|
83
|
-
<button type="button" class="btn"
|
|
83
|
+
<button type="button" class="btn" data-red-action="download-json" title="Export JSON">
|
|
84
84
|
<i class="bi bi-download"></i>
|
|
85
85
|
</button>
|
|
86
86
|
</div>
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
89
|
<!-- Main content: tabs + sidebar -->
|
|
90
|
-
<div
|
|
90
|
+
<div class="red-detail-grid">
|
|
91
91
|
<!-- Left: tabbed content -->
|
|
92
92
|
<div>
|
|
93
93
|
<!-- Tab bar -->
|
|
94
94
|
<div style="display: flex; gap: 0; border-bottom: 1px solid var(--border-primary); margin-bottom: var(--space-4);">
|
|
95
|
-
<button
|
|
95
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="details" class="red-tab active" id="tab-details" style="padding: 10px 16px; font-size: 13px; font-weight: 600; color: var(--accent); background: none; border: none; border-bottom: 2px solid var(--accent); margin-bottom: -1px; cursor: pointer;">
|
|
96
96
|
<i class="bi bi-code-slash" style="margin-right: 6px;"></i>Details
|
|
97
97
|
</button>
|
|
98
|
-
<button
|
|
98
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="context" class="red-tab" id="tab-context" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
99
99
|
<i class="bi bi-layers" style="margin-right: 6px;"></i>Context
|
|
100
100
|
</button>
|
|
101
|
-
<button
|
|
101
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="history" class="red-tab" id="tab-history" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
102
102
|
<i class="bi bi-clock-history" style="margin-right: 6px;"></i>History
|
|
103
103
|
</button>
|
|
104
104
|
<% if RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
105
|
-
<button
|
|
105
|
+
<button type="button" data-red-action="switch-tab" data-red-tab="issues" class="red-tab" id="tab-issues" style="padding: 10px 16px; font-size: 13px; font-weight: 400; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer;">
|
|
106
106
|
<i class="bi bi-github" style="margin-right: 6px;"></i>Issues
|
|
107
107
|
</button>
|
|
108
108
|
<% end %>
|
|
@@ -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 -->
|
|
@@ -225,8 +225,8 @@
|
|
|
225
225
|
|
|
226
226
|
<%= render "modals", error: @error %>
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
function
|
|
228
|
+
<%= red_javascript_tag do %>
|
|
229
|
+
window.switchTab = function(tabId) {
|
|
230
230
|
// Deactivate all tabs
|
|
231
231
|
document.querySelectorAll('.red-tab').forEach(function(el) {
|
|
232
232
|
el.style.color = 'var(--text-secondary)';
|
|
@@ -243,5 +243,5 @@ function switchTab(tabId) {
|
|
|
243
243
|
// Scroll to selected panel
|
|
244
244
|
var panel = document.getElementById('panel-' + tabId);
|
|
245
245
|
if (panel) { panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
246
|
-
}
|
|
247
|
-
|
|
246
|
+
};
|
|
247
|
+
<% end %>
|
|
@@ -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
|
|
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
|
|
98
|
-
<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
|
@@ -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
|
|
@@ -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.
|
|
4
|
+
version: 0.6.2
|
|
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.
|
|
500
|
+
RED (Rails Error Dashboard) v0.6.2
|
|
500
501
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
501
502
|
|
|
502
503
|
First install:
|