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
@@ -8,25 +8,25 @@
8
8
  <div class="card mb-4" id="section-deprecation-warnings" style="border-color: var(--bs-danger);">
9
9
  <div class="card-header">
10
10
  <h5 class="mb-0">
11
- <i class="bi bi-exclamation-triangle text-danger"></i> Deprecation Warnings
11
+ <i class="bi bi-exclamation-triangle text-danger"></i> <%= red_t("red.errors.deprecations.title") %>
12
12
  <span class="badge bg-danger"><%= deprecation_crumbs.size %></span>
13
13
  </h5>
14
- <small class="text-muted">Rails deprecation warnings detected during this request</small>
14
+ <small class="text-muted"><%= red_t("red.errors.deprecations.hint") %></small>
15
15
  </div>
16
16
  <div class="card-body p-0">
17
17
  <div class="table-responsive">
18
18
  <table class="table table-sm table-hover mb-0">
19
19
  <thead class="table-light">
20
20
  <tr>
21
- <th>Warning</th>
22
- <th width="300">Source</th>
21
+ <th><%= red_t("red.errors.deprecations.column_warning") %></th>
22
+ <th width="300"><%= red_t("red.errors.deprecations.column_source") %></th>
23
23
  </tr>
24
24
  </thead>
25
25
  <tbody>
26
26
  <% deprecation_crumbs.each do |crumb| %>
27
27
  <tr>
28
28
  <td><code style="font-size: 0.85em;"><%= crumb["m"] %></code></td>
29
- <td><small class="text-muted"><%= crumb.dig("meta", "caller") || "N/A" %></small></td>
29
+ <td><small class="text-muted"><%= crumb.dig("meta", "caller") || red_t("red.common.not_available") %></small></td>
30
30
  </tr>
31
31
  <% end %>
32
32
  </tbody>
@@ -35,11 +35,11 @@
35
35
  </div>
36
36
  <div class="card-footer bg-white border-top">
37
37
  <small class="text-muted">
38
- <i class="bi bi-lightbulb text-warning"></i> Fix deprecations before upgrading Rails to avoid breaking changes.
38
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.deprecations.footer_tip") %>
39
39
  </small>
40
40
  <small class="float-end">
41
41
  <a href="https://guides.rubyonrails.org/upgrading_ruby_on_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
42
- <i class="bi bi-book"></i> Rails Upgrade Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
42
+ <i class="bi bi-book"></i> <%= red_t("red.errors.deprecations.upgrade_guide") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
43
43
  </a>
44
44
  </small>
45
45
  </div>
@@ -52,19 +52,19 @@
52
52
  <div class="card mb-4" id="section-n-plus-one" style="border-color: var(--bs-warning);">
53
53
  <div class="card-header">
54
54
  <h5 class="mb-0">
55
- <i class="bi bi-arrow-repeat text-warning"></i> N+1 Query Detection
56
- <span class="badge bg-warning text-dark"><%= n_plus_one_patterns.size %> pattern<%= n_plus_one_patterns.size == 1 ? "" : "s" %></span>
55
+ <i class="bi bi-arrow-repeat text-warning"></i> <%= red_t("red.errors.n_plus_one.title") %>
56
+ <span class="badge bg-warning text-dark"><%= red_tp("red.errors.n_plus_one.patterns", count: n_plus_one_patterns.size) %></span>
57
57
  </h5>
58
- <small class="text-muted">Repeated query patterns detected in this request's SQL breadcrumbs</small>
58
+ <small class="text-muted"><%= red_t("red.errors.n_plus_one.hint") %></small>
59
59
  </div>
60
60
  <div class="card-body p-0">
61
61
  <div class="table-responsive">
62
62
  <table class="table table-sm table-hover mb-0">
63
63
  <thead class="table-light">
64
64
  <tr>
65
- <th width="80">Repeats</th>
66
- <th>Query Pattern</th>
67
- <th width="100">Total Time</th>
65
+ <th width="80"><%= red_t("red.errors.n_plus_one.column_repeats") %></th>
66
+ <th><%= red_t("red.errors.n_plus_one.column_query") %></th>
67
+ <th width="100"><%= red_t("red.errors.n_plus_one.column_total_time") %></th>
68
68
  </tr>
