rails_event_viewer 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +24 -1
  4. data/Rakefile +2 -0
  5. data/app/assets/javascripts/rails_event_viewer/application.js +2 -1
  6. data/app/assets/stylesheets/rails_event_viewer/application.css +1 -1
  7. data/app/controllers/rails_event_viewer/analytics_controller.rb +5 -30
  8. data/app/controllers/rails_event_viewer/application_controller.rb +11 -5
  9. data/app/controllers/rails_event_viewer/dashboard_controller.rb +1 -1
  10. data/app/controllers/rails_event_viewer/event_types_controller.rb +3 -1
  11. data/app/controllers/rails_event_viewer/events_controller.rb +10 -23
  12. data/app/controllers/rails_event_viewer/groups_controller.rb +4 -11
  13. data/app/helpers/rails_event_viewer/application_helper.rb +2 -2
  14. data/app/views/layouts/rails_event_viewer/application.html.erb +8 -0
  15. data/app/views/rails_event_viewer/analytics/overview.html.erb +2 -0
  16. data/app/views/rails_event_viewer/dashboard/index.html.erb +4 -3
  17. data/app/views/rails_event_viewer/event_types/index.html.erb +1 -1
  18. data/app/views/rails_event_viewer/event_types/show.html.erb +5 -3
  19. data/app/views/rails_event_viewer/events/_event_row.html.erb +3 -3
  20. data/app/views/rails_event_viewer/events/_filters_sidebar.html.erb +2 -1
  21. data/app/views/rails_event_viewer/events/index.html.erb +4 -4
  22. data/app/views/rails_event_viewer/events/show.html.erb +7 -7
  23. data/app/views/rails_event_viewer/groups/index.html.erb +2 -2
  24. data/app/views/rails_event_viewer/groups/show.html.erb +1 -5
  25. data/app/views/rails_event_viewer/shared/_navigation.html.erb +7 -5
  26. data/config/routes.rb +6 -9
  27. data/db/migrate/{20241208000001_create_rails_event_viewer_entries.rb → 20261002121337_create_rails_event_viewer_entries.rb} +9 -4
  28. data/lib/generators/rails_event_viewer/install/templates/create_rails_event_viewer_entries.rb.erb +8 -3
  29. data/lib/generators/rails_event_viewer/install/templates/initializer.rb.erb +7 -9
  30. data/lib/rails_event_viewer/adapter.rb +2 -2
  31. data/lib/rails_event_viewer/adapters/active_record.rb +15 -36
  32. data/lib/rails_event_viewer/adapters/concerns/in_memory_filtering.rb +29 -3
  33. data/lib/rails_event_viewer/adapters/memory.rb +1 -21
  34. data/lib/rails_event_viewer/adapters/null.rb +2 -2
  35. data/lib/rails_event_viewer/adapters/redis.rb +4 -28
  36. data/lib/rails_event_viewer/batch_writer.rb +2 -1
  37. data/lib/rails_event_viewer/events_relation.rb +1 -1
  38. data/lib/rails_event_viewer/json_query.rb +52 -60
  39. data/lib/rails_event_viewer/subscriber.rb +1 -2
  40. data/lib/rails_event_viewer/time_utils.rb +3 -10
  41. data/lib/rails_event_viewer/version.rb +1 -1
  42. data/lib/tasks/rails_event_viewer_tasks.rake +1 -1
  43. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d1fae32fbd34187f3531ab552271d62005f6c8877bd814ca08207053d349178
4
- data.tar.gz: 05b561609b0ad4d7d7065e61e0a67891a8d9b4f2b970366869097a38a4d17293
3
+ metadata.gz: 876a5c4676cfb1f5512d82e73464c1e69a19bd9b1a1b5c2854f93aef7c412a60
4
+ data.tar.gz: 766e263c994dc9552472e065e1abaea18064cc08858c45ef524148923693922f
5
5
  SHA512:
