rails_error_dashboard 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +156 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -73
  5. data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
  6. data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
  7. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
  8. data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
  9. data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
  10. data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
  11. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
  12. data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
  13. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
  14. data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
  15. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
  16. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
  17. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
  18. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
  19. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
  20. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
  21. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
  22. data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
  23. data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
  24. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
  25. data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
  26. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
  27. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
  28. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
  29. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
  30. data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
  31. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
  32. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
  33. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
  34. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
  35. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
  36. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
  37. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
  38. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
  39. data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
  40. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
  41. data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
  42. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
  43. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
  44. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
  45. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
  46. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
  47. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
  48. data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
  49. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
  50. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
  51. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
  52. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
  53. data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
  54. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
  55. data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
  56. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
  57. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
  58. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
  59. data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
  60. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
  61. data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
  62. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
  63. data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
  64. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
  65. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +39 -28
  66. data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
  67. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
  68. data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
  69. data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
  70. data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
  71. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
  72. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
  73. data/config/locales/de.yml +2007 -0
  74. data/config/locales/en.yml +2719 -0
  75. data/config/locales/es.yml +2079 -0
  76. data/config/locales/fr.yml +2082 -0
  77. data/config/locales/it.yml +2060 -0
  78. data/config/locales/ja.yml +1802 -0
  79. data/config/locales/pl.yml +2108 -0
  80. data/config/locales/pt-BR.yml +2064 -0
  81. data/config/locales/ru.yml +2109 -0
  82. data/config/locales/uk.yml +2106 -0
  83. data/config/locales/zh-CN.yml +1791 -0
  84. data/config/routes.rb +5 -0
  85. data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
  86. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
  87. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
  88. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
  89. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
  90. data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
  91. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
  92. data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
  93. data/lib/rails_error_dashboard/configuration.rb +23 -0
  94. data/lib/rails_error_dashboard/current.rb +56 -0
  95. data/lib/rails_error_dashboard/i18n_store.rb +293 -0
  96. data/lib/rails_error_dashboard/private_backend.rb +213 -0
  97. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
  98. data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
  99. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
  100. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
  101. data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
  102. data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
  103. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
  104. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
  105. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
  106. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
  107. data/lib/rails_error_dashboard/translation.rb +39 -0
  108. data/lib/rails_error_dashboard/version.rb +1 -1
  109. data/lib/rails_error_dashboard.rb +4 -0
  110. data/lib/tasks/error_dashboard.rake +5 -1
  111. metadata +24 -4
@@ -1,4 +1,4 @@
1
- <% content_for :page_title, "Error ##{@error.id}" %>
1
+ <% content_for :page_title, red_t("red.errors.show.page_title", id: @error.id) %>
2
2
 
3
3
  <%= render "show_scripts", error: @error %>
4
4
 
@@ -6,7 +6,7 @@
6
6
  <!-- Back + breadcrumb -->
7
7
  <div style="display: flex; align-items: center; gap: 8px; margin-bottom: var(--space-4); font-size: 13px; color: var(--text-tertiary);">
8
8
  <%= link_to errors_path(app_context), style: "background: none; border: none; color: var(--text-tertiary); display: flex; align-items: center; gap: 4px; text-decoration: none; font-size: 13px;" do %>
9
- <i class="bi bi-arrow-left"></i> Errors
9
+ <i class="bi bi-arrow-left"></i> <%= red_t("red.errors.show.breadcrumb_errors") %>
10
10
  <% end %>
11
11
  <span>/</span>
12
12
  <span style="color: var(--text-secondary);"><%= @error.error_type %></span>
@@ -18,74 +18,81 @@
18
18
  <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
19
19
  <span class="badge bg-<%= severity_color(@error.severity) %>" style="display: inline-flex; align-items: center; gap: 4px;">
20
20
  <span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
21
- <%= @error.severity %>
21
+ <%= red_t("red.common.severity.#{@error.severity}", default: @error.severity.to_s) %>
22
22
  </span>
23
23
  <%
