faultline-rails 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1f99904f339aa70945293928829f5ecb86d755a41203d9ad70dbc681e2b5213
4
- data.tar.gz: '09d8929d8fc7823186707312eccb2be742e38c0e13167f6a62cdefa801e5d4f1'
3
+ metadata.gz: effc17275c134ae8cff03dbcb56c9599ef226c5ef95cbeef465ce545355d583d
4
+ data.tar.gz: '086260474f100cf16be33eeaa478e0d7552106ae73b96fde52dc713ef6106c67'
5
5
  SHA512:
6
- metadata.gz: ef04fc828fda367fd3eca26f1e776163846397f610835497bfd6177a72c862a51270d2ef1e6d1c5bf24a33f4ee115d8ac240ebe8582fb0ccfb3c2fab6b83cc0c
7
- data.tar.gz: 8967bf842e86fbc7aa90d91d8e8988dc9e8a982ba4a1393ff309f268ce4287f570796857638d0fdc3102fc3c984a8e4717fc728ba9c2f5cbead2f8195df50efa
6
+ metadata.gz: e29aacc40843e06de209d838f83e52ce96b0e4f761b519b4668e27f31b7418fe7e10aedbf15b5fe79dc4c672a3255133d6aa01845f485e08f38ae4ec17ff4587
7
+ data.tar.gz: b8790e34a7620ed17e2960071558d92099fbf0f698c70521915088542ec97e0c4e3d72bbe6a3ea3bde625174ec779546d859035f8f91b99f6c98b5c74854e980
data/README.md CHANGED
@@ -4,10 +4,11 @@ Faultline is a production-friendly exception dashboard for Rails 8. It records u
4
4
 
5
5
  The dashboard is server-rendered and progressively enhanced with:
6
6
 
7
+ - **Tailwind CSS** layout that works with or without Tailwind installed.
8
+ - **Ransack** for advanced search and sortable columns.
7
9
  - Turbo Frames for filtering and opening exception details without full-page navigation.
8
10
  - Turbo Streams for deleting one, many, or all exceptions.
9
11
  - Stimulus for loading state and small interaction behavior.
10
- - Built-in CSS for a responsive dashboard UI that works out of the box.
11
12
 
12
13
  ## Requirements
13
14
 
@@ -36,6 +37,7 @@ This will:
36
37
  1. Copy the database migration with proper indexes.
37
38
  2. Create a configuration initializer at `config/initializers/faultline.rb`.
38
39
  3. Mount the engine in your routes.
40
+ 4. Add `rescue_from Exception, with: :log_exception_handler` to your `ApplicationController` for Rails 8 compatibility.
39
41
 
40
42
  The dashboard is now available at `/faultline`.
41
43
 
@@ -54,6 +56,18 @@ end
54
56
 
55
57
  Faultline logs the exception and then re-raises it so Rails keeps its normal error handling, status codes, and error pages.
56
58
 
59
+ ### Rails 8+ compatibility
60
+
61
+ Faultline's initializer automatically adds `rescue_from Exception, with: :log_exception_handler` to your `ApplicationController`. This ensures exceptions are logged even when Rails' built-in `rescue_action` pattern is no longer used.
62
+
63
+ If you need to handle this manually:
64
+
65
+ ```ruby
66
+ class ApplicationController < ActionController::Base
67
+ rescue_from Exception, with: :log_exception_handler
68
+ end
69
+ ```
70
+
57
71
  ## Protect the dashboard
58
72
 
59
73
  The dashboard contains sensitive information, including request parameters, environment variables, and source paths. **Do not expose it to unauthenticated public users.**
@@ -100,10 +114,12 @@ end
100
114
  You can also attach additional application data to each recorded exception:
101
115
 
102
116
  ```ruby
103
- ApplicationController.exception_data = lambda do |controller|
117
+ config.exception_data = lambda do |controller|
104
118
  {
105
119
  request_id: controller.request.request_id,
106
- user_id: controller.current_user&.id
120
+ user_id: controller.current_user&.id,
121
+ user_email: controller.current_user&.email,
122
+ environment: Rails.env
107
123
  }
108
124
  end
109
125
  ```
@@ -120,6 +136,33 @@ end
120
136
 
121
137
  Rails' `filter_parameters` configuration is respected before request parameters are stored.
122
138
 
