rails_event_viewer 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +8 -0
  4. data/app/assets/javascripts/rails_event_viewer/application.js +2 -1
  5. data/app/assets/stylesheets/rails_event_viewer/application.css +1 -1
  6. data/app/controllers/rails_event_viewer/application_controller.rb +7 -16
  7. data/app/controllers/rails_event_viewer/events_controller.rb +16 -29
  8. data/app/controllers/rails_event_viewer/groups_controller.rb +0 -9
  9. data/app/helpers/rails_event_viewer/application_helper.rb +23 -77
  10. data/app/models/rails_event_viewer/entry.rb +0 -26
  11. data/app/models/rails_event_viewer/page.rb +9 -0
  12. data/app/views/layouts/rails_event_viewer/application.html.erb +11 -4
  13. data/app/views/rails_event_viewer/analytics/overview.html.erb +13 -9
  14. data/app/views/rails_event_viewer/dashboard/index.html.erb +16 -15
  15. data/app/views/rails_event_viewer/event_types/index.html.erb +7 -7
  16. data/app/views/rails_event_viewer/event_types/show.html.erb +18 -16
  17. data/app/views/rails_event_viewer/events/_event_row.html.erb +9 -9
  18. data/app/views/rails_event_viewer/events/_filters_sidebar.html.erb +18 -15
  19. data/app/views/rails_event_viewer/events/index.html.erb +10 -10
  20. data/app/views/rails_event_viewer/events/show.html.erb +28 -28
  21. data/app/views/rails_event_viewer/groups/index.html.erb +8 -8
  22. data/app/views/rails_event_viewer/groups/show.html.erb +12 -16
  23. data/app/views/rails_event_viewer/shared/_navigation.html.erb +13 -17
  24. data/app/views/rails_event_viewer/shared/_pagination.html.erb +10 -10
  25. data/app/views/rails_event_viewer/shared/_stat_card.html.erb +5 -5
  26. data/config/routes.rb +1 -5
  27. data/db/migrate/20261002121337_create_rails_event_viewer_entries.rb +0 -1
  28. data/lib/generators/rails_event_viewer/install/templates/create_rails_event_viewer_entries.rb.erb +0 -1
  29. data/lib/rails_event_viewer/adapter.rb +0 -8
  30. data/lib/rails_event_viewer/adapters/active_record.rb +12 -44
  31. data/lib/rails_event_viewer/adapters/concerns/in_memory_filtering.rb +7 -7
  32. data/lib/rails_event_viewer/adapters/memory.rb +0 -4
  33. data/lib/rails_event_viewer/adapters/null.rb +0 -8
  34. data/lib/rails_event_viewer/adapters/redis.rb +6 -20
  35. data/lib/rails_event_viewer/events_relation.rb +1 -1
  36. data/lib/rails_event_viewer/flusher.rb +5 -5
  37. data/lib/rails_event_viewer/logging.rb +1 -11
  38. data/lib/rails_event_viewer/version.rb +1 -1
  39. data/lib/rails_event_viewer.rb +5 -5
  40. data/lib/tasks/rails_event_viewer_tasks.rake +1 -1
  41. metadata +7 -2
  42. data/app/assets/stylesheets/rails_event_viewer/tailwind.css +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9535859f9ef02bb389bd7b17f4d9c34329c199c4af3cf6e97effeaa5ad27b52
4
- data.tar.gz: 404478f207dae7bcfc10152b3832bb6a9200d4ec1e36c9a1a36ce1cd2ac4ff0c
3
+ metadata.gz: 935a008c1e9519c485ee47d472d8980a36e1e6e4778e83c76e38629350ed93e9
4
+ data.tar.gz: 9244a13f193475b62847af18366b4e53246a9602f84c13cd00114ec6ead86c72
5
5
  SHA512:
