rails_nexus 2.0.6 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +33 -6
  4. data/SECURITY.md +11 -0
  5. data/THIRD_PARTY_NOTICES.md +14 -0
  6. data/app/assets/config/rails_nexus_manifest.js +1 -1
  7. data/app/assets/javascripts/rails_nexus/application.js +55 -0
  8. data/app/assets/javascripts/rails_nexus/controllers.js +82 -5
  9. data/app/assets/stylesheets/rails_nexus/application.css +170 -60
  10. data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
  11. data/app/controllers/rails_nexus/application_controller.rb +6 -2
  12. data/app/controllers/rails_nexus/backup_controller.rb +22 -19
  13. data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
  14. data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
  15. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
  16. data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
  17. data/app/controllers/rails_nexus/settings_controller.rb +1 -39
  18. data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
  19. data/app/controllers/rails_nexus/stats_controller.rb +43 -43
  20. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  21. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
  22. data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
  23. data/app/javascript/controllers/rails_nexus_controller.js +6 -2
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +71 -3
  25. data/app/javascript/rails_nexus/application.js +15 -3
  26. data/app/jobs/rails_nexus/backup_job.rb +18 -0
  27. data/app/models/rails_nexus/backup.rb +11 -11
  28. data/app/models/rails_nexus/backup_config.rb +38 -2
  29. data/app/models/rails_nexus/logged_exception.rb +27 -13
  30. data/app/services/rails_nexus/backup_service.rb +342 -120
  31. data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +40 -10
  32. data/app/views/layouts/rails_nexus/application.html.erb +72 -14
  33. data/app/views/rails_nexus/backup/_form.html.erb +3 -0
  34. data/app/views/rails_nexus/backup/history.html.erb +17 -2
  35. data/app/views/rails_nexus/backup/index.html.erb +29 -72
  36. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +12 -44
  37. data/app/views/rails_nexus/logged_exceptions/index.html.erb +21 -99
  38. data/app/views/rails_nexus/stats/index.html.erb +1 -1
  39. data/db/seeds/backup_configs.rb +5 -4
  40. data/lib/generators/rails_nexus/install_generator.rb +1 -1
  41. data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
  42. data/lib/generators/rails_nexus/templates/migration.rb +8 -2
  43. data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
  44. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
  45. data/lib/rails_nexus/configuration.rb +10 -0
  46. data/lib/rails_nexus/database_adapter.rb +53 -0
  47. data/lib/rails_nexus/engine.rb +0 -13
  48. data/lib/rails_nexus/logger.rb +1 -12
  49. data/lib/rails_nexus/notifications.rb +27 -20
  50. data/lib/rails_nexus/version.rb +1 -1
  51. data/lib/rails_nexus/webhook_client.rb +151 -0
  52. data/lib/rails_nexus.rb +37 -23
  53. data/lib/tasks/rails_nexus.rake +4 -4
  54. data/rails_nexus.gemspec +3 -2
  55. metadata +9 -16
  56. data/config/importmap.rb +0 -15
@@ -33,6 +33,7 @@ table { border-collapse: collapse; width: 100%; }
33
33
  /* ─── Tailwind-like Utility Classes ─────────────────────────── */
34
34
 
35
35
  /* Display */
36
+ [hidden] { display: none !important; }
36
37
  .hidden { display: none !important; }
37
38
  .block { display: block; }
38
39
  .inline-block { display: inline-block; }
@@ -1026,6 +1027,7 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1026
1027
  /* ─── Sidebar ───────────────────────────────────────────────── */
1027
1028
  .rn-sidebar {
1028
1029
  width: 16rem;
1030
+ overflow: hidden;
1029
1031
  background: var(--rn-surface);
1030
1032
  color: var(--rn-text-secondary);
1031
1033
  display: flex;
@@ -1036,6 +1038,13 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1036
1038
  top: 0;
1037
1039
  height: 100vh;
1038
1040
  z-index: 30;
1041
+ transition: width 200ms ease, opacity 150ms ease, border-color 150ms ease;
1042
+ }
1043
+ .rn-sidebar.is-hidden {
1044
+ width: 0;
1045
+ border-right-color: transparent;
1046
+ opacity: 0;
1047
+ pointer-events: none;
1039
1048
  }