69
69
  </thead>
70
70
  <tbody>
@@ -73,14 +73,21 @@
73
73
  <td><span class="badge bg-warning text-dark"><%= pattern[:count] %>x</span></td>
74
74
  <td>
75
75
  <code style="word-break: break-all; font-size: 0.85em;"><%= truncate(pattern[:sample_query], length: 200) %></code>
76
- <br><small class="text-muted">Fingerprint: <%= truncate(pattern[:fingerprint], length: 120) %></small>
76
+ <br><small class="text-muted"><%= red_t("red.errors.n_plus_one.fingerprint", fingerprint: truncate(pattern[:fingerprint], length: 120)) %></small>
77
77
  <% table_name = extract_table_from_sql(pattern[:sample_query]) %>
78
78
  <br><small class="text-muted">
79
79
  <i class="bi bi-lightbulb text-warning"></i>
80
+ <%# Method names stay English; only the words
81
+ around them are translated. %>
80
82
  <% if table_name %>
81
- Tip: Preload <strong><%= table_name %></strong> with <code>.includes(:association)</code> or <code>.preload(:association)</code>
83
+ <%= red_t("red.errors.n_plus_one.tip_preload_html",
84
+ table: tag.strong(table_name),
85
+ includes: tag.code(".includes(:association)"),
86
+ preload: tag.code(".preload(:association)")) %>
82
87
  <% else %>
83
- Tip: Use <code>.includes</code> or <code>.preload</code> to eager load associations
88
+ <%= red_t("red.errors.n_plus_one.tip_generic_html",
89
+ includes: tag.code(".includes"),
90
+ preload: tag.code(".preload")) %>
84
91
  <% end %>
85
92
  </small>
86
93
  </td>
@@ -98,7 +105,7 @@
98
105
  <div class="card-footer bg-white border-top">
99
106
  <small class="float-end">
100
107
  <a href="https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations" target="_blank" rel="noopener" class="text-decoration-none">
101
- <i class="bi bi-book"></i> Rails Eager Loading Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
108
+ <i class="bi bi-book"></i> <%= red_t("red.errors.n_plus_one.eager_loading_guide") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
102
109
  </a>
103
110
  </small>
104
111
  </div>
@@ -111,30 +118,30 @@
111
118
  <div class="card mb-4" id="section-cache-health" style="border-color: var(--bs-info);">
112
119
  <div class="card-header">
113
120
  <h5 class="mb-0">
114
- <i class="bi bi-lightning-charge text-info"></i> Cache Health
115
- <span class="badge bg-info text-dark"><%= cache_analysis[:reads] + cache_analysis[:writes] %> operations</span>
121
+ <i class="bi bi-lightning-charge text-info"></i> <%= red_t("red.errors.cache_health.title") %>
122
+ <span class="badge bg-info text-dark"><%= red_tp("red.errors.cache_health.operations", count: cache_analysis[:reads] + cache_analysis[:writes]) %></span>
116
123
  </h5>
117
- <small class="text-muted">Cache activity analysis from this request's breadcrumbs</small>
124
+ <small class="text-muted"><%= red_t("red.errors.cache_health.hint") %></small>
118
125
  </div>
119
126
  <div class="card-body">
120
127
  <div class="row text-center mb-3">
121
128
  <div class="col-4">
122
129
  <div class="fs-4 fw-bold"><%= cache_analysis[:reads] %></div>
123
- <small class="text-muted">Reads</small>
130
+ <small class="text-muted"><%= red_t("red.errors.cache_health.reads") %></small>
124
131
  </div>
125
132
  <div class="col-4">
126
133
  <div class="fs-4 fw-bold"><%= cache_analysis[:writes] %></div>
127
- <small class="text-muted">Writes</small>
134
+ <small class="text-muted"><%= red_t("red.errors.cache_health.writes") %></small>
128
135
  </div>
129
136
  <div class="col-4">
130
137
  <% if cache_analysis[:hit_rate] %>
131
138
  <div class="fs-4 fw-bold text-<%= cache_analysis[:hit_rate] >= 80 ? 'success' : cache_analysis[:hit_rate] >= 50 ? 'warning' : 'danger' %>">
