rails_error_dashboard 0.8.4 → 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 +107 -4
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +91 -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 +30 -29
  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 +10 -3
  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,9 +1,9 @@
1
- <% content_for :page_title, "Dashboard" %>
1
+ <% content_for :page_title, red_t("red.errors.overview_page.page_title") %>
2
2
  <div style="max-width: 100%;">
3
3
  <div class="d-flex justify-content-between align-items-center mb-4">
4
- <h1 style="font-size: 20px; font-weight: 700; margin: 0;">Overview</h1>
4
+ <h1 style="font-size: 20px; font-weight: 700; margin: 0;"><%= red_t("red.errors.overview_page.title") %></h1>
5
5
  <span style="font-size: 12px; color: var(--text-tertiary);">
6
- Last updated: <%= local_time(Time.current, format: :full) %>
6
+ <%= red_t("red.errors.overview_page.last_updated_html", time: local_time(Time.current, format: :full)) %>
7
7
  </span>
8
8
  </div>
9
9
 
@@ -15,10 +15,10 @@
15
15
  %>
16
16
  <div class="alert-danger" style="display: flex; align-items: center; gap: 10px; padding: 10px var(--space-5); background: var(--status-critical-bg); border-radius: var(--radius-md); border: 1px solid var(--status-critical); margin-bottom: var(--space-6); font-size: 13px; color: var(--status-critical);">
17
17
  <span style="width: 8px; height: 8px; border-radius: 50%; background: var(--status-critical); animation: pulse 2s infinite; flex-shrink: 0;"></span>
18
- <strong><%= alert_count %> Critical/High Error<%= alert_count != 1 ? 's' : '' %> in Last Hour</strong>
18
+ <strong><%= red_tp("red.errors.overview_page.alert_banner", count: alert_count) %></strong>
19
19
  <span style="color: var(--text-secondary);">&mdash;</span>
20
- <span style="color: var(--text-secondary);"><%= link_to error_path(top_alert, **app_context), style: "background: transparent; padding: 0; color: var(--status-critical); text-decoration: none; font-family: var(--font-mono); font-size: 13px;" do %><%= top_alert.error_type %><% end %> &middot; <%= top_alert.occurrence_count %> occurrences</span>
21
- <%= link_to errors_path(app_context.merge(severity: 'critical')), style: "margin-left: auto; padding: 4px 12px; font-size: 12px; font-weight: 600; border-radius: var(--radius-full); border: 1px solid var(--status-critical); background: transparent; color: var(--status-critical); text-decoration: none; white-space: nowrap;" do %>View<% end %>
20
+ <span style="color: var(--text-secondary);"><%= link_to error_path(top_alert, **app_context), style: "background: transparent; padding: 0; color: var(--status-critical); text-decoration: none; font-family: var(--font-mono); font-size: 13px;" do %><%= top_alert.error_type %><% end %> &middot; <%= red_tp("red.errors.overview_page.alert_occurrences", count: top_alert.occurrence_count) %></span>
21
+ <%= link_to errors_path(app_context.merge(severity: 'critical')), style: "margin-left: auto; padding: 4px 12px; font-size: 12px; font-weight: 600; border-radius: var(--radius-full); border: 1px solid var(--status-critical); background: transparent; color: var(--status-critical); text-decoration: none; white-space: nowrap;" do %><%= red_t("red.errors.overview_page.alert_view") %><% end %>
22
22
  </div>
23
23
  <% end %>
24
24
 
@@ -35,13 +35,13 @@
35
35
  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
36
36
  <!-- Error Rate -->
37
37
  <div class="card stat-card" style="border-left: 3px solid var(--status-critical); padding: var(--space-5) var(--space-6);">
38
- <div class="stat-label" style="margin-bottom: 4px;">Error Rate</div>
38
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.error_rate") %></div>
39
39
  <div style="display: flex; align-items: baseline; gap: 8px;">
40
- <span class="stat-value" style="color: var(--status-critical);"><%= @stats[:error_rate] %>%</span>
40
+ <span class="stat-value" style="color: var(--status-critical);"><%= red_t("red.errors.pages.percent", value: @stats[:error_rate]) %></span>
41
41
  <% if @stats[:trend_percentage].present? && @stats[:trend_percentage] != 0 %>
