faultline-rails 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24fbd55d2484532bff1b060c9526e48e05192bb6199c38f05530958711b8dc53
4
- data.tar.gz: f736030fb9ce0e5d9cb40ad8751499bb27997be0126ab781b0907ac9901925c6
3
+ metadata.gz: 870d33f7e1f9311ee4e932c60c0b430f91a5bf3f5e2e5c446bf69753647f8eda
4
+ data.tar.gz: 36a4dfce3b186ad20c60f849cc0d7a220579014bf44aa630dab6843d4bd68efa
5
5
  SHA512:
6
- metadata.gz: 2b4e8ec5a835b68a52cf24c9b9875a652733db1a59a82e255aff0b579b0e210ec42c8f4ea4a4ec1fd1b92db428dbc8de582ba9166801c5bacc7c66967eafe12e
7
- data.tar.gz: 68cdb0c541e6b50305bee182b0f739bb70551acf81b576191915da68a9079d9bdfadbee88d151b08aff54b2ef5bcb588b3e75ac3cca00e96f6452b995d0298c8
6
+ metadata.gz: f9311ba143ef33928040c5fcbe360fcd71fd2741d1f03f7793eac409545c988b9c3043c019f64217a0d907a7dde4d593cc5c9e0fffdd8aae9d4d0e334a0b3de2
7
+ data.tar.gz: 7cdae081decc10a6c3d8c406486aec7887fd89ee0d388c7da2da26ea8c7597fb62d34b56912feb7e20263fbb5d745409c36dd2aae57066a648d379e8caf10219
data/README.md CHANGED
@@ -5,13 +5,27 @@
5
5
 
6
6
  Faultline is a production-friendly exception dashboard for Rails 8. It records unhandled application exceptions and gives your team a fast, searchable view of messages, requests, environments, and backtraces.
7
7
 
8
- The dashboard is server-rendered and progressively enhanced with:
9
-
10
- - **Tailwind CSS** layout that works with or without Tailwind installed.
11
- - **Ransack** for advanced search and sortable columns.
12
- - Turbo Frames for filtering and opening exception details without full-page navigation.
13
- - Turbo Streams for deleting one, many, or all exceptions.
14
- - Stimulus for loading state and small interaction behavior.
8
+ The dashboard features a polished dark/light theme, keyboard shortcuts, tabbed detail view, stats overview, and webhook notifications.
9
+
10
+ ## Features
11
+
12
+ - **Dark / Light theme** Toggle with the sun/moon button or press `t`. Remembers preference in localStorage.
13
+ - **Keyboard shortcuts** Press `?` to see all shortcuts. Navigate with `j`/`k`, focus search with `/`, open details with `Enter`.
14
+ - **Stats overview** Total, weekly, today, and unique class counts at a glance.
15
+ - **Advanced filters** — Exception type, controller, time range, and full-text search with debounced input.
16
+ - **Tabbed detail view** — Overview, Backtrace, Request, Environment, and User Info tabs.
17
+ - **Prev/Next navigation** — Navigate between exceptions without returning to the list.
18
+ - **Copy to clipboard** — One-click copy for exception ID, message, and backtrace.
19
+ - **Collapsible backtrace** — Long traces collapse automatically; expand on demand.
20
+ - **Metadata grid** — Structured detail cards for exception class, controller, action, time, IP, and user agent.
21
+ - **Page size selector** — Choose 25, 50, or 100 results per page.
22
+ - **Active filter pills** — See and dismiss active filters with one click.
23
+ - **RSS feed** — Subscribe to `/faultline/logged_exceptions/feed.rss`.
24
+ - **Webhook notifications** — POST to Slack, Discord, or custom endpoints on each new exception.
25
+ - **Telegram notifications** — Built-in notifier with rate limiting.
26
+ - **Sidekiq middleware** — Automatically logs background job exceptions.
27
+ - **Cleanup / Retention** — Auto-delete old exceptions via rake tasks.
28
+ - **Structured logging** — JSON log output with request context and metadata.
15
29
 
16
30
  ## Requirements
17
31
 
@@ -28,7 +42,7 @@ Add Faultline to your application:
28
42
  gem "faultline-rails"
