rails_nexus 2.1.0 → 2.1.3

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +68 -13
  4. data/SECURITY.md +11 -0
  5. data/THIRD_PARTY_NOTICES.md +14 -0
  6. data/app/assets/config/rails_nexus_manifest.js +1 -1
  7. data/app/assets/javascripts/rails_nexus/application.js +55 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
  9. data/app/controllers/rails_nexus/application_controller.rb +6 -2
  10. data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
  11. data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
  12. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
  13. data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
  14. data/app/controllers/rails_nexus/settings_controller.rb +1 -39
  15. data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
  16. data/app/controllers/rails_nexus/stats_controller.rb +33 -159
  17. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
  18. data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
  19. data/app/javascript/rails_nexus/application.js +15 -3
  20. data/app/models/rails_nexus/logged_exception.rb +27 -13
  21. data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
  22. data/app/views/layouts/rails_nexus/application.html.erb +2 -13
  23. data/app/views/rails_nexus/stats/index.html.erb +1 -102
  24. data/config/routes.rb +0 -11
  25. data/lib/generators/rails_nexus/install_generator.rb +1 -1
  26. data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
  27. data/lib/generators/rails_nexus/templates/migration.rb +0 -82
  28. data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
  29. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
  30. data/lib/rails_nexus/configuration.rb +10 -45
  31. data/lib/rails_nexus/database_adapter.rb +53 -0
  32. data/lib/rails_nexus/engine.rb +6 -17
  33. data/lib/rails_nexus/logger.rb +1 -12
  34. data/lib/rails_nexus/notifications.rb +27 -20
  35. data/lib/rails_nexus/version.rb +1 -1
  36. data/lib/rails_nexus/webhook_client.rb +151 -0
  37. data/lib/rails_nexus.rb +37 -23
  38. data/lib/tasks/rails_nexus.rake +0 -80
  39. data/rails_nexus.gemspec +4 -5
  40. metadata +12 -41
  41. data/app/controllers/rails_nexus/backup_controller.rb +0 -120
  42. data/app/jobs/rails_nexus/backup_job.rb +0 -18
  43. data/app/models/rails_nexus/backup.rb +0 -107
  44. data/app/models/rails_nexus/backup_config.rb +0 -158
  45. data/app/services/rails_nexus/backup_runner.rb +0 -62
  46. data/app/services/rails_nexus/backup_service.rb +0 -682
  47. data/app/views/rails_nexus/backup/_form.html.erb +0 -334
  48. data/app/views/rails_nexus/backup/config_history.html.erb +0 -67
  49. data/app/views/rails_nexus/backup/edit.html.erb +0 -15
  50. data/app/views/rails_nexus/backup/history.html.erb +0 -75
  51. data/app/views/rails_nexus/backup/index.html.erb +0 -112
  52. data/app/views/rails_nexus/backup/new.html.erb +0 -15
  53. data/config/importmap.rb +0 -15
  54. data/db/seeds/backup_configs.rb +0 -140
  55. data/lib/generators/rails_nexus/backup_generator.rb +0 -70
  56. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +0 -152
  57. data/lib/generators/rails_nexus/templates/backup/config.rb +0 -27
  58. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +0 -24
  59. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +0 -35
  60. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +0 -43
  61. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +0 -15
  62. data/lib/generators/rails_nexus/templates/backup/schedule.rb +0 -28
@@ -24,14 +24,14 @@ module RailsNexus
24
24
  scope :unassigned, -> { where(assigned_to: [nil, ""]) }
25
25
  scope :active, -> { not_muted.not_snoozed }
26
26
 
27
- def self.ransackable_attributes(auth_object = nil)
27
+ def self.ransackable_attributes(_auth_object = nil)
28
28
  %w[action_name backtrace cause_chain controller_name created_at device_type environment
29
29
  assigned_to comments_count exception_class fingerprint id instance_variables local_variables
30
30
  message muted occurrence_count platform platform_version priority remote_ip request
31
31
  snoozed_until system_health updated_at user_agent user_id user_info]
