rails_nexus 2.0.0

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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module RailsNexus
8
+ module Notifications
9
+ class << self
10
+ # Called after a LoggedException is created.
11
+ # Triggers webhooks and the after_create callback.
12
+ def notify(exception_record)
13
+ config = RailsNexus.configuration
14
+
15
+ # Custom after_create callback
16
+ config.after_create&.call(exception_record)
17
+
18
+ # Webhooks
19
+ notify_webhooks(exception_record) if config.webhooks.any?
20
+ end
21
+
22
+ private
23
+
24
+ def notify_webhooks(exception_record)
25
+ config = RailsNexus.configuration
26
+ payload = build_webhook_payload(exception_record)
27
+
28
+ config.webhooks.each do |url|
29
+ deliver_webhook(url, payload, config)
30
+ rescue StandardError => e
31
+ Rails.logger.error("[RailsNexus] Webhook failed for #{url}: #{e.message}")
32
+ log_webhook_delivery(url: url, payload: payload, error: e.message, event_type: "exception.logged")
33
+ end
34
+ end
35
+
36
+ def deliver_webhook(url, payload, config)
37
+ uri = URI(url)
38
+
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ http.use_ssl = uri.scheme == "https"
41
+ http.open_timeout = config.webhook_timeout
42
+ http.read_timeout = config.webhook_timeout
43
+
44
+ request = Net::HTTP::Post.new(uri)
45
+ request["Content-Type"] = "application/json"
46
+ config.webhook_headers.each { |k, v| request[k] = v }
47
+ request.body = payload.to_json
48
+
49
+ response = http.request(request)
50
+
51
+ unless response.is_a?(Net::HTTPSuccess)
52
+ Rails.logger.error("[RailsNexus] Webhook #{url} returned #{response.code}")
53
+ end
54
+ end
55
+
56
+ def log_webhook_delivery(url:, payload:, status_code: nil, response_body: nil, error: nil, event_type: nil)
57
+ return unless defined?(RailsNexus::WebhookDelivery) && RailsNexus.configuration.log_webhook_deliveries
58
+
59
+ if error.present?
60
+ RailsNexus::WebhookDelivery.log_failure!(
61
+ url: url,
62
+ request_body: payload.to_json,
63
+ error_message: error,
64
+ event_type: event_type
65
+ )
66
+ else
67
+ RailsNexus::WebhookDelivery.log_success!(
68
+ url: url,
69
+ request_body: payload.to_json,
70
+ response_code: status_code,
71
+ response_body: response_body,
72
+ duration: 0,
73
+ event_type: event_type
74
+ )
75
+ end
76
+ rescue StandardError => e
77
+ Rails.logger.error("[RailsNexus] Failed to log webhook delivery: #{e.message}")
78
+ end
79
+
80
+ def build_webhook_payload(exception_record)
81
+ {
82
+ event: "exception.logged",
83
+ timestamp: exception_record.created_at.iso8601,
84
+ exception: {
85
+ id: exception_record.id,
86
+ class: exception_record.exception_class,
87
+ message: exception_record.message&.truncate(500),
88
+ controller: exception_record.controller_name,
89
+ action: exception_record.action_name,
90
+ remote_ip: exception_record.remote_ip,
91
+ user_agent: exception_record.user_agent
92
+ },
93
+ dashboard_url: dashboard_url(exception_record)
94
+ }
95
+ end
96
+
97
+ def dashboard_url(exception_record)
98
+ host = Rails.application.routes.default_url_options[:host] ||
99
+ ENV.fetch("APP_HOST", "localhost:3000")
100
+ protocol = Rails.env.production? ? "https" : "http"
101
+ "#{protocol}://#{host}/rails_nexus/logged_exceptions/#{exception_record.id}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ransack configuration is now defined directly on RailsNexus::LoggedException
4
+ # in app/models/rails_nexus/logged_exception.rb.
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ # Storm Protection: Circuit breaker + adaptive sampling
5
+ # Prevents error floods from crashing the app with DB writes
6
+ module StormProtection
7
+ # Default thresholds
8
+ DEFAULT_PER_PROCESS_THRESHOLD = 50 # errors per second per process
9
+ DEFAULT_GLOBAL_THRESHOLD = 200 # errors per second globally
10
+ DEFAULT_COOLDOWN_SECONDS = 60 # circuit breaker cooldown
11
+ DEFAULT_SAMPLE_RATE_CALM = 1.0 # 100% capture when calm
12
+ DEFAULT_SAMPLE_RATE_STORM = 0.1 # 10% capture during storm
13
+
14
+ class << self
15
+ attr_reader :circuit_open, :error_counts, :last_reset
16
+
17
+ def enabled?
18
+ RailsNexus.configuration.storm_protection_enabled
19
+ end
20
+
21
+ def threshold
22
+ RailsNexus.configuration.storm_threshold_per_second || DEFAULT_PER_PROCESS_THRESHOLD
23
+ end
24
+
25
+ def cooldown
26
+ RailsNexus.configuration.storm_cooldown_seconds || DEFAULT_COOLDOWN_SECONDS
27
+ end
28
+
29
+ # Check if we should capture this error
30
+ # Returns true if allowed, false if shed
31
+ def allow_capture?
32
+ return true unless enabled?
33
+
34
+ reset_counts_if_needed
35
+
36
+ # If circuit is open (storm detected), use sampling
37
+ if circuit_open?
38
+ return should_sample?
39
+ end
40
+
41
+ # Increment count and check threshold
42
+ @error_counts[:total] += 1
43
+ @error_counts[:current_second] += 1
44
+
45
+ if @error_counts[:current_second] >= threshold
46
+ open_circuit!
47
+ return should_sample?
48
+ end
49
+
50
+ true
51
+ end
52
+
53
+ # Record that an error was captured
54
+ def record_captured
55
+ @error_counts[:captured] += 1
56
+ end
57
+
58
+ # Record that an error was shed
59
+ def record_shed
60
+ @error_counts[:shed] += 1
61
+ end
62
+
63
+ # Get current stats
64
+ def stats
65
+ reset_counts_if_needed
66
+ {
67
+ enabled: enabled?,
68
+ circuit_open: circuit_open?,
69
+ errors_this_second: @error_counts[:current_second] || 0,
70
+ threshold: threshold,
71
+ total_errors: @error_counts[:total] || 0,
72
+ total_captured: @error_counts[:captured] || 0,
73
+ total_shed: @error_counts[:shed] || 0,
74
+ last_reset: @last_reset,
75
+ sample_rate: circuit_open? ? DEFAULT_SAMPLE_RATE_STORM : DEFAULT_SAMPLE_RATE_CALM
76
+ }
77
+ end
78
+
79
+ # Reset circuit (manual or after cooldown)
80
+ def reset!
81
+ @circuit_open = false
82
+ @error_counts = { total: 0, current_second: 0, captured: 0, shed: 0 }
83
+ @last_reset = Time.now
84
+ @second_window = Time.now.to_i
85
+ end
86
+
87
+ private
88
+
89
+ def circuit_open?
90
+ return false unless @circuit_open
91
+
92
+ # Auto-reset after cooldown
93
+ if Time.now - @circuit_open_at > cooldown
94
+ reset!
95
+ return false
96
+ end
97
+
98
+ true
99
+ end
100
+
101
+ def open_circuit!
102
+ @circuit_open = true
103
+ @circuit_open_at = Time.now
104
+ Rails.logger.warn("[RailsNexus] Storm protection: circuit opened at #{@error_counts[:current_second]} errors/sec")
105
+ end
106
+
107
+ def reset_counts_if_needed
108
+ current_second = Time.now.to_i
109
+ if current_second != @second_window
110
+ @error_counts[:current_second] = 0
111
+ @second_window = current_second
112
+ end
113
+ end
114
+
115
+ def should_sample?
116
+ # Keep 10% of errors during storm
117
+ rand < DEFAULT_SAMPLE_RATE_STORM
118
+ end
119
+ end
120
+
121
+ # Initialize on load
122
+ reset!
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module RailsNexus
2
+ VERSION = "2.0.0"
3
+ end
@@ -0,0 +1,133 @@
1
+ require "rails"
2
+ require "importmap-rails"
3
+ require "turbo-rails"
4
+ require "stimulus-rails"
5
+ require "pagy"
6
+ require "ipaddr"
7
+ require "socket"
8
+ require "rails_nexus/version"
9
+ require "rails_nexus/configuration"
10
+ require "rails_nexus/logger"
11
+ require "rails_nexus/engine"
12
+
13
+ module RailsNexus
14
+ class << self
15
+ attr_writer :configuration
16
+
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield(configuration)
23
+ end
24
+
25
+ # Backward-compatible accessor for application_name
26
+ def application_name
27
+ configuration.application_name
28
+ end
29
+
30
+ def application_name=(value)
31
+ configuration.application_name = value
32
+ end
33
+
34
+ # Convenience logger accessor
35
+ def logger
36
+ RailsNexus::Logger
37
+ end
38
+ end
39
+
40
+ # Copyright (c) 2005 Jamis Buck
41
+ #
42
+ # Permission is hereby granted, free of charge, to any person obtaining
43
+ # a copy of this software and associated documentation files (the
44
+ # "Software"), to deal in the Software without restriction, including
45
+ # without limitation the rights to use, copy, modify, merge, publish,
46
+ # distribute, sublicense, and/or sell copies of the Software, and to
47
+ # permit persons to whom the Software is furnished to do so, subject to
48
+ # the following conditions:
49
+ #
50
+ # The above copyright notice and this permission notice shall be
51
+ # included in all copies or substantial portions of the Software.
52
+ #
53
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
54
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
55
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
56
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
57
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
58
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
59
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
60
+ module ExceptionLoggable
61
+ def self.included(target)
62
+ target.extend(ClassMethods)
63
+ target.class_attribute :local_addresses, :exception_data
64
+
65
+ target.local_addresses = [IPAddr.new("127.0.0.1")]
66
+ end
67
+
68
+ module ClassMethods
69
+ def consider_local(*args)
70
+ local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
71
+ end
72
+ end
73
+
74
+ def local_request?
75
+ remote = IPAddr.new(request.remote_ip)
76
+ !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
77
+ end
78
+
79
+ # we log the exception and raise it again, for the normal handling.
80
+ def log_exception_handler(exception)
81
+ log_exception(exception)
82
+ raise exception
83
+ end
84
+
85
+ def rescue_action(exception)
86
+ status = response_code_for_rescue(exception)
87
+ log_exception(exception) if status != :not_found
88
+ super
89
+ end
90
+
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
+ def log_exception(exception)
103
+ # Support both the legacy class_attribute and the new configuration DSL
104
+ deliverer = if self.class.exception_data
105
+ self.class.exception_data
106
+ elsif RailsNexus.configuration.exception_data
107
+ RailsNexus.configuration.exception_data
108
+ end
109
+
110
+ data = case deliverer
111
+ when nil then {}
112
+ when Symbol then send(deliverer)
113
+ when Proc then deliverer.call(self)
114
+ end
115
+
116
+ rails_filter_parameters = defined?(::Rails) ? ::Rails.application.config.filter_parameters : []
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?
125
+
126
+ # Log with structured logger
127
+ RailsNexus::Logger.error(exception, controller: self, extra: data)
128
+
129
+ # Create the record
130
+ LoggedException.create_from_exception(self, exception, data)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_nexus do
4
+ desc "Delete exceptions older than the configured retention period"
5
+ task cleanup: :environment do
6
+ require "rails_nexus/cleanup"
7
+ count = RailsNexus::Cleanup.run
8
+ if count
9
+ puts "✓ Cleaned up #{count} old exceptions."
10
+ else
11
+ puts "⚠ No retention_days configured. Set config.retention_days in your initializer."
12
+ end
13
+ end
14
+
15
+ desc "Show rails_nexus statistics"
16
+ task stats: :environment do
17
+ require "rails_nexus/logger"
18
+
19
+ stats = RailsNexus::Logger.stats
20
+
21
+ puts ""
22
+ puts " ⚡ RailsNexus v#{RailsNexus::VERSION}"
23
+ puts " ─────────────────────────────────────"
24
+ puts " Total exceptions: #{stats[:total]}"
25
+ puts ""
26
+
27
+ if stats[:top_exceptions].any?
28
+ puts " Top exceptions:"
29
+ stats[:top_exceptions].each do |klass, count|
30
+ puts " #{klass.ljust(40)} #{count}"
31
+ end
32
+ puts ""
33
+ end
34
+
35
+ if stats[:by_controller].any?
36
+ puts " By controller:"
37
+ stats[:by_controller].sort_by { |_, v| -v }.first(10).each do |ctrl, count|
38
+ puts " #{ctrl.to_s.ljust(40)} #{count}"
39
+ end
40
+ puts ""
41
+ end
42
+ end
43
+
44
+ desc "Tail rails_nexus logs in real-time"
45
+ task tail: :environment do
46
+ log_file = RailsNexus.configuration.log_file
47
+ if log_file.blank?
48
+ puts "⚠ No log_file configured. Set config.log_file in your initializer."
49
+ puts " Example: config.log_file = 'log/rails_nexus.log'"
50
+ next
51
+ end
52
+
53
+ puts "Tailing #{log_file}... (Ctrl+C to stop)"
54
+ puts ""
55
+
56
+ IO.popen(["tail", "-f", log_file]) do |io|
57
+ while (line = io.gets)
58
+ begin
59
+ entry = JSON.parse(line)
60
+ level = entry["level"].to_s.upcase.ljust(5)
61
+ ts = entry["timestamp"].to_s.sub(/T/, " ").sub(/\+.*/, "")
62
+ msg = entry["message"]
63
+ cls = entry["exception_class"]
64
+ ctrl = entry["controller"]
65
+
66
+ line_out = "[#{level}] #{ts}"
67
+ line_out += " #{cls}:" if cls.present?
68
+ line_out += " #{msg}"
69
+ line_out += " [#{ctrl["class"]}##{ctrl["action"]}]" if ctrl.present?
70
+
71
+ puts line_out
72
+ rescue JSON::ParserError
73
+ puts line
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ desc "Export exceptions to JSON"
80
+ task export: :environment do
81
+ require "json"
82
+
83
+ output_file = ENV.fetch("OUTPUT", "rails_nexus_export.json")
84
+ since = ENV["SINCE"] ? Time.parse(ENV["SINCE"]) : nil
85
+
86
+ scope = RailsNexus::LoggedException.order(created_at: :desc)
87
+ scope = scope.where("created_at >= ?", since) if since.present?
88
+
89
+ data = scope.map do |e|
90
+ {
91
+ id: e.id,
92
+ exception_class: e.exception_class,
93
+ controller_name: e.controller_name,
94
+ action_name: e.action_name,
95
+ message: e.message,
96
+ backtrace: e.backtrace,
97
+ environment: e.environment,
98
+ request: e.request,
99
+ user_info: e.user_info,
100
+ remote_ip: e.remote_ip,
101
+ user_agent: e.user_agent,
102
+ created_at: e.created_at.iso8601,
103
+ updated_at: e.updated_at.iso8601
104
+ }
105
+ end
106
+
107
+ File.write(output_file, JSON.pretty_generate(data))
108
+ puts "✓ Exported #{data.size} exceptions to #{output_file}"
109
+ end
110
+
111
+ desc "Test webhook configuration"
112
+ task test_webhook: :environment do
113
+ require "rails_nexus/notifications"
114
+
115
+ config = RailsNexus.configuration
116
+ if config.webhooks.empty?
117
+ puts "⚠ No webhooks configured. Set config.webhooks in your initializer."
118
+ next
119
+ end
120
+
121
+ puts "Testing webhooks..."
122
+ config.webhooks.each do |url|
123
+ print " #{url}... "
124
+ begin
125
+ uri = URI(url)
126
+ http = Net::HTTP.new(uri.host, uri.port)
127
+ http.use_ssl = uri.scheme == "https"
128
+ http.open_timeout = 5
129
+ http.read_timeout = 5
130
+
131
+ request = Net::HTTP::Post.new(uri)
132
+ request["Content-Type"] = "application/json"
133
+ config.webhook_headers.each { |k, v| request[k] = v }
134
+ request.body = { event: "test", message: "RailsNexus webhook test", timestamp: Time.current.iso8601 }.to_json
135
+
136
+ response = http.request(request)
137
+ if response.is_a?(Net::HTTPSuccess)
138
+ puts "✓ (#{response.code})"
139
+ else
140
+ puts "✗ (#{response.code} #{response.message})"
141
+ end
142
+ rescue StandardError => e
143
+ puts "✗ (#{e.message})"
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ namespace :rails_nexus do
150
+ desc "Run a task with cron job tracking"
151
+ task :cron, [:task_name] => :environment do |_t, args|
152
+ task_name = args[:task_name] || "unnamed_task"
153
+ job = nil
154
+
155
+ begin
156
+ job = RailsNexus::CronJob.start!(task_name)
157
+ puts "⚡ Starting cron job: #{task_name} (ID: #{job.id})"
158
+
159
+ job.succeed!(output: "Completed successfully")
160
+ puts "✓ Cron job completed: #{task_name} (#{job.duration.round(2)}s)"
161
+ rescue StandardError => e
162
+ job&.fail!(error: e.message)
163
+ puts "✗ Cron job failed: #{task_name} — #{e.message}"
164
+ raise e
165
+ end
166
+ end
167
+
168
+ desc "Wrap a rake task with RailsNexus cron tracking"
169
+ task :wrap_cron, [:task_name] => :environment do |_t, args|
170
+ task_name = args[:task_name]
171
+ if task_name.blank?
172
+ puts "Usage: rake rails_nexus:wrap_cron[rake:task:name]"
173
+ next
174
+ end
175
+
176
+ job = nil
177
+ begin
178
+ job = RailsNexus::CronJob.start!(task_name)
179
+ puts "⚡ Starting tracked task: #{task_name}"
180
+
181
+ Rake::Task[task_name].invoke
182
+
183
+ job.succeed!(output: "Completed successfully")
184
+ puts "✓ Task completed: #{task_name} (#{job.duration.round(2)}s)"
185
+ rescue StandardError => e
186
+ job&.fail!(error: e.message)
187
+ puts "✗ Task failed: #{task_name} — #{e.message}"
188
+ raise e
189
+ end
190
+ end
191
+
192
+ desc "Show cron job statistics"
193
+ task cron_stats: :environment do
194
+ stats = RailsNexus::CronJob.summary
195
+
196
+ puts ""
197
+ puts " ⏰ Cron Job Statistics"
198
+ puts " ─────────────────────────────────────"
199
+ puts " Total runs: #{stats[:total]}"
200
+ puts " Successful: #{stats[:successful]}"
201
+ puts " Failed: #{stats[:failed]}"
202
+ puts " Running: #{stats[:running]}"
203
+ puts " Failure rate: #{stats[:failure_rate]}%"
204
+ puts ""
205
+
206
+ if stats[:last_run]
207
+ last = stats[:last_run]
208
+ puts " Last run:"
209
+ puts " Name: #{last.name}"
210
+ puts " Status: #{last.status}"
211
+ puts " Time: #{last.created_at}"
212
+ puts " Duration: #{last.duration&.round(2)}s"
213
+ puts ""
214
+ end
215
+ end
216
+
217
+ desc "Clean up old cron job records (default: 30 days)"
218
+ task :cron_cleanup, [:days] => :environment do |_t, args|
219
+ days = (args[:days] || 30).to_i
220
+ count = RailsNexus::CronJob.where("created_at < ?", days.days.ago).delete_all
221
+ puts "✓ Deleted #{count} cron job records older than #{days} days"
222
+ end
223
+
224
+ desc "Clean up old webhook delivery records (default: 30 days)"
225
+ task :webhook_cleanup, [:days] => :environment do |_t, args|
226
+ days = (args[:days] || 30).to_i
227
+ count = RailsNexus::WebhookDelivery.where("created_at < ?", days.days.ago).delete_all
228
+ puts "✓ Deleted #{count} webhook delivery records older than #{days} days"
229
+ end
230
+ end
@@ -0,0 +1,37 @@
1
+ namespace :rails_nexus do
2
+ namespace :tailwind do
3
+ desc "Generate Tailwind v4 @source entries for RailsNexus views/helpers (OUT=app/assets/stylesheets/_rails_nexus_sources.css ENTRY=app/assets/stylesheets/application.tailwind.css)"
4
+ task sources: :environment do
5
+ root = RailsNexus::Engine.root
6
+ out = ENV["OUT"] || "app/assets/stylesheets/_rails_nexus_sources.css"
7
+
8
+ content = <<~CSS
9
+ /* Generated by bin/rails rails_nexus:tailwind:sources - do not edit. Re-run after gem updates or on new machines/containers. */
10
+ @source "#{root}/app/views/**/*.erb";
11
+ @source "#{root}/app/helpers/**/*.rb";
12
+ CSS
13
+
14
+ FileUtils.mkdir_p(File.dirname(out))
15
+ File.write(out, content)
16
+ puts "Wrote #{out}"
17
+
18
+ entry = ENV["ENTRY"]
19
+ entry ||= "app/assets/stylesheets/application.tailwind.css" if File.exist?("app/assets/stylesheets/application.tailwind.css")
20
+
21
+ if entry && File.file?(entry)
22
+ import_line = "@import \"./#{File.basename(out)}\";"
23
+ body = File.read(entry)
24
+
25
+ if body.include?(File.basename(out))
26
+ puts "#{entry} already imports #{File.basename(out)}"
27
+ else
28
+ File.write(entry, body.sub(/\A/, "#{import_line}\n"))
29
+ puts "Added #{import_line} to #{entry}"
30
+ end
31
+ elsif !entry
32
+ puts "Add this to your Tailwind entry stylesheet:"
33
+ puts " @import \"./#{File.basename(out)}\";"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "lib/rails_nexus/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rails_nexus"
5
+ spec.version = RailsNexus::VERSION
6
+ spec.authors = ["Tamiru Hailu"]
7
+ spec.email = ["tamiruhailu@gmail.com"]
8
+ spec.homepage = "https://github.com/tamiru/rails_nexus"
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, backup management, 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
+ spec.metadata["rubygems_mfa_required"] = "true"
12
+ spec.license = "MIT"
13
+
14
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/tamiru/rails_nexus/tree/main"
17
+ spec.metadata["changelog_uri"] = "https://github.com/tamiru/rails_nexus/blob/main/CHANGELOG.md"
18
+ spec.metadata["funding_uri"] = "https://github.com/sponsors/tamiru"
19
+
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "rails_nexus.gemspec"]
22
+ end
23
+
24
+ spec.required_ruby_version = ">= 3.2.0"
25
+
26
+ spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
27
+ spec.add_dependency "stimulus-rails", "~> 1.3"
28
+ spec.add_dependency "turbo-rails", "~> 2.0"
29
+ spec.add_dependency "importmap-rails", "~> 2.0"
30
+ spec.add_dependency "pagy", "~> 6.5"
31
+
32
+ spec.add_dependency "ransack", "~> 4.0"
33
+
34
+ # Note: Backup management requires 'backup' (~> 4.4) and 'whenever' (~> 1.1)
35
+ # gems to be added to your host application's Gemfile.
36
+ end