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 +4 -4
- data/README.md +144 -62
- data/app/assets/stylesheets/faultline/application.css +160 -25
- data/app/controllers/faultline/logged_exceptions_controller.rb +39 -10
- data/app/helpers/faultline/logged_exceptions_helper.rb +18 -2
- data/app/models/faultline/logged_exception.rb +17 -8
- data/app/views/faultline/logged_exceptions/_exceptions.html.erb +77 -26
- data/app/views/faultline/logged_exceptions/_feed.html.erb +2 -4
- data/app/views/faultline/logged_exceptions/_filter_group.html.erb +4 -4
- data/app/views/faultline/logged_exceptions/_show.html.erb +69 -30
- data/app/views/faultline/logged_exceptions/feed.rss.builder +1 -1
- data/app/views/faultline/logged_exceptions/index.html.erb +126 -46
- data/app/views/faultline/logged_exceptions/show.html.erb +1 -1
- data/app/views/layouts/faultline/application.html.erb +79 -26
- data/config/initializers/faultline.rb +18 -0
- data/db/migrate/20240330122311_create_faultline_logged_exceptions.rb +4 -0
- data/faultline-rails.gemspec +4 -1
- data/lib/faultline/configuration.rb +33 -0
- data/lib/faultline/engine.rb +12 -0
- data/lib/faultline/ransack_config.rb +24 -0
- data/lib/faultline/version.rb +1 -1
- data/lib/faultline.rb +29 -1
- data/lib/generators/faultline/install_generator.rb +63 -0
- data/lib/generators/faultline/templates/faultline.rb +49 -0
- data/lib/generators/faultline/templates/migration.rb +24 -0
- data/lib/tasks/faultline_tasks.rake +37 -4
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0dd621bcbf7b45c01c10e6bec8cb867886d31b19d5244c42a266cc9139e211b
|
|
4
|
+
data.tar.gz: 73b297adfadbc89baca14f1bfc6e563f6907d3dbc01653f3409bb077f9112189
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
28
|
+
Run the install generator to set up everything in one step:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
bundle install
|
|
31
|
-
bin/rails
|
|
32
|
+
bin/rails generate faultline:install
|
|
32
33
|
bin/rails db:migrate
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Rails.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
166
|
+
## Frontend setup
|
|
75
167
|
|
|
76
|
-
|
|
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
|
-
|
|
179
|
+
### Styling
|
|
84
180
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
```bash
|
|
190
|
+
bundle show faultline-rails
|
|
191
|
+
```
|
|
94
192
|
|
|
95
|
-
For Tailwind CSS v4, add a source entry
|
|
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
|
-
|
|
213
|
+
#### Non-importmap projects
|
|
116
214
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
class ApplicationController < ActionController::Base
|
|
143
|
-
include Faultline::ExceptionLoggable
|
|
221
|
+
### Bootstrap / Other CSS frameworks
|
|
144
222
|
|
|
145
|
-
|
|
146
|
-
end
|
|
147
|
-
```
|
|
223
|
+
Faultline's views use Tailwind CSS utility classes. To use Bootstrap or another framework:
|
|
148
224
|
|
|
149
|
-
|
|
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
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
- Delete
|
|
158
|
-
-
|
|
159
|
-
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 = [
|
|
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
|