rails_error_dashboard 0.14.4 → 0.16.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -8
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +97 -4
  5. data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
  6. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
  7. data/app/helpers/rails_error_dashboard/application_helper.rb +2 -2
  8. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +63 -16
  9. data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
  10. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +20 -2
  11. data/app/jobs/rails_error_dashboard/cascade_detection_job.rb +30 -0
  12. data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +33 -6
  13. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -1
  14. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -1
  15. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +31 -0
  16. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +6 -1
  17. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -1
  18. data/app/jobs/rails_error_dashboard/telegram_error_notification_job.rb +25 -0
  19. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +2 -18
  20. data/app/models/rails_error_dashboard/error_log.rb +54 -20
  21. data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
  22. data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
  23. data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
  24. data/app/views/layouts/rails_error_dashboard.html.erb +14 -2
  25. data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
  26. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +12 -3
  27. data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
  28. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +3 -1
  29. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +3 -9
  30. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -0
  31. data/app/views/rails_error_dashboard/errors/boot_errors.html.erb +130 -0
  32. data/app/views/rails_error_dashboard/errors/index.html.erb +1 -1
  33. data/app/views/rails_error_dashboard/errors/missing_translations.html.erb +143 -0
  34. data/app/views/rails_error_dashboard/errors/overview.html.erb +10 -7
  35. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +16 -11
  36. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +7 -4
  37. data/app/views/rails_error_dashboard/errors/settings.html.erb +23 -8
  38. data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
  39. data/config/locales/de.yml +87 -5
  40. data/config/locales/en.yml +99 -4
  41. data/config/locales/es.yml +88 -5
  42. data/config/locales/fr.yml +88 -5
  43. data/config/locales/it.yml +88 -5
  44. data/config/locales/ja.yml +86 -4
  45. data/config/locales/pl.yml +89 -5
  46. data/config/locales/pt-BR.yml +88 -5
  47. data/config/locales/ru.yml +89 -5
  48. data/config/locales/uk.yml +89 -5
  49. data/config/locales/zh-CN.yml +86 -4
  50. data/config/routes.rb +6 -0
  51. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
  52. data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
  53. data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
  54. data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
  55. data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
  56. data/lib/generators/rails_error_dashboard/install/install_generator.rb +9 -4
  57. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +41 -5
  58. data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
  59. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
  60. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +16 -3
  61. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
  62. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
  63. data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +101 -0
  64. data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
  65. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
  66. data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
  67. data/lib/rails_error_dashboard/commands/log_error.rb +20 -8
  68. data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
  69. data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -15
  70. data/lib/rails_error_dashboard/commands/update_error_status.rb +7 -0
  71. data/lib/rails_error_dashboard/configuration.rb +61 -4
  72. data/lib/rails_error_dashboard/engine.rb +42 -1
  73. data/lib/rails_error_dashboard/error_reporter.rb +4 -2
  74. data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
  75. data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
  76. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +7 -1
  77. data/lib/rails_error_dashboard/plugin.rb +19 -3
  78. data/lib/rails_error_dashboard/plugin_registry.rb +61 -12
  79. data/lib/rails_error_dashboard/queries/analytics_stats.rb +0 -12
  80. data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
  81. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
  82. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +3 -11
  83. data/lib/rails_error_dashboard/queries/errors_list.rb +8 -16
  84. data/lib/rails_error_dashboard/queries/health_status.rb +191 -0
  85. data/lib/rails_error_dashboard/queries/missing_translation_summary.rb +82 -0
  86. data/lib/rails_error_dashboard/queries/platform_comparison.rb +14 -6
  87. data/lib/rails_error_dashboard/services/active_storage_probe.rb +91 -0
  88. data/lib/rails_error_dashboard/services/backtrace_parser.rb +13 -7
  89. data/lib/rails_error_dashboard/services/backtrace_processor.rb +31 -7
  90. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
  91. data/lib/rails_error_dashboard/services/crash_capture.rb +90 -2
  92. data/lib/rails_error_dashboard/services/digest_builder.rb +9 -6
  93. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +6 -0
  94. data/lib/rails_error_dashboard/services/github_link_generator.rb +25 -14
  95. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
  96. data/lib/rails_error_dashboard/services/missing_translation_handler.rb +152 -0
  97. data/lib/rails_error_dashboard/services/missing_translation_tracker.rb +316 -0
  98. data/lib/rails_error_dashboard/services/repository_url.rb +18 -0
  99. data/lib/rails_error_dashboard/services/resolution_callbacks.rb +44 -0
  100. data/lib/rails_error_dashboard/services/severity_classifier.rb +52 -0
  101. data/lib/rails_error_dashboard/services/similarity_calculator.rb +3 -9
  102. data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
  103. data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
  104. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
  105. data/lib/rails_error_dashboard/services/telegram_delivery.rb +134 -0
  106. data/lib/rails_error_dashboard/services/telegram_payload_builder.rb +105 -0
  107. data/lib/rails_error_dashboard/services/webhook_delivery.rb +63 -0
  108. data/lib/rails_error_dashboard/services/webhook_signer.rb +80 -0
  109. data/lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb +23 -0
  110. data/lib/rails_error_dashboard/version.rb +1 -1
  111. data/lib/rails_error_dashboard.rb +28 -0
  112. data/lib/tasks/error_dashboard.rake +2 -3
  113. metadata +29 -2
