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.
- checksums.yaml +4 -4
- data/app/controllers/rails_nexus/backup_controller.rb +74 -53
- data/app/models/rails_nexus/backup_config.rb +90 -0
- data/app/services/rails_nexus/backup_runner.rb +62 -0
- data/app/services/rails_nexus/backup_service.rb +215 -214
- data/app/views/layouts/rails_nexus/application.html.erb +8 -0
- data/app/views/rails_nexus/backup/_form.html.erb +185 -0
- data/app/views/rails_nexus/backup/config_history.html.erb +67 -0
- data/app/views/rails_nexus/backup/edit.html.erb +15 -0
- data/app/views/rails_nexus/backup/history.html.erb +60 -0
- data/app/views/rails_nexus/backup/index.html.erb +91 -147
- data/app/views/rails_nexus/backup/new.html.erb +15 -0
- data/config/routes.rb +9 -6
- data/lib/generators/rails_nexus/install_generator.rb +1 -13
- data/lib/generators/rails_nexus/templates/migration.rb +119 -11
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +1 -1
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +71 -0
- metadata +8 -16
- data/app/models/rails_nexus/database_stat.rb +0 -128
- data/app/models/rails_nexus/event.rb +0 -101
- data/app/models/rails_nexus/metric.rb +0 -107
- data/app/models/rails_nexus/nginx_metric.rb +0 -112
- data/app/models/rails_nexus/server_metric.rb +0 -168
- data/app/views/rails_nexus/backup/files.html.erb +0 -53
- data/app/views/rails_nexus/backup/settings.html.erb +0 -158
- data/config/initializers/rails_nexus.rb +0 -18
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +0 -30
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +0 -120
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +0 -21
- 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
|