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,569 @@
1
+ <% page_title t(".title", default: "Server Statistics") %>
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">Server Statistics</span>
9
+ </nav>
10
+
11
+ <div class="flex items-center justify-between">
12
+ <h1 class="text-2xl font-bold" style="color: var(--rn-text)">Server Statistics</h1>
13
+ <button type="button" class="rn-btn rn-btn-ghost rn-btn-sm"
14
+ data-action="click->rails_nexus#refreshStats"
15
+ title="Refresh stats">
16
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
17
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182" />
18
+ </svg>
19
+ Refresh
20
+ </button>
21
+ </div>
22
+
23
+ <%# ─── System Overview Cards ──────────────────────────────── %>
24
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
25
+ <%# Memory Card %>
26
+ <div class="rn-card">
27
+ <div class="p-4">
28
+ <div class="flex items-center gap-2 mb-2">
29
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-primary-light)">
30
+ <svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
31
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25z" />
32
+ </svg>
33
+ </div>
34
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Memory</span>
35
+ </div>
36
+ <div class="text-2xl font-bold" style="color: var(--rn-text)"><%= @system_stats[:memory][:used_mb] %> MB</div>
37
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)">
38
+ of <%= @system_stats[:memory][:total_mb] %> MB total
39
+ </div>
40
+ <div class="mt-2 h-2 rounded-full overflow-hidden" style="background: var(--rn-bg-tertiary)">
41
+ <div class="h-full rounded-full transition-all duration-300" style="width: <%= @system_stats[:memory][:usage_percent] %>% ; background: <%= @system_stats[:memory][:usage_percent] > 80 ? 'var(--rn-danger)' : @system_stats[:memory][:usage_percent] > 60 ? 'var(--rn-warning)' : 'var(--rn-success)' %>"></div>
42
+ </div>
43
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)"><%= @system_stats[:memory][:usage_percent] %>% used</div>
44
+ </div>
45
+ </div>
46
+
47
+ <%# CPU Card %>
48
+ <div class="rn-card">
49
+ <div class="p-4">
50
+ <div class="flex items-center gap-2 mb-2">
51
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-warning-light)">
52
+ <svg class="w-4 h-4" style="color: var(--rn-warning)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
53
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25z" />
54
+ </svg>
55
+ </div>
56
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">CPU Load</span>
57
+ </div>
58
+ <div class="text-2xl font-bold" style="color: var(--rn-text)"><%= @system_stats[:cpu][:load_1m] %></div>
59
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)">
60
+ <%= @system_stats[:cpu][:load_5m] %> / <%= @system_stats[:cpu][:load_15m] %> (5m/15m)
61
+ </div>
62
+ <div class="mt-2 text-xs" style="color: var(--rn-text-muted)">
63
+ <%= @system_stats[:cpu][:cores] %> core<%= @system_stats[:cpu][:cores] == 1 ? '' : 's' %>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <%# Disk Card %>
69
+ <div class="rn-card">
70
+ <div class="p-4">
71
+ <div class="flex items-center gap-2 mb-2">
72
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-success-light)">
73
+ <svg class="w-4 h-4" style="color: var(--rn-success)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
74
+ <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" />
75
+ </svg>
76
+ </div>
77
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Disk</span>
78
+ </div>
79
+ <div class="text-2xl font-bold" style="color: var(--rn-text)"><%= @system_stats[:disk][:used_gb] %> GB</div>
80
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)">
81
+ of <%= @system_stats[:disk][:total_gb] %> GB total
82
+ </div>
83
+ <div class="mt-2 h-2 rounded-full overflow-hidden" style="background: var(--rn-bg-tertiary)">
84
+ <div class="h-full rounded-full transition-all duration-300" style="width: <%= @system_stats[:disk][:usage_percent] %>% ; background: <%= @system_stats[:disk][:usage_percent] > 90 ? 'var(--rn-danger)' : @system_stats[:disk][:usage_percent] > 70 ? 'var(--rn-warning)' : 'var(--rn-success)' %>"></div>
85
+ </div>
86
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)"><%= @system_stats[:disk][:usage_percent] %>% used</div>
87
+ </div>
88
+ </div>
89
+
90
+ <%# Exceptions Card %>
91
+ <div class="rn-card">
92
+ <div class="p-4">
93
+ <div class="flex items-center gap-2 mb-2">
94
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-danger-light)">
95
+ <svg class="w-4 h-4" style="color: var(--rn-danger)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
96
+ <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" />
97
+ </svg>
98
+ </div>
99
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Exceptions</span>
100
+ </div>
101
+ <div class="text-2xl font-bold" style="color: var(--rn-text)"><%= @exception_stats[:total] %></div>
102
+ <div class="mt-1 text-xs" style="color: var(--rn-text-muted)">
103
+ <%= @exception_stats[:today] %> today
104
+ </div>
105
+ <div class="mt-2 text-xs" style="color: var(--rn-text-muted)">
106
+ <%= @exception_stats[:unique_classes] %> unique types
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <%# ─── Environment Details ─────────────────────────────────── %>
113
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
114
+ <%# Ruby & Rails Info %>
115
+ <div class="rn-card">
116
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
117
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Environment</h2>
118
+ </div>
119
+ <div class="p-4 space-y-3">
120
+ <div class="flex justify-between text-sm">
121
+ <span style="color: var(--rn-text-muted)">Ruby Version</span>
122
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @ruby_stats[:ruby_version] %></span>
123
+ </div>
124
+ <div class="flex justify-between text-sm">
125
+ <span style="color: var(--rn-text-muted)">Platform</span>
126
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @ruby_stats[:ruby_platform] %></span>
127
+ </div>
128
+ <div class="flex justify-between text-sm">
129
+ <span style="color: var(--rn-text-muted)">Rails Version</span>
130
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @ruby_stats[:rails_version] %></span>
131
+ </div>
132
+ <div class="flex justify-between text-sm">
133
+ <span style="color: var(--rn-text-muted)">Environment</span>
134
+ <span class="rn-badge rn-badge-<%= Rails.env.production? ? 'danger' : 'info' %>"><%= @ruby_stats[:environment] %></span>
135
+ </div>
136
+ <div class="flex justify-between text-sm">
137
+ <span style="color: var(--rn-text-muted)">Database Adapter</span>
138
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @ruby_stats[:database_adapter] %></span>
139
+ </div>
140
+ <div class="flex justify-between text-sm">
141
+ <span style="color: var(--rn-text-muted)">RailsNexus Version</span>
142
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @ruby_stats[:rails_nexus_version] %></span>
143
+ </div>
144
+ <div class="flex justify-between text-sm">
145
+ <span style="color: var(--rn-text-muted)">Timezone</span>
146
+ <span class="font-medium" style="color: var(--rn-text)"><%= @ruby_stats[:timezone] %></span>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <%# Sidekiq Stats %>
152
+ <% if @sidekiq_stats && !@sidekiq_stats[:error] %>
153
+ <div class="rn-card">
154
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
155
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Sidekiq</h2>
156
+ </div>
157
+ <div class="p-4 space-y-3">
158
+ <div class="flex justify-between text-sm">
159
+ <span style="color: var(--rn-text-muted)">Processed</span>
160
+ <span class="font-medium" style="color: var(--rn-success)"><%= number_with_delimiter(@sidekiq_stats[:processed]) %></span>
161
+ </div>
162
+ <div class="flex justify-between text-sm">
163
+ <span style="color: var(--rn-text-muted)">Failed</span>
164
+ <span class="font-medium" style="color: var(--rn-danger)"><%= number_with_delimiter(@sidekiq_stats[:failed]) %></span>
165
+ </div>
166
+ <div class="flex justify-between text-sm">
167
+ <span style="color: var(--rn-text-muted)">Enqueued</span>
168
+ <span class="font-medium" style="color: var(--rn-text)"><%= number_with_delimiter(@sidekiq_stats[:enqueued]) %></span>
169
+ </div>
170
+ <div class="flex justify-between text-sm">
171
+ <span style="color: var(--rn-text-muted)">Workers</span>
172
+ <span class="font-medium" style="color: var(--rn-text)"><%= @sidekiq_stats[:workers] %></span>
173
+ </div>
174
+ <div class="flex justify-between text-sm">
175
+ <span style="color: var(--rn-text-muted)">Retry Queue</span>
176
+ <span class="font-medium" style="color: var(--rn-warning)"><%= @sidekiq_stats[:retry_size] %></span>
177
+ </div>
178
+ <div class="flex justify-between text-sm">
179
+ <span style="color: var(--rn-text-muted)">Scheduled</span>
180
+ <span class="font-medium" style="color: var(--rn-text)"><%= @sidekiq_stats[:scheduled_size] %></span>
181
+ </div>
182
+ <div class="flex justify-between text-sm">
183
+ <span style="color: var(--rn-text-muted)">Redis Memory</span>
184
+ <span class="font-medium rn-mono" style="color: var(--rn-text)"><%= @sidekiq_stats[:redis_memory_mb] %> MB</span>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <% elsif @sidekiq_stats && @sidekiq_stats[:error] %>
189
+ <div class="rn-card">
190
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
191
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Sidekiq</h2>
192
+ </div>
193
+ <div class="p-4">
194
+ <p class="text-sm" style="color: var(--rn-text-muted)">Not available: <%= @sidekiq_stats[:error] %></p>
195
+ </div>
196
+ </div>
197
+ <% end %>
198
+ </div>
199
+
200
+ <%# ─── Process Info ─────────────────────────────────────────── %>
201
+ <div class="rn-card">
202
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
203
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Process Information</h2>
204
+ </div>
205
+ <div class="p-4">
206
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
207
+ <div>
208
+ <div class="text-xs" style="color: var(--rn-text-muted)">PID</div>
209
+ <div class="text-sm font-medium rn-mono" style="color: var(--rn-text)"><%= @system_stats[:process][:pid] %></div>
210
+ </div>
211
+ <div>
212
+ <div class="text-xs" style="color: var(--rn-text-muted)">Parent PID</div>
213
+ <div class="text-sm font-medium rn-mono" style="color: var(--rn-text)"><%= @system_stats[:process][:ppid] %></div>
214
+ </div>
215
+ <div>
216
+ <div class="text-xs" style="color: var(--rn-text-muted)">Threads</div>
217
+ <div class="text-sm font-medium" style="color: var(--rn-text)"><%= @system_stats[:process][:thread_count] %></div>
218
+ </div>
219
+ <div>
220
+ <div class="text-xs" style="color: var(--rn-text-muted)">Open Files</div>
221
+ <div class="text-sm font-medium" style="color: var(--rn-text)"><%= @system_stats[:process][:open_files] %></div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <%# ─── Storm Protection ──────────────────────────────────── %>
228
+ <% if @storm_stats && @storm_stats[:enabled] %>
229
+ <div class="rn-card">
230
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
231
+ <div class="flex items-center gap-2">
232
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: <%= @storm_stats[:circuit_open] ? 'var(--rn-danger-light)' : 'var(--rn-success-light)' %>">
233
+ <svg class="w-4 h-4" style="color: <%= @storm_stats[:circuit_open] ? 'var(--rn-danger)' : 'var(--rn-success)' %>" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
234
+ <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" />
235
+ </svg>
236
+ </div>
237
+ <div>
238
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Storm Protection</h2>
239
+ <p class="text-xs" style="color: var(--rn-text-muted)">
240
+ Circuit breaker: <%= @storm_stats[:circuit_open] ? 'OPEN (shedding errors)' : 'Closed (capturing all)' %>
241
+ </p>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ <div class="p-4">
246
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
247
+ <div>
248
+ <div class="text-xs" style="color: var(--rn-text-muted)">Status</div>
249
+ <div class="flex items-center gap-2 mt-1">
250
+ <span class="w-2 h-2 rounded-full" style="background: <%= @storm_stats[:circuit_open] ? 'var(--rn-danger)' : 'var(--rn-success)' %>"></span>
251
+ <span class="text-sm font-medium" style="color: var(--rn-text)"><%= @storm_stats[:circuit_open] ? 'Storm Active' : 'Normal' %></span>
252
+ </div>
253
+ </div>
254
+ <div>
255
+ <div class="text-xs" style="color: var(--rn-text-muted)">Errors/sec</div>
256
+ <div class="text-lg font-bold" style="color: var(--rn-text)"><%= @storm_stats[:errors_this_second] %></div>
257
+ <div class="text-xs" style="color: var(--rn-text-muted)">threshold: <%= @storm_stats[:threshold] %></div>
258
+ </div>
259
+ <div>
260
+ <div class="text-xs" style="color: var(--rn-text-muted)">Captured</div>
261
+ <div class="text-lg font-bold" style="color: var(--rn-success)"><%= @storm_stats[:total_captured] %></div>
262
+ </div>
263
+ <div>
264
+ <div class="text-xs" style="color: var(--rn-text-muted)">Shed</div>
265
+ <div class="text-lg font-bold" style="color: var(--rn-danger)"><%= @storm_stats[:total_shed] %></div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ <% end %>
271
+
272
+ <%# ─── Database Health ─────────────────────────────────────── %>
273
+ <% if @database_stats && !@database_stats[:error] %>
274
+ <div class="rn-card">
275
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
276
+ <div class="flex items-center gap-2">
277
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-info-light)">
278
+ <svg class="w-4 h-4" style="color: var(--rn-info)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
279
+ <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" />
280
+ </svg>
281
+ </div>
282
+ <div>
283
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Database Health</h2>
284
+ <p class="text-xs" style="color: var(--rn-text-muted)"><%= @database_stats[:adapter] %> · <%= @database_stats[:version]&.truncate(60) %></p>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ <div class="p-4">
289
+ <%# Connection Stats %>
290
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
291
+ <div>
292
+ <div class="text-xs" style="color: var(--rn-text-muted)">Ping</div>
293
+ <div class="text-lg font-bold rn-mono" style="color: <%= @database_stats[:ping_ms] > 100 ? 'var(--rn-danger)' : @database_stats[:ping_ms] > 50 ? 'var(--rn-warning)' : 'var(--rn-success)' %>">
294
+ <%= @database_stats[:ping_ms] %>ms
295
+ </div>
296
+ </div>
297
+ <div>
298
+ <div class="text-xs" style="color: var(--rn-text-muted)">Active Connections</div>
299
+ <div class="text-lg font-bold" style="color: var(--rn-text)">
300
+ <%= @database_stats[:active_connections] %>
301
+ </div>
302
+ </div>
303
+ <div>
304
+ <div class="text-xs" style="color: var(--rn-text-muted)">Pool Size</div>
305
+ <div class="text-lg font-bold" style="color: var(--rn-text)">
306
+ <%= @database_stats[:pool_connections] %>/<%= @database_stats[:pool_size] %>
307
+ </div>
308
+ </div>
309
+ <div>
310
+ <div class="text-xs" style="color: var(--rn-text-muted)">Uptime</div>
311
+ <div class="text-lg font-bold" style="color: var(--rn-text)">
312
+ <%= @database_stats[:uptime] %>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <%# Table Stats %>
318
+ <% if @database_stats[:tables]&.any? %>
319
+ <div class="border-t pt-4" style="border-color: var(--rn-border-light)">
320
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">RailsNexus Tables</h3>
321
+ <div class="rn-table-wrap">
322
+ <table class="rn-table">
323
+ <thead>
324
+ <tr>
325
+ <th>Table</th>
326
+ <th>Rows</th>
327
+ <th>Size</th>
328
+ <th>Status</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ <% @database_stats[:tables].each do |table| %>
333
+ <tr>
334
+ <td>
335
+ <span class="rn-mono text-sm" style="color: var(--rn-text-secondary)"><%= table[:name] %></span>
336
+ </td>
337
+ <td>
338
+ <span class="font-medium" style="color: var(--rn-text)"><%= number_with_delimiter(table[:row_count]) %></span>
339
+ </td>
340
+ <td>
341
+ <span class="text-sm" style="color: var(--rn-text-muted)">
342
+ <%= table[:size_mb] > 0 ? "#{table[:size_mb]} MB" : "—" %>
343
+ </span>
344
+ </td>
345
+ <td>
346
+ <span class="rn-badge rn-badge-<%= table[:status] == 'active' ? 'success' : 'muted' %>">
347
+ <%= table[:status].capitalize %>
348
+ </span>
349
+ </td>
350
+ </tr>
351
+ <% end %>
352
+ </tbody>
353
+ </table>
354
+ </div>
355
+ </div>
356
+ <% end %>
357
+ </div>
358
+ </div>
359
+ <% elsif @database_stats && @database_stats[:error] %>
360
+ <div class="rn-card">
361
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
362
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Database Health</h2>
363
+ </div>
364
+ <div class="p-4">
365
+ <p class="text-sm" style="color: var(--rn-danger)"><%= @database_stats[:error] %></p>
366
+ </div>
367
+ </div>
368
+ <% end %>
369
+
370
+ <%# ─── Backup Health ──────────────────────────────────────── %>
371
+ <% if @backup_stats && @backup_stats[:detected] %>
372
+ <div class="rn-card">
373
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
374
+ <div class="flex items-center gap-2">
375
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-info-light)">
376
+ <svg class="w-4 h-4" style="color: var(--rn-info)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
377
+ <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
378
+ </svg>
379
+ </div>
380
+ <div>
381
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
382
+ <p class="text-xs" style="color: var(--rn-text-muted)"><%= @backup_stats[:gem] %> <%= @backup_stats[:version] ? "v#{@backup_stats[:version]}" : '' %></p>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ <div class="p-4">
387
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
388
+ <div>
389
+ <div class="text-xs" style="color: var(--rn-text-muted)">Status</div>
390
+ <div class="flex items-center gap-2 mt-1">
391
+ <span class="w-2 h-2 rounded-full" style="background: <%= case @backup_stats[:status]
392
+ when 'healthy' then 'var(--rn-success)'
393
+ when 'warning' then 'var(--rn-warning)'
394
+ when 'stale' then 'var(--rn-danger)'
395
+ when 'configured' then 'var(--rn-info)'
396
+ else 'var(--rn-text-muted)'
397
+ end %>"></span>
398
+ <span class="text-sm font-medium" style="color: var(--rn-text)"><%= @backup_stats[:status].capitalize %></span>
399
+ </div>
400
+ </div>
401
+ <div>
402
+ <div class="text-xs" style="color: var(--rn-text-muted)">Last Backup</div>
403
+ <div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
404
+ <% if @backup_stats[:last_backup] %>
405
+ <%= @backup_stats[:last_backup].strftime("%b %d, %Y %I:%M %p") %>
406
+ <% else %>
407
+ <span style="color: var(--rn-text-muted)">Unknown</span>
408
+ <% end %>
409
+ </div>
410
+ </div>
411
+ <div>
412
+ <div class="text-xs" style="color: var(--rn-text-muted)">Tool</div>
413
+ <div class="text-sm font-medium mt-1" style="color: var(--rn-text)"><%= @backup_stats[:gem] %></div>
414
+ </div>
415
+ <div>
416
+ <div class="text-xs" style="color: var(--rn-text-muted)">Config</div>
417
+ <div class="text-sm font-medium mt-1" style="color: var(--rn-text)">
418
+ <%= @backup_stats[:status] == 'configured' || @backup_stats[:status] == 'scheduled' ? 'Yes' : 'No' %>
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ <%# Backup Details %>
424
+ <% if @backup_stats[:details].any? %>
425
+ <div class="border-t pt-4" style="border-color: var(--rn-border-light)">
426
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Details</h3>
427
+ <div class="space-y-2">
428
+ <% @backup_stats[:details].each do |key, value| %>
429
+ <% next if value.is_a?(Hash) && value.empty? %>
430
+ <div class="flex items-start gap-3 text-sm">
431
+ <span class="rn-mono font-medium" style="color: var(--rn-text-secondary); min-width: 120px"><%= key.to_s.humanize %>:</span>
432
+ <% if value.is_a?(Array) %>
433
+ <div class="flex-1">
434
+ <% value.each do |item| %>
435
+ <div class="text-xs" style="color: var(--rn-text-muted)">
436
+ <% if item.is_a?(Hash) %>
437
+ <%= item[:name] %> <%= item[:schedule] ? "(#{item[:schedule]})" : '' %>
438
+ <% else %>
439
+ <%= item %>
440
+ <% end %>
441
+ </div>
442
+ <% end %>
443
+ </div>
444
+ <% elsif value.is_a?(Hash) %>
445
+ <div class="flex-1">
446
+ <% value.each do |k, v| %>
447
+ <div class="text-xs" style="color: var(--rn-text-muted)"><%= k %>: <%= v %></div>
448
+ <% end %>
449
+ </div>
450
+ <% else %>
451
+ <span class="flex-1 text-xs" style="color: var(--rn-text-muted)"><%= value %></span>
452
+ <% end %>
453
+ </div>
454
+ <% end %>
455
+ </div>
456
+ </div>
457
+ <% end %>
458
+ </div>
459
+ </div>
460
+ <% elsif @backup_stats && !@backup_stats[:detected] %>
461
+ <div class="rn-card">
462
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
463
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Status</h2>
464
+ </div>
465
+ <div class="p-4">
466
+ <p class="text-sm" style="color: var(--rn-text-muted)">No backup solution detected. Consider adding a backup gem like <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">backup</code> or configuring <code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">pg_dump</code>/<code class="rn-mono text-xs" style="background: var(--rn-bg-secondary); padding: 2px 6px; border-radius: 4px">mysqldump</code>.</p>
467
+ </div>
468
+ </div>
469
+ <% end %>
470
+
471
+ <%# ─── Platform Breakdown ──────────────────────────────────── %>
472
+ <% if @platform_stats && @platform_stats[:breakdown]&.any? %>
473
+ <div class="rn-card">
474
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
475
+ <div class="flex items-center gap-2">
476
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background: var(--rn-info-light)">
477
+ <svg class="w-4 h-4" style="color: var(--rn-info)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
478
+ <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
479
+ </svg>
480
+ </div>
481
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Platform Breakdown</h2>
482
+ </div>
483
+ </div>
484
+ <div class="p-4">
485
+ <div class="grid grid-cols-2 md:grid-cols-5 gap-4">
486
+ <% total = @platform_stats[:breakdown]&.sum(&:total) || 1 %>
487
+ <% [
488
+ { key: "web", label: "Web", icon: "🌐", color: "var(--rn-primary)" },
489
+ { key: "ios", label: "iOS", icon: "📱", color: "var(--rn-success)" },
490
+ { key: "android", label: "Android", icon: "🤖", color: "var(--rn-warning)" },
491
+ { key: "api", label: "API", icon: "⚡", color: "var(--rn-info)" },
492
+ { key: "bot", label: "Bots", icon: "🕷️", color: "var(--rn-text-muted)" }
493
+ ].each do |platform| %>
494
+ <% stats = @platform_stats[platform[:key].to_sym] %>
495
+ <% count = stats&.total || 0 %>
496
+ <% pct = total > 0 ? (count.to_f / total * 100).round(1) : 0 %>
497
+ <div class="p-3 rounded-lg text-center" style="background: var(--rn-bg-secondary)">
498
+ <div class="text-2xl mb-1"><%= platform[:icon] %></div>
499
+ <div class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)"><%= platform[:label] %></div>
500
+ <div class="text-xl font-bold mt-1" style="color: <%= platform[:color] %>"><%= number_with_delimiter(count) %></div>
501
+ <div class="text-xs" style="color: var(--rn-text-muted)"><%= pct %>%</div>
502
+ <% if stats %>
503
+ <div class="text-xs mt-1" style="color: var(--rn-text-muted)"><%= stats.unique_classes %> types</div>
504
+ <% end %>
505
+ </div>
506
+ <% end %>
507
+ </div>
508
+
509
+ <%# Platform bar chart %>
510
+ <div class="mt-6">
511
+ <h3 class="text-xs font-semibold uppercase tracking-wider mb-3" style="color: var(--rn-text-muted)">Distribution</h3>
512
+ <div class="flex rounded-full overflow-hidden" style="height: 8px">
513
+ <% [
514
+ { key: "web", color: "var(--rn-primary)" },
515
+ { key: "ios", color: "var(--rn-success)" },
516
+ { key: "android", color: "var(--rn-warning)" },
517
+ { key: "api", color: "var(--rn-info)" },
518
+ { key: "bot", color: "var(--rn-text-muted)" }
519
+ ].each do |platform| %>
520
+ <% stats = @platform_stats[platform[:key].to_sym] %>
521
+ <% pct = total > 0 ? ((stats&.total || 0).to_f / total * 100) : 0 %>
522
+ <% if pct > 0 %>
523
+ <div style="width: <%= pct %>% ; background: <%= platform[:color] %>" title="<%= platform[:key].capitalize %>: <%= number_with_delimiter(stats&.total || 0) %>"></div>
524
+ <% end %>
525
+ <% end %>
526
+ </div>
527
+ <div class="flex justify-between mt-2">
528
+ <% [
529
+ { key: "web", label: "Web", color: "var(--rn-primary)" },
530
+ { key: "ios", label: "iOS", color: "var(--rn-success)" },
531
+ { key: "android", label: "Android", color: "var(--rn-warning)" },
532
+ { key: "api", label: "API", color: "var(--rn-info)" },
533
+ { key: "bot", label: "Bot", color: "var(--rn-text-muted)" }
534
+ ].each do |platform| %>
535
+ <% if @platform_stats[platform[:key].to_sym] %>
536
+ <div class="flex items-center gap-1">
537
+ <span class="w-2 h-2 rounded-full" style="background: <%= platform[:color] %>"></span>
538
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= platform[:label] %></span>
539
+ </div>
540
+ <% end %>
541
+ <% end %>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ </div>
546
+ <% end %>
547
+
548
+ <%# ─── Exception Trend ─────────────────────────────────────── %>
549
+ <% if @exception_stats[:hourly_trend].any? %>
550
+ <div class="rn-card">
551
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
552
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">Exception Trend (24h)</h2>
553
+ </div>
554
+ <div class="p-4">
555
+ <div class="flex items-end gap-1" style="height: 120px">
556
+ <% max_count = @exception_stats[:hourly_trend].map { |h| h[:count] }.max || 1 %>
557
+ <% @exception_stats[:hourly_trend].each do |hour_data| %>
558
+ <div class="flex-1 flex flex-col items-center gap-1">
559
+ <div class="w-full rounded-t transition-all duration-300"
560
+ style="height: <%= [(hour_data[:count].to_f / max_count * 100), 2].max %>% ; min-height: 2px; background: var(--rn-primary); opacity: 0.8">
561
+ </div>
562
+ <span class="text-[10px]" style="color: var(--rn-text-muted)"><%= hour_data[:hour].split(":").first %></span>
563
+ </div>
564
+ <% end %>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ <% end %>
569
+ </div>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RailsNexus has its own entry point, so it never replaces the host app's
4
+ # "application" pin. Turbo and Stimulus are served from their Rails gems.
5
+ rails_nexus_entrypoint = "rails_nexus/application"
6
+
7
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: rails_nexus_entrypoint
8
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: rails_nexus_entrypoint
9
+ pin rails_nexus_entrypoint, to: "rails_nexus/application.js", preload: rails_nexus_entrypoint
10
+
11
+ # Gem controllers
12
+ pin "rails_nexus/controllers/rails_nexus", to: "controllers/rails_nexus_controller.js", preload: rails_nexus_entrypoint
13
+ pin "rails_nexus/controllers/rails_nexus_detail", to: "controllers/rails_nexus_detail_controller.js", preload: rails_nexus_entrypoint
14
+ pin "rails_nexus/controllers/rails_nexus_sidebar", to: "controllers/rails_nexus_sidebar_controller.js", preload: rails_nexus_entrypoint
15
+ pin "rails_nexus/controllers/rails_nexus_theme", to: "controllers/rails_nexus_theme_controller.js", preload: rails_nexus_entrypoint
@@ -0,0 +1,5 @@
1
+ Time::DATE_FORMATS.merge!(
2
+ :exc_full => "%A, %b %d, %Y at %l:%M %p",
3
+ :exc_date => "%b %d, %Y",
4
+ :exc_time => "%l:%M %p"
5
+ )
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.to_prepare do
4
+ RailsNexus.configure do |config|
5
+ # Protect the dashboard - only admin users can access
6
+ config.auth_block = lambda do |controller|
7
+ controller.current_user&.admin?
8
+ end
9
+
10
+ # Attach additional data to each exception record
11
+ config.exception_data = lambda do |controller|
12
+ {
13
+ user_id: controller.current_user&.id,
14
+ request_id: controller.request.request_id
15
+ }
16
+ end
17
+ end
18
+ end