@@ -0,0 +1,130 @@
1
+ <% content_for :page_title, red_t("red.errors.boot_errors_page.title") %>
2
+
3
+ <div>
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
+ <i class="bi bi-power me-2"></i>
7
+ <%= red_t("red.errors.boot_errors_page.title") %>
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to boot_errors_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ <%= red_t("red.common.days_filter.7") %>
13
+ <% end %>
14
+ <%= link_to boot_errors_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ <%= red_t("red.common.days_filter.30") %>
16
+ <% end %>
17
+ <%= link_to boot_errors_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ <%= red_t("red.common.days_filter.90") %>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @entry_count == 0 %>
24
+ <div class="red-empty-state">
25
+ <i class="bi bi-power display-1 text-success mb-3"></i>
26
+ <div class="red-empty-state-title"><%= red_t("red.errors.boot_errors_page.empty_title") %></div>
27
+ <p class="text-muted">
28
+ <%= red_t("red.errors.boot_errors_page.empty_body", days: @days) %>
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 560px;">
31
+ <div class="card-body text-start">
32
+ <h6><%= red_t("red.errors.boot_errors_page.how_title") %></h6>
33
+ <ul class="mb-0">
34
+ <li><%= red_t("red.errors.boot_errors_page.how_enabled_html", option: content_tag(:code, "enable_crash_capture = true")) %></li>
35
+ <li><%= red_t("red.errors.boot_errors_page.how_hooked") %></li>
36
+ <li><%= red_t("red.errors.boot_errors_page.how_captured_html", zeitwerk: content_tag(:code, "Zeitwerk::NameError"), syntax: content_tag(:code, "SyntaxError")) %></li>
37
+ <li><%= red_t("red.errors.boot_errors_page.how_imported") %></li>
38
+ <li><%= red_t("red.errors.boot_errors_page.how_never") %></li>
39
+ </ul>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <% else %>
44
+ <div class="row mb-4">
45
+ <div class="col-md-4">
46
+ <div class="card text-center">
47
+ <div class="card-body">
48
+ <div class="display-6 text-danger"><%= @unresolved_count %></div>
49
+ <small class="text-muted"><%= red_t("red.errors.boot_errors_page.unresolved") %></small>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <div class="col-md-4">
54
+ <div class="card text-center">
55
+ <div class="card-body">
56
+ <div class="display-6 text-warning"><%= @entry_count %></div>
57
+ <small class="text-muted"><%= red_t("red.errors.boot_errors_page.boot_crashes") %></small>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div class="col-md-4">
62
+ <div class="card text-center">
63
+ <div class="card-body">
64
+ <div class="display-6 text-info"><%= @total_occurrences %></div>
65
+ <small class="text-muted"><%= red_t("red.errors.boot_errors_page.failed_boots") %></small>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="card mb-4">
72
+ <div class="card-header d-flex justify-content-between align-items-center">
73
+ <h5 class="mb-0">
74
+ <i class="bi bi-power text-danger me-2"></i>
75
+ <%= red_t("red.errors.boot_errors_page.all_title") %>
76
+ <span class="badge bg-danger"><%= @entry_count %></span>
77
+ </h5>
78
+ <small class="text-muted"><%== @pagy.info_tag %></small>
79
+ </div>
80
+ <div class="card-body p-0">
81
+ <div class="table-responsive">
82
+ <table class="table table-hover mb-0">
83
+ <thead class="table-light">
84
+ <tr>
85
+ <th><%= red_t("red.errors.boot_errors_page.column_error") %></th>
86
+ <th><%= red_t("red.errors.boot_errors_page.column_file") %></th>
87
+ <th width="140"><%= red_t("red.errors.boot_errors_page.column_constant") %></th>
88
+ <th width="90"><%= red_t("red.errors.boot_errors_page.column_boots") %></th>
89
+ <th width="140"><%= red_t("red.errors.boot_errors_page.column_first_seen") %></th>
90
+ <th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
91
+ <th width="100"><%= red_t("red.errors.boot_errors_page.column_status") %></th>
92
+ </tr>
93
+ </thead>
94
+ <tbody>
95
+ <% @entries.each do |entry| %>
96
+ <tr>
97
+ <td>
98
+ <%# Exception class and message are machine values from the crash file, shown as-is. %>
99
+ <%= link_to entry[:error_type], error_path(entry[:id]), class: "fw-semibold text-decoration-none" %>
100
+ <div class="text-muted small text-truncate" style="max-width: 420px;" title="<%= entry[:message] %>"><%= entry[:message] %></div>
101
+ </td>
102
+ <td><% if entry[:file].present? %><code><%= entry[:file] %></code><% else %><span class="text-muted">&mdash;</span><% end %></td>
103
+ <td><% if entry[:constant].present? %><code><%= entry[:constant] %></code><% else %><span class="text-muted">&mdash;</span><% end %></td>
104
+ <td><strong><%= entry[:count] %></strong></td>
105
+ <td><%= local_time_ago(entry[:first_seen]) %></td>
106
+ <td><%= local_time_ago(entry[:last_seen]) %></td>
107
+ <td>
108
+ <% if entry[:resolved] %>
109
+ <span class="badge bg-success"><%= red_t("red.errors.boot_errors_page.status_resolved") %></span>
110
+ <% else %>
111
+ <span class="badge bg-danger"><%= red_t("red.errors.boot_errors_page.status_open") %></span>
112
+ <% end %>
113
+ </td>
114
+ </tr>
115
+ <% end %>
116
+ </tbody>
117
+ </table>
118
+ </div>
119
+ </div>
120
+ <div class="card-footer border-top d-flex justify-content-between align-items-center">
121
+ <small class="text-muted">
122
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.boot_errors_page.footer_tip") %>
123
+ </small>
124
+ <div>
125
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ <% end %>
130
+ </div>
@@ -272,7 +272,7 @@
272
272
  <%= button_tag type: "submit", name: "action_type", value: "resolve", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px;" do %>