32
32
  end
33
33
 
34
- def self.ransackable_associations(auth_object = nil)
34
+ def self.ransackable_associations(_auth_object = nil)
35
35
  []
36
36
  end
37
37
 
@@ -299,8 +299,8 @@ module RailsNexus
299
299
 
300
300
  # Look for ActiveRecord collection iteration patterns:
301
301
  # e.g., .each, .map, .find_each near ActiveRecord calls
302
- has_iteration = app_lines.any? { |l| l.match?(/\.each|\.map|\.select|\.reject|\.flat_map|\.find_each/) }
303
- has_ar_call = app_lines.any? { |l| l.match?(/ActiveRecord|_callback|association|belongs_to|has_many|load_target|reload/) }
302
+ app_lines.any? { |l| l.match?(/\.each|\.map|\.select|\.reject|\.flat_map|\.find_each/) }
303
+ app_lines.any? { |l| l.match?(/ActiveRecord|_callback|association|belongs_to|has_many|load_target|reload/) }
304
304
 
305
305
  # Also detect: repeated similar backtrace across multiple exceptions of same class
306
306
  fingerprint = backtrace_fingerprint(app_lines)
@@ -435,9 +435,11 @@ module RailsNexus
435
435
  end
436
436
 
437
437
  begin
438
+ stdout, _stderr, status = Open3.capture3("ps", "-o", "rss=,vsz=", "-p", Process.pid.to_s)
439
+ memory = status.success? ? stdout.split.map(&:to_i) : []
438
440
  health[:memory] = {
439
- rss_kb: `ps -o rss= -p #{Process.pid} 2>/dev/null`.strip.to_i,
440
- vsz_kb: `ps -o vsz= -p #{Process.pid} 2>/dev/null`.strip.to_i
441
+ rss_kb: memory[0] || 0,
442
+ vsz_kb: memory[1] || 0
441
443
  }
442
444
  rescue StandardError
443
445
  health[:memory] = { error: "unable to capture" }
@@ -495,17 +497,28 @@ module RailsNexus
495
497
  if request.is_a?(String)
496
498
  write_attribute :request, request
497
499
  elsif request.respond_to?(:env) && request.env.is_a?(Hash)
498
- max = request.env.keys.max { |a, b| a.length <=> b.length }
499
- env = request.env.keys.sort.inject [] do |memo, key|
500
- memo << "* %-*s: %s" % [max.length, key, request.env[key].to_s.strip]
500
+ filtered_env = RailsNexus.filter_sensitive_data(request.env)
501
+ keys = filtered_env.keys.map(&:to_s).sort
502
+ max_length = keys.map(&:length).max || 0
503
+ env = keys.each_with_object([]) do |key, memo|
504
+ value = filtered_env[key] || filtered_env[key.to_sym]
505
+ memo << ("* %-*s: %s" % [max_length, key, value.to_s.strip])
501
506
  end
502
507
  write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}").join("\n"))
503
508
 
504
509
  method_str = request.respond_to?(:get?) && request.get? ? "" : " #{request.respond_to?(:method) ? request.method.to_s.upcase : "GET"}"
510
+ parameters = request.respond_to?(:parameters) ? RailsNexus.filter_sensitive_data(request.parameters) : {}
511
+ request_path = if request.respond_to?(:path)
512
+ request.path
513
+ elsif request.respond_to?(:fullpath)
514
+ request.fullpath.to_s.split("?", 2).first
515
+ else
516
+ "/"
517
+ end
505
518
  write_attribute(:request, [
506
- "* URL:#{method_str} #{request.respond_to?(:protocol) ? request.protocol : "http://"}#{request.respond_to?(:env) ? request.env["HTTP_HOST"] : "localhost"}#{request.respond_to?(:fullpath) ? request.fullpath : "/"}",
519
+ "* URL:#{method_str} #{request.respond_to?(:protocol) ? request.protocol : "http://"}#{filtered_env["HTTP_HOST"] || "localhost"}#{request_path}",
507
520
  "* Format: #{request.respond_to?(:format) ? request.format.to_s : "html"}",
508
- "* Parameters: #{request.respond_to?(:parameters) ? request.parameters.inspect : "{}"}",
521
+ "* Parameters: #{parameters.inspect}",
509
522
  "* Rails Root: #{rails_root}"
510
523
  ].join("\n"))