6
- metadata.gz: 38ee32bc44c4fd20adf78b42e8d9110c1328615fe0072bfe00325d9c76fc7eba4ed3f909a98cabf0e6c345b4f8fb64cca6802255c5c7ab3721731bef3bfc91ee
7
- data.tar.gz: 0040bd1d2ce832bc80e35cac7248bc6461375f703f3ae299913264b43955d07e2b8a947bb0b9b349128f0aa45d352a5e5cc4b52401554943141e3fe5eec06962
6
+ metadata.gz: b3b9d72c97349b344106c874be9988e605c05efda5cafdb24e8230603830d313cb3a1b2fb72722a59ce6a27dab43e08751abebcd39606fd23ff2337c84392eac
7
+ data.tar.gz: 4e0cbc54525cdaa6b177832bd53e7207d1f3fc4c0a6167c3da250e85b00bea27bdbf1073e7a555b65887514bcc6e4e1ccdfe18b53c8a6fd1ffd49b74c82511a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-10-03)
4
+
5
+ Changed:
6
+
7
+ - The nav search goes to the events list and works together with the filters. The `/events/search` page is removed.
8
+ - The Analytics link is hidden when the adapter has no analytics.
9
+ - The group page no longer shows an Event Types count. It counted only the current page.
10
+
11
+ Fixed:
12
+
13
+ - Array, hash and invalid values in query params are ignored instead of raising.
14
+ - Event rows contain a real link, so keyboard and new-tab clicks work. Selecting text in a row no longer opens the event.
15
+ - `rails_event_viewer:clear` works with the ActiveRecord adapter.
16
+ - Redis keeps event times to the microsecond, and `delete_before` keeps an event at the exact cutoff.
17
+ - Payloads show `&`, `<` and `>` instead of unicode escapes.
18
+ - A banner explains a missing events table. A filtered empty list says no events match.
19
+ - Pages fit narrow screens, and long names and payloads wrap.
20
+ - Charts render when the host app sets its own Chartkick `content_for`.
21
+ - `with_name` accepts a symbol.
22
+
23
+ ## 0.3.0 (2026-10-02)
24
+
25
+ Breaking:
26
+
27
+ - Event type and group pages moved to `/event_type?name=...` and `/group?key=...&value=...`. Old links to those pages no longer work.
28
+ - Custom adapters: `counts_by_name` and `events_over_time` now take a `range:` argument.
29
+ - Unknown options for the Redis and memory adapters raise an `ArgumentError`.
30
+ - `with_tag` and `with_context` with a value of `""` or `false` now match that value, not every event that has the key.
31
+
32
+ Fixed:
33
+
34
+ - PostgreSQL: group pages no longer raise.
35
+ - MySQL: the migration runs and search works.
36
+ - Event names and group values with a slash no longer break the Event Types and Groups pages.
37
+ - Tag and context filters return the same events on every database and adapter. A value typed as text also matches numbers and booleans.
38
+ - Filter keys with a hyphen, dot, space or quote work on every database.
39
+ - Events the database rejects no longer hold up the events behind them.
40
+ - Payloads that cannot be serialized, such as invalid UTF-8, are stored as text.
41
+ - A dropped connection during the first table check no longer stops capture until restart.
42
+ - The analytics page respects its date range.
43
+ - The generated Redis config suggests `redis_options`, which the adapter reads.
44
+ - The generated initializer adds to `ignored_events` and keeps the defaults.
45
+
46
+ Changed:
47
+
48
+ - Removed two unused analytics endpoints.
49
+ - The engine migration has a new timestamp and targets Rails 8.1. Apps that already installed it need no action.
50
+
3
51
  ## 0.2.1 (2026-10-01)
4
52
 
5
53
  - Forked workers no longer duplicate the parent's buffered events on exit.
