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,118 @@
1
+ <%# Workflow Panel - Assignment, Priority, Snooze, Mute %>
2
+ <div id="workflow-panel" class="rn-card">
3
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
4
+ <div class="flex items-center gap-2">
5
+ <svg class="w-4 h-4" style="color: var(--rn-primary)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
6
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
7
+ </svg>
8
+ <h3 class="text-sm font-semibold" style="color: var(--rn-text)">Workflow</h3>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="p-4 space-y-4">
13
+ <%# Status Badges %>
14
+ <div class="flex flex-wrap gap-2">
15
+ <% if exception.muted? %>
16
+ <span class="rn-badge" style="background: var(--rn-bg-secondary); color: var(--rn-text-muted)">
17
+ 🔇 Muted
18
+ </span>
19
+ <% elsif exception.snoozed? %>
20
+ <span class="rn-badge" style="background: var(--rn-bg-warning-subtle); color: var(--rn-warning)">
21
+ 😴 Snoozed until <%= exception.snoozed_until&.strftime("%b %d %H:%M") %>
22
+ </span>
23
+ <% else %>
24
+ <span class="rn-badge" style="background: var(--rn-bg-success-subtle); color: var(--rn-success)">
25
+ ✅ Active
26
+ </span>
27
+ <% end %>
28
+
29
+ <% if exception.priority.present? %>
30
+ <span class="rn-badge rn-badge-<%= exception.priority == 'critical' ? 'danger' : exception.priority == 'high' ? 'warning' : exception.priority == 'medium' ? 'info' : '' %>">
31
+ <%= exception.priority.upcase %>
32
+ </span>
33
+ <% end %>
34
+
35
+ <% if exception.assigned_to.present? %>
36
+ <span class="rn-badge" style="background: var(--rn-bg-primary-subtle); color: var(--rn-primary)">
37
+ 👤 <%= exception.assigned_to %>
38
+ </span>
39
+ <% end %>
40
+ </div>
41
+
42
+ <%# Priority %>
43
+ <div>
44
+ <label class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Priority</label>
45
+ <div class="flex gap-1 mt-1">
46
+ <% %w[critical high medium low].each do |level| %>
47
+ <%= button_to level.capitalize,
48
+ set_priority_logged_exception_path(exception, priority: level),
49
+ method: :post,
50
+ class: "rn-btn rn-btn-xs #{exception.priority == level ? 'rn-btn-primary' : 'rn-btn-ghost'}",
51
+ data: { turbo_stream: true } %>
52
+ <% end %>
53
+ </div>
54
+ </div>
55
+
56
+ <%# Assignment %>
57
+ <div>
58
+ <label class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Assigned To</label>
59
+ <div class="flex gap-2 mt-1">
60
+ <%= form_with url: assign_logged_exception_path(exception), method: :post, class: "flex gap-2", data: { turbo_stream: true } do |f| %>
61
+ <%= f.text_field :assigned_to, value: exception.assigned_to, placeholder: "Name or email", class: "rn-input", style: "flex: 1" %>
62
+ <%= f.submit "Assign", class: "rn-btn rn-btn-primary" %>
63
+ <% end %>
64
+ </div>
65
+ </div>
66
+
67
+ <%# Snooze %>
68
+ <div>
69
+ <label class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Snooze</label>
70
+ <div class="flex gap-1 mt-1">
71
+ <% ["1 hour", "4 hours", "1 day", "1 week"].each do |duration| %>
72
+ <%= button_to duration,
73
+ snooze_logged_exception_path(exception, duration: duration),
74
+ method: :post,
75
+ class: "rn-btn rn-btn-xs rn-btn-ghost",
76
+ data: { turbo_stream: true } %>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+
81
+ <%# Mute/Unmute %>
82
+ <div>
83
+ <label class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Notifications</label>
84
+ <div class="mt-1">
85
+ <% if exception.muted? %>
86
+ <%= button_to "Unmute", unmute_logged_exception_path(exception),
87
+ method: :post,
88
+ class: "rn-btn rn-btn-sm rn-btn-primary",
89
+ data: { turbo_stream: true } %>
90
+ <% else %>
91
+ <%= button_to "Mute", mute_logged_exception_path(exception),
92
+ method: :post,
93
+ class: "rn-btn rn-btn-sm rn-btn-ghost",
94
+ style: "color: var(--rn-danger)",
95
+ data: { turbo_stream: true },
96
+ form: { data: { turbo_confirm: "Mute this exception? Notifications will be silenced." } } %>
97
+ <% end %>
98
+ </div>
99
+ </div>
100
+
101
+ <%# Activity Log (last 5) %>
102
+ <% recent_comments = exception.comments.recent.limit(5) %>
103
+ <% if recent_comments.any? %>
104
+ <div>
105
+ <label class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Recent Activity</label>
106
+ <div class="mt-2 space-y-2">
107
+ <% recent_comments.each do |comment| %>
108
+ <div class="text-xs p-2 rounded" style="background: var(--rn-bg-secondary)">
109
+ <span class="font-semibold" style="color: var(--rn-text)"><%= comment.author %></span>
110
+ <span style="color: var(--rn-text-muted)"><%= comment.body %></span>
111
+ <span class="ml-1" style="color: var(--rn-text-muted)"><%= time_ago_in_words(comment.created_at) %> ago</span>
112
+ </div>
113
+ <% end %>
114
+ </div>
115
+ </div>
116
+ <% end %>
117
+ </div>
118
+ </div>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,2 @@
1
+ <%= turbo_stream.remove dom_id(@exception) %>
2
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,20 @@
1
+ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
2
+
3
+ xml.rss "version" => "2.0" do
4
+ xml.channel do
5
+ xml.title RailsNexus.application_name || "RailsNexus"
6
+ xml.link url_for(:only_path => false, :skip_relative_url_root => false)
7
+ xml.language "en-us"
8
+ xml.ttl "60"
9
+
10
+ @exceptions.each do |exc|
11
+ xml.item do
12
+ xml.title "#{exc.name} @ #{exc.created_at.rfc822}"
13
+ xml.description exc.message
14
+ xml.pubDate exc.created_at.rfc822
15
+ xml.guid [request.host_with_port, 'exceptions', exc.id.to_s].join(":"), "isPermaLink" => "false"
16
+ xml.link url_for(:action => 'index', :anchor => "e#{exc.id}", :only_path => false, :skip_relative_url_root => false)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,323 @@
1
+ <% page_title t(".title") %>
2
+
3
+ <div class="space-y-4" data-controller="rails_nexus" data-rails_nexus-shortcuts-value="<%= RailsNexus.configuration.keyboard_shortcuts %>">
4
+
5
+ <%# ─── Breadcrumbs ─────────────────────────────────────────── %>
6
+ <nav class="rn-breadcrumbs" aria-label="Breadcrumb">
7
+ <a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
8
+ <span class="rn-breadcrumb-sep">/</span>
9
+ <span class="rn-breadcrumb-current">Exceptions</span>
10
+ <% if @stats && @stats[:today] > 0 %>
11
+ <span class="rn-breadcrumb-sep">·</span>
12
+ <span class="rn-badge rn-badge-danger" style="margin-left: 0.25rem"><span class="rn-pulse"></span> <%= @stats[:today] %> today</span>
13
+ <% end %>
14
+ </nav>
15
+
16
+ <%# ─── Stats Overview ──────────────────────────────────────── %>
17
+ <% if @stats %>
18
+ <div class="rn-stats">
19
+ <div class="rn-stat">
20
+ <span class="rn-stat-label">Total Exceptions</span>
21
+ <span class="rn-stat-value"><%= number_with_delimiter(@stats[:total]) %></span>
22
+ <span class="rn-stat-change">All time</span>
23
+ </div>
24
+ <div class="rn-stat">
25
+ <span class="rn-stat-label">This Week</span>
26
+ <span class="rn-stat-value <%= 'danger' if @stats[:this_week] > 10 %>"><%= @stats[:this_week] %></span>
27
+ <span class="rn-stat-change">Last 7 days</span>
28
+ </div>
29
+ <div class="rn-stat">
30
+ <span class="rn-stat-label">Today</span>
31
+ <span class="rn-stat-value <%= 'danger' if @stats[:today] > 5 %>"><%= @stats[:today] %></span>
32
+ <span class="rn-stat-change <%= 'up' if @stats[:today] > @stats[:yesterday] %>">
33
+ <% if @stats[:yesterday] > 0 %>
34
+ <%= ((@stats[:today].to_f / [@stats[:yesterday], 1].max - 1) * 100).round %>% vs yesterday
35
+ <% else %>
36
+ vs yesterday
37
+ <% end %>
38
+ </span>
39
+ </div>
40
+ <div class="rn-stat">
41
+ <span class="rn-stat-label">Unique Classes</span>
42
+ <span class="rn-stat-value"><%= @stats[:top_classes]&.size || 0 %></span>
43
+ <span class="rn-stat-change">Exception types</span>
44
+ </div>
45
+ </div>
46
+ <% end %>
47
+
48
+ <%# ─── User Impact Ranking ───────────────────────────────── %>
49
+ <% begin %>
50
+ <% if RailsNexus.configuration.user_impact_tracking %>
51
+ <% impact_data = RailsNexus::LoggedException.user_impact_ranking(limit: 5) %>
52
+ <% if impact_data.any? %>
53
+ <div class="rn-card">
54
+ <div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
55
+ <div class="flex items-center gap-2">
56
+ <svg class="w-4 h-4" style="color: var(--rn-danger)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
57
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
58
+ </svg>
59
+ <h2 class="text-sm font-semibold" style="color: var(--rn-text)">User Impact (Top 5)</h2>
60
+ </div>
61
+ <p class="text-xs mt-1" style="color: var(--rn-text-muted)">Errors ranked by unique users affected</p>
62
+ </div>
63
+ <div class="rn-table-wrap">
64
+ <table class="rn-table">
65
+ <thead>
66
+ <tr>
67
+ <th>Exception</th>
68
+ <th>Users Affected</th>
69
+ <th>Occurrences</th>
70
+ <th>Last Seen</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ <% impact_data.each do |row| %>
75
+ <tr>
76
+ <td>
77
+ <span class="rn-badge rn-badge-danger"><%= row.exception_class %></span>
78
+ <span class="text-xs ml-1" style="color: var(--rn-text-muted)"><%= row.controller_name %>#<%= row.action_name %></span>
79
+ </td>
80
+ <td>
81
+ <span class="font-bold" style="color: var(--rn-danger)"><%= row.unique_users %></span>
82
+ </td>
83
+ <td>
84
+ <span style="color: var(--rn-text-secondary)"><%= row.total_occurrences %></span>
85
+ </td>
86
+ <td>
87
+ <span class="text-xs" style="color: var(--rn-text-muted)"><%= row.last_seen&.strftime("%b %d %H:%M") %></span>
88
+ </td>
89
+ </tr>
90
+ <% end %>
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+ </div>
95
+ <% end %>
96
+ <% end %>
97
+ <% rescue => e %>
98
+ <%# Silently ignore if user_id column doesn't exist yet %>
99
+ <% end %>
100
+
101
+ <%# ─── Filters Card ────────────────────────────────────────── %>
102
+ <div class="rn-card">
103
+ <%# Filter header with icon %>
104
+ <div class="rn-filters" style="padding-bottom: 0.5rem">
105
+ <div class="flex items-center gap-2">
106
+ <svg class="w-4 h-4" style="color: var(--rn-text-muted)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" /></svg>
107
+ <span class="text-xs font-semibold uppercase tracking-wider" style="color: var(--rn-text-muted)">Filters</span>
108
+ <% if filtered? %>
109
+ <%= link_to "Clear all", logged_exceptions_path(per_page: params[:per_page]), class: "ml-auto text-xs font-medium", style: "color: var(--rn-primary)" %>
110
+ <% end %>
111
+ </div>
112
+ </div>
113
+
114
+ <%# Filter form %>
115
+ <%= search_form_for @q, url: query_logged_exceptions_path, data: { turbo_frame: "exceptions" } do |f| %>
116
+ <%= hidden_field_tag :per_page, params[:per_page] if params[:per_page].present? %>
117
+ <div class="rn-filters">
118
+ <div class="rn-filter-group">
119
+ <label>Exception Type</label>
120
+ <%= select_tag "q[exception_class_cont]",
121
+ options_for_select(@exception_names.map { |n| [n, n] }, params.dig(:q, :exception_class_cont)),
122
+ include_blank: "All types",
123
+ class: "rn-select",
124
+ data: { action: "change->rails_nexus#submit" } %>
125
+ </div>
126
+
127
+ <div class="rn-filter-group">
128
+ <label>Source</label>
129
+ <%= select_tag :controller_actions_filter,
130
+ options_for_select(@controller_actions.map { |source| [source, source] }, params[:controller_actions_filter]),
131
+ include_blank: "All controllers/actions",
132
+ class: "rn-select",
133
+ data: { action: "change->rails_nexus#submit" } %>
134
+ </div>
135
+
136
+ <% if exception_feature?(:platform) && exception_searchable?(:platform) %>
137
+ <div class="rn-filter-group">
138
+ <label>Platform</label>
139
+ <%= select_tag "q[platform_eq]",
140
+ options_for_select([
141
+ ["All platforms", ""], ["Web", "web"], ["iOS", "ios"],
142
+ ["Android", "android"], ["API", "api"], ["Bot", "bot"]
143
+ ], params.dig(:q, :platform_eq)),
144
+ class: "rn-select", data: { action: "change->rails_nexus#submit" } %>
145
+ </div>
146
+ <% end %>
147
+
148
+ <% if exception_feature?(:priority) && exception_searchable?(:priority) %>
149
+ <div class="rn-filter-group">
150
+ <label>Priority</label>
151
+ <%= select_tag "q[priority_eq]",
152
+ options_for_select([
153
+ ["All priorities", ""], ["Critical", "critical"], ["High", "high"],
154
+ ["Medium", "medium"], ["Low", "low"]
155
+ ], params.dig(:q, :priority_eq)),
156
+ class: "rn-select", data: { action: "change->rails_nexus#submit" } %>
157
+ </div>
158
+ <% end %>
159
+
160
+ <% if exception_feature?(:muted) || exception_feature?(:snoozed_until) || exception_feature?(:assigned_to) %>
161
+ <div class="rn-filter-group">
162
+ <label>Status</label>
163
+ <%= select_tag :status_filter,
164
+ options_for_select([
165
+ ["Any status", ""], ["Active", "active"], ["Muted", "muted"],
166
+ ["Snoozed", "snoozed"], ["Unassigned", "unassigned"]
167
+ ], params[:status_filter]),
168
+ class: "rn-select", data: { action: "change->rails_nexus#submit" } %>
169
+ </div>
170
+ <% end %>
171
+
172
+ <div class="rn-filter-group">
173
+ <label>Time Range</label>
174
+ <%= select_tag :date_ranges_filter,
175
+ options_for_select([
176
+ ["All time", ""], ["Today", "1"], ["Last 3 days", "3"],
177
+ ["Last 7 days", "7"], ["Last 30 days", "30"]
178
+ ], params[:date_ranges_filter]),
179
+ class: "rn-select", data: { action: "change->rails_nexus#submit" } %>
180
+ </div>
181
+
182
+ <% if exception_feature?(:occurrence_count) && exception_searchable?(:occurrence_count) %>
183
+ <div class="rn-filter-group">
184
+ <label>Occurrences</label>
185
+ <%= select_tag "q[occurrence_count_gteq]",
186
+ options_for_select([
187
+ ["Any count", ""], ["2+", 2], ["5+", 5], ["10+", 10], ["50+", 50]
188
+ ], params.dig(:q, :occurrence_count_gteq)),
189
+ class: "rn-select", data: { action: "change->rails_nexus#submit" } %>
190
+ </div>
191
+ <% end %>
192
+
193
+ <% if exception_feature?(:assigned_to) && exception_searchable?(:assigned_to) && @assignees.any? %>
194
+ <div class="rn-filter-group">
195
+ <label>Assigned To</label>
196
+ <%= select_tag "q[assigned_to_cont]",
197
+ options_for_select(@assignees.map { |name| [name, name] }, params.dig(:q, :assigned_to_cont)),
198
+ include_blank: "Anyone", class: "rn-select",
199
+ data: { action: "change->rails_nexus#submit" } %>
200
+ </div>
201
+ <% end %>
202
+
203
+ <div class="rn-filter-group" style="min-width: 240px">
204
+ <label>Search</label>
205
+ <div class="rn-search">
206
+ <svg class="rn-search-icon w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /></svg>
207
+ <%= f.search_field :message_or_exception_class_or_controller_name_or_action_name_cont,
208
+ placeholder: "Message, class, or source...",
209
+ class: "rn-input",
210
+ style: "padding-left: 2.25rem",
211
+ data: { action: "input->rails_nexus#debouncedSubmit" } %>
212
+ <span class="rn-search-kbd">/</span>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="flex items-center gap-2">
217
+ <%= f.submit "Apply", class: "rn-btn rn-btn-primary" %>
218
+ </div>
219
+ </div>
220
+
221
+ <%# Active filter pills %>
222
+ <% pills = active_filter_pills %>
223
+ <% if pills.any? %>
224
+ <div class="rn-filter-pills">
225
+ <% pills.each do |pill| %>
226
+ <span class="rn-filter-pill">
227
+ <strong><%= pill[:label] %>:</strong> <%= pill[:value] %>
228
+ <%= link_to "×", logged_exceptions_path(filter_params_without(pill)), class: "rn-filter-pill-x" %>
229
+ </span>
230
+ <% end %>
231
+ </div>
232
+ <% end %>
233
+ <% end %>
234
+ </div>
235
+
236
+ <%# ─── Content Area ────────────────────────────────────────── %>
237
+ <div class="flex gap-4" style="min-height: 400px">
238
+
239
+ <%# ─── Exception List ──────────────────────────────────── %>
240
+ <div class="flex-1 min-w-0">
241
+
242
+ <%# Loading indicator %>
243
+ <div id="activity" data-rails_nexus-target="activity" class="rn-card p-3 flex items-center gap-3" role="status" aria-live="polite" hidden>
244
+ <svg class="animate-spin h-4 w-4 shrink-0" style="color: var(--rn-primary)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
245
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
246
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
247
+ </svg>
248
+ <p class="text-sm font-medium" style="color: var(--rn-primary)"><%= t(".loading") %></p>
249
+ </div>
250
+
251
+ <%# Exception details (desktop split, mobile full) %>
252
+ <div id="exception-detail-desktop" class="hidden lg:block rn-card mb-4" hidden>
253
+ <%= turbo_frame_tag "exception-details" do %>
254
+ <div class="text-center py-12">
255
+ <div class="w-10 h-10 rounded-full flex items-center justify-center mx-auto" style="background: var(--rn-bg-secondary)">
256
+ <svg class="w-5 h-5" style="color: var(--rn-text-muted)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
257
+ <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" />
258
+ </svg>
259
+ </div>
260
+ <p class="mt-2 text-xs" style="color: var(--rn-text-muted)">Select an exception to view details</p>
261
+ <p class="mt-1 text-xs" style="color: var(--rn-text-muted)">Click a row or press <span class="rn-kbd">↓</span> <span class="rn-kbd">↑</span> to navigate</p>
262
+ </div>
263
+ <% end %>
264
+ </div>
265
+
266
+ <%# Exception table %>
267
+ <%= turbo_frame_tag "exceptions" do %>
268
+ <%= render "exceptions" %>
269
+ <% end %>
270
+ </div>
271
+ </div>
272
+
273
+ <%# ─── Keyboard Shortcuts Dialog ───────────────────────────── %>
274
+ <% if RailsNexus.configuration.keyboard_shortcuts %>
275
+ <div class="rn-shortcuts-overlay" data-rails_nexus-target="shortcutsDialog">
276
+ <div class="rn-shortcuts">
277
+ <div class="flex items-center justify-between mb-4">
278
+ <h3>Keyboard Shortcuts</h3>
279
+ <button type="button" class="rn-icon-btn" data-action="click->rails_nexus#closeShortcuts">
280
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
281
+ </button>
282
+ </div>
283
+
284
+ <div class="rn-shortcut-row">
285
+ <span class="rn-shortcut-label">Focus search</span>
286
+ <div class="rn-shortcut-keys"><span class="rn-kbd">/</span></div>
287
+ </div>
288
+ <div class="rn-shortcut-row">
289
+ <span class="rn-shortcut-label">Navigate down</span>
290
+ <div class="rn-shortcut-keys"><span class="rn-kbd">j</span> or <span class="rn-kbd">↓</span></div>
291
+ </div>
292
+ <div class="rn-shortcut-row">
293
+ <span class="rn-shortcut-label">Navigate up</span>
294
+ <div class="rn-shortcut-keys"><span class="rn-kbd">k</span> or <span class="rn-kbd">↑</span></div>
295
+ </div>
296
+ <div class="rn-shortcut-row">
297
+ <span class="rn-shortcut-label">Open selected</span>
298
+ <div class="rn-shortcut-keys"><span class="rn-kbd">Enter</span></div>
299
+ </div>
300
+ <div class="rn-shortcut-row">
301
+ <span class="rn-shortcut-label">Go to previous</span>
302
+ <div class="rn-shortcut-keys"><span class="rn-kbd">p</span> or <span class="rn-kbd">←</span></div>
303
+ </div>
304
+ <div class="rn-shortcut-row">
305
+ <span class="rn-shortcut-label">Go to next</span>
306
+ <div class="rn-shortcut-keys"><span class="rn-kbd">n</span> or <span class="rn-kbd">→</span></div>
307
+ </div>
308
+ <div class="rn-shortcut-row">
309
+ <span class="rn-shortcut-label">Toggle theme</span>
310
+ <div class="rn-shortcut-keys"><span class="rn-kbd">t</span></div>
311
+ </div>
312
+ <div class="rn-shortcut-row">
313
+ <span class="rn-shortcut-label">Close panel / Clear</span>
314
+ <div class="rn-shortcut-keys"><span class="rn-kbd">Esc</span></div>
315
+ </div>
316
+ <div class="rn-shortcut-row">
317
+ <span class="rn-shortcut-label">Show shortcuts</span>
318
+ <div class="rn-shortcut-keys"><span class="rn-kbd">?</span></div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ <% end %>
323
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "exceptions" do %>
2
+ <%= render "exceptions" %>
3
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "exception-details" do %>
2
+ <%= render "show" %>
3
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= turbo_stream.replace "exception-details" do %>
2
+ <%= turbo_frame_tag "exception-details", class: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
3
+ <%= render "show" %>
4
+ <% end %>
5
+ <% end %>