24
+ # Same treatment as _error_row: status_key stays the machine value and
25
+ # drives both the colour lookup and the translation key; only the
26
+ # label is translated. The two views read the same red.errors.row.status.*
27
+ # keys rather than keeping parallel copies that would drift.
24
28
  raw_status = @error.respond_to?(:status) ? @error.status : nil
25
- status_text = if @error.respond_to?(:muted?) && @error.muted?
29
+ status_key = if @error.respond_to?(:muted?) && @error.muted?
26
30
  'muted'
27
31
  elsif @error.respond_to?(:snoozed?) && @error.snoozed?
28
32
  'snoozed'
29
33
  elsif raw_status.present? && raw_status != 'new'
30
- raw_status.tr('_', ' ')
34
+ raw_status
31
35
  elsif @error.resolved?
32
36
  'resolved'
33
37
  else
34
38
  'unresolved'
35
39
  end
40
+ status_text = red_t("red.errors.row.status.#{status_key}", default: status_key.tr('_', ' '))
36
41
  status_colors = {
37
42
  'unresolved' => { bg: 'var(--status-critical-bg)', color: 'var(--status-critical)' },
38
43
  'resolved' => { bg: 'var(--status-success-bg)', color: 'var(--status-success)' },
39
- 'in progress' => { bg: 'var(--status-info-bg)', color: 'var(--status-info)' },
44
+ 'in_progress' => { bg: 'var(--status-info-bg)', color: 'var(--status-info)' },
40
45
  'investigating' => { bg: 'var(--status-caution-bg)', color: 'var(--status-caution)' },
41
- 'wont fix' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
46
+ 'wont_fix' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
42
47
  'assigned' => { bg: 'var(--accent-subtle)', color: 'var(--accent)' },
43
48
  'snoozed' => { bg: 'var(--status-caution-bg)', color: 'var(--status-caution)' },
44
49
  'muted' => { bg: 'var(--surface-tertiary)', color: 'var(--text-tertiary)' },
45
50
  }
46
- sc = status_colors[status_text] || status_colors['unresolved']
51
+ sc = status_colors[status_key] || status_colors['unresolved']
47
52
  %>
48
53
  <span style="display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; font-size: 11px; font-weight: 600; border-radius: var(--radius-full); background: <%= sc[:bg] %>; color: <%= sc[:color] %>; text-transform: capitalize;">
49
54
  <%= status_text %>
50
55
  </span>
51
56
  <% if @error.reopened? %>
52
57
  <span style="display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; font-size: 11px; font-weight: 600; border-radius: var(--radius-full); background: var(--status-warning-bg); color: var(--status-warning);">
53
- <i class="bi bi-arrow-counterclockwise" style="font-size: 10px;"></i> Reopened
58
+ <i class="bi bi-arrow-counterclockwise" style="font-size: 10px;"></i> <%= red_t("red.errors.show.hero.reopened") %>
54
59
  </span>
55
60
  <% end %>
56
61
  </div>
57
62
  <h2 style="font-size: 20px; font-weight: 700; color: var(--text-primary); font-family: var(--font-mono); margin: 0 0 4px; word-break: break-word;"><%= @error.error_type %></h2>
58
63
  <p style="font-size: 14px; color: var(--text-secondary); margin: 0; font-family: var(--font-mono);"><%= @error.message&.truncate(200) %></p>
59
64
  <div style="display: flex; gap: var(--space-6); margin-top: var(--space-4); font-size: 12px; color: var(--text-tertiary); flex-wrap: wrap;">
60
- <span><strong style="color: var(--text-primary);"><%= @error.occurrence_count %></strong> occurrences</span>
65
+ <span><%= red_tp("red.errors.show.hero.occurrences_html", count: @error.occurrence_count,
66
+ value: tag.strong(@error.occurrence_count, style: "color: var(--text-primary);")) %></span>
61
67
  <% if @error.application&.name.present? %>
62
68
  <span><i class="bi bi-layers" style="font-size: 10px;"></i> <strong style="color: var(--text-primary);"><%= @error.application.name %></strong></span>
63
69
  <% end %>
64
70
  <% if @error.user_id %>
