rails_nexus 2.0.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,554 @@
1
+ <div class="rn-detail-panel" style="border-left: none; border-radius: var(--rn-radius-lg); overflow: hidden" data-controller="rails_nexus-detail">
2
+
3
+ <%# ─── Detail Header ───────────────────────────────────────── %>
4
+ <div class="rn-detail-header">
5
+ <div class="min-w-0 flex-1">
6
+ <%# Breadcrumbs %>
7
+ <nav class="rn-breadcrumbs" style="padding: 0; margin-bottom: 0.5rem">
8
+ <a href="<%= logged_exceptions_path %>" class="rn-breadcrumb">Exceptions</a>
9
+ <span class="rn-breadcrumb-sep">/</span>
10
+ <span class="rn-breadcrumb-current"><%= @exception.exception_class %></span>
11
+ </nav>
12
+
13
+ <div class="flex items-center gap-2 flex-wrap">
14
+ <span class="rn-badge rn-badge-<%= severity_for(@exception) %>"><%= @exception.exception_class %></span>
15
+ <span class="rn-mono text-xs" style="color: var(--rn-text-secondary)"><%= @exception.controller_name %>#<%= @exception.action_name %></span>
16
+ </div>
17
+
18
+ <p class="mt-1.5 text-xs" style="color: var(--rn-text-muted)">
19
+ <time datetime="<%= @exception.created_at.iso8601 %>"><%= @exception.created_at.strftime("%B %d, %Y at %I:%M:%S %p") %></time>
20
+ <% if @exception.remote_ip.present? %>
21
+ <span style="color: var(--rn-border)">·</span>
22
+ <span class="rn-mono"><%= @exception.remote_ip %></span>
23
+ <% end %>
24
+ </p>
25
+ </div>
26
+
27
+ <div class="flex items-center gap-2 shrink-0">
28
+ <%# Copy exception ID %>
29
+ <button type="button"
30
+ class="rn-copy-btn"
31
+ data-action="click->rails_nexus-detail#copyId"
32
+ data-rails_nexus-detail-id-value="<%= @exception.id %>"
33
+ title="Copy exception ID">
34
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" /></svg>
35
+ <span>ID</span>
36
+ </button>
37
+
38
+ <%# Copy message %>
39
+ <button type="button"
40
+ class="rn-copy-btn"
41
+ data-action="click->rails_nexus-detail#copyMessage"
42
+ data-rails_nexus-detail-message-value="<%= @exception.message %>"
43
+ title="Copy message">
44
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>
45
+ <span>Message</span>
46
+ </button>
47
+
48
+ <%# Close %>
49
+ <button type="button"
50
+ class="rn-icon-btn shrink-0"
51
+ data-action="click->rails_nexus-detail#closeDetails"
52
+ title="Close (Esc)">
53
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
54
+ </button>
55
+ </div>
56
+ </div>
57
+
58
+ <%# ─── Tabs ─────────────────────────────────────────────────── %>
59
+ <% if @exception_tabs && @exception_tabs.values.count(true) > 1 %>
60
+ <div class="rn-tabs" data-rails_nexus-detail-target="tabs">
61
+ <button type="button" class="rn-tab active" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="overview">Overview</button>
62
+ <% if @exception_tabs[:backtrace] %>
63
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="backtrace">
64
+ Backtrace
65
+ <span class="rn-tab-badge"><%= @exception.backtrace.to_s.split("\n").length %></span>
66
+ </button>
67
+ <% end %>
68
+ <% if @exception_tabs[:request] %>
69
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="request">Request</button>
70
+ <% end %>
71
+ <% if @exception_tabs[:environment] %>
72
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="environment">Environment</button>
73
+ <% end %>
74
+ <% if @exception_tabs[:user_info] %>
75
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="user_info">User Info</button>
76
+ <% end %>
77
+ <% if @exception.respond_to?(:cause_chain) && @exception.cause_chain.present? && @exception.cause_chain.is_a?(Array) && @exception.cause_chain.length > 0 %>
78
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="cause_chain">
79
+ Cause Chain
80
+ <span class="rn-tab-badge"><%= @exception.cause_chain.length %></span>
81
+ </button>
82
+ <% end %>
83
+ <% if @exception.respond_to?(:breadcrumbs) && @exception.breadcrumbs.present? %>
84
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="breadcrumbs">Breadcrumbs</button>
85
+ <% end %>
86
+ <% if @exception.respond_to?(:system_health) && @exception.system_health.present? %>
87
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="system_health">System Health</button>
88
+ <% end %>
89
+ <button type="button" class="rn-tab" data-action="click->rails_nexus-detail#switchTab" data-rn-tab-param="workflow">
90
+ Workflow
91
+ <% if @exception.comments_count > 0 %>
92
+ <span class="rn-tab-badge"><%= @exception.comments_count %></span>
93
+ <% end %>
94
+ </button>
95
+ </div>
96
+ <% end %>
97
+
98
+ <%# ─── Detail Body ─────────────────────────────────────────── %>
99
+ <div class="rn-detail-body">
100
+
101
+ <%# ─── Tab: Overview ────────────────────────────────────── %>
102
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="overview">
103
+
104
+ <%# Message %>
105
+ <div class="rn-detail-section">
106
+ <div class="rn-detail-section-header">
107
+ <h3>Message</h3>
108
+ </div>
109
+ <div class="rn-detail-code"><%= @exception.message %></div>
110
+ </div>
111
+
112
+ <%# Metadata Grid %>
113
+ <% if RailsNexus.configuration.show_metadata %>
114
+ <div class="rn-detail-section">
115
+ <h3 style="margin-bottom: 0.5rem">Details</h3>
116
+ <div class="rn-meta">
117
+ <div class="rn-meta-item">
118
+ <div class="rn-meta-label">Exception</div>
119
+ <div class="rn-meta-value"><%= @exception.exception_class %></div>
120
+ </div>
121
+ <div class="rn-meta-item">
122
+ <div class="rn-meta-label">Controller</div>
123
+ <div class="rn-meta-value mono"><%= @exception.controller_name %></div>
124
+ </div>
125
+ <div class="rn-meta-item">
126
+ <div class="rn-meta-label">Action</div>
127
+ <div class="rn-meta-value mono"><%= @exception.action_name %></div>
128
+ </div>
129
+ <div class="rn-meta-item">
130
+ <div class="rn-meta-label">Time</div>
131
+ <div class="rn-meta-value"><time datetime="<%= @exception.created_at.iso8601 %>"><%= @exception.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></time></div>
132
+ </div>
133
+ <% if @exception.remote_ip.present? %>
134
+ <div class="rn-meta-item">
135
+ <div class="rn-meta-label">IP Address</div>
136
+ <div class="rn-meta-value mono"><%= @exception.remote_ip %></div>
137
+ </div>
138
+ <% end %>
139
+ <% if @exception.user_agent.present? %>
140
+ <div class="rn-meta-item" style="grid-column: span 2">
141
+ <div class="rn-meta-label">User Agent</div>
142
+ <div class="rn-meta-value" style="font-size: 0.8125rem; word-break: break-all"><%= @exception.user_agent %></div>
143
+ </div>
144
+ <% end %>
145
+ <div class="rn-meta-item">
146
+ <div class="rn-meta-label">Exception ID</div>
147
+ <div class="rn-meta-value mono"><%= @exception.id %></div>
148
+ </div>
149
+ <% if @exception.id_value.present? %>
150
+ <div class="rn-meta-item">
151
+ <div class="rn-meta-label">ID Value</div>
152
+ <div class="rn-meta-value mono"><%= @exception.id_value %></div>
153
+ </div>
154
+ <% end %>
155
+ </div>
156
+ </div>
157
+ <% end %>
158
+
159
+ <%# Request Params %>
160
+ <% if RailsNexus.configuration.show_request && @exception.request.present? %>
161
+ <div class="rn-detail-section">
162
+ <div class="rn-detail-section-header">
163
+ <h3>Request</h3>
164
+ </div>
165
+ <div class="rn-detail-code"><%= pretty_format(@exception.request) %></div>
166
+ </div>
167
+ <% end %>
168
+
169
+ <%# User Info %>
170
+ <% if @exception.user_info.present? %>
171
+ <div class="rn-detail-section">
172
+ <div class="rn-detail-section-header">
173
+ <h3>User Info</h3>
174
+ </div>
175
+ <div class="rn-detail-code"><%= pretty_format(@exception.user_info) %></div>
176
+ </div>
177
+ <% end %>
178
+ </div>
179
+
180
+ <%# ─── Tab: Backtrace ────────────────────────────────────── %>
181
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="backtrace" style="display: none">
182
+ <% if @exception.backtrace.present? %>
183
+ <% bt = format_backtrace(@exception.backtrace) %>
184
+ <div class="rn-detail-section">
185
+ <div class="rn-detail-section-header">
186
+ <h3>Stack Trace</h3>
187
+ <div class="flex items-center gap-2">
188
+ <button type="button" class="rn-copy-btn" data-action="click->rails_nexus-detail#copyBacktrace"
189
+ data-rails_nexus-detail-backtrace-value="<%= @exception.backtrace %>">
190
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" /></svg>
191
+ Copy
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ <%# Backtrace with inline source code %>
197
+ <div class="space-y-1">
198
+ <% lines = @exception.backtrace.to_s.split("\n") %>
199
+ <% display_limit = RailsNexus.configuration.backtrace_limit %>
200
+ <% lines.each_with_index do |line, idx| %>
201
+ <% parsed = parse_backtrace_line(line) %>
202
+ <% is_app_code = parsed && parsed[:file]&.start_with?(Rails.root.to_s) %>
203
+ <% is_top_frame = idx < display_limit %>
204
+
205
+ <div class="flex items-start gap-2 py-0.5 px-2 rounded text-xs font-mono
206
+ <%= 'rn-bt-app' if is_app_code %>
207
+ <%= 'rn-bt-highlight' if idx == 0 %>
208
+ <%= 'rn-bt-hidden' if !is_top_frame && lines.length > display_limit %>"
209
+ style="color: var(--rn-text-secondary); <%= 'display: none' if !is_top_frame && lines.length > display_limit %>"
210
+ data-rails_nexus-detail-target="<%= 'backtraceHidden' if !is_top_frame && lines.length > display_limit %>">
211
+
212
+ <%# Line indicator %>
213
+ <span class="select-none" style="color: var(--rn-text-muted); min-width: 1.5rem; text-align: right"><%= idx + 1 %></span>
214
+
215
+ <%# Frame content %>
216
+ <% if parsed %>
217
+ <span class="flex-1 min-w-0">
218
+ <% if is_app_code %>
219
+ <span style="color: var(--rn-primary); font-weight: 500"><%= parsed[:file].sub(Rails.root.to_s + '/', '') %></span>
220
+ <% else %>
221
+ <span><%= line %></span>
222
+ <% end %>
223
+ <span style="color: var(--rn-text-muted)">:<%= parsed[:line] %></span>
224
+ <% if parsed[:method] %>
225
+ <span style="color: var(--rn-text-muted)"> in `<%= parsed[:method] %>'</span>
226
+ <% end %>
227
+ </span>
228
+
229
+ <%# Source code toggle for app files %>
230
+ <% if is_app_code && RailsNexus.configuration.source_code_viewing %>
231
+ <button type="button" class="rn-source-toggle"
232
+ data-action="click->rails_nexus-detail#toggleSource"
233
+ data-file="<%= parsed[:file] %>"
234
+ data-line="<%= parsed[:line] %>"
235
+ title="View source code">
236
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" /></svg>
237
+ </button>
238
+ <% end %>
239
+ <% else %>
240
+ <span><%= line %></span>
241
+ <% end %>
242
+ </div>
243
+
244
+ <%# Source code inline (rendered for top 3 app frames) %>
245
+ <% if is_app_code && RailsNexus.configuration.source_code_viewing && idx < 3 %>
246
+ <% source_data = read_source_snippet(parsed[:file], parsed[:line], context_lines: 5) %>
247
+ <% blame_data = git_blame_for_line(parsed[:file], parsed[:line]) %>
248
+ <% if source_data %>
249
+ <div class="rn-source-panel" style="margin-left: 2rem">
250
+ <% if blame_data %>
251
+ <div class="rn-blame-header">
252
+ <span class="blame-author"><%= blame_data[:author] || 'unknown' %></span>
253
+ <span>·</span>
254
+ <span><%= blame_data[:date] %></span>
255
+ <span>·</span>
256
+ <span><%= blame_data[:message] %></span>
257
+ <span class="blame-sha"><%= blame_data[:sha] %></span>
258
+ </div>
259
+ <% end %>
260
+ <div class="rn-source-code" style="background: var(--rn-bg-primary)">
261
+ <% source_data[:lines].each do |src_line| %>
262
+ <div class="source-line<%= ' is-error' if src_line[:is_error] %>">
263
+ <span class="line-number"><%= src_line[:number] %></span>
264
+ <span class="line-content"><%= src_line[:content] %></span>
265
+ </div>
266
+ <% end %>
267
+ </div>
268
+ </div>
269
+ <% end %>
270
+ <% elsif is_app_code && RailsNexus.configuration.source_code_viewing %>
271
+ <div class="rn-source-panel" style="display: none; margin-left: 2rem"
272
+ data-rails_nexus-detail-target="sourcePanel"
273
+ data-file="<%= parsed[:file] %>"
274
+ data-line="<%= parsed[:line] %>">
275
+ <div class="rn-source-loading text-xs" style="color: var(--rn-text-muted); padding: 0.5rem">
276
+ Loading source...
277
+ </div>
278
+ </div>
279
+ <% end %>
280
+ <% end %>
281
+ </div>
282
+
283
+ <% if lines.length > display_limit %>
284
+ <button type="button" class="rn-btn rn-btn-ghost rn-btn-sm mt-2"
285
+ data-action="click->rails_nexus-detail#expandBacktrace">
286
+ Show <%= lines.length - display_limit %> more frames
287
+ </button>
288
+ <% end %>
289
+ </div>
290
+ <% else %>
291
+ <div class="text-center py-8">
292
+ <p class="text-sm" style="color: var(--rn-text-muted)">No backtrace available</p>
293
+ </div>
294
+ <% end %>
295
+ </div>
296
+
297
+ <%# ─── Tab: Request ──────────────────────────────────────── %>
298
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="request" style="display: none">
299
+ <% if @exception.request.present? %>
300
+ <div class="rn-detail-section">
301
+ <div class="rn-detail-section-header">
302
+ <h3>Request Data</h3>
303
+ </div>
304
+ <div class="rn-detail-code" style="max-height: 32rem; overflow: auto"><%= pretty_format(@exception.request) %></div>
305
+ </div>
306
+ <% else %>
307
+ <div class="text-center py-8">
308
+ <p class="text-sm" style="color: var(--rn-text-muted)">No request data available</p>
309
+ </div>
310
+ <% end %>
311
+ </div>
312
+
313
+ <%# ─── Tab: Environment ──────────────────────────────────── %>
314
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="environment" style="display: none">
315
+ <% if @exception.environment.present? %>
316
+ <div class="rn-detail-section">
317
+ <div class="rn-detail-section-header">
318
+ <h3>Environment Variables</h3>
319
+ </div>
320
+ <div class="rn-detail-code" style="max-height: 32rem; overflow: auto"><%= pretty_format(@exception.environment) %></div>
321
+ </div>
322
+ <% else %>
323
+ <div class="text-center py-8">
324
+ <p class="text-sm" style="color: var(--rn-text-muted)">No environment data available</p>
325
+ </div>
326
+ <% end %>
327
+ </div>
328
+
329
+ <%# ─── Tab: User Info ────────────────────────────────────── %>
330
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="user_info" style="display: none">
331
+ <% if @exception.user_info.present? %>
332
+ <div class="rn-detail-section">
333
+ <div class="rn-detail-section-header">
334
+ <h3>User Information</h3>
335
+ </div>
336
+ <div class="rn-detail-code"><%= pretty_format(@exception.user_info) %></div>
337
+ </div>
338
+ <% else %>
339
+ <div class="text-center py-8">
340
+ <p class="text-sm" style="color: var(--rn-text-muted)">No user info available</p>
341
+ </div>
342
+ <% end %>
343
+ </div>
344
+
345
+ </div>
346
+
347
+ <%# ─── Tab: Cause Chain ────────────────────────────────────── %>
348
+ <% if @exception.respond_to?(:cause_chain) && @exception.cause_chain.present? && @exception.cause_chain.is_a?(Array) && @exception.cause_chain.length > 0 %>
349
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="cause_chain" style="display: none">
350
+ <div class="rn-detail-section">
351
+ <div class="rn-detail-section-header">
352
+ <h3>Exception Cause Chain</h3>
353
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= @exception.cause_chain.length %> exception(s)</span>
354
+ </div>
355
+ <div class="space-y-3">
356
+ <% @exception.cause_chain.each_with_index do |cause, index| %>
357
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary); border-left: 3px solid <%= index == 0 ? 'var(--rn-danger)' : 'var(--rn-text-muted)' %>">
358
+ <div class="flex items-center gap-2 mb-1">
359
+ <span class="rn-badge rn-badge-<%= index == 0 ? 'danger' : 'muted' %>"><%= cause[:class_name] %></span>
360
+ <% if index > 0 %>
361
+ <span class="text-xs" style="color: var(--rn-text-muted)">caused by</span>
362
+ <% end %>
363
+ </div>
364
+ <p class="text-sm" style="color: var(--rn-text)"><%= cause[:message] %></p>
365
+ <% if cause[:backtrace].present? %>
366
+ <pre class="mt-2 text-xs p-2 rounded" style="background: var(--rn-bg-primary); color: var(--rn-text-secondary); overflow-x: auto"><%= cause[:backtrace] %></pre>
367
+ <% end %>
368
+ </div>
369
+ <% end %>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ <% end %>
374
+
375
+ <%# ─── Tab: Breadcrumbs ────────────────────────────────────── %>
376
+ <% if @exception.respond_to?(:breadcrumbs) && @exception.breadcrumbs.present? %>
377
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="breadcrumbs" style="display: none">
378
+ <div class="rn-detail-section">
379
+ <div class="rn-detail-section-header">
380
+ <h3>Activity Trail</h3>
381
+ <span class="text-xs" style="color: var(--rn-text-muted)">Events before the crash</span>
382
+ </div>
383
+ <% begin; breadcrumbs = @exception.breadcrumbs.is_a?(String) ? JSON.parse(@exception.breadcrumbs) : @exception.breadcrumbs; rescue => _e; breadcrumbs = []; end %>
384
+ <% if breadcrumbs.is_a?(Array) && breadcrumbs.any? %>
385
+ <div class="space-y-2">
386
+ <% breadcrumbs.each do |crumb| %>
387
+ <div class="flex items-start gap-3 p-2 rounded" style="background: var(--rn-bg-secondary)">
388
+ <span class="rn-badge rn-badge-<%= crumb[:type] == 'sql' ? 'info' : crumb[:type] == 'controller' ? 'success' : crumb[:type] == 'cache' ? 'warning' : 'muted' %>" style="min-width: 60px; text-align: center">
389
+ <%= crumb[:type]&.upcase || 'EVENT' %>
390
+ </span>
391
+ <div class="flex-1 min-w-0">
392
+ <p class="text-sm" style="color: var(--rn-text)"><%= crumb[:name] || crumb[:message] %></p>
393
+ <% if crumb[:duration] %>
394
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= crumb[:duration] %>ms</span>
395
+ <% end %>
396
+ <% if crumb[:payload].present? %>
397
+ <pre class="mt-1 text-xs p-1 rounded" style="background: var(--rn-bg-primary); color: var(--rn-text-secondary); overflow-x: auto"><%= crumb[:payload].to_s.truncate(200) %></pre>
398
+ <% end %>
399
+ </div>
400
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= crumb[:timestamp] %></span>
401
+ </div>
402
+ <% end %>
403
+ </div>
404
+ <% else %>
405
+ <p class="text-sm text-center py-4" style="color: var(--rn-text-muted)">No breadcrumbs recorded</p>
406
+ <% end %>
407
+ </div>
408
+ </div>
409
+ <% end %>
410
+
411
+ <%# ─── Tab: System Health ──────────────────────────────────── %>
412
+ <% if @exception.respond_to?(:system_health) && @exception.system_health.present? %>
413
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="system_health" style="display: none">
414
+ <div class="rn-detail-section">
415
+ <div class="rn-detail-section-header">
416
+ <h3>System Health at Crash Time</h3>
417
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= @exception.system_health.is_a?(String) ? 'JSON data' : 'Snapshot' %></span>
418
+ </div> <% begin; health = @exception.system_health.is_a?(String) ? JSON.parse(@exception.system_health) : @exception.system_health; rescue => _e; health = {}; end %>
419
+ <% if health.is_a?(Hash) && health.any? %>
420
+ <div class="grid grid-cols-2 gap-4">
421
+ <% if health[:gc] %>
422
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
423
+ <h4 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">Garbage Collector</h4>
424
+ <div class="space-y-1">
425
+ <div class="flex justify-between text-xs">
426
+ <span style="color: var(--rn-text-muted)">Allocated</span>
427
+ <span style="color: var(--rn-text)"><%= health[:gc][:total_allocated] %></span>
428
+ </div>
429
+ <div class="flex justify-between text-xs">
430
+ <span style="color: var(--rn-text-muted)">Freed</span>
431
+ <span style="color: var(--rn-text)"><%= health[:gc][:total_freed] %></span>
432
+ </div>
433
+ <div class="flex justify-between text-xs">
434
+ <span style="color: var(--rn-text-muted)">Heap Pages</span>
435
+ <span style="color: var(--rn-text)"><%= health[:gc][:heap_allocated] %></span>
436
+ </div>
437
+ </div>
438
+ </div>
439
+ <% end %>
440
+ <% if health[:memory] %>
441
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
442
+ <h4 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">Memory</h4>
443
+ <div class="space-y-1">
444
+ <div class="flex justify-between text-xs">
445
+ <span style="color: var(--rn-text-muted)">RSS</span>
446
+ <span style="color: var(--rn-text)"><%= health[:memory][:rss_kb] %> KB</span>
447
+ </div>
448
+ <div class="flex justify-between text-xs">
449
+ <span style="color: var(--rn-text-muted)">VSZ</span>
450
+ <span style="color: var(--rn-text)"><%= health[:memory][:vsz_kb] %> KB</span>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ <% end %>
455
+ <% if health[:db_pool] %>
456
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
457
+ <h4 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">DB Connection Pool</h4>
458
+ <div class="space-y-1">
459
+ <div class="flex justify-between text-xs">
460
+ <span style="color: var(--rn-text-muted)">Pool Size</span>
461
+ <span style="color: var(--rn-text)"><%= health[:db_pool][:size] %></span>
462
+ </div>
463
+ <div class="flex justify-between text-xs">
464
+ <span style="color: var(--rn-text-muted)">Active</span>
465
+ <span style="color: var(--rn-text)"><%= health[:db_pool][:active] %>/<%= health[:db_pool][:connections] %></span>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ <% end %>
470
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
471
+ <h4 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">Process</h4>
472
+ <div class="space-y-1">
473
+ <div class="flex justify-between text-xs">
474
+ <span style="color: var(--rn-text-muted)">Threads</span>
475
+ <span style="color: var(--rn-text)"><%= health[:threads] %></span>
476
+ </div>
477
+ <div class="flex justify-between text-xs">
478
+ <span style="color: var(--rn-text-muted)">PID</span>
479
+ <span style="color: var(--rn-text)"><%= health[:process_id] %></span>
480
+ </div>
481
+ <div class="flex justify-between text-xs">
482
+ <span style="color: var(--rn-text-muted)">Ruby</span>
483
+ <span style="color: var(--rn-text)"><%= health[:ruby_version] %></span>
484
+ </div>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ <% if health[:timestamp] %>
489
+ <p class="mt-3 text-xs text-center" style="color: var(--rn-text-muted)">Captured at: <%= health[:timestamp] %></p>
490
+ <% end %>
491
+ <% else %>
492
+ <p class="text-sm text-center py-4" style="color: var(--rn-text-muted)">No health data recorded</p>
493
+ <% end %>
494
+ </div>
495
+ </div>
496
+ <% end %>
497
+
498
+ <%# ─── Tab: Workflow ────────────────────────────────────── %>
499
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="workflow" style="display: none">
500
+ <%= render "rails_nexus/logged_exceptions/workflow_panel", exception: @exception %>
501
+ <%= render "rails_nexus/logged_exceptions/comments_list", exception: @exception %>
502
+ </div>
503
+
504
+ <%# ─── Actions Footer ──────────────────────────────────────── %>
505
+ <div class="flex items-center justify-between px-5 py-3" style="border-top: 1px solid var(--rn-border-light); background: var(--rn-bg-secondary)">
506
+ <p class="text-xs rn-mono" style="color: var(--rn-text-muted)">
507
+ ID: <%= @exception.id %>
508
+ <span style="color: var(--rn-border)">·</span>
509
+ <time datetime="<%= @exception.created_at.iso8601 %>"><%= @exception.created_at.iso8601 %></time>
510
+ </p>
511
+ <%= link_to logged_exception_path(@exception),
512
+ class: "rn-btn rn-btn-ghost rn-btn-sm",
513
+ style: "color: var(--rn-danger); border-color: var(--rn-danger)",
514
+ data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } do %>
515
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></svg>
516
+ Delete
517
+ <% end %>
518
+ </div>
519
+
520
+ <%# ─── Prev/Next Navigation ────────────────────────────────── %>
521
+ <% if RailsNexus.configuration.enable_navigation && (prev_exception || next_exception) %>
522
+ <div class="rn-nav-row">
523
+ <% if prev_exception %>
524
+ <%= link_to logged_exception_path(prev_exception.id, params_filters),
525
+ class: "rn-nav-link",
526
+ data: { turbo_frame: "exception-details", turbo_action: "advance" } do %>
527
+ <svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /></svg>
528
+ <div class="min-w-0">
529
+ <div class="rn-nav-link-label">Previous</div>
530
+ <div class="rn-nav-link-title"><%= prev_exception.id %></div>
531
+ </div>
532
+ <% end %>
533
+ <% else %>
534
+ <div></div>
535
+ <% end %>
536
+
537
+ <% if next_exception %>
538
+ <%= link_to logged_exception_path(next_exception.id, params_filters),
539
+ class: "rn-nav-link",
540
+ style: "text-align: right",
541
+ data: { turbo_frame: "exception-details", turbo_action: "advance" } do %>
542
+ <div class="min-w-0">
543
+ <div class="rn-nav-link-label">Next</div>
544
+ <div class="rn-nav-link-title"><%= next_exception.id %></div>
545
+ </div>
546
+ <svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /></svg>
547
+ <% end %>
548
+ <% else %>
549
+ <div></div>
550
+ <% end %>
551
+ </div>
552
+ <% end %>
553
+
554
+ </div>