273
273
  <i class="bi bi-check-circle"></i> <%= red_t("red.errors.index.batch.resolve") %>
274
274
  <% end %>
275
- <%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px; color: var(--status-critical);", data: { confirm: red_t("red.errors.index.batch.confirm_delete") } do %>
275
+ <%= button_tag type: "submit", name: "action_type", value: "delete", class: "btn btn-sm", style: "padding: 4px 12px; font-size: 12px; color: var(--status-critical);", data: { red_action: "confirm-submit", red_confirm_message: red_t("red.errors.index.batch.confirm_delete") } do %>
276
276
  <i class="bi bi-trash"></i> <%= red_t("red.errors.index.batch.delete") %>
277
277
  <% end %>
278
278
  <% end %>
@@ -0,0 +1,143 @@
1
+ <% content_for :page_title, red_t("red.errors.missing_translations_page.title") %>
2
+
3
+ <div>
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 style="font-size: 20px; font-weight: 700; margin: 0;">
6
+ <i class="bi bi-translate me-2"></i>
7
+ <%= red_t("red.errors.missing_translations_page.title") %>
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to missing_translations_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ <%= red_t("red.common.days_filter.7") %>
13
+ <% end %>
14
+ <%= link_to missing_translations_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ <%= red_t("red.common.days_filter.30") %>
16
+ <% end %>
17
+ <%= link_to missing_translations_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ <%= red_t("red.common.days_filter.90") %>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @entry_count == 0 %>
24
+ <div class="red-empty-state">
25
+ <i class="bi bi-translate display-1 text-success mb-3"></i>
26
+ <div class="red-empty-state-title"><%= red_t("red.errors.missing_translations_page.empty_title") %></div>
27
+ <p class="text-muted">
28
+ <%= red_t("red.errors.missing_translations_page.empty_body", days: @days) %>
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 560px;">
31
+ <div class="card-body text-start">
32
+ <h6><%= red_t("red.errors.missing_translations_page.how_title") %></h6>
33
+ <ul class="mb-0">
34
+ <li><%= red_t("red.errors.missing_translations_page.how_enabled_html", option: content_tag(:code, "enable_missing_translation_tracking = true")) %></li>
35
+ <li><%= red_t("red.errors.missing_translations_page.how_hooked") %></li>
36
+ <li><%= red_t("red.errors.missing_translations_page.how_recorded") %></li>
37
+ <li><%= red_t("red.errors.missing_translations_page.how_not_seen") %></li>
38
+ <li><%= red_t("red.errors.missing_translations_page.how_buffered") %></li>
39
+ </ul>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <% else %>
44
+ <div class="row mb-4">
45
+ <div class="col-md-4">
46
+ <div class="card text-center">
47
+ <div class="card-body">
48
+ <div class="display-6 text-warning"><%= @unique_keys %></div>
49
+ <small class="text-muted"><%= red_t("red.errors.missing_translations_page.missing_keys") %></small>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <div class="col-md-4">
54
+ <div class="card text-center">
55
+ <div class="card-body">
56
+ <div class="display-6 text-danger"><%= @total_misses %></div>
57
+ <small class="text-muted"><%= red_t("red.errors.missing_translations_page.total_misses") %></small>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div class="col-md-4">
62
+ <div class="card text-center">
63
+ <div class="card-body">
64
+ <div class="display-6 text-info"><%= @locale_count %></div>
65
+ <small class="text-muted"><%= red_t("red.errors.missing_translations_page.locales_affected") %></small>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <% if @overflow_count.to_i.positive? %>
72
+ <div class="alert alert-warning d-flex align-items-start" role="alert">
73
+ <i class="bi bi-exclamation-triangle me-2"></i>
74
+ <div>
75
+ <%= red_t("red.errors.missing_translations_page.overflow_notice",
76
+ count: @overflow_count,
77
+ limit: RailsErrorDashboard::Services::MissingTranslationTracker::MAX_BUFFERED_KEYS) %>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
82
+ <div class="card mb-4">
83
+ <div class="card-header d-flex justify-content-between align-items-center">
84
+ <h5 class="mb-0">
85
+ <i class="bi bi-translate text-warning me-2"></i>
86
+ <%= red_t("red.errors.missing_translations_page.all_title") %>
87
+ <span class="badge bg-warning text-dark"><%= @entry_count %></span>
88
+ </h5>
89
+ <small class="text-muted"><%== @pagy.info_tag %></small>
90
+ </div>
91
+ <div class="card-body p-0">
92
+ <div class="table-responsive">
93
+ <table class="table table-hover mb-0">
94
+ <thead class="table-light">
95
+ <tr>
96
+ <th><%= red_t("red.errors.missing_translations_page.column_key") %></th>
97
+ <th width="90"><%= red_t("red.errors.missing_translations_page.column_locale") %></th>
98
+ <th width="90"><%= red_t("red.errors.missing_translations_page.column_misses") %></th>
99
+ <th><%= red_t("red.errors.missing_translations_page.column_source") %></th>
100
+ <th width="140"><%= red_t("red.errors.missing_translations_page.column_first_seen") %></th>
101
+ <th width="140"><%= red_t("red.errors.pages.column_last_seen") %></th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ <% @entries.each do |entry| %>
106
+ <tr>
107
+ <td><code><%= entry[:key] %></code></td>
108
+ <%# A locale tag is a machine value, shown as-is. %>
109
+ <td><span class="badge bg-light text-dark"><%= entry[:locale] %></span></td>
110
+ <td><strong><%= entry[:count] %></strong></td>
111
+ <td>
112
+ <% if entry[:source].present? %>
113
+ <code><%= entry[:source] %></code>
114
+ <% else %>
115
+ <span class="text-muted">&mdash;</span>
116
+ <% end %>
117
+ </td>
118
+ <td><%= local_time_ago(entry[:first_seen]) %></td>
119
+ <td><%= local_time_ago(entry[:last_seen]) %></td>
120
+ </tr>
121
+ <% end %>
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+ </div>
126
+ <div class="card-footer border-top d-flex justify-content-between align-items-center">
127
+ <div>
128
+ <small class="text-muted">
129
+ <i class="bi bi-lightbulb text-warning"></i> <%= red_t("red.errors.missing_translations_page.footer_tip") %>
130
+ </small>
131
+ <small class="ms-3">
132
+ <a href="https://guides.rubyonrails.org/i18n.html" target="_blank" rel="noopener" class="text-decoration-none">
133
+ <i class="bi bi-book"></i> <%= red_t("red.errors.missing_translations_page.docs") %> <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
134
+ </a>
135
+ </small>
136
+ </div>
137
+ <div>
138
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <% end %>
143
+ </div>
@@ -201,14 +201,17 @@
201
201
  morphology applied to an identifier — it renders "iOS" as