1040
1049
  @media (max-width: 1023px) { .rn-sidebar { display: none; } }
1041
1050
 
@@ -1084,41 +1093,104 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1084
1093
 
1085
1094
  .rn-sidebar-nav {
1086
1095
  flex: 1;
1087
- padding: 0.75rem;
1096
+ padding: 0.75rem 0.625rem;
1088
1097
  display: flex;
1089
1098
  flex-direction: column;
1090
- gap: 0.125rem;
1099
+ gap: 0.375rem;
1091
1100
  overflow-y: auto;
1092
1101
  }
1093
1102
  .rn-nav-section {
1103
+ display: block;
1104
+ border: 1px solid transparent;
1105
+ border-radius: 0.625rem;
1106
+ transition: background var(--rn-transition), border-color var(--rn-transition);
1107
+ }
1108
+ .rn-nav-section[open] {
1109
+ border-color: var(--rn-border-light);
1110
+ background: color-mix(in srgb, var(--rn-bg-secondary) 55%, transparent);
1111
+ }
1112
+ .rn-nav-section-toggle {
1113
+ min-height: 2.5rem;
1114
+ padding: 0.375rem 0.5rem;
1115
+ border-radius: 0.5625rem;
1094
1116
  display: flex;
1095
- flex-direction: column;
1096
- gap: 0.125rem;
1117
+ align-items: center;
1118
+ justify-content: space-between;
1119
+ gap: 0.5rem;
1120
+ color: var(--rn-text-secondary);
1121
+ cursor: pointer;
1122
+ font-size: 0.75rem;
1123
+ font-weight: 650;
1124
+ letter-spacing: 0.01em;
1125
+ list-style: none;
1126
+ transition: color var(--rn-transition), background var(--rn-transition);
1127
+ user-select: none;
1097
1128
  }