511
524
  else
@@ -545,10 +558,11 @@ module RailsNexus
545
558
 
546
559
  # Build hourly time series of error counts
547
560
  def self.build_time_series(days: 30)
561
+ days = (Integer(days, exception: false) || 30).clamp(1, 90)
548
562
  start_time = days.days.ago.beginning_of_hour
563
+ bucket = RailsNexus::DatabaseAdapter.time_bucket_expression
549
564
  raw = where("created_at >= ?", start_time)
550
- .group(Arel.sql("DATE_FORMAT(created_at, '%Y-%m-%d %H:00')"))
551
- .order(Arel.sql("DATE_FORMAT(created_at, '%Y-%m-%d %H:00')"))
565
+ .group(Arel.sql(bucket))
552
566
  .count
553
567
 
554
568
  # Fill gaps with zeros
@@ -60,17 +60,6 @@
60
60
  Cron Jobs
61
61
  <% end %>
62
62
 
63
- <%= link_to backup_path,
64
- class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && !%w[all_history history].include?(action_name) ? 'active' : ''}" do %>
65
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>
66
- Backups
67
- <% end %>
68
-
69
- <%= link_to backup_history_path,
70
- class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && %w[all_history history].include?(action_name) ? 'active' : ''}" do %>
71
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
72
- Backup History
73
- <% end %>
74
63
 
75
64
  <%= link_to settings_path,
76
65
  class: "rn-nav-item #{request.path == rails_nexus.settings_path ? 'active' : ''}" do %>
@@ -9,7 +9,7 @@
9
9
 
10
10
  <%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
11
11
 
12
- <%= javascript_importmap_tags "rails_nexus/application" %>
12
+ <%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
13
13
 
14
14
  <% if respond_to?(:turbo_refreshes_with) %>
15
15
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
@@ -24,7 +24,7 @@
24
24
  rails_nexus/analytics rails_nexus/stats
25
25
  ].include?(controller_path)
26
26
  operations_menu_active = %w[
27
- rails_nexus/cron_jobs rails_nexus/backup rails_nexus/settings
27
+ rails_nexus/cron_jobs rails_nexus/settings backup_nexus/backup
28
28
  ].include?(controller_path)
29
29
  resources_menu_active = RailsNexus.configuration.sidebar_links.any? do |link|
30
30
  link[:url].to_s == request.path
@@ -109,17 +109,6 @@
109
109
  Cron Jobs
110
110
  <% end %>
111
111
 
112
- <%= link_to backup_path,
113
- class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && !%w[all_history history].include?(action_name) ? 'active' : ''}" do %>
114
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>
115
- Backups
116
- <% end %>
117
-
118
- <%= link_to backup_history_path,
119
- class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && %w[all_history history].include?(action_name) ? 'active' : ''}" do %>
120
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
121
- Backup History
122
- <% end %>
123
112
 
124
113
  <%= link_to settings_path,
125
114
  class: "rn-nav-item #{request.path == rails_nexus.settings_path ? 'active' : ''}" do %>
@@ -339,7 +339,7 @@
339
339
  </td>
340
340
  <td>
341
341
  <span class="text-sm" style="color: var(--rn-text-muted)">
342
- <%= table[:size_mb] > 0 ? "#{table[:size_mb]} MB" : "—" %>
342
+ <%= table[:size_mb].to_f > 0 ? "#{table[:size_mb]} MB" : "—" %>
343
343
  </span>
344
344
  </td>
345
345
  <td>
@@ -367,107 +367,6 @@
367
367
  </div>