42
42
  <span style="display: inline-flex; align-items: center; gap: 2px; font-size: 12px; font-weight: 600; color: <%= @stats[:trend_percentage] > 0 ? 'var(--status-critical)' : 'var(--status-success)' %>;">
43
43
  <i class="bi <%= @stats[:trend_percentage] > 0 ? 'bi-arrow-up-right' : 'bi-arrow-down-right' %>" style="font-size: 10px;"></i>
44
- <%= @stats[:trend_percentage].abs %>%
44
+ <%= red_t("red.errors.pages.percent", value: @stats[:trend_percentage].abs) %>
45
45
  </span>
46
46
  <% end %>
47
47
  </div>
@@ -52,12 +52,12 @@
52
52
  <% end %>
53
53
  </div>
54
54
  <% end %>
55
- <small style="color: var(--text-tertiary); margin-top: var(--space-1);">Errors per hour today</small>
55
+ <small style="color: var(--text-tertiary); margin-top: var(--space-1);"><%= red_t("red.errors.overview_page.error_rate_hint") %></small>
56
56
  </div>
57
57
 
58
58
  <!-- Unresolved -->
59
59
  <div class="card stat-card" style="border-left: 3px solid var(--accent); padding: var(--space-5) var(--space-6);">
60
- <div class="stat-label" style="margin-bottom: 4px;">Unresolved</div>
60
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.unresolved") %></div>
61
61
  <div style="display: flex; align-items: baseline; gap: 8px;">
62
62
  <span class="stat-value"><%= @stats[:unresolved] %></span>
63
63
  </div>
@@ -68,14 +68,14 @@
68
68
  <% end %>
69
69
  </div>
70
70
  <% end %>
71
- <small style="color: var(--text-tertiary); margin-top: var(--space-1);">Pending resolution</small>
71
+ <small style="color: var(--text-tertiary); margin-top: var(--space-1);"><%= red_t("red.errors.overview_page.unresolved_hint") %></small>
72
72
  </div>
73
73
 
74
74
  <!-- Resolution Rate -->
75
75
  <div class="card stat-card" style="border-left: 3px solid var(--status-success); padding: var(--space-5) var(--space-6);">
76
- <div class="stat-label" style="margin-bottom: 4px;">Resolution Rate</div>
76
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.resolution_rate") %></div>
77
77
  <div style="display: flex; align-items: baseline; gap: 8px;">
78
- <span class="stat-value" style="color: var(--status-success);"><%= resolution_rate %>%</span>
78
+ <span class="stat-value" style="color: var(--status-success);"><%= red_t("red.errors.pages.percent", value: resolution_rate) %></span>
79
79
  </div>
80
80
  <% if trend_values.any? %>
81
81
  <div style="display: flex; align-items: flex-end; gap: 2px; height: 24px; margin-top: var(--space-2);">
@@ -84,7 +84,7 @@
84
84
  <% end %>
85
85
  </div>
86
86
  <% end %>
87
- <small style="color: var(--text-tertiary); margin-top: var(--space-1);"><%= @stats[:resolved] %> of <%= total_errors %> resolved</small>
87
+ <small style="color: var(--text-tertiary); margin-top: var(--space-1);"><%= red_t("red.errors.overview_page.resolution_hint", resolved: @stats[:resolved], total: total_errors) %></small>
88
88
  </div>
89
89
  </div>
90
90
 
@@ -92,13 +92,13 @@
92
92
  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
93
93
  <!-- Affected Users -->
94
94
  <div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
95
- <div class="stat-label" style="margin-bottom: 4px;">Affected Users (Today)</div>
95
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.affected_users") %></div>
96
96
  <div style="display: flex; align-items: baseline; gap: 8px;">
97
97
  <span class="stat-value"><%= @stats[:affected_users_today] %></span>
98
98
  <% if @stats[:affected_users_change] != 0 %>
99
99
  <span style="display: inline-flex; align-items: center; gap: 2px; font-size: 12px; font-weight: 600; color: <%= @stats[:affected_users_change] > 0 ? 'var(--status-critical)' : 'var(--status-success)' %>;">
100
100
  <i class="bi <%= @stats[:affected_users_change] > 0 ? 'bi-arrow-up-right' : 'bi-arrow-down-right' %>" style="font-size: 10px;"></i>
101
- <%= @stats[:affected_users_change].abs %> from yesterday
101
+ <%= red_t("red.errors.overview_page.users_change", count: @stats[:affected_users_change].abs) %>
102
102
  </span>
