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,603 @@
1
+ // RailsNexus Stimulus Controllers Bundle
2
+ // Auto-registers all controllers with window.Stimulus
3
+ // No ES module imports — works with Propshaft, Sprockets, or importmaps
4
+
5
+ (function() {
6
+ "use strict"
7
+
8
+ // ═══════════════════════════════════════════════════════════════
9
+ // Theme Controller
10
+ // ═══════════════════════════════════════════════════════════════
11
+
12
+ class RailsNexusThemeController {
13
+ static get targets() { return [] }
14
+ static get values() { return { theme: String } }
15
+
16
+ connect() {
17
+ var saved = localStorage.getItem("rails_nexus-theme")
18
+ if (saved) {
19
+ this.applyTheme(saved)
20
+ } else {
21
+ var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
22
+ this.applyTheme(prefersDark ? "dark" : "light")
23
+ }
24
+ }
25
+
26
+ toggle() {
27
+ var root = document.documentElement
28
+ var isDark = root.classList.contains("dark")
29
+ this.applyTheme(isDark ? "light" : "dark")
30
+ }
31
+
32
+ applyTheme(theme) {
33
+ var root = document.documentElement
34
+ root.classList.remove("dark", "light")
35
+ root.classList.add(theme)
36
+ localStorage.setItem("rails_nexus-theme", theme)
37
+ }
38
+ }
39
+
40
+
41
+ // ═══════════════════════════════════════════════════════════════
42
+ // Sidebar Controller (mobile)
43
+ // ═══════════════════════════════════════════════════════════════
44
+
45
+ class RailsNexusSidebarController {
46
+ static get targets() { return ["drawer", "overlay"] }
47
+
48
+ toggle() {
49
+ if (this.hasDrawerTarget) this.drawerTarget.classList.toggle("open")
50
+ if (this.hasOverlayTarget) this.overlayTarget.classList.toggle("open")
51
+ }
52
+
53
+ close() {
54
+ if (this.hasDrawerTarget) this.drawerTarget.classList.remove("open")
55
+ if (this.hasOverlayTarget) this.overlayTarget.classList.remove("open")
56
+ }
57
+ }
58
+
59
+
60
+ // ═══════════════════════════════════════════════════════════════
61
+ // Main RailsNexus Controller (keyboard nav, tabs, search)
62
+ // ═══════════════════════════════════════════════════════════════
63
+
64
+ class RailsNexusMainController {
65
+ static get targets() {
66
+ return ["activity", "shortcutsDialog", "tabs", "tabContent", "exceptionList", "backtraceHidden", "backtraceContent"]
67
+ }
68
+ static get values() {
69
+ return { shortcuts: Boolean }
70
+ }
71
+
72
+ connect() {
73
+ this.selectedIndex = -1
74
+ this.rows = []
75
+
76
+ this._showActivity = this.showActivity.bind(this)
77
+ this._hideActivity = this.hideActivity.bind(this)
78
+
79
+ document.addEventListener("turbo:before-fetch-request", this._showActivity)
80
+ document.addEventListener("turbo:before-fetch-response", this._hideActivity)
81
+ document.addEventListener("turbo:submit-end", this._hideActivity)
82
+
83
+ if (this.shortcutsValue) {
84
+ this._boundKeydown = this.handleKeydown.bind(this)
85
+ document.addEventListener("keydown", this._boundKeydown)
86
+ }
87
+ }
88
+
89
+ disconnect() {
90
+ document.removeEventListener("turbo:before-fetch-request", this._showActivity)
91
+ document.removeEventListener("turbo:before-fetch-response", this._hideActivity)
92
+ document.removeEventListener("turbo:submit-end", this._hideActivity)
93
+ if (this._boundKeydown) {
94
+ document.removeEventListener("keydown", this._boundKeydown)
95
+ }
96
+ }
97
+
98
+ // ─── Keyboard Navigation ──────────────────────────────────
99
+
100
+ handleKeydown(event) {
101
+ var tag = event.target.tagName.toLowerCase()
102
+ if (tag === "input" || tag === "textarea" || tag === "select") {
103
+ if (event.key === "Escape") { event.target.blur(); event.preventDefault() }
104
+ return
105
+ }
106
+
107
+ switch (event.key) {
108
+ case "/":
109
+ event.preventDefault(); this.focusSearch(); break
110
+ case "j": case "ArrowDown":
111
+ event.preventDefault(); this.navigateDown(); break
112
+ case "k": case "ArrowUp":
113
+ event.preventDefault(); this.navigateUp(); break
114
+ case "Enter":
115
+ event.preventDefault(); this.openSelected(); break
116
+ case "p": case "ArrowLeft":
117
+ event.preventDefault(); this.navigateToPrev(); break
118
+ case "n": case "ArrowRight":
119
+ event.preventDefault(); this.navigateToNext(); break
120
+ case "t":
121
+ event.preventDefault(); this.toggleTheme(); break
122
+ case "Escape":
123
+ event.preventDefault(); this.closeDetails(); this.closeShortcuts(); break
124
+ case "?":
125
+ event.preventDefault(); this.toggleShortcuts(); break
126
+ case "1": case "2": case "3": case "4": case "5":
127
+ event.preventDefault(); this.switchTabByIndex(parseInt(event.key) - 1); break
128
+ }
129
+ }
130
+
131
+ focusSearch() {
132
+ var search = this.element.querySelector(".rn-search .rn-input, input[type='search']")
133
+ if (search) { search.focus(); search.select() }
134
+ }
135
+
136
+ refreshRows() {
137
+ if (this.hasExceptionListTarget) {
138
+ this.rows = Array.from(this.exceptionListTarget.querySelectorAll("tr[data-url]"))
139
+ } else {
140
+ this.rows = Array.from(this.element.querySelectorAll("tr[data-url]"))
141
+ }
142
+ }
143
+
144
+ navigateDown() {
145
+ this.refreshRows()
146
+ if (this.rows.length === 0) return
147
+ this.selectedIndex = Math.min(this.selectedIndex + 1, this.rows.length - 1)
148
+ this.highlightRow()
149
+ }
150
+
151
+ navigateUp() {
152
+ this.refreshRows()
153
+ if (this.rows.length === 0) return
154
+ this.selectedIndex = Math.max(this.selectedIndex - 1, 0)
155
+ this.highlightRow()
156
+ }
157
+
158
+ highlightRow() {
159
+ var self = this
160
+ this.rows.forEach(function(row, i) {
161
+ row.classList.toggle("selected", i === self.selectedIndex)
162
+ })
163
+ var row = this.rows[this.selectedIndex]
164
+ if (row) row.scrollIntoView({ block: "nearest", behavior: "smooth" })
165
+ }
166
+
167
+ openSelected() {
168
+ this.refreshRows()
169
+ if (this.selectedIndex < 0 || this.selectedIndex >= this.rows.length) return
170
+ var row = this.rows[this.selectedIndex]
171
+ if (row.dataset.url) {
172
+ this.selectRow({ currentTarget: row, preventDefault: function() {} })
173
+ }
174
+ }
175
+
176
+ // ─── Row Selection ────────────────────────────────────────
177
+
178
+ selectRow(event) {
179
+ event.preventDefault()
180
+ var row = event.currentTarget
181
+ var url = row.dataset.url
182
+ if (!url) return
183
+
184
+ this.refreshRows()
185
+ this.rows.forEach(function(r) { r.classList.remove("selected") })
186
+ row.classList.add("selected")
187
+
188
+ var detailPanel = document.getElementById("exception-detail-desktop")
189
+ if (detailPanel) {
190
+ detailPanel.hidden = false
191
+ detailPanel.classList.remove("hidden")
192
+ }
193
+
194
+ var frame = document.getElementById("exception-details")
195
+ if (frame) {
196
+ frame.innerHTML = ""
197
+ frame.src = url
198
+ }
199
+ }
200
+
201
+ stopPropagation(event) {
202
+ event.stopPropagation()
203
+ }
204
+
205
+ // ─── Close Details ────────────────────────────────────────
206
+
207
+ closeDetails(event) {
208
+ if (event && event.preventDefault) event.preventDefault()
209
+
210
+ this.refreshRows()
211
+ this.rows.forEach(function(r) { r.classList.remove("selected") })
212
+ this.selectedIndex = -1
213
+
214
+ var detailPanel = document.getElementById("exception-detail-desktop")
215
+ if (detailPanel) {
216
+ detailPanel.hidden = true
217
+ detailPanel.classList.add("hidden")
218
+ }
219
+
220
+ var frame = document.getElementById("exception-details")
221
+ if (frame) {
222
+ frame.innerHTML = '<div class="text-center py-12"><div class="w-10 h-10 rounded-full flex items-center justify-center mx-auto" style="background: var(--rn-bg-secondary)"><svg class="w-5 h-5" style="color: var(--rn-text-muted)" 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></div><p class="mt-2 text-xs" style="color: var(--rn-text-muted)">Select an exception to view details</p></div>'
223
+ }
224
+ }
225
+
226
+ // ─── Tab Switching ────────────────────────────────────────
227
+
228
+ switchTab(event) {
229
+ var tabName = event.currentTarget.dataset.flTabParam || event.currentTarget.dataset.flTab
230
+ this.activateTab(tabName)
231
+ }
232
+
233
+ switchTabByIndex(index) {
234
+ if (!this.hasTabsTarget) return
235
+ var tabs = this.tabsTarget.querySelectorAll(".rn-tab")
236
+ if (index >= 0 && index < tabs.length) {
237
+ var tabName = tabs[index].dataset.flTabParam || tabs[index].dataset.flTab
238
+ this.activateTab(tabName)
239
+ }
240
+ }
241
+
242
+ activateTab(tabName) {
243
+ if (this.hasTabsTarget) {
244
+ this.tabsTarget.querySelectorAll(".rn-tab").forEach(function(tab) {
245
+ var isActive = (tab.dataset.flTabParam || tab.dataset.flTab) === tabName
246
+ tab.classList.toggle("active", isActive)
247
+ })
248
+ }
249
+ this.tabContentTargets.forEach(function(content) {
250
+ var isTarget = content.dataset.flTabContent === tabName
251
+ content.style.display = isTarget ? "" : "none"
252
+ })
253
+ }
254
+
255
+ // ─── Theme Toggle ─────────────────────────────────────────
256
+
257
+ toggleTheme() {
258
+ var themeController = this.application.getControllerForElementAndIdentifier(
259
+ document.documentElement, "rails_nexus-theme"
260
+ )
261
+ if (themeController) {
262
+ themeController.toggle()
263
+ } else {
264
+ var root = document.documentElement
265
+ var isDark = root.classList.contains("dark")
266
+ root.classList.remove("dark", "light")
267
+ root.classList.add(isDark ? "light" : "dark")
268
+ localStorage.setItem("rails_nexus-theme", isDark ? "light" : "dark")
269
+ }
270
+ }
271
+
272
+ // ─── Keyboard Shortcuts Dialog ────────────────────────────
273
+
274
+ toggleShortcuts() {
275
+ if (!this.hasShortcutsDialogTarget) return
276
+ if (this.shortcutsDialogTarget.classList.contains("open")) {
277
+ this.closeShortcuts()
278
+ } else {
279
+ this.shortcutsDialogTarget.classList.add("open")
280
+ }
281
+ }
282
+
283
+ closeShortcuts() {
284
+ if (this.hasShortcutsDialogTarget) this.shortcutsDialogTarget.classList.remove("open")
285
+ }
286
+
287
+ // ─── Navigation (prev/next) ───────────────────────────────
288
+
289
+ navigateToPrev() {
290
+ var links = document.querySelectorAll(".rn-nav-link")
291
+ for (var i = 0; i < links.length; i++) {
292
+ var label = links[i].querySelector(".rn-nav-link-label")
293
+ if (label && label.textContent === "Previous") { links[i].click(); return }
294
+ }
295
+ }
296
+
297
+ navigateToNext() {
298
+ var links = document.querySelectorAll(".rn-nav-link")
299
+ for (var i = 0; i < links.length; i++) {
300
+ var label = links[i].querySelector(".rn-nav-link-label")
301
+ if (label && label.textContent === "Next") { links[i].click(); return }
302
+ }
303
+ }
304
+
305
+ // ─── Copy to Clipboard ───────────────────────────────────
306
+
307
+ copyId(event) {
308
+ var id = event.currentTarget.dataset.rails_nexusIdValue
309
+ if (id) this.copyToClipboard(id, event.currentTarget)
310
+ }
311
+
312
+ copyMessage(event) {
313
+ var message = event.currentTarget.dataset.rails_nexusMessageValue
314
+ if (message) this.copyToClipboard(message, event.currentTarget)
315
+ }
316
+
317
+ copyBacktrace(event) {
318
+ var backtrace = event.currentTarget.dataset.rails_nexusBacktraceValue
319
+ if (backtrace) this.copyToClipboard(backtrace, event.currentTarget)
320
+ }
321
+
322
+ copyToClipboard(text, button) {
323
+ var self = this
324
+ try {
325
+ navigator.clipboard.writeText(text).then(function() { self.showCopied(button) })
326
+ } catch (err) {
327
+ var textarea = document.createElement("textarea")
328
+ textarea.value = text
329
+ textarea.style.position = "fixed"
330
+ textarea.style.opacity = "0"
331
+ document.body.appendChild(textarea)
332
+ textarea.select()
333
+ document.execCommand("copy")
334
+ document.body.removeChild(textarea)
335
+ this.showCopied(button)
336
+ }
337
+ }
338
+
339
+ showCopied(button) {
340
+ var orig = button.innerHTML
341
+ button.classList.add("copied")
342
+ button.innerHTML = '<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="M4.5 12.75l6 6 9-13.5" /></svg> Copied!'
343
+ setTimeout(function() { button.classList.remove("copied"); button.innerHTML = orig }, 2000)
344
+ }
345
+
346
+ // ─── Backtrace Expand ─────────────────────────────────────
347
+
348
+ expandBacktrace(event) {
349
+ event.preventDefault()
350
+ var hidden = this.backtraceHiddenTarget
351
+ if (hidden) {
352
+ hidden.style.display = hidden.style.display === "none" ? "" : "none"
353
+ event.currentTarget.textContent = hidden.style.display === "none"
354
+ ? "Show more frames"
355
+ : "Hide additional frames"
356
+ }
357
+ }
358
+
359
+ // ─── Submit Helpers ───────────────────────────────────────
360
+
361
+ submit() {
362
+ this.showActivity()
363
+ var form = this.element.querySelector("form[data-turbo-frame]")
364
+ if (form) form.requestSubmit()
365
+ }
366
+
367
+ debouncedSubmit() {
368
+ var self = this
369
+ clearTimeout(this._debounceTimer)
370
+ this._debounceTimer = setTimeout(function() { self.submit() }, 400)
371
+ }
372
+
373
+ // ─── Loading State ────────────────────────────────────────
374
+
375
+ showActivity() {
376
+ if (this.hasActivityTarget) this.activityTarget.classList.remove("hidden")
377
+ }
378
+
379
+ hideActivity() {
380
+ if (this.hasActivityTarget) this.activityTarget.classList.add("hidden")
381
+ }
382
+ }
383
+
384
+
385
+ // ═══════════════════════════════════════════════════════════════
386
+ // Detail Controller (exception detail page tabs, source, copy)
387
+ // ═══════════════════════════════════════════════════════════════
388
+
389
+ class RailsNexusDetailController {
390
+ static get targets() {
391
+ return ["tabs", "tabContent", "backtraceContent", "backtraceHidden", "sourcePanel"]
392
+ }
393
+
394
+ switchTab(event) {
395
+ var tabName = event.params ? event.params.tab : null
396
+ if (!tabName) tabName = event.currentTarget.getAttribute("data-rn-tab-param")
397
+ if (!tabName) return
398
+
399
+ this.tabsTarget.querySelectorAll(".rn-tab").forEach(function(tab) {
400
+ tab.classList.toggle("active", tab.getAttribute("data-rn-tab-param") === tabName)
401
+ })
402
+
403
+ this.tabContentTargets.forEach(function(content) {
404
+ content.style.display = content.getAttribute("data-rn-tab-content") === tabName ? "" : "none"
405
+ })
406
+ }
407
+
408
+ copyId(event) {
409
+ var id = event.currentTarget.getAttribute("data-rails_nexus-detail-id-value") ||
410
+ event.currentTarget.dataset.rails_nexusDetailIdValue
411
+ if (id) this.copyToClipboard(id, event.currentTarget)
412
+ }
413
+
414
+ copyMessage(event) {
415
+ var msg = event.currentTarget.getAttribute("data-rails_nexus-detail-message-value") ||
416
+ event.currentTarget.dataset.rails_nexusDetailMessageValue
417
+ if (msg) this.copyToClipboard(msg, event.currentTarget)
418
+ }
419
+
420
+ copyBacktrace(event) {
421
+ var bt = event.currentTarget.getAttribute("data-rails_nexus-detail-backtrace-value") ||
422
+ event.currentTarget.dataset.rails_nexusDetailBacktraceValue
423
+ if (bt) this.copyToClipboard(bt, event.currentTarget)
424
+ }
425
+
426
+ copyToClipboard(text, button) {
427
+ var self = this
428
+ try {
429
+ navigator.clipboard.writeText(text).then(function() { self.showCopied(button) })
430
+ } catch (err) {
431
+ var textarea = document.createElement("textarea")
432
+ textarea.value = text
433
+ textarea.style.position = "fixed"
434
+ textarea.style.opacity = "0"
435
+ document.body.appendChild(textarea)
436
+ textarea.select()
437
+ document.execCommand("copy")
438
+ document.body.removeChild(textarea)
439
+ this.showCopied(button)
440
+ }
441
+ }
442
+
443
+ showCopied(button) {
444
+ var orig = button.innerHTML
445
+ button.classList.add("copied")
446
+ button.innerHTML = '<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="M4.5 12.75l6 6 9-13.5" /></svg> Copied!'
447
+ setTimeout(function() { button.classList.remove("copied"); button.innerHTML = orig }, 2000)
448
+ }
449
+
450
+ expandBacktrace(event) {
451
+ event.preventDefault()
452
+ var hidden = this.backtraceHiddenTarget
453
+ if (hidden) {
454
+ hidden.style.display = hidden.style.display === "none" ? "" : "none"
455
+ event.currentTarget.textContent = hidden.style.display === "none"
456
+ ? "Show more frames"
457
+ : "Hide additional frames"
458
+ }
459
+ }
460
+
461
+ toggleSource(event) {
462
+ event.preventDefault()
463
+ var btn = event.currentTarget
464
+ var file = btn.dataset.file
465
+ var line = btn.dataset.line
466
+ var self = this
467
+
468
+ var panels = this.sourcePanelTargets
469
+ var panel = null
470
+ for (var i = 0; i < panels.length; i++) {
471
+ if (panels[i].dataset.file === file && panels[i].dataset.line === line) {
472
+ panel = panels[i]; break
473
+ }
474
+ }
475
+ if (!panel) return
476
+
477
+ if (panel.style.display === "none") {
478
+ panel.style.display = ""
479
+ var loading = panel.querySelector(".rn-source-loading")
480
+ if (loading) loading.style.display = ""
481
+
482
+ fetch("/rails_nexus/source_code?file=" + encodeURIComponent(file) + "&line=" + line + "&context=5", {
483
+ headers: { "Accept": "application/json" }
484
+ })
485
+ .then(function(r) { return r.json() })
486
+ .then(function(data) {
487
+ if (data.source) {
488
+ panel.innerHTML = self.renderSourceSnippet(data.source, data.blame)
489
+ } else {
490
+ panel.innerHTML = '<div class="text-xs" style="color: var(--rn-text-muted); padding: 0.5rem">Source not available</div>'
491
+ }
492
+ })
493
+ .catch(function() {
494
+ panel.innerHTML = '<div class="text-xs" style="color: var(--rn-text-muted); padding: 0.5rem">Failed to load source</div>'
495
+ })
496
+ } else {
497
+ panel.style.display = "none"
498
+ }
499
+ }
500
+
501
+ renderSourceSnippet(snippet, blame) {
502
+ var html = '<div class="rn-source-code rounded" style="background: var(--rn-bg-primary); border: 1px solid var(--rn-border-light); overflow-x: auto; font-size: 0.75rem; font-family: monospace">'
503
+
504
+ if (blame) {
505
+ html += '<div class="flex items-center gap-2 px-3 py-1.5" style="background: var(--rn-bg-secondary); border-bottom: 1px solid var(--rn-border-light); font-size: 0.7rem; color: var(--rn-text-muted)">'
506
+ html += '<span style="color: var(--rn-primary)">' + (blame.author || 'unknown') + '</span>'
507
+ html += '<span>\u00b7</span>'
508
+ html += '<span>' + (blame.date || '') + '</span>'
509
+ html += '<span>\u00b7</span>'
510
+ html += '<span style="color: var(--rn-text-muted)">' + (blame.message || '') + '</span>'
511
+ html += '<span class="ml-auto font-mono" style="color: var(--rn-text-muted)">' + (blame.sha || '') + '</span>'
512
+ html += '</div>'
513
+ }
514
+
515
+ snippet.lines.forEach(function(line) {
516
+ var bg = line.is_error ? 'var(--rn-bg-warning-subtle)' : 'transparent'
517
+ html += '<div class="flex" style="background: ' + bg + '; padding: 0 0.75rem">'
518
+ html += '<span style="min-width: 2.5rem; text-align: right; color: var(--rn-text-muted); user-select: none; padding-right: 1rem">' + line.number + '</span>'
519
+ var content = (line.content || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
520
+ html += '<span style="white-space: pre; color: var(--rn-text)">' + content + '</span>'
521
+ html += '</div>'
522
+ })
523
+
524
+ html += '</div>'
525
+ return html
526
+ }
527
+
528
+ closeDetails() {
529
+ var frame = document.getElementById("exception-details")
530
+ if (frame) {
531
+ frame.innerHTML = '<div class="text-center py-12"><div class="w-10 h-10 rounded-full flex items-center justify-center mx-auto" style="background: var(--rn-bg-secondary)"><svg class="w-5 h-5" style="color: var(--rn-text-muted)" 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></div><p class="mt-2 text-xs" style="color: var(--rn-text-muted)">Select an exception to view details</p></div>'
532
+ }
533
+ document.querySelectorAll(".rn-table tbody tr.selected").forEach(function(r) { r.classList.remove("selected") })
534
+ }
535
+ }
536
+
537
+
538
+ // ═══════════════════════════════════════════════════════════════
539
+ // Auto-register all controllers with Stimulus
540
+ // ═══════════════════════════════════════════════════════════════
541
+
542
+ function register() {
543
+ if (!window.Stimulus) {
544
+ console.warn("[RailsNexus] window.Stimulus not found. Ensure @hotwired/stimulus is loaded.")
545
+ return
546
+ }
547
+
548
+ window.Stimulus.register("rails_nexus", RailsNexusMainController)
549
+ window.Stimulus.register("rails_nexus-theme", RailsNexusThemeController)
550
+ window.Stimulus.register("rails_nexus-sidebar", RailsNexusSidebarController)
551
+ window.Stimulus.register("rails_nexus-detail", RailsNexusDetailController)
552
+ console.log("[RailsNexus] 4 Stimulus controllers registered")
553
+
554
+ // Reconnect: Stimulus already scanned the DOM before our controllers were
555
+ // registered, so existing elements with data-controller won't be connected.
556
+ // Trigger a DOM mutation on each engine-controlled element so Stimulus's
557
+ // MutationObserver picks them up and creates controller instances.
558
+ var identifiers = ["rails_nexus", "rails_nexus-theme", "rails_nexus-sidebar", "rails_nexus-detail"]
559
+ identifiers.forEach(function(id) {
560
+ document.querySelectorAll("[data-controller~='" + id + "']").forEach(function(el) {
561
+ var val = el.getAttribute("data-controller")
562
+ el.removeAttribute("data-controller")
563
+ el.setAttribute("data-controller", val)
564
+ })
565
+ })
566
+ }
567
+
568
+ // Register when ready
569
+ // The 'load' event fires AFTER all scripts (including type="module") have executed.
570
+ // This is critical because esbuild/bun/host apps load Stimulus as a module script,
571
+ // which is deferred and runs after DOMContentLoaded but before 'load'.
572
+ function tryRegister() {
573
+ if (window.Stimulus) {
574
+ register()
575
+ return true
576
+ }
577
+ return false
578
+ }
579
+
580
+ // Strategy 1: Already available (rare, but possible)
581
+ if (tryRegister()) return
582
+
583
+ // Strategy 2: Wait for the 'load' event (after all modules execute)
584
+ window.addEventListener("load", function() {
585
+ if (tryRegister()) return
586
+
587
+ // Strategy 3: Poll as a last resort (handles edge cases)
588
+ var attempts = 0
589
+ var interval = setInterval(function() {
590
+ attempts++
591
+ if (tryRegister() || attempts > 30) {
592
+ clearInterval(interval)
593
+ if (attempts > 30) console.warn("[RailsNexus] Stimulus not available. Ensure @hotwired/stimulus is loaded.")
594
+ }
595
+ }, 100)
596
+ })
597
+
598
+ // Strategy 4: Also try on DOMContentLoaded for non-module setups
599
+ document.addEventListener("DOMContentLoaded", function() {
600
+ tryRegister()
601
+ })
602
+
603
+ })()