rails_nexus 2.1.2 → 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/README.md +38 -10
- data/app/controllers/rails_nexus/stats_controller.rb +0 -126
- data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
- data/app/views/layouts/rails_nexus/application.html.erb +1 -12
- data/app/views/rails_nexus/stats/index.html.erb +0 -101
- data/config/routes.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration.rb +0 -82
- data/lib/rails_nexus/configuration.rb +0 -45
- data/lib/rails_nexus/engine.rb +6 -4
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +0 -80
- data/rails_nexus.gemspec +1 -3
- metadata +5 -26
- data/app/controllers/rails_nexus/backup_controller.rb +0 -119
- 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 -189
- data/app/services/rails_nexus/backup_runner.rb +0 -62
- data/app/services/rails_nexus/backup_service.rb +0 -817
- 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/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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78f69603552ed8908670a66523d222bd7e7221fb4202133db0e4967b7676e069
|
|
4
|
+
data.tar.gz: 8fce257592b3eff5989d41dec5cd6be2a26351b64e98558ed566b889a98400a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8cfe97fc4a57e6dbaed6c007da8ae2045a3039ea0aee70e4cd82b654932faa4a5f8419740a494f537c31aa11b4aef837ce4cfc420e88be312e212b880de667b
|
|
7
|
+
data.tar.gz: 4b3ad524e5e0d44b008cf44e63bb2b6941f580de2a6163daa44f9079054eb69bea0100810b6e6e944da5d3a630805ae6302e89b7708d4c1577595d017f1f0667
|
data/README.md
CHANGED
|
@@ -33,13 +33,6 @@ RailsNexus is an extensible operations and administration console for Rails appl
|
|
|
33
33
|
- **Execution Times** — Monitor job duration and detect slow jobs
|
|
34
34
|
- **Cleanup** — Automatic old job record removal
|
|
35
35
|
|
|
36
|
-
### 💾 Backup Management
|
|
37
|
-
- **Backup Dashboard** — Health status, model listing, recent backups
|
|
38
|
-
- **File Browser** — View all backup files with size, age, and format
|
|
39
|
-
- **Trigger from UI** — Run backups directly from the dashboard
|
|
40
|
-
- **Settings Editor** — Configure paths, thresholds, and notifications
|
|
41
|
-
- **Health Monitoring** — Alerts when backups are stale or missing
|
|
42
|
-
- **Cron Schedule** — View and manage backup schedules
|
|
43
36
|
|
|
44
37
|
### 🖥️ Server Statistics
|
|
45
38
|
- **Memory Usage** — RAM and swap monitoring
|
|
@@ -112,6 +105,16 @@ This will:
|
|
|
112
105
|
|
|
113
106
|
The dashboard is now available at `/rails_nexus`.
|
|
114
107
|
|
|
108
|
+
Backup management is provided separately by
|
|
109
|
+
[backup_nexus](https://github.com/tamiru/backup_nexus). Add it when your
|
|
110
|
+
application needs the backup dashboard and runner:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
gem "backup_nexus"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then run bin/rails generate backup_nexus:install.
|
|
117
|
+
|
|
115
118
|
### Optional UI customization
|
|
116
119
|
|
|
117
120
|
The mounted dashboard already includes its CSS, Turbo, and Stimulus controllers.
|
|
@@ -405,9 +408,6 @@ RailsNexus applies Rails' configured parameter filters plus built-in password, t
|
|
|
405
408
|
|
|
406
409
|
Analytics time grouping works on SQLite, PostgreSQL, and MySQL/MariaDB. Row counts work on all three; physical table-size and server uptime/connection metrics are displayed only where the adapter exposes them safely, and otherwise show as unavailable.
|
|
407
410
|
|
|
408
|
-
## Backup safety and compatibility
|
|
409
|
-
|
|
410
|
-
Backup commands are executed without a shell and validate database identifiers, ports, hosts, paths, remote destinations, and MySQL options. Database and encryption passwords are passed through protected environment variables or mode-`0600` temporary files rather than command-line arguments. Free-form notification shell commands are intentionally unsupported; configure a direct executable and arguments instead.
|
|
411
411
|
|
|
412
412
|
## Security
|
|
413
413
|
|
|
@@ -441,6 +441,34 @@ gem build rails_nexus.gemspec
|
|
|
441
441
|
|
|
442
442
|
The repository includes a Rails dummy application under `test/dummy` for engine integration testing.
|
|
443
443
|
|
|
444
|
+
## CI/CD
|
|
445
|
+
|
|
446
|
+
GitHub Actions runs the test matrix, database compatibility checks, asset builds,
|
|
447
|
+
and security checks on pushes and pull requests. Releases are published to
|
|
448
|
+
RubyGems when a version tag is pushed.
|
|
449
|
+
|
|
450
|
+
Before the first release, configure a RubyGems [Trusted Publisher](https://guides.rubygems.org/trusted-publishing/)
|
|
451
|
+
for:
|
|
452
|
+
|
|
453
|
+
- Owner: `tamiru`
|
|
454
|
+
- Repository: `rails_nexus`
|
|
455
|
+
- Workflow: `release.yml`
|
|
456
|
+
- GitHub environment: `release`
|
|
457
|
+
|
|
458
|
+
Create the `release` environment in GitHub and add required reviewers if you
|
|
459
|
+
want a manual approval before publishing. No RubyGems API key is needed.
|
|
460
|
+
|
|
461
|
+
To release a version, update `lib/rails_nexus/version.rb` and `CHANGELOG.md`,
|
|
462
|
+
merge the changes to `main`, then push a matching tag:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
git tag v2.1.2
|
|
466
|
+
git push origin v2.1.2
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The workflow verifies that the tag matches the gem version, runs the tests,
|
|
470
|
+
builds and validates the gem, then publishes it and creates the GitHub release.
|
|
471
|
+
|
|
444
472
|
## Support
|
|
445
473
|
|
|
446
474
|
If you find RailsNexus useful, consider supporting the project:
|
|
@@ -11,7 +11,6 @@ module RailsNexus
|
|
|
11
11
|
@exception_stats = collect_exception_stats
|
|
12
12
|
@platform_stats = collect_platform_stats
|
|
13
13
|
@database_stats = collect_database_stats
|
|
14
|
-
@backup_stats = collect_backup_stats
|
|
15
14
|
@storm_stats = collect_storm_stats if defined?(RailsNexus::StormProtection)
|
|
16
15
|
end
|
|
17
16
|
|
|
@@ -243,131 +242,6 @@ module RailsNexus
|
|
|
243
242
|
{ error: e.message }
|
|
244
243
|
end
|
|
245
244
|
|
|
246
|
-
def collect_backup_stats
|
|
247
|
-
stats = { detected: false, gem: nil, status: "unknown", last_backup: nil, details: {} }
|
|
248
|
-
|
|
249
|
-
# Check for backup gem
|
|
250
|
-
if defined?(Backup)
|
|
251
|
-
stats[:detected] = true
|
|
252
|
-
stats[:gem] = "backup"
|
|
253
|
-
stats[:version] = Backup::VERSION if Backup.const_defined?(:VERSION)
|
|
254
|
-
|
|
255
|
-
begin
|
|
256
|
-
# Check for backup configuration
|
|
257
|
-
config_path = Rails.root.join("config", "backup")
|
|
258
|
-
if config_path.exist?
|
|
259
|
-
stats[:details][:config_path] = config_path.to_s
|
|
260
|
-
stats[:status] = "configured"
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Check for backup logs
|
|
264
|
-
log_path = Rails.root.join("log", "backup")
|
|
265
|
-
if log_path.exist?
|
|
266
|
-
log_files = log_path.glob("*.log").sort_by(&:mtime).reverse
|
|
267
|
-
if log_files.any?
|
|
268
|
-
last_log = log_files.first
|
|
269
|
-
stats[:details][:log_file] = last_log.to_s
|
|
270
|
-
stats[:details][:log_size] = (last_log.size / 1024.0).round(1)
|
|
271
|
-
|
|
272
|
-
# Parse last backup time from log
|
|
273
|
-
log_content = last_log.read(1000) # Read first 1KB
|
|
274
|
-
if log_content.match(/started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/)
|
|
275
|
-
stats[:last_backup] = Time.parse($1)
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Check for backup models
|
|
281
|
-
models = Backup::Model.all rescue []
|
|
282
|
-
if models.any?
|
|
283
|
-
stats[:details][:models] = models.map do |m|
|
|
284
|
-
{
|
|
285
|
-
name: m.name,
|
|
286
|
-
schedule: m.schedule&.to_s,
|
|
287
|
-
triggers: m.triggers.map(&:name)
|
|
288
|
-
}
|
|
289
|
-
end
|
|
290
|
-
stats[:status] = "configured"
|
|
291
|
-
end
|
|
292
|
-
rescue StandardError => e
|
|
293
|
-
stats[:details][:error] = e.message
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Check for pg_dump (PostgreSQL)
|
|
298
|
-
if (version = executable_version("pg_dump"))
|
|
299
|
-
stats[:detected] = true
|
|
300
|
-
stats[:gem] ||= "pg_dump"
|
|
301
|
-
stats[:details][:pg_dump] = version
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Check for mysqldump (MySQL)
|
|
305
|
-
if (version = executable_version("mysqldump"))
|
|
306
|
-
stats[:detected] = true
|
|
307
|
-
stats[:gem] ||= "mysqldump"
|
|
308
|
-
stats[:details][:mysqldump] = version
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
# Check for AWS CLI (RDS backups)
|
|
312
|
-
if (version = executable_version("aws"))
|
|
313
|
-
stats[:detected] = true
|
|
314
|
-
stats[:gem] ||= "aws-cli"
|
|
315
|
-
stats[:details][:aws_cli] = version
|
|
316
|
-
|
|
317
|
-
# Check for RDS snapshots if configured
|
|
318
|
-
begin
|
|
319
|
-
rds_client = Aws::RDS::Client.new
|
|
320
|
-
snapshots = rds_client.describe_db_snapshots(db_snapshot_identifier: "automated-*")
|
|
321
|
-
stats[:details][:rds_snapshots] = snapshots.db_snapshots.count
|
|
322
|
-
stats[:status] = "configured" if snapshots.db_snapshots.any?
|
|
323
|
-
rescue StandardError
|
|
324
|
-
# AWS not configured or no access
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Check for docker backup solutions
|
|
329
|
-
if File.exist?("/var/lib/docker") && (version = executable_version("docker"))
|
|
330
|
-
stats[:detected] = true
|
|
331
|
-
stats[:gem] ||= "docker"
|
|
332
|
-
stats[:details][:docker] = version
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
# Check for crontab entries related to backups
|
|
336
|
-
begin
|
|
337
|
-
crontab, _stderr, status = Open3.capture3("crontab", "-l")
|
|
338
|
-
if status.success? && (crontab.include?("backup") || crontab.include?("dump"))
|
|
339
|
-
stats[:details][:crontab_backup] = true
|
|
340
|
-
stats[:status] = "scheduled" if stats[:status] == "unknown"
|
|
341
|
-
end
|
|
342
|
-
rescue StandardError
|
|
343
|
-
# Cannot read crontab
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
# Check for backup-related environment variables
|
|
347
|
-
backup_env_vars = %w[BACKUP_DIR BACKUP_S3_BUCKET BACKUP_GCS_BUCKET DATABASE_BACKUP_URL]
|
|
348
|
-
found_env = backup_env_vars.select { |var| ENV[var] }
|
|
349
|
-
if found_env.any?
|
|
350
|
-
stats[:detected] = true
|
|
351
|
-
stats[:details][:env_vars] = found_env
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
# Determine overall status
|
|
355
|
-
if stats[:last_backup]
|
|
356
|
-
hours_since = (Time.now - stats[:last_backup]) / 3600
|
|
357
|
-
if hours_since < 24
|
|
358
|
-
stats[:status] = "healthy"
|
|
359
|
-
elsif hours_since < 168 # 7 days
|
|
360
|
-
stats[:status] = "warning"
|
|
361
|
-
else
|
|
362
|
-
stats[:status] = "stale"
|
|
363
|
-
end
|
|
364
|
-
elsif stats[:status] == "unknown"
|
|
365
|
-
stats[:status] = "not_configured"
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
stats
|
|
369
|
-
end
|
|
370
|
-
|
|
371
245
|
def collect_table_stats
|
|
372
246
|
tables = []
|
|
373
247
|
|
|
@@ -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 %>
|
|
@@ -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 %>
|
|
@@ -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
|
|
@@ -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
|
|
@@ -183,15 +183,6 @@ module RailsNexus
|
|
|
183
183
|
|
|
184
184
|
# Source Code Viewing
|
|
185
185
|
attr_accessor :source_code_viewing
|
|
186
|
-
|
|
187
|
-
# Backup Management
|
|
188
|
-
attr_accessor :backup_enabled
|
|
189
|
-
attr_accessor :backup_config_path
|
|
190
|
-
attr_accessor :backup_models_path
|
|
191
|
-
attr_accessor :backup_dump_path
|
|
192
|
-
attr_accessor :backup_notify_command
|
|
193
|
-
attr_accessor :backup_alert_threshold_hours
|
|
194
|
-
|
|
195
186
|
# ─── Database ───────────────────────────────────────────────
|
|
196
187
|
# Use a separate database for RailsNexus tables.
|
|
197
188
|
# Options:
|
|
@@ -282,15 +273,6 @@ module RailsNexus
|
|
|
282
273
|
|
|
283
274
|
# Source Code Viewing
|
|
284
275
|
@source_code_viewing = true
|
|
285
|
-
|
|
286
|
-
# Backup Management
|
|
287
|
-
@backup_enabled = false
|
|
288
|
-
@backup_config_path = default_backup_config_path
|
|
289
|
-
@backup_models_path = default_backup_models_path
|
|
290
|
-
@backup_dump_path = default_backup_dump_path
|
|
291
|
-
@backup_notify_command = nil
|
|
292
|
-
@backup_alert_threshold_hours = 24
|
|
293
|
-
|
|
294
276
|
# Database
|
|
295
277
|
@database_url = nil
|
|
296
278
|
@database_name = nil
|
|
@@ -298,32 +280,5 @@ module RailsNexus
|
|
|
298
280
|
@table_prefix = "rails_nexus_"
|
|
299
281
|
end
|
|
300
282
|
|
|
301
|
-
private
|
|
302
|
-
|
|
303
|
-
def default_backup_config_path
|
|
304
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
305
|
-
path = Rails.root.join("config/backup/config.rb")
|
|
306
|
-
path.exist? ? path.to_s : nil
|
|
307
|
-
else
|
|
308
|
-
nil
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def default_backup_models_path
|
|
313
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
314
|
-
path = Rails.root.join("config/backup/models")
|
|
315
|
-
path.exist? ? path.to_s : nil
|
|
316
|
-
else
|
|
317
|
-
nil
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def default_backup_dump_path
|
|
322
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
323
|
-
Rails.root.join("storage/rails_nexus/backups").to_s
|
|
324
|
-
else
|
|
325
|
-
"storage/rails_nexus/backups"
|
|
326
|
-
end
|
|
327
|
-
end
|
|
328
283
|
end
|
|
329
284
|
end
|
data/lib/rails_nexus/engine.rb
CHANGED
|
@@ -92,10 +92,12 @@ module RailsNexus
|
|
|
92
92
|
|
|
93
93
|
initializer "rails_nexus.after_create" do
|
|
94
94
|
ActiveSupport.on_load(:active_record) do
|
|
95
|
-
RailsNexus::LoggedException
|
|
96
|
-
RailsNexus::
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
end
|
data/lib/rails_nexus/version.rb
CHANGED
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
|