6
- metadata.gz: 15c2c7235db0b5e5b4694c5ddffd7f71c543560670011db7984d19e68dd4ba7848fa71934a6755cac6055f2e29414b4f9d3318ee4e0d3b7382c37ba23303cd0f
7
- data.tar.gz: a32771c3b8d554d69445b037aa8e8b64a1fb2dd2d7e84169a665c61bd83b632cac2f863ec3d5c65c195f772fd49c0af7af56cdfa0af08712d4381478bb7c614e
6
+ metadata.gz: af5581280410b7b5eb3a79ce61b3dc53ca8dac9ddb083d79554c61f950977319049714334c6fb19713d6aa8c67b38bb63a2bb1dd9042a08a71da7def2bd6b5e1
7
+ data.tar.gz: bc898a58889c8786fcc83c6467084275637c5e27ddc8ee33a233c571d917724b65445f235eebcf3e99be1e2aea70c4eb6a301d764aa5a3bb2ac53ce40be16750
data/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (2026-10-03)
4
+
5
+ Breaking:
6
+
7
+ - Removed `Adapter#distinct_group_values` and `Adapter#supports_retention?`. Nothing called them.
8
+ - Removed `Entry#source_location`, `#short_filepath`, `#tagged?`, `#has_context?` and `#tag`.
9
+ - The ActiveRecord adapter raises on unknown `adapter_options`, like the other adapters.
10
+ - Times show in the host app's time zone. The engine no longer reads a `timezone` cookie.
11
+
12
+ Changed:
13
+
14
+ - New installs skip the index on `name` alone. The `(name, occurred_at)` index covers it. Existing apps can drop it with `remove_index :rails_event_viewer_entries, :name`.
15
+ - Chartkick is pinned below 6, to match the bundled Chartkick.js.
16
+ - Dashboard scripts load with `defer`.
17
+
18
+ Fixed:
19
+
20
+ - Events with the same timestamp keep a stable order across pages.
21
+ - Pagination renders only the page links it shows, not one loop per page.
22
+ - Related events look one hour either side of the event, so the lookup no longer scans the whole table.
23
+ - Form fields have labels, tables have column headers, and the current page is marked for screen readers. Grey text has more contrast.
24
+
25
+ ## 0.3.1 (2026-10-03)
26
+
27
+ Changed:
28
+
29
+ - The nav search goes to the events list and works together with the filters. The `/events/search` page is removed.
30
+ - The Analytics link is hidden when the adapter has no analytics.
31
+ - The group page no longer shows an Event Types count. It counted only the current page.
32
+
33
+ Fixed:
34
+
35
+ - Array, hash and invalid values in query params are ignored instead of raising.
36
+ - Event rows contain a real link, so keyboard and new-tab clicks work. Selecting text in a row no longer opens the event.
37
+ - `rails_event_viewer:clear` works with the ActiveRecord adapter.
38
+ - Redis keeps event times to the microsecond, and `delete_before` keeps an event at the exact cutoff.
39
+ - Payloads show `&`, `<` and `>` instead of unicode escapes.
40
+ - A banner explains a missing events table. A filtered empty list says no events match.
41
+ - Pages fit narrow screens, and long names and payloads wrap.
42
+ - Charts render when the host app sets its own Chartkick `content_for`.
43
+ - `with_name` accepts a symbol.
44
+
3
45
  ## 0.3.0 (2026-10-02)
4
46
 
5
47
  Breaking:
data/README.md CHANGED
@@ -8,6 +8,9 @@ A Rails engine that captures events emitted with `Rails.event` and gives you a d
8
8
 
9
9
  - Rails 8.1+
10
10
  - Ruby 3.3+
11
+ - SQLite, PostgreSQL, or MySQL 8 for the default ActiveRecord adapter
12
+
13
+ API-only apps need an asset pipeline to serve the dashboard's CSS and JavaScript. Add `gem "propshaft"`, and run `assets:precompile` when you deploy.
11
14
 