132
139
  <%= cache_analysis[:hit_rate] %>%
133
140
  </div>
134
- <small class="text-muted">Hit Rate</small>
141
+ <small class="text-muted"><%= red_t("red.errors.cache_health.hit_rate") %></small>
135
142
  <% else %>
136
143
  <div class="fs-4 fw-bold text-muted">--</div>
137
- <small class="text-muted">Hit Rate</small>
144
+ <small class="text-muted"><%= red_t("red.errors.cache_health.hit_rate") %></small>
138
145
  <% end %>
139
146
  </div>
140
147
  </div>
@@ -142,8 +149,8 @@
142
149
  <% if cache_analysis[:hits] > 0 || cache_analysis[:misses] > 0 %>
143
150
  <div class="mb-3">
144
151
  <div class="d-flex justify-content-between mb-1">
145
- <small><span class="badge bg-success"><%= cache_analysis[:hits] %> hits</span></small>
146
- <small><span class="badge bg-danger"><%= cache_analysis[:misses] %> misses</span></small>
152
+ <small><span class="badge bg-success"><%= red_tp("red.errors.cache_health.hits", count: cache_analysis[:hits]) %></span></small>
153
+ <small><span class="badge bg-danger"><%= red_tp("red.errors.cache_health.misses", count: cache_analysis[:misses]) %></span></small>
147
154
  </div>
148
155
  <% hit_pct = cache_analysis[:hit_rate] || 0 %>
149
156
  <div class="progress" style="height: 6px;">
@@ -155,14 +162,14 @@
155
162
 
156
163
  <% if cache_analysis[:unknown] > 0 %>
157
164
  <small class="text-muted d-block mb-2">
158
- <i class="bi bi-info-circle"></i> <%= cache_analysis[:unknown] %> read<%= cache_analysis[:unknown] == 1 ? "" : "s" %> with unknown hit status (older breadcrumbs)
165
+ <i class="bi bi-info-circle"></i> <%= red_tp("red.errors.cache_health.unknown_status", count: cache_analysis[:unknown]) %>
159
166
  </small>
160
167
  <% end %>
161
168
 
162
169
  <div class="d-flex justify-content-between">
163
- <small class="text-muted">Total cache time: <strong><%= "%.1f" % cache_analysis[:total_duration_ms] %>ms</strong></small>
170
+ <small class="text-muted"><%= red_t("red.errors.cache_health.total_time_html", duration: tag.strong("#{"%.1f" % cache_analysis[:total_duration_ms]}ms")) %></small>
164
171
  <% if cache_analysis[:slowest] && cache_analysis[:slowest][:duration_ms] > 0 %>
165
- <small class="text-muted">Slowest: <strong><%= "%.1f" % cache_analysis[:slowest][:duration_ms] %>ms</strong></small>
172
+ <small class="text-muted"><%= red_t("red.errors.cache_health.slowest_html", duration: tag.strong("#{"%.1f" % cache_analysis[:slowest][:duration_ms]}ms")) %></small>
166
173
  <% end %>
167
174
  </div>
168
175
 
@@ -171,9 +178,9 @@
171
178
  <small>
172
179
  <i class="bi bi-lightbulb text-warning"></i>
173
180
  <% if cache_analysis[:hit_rate] < 50 %>
174
- Low hit rate — review cache key design and TTL settings.
181
+ <%= red_t("red.errors.cache_health.low_hit_rate") %>
175
182
  <% else %>
176
- Moderate hit rate — consider warming caches or adjusting expiration.
183
+ <%= red_t("red.errors.cache_health.moderate_hit_rate") %>
177
184
  <% end %>
178
185
  </small>
179
186
  </div>
@@ -182,7 +189,7 @@
182
189
  <div class="card-footer bg-white border-top">
183
190
  <small class="float-end">
184
191
  <a href="https://guides.rubyonrails.org/caching_with_rails.html" target="_blank" rel="noopener" class="text-decoration-none">
185
- <i class="bi bi-book"></i> Rails Caching Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
192
+ <i class="bi bi-book"></i> <%= red_t("red.errors.cache_health.caching_guide") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
186
193
  </a>
187
194
  </small>
188
195
  </div>