139
+ ## Search & Filter (Ransack)
140
+
141
+ Faultline includes [Ransack](https://github.com/activerecord-hackery/ransack) for advanced search and filtering. The dashboard provides:
142
+
143
+ - **Search form** — Search by exception class, controller name, and message text.
144
+ - **Sortable columns** — Click column headers to sort by exception class, controller, action, or date.
145
+ - **Sidebar filters** — Quick filter by exception class, controller/action, and time range (today, 3 days, 7 days, 30 days).
146
+
147
+ If your app doesn't include Ransack, Faultline falls back to the built-in sidebar filters automatically.
148
+
149
+ ### Searching
150
+
151
+ Type in the search form fields and click "Search" to filter exceptions:
152
+
153
+ - **Exception Class** — Search by exception type (e.g., `RuntimeError`, `ActiveRecord::RecordNotFound`)
154
+ - **Controller** — Search by controller name (e.g., `users`, `posts`)
155
+ - **Message** — Full-text search across exception messages
156
+
157
+ ### Sorting
158
+
159
+ Click any column header in the exceptions table to sort:
160
+
161
+ - **Exception** — Sort alphabetically by exception class
162
+ - **Controller** — Sort by controller name
163
+ - **Action** — Sort by action name
164
+ - **Date** — Sort by creation date (newest/oldest first)
165
+
123
166
  ## Frontend setup
124
167
 
125
168
  ### Hotwire (default)
@@ -137,7 +180,11 @@ bin/rails stimulus:install
137
180
 
138
181
  Faultline ships with its own built-in CSS stylesheet that works out of the box. No Tailwind configuration is required.
139
182
 
140
- If your application uses Tailwind CSS and you want Faultline to use your Tailwind theme instead, you can configure the gem's view directory as a Tailwind source. Use the absolute path returned by Bundler:
183
+ #### Tailwind CSS
184
+
185
+ If your application uses Tailwind CSS, Faultline's views are already Tailwind-styled and will automatically use your Tailwind theme.
186
+
187
+ If you want to configure the gem's view directory as a Tailwind source, use the absolute path returned by Bundler:
141
188
 
142
189
  ```bash
143
190
  bundle show faultline-rails
@@ -163,15 +210,35 @@ module.exports = {
163
210
  }
164
211
  ```
165
212
 
213
+ #### Non-importmap projects
214
+
215
+ Faultline works with **importmap**, **sprockets**, **propshaft**, or **no JS pipeline** at all. The layout adapts to your asset pipeline:
216
+
217
+ - If `javascript_importmap_tags` is available, it uses importmap.
218
+ - If `turbo_refreshes_with` is available, it enables Turbo morph scrolling.
219
+ - If neither is available, the dashboard works without JavaScript.
220
+
221
+ ### Bootstrap / Other CSS frameworks
222
+
223
+ Faultline's views use Tailwind CSS utility classes. To use Bootstrap or another framework:
224
+
225
+ 1. Override the views by copying them to your app:
226
+ ```bash
227
+ cp -r $(bundle show faultline-rails)/app/views/faultline app/views/faultline
228
+ ```
229
+ 2. Rewrite the Tailwind classes with your framework's classes.
230
+ 3. The built-in CSS in `faultline/application.css` provides fallback styles.
231
+
166
232
  ## Dashboard features
167
233
 
168
- - Search exception messages.
169
- - Filter by exception class, controller/action, or age.
170
- - Open full exception details in a Turbo Frame.
171
- - Delete individual exceptions without leaving the page.
172
- - Delete the currently visible result set.
173
- - Clear the complete history.
174
- - Subscribe to the RSS feed at `/faultline/logged_exceptions/feed.rss`.
234
+ - **Search** — Full-text search across exception messages, classes, and controllers.
235
+ - **Sortable columns** — Sort by exception class, controller, action, or date.
236
+ - **Filter** Filter by exception class, controller/action, or age.
237
+ - **Exception details** Open full exception details in a Turbo Frame.
238
+ - **Delete** Delete individual exceptions without leaving the page.
239
+ - **Bulk delete** — Delete the currently visible result set.
240
+ - **Clear history** — Clear the complete exception history.
241
+ - **RSS feed** — Subscribe to `/faultline/logged_exceptions/feed.rss`.
175
242
 
176
243
  ## Data storage
177
244
 
@@ -1,5 +1,10 @@
1
- /* Faultline Dashboard - Self-contained styles */
2
- /* Works without Tailwind CSS. If you have Tailwind, these serve as fallbacks. */
1
+ /* Faultline Dashboard - Tailwind CSS with fallbacks */
2
+ /* If Tailwind is loaded, these utilities take precedence. */
3
+ /* If Tailwind is NOT loaded, these fallback styles provide the UI. */
4
+
5
+ /* ═══════════════════════════════════════════════════════════════════
6
+ FALLBACK STYLES (used when Tailwind CSS is NOT available)
7
+ ═══════════════════════════════════════════════════════════════════ */
3
8
 
4
9
  :root {
5
10
  --fl-primary: #4f46e5;
@@ -31,7 +36,7 @@ table { border-collapse: collapse; width: 100%; }
31
36
  @media (max-width: 1024px) { .fl-grid { grid-template-columns: 1fr; } }
32
37
 
33
38
  /* Card */
34
- .fl-card { background: var(--fl-surface); border: 1px solid var(--fl-border); border-radius: var(--fl-radius); box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
39
+ .fl-card { background: var(--fl-surface); border: 1px solid var(--fl-border); border-radius: var(--fl-radius); box-shadow: 0 1px 3px rgba(0,0,0,0.06); padding: 1.25rem; }
35
40
 
36
41
  /* Header */
37
42
  .fl-header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 2rem; }
@@ -48,7 +53,6 @@ table { border-collapse: collapse; width: 100%; }
48
53
  .fl-nav a { display: block; padding: 0.5rem 0.75rem; border-radius: var(--fl-radius-sm); font-size: 0.875rem; color: var(--fl-text-secondary); }
49
54
  .fl-nav a:hover { background: #f1f5f9; color: var(--fl-text); text-decoration: none; }
50
55
  .fl-nav-scroll { max-height: 10rem; overflow-y: auto; }
51
- .fl-sidebar hr { border: none; border-top: 1px solid var(--fl-border); margin: 1.5rem 0; padding-top: 1.25rem; }
52
56
 
53
57
  /* Search */
54
58
  .fl-search { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
@@ -119,3 +123,46 @@ table { border-collapse: collapse; width: 100%; }
119
123
  /* Utility */
120
124
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
121
125
  .sr-only-focusable:focus { position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; border: 0; }
126
+
127
+ /* ═══════════════════════════════════════════════════════════════════
128
+ TAILWIND OVERRIDES (used when Tailwind CSS IS available)
129
+ ═══════════════════════════════════════════════════════════════════ */
130
+
131
+ /* Tailwind-compatible pagination */
132
+ .fl-pagination .pagination {
133
+ @apply flex flex-wrap gap-2 items-center list-none m-0 p-0;
134
+ }
135
+ .fl-pagination .pagination a {
136
+ @apply px-3 py-1.5 rounded-md text-indigo-600 hover:bg-indigo-50 no-underline text-sm;
137
+ }
138
+ .fl-pagination .pagination span.current {
139
+ @apply px-3 py-1.5 rounded-md bg-indigo-600 text-white font-semibold text-sm;
140
+ }
141
+ .fl-pagination .pagination span.gap {
142
+ @apply px-2 py-1.5 text-gray-400 text-sm;
143
+ }
144
+
145
+ /* Tailwind-compatible search input */
146
+ .fl-search input[type="search"] {
147
+ @apply flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none;
148
+ }
149
+
150
+ /* Tailwind-compatible buttons */
151
+ .fl-btn {
152
+ @apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md text-sm font-semibold cursor-pointer no-underline;
153
+ }
154
+ .fl-btn-primary {
155
+ @apply bg-indigo-600 text-white hover:bg-indigo-700;
156
+ }
157
+ .fl-btn-danger {
158
+ @apply bg-red-600 text-white hover:bg-red-700;
159
+ }
160
+ .fl-btn-outline {
161
+ @apply bg-transparent border border-gray-300 text-red-700 hover:bg-red-50;
162
+ }
163
+ .fl-btn-sm {
164
+ @apply px-3 py-1.5 text-xs;
165
+ }
166
+ .fl-btn-ghost {
167
+ @apply bg-transparent text-gray-500 hover:bg-gray-100;
168
+ }
@@ -9,11 +9,15 @@ module Faultline
9
9
  def index
10
10
  @exception_names = LoggedException.class_names
11
11
  @controller_actions = LoggedException.controller_actions
12
- @exceptions = filtered_exceptions
12
+ @q = ransack_search
13
+ @exceptions = @q.result(distinct: true)
14
+ .paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
13
15
  end
14
16
 
15
17
  def query
16
- @exceptions = filtered_exceptions
18
+ @q = ransack_search
19
+ @exceptions = @q.result(distinct: true)
20
+ .paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
17
21
 
18
22
  respond_to do |format|
19
23
  format.turbo_stream
@@ -55,7 +59,10 @@ module Faultline
55
59
  filtered_scope
56
60
  end
57
61
  exceptions.delete_all
58
- @exceptions = filtered_exceptions
62
+
63
+ @q = ransack_search
64
+ @exceptions = @q.result(distinct: true)
65
+ .paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
59
66
 
60
67
  respond_to do |format|
61
68
  format.turbo_stream
@@ -65,11 +72,14 @@ module Faultline
65
72
 
66
73
  def clear
67
74
  LoggedException.delete_all
68
- @exceptions = filtered_exceptions
75
+
76
+ @q = ransack_search
77
+ @exceptions = @q.result(distinct: true)
78
+ .paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
69
79
 
70
80
  respond_to do |format|
71
81
  format.turbo_stream
72
- format.html { redirect_back fallback_location: root_path }
82
+ format.html { redirect_back fallback_location: faultline_root_path }
73
83
  end
74
84
  end
75
85
 
@@ -82,8 +92,22 @@ module Faultline
82
92
  head :forbidden
83
93
  end
84
94
 
95
+ def faultline_root_path
96
+ main_app.respond_to?(:root_path) ? main_app.root_path : "/"
97
+ end
98
+
99
+ def ransack_search
100
+ if defined?(Ransack)
101
+ LoggedException.ransack(params[:q])
102
+ else
103
+ scope = filtered_scope
104
+ Struct.new(:result).new(scope)
105
+ end
106
+ end
107
+
85
108
  def params_filters
86
109
  {
110
+ q: params[:q],
87
111
  query: params[:query],
88
112
  date_ranges_filter: params[:date_ranges_filter],
89
113
  exception_names_filter: params[:exception_names_filter],
@@ -91,10 +115,6 @@ module Faultline
91
115
  }.compact
92
116
  end
93
117
 
94
- def filtered_exceptions
95
- filtered_scope.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
96
- end
97
-
98
118
  def filtered_scope
99
119
  exceptions = LoggedException.sorted
100
120
  exceptions = exceptions.where(id: params[:id]) if params[:id].present?
@@ -11,7 +11,8 @@ module Faultline
11
11
  end
12
12
 
13
13
  def filtered?
14
- [:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] }
14
+ [:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] } ||
15
+ params[:q].present?
15
16
  end
16
17
 
17
18
  def listify(text)
@@ -32,5 +33,18 @@ module Faultline
32
33
  simple_format(text).html_safe
33
34
  end
34
35
  end
36
+
37
+ # Sort link helper for Ransack-compatible table headers.
38
+ # Falls back to plain link if Ransack is not available.
39
+ def sort_link(search, attribute, name = nil, **options, &block)
40
+ name ||= attribute.to_s.humanize
41
+
42
+ if defined?(Ransack) && search.respond_to?(:result)
43
+ # Delegate to Ransack's built-in sort_link helper
44
+ Ransack::Helpers::FormHelper.instance_method(:sort_link).bind(self).call(search, attribute, name, **options, &block)
45
+ else
46
+ link_to name, "#", **options, &block
47
+ end
48
+ end
35
49
  end
36
50
  end
@@ -1,45 +1,79 @@
1
- <div id="exceptions-content" class="fl-card">
2
- <div class="fl-detail-header">
1
+ <div id="exceptions-content" class="bg-white shadow rounded-lg overflow-hidden">
2
+ <%# Header %>
3
+ <div class="px-6 py-4 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
3
4
  <div>
4
- <h2>
5
+ <h2 class="text-lg font-medium text-gray-900">
5
6
  <%= t(".heading") %>
6
7
  <% if filtered? %>
7
- <span style="font-weight: 400; color: var(--fl-text-secondary);">(<%= t(".filtered") %>)</span>
8
+ <span class="text-sm font-normal text-gray-500">(<%= t(".filtered") %>)</span>
8
9
  <% end %>
9
10
  </h2>
10
- <p class="fl-detail-time" style="margin-top: 0.25rem;"><%= t(".count", count: @exceptions.total_entries) %></p>
11
+ <p class="mt-1 text-sm text-gray-500"><%= t(".count", count: @exceptions.total_entries) %></p>
12
+ </div>
13
+ <div class="mt-4 sm:mt-0">
14
+ <%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
15
+ class: "inline-flex items-center px-3 py-2 border border-red-300 text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
16
+ data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
11
17
  </div>
12
-
13
- <%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
14
- class: "fl-btn fl-btn-outline fl-btn-sm",
15
- data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
16
18
  </div>
17
19
 
18
20
  <% if @exceptions.empty? %>
19
- <div class="fl-empty"><%= t(".empty") %></div>
21
+ <div class="px-6 py-12 text-center">
22
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
23
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
24
+ </svg>
25
+ <p class="mt-2 text-sm text-gray-500"><%= t(".empty") %></p>
26
+ </div>
20
27
  <% else %>
21
- <div class="fl-table-wrap">
22
- <table class="fl-table">
23
- <thead>
28
+ <div class="overflow-x-auto">
29
+ <table class="min-w-full divide-y divide-gray-200">
30
+ <thead class="bg-gray-50">
24
31
  <tr>
25
- <th><%= t(".exception") %></th>
26
- <th><%= t(".date") %></th>
27
- <th><span class="sr-only"><%= t(".actions") %></span></th>
32
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
33
+ <%= sort_link @q, :exception_class, "Exception", data: { turbo_frame: "exceptions" } %>
34
+ </th>
35
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
36
+ <%= sort_link @q, :controller_name, "Controller", data: { turbo_frame: "exceptions" } %>
37
+ </th>
38
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
39
+ <%= sort_link @q, :action_name, "Action", data: { turbo_frame: "exceptions" } %>
40
+ </th>
41
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
42
+ Message
43
+ </th>
44
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
45
+ <%= sort_link @q, :created_at, "Date", data: { turbo_frame: "exceptions" } %>
46
+ </th>
47
+ <th class="relative px-6 py-3">
48
+ <span class="sr-only"><%= t(".actions") %></span>
49
+ </th>
28
50
  </tr>
29
51
  </thead>
30
- <tbody>
52
+ <tbody class="bg-white divide-y divide-gray-200">
31
53
  <% @exceptions.each do |exception| %>
32
- <tr id="<%= dom_id(exception) %>">
33
- <td>
34
- <%= link_to exception.name, logged_exception_path(exception),
35
- class: "fl-name",
54
+ <tr id="<%= dom_id(exception) %>" class="hover:bg-gray-50">
55
+ <td class="px-6 py-4 whitespace-nowrap">
56
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
57
+ <%= exception.exception_class %>
58
+ </span>
59
+ </td>
60
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
61
+ <%= exception.controller_name %>
62
+ </td>
63
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
64
+ <%= exception.action_name %>
65
+ </td>
66
+ <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
67
+ <%= link_to exception.message&.truncate(100), logged_exception_path(exception),
68
+ class: "text-indigo-600 hover:text-indigo-900",
36
69
  data: { turbo_frame: "exception-details" } %>
37
- <p class="fl-msg"><%= exception.message %></p>
38
70
  </td>
39
- <td class="fl-date"><%= pretty_exception_date(exception) %></td>
40
- <td class="fl-actions">
71
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
72
+ <%= pretty_exception_date(exception) %>
73
+ </td>
74
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
41
75
  <%= link_to t(".delete"), logged_exception_path(exception),
42
- class: "fl-delete-link",
76
+ class: "text-red-600 hover:text-red-900",
43
77
  data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
44
78
  </td>
45
79
  </tr>
@@ -49,7 +83,24 @@
49
83
  </div>
50
84
  <% end %>
51
85
 
52
- <div class="fl-pagination">
53
- <%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters) %>
86
+ <%# Pagination %>
87
+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
88
+ <div class="flex-1 flex justify-between sm:hidden">
89
+ <%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters),
90
+ inner_window: 1, outer_window: 0, previous_label: "Previous", next_label: "Next" %>
91
+ </div>
92
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
93
+ <div>
94
+ <p class="text-sm text-gray-700">
95
+ Showing <span class="font-medium"><%= @exceptions.offset + 1 %></span>
96
+ to <span class="font-medium"><%= @exceptions.offset + @exceptions.length %></span>
97
+ of <span class="font-medium"><%= @exceptions.total_entries %></span> exceptions
98
+ </p>
99
+ </div>
100
+ <div>
101
+ <%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters),
102
+ inner_window: 2, outer_window: 1, previous_label: "&laquo;", next_label: "&raquo;" %>
103
+ </div>
104
+ </div>
54
105
  </div>