368
368
  <% end %>
369
369
 
370
- <%# ─── Backup Health ──────────────────────────────────────── %>
371
- <% if @backup_stats && @backup_stats[:detected] %>
372
- <div class="rn-card">
373
- <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
374
- <div class="flex items-center gap-2">
375
- <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-info-light)">
376
- <svg class="w-4 h-4" style="color: var(--rn-info)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
377
- <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
378
- </svg>
379
- </div>
380
- <div>
381
- <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
382
- <p class="text-xs" style="color: var(--rn-text-muted)"><%= @backup_stats[:gem] %> <%= @backup_stats[:version] ? "v#{@backup_stats[:version]}" : '' %></p>
383
- </div>
384
- </div>
385
- </div>
386
- <div class="p-4">
387
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
388
- <div>
389
- <div class="text-xs" style="color: var(--rn-text-muted)">Status</div>
390
- <div class="flex items-center gap-2 mt-1">
391
- <span class="w-2 h-2 rounded-full" style="background: <%= case @backup_stats[:status]
392
- when 'healthy' then 'var(--rn-success)'
393
- when 'warning' then 'var(--rn-warning)'
394
- when 'stale' then 'var(--rn-danger)'
395
- when 'configured' then 'var(--rn-info)'
396
- else 'var(--rn-text-muted)'
397
- end %>"></span>
398
- <span class="text-sm font-medium" style="color: var(--rn-text)"><%= @backup_stats[:status].capitalize %></span>
399
- </div>
400
- </div>
401
- <div>
402
- <div class="text-xs" style="color: var(--rn-text-muted)">Last Backup</div>
403
- <div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
404
- <% if @backup_stats[:last_backup] %>
405
- <%= @backup_stats[:last_backup].strftime("%b %d, %Y %I:%M %p") %>
406
- <% else %>
407
- <span style="color: var(--rn-text-muted)">Unknown</span>
408
- <% end %>
409
- </div>
410
- </div>
411
- <div>
412
- <div class="text-xs" style="color: var(--rn-text-muted)">Tool</div>
413
- <div class="text-sm font-medium mt-1" style="color: var(--rn-text)"><%= @backup_stats[:gem] %></div>
414
- </div>
415
- <div>
416
- <div class="text-xs" style="color: var(--rn-text-muted)">Config</div>
417
- <div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
418
- <%= @backup_stats[:status] == 'configured' || @backup_stats[:status] == 'scheduled' ? 'Yes' : 'No' %>
419
- </div>
420
- </div>
421
- </div>
422
-
423
- <%# Backup Details %>
424
- <% if @backup_stats[:details].any? %>
425
- <div class="border-t pt-4" style="border-color: var(--rn-border-light)">
426
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Details</h3>
427
- <div class="space-y-2">
428
- <% @backup_stats[:details].each do |key, value| %>
429
- <% next if value.is_a?(Hash) && value.empty? %>
430
- <div class="flex items-start gap-3 text-sm">
431
- <span class="rn-mono font-medium" style="color: var(--rn-text-secondary); min-width: 120px"><%= key.to_s.humanize %>:</span>
432
- <% if value.is_a?(Array) %>
433
- <div class="flex-1">
434
- <% value.each do |item| %>
435
- <div class="text-xs" style="color: var(--rn-text-muted)">
436
- <% if item.is_a?(Hash) %>
437
- <%= item[:name] %> <%= item[:schedule] ? "(#{item[:schedule]})" : '' %>
438
- <% else %>
439
- <%= item %>
440
- <% end %>
441
- </div>
442
- <% end %>
443
- </div>
444
- <% elsif value.is_a?(Hash) %>
445
- <div class="flex-1">
446
- <% value.each do |k, v| %>
447
- <div class="text-xs" style="color: var(--rn-text-muted)"><%= k %>: <%= v %></div>
448
- <% end %>
449
- </div>
450
- <% else %>
451
- <span class="flex-1 text-xs" style="color: var(--rn-text-muted)"><%= value %></span>
452
- <% end %>
453
- </div>
454
- <% end %>
455
- </div>
456
- </div>
457
- <% end %>
458
- </div>
459
- </div>
460
- <% elsif @backup_stats && !@backup_stats[:detected] %>
461
- <div class="rn-card">
462
- <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
463
- <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
464
- </div>
465
- <div class="p-4">
466
- <p class="text-sm" style="color: var(--rn-text-muted)">No backup solution detected. Consider adding a backup gem like <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">backup</code> or configuring <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">pg_dump</code>/<code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">mysqldump</code>.</p>
467
- </div>
468
- </div>
469
- <% end %>
470
-
471
370
  <%# ─── Platform Breakdown ──────────────────────────────────── %>