103
103
  <% end %>
104
104
  </div>
@@ -106,22 +106,22 @@
106
106
 
107
107
  <!-- Avg Resolution Time -->
108
108
  <div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
109
- <div class="stat-label" style="margin-bottom: 4px;">Avg Resolution</div>
109
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.avg_resolution") %></div>
110
110
  <div style="display: flex; align-items: baseline; gap: 8px;">
111
111
  <% if @stats[:average_resolution_time].present? %>
112
- <span class="stat-value"><%= @stats[:average_resolution_time] %>h</span>
112
+ <span class="stat-value"><%= red_t("red.errors.overview_page.hours_short", value: @stats[:average_resolution_time]) %></span>
113
113
  <% else %>
114
114
  <span class="stat-value" style="color: var(--text-tertiary);">&mdash;</span>
115
115
  <% end %>
116
116
  </div>
117
- <small style="color: var(--text-tertiary);">Mean time to resolution</small>
117
+ <small style="color: var(--text-tertiary);"><%= red_t("red.errors.overview_page.avg_resolution_hint") %></small>
118
118
  </div>
119
119
 
120
120
  <!-- Error Trend -->
121
121
  <div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
122
- <div class="stat-label" style="margin-bottom: 4px;">Error Trend</div>
122
+ <div class="stat-label" style="margin-bottom: 4px;"><%= red_t("red.errors.overview_page.error_trend") %></div>
123
123
  <div style="display: flex; align-items: baseline; gap: 8px;">
124
- <span class="stat-value"><%= trend_arrow(@stats[:trend_percentage]) %> <%= @stats[:trend_percentage].abs %>%</span>
124
+ <span class="stat-value"><%= trend_arrow(@stats[:trend_percentage]) %> <%= red_t("red.errors.pages.percent", value: @stats[:trend_percentage].abs) %></span>
125
125
  </div>
126
126
  <small style="color: var(--text-tertiary);"><%= trend_text(@stats[:trend_direction]) %></small>
127
127
  </div>
@@ -131,8 +131,8 @@
131
131
  <% if @stats[:top_errors_by_impact].any? %>
132
132
  <div class="card" style="margin-bottom: var(--space-6);">
133
133
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
134
- <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);">Top Errors by Impact</span>
135
- <%= link_to errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %>View all &rarr;<% end %>
134
+ <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);"><%= red_t("red.errors.overview_page.top_errors") %></span>
135
+ <%= link_to errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %><%= red_t("red.errors.overview_page.view_all") %><% end %>
136
136
  </div>
137
137
  <div>
138
138
  <% @stats[:top_errors_by_impact].each_with_index do |error, i| %>
@@ -140,7 +140,7 @@
140
140
  <span style="width: 24px; text-align: center; font-size: 12px; font-weight: 600; color: var(--text-tertiary);"><%= i + 1 %></span>
141
141
  <span class="badge bg-<%= severity_color(error[:severity]) %>" style="display: inline-flex; align-items: center; gap: 4px;">
142
142
  <span style="width: 6px; height: 6px; border-radius: 50%; background: currentColor;"></span>
143
- <%= error[:severity] %>
143
+ <%= red_t("red.common.severity.#{error[:severity]}") %>
144
144
  </span>
145
145
  <div style="flex: 1; min-width: 0;">
146
146
  <div style="font-size: 13px; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-mono); font-size: 12px;">
@@ -150,7 +150,7 @@
150
150
  </div>
151
151
  <div style="text-align: right; flex-shrink: 0;">
152
152
  <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums;"><%= number_with_delimiter(error[:occurrence_count]) %></div>
153
- <div style="font-size: 11px; color: var(--text-tertiary);"><%= error[:affected_users] %> users</div>
153
+ <div style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.errors.overview_page.users", count: error[:affected_users]) %></div>
154
154
  </div>
155
155
  <% end %>
156
156
  <% end %>
@@ -162,9 +162,9 @@
162
162
  <% if @platform_health.any? %>
163
163
  <div class="card" style="margin-bottom: var(--space-6);">
164
164
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
165
- <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);">Platform Health</span>
165
+ <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);"><%= red_t("red.errors.overview_page.platform_health") %></span>
166
166
  <% if RailsErrorDashboard.configuration.enable_platform_comparison %>