55
106
  </div>
@@ -1,9 +1,10 @@
1
1
  <div>
2
- <h3><%= title %></h3>
3
- <nav class="fl-nav fl-nav-scroll">
2
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider"><%= title %></h3>
3
+ <nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
4
4
  <% values.each do |value| %>
5
- <li><%= link_to value, query_logged_exceptions_path(parameter => value),
6
- data: { turbo_frame: "exceptions" } %></li>
5
+ <%= link_to value, query_logged_exceptions_path(parameter => value),
6
+ class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
7
+ data: { turbo_frame: "exceptions" } %>
7
8
  <% end %>
8
9
  </nav>
9
10
  </div>
@@ -1,37 +1,76 @@
1
- <div class="fl-detail-header">
2
- <div>
3
- <p class="fl-detail-time"><%= @exception.created_at.strftime(Time::DATE_FORMATS[:exc_full]) %></p>
4
- <h2><%= @exception.name %></h2>
1
+ <div class="space-y-6">
2
+ <%# Header %>
3
+ <div class="flex items-start justify-between">
4
+ <div>
5
+ <p class="text-sm text-gray-500"><%= @exception.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
6
+ <h2 class="mt-1 text-xl font-semibold text-gray-900"><%= @exception.name %></h2>
7
+ <div class="mt-2 flex items-center space-x-2">
8
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
9
+ <%= @exception.exception_class %>
10
+ </span>
11
+ <span class="text-sm text-gray-500">•</span>
12
+ <span class="text-sm text-gray-500"><%= @exception.controller_name %>#<%= @exception.action_name %></span>
13
+ <span class="text-sm text-gray-500">•</span>
14
+ <span class="text-sm text-gray-500"><%= @exception.remote_ip %></span>
15
+ </div>
16
+ </div>
17
+ <button type="button"
18
+ class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
19
+ data-action="click->faultline#closeDetails">
20
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
21
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
22
+ </svg>
23
+ </button>
24
+ </div>
25
+
26
+ <%# Request Info %>
27
+ <div class="bg-gray-50 rounded-lg p-4">
28
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Request</h3>
29
+ <pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.request) %></pre>
5
30
  </div>