202
202
  "IOS" in some engines and mangles the Turkish dotless i. %>
203
203
  <span style="font-size: 13px; font-weight: 600;"><%= platform %></span>
204
- <span class="badge bg-<%= health_status_color(health[:health_status]) %>"><%= health_status_text(health[:health_status]) %></span>
205
- </div>
206
- <div style="margin-bottom: var(--space-2);">
207
- <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>
208
- </div>
209
- <div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
210
- <div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
204
+ <span class="badge bg-<%= health_status_color(health[:health_status]) %>"<% if health[:health_status] == :not_rated %> title="<%= red_t("red.common.health_status.not_rated_hint") %>"<% end %>><%= health_status_text(health[:health_status]) %></span>
211
205
  </div>
206
+ <%# A lone platform has no score: a rating compares platforms %>
207
+ <% if health[:stability_score] %>
208
+ <div style="margin-bottom: var(--space-2);">
209
+ <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>
210
+ </div>
211
+ <div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
212
+ <div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
213
+ </div>
214
+ <% end %>
212
215
  <div style="font-size: 12px; color: var(--text-tertiary);">
213
216
  <%= red_t("red.errors.overview_page.platform_summary", total: health[:total_errors], critical: health[:critical_errors], unresolved: health[:unresolved_errors]) %>
