rails_error_dashboard 0.8.4 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +35 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +107 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +91 -73
- data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
- data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
- data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
- data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
- data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
- data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
- data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
- data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
- data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
- data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
- data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
- data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
- data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +30 -29
- data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
- data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
- data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
- data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
- data/config/locales/de.yml +2007 -0
- data/config/locales/en.yml +2719 -0
- data/config/locales/es.yml +2079 -0
- data/config/locales/fr.yml +2082 -0
- data/config/locales/it.yml +2060 -0
- data/config/locales/ja.yml +1802 -0
- data/config/locales/pl.yml +2108 -0
- data/config/locales/pt-BR.yml +2064 -0
- data/config/locales/ru.yml +2109 -0
- data/config/locales/uk.yml +2106 -0
- data/config/locales/zh-CN.yml +1791 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
- data/lib/rails_error_dashboard/configuration.rb +23 -6
- data/lib/rails_error_dashboard/current.rb +56 -0
- data/lib/rails_error_dashboard/i18n_store.rb +293 -0
- data/lib/rails_error_dashboard/private_backend.rb +213 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
- data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
- data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
- data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
- data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
- data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
- data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
- data/lib/rails_error_dashboard/translation.rb +39 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +4 -0
- data/lib/tasks/error_dashboard.rake +5 -1
- metadata +35 -8
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
nil
|
|
27
27
|
end
|
|
28
28
|
%>
|
|
29
|
-
<html lang="
|
|
29
|
+
<html lang="<%= red_locale %>" data-theme="light">
|
|
30
30
|
<head>
|
|
31
31
|
<title><%= content_for?(:page_title) ? "#{content_for(:page_title)} | " : "" %><%= Rails.application.class.module_parent_name %> - RED</title>
|
|
32
32
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -48,6 +48,48 @@
|
|
|
48
48
|
})();
|
|
49
49
|
</script>
|
|
50
50
|
|
|
51
|
+
<!-- Translation payload for client-rendered strings (P3-T1).
|
|
52
|
+
In <head> and before every other inline block so any script, in head
|
|
53
|
+
or body, can call redT() without ordering assumptions. -->
|
|
54
|
+
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
|
|
55
|
+
window.RED_I18N = <%= js_safe_json(red_js_translations) %>;
|
|
56
|
+
(function(){
|
|
57
|
+
// Lookup + %{name} interpolation. Total by construction: a missing key
|
|
58
|
+
// returns the key itself, so a forgotten translation shows as
|
|
59
|
+
// "js.copied" on screen rather than "undefined" or a thrown error that
|
|
60
|
+
// would take the rest of the block down with it.
|
|
61
|
+
window.redT = function(key, params) {
|
|
62
|
+
var value;
|
|
63
|
+
try {
|
|
64
|
+
value = String(key).split('.').reduce(function(node, segment) {
|
|
65
|
+
return (node && typeof node === 'object') ? node[segment] : undefined;
|
|
66
|
+
}, window.RED_I18N);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return String(key);
|
|
69
|
+
}
|
|
70
|
+
// A plural key resolves to a branch of forms, not a string. When the
|
|
71
|
+
// caller passes a count, select the form here rather than making every
|
|
72
|
+
// call site do it — a ternary on "s" at the call site is the exact
|
|
73
|
+
// anti-pattern this whole namespace exists to avoid.
|
|
74
|
+
if (value && typeof value === 'object' && params && typeof params.count === 'number') {
|
|
75
|
+
var form = (params.count === 1 && typeof value.one === 'string') ? value.one : value.other;
|
|
76
|
+
// A locale supplying only :other still renders; so does one that
|
|
77
|
+
// supplies only :one, rather than either becoming "undefined".
|
|
78
|
+
if (typeof form !== 'string') form = value.one;
|
|
79
|
+
if (typeof form !== 'string') return String(key);
|
|
80
|
+
value = form;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== 'string') return String(key);
|
|
83
|
+
if (!params) return value;
|
|
84
|
+
// Leaves an unknown %{name} in place rather than blanking it — a
|
|
85
|
+
// visible marker beats silently losing the interpolated value.
|
|
86
|
+
return value.replace(/%\{(\w+)\}/g, function(match, name) {
|
|
87
|
+
return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
})();
|
|
91
|
+
</script>
|
|
92
|
+
|
|
51
93
|
<!-- Fonts -->
|
|
52
94
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
53
95
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -940,7 +982,7 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
940
982
|
<!-- Sidebar -->
|
|
941
983
|
<nav class="red-sidebar d-none d-md-flex" id="sidebar">
|
|
942
984
|
<!-- Logo -->
|
|
943
|
-
<a href="/" class="red-sidebar-logo" title="
|
|
985
|
+
<a href="/" class="red-sidebar-logo" title="<%= red_t("red.sidebar.back_to_app", app: Rails.application.class.module_parent_name) %>" style="text-decoration: none; color: inherit;">
|
|
944
986
|
<div class="red-sidebar-logo-icon">R</div>
|
|
945
987
|
<div>
|
|
946
988
|
<div style="font-size: 14px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em;">RED</div>
|
|
@@ -959,17 +1001,17 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
959
1001
|
|
|
960
1002
|
<!-- CORE section -->
|
|
961
1003
|
<div style="margin-bottom: var(--space-2);">
|
|
962
|
-
<div class="red-sidebar-section-label"
|
|
1004
|
+
<div class="red-sidebar-section-label"><%= red_t("red.nav.sections.core") %></div>
|
|
963
1005
|
<ul class="nav flex-column">
|
|
964
1006
|
<li class="nav-item">
|
|
965
1007
|
<%= link_to root_path(nav_params), class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
|
|
966
|
-
<i class="bi bi-grid-1x2"></i>
|
|
1008
|
+
<i class="bi bi-grid-1x2"></i> <%= red_t("red.nav.overview") %>
|
|
967
1009
|
<% end %>
|
|
968
1010
|
</li>
|
|
969
1011
|
<li class="nav-item">
|
|
970
1012
|
<%= link_to errors_path(nav_params), class: "nav-link #{(controller_name == 'errors' && action_name == 'index') ? 'active' : ''}" do %>
|
|
971
1013
|
<i class="bi bi-bug"></i>
|
|
972
|
-
<span style="flex: 1;"
|
|
1014
|
+
<span style="flex: 1;"><%= red_t("red.nav.errors") %></span>
|
|
973
1015
|
<% if unresolved_badge_count && unresolved_badge_count > 0 %>
|
|
974
1016
|
<span class="red-nav-badge"><%= unresolved_badge_count %></span>
|
|
975
1017
|
<% end %>
|
|
@@ -977,7 +1019,7 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
977
1019
|
</li>
|
|
978
1020
|
<li class="nav-item">
|
|
979
1021
|
<%= link_to analytics_errors_path(nav_params), class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}" do %>
|
|
980
|
-
<i class="bi bi-bar-chart-line"></i>
|
|
1022
|
+
<i class="bi bi-bar-chart-line"></i> <%= red_t("red.nav.analytics") %>
|
|
981
1023
|
<% end %>
|
|
982
1024
|
</li>
|
|
983
1025
|
</ul>
|
|
@@ -985,31 +1027,31 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
985
1027
|
|
|
986
1028
|
<!-- HEALTH section (feature-gated) -->
|
|
987
1029
|
<% health_items = [] %>
|
|
988
|
-
<% health_items << { path: platform_comparison_errors_path(nav_params), icon: 'bi-cpu', label:
|
|
1030
|
+
<% health_items << { path: platform_comparison_errors_path(nav_params), icon: 'bi-cpu', label: red_t("red.nav.platform") } %>
|
|
989
1031
|
<% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
990
|
-
<% health_items << { path: cache_health_summary_errors_path(nav_params), icon: 'bi-hdd-stack', label:
|
|
1032
|
+
<% health_items << { path: cache_health_summary_errors_path(nav_params), icon: 'bi-hdd-stack', label: red_t("red.nav.cache") } %>
|
|
991
1033
|
<% end %>
|
|
992
1034
|
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
993
|
-
<% health_items << { path: job_health_summary_errors_path(nav_params), icon: 'bi-gear-wide-connected', label:
|
|
994
|
-
<% health_items << { path: database_health_summary_errors_path(nav_params), icon: 'bi-database', label:
|
|
1035
|
+
<% health_items << { path: job_health_summary_errors_path(nav_params), icon: 'bi-gear-wide-connected', label: red_t("red.nav.jobs") } %>
|
|
1036
|
+
<% health_items << { path: database_health_summary_errors_path(nav_params), icon: 'bi-database', label: red_t("red.nav.database") } %>
|
|
995
1037
|
<% end %>
|
|
996
1038
|
<% if RailsErrorDashboard.configuration.enable_rack_attack_tracking %>
|
|
997
|
-
<% health_items << { path: rack_attack_summary_errors_path(nav_params), icon: 'bi-shield-exclamation', label:
|
|
1039
|
+
<% health_items << { path: rack_attack_summary_errors_path(nav_params), icon: 'bi-shield-exclamation', label: red_t("red.nav.rate_limits") } %>
|
|
998
1040
|
<% end %>
|
|
999
1041
|
<% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1000
|
-
<% health_items << { path: actioncable_health_summary_errors_path(nav_params), icon: 'bi-broadcast', label:
|
|
1042
|
+
<% health_items << { path: actioncable_health_summary_errors_path(nav_params), icon: 'bi-broadcast', label: red_t("red.nav.actioncable") } %>
|
|
1001
1043
|
<% end %>
|
|
1002
1044
|
<% if RailsErrorDashboard.configuration.enable_activestorage_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1003
|
-
<% health_items << { path: activestorage_health_summary_errors_path(nav_params), icon: 'bi-cloud-arrow-up', label:
|
|
1045
|
+
<% health_items << { path: activestorage_health_summary_errors_path(nav_params), icon: 'bi-cloud-arrow-up', label: red_t("red.nav.activestorage") } %>
|
|
1004
1046
|
<% end %>
|
|
1005
1047
|
<% if RailsErrorDashboard.configuration.enable_llm_observability && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1006
|
-
<% health_items << { path: llm_health_summary_errors_path(nav_params), icon: 'bi-cpu-fill', label:
|
|
1048
|
+
<% health_items << { path: llm_health_summary_errors_path(nav_params), icon: 'bi-cpu-fill', label: red_t("red.nav.llm") } %>
|
|
1007
1049
|
<% end %>
|
|
1008
1050
|
|
|
1009
1051
|
<% if health_items.any? %>
|
|
1010
1052
|
<div style="margin-bottom: var(--space-2);" id="navHealthSection">
|
|
1011
1053
|
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navHealthItems">
|
|
1012
|
-
|
|
1054
|
+
<%= red_t("red.nav.sections.health") %>
|
|
1013
1055
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navHealthChevron"></i>
|
|
1014
1056
|
</button>
|
|
1015
1057
|
<ul class="nav flex-column" id="navHealthItems">
|
|
@@ -1027,23 +1069,23 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1027
1069
|
<!-- DIAGNOSTICS section (feature-gated) -->
|
|
1028
1070
|
<% diag_items = [] %>
|
|
1029
1071
|
<% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1030
|
-
<% diag_items << { path: n_plus_one_summary_errors_path(nav_params), icon: 'bi-layers', label:
|
|
1031
|
-
<% diag_items << { path: deprecations_errors_path(nav_params), icon: 'bi-exclamation-triangle', label:
|
|
1072
|
+
<% diag_items << { path: n_plus_one_summary_errors_path(nav_params), icon: 'bi-layers', label: red_t("red.nav.n_plus_one") } %>
|
|
1073
|
+
<% diag_items << { path: deprecations_errors_path(nav_params), icon: 'bi-exclamation-triangle', label: red_t("red.nav.deprecations") } %>
|
|
1032
1074
|
<% end %>
|
|
1033
1075
|
<% if RailsErrorDashboard.configuration.detect_swallowed_exceptions %>
|
|
1034
|
-
<% diag_items << { path: swallowed_exceptions_errors_path(nav_params), icon: 'bi-eye-slash', label:
|
|
1076
|
+
<% diag_items << { path: swallowed_exceptions_errors_path(nav_params), icon: 'bi-eye-slash', label: red_t("red.nav.swallowed") } %>
|
|
1035
1077
|
<% end %>
|
|
1036
1078
|
<% if RailsErrorDashboard.configuration.enable_diagnostic_dump %>
|
|
1037
|
-
<% diag_items << { path: diagnostic_dumps_errors_path(nav_params), icon: 'bi-clipboard-pulse', label:
|
|
1079
|
+
<% diag_items << { path: diagnostic_dumps_errors_path(nav_params), icon: 'bi-clipboard-pulse', label: red_t("red.nav.diagnostic_dumps") } %>
|
|
1038
1080
|
<% end %>
|
|
1039
1081
|
<% if RailsErrorDashboard.configuration.enable_storm_protection %>
|
|
1040
|
-
<% diag_items << { path: storms_errors_path(nav_params), icon: 'bi-cloud-lightning-rain', label:
|
|
1082
|
+
<% diag_items << { path: storms_errors_path(nav_params), icon: 'bi-cloud-lightning-rain', label: red_t("red.nav.storms") } %>
|
|
1041
1083
|
<% end %>
|
|
1042
1084
|
|
|
1043
1085
|
<% if diag_items.any? %>
|
|
1044
1086
|
<div style="margin-bottom: var(--space-2);" id="navDiagSection">
|
|
1045
1087
|
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navDiagItems">
|
|
1046
|
-
|
|
1088
|
+
<%= red_t("red.nav.sections.diagnostics") %>
|
|
1047
1089
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navDiagChevron"></i>
|
|
1048
1090
|
</button>
|
|
1049
1091
|
<ul class="nav flex-column" id="navDiagItems">
|
|
@@ -1061,23 +1103,23 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1061
1103
|
<!-- INSIGHTS section -->
|
|
1062
1104
|
<div style="margin-bottom: var(--space-2);" id="navInsightsSection">
|
|
1063
1105
|
<button type="button" class="red-sidebar-section-label" data-red-action="toggle-nav-section" data-red-target="navInsightsItems">
|
|
1064
|
-
|
|
1106
|
+
<%= red_t("red.nav.sections.insights") %>
|
|
1065
1107
|
<i class="bi bi-chevron-down" style="font-size: 10px;" id="navInsightsChevron"></i>
|
|
1066
1108
|
</button>
|
|
1067
1109
|
<ul class="nav flex-column" id="navInsightsItems">
|
|
1068
1110
|
<li class="nav-item">
|
|
1069
1111
|
<%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
|
|
1070
|
-
<i class="bi bi-diagram-3"></i>
|
|
1112
|
+
<i class="bi bi-diagram-3"></i> <%= red_t("red.nav.correlation") %>
|
|
1071
1113
|
<% end %>
|
|
1072
1114
|
</li>
|
|
1073
1115
|
<li class="nav-item">
|
|
1074
1116
|
<%= link_to releases_errors_path(nav_params), class: "nav-link #{request.path == releases_errors_path ? 'active' : ''}" do %>
|
|
1075
|
-
<i class="bi bi-rocket-takeoff"></i>
|
|
1117
|
+
<i class="bi bi-rocket-takeoff"></i> <%= red_t("red.nav.releases") %>
|
|
1076
1118
|
<% end %>
|
|
1077
1119
|
</li>
|
|
1078
1120
|
<li class="nav-item">
|
|
1079
1121
|
<%= link_to user_impact_errors_path(nav_params), class: "nav-link #{request.path == user_impact_errors_path ? 'active' : ''}" do %>
|
|
1080
|
-
<i class="bi bi-people"></i>
|
|
1122
|
+
<i class="bi bi-people"></i> <%= red_t("red.nav.user_impact") %>
|
|
1081
1123
|
<% end %>
|
|
1082
1124
|
</li>
|
|
1083
1125
|
</ul>
|
|
@@ -1089,7 +1131,7 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1089
1131
|
<ul class="nav flex-column">
|
|
1090
1132
|
<li class="nav-item">
|
|
1091
1133
|
<%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
|
|
1092
|
-
<i class="bi bi-sliders"></i>
|
|
1134
|
+
<i class="bi bi-sliders"></i> <%= red_t("red.nav.settings") %>
|
|
1093
1135
|
<% end %>
|
|
1094
1136
|
</li>
|
|
1095
1137
|
</ul>
|
|
@@ -1106,13 +1148,13 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1106
1148
|
<i class="bi bi-list"></i>
|
|
1107
1149
|
</button>
|
|
1108
1150
|
<!-- Desktop sidebar toggle -->
|
|
1109
|
-
<button class="btn btn-link d-none d-md-flex" type="button" id="sidebarToggle" title="
|
|
1151
|
+
<button class="btn btn-link d-none d-md-flex" type="button" id="sidebarToggle" title="<%= red_t("red.navbar.toggle_sidebar") %>" style="font-size: 16px; color: var(--text-secondary);">
|
|
1110
1152
|
<i class="bi bi-layout-sidebar-inset"></i>
|
|
1111
1153
|
</button>
|
|
1112
1154
|
<!-- Search -->
|
|
1113
1155
|
<div style="position: relative;">
|
|
1114
1156
|
<i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
|
|
1115
|
-
<input class="red-search-input" placeholder="
|
|
1157
|
+
<input class="red-search-input" placeholder="<%= red_t("red.navbar.search_placeholder") %>" id="globalSearch" />
|
|
1116
1158
|
</div>
|
|
1117
1159
|
</div>
|
|
1118
1160
|
<div style="display: flex; align-items: center; gap: var(--space-3);">
|
|
@@ -1122,9 +1164,9 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1122
1164
|
<button class="red-app-switcher dropdown-toggle" type="button" id="appSwitcher" data-bs-toggle="dropdown" aria-expanded="false">
|
|
1123
1165
|
<i class="bi bi-layers"></i>
|
|
1124
1166
|
<% if params[:application_id].present? %>
|
|
1125
|
-
<%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first ||
|
|
1167
|
+
<%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || red_t("red.common.unknown") %>
|
|
1126
1168
|
<% else %>
|
|
1127
|
-
|
|
1169
|
+
<%= red_t("red.navbar.all_apps", count: @applications.size) %>
|
|
1128
1170
|
<% end %>
|
|
1129
1171
|
</button>
|
|
1130
1172
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
|
|
@@ -1141,7 +1183,7 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1141
1183
|
end
|
|
1142
1184
|
%>
|
|
1143
1185
|
<li>
|
|
1144
|
-
<%= link_to "
|
|
1186
|
+
<%= link_to red_t("red.navbar.all_applications"),
|
|
1145
1187
|
url_for(switcher_base.merge(switcher_filter_params.except(:application_id))),
|
|
1146
1188
|
class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
|
|
1147
1189
|
</li>
|
|
@@ -1156,8 +1198,42 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1156
1198
|
</ul>
|
|
1157
1199
|
</div>
|
|
1158
1200
|
<% end %>
|
|
1159
|
-
|
|
1160
|
-
|
|
1201
|
+
<%# Language picker (P5-T1). Only rendered when there is a choice
|
|
1202
|
+
to make: with a single shipped locale a dropdown listing it is
|
|
1203
|
+
noise. Each option is its own button_to, so switching is a real
|
|
1204
|
+
CSRF-protected POST and works without JavaScript. %>
|
|
1205
|
+
<% red_locale_options = RailsErrorDashboard::I18nStore.locale_options %>
|
|
1206
|
+
<% if red_locale_options.size > 1 %>
|
|
1207
|
+
<div class="dropdown">
|
|
1208
|
+
<button class="red-app-switcher dropdown-toggle" type="button" id="localePicker"
|
|
1209
|
+
data-bs-toggle="dropdown" aria-expanded="false"
|
|
1210
|
+
aria-label="<%= red_t("red.navbar.choose_language") %>"
|
|
1211
|
+
title="<%= red_t("red.navbar.choose_language") %>">
|
|
1212
|
+
<i class="bi bi-translate"></i>
|
|
1213
|
+
<span class="d-none d-md-inline"><%= RailsErrorDashboard::I18nStore::ENDONYMS.fetch(red_locale, red_locale) %></span>
|
|
1214
|
+
</button>
|
|
1215
|
+
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="localePicker">
|
|
1216
|
+
<% red_locale_options.each do |locale_code, endonym| %>
|
|
1217
|
+
<li>
|
|
1218
|
+
<%# The endonym is deliberately NOT translated — see
|
|
1219
|
+
I18nStore::ENDONYMS. lang= tells a screen reader to
|
|
1220
|
+
pronounce each entry in its own language. %>
|
|
1221
|
+
<%= button_to locale_path,
|
|
1222
|
+
method: :post,
|
|
1223
|
+
params: { locale: locale_code },
|
|
1224
|
+
class: "dropdown-item #{'active' if locale_code == red_locale}",
|
|
1225
|
+
form: { style: "display: contents;" },
|
|
1226
|
+
lang: locale_code,
|
|
1227
|
+
"aria-current": (locale_code == red_locale ? "true" : nil) do %>
|
|
1228
|
+
<%= endonym %>
|
|
1229
|
+
<% end %>
|
|
1230
|
+
</li>
|
|
1231
|
+
<% end %>
|
|
1232
|
+
</ul>
|
|
1233
|
+
</div>
|
|
1234
|
+
<% end %>
|
|
1235
|
+
<button type="button" title="<%= red_t("red.navbar.keyboard_shortcuts_title") %>" data-bs-toggle="modal" data-bs-target="#keyboardShortcutsModal" style="background: none; border: none; padding: 0; cursor: pointer; line-height: 1;"><kbd>?</kbd></button>
|
|
1236
|
+
<button class="red-theme-toggle" id="themeToggle" title="<%= red_t("red.navbar.toggle_theme") %>">
|
|
1161
1237
|
<i class="bi bi-moon" id="themeIcon"></i>
|
|
1162
1238
|
</button>
|
|
1163
1239
|
<span class="red-env-badge" style="background: var(--status-success-bg); color: var(--status-success);"><%= Rails.env %></span>
|
|
@@ -1170,11 +1246,9 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1170
1246
|
<div id="security-warning" class="alert alert-warning" style="display: flex; align-items: center; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid var(--status-warning);">
|
|
1171
1247
|
<i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; flex-shrink: 0;"></i>
|
|
1172
1248
|
<div style="flex: 1;">
|
|
1173
|
-
<strong
|
|
1174
|
-
Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
|
|
1175
|
-
or configure <code>authenticate_with</code> in your initializer.
|
|
1249
|
+
<strong><%= red_t("red.banners.credentials.label") %></strong> <%= red_t("red.banners.credentials.body_html") %>
|
|
1176
1250
|
</div>
|
|
1177
|
-
<button type="button" data-red-action="dismiss-creds-warning" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0 4px; flex-shrink: 0; line-height: 1;" title="
|
|
1251
|
+
<button type="button" data-red-action="dismiss-creds-warning" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0 4px; flex-shrink: 0; line-height: 1;" title="<%= red_t("red.banners.credentials.dismiss") %>">×</button>
|
|
1178
1252
|
</div>
|
|
1179
1253
|
<script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
|
|
1180
1254
|
<% end %>
|
|
@@ -1184,15 +1258,20 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1184
1258
|
<i class="bi <%= storm_active ? 'bi-cloud-lightning-rain-fill' : 'bi-cloud-check' %>" style="font-size: 18px; flex-shrink: 0;"></i>
|
|
1185
1259
|
<div style="flex: 1;">
|
|
1186
1260
|
<% if storm_active %>
|
|
1187
|
-
<strong
|
|
1188
|
-
|
|
1189
|
-
|
|
1261
|
+
<strong><%= red_t("red.banners.storm.active_title") %></strong>
|
|
1262
|
+
<%= red_t(
|
|
1263
|
+
@storm_banner_event.reached_open ? "red.banners.storm.active_paused" : "red.banners.storm.active_sampled",
|
|
1264
|
+
time: @storm_banner_event.started_at.strftime("%H:%M %Z")
|
|
1265
|
+
) %>
|
|
1190
1266
|
<% else %>
|
|
1191
|
-
<strong
|
|
1192
|
-
<%=
|
|
1193
|
-
|
|
1267
|
+
<strong><%= red_t("red.banners.storm.recent_title") %></strong>
|
|
1268
|
+
<%= red_t(
|
|
1269
|
+
"red.banners.storm.recent_body",
|
|
1270
|
+
time: @storm_banner_event.ended_at.strftime("%H:%M %Z"),
|
|
1271
|
+
count: number_with_delimiter(@storm_banner_event.events_counted_only.to_i + @storm_banner_event.events_overflow.to_i)
|
|
1272
|
+
) %>
|
|
1194
1273
|
<% end %>
|
|
1195
|
-
<a href="<%= storms_errors_path %>" style="margin-left: 4px;"
|
|
1274
|
+
<a href="<%= storms_errors_path %>" style="margin-left: 4px;"><%= red_t("red.banners.storm.view_history") %></a>
|
|
1196
1275
|
</div>
|
|
1197
1276
|
</div>
|
|
1198
1277
|
<% end %>
|
|
@@ -1203,10 +1282,10 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1203
1282
|
<footer class="red-footer">
|
|
1204
1283
|
<p style="margin-bottom: 4px;">
|
|
1205
1284
|
<i class="bi bi-bug-fill" style="color: var(--accent);"></i>
|
|
1206
|
-
|
|
1285
|
+
<%= red_t("red.footer.built_with") %> <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank"><strong>RED</strong> — Rails Error Dashboard</a>
|
|
1207
1286
|
</p>
|
|
1208
1287
|
<p style="font-size: 12px; color: var(--text-tertiary); margin: 0;">
|
|
1209
|
-
v<%= RailsErrorDashboard::VERSION %> ·
|
|
1288
|
+
v<%= RailsErrorDashboard::VERSION %> · <%= red_t("red.footer.credit_html", author: '<a href="https://anjan.dev" target="_blank">Anjan Jagirdar</a>'.html_safe) %>
|
|
1210
1289
|
</p>
|
|
1211
1290
|
</footer>
|
|
1212
1291
|
</div>
|
|
@@ -1215,25 +1294,25 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1215
1294
|
<!-- Mobile Sidebar (Offcanvas) -->
|
|
1216
1295
|
<div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
|
|
1217
1296
|
<div class="offcanvas-header">
|
|
1218
|
-
<a href="/" style="display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit;" title="
|
|
1297
|
+
<a href="/" style="display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit;" title="<%= red_t("red.sidebar.back_to_app_home") %>">
|
|
1219
1298
|
<div class="red-sidebar-logo-icon">R</div>
|
|
1220
1299
|
<strong>RED</strong>
|
|
1221
1300
|
</a>
|
|
1222
|
-
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="
|
|
1301
|
+
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="<%= red_t("red.common.close") %>"></button>
|
|
1223
1302
|
</div>
|
|
1224
1303
|
<div class="offcanvas-body">
|
|
1225
1304
|
<%
|
|
1226
1305
|
nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
|
|
1227
1306
|
%>
|
|
1228
1307
|
<ul class="nav flex-column">
|
|
1229
|
-
<li class="nav-item"><%= link_to root_path(nav_params), class: "nav-link" do %><i class="bi bi-grid-1x2"></i>
|
|
1230
|
-
<li class="nav-item"><%= link_to errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bug"></i>
|
|
1231
|
-
<li class="nav-item"><%= link_to analytics_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bar-chart-line"></i>
|
|
1232
|
-
<li class="nav-item"><%= link_to platform_comparison_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-cpu"></i>
|
|
1233
|
-
<li class="nav-item"><%= link_to correlation_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-diagram-3"></i>
|
|
1234
|
-
<li class="nav-item"><%= link_to releases_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-rocket-takeoff"></i>
|
|
1235
|
-
<li class="nav-item"><%= link_to user_impact_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-people"></i>
|
|
1236
|
-
<li class="nav-item"><%= link_to settings_path(nav_params), class: "nav-link" do %><i class="bi bi-sliders"></i>
|
|
1308
|
+
<li class="nav-item"><%= link_to root_path(nav_params), class: "nav-link" do %><i class="bi bi-grid-1x2"></i> <%= red_t("red.nav.overview") %><% end %></li>
|
|
1309
|
+
<li class="nav-item"><%= link_to errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bug"></i> <%= red_t("red.nav.errors") %><% end %></li>
|
|
1310
|
+
<li class="nav-item"><%= link_to analytics_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bar-chart-line"></i> <%= red_t("red.nav.analytics") %><% end %></li>
|
|
1311
|
+
<li class="nav-item"><%= link_to platform_comparison_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-cpu"></i> <%= red_t("red.nav.platform") %><% end %></li>
|
|
1312
|
+
<li class="nav-item"><%= link_to correlation_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-diagram-3"></i> <%= red_t("red.nav.correlation") %><% end %></li>
|
|
1313
|
+
<li class="nav-item"><%= link_to releases_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-rocket-takeoff"></i> <%= red_t("red.nav.releases") %><% end %></li>
|
|
1314
|
+
<li class="nav-item"><%= link_to user_impact_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-people"></i> <%= red_t("red.nav.user_impact") %><% end %></li>
|
|
1315
|
+
<li class="nav-item"><%= link_to settings_path(nav_params), class: "nav-link" do %><i class="bi bi-sliders"></i> <%= red_t("red.nav.settings") %><% end %></li>
|
|
1237
1316
|
</ul>
|
|
1238
1317
|
</div>
|
|
1239
1318
|
</div>
|
|
@@ -1244,30 +1323,31 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1244
1323
|
<div class="modal-content">
|
|
1245
1324
|
<div class="modal-header">
|
|
1246
1325
|
<h5 class="modal-title" id="keyboardShortcutsModalLabel">
|
|
1247
|
-
<i class="bi bi-keyboard"></i>
|
|
1326
|
+
<i class="bi bi-keyboard"></i> <%= red_t("red.shortcuts.title") %>
|
|
1248
1327
|
</h5>
|
|
1249
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="
|
|
1328
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<%= red_t("red.common.close") %>"></button>
|
|
1250
1329
|
</div>
|
|
1251
1330
|
<div class="modal-body">
|
|
1252
1331
|
<div class="list-group list-group-flush">
|
|
1332
|
+
<%# Key names (R, /, A, S, ?) stay literal — they are physical keys, not words. %>
|
|
1253
1333
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1254
|
-
<span><i class="bi bi-arrow-clockwise" style="color: var(--accent);"></i>
|
|
1334
|
+
<span><i class="bi bi-arrow-clockwise" style="color: var(--accent);"></i> <%= red_t("red.shortcuts.refresh") %></span>
|
|
1255
1335
|
<kbd>R</kbd>
|
|
1256
1336
|
</div>
|
|
1257
1337
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1258
|
-
<span><i class="bi bi-search" style="color: var(--accent);"></i>
|
|
1338
|
+
<span><i class="bi bi-search" style="color: var(--accent);"></i> <%= red_t("red.shortcuts.focus_search") %></span>
|
|
1259
1339
|
<kbd>/</kbd>
|
|
1260
1340
|
</div>
|
|
1261
1341
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1262
|
-
<span><i class="bi bi-bar-chart-line" style="color: var(--accent);"></i>
|
|
1342
|
+
<span><i class="bi bi-bar-chart-line" style="color: var(--accent);"></i> <%= red_t("red.shortcuts.analytics") %></span>
|
|
1263
1343
|
<kbd>A</kbd>
|
|
1264
1344
|
</div>
|
|
1265
1345
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1266
|
-
<span><i class="bi bi-layout-sidebar" style="color: var(--accent);"></i>
|
|
1346
|
+
<span><i class="bi bi-layout-sidebar" style="color: var(--accent);"></i> <%= red_t("red.shortcuts.toggle_sidebar") %></span>
|
|
1267
1347
|
<kbd>S</kbd>
|
|
1268
1348
|
</div>
|
|
1269
1349
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1270
|
-
<span><i class="bi bi-keyboard" style="color: var(--accent);"></i>
|
|
1350
|
+
<span><i class="bi bi-keyboard" style="color: var(--accent);"></i> <%= red_t("red.shortcuts.show_shortcuts") %></span>
|
|
1271
1351
|
<kbd>?</kbd>
|
|
1272
1352
|
</div>
|
|
1273
1353
|
</div>
|
|
@@ -1339,7 +1419,7 @@ tr[data-red-row-href]:hover .sev-bar { opacity: 1 !important; }
|
|
|
1339
1419
|
this.submitButtonTargets.forEach(function(btn) {
|
|
1340
1420
|
btn.disabled = true;
|
|
1341
1421
|
if (!btn.dataset.loadingOriginalHtml) { btn.dataset.loadingOriginalHtml = btn.innerHTML; }
|
|
1342
|
-
btn.innerHTML = '<span class="loading-spinner"></span>
|
|
1422
|
+
btn.innerHTML = '<span class="loading-spinner"></span> <%= red_js_t("red.ui_js.toast.loading") %>';
|
|
1343
1423
|
});
|
|
1344
1424
|
}
|
|
1345
1425
|
|
|
@@ -1511,18 +1591,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1511
1591
|
window.copyToClipboard = function(text, button) {
|
|
1512
1592
|
navigator.clipboard.writeText(text).then(function() {
|
|
1513
1593
|
var originalHTML = button.innerHTML;
|
|
1514
|
-
button.innerHTML = '<i class="bi bi-check"></i>
|
|
1594
|
+
button.innerHTML = '<i class="bi bi-check"></i> <%= red_js_t("red.ui_js.toast.copied") %>';
|
|
1515
1595
|
button.classList.remove('btn-outline-secondary');
|
|
1516
1596
|
button.classList.add('btn-success');
|
|
1517
|
-
showToast('
|
|
1597
|
+
showToast('<%= red_js_t("red.ui_js.toast.copied_to_clipboard") %>', 'success');
|
|
1518
1598
|
setTimeout(function() {
|
|
1519
1599
|
button.innerHTML = originalHTML;
|
|
1520
1600
|
button.classList.remove('btn-success');
|
|
1521
1601
|
button.classList.add('btn-outline-secondary');
|
|
1522
1602
|
}, 2000);
|
|
1523
1603
|
}).catch(function() {
|
|
1524
|
-
button.innerHTML = '<i class="bi bi-x"></i>
|
|
1525
|
-
showToast('
|
|
1604
|
+
button.innerHTML = '<i class="bi bi-x"></i> <%= red_js_t("red.ui_js.toast.failed") %>';
|
|
1605
|
+
showToast('<%= red_js_t("red.ui_js.toast.copy_failed") %>', 'danger');
|
|
1526
1606
|
});
|
|
1527
1607
|
};
|
|
1528
1608
|
|
|
@@ -1536,7 +1616,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1536
1616
|
'<div id="' + toastId + '" class="toast align-items-center text-white ' + bgClass + ' border-0" role="alert" aria-live="assertive" aria-atomic="true">' +
|
|
1537
1617
|
'<div class="d-flex">' +
|
|
1538
1618
|
'<div class="toast-body"><i class="bi ' + iconClass + ' me-2"></i>' + message + '</div>' +
|
|
1539
|
-
'<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="
|
|
1619
|
+
'<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="<%= red_js_t("red.common.close") %>"></button>' +
|
|
1540
1620
|
'</div>' +
|
|
1541
1621
|
'</div>';
|
|
1542
1622
|
var container = document.querySelector('.toast-container');
|
|
@@ -1561,7 +1641,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1561
1641
|
var formatted = formatDateTime(date, formatString);
|
|
1562
1642
|
var timezone = getTimezoneAbbreviation(date);
|
|
1563
1643
|
element.textContent = formatted + ' ' + timezone;
|
|
1564
|
-
element.title = '
|
|
1644
|
+
element.title = redDate('titles').local_time || RED_DATE_EN.titles.local_time;
|
|
1565
1645
|
element.style.cursor = 'pointer';
|
|
1566
1646
|
element.dataset.originalUtc = utcString;
|
|
1567
1647
|
element.dataset.localFormatted = formatted + ' ' + timezone;
|
|
@@ -1570,11 +1650,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1570
1650
|
if (this.dataset.showingLocal === 'true') {
|
|
1571
1651
|
var utcDate = new Date(this.dataset.originalUtc);
|
|
1572
1652
|
this.textContent = formatDateTime(utcDate, formatString) + ' UTC';
|
|
1573
|
-
this.title = '
|
|
1653
|
+
this.title = redDate('titles').utc_time || RED_DATE_EN.titles.utc_time;
|
|
1574
1654
|
this.dataset.showingLocal = 'false';
|
|
1575
1655
|
} else {
|
|
1576
1656
|
this.textContent = this.dataset.localFormatted;
|
|
1577
|
-
this.title = '
|
|
1657
|
+
this.title = redDate('titles').local_time || RED_DATE_EN.titles.local_time;
|
|
1578
1658
|
this.dataset.showingLocal = 'true';
|
|
1579
1659
|
}
|
|
1580
1660
|
});
|
|
@@ -1588,19 +1668,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1588
1668
|
var date = new Date(utcString);
|
|
1589
1669
|
if (isNaN(date.getTime())) return;
|
|
1590
1670
|
element.textContent = formatRelativeTime(new Date() - date);
|
|
1591
|
-
element.title = '
|
|
1671
|
+
element.title = redDate('titles').exact_time || RED_DATE_EN.titles.exact_time;
|
|
1592
1672
|
element.style.cursor = 'pointer';
|
|
1593
1673
|
element.dataset.originalUtc = utcString;
|
|
1594
1674
|
element.dataset.showingRelative = 'true';
|
|
1595
1675
|
element.addEventListener('click', function() {
|
|
1596
1676
|
if (this.dataset.showingRelative === 'true') {
|
|
1597
1677
|
var d = new Date(this.dataset.originalUtc);
|
|
1598
|
-
|
|
1599
|
-
this.
|
|
1678
|
+
var fullFmt = (window.RED_I18N && window.RED_I18N.formats && typeof window.RED_I18N.formats.full === 'string') ? window.RED_I18N.formats.full : '%B %d, %Y %I:%M:%S %p';
|
|
1679
|
+
this.textContent = formatDateTime(d, fullFmt) + ' ' + getTimezoneAbbreviation(d);
|
|
1680
|
+
this.title = redDate('titles').relative_time || RED_DATE_EN.titles.relative_time;
|
|
1600
1681
|
this.dataset.showingRelative = 'false';
|
|
1601
1682
|
} else {
|
|
1602
1683
|
this.textContent = formatRelativeTime(new Date() - new Date(this.dataset.originalUtc));
|
|
1603
|
-
this.title = '
|
|
1684
|
+
this.title = redDate('titles').exact_time || RED_DATE_EN.titles.exact_time;
|
|
1604
1685
|
this.dataset.showingRelative = 'true';
|
|
1605
1686
|
}
|
|
1606
1687
|
});
|
|
@@ -1608,14 +1689,62 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1608
1689
|
});
|
|
1609
1690
|
}
|
|
1610
1691
|
|
|
1692
|
+
// English defaults for every localized value below. RED_I18N is written in
|
|
1693
|
+
// <head> and should always be there, but a cached page or a JS error earlier
|
|
1694
|
+
// in the document would leave these functions rendering "undefined" into
|
|
1695
|
+
// every timestamp on the page. Falling back to English is the same choice
|
|
1696
|
+
// I18nStore makes on the server.
|
|
1697
|
+
var RED_DATE_EN = {
|
|
1698
|
+
months: ['January','February','March','April','May','June','July','August','September','October','November','December'],
|
|
1699
|
+
months_short: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
|
1700
|
+
days: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
|
1701
|
+
days_short: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
|
|
1702
|
+
meridian: { am: 'AM', pm: 'PM' },
|
|
1703
|
+
intl_locale: 'en-US',
|
|
1704
|
+
duration: {
|
|
1705
|
+
seconds: { one: '1 second', other: '%{count} seconds' },
|
|
1706
|
+
minutes: { one: '1 minute', other: '%{count} minutes' },
|
|
1707
|
+
hours: { one: '1 hour', other: '%{count} hours' },
|
|
1708
|
+
days: { one: '1 day', other: '%{count} days' },
|
|
1709
|
+
months: { one: '1 month', other: '%{count} months' },
|
|
1710
|
+
years: { one: '1 year', other: '%{count} years' }
|
|
1711
|
+
},
|
|
1712
|
+
titles: {
|
|
1713
|
+
local_time: 'Your local time (click to see UTC)',
|
|
1714
|
+
utc_time: 'UTC time (click to see local time)',
|
|
1715
|
+
exact_time: 'Click to see exact time',
|
|
1716
|
+
relative_time: 'Click to see relative time'
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
// Read one localized date value, falling back to the English default at the
|
|
1721
|
+
// same path. Only accepts a value of the same shape as the default, so a
|
|
1722
|
+
// half-written locale cannot substitute a string where an array is indexed.
|
|
1723
|
+
function redDate(name) {
|
|
1724
|
+
var fallback = RED_DATE_EN[name];
|
|
1725
|
+
try {
|
|
1726
|
+
var value = window.RED_I18N && window.RED_I18N.js ? window.RED_I18N.js[name] : undefined;
|
|
1727
|
+
if (Array.isArray(fallback)) {
|
|
1728
|
+
return (Array.isArray(value) && value.length === fallback.length) ? value : fallback;
|
|
1729
|
+
}
|
|
1730
|
+
if (value && typeof value === 'object') return value;
|
|
1731
|
+
return fallback;
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
return fallback;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1611
1737
|
function formatDateTime(date, fmt) {
|
|
1612
|
-
var months =
|
|
1613
|
-
var
|
|
1614
|
-
var
|
|
1615
|
-
var daysShort = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
1738
|
+
var months = redDate('months'), monthsShort = redDate('months_short');
|
|
1739
|
+
var days = redDate('days'), daysShort = redDate('days_short');
|
|
1740
|
+
var meridian = redDate('meridian');
|
|
1616
1741
|
var y = date.getFullYear(), mo = date.getMonth(), d = date.getDate();
|
|
1617
1742
|
var h = date.getHours(), mi = date.getMinutes(), s = date.getSeconds(), dow = date.getDay();
|
|
1618
|
-
|
|
1743
|
+
// A 24-hour locale may translate these to empty strings, which is correct
|
|
1744
|
+
// and must not become the string "undefined".
|
|
1745
|
+
var pm = typeof meridian.pm === 'string' ? meridian.pm : RED_DATE_EN.meridian.pm;
|
|
1746
|
+
var am = typeof meridian.am === 'string' ? meridian.am : RED_DATE_EN.meridian.am;
|
|
1747
|
+
var h12 = h % 12 || 12, ampm = h >= 12 ? pm : am;
|
|
1619
1748
|
var pad = function(n) { return n.toString().padStart(2, '0'); };
|
|
1620
1749
|
return fmt.replace('%Y',y).replace('%y',y.toString().substr(2)).replace('%B',months[mo]).replace('%b',monthsShort[mo])
|
|
1621
1750
|
.replace('%m',pad(mo+1)).replace('%d',pad(d)).replace('%e',d).replace('%A',days[dow]).replace('%a',daysShort[dow])
|
|
@@ -1623,18 +1752,52 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1623
1752
|
}
|
|
1624
1753
|
|
|
1625
1754
|
function getTimezoneAbbreviation(date) {
|
|
1626
|
-
|
|
1755
|
+
// RED's locale set and the browser's Intl data need not agree, so an
|
|
1756
|
+
// unsupported tag throws RangeError here rather than degrading. Catch it
|
|
1757
|
+
// and use en-US: an abbreviation in the wrong language beats no timestamp.
|
|
1758
|
+
var tag = redDate('intl_locale');
|
|
1759
|
+
if (typeof tag !== 'string' || !tag) tag = RED_DATE_EN.intl_locale;
|
|
1760
|
+
var parts;
|
|
1761
|
+
try {
|
|
1762
|
+
parts = date.toLocaleTimeString(tag, { timeZoneName: 'short' }).split(' ');
|
|
1763
|
+
} catch (e) {
|
|
1764
|
+
parts = date.toLocaleTimeString(RED_DATE_EN.intl_locale, { timeZoneName: 'short' }).split(' ');
|
|
1765
|
+
}
|
|
1627
1766
|
return parts[parts.length - 1];
|
|
1628
1767
|
}
|
|
1629
1768
|
|
|
1769
|
+
// Plural selection. English needs one/other; other languages need more, and
|
|
1770
|
+
// some need only one form. Falling back to 'other' means a locale that ships
|
|
1771
|
+
// a partial set still renders rather than showing "undefined".
|
|
1772
|
+
function redPluralize(forms, count) {
|
|
1773
|
+
if (!forms || typeof forms !== 'object') return String(count);
|
|
1774
|
+
var form = (count === 1 && typeof forms.one === 'string') ? forms.one : forms.other;
|
|
1775
|
+
if (typeof form !== 'string') form = forms.one;
|
|
1776
|
+
if (typeof form !== 'string') return String(count);
|
|
1777
|
+
return form.replace(/%\{count\}/g, String(count));
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1630
1780
|
function formatRelativeTime(diffMs) {
|
|
1631
1781
|
var sec = Math.floor(diffMs / 1000), min = Math.floor(sec / 60), hr = Math.floor(min / 60), d = Math.floor(hr / 24), mo = Math.floor(d / 30), yr = Math.floor(d / 365);
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1782
|
+
var units = redDate('duration'), duration;
|
|
1783
|
+
var pick = function(name, n) {
|
|
1784
|
+
var forms = units && units[name] ? units[name] : RED_DATE_EN.duration[name];
|
|
1785
|
+
return redPluralize(forms, n);
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// Thresholds unchanged from the pre-i18n version, including sec <= 1
|
|
1789
|
+
// reading as "1 second" for a 0-second difference.
|
|
1790
|
+
if (sec < 60) duration = pick('seconds', sec <= 1 ? 1 : sec);
|
|
1791
|
+
else if (min < 60) duration = pick('minutes', min);
|
|
1792
|
+
else if (hr < 24) duration = pick('hours', hr);
|
|
1793
|
+
else if (d < 30) duration = pick('days', d);
|
|
1794
|
+
else if (mo < 12) duration = pick('months', mo);
|
|
1795
|
+
else duration = pick('years', yr);
|
|
1796
|
+
|
|
1797
|
+
// "%{duration} ago" as one key — never duration + ' ago'. Several
|
|
1798
|
+
// languages put the equivalent of "ago" first. Same key the server uses.
|
|
1799
|
+
var template = (window.RED_I18N && typeof window.RED_I18N.ago === 'string') ? window.RED_I18N.ago : '%{duration} ago';
|
|
1800
|
+
return template.replace(/%\{duration\}/g, duration);
|
|
1638
1801
|
}
|
|
1639
1802
|
|
|
1640
1803
|
convertToLocalTime();
|
|
@@ -1823,13 +1986,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1823
1986
|
var button = form.querySelector('button[type="submit"]');
|
|
1824
1987
|
var question = input ? input.value.trim() : '';
|
|
1825
1988
|
if (!question) {
|
|
1826
|
-
if (status) status.textContent = '
|
|
1989
|
+
if (status) status.textContent = '<%= red_js_t("red.ui_js.ai_help.question_required") %>';
|
|
1827
1990
|
return;
|
|
1828
1991
|
}
|
|
1829
1992
|
|
|
1830
1993
|
appendAiHelpMessage('user', question);
|
|
1831
1994
|
if (input) input.value = '';
|
|
1832
|
-
if (status) status.textContent = '
|
|
1995
|
+
if (status) status.textContent = '<%= red_js_t("red.ui_js.ai_help.streaming") %>';
|
|
1833
1996
|
if (button) button.disabled = true;
|
|
1834
1997
|
|
|
1835
1998
|
var tokenMeta = document.querySelector('meta[name="csrf-token"]');
|
|
@@ -1848,7 +2011,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1848
2011
|
});
|
|
1849
2012
|
}
|
|
1850
2013
|
if (!response.body || !window.TextDecoder) {
|
|
1851
|
-
throw new Error('
|
|
2014
|
+
throw new Error('<%= red_js_t("red.ui_js.ai_help.streaming_unsupported") %>');
|
|
1852
2015
|
}
|
|
1853
2016
|
|
|
1854
2017
|
var assistantMessage = appendAiHelpMessage('assistant', '');
|
|
@@ -1916,7 +2079,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
1916
2079
|
return readNext();
|
|
1917
2080
|
}).catch(function(error) {
|
|
1918
2081
|
appendAiHelpMessage('error', error.message);
|
|
1919
|
-
if (status) status.textContent = '
|
|
2082
|
+
if (status) status.textContent = '<%= red_js_t("red.ui_js.ai_help.request_failed") %>';
|
|
1920
2083
|
}).finally(function() {
|
|
1921
2084
|
if (button) button.disabled = false;
|
|
1922
2085
|
if (input) input.focus();
|