@@ -192,10 +199,10 @@
192
199
  <div class="card mb-4" id="section-breadcrumbs">
193
200
  <div class="card-header">
194
201
  <h5 class="mb-0">
195
- <i class="bi bi-signpost-2"></i> Breadcrumbs
196
- <span class="badge bg-secondary"><%= breadcrumbs.size %> events</span>
202
+ <i class="bi bi-signpost-2"></i> <%= red_t("red.errors.breadcrumbs.title") %>
203
+ <span class="badge bg-secondary"><%= red_tp("red.errors.breadcrumbs.events", count: breadcrumbs.size) %></span>
197
204
  </h5>
198
- <small class="text-muted">Activity trail leading up to this error</small>
205
+ <small class="text-muted"><%= red_t("red.errors.breadcrumbs.hint") %></small>
199
206
  </div>
200
207
  <div class="card-body p-0" style="max-height: 400px; overflow-y: auto;">
201
208
  <div class="table-responsive">
@@ -203,9 +210,9 @@
203
210
  <thead class="table-light">
204
211
  <tr>
205
212
  <th width="60">#</th>
206
- <th width="100">Category</th>
207
- <th>Message</th>
208
- <th width="100">Duration</th>
213
+ <th width="100"><%= red_t("red.errors.breadcrumbs.column_category") %></th>
214
+ <th><%= red_t("red.errors.breadcrumbs.column_message") %></th>
215
+ <th width="100"><%= red_t("red.errors.breadcrumbs.column_duration") %></th>
209
216
  </tr>
210
217
  </thead>
211
218
  <tbody>
@@ -1,25 +1,28 @@
1
1
  <!-- Co-occurring Errors -->
2
2
  <% if RailsErrorDashboard.configuration.enable_co_occurring_errors && error.respond_to?(:co_occurring_errors) %>
3
- <% co_occurring = error.co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 5) %>
3
+ <%# The window feeds both the query and the hint below, so it is named
4
+ once rather than written as a literal in the prose too. %>
5
+ <% co_occurring_window_minutes = 5 %>
6
+ <% co_occurring = error.co_occurring_errors(window_minutes: co_occurring_window_minutes, min_frequency: 2, limit: 5) %>
4
7
  <% if co_occurring.any? %>
5
8
  <div class="card mb-4" id="section-co-occurring">
6
9
  <div class="card-header">
7
10
  <h5 class="mb-0">
8
- <i class="bi bi-clock-history"></i> Co-occurring Errors
9
- <span class="badge bg-info text-dark">Time Patterns</span>
11
+ <i class="bi bi-clock-history"></i> <%= red_t("red.errors.co_occurring.title") %>
12
+ <span class="badge bg-info text-dark"><%= red_t("red.errors.co_occurring.badge") %></span>
10
13
  </h5>
11
- <small class="text-muted">Errors that occur within 5 minutes of this error</small>
14
+ <small class="text-muted"><%= red_t("red.errors.co_occurring.hint", minutes: co_occurring_window_minutes) %></small>
12
15
  </div>
13
16
  <div class="card-body p-0">
14
17
  <div class="table-responsive">
15
18
  <table class="table table-hover mb-0">
16
19
  <thead class="table-light">
17
20
  <tr>
18
- <th>Error Type</th>
19
- <th>Message</th>
20
- <th>Platform</th>
21
- <th>Frequency</th>
22
- <th>Avg Delay</th>
21
+ <th><%= red_t("red.errors.co_occurring.column_error_type") %></th>
22
+ <th><%= red_t("red.errors.co_occurring.column_message") %></th>
23
+ <th><%= red_t("red.errors.co_occurring.column_platform") %></th>
24
+ <th><%= red_t("red.errors.co_occurring.column_frequency") %></th>
25
+ <th><%= red_t("red.errors.co_occurring.column_avg_delay") %></th>
23
26
  <th></th>
24
27
  </tr>
25
28
  </thead>
@@ -44,20 +47,20 @@
44
47
  </td>
45
48
  <td>
46
49
  <span class="badge bg-info text-dark">
47
- <%= frequency %> time<%= frequency > 1 ? 's' : '' %>
50
+ <%= red_tp("red.errors.co_occurring.times", count: frequency) %>
48
51
  </span>
