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
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class BackupController < ApplicationController
5
- before_action :rails_nexus_require_auth!
6
- before_action :check_backup_enabled
7
- before_action :set_config, only: %i[edit update destroy trigger]
8
-
9
- # GET /rails_nexus/backup
10
- def index
11
- @configs = RailsNexus::BackupConfig.order(:name)
12
- end
13
-
14
- # GET /rails_nexus/backup/new
15
- def new
16
- @config = RailsNexus::BackupConfig.new(
17
- adapter: "mysql",
18
- host: "localhost",
19
- port: 3306,
20
- storage_path: "~/dumps",
21
- keep_count: 30,
22
- compress: true
23
- )
24
- end
25
-
26
- # POST /rails_nexus/backup
27
- def create
28
- @config = RailsNexus::BackupConfig.new(config_params)
29
-
30
- if @config.save
31
- redirect_to backup_path, notice: "Backup config '#{@config.name}' created."
32
- else
33
- render :new, status: :unprocessable_entity
34
- end
35
- end
36
-
37
- # GET /rails_nexus/backup/:id/edit
38
- def edit; end
39
-
40
- # PATCH /rails_nexus/backup/:id
41
- def update
42
- # Strip blank password fields so they don't overwrite stored values
43
- filtered = config_params
44
- %w[password encryption_password gpg_password].each do |attr|
45
- filtered = filtered.except(attr) if filtered[attr].blank?
46
- end
47
-
48
- if @config.update(filtered)
49
- redirect_to backup_path, notice: "Backup config '#{@config.name}' updated."
50
- else
51
- render :edit, status: :unprocessable_entity
52
- end
53
- end
54
-
55
- # DELETE /rails_nexus/backup/:id
56
- def destroy
57
- name = @config.name
58
- @config.destroy
59
- redirect_to backup_path, notice: "Backup config '#{name}' deleted."
60
- end
61
-
62
- # POST /rails_nexus/backup/:id/trigger
63
- def trigger
64
- begin
65
- RailsNexus::BackupJob.perform_later(@config.id)
66
- redirect_to backup_path, notice: "Backup '#{@config.name}' started in background."
67
- rescue => e
68
- # Fallback to synchronous if Active Job is not configured
69
- result = RailsNexus::BackupService.run(@config)
70
- if result[:success]
71
- redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
72
- else
73
- redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
74
- end
75
- end
76
- end
77
-
78
- # GET /rails_nexus/backup/:id/history
79
- def history
80
- @config = RailsNexus::BackupConfig.find(params[:id])
81
- @records = RailsNexus::Backup.where(config_name: @config.name)
82
- .order(started_at: :desc)
83
- .limit(50)
84
- end
85
-
86
- # GET /rails_nexus/backup/history
87
- def all_history
88
- @records = RailsNexus::Backup.order(started_at: :desc).limit(100)
89
- render :history
90
- end
91
-
92
- private
93
-
94
- def set_config
95
- @config = RailsNexus::BackupConfig.find(params[:id])
96
- end
97
-
98
- def config_params
99
- params.require(:backup_config).permit(
100
- :name, :description, :database_name, :adapter, :host, :port,
101
- :username, :password, :storage_path, :keep_count,
102
- :compress, :encrypted, :encryption_password,
103
- :rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror, :rsync_archive, :rsync_directories, :rsync_excludes,
104
- :notify_command, :notify_on_success, :notify_on_failure,
105
- :schedule_cron, :enabled,
106
- :s3_enabled, :s3_access_key, :s3_secret_key, :s3_bucket, :s3_region, :s3_prefix,
107
- :gpg_enabled, :gpg_password,
108
- :email_notify, :email_to,
109
- :archive_enabled, :bzip2_compress, :split_chunks, :encrypt_base64, :mysql_additional_options,
110
- skip_tables: [], archive_paths: [], archive_excludes: []
111
- )
112
- end
113
-
114
- def check_backup_enabled
115
- unless RailsNexus.configuration.backup_enabled
116
- render plain: "Backup management not enabled", status: :forbidden
117
- end
118
- end
119
- end
120
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class BackupJob < ApplicationJob
5
- queue_as :rails_nexus_backups
6
-
7
- # Accept either a BackupConfig record ID or a BackupConfig record
8
- def perform(backup_config_id_or_record)
9
- config = if backup_config_id_or_record.is_a?(RailsNexus::BackupConfig)
10
- backup_config_id_or_record
11
- else
12
- RailsNexus::BackupConfig.find(backup_config_id_or_record)
13
- end
14
-
15
- RailsNexus::BackupService.run(config)
16
- end
17
- end
18
- end
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class Backup < BaseRecord
5
- self.table_name = "rails_nexus_backups"
6
-
7
- validates :config_name, presence: true
8
- validates :status, presence: true, inclusion: { in: %w[running success failed] }
9
- validates :started_at, presence: true
10
-
11
- scope :recent, ->(hours = 24) { where("started_at >= ?", hours.hours.ago) }
12
- scope :successful, -> { where(status: "success") }
13
- scope :failed, -> { where(status: "failed") }
14
- scope :running, -> { where(status: "running") }
15
- scope :by_config, ->(name) { where(config_name: name) }
16
- scope :latest_first, -> { order(started_at: :desc) }
17
-
18
- # Start tracking a backup run
19
- def self.start!(config_name:, triggered_by: "system")
20
- create!(
21
- config_name: config_name,
22
- status: "running",
23
- triggered_by: triggered_by,
24
- started_at: Time.current
25
- )
26
- end
27
-
28
- # Mark backup as completed
29
- def succeed!(file_path: nil, file_size: nil)
30
- update!(
31
- status: "success",
32
- file_path: file_path,
33
- file_size: file_size,
34
- duration: (Time.current - started_at).round(2),
35
- completed_at: Time.current
36
- )
37
- end
38
-
39
- # Mark backup as failed
40
- def fail!(error_message:)
41
- update!(
42
- status: "failed",
43
- error_message: error_message,
44
- duration: (Time.current - started_at).round(2),
45
- completed_at: Time.current
46
- )
47
- end
48
-
49
- # Get success rate for a model over the last N days
50
- def self.success_rate(config_name:, days: 7)
51
- records = by_config(config_name).where("started_at >= ?", days.days.ago).where.not(status: "running")
52
- return 0 if records.empty?
53
- (records.successful.count.to_f / records.count * 100).round(1)
54
- end
55
-
56
- # Get backup health summary
57
- def self.health_summary(alert_threshold_hours: 24)
58
- configs = RailsNexus::BackupConfig.pluck(:name)
59
- results = configs.map do |config_name|
60
- latest = by_config(config_name).successful.latest_first.first
61
- age_hours = latest ? ((Time.current - latest.started_at) / 3600).round(1) : nil
62
-
63
- {
64
- config_name: config_name,
65
- last_backup: latest&.started_at,
66
- age_hours: age_hours,
67
- status: age_hours.nil? ? "unknown" : (age_hours < alert_threshold_hours ? "healthy" : "stale"),
68
- success_rate: success_rate(config_name: config_name),
69
- last_file_size: latest&.file_size
70
- }
71
- end
72
-
73
- {
74
- healthy: results.all? { |r| r[:status] == "healthy" },
75
- models: results,
76
- alerts: results.select { |r| r[:status] == "stale" || r[:status] == "unknown" }
77
- }
78
- end
79
-
80
- # Cleanup old backup records
81
- def self.cleanup!(retention_days: 30)
82
- where("started_at < ?", retention_days.days.ago).delete_all
83
- end
84
-
85
- # Format file size
86
- def file_size_human
87
- return "—" unless file_size
88
- units = ["B", "KB", "MB", "GB"]
89
- size = file_size.to_f
90
- units.each do |unit|
91
- return "#{size.round(1)} #{unit}" if size < 1024
92
- size /= 1024
93
- end
94
- "#{size.round(1)} TB"
95
- end
96
-
97
- # Duration formatted
98
- def duration_human
99
- return "—" unless duration
100
- if duration < 60
101
- "#{duration.round(1)}s"
102
- else
103
- "#{(duration / 60).floor}m #{(duration % 60).round}s"
104
- end
105
- end
106
- end
107
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class BackupConfig < BaseRecord
5
- self.table_name = "rails_nexus_backup_configs"
6
-
7
- ADAPTERS = %w[mysql postgresql sqlite mongodb redis].freeze
8
-
9
- # ─── Boolean defaults ──────────────────────────────────────────
10
- # Rails doesn't apply DB defaults to new in-memory records, so
11
- # check_box helpers in form_for render unchecked. These declarations
12
- # ensure the form reflects the intended defaults.
13
- attribute :enabled, :boolean, default: true
14
- attribute :compress, :boolean, default: true
15
- attribute :encrypted, :boolean, default: false
16
- attribute :encrypt_base64, :boolean, default: false
17
- attribute :rsync_enabled, :boolean, default: false
18
- attribute :rsync_mirror, :boolean, default: false
19
- attribute :notify_on_success, :boolean, default: true
20
- attribute :notify_on_failure, :boolean, default: true
21
- attribute :s3_enabled, :boolean, default: false
22
- attribute :gpg_enabled, :boolean, default: false
23
- attribute :email_notify, :boolean, default: false
24
- attribute :archive_enabled, :boolean, default: false
25
- attribute :bzip2_compress, :boolean, default: false
26
- attribute :split_chunks, :boolean, default: false
27
-
28
- validates :name, presence: true, uniqueness: true
29
- validates :database_name, presence: true, if: -> { adapter != "redis" }
30
- validates :adapter, presence: true, inclusion: { in: ADAPTERS }
31
- validates :storage_path, presence: true
32
- validates :keep_count, numericality: { greater_than: 0 }
33
- validates :encryption_password, presence: true, if: :encrypted?
34
- validates :rsync_host, presence: true, if: :rsync_enabled?
35
- validates :rsync_user, presence: true, if: :rsync_enabled?
36
- validates :s3_bucket, presence: true, if: :s3_enabled?
37
- validates :s3_access_key, presence: true, if: :s3_enabled?
38
- validates :s3_secret_key, presence: true, if: :s3_enabled?
39
- validates :gpg_password, presence: true, if: :gpg_enabled?
40
- validates :email_to, presence: true, if: :email_notify?
41
-
42
- scope :enabled, -> { where(enabled: true) }
43
- scope :disabled, -> { where(enabled: false) }
44
-
45
- serialize :skip_tables, coder: JSON
46
- serialize :archive_paths, coder: JSON
47
- serialize :archive_excludes, coder: JSON
48
-
49
- # ─── Skip Tables ───────────────────────────────────────────────
50
- def skip_tables_list
51
- return [] if skip_tables.blank?
52
- skip_tables.is_a?(Array) ? skip_tables : JSON.parse(skip_tables.to_s) rescue []
53
- end
54
-
55
- def skip_tables_list=(value)
56
- return if value.blank?
57
- self.skip_tables = if value.is_a?(String)
58
- value.split(",").map(&:strip).reject(&:blank?)
59
- else
60
- value
61
- end
62
- end
63
-
64
- # ─── Archive Paths ─────────────────────────────────────────────
65
- def archive_paths_list
66
- return [] if archive_paths.blank?
67
- archive_paths.is_a?(Array) ? archive_paths : JSON.parse(archive_paths.to_s) rescue []
68
- end
69
-
70
- def archive_paths_list=(value)
71
- return if value.blank?
72
- self.archive_paths = if value.is_a?(String)
73
- value.split(",").map(&:strip).reject(&:blank?)
74
- else
75
- value
76
- end
77
- end
78
-
79
- def archive_excludes_list
80
- return [] if archive_excludes.blank?
81
- archive_excludes.is_a?(Array) ? archive_excludes : JSON.parse(archive_excludes.to_s) rescue []
82
- end
83
-
84
- def archive_excludes_list=(value)
85
- return if value.blank?
86
- self.archive_excludes = if value.is_a?(String)
87
- value.split(",").map(&:strip).reject(&:blank?)
88
- else
89
- value
90
- end
91
- end
92
-
93
- # ─── Adapter checks ────────────────────────────────────────────
94
- def mysql?; adapter == "mysql"; end
95
- def postgresql?; adapter == "postgresql"; end
96
- def sqlite?; adapter == "sqlite"; end
97
- def mongodb?; adapter == "mongodb"; end
98
- def redis?; adapter == "redis"; end
99
-
100
- # ─── Path helpers ──────────────────────────────────────────────
101
- def storage_path_expanded
102
- storage_path.gsub("~", Dir.home)
103
- end
104
-
105
- def dump_filename
106
- timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
107
- ext = dump_extension
108
- "#{name}_#{timestamp}.#{ext}"
109
- end
110
-
111
- def dump_filepath
112
- File.join(storage_path_expanded, dump_filename)
113
- end
114
-
115
- # ─── Stats ─────────────────────────────────────────────────────
116
- def recent_backups(limit: 10)
117
- RailsNexus::Backup.where(config_name: name).order(started_at: :desc).limit(limit)
118
- end
119
-
120
- def last_failure
121
- RailsNexus::Backup.where(config_name: name, status: "failed").order(started_at: :desc).first
122
- end
123
-
124
- def stats
125
- backups = RailsNexus::Backup.where(config_name: name)
126
- recent = backups.where("started_at >= ?", 7.days.ago)
127
- {
128
- total: backups.count,
129
- successful: backups.successful.count,
130
- failed: backups.failed.count,
131
- recent_count: recent.count,
132
- recent_success: recent.successful.count,
133
- recent_failed: recent.failed.count,
134
- last_backup: backups.successful.latest_first.first,
135
- total_size: backups.sum(:file_size).to_i,
136
- total_size_human: human_size(backups.sum(:file_size).to_i)
137
- }
138
- end
139
-
140
- private
141
-
142
- def dump_extension
143
- parts = ["sql"]
144
- parts << "gz" if compress? && !bzip2_compress?
145
- parts << "bz2" if bzip2_compress?
146
- parts << "enc" if encrypted?
147
- parts.join(".")
148
- end
149
-
150
- def human_size(bytes)
151
- return "0 B" if bytes.zero?
152
- units = %w[B KB MB GB TB]
153
- exp = (Math.log(bytes) / Math.log(1024)).to_i
154
- exp = units.size - 1 if exp >= units.size
155
- "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
156
- end
157
- end
158
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class BackupRunner
5
- # Run a specific backup config by name
6
- def self.run(name)
7
- config = RailsNexus::BackupConfig.find_by!(name: name)
8
- RailsNexus::BackupService.run(config)
9
- rescue ActiveRecord::RecordNotFound
10
- { success: false, error: "Backup config '#{name}' not found" }
11
- end
12
-
13
- # Run all enabled backup configs
14
- def self.run_all
15
- results = []
16
- RailsNexus::BackupConfig.enabled.find_each do |config|
17
- results << RailsNexus::BackupService.run(config)
18
- end
19
- results
20
- end
21
-
22
- # Run backups matching a cron schedule
23
- def self.run_scheduled
24
- now = Time.current
25
- results = []
26
-
27
- RailsNexus::BackupConfig.enabled.where.not(schedule_cron: [nil, ""]).find_each do |config|
28
- if matches_cron?(config.schedule_cron, now)
29
- results << RailsNexus::BackupService.run(config)
30
- end
31
- end
32
-
33
- results
34
- end
35
-
36
- # Check if a cron expression matches the current time
37
- def self.matches_cron?(cron_expression, time)
38
- return false if cron_expression.blank?
39
-
40
- parts = cron_expression.strip.split(/\s+/)
41
- return false unless parts.size == 5
42
-
43
- minute, hour, day, month, wday = parts
44
-
45
- match_field(minute, time.min) &&
46
- match_field(hour, time.hour) &&
47
- match_field(day, time.day) &&
48
- match_field(month, time.mon) &&
49
- match_field(wday, time.wday)
50
- end
51
-
52
- def self.match_field(pattern, value)
53
- return true if pattern == "*"
54
- return true if pattern.include?(",") && pattern.split(",").map(&:strip).include?(value.to_s)
55
- return true if pattern.include?("-")
56
- return true if pattern.include?("/")
57
-
58
- pattern.to_i == value
59
- end
60
- private_class_method :match_field
61
- end
62
- end