12
15
  ## Installation
13
16
 
@@ -43,6 +46,7 @@ RailsEventViewer.events
43
46
  ```
44
47
 
45
48
  Other filters: `with_context`, `search`, `until`, `offset`, and `count`.
49
+ Without `limit`, a query returns `config.per_page` events, 25 by default.
46
50
 
47
51
  ## Configuration
48
52
 
@@ -90,6 +94,8 @@ config.authentication = ->(controller) {
90
94
 
91
95
  Set `RAILS_ENV` on every server. Without it, Rails falls back to development and the dashboard is open.
92
96
 
97
+ If both are set, HTTP Basic Auth is used and `config.authentication` is ignored.
98
+
93
99
  ## Storage adapters
94
100
 
95
101
  Set `config.storage_adapter` to `:active_record` (default), `:redis`, `:memory`, or `:null`. Redis needs the `redis` gem and takes options through `config.adapter_options`.
@@ -127,6 +133,8 @@ production:
127
133
 
128
134
  ## Development
129
135
 
136
+ The dashboard ships Chart.js 4.4.1, chartjs-adapter-date-fns 3.0.0 and Chartkick.js 5.0.1.
137
+
130
138
  ```bash
131
139
  bundle install
132
140
  bundle exec rake 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}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.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)}.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-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/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}}
@@ -3,9 +3,8 @@ module RailsEventViewer
3
3
  layout "rails_event_viewer/application"
4
4
 
5
5
  before_action :authenticate!
6
- around_action :use_viewer_time_zone
7
6
 
8
- helper_method :current_timezone, :current_adapter, :analytics_supported?, :persistence_supported?
7
+ helper_method :current_adapter, :analytics_supported?, :persistence_supported?
9
8
 
10
9
  private
11
10
 
@@ -37,28 +36,20 @@ module RailsEventViewer
37
36
  render plain: "Unauthorized", status: :unauthorized unless auth.call(self) || performed?
38
37
  end
39
38
 
40
- def use_viewer_time_zone(&block)
41
- zone = ActiveSupport::TimeZone[cookies[:timezone].to_s] || ActiveSupport::TimeZone["UTC"]
42
- Time.use_zone(zone, &block)
43
- end
44
-
45
39
  def string_param(key)
46
40
  value = params[key]
47
41
  value if value.is_a?(String) && !value.empty?
48
42
  end
49
43
 
50
44
  def safe_parse_date(date_string, default: nil)
51
- return default if date_string.blank?
45
+ return default unless date_string.is_a?(String) && date_string.present?
52
46
 
53
- Date.parse(date_string)
54
- rescue Date::Error, TypeError
47
+ date = Date.parse(date_string)
48
+ date.year.between?(1000, 9999) ? date : default
49
+ rescue ArgumentError
55
50
  default
56
51
  end
57
52
 
58
- def current_timezone
59
- Time.zone.name
60
- end
61
-
62
53
  def current_adapter
63
54
  RailsEventViewer.adapter
64
55
  end
@@ -72,11 +63,11 @@ module RailsEventViewer
72
63
  end
73
64
 
74
65
  def per_page
75
- (params[:per_page] || RailsEventViewer.per_page).to_i.clamp(1, 100)
66
+ (string_param(:per_page) || RailsEventViewer.per_page).to_i.clamp(1, 100)
76
67
  end
77
68
 
78
69
  def paginate(relation)
79
- pagination = Page.new(count: relation.count, per_page: per_page, page: params[:page])
70
+ pagination = Page.new(count: relation.count, per_page: per_page, page: string_param(:page))
80
71
  [pagination, relation.offset(pagination.offset).limit(pagination.per_page).to_a]
81
72
  end
82
73
  end
@@ -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,55 +18,41 @@ 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
 
54
41
  def find_related_events(event)
55
- context = event.respond_to?(:context) ? event.context : event[:context]
42
+ context = event[:context]
56
43
  return [] unless context.present?
