rails_nexus 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,363 @@
1
+ <% page_title t(".title", default: "Real-time Analytics") %>
2
+
3
+ <div class="space-y-6">
4
+ <%# ─── Header ──────────────────────────────────────────────── %>
5
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
+ <span class="rn-breadcrumb-sep">/</span>
8
+ <span class="rn-breadcrumb-current">Analytics</span>
9
+ </nav>
10
+
11
+ <div class="flex items-center justify-between">
12
+ <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Real-time Analytics</h1>
13
+ <div class="flex items-center gap-2">
14
+ <% [7, 14, 30].each do |days| %>
15
+ <%= link_to "#{days}d", analytics_path(days: days),
16
+ class: "rn-btn rn-btn-xs #{params[:days].to_i == days ? 'rn-btn-primary' : 'rn-btn-ghost'}" %>
17
+ <% end %>
18
+ </div>
19
+ </div>
20
+
21
+ <%# ─── Baseline Health Status ──────────────────────────────── %>
22
+ <% if @baseline_monitoring %>
23
+ <% status = @baseline_monitoring[:baseline_health] %>
24
+ <div class="rn-card" style="border-left: 3px solid <%= status == 'healthy' ? 'var(--rn-success)' : status == 'warning' ? 'var(--rn-warning)' : 'var(--rn-danger)' %>">
25
+ <div class="p-4 flex items-center gap-3">
26
+ <span class="w-3 h-3 rounded-full" style="background: <%= status == 'healthy' ? 'var(--rn-success)' : status == 'warning' ? 'var(--rn-warning)' : 'var(--rn-danger)' %>"></span>
27
+ <div>
28
+ <span class="text-sm font-semibold" style="color: var(--rn-text)">Baseline: <%= status.capitalize %></span>
29
+ <% if @baseline_monitoring[:anomalies].any? %>
30
+ <span class="text-xs ml-2" style="color: var(--rn-danger)"><%= @baseline_monitoring[:anomalies].size %> anomalies detected</span>
31
+ <% end %>
32
+ <% if @baseline_monitoring[:spike_detection][:detected] %>
33
+ <span class="text-xs ml-2" style="color: var(--rn-danger)">Spike: <%= @baseline_monitoring[:spike_detection][:ratio] %>x increase</span>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ <% end %>
39
+
40
+ <%# ─── Error Trends ────────────────────────────────────────── %>
41
+ <% if @error_trends %>
42
+ <div class="rn-card">
43
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
44
+ <div class="flex items-center justify-between">
45
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Error Trends</h2>
46
+ <div class="flex items-center gap-4 text-xs" style="color: var(--rn-text-muted)">
47
+ <span>Total: <strong style="color: var(--rn-text)"><%= number_with_delimiter(@error_trends[:total_count]) %></strong></span>
48
+ <span>vs prev: <strong style="color: <%= @error_trends[:trend_direction] == 'up' ? 'var(--rn-danger)' : @error_trends[:trend_direction] == 'down' ? 'var(--rn-success)' : 'var(--rn-text)' %>">
49
+ <%= @error_trends[:trend_direction] == 'up' ? '↑' : @error_trends[:trend_direction] == 'down' ? '↓' : '→' %>
50
+ </strong></span>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <div class="p-4">
55
+ <%# Daily trend bar chart %>
56
+ <% if @error_trends[:daily]&.any? %>
57
+ <% max_daily = @error_trends[:daily].map { |d| d[:count] }.max || 1 %>
58
+ <div class="flex items-end gap-1" style="height: 120px">
59
+ <% @error_trends[:daily].each do |day| %>
60
+ <div class="flex-1 flex flex-col items-center gap-1" title="<%= day[:date] %>: <%= day[:count] %> errors">
61
+ <span class="text-[10px]" style="color: var(--rn-text-muted)"><%= day[:count] if day[:count] > 0 %></span>
62
+ <div class="w-full rounded-t transition-all duration-300" style="height: <%= [(day[:count].to_f / max_daily * 100), 2].max %>% ; min-height: 2px; background: var(--rn-primary); opacity: 0.8"></div>
63
+ <span class="text-[9px]" style="color: var(--rn-text-muted)"><%= day[:date].strftime("%d") %></span>
64
+ </div>
65
+ <% end %>
66
+ </div>
67
+ <% end %>
68
+ </div>
69
+ </div>
70
+ <% end %>
71
+
72
+ <%# ─── Platform Health ──────────────────────────────────────── %>
73
+ <% if @platform_health&.any? %>
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)">Platform Health</h2>
77
+ </div>
78
+ <div class="p-4">
79
+ <div class="grid grid-cols-2 md:grid-cols-5 gap-3">
80
+ <% @platform_health.each do |ph| %>
81
+ <% next if ph[:total] == 0 %>
82
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
83
+ <div class="flex items-center justify-between mb-2">
84
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)"><%= ph[:platform] %></span>
85
+ <span class="w-2 h-2 rounded-full" style="background: <%= ph[:health_score] > 80 ? 'var(--rn-success)' : ph[:health_score] > 50 ? 'var(--rn-warning)' : 'var(--rn-danger)' %>"></span>
86
+ </div>
87
+ <div class="text-xl font-bold" style="color: var(--rn-text)"><%= ph[:total] %></div>
88
+ <div class="text-xs" style="color: var(--rn-text-muted)"><%= ph[:error_rate_per_hour] %>/hr</div>
89
+ <div class="mt-2 h-1 rounded-full overflow-hidden" style="background: var(--rn-bg-tertiary)">
90
+ <div class="h-full rounded-full" style="width: <%= ph[:health_score] %>% ; background: <%= ph[:health_score] > 80 ? 'var(--rn-success)' : ph[:health_score] > 50 ? 'var(--rn-warning)' : 'var(--rn-danger)' %>"></div>
91
+ </div>
92
+ <div class="text-[10px] mt-1" style="color: var(--rn-text-muted)">Score: <%= ph[:health_score] %>/100</div>
93
+ </div>
94
+ <% end %>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <% end %>
99
+
100
+ <%# ─── Hourly Heatmap ──────────────────────────────────────── %>
101
+ <% if @hourly_heatmap&.any? %>
102
+ <div class="rn-card">
103
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
104
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Hourly Heatmap (7 days)</h2>
105
+ </div>
106
+ <div class="p-4 overflow-x-auto">
107
+ <table class="rn-table" style="font-size: 10px">
108
+ <thead>
109
+ <tr>
110
+ <th></th>
111
+ <% 24.times do |h| %>
112
+ <th style="text-align: center; padding: 2px"><%= h %></th>
113
+ <% end %>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ <% max_heat = @hourly_heatmap.values.flat_map(&:values).max || 1 %>
118
+ <% @hourly_heatmap.each do |day, hours| %>
119
+ <tr>
120
+ <td style="font-weight: 600; padding: 2px"><%= day[0..2] %></td>
121
+ <% 24.times do |h| %>
122
+ <% count = hours[h] || 0 %>
123
+ <% intensity = max_heat > 0 ? (count.to_f / max_heat) : 0 %>
124
+ <td style="text-align: center; padding: 2px">
125
+ <div style="width: 16px; height: 16px; border-radius: 2px; background: <%= count == 0 ? 'var(--rn-bg-secondary)' : "rgba(99, 102, 241, #{[intensity, 0.1].max})" %>; display: inline-flex; align-items: center; justify-content: center; font-size: 8px; color: <%= intensity > 0.5 ? 'white' : 'var(--rn-text-muted)' %>"><%= count if count > 0 %></div>
126
+ </td>
127
+ <% end %>
128
+ </tr>
129
+ <% end %>
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ </div>
134
+ <% end %>
135
+
136
+ <%# ─── Correlation Insights ─────────────────────────────────── %>
137
+ <% if @correlation_insights %>
138
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
139
+ <%# By Controller %>
140
+ <% if @correlation_insights[:by_controller]&.any? %>
141
+ <div class="rn-card">
142
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
143
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Errors by Controller</h2>
144
+ </div>
145
+ <div class="p-4 space-y-2">
146
+ <% @correlation_insights[:by_controller].each do |item| %>
147
+ <div class="flex items-center gap-3">
148
+ <span class="rn-mono text-xs flex-1 truncate" style="color: var(--rn-text-secondary)"><%= item[:controller] %></span>
149
+ <div class="w-24 h-2 rounded-full overflow-hidden" style="background: var(--rn-bg-tertiary)">
150
+ <div class="h-full rounded-full" style="width: <%= item[:percentage] %>% ; background: var(--rn-primary)"></div>
151
+ </div>
152
+ <span class="text-xs font-medium" style="color: var(--rn-text); min-width: 40px; text-align: right"><%= item[:count] %></span>
153
+ <span class="text-xs" style="color: var(--rn-text-muted); min-width: 40px; text-align: right"><%= item[:percentage] %>%</span>
154
+ </div>
155
+ <% end %>
156
+ </div>
157
+ </div>
158
+ <% end %>
159
+
160
+ <%# By Exception Class %>
161
+ <% if @correlation_insights[:by_exception_class]&.any? %>
162
+ <div class="rn-card">
163
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
164
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Errors by Type</h2>
165
+ </div>
166
+ <div class="p-4 space-y-2">
167
+ <% @correlation_insights[:by_exception_class].each do |item| %>
168
+ <div class="flex items-center gap-3">
169
+ <span class="text-xs flex-1 truncate" style="color: var(--rn-text-secondary)"><%= item[:exception_class] %></span>
170
+ <div class="w-24 h-2 rounded-full overflow-hidden" style="background: var(--rn-bg-tertiary)">
171
+ <div class="h-full rounded-full" style="width: <%= item[:percentage] %>% ; background: var(--rn-danger)"></div>
172
+ </div>
173
+ <span class="text-xs font-medium" style="color: var(--rn-text); min-width: 40px; text-align: right"><%= item[:count] %></span>
174
+ </div>
175
+ <% end %>
176
+ </div>
177
+ </div>
178
+ <% end %>
179
+
180
+ <%# Time of Day Distribution %>
181
+ <% if @correlation_insights[:by_time_of_day]&.any? %>
182
+ <div class="rn-card">
183
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
184
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Errors by Hour</h2>
185
+ </div>
186
+ <div class="p-4">
187
+ <% max_hourly = @correlation_insights[:by_time_of_day].map { |h| h[:count] }.max || 1 %>
188
+ <div class="flex items-end gap-0.5" style="height: 80px">
189
+ <% @correlation_insights[:by_time_of_day].each do |hour| %>
190
+ <div class="flex-1 flex flex-col items-center" title="<%= hour[:hour] %>h: <%= hour[:count] %>">
191
+ <div class="w-full rounded-t" style="height: <%= [(hour[:count].to_f / max_hourly * 100), 1].max %>% ; min-height: 1px; background: var(--rn-info); opacity: 0.7"></div>
192
+ <span class="text-[8px]" style="color: var(--rn-text-muted)"><%= hour[:hour] %></span>
193
+ </div>
194
+ <% end %>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ <% end %>
199
+
200
+ <%# Co-occurring Errors %>
201
+ <% if @correlation_insights[:co_occurring]&.any? %>
202
+ <div class="rn-card">
203
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
204
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Co-occurring Errors</h2>
205
+ <p class="text-xs" style="color: var(--rn-text-muted)">Errors happening within 5 minutes of each other</p>
206
+ </div>
207
+ <div class="p-4 space-y-3">
208
+ <% @correlation_insights[:co_occurring].each do |pair| %>
209
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
210
+ <div class="flex items-center gap-2 text-xs">
211
+ <span class="rn-badge rn-badge-danger"><%= pair[:error_a] %></span>
212
+ <span style="color: var(--rn-text-muted)">↔</span>
213
+ <span class="rn-badge rn-badge-danger"><%= pair[:error_b] %></span>
214
+ </div>
215
+ <div class="text-xs mt-1" style="color: var(--rn-text-muted)"><%= pair[:co_occurrences] %> co-occurrences</div>
216
+ </div>
217
+ <% end %>
218
+ </div>
219
+ </div>
220
+ <% end %>
221
+ </div>
222
+ <% end %>
223
+
224
+ <%# ─── Occurrence Patterns ──────────────────────────────────── %>
225
+ <% if @occurrence_patterns && @occurrence_patterns[:patterns] %>
226
+ <div class="rn-card">
227
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
228
+ <div class="flex items-center gap-2">
229
+ <svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
230
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
231
+ </svg>
232
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Occurrence Patterns</h2>
233
+ </div>
234
+ </div>
235
+ <div class="p-4">
236
+ <%# Detected Patterns %>
237
+ <% if @occurrence_patterns[:patterns]&.any? %>
238
+ <div class="space-y-3 mb-4">
239
+ <% @occurrence_patterns[:patterns].each do |pattern| %>
240
+ <div class="flex items-start gap-3 p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
241
+ <span class="w-2 h-2 rounded-full mt-1.5 flex-shrink-0" style="background: <%= case pattern[:severity]
242
+ when :high then 'var(--rn-danger)'
243
+ when :medium then 'var(--rn-warning)'
244
+ when :positive then 'var(--rn-success)'
245
+ else 'var(--rn-text-muted)'
246
+ end %>"></span>
247
+ <div class="flex-1">
248
+ <p class="text-sm font-medium" style="color: var(--rn-text)"><%= pattern[:description] %></p>
249
+ <% if pattern[:recommendation] %>
250
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">💡 <%= pattern[:recommendation] %></p>
251
+ <% end %>
252
+ </div>
253
+ </div>
254
+ <% end %>
255
+ </div>
256
+ <% else %>
257
+ <p class="text-sm mb-4" style="color: var(--rn-text-muted)">No significant patterns detected.</p>
258
+ <% end %>
259
+
260
+ <%# Trend %>
261
+ <% if @occurrence_patterns[:trend] && @occurrence_patterns[:trend][:direction] != :stable %>
262
+ <div class="p-3 rounded-lg mb-4" style="background: <%= @occurrence_patterns[:trend][:direction] == :increasing ? 'var(--rn-bg-danger-subtle)' : 'var(--rn-bg-success-subtle)' %>">
263
+ <div class="flex items-center gap-2">
264
+ <span class="text-lg"><%= @occurrence_patterns[:trend][:direction] == :increasing ? '📈' : '📉' %></span>
265
+ <div>
266
+ <span class="text-sm font-semibold" style="color: var(--rn-text)"><%= @occurrence_patterns[:trend][:direction].to_s.capitalize %> Trend</span>
267
+ <span class="text-xs ml-2" style="color: var(--rn-text-muted)"><%= @occurrence_patterns[:trend][:pct_change] %>% change</span>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ <% end %>
272
+
273
+ <%# Hotspots + Cycles side by side %>
274
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
275
+ <%# Hotspots %>
276
+ <% if @occurrence_patterns[:hotspots]&.any? %>
277
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
278
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">🔥 Error Hotspots</h3>
279
+ <div class="space-y-2">
280
+ <% @occurrence_patterns[:hotspots].each do |hotspot| %>
281
+ <div class="flex items-center justify-between">
282
+ <span class="text-xs" style="color: var(--rn-text)"><%= hotspot[:label] %></span>
283
+ <span class="text-xs font-semibold" style="color: var(--rn-danger)"><%= hotspot[:avg_errors] %> avg</span>
284
+ </div>
285
+ <% end %>
286
+ </div>
287
+ </div>
288
+ <% end %>
289
+
290
+ <%# Cycle Info %>
291
+ <% cycle = @occurrence_patterns[:cycle_info] %>
292
+ <% if cycle && cycle[:detected] %>
293
+ <div class="p-3 rounded-lg" style="background: var(--rn-bg-secondary)">
294
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">🔄 Cycle Detected</h3>
295
+ <p class="text-sm" style="color: var(--rn-text)"><%= cycle[:description] %></p>
296
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Peak: <%= cycle[:peak_hour] %>:00 · Off-peak: <%= cycle[:off_peak_hour] %>:00</p>
297
+ </div>
298
+ <% end %>
299
+ </div>
300
+
301
+ <%# Burst Periods %>
302
+ <% if @occurrence_patterns[:burst_periods]&.any? %>
303
+ <div class="mt-4">
304
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-2" style="color: var(--rn-text-muted)">⚡ Burst Periods</h3>
305
+ <div class="rn-table-wrap">
306
+ <table class="rn-table">
307
+ <thead>
308
+ <tr>
309
+ <th>Time</th>
310
+ <th>Count</th>
311
+ <th>Spike Ratio</th>
312
+ <th>Severity</th>
313
+ </tr>
314
+ </thead>
315
+ <tbody>
316
+ <% @occurrence_patterns[:burst_periods].each do |burst| %>
317
+ <tr>
318
+ <td><span class="text-xs"><%= burst[:time] %></span></td>
319
+ <td><span class="font-bold" style="color: var(--rn-danger)"><%= burst[:count] %></span></td>
320
+ <td><span class="text-xs"><%= burst[:spike_ratio] %>x</span></td>
321
+ <td><span class="rn-badge rn-badge-<%= burst[:severity] == :critical ? 'danger' : burst[:severity] == :high ? 'warning' : 'info' %>"><%= burst[:severity] %></span></td>
322
+ </tr>
323
+ <% end %>
324
+ </tbody>
325
+ </table>
326
+ </div>
327
+ </div>
328
+ <% end %>
329
+ </div>
330
+ </div>
331
+ <% end %>
332
+
333
+ <%# ─── Top Errors ──────────────────────────────────────────── %>
334
+ <% if @top_errors&.any? %>
335
+ <div class="rn-card">
336
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
337
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Top Errors</h2>
338
+ </div>
339
+ <div class="rn-table-wrap">
340
+ <table class="rn-table">
341
+ <thead>
342
+ <tr>
343
+ <th>Exception</th>
344
+ <th>Controller#Action</th>
345
+ <th>Count</th>
346
+ <th>Last Seen</th>
347
+ </tr>
348
+ </thead>
349
+ <tbody>
350
+ <% @top_errors.each do |error| %>
351
+ <tr>
352
+ <td><span class="rn-badge rn-badge-danger"><%= error[:exception_class] %></span></td>
353
+ <td><span class="rn-mono text-xs" style="color: var(--rn-text-secondary)"><%= error[:controller] %>#<%= error[:action] %></span></td>
354
+ <td><span class="font-bold" style="color: var(--rn-text)"><%= error[:count] %></span></td>
355
+ <td><span class="text-xs" style="color: var(--rn-text-muted)"><%= error[:last_seen]&.strftime("%b %d %H:%M") %></span></td>
356
+ </tr>
357
+ <% end %>
358
+ </tbody>
359
+ </table>
360
+ </div>
361
+ </div>
362
+ <% end %>
363
+ </div>
@@ -0,0 +1,53 @@
1
+ <% page_title t(".title", default: "Backup Files") %>
2
+
3
+ <div class="space-y-6">
4
+ <%# ─── Header ──────────────────────────────────────────────── %>
5
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
+ <span class="rn-breadcrumb-sep">/</span>
8
+ <a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
9
+ <span class="rn-breadcrumb-sep">/</span>
10
+ <span class="rn-breadcrumb-current">Files</span>
11
+ </nav>
12
+
13
+ <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Files</h1>
14
+
15
+ <%# ─── Files Table ─────────────────────────────────────────── %>
16
+ <div class="rn-card">
17
+ <% if @files.any? %>
18
+ <div class="rn-table-wrap">
19
+ <table class="rn-table">
20
+ <thead>
21
+ <tr>
22
+ <th>File</th>
23
+ <th>Size</th>
24
+ <th>Created</th>
25
+ <th>Age</th>
26
+ <th>Format</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <% @files.each do |file| %>
31
+ <tr>
32
+ <td>
33
+ <span class="rn-mono text-xs" style="color: var(--rn-text)"><%= file[:name] %></span>
34
+ </td>
35
+ <td><span class="text-sm" style="color: var(--rn-text-secondary)"><%= file[:size_human] %></span></td>
36
+ <td><span class="text-xs" style="color: var(--rn-text-muted)"><%= file[:created_at].strftime("%b %d, %Y %I:%M %p") %></span></td>
37
+ <td><span class="text-xs" style="color: var(--rn-text-muted)"><%= file[:age_hours] %>h</span></td>
38
+ <td>
39
+ <span class="rn-badge <%= file[:encrypted] ? 'rn-badge-success' : '' %>"><%= file[:encrypted] ? 'Encrypted' : 'Plain' %></span>
40
+ <span class="rn-badge <%= file[:compressed] ? 'rn-badge-info' : '' %>"><%= file[:compressed] ? 'Gzipped' : 'Raw' %></span>
41
+ </td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ </div>
47
+ <% else %>
48
+ <div class="p-8 text-center">
49
+ <p class="text-sm" style="color: var(--rn-text-muted)">No backup files found.</p>
50
+ </div>
51
+ <% end %>
52
+ </div>
53
+ </div>
@@ -0,0 +1,211 @@
1
+ <% page_title t(".title", default: "Backup Management") %>
2
+
3
+ <div class="space-y-6">
4
+ <%# ─── Header ──────────────────────────────────────────────── %>
5
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
6
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
7
+ <span class="rn-breadcrumb-sep">/</span>
8
+ <span class="rn-breadcrumb-current">Backups</span>
9
+ </nav>
10
+
11
+ <div class="flex items-center justify-between">
12
+ <div class="flex items-center gap-3">
13
+ <svg class="w-6 h-6" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
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>
22
+ </div>
23
+
24
+ <%# ─── Health Status ───────────────────────────────────────── %>
25
+ <% if @health[:status] == "not_configured" %>
26
+ <div class="rn-card" style="border-left: 3px solid var(--rn-text-muted)">
27
+ <div class="p-4 flex items-center gap-3">
28
+ <span class="w-3 h-3 rounded-full" style="background: var(--rn-text-muted)"></span>
29
+ <div>
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>
34
+ </div>
35
+ <% else %>
36
+ <div class="rn-card" style="border-left: 3px solid <%= @health[:status] == 'healthy' ? 'var(--rn-success)' : @health[:status] == 'warning' ? 'var(--rn-warning)' : 'var(--rn-danger)' %>">
37
+ <div class="p-4 flex items-center gap-3">
38
+ <span class="w-3 h-3 rounded-full" style="background: <%= @health[:status] == 'healthy' ? 'var(--rn-success)' : @health[:status] == 'warning' ? 'var(--rn-warning)' : 'var(--rn-danger)' %>"></span>
39
+ <div>
40
+ <span class="text-sm font-semibold" style="color: var(--rn-text)"><%= @health[:message] %></span>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <% end %>
45
+
46
+ <%# ─── Summary Stats ───────────────────────────────────────── %>
47
+ <% if @summary %>
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>
71
+ </div>
72
+ <% end %>
73
+
74
+ <%# ─── Alerts ──────────────────────────────────────────────── %>
75
+ <% if @summary && @summary[:alerts]&.any? %>
76
+ <% @summary[:alerts].each do |alert| %>
77
+ <div class="rn-card" style="border-left: 3px solid <%= alert[:level] == 'critical' ? 'var(--rn-danger)' : alert[:level] == 'warning' ? 'var(--rn-warning)' : 'var(--rn-info)' %>">
78
+ <div class="p-3 flex items-center gap-2">
79
+ <span class="text-sm" style="color: var(--rn-text)"><%= alert[:message] %></span>
80
+ </div>
81
+ </div>
82
+ <% end %>
83
+ <% end %>
84
+
85
+ <%# ─── Backup Models ───────────────────────────────────────── %>
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>
96
+ <div class="rn-table-wrap">
97
+ <table class="rn-table">
98
+ <thead>
99
+ <tr>
100
+ <th>Model</th>
101
+ <th>Database</th>
102
+ <th>Storage</th>
103
+ <th>Encryption</th>
104
+ <th>Keep</th>
105
+ <th style="text-align: right">Actions</th>
106
+ </tr>
107
+ </thead>
108
+ <tbody>
109
+ <% @models.each do |model| %>
110
+ <tr>
111
+ <td>
112
+ <span class="font-medium" style="color: var(--rn-text)"><%= model[:name] %></span>
113
+ <br><span class="text-xs" style="color: var(--rn-text-muted)"><%= model[:description] %></span>
114
+ </td>
115
+ <td><span class="rn-badge <%= model[:has_mysql] ? 'rn-badge-success' : 'rn-badge-muted' %>">MySQL</span></td>
116
+ <td>
117
+ <span class="rn-badge <%= model[:has_local] ? 'rn-badge-info' : '' %>">Local</span>
118
+ <% if model[:has_rsync] %><span class="rn-badge rn-badge-warning">RSync</span><% end %>
119
+ </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
+ <td style="text-align: right">
123
+ <%= button_to "Trigger", backup_trigger_path(model: model[:name]),
124
+ method: :post,
125
+ class: "rn-btn rn-btn-primary rn-btn-xs",
126
+ data: { turbo_confirm: "Run #{model[:name]} backup now?" } %>
127
+ </td>
128
+ </tr>
129
+ <% end %>
130
+ </tbody>
131
+ </table>
132
+ </div>
133
+ </div>
134
+ <% end %>
135
+
136
+ <%# ─── Recent Backups ──────────────────────────────────────── %>
137
+ <% if @backup_service.backup_files.any? %>
138
+ <div class="rn-card">
139
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
140
+ <div class="flex items-center gap-2">
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>
144
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Recent Backups</h2>
145
+ </div>
146
+ </div>
147
+ <div class="rn-table-wrap">
148
+ <table class="rn-table">
149
+ <thead>
150
+ <tr>
151
+ <th>File</th>
152
+ <th>Size</th>
153
+ <th>Created</th>
154
+ <th>Status</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody>
158
+ <% @backup_service.backup_files.first(10).each do |file| %>
159
+ <tr>
160
+ <td>
161
+ <span class="rn-mono text-xs" style="color: var(--rn-text)"><%= file[:name] %></span>
162
+ </td>
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' %>
168
+ </span>
169
+ </td>
170
+ </tr>
171
+ <% end %>
172
+ </tbody>
173
+ </table>
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>
204
+ </tr>
205
+ <% end %>
206
+ </tbody>
207
+ </table>
208
+ </div>
209
+ </div>
210
+ <% end %>
211
+ </div>