faultline-rails 0.1.1 → 0.3.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: 220502edbdb1e99c681f01b719d8f2987c6c18fef9e45bccd9a67bb98a17483f
4
- data.tar.gz: a20654cfb53fec6a6778e9e0f8ca7c698ab210fc0aa07b4a78e1950f9ec0a270
3
+ metadata.gz: d0dd621bcbf7b45c01c10e6bec8cb867886d31b19d5244c42a266cc9139e211b
4
+ data.tar.gz: 73b297adfadbc89baca14f1bfc6e563f6907d3dbc01653f3409bb077f9112189
5
5
  SHA512:
6
- metadata.gz: 061df31c39e8a80be6ce09f3039d432ca44ba66470ca5c91c2c83a12a006332792eab3e088de0ab56b1e6ede23c0077f883a5195d0f3b50c3d2e7cace7143b0f
7
- data.tar.gz: 5e23e10e12fea737bf89934a5a2bcf09cf11ca0fd0241cbcd7c64bef5ef7a58f7537e29faf1b55ac6c749fc182bd696393decd9219ca6051cc510e6fafbd81f5
6
+ metadata.gz: 32978645002279c408e17ee353b4493f79d0240a32286274c6ad761e9acc03bf52ab737429c9f51103ae7cfe37aa9496e9a6410f25322196b2fc24eecdaccff1
7
+ data.tar.gz: 31dd749945b65059c43e8e57e49744f5433c5dc101f26e649bdc1c811770fe5b12eaab9233f728bfd88bb7a635c5efe265dd74856cbfbb1afcda7b83774819e3
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
- - Tailwind-compatible utility classes for a responsive dashboard UI.
11
12
 
12
13
  ## Requirements
13
14
 
@@ -24,25 +25,24 @@ Add Faultline to your application:
24
25
  gem "faultline-rails"
25
26
  ```
26
27
 
27
- Install the dependencies and copy the engine migration:
28
+ Run the install generator to set up everything in one step:
28
29
 
29
30
  ```bash
30
31
  bundle install
31
- bin/rails app:faultline:install:migrations
32
+ bin/rails generate faultline:install
32
33
  bin/rails db:migrate
33
34
  ```
34
35
 
35
- Mount the dashboard in your application:
36
-
37
- ```ruby
38
- # config/routes.rb
39
- Rails.application.routes.draw do
40
- mount Faultline::Engine => "/faultline"
41
- end
42
- ```
36
+ This will:
37
+ 1. Copy the database migration with proper indexes.
38
+ 2. Create a configuration initializer at `config/initializers/faultline.rb`.
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.
43
41
 
44
42
  The dashboard is now available at `/faultline`.
45
43
 
44
+ > **Note:** The generator mounts the engine at `/faultline` by default. You can change the mount path by editing `config/routes.rb`.
45
+
46
46
  ## Start logging exceptions
47
47
 
48
48
  Include `Faultline::ExceptionLoggable` in your application controller:
@@ -56,43 +56,141 @@ end
56
56
 
57
57
  Faultline logs the exception and then re-raises it so Rails keeps its normal error handling, status codes, and error pages.
58
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
+
59
71
  ## Protect the dashboard
60
72
 
61
- The dashboard contains sensitive information, including request parameters, environment variables, and source paths. Do not expose it to unauthenticated public users.
73
+ The dashboard contains sensitive information, including request parameters, environment variables, and source paths. **Do not expose it to unauthenticated public users.**
62
74
 
63
- Attach your application's authorization callback:
75
+ By default, the dashboard returns `403 Forbidden` for all requests. Configure authentication in your initializer:
64
76
 
65
77
  ```ruby
66
78
  # config/initializers/faultline.rb
67
79
  Rails.application.config.to_prepare do