29
43
  ```
30
44
 
31
- Run the install generator to set up everything in one step:
45
+ Run the install generator:
32
46
 
33
47
  ```bash
34
48
  bundle install
@@ -40,53 +54,74 @@ This will:
40
54
  1. Copy the database migration with proper indexes.
41
55
  2. Create a configuration initializer at `config/initializers/faultline.rb`.
42
56
  3. Mount the engine in your routes.
43
- 4. Add `rescue_from Exception, with: :log_exception_handler` to your `ApplicationController` for Rails 8 compatibility.
57
+ 4. Add `rescue_from Exception, with: :log_exception_handler` to `ApplicationController`.
44
58
 
45
59
  The dashboard is now available at `/faultline`.
46
60
 
47
- > **Note:** The generator mounts the engine at `/faultline` by default. You can change the mount path by editing `config/routes.rb`.
61
+ ### Modern UI setup
62
+
63
+ Generate the polished dark/light theme with keyboard shortcuts:
64
+
65
+ ```bash
66
+ bin/rails generate faultline:customize
67
+ bin/rails stimulus:manifest:update
68
+ ```
69
+
70
+ This generates:
71
+ - Self-contained layout with sidebar, topbar, and theme toggle
72
+ - Tabbed detail view with metadata grid
73
+ - Keyboard shortcuts dialog
74
+ - Stimulus controllers for theme, sidebar, and interactions
75
+
76
+ Options: `--layout-only`, `--views-only`, `--stimulus-only`, `--initializer-only`
48
77
 
49
78
  ## Start logging exceptions
50
79
 
51
80
  Include `Faultline::ExceptionLoggable` in your application controller:
52
81
 
53
82
  ```ruby
54
- # app/controllers/application_controller.rb
55
83
  class ApplicationController < ActionController::Base
56
84
  include Faultline::ExceptionLoggable
57
85
  end
58
86
  ```
59
87
 
60
- Faultline logs the exception and then re-raises it so Rails keeps its normal error handling, status codes, and error pages.
88
+ Faultline logs the exception and then re-raises it so Rails keeps its normal error handling.
61
89
 
62
- ### Rails 8+ compatibility
90
+ ### API controllers
63
91
 
64
- 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.
92
+ For `ActionController::API` subclasses (e.g., API namespaces):
65
93
 
66
- If you need to handle this manually:
94
+ ```ruby
95
+ class Api::V1::BaseController < ActionController::API
96
+ include Faultline::ExceptionLoggable
97
+ end
98
+ ```
99
+
100
+ ### Background jobs (Sidekiq)
101
+
102
+ Faultline automatically logs Sidekiq job exceptions when the middleware is configured:
67
103
 
68
104
  ```ruby
69
- class ApplicationController < ActionController::Base
70
- rescue_from Exception, with: :log_exception_handler
105
+ # config/initializers/sidekiq.rb
106
+ Sidekiq.configure_server do |config|
107
+ config.server_middleware do |chain|
108
+ chain.add FaultlineSidekiqMiddleware
109
+ end
71
110
  end
72
111
  ```
73
112
 
74
113
  ## Protect the dashboard
75
114
 
76
- The dashboard contains sensitive information, including request parameters, environment variables, and source paths. **Do not expose it to unauthenticated public users.**
115
+ The dashboard contains sensitive information. **Do not expose it to unauthenticated users.**
77
116
 
78
- By default, the dashboard returns `403 Forbidden` for all requests. Configure authentication in your initializer:
117
+ By default, the dashboard returns `403 Forbidden` for all requests. Configure authentication:
79
118
 
80
119
  ```ruby
81
120
  # config/initializers/faultline.rb
82
121
  Rails.application.config.to_prepare do
83
122
  Faultline.configure do |config|
84
123
  config.auth_block = lambda do |controller|
85
- # Return true if the user is authorized to view the dashboard.
86
- # Examples:
87
- controller.authenticate_user! # Devise
88
- # controller.current_user&.admin? # Custom auth
89
- # false # Block everyone (default)
124
+ controller.current_user&.admin?
90
125
  end
91
126
  end
92
127
  end
@@ -94,167 +129,240 @@ end
94
129
 
95
130
  ## Configuration
96
131
 
97
- Configure Faultline through the block-style DSL in your initializer:
98
-
99
132
  ```ruby