57
44
 
58
- # Find events with matching request_id in context
59
45
  request_id = context["request_id"] || context[:request_id]
60
- return [] unless request_id
61
-
62
- event_id = event.respond_to?(:id) ? event.id : event[:id]
46
+ occurred_at = event[:occurred_at]
47
+ return [] unless request_id && occurred_at
63
48
 
64
49
  RailsEventViewer.events
65
50
  .with_context("request_id", request_id)
51
+ .since(occurred_at - 1.hour)
52
+ .until(occurred_at + 1.hour)
66
53
  .limit(11)
67
54
  .to_a
68
- .reject { |e| (e.respond_to?(:id) ? e.id : e[:id]) == event_id }
55
+ .reject { |related| related[:id] == event[:id] }
69
56
  .first(10)
70
57
  end
71
58
  end
@@ -31,7 +31,6 @@ module RailsEventViewer
31
31
 
32
32
  @pagination, @events = paginate(events)
33
33
  @total_count = @pagination.count
34
- @event_names = @events.map { |e| event_name(e) }.uniq
35
34
 
36
35
  @first_event_at, @last_event_at = current_adapter.event_time_span(events)
37
36
  @duration = @last_event_at - @first_event_at if @first_event_at && @last_event_at
@@ -42,13 +41,5 @@ module RailsEventViewer
42
41
  def source_param
43
42
  params[:source].presence_in(%w[context tags])&.to_sym || :context
44
43
  end
45
-
46
- def event_name(event)
47
- event.respond_to?(:name) ? event.name : event[:name]
48
- end
49
-
50
- def event_occurred_at(event)
51
- event.respond_to?(:occurred_at) ? event.occurred_at : event[:occurred_at]
52
- end
53
44
  end
54
45
  end
@@ -1,101 +1,55 @@
1
1
  module RailsEventViewer
2
2
  module ApplicationHelper
3
- def nav_link_class(controller)
4
- base_classes = "rounded-md px-3 py-2 text-sm font-medium"
3
+ def nav_link(label, path, controller)
4
+ active = controller_name == controller
5
+ state_classes = active ? "bg-indigo-700 text-white" : "text-indigo-200 hover:bg-indigo-500 hover:text-white"
5
6
 
6
- if controller_name == controller
7
- "#{base_classes} bg-indigo-700 text-white"
8
- else
9
- "#{base_classes} text-indigo-200 hover:bg-indigo-500 hover:text-white"
10
- end
7
+ link_to label, path, class: "rounded-md px-3 py-2 text-sm font-medium #{state_classes}", aria: { current: ("page" if active) }
11
8
  end
12
9
 
13
- # Format event time - works with both Entry model and Hash
14
10
  def format_event_time(event)
15
- time = event_occurred_at(event)
11
+ time = event[:occurred_at]
16
12
  return "N/A" unless time
17
13
 
18
- time.strftime("%Y-%m-%d %H:%M:%S.%3N")
14
+ time.in_time_zone.strftime("%Y-%m-%d %H:%M:%S.%3N")
19
15
  end
20
16
 
21
17
  def time_ago_with_title(time)
22
18
  return "N/A" unless time
23
19
 
24
- content_tag(:span, time_ago_in_words(time) + " ago", title: time.strftime("%Y-%m-%d %H:%M:%S %Z"))
20
+ content_tag(:span, time_ago_in_words(time) + " ago", title: time.in_time_zone.strftime("%Y-%m-%d %H:%M:%S %Z"))
25
21
  end
26
22
 
27
23
  def truncate_json(json, length: 100)
28
- text = json.is_a?(Hash) ? json.to_json : json.to_s
24
+ text = json.is_a?(Hash) ? JSON.generate(json) : json.to_s
29
25
  truncate(text, length: length)
30
26
  end
31
27
 