167
- <%= link_to platform_comparison_errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %>Full comparison &rarr;<% end %>
167
+ <%= link_to platform_comparison_errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %><%= red_t("red.errors.overview_page.full_comparison") %><% end %>
168
168
  <% end %>
169
169
  </div>
170
170
  <div style="padding: var(--space-5) var(--space-6);">
@@ -172,17 +172,21 @@
172
172
  <% @platform_health.each do |platform, health| %>
173
173
  <div style="padding: var(--space-4); border: 1px solid var(--border-primary); border-radius: var(--radius-md);">
174
174
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-3);">
175
- <span style="font-size: 13px; font-weight: 600; text-transform: capitalize;"><%= platform %></span>
175
+ <%# No text-transform: the platform is a brand value stored as
176
+ written (iOS, Android, Web). CSS capitalize is English
177
+ morphology applied to an identifier — it renders "iOS" as
178
+ "IOS" in some engines and mangles the Turkish dotless i. %>
179
+ <span style="font-size: 13px; font-weight: 600;"><%= platform %></span>
176
180
  <span class="badge bg-<%= health_status_color(health[:health_status]) %>"><%= health_status_text(health[:health_status]) %></span>
177
181
  </div>
178
182
  <div style="margin-bottom: var(--space-2);">
179
- <div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= health[:stability_score] %><span style="font-size: 12px; color: var(--text-tertiary); font-weight: 400;">/100</span></div>
183
+ <div style="font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;"><%= red_t("red.errors.overview_page.stability_score_html", score: content_tag(:span, health[:stability_score])) %></div>
180
184
  </div>
181
185
  <div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
182
186
  <div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
183
187
  </div>
184
188
  <div style="font-size: 12px; color: var(--text-tertiary);">
185
- <%= health[:total_errors] %> errors &middot; <%= health[:critical_errors] %> critical &middot; <%= health[:unresolved_errors] %> open
189
+ <%= red_t("red.errors.overview_page.platform_summary", total: health[:total_errors], critical: health[:critical_errors], unresolved: health[:unresolved_errors]) %>
186
190
  </div>
187
191
  </div>
188
192
  <% end %>
@@ -195,20 +199,20 @@
195
199
  <% if RailsErrorDashboard.configuration.enable_error_correlation && (@problematic_releases.any? || @time_correlated_errors.any? || @multi_error_users.any?) %>
196
200
  <div class="card" style="margin-bottom: var(--space-6);">
197
201
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
198
- <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);">Correlation Insights</span>
199
- <%= link_to correlation_errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %>Full analysis &rarr;<% end %>
202
+ <span style="font-size: 13px; font-weight: 600; color: var(--text-primary);"><%= red_t("red.errors.overview_page.correlation_title") %></span>
203
+ <%= link_to correlation_errors_path(app_context), style: "font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500;" do %><%= red_t("red.errors.overview_page.full_analysis") %><% end %>
200
204
  </div>
201
205
  <div style="padding: var(--space-5) var(--space-6);">
202
206
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-4);">
203
207
  <% if @problematic_releases.any? %>
204
208
  <div style="padding: var(--space-4); border: 1px solid var(--border-primary); border-radius: var(--radius-md); border-left: 3px solid var(--status-warning);">
205
209
  <div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: var(--space-3);">
206
- <i class="bi bi-tag me-1"></i> Problematic Releases
210
+ <i class="bi bi-tag me-1"></i> <%= red_t("red.errors.overview_page.problematic_releases") %>
207
211
  </div>
208
212
  <% @problematic_releases.each do |release| %>
209
213
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-2) 0; border-bottom: 1px solid var(--border-primary); font-size: 12px;">
210
214
  <code style="font-size: 11px;"><%= release[:version] || release[:git_sha]&.truncate(8, omission: '') %></code>
211
- <span style="font-weight: 600;"><%= release[:error_count] %> errors</span>
215
+ <span style="font-weight: 600;"><%= red_tp("red.errors.overview_page.release_errors", count: release[:error_count]) %></span>
212
216
  </div>
213
217
  <% end %>
214
218
  </div>
@@ -217,7 +221,7 @@
217
221
  <% if @time_correlated_errors.any? %>
218
222
  <div style="padding: var(--space-4); border: 1px solid var(--border-primary); border-radius: var(--radius-md); border-left: 3px solid var(--status-info);">
219
223
  <div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: var(--space-3);">