100
133
  # config/initializers/faultline.rb
101
134
  Rails.application.config.to_prepare do
102
135
  Faultline.configure do |config|
103
- # Dashboard title
104
- config.application_name = "Acme"
136
+ # ─── General ─────────────────────────────────────────────
137
+ config.application_name = "My App" # Dashboard title
138
+ config.per_page = 50 # Items per page (default: 30)
105
139
 
106
- # Items per page (default: 30)
107
- config.per_page = 50
108
-
109
- # Authentication block (see "Protect the dashboard" above)
140
+ # ─── Authentication ──────────────────────────────────────
110
141
  config.auth_block = lambda do |controller|
111
142
  controller.current_user&.admin?
112
143
  end
113
- end
114
- end
115
- ```
116
144
 
117
- You can also attach additional application data to each recorded exception:
145
+ # ─── Exception Data ──────────────────────────────────────
146
+ config.exception_data = lambda do |controller|
147
+ {
148
+ user_id: controller.current_user&.id,
149
+ request_id: controller.request.request_id,
150
+ environment: Rails.env
151
+ }
152
+ end
118
153
 
119
- ```ruby
120
- config.exception_data = lambda do |controller|
121
- {
122
- request_id: controller.request.request_id,
123
- user_id: controller.current_user&.id,
124
- user_email: controller.current_user&.email,
125
- environment: Rails.env
126
- }
154
+ # ─── Notifications ───────────────────────────────────────
155
+ config.after_create = lambda do |exception|
156
+ MyNotifier.alert(exception)
157
+ end
158
+
159
+ config.webhooks = ["https://hooks.slack.com/services/xxx"]
160
+ config.webhook_timeout = 5
161
+ config.webhook_headers = { "Authorization" => "Bearer token" }
162
+
163
+ # ─── Cleanup ─────────────────────────────────────────────
164
+ config.retention_days = 90 # Auto-delete after N days
165
+
166
+ # ─── Appearance ──────────────────────────────────────────
167
+ config.theme = "auto" # "light", "dark", or "auto"
168
+ config.body_class = "" # Custom body CSS class
169
+ config.use_host_layout = false # Use host app's layout
170
+
171
+ # ─── Advanced UI ─────────────────────────────────────────
172
+ config.show_stats = true # Stats overview cards
173
+ config.keyboard_shortcuts = true # Keyboard shortcuts
174
+ config.enable_navigation = true # Prev/next in detail view
175
+ config.backtrace_limit = 30 # Frames before collapse
176
+ config.show_metadata = true # Metadata grid
177
+ config.show_environment = false # Environment tab
178
+ config.show_request = true # Request params tab
179
+ config.page_size_options = [25, 50, 100] # Page size selector
180
+
181
+ # ─── Sidebar Links ───────────────────────────────────────
182
+ config.sidebar_links = [
183
+ { label: "GitHub", url: "https://github.com/myorg/myapp" },
184
+ { label: "Docs", url: "/docs" }
185
+ ]
186
+
187
+ # ─── Logging ─────────────────────────────────────────────
188
+ config.logging_enabled = true
189
+ config.log_level = :info # :debug, :info, :warn, :error, :fatal
190
+ config.log_file = "log/faultline.log" # JSON log file (nil = none)
191
+ config.log_backtrace = true
192
+ config.log_params = true
193
+ config.log_user_info = true
194
+ config.log_backtrace_limit = 20
195
+ config.deduplication_enabled = false
196
+ config.deduplication_window = 300 # Seconds
197
+ end
127
198
  end
128
199
  ```
129
200
 
130
- Exclude trusted private networks from the dashboard's local-request handling:
201
+ ## Keyboard shortcuts
131
202
 
132
- ```ruby
133
- class ApplicationController < ActionController::Base
134
- include Faultline::ExceptionLoggable
203
+ Press `?` in the dashboard to see all shortcuts:
135
204
 