65
- <span>User <strong style="color: var(--text-primary);">#<%= @error.user_id %></strong></span>
71
+ <span><%= red_t("red.errors.show.hero.user_html",
72
+ value: tag.strong("##{@error.user_id}", style: "color: var(--text-primary);")) %></span>
66
73
  <% end %>
67
- <span>First seen <%= local_time_ago(@error.first_seen_at) %></span>
68
- <span>Last seen <%= local_time_ago(@error.last_seen_at) %></span>
74
+ <span><%= red_t("red.errors.show.hero.first_seen_html", time: local_time_ago(@error.first_seen_at)) %></span>
75
+ <span><%= red_t("red.errors.show.hero.last_seen_html", time: local_time_ago(@error.last_seen_at)) %></span>
69
76
  </div>
70
77
  </div>
71
78
  <div style="display: flex; gap: 6px; flex-shrink: 0;">
72
79
  <% unless @error.resolved? %>
73
80
  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#resolveModal" style="font-weight: 600;">
74
- Resolve
81
+ <%= red_t("red.errors.show.actions.resolve") %>
75
82
  </button>
76
83
  <% end %>
77
84
  <button type="button" class="btn" data-bs-toggle="modal" data-bs-target="#assignModal" id="hero-assign-btn">
78
- Assign
85
+ <%= red_t("red.errors.show.actions.assign") %>
79
86
  </button>
80
87
  <button type="button" class="btn" data-red-action="copy-markdown" data-markdown="<%= j @error_markdown %>">
81
- <i class="bi bi-clipboard" style="margin-right: 4px;"></i>Copy for LLM
88
+ <i class="bi bi-clipboard" style="margin-right: 4px;"></i><%= red_t("red.errors.show.actions.copy_for_llm") %>
82
89
  </button>
83
90
  <% if RailsErrorDashboard.configuration.llm_configured? %>
84
91
  <button type="button" class="btn" data-red-action="open-ai-help">
85
- <i class="bi bi-stars" style="margin-right: 4px;"></i>AI Help
92
+ <i class="bi bi-stars" style="margin-right: 4px;"></i><%= red_t("red.errors.show.actions.ai_help") %>
86
93
  </button>
87
94
  <% end %>
88
- <button type="button" class="btn" data-red-action="download-json" title="Export JSON">
95
+ <button type="button" class="btn" data-red-action="download-json" title="<%= red_t("red.errors.show.actions.export_json") %>">
89
96
  <i class="bi bi-download"></i>
90
97
  </button>
91
98
  </div>
@@ -98,17 +105,17 @@
98
105
  <!-- Tab bar -->
99
106
  <div style="display: flex; gap: 0; border-bottom: 1px solid var(--border-primary); margin-bottom: var(--space-4);">
100
107
  <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;">
101
- <i class="bi bi-code-slash" style="margin-right: 6px;"></i>Details
108
+ <i class="bi bi-code-slash" style="margin-right: 6px;"></i><%= red_t("red.errors.show.tabs.details") %>
102
109
  </button>
103
110
  <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;">
104
- <i class="bi bi-layers" style="margin-right: 6px;"></i>Context
111
+ <i class="bi bi-layers" style="margin-right: 6px;"></i><%= red_t("red.errors.show.tabs.context") %>
105
112
  </button>
106
113
  <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;">
107
- <i class="bi bi-clock-history" style="margin-right: 6px;"></i>History
114
+ <i class="bi bi-clock-history" style="margin-right: 6px;"></i><%= red_t("red.errors.show.tabs.history") %>
108
115
  </button>
109
116
  <% if RailsErrorDashboard.configuration.enable_issue_tracking %>
110
117
  <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;">
111
- <i class="bi bi-github" style="margin-right: 6px;"></i>Issues
118
+ <i class="bi bi-github" style="margin-right: 6px;"></i><%= red_t("red.errors.show.tabs.issues") %>
112
119
  </button>
113
120
  <% end %>
114
121
  </div>
@@ -119,16 +126,16 @@
119
126
  <div class="alert alert-info mb-3" style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-3) var(--space-4);">