220
- <i class="bi bi-clock-history me-1"></i> Time-Correlated
224
+ <i class="bi bi-clock-history me-1"></i> <%= red_t("red.errors.overview_page.time_correlated") %>
221
225
  </div>
222
226
  <% @time_correlated_errors.each do |_, data| %>
223
227
  <div style="padding: var(--space-2) 0; border-bottom: 1px solid var(--border-primary); font-size: 12px;">
@@ -227,7 +231,9 @@
227
231
  <code style="font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px;"><%= data[:error_type_b] %></code>
228
232
  </div>
229
233
  <div style="color: var(--text-tertiary); margin-top: 2px;">
230
- <strong><%= (data[:correlation] * 100).round(0) %>%</strong> correlation (<%= data[:strength] %>)
234
+ <%= red_t("red.errors.overview_page.correlation_value_html",
235
+ percent: content_tag(:strong, red_t("red.errors.pages.percent", value: (data[:correlation] * 100).round(0))),
236
+ strength: red_t("red.analytics.correlation_strength.#{data[:strength]}")) %>
231
237
  </div>
232
238
  </div>
233
239
  <% end %>
@@ -237,11 +243,11 @@
237
243
  <% if @multi_error_users.any? %>
238
244
  <div style="padding: var(--space-4); border: 1px solid var(--border-primary); border-radius: var(--radius-md); border-left: 3px solid var(--status-critical);">
239
245
  <div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: var(--space-3);">
240
- <i class="bi bi-people-fill me-1"></i> Multi-Error Users
246
+ <i class="bi bi-people-fill me-1"></i> <%= red_t("red.errors.overview_page.multi_error_users") %>
241
247
  </div>
242
248
  <% @multi_error_users.each do |user| %>
243
249
  <div style="display: flex; justify-content: space-between; align-items: center; padding: var(--space-2) 0; border-bottom: 1px solid var(--border-primary); font-size: 12px;">
244
- <span>User <strong><%= user[:user_id] %></strong> &middot; <%= user[:error_types].size %> types</span>
250
+ <span><%= red_t("red.errors.overview_page.user_types_html", user: content_tag(:strong, user[:user_id]), types: red_tp("red.errors.overview_page.types", count: user[:error_types].size)) %></span>
245
251
  <span style="font-weight: 600; color: var(--status-critical);"><%= user[:total_errors] %></span>
246
252
  </div>
247
253
  <% end %>