136
- consider_local "10.0.0.0/8", "192.168.0.0/16"
137
- end
138
- ```
205
+ | Key | Action |
206
+ |---|---|
207
+ | `/` | Focus search |
208
+ | `j` or `↓` | Navigate down |
209
+ | `k` or `↑` | Navigate up |
210
+ | `Enter` | Open selected exception |
211
+ | `p` or `←` | Previous exception |
212
+ | `n` or `→` | Next exception |
213
+ | `t` | Toggle dark/light theme |
214
+ | `1`-`5` | Switch tabs |
215
+ | `Esc` | Close panel / Clear |
216
+ | `?` | Show shortcuts dialog |
139
217
 
140
- Rails' `filter_parameters` configuration is respected before request parameters are stored.
218
+ ## Search & Filter
141
219
 
142
- ## Search & Filter (Ransack)
220
+ - **Exception Type** Select dropdown, auto-submits on change
221
+ - **Controller** — Select dropdown, auto-submits on change
222
+ - **Time Range** — Today, 3 days, 7 days, 30 days
223
+ - **Search** — Full-text search with 400ms debounce
224
+ - **Active Filters** — Pills with one-click dismiss
143
225
 
144
- Faultline includes [Ransack](https://github.com/activerecord-hackery/ransack) for advanced search and filtering. The dashboard provides:
226
+ All filters work with Ransack for advanced search, or fall back to built-in scopes.
145
227
 
146
- - **Search form** — Search by exception class, controller name, and message text.
147
- - **Sortable columns** — Click column headers to sort by exception class, controller, action, or date.
148
- - **Sidebar filters** — Quick filter by exception class, controller/action, and time range (today, 3 days, 7 days, 30 days).
228
+ ## Notifications
149
229
 
150
- If your app doesn't include Ransack, Faultline falls back to the built-in sidebar filters automatically.
230
+ ### Webhooks
151
231
 
152
- ### Searching
232
+ ```ruby
233
+ config.webhooks = [
234
+ "https://hooks.slack.com/services/T00/B00/xxx",
235
+ "https://discord.com/api/webhooks/xxx"
236
+ ]
237
+ config.webhook_timeout = 5
238
+ config.webhook_headers = { "Authorization" => "Bearer token" }
239
+ ```
153
240
 
154
- Type in the search form fields and click "Search" to filter exceptions:
241
+ POST JSON to each URL on every new exception:
242
+ ```json
243
+ {
244
+ "exception_class": "RuntimeError",
245
+ "controller_name": "users",
246
+ "action_name": "show",
247
+ "message": "Something went wrong",
248
+ "created_at": "2026-08-22T12:00:00Z",
249
+ "dashboard_url": "http://localhost:3000/faultline/logged_exceptions/42"
250
+ }
251
+ ```
155
252
 
156
- - **Exception Class** — Search by exception type (e.g., `RuntimeError`, `ActiveRecord::RecordNotFound`)
157
- - **Controller** — Search by controller name (e.g., `users`, `posts`)
158
- - **Message** — Full-text search across exception messages
253
+ ### Telegram
159
254
 
160
- ### Sorting
255
+ Set `TELEGRAM_TOKEN` and `TELEGRAM_CHAT_ID` environment variables:
161
256
 
162
- Click any column header in the exceptions table to sort:
257
+ ```bash
258
+ export TELEGRAM_TOKEN="your-bot-token"
259
+ export TELEGRAM_CHAT_ID="your-chat-id"
260
+ ```
163
261
 
164
- - **Exception** — Sort alphabetically by exception class
165
- - **Controller** — Sort by controller name
166
- - **Action** — Sort by action name
167
- - **Date** — Sort by creation date (newest/oldest first)
262
+ Configure the notifier:
168
263
 
169
- ## Frontend setup
264
+ ```ruby
265
+ config.after_create = lambda do |exception|
266
+ Faultline::TelegramNotifier.new.notify(exception)
267
+ end
268
+ ```
170
269
 
171
- ### Hotwire (default)
270
+ Rate limiting: 10-minute cooldown per exception class + controller combination.
271
+
272
+ ## Cleanup / Retention
172
273
 
173
- 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.
274
+ ```ruby
275
+ config.retention_days = 90 # Auto-delete after 90 days
276
+ ```
174
277
 
175
- If you need to install Hotwire:
278
+ Rake tasks:
176
279
 
177
280
  ```bash