68
- Faultline::LoggedExceptionsController.before_action :require_admin!
80
+ Faultline.configure do |config|
81
+ config.auth_block = lambda do |controller|
82
+ # Return true if the user is authorized to view the dashboard.
83
+ # Examples:
84
+ controller.authenticate_user! # Devise
85
+ # controller.current_user&.admin? # Custom auth
86
+ # false # Block everyone (default)
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ Configure Faultline through the block-style DSL in your initializer:
95
+
96
+ ```ruby
97
+ # config/initializers/faultline.rb
98
+ Rails.application.config.to_prepare do
99
+ Faultline.configure do |config|
100
+ # Dashboard title
101
+ config.application_name = "Acme"
102
+
103
+ # Items per page (default: 30)
104
+ config.per_page = 50
105
+
106
+ # Authentication block (see "Protect the dashboard" above)
107
+ config.auth_block = lambda do |controller|
108
+ controller.current_user&.admin?
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ You can also attach additional application data to each recorded exception:
115
+
116
+ ```ruby
117
+ config.exception_data = lambda do |controller|
118
+ {
119
+ request_id: controller.request.request_id,
120
+ user_id: controller.current_user&.id,
121
+ user_email: controller.current_user&.email,
122
+ environment: Rails.env
123
+ }
124
+ end
125
+ ```
126
+
127
+ Exclude trusted private networks from the dashboard's local-request handling:
128
+
129
+ ```ruby
130
+ class ApplicationController < ActionController::Base
131
+ include Faultline::ExceptionLoggable
132
+
133
+ consider_local "10.0.0.0/8", "192.168.0.0/16"
69
134
  end
70
135
  ```
71
136
 
72
- Replace `require_admin!` with the authentication method provided by your application. You can also configure a policy, basic authentication, or an internal-only route at the host application level.
137
+ Rails' `filter_parameters` configuration is respected before request parameters are stored.
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)
73
165
 
74
- ## Rails 8 frontend setup
166
+ ## Frontend setup
75
167
 
76
- Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. Install the host application's Hotwire entry points when needed:
168
+ ### Hotwire (default)
169
+
170
+ Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. If your Rails app already has Hotwire installed (the default for Rails 8), no extra setup is needed.
171
+
172
+ If you need to install Hotwire:
77
173
 
78
174
  ```bash
79
175
  bin/rails turbo:install
80
176
  bin/rails stimulus:install
81
177
  ```
82
178
 
83
- The default Rails 8 import-map setup will load Faultline's Stimulus controller through the engine asset pipeline. If your application uses a JavaScript bundler, register the controller from `app/javascript/controllers/faultline_controller.js` in your Stimulus application:
179
+ ### Styling
84
180
 
85
- ```javascript
86
- import FaultlineController from "./faultline_controller"
181
+ Faultline ships with its own built-in CSS stylesheet that works out of the box. No Tailwind configuration is required.
87
182
 
88
- application.register("faultline", FaultlineController)
89
- ```
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.
90
186
 
91
- ## Tailwind setup
187
+ If you want to configure the gem's view directory as a Tailwind source, use the absolute path returned by Bundler:
92
188
 
93
- Faultline's dashboard uses Tailwind utility classes. Add the gem's view directory to the sources scanned by your Tailwind build; otherwise the host application will purge the dashboard classes.
189
+ ```bash
190
+ bundle show faultline-rails
191
+ ```
94
192
 
95
- For Tailwind CSS v4, add a source entry to the application's Tailwind stylesheet. Use the absolute path returned by Bundler for the installed gem:
193
+ For Tailwind CSS v4, add a source entry:
96
194
 
97
195
  ```css
98
196
  @import "tailwindcss";
@@ -112,51 +210,35 @@ module.exports = {
112
210
  }
113
211
  ```
114
212
 
115
- You can find the installed path with:
213
+ #### Non-importmap projects
116
214
 
117
- ```bash
118
- bundle show faultline-rails
119
- ```
120
-
121
- ## Configuration
122
-
123
- Set a title for the dashboard and attach additional application data to each record:
124
-
125
- ```ruby
126
- # config/initializers/faultline.rb
127
- Rails.application.config.to_prepare do
128
- Faultline::LoggedExceptionsController.application_name = "Acme"
129
-
130
- ApplicationController.exception_data = lambda do |controller|
131
- {
132
- request_id: controller.request.request_id,
133
- user_id: controller.current_user&.id
134
- }
135
- end
136
- end
137
- ```
215
+ Faultline works with **importmap**, **sprockets**, **propshaft**, or **no JS pipeline** at all. The layout adapts to your asset pipeline:
138
216
 
139
- Exclude trusted private networks from the dashboard's local-request handling:
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.
140
220
 
141
- ```ruby
142
- class ApplicationController < ActionController::Base
143
- include Faultline::ExceptionLoggable
221
+ ### Bootstrap / Other CSS frameworks
144
222
 