6
- <button type="button" class="fl-btn fl-btn-ghost fl-btn-sm" data-action="click->faultline#closeDetails">
7
- <%= t(".close") %>
8
- </button>
9
- </div>
10
31
 
11
- <div class="fl-detail-body">
12
- <section class="fl-detail-section">
13
- <h3><%= t(".request") %></h3>
14
- <div class="fl-detail-code"><%= pretty_format(@exception.request) %></div>
15
- </section>
32
+ <%# Message %>
33
+ <div>
34
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Message</h3>
35
+ <div class="bg-gray-50 rounded-lg p-4">
36
+ <pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= @exception.message %></pre>
37
+ </div>
38
+ </div>
16
39
 
17
- <section class="fl-detail-section">
18
- <h3><%= t(".message") %></h3>
19
- <div class="fl-detail-code"><%= simple_format(@exception.message) %></div>
20
- </section>
40
+ <%# Backtrace %>
41
+ <div>
42
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Backtrace</h3>
43
+ <div class="bg-gray-900 rounded-lg p-4 overflow-auto max-h-96">
44
+ <pre class="text-sm text-gray-300 whitespace-pre-wrap font-mono leading-relaxed"><%= @exception.backtrace %></pre>
45
+ </div>
46
+ </div>
21
47
 
22
- <section class="fl-detail-section">
23
- <h3><%= t(".backtrace") %></h3>
24
- <pre class="fl-detail-pre"><%= @exception.backtrace %></pre>
25
- </section>
48
+ <%# Environment %>
49
+ <div>
50
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Environment</h3>
51
+ <div class="bg-gray-50 rounded-lg p-4 overflow-auto max-h-64">
52
+ <pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.environment) %></pre>
53
+ </div>
54
+ </div>
26
55
 