178
- bin/rails turbo:install
179
- bin/rails stimulus:install
281
+ rake faultline:cleanup # Delete exceptions older than retention_days
282
+ rake faultline:stats # Show exception statistics
283
+ rake faultline:tail # Tail JSON logs in real-time
284
+ rake faultline:export # Export exceptions to JSON
285
+ rake faultline:test_webhook # Test webhook configuration
180
286
  ```
181
287
 
182
- ### Styling
288
+ ## Frontend setup
183
289
 
184
- Faultline ships with its own built-in CSS stylesheet that works out of the box. No Tailwind configuration is required.
290
+ ### Hotwire (default)
185
291
 
186
- #### Tailwind CSS
292
+ Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. For Rails 8, no extra setup is needed.
187
293
 
188
- If your application uses Tailwind CSS, Faultline's views are already Tailwind-styled and will automatically use your Tailwind theme.
294
+ ### Styling
189
295
 
190
- If you want to configure the gem's view directory as a Tailwind source, use the absolute path returned by Bundler:
296
+ Faultline ships with its own CSS (`faultline/dashboard.css`) that works out of the box with dark/light themes.
191
297
 
192
- ```bash
193
- bundle show faultline-rails
194
- ```
298
+ #### Tailwind CSS
195
299
 
196
- For Tailwind CSS v4, add a source entry:
300
+ If your app uses Tailwind, add the gem's views as a source:
197
301
 
198
302
  ```css
303
+ /* Tailwind v4 */
199
304
  @import "tailwindcss";
200
- @source "/absolute/path/to/faultline-rails/app/views";
305
+ @source "/path/to/faultline-rails/app/views";
201
306
  ```
202
307
 
203
- For Tailwind CSS v3, add the gem path to `content` in `tailwind.config.js`:
308
+ #### Esbuild / Propshaft
204
309
 
205
- ```javascript
206
- const faultlinePath = "/absolute/path/to/faultline-rails"
310
+ The layout automatically detects your asset pipeline and loads `application.js`:
207
311
 
208
- module.exports = {
209
- content: [
210
- "./app/views/**/*.{erb,html}",
211
- `${faultlinePath}/app/views/**/*.html.erb`
212
- ]
213
- }
312
+ ```erb
313
+ <% if defined?(Propshaft) || Rails.application.assets&.find_asset("application.js") %>
314
+ <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
315
+ <% elsif respond_to?(:javascript_importmap_tags) %>
316
+ <%= javascript_importmap_tags %>
317
+ <% end %>
214
318
  ```
215
319
 
216
- #### Non-importmap projects
217
-
218
- Faultline works with **importmap**, **sprockets**, **propshaft**, or **no JS pipeline** at all. The layout adapts to your asset pipeline:
320
+ #### Importmap
219
321
 
220
- - If `javascript_importmap_tags` is available, it uses importmap.
221
- - If `turbo_refreshes_with` is available, it enables Turbo morph scrolling.
222
- - If neither is available, the dashboard works without JavaScript.
322
+ Works automatically. Run `stimulus:manifest:update` to register the faultline controllers.
223
323
 
224
- ### Bootstrap / Other CSS frameworks
324
+ ### Stimulus controllers
225
325
 
226
- Faultline's views use Tailwind CSS utility classes. To use Bootstrap or another framework:
326
+ Three controllers are included:
227
327
 
228
- 1. Override the views by copying them to your app:
229
- ```bash
230
- cp -r $(bundle show faultline-rails)/app/views/faultline app/views/faultline
231
- ```
232
- 2. Rewrite the Tailwind classes with your framework's classes.
233
- 3. The built-in CSS in `faultline/application.css` provides fallback styles.
328
+ | Controller | Registered as | Purpose |
329
+ |---|---|---|
330
+ | `faultline_controller.js` | `faultline` | Keyboard nav, search, tabs, copy, backtrace expand |
331
+ | `faultline_sidebar_controller.js` | `faultline-sidebar` | Mobile drawer toggle |
332
+ | `faultline_theme_controller.js` | `faultline-theme` | Dark/light/auto theme toggle |
234
333
 