49
52
  </td>
50
53
  <td>
51
54
  <% if avg_delay < 0 %>
52
- <span class="text-warning"><%= number_to_human(avg_delay.abs, units: {unit: "s", thousand: "s"}, precision: 1) %> before</span>
55
+ <span class="text-warning"><%= red_t("red.errors.co_occurring.before", duration: number_to_human(avg_delay.abs, units: { unit: "s", thousand: "s" }, precision: 1)) %></span>
53
56
  <% elsif avg_delay > 0 %>
54
- <span class="text-success"><%= number_to_human(avg_delay, units: {unit: "s", thousand: "s"}, precision: 1) %> after</span>
57
+ <span class="text-success"><%= red_t("red.errors.co_occurring.after", duration: number_to_human(avg_delay, units: { unit: "s", thousand: "s" }, precision: 1)) %></span>
55
58
  <% else %>
56
- <span class="text-muted">simultaneous</span>
59
+ <span class="text-muted"><%= red_t("red.errors.co_occurring.simultaneous") %></span>
57
60
  <% end %>
58
61
  </td>
59
62
  <td class="text-end">
60
- <%= link_to "View", error_path(error_record), class: "btn btn-sm btn-outline-primary" %>
63
+ <%= link_to red_t("red.errors.co_occurring.view"), error_path(error_record), class: "btn btn-sm btn-outline-primary" %>
61
64
  </td>
62
65
  </tr>
63
66
  <% end %>
@@ -3,17 +3,35 @@
3
3
  <% platform_comments = @platform_comments || [] %>
4
4
  <% issue_tracking_enabled = RailsErrorDashboard.configuration.enable_issue_tracking %>
5
5
 
6
+ <%#
7
+ Provider display name. github -> "GitHub", not provider.capitalize's "Github".
8
+ An unrecognised provider falls back to its raw value; a missing one falls back
9
+ to the translated generic noun.
10
+ %>
11
+ <%
12
+ discussion_provider = if error.external_issue_provider.present?
13
+ red_t("red.errors.providers.#{error.external_issue_provider}",
14
+ default: error.external_issue_provider.to_s)
15
+ else
16
+ red_t("red.errors.providers.generic")
17
+ end
18
+ discussion_provider_lc = if error.external_issue_provider.present?
19
+ discussion_provider
20
+ else
21
+ red_t("red.errors.providers.generic_lowercase")
22
+ end
23
+ %>
6
24
  <% if has_linked_issue && (platform_comments.any? || has_audit_comments) %>
7
25
  <div class="card mb-4" id="section-discussion">
8
26
  <div class="card-header d-flex justify-content-between align-items-center">
9
27
  <h5 class="mb-0">
10
- <i class="bi bi-chat-dots"></i> Discussion
28
+ <i class="bi bi-chat-dots"></i> <%= red_t("red.errors.discussion.title") %>
11
29
  <% if platform_comments.any? %>
12
30
  <span class="badge bg-secondary ms-2"><%= platform_comments.size %></span>
13
31
  <% end %>
14
32
  </h5>
15
33
  <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
16
- <i class="bi bi-box-arrow-up-right me-1"></i> Reply on <%= error.external_issue_provider&.capitalize || "Platform" %>
34
+ <i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.discussion.reply_on", provider: discussion_provider) %>
17
35
  </a>
18
36
  </div>
19
37
  <div class="card-body p-0">
@@ -28,8 +46,8 @@
28
46
  <i class="bi bi-person-circle" style="font-size: 1.3em; flex-shrink: 0;"></i>
29
47
  <% end %>
30
48
  <div>
31
- <strong class="text-primary"><%= comment[:author] || "Unknown" %></strong>
32
- <small class="text-muted ms-2"><%= comment[:created_at] ? Time.parse(comment[:created_at]).strftime("%b %d, %Y %H:%M") : "" %></small>
49
+ <strong class="text-primary"><%= comment[:author] || red_t("red.common.unknown") %></strong>
50
+ <small class="text-muted ms-2"><%= comment[:created_at] ? Time.parse(comment[:created_at]).strftime(red_time_format(:datetime)) : "" %></small>
33
51
  </div>
34
52
  </div>