27
- <section class="fl-detail-section">
28
- <h3><%= t(".environment") %></h3>
29
- <div class="fl-detail-code"><%= pretty_format(@exception.environment) %></div>
30
- </section>
31
- </div>
56
+ <%# User Info %>
57
+ <% if @exception.user_info.present? %>
58
+ <div>
59
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">User Info</h3>
60
+ <div class="bg-gray-50 rounded-lg p-4">
61
+ <pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.user_info) %></pre>
62
+ </div>
63
+ </div>
64
+ <% end %>
32
65
 
33
- <div class="fl-detail-footer">
34
- <%= link_to t(".delete"), logged_exception_path(@exception),
35
- class: "fl-delete-link",
36
- data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
66
+ <%# Actions %>
67
+ <div class="flex items-center justify-between pt-4 border-t border-gray-200">
68
+ <p class="text-sm text-gray-500">
69
+ ID: <%= @exception.id %>
70
+ Created: <%= @exception.created_at.iso8601 %>
71
+ </p>
72
+ <%= link_to t(".delete"), logged_exception_path(@exception),
73
+ class: "inline-flex items-center px-3 py-2 border border-red-300 text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
74
+ data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
75
+ </div>
37
76
  </div>
@@ -1,63 +1,145 @@
1
1
  <% page_title t(".title") %>