235
- ## Dashboard features
236
-
237
- - **Search** — Full-text search across exception messages, classes, and controllers.
238
- - **Sortable columns** — Sort by exception class, controller, action, or date.
239
- - **Filter** — Filter by exception class, controller/action, or age.
240
- - **Exception details** — Open full exception details in a Turbo Frame.
241
- - **Delete** — Delete individual exceptions without leaving the page.
242
- - **Bulk delete** — Delete the currently visible result set.
243
- - **Clear history** — Clear the complete exception history.
244
- - **RSS feed** — Subscribe to `/faultline/logged_exceptions/feed.rss`.
334
+ Run `bin/rails stimulus:manifest:update` after installing to auto-register them.
245
335
 
246
336
  ## Data storage
247
337
 
248
- The engine creates a `faultline_logged_exceptions` table containing the exception class, controller/action, message, backtrace, request, environment, user information, user agent, remote IP, and timestamps.
338
+ The engine creates a `faultline_logged_exceptions` table with columns for exception class, controller/action, message, backtrace, request, environment, user information, user agent, remote IP, and timestamps.
249
339
 
250
340
  Exception records can contain secrets. Apply your normal database encryption, retention, backup, and access-control policies.
251
341
 
342
+ ## Generators
343
+
344
+ | Generator | Description |
345
+ |---|---|
346
+ | `rails generate faultline:install` | Full setup (migration, initializer, routes) |
347
+ | `rails generate faultline:install --modern` | Install + generate all customizations |
348
+ | `rails generate faultline:customize` | Generate customizable views, layout, and JS |
349
+ | `rails generate faultline:customize --layout-only` | Only the layout |
350
+ | `rails generate faultline:customize --views-only` | Only the views |
351
+ | `rails generate faultline:customize --stimulus-only` | Only the Stimulus controller |
352
+
252
353
  ## Development
253
354
 
254
- Run the test suite with:
355
+ Run the test suite:
356
+
357
+ ```bash
358
+ cd /path/to/faultline-rails
359
+ RAILS_ENV=test bundle exec rails test
360
+ ```
361
+
362
+ Build the gem:
255
363
 
256
364
  ```bash
257
- bin/rails test
365
+ gem build faultline-rails.gemspec
258
366
  ```
259
367
 
260
368
  The repository includes a Rails dummy application under `test/dummy` for engine integration testing.