472
371
  <% if @platform_stats && @platform_stats[:breakdown]&.any? %>
473
372
  <div class="rn-card">
data/config/routes.rb CHANGED
@@ -41,17 +41,6 @@ RailsNexus::Engine.routes.draw do
41
41
  # N+1 query patterns
42
42
  get "n1_patterns", to: "n1_patterns#index", as: :n1_patterns
43
43
 
44
- # Backup management
45
- get "backup", to: "backup#index", as: :backup
46
- get "backup/new", to: "backup#new", as: :new_backup
47
- post "backup", to: "backup#create"
48
- get "backup/history", to: "backup#all_history", as: :backup_history
49
- get "backup/:id/edit", to: "backup#edit", as: :edit_backup
50
- patch "backup/:id", to: "backup#update"
51
- delete "backup/:id", to: "backup#destroy", as: :backup_destroy
52
- post "backup/:id/trigger", to: "backup#trigger", as: :backup_trigger
53
- get "backup/:id/history", to: "backup#history", as: :backup_config_history
54
-
55
44
  # Settings and webhook management
56
45
  get "settings", to: "settings#index", as: :settings
57
46
  post "settings/test_webhook", to: "settings#test_webhook", as: :test_webhook
@@ -35,7 +35,7 @@ module RailsNexus
35
35
  content = File.read(destination_controller)
36
36
  unless content.include?("log_exception_handler")
37
37
  inject_into_class app_controller, "ApplicationController" do
38
- "\n rescue_from Exception, with: :log_exception_handler\n"
38
+ "\n rescue_from StandardError, with: :log_exception_handler\n"
39
39
  end
40
40
  end
41
41
  end
@@ -7,3 +7,4 @@ Next steps:
7
7
  3. Visit `/rails_nexus` in the host application.
8
8
 
9
9
  The dashboard denies access until authentication is configured.