214
217
  </div>
@@ -36,7 +36,7 @@
36
36
  <div class="row mb-4">
37
37
  <% @platform_health.each do |platform, health| %>
38
38
  <div class="col-md-6 col-lg-3 mb-3">
39
- <div class="card h-100 <%= health[:health_status] == :healthy ? 'border-success' : health[:health_status] == :warning ? 'border-warning' : 'border-danger' %>">
39
+ <div class="card h-100 <%= { healthy: 'border-success', warning: 'border-warning', not_rated: 'border-secondary' }.fetch(health[:health_status], 'border-danger') %>">
40
40
  <div class="card-body">
41
41
  <h5 class="card-title text-capitalize">
42
42
  <%= platform || red_t("red.common.unknown") %>
@@ -44,23 +44,28 @@
44
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
46
  <span class="badge bg-warning float-end"><%= red_t("red.analytics.platform_comparison.health_card.warning") %></span>
47
+ <% elsif health[:health_status] == :not_rated %>
48
+ <span class="badge bg-secondary float-end" title="<%= red_t("red.common.health_status.not_rated_hint") %>"><%= red_t("red.common.health_status.not_rated") %></span>
47
49
  <% else %>
48
50
  <span class="badge bg-danger float-end"><%= red_t("red.analytics.platform_comparison.health_card.critical") %></span>