data/README.md CHANGED
@@ -43,6 +43,7 @@ RailsEventViewer.events
43
43
  ```
44
44
 
45
45
  Other filters: `with_context`, `search`, `until`, `offset`, and `count`.
46
+ Without `limit`, a query returns `config.per_page` events, 25 by default.
46
47
 
47
48
  ## Configuration
48
49
 
@@ -90,10 +91,23 @@ config.authentication = ->(controller) {
90
91
 
91
92
  Set `RAILS_ENV` on every server. Without it, Rails falls back to development and the dashboard is open.
92
93
 
94
+ If both are set, HTTP Basic Auth is used and `config.authentication` is ignored.
95
+
93
96
  ## Storage adapters
94
97
 
95
98
  Set `config.storage_adapter` to `:active_record` (default), `:redis`, `:memory`, or `:null`. Redis needs the `redis` gem and takes options through `config.adapter_options`.
96
99
 
100
+ ```ruby
101
+ config.storage_adapter = :redis
102
+ config.adapter_options = {
103
+ redis_options: { url: ENV["REDIS_URL"] }, # passed to Redis.new
104
+ key_prefix: "rails_event_viewer",
105
+ max_events: 10_000
106
+ }
107
+ ```
108
+
109
+ Pass `pool:` to use your own `ConnectionPool`, or `pool_size:` and `pool_timeout:` to size the built-in one. The memory adapter takes `max_events:`. An unknown option raises an `ArgumentError`.
110
+
97
111
  Filtered queries on Redis scan every stored event, so keep `max_events` (how many events Redis keeps, set in `adapter_options`) modest. For your own storage, include `RailsEventViewer::Adapter`.
98
112
 
99
113
  ## Rake tasks
@@ -119,7 +133,16 @@ production:
119
133
  ```bash
120
134
  bundle install
121
135
  bundle exec rake test