@@ -1,21 +1,21 @@
1
- <% content_for :page_title, "Platform Comparison" %>
1
+ <% content_for :page_title, red_t("red.analytics.platform_comparison.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-phone me-2"></i>
7
- Platform Health Comparison
7
+ <%= red_t("red.analytics.platform_comparison.title") %>
8
8
  </h1>
9
9
 
10
10
  <div class="btn-group" role="group">
11
11
  <%= link_to platform_comparison_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 platform_comparison_errors_path(days: 14), class: "btn btn-sm #{@days == 14 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
- 14 Days
15
+ <%= red_t("red.common.days_filter.14") %>
16
16
  <% end %>
17
17
  <%= link_to platform_comparison_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
- 30 Days
18
+ <%= red_t("red.common.days_filter.30") %>
19
19
  <% end %>
20
20
  </div>
21
21
  </div>
@@ -23,12 +23,12 @@
23
23
  <% if @platform_health.empty? %>
24
24
  <div class="red-empty-state">
25
25
  <i class="bi bi-phone display-1 text-muted mb-3"></i>
26
- <div class="red-empty-state-title">No Platform Data Available</div>
26
+ <div class="red-empty-state-title"><%= red_t("red.analytics.platform_comparison.empty.title") %></div>
27
27
  <p class="text-muted">
28
- No errors were logged for the selected time period (<%= @days %> days).
28
+ <%= red_t("red.analytics.platform_comparison.empty.message", days: @days) %>
29
29
  </p>
30
30
  <small class="text-muted d-block mt-3">
31
- <i class="bi bi-lightbulb"></i> Try selecting a longer time period or check back later.
31
+ <i class="bi bi-lightbulb"></i> <%= red_t("red.analytics.platform_comparison.empty.hint") %>
32
32
  </small>
33
33
  </div>
34
34
  <% else %>
@@ -39,19 +39,19 @@
39
39
  <div class="card h-100 <%= health[:health_status] == :healthy ? 'border-success' : health[:health_status] == :warning ? 'border-warning' : 'border-danger' %>">
40
40
  <div class="card-body">
41
41
  <h5 class="card-title text-capitalize">
42
- <%= platform || 'Unknown' %>
42
+ <%= platform || red_t("red.common.unknown") %>
43
43
  <% if health[:health_status] == :healthy %>
44
- <span class="badge bg-success float-end">Healthy</span>
44
+ <span class="badge bg-success float-end"><%= red_t("red.analytics.platform_comparison.health_card.healthy") %></span>
45
45
  <% elsif health[:health_status] == :warning %>
46
- <span class="badge bg-warning float-end">Warning</span>
46
+ <span class="badge bg-warning float-end"><%= red_t("red.analytics.platform_comparison.health_card.warning") %></span>
47
47
  <% else %>
48
- <span class="badge bg-danger float-end">Critical</span>
48
+ <span class="badge bg-danger float-end"><%= red_t("red.analytics.platform_comparison.health_card.critical") %></span>
49
49
  <% end %>
50
50
  </h5>
51
51
 
52
52
  <div class="mb-3">
53
53
  <div class="d-flex justify-content-between align-items-center mb-1">
54
- <small class="text-muted">Stability Score</small>
54
+ <small class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.stability_score") %></small>
55
55
  <strong><%= health[:stability_score] %>/100</strong>
56
56
  </div>
57
57
  <div class="progress" style="height: 8px;">
@@ -64,23 +64,23 @@
64
64
 
65
65
  <div class="small">
66
66
  <div class="d-flex justify-content-between mb-1">
67
- <span class="text-muted">Total Errors:</span>
67
+ <span class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.total_errors") %></span>
68
68
  <strong><%= health[:total_errors] %></strong>
69
69
  </div>
70
70
  <div class="d-flex justify-content-between mb-1">
71
- <span class="text-muted">Critical:</span>
71
+ <span class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.critical_errors") %></span>
72
72
  <strong class="text-danger"><%= health[:critical_errors] %></strong>
73
73
  </div>
74
74
  <div class="d-flex justify-content-between mb-1">
75
- <span class="text-muted">Unresolved:</span>
75
+ <span class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.unresolved") %></span>
76
76
  <strong><%= health[:unresolved_errors] %></strong>
77
77
  </div>
78
78
  <div class="d-flex justify-content-between mb-1">
79
- <span class="text-muted">Resolution Rate:</span>
79
+ <span class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.resolution_rate") %></span>
80
80
  <strong><%= health[:resolution_rate] %>%</strong>
81
81
  </div>
82
82
  <div class="d-flex justify-content-between">
83
- <span class="text-muted">Error Velocity:</span>
83
+ <span class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.error_velocity") %></span>
84
84
  <strong class="<%= health[:error_velocity] > 0 ? 'text-danger' : 'text-success' %>">
85
85
  <%= health[:error_velocity] > 0 ? '+' : '' %><%= health[:error_velocity] %>%
86
86
  </strong>
@@ -88,7 +88,7 @@
88
88
  </div>
89
89
  </div>
90
90
  <div class="card-footer border-top">
91
- <%= link_to "View #{platform} Errors", errors_path(platform: platform, unresolved: '0'), class: "btn btn-sm btn-outline-primary w-100" %>
91
+ <%= link_to red_t("red.analytics.platform_comparison.health_card.view_errors", platform: platform), errors_path(platform: platform, unresolved: '0'), class: "btn btn-sm btn-outline-primary w-100" %>
92
92
  </div>
93
93
  </div>
94
94
  </div>
@@ -100,7 +100,7 @@
100
100
  <div class="card-header">
101
101
  <h5 class="mb-0">
102
102
  <i class="bi bi-bar-chart me-2"></i>
103
- Error Rate by Platform
103
+ <%= red_t("red.analytics.platform_comparison.error_rate_chart.title") %>
104
104
  </h5>
105
105
  </div>
106
106
  <div class="card-body">
@@ -113,7 +113,7 @@
113
113
  <div class="card-header">
114
114
  <h5 class="mb-0">
115
115
  <i class="bi bi-graph-up me-2"></i>
116
- Daily Error Trends
116
+ <%= red_t("red.analytics.platform_comparison.daily_trend_chart.title") %>
117
117
  </h5>
118
118
  </div>
119
119
  <div class="card-body">
@@ -128,13 +128,13 @@
128
128
  <div class="card-header">
129
129
  <h5 class="mb-0">
130
130
  <i class="bi bi-exclamation-triangle me-2"></i>
131
- Severity Distribution
131
+ <%= red_t("red.analytics.platform_comparison.severity_distribution.title") %>
132
132
  </h5>
133
133
  </div>
134
134
  <div class="card-body">
135
135
  <% @severity_distribution.each do |platform, severities| %>
136
136
  <div class="mb-3">
137
- <h6 class="text-capitalize"><%= platform || 'Unknown' %></h6>
137
+ <h6 class="text-capitalize"><%= platform || red_t("red.common.unknown") %></h6>
138
138
  <% total = severities.values.sum %>
139
139
  <% if total > 0 %>
140
140
  <div class="progress" style="height: 25px;">
@@ -142,7 +142,7 @@
142
142
  <div class="progress-bar bg-danger"
143
143
  role="progressbar"
144
144
  style="width: <%= (severities[:critical].to_f / total * 100).round(1) %>%"
145
- title="Critical: <%= severities[:critical] %>">
145
+ title="<%= red_t('red.analytics.platform_comparison.severity_distribution.severity_count', label: red_t('red.common.severity.critical'), count: severities[:critical]) %>">
146
146
  <%= severities[:critical] if (severities[:critical].to_f / total) > 0.1 %>
147
147
  </div>
148
148
  <% end %>
@@ -150,7 +150,7 @@
150
150
  <div class="progress-bar bg-warning"
151
151
  role="progressbar"
152
152
  style="width: <%= (severities[:high].to_f / total * 100).round(1) %>%"
153
- title="High: <%= severities[:high] %>">
153
+ title="<%= red_t('red.analytics.platform_comparison.severity_distribution.severity_count', label: red_t('red.common.severity.high'), count: severities[:high]) %>">
154
154
  <%= severities[:high] if (severities[:high].to_f / total) > 0.1 %>
155
155
  </div>
156
156
  <% end %>
@@ -158,7 +158,7 @@
158
158
  <div class="progress-bar bg-info"
159
159
  role="progressbar"
160
160
  style="width: <%= (severities[:medium].to_f / total * 100).round(1) %>%"
161
- title="Medium: <%= severities[:medium] %>">
161
+ title="<%= red_t('red.analytics.platform_comparison.severity_distribution.severity_count', label: red_t('red.common.severity.medium'), count: severities[:medium]) %>">
162
162
  <%= severities[:medium] if (severities[:medium].to_f / total) > 0.1 %>
163
163
  </div>
164
164
  <% end %>
@@ -166,19 +166,20 @@
166
166
  <div class="progress-bar bg-secondary"
167
167
  role="progressbar"
168
168
  style="width: <%= (severities[:low].to_f / total * 100).round(1) %>%"
169
- title="Low: <%= severities[:low] %>">
169
+ title="<%= red_t('red.analytics.platform_comparison.severity_distribution.severity_count', label: red_t('red.common.severity.low'), count: severities[:low]) %>">
170
170
  <%= severities[:low] if (severities[:low].to_f / total) > 0.1 %>
171
171
  </div>
172
172
  <% end %>
173
173
  </div>
174
174
  <div class="small text-muted mt-1">
175
- Critical: <%= severities[:critical] || 0 %> |
176
- High: <%= severities[:high] || 0 %> |
177
- Medium: <%= severities[:medium] || 0 %> |
178
- Low: <%= severities[:low] || 0 %>
175
+ <%= red_t("red.analytics.platform_comparison.severity_distribution.breakdown_html",
176
+ critical: red_t("red.analytics.platform_comparison.severity_distribution.severity_count", label: red_t("red.common.severity.critical"), count: severities[:critical] || 0),
177
+ high: red_t("red.analytics.platform_comparison.severity_distribution.severity_count", label: red_t("red.common.severity.high"), count: severities[:high] || 0),
178
+ medium: red_t("red.analytics.platform_comparison.severity_distribution.severity_count", label: red_t("red.common.severity.medium"), count: severities[:medium] || 0),
179
+ low: red_t("red.analytics.platform_comparison.severity_distribution.severity_count", label: red_t("red.common.severity.low"), count: severities[:low] || 0)) %>
179
180
  </div>
180
181
  <% else %>
181
- <p class="text-muted small mb-0">No errors</p>
182
+ <p class="text-muted small mb-0"><%= red_t("red.analytics.platform_comparison.severity_distribution.empty") %></p>
182
183
  <% end %>
183
184
  </div>
184
185
  <% end %>
@@ -192,7 +193,7 @@
192
193
  <div class="card-header">
193
194
  <h5 class="mb-0">
194
195
  <i class="bi bi-clock-history me-2"></i>
195
- Average Resolution Time
196
+ <%= red_t("red.analytics.platform_comparison.resolution_time_chart.title") %>
196
197
  </h5>
197
198
  </div>
198
199
  <div class="card-body">
@@ -208,7 +209,7 @@
208
209
  <div class="card-header">
209
210
  <h5 class="mb-0">
210
211
  <i class="bi bi-layers me-2"></i>
211
- Cross-Platform Errors
212
+ <%= red_t("red.analytics.platform_comparison.cross_platform.title") %>
212
213
  <span class="badge bg-secondary ms-2"><%= @cross_platform_errors.count %></span>
213
214
  </h5>
214
215
  </div>
@@ -217,10 +218,10 @@
217
218
  <table class="table table-hover">
218
219
  <thead>
219
220
  <tr>
220
- <th>Error Type</th>
221
- <th>Platforms</th>
222
- <th>Total Occurrences</th>
223
- <th>Breakdown</th>
221
+ <th><%= red_t("red.analytics.platform_comparison.cross_platform.column_error_type") %></th>
222
+ <th><%= red_t("red.analytics.platform_comparison.cross_platform.column_platforms") %></th>
223
+ <th><%= red_t("red.analytics.platform_comparison.cross_platform.column_total_occurrences") %></th>
224
+ <th><%= red_t("red.analytics.platform_comparison.cross_platform.column_breakdown") %></th>
224
225
  </tr>
225
226
  </thead>
226
227
  <tbody>
@@ -259,9 +260,9 @@
259
260
  <div class="col-lg-6 mb-4">
260
261
  <div class="card h-100">
261
262
  <div class="card-header">
262
- <h5 class="mb-0 text-capitalize">
263
+ <h5 class="mb-0">
263
264
  <i class="bi bi-bug me-2"></i>
264
- Top Errors - <%= platform || 'Unknown' %>
265
+ <%= red_t("red.analytics.platform_comparison.top_errors.title", platform: platform || red_t("red.common.unknown")) %>
265
266
  </h5>
266
267
  </div>
267
268
  <div class="card-body">
@@ -282,7 +283,7 @@
282
283
  <% end %>
283
284
  </div>
284
285
  <% else %>
285
- <p class="text-muted mb-0">No errors found</p>
286
+ <p class="text-muted mb-0"><%= red_t("red.analytics.platform_comparison.top_errors.empty") %></p>
286
287
  <% end %>
287
288
  </div>
288
289
  </div>
@@ -314,7 +315,7 @@
314
315
  data: {
315
316
  labels: <%= raw @error_rate_by_platform.keys.map { |k| k.to_s.capitalize }.to_json %>,
316
317
  datasets: [{
317
- label: 'Total Errors',
318
+ label: '<%= red_js_t("red.ui_js.charts.series_total_errors") %>',
318
319
  data: <%= raw @error_rate_by_platform.values.to_json %>,
319
320
  backgroundColor: 'rgba(37, 99, 235, 0.5)', // Blue with transparency
320
321
  borderColor: '#2563EB', // Blue
@@ -384,7 +385,7 @@
384
385
  data: {
385
386
  labels: platforms,
386
387
  datasets: [{
387
- label: 'Hours to Resolve',
388
+ label: '<%= red_js_t("red.ui_js.charts.series_hours_to_resolve") %>',
388
389
  data: times,
389
390
  backgroundColor: 'rgba(245, 158, 11, 0.5)', // Amber with transparency
390
391
  borderColor: '#F59E0B', // Amber
@@ -399,7 +400,7 @@
399
400
  legend: { display: false }
400
401
  },
401
402
  scales: {
402
- x: { beginAtZero: true, title: { display: true, text: 'Hours' } }
403
+ x: { beginAtZero: true, title: { display: true, text: '<%= red_js_t("red.ui_js.charts.axis_hours") %>' } }
403
404
  }
404
405
  }
405
406
  });