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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -0
- data/README.md +68 -13
- data/SECURITY.md +11 -0
- data/THIRD_PARTY_NOTICES.md +14 -0
- data/app/assets/config/rails_nexus_manifest.js +1 -1
- data/app/assets/javascripts/rails_nexus/application.js +55 -0
- data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
- data/app/controllers/rails_nexus/application_controller.rb +6 -2
- data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
- data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
- data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
- data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
- data/app/controllers/rails_nexus/settings_controller.rb +1 -39
- data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
- data/app/controllers/rails_nexus/stats_controller.rb +33 -159
- data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
- data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
- data/app/javascript/rails_nexus/application.js +15 -3
- data/app/models/rails_nexus/logged_exception.rb +27 -13
- data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
- data/app/views/layouts/rails_nexus/application.html.erb +2 -13
- data/app/views/rails_nexus/stats/index.html.erb +1 -102
- data/config/routes.rb +0 -11
- data/lib/generators/rails_nexus/install_generator.rb +1 -1
- data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
- data/lib/generators/rails_nexus/templates/migration.rb +0 -82
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
- data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
- data/lib/rails_nexus/configuration.rb +10 -45
- data/lib/rails_nexus/database_adapter.rb +53 -0
- data/lib/rails_nexus/engine.rb +6 -17
- data/lib/rails_nexus/logger.rb +1 -12
- data/lib/rails_nexus/notifications.rb +27 -20
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/rails_nexus/webhook_client.rb +151 -0
- data/lib/rails_nexus.rb +37 -23
- data/lib/tasks/rails_nexus.rake +0 -80
- data/rails_nexus.gemspec +4 -5
- metadata +12 -41
- data/app/controllers/rails_nexus/backup_controller.rb +0 -120
- data/app/jobs/rails_nexus/backup_job.rb +0 -18
- data/app/models/rails_nexus/backup.rb +0 -107
- data/app/models/rails_nexus/backup_config.rb +0 -158
- data/app/services/rails_nexus/backup_runner.rb +0 -62
- data/app/services/rails_nexus/backup_service.rb +0 -682
- data/app/views/rails_nexus/backup/_form.html.erb +0 -334
- data/app/views/rails_nexus/backup/config_history.html.erb +0 -67
- data/app/views/rails_nexus/backup/edit.html.erb +0 -15
- data/app/views/rails_nexus/backup/history.html.erb +0 -75
- data/app/views/rails_nexus/backup/index.html.erb +0 -112
- data/app/views/rails_nexus/backup/new.html.erb +0 -15
- data/config/importmap.rb +0 -15
- data/db/seeds/backup_configs.rb +0 -140
- data/lib/generators/rails_nexus/backup_generator.rb +0 -70
- data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +0 -152
- data/lib/generators/rails_nexus/templates/backup/config.rb +0 -27
- data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +0 -24
- data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +0 -35
- data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +0 -43
- data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +0 -15
- data/lib/generators/rails_nexus/templates/backup/schedule.rb +0 -28
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "uri"
|
|
5
3
|
require "json"
|
|
4
|
+
require "rails_nexus/webhook_client"
|
|
6
5
|
|
|
7
6
|
module RailsNexus
|
|
8
7
|
module Notifications
|
|
@@ -28,28 +27,36 @@ module RailsNexus
|
|
|
28
27
|
config.webhooks.each do |url|
|
|
29
28
|
deliver_webhook(url, payload, config)
|
|
30
29
|
rescue StandardError => e
|
|
31
|
-
Rails.logger.error("[RailsNexus] Webhook failed
|
|
32
|
-
log_webhook_delivery(
|
|
30
|
+
Rails.logger.error("[RailsNexus] Webhook delivery failed: #{e.class}")
|
|
31
|
+
log_webhook_delivery(
|
|
32
|
+
url: RailsNexus::WebhookClient.redacted_url(url),
|
|
33
|
+
payload: payload,
|
|
34
|
+
error: "Webhook delivery failed",
|
|
35
|
+
event_type: "exception.logged"
|
|
36
|
+
)
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def deliver_webhook(url, payload, config)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
logged_url = RailsNexus::WebhookClient.redacted_url(url)
|
|
42
|
+
result = RailsNexus::WebhookClient.deliver(
|
|
43
|
+
url: url,
|
|
44
|
+
payload: payload,
|
|
45
|
+
headers: config.webhook_headers,
|
|
46
|
+
timeout: config.webhook_timeout
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if result[:success]
|
|
50
|
+
log_webhook_delivery(
|
|
51
|
+
url: logged_url,
|
|
52
|
+
payload: payload,
|
|
53
|
+
status_code: result[:status_code],
|
|
54
|
+
response_body: result[:body],
|
|
55
|
+
event_type: "exception.logged"
|
|
56
|
+
)
|
|
57
|
+
else
|
|
58
|
+
Rails.logger.error("[RailsNexus] Webhook delivery failed: #{result[:error]}")
|
|
59
|
+
log_webhook_delivery(url: logged_url, payload: payload, error: result[:error], event_type: "exception.logged")
|
|
53
60
|
end
|
|
54
61
|
end
|
|
55
62
|
|
data/lib/rails_nexus/version.rb
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "socket"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module RailsNexus
|
|
11
|
+
class WebhookClient
|
|
12
|
+
class ValidationError < StandardError; end
|
|
13
|
+
|
|
14
|
+
BLOCKED_NETWORKS = %w[
|
|
15
|
+
0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16
|
|
16
|
+
172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 192.168.0.0/16
|
|
17
|
+
198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
|
|
18
|
+
::/128 ::1/128 ::ffff:0:0/96 100::/64 2001:db8::/32 fc00::/7 fe80::/10 ff00::/8
|
|
19
|
+
].map { |network| IPAddr.new(network) }.freeze
|
|
20
|
+
|
|
21
|
+
def self.deliver(url:, payload:, headers: {}, timeout: nil)
|
|
22
|
+
new(url: url, payload: payload, headers: headers, timeout: timeout).deliver
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.redacted_url(url)
|
|
26
|
+
uri = URI.parse(url.to_s)
|
|
27
|
+
return "invalid-webhook-url" if uri.host.blank?
|
|
28
|
+
|
|
29
|
+
port = uri.port && uri.port != uri.default_port ? ":#{uri.port}" : ""
|
|
30
|
+
"#{uri.scheme}://#{uri.host}#{port}/[REDACTED]"
|
|
31
|
+
rescue URI::InvalidURIError
|
|
32
|
+
"invalid-webhook-url"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(url:, payload:, headers: {}, timeout: nil, resolver: nil, http_factory: nil)
|
|
36
|
+
@url = url.to_s
|
|
37
|
+
@payload = payload
|
|
38
|
+
@headers = headers || {}
|
|
39
|
+
@timeout = Integer(timeout || RailsNexus.configuration.webhook_timeout || 5)
|
|
40
|
+
@resolver = resolver || method(:resolve_addresses)
|
|
41
|
+
@http_factory = http_factory || ->(host, port) { Net::HTTP.new(host, port) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def deliver
|
|
45
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
|
+
uri, address = validated_destination
|
|
47
|
+
http = build_http(uri, address)
|
|
48
|
+
response = http.request(build_request(uri))
|
|
49
|
+
status = response.code.to_i
|
|
50
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
51
|
+
|
|
52
|
+
if status.between?(300, 399)
|
|
53
|
+
return failure("Webhook redirects are not allowed", status_code: response.code, duration: duration)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
success: status.between?(200, 299),
|
|
58
|
+
status_code: response.code,
|
|
59
|
+
body: response.body.to_s.truncate(500),
|
|
60
|
+
duration: duration.round(3),
|
|
61
|
+
error: status.between?(200, 299) ? nil : "Webhook returned HTTP #{status}"
|
|
62
|
+
}
|
|
63
|
+
rescue ValidationError => error
|
|
64
|
+
failure(error.message)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
failure("Webhook delivery failed")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def validated_destination
|
|
72
|
+
uri = URI.parse(@url)
|
|
73
|
+
allowed_schemes = ["https"]
|
|
74
|
+
if Rails.env.development? && RailsNexus.configuration.webhook_allow_http_in_development
|
|
75
|
+
allowed_schemes << "http"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
raise ValidationError, "Webhook URL must use HTTPS" unless allowed_schemes.include?(uri.scheme)
|
|
79
|
+
raise ValidationError, "Webhook URL must include a hostname" if uri.host.blank?
|
|
80
|
+
raise ValidationError, "Webhook URL credentials are not allowed" if uri.userinfo.present?
|
|
81
|
+
validate_host_policy!(uri.host)
|
|
82
|
+
|
|
83
|
+
addresses = Array(@resolver.call(uri.host)).map { |address| IPAddr.new(address.to_s) }.uniq
|
|
84
|
+
raise ValidationError, "Webhook host could not be resolved" if addresses.empty?
|
|
85
|
+
raise ValidationError, "Webhook destination is not publicly routable" if addresses.any? { |address| blocked_address?(address) }
|
|
86
|
+
|
|
87
|
+
[uri, addresses.first.to_s]
|
|
88
|
+
rescue URI::InvalidURIError, IPAddr::InvalidAddressError
|
|
89
|
+
raise ValidationError, "Webhook URL is invalid"
|
|
90
|
+
rescue SocketError
|
|
91
|
+
raise ValidationError, "Webhook host could not be resolved"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def resolve_addresses(host)
|
|
95
|
+
Addrinfo.getaddrinfo(host, nil, :UNSPEC, :STREAM).map(&:ip_address)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blocked_address?(address)
|
|
99
|
+
BLOCKED_NETWORKS.any? { |network| network.include?(address) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def validate_host_policy!(host)
|
|
103
|
+
denied = Array(RailsNexus.configuration.webhook_denied_hosts)
|
|
104
|
+
allowed = Array(RailsNexus.configuration.webhook_allowed_hosts)
|
|
105
|
+
|
|
106
|
+
raise ValidationError, "Webhook hostname is denied" if denied.any? { |pattern| host_matches?(host, pattern) }
|
|
107
|
+
if allowed.any? && allowed.none? { |pattern| host_matches?(host, pattern) }
|
|
108
|
+
raise ValidationError, "Webhook hostname is not allowed"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def host_matches?(host, pattern)
|
|
113
|
+
candidate = pattern.to_s.downcase
|
|
114
|
+
actual = host.to_s.downcase
|
|
115
|
+
return actual.end_with?(candidate.delete_prefix("*")) if candidate.start_with?("*.")
|
|
116
|
+
actual == candidate
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_http(uri, address)
|
|
120
|
+
http = @http_factory.call(uri.host, uri.port)
|
|
121
|
+
http.ipaddr = address
|
|
122
|
+
http.use_ssl = uri.scheme == "https"
|
|
123
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
|
|
124
|
+
http.open_timeout = @timeout
|
|
125
|
+
http.read_timeout = @timeout
|
|
126
|
+
http
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_request(uri)
|
|
130
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
131
|
+
request["Host"] = uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
|
|
132
|
+
request["Content-Type"] = "application/json"
|
|
133
|
+
@headers.each do |name, value|
|
|
134
|
+
validate_header!(name, value)
|
|
135
|
+
request[name.to_s] = value.to_s
|
|
136
|
+
end
|
|
137
|
+
request.body = @payload.to_json
|
|
138
|
+
request
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_header!(name, value)
|
|
142
|
+
if name.to_s.match?(/[\r\n]/) || value.to_s.match?(/[\r\n]/)
|
|
143
|
+
raise ValidationError, "Webhook header is invalid"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def failure(message, status_code: nil, duration: nil)
|
|
148
|
+
{ success: false, error: message, status_code: status_code, duration: duration&.round(3) }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/rails_nexus.rb
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
require "rails"
|
|
2
|
-
require "
|
|
2
|
+
require "active_support/parameter_filter"
|
|
3
3
|
require "turbo-rails"
|
|
4
4
|
require "stimulus-rails"
|
|
5
5
|
require "pagy"
|
|
6
6
|
require "ipaddr"
|
|
7
7
|
require "socket"
|
|
8
|
+
require "open3"
|
|
8
9
|
require "rails_nexus/version"
|
|
9
10
|
require "rails_nexus/configuration"
|
|
10
11
|
require "rails_nexus/logger"
|
|
12
|
+
require "rails_nexus/database_adapter"
|
|
11
13
|
require "rails_nexus/engine"
|
|
12
14
|
|
|
13
15
|
module RailsNexus
|
|
16
|
+
DEFAULT_FILTER_PARAMETERS = %i[
|
|
17
|
+
password passw passwd secret token api_key apikey authorization cookie session
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
14
20
|
class << self
|
|
15
21
|
attr_writer :configuration
|
|
16
22
|
|
|
@@ -35,6 +41,24 @@ module RailsNexus
|
|
|
35
41
|
def logger
|
|
36
42
|
RailsNexus::Logger
|
|
37
43
|
end
|
|
44
|
+
|
|
45
|
+
def parameter_filter
|
|
46
|
+
configured = defined?(::Rails) ? ::Rails.application.config.filter_parameters : []
|
|
47
|
+
ActiveSupport::ParameterFilter.new(DEFAULT_FILTER_PARAMETERS + Array(configured))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def filter_sensitive_data(value)
|
|
51
|
+
value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
|
|
52
|
+
|
|
53
|
+
case value
|
|
54
|
+
when Hash
|
|
55
|
+
parameter_filter.filter(value)
|
|
56
|
+
when Array
|
|
57
|
+
value.map { |entry| filter_sensitive_data(entry) }
|
|
58
|
+
else
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
38
62
|
end
|
|
39
63
|
|
|
40
64
|
# Copyright (c) 2005 Jamis Buck
|
|
@@ -78,28 +102,19 @@ module RailsNexus
|
|
|
78
102
|
|
|
79
103
|
# we log the exception and raise it again, for the normal handling.
|
|
80
104
|
def log_exception_handler(exception)
|
|
81
|
-
log_exception(exception)
|
|
105
|
+
log_exception(exception) if exception.is_a?(StandardError)
|
|
82
106
|
raise exception
|
|
83
107
|
end
|
|
84
108
|
|
|
85
109
|
def rescue_action(exception)
|
|
86
110
|
status = response_code_for_rescue(exception)
|
|
87
|
-
log_exception(exception) if status != :not_found
|
|
111
|
+
log_exception(exception) if exception.is_a?(StandardError) && status != :not_found
|
|
88
112
|
super
|
|
89
113
|
end
|
|
90
114
|
|
|
91
|
-
def filter_sub_parameters(params, rails_filter_parameters)
|
|
92
|
-
params.each do |key, value|
|
|
93
|
-
if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
|
|
94
|
-
params[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
|
|
95
|
-
else
|
|
96
|
-
params[key] = filter_sub_parameters(value, rails_filter_parameters)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
params
|
|
100
|
-
end
|
|
101
|
-
|
|
102
115
|
def log_exception(exception)
|
|
116
|
+
return unless exception.is_a?(StandardError)
|
|
117
|
+
|
|
103
118
|
# Support both the legacy class_attribute and the new configuration DSL
|
|
104
119
|
deliverer = if self.class.exception_data
|
|
105
120
|
self.class.exception_data
|
|
@@ -113,15 +128,7 @@ module RailsNexus
|
|
|
113
128
|
when Proc then deliverer.call(self)
|
|
114
129
|
end
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
self.request.parameters.each do |key, value|
|
|
119
|
-
if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
|
|
120
|
-
self.request.parameters[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
|
|
121
|
-
else
|
|
122
|
-
self.request.parameters[key] = filter_sub_parameters(value, rails_filter_parameters)
|
|
123
|
-
end
|
|
124
|
-
end if rails_filter_parameters.any?
|
|
131
|
+
data = RailsNexus.filter_sensitive_data(data || {})
|
|
125
132
|
|
|
126
133
|
# Log with structured logger
|
|
127
134
|
RailsNexus::Logger.error(exception, controller: self, extra: data)
|
|
@@ -131,3 +138,10 @@ module RailsNexus
|
|
|
131
138
|
end
|
|
132
139
|
end
|
|
133
140
|
end
|
|
141
|
+
|
|
142
|
+
# Compatibility for applications that integrated the exception concern under
|
|
143
|
+
# the pre-RailsNexus namespace. New integrations should use
|
|
144
|
+
# RailsNexus::ExceptionLoggable.
|
|
145
|
+
module RailsOps
|
|
146
|
+
ExceptionLoggable = RailsNexus::ExceptionLoggable unless const_defined?(:ExceptionLoggable, false)
|
|
147
|
+
end
|
data/lib/tasks/rails_nexus.rake
CHANGED
|
@@ -228,84 +228,4 @@ namespace :rails_nexus do
|
|
|
228
228
|
puts "✓ Deleted #{count} webhook delivery records older than #{days} days"
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
-
# ─── Backup Tasks ──────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
desc "Run a specific backup by name: rails_nexus:backup[daily_backup]"
|
|
234
|
-
task :backup, [:name] => :environment do |_t, args|
|
|
235
|
-
name = args[:name]
|
|
236
|
-
if name.blank?
|
|
237
|
-
puts "Usage: rake rails_nexus:backup[config_name]"
|
|
238
|
-
puts "Available configs:"
|
|
239
|
-
RailsNexus::BackupConfig.enabled.pluck(:name).each { |n| puts " - #{n}" }
|
|
240
|
-
next
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
result = RailsNexus::BackupRunner.run(name)
|
|
244
|
-
if result[:success]
|
|
245
|
-
puts "✓ Backup "#{name}" completed successfully (#{result[:record].duration_human})"
|
|
246
|
-
else
|
|
247
|
-
puts "✗ Backup "#{name}" failed: #{result[:error]}"
|
|
248
|
-
exit 1
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
desc "Run all enabled backup configs"
|
|
253
|
-
task backup_all: :environment do
|
|
254
|
-
results = RailsNexus::BackupRunner.run_all
|
|
255
|
-
results.each do |r|
|
|
256
|
-
if r[:success]
|
|
257
|
-
puts "✓ #{r[:record].config_name} completed"
|
|
258
|
-
else
|
|
259
|
-
puts "✗ #{r[:record]&.config_name || '?'} failed: #{r[:error]}"
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
desc "Run backups matching current cron schedule"
|
|
265
|
-
task backup_scheduled: :environment do
|
|
266
|
-
results = RailsNexus::BackupRunner.run_scheduled
|
|
267
|
-
if results.empty?
|
|
268
|
-
puts "No backups scheduled for this time"
|
|
269
|
-
else
|
|
270
|
-
results.each do |r|
|
|
271
|
-
if r[:success]
|
|
272
|
-
puts "✓ #{r[:record].config_name} completed"
|
|
273
|
-
else
|
|
274
|
-
puts "✗ #{r[:record]&.config_name || '?'} failed: #{r[:error]}"
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
desc "Clean up old backup records and files (default: 30 days)"
|
|
281
|
-
task :backup_cleanup, [:days] => :environment do |_t, args|
|
|
282
|
-
days = (args[:days] || 30).to_i
|
|
283
|
-
count = RailsNexus::Backup.where("started_at < ?", days.days.ago).delete_all
|
|
284
|
-
puts "✓ Deleted #{count} backup records older than #{days} days"
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
desc "List all backup configs and their status"
|
|
288
|
-
task backup_list: :environment do
|
|
289
|
-
configs = RailsNexus::BackupConfig.order(:name)
|
|
290
|
-
if configs.empty?
|
|
291
|
-
puts "No backup configs found"
|
|
292
|
-
else
|
|
293
|
-
configs.each do |c|
|
|
294
|
-
status = c.enabled? ? "✓" : "✗"
|
|
295
|
-
last = c.recent_backups(limit: 1).first
|
|
296
|
-
last_str = last ? "#{last.status} #{last.started_at&.strftime('%b %d %H:%M')}" : "never"
|
|
297
|
-
puts "#{status} #{c.name.ljust(20)} #{c.adapter.ljust(12)} #{c.database_name.ljust(20)} last: #{last_str}"
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
desc "Seed backup configs from ~/Backup gem models"
|
|
303
|
-
task seed_backups: :environment do
|
|
304
|
-
seed_file = File.join(RailsNexus::Engine.root, "db", "seeds", "backup_configs.rb")
|
|
305
|
-
if File.exist?(seed_file)
|
|
306
|
-
load seed_file
|
|
307
|
-
else
|
|
308
|
-
puts "Seed file not found: #{seed_file}"
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
231
|
end
|
data/rails_nexus.gemspec
CHANGED
|
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.email = ["tamiruhailu@gmail.com"]
|
|
8
8
|
spec.homepage = "https://github.com/tamiru/rails_nexus"
|
|
9
9
|
spec.summary = "The extensible control plane for Rails applications"
|
|
10
|
-
spec.description = "RailsNexus is an extensible operations and administration console for Rails applications. It provides error monitoring,
|
|
10
|
+
spec.description = "RailsNexus is an extensible operations and administration console for Rails applications. It provides error monitoring, cron job tracking, server statistics, database health, real-time analytics, workflow management, source code viewing, N+1 query detection, and storm protection. Built with Hotwire, Stimulus, and Tailwind-compatible CSS."
|
|
11
11
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
|
|
@@ -15,10 +15,12 @@ Gem::Specification.new do |spec|
|
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
16
|
spec.metadata["source_code_uri"] = "https://github.com/tamiru/rails_nexus/tree/main"
|
|
17
17
|
spec.metadata["changelog_uri"] = "https://github.com/tamiru/rails_nexus/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/tamiru/rails_nexus/issues"
|
|
18
19
|
spec.metadata["funding_uri"] = "https://github.com/sponsors/tamiru"
|
|
19
20
|
|
|
20
21
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
21
|
-
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "rails_nexus.gemspec"]
|
|
22
|
+
Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "SECURITY.md", "THIRD_PARTY_NOTICES.md", "MIT-LICENSE", "Rakefile", "README.md", "rails_nexus.gemspec"]
|
|
23
|
+
.reject { |file| file.match?(%r{(?:^|/)(?:\.idea|log|tmp|storage|pkg)(?:/|$)|\.(?:gem|sqlite3|log)\z|Zone\.Identifier\z}) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
spec.required_ruby_version = ">= 3.2.0"
|
|
@@ -26,11 +28,8 @@ Gem::Specification.new do |spec|
|
|
|
26
28
|
spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
|
|
27
29
|
spec.add_dependency "stimulus-rails", "~> 1.3"
|
|
28
30
|
spec.add_dependency "turbo-rails", "~> 2.0"
|
|
29
|
-
spec.add_dependency "importmap-rails", "~> 2.0"
|
|
30
31
|
spec.add_dependency "pagy", "~> 6.5"
|
|
31
32
|
|
|
32
33
|
spec.add_dependency "ransack", "~> 4.0"
|
|
33
34
|
|
|
34
|
-
# Note: Backup management requires 'backup' (~> 4.4) and 'whenever' (~> 1.1)
|
|
35
|
-
# gems to be added to your host application's Gemfile.
|
|
36
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_nexus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tamiru Hailu
|
|
@@ -57,20 +57,6 @@ dependencies:
|
|
|
57
57
|
- - "~>"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '2.0'
|
|
60
|
-
- !ruby/object:Gem::Dependency
|
|
61
|
-
name: importmap-rails
|
|
62
|
-
requirement: !ruby/object:Gem::Requirement
|
|
63
|
-
requirements:
|
|
64
|
-
- - "~>"
|
|
65
|
-
- !ruby/object:Gem::Version
|
|
66
|
-
version: '2.0'
|
|
67
|
-
type: :runtime
|
|
68
|
-
prerelease: false
|
|
69
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
-
requirements:
|
|
71
|
-
- - "~>"
|
|
72
|
-
- !ruby/object:Gem::Version
|
|
73
|
-
version: '2.0'
|
|
74
60
|
- !ruby/object:Gem::Dependency
|
|
75
61
|
name: pagy
|
|
76
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,25 +86,28 @@ dependencies:
|
|
|
100
86
|
- !ruby/object:Gem::Version
|
|
101
87
|
version: '4.0'
|
|
102
88
|
description: RailsNexus is an extensible operations and administration console for
|
|
103
|
-
Rails applications. It provides error monitoring,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
89
|
+
Rails applications. It provides error monitoring, cron job tracking, server statistics,
|
|
90
|
+
database health, real-time analytics, workflow management, source code viewing,
|
|
91
|
+
N+1 query detection, and storm protection. Built with Hotwire, Stimulus, and Tailwind-compatible
|
|
92
|
+
CSS.
|
|
107
93
|
email:
|
|
108
94
|
- tamiruhailu@gmail.com
|
|
109
95
|
executables: []
|
|
110
96
|
extensions: []
|
|
111
97
|
extra_rdoc_files: []
|
|
112
98
|
files:
|
|
99
|
+
- CHANGELOG.md
|
|
113
100
|
- MIT-LICENSE
|
|
114
101
|
- README.md
|
|
115
102
|
- Rakefile
|
|
103
|
+
- SECURITY.md
|
|
104
|
+
- THIRD_PARTY_NOTICES.md
|
|
116
105
|
- app/assets/config/rails_nexus_manifest.js
|
|
106
|
+
- app/assets/javascripts/rails_nexus/application.js
|
|
117
107
|
- app/assets/javascripts/rails_nexus/controllers.js
|
|
118
108
|
- app/assets/stylesheets/rails_nexus/application.css
|
|
119
109
|
- app/controllers/rails_nexus/analytics_controller.rb
|
|
120
110
|
- app/controllers/rails_nexus/application_controller.rb
|
|
121
|
-
- app/controllers/rails_nexus/backup_controller.rb
|
|
122
111
|
- app/controllers/rails_nexus/cron_jobs_controller.rb
|
|
123
112
|
- app/controllers/rails_nexus/database_health_controller.rb
|
|
124
113
|
- app/controllers/rails_nexus/logged_exceptions_controller.rb
|
|
@@ -136,28 +125,17 @@ files:
|
|
|
136
125
|
- app/javascript/controllers/rails_nexus_theme_controller.js
|
|
137
126
|
- app/javascript/rails_nexus/application.js
|
|
138
127
|
- app/jobs/rails_nexus/application_job.rb
|
|
139
|
-
- app/jobs/rails_nexus/backup_job.rb
|
|
140
128
|
- app/mailers/rails_nexus/application_mailer.rb
|
|
141
129
|
- app/models/rails_nexus/application_record.rb
|
|
142
|
-
- app/models/rails_nexus/backup.rb
|
|
143
|
-
- app/models/rails_nexus/backup_config.rb
|
|
144
130
|
- app/models/rails_nexus/base_record.rb
|
|
145
131
|
- app/models/rails_nexus/comment.rb
|
|
146
132
|
- app/models/rails_nexus/cron_job.rb
|
|
147
133
|
- app/models/rails_nexus/logged_exception.rb
|
|
148
134
|
- app/models/rails_nexus/logged_exception_n1.rb
|
|
149
135
|
- app/models/rails_nexus/webhook_delivery.rb
|
|
150
|
-
- app/services/rails_nexus/backup_runner.rb
|
|
151
|
-
- app/services/rails_nexus/backup_service.rb
|
|
152
136
|
- app/views/layouts/rails_nexus/_sidebar_navigation.html.erb
|
|
153
137
|
- app/views/layouts/rails_nexus/application.html.erb
|
|
154
138
|
- app/views/rails_nexus/analytics/index.html.erb
|
|
155
|
-
- app/views/rails_nexus/backup/_form.html.erb
|
|
156
|
-
- app/views/rails_nexus/backup/config_history.html.erb
|
|
157
|
-
- app/views/rails_nexus/backup/edit.html.erb
|
|
158
|
-
- app/views/rails_nexus/backup/history.html.erb
|
|
159
|
-
- app/views/rails_nexus/backup/index.html.erb
|
|
160
|
-
- app/views/rails_nexus/backup/new.html.erb
|
|
161
139
|
- app/views/rails_nexus/cron_jobs/index.html.erb
|
|
162
140
|
- app/views/rails_nexus/database_health/index.html.erb
|
|
163
141
|
- app/views/rails_nexus/logged_exceptions/_comments_list.html.erb
|
|
@@ -178,22 +156,12 @@ files:
|
|
|
178
156
|
- app/views/rails_nexus/n1_patterns/index.html.erb
|
|
179
157
|
- app/views/rails_nexus/settings/index.html.erb
|
|
180
158
|
- app/views/rails_nexus/stats/index.html.erb
|
|
181
|
-
- config/importmap.rb
|
|
182
159
|
- config/initializers/date_formats.rb
|
|
183
160
|
- config/locales/en.yml
|
|
184
161
|
- config/routes.rb
|
|
185
|
-
- db/seeds/backup_configs.rb
|
|
186
|
-
- lib/generators/rails_nexus/backup_generator.rb
|
|
187
162
|
- lib/generators/rails_nexus/customize_generator.rb
|
|
188
163
|
- lib/generators/rails_nexus/install_generator.rb
|
|
189
164
|
- lib/generators/rails_nexus/templates/INSTALL.md
|
|
190
|
-
- lib/generators/rails_nexus/templates/backup/backup_helper.sh
|
|
191
|
-
- lib/generators/rails_nexus/templates/backup/config.rb
|
|
192
|
-
- lib/generators/rails_nexus/templates/backup/models/daily_backup.rb
|
|
193
|
-
- lib/generators/rails_nexus/templates/backup/models/full_backup.rb
|
|
194
|
-
- lib/generators/rails_nexus/templates/backup/models/sync_backup.rb
|
|
195
|
-
- lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf
|
|
196
|
-
- lib/generators/rails_nexus/templates/backup/schedule.rb
|
|
197
165
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js
|
|
198
166
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js
|
|
199
167
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js
|
|
@@ -208,12 +176,14 @@ files:
|
|
|
208
176
|
- lib/rails_nexus/breadcrumbs.rb
|
|
209
177
|
- lib/rails_nexus/cleanup.rb
|
|
210
178
|
- lib/rails_nexus/configuration.rb
|
|
179
|
+
- lib/rails_nexus/database_adapter.rb
|
|
211
180
|
- lib/rails_nexus/engine.rb
|
|
212
181
|
- lib/rails_nexus/logger.rb
|
|
213
182
|
- lib/rails_nexus/notifications.rb
|
|
214
183
|
- lib/rails_nexus/ransack_config.rb
|
|
215
184
|
- lib/rails_nexus/storm_protection.rb
|
|
216
185
|
- lib/rails_nexus/version.rb
|
|
186
|
+
- lib/rails_nexus/webhook_client.rb
|
|
217
187
|
- lib/tasks/rails_nexus.rake
|
|
218
188
|
- lib/tasks/rails_nexus_tasks.rake
|
|
219
189
|
- rails_nexus.gemspec
|
|
@@ -226,6 +196,7 @@ metadata:
|
|
|
226
196
|
homepage_uri: https://github.com/tamiru/rails_nexus
|
|
227
197
|
source_code_uri: https://github.com/tamiru/rails_nexus/tree/main
|
|
228
198
|
changelog_uri: https://github.com/tamiru/rails_nexus/blob/main/CHANGELOG.md
|
|
199
|
+
bug_tracker_uri: https://github.com/tamiru/rails_nexus/issues
|
|
229
200
|
funding_uri: https://github.com/sponsors/tamiru
|
|
230
201
|
rdoc_options: []
|
|
231
202
|
require_paths:
|