120
127
  <div style="font-size: 12px;">
121
128
  <% if RailsErrorDashboard::Services::CoverageTracker.active? %>
122
- <i class="bi bi-broadcast"></i> <strong>Coverage Active</strong> — expand source code to see executed lines
129
+ <i class="bi bi-broadcast"></i> <strong><%= red_t("red.errors.show.coverage.active_label") %></strong> — <%= red_t("red.errors.show.coverage.active_body") %>
123
130
  <% else %>
124
- <i class="bi bi-code-square"></i> <strong>Code Path Coverage</strong> — enable to see executed lines
131
+ <i class="bi bi-code-square"></i> <strong><%= red_t("red.errors.show.coverage.inactive_label") %></strong> — <%= red_t("red.errors.show.coverage.inactive_body") %>
125
132
  <% end %>
126
133
  </div>
127
134
  <div>
128
135
  <% if RailsErrorDashboard::Services::CoverageTracker.active? %>
129
- <%= button_to "Disable", disable_coverage_errors_path, method: :post, class: "btn btn-sm" %>
136
+ <%= button_to red_t("red.errors.show.coverage.disable"), disable_coverage_errors_path, method: :post, class: "btn btn-sm" %>
130
137
  <% else %>
131
- <%= button_to "Enable", enable_coverage_errors_path, method: :post, class: "btn btn-sm btn-primary" %>
138
+ <%= button_to red_t("red.errors.show.coverage.enable"), enable_coverage_errors_path, method: :post, class: "btn btn-sm btn-primary" %>
132
139
  <% end %>
133
140
  </div>
134
141
  </div>
@@ -178,24 +185,24 @@
178
185
  <% if baselines[:hourly].present? || baselines[:daily].present? || baselines[:weekly].present? %>
179
186
  <div class="card">
180
187
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-primary);">
181
- <span style="font-size: 13px; font-weight: 600;">Baseline Statistics</span>
188
+ <span style="font-size: 13px; font-weight: 600;"><%= red_t("red.errors.show.baselines.title") %></span>
182
189
  </div>
183
190
  <div style="padding: var(--space-3) var(--space-5);">
184
191
  <% anomaly = @error.baseline_anomaly %>
185
192
  <% if anomaly[:anomaly] %>
186
193
  <div class="alert alert-<%= anomaly[:level] == :critical ? 'danger' : anomaly[:level] == :high ? 'warning' : 'info' %>" style="padding: var(--space-2) var(--space-3); font-size: 12px; margin-bottom: var(--space-3);">
187
- <strong>Anomaly:</strong> <%= anomaly[:std_devs_above]&.round(1) %>σ above baseline (<%= anomaly[:level].to_s.upcase %>)
194
+ <strong><%= red_t("red.errors.show.baselines.anomaly_label") %></strong> <%= red_t("red.errors.show.baselines.anomaly_body", sigma: anomaly[:std_devs_above]&.round(1), level: red_t("red.errors.show.baselines.levels.#{anomaly[:level]}", default: anomaly[:level].to_s.upcase)) %>
188
195
  </div>
189
196
  <% end %>
190
197
  <% [:hourly, :daily, :weekly].each do |type| %>
191
198
  <% baseline = baselines[type] %>
192
199
  <% next unless baseline %>
193
200
  <div style="padding: var(--space-2) 0; border-bottom: 1px solid var(--border-primary); font-size: 12px;">
194
- <div style="color: var(--text-tertiary); font-weight: 500; text-transform: capitalize; margin-bottom: 4px;"><%= type %></div>
201
+ <div style="color: var(--text-tertiary); font-weight: 500; text-transform: capitalize; margin-bottom: 4px;"><%= red_t("red.errors.show.baselines.#{type}", default: type.to_s) %></div>
195
202
  <div style="display: flex; gap: var(--space-4);">