@@ -959,3 +959,11 @@ code, pre { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", mon
959
959
  .fl-text-sm { font-size: 0.875rem; }
960
960
  .fl-text-muted { color: var(--fl-text-muted); }
961
961
  .fl-font-semibold { font-weight: 600; }
962
+
963
+ /* ─── Layout (override host app resets) ─────────────────── */
964
+ html { height: 100%; }
965
+ body { height: 100vh; overflow: hidden; margin: 0; }
966
+ body > div.min-h-full { height: 100vh; min-height: 100vh; }
967
+ body > div.min-h-full > .fl-sidebar { height: 100vh; min-height: 100vh; }
968
+ body > div.min-h-full > div:last-child { display: flex; flex-direction: column; overflow: hidden; }
969
+ body > div.min-h-full > div:last-child > main { flex: 1; overflow-y: auto; }
@@ -186,12 +186,12 @@ module Faultline
186
186
 
187
187
  def prev_exception
188
188
  return nil unless @prev_exception_id
189
- OpenStruct.new(id: @prev_exception_id)
189
+ Struct.new(:id).new(@prev_exception_id)
190
190
  end
191
191
 
192
192
  def next_exception
193
193
  return nil unless @next_exception_id
194
- OpenStruct.new(id: @next_exception_id)
194
+ Struct.new(:id).new(@next_exception_id)
195
195
  end
196
196
 
197
197
  def load_exception_tabs
@@ -251,7 +251,7 @@ export default class extends Controller {
251
251
  toggleTheme() {
252
252
  const themeController = this.application.getControllerForElementAndIdentifier(
253
253
  document.documentElement,
254
- "faultline--theme"
254
+ "faultline-theme"
255
255
  )
256
256
  if (themeController) {
257
257
  themeController.toggle()
@@ -0,0 +1,98 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["tabs", "tabContent", "backtraceContent", "backtraceHidden"]
5
+
6
+ // ─── Tab Switching ────────────────────────────────────────
7
+
8
+ switchTab(event) {
9
+ const tabName = event.params.tab || event.currentTarget.getAttribute("data-fl-tab-param")
10
+ if (!tabName) return
11
+
12
+ // Update tab buttons
13
+ this.tabsTarget.querySelectorAll(".fl-tab").forEach(tab => {
14
+ const isActive = tab.getAttribute("data-fl-tab-param") === tabName
15
+ tab.classList.toggle("active", isActive)
16
+ })
17
+
18
+ // Show/hide tab content
19
+ this.tabContentTargets.forEach(content => {
20
+ const isTarget = content.getAttribute("data-fl-tab-content") === tabName
21
+ content.style.display = isTarget ? "" : "none"
22
+ })
23
+ }
24
+
25
+ // ─── Copy to Clipboard ───────────────────────────────────
26
+
27
+ copyId(event) {
28
+ const id = event.currentTarget.getAttribute("data-faultline-detail-id-value") ||
29
+ event.currentTarget.dataset.faultlineDetailIdValue
30
+ if (id) this.copyToClipboard(id, event.currentTarget)
31
+ }
32
+
33
+ copyMessage(event) {
34
+ const msg = event.currentTarget.getAttribute("data-faultline-detail-message-value") ||
35
+ event.currentTarget.dataset.faultlineDetailMessageValue
36
+ if (msg) this.copyToClipboard(msg, event.currentTarget)
37
+ }
38
+
39
+ copyBacktrace(event) {
40
+ const bt = event.currentTarget.getAttribute("data-faultline-detail-backtrace-value") ||
41
+ event.currentTarget.dataset.faultlineDetailBacktraceValue
42
+ if (bt) this.copyToClipboard(bt, event.currentTarget)
43
+ }
44
+
45
+ async copyToClipboard(text, button) {
46
+ try {
47
+ await navigator.clipboard.writeText(text)
48
+ } catch {
49
+ const textarea = document.createElement("textarea")
50
+ textarea.value = text
51
+ textarea.style.position = "fixed"
52
+ textarea.style.opacity = "0"
53
+ document.body.appendChild(textarea)
54
+ textarea.select()
55
+ document.execCommand("copy")
56
+ document.body.removeChild(textarea)
57
+ }
58
+ const orig = button.innerHTML
59
+ button.classList.add("copied")
60
+ button.innerHTML = `<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg> Copied!`
61
+ setTimeout(() => { button.classList.remove("copied"); button.innerHTML = orig }, 2000)
62
+ }
63
+
64
+ // ─── Backtrace Expand ────────────────────────────────────
65
+
66
+ expandBacktrace(event) {
67
+ event.preventDefault()
68
+ const hidden = this.backtraceHiddenTarget
69
+ if (hidden) {
70
+ hidden.style.display = hidden.style.display === "none" ? "" : "none"
71
+ event.currentTarget.textContent = hidden.style.display === "none"
72
+ ? "Show more frames"
73
+ : "Hide additional frames"
74
+ }
75
+ }
76
+
77
+ // ─── Close Details ───────────────────────────────────────
78
+
79
+ closeDetails() {
80
+ const frame = document.getElementById("exception-details")
81
+ if (frame) {
82
+ frame.innerHTML = `
83
+ <div class="text-center py-12">
84
+ <div class="w-10 h-10 rounded-full flex items-center justify-center mx-auto" style="background: var(--fl-bg-secondary)">
85
+ <svg class="w-5 h-5" style="color: var(--fl-text-muted)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
86
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
87
+ </svg>
88
+ </div>
89
+ <p class="mt-2 text-xs" style="color: var(--fl-text-muted)">Select an exception to view details</p>
90
+ <p class="mt-1 text-xs" style="color: var(--fl-text-muted)">Click a row or press <span class="fl-kbd">↓</span> <span class="fl-kbd">↑</span> to navigate</p>
91
+ </div>
92
+ `
93
+ }
94
+
95
+ // Deselect rows
96
+ document.querySelectorAll(".fl-table tbody tr.selected").forEach(r => r.classList.remove("selected"))
97
+ }
98
+ }