145
- consider_local "10.0.0.0/8", "192.168.0.0/16"
146
- end
147
- ```
223
+ Faultline's views use Tailwind CSS utility classes. To use Bootstrap or another framework:
148
224
 
149
- Rails' `filter_parameters` configuration is respected before request parameters are stored.
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.
150
231
 
151
232
  ## Dashboard features
152
233
 
153
- - Search exception messages.
154
- - Filter by exception class, controller/action, or age.
155
- - Open full exception details in a Turbo Frame.
156
- - Delete individual exceptions without leaving the page.
157
- - Delete the currently visible result set.
158
- - Clear the complete history.
159
- - 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`.
160
242
 
161
243
  ## Data storage
162
244
 
@@ -1,33 +1,168 @@
1
- /*
2
- * Faultline's views use Tailwind utility classes. Add the gem's view path to
3
- * the host application's Tailwind sources so those classes are compiled.
4
- *
5
- * This small fallback keeps will_paginate usable when a host app has not yet
6
- * added Tailwind's pagination styles.
7
- */
8
- .faultline-pagination .pagination {
9
- display: flex;
10
- flex-wrap: wrap;
11
- gap: 0.5rem;
12
- align-items: center;
13
- }
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
+ ═══════════════════════════════════════════════════════════════════ */
14
8
 