196
- <span>Mean: <strong><%= baseline.mean&.round(1) || 'N/A' %></strong></span>
197
- <span>σ: <strong><%= baseline.std_dev&.round(1) || 'N/A' %></strong></span>
198
- <span>P95: <strong><%= baseline.percentile_95&.round(1) || 'N/A' %></strong></span>
203
+ <span><%= red_t("red.errors.show.baselines.mean") %> <strong><%= baseline.mean&.round(1) || red_t("red.common.not_available") %></strong></span>
204
+ <span><%= red_t("red.errors.show.baselines.std_dev") %> <strong><%= baseline.std_dev&.round(1) || red_t("red.common.not_available") %></strong></span>
205
+ <span><%= red_t("red.errors.show.baselines.percentile_95") %> <strong><%= baseline.percentile_95&.round(1) || red_t("red.common.not_available") %></strong></span>
199
206
  </div>
200
207
  </div>
201
208
  <% end %>
@@ -207,24 +214,24 @@
207
214
  <!-- Quick Actions -->
208
215
  <div class="card">
209
216
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-primary);">
210
- <span style="font-size: 13px; font-weight: 600;">Quick Actions</span>
217
+ <span style="font-size: 13px; font-weight: 600;"><%= red_t("red.errors.show.quick_actions.title") %></span>
211
218
  </div>
212
219
  <div style="padding: var(--space-3) var(--space-5); display: flex; flex-direction: column; gap: var(--space-2);">
213
220
  <%= link_to errors_path(app_context.merge(error_type: @error.error_type, unresolved: '0')), class: "btn btn-sm" do %>
214
- <i class="bi bi-filter"></i> View Similar Errors
221
+ <i class="bi bi-filter"></i> <%= red_t("red.errors.show.quick_actions.view_similar") %>
215
222
  <% end %>
216
223
  <% if @error.user_id %>
217
224
  <%= link_to errors_path(app_context.merge(user_id: @error.user_id, unresolved: '0')), class: "btn btn-sm" do %>
218
- <i class="bi bi-person"></i> View User's Errors
225
+ <i class="bi bi-person"></i> <%= red_t("red.errors.show.quick_actions.view_user_errors") %>
219
226
  <% end %>
220
227
  <% end %>
221
228
  <% if @error.platform.present? %>
222
229
  <%= link_to errors_path(app_context.merge(platform: @error.platform, unresolved: '0')), class: "btn btn-sm" do %>
223
- <i class="bi bi-phone"></i> View <%= @error.platform %> Errors
230
+ <i class="bi bi-phone"></i> <%= red_t("red.errors.show.quick_actions.view_platform_errors", platform: @error.platform) %>
224
231
  <% end %>
225
232
  <% end %>
226
233
  <%= link_to analytics_errors_path(app_context), class: "btn btn-sm" do %>
227
- <i class="bi bi-bar-chart-line"></i> Analytics
234
+ <i class="bi bi-bar-chart-line"></i> <%= red_t("red.errors.show.quick_actions.analytics") %>
228
235
  <% end %>
229
236
  </div>
230
237
  </div>
@@ -1,35 +1,35 @@
1
- <% content_for :page_title, "Storm History" %>
1
+ <% content_for :page_title, red_t("red.errors.storms_page.title") %>
2
2
 
3
3
  <div>
4
4
  <div class="d-flex justify-content-between align-items-center mb-4">
5
5
  <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
6
  <i class="bi bi-cloud-lightning-rain me-2"></i>
7
- Storm History
7
+ <%= red_t("red.errors.storms_page.title") %>
8
8
  </h1>
9
9
  </div>
10
10
 
11
11
  <p class="text-muted" style="font-size: 13px; margin-bottom: var(--space-4);">
12
- When the error rate spikes (a bad deploy, a dependency outage), storm protection
13
- limits the gem's own database writes so it never amplifies the incident.
14
- Occurrences are always counted exactly — only per-event detail is sampled.
15
- Thresholds are configurable per process via <code>storm_*</code> options.
12
+ <%= red_t("red.errors.storms_page.intro_html", options: content_tag(:code, "storm_*")) %>
16
13
  </p>
17
14
 
18
15
  <% if @active_storm %>
19
16
  <div class="alert alert-danger" style="border-left: 4px solid var(--status-critical);">
20
17
  <i class="bi bi-cloud-lightning-rain-fill me-1"></i>