2
2
 
3
- <div data-controller="faultline" class="fl-container">
4
- <div class="fl-header">
5
- <div>
6
- <p class="fl-brand">Faultline</p>
7
- <h1><%= t(".title") %></h1>
8
- <p class="fl-subtitle"><%= t(".subtitle") %></p>
3
+ <div class="space-y-6" data-controller="faultline">
4
+ <%# Header %>
5
+ <div class="md:flex md:items-center md:justify-between">
6
+ <div class="flex-1 min-w-0">
7
+ <h1 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
8
+ <%= t(".title") %>
9
+ </h1>
10
+ <p class="mt-1 text-sm text-gray-500"><%= t(".subtitle") %></p>
11
+ </div>
12
+ <div class="mt-4 flex md:mt-0 md:ml-4">
13
+ <%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
14
+ class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
15
+ form: { data: { turbo_stream: true } },
16
+ data: { turbo_confirm: t(".confirm_clear") } %>
9
17
  </div>
10
- <%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
11
- class: "fl-btn fl-btn-danger",
12
- form: { data: { turbo_stream: true } },
13
- data: { turbo_confirm: t(".confirm_clear") } %>
14
18
  </div>
15
19
 
16
- <div class="fl-grid">
17
- <aside class="fl-card fl-sidebar">
18
- <h2><%= t(".filters") %></h2>
20
+ <%# Ransack Search Form %>
21
+ <div class="bg-white shadow rounded-lg p-4">
22
+ <%= search_form_for @q, url: query_logged_exceptions_path, data: { turbo_frame: "exceptions" } do |f| %>
23
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
24
+ <div>
25
+ <%= f.label :exception_class_cont, "Exception Class", class: "block text-sm font-medium text-gray-700" %>
26
+ <%= f.search_field :exception_class_cont,
27
+ class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
28
+ placeholder: "e.g. RuntimeError" %>
29
+ </div>
30
+ <div>
31
+ <%= f.label :controller_name_cont, "Controller", class: "block text-sm font-medium text-gray-700" %>
32
+ <%= f.search_field :controller_name_cont,
33
+ class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
34
+ placeholder: "e.g. users" %>
35
+ </div>
36
+ <div>
37
+ <%= f.label :message_cont, "Message", class: "block text-sm font-medium text-gray-700" %>
38
+ <%= f.search_field :message_cont,
39
+ class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
40
+ placeholder: "Search messages..." %>
41
+ </div>
42
+ <div class="flex items-end">
43
+ <%= f.submit "Search", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
44
+ </div>
45
+ </div>
46
+ <% end %>
47
+ </div>
19
48
 
20
- <nav class="fl-nav" aria-label="<%= t(".filters") %>">
21
- <li><%= link_to t(".latest_exceptions"), logged_exceptions_path,
22
- data: { turbo_frame: "exceptions" } %></li>
23
- </nav>
49
+ <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
50
+ <%# Sidebar %>
51
+ <div class="lg:col-span-1">
52
+ <div class="bg-white shadow rounded-lg p-4 space-y-6">
53
+ <%# Exception Class Filter %>
54
+ <div>
55
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Exception Class</h3>
56
+ <nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
57
+ <% @exception_names.each do |name| %>
58
+ <%= link_to name,
59
+ query_logged_exceptions_path(exception_names_filter: name),
60
+ class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
61
+ data: { turbo_frame: "exceptions" } %>
62
+ <% end %>
63
+ </nav>
64
+ </div>
24
65
 
25
- <%= render "filter_group", title: t(".exception"), values: @exception_names, parameter: :exception_names_filter %>
26
- <%= render "filter_group", title: t(".controller_action"), values: @controller_actions, parameter: :controller_actions_filter %>
66
+ <%# Controller/Action Filter %>
67
+ <div>
68
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Controller</h3>
69
+ <nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
70
+ <% @controller_actions.each do |action| %>
71
+ <%= link_to action,
72
+ query_logged_exceptions_path(controller_actions_filter: action),
73
+ class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
74
+ data: { turbo_frame: "exceptions" } %>
75
+ <% end %>
76
+ </nav>
77
+ </div>
27
78
 
28
- <div>
29
- <h3><%= t(".dates") %></h3>
30
- <nav class="fl-nav">
31
- <% [[t(".today"), 1], [t(".last_few_days"), 3], [t(".last_7_days"), 7], [t(".last_30_days"), 30]].each do |label, days| %>
32
- <li><%= link_to label, query_logged_exceptions_path(date_ranges_filter: days),
33
- data: { turbo_frame: "exceptions" } %></li>
79
+ <%# Date Filter %>
80
+ <div>
81
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Time Range</h3>
82
+ <nav class="mt-2 space-y-1">
83
+ <% [[t(".today"), 1], [t(".last_few_days"), 3], [t(".last_7_days"), 7], [t(".last_30_days"), 30]].each do |label, days| %>
84
+ <%= link_to label,
85
+ query_logged_exceptions_path(date_ranges_filter: days),
86
+ class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
87
+ data: { turbo_frame: "exceptions" } %>
88
+ <% end %>
89
+ </nav>
90
+ </div>
91
+
92
+ <%# Text Search %>
93
+ <div>
94
+ <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Quick Search</h3>
95
+ <%= form_with url: query_logged_exceptions_path, method: :get,
96
+ data: { turbo_frame: "exceptions", action: "submit->faultline#submit" } do |form| %>
97
+ <div class="mt-2 flex gap-2">
98
+ <%= form.search_field :query,
99
+ placeholder: "Search...",
100
+ class: "flex-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" %>
101
+ <%= form.submit "Go",
102
+ class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
103
+ </div>
34
104
  <% end %>