15
- .faultline-pagination .pagination a,
16
- .faultline-pagination .pagination span {
17
- padding: 0.35rem 0.65rem;
18
- border-radius: 0.5rem;
9
+ :root {
10
+ --fl-primary: #4f46e5;
11
+ --fl-primary-hover: #4338ca;
12
+ --fl-bg: #f8fafc;
13
+ --fl-surface: #ffffff;
14
+ --fl-border: #e2e8f0;
15
+ --fl-text: #0f172a;
16
+ --fl-text-secondary: #64748b;
17
+ --fl-text-muted: #94a3b8;
18
+ --fl-danger: #dc2626;
19
+ --fl-danger-hover: #b91c1c;
20
+ --fl-danger-bg: #fef2f2;
21
+ --fl-radius: 0.75rem;
22
+ --fl-radius-sm: 0.5rem;
19
23
  }
20
24
 
21
- .faultline-pagination .pagination a {
22
- color: #4338ca;
25
+ /* Base reset */
26
+ *, *::before, *::after { box-sizing: border-box; }
27
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.5; color: var(--fl-text); background: var(--fl-bg); }
28
+ a { color: var(--fl-primary); text-decoration: none; }
29
+ a:hover { text-decoration: underline; }
30
+ h1, h2, h3, h4, p { margin: 0; }
31
+ table { border-collapse: collapse; width: 100%; }
32
+
33
+ /* Layout */
34
+ .fl-container { max-width: 80rem; margin: 0 auto; padding: 2rem 1rem; }
35
+ .fl-grid { display: grid; gap: 1.5rem; grid-template-columns: 16rem minmax(0, 1fr); }
36
+ @media (max-width: 1024px) { .fl-grid { grid-template-columns: 1fr; } }
37
+
38
+ /* Card */
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; }
40
+
41
+ /* Header */
42
+ .fl-header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 2rem; }
43
+ .fl-header h1 { font-size: 1.875rem; font-weight: 700; letter-spacing: -0.025em; margin-top: 0.5rem; }
44
+ .fl-brand { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--fl-primary); }
45
+ .fl-subtitle { color: var(--fl-text-secondary); margin-top: 0.5rem; }
46
+
47
+ /* Sidebar */
48
+ .fl-sidebar { padding: 1.25rem; }
49
+ .fl-sidebar h2 { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); }
50
+ .fl-sidebar h3 { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); margin-top: 1.5rem; }
51
+ .fl-nav { list-style: none; padding: 0; margin: 0.5rem 0 0; }
52
+ .fl-nav li { margin: 0.25rem 0; }
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); }
54
+ .fl-nav a:hover { background: #f1f5f9; color: var(--fl-text); text-decoration: none; }
55
+ .fl-nav-scroll { max-height: 10rem; overflow-y: auto; }
56
+
57
+ /* Search */
58
+ .fl-search { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
59
+ .fl-search input[type="search"] { flex: 1; min-width: 0; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: var(--fl-radius-sm); font-size: 0.875rem; }
60
+ .fl-search input[type="search"]:focus { outline: none; border-color: var(--fl-primary); box-shadow: 0 0 0 2px rgba(79,70,229,0.2); }
61
+
62
+ /* Buttons */
63
+ .fl-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.5rem 1rem; border: none; border-radius: var(--fl-radius-sm); font-size: 0.875rem; font-weight: 600; cursor: pointer; text-decoration: none; }
64
+ .fl-btn-primary { background: var(--fl-primary); color: white; }
65
+ .fl-btn-primary:hover { background: var(--fl-primary-hover); text-decoration: none; }
66
+ .fl-btn-danger { background: var(--fl-danger); color: white; }
67
+ .fl-btn-danger:hover { background: var(--fl-danger-hover); text-decoration: none; }
68
+ .fl-btn-outline { background: transparent; border: 1px solid #e2e8f0; color: #b91c1c; }
69
+ .fl-btn-outline:hover { background: var(--fl-danger-bg); text-decoration: none; }
70
+ .fl-btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
71
+ .fl-btn-ghost { background: transparent; color: var(--fl-text-secondary); }
72
+ .fl-btn-ghost:hover { background: #f1f5f9; text-decoration: none; }
73
+
74
+ /* Table */
75
+ .fl-table-wrap { overflow-x: auto; }
76
+ .fl-table { min-width: 100%; }
77
+ .fl-table thead { background: #f8fafc; }
78
+ .fl-table th { padding: 0.75rem 1.25rem; text-align: left; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); }
79
+ .fl-table td { padding: 1rem 1.25rem; vertical-align: top; border-top: 1px solid #f1f5f9; }
80
+ .fl-table tr:hover { background: #f8fafc; }
81
+ .fl-table .fl-name { font-weight: 500; color: var(--fl-primary); }
82
+ .fl-table .fl-name:hover { text-decoration: underline; }
83
+ .fl-table .fl-msg { margin-top: 0.25rem; font-size: 0.875rem; color: var(--fl-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40rem; }
84
+ .fl-table .fl-date { font-size: 0.875rem; color: var(--fl-text-secondary); white-space: nowrap; }
85
+
86
+ /* Actions */
87
+ .fl-actions { text-align: right; white-space: nowrap; }
88
+ .fl-delete-link { font-size: 0.875rem; font-weight: 500; color: var(--fl-danger); }
89
+ .fl-delete-link:hover { color: var(--fl-danger-hover); text-decoration: underline; }
90
+
91
+ /* Detail panel */
92
+ .fl-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--fl-border); }
93
+ .fl-detail-header h2 { font-size: 1.25rem; font-weight: 600; word-break: break-word; margin-top: 0.25rem; }
94
+ .fl-detail-time { font-size: 0.875rem; color: var(--fl-text-secondary); }
95
+ .fl-detail-body { margin-top: 1.25rem; }
96
+ .fl-detail-section { margin-bottom: 1.25rem; }
97
+ .fl-detail-section h3 { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); margin-bottom: 0.5rem; }
98
+ .fl-detail-code { background: #f8fafc; padding: 1rem; border-radius: var(--fl-radius-sm); overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-size: 0.875rem; }
99
+ .fl-detail-pre { background: #0f172a; color: #e2e8f0; padding: 1rem; border-radius: var(--fl-radius-sm); overflow: auto; max-height: 24rem; font-size: 0.75rem; line-height: 1.625; }
100
+ .fl-detail-footer { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--fl-border); }
101
+
102
+ /* Empty state */
103
+ .fl-empty { padding: 4rem 1.25rem; text-align: center; color: var(--fl-text-secondary); font-size: 0.875rem; }
104
+
105
+ /* Status bar */
106
+ .fl-status { background: #eef2ff; color: #4338ca; padding: 0.75rem 1rem; border-radius: var(--fl-radius-sm); font-size: 0.875rem; margin-bottom: 1rem; }
107
+ .fl-status[hidden] { display: none; }
108
+
109
+ /* Pagination */
110
+ .fl-pagination { border-top: 1px solid var(--fl-border); padding: 1rem 1.25rem; font-size: 0.875rem; }
111
+ .fl-pagination .pagination { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; list-style: none; margin: 0; padding: 0; }
112
+ .fl-pagination .pagination a,
113
+ .fl-pagination .pagination span { padding: 0.375rem 0.75rem; border-radius: var(--fl-radius-sm); }
114
+ .fl-pagination .pagination a { color: var(--fl-primary); }
115
+ .fl-pagination .pagination a:hover { background: #eef2ff; text-decoration: none; }
116
+ .fl-pagination .pagination .current { background: var(--fl-primary); color: white; font-weight: 600; }
117
+
118
+ /* Feed link */
119
+ .fl-feed { margin-top: 1rem; }
120
+ .fl-feed a { font-size: 0.8125rem; color: var(--fl-text-muted); }
121
+ .fl-feed a:hover { color: var(--fl-primary); }
122
+
123
+ /* Utility */
124
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
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;
23
143
  }
24
144
 
25
- .faultline-pagination .pagination a:hover {
26
- background: #eef2ff;
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;
27
148
  }
28
149
 
29
- .faultline-pagination .pagination .current {
30
- background: #4f46e5;
31
- color: white;
32
- font-weight: 600;
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;
33
168
  }
@@ -1,17 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faultline
2
4
  class LoggedExceptionsController < ApplicationController
3
- cattr_accessor :application_name
5
+ before_action :faultline_require_auth!
4
6
 
5
7
  helper_method :params_filters
6
8
 
7
9
  def index
8
10
  @exception_names = LoggedException.class_names
9
11
  @controller_actions = LoggedException.controller_actions
10
- @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)
11
15
  end
12
16
 
13
17
  def query
14
- @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)
15
21
 
16
22
  respond_to do |format|
17
23
  format.turbo_stream
@@ -53,7 +59,10 @@ module Faultline
53
59
  filtered_scope
54
60
  end
55
61
  exceptions.delete_all
56
- @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)
57
66
 
58
67
  respond_to do |format|
59
68
  format.turbo_stream
@@ -63,18 +72,42 @@ module Faultline
63
72
 
64
73
  def clear
65
74
  LoggedException.delete_all
66
- @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)
67
79
 
68
80
  respond_to do |format|
69
81
  format.turbo_stream
70
- format.html { redirect_back fallback_location: root_path }
82
+ format.html { redirect_back fallback_location: faultline_root_path }
71
83
  end
72
84
  end
73
85
 
74
86
  private
75
87
 
88
+ def faultline_require_auth!
89
+ auth_block = Faultline.configuration.auth_block
90
+ return if auth_block&.call(self)
91
+
92
+ head :forbidden
93
+ end
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
+
76
108
  def params_filters
77
109
  {
110
+ q: params[:q],
78
111
  query: params[:query],
79
112
  date_ranges_filter: params[:date_ranges_filter],
80
113
  exception_names_filter: params[:exception_names_filter],
@@ -82,10 +115,6 @@ module Faultline
82
115
  }.compact
83
116
  end
84
117
 
85
- def filtered_exceptions
86
- filtered_scope.paginate(page: params[:page], per_page: 30)
87
- end
88
-
89
118
  def filtered_scope
90
119
  exceptions = LoggedException.sorted
91
120
  exceptions = exceptions.where(id: params[:id]) if params[:id].present?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faultline
2
4
  module LoggedExceptionsHelper
3
5
  def pretty_exception_date(exception)
@@ -9,7 +11,8 @@ module Faultline
9
11
  end
10
12
 
11
13
  def filtered?
12
- [: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?
13
16
  end
14
17
 
15
18
  def listify(text)
@@ -18,7 +21,7 @@ module Faultline
18
21
  end
19
22
 
20
23
  def page_title(text)
21
- title = [controller.application_name.presence, text].compact.join(" :: ")
24
+ title = [Faultline.application_name.presence, text].compact.join(" :: ")
22
25
  content_for(:title, title)
23
26
  end
24
27
 
@@ -30,5 +33,18 @@ module Faultline
30
33
  simple_format(text).html_safe
31
34
  end
32
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
33
49
  end
34
50
  end