1098
- .rn-nav-section + .rn-nav-section { margin-top: 0.75rem; }
1099
- .rn-nav-section-label {
1100
- display: block;
1101
- margin: 0.625rem 0.75rem 0.25rem;
1129
+ .rn-nav-section-toggle::-webkit-details-marker { display: none; }
1130
+ .rn-nav-section-toggle:hover,
1131
+ .rn-nav-section-toggle:focus-visible {
1132
+ background: var(--rn-surface-hover);
1133
+ color: var(--rn-text);
1134
+ outline: none;
1135
+ }
1136
+ .rn-nav-section-toggle:focus-visible { box-shadow: inset 0 0 0 2px var(--rn-primary-ring); }
1137
+ .rn-nav-section-title {
1138
+ min-width: 0;
1139
+ display: flex;
1140
+ align-items: center;
1141
+ gap: 0.625rem;
1142
+ }
1143
+ .rn-nav-section-icon {
1144
+ width: 1.625rem;
1145
+ height: 1.625rem;
1146
+ border: 1px solid var(--rn-border);
1147
+ border-radius: 0.5rem;
1148
+ display: inline-flex;
1149
+ align-items: center;
1150
+ justify-content: center;
1151
+ flex-shrink: 0;
1152
+ background: var(--rn-surface);
1102
1153
  color: var(--rn-text-muted);
1103
- font-size: 0.625rem;
1104
- font-weight: 700;
1105
- letter-spacing: 0.1em;
1106
- line-height: 1;
1107
- text-transform: uppercase;
1154
+ box-shadow: var(--rn-shadow-sm);
1155
+ transition: color var(--rn-transition), border-color var(--rn-transition), background var(--rn-transition);
1156
+ }
1157
+ .rn-nav-section-chevron {
1158
+ width: 1.125rem;
1159
+ height: 1.125rem;
1160
+ padding: 0.125rem;
1161
+ border-radius: 999px;
1162
+ flex-shrink: 0;
1163
+ color: var(--rn-text-muted);
1164
+ transition: transform var(--rn-transition);
1165
+ }
1166
+ .rn-nav-section[open] > .rn-nav-section-toggle { color: var(--rn-text); }
1167
+ .rn-nav-section[open] > .rn-nav-section-toggle .rn-nav-section-icon {
1168
+ border-color: color-mix(in srgb, var(--rn-primary) 25%, var(--rn-border));
1169
+ background: var(--rn-primary-light);
1170
+ color: var(--rn-primary);
1171
+ box-shadow: none;
1172
+ }
1173
+ .rn-nav-section[open] > .rn-nav-section-toggle .rn-nav-section-chevron { transform: rotate(90deg); }
1174
+ .rn-nav-section-items {
1175
+ margin: 0.125rem 0.5rem 0.5rem 1.3125rem;
1176
+ padding-left: 0.6875rem;
1177
+ border-left: 1px solid var(--rn-border);
1178
+ display: grid;
1179
+ gap: 0.125rem;
1108
1180
  }
1109
- .rn-sidebar-nav > .rn-nav-section-label:not(:first-child) { margin-top: 1rem; }
1110
1181
 
1111
1182
  .rn-nav-item {
1112
1183
  position: relative;
1184
+ min-height: 2.125rem;
1113
1185
  display: flex;
1114
1186
  align-items: center;
1115
- gap: 0.625rem;
1116
- padding: 0.5rem 0.75rem;
1117
- border-radius: 0.5rem;
1118
- font-size: 0.875rem;
1187
+ gap: 0.5625rem;
1188
+ padding: 0.375rem 0.625rem;
1189
+ border-radius: 0.4375rem;
1190
+ font-size: 0.8125rem;
1119
1191
  font-weight: 500;
1120
1192
  color: var(--rn-text-secondary);
1121
- transition: all var(--rn-transition);
1193
+ transition: color var(--rn-transition), background var(--rn-transition);
1122
1194
  text-decoration: none;
1123
1195
  }
1124
1196
  .rn-nav-item:hover {
@@ -1132,8 +1204,8 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1132
1204
  }
1133
1205
  .rn-nav-item.active::before {
1134
1206
  position: absolute;
1135
- top: 0.625rem;
1136
- bottom: 0.625rem;
1207
+ top: 0.5rem;
1208
+ bottom: 0.5rem;
1137
1209
  left: -0.75rem;
1138
1210
  width: 2px;
1139
1211
  border-radius: 999px;
@@ -1142,7 +1214,6 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1142
1214
  }
1143
1215
  .rn-nav-item svg { flex-shrink: 0; }
1144
1216
  .rn-nav-item--muted {
1145
- font-size: 0.8125rem;
1146
1217
  opacity: 0.8;
1147
1218
  }
1148
1219
  .rn-nav-item--muted:hover {
@@ -1158,6 +1229,10 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1158
1229
  color: var(--rn-text-secondary);
1159
1230
  line-height: 1.25;
1160
1231
  }
1232
+ .rn-nav-item.active .rn-nav-badge {
1233
+ background: color-mix(in srgb, var(--rn-primary) 12%, var(--rn-surface));
1234
+ color: var(--rn-primary);
1235
+ }
1161
1236
  .rn-sidebar-section-divider {
1162
1237
  height: 1px;
1163
1238
  background: var(--rn-border);
@@ -1251,13 +1326,11 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1251
1326
  font-size: 0.6875rem;
1252
1327
  line-height: 1.1;
1253
1328
  }
1254
- .rn-mobile-menu-button { display: none; }
1255
- @media (max-width: 1023px) {
1256
- .rn-mobile-menu-button { display: inline-flex; }
1257
- }
1329
+ .rn-sidebar-toggle { display: inline-flex; }
1258
1330
 
1259
1331
  /* ─── Shell Footer ─────────────────────────────────────────── */
1260
1332
  .rn-footer {
1333
+ position: static;
1261
1334
  min-height: 3.25rem;
1262
1335
  padding: 0.75rem 1.5rem;
1263
1336
  border-top: 1px solid var(--rn-border-light);
@@ -1713,6 +1786,39 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1713
1786
  white-space: nowrap;
1714
1787
  }
1715
1788
 
1789
+ .rn-exceptions-panel { overflow: hidden; }
1790
+ .rn-exceptions-main {
1791
+ min-width: 0;
1792
+ display: flex;
1793
+ flex-direction: column;
1794
+ gap: 0;
1795
+ }
1796
+ .rn-exceptions-results { min-width: 0; }
1797
+ .rn-filter-disclosure { overflow: hidden; }
1798
+ .rn-filter-summary {
1799
+ padding: 0.75rem 0.875rem;
1800
+ display: flex;
1801
+ align-items: center;
1802
+ justify-content: space-between;
1803
+ gap: 0.75rem;
1804
+ color: var(--rn-text-muted);
1805
+ cursor: pointer;
1806
+ list-style: none;
1807
+ user-select: none;
1808
+ }
1809
+ .rn-filter-summary::-webkit-details-marker { display: none; }
1810
+ .rn-filter-summary:hover { background: var(--rn-surface-hover); }
1811
+ .rn-filter-count {
1812
+ padding: 0.1rem 0.4rem;
1813
+ border-radius: 999px;
1814
+ background: var(--rn-primary-light);
1815
+ color: var(--rn-primary);
1816
+ font-size: 0.625rem;
1817
+ font-weight: 700;
1818
+ }
1819
+ .rn-filter-chevron { flex-shrink: 0; transition: transform var(--rn-transition); }
1820
+ .rn-filter-disclosure[open] .rn-filter-chevron { transform: rotate(180deg); }
1821
+ .rn-filter-disclosure[open] .rn-filter-summary { border-bottom: 1px solid var(--rn-border-light); }
1716
1822
  .rn-exceptions-toolbar {
1717
1823
  min-height: 3.25rem;
1718
1824
  padding: 0.625rem 1rem;
@@ -1727,21 +1833,26 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1727
1833
  align-items: center;
1728
1834
  gap: 0.75rem;
1729
1835
  }
1836
+ .rn-exceptions-toolbar-actions { margin-left: auto; }
1730
1837
  .rn-exceptions-table {
1731
1838
  table-layout: fixed;
1732
- min-width: 64rem;
1839
+ width: 100%;
1840
+ min-width: 0;
1733
1841
  }
1734
1842
  .rn-exceptions-table th,
1735
1843
  .rn-exceptions-table td { padding-right: 0.75rem; padding-left: 0.75rem; }
1736
- .rn-exceptions-table .rn-col-severity { width: 1.5rem; padding-right: 0; }
1737
- .rn-exceptions-table .rn-col-exception { width: 10.5rem; }
1738
- .rn-exceptions-table .rn-col-platform { width: 5.5rem; }
1739
- .rn-exceptions-table .rn-col-source { width: 10rem; }
1844
+ .rn-exceptions-table .rn-col-exception { width: 11rem; }
1845
+ .rn-exceptions-table .rn-col-source { width: 9rem; }
1740
1846
  .rn-exceptions-table .rn-col-message { width: auto; }
1741
- .rn-exceptions-table .rn-col-count { width: 4.5rem; text-align: center; }
1742
- .rn-exceptions-table .rn-col-workflow { width: 10.5rem; }
1743
- .rn-exceptions-table .rn-col-time { width: 6.5rem; }
1744
- .rn-exceptions-table .rn-col-actions { width: 4.5rem; }
1847
+ .rn-exceptions-table .rn-col-count { width: 4rem; text-align: center; }
1848
+ .rn-exceptions-table .rn-col-time { width: 6rem; }
1849
+ .rn-exceptions-table .rn-col-actions { width: 4rem; }
1850
+ .rn-exceptions-table .rn-exception-cell {
1851
+ min-width: 0;
1852
+ display: flex;
1853
+ align-items: flex-start;
1854
+ gap: 0.625rem;
1855
+ }
1745
1856
  .rn-exceptions-table .rn-exception-name {
1746
1857
  display: block;
1747
1858
  overflow: hidden;
@@ -1842,6 +1953,9 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1842
1953
  }
1843
1954
 
1844
1955
  @media (max-width: 1100px) {
1956
+ .rn-exceptions-main { width: 100%; }
1957
+ .rn-exceptions-toolbar { flex-wrap: wrap; }
1958
+ .rn-exceptions-toolbar-actions { flex-wrap: wrap; }
1845
1959
  .rn-exceptions-table { display: block; min-width: 0; }
1846
1960
  .rn-exceptions-table thead { display: none; }
1847
1961
  .rn-exceptions-table tbody {
@@ -1855,13 +1969,11 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1855
1969
  border-left: 3px solid transparent;
1856
1970
  border-radius: 0.625rem;
1857
1971
  display: grid;
1858
- grid-template-columns: 0.5rem minmax(0, 1fr) auto auto;
1972
+ grid-template-columns: minmax(0, 1fr) auto auto;
1859
1973
  grid-template-areas:
1860
- "severity exception exception actions"
1861
- "severity message message message"
1862
- "severity source source source"
1863
- "severity platform count time"
1864
- "severity workflow workflow workflow";
1974
+ "exception exception actions"
1975
+ "message message message"
1976
+ "source count time";
1865
1977
  column-gap: 0.75rem;
1866
1978
  row-gap: 0.5rem;
1867
1979
  background: var(--rn-surface);
@@ -1879,13 +1991,10 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1879
1991
  border: 0;
1880
1992
  min-width: 0;
1881
1993
  }
1882
- .rn-exceptions-table .rn-col-severity { grid-area: severity; }
1883
1994
  .rn-exceptions-table .rn-col-exception { grid-area: exception; }
1884
- .rn-exceptions-table .rn-col-platform { grid-area: platform; }
1885
1995
  .rn-exceptions-table .rn-col-source { grid-area: source; }
1886
1996
  .rn-exceptions-table .rn-col-message { grid-area: message; }
1887
1997
  .rn-exceptions-table .rn-col-count { grid-area: count; text-align: left; }
1888
- .rn-exceptions-table .rn-col-workflow { grid-area: workflow; }
1889
1998
  .rn-exceptions-table .rn-col-time { grid-area: time; text-align: right; }
1890
1999
  .rn-exceptions-table .rn-col-actions { grid-area: actions; }
1891
2000
  .rn-exceptions-table .rn-exception-name { font-size: 0.875rem; }
@@ -1897,25 +2006,21 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
1897
2006
  font-size: 0.6875rem;
1898
2007
  }
1899
2008
  .rn-exceptions-table .rn-row-actions { opacity: 1; }
1900
- .rn-exceptions-table .rn-severity-dot { height: 100%; min-height: 1.5rem; margin-top: 0; }
1901
- .rn-exceptions-table .rn-workflow-state { align-items: center; }
1902
2009
  }