35
53
  <div class="ms-4 text-break" style="font-size: 0.9em;">
@@ -43,7 +61,7 @@
43
61
  <% if has_audit_comments %>
44
62
  <!-- Audit trail — workflow actions -->
45
63
  <div class="px-3 py-2 bg-light border-top">
46
- <small class="text-muted fw-bold"><i class="bi bi-clock-history"></i> Activity Log</small>
64
+ <small class="text-muted fw-bold"><i class="bi bi-clock-history"></i> <%= red_t("red.errors.discussion.activity_log") %></small>
47
65
  </div>
48
66
  <% error.comments.recent_first.each_with_index do |comment, index| %>
49
67
  <div class="px-3 py-2 <%= index < error.comments.count - 1 ? 'border-bottom' : '' %>">
@@ -64,14 +82,14 @@
64
82
  <div class="card mb-4" id="section-discussion">
65
83
  <div class="card-header d-flex justify-content-between align-items-center">
66
84
  <h5 class="mb-0">
67
- <i class="bi bi-chat-dots"></i> Discussion
85
+ <i class="bi bi-chat-dots"></i> <%= red_t("red.errors.discussion.title") %>
68
86
  </h5>
69
87
  <a href="<%= error.external_issue_url %>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-primary">
70
- <i class="bi bi-box-arrow-up-right me-1"></i> Start discussion on <%= error.external_issue_provider&.capitalize || "Platform" %>
88
+ <i class="bi bi-box-arrow-up-right me-1"></i> <%= red_t("red.errors.discussion.start_on", provider: discussion_provider) %>
71
89
  </a>
72
90
  </div>
73
91
  <div class="card-body text-center py-3">
74
- <p class="text-muted small mb-0">No comments yet. Be the first to discuss this error on <%= error.external_issue_provider&.capitalize || "the platform" %>.</p>
92
+ <p class="text-muted small mb-0"><%= red_t("red.errors.discussion.empty", provider: discussion_provider_lc) %></p>
75
93
  </div>
76
94
  </div>
77
95
  <% elsif has_audit_comments %>
@@ -79,7 +97,7 @@
79
97
  <div class="card mb-4" id="section-discussion">
80
98
  <div class="card-header">
81
99
  <h5 class="mb-0">
82
- <i class="bi bi-clock-history"></i> Activity Log
100
+ <i class="bi bi-clock-history"></i> <%= red_t("red.errors.discussion.activity_log") %>
83
101
  </h5>
84
102
  </div>
85
103
  <div class="card-body p-0">
@@ -1,12 +1,16 @@
1
1
  <!-- Error Cascades -->
2
2
  <% if RailsErrorDashboard.configuration.enable_error_cascades %>
3
3
  <% cascades = error.error_cascades %>
4
+ <%# The thresholds are named once: they drive the badge colour on each
5
+ row AND the footnote that cites them, which must not drift apart. %>
6
+ <% strong_cascade_probability = 70 %>
7
+ <% strong_cascade_frequency = 3 %>
4
8
  <% if cascades[:parents].any? || cascades[:children].any? %>
5
9
  <div class="card" id="section-error-cascades">
6
10
  <div class="card-header">
7
11
  <h5 class="mb-0">
8
- <i class="bi bi-diagram-3"></i> Error Cascades
9
- <small class="text-muted ms-2">Causal relationships between errors</small>
12
+ <i class="bi bi-diagram-3"></i> <%= red_t("red.errors.cascades.title") %>
13
+ <small class="text-muted ms-2"><%= red_t("red.errors.cascades.hint") %></small>
10
14
  </h5>
11
15
  </div>
12
16
  <div class="card-body">
@@ -14,18 +18,18 @@
14
18
  <% if cascades[:parents].any? %>
15
19
  <div class="mb-4">
16
20
  <h6 class="text-danger">
17
- <i class="bi bi-arrow-down-circle"></i> Triggered By
18
- <small class="text-muted">(errors that cause this one)</small>
21
+ <i class="bi bi-arrow-down-circle"></i> <%= red_t("red.errors.cascades.triggered_by") %>
22
+ <small class="text-muted"><%= red_t("red.errors.cascades.triggered_by_hint") %></small>
19
23
  </h6>
