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,494 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class StatsController < ApplicationController
5
+ before_action :verify_access
6
+
7
+ def index
8
+ @system_stats = collect_system_stats
9
+ @ruby_stats = collect_ruby_stats
10
+ @sidekiq_stats = collect_sidekiq_stats if defined?(Sidekiq)
11
+ @cron_job_stats = CronJob.summary if defined?(CronJob)
12
+ @webhook_stats = WebhookDelivery.summary if defined?(WebhookDelivery)
13
+ @exception_stats = collect_exception_stats
14
+ @platform_stats = collect_platform_stats
15
+ @database_stats = collect_database_stats
16
+ @backup_stats = collect_backup_stats
17
+ @storm_stats = collect_storm_stats if defined?(RailsNexus::StormProtection)
18
+ end
19
+
20
+ private
21
+
22
+ def verify_access
23
+ config = RailsNexus.configuration
24
+ return if config.auth_block.nil?
25
+ unless config.auth_block&.call(self)
26
+ render plain: "Forbidden", status: :forbidden
27
+ end
28
+ end
29
+
30
+ def collect_system_stats
31
+ stats = {}
32
+
33
+ # Memory
34
+ if File.exist?("/proc/meminfo")
35
+ meminfo = File.read("/proc/meminfo")
36
+ total = meminfo[/MemTotal:\s+(\d+)/, 1].to_i
37
+ available = meminfo[/MemAvailable:\s+(\d+)/, 1].to_i
38
+ used = total - available
39
+ stats[:memory] = {
40
+ total_mb: (total / 1024.0).round(1),
41
+ used_mb: (used / 1024.0).round(1),
42
+ available_mb: (available / 1024.0).round(1),
43
+ usage_percent: total > 0 ? (used.to_f / total * 100).round(1) : 0
44
+ }
45
+ else
46
+ stats[:memory] = collect_memory_via_ruby
47
+ end
48
+
49
+ # CPU Load
50
+ if File.exist?("/proc/loadavg")
51
+ loadavg = File.read("/proc/loadavg").split
52
+ stats[:cpu] = {
53
+ load_1m: loadavg[0].to_f,
54
+ load_5m: loadavg[1].to_f,
55
+ load_15m: loadavg[2].to_f,
56
+ cores: cpu_core_count
57
+ }
58
+ else
59
+ stats[:cpu] = {
60
+ load_1m: 0,
61
+ load_5m: 0,
62
+ load_15m: 0,
63
+ cores: cpu_core_count
64
+ }
65
+ end
66
+
67
+ # Disk
68
+ stats[:disk] = collect_disk_stats
69
+
70
+ # Process
71
+ process_stats = {}
72
+ process_stats[:pid] = Process.pid
73
+ process_stats[:ppid] = Process.ppid
74
+ begin
75
+ process_stats[:uptime_seconds] = (Time.now - Process::Times.new.stime).to_i
76
+ rescue StandardError
77
+ process_stats[:uptime_seconds] = 0
78
+ end
79
+ pid = Process.pid
80
+ begin
81
+ process_stats[:thread_count] = Dir.glob("/proc/#{pid}/task/*").size
82
+ rescue StandardError
83
+ process_stats[:thread_count] = "N/A"
84
+ end
85
+ begin
86
+ process_stats[:open_files] = `ls /proc/#{pid}/fd 2>/dev/null | wc -l`.strip.to_i
87
+ rescue StandardError
88
+ process_stats[:open_files] = 0
89
+ end
90
+ stats[:process] = process_stats
91
+
92
+ stats
93
+ end
94
+
95
+ def collect_memory_via_ruby
96
+ # Fallback for non-Linux systems
97
+ gc_stat = GC.stat
98
+ heap_pages = gc_stat[:heap_allocated_pages] || 0
99
+ page_size = (gc_stat[:heap_page_size] || 4096)
100
+ total_bytes = heap_pages * page_size
101
+ {
102
+ total_mb: (total_bytes / 1024.0 / 1024.0).round(1),
103
+ used_mb: 0,
104
+ available_mb: 0,
105
+ usage_percent: 0,
106
+ gc_stat: {
107
+ total_allocated: gc_stat[:total_allocated_objects],
108
+ total_freed: gc_stat[:total_freed_objects],
109
+ heap_allocated: heap_pages
110
+ }
111
+ }
112
+ end
113
+
114
+ def cpu_core_count
115
+ if File.exist?("/proc/cpuinfo")
116
+ File.read("/proc/cpuinfo").scan(/^processor\s*:/).size
117
+ else
118
+ Etc.nprocessors rescue 1
119
+ end
120
+ end
121
+
122
+ def collect_disk_stats
123
+ stat = Sys::Filesystem.stat("/") rescue nil
124
+ if stat
125
+ total = stat.blocks * stat.fragment_size
126
+ available = stat.bavail * stat.fragment_size
127
+ used = total - available
128
+ {
129
+ total_gb: (total / 1024.0 / 1024.0 / 1024.0).round(1),
130
+ used_gb: (used / 1024.0 / 1024.0 / 1024.0).round(1),
131
+ available_gb: (available / 1024.0 / 1024.0 / 1024.0).round(1),
132
+ usage_percent: total > 0 ? (used.to_f / total * 100).round(1) : 0
133
+ }
134
+ else
135
+ # Fallback: use `df` command
136
+ df_output = `df -h / 2>/dev/null`.split("\n").last&.split
137
+ if df_output
138
+ {
139
+ total_gb: df_output[1],
140
+ used_gb: df_output[2],
141
+ available_gb: df_output[3],
142
+ usage_percent: df_output[4]&.to_i || 0
143
+ }
144
+ else
145
+ { total_gb: "N/A", used_gb: "N/A", available_gb: "N/A", usage_percent: 0 }
146
+ end
147
+ end
148
+ end
149
+
150
+ def collect_ruby_stats
151
+ {
152
+ ruby_version: RUBY_VERSION,
153
+ ruby_platform: RUBY_PLATFORM,
154
+ rails_version: Rails::VERSION::STRING,
155
+ bundler_version: Bundler::VERSION,
156
+ environment: Rails.env,
157
+ app_name: RailsNexus.configuration.application_name || "RailsNexus",
158
+ rails_nexus_version: RailsNexus::VERSION,
159
+ process_uptime: process_uptime,
160
+ timezone: Time.zone.name,
161
+ database_adapter: ActiveRecord::Base.connection.adapter_name
162
+ }
163
+ end
164
+
165
+ def collect_sidekiq_stats
166
+ require "sidekiq/api"
167
+ redis = Sidekiq.redis { |c| c }
168
+ stats = Sidekiq::Stats.new
169
+ {
170
+ processed: stats.processed,
171
+ failed: stats.failed,
172
+ enqueued: stats.enqueued,
173
+ queues: stats.queues.transform_values { |v| v },
174
+ workers: Sidekiq::Workers.new.size,
175
+ redis_version: redis.info["redis_version"],
176
+ redis_uptime_seconds: redis.info["uptime_in_seconds"].to_i,
177
+ redis_memory_mb: (redis.info["used_memory"].to_f / 1024 / 1024).round(2),
178
+ retry_size: stats.retry_size,
179
+ scheduled_size: stats.scheduled_size
180
+ }
181
+ rescue StandardError => e
182
+ { error: e.message }
183
+ end
184
+
185
+ def collect_storm_stats
186
+ return { enabled: false } unless defined?(RailsNexus::StormProtection)
187
+ RailsNexus::StormProtection.stats
188
+ rescue StandardError => e
189
+ { enabled: false, error: e.message }
190
+ end
191
+
192
+ def collect_database_stats
193
+ stats = {}
194
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
195
+
196
+ # Connection pool
197
+ pool = ActiveRecord::Base.connection_pool
198
+ stats[:adapter] = ActiveRecord::Base.connection.adapter_name
199
+ stats[:pool_size] = pool.size
200
+ stats[:pool_connections] = pool.connections.size
201
+ stats[:pool_available] = pool.available? ? pool.connections.size : 0
202
+
203
+ # Database version
204
+ begin
205
+ stats[:version] = ActiveRecord::Base.connection.select_value("SELECT version()")
206
+ rescue StandardError
207
+ stats[:version] = "N/A"
208
+ end
209
+
210
+ # Table stats (rails_nexus tables)
211
+ stats[:tables] = collect_table_stats
212
+
213
+ # Connection test
214
+ start_time = Time.now
215
+ ActiveRecord::Base.connection.execute("SELECT 1")
216
+ stats[:ping_ms] = ((Time.now - start_time) * 1000).round(2)
217
+
218
+ # Active connections
219
+ begin
220
+ if adapter.include?("mysql") || adapter.include?("trilogy")
221
+ result = ActiveRecord::Base.connection.execute("SHOW STATUS WHERE Variable_name = 'Threads_connected'")
222
+ row = result.first
223
+ stats[:active_connections] = row ? row["Value"].to_i : 0
224
+ elsif adapter.include?("postgresql")
225
+ stats[:active_connections] = ActiveRecord::Base.connection.select_value(
226
+ "SELECT count(*) FROM pg_stat_activity WHERE state = 'active'"
227
+ )
228
+ else
229
+ stats[:active_connections] = pool.connections.size
230
+ end
231
+ rescue StandardError
232
+ stats[:active_connections] = pool.connections.size
233
+ end
234
+
235
+ # Uptime
236
+ begin
237
+ if adapter.include?("mysql") || adapter.include?("trilogy")
238
+ result = ActiveRecord::Base.connection.execute("SHOW STATUS WHERE Variable_name = 'Uptime'")
239
+ row = result.first
240
+ uptime_seconds = row ? row["Value"].to_i : 0
241
+ days = uptime_seconds / 86400
242
+ hours = (uptime_seconds % 86400) / 3600
243
+ stats[:uptime] = "#{days}d #{hours}h"
244
+ elsif adapter.include?("postgresql")
245
+ stats[:uptime] = ActiveRecord::Base.connection.select_value(
246
+ "SELECT now() - pg_postmaster_start_time()").to_s
247
+ else
248
+ stats[:uptime] = "N/A"
249
+ end
250
+ rescue StandardError
251
+ stats[:uptime] = "N/A"
252
+ end
253
+
254
+ stats
255
+ rescue StandardError => e
256
+ { error: e.message }
257
+ end
258
+
259
+ def collect_backup_stats
260
+ stats = { detected: false, gem: nil, status: "unknown", last_backup: nil, details: {} }
261
+
262
+ # Check for backup gem
263
+ if defined?(Backup)
264
+ stats[:detected] = true
265
+ stats[:gem] = "backup"
266
+ stats[:version] = Backup::VERSION if Backup.const_defined?(:VERSION)
267
+
268
+ begin
269
+ # Check for backup configuration
270
+ config_path = Rails.root.join("config", "backup")
271
+ if config_path.exist?
272
+ stats[:details][:config_path] = config_path.to_s
273
+ stats[:status] = "configured"
274
+ end
275
+
276
+ # Check for backup logs
277
+ log_path = Rails.root.join("log", "backup")
278
+ if log_path.exist?
279
+ log_files = log_path.glob("*.log").sort_by(&:mtime).reverse
280
+ if log_files.any?
281
+ last_log = log_files.first
282
+ stats[:details][:log_file] = last_log.to_s
283
+ stats[:details][:log_size] = (last_log.size / 1024.0).round(1)
284
+
285
+ # Parse last backup time from log
286
+ log_content = last_log.read(1000) # Read first 1KB
287
+ if log_content.match(/started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/)
288
+ stats[:last_backup] = Time.parse($1)
289
+ end
290
+ end
291
+ end
292
+
293
+ # Check for backup models
294
+ models = Backup::Model.all rescue []
295
+ if models.any?
296
+ stats[:details][:models] = models.map do |m|
297
+ {
298
+ name: m.name,
299
+ schedule: m.schedule&.to_s,
300
+ triggers: m.triggers.map(&:name)
301
+ }
302
+ end
303
+ stats[:status] = "configured"
304
+ end
305
+ rescue StandardError => e
306
+ stats[:details][:error] = e.message
307
+ end
308
+ end
309
+
310
+ # Check for pg_dump (PostgreSQL)
311
+ if system("which pg_dump > /dev/null 2>&1")
312
+ stats[:detected] = true
313
+ stats[:gem] ||= "pg_dump"
314
+ stats[:details][:pg_dump] = `pg_dump --version 2>/dev/null`.strip
315
+ end
316
+
317
+ # Check for mysqldump (MySQL)
318
+ if system("which mysqldump > /dev/null 2>&1")
319
+ stats[:detected] = true
320
+ stats[:gem] ||= "mysqldump"
321
+ stats[:details][:mysqldump] = `mysqldump --version 2>/dev/null`.strip
322
+ end
323
+
324
+ # Check for AWS CLI (RDS backups)
325
+ if system("which aws > /dev/null 2>&1")
326
+ stats[:detected] = true
327
+ stats[:gem] ||= "aws-cli"
328
+ stats[:details][:aws_cli] = `aws --version 2>/dev/null`.strip
329
+
330
+ # Check for RDS snapshots if configured
331
+ begin
332
+ rds_client = Aws::RDS::Client.new
333
+ snapshots = rds_client.describe_db_snapshots(db_snapshot_identifier: "automated-*")
334
+ stats[:details][:rds_snapshots] = snapshots.db_snapshots.count
335
+ stats[:status] = "configured" if snapshots.db_snapshots.any?
336
+ rescue StandardError
337
+ # AWS not configured or no access
338
+ end
339
+ end
340
+
341
+ # Check for docker backup solutions
342
+ if File.exist?("/var/lib/docker")
343
+ stats[:detected] = true
344
+ stats[:gem] ||= "docker"
345
+ stats[:details][:docker] = `docker --version 2>/dev/null`.strip
346
+ end
347
+
348
+ # Check for crontab entries related to backups
349
+ begin
350
+ crontab = `crontab -l 2>/dev/null`
351
+ if crontab.include?("backup") || crontab.include?("dump")
352
+ stats[:details][:crontab_backup] = true
353
+ stats[:status] = "scheduled" if stats[:status] == "unknown"
354
+ end
355
+ rescue StandardError
356
+ # Cannot read crontab
357
+ end
358
+
359
+ # Check for backup-related environment variables
360
+ backup_env_vars = %w[BACKUP_DIR BACKUP_S3_BUCKET BACKUP_GCS_BUCKET DATABASE_BACKUP_URL]
361
+ found_env = backup_env_vars.select { |var| ENV[var] }
362
+ if found_env.any?
363
+ stats[:detected] = true
364
+ stats[:details][:env_vars] = found_env
365
+ end
366
+
367
+ # Determine overall status
368
+ if stats[:last_backup]
369
+ hours_since = (Time.now - stats[:last_backup]) / 3600
370
+ if hours_since < 24
371
+ stats[:status] = "healthy"
372
+ elsif hours_since < 168 # 7 days
373
+ stats[:status] = "warning"
374
+ else
375
+ stats[:status] = "stale"
376
+ end
377
+ elsif stats[:status] == "unknown"
378
+ stats[:status] = "not_configured"
379
+ end
380
+
381
+ stats
382
+ end
383
+
384
+ def collect_table_stats
385
+ tables = []
386
+
387
+ # RailsNexus tables
388
+ rails_nexus_tables = %w[rails_nexus_exceptions rails_nexus_cron_jobs rails_nexus_webhook_deliveries]
389
+
390
+ rails_nexus_tables.each do |table_name|
391
+ begin
392
+ next unless ActiveRecord::Base.connection.table_exists?(table_name)
393
+
394
+ count = ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM #{table_name}")
395
+ tables << {
396
+ name: table_name,
397
+ row_count: count,
398
+ size_mb: 0,
399
+ status: count > 0 ? "active" : "empty"
400
+ }
401
+ rescue StandardError
402
+ # Table might not exist yet
403
+ end
404
+ end
405
+
406
+ # Try to get table sizes if available
407
+ begin
408
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
409
+ if adapter.include?("mysql") || adapter.include?("trilogy")
410
+ result = ActiveRecord::Base.connection.execute(<<~SQL)
411
+ SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb
412
+ FROM information_schema.tables
413
+ WHERE table_schema = DATABASE()
414
+ AND table_name IN ('rails_nexus_exceptions', 'rails_nexus_cron_jobs', 'rails_nexus_webhook_deliveries')
415
+ SQL
416
+ result.each do |row|
417
+ table = tables.find { |t| t[:name] == row["table_name"] }
418
+ table[:size_mb] = row["size_mb"] if table
419
+ end
420
+ elsif adapter.include?("postgresql")
421
+ result = ActiveRecord::Base.connection.execute(<<~SQL)
422
+ SELECT relname as table_name,
423
+ pg_size_pretty(pg_total_relation_size(relid)) as size_pretty,
424
+ pg_total_relation_size(relid) as size_bytes
425
+ FROM pg_catalog.pg_statio_user_tables
426
+ WHERE relname IN ('rails_nexus_exceptions', 'rails_nexus_cron_jobs', 'rails_nexus_webhook_deliveries')
427
+ SQL
428
+ result.each do |row|
429
+ table = tables.find { |t| t[:name] == row["table_name"] }
430
+ table[:size_mb] = (row["size_bytes"].to_f / 1024 / 1024).round(2) if table
431
+ end
432
+ end
433
+ rescue StandardError
434
+ # Ignore size collection errors
435
+ end
436
+
437
+ tables
438
+ end
439
+
440
+ def collect_platform_stats
441
+ {
442
+ breakdown: LoggedException.platform_stats,
443
+ web: LoggedException.platform_stats.find { |p| p.platform == "web" },
444
+ ios: LoggedException.platform_stats.find { |p| p.platform == "ios" },
445
+ android: LoggedException.platform_stats.find { |p| p.platform == "android" },
446
+ api: LoggedException.platform_stats.find { |p| p.platform == "api" },
447
+ bot: LoggedException.platform_stats.find { |p| p.platform == "bot" }
448
+ }
449
+ rescue StandardError
450
+ {}
451
+ end
452
+
453
+ def collect_exception_stats
454
+ {
455
+ total: LoggedException.count,
456
+ today: LoggedException.where("created_at >= ?", Time.current.beginning_of_day).count,
457
+ this_week: LoggedException.where("created_at >= ?", 1.week.ago).count,
458
+ this_month: LoggedException.where("created_at >= ?", 1.month.ago).count,
459
+ unique_classes: LoggedException.distinct.count(:exception_class),
460
+ top_classes: LoggedException.group(:exception_class).order("count_all DESC").limit(5).count,
461
+ hourly_trend: hourly_trend_data
462
+ }
463
+ end
464
+
465
+ def hourly_trend_data
466
+ return [] unless defined?(LoggedException)
467
+ 24.times.map do |i|
468
+ hour = i.hours.ago
469
+ {
470
+ hour: hour.strftime("%H:00"),
471
+ count: LoggedException.where(
472
+ created_at: hour.beginning_of_hour..hour.end_of_hour
473
+ ).count
474
+ }
475
+ end.reverse
476
+ end
477
+
478
+ def process_uptime
479
+ start_time = File.read("/proc/#{Process.pid}/stat").split[21].to_i rescue nil
480
+ if start_time
481
+ uptime_seconds = Time.now.to_i - (Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i - start_time)
482
+ seconds = Time.now.to_i - start_time
483
+ days = seconds / 86400
484
+ hours = (seconds % 86400) / 3600
485
+ minutes = (seconds % 3600) / 60
486
+ "#{days}d #{hours}h #{minutes}m"
487
+ else
488
+ "N/A"
489
+ end
490
+ rescue StandardError
491
+ "N/A"
492
+ end
493
+ end
494
+ end
@@ -0,0 +1,96 @@
1
+ module RailsNexus
2
+ class WorkflowController < ApplicationController
3
+ before_action :find_exception
4
+
5
+ # POST /rails_nexus/logged_exceptions/:id/assign
6
+ def assign
7
+ @exception.assign_to(params[:assigned_to], author: current_author)
8
+ respond_to do |format|
9
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("workflow-panel", partial: "rails_nexus/logged_exceptions/workflow_panel", locals: { exception: @exception }) }
10
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Assigned to #{params[:assigned_to]}" }
11
+ format.json { render json: { status: "ok", assigned_to: @exception.assigned_to } }
12
+ end
13
+ end
14
+
15
+ # POST /rails_nexus/logged_exceptions/:id/priority
16
+ def set_priority
17
+ @exception.set_priority(params[:priority], author: current_author)
18
+ respond_to do |format|
19
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("workflow-panel", partial: "rails_nexus/logged_exceptions/workflow_panel", locals: { exception: @exception }) }
20
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Priority set to #{params[:priority]}" }
21
+ format.json { render json: { status: "ok", priority: @exception.priority } }
22
+ end
23
+ end
24
+
25
+ # POST /rails_nexus/logged_exceptions/:id/snooze
26
+ def snooze
27
+ duration = parse_duration(params[:duration])
28
+ @exception.snooze(duration, author: current_author)
29
+ respond_to do |format|
30
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("workflow-panel", partial: "rails_nexus/logged_exceptions/workflow_panel", locals: { exception: @exception }) }
31
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Snoozed for #{duration.inspect}" }
32
+ format.json { render json: { status: "ok", snoozed_until: @exception.snoozed_until } }
33
+ end
34
+ end
35
+
36
+ # POST /rails_nexus/logged_exceptions/:id/mute
37
+ def mute
38
+ @exception.mute!(author: current_author)
39
+ respond_to do |format|
40
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("workflow-panel", partial: "rails_nexus/logged_exceptions/workflow_panel", locals: { exception: @exception }) }
41
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Muted" }
42
+ format.json { render json: { status: "ok", muted: @exception.muted } }
43
+ end
44
+ end
45
+
46
+ # POST /rails_nexus/logged_exceptions/:id/unmute
47
+ def unmute
48
+ @exception.unmute!(author: current_author)
49
+ respond_to do |format|
50
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("workflow-panel", partial: "rails_nexus/logged_exceptions/workflow_panel", locals: { exception: @exception }) }
51
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Unmuted" }
52
+ format.json { render json: { status: "ok", muted: @exception.muted } }
53
+ end
54
+ end
55
+
56
+ # POST /rails_nexus/logged_exceptions/:id/comment
57
+ def add_comment
58
+ @exception.add_comment(author: current_author, body: params[:body], comment_type: params[:comment_type] || "comment")
59
+ respond_to do |format|
60
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("comments-list", partial: "rails_nexus/logged_exceptions/comments_list", locals: { exception: @exception }) }
61
+ format.html { redirect_back fallback_location: logged_exception_path(@exception), notice: "Comment added" }
62
+ format.json { render json: { status: "ok", comments_count: @exception.comments_count } }
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def find_exception
69
+ @exception = RailsNexus::LoggedException.find(params[:id])
70
+ end
71
+
72
+ def current_author
73
+ # Try to get current user from the host app's auth system
74
+ if respond_to?(:current_user, true) && current_user
75
+ current_user.respond_to?(:email) ? current_user.email : current_user.to_s
76
+ elsif RailsNexus.configuration.current_user_block
77
+ RailsNexus.configuration.current_user_block.call(self)
78
+ else
79
+ "anonymous"
80
+ end
81
+ end
82
+
83
+ def parse_duration(str)
84
+ case str.to_s
85
+ when /(\d+)\s*hour/
86
+ $1.to_i.hours
87
+ when /(\d+)\s*day/
88
+ $1.to_i.days
89
+ when /(\d+)\s*week/
90
+ $1.to_i.weeks
91
+ else
92
+ 24.hours # default: 1 day
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ module RailsNexus
2
+ module ApplicationHelper
3
+ include Pagy::Frontend
4
+ end
5
+ end