1903
2010
 
1904
2011
  @media (max-width: 639px) {
1905
2012
  .rn-exceptions-toolbar { align-items: flex-start; flex-direction: column; }
1906
2013
  .rn-exceptions-toolbar-summary { width: 100%; justify-content: space-between; }
1907
- .rn-exceptions-toolbar-actions { width: 100%; justify-content: flex-end; }
1908
- .rn-exceptions-toolbar-actions > span { display: none; }
2014
+ .rn-exceptions-toolbar-actions { width: 100%; margin-left: 0; justify-content: flex-end; }
2015
+ .rn-shortcuts-hint { display: none; }
1909
2016
  .rn-exceptions-table tbody { padding: 0.5rem; }
1910
2017
  .rn-exceptions-table tbody tr {
1911
- grid-template-columns: 0.5rem minmax(0, 1fr) auto;
2018
+ grid-template-columns: minmax(0, 1fr) auto auto;
1912
2019
  grid-template-areas:
1913
- "severity exception actions"
1914
- "severity message message"
1915
- "severity source source"
1916
- "severity workflow workflow"
1917
- "severity platform count"
1918
- "severity time time";
2020
+ "exception actions actions"
2021
+ "message message message"
2022
+ "source source source"
2023
+ "count time time";
1919
2024
  }
1920
2025
  .rn-exceptions-table .rn-col-time { text-align: left; }
1921
2026
  }