21
- <strong>Storm in progress</strong> since <%= @active_storm.started_at.strftime("%Y-%m-%d %H:%M %Z") %>
22
- peak rate <%= number_with_delimiter(@active_storm.peak_rate_per_minute) %>/min.
18
+ <%# One sentence rather than a <strong> label with fragments trailing
19
+ after it: the clause order does not hold in every language. %>
20
+ <%= red_t("red.errors.storms_page.active_html",
21
+ label: content_tag(:strong, red_t("red.errors.storms_page.active_label")),
22
+ time: @active_storm.started_at.strftime(red_time_format(:datetime)),
23
+ rate: red_t("red.errors.storms_page.rate_per_minute", value: number_with_delimiter(@active_storm.peak_rate_per_minute))) %>
23
24
  </div>
24
25
  <% end %>
25
26
 
26
27
  <% if @storm_events.empty? %>
27
28
  <div class="red-empty-state">
28
29
  <i class="bi bi-cloud-sun display-1 text-muted mb-3"></i>
29
- <div class="red-empty-state-title">No Storms Recorded</div>
30
+ <div class="red-empty-state-title"><%= red_t("red.errors.storms_page.empty_title") %></div>
30
31
  <p class="text-muted">
31
- Storm protection is standing by. If an error flood ever hits this app,
32
- episodes will appear here with exact counts of what was shed.
32
+ <%= red_t("red.errors.storms_page.empty_body") %>
33
33
  </p>
34
34
  </div>
35
35
  <% else %>
@@ -38,37 +38,43 @@
38
38
  <table class="table" style="margin: 0;">
39
39
  <thead>
40
40
  <tr>
41
- <th>Started</th>
42
- <th>Duration</th>
43
- <th>Peak Rate</th>
44
- <th>Mode</th>
45
- <th class="text-end">Counted (exact)</th>
46
- <th class="text-end">Overflow</th>
47
- <th>Top Errors</th>
41
+ <th><%= red_t("red.errors.storms_page.column_started") %></th>
42
+ <th><%= red_t("red.errors.storms_page.column_duration") %></th>
43
+ <th><%= red_t("red.errors.storms_page.column_peak_rate") %></th>
44
+ <th><%= red_t("red.errors.storms_page.column_mode") %></th>
45
+ <th class="text-end"><%= red_t("red.errors.storms_page.column_counted") %></th>
46
+ <th class="text-end"><%= red_t("red.errors.storms_page.column_overflow") %></th>
47
+ <th><%= red_t("red.errors.storms_page.column_top_errors") %></th>
48
48
  </tr>
49
49
  </thead>
50
50
  <tbody>
51
51
  <% @storm_events.each do |event| %>
52
52
  <tr>
53
53
  <td style="white-space: nowrap;">
54
- <%= event.started_at.strftime("%Y-%m-%d %H:%M") %>
54
+ <%= event.started_at.strftime(red_time_format(:datetime)) %>
55
55
  <% if event.active? %>
56
- <span class="badge bg-danger ms-1">active</span>
56
+ <span class="badge bg-danger ms-1"><%= red_t("red.errors.storms_page.active_badge") %></span>
57
57
  <% end %>
58
58
  </td>
59
59
  <td>
60
60
  <% if event.duration_seconds %>
61
+ <%# Duration#inspect emits English ("1 hour and 3 minutes").
62
+ Rails' distance_of_time_in_words would translate, but
63
+ through the HOST's I18n, which RED must not read — the
64
+ same limitation local_time_ago documents in
65
+ application_helper. Left English until RED owns a
66
+ duration formatter of its own. %>
61
67
  <%= ActiveSupport::Duration.build(event.duration_seconds).inspect %>
62
68
  <% else %>
63
69
  &mdash;
64
70
  <% end %>
65
71
  </td>
66
- <td><%= number_with_delimiter(event.peak_rate_per_minute) %>/min</td>
72
+ <td><%= red_t("red.errors.storms_page.rate_per_minute", value: number_with_delimiter(event.peak_rate_per_minute)) %></td>
67
73
  <td>
