rails_nexus 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +430 -0
- data/Rakefile +6 -0
- data/app/assets/config/rails_nexus_manifest.js +2 -0
- data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
- data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
- data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
- data/app/controllers/rails_nexus/application_controller.rb +15 -0
- data/app/controllers/rails_nexus/backup_controller.rb +91 -0
- data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
- data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
- data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
- data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
- data/app/controllers/rails_nexus/settings_controller.rb +100 -0
- data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
- data/app/controllers/rails_nexus/stats_controller.rb +494 -0
- data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
- data/app/helpers/rails_nexus/application_helper.rb +5 -0
- data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
- data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
- data/app/javascript/controllers/rails_nexus_controller.js +380 -0
- data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
- data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
- data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
- data/app/javascript/rails_nexus/application.js +17 -0
- data/app/jobs/rails_nexus/application_job.rb +4 -0
- data/app/mailers/rails_nexus/application_mailer.rb +6 -0
- data/app/models/rails_nexus/application_record.rb +5 -0
- data/app/models/rails_nexus/backup.rb +107 -0
- data/app/models/rails_nexus/base_record.rb +26 -0
- data/app/models/rails_nexus/comment.rb +13 -0
- data/app/models/rails_nexus/cron_job.rb +72 -0
- data/app/models/rails_nexus/database_stat.rb +128 -0
- data/app/models/rails_nexus/event.rb +101 -0
- data/app/models/rails_nexus/logged_exception.rb +864 -0
- data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
- data/app/models/rails_nexus/metric.rb +107 -0
- data/app/models/rails_nexus/nginx_metric.rb +112 -0
- data/app/models/rails_nexus/server_metric.rb +168 -0
- data/app/models/rails_nexus/webhook_delivery.rb +64 -0
- data/app/services/rails_nexus/backup_service.rb +275 -0
- data/app/views/layouts/rails_nexus/application.html.erb +185 -0
- data/app/views/rails_nexus/analytics/index.html.erb +363 -0
- data/app/views/rails_nexus/backup/files.html.erb +53 -0
- data/app/views/rails_nexus/backup/index.html.erb +211 -0
- data/app/views/rails_nexus/backup/settings.html.erb +158 -0
- data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
- data/app/views/rails_nexus/database_health/index.html.erb +139 -0
- data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
- data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
- data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
- data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
- data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
- data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
- data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
- data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
- data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
- data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
- data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
- data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
- data/app/views/rails_nexus/settings/index.html.erb +140 -0
- data/app/views/rails_nexus/stats/index.html.erb +569 -0
- data/config/importmap.rb +15 -0
- data/config/initializers/date_formats.rb +5 -0
- data/config/initializers/rails_nexus.rb +18 -0
- data/config/locales/en.yml +47 -0
- data/config/routes.rb +58 -0
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
- data/lib/generators/rails_nexus/backup_generator.rb +70 -0
- data/lib/generators/rails_nexus/customize_generator.rb +159 -0
- data/lib/generators/rails_nexus/install_generator.rb +61 -0
- data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
- data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
- data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
- data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
- data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
- data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
- data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
- data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
- data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
- data/lib/generators/rails_nexus/templates/migration.rb +24 -0
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
- data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
- data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
- data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
- data/lib/rails_nexus/breadcrumbs.rb +161 -0
- data/lib/rails_nexus/cleanup.rb +32 -0
- data/lib/rails_nexus/configuration.rb +319 -0
- data/lib/rails_nexus/engine.rb +135 -0
- data/lib/rails_nexus/logger.rb +188 -0
- data/lib/rails_nexus/notifications.rb +105 -0
- data/lib/rails_nexus/ransack_config.rb +4 -0
- data/lib/rails_nexus/storm_protection.rb +124 -0
- data/lib/rails_nexus/version.rb +3 -0
- data/lib/rails_nexus.rb +133 -0
- data/lib/tasks/rails_nexus.rake +230 -0
- data/lib/tasks/rails_nexus_tasks.rake +37 -0
- data/rails_nexus.gemspec +36 -0
- metadata +252 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module RailsNexus
|
|
6
|
+
class BackupService
|
|
7
|
+
attr_reader :config_path, :models_path, :dump_path
|
|
8
|
+
|
|
9
|
+
CONFIG_FILE = "rails_nexus_backup.yml"
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
saved = self.class.load_config
|
|
13
|
+
@config_path = saved[:config_path] || RailsNexus.configuration.backup_config_path || default_config_path
|
|
14
|
+
@models_path = saved[:models_path] || RailsNexus.configuration.backup_models_path || default_models_path
|
|
15
|
+
@dump_path = saved[:dump_path] || RailsNexus.configuration.backup_dump_path || default_dump_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Load saved config from YAML file
|
|
19
|
+
def self.load_config
|
|
20
|
+
path = config_file_path
|
|
21
|
+
return default_config unless File.exist?(path)
|
|
22
|
+
|
|
23
|
+
data = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time]) || {}
|
|
24
|
+
default_config.merge(data.transform_keys(&:to_sym))
|
|
25
|
+
rescue StandardError
|
|
26
|
+
default_config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Save config to YAML file
|
|
30
|
+
def self.save_config(params)
|
|
31
|
+
config = load_config
|
|
32
|
+
|
|
33
|
+
# Merge new params
|
|
34
|
+
params.each do |key, value|
|
|
35
|
+
config[key.to_sym] = value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Ensure directory exists
|
|
39
|
+
dir = File.dirname(config_file_path)
|
|
40
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
41
|
+
|
|
42
|
+
# Write YAML
|
|
43
|
+
File.write(config_file_path, config.to_yaml)
|
|
44
|
+
|
|
45
|
+
{ success: true }
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
{ success: false, error: e.message }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Delete saved config
|
|
51
|
+
def self.reset_config
|
|
52
|
+
File.delete(config_file_path) if File.exist?(config_file_path)
|
|
53
|
+
{ success: true }
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
{ success: false, error: e.message }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if backup gem is configured
|
|
59
|
+
def configured?
|
|
60
|
+
File.exist?(@config_path) && Dir.exist?(@models_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get all backup models
|
|
64
|
+
def models
|
|
65
|
+
return [] unless configured?
|
|
66
|
+
|
|
67
|
+
Dir.glob(File.join(@models_path, "*.rb")).map do |path|
|
|
68
|
+
parse_model_file(path)
|
|
69
|
+
end.compact
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get backup files
|
|
73
|
+
def backup_files
|
|
74
|
+
return [] unless @dump_path && Dir.exist?(@dump_path)
|
|
75
|
+
|
|
76
|
+
Dir.glob(File.join(@dump_path, "*")).select { |f| File.file?(f) }.map do |path|
|
|
77
|
+
{
|
|
78
|
+
name: File.basename(path),
|
|
79
|
+
path: path,
|
|
80
|
+
size: File.size(path),
|
|
81
|
+
size_human: human_size(File.size(path)),
|
|
82
|
+
created_at: File.mtime(path),
|
|
83
|
+
age_hours: ((Time.current - File.mtime(path)) / 3600).round(1),
|
|
84
|
+
encrypted: path.end_with?(".enc"),
|
|
85
|
+
compressed: path.end_with?(".gz") || path.end_with?(".tar.gz")
|
|
86
|
+
}
|
|
87
|
+
end.sort_by { |f| -f[:created_at].to_i }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get backup summary stats
|
|
91
|
+
def summary
|
|
92
|
+
files = backup_files
|
|
93
|
+
{
|
|
94
|
+
total_files: files.size,
|
|
95
|
+
total_size: files.sum { |f| f[:size] },
|
|
96
|
+
total_size_human: human_size(files.sum { |f| f[:size] }),
|
|
97
|
+
last_backup: files.first&.dig(:created_at),
|
|
98
|
+
last_backup_age_hours: files.first&.dig(:age_hours),
|
|
99
|
+
oldest_backup: files.last&.dig(:created_at),
|
|
100
|
+
models_count: models.size,
|
|
101
|
+
healthy: last_backup_healthy?,
|
|
102
|
+
alerts: generate_alerts
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get cron schedule from whenever config
|
|
107
|
+
def cron_schedule
|
|
108
|
+
schedule_file = File.join(File.dirname(@config_path), "config", "schedule.rb")
|
|
109
|
+
return nil unless File.exist?(schedule_file)
|
|
110
|
+
|
|
111
|
+
content = File.read(schedule_file)
|
|
112
|
+
{
|
|
113
|
+
file: schedule_file,
|
|
114
|
+
entries: parse_schedule(content),
|
|
115
|
+
raw: content
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Run a backup model
|
|
120
|
+
def trigger_backup(model_name)
|
|
121
|
+
return { success: false, error: "Backup not configured" } unless configured?
|
|
122
|
+
return { success: false, error: "Invalid model: #{model_name}" } unless valid_model?(model_name)
|
|
123
|
+
|
|
124
|
+
runner = find_runner_script
|
|
125
|
+
return { success: false, error: "Runner script not found" } unless runner
|
|
126
|
+
|
|
127
|
+
log_file = File.join(File.dirname(@config_path), "log", "cron.log")
|
|
128
|
+
error_log = File.join(File.dirname(@config_path), "log", "cron-error.log")
|
|
129
|
+
|
|
130
|
+
command = "bash #{runner} #{model_name} >> #{log_file} 2>> #{error_log}"
|
|
131
|
+
pid = Process.spawn(command)
|
|
132
|
+
Process.detach(pid)
|
|
133
|
+
|
|
134
|
+
{ success: true, pid: pid, model: model_name }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get backup health status
|
|
138
|
+
def health_status
|
|
139
|
+
return { status: "not_configured", message: "Backup not configured" } unless configured?
|
|
140
|
+
|
|
141
|
+
summary_data = summary
|
|
142
|
+
threshold = saved_config[:alert_threshold_hours]&.to_i || RailsNexus.configuration.backup_alert_threshold_hours || 24
|
|
143
|
+
|
|
144
|
+
if summary_data[:last_backup].nil?
|
|
145
|
+
{ status: "warning", message: "No backups found", details: summary_data }
|
|
146
|
+
elsif summary_data[:last_backup_age_hours] > threshold * 2
|
|
147
|
+
{ status: "critical", message: "Last backup #{summary_data[:last_backup_age_hours]}h ago (>#{threshold * 2}h)", details: summary_data }
|
|
148
|
+
elsif summary_data[:last_backup_age_hours] > threshold
|
|
149
|
+
{ status: "warning", message: "Last backup #{summary_data[:last_backup_age_hours]}h ago (>#{threshold}h)", details: summary_data }
|
|
150
|
+
else
|
|
151
|
+
{ status: "healthy", message: "Last backup #{summary_data[:last_backup_age_hours]}h ago", details: summary_data }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get saved config
|
|
156
|
+
def saved_config
|
|
157
|
+
@saved_config ||= self.class.load_config
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def self.config_file_path
|
|
163
|
+
Rails.root.join("config", CONFIG_FILE)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.default_config
|
|
167
|
+
{
|
|
168
|
+
enabled: true,
|
|
169
|
+
config_path: nil,
|
|
170
|
+
models_path: nil,
|
|
171
|
+
dump_path: nil,
|
|
172
|
+
notify_command: nil,
|
|
173
|
+
alert_threshold_hours: 24,
|
|
174
|
+
rsync_host: nil,
|
|
175
|
+
rsync_port: 22,
|
|
176
|
+
rsync_user: nil,
|
|
177
|
+
rsync_path: nil,
|
|
178
|
+
encrypt_password: nil,
|
|
179
|
+
auto_cleanup_days: 30
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def default_config_path
|
|
184
|
+
Rails.root.join("config", "backup", "config.rb").to_s
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def default_models_path
|
|
188
|
+
Rails.root.join("config", "backup", "models").to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def default_dump_path
|
|
192
|
+
Rails.root.join("storage", "rails_nexus", "backups").to_s
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_model_file(path)
|
|
196
|
+
content = File.read(path)
|
|
197
|
+
name_match = content.match(/Model\.new\(:(\w+)/)
|
|
198
|
+
desc_match = content.match(/'([^']+)'/)
|
|
199
|
+
|
|
200
|
+
return nil unless name_match
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
name: name_match[1],
|
|
204
|
+
description: desc_match ? desc_match[1] : name_match[1],
|
|
205
|
+
file: path,
|
|
206
|
+
has_mysql: content.include?("database MySQL"),
|
|
207
|
+
has_local: content.include?("store_with Local"),
|
|
208
|
+
has_rsync: content.include?("sync_with RSync"),
|
|
209
|
+
has_encryption: content.include?("encrypt_with OpenSSL"),
|
|
210
|
+
has_compression: content.include?("compress_with Gzip"),
|
|
211
|
+
skip_tables: content.scan(/skip_tables\s*=\s*\[([^\]]+)\]/).flatten.first,
|
|
212
|
+
keep_count: content.scan(/keep\s*=\s*(\d+)/).flatten.first&.to_i
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def parse_schedule(content)
|
|
217
|
+
entries = []
|
|
218
|
+
content.scan(/every\s+([^,]+)(?:,\s*at:\s*"([^"]+)")?\s+do\s+command\s+"([^"]+)"/) do |match|
|
|
219
|
+
entries << {
|
|
220
|
+
frequency: match[0].strip,
|
|
221
|
+
time: match[1],
|
|
222
|
+
command: match[2]
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
entries
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def find_runner_script
|
|
229
|
+
runner = File.join(File.dirname(@config_path), "bin", "run-backup")
|
|
230
|
+
runner if File.exist?(runner)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def valid_model?(name)
|
|
234
|
+
models.any? { |m| m[:name] == name }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def last_backup_healthy?
|
|
238
|
+
files = backup_files
|
|
239
|
+
return false if files.empty?
|
|
240
|
+
|
|
241
|
+
threshold = saved_config[:alert_threshold_hours]&.to_i || RailsNexus.configuration.backup_alert_threshold_hours || 24
|
|
242
|
+
files.first[:age_hours] <= threshold
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def generate_alerts
|
|
246
|
+
alerts = []
|
|
247
|
+
files = backup_files
|
|
248
|
+
threshold = saved_config[:alert_threshold_hours]&.to_i || RailsNexus.configuration.backup_alert_threshold_hours || 24
|
|
249
|
+
|
|
250
|
+
if files.empty?
|
|
251
|
+
alerts << { level: "warning", message: "No backup files found" }
|
|
252
|
+
elsif files.first[:age_hours] > threshold
|
|
253
|
+
alerts << { level: "critical", message: "No backup in #{files.first[:age_hours]}h (threshold: #{threshold}h)" }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check for old backups consuming space
|
|
257
|
+
old_files = files.select { |f| f[:age_hours] > 720 } # 30 days
|
|
258
|
+
if old_files.size > 10
|
|
259
|
+
alerts << { level: "info", message: "#{old_files.size} backups older than 30 days (#{human_size(old_files.sum { |f| f[:size] })})" }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
alerts
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def human_size(bytes)
|
|
266
|
+
return "0 B" if bytes == 0
|
|
267
|
+
|
|
268
|
+
units = %w[B KB MB GB TB]
|
|
269
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
270
|
+
exp = units.size - 1 if exp >= units.size
|
|
271
|
+
|
|
272
|
+
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full" data-controller="rails_nexus-theme" data-rails_nexus-theme-theme-value="<%= RailsNexus.configuration.theme %>">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title><%= content_for?(:title) ? yield(:title) : RailsNexus.configuration.application_name || "RailsNexus" %></title>
|
|
8
|
+
|
|
9
|
+
<%= stylesheet_link_tag "rails_nexus/application", "data-turbo-track": "reload" %>
|
|
10
|
+
|
|
11
|
+
<%= javascript_importmap_tags "rails_nexus/application" %>
|
|
12
|
+
|
|
13
|
+
<% if respond_to?(:turbo_refreshes_with) %>
|
|
14
|
+
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<%= csrf_meta_tags %>
|
|
17
|
+
<%= csp_meta_tag %>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="h-full <%= RailsNexus.configuration.body_class %>">
|
|
20
|
+
<div class="min-h-full flex">
|
|
21
|
+
|
|
22
|
+
<%# Sidebar %>
|
|
23
|
+
<aside class="rn-sidebar">
|
|
24
|
+
<%# Logo %>
|
|
25
|
+
<div class="rn-sidebar-logo">
|
|
26
|
+
<%= link_to root_path, class: "flex items-center gap-2.5" do %>
|
|
27
|
+
<div class="rn-logo-icon">⚡</div>
|
|
28
|
+
<span class="rn-logo-text"><%= RailsNexus.configuration.application_name || "RailsNexus" %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<%# Navigation %>
|
|
33
|
+
<nav class="rn-sidebar-nav">
|
|
34
|
+
<%= link_to root_path,
|
|
35
|
+
class: "rn-nav-item #{request.path == rails_nexus.root_path ? 'active' : ''}" do %>
|
|
36
|
+
<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="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" /></svg>
|
|
37
|
+
Dashboard
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<%= link_to database_health_path,
|
|
41
|
+
class: "rn-nav-item #{request.path == rails_nexus.database_health_path ? 'active' : ''}" do %>
|
|
42
|
+
<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>
|
|
43
|
+
DB Health
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%= link_to n1_patterns_path,
|
|
47
|
+
class: "rn-nav-item #{request.path == rails_nexus.n1_patterns_path ? 'active' : ''}" do %>
|
|
48
|
+
<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 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>
|
|
49
|
+
N+1 Queries
|
|
50
|
+
<% end %>
|
|
51
|
+
|
|
52
|
+
<%= link_to analytics_path,
|
|
53
|
+
class: "rn-nav-item #{request.path == rails_nexus.analytics_path ? 'active' : ''}" do %>
|
|
54
|
+
<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="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5" /></svg>
|
|
55
|
+
Analytics
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<%= link_to stats_path,
|
|
59
|
+
class: "rn-nav-item #{request.path == rails_nexus.stats_path ? 'active' : ''}" do %>
|
|
60
|
+
<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="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /></svg>
|
|
61
|
+
Statistics
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<%= link_to cron_jobs_path,
|
|
65
|
+
class: "rn-nav-item #{request.path.start_with?(rails_nexus.cron_jobs_path) ? 'active' : ''}" do %>
|
|
66
|
+
<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>
|
|
67
|
+
Cron Jobs
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<%= link_to backup_path,
|
|
71
|
+
class: "rn-nav-item #{request.path.start_with?(rails_nexus.backup_path) ? 'active' : ''}" do %>
|
|
72
|
+
<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>
|
|
73
|
+
Backups
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
76
|
+
<%= link_to settings_path,
|
|
77
|
+
class: "rn-nav-item #{request.path == rails_nexus.settings_path ? 'active' : ''}" do %>
|
|
78
|
+
<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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
79
|
+
Settings
|
|
80
|
+
<% end %>
|
|
81
|
+
|
|
82
|
+
<%# ─── Tools section ──────────────────────────────────────── %>
|
|
83
|
+
<div class="rn-sidebar-section-divider"></div>
|
|
84
|
+
|
|
85
|
+
<%= link_to feed_logged_exceptions_path(format: :rss),
|
|
86
|
+
class: "rn-nav-item rn-nav-item--muted" do %>
|
|
87
|
+
<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.75 19.5v-.75a7.5 7.5 0 00-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
|
|
88
|
+
RSS Feed
|
|
89
|
+
<% end %>
|
|
90
|
+
|
|
91
|
+
<% RailsNexus.configuration.sidebar_links.each do |link| %>
|
|
92
|
+
<%= link_to link[:url],
|
|
93
|
+
class: "rn-nav-item",
|
|
94
|
+
target: link[:target] || "_self" do %>
|
|
95
|
+
<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="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>
|
|
96
|
+
<%= link[:label] %>
|
|
97
|
+
<% end %>
|
|
98
|
+
<% end %>
|
|
99
|
+
</nav>
|
|
100
|
+
|
|
101
|
+
<%# Footer %>
|
|
102
|
+
<div class="rn-sidebar-footer">
|
|
103
|
+
v<%= RailsNexus::VERSION %> · <%= RailsNexus::LoggedException.count %> exceptions
|
|
104
|
+
</div>
|
|
105
|
+
</aside>
|
|
106
|
+
|
|
107
|
+
<%# Main Content %>
|
|
108
|
+
<div class="flex-1 flex flex-col min-w-0">
|
|
109
|
+
<%# Top Bar %>
|
|
110
|
+
<header class="rn-topbar">
|
|
111
|
+
<span class="text-sm font-semibold"><%= RailsNexus.configuration.application_name || "RailsNexus" %></span>
|
|
112
|
+
|
|
113
|
+
<div class="flex items-center gap-1.5">
|
|
114
|
+
<% if RailsNexus.configuration.keyboard_shortcuts %>
|
|
115
|
+
<span class="text-xs flex items-center gap-1" style="color: var(--rn-text-muted)">
|
|
116
|
+
<span class="rn-kbd">?</span>
|
|
117
|
+
</span>
|
|
118
|
+
<% end %>
|
|
119
|
+
|
|
120
|
+
<button type="button"
|
|
121
|
+
class="rn-icon-btn"
|
|
122
|
+
data-action="click->rails_nexus-theme#toggle"
|
|
123
|
+
title="Toggle theme (t)">
|
|
124
|
+
<svg class="w-4 h-4 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /></svg>
|
|
125
|
+
<svg class="w-4 h-4 hidden dark:block" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /></svg>
|
|
126
|
+
</button>
|
|
127
|
+
|
|
128
|
+
<% if main_app.respond_to?(:root_path) %>
|
|
129
|
+
<%= link_to main_app.root_path,
|
|
130
|
+
class: "rn-icon-btn",
|
|
131
|
+
title: "Back to app" do %>
|
|
132
|
+
<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="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>
|
|
133
|
+
<% end %>
|
|
134
|
+
<% end %>
|
|
135
|
+
</div>
|
|
136
|
+
</header>
|
|
137
|
+
|
|
138
|
+
<%# Flash Messages %>
|
|
139
|
+
<% if notice.present? %>
|
|
140
|
+
<div class="mx-4 sm:mx-6 mt-4">
|
|
141
|
+
<div class="rn-alert rn-alert-success">
|
|
142
|
+
<svg class="w-5 h-5 shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>
|
|
143
|
+
<p class="text-sm font-medium"><%= notice %></p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<% end %>
|
|
147
|
+
|
|
148
|
+
<% if alert.present? %>
|
|
149
|
+
<div class="mx-4 sm:mx-6 mt-4">
|
|
150
|
+
<div class="rn-alert rn-alert-error">
|
|
151
|
+
<svg class="w-5 h-5 shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /></svg>
|
|
152
|
+
<p class="text-sm font-medium"><%= alert %></p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<% end %>
|
|
156
|
+
|
|
157
|
+
<%# Main Content %>
|
|
158
|
+
<main class="flex-1 overflow-y-auto p-4 sm:p-6">
|
|
159
|
+
<%= yield %>
|
|
160
|
+
</main>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<%# Mobile sidebar overlay %>
|
|
165
|
+
<div class="rn-overlay" data-rails_nexus-sidebar-target="overlay" data-action="click->rails_nexus-sidebar#close"></div>
|
|
166
|
+
|
|
167
|
+
<%# Mobile sidebar drawer %>
|
|
168
|
+
<div class="rn-mobile-sidebar" data-rails_nexus-sidebar-target="drawer">
|
|
169
|
+
<div class="rn-sidebar-logo">
|
|
170
|
+
<span class="rn-logo-text"><%= RailsNexus.configuration.application_name || "RailsNexus" %></span>
|
|
171
|
+
<button type="button" class="rn-icon-btn" data-action="click->rails_nexus-sidebar#close">
|
|
172
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
<nav class="rn-sidebar-nav">
|
|
176
|
+
<%= link_to root_path, class: "rn-nav-item" do %>
|
|
177
|
+
<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="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6z" /></svg>
|
|
178
|
+
Dashboard
|
|
179
|
+
<% end %>
|
|
180
|
+
</nav>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<%= turbo_frame_tag :remote_modal, target: :_top %>
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|