@@ -2222,9 +2327,14 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
2222
2327
  .rn-font-semibold { font-weight: 600; }
2223
2328
 
2224
2329
  /* ─── Layout (override host app resets) ─────────────────── */
2225
- html { height: 100%; }
2226
- body { height: 100vh; overflow: hidden; margin: 0; }
2227
- body > div.min-h-full { height: 100vh; min-height: 100vh; }
2330
+ html { min-height: 100%; }
2331
+ body { min-height: 100%; height: auto; overflow: visible; margin: 0; }
2332
+ body > div.min-h-full { min-height: 100vh; height: auto; }
2228
2333
  body > div.min-h-full > .rn-sidebar { height: 100vh; min-height: 100vh; }
2229
- body > div.min-h-full > div:last-child { display: flex; flex-direction: column; overflow: hidden; }
2230
- body > div.min-h-full > div:last-child > main { flex: 1; overflow-y: auto; }
2334
+ body > div.min-h-full > div:last-child {
2335
+ min-height: 100vh;
2336
+ display: flex;
2337
+ flex-direction: column;
2338
+ overflow: visible;
2339
+ }
2340
+ body > div.min-h-full > div:last-child > main { flex: 1; overflow: visible; }
@@ -2,8 +2,6 @@
2
2
 
3
3
  module RailsNexus