32
- def json_tree(data, depth: 0, max_depth: nil)
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)
35
- return content_tag(:span, collapsed_summary(data), class: "text-gray-400") if max_depth && depth >= max_depth
28
+ def json_tree(data)
29
+ return content_tag(:span, "null", class: "text-gray-500") if data.nil?
30
+ return content_tag(:span, JSON.generate(data), class: json_value_class(data)) unless data.is_a?(Hash) || data.is_a?(Array)
36
31
 
37
- if data.is_a?(Hash)
38
- render_hash_tree(data, depth, max_depth)
39
- else
40
- render_array_tree(data, depth, max_depth)
41
- end
32
+ data.is_a?(Hash) ? render_hash_tree(data) : render_array_tree(data)
42
33
  end
43
34
 
44
35
  def page_path(page)
45
36
  "#{request.path}?#{request.query_parameters.merge("page" => page.to_s).to_query}"
46
37
  end
47
38
 
48
- def timezone_options
49
- ActiveSupport::TimeZone.all.map { |tz| [tz.to_s, tz.name] }
50
- end
51
-
52
- # Helper to get attribute from either Entry model or Hash
53
- def event_attribute(event, attr)
54
- if event.respond_to?(attr)
55
- event.public_send(attr)
56
- elsif event.is_a?(Hash)
57
- event[attr] || event[attr.to_s]
58
- end
59
- end
60
-
61
- def event_id(event)
62
- event_attribute(event, :id)
63
- end
64
-
65
- def event_name(event)
66
- event_attribute(event, :name)
67
- end
68
-
69
39
  def event_payload(event)
70
- event_attribute(event, :payload) || {}
40
+ event[:payload] || {}
71
41
  end
72
42
 
73
43
  def event_tags(event)
74
- event_attribute(event, :tags) || {}
44
+ event[:tags] || {}
75
45
  end
76
46
 
77
47
  def event_context(event)
78
- event_attribute(event, :context) || {}
79
- end
80
-
81
- def event_occurred_at(event)
82
- event_attribute(event, :occurred_at)
83
- end
84
-
85
- def event_source_file(event)
86
- event_attribute(event, :source_file)
87
- end
88
-
89
- def event_source_line(event)
90
- event_attribute(event, :source_line)
91
- end
92
-
93
- def event_source_label(event)
94
- event_attribute(event, :source_label)
48
+ event[:context] || {}
95
49
  end
96
50
 
97
51
  def event_short_filepath(event)
98
- filepath = event_source_file(event)
52
+ filepath = event[:source_file]
99
53
  return nil unless filepath
100
54
 
101
55
  filepath.gsub(%r{.*/app/}, "app/")
@@ -130,35 +84,27 @@ module RailsEventViewer
130
84
  end
131
85
  end
132
86
 
133
- def collapsed_summary(data)
134
- if data.is_a?(Hash)
135
- data.empty? ? "{}" : "{#{data.size} #{"key".pluralize(data.size)}}"
136
- else
137
- data.empty? ? "[]" : "[#{data.size} #{"item".pluralize(data.size)}]"
138
- end
139
- end
140
-
141
- def render_hash_tree(hash, depth, max_depth)
142
- return content_tag(:span, "{}", class: "text-gray-400") if hash.empty?
87
+ def render_hash_tree(hash)
88
+ return content_tag(:span, "{}", class: "text-gray-500") if hash.empty?
143
89
 
144
90
  content_tag(:div, class: "pl-4") do
145
91
  hash.map do |key, value|
146
92
  content_tag(:div, class: "py-0.5") do
147
93
  content_tag(:span, "#{key}:", class: "text-gray-600 font-medium mr-2") +
148
- json_tree(value, depth: depth + 1, max_depth: max_depth)
94
+ json_tree(value)
149
95
  end
150
96
  end.join.html_safe
151
97
  end
152
98
  end
153
99
 
