rails_nexus 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/rails_nexus/backup_controller.rb +74 -53
- data/app/models/rails_nexus/backup_config.rb +90 -0
- data/app/services/rails_nexus/backup_runner.rb +62 -0
- data/app/services/rails_nexus/backup_service.rb +215 -214
- data/app/views/layouts/rails_nexus/application.html.erb +8 -0
- data/app/views/rails_nexus/backup/_form.html.erb +185 -0
- data/app/views/rails_nexus/backup/config_history.html.erb +67 -0
- data/app/views/rails_nexus/backup/edit.html.erb +15 -0
- data/app/views/rails_nexus/backup/history.html.erb +60 -0
- data/app/views/rails_nexus/backup/index.html.erb +91 -147
- data/app/views/rails_nexus/backup/new.html.erb +15 -0
- data/config/routes.rb +9 -6
- data/lib/generators/rails_nexus/install_generator.rb +1 -13
- data/lib/generators/rails_nexus/templates/migration.rb +119 -11
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +1 -1
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +71 -0
- metadata +8 -16
- data/app/models/rails_nexus/database_stat.rb +0 -128
- data/app/models/rails_nexus/event.rb +0 -101
- data/app/models/rails_nexus/metric.rb +0 -107
- data/app/models/rails_nexus/nginx_metric.rb +0 -112
- data/app/models/rails_nexus/server_metric.rb +0 -168
- data/app/views/rails_nexus/backup/files.html.erb +0 -53
- data/app/views/rails_nexus/backup/settings.html.erb +0 -158
- data/config/initializers/rails_nexus.rb +0 -18
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +0 -30
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +0 -120
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +0 -21
- data/lib/generators/rails_nexus/templates/migration_workflow.rb +0 -29
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<%= form_for @config, url: (@config.persisted? ? backup_path(@config) : backup_index_path), class: "space-y-6" do |f| %>
|
|
2
|
+
<% if @config.errors.any? %>
|
|
3
|
+
<div class="rn-card" style="border-left: 3px solid var(--rn-danger)">
|
|
4
|
+
<div class="p-4">
|
|
5
|
+
<p class="text-sm font-semibold" style="color: var(--rn-danger)"><%= @config.errors.count %> error(s) prevented this config from being saved:</p>
|
|
6
|
+
<ul class="mt-2 text-xs" style="color: var(--rn-text-secondary)">
|
|
7
|
+
<% @config.errors.full_messages.each do |msg| %>
|
|
8
|
+
<li>• <%= msg %></li>
|
|
9
|
+
<% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%# ─── General ─────────────────────────────────────────────── %>
|
|
16
|
+
<div class="rn-card">
|
|
17
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
18
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">General</h2>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
21
|
+
<div class="rn-filter-group">
|
|
22
|
+
<label>Name *</label>
|
|
23
|
+
<%= f.text_field :name, class: "rn-input", placeholder: "e.g. daily_backup", required: true %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="rn-filter-group">
|
|
26
|
+
<label>Description</label>
|
|
27
|
+
<%= f.text_field :description, class: "rn-input", placeholder: "e.g. TTI Daily Backup" %>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="rn-filter-group">
|
|
30
|
+
<label>Enabled</label>
|
|
31
|
+
<%= f.check_box :enabled, class: "rn-input" %>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<%# ─── Database ────────────────────────────────────────────── %>
|
|
37
|
+
<div class="rn-card">
|
|
38
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
39
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Database</h2>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
42
|
+
<div class="rn-filter-group">
|
|
43
|
+
<label>Adapter *</label>
|
|
44
|
+
<%= f.select :adapter, options_for_select([["MySQL", "mysql"], ["PostgreSQL", "postgresql"], ["SQLite", "sqlite"]], @config.adapter), {}, class: "rn-select" %>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="rn-filter-group">
|
|
47
|
+
<label>Database Name *</label>
|
|
48
|
+
<%= f.text_field :database_name, class: "rn-input", placeholder: "e.g. myapp_production" %>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="rn-filter-group">
|
|
51
|
+
<label>Host</label>
|
|
52
|
+
<%= f.text_field :host, class: "rn-input", placeholder: "localhost" %>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="rn-filter-group">
|
|
55
|
+
<label>Port</label>
|
|
56
|
+
<%= f.number_field :port, class: "rn-input", placeholder: "3306" %>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="rn-filter-group">
|
|
59
|
+
<label>Username</label>
|
|
60
|
+
<%= f.text_field :username, class: "rn-input", placeholder: "root" %>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="rn-filter-group">
|
|
63
|
+
<label>Password</label>
|
|
64
|
+
<%= f.password_field :password, class: "rn-input", placeholder: "••••••••" %>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="rn-filter-group md:col-span-3">
|
|
67
|
+
<label>Skip Tables (comma-separated)</label>
|
|
68
|
+
<%= text_field_tag "backup_config[skip_tables][]", @config.skip_tables_list.join(", "), class: "rn-input", placeholder: "e.g. sessions, versions" %>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<%# ─── Storage ─────────────────────────────────────────────── %>
|
|
74
|
+
<div class="rn-card">
|
|
75
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
76
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Storage</h2>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
79
|
+
<div class="rn-filter-group">
|
|
80
|
+
<label>Storage Path *</label>
|
|
81
|
+
<%= f.text_field :storage_path, class: "rn-input", placeholder: "~/dumps" %>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="rn-filter-group">
|
|
84
|
+
<label>Keep Count</label>
|
|
85
|
+
<%= f.number_field :keep_count, class: "rn-input", min: 1 %>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<%# ─── Compression & Encryption ────────────────────────────── %>
|
|
91
|
+
<div class="rn-card">
|
|
92
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
93
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Compression & Encryption</h2>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
96
|
+
<div class="rn-filter-group">
|
|
97
|
+
<label>Compress (Gzip)</label>
|
|
98
|
+
<%= f.check_box :compress, class: "rn-input" %>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="rn-filter-group">
|
|
101
|
+
<label>Encrypt (OpenSSL AES-256)</label>
|
|
102
|
+
<%= f.check_box :encrypt, class: "rn-input", data: { action: "change->rails_nexus#toggle" } %>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="rn-filter-group" data-rails_nexus-target="encryptPassword" style="<%= 'display:none' unless @config.encrypt? %>">
|
|
105
|
+
<label>Encryption Password</label>
|
|
106
|
+
<%= f.password_field :encrypt_password, class: "rn-input", placeholder: "••••••••" %>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<%# ─── Remote Sync ────────────────────────────────────────── %>
|
|
112
|
+
<div class="rn-card">
|
|
113
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
114
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Remote Sync (RSync)</h2>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
117
|
+
<div class="rn-filter-group">
|
|
118
|
+
<label>Enable RSync</label>
|
|
119
|
+
<%= f.check_box :rsync_enabled, class: "rn-input" %>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="rn-filter-group">
|
|
122
|
+
<label>Host</label>
|
|
123
|
+
<%= f.text_field :rsync_host, class: "rn-input", placeholder: "192.168.1.100" %>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="rn-filter-group">
|
|
126
|
+
<label>Port</label>
|
|
127
|
+
<%= f.number_field :rsync_port, class: "rn-input", value: @config.rsync_port || 22 %>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="rn-filter-group">
|
|
130
|
+
<label>SSH User</label>
|
|
131
|
+
<%= f.text_field :rsync_user, class: "rn-input", placeholder: "backup" %>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="rn-filter-group">
|
|
134
|
+
<label>Remote Path</label>
|
|
135
|
+
<%= f.text_field :rsync_path, class: "rn-input", placeholder: "/backups" %>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="rn-filter-group">
|
|
138
|
+
<label>Mirror Mode</label>
|
|
139
|
+
<%= f.check_box :rsync_mirror, class: "rn-input" %>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<%# ─── Notifications ──────────────────────────────────────── %>
|
|
145
|
+
<div class="rn-card">
|
|
146
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
147
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Notifications</h2>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
150
|
+
<div class="rn-filter-group md:col-span-2">
|
|
151
|
+
<label>Notify Command</label>
|
|
152
|
+
<%= f.text_field :notify_command, class: "rn-input", placeholder: "e.g. curl -X POST https://hooks.slack.com/..." %>
|
|
153
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Variables: {STATUS}, {MODEL}, {ERROR}, {TIME}</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="rn-filter-group">
|
|
156
|
+
<label>Notify on Success</label>
|
|
157
|
+
<%= f.check_box :notify_on_success, class: "rn-input" %>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="rn-filter-group">
|
|
160
|
+
<label>Notify on Failure</label>
|
|
161
|
+
<%= f.check_box :notify_on_failure, class: "rn-input" %>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<%# ─── Schedule ────────────────────────────────────────────── %>
|
|
167
|
+
<div class="rn-card">
|
|
168
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
169
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Schedule</h2>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="p-4">
|
|
172
|
+
<div class="rn-filter-group" style="max-width: 300px">
|
|
173
|
+
<label>Cron Expression</label>
|
|
174
|
+
<%= f.text_field :schedule_cron, class: "rn-input", placeholder: "e.g. 0 2 * * * (daily at 2am)" %>
|
|
175
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">min hour day month wday — leave blank for manual only</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<%# ─── Submit ──────────────────────────────────────────────── %>
|
|
181
|
+
<div class="flex items-center gap-3">
|
|
182
|
+
<%= f.submit @config.persisted? ? "Update Config" : "Create Config", class: "rn-btn rn-btn-primary" %>
|
|
183
|
+
<%= link_to "Cancel", backup_path, class: "rn-btn rn-btn-ghost" %>
|
|
184
|
+
</div>
|
|
185
|
+
<% end %>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% page_title "History: #{@config.name}" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current"><%= @config.name %> History</span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<div class="flex items-center justify-between">
|
|
13
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">History: <%= @config.name %></h1>
|
|
14
|
+
<div class="flex gap-2">
|
|
15
|
+
<%= button_to "Run Backup", backup_trigger_path(@config),
|
|
16
|
+
method: :post,
|
|
17
|
+
class: "rn-btn rn-btn-primary",
|
|
18
|
+
data: { turbo_confirm: "Run #{@config.name} backup now?" } %>
|
|
19
|
+
<%= link_to "Edit Config", edit_backup_path(@config), class: "rn-btn rn-btn-ghost" %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rn-card">
|
|
24
|
+
<% if @records.any? %>
|
|
25
|
+
<div class="rn-table-wrap">
|
|
26
|
+
<table class="rn-table">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Status</th>
|
|
30
|
+
<th>File</th>
|
|
31
|
+
<th>Size</th>
|
|
32
|
+
<th>Duration</th>
|
|
33
|
+
<th>Triggered By</th>
|
|
34
|
+
<th>Time</th>
|
|
35
|
+
<th>Error</th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody>
|
|
39
|
+
<% @records.each do |record| %>
|
|
40
|
+
<tr>
|
|
41
|
+
<td>
|
|
42
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
43
|
+
<%= record.status.capitalize %>
|
|
44
|
+
</span>
|
|
45
|
+
</td>
|
|
46
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= record.file_path ? File.basename(record.file_path) : "—" %></span></td>
|
|
47
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.file_size ? number_to_human_size(record.file_size) : "—" %></span></td>
|
|
48
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.duration ? "#{record.duration.round(1)}s" : "—" %></span></td>
|
|
49
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.triggered_by || "—" %></span></td>
|
|
50
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.started_at&.strftime("%b %d %H:%M") %></span></td>
|
|
51
|
+
<td>
|
|
52
|
+
<% if record.error_message.present? %>
|
|
53
|
+
<span class="text-xs" style="color: var(--rn-danger)" title="<%= record.error_message %>">⚠</span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<% end %>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
<% else %>
|
|
62
|
+
<div class="p-8 text-center">
|
|
63
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backups recorded for this config yet.</p>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% page_title "Edit #{@config.name}" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current">Edit <%= @config.name %></span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Edit: <%= @config.name %></h1>
|
|
13
|
+
|
|
14
|
+
<%= render "form" %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<% page_title "Backup History" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current">History</span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup History</h1>
|
|
13
|
+
|
|
14
|
+
<div class="rn-card">
|
|
15
|
+
<% if @records.any? %>
|
|
16
|
+
<div class="rn-table-wrap">
|
|
17
|
+
<table class="rn-table">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr>
|
|
20
|
+
<th>Config</th>
|
|
21
|
+
<th>Status</th>
|
|
22
|
+
<th>File</th>
|
|
23
|
+
<th>Size</th>
|
|
24
|
+
<th>Duration</th>
|
|
25
|
+
<th>Triggered By</th>
|
|
26
|
+
<th>Time</th>
|
|
27
|
+
<th>Error</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
<% @records.each do |record| %>
|
|
32
|
+
<tr>
|
|
33
|
+
<td><span class="font-medium" style="color: var(--rn-text)"><%= record.model_name %></span></td>
|
|
34
|
+
<td>
|
|
35
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
36
|
+
<%= record.status.capitalize %>
|
|
37
|
+
</span>
|
|
38
|
+
</td>
|
|
39
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= record.file_path ? File.basename(record.file_path) : "—" %></span></td>
|
|
40
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.file_size ? number_to_human_size(record.file_size) : "—" %></span></td>
|
|
41
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.duration ? "#{record.duration.round(1)}s" : "—" %></span></td>
|
|
42
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.triggered_by || "—" %></span></td>
|
|
43
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.started_at&.strftime("%b %d %H:%M") %></span></td>
|
|
44
|
+
<td>
|
|
45
|
+
<% if record.error_message.present? %>
|
|
46
|
+
<span class="text-xs" style="color: var(--rn-danger)" title="<%= record.error_message %>">⚠</span>
|
|
47
|
+
<% end %>
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
<% else %>
|
|
55
|
+
<div class="p-8 text-center">
|
|
56
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backup records found.</p>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
<% page_title
|
|
1
|
+
<% page_title "Backup Management" %>
|
|
2
2
|
|
|
3
3
|
<div class="space-y-6">
|
|
4
|
-
<%# ─── Header ──────────────────────────────────────────────── %>
|
|
5
4
|
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
6
5
|
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
7
6
|
<span class="rn-breadcrumb-sep">/</span>
|
|
@@ -9,198 +8,143 @@
|
|
|
9
8
|
</nav>
|
|
10
9
|
|
|
11
10
|
<div class="flex items-center justify-between">
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
<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" />
|
|
15
|
-
</svg>
|
|
16
|
-
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Management</h1>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="flex gap-2">
|
|
19
|
-
<a href="<%= backup_settings_path %>" class="rn-btn rn-btn-ghost">Settings</a>
|
|
20
|
-
<a href="<%= backup_files_path %>" class="rn-btn rn-btn-ghost">View All Files</a>
|
|
21
|
-
</div>
|
|
11
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Management</h1>
|
|
12
|
+
<%= link_to "New Config", new_backup_path, class: "rn-btn rn-btn-primary" %>
|
|
22
13
|
</div>
|
|
23
14
|
|
|
24
|
-
<%# ───
|
|
25
|
-
|
|
26
|
-
<div class="rn-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<span class="text-sm font-semibold" style="color: var(--rn-text)">Backup not configured</span>
|
|
31
|
-
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Set <code>config.backup_enabled = true</code> and configure paths in your initializer.</p>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
15
|
+
<%# ─── Stats ──────────────────────────────────────────────── %>
|
|
16
|
+
<div class="rn-stats">
|
|
17
|
+
<div class="rn-stat">
|
|
18
|
+
<span class="rn-stat-label">Configs</span>
|
|
19
|
+
<span class="rn-stat-value"><%= @stats[:total_configs] %></span>
|
|
20
|
+
<span class="rn-stat-change"><%= @stats[:enabled_configs] %> enabled</span>
|
|
34
21
|
</div>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<div>
|
|
40
|
-
<span class="text-sm font-semibold" style="color: var(--rn-text)"><%= @health[:message] %></span>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
22
|
+
<div class="rn-stat">
|
|
23
|
+
<span class="rn-stat-label">Total Backups</span>
|
|
24
|
+
<span class="rn-stat-value"><%= @stats[:total_backups] %></span>
|
|
25
|
+
<span class="rn-stat-change">All time</span>
|
|
43
26
|
</div>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<div class="rn-stats">
|
|
49
|
-
<div class="rn-stat">
|
|
50
|
-
<span class="rn-stat-label">Total Backups</span>
|
|
51
|
-
<span class="rn-stat-value"><%= @summary[:total_files] %></span>
|
|
52
|
-
<span class="rn-stat-change"><%= @summary[:total_size_human] %></span>
|
|
53
|
-
</div>
|
|
54
|
-
<div class="rn-stat">
|
|
55
|
-
<span class="rn-stat-label">Last Backup</span>
|
|
56
|
-
<span class="rn-stat-value"><%= @summary[:last_backup] ? time_ago_in_words(@summary[:last_backup]) + " ago" : "Never" %></span>
|
|
57
|
-
<span class="rn-stat-change"><%= @summary[:last_backup_age_hours] ? "#{@summary[:last_backup_age_hours]}h ago" : "" %></span>
|
|
58
|
-
</div>
|
|
59
|
-
<div class="rn-stat">
|
|
60
|
-
<span class="rn-stat-label">Backup Models</span>
|
|
61
|
-
<span class="rn-stat-value"><%= @summary[:models_count] %></span>
|
|
62
|
-
<span class="rn-stat-change">Configured</span>
|
|
63
|
-
</div>
|
|
64
|
-
<div class="rn-stat">
|
|
65
|
-
<span class="rn-stat-label">Health</span>
|
|
66
|
-
<span class="rn-stat-value" style="color: <%= @summary[:healthy] ? 'var(--rn-success)' : 'var(--rn-danger)' %>">
|
|
67
|
-
<%= @summary[:healthy] ? "✓ Healthy" : "⚠ Issues" %>
|
|
68
|
-
</span>
|
|
69
|
-
<span class="rn-stat-change"><%= @summary[:alerts]&.size || 0 %> alerts</span>
|
|
70
|
-
</div>
|
|
27
|
+
<div class="rn-stat">
|
|
28
|
+
<span class="rn-stat-label">Successful</span>
|
|
29
|
+
<span class="rn-stat-value" style="color: var(--rn-success)"><%= @stats[:successful] %></span>
|
|
30
|
+
<span class="rn-stat-change">All time</span>
|
|
71
31
|
</div>
|
|
72
|
-
|
|
32
|
+
<div class="rn-stat">
|
|
33
|
+
<span class="rn-stat-label">Failed</span>
|
|
34
|
+
<span class="rn-stat-value" style="color: var(--rn-danger)"><%= @stats[:failed] %></span>
|
|
35
|
+
<span class="rn-stat-change">All time</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
73
38
|
|
|
74
|
-
<%# ───
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
<% end %>
|
|
83
|
-
<% end %>
|
|
39
|
+
<%# ─── Backup Configs ─────────────────────────────────────── %>
|
|
40
|
+
<div class="rn-card">
|
|
41
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
42
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Configurations</h2>
|
|
43
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Configure and run database backups</p>
|
|
44
|
+
</div>
|
|
84
45
|
|
|
85
|
-
|
|
86
|
-
<% if @models.any? %>
|
|
87
|
-
<div class="rn-card">
|
|
88
|
-
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
89
|
-
<div class="flex items-center gap-2">
|
|
90
|
-
<svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
91
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
92
|
-
</svg>
|
|
93
|
-
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Models</h2>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
46
|
+
<% if @configs.any? %>
|
|
96
47
|
<div class="rn-table-wrap">
|
|
97
48
|
<table class="rn-table">
|
|
98
49
|
<thead>
|
|
99
50
|
<tr>
|
|
100
|
-
<th>
|
|
51
|
+
<th>Name</th>
|
|
52
|
+
<th>Adapter</th>
|
|
101
53
|
<th>Database</th>
|
|
102
54
|
<th>Storage</th>
|
|
103
|
-
<th>
|
|
104
|
-
<th>
|
|
55
|
+
<th>Schedule</th>
|
|
56
|
+
<th>Status</th>
|
|
105
57
|
<th style="text-align: right">Actions</th>
|
|
106
58
|
</tr>
|
|
107
59
|
</thead>
|
|
108
60
|
<tbody>
|
|
109
|
-
<% @
|
|
61
|
+
<% @configs.each do |config| %>
|
|
110
62
|
<tr>
|
|
111
63
|
<td>
|
|
112
|
-
<span class="font-medium" style="color: var(--rn-text)"><%=
|
|
113
|
-
|
|
64
|
+
<span class="font-medium" style="color: var(--rn-text)"><%= config.name %></span>
|
|
65
|
+
<% if config.description.present? %>
|
|
66
|
+
<br><span class="text-xs" style="color: var(--rn-text-muted)"><%= config.description %></span>
|
|
67
|
+
<% end %>
|
|
114
68
|
</td>
|
|
115
|
-
<td><span class="rn-badge
|
|
69
|
+
<td><span class="rn-badge rn-badge-info"><%= config.adapter.upcase %></span></td>
|
|
70
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= config.database_name %></span></td>
|
|
71
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= config.storage_path %> (keep <%= config.keep_count %>)</span></td>
|
|
116
72
|
<td>
|
|
117
|
-
|
|
118
|
-
|
|
73
|
+
<% if config.schedule_cron.present? %>
|
|
74
|
+
<span class="rn-badge rn-badge-warning"><%= config.schedule_cron %></span>
|
|
75
|
+
<% else %>
|
|
76
|
+
<span class="text-xs" style="color: var(--rn-text-muted)">Manual</span>
|
|
77
|
+
<% end %>
|
|
78
|
+
</td>
|
|
79
|
+
<td>
|
|
80
|
+
<span class="rn-badge <%= config.enabled? ? 'rn-badge-success' : 'rn-badge-muted' %>">
|
|
81
|
+
<%= config.enabled? ? 'Enabled' : 'Disabled' %>
|
|
82
|
+
</span>
|
|
83
|
+
<% if config.encrypt? %><span class="rn-badge rn-badge-warning">Encrypted</span><% end %>
|
|
84
|
+
<% if config.rsync_enabled? %><span class="rn-badge rn-badge-info">RSync</span><% end %>
|
|
119
85
|
</td>
|
|
120
|
-
<td><span class="rn-badge <%= model[:has_encryption] ? 'rn-badge-success' : 'rn-badge-muted' %>"><%= model[:has_encryption] ? 'Encrypted' : 'None' %></span></td>
|
|
121
|
-
<td><%= model[:keep_count] || '∞' %></td>
|
|
122
86
|
<td style="text-align: right">
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
87
|
+
<div class="flex items-center justify-end gap-1">
|
|
88
|
+
<%= button_to "Run", backup_trigger_path(config),
|
|
89
|
+
method: :post,
|
|
90
|
+
class: "rn-btn rn-btn-primary rn-btn-xs",
|
|
91
|
+
data: { turbo_confirm: "Run #{config.name} backup now?" } %>
|
|
92
|
+
<%= link_to "History", backup_config_history_path(config), class: "rn-btn rn-btn-ghost rn-btn-xs" %>
|
|
93
|
+
<%= link_to "Edit", edit_backup_path(config), class: "rn-btn rn-btn-ghost rn-btn-xs" %>
|
|
94
|
+
<%= button_to "Delete", backup_destroy_path(config),
|
|
95
|
+
method: :delete,
|
|
96
|
+
class: "rn-btn rn-btn-ghost rn-btn-xs",
|
|
97
|
+
style: "color: var(--rn-danger)",
|
|
98
|
+
data: { turbo_confirm: "Delete config '#{config.name}'?" } %>
|
|
99
|
+
</div>
|
|
127
100
|
</td>
|
|
128
101
|
</tr>
|
|
129
102
|
<% end %>
|
|
130
103
|
</tbody>
|
|
131
104
|
</table>
|
|
132
105
|
</div>
|
|
133
|
-
|
|
134
|
-
|
|
106
|
+
<% else %>
|
|
107
|
+
<div class="p-8 text-center">
|
|
108
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backup configurations yet.</p>
|
|
109
|
+
<%= link_to "Create your first config", new_backup_path, class: "mt-3 inline-block text-xs font-medium", style: "color: var(--rn-primary)" %>
|
|
110
|
+
</div>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
|
135
113
|
|
|
136
|
-
<%# ─── Recent Backups
|
|
137
|
-
<% if @
|
|
114
|
+
<%# ─── Recent Backups ─────────────────────────────────────── %>
|
|
115
|
+
<% if @records.any? %>
|
|
138
116
|
<div class="rn-card">
|
|
139
117
|
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
140
|
-
<div class="flex items-center
|
|
141
|
-
<svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
142
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
143
|
-
</svg>
|
|
118
|
+
<div class="flex items-center justify-between">
|
|
144
119
|
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Recent Backups</h2>
|
|
120
|
+
<%= link_to "View All", backup_history_path, class: "text-xs font-medium", style: "color: var(--rn-primary)" %>
|
|
145
121
|
</div>
|
|
146
122
|
</div>
|
|
147
123
|
<div class="rn-table-wrap">
|
|
148
124
|
<table class="rn-table">
|
|
149
125
|
<thead>
|
|
150
126
|
<tr>
|
|
151
|
-
<th>
|
|
152
|
-
<th>Size</th>
|
|
153
|
-
<th>Created</th>
|
|
127
|
+
<th>Config</th>
|
|
154
128
|
<th>Status</th>
|
|
129
|
+
<th>Size</th>
|
|
130
|
+
<th>Duration</th>
|
|
131
|
+
<th>Triggered By</th>
|
|
132
|
+
<th>Time</th>
|
|
155
133
|
</tr>
|
|
156
134
|
</thead>
|
|
157
135
|
<tbody>
|
|
158
|
-
<% @
|
|
136
|
+
<% @records.each do |record| %>
|
|
159
137
|
<tr>
|
|
138
|
+
<td><span class="font-medium" style="color: var(--rn-text)"><%= record.model_name %></span></td>
|
|
160
139
|
<td>
|
|
161
|
-
<span class="rn-
|
|
162
|
-
|
|
163
|
-
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= file[:size_human] %></span></td>
|
|
164
|
-
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= time_ago_in_words(file[:created_at]) %> ago</span></td>
|
|
165
|
-
<td>
|
|
166
|
-
<span class="rn-badge <%= file[:age_hours] < 24 ? 'rn-badge-success' : file[:age_hours] < 72 ? 'rn-badge-warning' : 'rn-badge-muted' %>">
|
|
167
|
-
<%= file[:age_hours] < 24 ? 'Recent' : file[:age_hours] < 72 ? 'Aging' : 'Old' %>
|
|
140
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
141
|
+
<%= record.status.capitalize %>
|
|
168
142
|
</span>
|
|
169
143
|
</td>
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
<% end %>
|
|
177
|
-
|
|
178
|
-
<%# ─── Cron Schedule ───────────────────────────────────────── %>
|
|
179
|
-
<% if @schedule && @schedule[:entries].any? %>
|
|
180
|
-
<div class="rn-card">
|
|
181
|
-
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
182
|
-
<div class="flex items-center gap-2">
|
|
183
|
-
<svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
184
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
|
185
|
-
</svg>
|
|
186
|
-
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Schedule</h2>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<div class="rn-table-wrap">
|
|
190
|
-
<table class="rn-table">
|
|
191
|
-
<thead>
|
|
192
|
-
<tr>
|
|
193
|
-
<th>Frequency</th>
|
|
194
|
-
<th>Time</th>
|
|
195
|
-
<th>Command</th>
|
|
196
|
-
</tr>
|
|
197
|
-
</thead>
|
|
198
|
-
<tbody>
|
|
199
|
-
<% @schedule[:entries].each do |entry| %>
|
|
200
|
-
<tr>
|
|
201
|
-
<td><span class="text-sm" style="color: var(--rn-text)"><%= entry[:frequency] %></span></td>
|
|
202
|
-
<td><span class="text-sm" style="color: var(--rn-text-secondary)"><%= entry[:time] || "All day" %></span></td>
|
|
203
|
-
<td><span class="rn-mono text-xs" style="color: var(--rn-text-muted)"><%= entry[:command] %></span></td>
|
|
144
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.file_size ? number_to_human_size(record.file_size) : "—" %></span></td>
|
|
145
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.duration ? "#{record.duration.round(1)}s" : "—" %></span></td>
|
|
146
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.triggered_by || "—" %></span></td>
|
|
147
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.started_at ? time_ago_in_words(record.started_at) + " ago" : "—" %></span></td>
|
|
204
148
|
</tr>
|
|
205
149
|
<% end %>
|
|
206
150
|
</tbody>
|