rails_nexus 2.0.0 → 2.0.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/rails_nexus/backup_controller.rb +74 -53
  3. data/app/models/rails_nexus/backup_config.rb +90 -0
  4. data/app/services/rails_nexus/backup_runner.rb +62 -0
  5. data/app/services/rails_nexus/backup_service.rb +215 -214
  6. data/app/views/layouts/rails_nexus/application.html.erb +8 -0
  7. data/app/views/rails_nexus/backup/_form.html.erb +185 -0
  8. data/app/views/rails_nexus/backup/config_history.html.erb +67 -0
  9. data/app/views/rails_nexus/backup/edit.html.erb +15 -0
  10. data/app/views/rails_nexus/backup/history.html.erb +60 -0
  11. data/app/views/rails_nexus/backup/index.html.erb +91 -147
  12. data/app/views/rails_nexus/backup/new.html.erb +15 -0
  13. data/config/routes.rb +9 -6
  14. data/lib/generators/rails_nexus/install_generator.rb +1 -13
  15. data/lib/generators/rails_nexus/templates/migration.rb +119 -11
  16. data/lib/generators/rails_nexus/templates/rails_nexus.rb +1 -1
  17. data/lib/rails_nexus/version.rb +1 -1
  18. data/lib/tasks/rails_nexus.rake +71 -0
  19. metadata +8 -16
  20. data/app/models/rails_nexus/database_stat.rb +0 -128
  21. data/app/models/rails_nexus/event.rb +0 -101
  22. data/app/models/rails_nexus/metric.rb +0 -107
  23. data/app/models/rails_nexus/nginx_metric.rb +0 -112
  24. data/app/models/rails_nexus/server_metric.rb +0 -168
  25. data/app/views/rails_nexus/backup/files.html.erb +0 -53
  26. data/app/views/rails_nexus/backup/settings.html.erb +0 -158
  27. data/config/initializers/rails_nexus.rb +0 -18
  28. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +0 -22
  29. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +0 -30
  30. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +0 -22
  31. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +0 -120
  32. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +0 -11
  33. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +0 -21
  34. data/lib/generators/rails_nexus/templates/migration_workflow.rb +0 -29