4
4
  class AnalyticsController < ApplicationController
5
- before_action :verify_access
6
-
7
5
  def index
8
6
  @time_range = parse_time_range
9
7
  @error_trends = collect_error_trends
@@ -18,14 +16,6 @@ module RailsNexus
18
16
 
19
17
  private
20
18
 
21
- def verify_access
22
- config = RailsNexus.configuration
23
- return if config.auth_block.nil?
24
- unless config.auth_block&.call(self)
25
- render plain: "Forbidden", status: :forbidden
26
- end
27
- end
28
-
29
19
  def parse_time_range
30
20
  days = params[:days]&.to_i || 7
31
21
  days = [days, 1].max
@@ -155,21 +145,21 @@ module RailsNexus
155
145
  end
156
146
 
157
147
  def correlation_by_time_of_day
158
- (0..23).map do |hour|
159
- count = LoggedException.where(created_at: @time_range)
160
- .where("HOUR(created_at) = ?", hour)
161
- .count
162
- { hour: hour, count: count }
163
- end
148
+ expression = RailsNexus::DatabaseAdapter.time_component_expression(component: :hour)
149
+ counts = LoggedException.where(created_at: @time_range).group(Arel.sql(expression)).count
150
+ (0..23).map { |hour| { hour: hour, count: counts[hour] || counts[hour.to_s] || 0 } }
151
+ rescue NotImplementedError
152
+ (0..23).map { |hour| { hour: hour, count: nil } }
164
153
  end
165
154
 
166
155
  def correlation_by_day_of_week
167
- %w[Mon Tue Wed Thu Fri Sat Sun].each_with_index.map do |day, index|
168
- count = LoggedException.where(created_at: @time_range)
169
- .where("DAYOFWEEK(created_at) = ?", index + 1)
170
- .count
171
- { day: day, count: count }
156
+ expression = RailsNexus::DatabaseAdapter.time_component_expression(component: :weekday)
157
+ counts = LoggedException.where(created_at: @time_range).group(Arel.sql(expression)).count
158
+ %w[Mon Tue Wed Thu Fri Sat Sun].zip([1, 2, 3, 4, 5, 6, 0]).map do |day, weekday|
159
+ { day: day, count: counts[weekday] || counts[weekday.to_s] || 0 }
172
160
  end
161
+ rescue NotImplementedError
162
+ %w[Mon Tue Wed Thu Fri Sat Sun].map { |day| { day: day, count: nil } }
173
163
  end
174
164
 
175
165
  def find_co_occurring_errors
@@ -274,13 +264,10 @@ module RailsNexus
274
264
  end
275
265
 
276
266
  def baseline_health_status
277
- baseline = calculate_baseline
278
267
  anomalies = detect_anomalies
279
268
  spikes = detect_spikes
280
269
 
