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,297 @@
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
+ </div>
78
+ <% end %>
79
+
80
+ <%# ─── Detail Body ─────────────────────────────────────────── %>
81
+ <div class="rn-detail-body">
82
+
83
+ <%# ─── Tab: Overview ────────────────────────────────────── %>
84
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="overview">
85
+
86
+ <%# Message %>
87
+ <div class="rn-detail-section">
88
+ <div class="rn-detail-section-header">
89
+ <h3>Message</h3>
90
+ </div>
91
+ <div class="rn-detail-code"><%= @exception.message %></div>
92
+ </div>
93
+
94
+ <%# Metadata Grid %>
95
+ <% if RailsNexus.configuration.show_metadata %>
96
+ <div class="rn-detail-section">
97
+ <h3 style="margin-bottom: 0.5rem">Details</h3>
98
+ <div class="rn-meta">
99
+ <div class="rn-meta-item">
100
+ <div class="rn-meta-label">Exception</div>
101
+ <div class="rn-meta-value"><%= @exception.exception_class %></div>
102
+ </div>
103
+ <div class="rn-meta-item">
104
+ <div class="rn-meta-label">Controller</div>
105
+ <div class="rn-meta-value mono"><%= @exception.controller_name %></div>
106
+ </div>
107
+ <div class="rn-meta-item">
108
+ <div class="rn-meta-label">Action</div>
109
+ <div class="rn-meta-value mono"><%= @exception.action_name %></div>
110
+ </div>
111
+ <div class="rn-meta-item">
112
+ <div class="rn-meta-label">Time</div>
113
+ <div class="rn-meta-value"><time datetime="<%= @exception.created_at.iso8601 %>"><%= @exception.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></time></div>
114
+ </div>
115
+ <% if @exception.remote_ip.present? %>
116
+ <div class="rn-meta-item">
117
+ <div class="rn-meta-label">IP Address</div>
118
+ <div class="rn-meta-value mono"><%= @exception.remote_ip %></div>
119
+ </div>
120
+ <% end %>
121
+ <% if @exception.user_agent.present? %>
122
+ <div class="rn-meta-item" style="grid-column: span 2">
123
+ <div class="rn-meta-label">User Agent</div>
124
+ <div class="rn-meta-value" style="font-size: 0.8125rem; word-break: break-all"><%= @exception.user_agent %></div>
125
+ </div>
126
+ <% end %>
127
+ <div class="rn-meta-item">
128
+ <div class="rn-meta-label">Exception ID</div>
129
+ <div class="rn-meta-value mono"><%= @exception.id %></div>
130
+ </div>
131
+ <% if @exception.id_value.present? %>
132
+ <div class="rn-meta-item">
133
+ <div class="rn-meta-label">ID Value</div>
134
+ <div class="rn-meta-value mono"><%= @exception.id_value %></div>
135
+ </div>
136
+ <% end %>
137
+ </div>
138
+ </div>
139
+ <% end %>
140
+
141
+ <%# Request Params %>
142
+ <% if RailsNexus.configuration.show_request && @exception.request.present? %>
143
+ <div class="rn-detail-section">
144
+ <div class="rn-detail-section-header">
145
+ <h3>Request</h3>
146
+ </div>
147
+ <div class="rn-detail-code"><%= pretty_format(@exception.request) %></div>
148
+ </div>
149
+ <% end %>
150
+
151
+ <%# User Info %>
152
+ <% if @exception.user_info.present? %>
153
+ <div class="rn-detail-section">
154
+ <div class="rn-detail-section-header">
155
+ <h3>User Info</h3>
156
+ </div>
157
+ <div class="rn-detail-code"><%= pretty_format(@exception.user_info) %></div>
158
+ </div>
159
+ <% end %>
160
+ </div>
161
+
162
+ <%# ─── Tab: Backtrace ────────────────────────────────────── %>
163
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="backtrace" style="display: none">
164
+ <% if @exception.backtrace.present? %>
165
+ <% bt = format_backtrace(@exception.backtrace) %>
166
+ <div class="rn-detail-section">
167
+ <div class="rn-detail-section-header">
168
+ <h3>Stack Trace</h3>
169
+ <div class="flex items-center gap-2">
170
+ <button type="button" class="rn-copy-btn" data-action="click->rails_nexus-detail#copyBacktrace"
171
+ data-rails_nexus-detail-backtrace-value="<%= @exception.backtrace %>">
172
+ <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>
173
+ Copy
174
+ </button>
175
+ </div>
176
+ </div>
177
+ <div class="rn-detail-pre" data-rails_nexus-detail-target="backtraceContent">
178
+ <%= bt[:visible] %>
179
+ </div>
180
+ <% if bt[:hidden_count] > 0 %>
181
+ <button type="button" class="rn-btn rn-btn-ghost rn-btn-sm mt-2"
182
+ data-action="click->rails_nexus-detail#expandBacktrace">
183
+ Show <%= bt[:hidden_count] %> more frames
184
+ </button>
185
+ <div class="rn-detail-pre mt-2" style="display: none" data-rails_nexus-detail-target="backtraceHidden">
186
+ <%= bt[:hidden] %>
187
+ </div>
188
+ <% end %>
189
+ </div>
190
+ <% else %>
191
+ <div class="text-center py-8">
192
+ <p class="text-sm" style="color: var(--rn-text-muted)">No backtrace available</p>
193
+ </div>
194
+ <% end %>
195
+ </div>
196
+
197
+ <%# ─── Tab: Request ──────────────────────────────────────── %>
198
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="request" style="display: none">
199
+ <% if @exception.request.present? %>
200
+ <div class="rn-detail-section">
201
+ <div class="rn-detail-section-header">
202
+ <h3>Request Data</h3>
203
+ </div>
204
+ <div class="rn-detail-code" style="max-height: 32rem; overflow: auto"><%= pretty_format(@exception.request) %></div>
205
+ </div>
206
+ <% else %>
207
+ <div class="text-center py-8">
208
+ <p class="text-sm" style="color: var(--rn-text-muted)">No request data available</p>
209
+ </div>
210
+ <% end %>
211
+ </div>
212
+
213
+ <%# ─── Tab: Environment ──────────────────────────────────── %>
214
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="environment" style="display: none">
215
+ <% if @exception.environment.present? %>
216
+ <div class="rn-detail-section">
217
+ <div class="rn-detail-section-header">
218
+ <h3>Environment Variables</h3>
219
+ </div>
220
+ <div class="rn-detail-code" style="max-height: 32rem; overflow: auto"><%= pretty_format(@exception.environment) %></div>
221
+ </div>
222
+ <% else %>
223
+ <div class="text-center py-8">
224
+ <p class="text-sm" style="color: var(--rn-text-muted)">No environment data available</p>
225
+ </div>
226
+ <% end %>
227
+ </div>
228
+
229
+ <%# ─── Tab: User Info ────────────────────────────────────── %>
230
+ <div data-rails_nexus-detail-target="tabContent" data-rn-tab-content="user_info" style="display: none">
231
+ <% if @exception.user_info.present? %>
232
+ <div class="rn-detail-section">
233
+ <div class="rn-detail-section-header">
234
+ <h3>User Information</h3>
235
+ </div>
236
+ <div class="rn-detail-code"><%= pretty_format(@exception.user_info) %></div>
237
+ </div>
238
+ <% else %>
239
+ <div class="text-center py-8">
240
+ <p class="text-sm" style="color: var(--rn-text-muted)">No user info available</p>
241
+ </div>
242
+ <% end %>
243
+ </div>
244
+
245
+ </div>
246
+
247
+ <%# ─── Actions Footer ──────────────────────────────────────── %>
248
+ <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)">
249
+ <p class="text-xs rn-mono" style="color: var(--rn-text-muted)">
250
+ ID: <%= @exception.id %>
251
+ <span style="color: var(--rn-border)">·</span>
252
+ <time datetime="<%= @exception.created_at.iso8601 %>"><%= @exception.created_at.iso8601 %></time>
253
+ </p>
254
+ <%= link_to logged_exception_path(@exception),
255
+ class: "rn-btn rn-btn-ghost rn-btn-sm",
256
+ style: "color: var(--rn-danger); border-color: var(--rn-danger)",
257
+ data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } do %>
258
+ <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>
259
+ Delete
260
+ <% end %>
261
+ </div>
262
+
263
+ <%# ─── Prev/Next Navigation ────────────────────────────────── %>
264
+ <% if RailsNexus.configuration.enable_navigation && (prev_exception || next_exception) %>
265
+ <div class="rn-nav-row">
266
+ <% if prev_exception %>
267
+ <%= link_to logged_exception_path(prev_exception.id, params_filters),
268
+ class: "rn-nav-link",
269
+ data: { turbo_frame: "exception-details", turbo_action: "advance" } do %>
270
+ <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>
271
+ <div class="min-w-0">
272
+ <div class="rn-nav-link-label">Previous</div>
273
+ <div class="rn-nav-link-title"><%= prev_exception.id %></div>
274
+ </div>
275
+ <% end %>
276
+ <% else %>
277
+ <div></div>
278
+ <% end %>
279
+
280
+ <% if next_exception %>
281
+ <%= link_to logged_exception_path(next_exception.id, params_filters),
282
+ class: "rn-nav-link",
283
+ style: "text-align: right",
284
+ data: { turbo_frame: "exception-details", turbo_action: "advance" } do %>
285
+ <div class="min-w-0">
286
+ <div class="rn-nav-link-label">Next</div>
287
+ <div class="rn-nav-link-title"><%= next_exception.id %></div>
288
+ </div>
289
+ <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>
290
+ <% end %>
291
+ <% else %>
292
+ <div></div>
293
+ <% end %>
294
+ </div>
295
+ <% end %>
296
+
297
+ </div>
@@ -0,0 +1,113 @@
1
+ <% page_title t(".title") %>
2
+
3
+ <div class="space-y-4" data-controller="rails_nexus">
4
+ <%# Header with stats %>
5
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
6
+ <div>
7
+ <h1 class="text-xl font-bold text-gray-900 tracking-tight"><%= t(".title") %></h1>
8
+ <p class="mt-0.5 text-xs text-gray-400"><%= t(".subtitle") %></p>
9
+ </div>
10
+ <div class="flex items-center gap-2">
11
+ <span class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-full bg-red-50 text-red-700 border border-red-100">
12
+ <span class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span>
13
+ <%= RailsNexus::LoggedException.count %> total
14
+ </span>
15
+ <%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
16
+ class: "inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-red-600 transition-colors",
17
+ form: { data: { turbo_stream: true } },
18
+ data: { turbo_confirm: t(".confirm_clear") } %>
19
+ </div>
20
+ </div>
21
+
22
+ <%# Filters Bar %>
23
+ <div class="bg-white rounded-xl border border-gray-200/60 shadow-sm">
24
+ <div class="px-4 py-3 border-b border-gray-100 flex items-center gap-2">
25
+ <svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" /></svg>
26
+ <span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Filters</span>
27
+ <% if filtered? %>
28
+ <%= link_to "Clear", logged_exceptions_path, class: "ml-auto text-xs text-indigo-600 hover:text-indigo-800 font-medium" %>
29
+ <% end %>
30
+ </div>
31
+
32
+ <%= search_form_for @q, url: query_logged_exceptions_path, data: { turbo_frame: "exceptions" } do |f| %>
33
+ <div class="px-4 py-3 flex flex-wrap items-end gap-3">
34
+ <%# Exception Class Select %>
35
+ <div class="flex-1 min-w-[160px]">
36
+ <label class="block text-[11px] font-medium text-gray-400 uppercase tracking-wider mb-1">Exception</label>
37
+ <%= f.select :exception_class_cont,
38
+ options_for_select(@exception_names.map { |n| [n, n] }, params.dig(:q, :exception_class_cont)),
39
+ { include_blank: "All exceptions" },
40
+ class: "block w-full rounded-lg border-gray-200 bg-gray-50/50 text-sm py-2 pl-3 pr-8 focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500/20 appearance-none cursor-pointer",
41
+ data: { action: "change->rails_nexus#submit" } %>
42
+ </div>
43
+
44
+ <%# Controller Select %>
45
+ <div class="flex-1 min-w-[160px]">
46
+ <label class="block text-[11px] font-medium text-gray-400 uppercase tracking-wider mb-1">Controller</label>
47
+ <%= f.select :controller_name_cont,
48
+ options_for_select(@controller_actions.map { |a| [a, a] }, params.dig(:q, :controller_name_cont)),
49
+ { include_blank: "All controllers" },
50
+ class: "block w-full rounded-lg border-gray-200 bg-gray-50/50 text-sm py-2 pl-3 pr-8 focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500/20 appearance-none cursor-pointer",
51
+ data: { action: "change->rails_nexus#submit" } %>
52
+ </div>
53
+
54
+ <%# Time Range Select %>
55
+ <div class="flex-1 min-w-[160px]">
56
+ <label class="block text-[11px] font-medium text-gray-400 uppercase tracking-wider mb-1">Time Range</label>
57
+ <%= select_tag :date_ranges_filter,
58
+ options_for_select([
59
+ ["All time", ""],
60
+ ["Today", "1"],
61
+ ["Last 3 days", "3"],
62
+ ["Last 7 days", "7"],
63
+ ["Last 30 days", "30"]
64
+ ], params[:date_ranges_filter]),
65
+ class: "block w-full rounded-lg border-gray-200 bg-gray-50/50 text-sm py-2 pl-3 pr-8 focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500/20 appearance-none cursor-pointer",
66
+ data: { action: "change->rails_nexus#submit" } %>
67
+ </div>
68
+
69
+ <%# Message Search %>
70
+ <div class="flex-1 min-w-[200px]">
71
+ <label class="block text-[11px] font-medium text-gray-400 uppercase tracking-wider mb-1">Search</label>
72
+ <%= f.search_field :message_cont,
73
+ placeholder: "Search messages...",
74
+ class: "block w-full rounded-lg border-gray-200 bg-gray-50/50 text-sm py-2 px-3 focus:border-indigo-500 focus:bg-white focus:ring-1 focus:ring-indigo-500/20",
75
+ data: { action: "input->rails_nexus#debouncedSubmit" } %>
76
+ </div>
77
+
78
+ <div class="flex gap-2">
79
+ <%= f.submit "Apply", class: "px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors" %>
80
+ </div>
81
+ </div>
82
+ <% end %>
83
+ </div>
84
+
85
+ <%# Content %>
86
+ <div class="space-y-4">
87
+ <%# Activity indicator %>
88
+ <div id="activity" data-rails_nexus-target="activity" class="rounded-lg bg-indigo-50 border border-indigo-200 p-3 flex items-center gap-3" role="status" aria-live="polite" hidden>
89
+ <svg class="animate-spin h-4 w-4 text-indigo-500 shrink-0" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
90
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
91
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
92
+ </svg>
93
+ <p class="text-sm font-medium text-indigo-700"><%= t(".loading") %></p>
94
+ </div>
95
+
96
+ <%# Exception Details Panel %>
97
+ <%= turbo_frame_tag "exception-details", class: "bg-white rounded-xl border border-gray-200/60 shadow-sm" do %>
98
+ <div class="text-center py-12">
99
+ <div class="w-10 h-10 rounded-full bg-gray-50 flex items-center justify-center mx-auto border border-gray-100">
100
+ <svg class="w-5 h-5 text-gray-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
101
+ <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" />
102
+ </svg>
103
+ </div>
104
+ <p class="mt-2 text-xs text-gray-400">Select an exception to view details</p>
105
+ </div>
106
+ <% end %>
107
+
108
+ <%# Exceptions List %>
109
+ <%= turbo_frame_tag "exceptions" do %>
110
+ <%= render "exceptions" %>
111
+ <% end %>
112
+ </div>
113
+ </div>
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/notifications"
4
+
5
+ module RailsNexus
6
+ # Breadcrumbs capture activity trail before a crash.
7
+ # Uses ActiveSupport::Notifications to subscribe to SQL, controller,
8
+ # cache, and other instrumentation events.
9
+ module Breadcrumbs
10
+ RING_BUFFER_SIZE = 50
11
+
12
+ class << self
13
+ # Thread-local ring buffer of breadcrumbs
14
+ def buffer
15
+ Thread.current[:rails_nexus_breadcrumbs] ||= []
16
+ end
17
+
18
+ def buffer=(val)
19
+ Thread.current[:rails_nexus_breadcrumbs] = val
20
+ end
21
+
22
+ # Capture a breadcrumb manually
23
+ def add(type:, name:, payload: nil, duration: nil)
24
+ buffer << {
25
+ type: type,
26
+ name: name,
27
+ payload: payload,
28
+ duration: duration,
29
+ timestamp: Time.current.iso8601(3)
30
+ }
31
+ # Keep only last N entries
32
+ buffer.shift if buffer.size > RING_BUFFER_SIZE
33
+ end
34
+
35
+ # Flush buffer and return captured breadcrumbs
36
+ def flush
37
+ crumbs = buffer.dup
38
+ self.buffer = []
39
+ crumbs
40
+ end
41
+
42
+ # Start subscribing to instrumentation events
43
+ def subscribe!
44
+ return if @subscribed
45
+
46
+ # SQL queries
47
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
48
+ next if payload[:name] == "SCHEMA"
49
+ next if payload[:sql]&.start_with?("SET")
50
+ next if payload[:sql]&.start_with?("SELECT 1")
51
+
52
+ add(
53
+ type: "sql",
54
+ name: payload[:sql]&.truncate(200),
55
+ payload: {
56
+ connection_id: payload[:connection_id],
57
+ cached: payload[:cached]
58
+ },
59
+ duration: payload[:runtime]
60
+ )
61
+ end
62
+
63
+ # Controller action
64
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_name, _start, _finish, _id, payload|
65
+ add(
66
+ type: "controller",
67
+ name: "#{payload[:controller]}##{payload[:action]}",
68
+ payload: {
69
+ format: payload[:format],
70
+ status: payload[:status],
71
+ view_runtime: payload[:view_runtime]&.round(2),
72
+ db_runtime: payload[:db_runtime]&.round(2)
73
+ },
74
+ duration: payload[:runtime]&.round(2)
75
+ )
76
+ end
77
+
78
+ # Cache reads/writes
79
+ ActiveSupport::Notifications.subscribe("cache_read.active_support") do |_name, _start, _finish, _id, payload|
80
+ next unless payload[:hit] # Only log cache hits
81
+
82
+ add(
83
+ type: "cache",
84
+ name: "Cache read: #{payload[:key]}",
85
+ payload: { hit: true, super: payload[:super] }
86
+ )
87
+ end
88
+
89
+ ActiveSupport::Notifications.subscribe("cache_write.active_support") do |_name, _start, _finish, _id, payload|
90
+ add(
91
+ type: "cache",
92
+ name: "Cache write: #{payload[:key]}",
93
+ payload: { hit: false }
94
+ )
95
+ end
96
+
97
+ # Active Job
98
+ ActiveSupport::Notifications.subscribe("perform_start.active_job") do |_name, _start, _finish, _id, payload|
99
+ add(
100
+ type: "job",
101
+ name: "Job started: #{payload[:job_class]}",
102
+ payload: {
103
+ job_id: payload[:job_id],
104
+ queue: payload[:queue_name],
105
+ arguments: payload[:arguments]&.first(200)
106
+ }
107
+ )
108
+ end
109
+
110
+ ActiveSupport::Notifications.subscribe("perform.active_job") do |_name, _start, _finish, _id, payload|
111
+ add(
112
+ type: "job",
113
+ name: "Job completed: #{payload[:job_class]}",
114
+ payload: {
115
+ job_id: payload[:job_id],
116
+ queue: payload[:queue_name],
117
+ error: payload[:exception_object]&.message
118
+ },
119
+ duration: payload[:runtime]&.round(2)
120
+ )
121
+ end
122
+
123
+ # Action Mailer
124
+ ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |_name, _start, _finish, _id, payload|
125
+ add(
126
+ type: "mailer",
127
+ name: "Mail delivered: #{payload[:mailer]}##{payload[:message_id]}",
128
+ payload: {
129
+ mailer: payload[:mailer],
130
+ message_id: payload[:message_id]
131
+ }
132
+ )
133
+ end
134
+
135
+ # Render partials
136
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |_name, _start, _finish, _id, payload|
137
+ add(
138
+ type: "render",
139
+ name: "Render: #{payload[:identifier]}",
140
+ payload: {
141
+ cache_hits: payload[:cache_hit]
142
+ },
143
+ duration: payload[:duration]&.round(2)
144
+ )
145
+ end
146
+
147
+ @subscribed = true
148
+ end
149
+
150
+ def subscribed?
151
+ @subscribed
152
+ end
153
+
154
+ # Reset state (for testing)
155
+ def reset!
156
+ self.buffer = []
157
+ @subscribed = false
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ module Cleanup
5
+ class << self
6
+ # Delete exceptions older than the configured retention period.
7
+ # Call this from a cron job, Sidekiq scheduler, or Rails runner.
8
+ #
9
+ # Usage:
10
+ # bin/rails runner "RailsNexus::Cleanup.run"
11
+ #
12
+ # Or schedule with sidekiq-cron / whenever / good_job:
13
+ # RailsNexus::Cleanup.run
14
+ def run
15
+ config = RailsNexus.configuration
16
+ return unless config.retention_days.present?
17
+
18
+ cutoff = config.retention_days.days.ago
19
+ count = RailsNexus::LoggedException.where("created_at < ?", cutoff).delete_all
20
+
21
+ Rails.logger.info("[RailsNexus] Cleaned up #{count} exceptions older than #{config.retention_days} days") if count > 0
22
+ count
23
+ end
24
+
25
+ # Generate a rake task for cleanup.
26
+ # Adds `rake rails_nexus:cleanup` to your app.
27
+ def self.install_rake_task
28
+ # This is called automatically by the engine
29
+ end
30
+ end
31
+ end
32
+ end