68
74
  <% if event.reached_open %>
69
- <span class="badge bg-danger">count-only</span>
75
+ <span class="badge bg-danger"><%= red_t("red.errors.storms_page.mode_count_only") %></span>
70
76
  <% else %>
71
- <span class="badge bg-warning text-dark">shedding</span>
77
+ <span class="badge bg-warning text-dark"><%= red_t("red.errors.storms_page.mode_shedding") %></span>
72
78
  <% end %>
73
79
  </td>
74
80
  <td class="text-end"><%= number_with_delimiter(event.events_counted_only) %></td>
@@ -77,7 +83,7 @@
77
83
  <% event.top_fingerprints_list.first(3).each do |fp| %>
78
84
  <div style="font-size: 12px; font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<%= fp["message"] %>">
79
85
  <strong><%= fp["class"] %></strong>
80
- <span class="text-muted">&times;<%= number_with_delimiter(fp["count"]) %></span>
86
+ <span class="text-muted"><%= red_t("red.errors.storms_page.occurrence_multiplier", count: number_with_delimiter(fp["count"])) %></span>
81
87
  </div>
82
88
  <% end %>
83
89
  </td>
@@ -1,21 +1,21 @@
1
- <% content_for :page_title, "Swallowed Exceptions" %>
1
+ <% content_for :page_title, red_t("red.errors.swallowed_page.title") %>
2
2
 
3
3
  <div>
4
4
  <div class="d-flex justify-content-between align-items-center mb-4">
5
5
  <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
6
  <i class="bi bi-eye-slash me-2"></i>
7
- Swallowed Exceptions
7
+ <%= red_t("red.errors.swallowed_page.title") %>
8
8
  </h1>
9
9
 
10
10
  <div class="btn-group" role="group">
11
11
  <%= link_to swallowed_exceptions_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
- 7 Days
12
+ <%= red_t("red.common.days_filter.7") %>
13
13
  <% end %>
14
14
  <%= link_to swallowed_exceptions_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
- 30 Days
15
+ <%= red_t("red.common.days_filter.30") %>
16
16
  <% end %>
17
17
  <%= link_to swallowed_exceptions_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
- 90 Days
18
+ <%= red_t("red.common.days_filter.90") %>
19
19
  <% end %>
20
20
  </div>
21
21
  </div>
@@ -23,18 +23,18 @@
23
23
  <% if @unique_count == 0 %>
24
24
  <div class="red-empty-state">
25
25
  <div class="red-empty-state-icon" style="background: var(--status-success-bg); color: var(--status-success);"><i class="bi bi-check-lg"></i></div>
26
- <div class="red-empty-state-title">No Swallowed Exceptions Detected</div>
26
+ <div class="red-empty-state-title"><%= red_t("red.errors.swallowed_page.empty_title") %></div>
27
27
  <p class="text-muted">
28
- No exceptions with a rescue ratio above <%= (RailsErrorDashboard.configuration.swallowed_exception_threshold * 100).round %>% were detected in the last <%= @days %> days.
28
+ <%= red_t("red.errors.swallowed_page.empty_body", threshold: (RailsErrorDashboard.configuration.swallowed_exception_threshold * 100).round, days: @days) %>
29
29
  </p>
30
30
  <div class="card mx-auto" style="max-width: 500px;">
31
31
  <div class="card-body text-start">
32
- <h6>How swallowed exceptions are detected:</h6>
32
+ <h6><%= red_t("red.errors.swallowed_page.how_title") %></h6>
33
33
  <ul class="mb-0">
34
- <li>TracePoint(:raise) counts every exception raised</li>
35
- <li>TracePoint(:rescue) counts every exception rescued</li>
36
- <li>High rescue/raise ratio = likely swallowed (silently rescued)</li>
37
- <li>Requires Ruby 3.3+ and <code>detect_swallowed_exceptions = true</code></li>
34
+ <li><%= red_t("red.errors.swallowed_page.how_raise") %></li>
35
+ <li><%= red_t("red.errors.swallowed_page.how_rescue") %></li>
36
+ <li><%= red_t("red.errors.swallowed_page.how_ratio") %></li>
37
+ <li><%= red_t("red.errors.swallowed_page.how_requires_html", option: content_tag(:code, "detect_swallowed_exceptions = true")) %></li>
38
38
  </ul>