49
51
  <% end %>
50
52
  </h5>
51
53
 
52
- <div class="mb-3">
53
- <div class="d-flex justify-content-between align-items-center mb-1">
54
- <small class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.stability_score") %></small>
55
- <strong><%= health[:stability_score] %>/100</strong>
56
- </div>
57
- <div class="progress" style="height: 8px;">
58
- <div class="progress-bar <%= health[:stability_score] >= 80 ? 'bg-success' : health[:stability_score] >= 60 ? 'bg-warning' : 'bg-danger' %>"
59
- role="progressbar"
60
- style="width: <%= health[:stability_score] %>%">
54
+ <%# A lone platform has no score: a rating compares platforms %>
55
+ <% if health[:stability_score] %>
56
+ <div class="mb-3">
57
+ <div class="d-flex justify-content-between align-items-center mb-1">
58
+ <small class="text-muted"><%= red_t("red.analytics.platform_comparison.health_card.stability_score") %></small>
59
+ <strong><%= health[:stability_score] %>/100</strong>
60
+ </div>
61
+ <div class="progress" style="height: 8px;">
62
+ <div class="progress-bar <%= health[:stability_score] >= 80 ? 'bg-success' : health[:stability_score] >= 60 ? 'bg-warning' : 'bg-danger' %>"
63
+ role="progressbar"
64
+ style="width: <%= health[:stability_score] %>%">
65
+ </div>
61
66
  </div>
62
67
  </div>
63
- </div>
68
+ <% end %>
64
69
 
65
70
  <div class="small">
66
71
  <div class="d-flex justify-content-between mb-1">
@@ -265,16 +265,19 @@
265
265
  end
266
266
 
267
267
  when :retention_days
268
- # Show retention configuration with manual cleanup instructions
268
+ # Retention is enforced by RetentionCleanupJob, which the host schedules
269
+ # (docs/PRODUCTION.md). The badge used to claim "manual cleanup only" and
270
+ # cite cleanup_resolved, a different task that deletes only resolved
271
+ # errors and ignores this setting.
269
272
  if value.present?
270
273
  %>
271
274
  <div>
272
275
  <span class="badge bg-warning text-dark fs-6"><%= red_tp("red.settings.values.days", count: value) %></span>
273
- <small class="text-danger d-block">
274
- <i class="bi bi-exclamation-triangle"></i> <%= red_t("red.settings.values.manual_cleanup_required") %>
276
+ <small class="text-muted d-block">
277
+ <i class="bi bi-clock-history"></i> <%= red_t("red.settings.values.retention_enforced_by_job") %>
275
278
  </small>
276
279
  <small class="text-muted d-block mt-1">
277
- <%= red_t("red.settings.values.run_command") %> <code class="small">rails error_dashboard:cleanup_resolved DAYS=<%= value %></code>
280
+ <%= red_t("red.settings.values.run_command") %> <code class="small">RailsErrorDashboard::RetentionCleanupJob.perform_now</code>
278
281
  </small>
279
282
  </div>
280
283
  <%
@@ -10,6 +10,7 @@
10
10
  enable_middleware: { type: :boolean },
11
11
  enable_error_subscriber: { type: :boolean },
12
12
  retention_days: { type: :retention_days },