122
- cd test/dummy && bin/rails db:migrate && bin/rails server
136
+ bin/rails db:migrate && bin/rails server
137
+ ```
138
+
139
+ `rake test` prepares the test database first. Run it once before running a single test file.
140
+
141
+ To test against PostgreSQL or MySQL, set `DATABASE_URL`:
142
+
143
+ ```bash
144
+ DATABASE_URL=postgresql://localhost/rails_event_viewer_test bundle exec rake test
145
+ DATABASE_URL=trilogy://root@127.0.0.1/rails_event_viewer_test bundle exec rake test
123
146
  ```
124
147
 
125
148
  ## License
data/Rakefile CHANGED
@@ -13,4 +13,6 @@ Rake::TestTask.new(:test) do |t|
13
13
  t.verbose = false
14
14
  end
15
15
 
16
+ task test: "app:db:prepare"
17
+
16
18
  task default: :test
@@ -22,7 +22,8 @@ document.addEventListener("click", (event) => {
22
22
  }
23
23
 
24
24
  const row = event.target.closest("[data-event-viewer-href]")
25
- if (row && !event.target.closest("a, button")) {
25
+ const selectingText = window.getSelection().toString() !== ""
26
+ if (row && !selectingText && !event.target.closest("a, button")) {
26
27
  window.location = row.dataset.eventViewerHref
27
28
  }
28
29
  })
@@ -1 +1 @@
1
- *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-10{left:2.5rem}.left-12{left:3rem}.top-0{top:0}.top-4{top:1rem}.top-6{top:1.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-72{height:18rem}.h-full{height:100%}.min-h-full{min-height:100%}.w-0{width:0}.w-0\.5{width:.125rem}.w-1\/5{width:20%}.w-12{width:3rem}.w-2\/5{width:40%}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-72{width:18rem}.w-9{width:2.25rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-0{min-width:0}.min-w-full{min-width:100%}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity,1))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-indigo-600{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity,1))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-10{padding-left:2.5rem}.pl-16{padding-left:4rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-6{padding-right:1.5rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-indigo-800{--tw-text-opacity:1;color:rgb(55 48 163/var(--tw-text-opacity,1))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.placeholder-indigo-200::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(199 210 254/var(--tw-placeholder-opacity,1))}.placeholder-indigo-200::placeholder{--tw-placeholder-opacity:1;color:rgb(199 210 254/var(--tw-placeholder-opacity,1))}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.json-tree{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.875rem}.json-tree-key{color:#4b5563;font-weight:500}.json-tree-string{color:#059669}.json-tree-number{color:#2563eb}.json-tree-boolean{color:#7c3aed}.json-tree-null{color:#9ca3af}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:text-indigo-500:hover{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.hover\:text-indigo-900:hover{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.focus\:text-gray-900:focus{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.focus\:placeholder-gray-400:focus::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity,1))}.focus\:placeholder-gray-400:focus::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus\:ring-white:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity,1))}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.left-0{left:0}.left-10{left:2.5rem}.left-12{left:3rem}.top-0{top:0}.top-4{top:1rem}.top-6{top:1.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-12{height:3rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-72{height:18rem}.h-full{height:100%}.min-h-16{min-height:4rem}.min-h-full{min-height:100%}.w-0{width:0}.w-0\.5{width:.125rem}.w-1\/5{width:20%}.w-12{width:3rem}.w-2\/5{width:40%}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-9{width:2.25rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.min-w-0{min-width:0}.min-w-full{min-width:100%}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-y-2{row-gap:.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity,1))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-indigo-600{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity,1))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-10{padding-left:2.5rem}.pl-16{padding-left:4rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.pr-6{padding-right:1.5rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tracking-wider{letter-spacing:.05em}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-indigo-800{--tw-text-opacity:1;color:rgb(55 48 163/var(--tw-text-opacity,1))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.placeholder-indigo-200::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(199 210 254/var(--tw-placeholder-opacity,1))}.placeholder-indigo-200::placeholder{--tw-placeholder-opacity:1;color:rgb(199 210 254/var(--tw-placeholder-opacity,1))}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.json-tree{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.875rem}.json-tree-key{color:#4b5563;font-weight:500}.json-tree-string{color:#059669}.json-tree-number{color:#2563eb}.json-tree-boolean{color:#7c3aed}.json-tree-null{color:#9ca3af}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:text-indigo-500:hover{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity,1))}.hover\:text-indigo-900:hover{--tw-text-opacity:1;color:rgb(49 46 129/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.focus\:text-gray-900:focus{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.focus\:placeholder-gray-400:focus::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity,1))}.focus\:placeholder-gray-400:focus::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus\:ring-white:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity,1))}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:w-72{width:18rem}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
@@ -4,42 +4,17 @@ module RailsEventViewer
4
4
 
5
5
  def overview
6
6
  @date_range = parse_date_range
7
+ counts = current_adapter.counts_by_name(range: @date_range)
7
8
 
8
- relation = RailsEventViewer.events
9
- .since(@date_range.begin)
10
- .until(@date_range.end)
11
-
12
- @total_events = relation.count
13
- @unique_event_types = current_adapter.distinct_event_names.size
14
- @events_by_type = current_adapter.counts_by_name(limit: 20)
15
- @events_over_time = current_adapter.events_over_time(since: @date_range.begin, interval: period_for_range)
9
+ @total_events = counts.values.sum
10
+ @unique_event_types = counts.size
11
+ @events_by_type = counts.first(20).to_h
12
+ @events_over_time = current_adapter.events_over_time(range: @date_range, interval: period_for_range)
16
13
 
17
14
  @avg_events_per_hour = calculate_avg_events_per_hour
18
15
  @peak_period = find_peak_period
19
16
  end
20
17
 
21
- def events_over_time
22
- since = params[:since]&.to_i&.seconds&.ago || 24.hours.ago
23
- interval = params[:interval]&.to_sym || :hour
24
-
25
- @events_data = current_adapter.events_over_time(since: since, interval: interval)
26
-
27
- respond_to do |format|
28
- format.json { render json: @events_data }
29
- format.html { render partial: "events_over_time_chart" }
30
- end
31
- end
32
-
33
- def events_by_type
34
- limit = (params[:limit] || 20).to_i.clamp(1, 100)
35
- @data = current_adapter.counts_by_name(limit: limit)
36
-
37
- respond_to do |format|
38
- format.json { render json: @data }
39
- format.html { render partial: "events_by_type_chart" }
40
- end
41
- end
42
-
43
18
  private
44
19
 
45
20
  def check_analytics_support
@@ -42,11 +42,17 @@ module RailsEventViewer
42
42
  Time.use_zone(zone, &block)
43
43
  end
44
44
 
45
+ def string_param(key)
46
+ value = params[key]
47
+ value if value.is_a?(String) && !value.empty?
48
+ end
49
+
45
50
  def safe_parse_date(date_string, default: nil)
46
- return default if date_string.blank?
51
+ return default unless date_string.is_a?(String) && date_string.present?
47
52
 
48
- Date.parse(date_string)
49
- rescue Date::Error, TypeError
53
+ date = Date.parse(date_string)
54
+ date.year.between?(1000, 9999) ? date : default
55
+ rescue ArgumentError
50
56
  default
51
57
  end
52
58
 
@@ -67,11 +73,11 @@ module RailsEventViewer
67
73
  end
68
74
 
69
75
  def per_page
70
- (params[:per_page] || RailsEventViewer.per_page).to_i.clamp(1, 100)
76
+ (string_param(:per_page) || RailsEventViewer.per_page).to_i.clamp(1, 100)
71
77
  end
72
78
 
73
79
  def paginate(relation)
74
- pagination = Page.new(count: relation.count, per_page: per_page, page: params[:page])
80
+ pagination = Page.new(count: relation.count, per_page: per_page, page: string_param(:page))
75
81
  [pagination, relation.offset(pagination.offset).limit(pagination.per_page).to_a]
76
82
  end
77
83
  end
@@ -8,7 +8,7 @@ module RailsEventViewer
8
8
 
9
9
  if analytics_supported?
10
10
  @events_by_type = current_adapter.counts_by_name(limit: 10)
11
- @events_over_time = current_adapter.events_over_time(since: 24.hours.ago, interval: :hour)
11
+ @events_over_time = current_adapter.events_over_time(range: 24.hours.ago.., interval: :hour)
12
12
  else
13
13
  @events_by_type = {}
14
14
  @events_over_time = {}
@@ -5,7 +5,9 @@ module RailsEventViewer
5
5
  end
6
6
 
7
7
  def show
8
- @event_type_name = params[:name]
8
+ @event_type_name = string_param(:name)
9
+ return redirect_to event_types_path unless @event_type_name
10
+
9
11
  total_events = RailsEventViewer.events.with_name(@event_type_name)
10
12
 
11
13
  @pagination, @events = paginate(total_events)
@@ -2,6 +2,7 @@ module RailsEventViewer
2
2
  class EventsController < ApplicationController
3
3
  def index
4
4
  relation = apply_filters(RailsEventViewer.events)
5
+ @filtered = relation.filtered?
5
6
  @pagination, @events = paginate(relation)
6
7
  @event_names = current_adapter.distinct_event_names
7
8
  end
@@ -17,37 +18,23 @@ module RailsEventViewer
17
18
  @related_events = find_related_events(@event)
18
19
  end
19
20
 
20
- def search
21
- @query = params[:q]
22
- relation = RailsEventViewer.events.search(@query)
23
- @pagination, @events = paginate(relation)
24
- @event_names = current_adapter.distinct_event_names
25
-
26
- render :index
27
- end
28
-
29
21
  private
30
22
 
31
23
  def apply_filters(relation)
32
- relation = relation.with_name(params[:name]) if params[:name].present?
24
+ name = string_param(:name)
25
+ tag_key = string_param(:tag_key)
26
+ context_key = string_param(:context_key)
27
+ @query = string_param(:q)
33
28
 
34
29
  @start_date = safe_parse_date(params[:start_date])
35
30
  @end_date = safe_parse_date(params[:end_date])
31
+
32
+ relation = relation.with_name(name) if name
36
33
  relation = relation.since(@start_date.beginning_of_day) if @start_date
37
34
  relation = relation.until(@end_date.end_of_day) if @end_date
38
-
39
- if params[:tag_key].present?
40
- relation = relation.with_tag(params[:tag_key], params[:tag_value].presence)
41
- end
42
-
43
- if params[:context_key].present?
44
- relation = relation.with_context(params[:context_key], params[:context_value].presence)
45
- end
46
-
47
- if params[:q].present?
48
- relation = relation.search(params[:q])
49
- end
50
-
35
+ relation = relation.with_tag(tag_key, string_param(:tag_value)) if tag_key
36
+ relation = relation.with_context(context_key, string_param(:context_value)) if context_key
37
+ relation = relation.search(@query) if @query
51
38
  relation
52
39
  end
53
40
 
@@ -17,8 +17,10 @@ module RailsEventViewer
17
17
  end
18
18
 
19
19
  def show
20
- @key = params[:key]
21
- @value = params[:id]
20
+ @key = string_param(:key)
21
+ @value = string_param(:value)
22
+ return redirect_to groups_path unless @key && @value
23
+
22
24
  @source = source_param
23
25
 
24
26
  events = if @source == :tags
@@ -29,7 +31,6 @@ module RailsEventViewer
29
31
 
30
32
  @pagination, @events = paginate(events)
31
33
  @total_count = @pagination.count
32
- @event_names = @events.map { |e| event_name(e) }.uniq
33
34
 
34
35
  @first_event_at, @last_event_at = current_adapter.event_time_span(events)
35
36
  @duration = @last_event_at - @first_event_at if @first_event_at && @last_event_at
@@ -40,13 +41,5 @@ module RailsEventViewer
40
41
  def source_param
41
42
  params[:source].presence_in(%w[context tags])&.to_sym || :context
42
43
  end
43
-
44
- def event_name(event)
45
- event.respond_to?(:name) ? event.name : event[:name]
46
- end
47
-
48
- def event_occurred_at(event)
49
- event.respond_to?(:occurred_at) ? event.occurred_at : event[:occurred_at]
50
- end
51
44
  end
52
45
  end
@@ -25,13 +25,13 @@ module RailsEventViewer
25
25
  end
26
26
 
27
27
  def truncate_json(json, length: 100)
28
- text = json.is_a?(Hash) ? json.to_json : json.to_s
28
+ text = json.is_a?(Hash) ? JSON.generate(json) : json.to_s
29
29
  truncate(text, length: length)
30
30
  end
31
31
 
32
32
  def json_tree(data, depth: 0, max_depth: nil)
33
33
  return content_tag(:span, "null", class: "text-gray-400") if data.nil?
34
- return content_tag(:span, data.to_json, class: json_value_class(data)) unless data.is_a?(Hash) || data.is_a?(Array)
34
+ return content_tag(:span, JSON.generate(data), class: json_value_class(data)) unless data.is_a?(Hash) || data.is_a?(Array)
35
35
  return content_tag(:span, collapsed_summary(data), class: "text-gray-400") if max_depth && depth >= max_depth
36
36
 
37
37
  if data.is_a?(Hash)
@@ -33,6 +33,14 @@
33
33
  </div>
34
34
  <% end %>
35
35
 
36
+ <% if RailsEventViewer.storage_adapter == :active_record && !persistence_supported? %>
37
+ <div class="mb-4 rounded-md bg-red-50 p-4" role="alert">
38
+ <p class="text-sm font-medium text-red-800">
39
+ The events table is missing, so no events are being saved. Run <code>bin/rails generate rails_event_viewer:install</code> and <code>bin/rails db:migrate</code>, then restart the app.
40
+ </p>
41
+ </div>
42
+ <% end %>
43
+
36
44
  <% if RailsEventViewer.sample_rate.to_f < 1.0 %>
37
45
  <div class="mb-4 rounded-md bg-yellow-50 border border-yellow-200 p-4">
38
46
  <p class="text-sm font-medium text-yellow-800">
@@ -47,6 +47,7 @@
47
47
  <%= line_chart @events_over_time,
48
48
  id: "analytics-timeline",
49
49
  html: '<div id="%{id}" class="h-full w-full">%{loading}</div>',
50
+ content_for: nil,
50
51
  colors: ["#4F46E5"],
51
52
  library: {
52
53
  plugins: { legend: { display: false } },
@@ -67,6 +68,7 @@
67
68
  <%= bar_chart @events_by_type,
68
69
  id: "analytics-by-type",
69
70
  html: '<div id="%{id}" class="h-full w-full">%{loading}</div>',
71
+ content_for: nil,
70
72
  colors: ["#4F46E5"],
71
73
  library: {
72
74
  indexAxis: 'y',
@@ -34,6 +34,7 @@
34
34
  <%= line_chart @events_over_time,
35
35
  id: "events-timeline",
36
36
  html: '<div id="%{id}" class="h-full w-full">%{loading}</div>',
37
+ content_for: nil,
37
38
  colors: ["#4F46E5"],
38
39
  library: { plugins: { legend: { display: false } } } %>
39
40
  <% else %>
@@ -52,6 +53,7 @@
52
53
  <%= pie_chart @events_by_type,
53
54
  id: "events-by-type",
54
55
  html: '<div id="%{id}" class="h-full w-full">%{loading}</div>',
56
+ content_for: nil,
55
57
  donut: true,
56
58
  colors: ["#4F46E5", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"] %>
57
59
  <% else %>
@@ -85,9 +87,8 @@
85
87
  <% @recent_events.each do |event| %>
86
88
  <tr class="hover:bg-gray-50 cursor-pointer" data-event-viewer-href="<%= rails_event_viewer.event_path(event_id(event)) %>">
87
89
  <td class="px-6 py-4 whitespace-nowrap">
88
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
89
- <%= event_name(event) %>
90
- </span>
90
+ <%= link_to event_name(event), rails_event_viewer.event_path(event_id(event)),
91
+ class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800" %>
91
92
  </td>
92
93
  <td class="px-6 py-4 text-sm text-gray-500">
93
94
  <code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= truncate_json(event_payload(event), length: 60) %></code>
@@ -9,7 +9,7 @@
9
9
  </div>
10
10
 
11
11
  <% if @event_types.any? %>
12
- <div class="bg-white shadow rounded-lg overflow-hidden">
12
+ <div class="bg-white shadow rounded-lg overflow-x-auto">
13
13
  <table class="min-w-full divide-y divide-gray-200">
14
14
  <thead class="bg-gray-50">
15
15
  <tr>
@@ -9,9 +9,9 @@
9
9
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
10
10
  </svg>
11
11
  <% end %>
12
- <div>
12
+ <div class="min-w-0">
13
13
  <h1 class="text-2xl font-bold text-gray-900">
14
- <span class="inline-flex items-center px-3 py-1 rounded-full text-xl font-medium bg-indigo-100 text-indigo-800">
14
+ <span class="inline-block max-w-full break-words px-3 py-1 rounded-full text-xl font-medium bg-indigo-100 text-indigo-800">
15
15
  <%= @event_type_name %>
16
16
  </span>
17
17
  </h1>
@@ -56,7 +56,9 @@
56
56
  <% @events.each do |event| %>
57
57
  <tr class="hover:bg-gray-50 cursor-pointer" data-event-viewer-href="<%= rails_event_viewer.event_path(event_id(event)) %>">
58
58
  <td class="px-6 py-4">
59
- <code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= truncate_json(event_payload(event), length: 80) %></code>
59
+ <%= link_to rails_event_viewer.event_path(event_id(event)) do %>
60
+ <code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= truncate_json(event_payload(event), length: 80) %></code>
61
+ <% end %>
60
62
  </td>
61
63
  <td class="px-6 py-4 whitespace-nowrap">
62
64
  <% tags = event_tags(event) %>
@@ -1,9 +1,9 @@
1
1
  <tr class="hover:bg-gray-50 cursor-pointer" data-event-viewer-href="<%= rails_event_viewer.event_path(event_id(event)) %>">
2
2
  <td class="px-6 py-4">
3
3
  <div class="flex flex-col max-w-md">
4
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 w-fit">
5
- <%= event_name(event) %>
6
- </span>
4
+ <%= link_to event_name(event), rails_event_viewer.event_path(event_id(event)),
5
+ title: event_name(event),
6
+ class: "inline-block max-w-full truncate px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 w-fit" %>
7
7
  <div class="text-sm text-gray-500 mt-1 truncate">
8
8
  <code class="text-xs"><%= truncate_json(event_payload(event), length: 50) %></code>
9
9
  </div>
@@ -1,7 +1,8 @@
1
1
  <div class="bg-white rounded-lg shadow p-5 space-y-5">
2
2
  <h3 class="font-medium text-gray-900">Filters</h3>
3
3
 
4
- <%= form_with url: rails_event_viewer.events_path, method: :get, data: { turbo_frame: "events_list" }, class: "space-y-5" do |f| %>
4
+ <%= form_with url: rails_event_viewer.events_path, method: :get, class: "space-y-5" do |f| %>
5
+ <%= f.hidden_field :q, value: @query if @query %>
5
6
 
6
7
  <!-- Event name filter -->
7
8
  <div>
@@ -11,13 +11,13 @@
11
11
  <% end %>
12
12
  </h1>
13
13
  <div class="text-sm text-gray-500">
14
- <%= @pagination.count %> total events
14
+ <%= @pagination.count %> events
15
15
  </div>
16
16
  </div>
17
17
 
18
- <div class="flex gap-6">
18
+ <div class="flex flex-col lg:flex-row gap-6">
19
19
  <!-- Filters sidebar -->
20
- <aside class="w-72 flex-shrink-0">
20
+ <aside class="w-full lg:w-72 flex-shrink-0">
21
21
  <%= render "rails_event_viewer/events/filters_sidebar", event_names: @event_names %>
22
22
  </aside>
23
23
 
@@ -51,7 +51,7 @@
51
51
  </svg>
52
52
  <h3 class="mt-2 text-sm font-medium text-gray-900">No events found</h3>
53
53
  <p class="mt-1 text-sm text-gray-500">
54
- <% if @query.present? || params[:name].present? %>
54
+ <% if @filtered %>
55
55
  Try adjusting your filters or search query.
56
56
  <% else %>
57
57
  Events will appear here once they are captured.
@@ -8,21 +8,21 @@
8
8
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
9
9
  </svg>
10
10
  <% end %>
11
- <div>
12
- <h1 class="text-2xl font-bold text-gray-900"><%= event_name(@event) %></h1>
11
+ <div class="min-w-0">
12
+ <h1 class="text-2xl font-bold text-gray-900 break-words"><%= event_name(@event) %></h1>
13
13
  <p class="text-sm text-gray-500">Event ID: <%= event_id(@event) %></p>
14
14
  </div>
15
15
  </div>
16
16
 
17
17
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
18
18
  <!-- Main content -->
19
- <div class="lg:col-span-2 space-y-6">
19
+ <div class="lg:col-span-2 space-y-6 min-w-0">
20
20
  <!-- Payload -->
21
21
  <div class="bg-white rounded-lg shadow">
22
22
  <div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
23
23
  <h3 class="text-lg font-medium text-gray-900">Payload</h3>
24
24
  <button type="button"
25
- data-event-viewer-copy="<%= event_payload(@event).to_json %>"
25
+ data-event-viewer-copy="<%= JSON.generate(event_payload(@event)) %>"
26
26
  class="text-indigo-600 hover:text-indigo-500 text-sm font-medium">
27
27
  Copy
28
28
  </button>
@@ -30,7 +30,7 @@
30
30
  <div class="p-6">
31
31
  <% payload = event_payload(@event) %>
32
32
  <% if payload.present? %>
33
- <div class="font-mono text-sm">
33
+ <div class="font-mono text-sm break-words">
34
34
  <%= json_tree(payload) %>
35
35
  </div>
36
36
  <% else %>
@@ -47,7 +47,7 @@
47
47
  <h3 class="text-lg font-medium text-gray-900">Tags</h3>
48
48
  </div>
49
49
  <div class="p-6">
50
- <div class="font-mono text-sm">
50
+ <div class="font-mono text-sm break-words">
51
51
  <%= json_tree(tags) %>
52
52
  </div>
53
53
  </div>
@@ -62,7 +62,7 @@
62
62
  <h3 class="text-lg font-medium text-gray-900">Context</h3>
63
63
  </div>
64
64
  <div class="p-6">
65
- <div class="font-mono text-sm">
65
+ <div class="font-mono text-sm break-words">
66
66
  <%= json_tree(context) %>
67
67
  </div>
68
68
  </div>