39
39
  </div>
40
40
  </div>
@@ -45,7 +45,7 @@
45
45
  <div class="card text-center">
46
46
  <div class="card-body">
47
47
  <div class="display-6 text-danger"><%= @unique_count %></div>
48
- <small class="text-muted">Swallowed Patterns</small>
48
+ <small class="text-muted"><%= red_t("red.errors.swallowed_page.swallowed_patterns") %></small>
49
49
  </div>
50
50
  </div>
51
51
  </div>
@@ -53,7 +53,7 @@
53
53
  <div class="card text-center">
54
54
  <div class="card-body">
55
55
  <div class="display-6 text-warning"><%= number_with_delimiter(@total_rescue_count) %></div>
56
- <small class="text-muted">Total Rescues</small>
56
+ <small class="text-muted"><%= red_t("red.errors.swallowed_page.total_rescues") %></small>
57
57
  </div>
58
58
  </div>
59
59
  </div>
@@ -61,7 +61,7 @@
61
61
  <div class="card text-center">
62
62
  <div class="card-body">
63
63
  <div class="display-6 text-info"><%= number_with_delimiter(@total_raise_count) %></div>
64
- <small class="text-muted">Total Raises</small>
64
+ <small class="text-muted"><%= red_t("red.errors.swallowed_page.total_raises") %></small>
65
65
  </div>
66
66
  </div>
67
67
  </div>
@@ -71,7 +71,7 @@
71
71
  <div class="card-header d-flex justify-content-between align-items-center">
72
72
  <h5 class="mb-0">
73
73
  <i class="bi bi-eye-slash text-danger me-2"></i>
74
- Swallowed Exception Patterns
74
+ <%= red_t("red.errors.swallowed_page.all_title") %>
75
75
  <span class="badge bg-danger"><%= @unique_count %></span>
76
76
  </h5>
77
77
  <small class="text-muted"><%== @pagy.info_tag %></small>
@@ -81,25 +81,25 @@
81
81
  <table class="table table-hover mb-0">
82
82
  <thead>
83
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>
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;"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.swallowed_page.column_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"><%= red_t("red.errors.pages.column_last_seen") %></th>
91
91
  </tr>
92
92
  </thead>
93
93
  <tbody>
94
94
  <% @entries.each do |entry| %>
95
95
  <tr>
96
96
  <td>
97
- <%= link_to errors_path(error_type: entry[:exception_class], unresolved: '0'), class: "text-decoration-none", title: "View any unrescued #{entry[:exception_class]} errors that leaked into error_log" do %>
97
+ <%= link_to errors_path(error_type: entry[:exception_class], unresolved: '0'), class: "text-decoration-none", title: red_t("red.errors.swallowed_page.view_unrescued", exception: entry[:exception_class]) do %>
98
98
  <code style="font-size: 0.85em;"><%= entry[:exception_class] %></code>
99
99
  <% end %>
100
100
  </td>
101
101
  <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:raise_location] %></td>
102
- <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:rescue_location] || "Unknown" %></td>
102
+ <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:rescue_location] || red_t("red.common.unknown") %></td>
103
103
  <td><span class="badge bg-info text-dark"><%= number_with_delimiter(entry[:raise_count]) %></span></td>
104
104
  <td><span class="badge bg-warning text-dark"><%= number_with_delimiter(entry[:rescue_count]) %></span></td>
105
105
  <td>
@@ -118,7 +118,7 @@
118
118
  <div class="card-footer border-top d-flex justify-content-between align-items-center">
119
119
  <div>
120
120
  <small class="text-muted">
121
- <i class="bi bi-lightbulb text-warning"></i> Swallowed exceptions are raised then silently rescued. They may hide real bugs.
121
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.swallowed_page.footer_tip") %>
122
122
  </small>
123
123
  </div>
124
124
  <div>