13
+ occurrence_context_limit: { type: :integer, unit: :occurrences },
13
14
  max_backtrace_lines: { type: :integer, unit: :lines },
14
15
  sampling_rate: { type: :percentage }
15
16
  }
@@ -56,10 +57,15 @@
56
57
  notification_email_from: { type: :string, show_if: :enable_email_notifications },
57
58
  enable_discord_notifications: { type: :boolean },
58
59
  discord_webhook_url: { type: :url, show_if: :enable_discord_notifications },
60
+ enable_telegram_notifications: { type: :boolean },
61
+ telegram_bot_token: { type: :secret, show_if: :enable_telegram_notifications },
62
+ telegram_chat_id: { type: :string, show_if: :enable_telegram_notifications },
63
+ telegram_api_base_url: { type: :url, show_if: :enable_telegram_notifications },
59
64
  enable_pagerduty_notifications: { type: :boolean },
60
65
  pagerduty_integration_key: { type: :string, show_if: :enable_pagerduty_notifications },
61
66
  enable_webhook_notifications: { type: :boolean },
62
67
  webhook_urls: { type: :array, show_if: :enable_webhook_notifications },
68
+ webhook_signing_secret: { type: :secret, show_if: :enable_webhook_notifications },
63
69
  enable_scheduled_digests: { type: :boolean },
64
70
  digest_frequency: { type: :symbol, show_if: :enable_scheduled_digests },
65
71
  digest_recipients: { type: :array, show_if: :enable_scheduled_digests }
@@ -100,7 +106,7 @@
100
106
  enable_breadcrumbs: { type: :boolean },
101
107
  breadcrumb_buffer_size: { type: :integer, show_if: :enable_breadcrumbs, unit: :events },
102
108
  enable_n_plus_one_detection: { type: :boolean, show_if: :enable_breadcrumbs },
103
- n_plus_one_threshold: { type: :integer, show_if: :enable_n_plus_one_detection, unit: :queries }
109
+ n_plus_one_threshold: { type: :integer, show_if: [ :enable_breadcrumbs, :enable_n_plus_one_detection ], unit: :queries }
104
110
  }
105
111
  },
106
112
  :system_health => {
@@ -149,6 +155,7 @@
149
155
  color: "info",
150
156
  settings: {
151
157
  enable_rack_attack_tracking: { type: :boolean },
158
+ enable_missing_translation_tracking: { type: :boolean },
152
159
  enable_actioncable_tracking: { type: :boolean },
153
160
  enable_activestorage_tracking: { type: :boolean }
154
161
  }
@@ -166,7 +173,9 @@
166
173
  color: "light",
167
174
  settings: {
168
175
  enable_internal_logging: { type: :boolean },
169
- log_level: { type: :symbol, show_if: :enable_internal_logging }
176
+ # Not gated on enable_internal_logging: Logger.error honours log_level
177
+ # whether or not internal logging is on.
178
+ log_level: { type: :symbol }
170
179
  }
171
180
  }
172
181
  }
@@ -187,27 +196,33 @@
187
196
 
188
197
  <!-- Dynamically render setting groups -->
189
198
  <% setting_groups.each do |group_slug, group_config| %>
199
+ <%# One flag, or every flag in an array, must be on for a row to show. The
200
+ count and the rows both use this, so the count says what is shown. %>
201
+ <% shown_settings = group_config[:settings].select { |_key, meta| Array(meta[:show_if]).all? { |flag| @config.send(flag) } } %>
190
202
  <div class="card" style="margin-bottom: var(--space-4);">
191
203
  <button type="button" data-red-action="toggle-next" data-red-display="block" style="display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-4) var(--space-6); background: none; border: none; cursor: pointer; border-bottom: 1px solid var(--border-primary);">
192
204
  <span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="<%= group_config[:icon] %>" style="margin-right: 6px;"></i> <%= red_t("red.settings.groups.#{group_slug}") %></span>
193
205
  <div style="display: flex; align-items: center; gap: 8px;">