10
+ The generator captures `StandardError` application failures and deliberately leaves fatal process and VM exceptions to Rails and Ruby.
@@ -111,87 +111,5 @@ class CreateRailsNexusTables < ActiveRecord::Migration[<%= "[#{ActiveRecord::Mig
111
111
  add_index :rails_nexus_webhook_deliveries, :status
112
112
  add_index :rails_nexus_webhook_deliveries, :created_at
113
113
  add_index :rails_nexus_webhook_deliveries, :url
114
-
115
- # ─── rails_nexus_backups ─────────────────────────────────────────
116
- create_table :rails_nexus_backups do |t|
117
- t.string :config_name, null: false
118
- t.string :status, null: false
119
- t.text :file_path
120
- t.bigint :file_size
121
- t.float :duration
122
- t.text :error_message
123
- t.string :triggered_by
124
- t.datetime :started_at, null: false
125
- t.datetime :completed_at
126
- end
127
-
128
- add_index :rails_nexus_backups, [:config_name, :started_at]
129
- add_index :rails_nexus_backups, :status
130
- add_index :rails_nexus_backups, :started_at
131
-
132
- # ─── rails_nexus_backup_configs ────────────────────────────────
133
- create_table :rails_nexus_backup_configs do |t|
134
- t.string :name, null: false
135
- t.string :description
136
- t.string :database_name, null: false
137
- t.string :adapter, null: false, default: "mysql"
138
- t.string :host, default: "localhost"
139
- t.integer :port
140
- t.string :username
141
- t.string :password
142
- t.string :skip_tables
143
-
144
- t.string :storage_path, null: false
145
- t.integer :keep_count, default: 30
146
-
147
- t.boolean :compress, default: true
148
- t.boolean :bzip2_compress, default: false
149
- t.boolean :encrypted, default: false
150
- t.string :encryption_password
151
- t.boolean :encrypt_base64, default: false
152
- t.boolean :gpg_enabled, default: false
153
- t.string :gpg_password
154
-
155
- t.boolean :rsync_enabled, default: false
156
- t.string :rsync_host
157
- t.integer :rsync_port, default: 22
158
- t.string :rsync_user
159
- t.string :rsync_path
160
- t.boolean :rsync_mirror, default: false
161
- t.boolean :rsync_archive, default: true
162
- t.string :rsync_directories
163
- t.string :rsync_excludes
164
-
165
- t.string :mysql_additional_options
166
-
167
- # S3 storage
168
- t.boolean :s3_enabled, default: false
169
- t.string :s3_access_key
170
- t.string :s3_secret_key
171
- t.string :s3_bucket
172
- t.string :s3_region, default: "us-east-1"
173
- t.string :s3_prefix
174
-
175
- # Archives (file/dir backup)
176
- t.boolean :archive_enabled, default: false
177
- t.string :archive_paths
178
- t.string :archive_excludes
179
- t.boolean :split_chunks, default: false
180
-
181
- # Notifications
182
- t.string :notify_command
183
- t.boolean :notify_on_success, default: true
184
- t.boolean :notify_on_failure, default: true
185
- t.boolean :email_notify, default: false
186
- t.string :email_to
187
-
188
- t.string :schedule_cron
189
- t.boolean :enabled, default: true
190
-
191
- t.timestamps
192
- end
193
-
194
- add_index :rails_nexus_backup_configs, :name, unique: true
195
- add_index :rails_nexus_backup_configs, :enabled
196
114
  end
197
115
  end
@@ -34,6 +34,9 @@ Rails.application.config.to_prepare do
34
34
  # config.webhooks = ["https://hooks.slack.com/services/xxx"]
35
35
  # config.webhook_timeout = 5
36
36
  # config.webhook_headers = { "Authorization" => "Bearer token" }
37
+ # config.webhook_allowed_hosts = ["hooks.example.com", "*.example.org"]
38
+ # config.webhook_denied_hosts = ["blocked.example.org"]
39
+ # config.webhook_allow_http_in_development = false
37
40
 
38
41
  # ─── Cleanup ─────────────────────────────────────────────
39
42
  # Auto-delete exceptions older than N days
@@ -7,7 +7,7 @@
7
7
  <title><%= content_for?(:title) ? yield(:title) : RailsNexus.configuration.application_name || "RailsNexus" %></title>
8
8
 
9
9
  <%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
10
- <%= javascript_importmap_tags "rails_nexus/application" %>
10
+ <%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
11
11
 
12
12
  <% if respond_to?(:turbo_refreshes_with) %>
13
13
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
@@ -49,6 +49,13 @@ module RailsNexus
49
49
  # config.webhook_headers = { "Authorization" => "Bearer token123" }
50
50
  attr_accessor :webhook_headers
51
51
 
52
+ # Host allow/deny policies. Wildcards such as "*.example.com" are accepted.
53
+ attr_accessor :webhook_allowed_hosts
54
+ attr_accessor :webhook_denied_hosts
55
+
56
+ # HTTP remains disabled except when this is true and Rails.env is development.
57
+ attr_accessor :webhook_allow_http_in_development
58
+
52
59
  # ─── Logging ───────────────────────────────────────────────
53
60
 
54
61
  # Enable structured logging (default: true).
@@ -176,15 +183,6 @@ module RailsNexus
176
183
 
177
184
  # Source Code Viewing
178
185
  attr_accessor :source_code_viewing
179
-
180
- # Backup Management
181
- attr_accessor :backup_enabled
182
- attr_accessor :backup_config_path
183
- attr_accessor :backup_models_path
184
- attr_accessor :backup_dump_path
185
- attr_accessor :backup_notify_command
186
- attr_accessor :backup_alert_threshold_hours
187
-
188
186
  # ─── Database ───────────────────────────────────────────────
189
187
  # Use a separate database for RailsNexus tables.
190
188
  # Options:
@@ -209,6 +207,9 @@ module RailsNexus
209
207
  @webhooks = []
210
208
  @webhook_timeout = 5
211
209
  @webhook_headers = {}
210
+ @webhook_allowed_hosts = []
211
+ @webhook_denied_hosts = []
212
+ @webhook_allow_http_in_development = false
212
213
 
213
214
  # Logging
214
215
  @logging_enabled = true
@@ -272,15 +273,6 @@ module RailsNexus
272
273
 
273
274
  # Source Code Viewing
274
275
  @source_code_viewing = true
275
-
276
- # Backup Management
277
- @backup_enabled = false
278
- @backup_config_path = default_backup_config_path
279
- @backup_models_path = default_backup_models_path
280
- @backup_dump_path = default_backup_dump_path
281
- @backup_notify_command = nil
282
- @backup_alert_threshold_hours = 24
283
-
284
276
  # Database
285
277
  @database_url = nil
286
278
  @database_name = nil
@@ -288,32 +280,5 @@ module RailsNexus
288
280
  @table_prefix = "rails_nexus_"
289
281
  end
290
282
 
291
- private
292
-
293
- def default_backup_config_path
294
- if defined?(Rails) && Rails.respond_to?(:root)
295
- path = Rails.root.join("config/backup/config.rb")
296
- path.exist? ? path.to_s : nil
297
- else
298
- nil
299
- end
300
- end
301
-
302
- def default_backup_models_path
303
- if defined?(Rails) && Rails.respond_to?(:root)
304
- path = Rails.root.join("config/backup/models")
305
- path.exist? ? path.to_s : nil
306
- else
307
- nil
308
- end
309
- end
310
-
311
- def default_backup_dump_path
312
- if defined?(Rails) && Rails.respond_to?(:root)
313
- Rails.root.join("storage/rails_nexus/backups").to_s
314
- else
315
- "storage/rails_nexus/backups"
316
- end
317
- end
318
283
  end
319
284
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ module DatabaseAdapter
5
+ module_function
6
+
7
+ def family(connection = ActiveRecord::Base.connection)
8
+ name = connection.adapter_name.to_s.downcase
9
+ return :postgresql if name.include?("postgres")
10
+ return :mysql if name.include?("mysql") || name.include?("trilogy")
11
+ return :sqlite if name.include?("sqlite")
12
+
13
+ :unsupported
14
+ end
15
+
16
+ def time_bucket_expression(column: "created_at", period: :hour, connection: ActiveRecord::Base.connection)
17
+ column = connection.quote_column_name(column)
18
+
19
+ case [family(connection), period.to_sym]
20
+ when [:postgresql, :hour] then "TO_CHAR(DATE_TRUNC('hour', #{column}), 'YYYY-MM-DD HH24:00')"
21
+ when [:mysql, :hour] then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:00')"
22
+ when [:sqlite, :hour] then "strftime('%Y-%m-%d %H:00', #{column})"
23
+ else
24
+ raise NotImplementedError, "Time grouping is not supported by this database adapter"
25
+ end
26
+ end
27
+
28
+ def time_component_expression(column: "created_at", component:, connection: ActiveRecord::Base.connection)
29
+ column = connection.quote_column_name(column)
30
+
31
+ case [family(connection), component.to_sym]
32
+ when [:postgresql, :hour] then "CAST(EXTRACT(HOUR FROM #{column}) AS INTEGER)"
33
+ when [:mysql, :hour] then "HOUR(#{column})"
34
+ when [:sqlite, :hour] then "CAST(strftime('%H', #{column}) AS INTEGER)"
35
+ when [:postgresql, :weekday] then "CAST(EXTRACT(DOW FROM #{column}) AS INTEGER)"
36
+ when [:mysql, :weekday] then "DAYOFWEEK(#{column}) - 1"
37
+ when [:sqlite, :weekday] then "CAST(strftime('%w', #{column}) AS INTEGER)"
38
+ else
39
+ raise NotImplementedError, "Time component grouping is not supported by this database adapter"
40
+ end
41
+ end
42
+
43
+ def database_version(connection = ActiveRecord::Base.connection)
44
+ case family(connection)
45
+ when :sqlite then connection.select_value("SELECT sqlite_version()")
46
+ when :postgresql, :mysql then connection.select_value("SELECT version()")
47
+ else "N/A"
48
+ end
49
+ rescue ActiveRecord::StatementInvalid
50
+ "N/A"
51
+ end
52
+ end
53
+ end
@@ -11,8 +11,6 @@ module RailsNexus
11
11
  initializer "rails_nexus.assets", before: "propshaft.assets_middleware", group: :all do |app|
12
12
  next unless app.config.respond_to?(:assets)
13
13
 
14
- app.config.assets.paths << root.join("app/javascript")
15
-
16
14
  # Sprockets 4 only compiles assets listed in a manifest or in the
17
15
  # precompile list. Register these here so mounting applications do not
18
16
  # need to modify their own asset manifest.
@@ -20,21 +18,10 @@ module RailsNexus
20
18
  app.config.assets.precompile += %w[
21
19
  rails_nexus/application.css
22
20
  rails_nexus/application.js
23
- controllers/rails_nexus_controller.js
24
- controllers/rails_nexus_detail_controller.js
25
- controllers/rails_nexus_sidebar_controller.js
26
- controllers/rails_nexus_theme_controller.js
27
21
  ]
28
22
  end
29
23
  end
30
24
 
31
- # ─── Importmap support (for importmap-rails users) ────────────
32
-
33
- initializer "rails_nexus.importmap", before: "importmap" do |app|
34
- app.config.importmap.paths << root.join("config/importmap.rb")
35
- app.config.importmap.cache_sweepers << root.join("app/javascript")
36
- end
37
-
38
25
  # ─── Database connection (separate DB support) ─────────────────
39
26
 
40
27
  initializer "rails_nexus.database" do
@@ -105,10 +92,12 @@ module RailsNexus
105
92
 
106
93
  initializer "rails_nexus.after_create" do
107
94
  ActiveSupport.on_load(:active_record) do
108
- RailsNexus::LoggedException.after_create do |exception|
109
- RailsNexus::Notifications.notify(exception)
110
- rescue StandardError => e
111
- Rails.logger.error("[RailsNexus] Notification error: #{e.message}")
95
+ if defined?(RailsNexus::LoggedException)
96
+ RailsNexus::LoggedException.after_create do |exception|
97
+ RailsNexus::Notifications.notify(exception)
98
+ rescue StandardError => e
99
+ Rails.logger.error("[RailsNexus] Notification error: #{e.message}")
100
+ end
112
101
  end
113
102
  end
114
103
  end
@@ -170,18 +170,7 @@ module RailsNexus
170
170
  end
171
171
 
172
172
  def filter_params(params)
173
- return {} unless params.is_a?(Hash)
174
-
175
- rails_filters = Rails.application.config.filter_parameters
176
- params.each_with_object({}) do |(key, value), result|
177
- if rails_filters.any? { |f| key.to_s.include?(f.to_s) }
178
- result[key] = "[FILTERED]"
179
- elsif value.is_a?(Hash)
180
- result[key] = filter_params(value)
181
- else
182
- result[key] = value
183
- end
184
- end
173
+ RailsNexus.filter_sensitive_data(params)
185
174
  end
186
175
  end
187
176
  end