281
- if spikes[:detected] && spikes[:severity] == "critical"
282
- "critical"
283
- elsif anomalies.any? { |a| a[:severity] == "critical" }
270
+ if (spikes[:detected] && spikes[:severity] == "critical") || anomalies.any? { |a| a[:severity] == "critical" }
284
271
  "critical"
285
272
  elsif anomalies.any?
286
273
  "warning"
@@ -350,7 +337,7 @@ module RailsNexus
350
337
 
351
338
  24.times do |hour|
352
339
  count = LoggedException.where(
353
- created_at: date.beginning_of_day + hour.hours..date.beginning_of_day + (hour + 1).hours
340
+ created_at: (date.beginning_of_day + hour.hours)..(date.beginning_of_day + (hour + 1).hours)
354
341
  ).count
355
342
  heatmap[day_name][hour] = count
356
343
  end
@@ -1,14 +1,18 @@
1
1
  module RailsNexus
2
2
  class ApplicationController < ActionController::Base
3
3
  include Pagy::Backend
4
+ protect_from_forgery with: :exception
5
+ before_action :rails_nexus_require_auth!
4
6
 
5
7
  private
6
8
 
7
9
  def rails_nexus_require_auth!
8
10
  auth_block = RailsNexus.configuration.auth_block
9
- return if auth_block.nil?
10
- return if auth_block.call(self)
11
+ return if auth_block&.call(self)
11
12
 
13
+ head :forbidden
14
+ rescue StandardError => error
15
+ Rails.logger.error("[RailsNexus] Authentication error: #{error.class}: #{error.message}")
12
16
  head :forbidden
13
17
  end
14
18
  end
@@ -2,21 +2,12 @@
2
2
 
3
3
  module RailsNexus
4
4
  class BackupController < ApplicationController
5
- before_action :rails_nexus_require_auth!
6
5
  before_action :check_backup_enabled
7
6
  before_action :set_config, only: %i[edit update destroy trigger]
8
7
 
9
8
  # GET /rails_nexus/backup
10
9
  def index
11
10
  @configs = RailsNexus::BackupConfig.order(:name)
12
- @records = RailsNexus::Backup.order(started_at: :desc).limit(20)
13
- @stats = {
14
- total_configs: RailsNexus::BackupConfig.count,
15
- enabled_configs: RailsNexus::BackupConfig.enabled.count,
16
- total_backups: RailsNexus::Backup.count,
17
- successful: RailsNexus::Backup.successful.count,
18
- failed: RailsNexus::Backup.failed.count
19
- }
20
11
  end
21
12
 
22
13
  # GET /rails_nexus/backup/new
@@ -47,7 +38,13 @@ module RailsNexus
47
38
 
48
39
  # PATCH /rails_nexus/backup/:id
49
40
  def update
50
- if @config.update(config_params)
41
+ # Strip blank password fields so they don't overwrite stored values
42
+ filtered = config_params
43
+ %w[password encryption_password gpg_password].each do |attr|
44
+ filtered = filtered.except(attr) if filtered[attr].blank?
45
+ end
46
+
47
+ if @config.update(filtered)
51
48
  redirect_to backup_path, notice: "Backup config '#{@config.name}' updated."
52
49
  else
53
50
  render :edit, status: :unprocessable_entity
@@ -63,19 +60,24 @@ module RailsNexus
63
60
 
64
61
  # POST /rails_nexus/backup/:id/trigger
65
62
  def trigger
66
- result = RailsNexus::BackupService.run(@config)
67
-
68
- if result[:success]
69
- redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
70
- else
71
- redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
63
+ begin
64
+ RailsNexus::BackupJob.perform_later(@config.id)
65
+ redirect_to backup_path, notice: "Backup '#{@config.name}' started in background."
66
+ rescue
67
+ # Fallback to synchronous if Active Job is not configured
68
+ result = RailsNexus::BackupService.run(@config)
69
+ if result[:success]
70
+ redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
71
+ else
72
+ redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
73
+ end
72
74
  end
73
75
  end
74
76
 
75
77
  # GET /rails_nexus/backup/:id/history
76
78
  def history
77
79
  @config = RailsNexus::BackupConfig.find(params[:id])