194
- <span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count: group_config[:settings].size) %></span>
206
+ <span style="font-size: 11px; color: var(--text-tertiary);"><%= red_tp("red.settings.option_count", count: shown_settings.size) %></span>
195
207
  <i class="bi bi-chevron-up chev" style="font-size: 12px; color: var(--text-tertiary);"></i>
196
208
  </div>
197
209
  </button>
198
210
  <div>
199
- <% group_config[:settings].each do |setting_key, setting_meta| %>
211
+ <% shown_settings.each do |setting_key, setting_meta| %>
200
212
  <%
201
- show_if_key = setting_meta[:show_if]
202
- should_show = show_if_key.nil? || @config.send(show_if_key)
203
- next unless should_show
204
213
  value = @config.send(setting_meta[:method] || setting_key)
214
+ # A credential is shown as Set / Not set whatever shape it has. The
215
+ # per-row :type documents the value's shape; this one rule is what
216
+ # keeps a webhook URL or an API key off the page, the same list
217
+ # Configuration#inspect masks, so a new secret cannot leak by a
218
+ # forgotten per-row type.
219
+ badge_type = RailsErrorDashboard::Configuration::SECRET_ATTRIBUTES.include?(setting_key) ? :secret : setting_meta[:type]
205
220
  %>
206
221
  <div style="display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--border-primary); font-size: 13px;">
207
222
  <span style="font-family: var(--font-mono); font-size: 12px; color: var(--accent); font-weight: 500; width: 240px; flex-shrink: 0;"><%= setting_key %></span>
208
223
  <span style="font-family: var(--font-mono); font-size: 12px; color: var(--text-primary); font-weight: 500; width: 180px; flex-shrink: 0;">
209
224
  <%= render partial: 'rails_error_dashboard/errors/settings/value_badge',
210
- locals: { value: value, type: setting_meta[:type], unit: setting_meta[:unit] } %>
225
+ locals: { value: value, type: badge_type, unit: setting_meta[:unit] } %>
211
226
  </span>
212
227
  <%# Keyed by the config option's own name, so a key is greppable
213
228
  from the initializer that sets it. %>
@@ -98,6 +98,8 @@
98
98
  </div>
99
99
  </div>
100
100
 
101
+ <%= render "occurrence_banner", error: @error, occurrence_context: @occurrence_context %>
102
+
101
103
  <!-- Main content: tabs + sidebar -->
102
104
  <div class="red-detail-grid">
103
105
  <!-- Left: tabbed content -->
@@ -142,18 +144,19 @@
142
144
  <% end %>
143
145
 
144
146
  <%= render "error_info", error: @error %>
145
- <%= render "local_variables", error: @error %>
146
- <%= render "instance_variables", error: @error %>
147
+ <%= render "local_variables", error: @snapshot %>
148
+ <%= render "instance_variables", error: @snapshot %>
147
149
  </div>
148
150
 
149
151
  <!-- Tab: Context -->
150
152
  <div id="panel-context">
151
153
  <%= render "request_context", error: @error %>
152
- <%= render "breadcrumbs_group", error: @error %>
154
+ <%= render "breadcrumbs_group", error: @snapshot %>
153
155
  </div>
154
156
 
155
157
  <!-- Tab: History -->
156
158
  <div id="panel-history">
159
+ <%= render "captured_occurrences", error: @error, occurrences: @captured_occurrences, current_id: @occurrence_context&.error_occurrence_id %>
157
160
  <%= render "timeline" %>
158
161
  <% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
159
162
  <%= render "discussion", error: @error %>
@@ -175,7 +178,7 @@
175
178
 
176
179
  <!-- Right: sidebar -->
177
180
  <div class="error-sidebar" style="display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; overflow-wrap: break-word;">
178
- <%= render "sidebar_metadata", error: @error, related_errors: @related_errors %>
181
+ <%= render "sidebar_metadata", error: @snapshot, related_errors: @related_errors %>
179
182
 
180
183
  <%= render "llm_summary", error: @error %>
181
184