35
- </nav>
36
- </div>
105
+ </div>
37
106
 
38
- <div>
39
- <%= form_with url: query_logged_exceptions_path, method: :get,
40
- data: { turbo_frame: "exceptions", action: "submit->faultline#submit" } do |form| %>
41
- <h3><%= form.label :query, t(".search") %></h3>
42
- <div class="fl-search">
43
- <%= form.search_field :query, placeholder: t(".search_placeholder") %>
44
- <%= form.submit t(".find"), class: "fl-btn fl-btn-primary fl-btn-sm" %>
45
- </div>
46
- <% end %>
107
+ <%# RSS Feed %>
108
+ <div class="pt-4 border-t border-gray-200">
109
+ <%= link_to "📡 RSS Feed", feed_logged_exceptions_path(format: :rss),
110
+ class: "text-sm text-gray-500 hover:text-indigo-600" %>
111
+ </div>
47
112
  </div>
113
+ </div>
48
114
 
49
- <%= render "feed" %>
50
- </aside>
51
-
52
- <div>
53
- <div id="activity" data-faultline-target="activity" class="fl-status" role="status" aria-live="polite" hidden>
54
- <%= t(".loading") %>
115
+ <%# Main Content %>
116
+ <div class="lg:col-span-3 space-y-6">
117
+ <%# Activity indicator %>
118
+ <div id="activity" data-faultline-target="activity" class="rounded-md bg-blue-50 p-4" role="status" aria-live="polite" hidden>
119
+ <div class="flex">
120
+ <div class="flex-shrink-0">
121
+ <svg class="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
122
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
123
+ <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>
124
+ </svg>
125
+ </div>
126
+ <div class="ml-3">
127
+ <p class="text-sm font-medium text-blue-800"><%= t(".loading") %></p>
128
+ </div>
129
+ </div>
55
130
  </div>
56
131
 
57
- <%= turbo_frame_tag "exception-details", class: "fl-card" do %>
58
- <div class="fl-empty"><%= t(".select_exception") %></div>
132
+ <%# Exception Details Panel %>
133
+ <%= turbo_frame_tag "exception-details", class: "bg-white shadow rounded-lg p-6" do %>
134
+ <div class="text-center py-12 text-gray-500">
135
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
136
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
137
+ </svg>
138
+ <p class="mt-2 text-sm"><%= t(".select_exception") %></p>
139
+ </div>
59
140
  <% end %>
60
141
 
142
+ <%# Exceptions List %>
61
143
  <%= turbo_frame_tag "exceptions" do %>
62
144
  <%= render "exceptions" %>
63
145
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_tag "exception-details", class: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
1
+ <%= turbo_frame_tag "exception-details", class: "bg-white shadow rounded-lg p-6" do %>
2
2
  <%= render "show" %>
3
3
  <% end %>
@@ -1,17 +1,83 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <title><%= content_for?(:title) ? yield(:title) : "Faultline" %></title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <%= csrf_meta_tags %>
7
- <%= csp_meta_tag %>
8
- <%= stylesheet_link_tag "faultline/application", "data-turbo-track": "reload" %>
9
- <%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
10
- <%= javascript_include_tag "faultline/application", "data-turbo-track": "reload", type: "module" %>
11
- </head>
12
- <body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
13
- <main>
2
+ <html lang="en" class="h-full bg-gray-50">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= content_for?(:title) ? yield(:title) : "Faultline" %></title>
7
+
8
+ <%= stylesheet_link_tag "faultline/application", "data-turbo-track": "reload" %>
9
+
10
+ <% if respond_to?(:javascript_importmap_tags) %>
11
+ <%= javascript_importmap_tags %>
12
+ <% end %>
13
+
14
+ <% if respond_to?(:turbo_refreshes_with) %>
15
+ <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
16
+ <% end %>
17
+ <%= csrf_meta_tags %>
18
+ </head>
19
+ <body class="h-full">
20
+ <div class="min-h-full">
21
+ <nav class="bg-white shadow-sm border-b border-gray-200">
22
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
23
+ <div class="flex justify-between h-16">
24
+ <div class="flex items-center">
25
+ <div class="flex-shrink-0 flex items-center">
26
+ <span class="text-indigo-600 font-bold text-xl">⚡</span>
27
+ <span class="ml-2 text-lg font-semibold text-gray-900">Faultline</span>
28
+ </div>
29
+ </div>
30
+ <div class="flex items-center space-x-4">
31
+ <% if main_app.respond_to?(:root_path) %>
32
+ <%= link_to "Dashboard", main_app.root_path, class: "text-gray-500 hover:text-gray-700 text-sm font-medium" %>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </nav>
38
+
39
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
40
+ <% if notice.present? %>
41
+ <div class="mb-4 rounded-md bg-green-50 p-4">
42
+ <div class="flex">
43
+ <div class="flex-shrink-0">
44
+ <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
45
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
46
+ </svg>
47
+ </div>
48
+ <div class="ml-3">
49
+ <p class="text-sm font-medium text-green-800"><%= notice %></p>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+
55
+ <% if alert.present? %>
56
+ <div class="mb-4 rounded-md bg-red-50 p-4">
57
+ <div class="flex">
58
+ <div class="flex-shrink-0">
59
+ <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
60
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
61
+ </svg>
62
+ </div>
63
+ <div class="ml-3">
64
+ <p class="text-sm font-medium text-red-800"><%= alert %></p>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ <% end %>
69
+
14
70
  <%= yield %>