154
- def render_array_tree(array, depth, max_depth)
155
- return content_tag(:span, "[]", class: "text-gray-400") if array.empty?
100
+ def render_array_tree(array)
101
+ return content_tag(:span, "[]", class: "text-gray-500") if array.empty?
156
102
 
157
103
  content_tag(:div, class: "pl-4") do
158
104
  array.each_with_index.map do |value, index|
159
105
  content_tag(:div, class: "py-0.5") do
160
- content_tag(:span, "[#{index}]:", class: "text-gray-400 mr-2") +
161
- json_tree(value, depth: depth + 1, max_depth: max_depth)
106
+ content_tag(:span, "[#{index}]:", class: "text-gray-500 mr-2") +
107
+ json_tree(value)
162
108
  end
163
109
  end.join.html_safe
164
110
  end
@@ -4,31 +4,5 @@ module RailsEventViewer
4
4
 
5
5
  validates :name, presence: true
6
6
  validates :occurred_at, presence: true
7
-
8
- def source_location
9
- {
10
- filepath: source_file,
11
- lineno: source_line,
12
- label: source_label
13
- }.compact
14
- end
15
-
16
- def short_filepath
17
- return nil unless source_file
18
-
19
- source_file.gsub(%r{.*/app/}, "app/")
20
- end
21
-
22
- def tagged?
23
- tags.present? && tags.any?
24
- end
25
-
26
- def has_context?
27
- context.present? && context.any?
28
- end
29
-
30
- def tag(key)
31
- tags&.dig(key.to_s)
32
- end
33
7
  end
34
8
  end
@@ -31,5 +31,14 @@ module RailsEventViewer
31
31
  def next
32
32
  page + 1 if page < pages
33
33
  end
34
+
35
+ def window
36
+ numbers = [1, *(page - 2..page + 2), pages].select { |number| number.between?(1, pages) }.uniq.sort
37
+
38
+ numbers.each_with_object([]) do |number, items|
39
+ items << :gap if items.any? && number > items.last + 1
40
+ items << number
41
+ end
42
+ end
34
43
  end
35
44
  end
@@ -11,9 +11,8 @@
11
11
  "rails_event_viewer/chartjs-adapter-date-fns.bundle.min",
12
12
  "rails_event_viewer/chartkick.min",
13
13
  "rails_event_viewer/application",
14
+ defer: true,
14
15
  nonce: true %>
15
-
16
- <%= yield :head %>
17
16
  </head>
18
17
  <body class="h-full">
19
18
  <div class="min-h-full">
@@ -22,17 +21,25 @@
22
21
  <main class="py-6">
23
22
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
24
23
  <% if notice.present? %>
25
- <div class="mb-4 rounded-md bg-green-50 p-4">
24
+ <div class="mb-4 rounded-md bg-green-50 p-4" role="status">
26
25
  <p class="text-sm font-medium text-green-800"><%= notice %></p>
27
26
  </div>
28
27
  <% end %>
29
28
 
30
29
  <% if alert.present? %>
31
- <div class="mb-4 rounded-md bg-red-50 p-4">
30
+ <div class="mb-4 rounded-md bg-red-50 p-4" role="alert">
32
31
  <p class="text-sm font-medium text-red-800"><%= alert %></p>
33
32
  </div>
34
33
  <% end %>
35
34
 
35
+ <% if RailsEventViewer.storage_adapter == :active_record && !persistence_supported? %>
36
+ <div class="mb-4 rounded-md bg-red-50 p-4" role="alert">
37
+ <p class="text-sm font-medium text-red-800">
38
+ 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.
39
+ </p>
40
+ </div>
41
+ <% end %>
42
+
36
43
  <% if RailsEventViewer.sample_rate.to_f < 1.0 %>
37
44
  <div class="mb-4 rounded-md bg-yellow-50 border border-yellow-200 p-4">
38
45
  <p class="text-sm font-medium text-yellow-800">