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
|
@@ -24,14 +24,14 @@ module RailsNexus
|
|
|
24
24
|
scope :unassigned, -> { where(assigned_to: [nil, ""]) }
|
|
25
25
|
scope :active, -> { not_muted.not_snoozed }
|
|
26
26
|
|
|
27
|
-
def self.ransackable_attributes(
|
|
27
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
28
28
|
%w[action_name backtrace cause_chain controller_name created_at device_type environment
|
|
29
29
|
assigned_to comments_count exception_class fingerprint id instance_variables local_variables
|
|
30
30
|
message muted occurrence_count platform platform_version priority remote_ip request
|
|
31
31
|
snoozed_until system_health updated_at user_agent user_id user_info]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def self.ransackable_associations(
|
|
34
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
35
35
|
[]
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -299,8 +299,8 @@ module RailsNexus
|
|
|
299
299
|
|
|
300
300
|
# Look for ActiveRecord collection iteration patterns:
|
|
301
301
|
# e.g., .each, .map, .find_each near ActiveRecord calls
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
app_lines.any? { |l| l.match?(/\.each|\.map|\.select|\.reject|\.flat_map|\.find_each/) }
|
|
303
|
+
app_lines.any? { |l| l.match?(/ActiveRecord|_callback|association|belongs_to|has_many|load_target|reload/) }
|
|
304
304
|
|
|
305
305
|
# Also detect: repeated similar backtrace across multiple exceptions of same class
|
|
306
306
|
fingerprint = backtrace_fingerprint(app_lines)
|
|
@@ -435,9 +435,11 @@ module RailsNexus
|
|
|
435
435
|
end
|
|
436
436
|
|
|
437
437
|
begin
|
|
438
|
+
stdout, _stderr, status = Open3.capture3("ps", "-o", "rss=,vsz=", "-p", Process.pid.to_s)
|
|
439
|
+
memory = status.success? ? stdout.split.map(&:to_i) : []
|
|
438
440
|
health[:memory] = {
|
|
439
|
-
rss_kb:
|
|
440
|
-
vsz_kb:
|
|
441
|
+
rss_kb: memory[0] || 0,
|
|
442
|
+
vsz_kb: memory[1] || 0
|
|
441
443
|
}
|
|
442
444
|
rescue StandardError
|
|
443
445
|
health[:memory] = { error: "unable to capture" }
|
|
@@ -495,17 +497,28 @@ module RailsNexus
|
|
|
495
497
|
if request.is_a?(String)
|
|
496
498
|
write_attribute :request, request
|
|
497
499
|
elsif request.respond_to?(:env) && request.env.is_a?(Hash)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
500
|
+
filtered_env = RailsNexus.filter_sensitive_data(request.env)
|
|
501
|
+
keys = filtered_env.keys.map(&:to_s).sort
|
|
502
|
+
max_length = keys.map(&:length).max || 0
|
|
503
|
+
env = keys.each_with_object([]) do |key, memo|
|
|
504
|
+
value = filtered_env[key] || filtered_env[key.to_sym]
|
|
505
|
+
memo << ("* %-*s: %s" % [max_length, key, value.to_s.strip])
|
|
501
506
|
end
|
|
502
507
|
write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}").join("\n"))
|
|
503
508
|
|
|
504
509
|
method_str = request.respond_to?(:get?) && request.get? ? "" : " #{request.respond_to?(:method) ? request.method.to_s.upcase : "GET"}"
|
|
510
|
+
parameters = request.respond_to?(:parameters) ? RailsNexus.filter_sensitive_data(request.parameters) : {}
|
|
511
|
+
request_path = if request.respond_to?(:path)
|
|
512
|
+
request.path
|
|
513
|
+
elsif request.respond_to?(:fullpath)
|
|
514
|
+
request.fullpath.to_s.split("?", 2).first
|
|
515
|
+
else
|
|
516
|
+
"/"
|
|
517
|
+
end
|
|
505
518
|
write_attribute(:request, [
|
|
506
|
-
"* URL:#{method_str} #{request.respond_to?(:protocol) ? request.protocol : "http://"}#{
|
|
519
|
+
"* URL:#{method_str} #{request.respond_to?(:protocol) ? request.protocol : "http://"}#{filtered_env["HTTP_HOST"] || "localhost"}#{request_path}",
|
|
507
520
|
"* Format: #{request.respond_to?(:format) ? request.format.to_s : "html"}",
|
|
508
|
-
"* Parameters: #{
|
|
521
|
+
"* Parameters: #{parameters.inspect}",
|
|
509
522
|
"* Rails Root: #{rails_root}"
|
|
510
523
|
].join("\n"))
|
|
511
524
|
else
|
|
@@ -545,10 +558,11 @@ module RailsNexus
|
|
|
545
558
|
|
|
546
559
|
# Build hourly time series of error counts
|
|
547
560
|
def self.build_time_series(days: 30)
|
|
561
|
+
days = (Integer(days, exception: false) || 30).clamp(1, 90)
|
|
548
562
|
start_time = days.days.ago.beginning_of_hour
|
|
563
|
+
bucket = RailsNexus::DatabaseAdapter.time_bucket_expression
|
|
549
564
|
raw = where("created_at >= ?", start_time)
|
|
550
|
-
.group(Arel.sql(
|
|
551
|
-
.order(Arel.sql("DATE_FORMAT(created_at, '%Y-%m-%d %H:00')"))
|
|
565
|
+
.group(Arel.sql(bucket))
|
|
552
566
|
.count
|
|
553
567
|
|
|
554
568
|
# Fill gaps with zeros
|
|
@@ -60,17 +60,6 @@
|
|
|
60
60
|
Cron Jobs
|
|
61
61
|
<% end %>
|
|
62
62
|
|
|
63
|
-
<%= link_to backup_path,
|
|
64
|
-
class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && !%w[all_history history].include?(action_name) ? 'active' : ''}" do %>
|
|
65
|
-
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>
|
|
66
|
-
Backups
|
|
67
|
-
<% end %>
|
|
68
|
-
|
|
69
|
-
<%= link_to backup_history_path,
|
|
70
|
-
class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && %w[all_history history].include?(action_name) ? 'active' : ''}" do %>
|
|
71
|
-
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
72
|
-
Backup History
|
|
73
|
-
<% end %>
|
|
74
63
|
|
|
75
64
|
<%= link_to settings_path,
|
|
76
65
|
class: "rn-nav-item #{request.path == rails_nexus.settings_path ? 'active' : ''}" do %>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
<%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
|
|
11
11
|
|
|
12
|
-
<%=
|
|
12
|
+
<%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
|
|
13
13
|
|
|
14
14
|
<% if respond_to?(:turbo_refreshes_with) %>
|
|
15
15
|
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
rails_nexus/analytics rails_nexus/stats
|
|
25
25
|
].include?(controller_path)
|
|
26
26
|
operations_menu_active = %w[
|
|
27
|
-
rails_nexus/cron_jobs rails_nexus/
|
|
27
|
+
rails_nexus/cron_jobs rails_nexus/settings backup_nexus/backup
|
|
28
28
|
].include?(controller_path)
|
|
29
29
|
resources_menu_active = RailsNexus.configuration.sidebar_links.any? do |link|
|
|
30
30
|
link[:url].to_s == request.path
|
|
@@ -109,17 +109,6 @@
|
|
|
109
109
|
Cron Jobs
|
|
110
110
|
<% end %>
|
|
111
111
|
|
|
112
|
-
<%= link_to backup_path,
|
|
113
|
-
class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && !%w[all_history history].include?(action_name) ? 'active' : ''}" do %>
|
|
114
|
-
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>
|
|
115
|
-
Backups
|
|
116
|
-
<% end %>
|
|
117
|
-
|
|
118
|
-
<%= link_to backup_history_path,
|
|
119
|
-
class: "rn-nav-item #{controller_path == 'rails_nexus/backup' && %w[all_history history].include?(action_name) ? 'active' : ''}" do %>
|
|
120
|
-
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
121
|
-
Backup History
|
|
122
|
-
<% end %>
|
|
123
112
|
|
|
124
113
|
<%= link_to settings_path,
|
|
125
114
|
class: "rn-nav-item #{request.path == rails_nexus.settings_path ? 'active' : ''}" do %>
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
</td>
|
|
340
340
|
<td>
|
|
341
341
|
<span class="text-sm" style="color: var(--rn-text-muted)">
|
|
342
|
-
<%= table[:size_mb] > 0 ? "#{table[:size_mb]} MB" : "—" %>
|
|
342
|
+
<%= table[:size_mb].to_f > 0 ? "#{table[:size_mb]} MB" : "—" %>
|
|
343
343
|
</span>
|
|
344
344
|
</td>
|
|
345
345
|
<td>
|
|
@@ -367,107 +367,6 @@
|
|
|
367
367
|
</div>
|
|
368
368
|
<% end %>
|
|
369
369
|
|
|
370
|
-
<%# ─── Backup Health ──────────────────────────────────────── %>
|
|
371
|
-
<% if @backup_stats && @backup_stats[:detected] %>
|
|
372
|
-
<div class="rn-card">
|
|
373
|
-
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
374
|
-
<div class="flex items-center gap-2">
|
|
375
|
-
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-info-light)">
|
|
376
|
-
<svg class="w-4 h-4" style="color: var(--rn-info)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
377
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
|
378
|
-
</svg>
|
|
379
|
-
</div>
|
|
380
|
-
<div>
|
|
381
|
-
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
|
|
382
|
-
<p class="text-xs" style="color: var(--rn-text-muted)"><%= @backup_stats[:gem] %> <%= @backup_stats[:version] ? "v#{@backup_stats[:version]}" : '' %></p>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
<div class="p-4">
|
|
387
|
-
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
388
|
-
<div>
|
|
389
|
-
<div class="text-xs" style="color: var(--rn-text-muted)">Status</div>
|
|
390
|
-
<div class="flex items-center gap-2 mt-1">
|
|
391
|
-
<span class="w-2 h-2 rounded-full" style="background: <%= case @backup_stats[:status]
|
|
392
|
-
when 'healthy' then 'var(--rn-success)'
|
|
393
|
-
when 'warning' then 'var(--rn-warning)'
|
|
394
|
-
when 'stale' then 'var(--rn-danger)'
|
|
395
|
-
when 'configured' then 'var(--rn-info)'
|
|
396
|
-
else 'var(--rn-text-muted)'
|
|
397
|
-
end %>"></span>
|
|
398
|
-
<span class="text-sm font-medium" style="color: var(--rn-text)"><%= @backup_stats[:status].capitalize %></span>
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
401
|
-
<div>
|
|
402
|
-
<div class="text-xs" style="color: var(--rn-text-muted)">Last Backup</div>
|
|
403
|
-
<div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
|
|
404
|
-
<% if @backup_stats[:last_backup] %>
|
|
405
|
-
<%= @backup_stats[:last_backup].strftime("%b %d, %Y %I:%M %p") %>
|
|
406
|
-
<% else %>
|
|
407
|
-
<span style="color: var(--rn-text-muted)">Unknown</span>
|
|
408
|
-
<% end %>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
<div>
|
|
412
|
-
<div class="text-xs" style="color: var(--rn-text-muted)">Tool</div>
|
|
413
|
-
<div class="text-sm font-medium mt-1" style="color: var(--rn-text)"><%= @backup_stats[:gem] %></div>
|
|
414
|
-
</div>
|
|
415
|
-
<div>
|
|
416
|
-
<div class="text-xs" style="color: var(--rn-text-muted)">Config</div>
|
|
417
|
-
<div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
|
|
418
|
-
<%= @backup_stats[:status] == 'configured' || @backup_stats[:status] == 'scheduled' ? 'Yes' : 'No' %>
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
<%# Backup Details %>
|
|
424
|
-
<% if @backup_stats[:details].any? %>
|
|
425
|
-
<div class="border-t pt-4" style="border-color: var(--rn-border-light)">
|
|
426
|
-
<h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Details</h3>
|
|
427
|
-
<div class="space-y-2">
|
|
428
|
-
<% @backup_stats[:details].each do |key, value| %>
|
|
429
|
-
<% next if value.is_a?(Hash) && value.empty? %>
|
|
430
|
-
<div class="flex items-start gap-3 text-sm">
|
|
431
|
-
<span class="rn-mono font-medium" style="color: var(--rn-text-secondary); min-width: 120px"><%= key.to_s.humanize %>:</span>
|
|
432
|
-
<% if value.is_a?(Array) %>
|
|
433
|
-
<div class="flex-1">
|
|
434
|
-
<% value.each do |item| %>
|
|
435
|
-
<div class="text-xs" style="color: var(--rn-text-muted)">
|
|
436
|
-
<% if item.is_a?(Hash) %>
|
|
437
|
-
<%= item[:name] %> <%= item[:schedule] ? "(#{item[:schedule]})" : '' %>
|
|
438
|
-
<% else %>
|
|
439
|
-
<%= item %>
|
|
440
|
-
<% end %>
|
|
441
|
-
</div>
|
|
442
|
-
<% end %>
|
|
443
|
-
</div>
|
|
444
|
-
<% elsif value.is_a?(Hash) %>
|
|
445
|
-
<div class="flex-1">
|
|
446
|
-
<% value.each do |k, v| %>
|
|
447
|
-
<div class="text-xs" style="color: var(--rn-text-muted)"><%= k %>: <%= v %></div>
|
|
448
|
-
<% end %>
|
|
449
|
-
</div>
|
|
450
|
-
<% else %>
|
|
451
|
-
<span class="flex-1 text-xs" style="color: var(--rn-text-muted)"><%= value %></span>
|
|
452
|
-
<% end %>
|
|
453
|
-
</div>
|
|
454
|
-
<% end %>
|
|
455
|
-
</div>
|
|
456
|
-
</div>
|
|
457
|
-
<% end %>
|
|
458
|
-
</div>
|
|
459
|
-
</div>
|
|
460
|
-
<% elsif @backup_stats && !@backup_stats[:detected] %>
|
|
461
|
-
<div class="rn-card">
|
|
462
|
-
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
463
|
-
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
|
|
464
|
-
</div>
|
|
465
|
-
<div class="p-4">
|
|
466
|
-
<p class="text-sm" style="color: var(--rn-text-muted)">No backup solution detected. Consider adding a backup gem like <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">backup</code> or configuring <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">pg_dump</code>/<code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">mysqldump</code>.</p>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
<% end %>
|
|
470
|
-
|
|
471
370
|
<%# ─── Platform Breakdown ──────────────────────────────────── %>
|
|
472
371
|
<% if @platform_stats && @platform_stats[:breakdown]&.any? %>
|
|
473
372
|
<div class="rn-card">
|
data/config/routes.rb
CHANGED
|
@@ -41,17 +41,6 @@ RailsNexus::Engine.routes.draw do
|
|
|
41
41
|
# N+1 query patterns
|
|
42
42
|
get "n1_patterns", to: "n1_patterns#index", as: :n1_patterns
|
|
43
43
|
|
|
44
|
-
# Backup management
|
|
45
|
-
get "backup", to: "backup#index", as: :backup
|
|
46
|
-
get "backup/new", to: "backup#new", as: :new_backup
|
|
47
|
-
post "backup", to: "backup#create"
|
|
48
|
-
get "backup/history", to: "backup#all_history", as: :backup_history
|
|
49
|
-
get "backup/:id/edit", to: "backup#edit", as: :edit_backup
|
|
50
|
-
patch "backup/:id", to: "backup#update"
|
|
51
|
-
delete "backup/:id", to: "backup#destroy", as: :backup_destroy
|
|
52
|
-
post "backup/:id/trigger", to: "backup#trigger", as: :backup_trigger
|
|
53
|
-
get "backup/:id/history", to: "backup#history", as: :backup_config_history
|
|
54
|
-
|
|
55
44
|
# Settings and webhook management
|
|
56
45
|
get "settings", to: "settings#index", as: :settings
|
|
57
46
|
post "settings/test_webhook", to: "settings#test_webhook", as: :test_webhook
|
|
@@ -35,7 +35,7 @@ module RailsNexus
|
|
|
35
35
|
content = File.read(destination_controller)
|
|
36
36
|
unless content.include?("log_exception_handler")
|
|
37
37
|
inject_into_class app_controller, "ApplicationController" do
|
|
38
|
-
"\n rescue_from
|
|
38
|
+
"\n rescue_from StandardError, with: :log_exception_handler\n"
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -111,87 +111,5 @@ class CreateRailsNexusTables < ActiveRecord::Migration[<%= "[#{ActiveRecord::Mig
|
|
|
111
111
|
add_index :rails_nexus_webhook_deliveries, :status
|
|
112
112
|
add_index :rails_nexus_webhook_deliveries, :created_at
|
|
113
113
|
add_index :rails_nexus_webhook_deliveries, :url
|
|
114
|
-
|
|
115
|
-
# ─── rails_nexus_backups ─────────────────────────────────────────
|
|
116
|
-
create_table :rails_nexus_backups do |t|
|
|
117
|
-
t.string :config_name, null: false
|
|
118
|
-
t.string :status, null: false
|
|
119
|
-
t.text :file_path
|
|
120
|
-
t.bigint :file_size
|
|
121
|
-
t.float :duration
|
|
122
|
-
t.text :error_message
|
|
123
|
-
t.string :triggered_by
|
|
124
|
-
t.datetime :started_at, null: false
|
|
125
|
-
t.datetime :completed_at
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
add_index :rails_nexus_backups, [:config_name, :started_at]
|
|
129
|
-
add_index :rails_nexus_backups, :status
|
|
130
|
-
add_index :rails_nexus_backups, :started_at
|
|
131
|
-
|
|
132
|
-
# ─── rails_nexus_backup_configs ────────────────────────────────
|
|
133
|
-
create_table :rails_nexus_backup_configs do |t|
|
|
134
|
-
t.string :name, null: false
|
|
135
|
-
t.string :description
|
|
136
|
-
t.string :database_name, null: false
|
|
137
|
-
t.string :adapter, null: false, default: "mysql"
|
|
138
|
-
t.string :host, default: "localhost"
|
|
139
|
-
t.integer :port
|
|
140
|
-
t.string :username
|
|
141
|
-
t.string :password
|
|
142
|
-
t.string :skip_tables
|
|
143
|
-
|
|
144
|
-
t.string :storage_path, null: false
|
|
145
|
-
t.integer :keep_count, default: 30
|
|
146
|
-
|
|
147
|
-
t.boolean :compress, default: true
|
|
148
|
-
t.boolean :bzip2_compress, default: false
|
|
149
|
-
t.boolean :encrypted, default: false
|
|
150
|
-
t.string :encryption_password
|
|
151
|
-
t.boolean :encrypt_base64, default: false
|
|
152
|
-
t.boolean :gpg_enabled, default: false
|
|
153
|
-
t.string :gpg_password
|
|
154
|
-
|
|
155
|
-
t.boolean :rsync_enabled, default: false
|
|
156
|
-
t.string :rsync_host
|
|
157
|
-
t.integer :rsync_port, default: 22
|
|
158
|
-
t.string :rsync_user
|
|
159
|
-
t.string :rsync_path
|
|
160
|
-
t.boolean :rsync_mirror, default: false
|
|
161
|
-
t.boolean :rsync_archive, default: true
|
|
162
|
-
t.string :rsync_directories
|
|
163
|
-
t.string :rsync_excludes
|
|
164
|
-
|
|
165
|
-
t.string :mysql_additional_options
|
|
166
|
-
|
|
167
|
-
# S3 storage
|
|
168
|
-
t.boolean :s3_enabled, default: false
|
|
169
|
-
t.string :s3_access_key
|
|
170
|
-
t.string :s3_secret_key
|
|
171
|
-
t.string :s3_bucket
|
|
172
|
-
t.string :s3_region, default: "us-east-1"
|
|
173
|
-
t.string :s3_prefix
|
|
174
|
-
|
|
175
|
-
# Archives (file/dir backup)
|
|
176
|
-
t.boolean :archive_enabled, default: false
|
|
177
|
-
t.string :archive_paths
|
|
178
|
-
t.string :archive_excludes
|
|
179
|
-
t.boolean :split_chunks, default: false
|
|
180
|
-
|
|
181
|
-
# Notifications
|
|
182
|
-
t.string :notify_command
|
|
183
|
-
t.boolean :notify_on_success, default: true
|
|
184
|
-
t.boolean :notify_on_failure, default: true
|
|
185
|
-
t.boolean :email_notify, default: false
|
|
186
|
-
t.string :email_to
|
|
187
|
-
|
|
188
|
-
t.string :schedule_cron
|
|
189
|
-
t.boolean :enabled, default: true
|
|
190
|
-
|
|
191
|
-
t.timestamps
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
add_index :rails_nexus_backup_configs, :name, unique: true
|
|
195
|
-
add_index :rails_nexus_backup_configs, :enabled
|
|
196
114
|
end
|
|
197
115
|
end
|
|
@@ -34,6 +34,9 @@ Rails.application.config.to_prepare do
|
|
|
34
34
|
# config.webhooks = ["https://hooks.slack.com/services/xxx"]
|
|
35
35
|
# config.webhook_timeout = 5
|
|
36
36
|
# config.webhook_headers = { "Authorization" => "Bearer token" }
|
|
37
|
+
# config.webhook_allowed_hosts = ["hooks.example.com", "*.example.org"]
|
|
38
|
+
# config.webhook_denied_hosts = ["blocked.example.org"]
|
|
39
|
+
# config.webhook_allow_http_in_development = false
|
|
37
40
|
|
|
38
41
|
# ─── Cleanup ─────────────────────────────────────────────
|
|
39
42
|
# Auto-delete exceptions older than N days
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<title><%= content_for?(:title) ? yield(:title) : RailsNexus.configuration.application_name || "RailsNexus" %></title>
|
|
8
8
|
|
|
9
9
|
<%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
|
|
10
|
-
<%=
|
|
10
|
+
<%= javascript_include_tag "rails_nexus/application", defer: true, "data-turbo-track": "reload" %>
|
|
11
11
|
|
|
12
12
|
<% if respond_to?(:turbo_refreshes_with) %>
|
|
13
13
|
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
@@ -49,6 +49,13 @@ module RailsNexus
|
|
|
49
49
|
# config.webhook_headers = { "Authorization" => "Bearer token123" }
|
|
50
50
|
attr_accessor :webhook_headers
|
|
51
51
|
|
|
52
|
+
# Host allow/deny policies. Wildcards such as "*.example.com" are accepted.
|
|
53
|
+
attr_accessor :webhook_allowed_hosts
|
|
54
|
+
attr_accessor :webhook_denied_hosts
|
|
55
|
+
|
|
56
|
+
# HTTP remains disabled except when this is true and Rails.env is development.
|
|
57
|
+
attr_accessor :webhook_allow_http_in_development
|
|
58
|
+
|
|
52
59
|
# ─── Logging ───────────────────────────────────────────────
|
|
53
60
|
|
|
54
61
|
# Enable structured logging (default: true).
|
|
@@ -176,15 +183,6 @@ module RailsNexus
|
|
|
176
183
|
|
|
177
184
|
# Source Code Viewing
|
|
178
185
|
attr_accessor :source_code_viewing
|
|
179
|
-
|
|
180
|
-
# Backup Management
|
|
181
|
-
attr_accessor :backup_enabled
|
|
182
|
-
attr_accessor :backup_config_path
|
|
183
|
-
attr_accessor :backup_models_path
|
|
184
|
-
attr_accessor :backup_dump_path
|
|
185
|
-
attr_accessor :backup_notify_command
|
|
186
|
-
attr_accessor :backup_alert_threshold_hours
|
|
187
|
-
|
|
188
186
|
# ─── Database ───────────────────────────────────────────────
|
|
189
187
|
# Use a separate database for RailsNexus tables.
|
|
190
188
|
# Options:
|
|
@@ -209,6 +207,9 @@ module RailsNexus
|
|
|
209
207
|
@webhooks = []
|
|
210
208
|
@webhook_timeout = 5
|
|
211
209
|
@webhook_headers = {}
|
|
210
|
+
@webhook_allowed_hosts = []
|
|
211
|
+
@webhook_denied_hosts = []
|
|
212
|
+
@webhook_allow_http_in_development = false
|
|
212
213
|
|
|
213
214
|
# Logging
|
|
214
215
|
@logging_enabled = true
|
|
@@ -272,15 +273,6 @@ module RailsNexus
|
|
|
272
273
|
|
|
273
274
|
# Source Code Viewing
|
|
274
275
|
@source_code_viewing = true
|
|
275
|
-
|
|
276
|
-
# Backup Management
|
|
277
|
-
@backup_enabled = false
|
|
278
|
-
@backup_config_path = default_backup_config_path
|
|
279
|
-
@backup_models_path = default_backup_models_path
|
|
280
|
-
@backup_dump_path = default_backup_dump_path
|
|
281
|
-
@backup_notify_command = nil
|
|
282
|
-
@backup_alert_threshold_hours = 24
|
|
283
|
-
|
|
284
276
|
# Database
|
|
285
277
|
@database_url = nil
|
|
286
278
|
@database_name = nil
|
|
@@ -288,32 +280,5 @@ module RailsNexus
|
|
|
288
280
|
@table_prefix = "rails_nexus_"
|
|
289
281
|
end
|
|
290
282
|
|
|
291
|
-
private
|
|
292
|
-
|
|
293
|
-
def default_backup_config_path
|
|
294
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
295
|
-
path = Rails.root.join("config/backup/config.rb")
|
|
296
|
-
path.exist? ? path.to_s : nil
|
|
297
|
-
else
|
|
298
|
-
nil
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def default_backup_models_path
|
|
303
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
304
|
-
path = Rails.root.join("config/backup/models")
|
|
305
|
-
path.exist? ? path.to_s : nil
|
|
306
|
-
else
|
|
307
|
-
nil
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def default_backup_dump_path
|
|
312
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
313
|
-
Rails.root.join("storage/rails_nexus/backups").to_s
|
|
314
|
-
else
|
|
315
|
-
"storage/rails_nexus/backups"
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
283
|
end
|
|
319
284
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
module DatabaseAdapter
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def family(connection = ActiveRecord::Base.connection)
|
|
8
|
+
name = connection.adapter_name.to_s.downcase
|
|
9
|
+
return :postgresql if name.include?("postgres")
|
|
10
|
+
return :mysql if name.include?("mysql") || name.include?("trilogy")
|
|
11
|
+
return :sqlite if name.include?("sqlite")
|
|
12
|
+
|
|
13
|
+
:unsupported
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def time_bucket_expression(column: "created_at", period: :hour, connection: ActiveRecord::Base.connection)
|
|
17
|
+
column = connection.quote_column_name(column)
|
|
18
|
+
|
|
19
|
+
case [family(connection), period.to_sym]
|
|
20
|
+
when [:postgresql, :hour] then "TO_CHAR(DATE_TRUNC('hour', #{column}), 'YYYY-MM-DD HH24:00')"
|
|
21
|
+
when [:mysql, :hour] then "DATE_FORMAT(#{column}, '%Y-%m-%d %H:00')"
|
|
22
|
+
when [:sqlite, :hour] then "strftime('%Y-%m-%d %H:00', #{column})"
|
|
23
|
+
else
|
|
24
|
+
raise NotImplementedError, "Time grouping is not supported by this database adapter"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def time_component_expression(column: "created_at", component:, connection: ActiveRecord::Base.connection)
|
|
29
|
+
column = connection.quote_column_name(column)
|
|
30
|
+
|
|
31
|
+
case [family(connection), component.to_sym]
|
|
32
|
+
when [:postgresql, :hour] then "CAST(EXTRACT(HOUR FROM #{column}) AS INTEGER)"
|
|
33
|
+
when [:mysql, :hour] then "HOUR(#{column})"
|
|
34
|
+
when [:sqlite, :hour] then "CAST(strftime('%H', #{column}) AS INTEGER)"
|
|
35
|
+
when [:postgresql, :weekday] then "CAST(EXTRACT(DOW FROM #{column}) AS INTEGER)"
|
|
36
|
+
when [:mysql, :weekday] then "DAYOFWEEK(#{column}) - 1"
|
|
37
|
+
when [:sqlite, :weekday] then "CAST(strftime('%w', #{column}) AS INTEGER)"
|
|
38
|
+
else
|
|
39
|
+
raise NotImplementedError, "Time component grouping is not supported by this database adapter"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def database_version(connection = ActiveRecord::Base.connection)
|
|
44
|
+
case family(connection)
|
|
45
|
+
when :sqlite then connection.select_value("SELECT sqlite_version()")
|
|
46
|
+
when :postgresql, :mysql then connection.select_value("SELECT version()")
|
|
47
|
+
else "N/A"
|
|
48
|
+
end
|
|
49
|
+
rescue ActiveRecord::StatementInvalid
|
|
50
|
+
"N/A"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/rails_nexus/engine.rb
CHANGED
|
@@ -11,8 +11,6 @@ module RailsNexus
|
|
|
11
11
|
initializer "rails_nexus.assets", before: "propshaft.assets_middleware", group: :all do |app|
|
|
12
12
|
next unless app.config.respond_to?(:assets)
|
|
13
13
|
|
|
14
|
-
app.config.assets.paths << root.join("app/javascript")
|
|
15
|
-
|
|
16
14
|
# Sprockets 4 only compiles assets listed in a manifest or in the
|
|
17
15
|
# precompile list. Register these here so mounting applications do not
|
|
18
16
|
# need to modify their own asset manifest.
|
|
@@ -20,21 +18,10 @@ module RailsNexus
|
|
|
20
18
|
app.config.assets.precompile += %w[
|
|
21
19
|
rails_nexus/application.css
|
|
22
20
|
rails_nexus/application.js
|
|
23
|
-
controllers/rails_nexus_controller.js
|
|
24
|
-
controllers/rails_nexus_detail_controller.js
|
|
25
|
-
controllers/rails_nexus_sidebar_controller.js
|
|
26
|
-
controllers/rails_nexus_theme_controller.js
|
|
27
21
|
]
|
|
28
22
|
end
|
|
29
23
|
end
|
|
30
24
|
|
|
31
|
-
# ─── Importmap support (for importmap-rails users) ────────────
|
|
32
|
-
|
|
33
|
-
initializer "rails_nexus.importmap", before: "importmap" do |app|
|
|
34
|
-
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
35
|
-
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
25
|
# ─── Database connection (separate DB support) ─────────────────
|
|
39
26
|
|
|
40
27
|
initializer "rails_nexus.database" do
|
|
@@ -105,10 +92,12 @@ module RailsNexus
|
|
|
105
92
|
|
|
106
93
|
initializer "rails_nexus.after_create" do
|
|
107
94
|
ActiveSupport.on_load(:active_record) do
|
|
108
|
-
RailsNexus::LoggedException
|
|
109
|
-
RailsNexus::
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
if defined?(RailsNexus::LoggedException)
|
|
96
|
+
RailsNexus::LoggedException.after_create do |exception|
|
|
97
|
+
RailsNexus::Notifications.notify(exception)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
Rails.logger.error("[RailsNexus] Notification error: #{e.message}")
|
|
100
|
+
end
|
|
112
101
|
end
|
|
113
102
|
end
|
|
114
103
|
end
|
data/lib/rails_nexus/logger.rb
CHANGED
|
@@ -170,18 +170,7 @@ module RailsNexus
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def filter_params(params)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
rails_filters = Rails.application.config.filter_parameters
|
|
176
|
-
params.each_with_object({}) do |(key, value), result|
|
|
177
|
-
if rails_filters.any? { |f| key.to_s.include?(f.to_s) }
|
|
178
|
-
result[key] = "[FILTERED]"
|
|
179
|
-
elsif value.is_a?(Hash)
|
|
180
|
-
result[key] = filter_params(value)
|
|
181
|
-
else
|
|
182
|
-
result[key] = value
|
|
183
|
-
end
|
|
184
|
-
end
|
|
173
|
+
RailsNexus.filter_sensitive_data(params)
|
|
185
174
|
end
|
|
186
175
|
end
|
|
187
176
|
end
|