78
- @records = RailsNexus::Backup.where(model_name: @config.name)
80
+ @records = RailsNexus::Backup.where(config_name: @config.name)
79
81
  .order(started_at: :desc)
80
82
  .limit(50)
81
83
  end
@@ -83,6 +85,7 @@ module RailsNexus
83
85
  # GET /rails_nexus/backup/history
84
86
  def all_history
85
87
  @records = RailsNexus::Backup.order(started_at: :desc).limit(100)
88
+ render :history
86
89
  end
87
90
 
88
91
  private
@@ -96,13 +99,13 @@ module RailsNexus
96
99
  :name, :description, :database_name, :adapter, :host, :port,
97
100
  :username, :password, :storage_path, :keep_count,
98
101
  :compress, :encrypted, :encryption_password,
99
- :rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror,
102
+ :rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror, :rsync_archive, :rsync_directories, :rsync_excludes,
100
103
  :notify_command, :notify_on_success, :notify_on_failure,
101
104
  :schedule_cron, :enabled,
102
105
  :s3_enabled, :s3_access_key, :s3_secret_key, :s3_bucket, :s3_region, :s3_prefix,
103
106
  :gpg_enabled, :gpg_password,
104
107
  :email_notify, :email_to,
105
- :archive_enabled, :bzip2_compress, :split_chunks,
108
+ :archive_enabled, :bzip2_compress, :split_chunks, :encrypt_base64, :mysql_additional_options,
106
109
  skip_tables: [], archive_paths: [], archive_excludes: []
107
110
  )
108
111
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module RailsNexus
4
4
  class CronJobsController < ApplicationController
5
- before_action :verify_access
6
5
  before_action :set_cron_job, only: [:show, :destroy]
7
6
 
8
7
  def index
@@ -36,7 +35,7 @@ module RailsNexus
36
35
  end
37
36
 
38
37
  def clear_old
39
- days = params[:days].to_i || 30
38
+ days = params[:days].to_i
40
39
  CronJob.where("created_at < ?", days.days.ago).delete_all
41
40
  redirect_to cron_jobs_path, notice: "Deleted cron jobs older than #{days} days."
42
41
  end
@@ -53,12 +52,5 @@ module RailsNexus
53
52
  @cron_job = CronJob.find(params[:id])
54
53
  end
55
54
 
56
- def verify_access
57
- config = RailsNexus.configuration
58
- return if config.auth_block.nil?
59
- unless config.auth_block&.call(self)
60
- render plain: "Forbidden", status: :forbidden
61
- end
62
- end
63
55
  end
64
56
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module RailsNexus
4
4
  class DatabaseHealthController < ApplicationController
5
- before_action :verify_access
6
-
7
5
  def index
8
6
  @period = params[:period]&.to_i || 7
9
7
  @period = [@period, 1].max
@@ -21,14 +19,6 @@ module RailsNexus
21
19
 
22
20
  private
23
21
 
24
- def verify_access
25
- config = RailsNexus.configuration
26
- return if config.auth_block.nil?
27
- unless config.auth_block&.call(self)
28
- render plain: "Forbidden", status: :forbidden
29
- end
30
- end
31
-
32
22
  def collect_pool_by_error(time_range)
33
23
  # Get all errors with system_health data in the time range
34
24
  exceptions = LoggedException.where(created_at: time_range)
@@ -2,8 +2,6 @@
2
2
 
3
3
  module RailsNexus
4
4
  class LoggedExceptionsController < ApplicationController
5
- before_action :rails_nexus_require_auth!
6
-
7
5
  RANSACK_FILTERS = %i[
8
6
  action_name_cont assigned_to_cont controller_name_cont created_at_gteq created_at_lteq
9
7
  exception_class_cont fingerprint_cont message_cont
@@ -1,7 +1,5 @@
1
1
  module RailsNexus
2
2
  class N1PatternsController < ApplicationController
3
- before_action :rails_nexus_require_auth!
4
-
5
3
  def index
6
4
  @patterns = RailsNexus::LoggedException.n_plus_one_patterns(limit: 50)
7
5
  @summary = RailsNexus::LoggedException.n_plus_one_summary