@@ -1,107 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class Metric < BaseRecord
5
- self.table_name = "rails_nexus_metrics"
6
-
7
- validates :metric_type, presence: true
8
- validates :value, presence: true
9
- validates :recorded_at, presence: true
10
-
11
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
12
- scope :by_type, ->(type) { where(metric_type: type) }
13
- scope :cpu, -> { by_type("cpu") }
14
- scope :memory, -> { by_type("memory") }
15
- scope :threads, -> { by_type("threads") }
16
- scope :db_pool, -> { by_type("db_pool") }
17
- scope :disk, -> { by_type("disk") }
18
-
19
- # Collect current system metrics
20
- def self.collect!
21
- now = Time.current
22
-
23
- # CPU usage
24
- cpu_usage = detect_cpu_usage
25
- create!(metric_type: "cpu", value: cpu_usage, unit: "%", recorded_at: now) if cpu_usage
26
-
27
- # Memory
28
- mem = detect_memory_usage
29
- if mem
30
- create!(metric_type: "memory_total", value: mem[:total], unit: "mb", recorded_at: now)
31
- create!(metric_type: "memory_used", value: mem[:used], unit: "mb", recorded_at: now)
32
- create!(metric_type: "memory_free", value: mem[:free], unit: "mb", recorded_at: now)
33
- end
34
-
35
- # Threads
36
- create!(metric_type: "threads", value: Thread.list.size, unit: "count", recorded_at: now)
37
-
38
- # DB pool
39
- db_pool = detect_db_pool
40
- if db_pool
41
- create!(metric_type: "db_pool_size", value: db_pool[:size], unit: "count", recorded_at: now)
42
- create!(metric_type: "db_pool_busy", value: db_pool[:busy], unit: "count", recorded_at: now)
43
- create!(metric_type: "db_pool_idle", value: db_pool[:idle], unit: "count", recorded_at: now)
44
- end
45
-
46
- # GC stats
47
- gc = GC.stat
48
- create!(metric_type: "gc_count", value: gc[:count].to_f, unit: "count", recorded_at: now)
49
- create!(metric_type: "gc_heap_allocated", value: (gc[:heap_allocated_objects].to_f / 1_000_000), unit: "m", recorded_at: now)
50
- end
51
-
52
- # Get metric as a time series for charts
53
- def self.time_series(type, hours: 24, interval: 15)
54
- by_type(type)
55
- .where("recorded_at >= ?", hours.hours.ago)
56
- .order(:recorded_at)
57
- .group_by { |m| (m.recorded_at.to_i / (interval * 60)) * (interval * 60) }
58
- .map { |ts, records| { time: Time.at(ts), value: records.map(&:value).sum / records.size.to_f } }
59
- end
60
-
61
- # Cleanup old metrics
62
- def self.cleanup!(retention_hours: 168)
63
- where("recorded_at < ?", retention_hours.hours.ago).delete_all
64
- end
65
-
66
- private
67
-
68
- def self.detect_cpu_usage
69
- # Linux: read /proc/stat
70
- if File.exist?("/proc/stat")
71
- line = File.readlines("/proc/stat").first
72
- values = line.split[1..].map(&:to_f)
73
- idle = values[3]
74
- total = values.sum
75
- # Approximate — real implementation needs two readings
76
- ((total - idle) / total * 100).round(1)
77
- end
78
- rescue StandardError
79
- nil
80
- end
81
-
82
- def self.detect_memory_usage
83
- if File.exist?("/proc/meminfo")
84
- info = {}
85
- File.readlines("/proc/meminfo").each do |line|
86
- key, val = line.split(":")
87
- info[key.strip] = val.strip.split.first.to_i if key && val
88
- end
89
- total = (info["MemTotal"] || 0) / 1024
90
- free = (info["MemAvailable"] || info["MemFree"] || 0) / 1024
91
- used = total - free
92
- { total: total.round, used: used.round, free: free.round }
93
- end
94
- rescue StandardError
95
- nil
96
- end
97
-
98
- def self.detect_db_pool
99
- if ActiveRecord::Base.connection_pool
100
- pool = ActiveRecord::Base.connection_pool
101
- { size: pool.size, busy: pool.connections.size, idle: pool.size - pool.connections.size }
102
- end
103
- rescue StandardError
104
- nil
105
- end
106
- end
107
- end
@@ -1,112 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class NginxMetric < BaseRecord
5
- self.table_name = "rails_nexus_nginx_metrics"
6
-
7
- validates :status_code, presence: true
8
- validates :recorded_at, presence: true
9
-
10
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
11
- scope :by_status, ->(code) { where(status_code: code) }
12
- scope :errors, -> { where("status_code >= 400") }
13
- scope :by_path, ->(path) { where(request_path: path) }
14
-
15
- # Parse nginx access log file
16
- def self.parse_log!(log_path, limit: 1000)
17
- return unless File.exist?(log_path)
18
- return unless File.readable?(log_path)
19
-
20
- entries = []
21
- File.foreach(log_path).last(limit).each do |line|
22
- entry = parse_log_line(line)
23
- entries << entry if entry
24
- end
25
-
26
- insert_all(entries) if entries.any?
27
- entries.size
28
- end
29
-
30
- # Get status code distribution
31
- def self.status_distribution(hours: 24)
32
- recent(hours)
33
- .group(:status_code)
34
- .order("count_all DESC")
35
- .count
36
- end
37
-
38
- # Get top slow endpoints
39
- def self.slow_endpoints(hours: 24, limit: 10)
40
- recent(hours)
41
- .where.not(response_time: nil)
42
- .group(:request_path)
43
- .order("AVG(response_time) DESC")
44
- .limit(limit)
45
- .average(:response_time)
46
- .map { |path, avg_time| { path: path, avg_response_time: avg_time&.round(3) } }
47
- end
48
-
49
- # Get requests per minute over time
50
- def self.requests_over_time(hours: 24, interval_minutes: 5)
51
- recent(hours)
52
- .order(:recorded_at)
53
- .group_by { |r| (r.recorded_at.to_i / (interval_minutes * 60)) * (interval_minutes * 60) }
54
- .map do |ts, records|
55
- {
56
- time: Time.at(ts),
57
- total: records.size,
58
- errors: records.count { |r| r.status_code >= 400 },
59
- avg_response_time: records.filter_map(&:response_time).then { |t| t.any? ? (t.sum / t.size).round(3) : nil }
60
- }
61
- end
62
- end
63
-
64
- # Get error rate
65
- def self.error_rate(hours: 24)
66
- total = recent(hours).count
67
- return 0 if total.zero?
68
- errors = recent(hours).errors.count
69
- (errors.to_f / total * 100).round(2)
70
- end
71
-
72
- # Cleanup old metrics
73
- def self.cleanup!(retention_days: 7)
74
- where("recorded_at < ?", retention_days.days.ago).delete_all
75
- end
76
-
77
- private
78
-
79
- # Parse a single nginx log line (combined format)
80
- # Format: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_time $upstream_response_time
81
- def self.parse_log_line(line)
82
- pattern = /^(\S+) \S+ (\S+) \[([^\]]+)\] "(\S+) (\S+)(?:\s+\S+)?" (\d{3}) (\d+|-) "([^"]*)" "([^"]*)" ?(\d+\.?\d*)? ?(\d+\.?\d*)?$/
83
-
84
- match = line.match(pattern)
85
- return unless match
86
-
87
- status_code = match[6].to_i
88
- response_time = match[10]&.to_f
89
- upstream_time = match[11]&.to_f
90
-
91
- {
92
- status_code: status_code,
93
- request_method: match[4],
94
- request_path: match[5],
95
- response_time: response_time,
96
- upstream_time: upstream_time,
97
- remote_addr: match[1],
98
- user_agent: match[9],
99
- referer: match[8] != "-" ? match[8] : nil,
100
- recorded_at: parse_nginx_time(match[3])
101
- }
102
- rescue StandardError
103
- nil
104
- end
105
-
106
- def self.parse_nginx_time(time_str)
107
- Time.parse(time_str)
108
- rescue StandardError
109
- Time.current
110
- end
111
- end
112
- end
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsNexus
4
- class ServerMetric < BaseRecord
5
- self.table_name = "rails_nexus_server_metrics"
6
-
7
- validates :recorded_at, presence: true
8
-
9
- scope :recent, ->(hours = 24) { where("recorded_at >= ?", hours.hours.ago) }
10
- scope :latest, -> { order(recorded_at: :desc).first }
11
-
12
- # Collect current server metrics
13
- def self.collect!
14
- now = Time.current
15
- data = {
16
- hostname: Socket.gethostname,
17
- ruby_version: RUBY_VERSION,
18
- rails_version: defined?(Rails) ? Rails::VERSION::STRING : nil,
19
- os_info: detect_os,
20
- recorded_at: now
21
- }
22
-
23
- # CPU
24
- cpu = detect_cpu
25
- data.merge!(cpu) if cpu
26
-
27
- # Memory
28
- mem = detect_memory
29
- data.merge!(mem) if mem
30
-
31
- # Load averages
32
- load_avg = detect_load_average
33
- data.merge!(load_avg) if load_avg
34
-
35
- # Disk
36
- disk = detect_disk
37
- data.merge!(disk) if disk
38
-
39
- # Processes
40
- procs = detect_processes
41
- data.merge!(procs) if procs
42
-
43
- # Uptime
44
- data[:uptime_seconds] = detect_uptime
45
-
46
- create!(data)
47
- end
48
-
49
- # Get latest metrics
50
- def self.current
51
- latest || collect!
52
- end
53
-
54
- # Get metric trend over time
55
- def self.trend(field, hours: 24)
56
- where("recorded_at >= ?", hours.hours.ago)
57
- .order(:recorded_at)
58
- .pluck(:recorded_at, field.to_sym)
59
- .map { |time, value| { time: time, value: value&.to_f } }
60
- end
61
-
62
- # Cleanup old metrics
63
- def self.cleanup!(retention_days: 7)
64
- where("recorded_at < ?", retention_days.days.ago).delete_all
65
- end
66
-
67
- private
68
-
69
- def self.detect_os
70
- if File.exist?("/etc/os-release")
71
- info = {}
72
- File.readlines("/etc/os-release").each do |line|
73
- key, val = line.split("=", 2)
74
- info[key.strip] = val&.strip&.gsub('"', '') if key && val
75
- end
76
- "#{info['PRETTY_NAME'] || info['NAME']} #{info['VERSION']}"
77
- elsif RUBY_PLATFORM =~ /darwin/
78
- `sw_vers -productVersion`.strip
79
- else
80
- RUBY_PLATFORM
81
- end
82
- rescue StandardError
83
- RUBY_PLATFORM
84
- end
85
-
86
- def self.detect_cpu
87
- return unless File.exist?("/proc/cpuinfo")
88
- cores = File.readlines("/proc/cpuinfo").count { |l| l =~ /^processor\s*:/ }
89
- { cpu_cores: cores }
90
- rescue StandardError
91
- nil
92
- end
93
-
94
- def self.detect_memory
95
- return unless File.exist?("/proc/meminfo")
96
- info = {}
97
- File.readlines("/proc/meminfo").each do |line|
98
- key, val = line.split(":")
99
- next unless key && val
100
- kb = val.strip.split.first.to_i
101
- case key.strip
102
- when "MemTotal" then info[:memory_total] = kb / 1024
103
- when "MemAvailable" then info[:memory_free] = kb / 1024
104
- when "MemFree" then info[:memory_free] ||= kb / 1024
105
- when "SwapTotal" then info[:swap_total] = kb / 1024
106
- when "SwapFree" then info[:swap_used] = ((info[:swap_total] || 0) - (kb / 1024))
107
- end
108
- end
109
- info[:memory_used] = (info[:memory_total] || 0) - (info[:memory_free] || 0) if info[:memory_total]
110
- info
111
- rescue StandardError
112
- nil
113
- end
114
-
115
- def self.detect_load_average
116
- if File.exist?("/proc/loadavg")
117
- parts = File.read("/proc/loadavg").split
118
- { load_avg_1m: parts[0].to_f, load_avg_5m: parts[1].to_f, load_avg_15m: parts[2].to_f }
119
- end
120
- rescue StandardError
121
- nil
122
- end
123
-
124
- def self.detect_disk
125
- output = `df -B1 / 2>/dev/null`.lines.last
126
- return unless output
127
- parts = output.split
128
- total = parts[1].to_i
129
- used = parts[2].to_i
130
- { disk_total: total, disk_used: used, disk_usage_percent: total > 0 ? (used.to_f / total * 100).round(1) : 0 }
131
- rescue StandardError
132
- nil
133
- end
134
-
135
- def self.detect_processes
136
- data = {}
137
-
138
- # Puma
139
- if defined?(Puma) && Puma.respond_to?(:stats)
140
- stats = Puma.stats_hash rescue {}
141
- data[:puma_workers] = stats[:workers]&.size || 0
142
- data[:puma_threads] = stats[:workers]&.sum { |w| w[:last_status]&.dig(:max_threads) || 0 } || 0
143
- end
144
-
145
- # Sidekiq
146
- if defined?(Sidekiq)
147
- require "sidekiq/api"
148
- stats = Sidekiq::Stats.new
149
- data[:sidekiq_processed] = stats.processed
150
- data[:sidekiq_failed] = stats.failed
151
- data[:sidekiq_enqueued] = stats.enqueued
152
- data[:sidekiq_workers] = Sidekiq::ProcessSet.new.size rescue 0
153
- end
154
-
155
- data
156
- rescue StandardError
157
- {}
158
- end
159
-
160
- def self.detect_uptime
161
- if File.exist?("/proc/uptime")
162
- File.read("/proc/uptime").split.first.to_f
163
- end
164
- rescue StandardError
165
- nil
166
- end
167
- end
168
- end
@@ -1,53 +0,0 @@
1
- <% page_title t(".title", default: "Backup Files") %>
2
-
3
- <div class="space-y-6">
4
- <%# ─── Header ──────────────────────────────────────────────── %>
5
- <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
- <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
- <span class="rn-breadcrumb-sep">/</span>
8
- <a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
9
- <span class="rn-breadcrumb-sep">/</span>
10
- <span class="rn-breadcrumb-current">Files</span>
11
- </nav>
12
-
13
- <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Files</h1>
14
-
15
- <%# ─── Files Table ─────────────────────────────────────────── %>
16
- <div class="rn-card">
17
- <% if @files.any? %>
18
- <div class="rn-table-wrap">
19
- <table class="rn-table">
20
- <thead>
21
- <tr>
22
- <th>File</th>
23
- <th>Size</th>
24
- <th>Created</th>
25
- <th>Age</th>
26
- <th>Format</th>
27
- </tr>
28
- </thead>
29
- <tbody>
30
- <% @files.each do |file| %>
31
- <tr>
32
- <td>
33
- <span class="rn-mono text-xs" style="color: var(--rn-text)"><%= file[:name] %></span>
34
- </td>
35
- <td><span class="text-sm" style="color: var(--rn-text-secondary)"><%= file[:size_human] %></span></td>
36
- <td><span class="text-xs" style="color: var(--rn-text-muted)"><%= file[:created_at].strftime("%b %d, %Y %I:%M %p") %></span></td>
37
- <td><span class="text-xs" style="color: var(--rn-text-muted)"><%= file[:age_hours] %>h</span></td>
38
- <td>
39
- <span class="rn-badge <%= file[:encrypted] ? 'rn-badge-success' : '' %>"><%= file[:encrypted] ? 'Encrypted' : 'Plain' %></span>
40
- <span class="rn-badge <%= file[:compressed] ? 'rn-badge-info' : '' %>"><%= file[:compressed] ? 'Gzipped' : 'Raw' %></span>
41
- </td>
42
- </tr>
43
- <% end %>
44
- </tbody>
45
- </table>
46
- </div>
47
- <% else %>
48
- <div class="p-8 text-center">
49
- <p class="text-sm" style="color: var(--rn-text-muted)">No backup files found.</p>
50
- </div>
51
- <% end %>
52
- </div>
53
- </div>
@@ -1,158 +0,0 @@
1
- <% page_title t(".title", default: "Backup Settings") %>
2
-
3
- <div class="space-y-6">
4
- <%# ─── Header ──────────────────────────────────────────────── %>
5
- <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
- <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
- <span class="rn-breadcrumb-sep">/</span>
8
- <a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
9
- <span class="rn-breadcrumb-sep">/</span>
10
- <span class="rn-breadcrumb-current">Settings</span>
11
- </nav>
12
-
13
- <div class="flex items-center gap-3">
14
- <svg class="w-6 h-6" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
15
- <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
16
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
17
- </svg>
18
- <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Settings</h1>
19
- </div>
20
-
21
- <%# ─── Settings Form ───────────────────────────────────────── %>
22
- <div class="rn-card">
23
- <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
24
- <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Configuration</h2>
25
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Settings are saved to <code>config/rails_nexus_backup.yml</code></p>
26
- </div>
27
-
28
- <%= form_with url: backup_update_settings_path, method: :patch, class: "p-4 space-y-6" do |f| %>
29
- <%# General %>
30
- <div>
31
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">General</h3>
32
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
33
- <div>
34
- <label class="text-xs font-medium" style="color: var(--rn-text)">Backup Enabled</label>
35
- <%= f.select :enabled, options_for_select([["Yes", true], ["No", false]], @backup_config[:enabled]), {}, class: "rn-input mt-1" %>
36
- </div>
37
- <div>
38
- <label class="text-xs font-medium" style="color: var(--rn-text)">Alert Threshold (hours)</label>
39
- <%= f.number_field :alert_threshold_hours, value: @backup_config[:alert_threshold_hours] || 24, class: "rn-input mt-1" %>
40
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Alert when no backup in this many hours</p>
41
- </div>
42
- </div>
43
- </div>
44
-
45
- <%# Paths %>
46
- <div>
47
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Paths</h3>
48
- <div class="space-y-4">
49
- <div>
50
- <label class="text-xs font-medium" style="color: var(--rn-text)">Backup Config File</label>
51
- <%= f.text_field :config_path, value: @backup_config[:config_path], placeholder: "~/Backup/config.rb", class: "rn-input mt-1" %>
52
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Path to backup gem's config.rb</p>
53
- </div>
54
- <div>
55
- <label class="text-xs font-medium" style="color: var(--rn-text)">Backup Models Directory</label>
56
- <%= f.text_field :models_path, value: @backup_config[:models_path], placeholder: "~/Backup/models", class: "rn-input mt-1" %>
57
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Directory containing backup model files</p>
58
- </div>
59
- <div>
60
- <label class="text-xs font-medium" style="color: var(--rn-text)">Dump Directory</label>
61
- <%= f.text_field :dump_path, value: @backup_config[:dump_path], placeholder: "~/dumps", class: "rn-input mt-1" %>
62
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Where backup files are stored</p>
63
- </div>
64
- </div>
65
- </div>
66
-
67
- <%# Notification %>
68
- <div>
69
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Notification</h3>
70
- <div>
71
- <label class="text-xs font-medium" style="color: var(--rn-text)">Notify Command</label>
72
- <%= f.text_field :notify_command, value: @backup_config[:notify_command], placeholder: "bk-send", class: "rn-input mt-1" %>
73
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Shell command to run on backup success/failure</p>
74
- </div>
75
- </div>
76
-
77
- <%# RSync %>
78
- <div>
79
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">RSync (Remote Sync)</h3>
80
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
81
- <div>
82
- <label class="text-xs font-medium" style="color: var(--rn-text)">Host</label>
83
- <%= f.text_field :rsync_host, value: @backup_config[:rsync_host], placeholder: "192.168.1.100", class: "rn-input mt-1" %>
84
- </div>
85
- <div>
86
- <label class="text-xs font-medium" style="color: var(--rn-text)">Port</label>
87
- <%= f.number_field :rsync_port, value: @backup_config[:rsync_port] || 22, class: "rn-input mt-1" %>
88
- </div>
89
- <div>
90
- <label class="text-xs font-medium" style="color: var(--rn-text)">SSH User</label>
91
- <%= f.text_field :rsync_user, value: @backup_config[:rsync_user], placeholder: "backup", class: "rn-input mt-1" %>
92
- </div>
93
- <div>
94
- <label class="text-xs font-medium" style="color: var(--rn-text)">Remote Path</label>
95
- <%= f.text_field :rsync_path, value: @backup_config[:rsync_path], placeholder: "backups/", class: "rn-input mt-1" %>
96
- </div>
97
- </div>
98
- </div>
99
-
100
- <%# Encryption %>
101
- <div>
102
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Encryption</h3>
103
- <div>
104
- <label class="text-xs font-medium" style="color: var(--rn-text)">Encryption Password</label>
105
- <%= f.password_field :encrypt_password, value: @backup_config[:encrypt_password], placeholder: "Enter password", class: "rn-input mt-1" %>
106
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Used for OpenSSL encryption of backup files</p>
107
- </div>
108
- </div>
109
-
110
- <%# Cleanup %>
111
- <div>
112
- <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Cleanup</h3>
113
- <div>
114
- <label class="text-xs font-medium" style="color: var(--rn-text)">Auto Cleanup (days)</label>
115
- <%= f.number_field :auto_cleanup_days, value: @backup_config[:auto_cleanup_days] || 30, class: "rn-input mt-1" %>
116
- <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Delete backups older than this (0 = never)</p>
117
- </div>
118
- </div>
119
-
120
- <%# Actions %>
121
- <div class="flex items-center gap-3 pt-4" style="border-top: 1px solid var(--rn-border-light)">
122
- <%= f.submit "Save Settings", class: "rn-btn rn-btn-primary" %>
123
- <a href="<%= backup_path %>" class="rn-btn rn-btn-ghost">Cancel</a>
124
- </div>
125
- <% end %>
126
- </div>
127
-
128
- <%# ─── Current Status ──────────────────────────────────────── %>
129
- <div class="rn-card">
130
- <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
131
- <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Current Status</h2>
132
- </div>
133
- <div class="p-4">
134
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
135
- <div>
136
- <span style="color: var(--rn-text-muted)">Config File</span>
137
- <p class="font-medium mt-1" style="color: var(--rn-text)"><%= @backup_config[:config_path] || "Not set" %></p>
138
- <p class="mt-1"><span class="rn-badge <%= File.exist?(@backup_config[:config_path].to_s) ? 'rn-badge-success' : 'rn-badge-danger' %>"><%= File.exist?(@backup_config[:config_path].to_s) ? 'Exists' : 'Missing' %></span></p>
139
- </div>
140
- <div>
141
- <span style="color: var(--rn-text-muted)">Models Dir</span>
142
- <p class="font-medium mt-1" style="color: var(--rn-text)"><%= @backup_config[:models_path] || "Not set" %></p>
143
- <p class="mt-1"><span class="rn-badge <%= Dir.exist?(@backup_config[:models_path].to_s) ? 'rn-badge-success' : 'rn-badge-danger' %>"><%= Dir.exist?(@backup_config[:models_path].to_s) ? 'Exists' : 'Missing' %></span></p>
144
- </div>
145
- <div>
146
- <span style="color: var(--rn-text-muted)">Dump Dir</span>
147
- <p class="font-medium mt-1" style="color: var(--rn-text)"><%= @backup_config[:dump_path] || "Not set" %></p>
148
- <p class="mt-1"><span class="rn-badge <%= Dir.exist?(@backup_config[:dump_path].to_s) ? 'rn-badge-success' : 'rn-badge-danger' %>"><%= Dir.exist?(@backup_config[:dump_path].to_s) ? 'Exists' : 'Missing' %></span></p>
149
- </div>
150
- <div>
151
- <span style="color: var(--rn-text-muted)">Config File</span>
152
- <p class="font-medium mt-1" style="color: var(--rn-text)">config/rails_nexus_backup.yml</p>
153
- <p class="mt-1"><span class="rn-badge <%= File.exist?(Rails.root.join('config', 'rails_nexus_backup.yml')) ? 'rn-badge-success' : 'rn-badge-muted' %>"><%= File.exist?(Rails.root.join('config', 'rails_nexus_backup.yml')) ? 'Saved' : 'Default' %></span></p>
154
- </div>
155
- </div>
156
- </div>
157
- </div>
158
- </div>
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Rails.application.config.to_prepare do
4
- RailsNexus.configure do |config|
5
- # Protect the dashboard - only admin users can access
6
- config.auth_block = lambda do |controller|
7
- controller.current_user&.admin?
8
- end
9
-
10
- # Attach additional data to each exception record
11
- config.exception_data = lambda do |controller|
12
- {
13
- user_id: controller.current_user&.id,
14
- request_id: controller.request.request_id
15
- }
16
- end
17
- end
18
- end
@@ -1,22 +0,0 @@
1
- class CreateRailsNexusLoggedExceptions < ActiveRecord::Migration[8.0]
2
- def change
3
- create_table :rails_nexus_exceptions do |t|
4
- t.string :exception_class
5
- t.string :controller_name
6
- t.string :action_name
7
- t.text :message
8
- t.text :backtrace
9
- t.text :environment
10
- t.text :request
11
- t.string :user_info
12
- t.string :user_agent
13
- t.string :remote_ip
14
-
15
- t.timestamps
16
- end
17
-
18
- add_index :rails_nexus_exceptions, :created_at
19
- add_index :rails_nexus_exceptions, :exception_class
20
- add_index :rails_nexus_exceptions, [:controller_name, :action_name]
21
- end
22
- end
@@ -1,30 +0,0 @@
1
- class AddAdvancedFeaturesToRailsNexusLoggedExceptions < ActiveRecord::Migration[8.0]
2
- def change
3
- # Exception cause chain
4
- add_column :rails_nexus_exceptions, :cause_chain, :text
5
-
6
- # Breadcrumbs (activity trail before crash)
7
- add_column :rails_nexus_exceptions, :breadcrumbs, :text
8
-
9
- # System health snapshot at crash time
10
- add_column :rails_nexus_exceptions, :system_health, :text
11
-
12
- # User impact tracking
13
- add_column :rails_nexus_exceptions, :user_id, :string
14
- add_column :rails_nexus_exceptions, :user_type, :string
15
-
16
- # Storm protection: occurrence count (deduplication)
17
- add_column :rails_nexus_exceptions, :occurrence_count, :integer, default: 1
18
-
19
- # Custom fingerprint for grouping
20
- add_column :rails_nexus_exceptions, :fingerprint, :string
21
-
22
- # Local/instance variables captured at raise time
23
- add_column :rails_nexus_exceptions, :local_variables, :text
24
- add_column :rails_nexus_exceptions, :instance_variables, :text
25
-
26
- add_index :rails_nexus_exceptions, :user_id
27
- add_index :rails_nexus_exceptions, :fingerprint
28
- add_index :rails_nexus_exceptions, :occurrence_count
29
- end
30
- end