20
24
  <div class="table-responsive">
21
25
  <table class="table table-sm table-hover mb-0">
22
26
  <thead class="table-light">
23
27
  <tr>
24
- <th>Error Type</th>
25
- <th>Message</th>
26
- <th>Probability</th>
27
- <th>Frequency</th>
28
- <th>Avg Delay</th>
28
+ <th><%= red_t("red.errors.cascades.column_error_type") %></th>
29
+ <th><%= red_t("red.errors.cascades.column_message") %></th>
30
+ <th><%= red_t("red.errors.cascades.column_probability") %></th>
31
+ <th><%= red_t("red.errors.cascades.column_frequency") %></th>
32
+ <th><%= red_t("red.errors.cascades.column_avg_delay") %></th>
29
33
  <th></th>
30
34
  </tr>
31
35
  </thead>
@@ -45,7 +49,7 @@
45
49
  </td>
46
50
  <td>
47
51
  <% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
48
- <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
52
+ <span class="badge <%= probability_pct >= strong_cascade_probability ? 'bg-danger' : 'bg-warning' %>">
49
53
  <%= probability_pct %>%
50
54
  </span>
51
55
  </td>
@@ -58,7 +62,7 @@
58
62
  </small>
59
63
  </td>
60
64
  <td>
61
- <%= link_to "View", error_path(parent), class: "btn btn-sm btn-outline-primary" %>
65
+ <%= link_to red_t("red.errors.cascades.view"), error_path(parent), class: "btn btn-sm btn-outline-primary" %>
62
66
  </td>
63
67
  </tr>
64
68
  <% end %>
@@ -72,18 +76,18 @@
72
76
  <% if cascades[:children].any? %>
73
77
  <div>
74
78
  <h6 class="text-warning">
75
- <i class="bi bi-arrow-up-circle"></i> Triggers
76
- <small class="text-muted">(errors caused by this one)</small>
79
+ <i class="bi bi-arrow-up-circle"></i> <%= red_t("red.errors.cascades.triggers") %>
80
+ <small class="text-muted"><%= red_t("red.errors.cascades.triggers_hint") %></small>
77
81
  </h6>
78
82
  <div class="table-responsive">
79
83
  <table class="table table-sm table-hover mb-0">
80
84
  <thead class="table-light">
81
85
  <tr>
82
- <th>Error Type</th>
83
- <th>Message</th>
84
- <th>Probability</th>
85
- <th>Frequency</th>
86
- <th>Avg Delay</th>
86
+ <th><%= red_t("red.errors.cascades.column_error_type") %></th>
87
+ <th><%= red_t("red.errors.cascades.column_message") %></th>
88
+ <th><%= red_t("red.errors.cascades.column_probability") %></th>
89
+ <th><%= red_t("red.errors.cascades.column_frequency") %></th>
90
+ <th><%= red_t("red.errors.cascades.column_avg_delay") %></th>
87
91
  <th></th>
88
92
  </tr>
89
93
  </thead>
@@ -103,7 +107,7 @@
103
107
  </td>
104
108
  <td>
105
109
  <% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
106
- <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
110
+ <span class="badge <%= probability_pct >= strong_cascade_probability ? 'bg-danger' : 'bg-warning' %>">
107
111
  <%= probability_pct %>%
108
112
  </span>
109
113
  </td>
@@ -116,7 +120,7 @@
116
120
  </small>
117
121
  </td>
118
122
  <td>
119
- <%= link_to "View", error_path(child), class: "btn btn-sm btn-outline-primary" %>
123
+ <%= link_to red_t("red.errors.cascades.view"), error_path(child), class: "btn btn-sm btn-outline-primary" %>
120
124
  </td>
121
125
  </tr>
122
126
  <% end %>
@@ -129,7 +133,7 @@
129
133
  <div class="mt-3">
130
134
  <small class="text-muted">
131
135
  <i class="bi bi-info-circle"></i>
132
- Cascade patterns show causal relationships. High probability (≥70%) and frequency (≥3) indicate strong cascades.
136
+ <%= red_t("red.errors.cascades.footnote", probability: strong_cascade_probability, frequency: strong_cascade_frequency) %>
133
137
  </small>
134
138
  </div>
135
139
  </div>