15
71
  </main>
16
- </body>
72
+
73
+ <footer class="border-t border-gray-200 mt-8">
74
+ <div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
75
+ <p class="text-center text-xs text-gray-500">
76
+ Faultline v<%= Faultline::VERSION %> •
77
+ <%= link_to "RSS Feed", feed_logged_exceptions_path(format: :rss), class: "text-indigo-600 hover:text-indigo-500" %>
78
+ </p>
79
+ </div>
80
+ </footer>
81
+ </div>
82
+ </body>
17
83
  </html>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.to_prepare do
4
+ Faultline.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
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ["tamiruhailu@gmail.com"]
8
8
  spec.homepage = "https://github.com/tamiru/faultline-rails"
9
9
  spec.summary = "A Rails 8 exception dashboard powered by Hotwire."
10
- spec.description = "Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible dashboard."
10
+ spec.description = "Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible dashboard with Ransack search."
11
11
  spec.license = "MIT"
12
12
 
13
13
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
@@ -25,4 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "stimulus-rails", "~> 1.3"
26
26
  spec.add_dependency "turbo-rails", "~> 2.0"
27
27
  spec.add_dependency "will_paginate", "~> 4.0"
28
+
29
+ # Optional: Ransack provides advanced search/filter UI
30
+ spec.add_dependency "ransack", "~> 4.0"
28
31
  end
@@ -19,5 +19,15 @@ module Faultline
19
19
  app.config.assets.paths << asset_root
20
20
  end
21
21
  end
22
+
23
+ # Configure Ransack for Faultline models
24
+ initializer "faultline.ransack" do
25
+ ActiveSupport.on_load(:active_record) do
26
+ if defined?(Ransack)
27
+ # Enable Ransack search on LoggedException
28
+ require "faultline/ransack_config"
29
+ end
30
+ end
31
+ end
22
32
  end
23
33
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure Ransack for Faultline LoggedException model.
4
+ # Ransack 4.x automatically adds `.ransack` to ActiveRecord::Relation via its railtie.
5
+ # We just need to declare which attributes are searchable.
6
+ module Faultline
7
+ module RansackConfig
8
+ def self.apply!
9
+ return unless defined?(Ransack)
10
+
11
+ Faultline::LoggedException.class_eval do
12
+ def self.ransackable_attributes(auth_object = nil)
13
+ %w[action_name backtrace controller_name created_at environment
14
+ exception_class id message remote_ip request
15
+ updated_at user_agent user_info]
16
+ end
17
+
18
+ def self.ransackable_associations(auth_object = nil)
19
+ []
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ Faultline::RansackConfig.apply!
@@ -1,3 +1,3 @@
1
1
  module Faultline
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -14,7 +14,8 @@ module Faultline
14
14
  desc "Install Faultline: creates migration, initializer, and mounts routes."
15
15
 
16
16
  def self.next_migration_number(path)
17
- ActiveRecord::Generators::MigrationGenerator.next_migration_number(path)
17
+ # Generate timestamp-based migration number for Rails 8.1+ compatibility
18
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
18
19
  end
19
20
 
20
21
  def create_migration
@@ -39,3 +39,11 @@ Rails.application.config.to_prepare do
39
39
  # config.per_page = 30
40
40
  end
41
41
  end
42
+
43
+ # Rails 8+ compatibility: Faultline's old rescue_action pattern doesn't work
44
+ # anymore. This registers rescue_from to log exceptions via log_exception_handler.
45
+ #
46
+ # Remove this block if you have your own global exception logging.
47
+ ApplicationController.class_eval do
48
+ rescue_from Exception, with: :log_exception_handler
49
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: faultline-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tamiru Hailu
@@ -71,8 +71,22 @@ dependencies:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
73
  version: '4.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: ransack
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '4.0'
81
+ type: :runtime
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '4.0'
74
88
  description: Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible
75
- dashboard.
89
+ dashboard with Ransack search.
76
90
  email:
77
91
  - tamiruhailu@gmail.com
78
92
  executables: []
@@ -109,6 +123,7 @@ files:
109
123
  - app/views/faultline/logged_exceptions/show.turbo_stream.erb
110
124
  - app/views/layouts/faultline/application.html.erb
111
125
  - config/initializers/date_formats.rb
126
+ - config/initializers/faultline.rb
112
127
  - config/locales/en.yml
113
128
  - config/routes.rb
114
129
  - db/migrate/20240330122311_create_faultline_logged_exceptions.rb
@@ -117,6 +132,7 @@ files:
117
132
  - lib/faultline.rb
118
133
  - lib/faultline/configuration.rb
119
134
  - lib/faultline/engine.rb
135
+ - lib/faultline/ransack_config.rb
120
136
  - lib/faultline/version.rb
121
137
  - lib/generators/faultline/install_generator.rb
122
138
  - lib/generators/faultline/templates/faultline.rb
@@ -144,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
160
  - !ruby/object:Gem::Version
145
161
  version: '0'
146
162
  requirements: []
147
- rubygems_version: 4.0.9
163
+ rubygems_version: 4.0.10
148
164
  specification_version: 4
149
165
  summary: A Rails 8 exception dashboard powered by Hotwire.
150
166
  test_files: []