solid_ops 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +308 -0
  7. data/Rakefile +12 -0
  8. data/app/assets/stylesheets/solid_ops/application.css +1 -0
  9. data/app/controllers/solid_ops/application_controller.rb +127 -0
  10. data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
  11. data/app/controllers/solid_ops/channels_controller.rb +30 -0
  12. data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
  13. data/app/controllers/solid_ops/events_controller.rb +37 -0
  14. data/app/controllers/solid_ops/jobs_controller.rb +64 -0
  15. data/app/controllers/solid_ops/processes_controller.rb +11 -0
  16. data/app/controllers/solid_ops/queues_controller.rb +75 -0
  17. data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
  18. data/app/helpers/solid_ops/application_helper.rb +112 -0
  19. data/app/jobs/solid_ops/purge_job.rb +16 -0
  20. data/app/models/solid_ops/event.rb +34 -0
  21. data/app/views/layouts/solid_ops/application.html.erb +118 -0
  22. data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
  23. data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
  24. data/app/views/solid_ops/channels/index.html.erb +81 -0
  25. data/app/views/solid_ops/channels/show.html.erb +66 -0
  26. data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
  27. data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
  28. data/app/views/solid_ops/dashboard/index.html.erb +169 -0
  29. data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
  30. data/app/views/solid_ops/events/index.html.erb +98 -0
  31. data/app/views/solid_ops/events/show.html.erb +108 -0
  32. data/app/views/solid_ops/jobs/failed.html.erb +89 -0
  33. data/app/views/solid_ops/jobs/running.html.erb +134 -0
  34. data/app/views/solid_ops/jobs/show.html.erb +116 -0
  35. data/app/views/solid_ops/processes/index.html.erb +69 -0
  36. data/app/views/solid_ops/queues/index.html.erb +182 -0
  37. data/app/views/solid_ops/queues/show.html.erb +121 -0
  38. data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
  39. data/app/views/solid_ops/shared/_nav.html.erb +50 -0
  40. data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
  41. data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
  42. data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
  43. data/config/routes.rb +49 -0
  44. data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
  45. data/lib/generators/solid_ops/install/install_generator.rb +348 -0
  46. data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
  47. data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
  48. data/lib/solid_ops/configuration.rb +28 -0
  49. data/lib/solid_ops/context.rb +34 -0
  50. data/lib/solid_ops/current.rb +10 -0
  51. data/lib/solid_ops/engine.rb +60 -0
  52. data/lib/solid_ops/job_extension.rb +50 -0
  53. data/lib/solid_ops/middleware.rb +52 -0
  54. data/lib/solid_ops/subscribers.rb +215 -0
  55. data/lib/solid_ops/version.rb +5 -0
  56. data/lib/solid_ops.rb +25 -0
  57. data/lib/tasks/solid_ops.rake +32 -0
  58. data/log/test.log +2 -0
  59. data/sig/solid_ops.rbs +4 -0
  60. metadata +119 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c23d7cb9a0254d9ad25a6607b818be3131397965279c8b6b6fe4f61ff5a3af10
4
+ data.tar.gz: 51aeada8c37701a73c706576fd990365021f0e095057376adc7b496c184de99e
5
+ SHA512:
6
+ metadata.gz: bbdfa855cc0f2204f46f3c90c4f6a6def166d07081098ea60390c77c5d203e8fccd02756c0af12868020ebbf5ebf902fa1d8cef356d76776198b233c350d8c5e
7
+ data.tar.gz: 98fdad7ca5334957a47e6bc95d06e6806841de55ccc57828809c47f53c5932dd8f0015982aa9e9364fe07f0f110db866b566bb84d30266d916bb6f9dcf14e26f
data/.DS_Store ADDED
Binary file
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-02-24
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "solid_ops" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["samuelmurphy15@gmail.com"](mailto:"samuelmurphy15@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 samuel-murphy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # SolidOps
2
+
3
+ Rails-native observability and control plane for the **Solid Trifecta** — [Solid Queue](https://github.com/rails/solid_queue), [Solid Cache](https://github.com/rails/solid_cache), and [Solid Cable](https://github.com/rails/solid_cable).
4
+
5
+ A mountable Rails engine that gives you a real-time dashboard and management UI with zero JavaScript dependencies.
6
+
7
+ ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red) ![Rails](https://img.shields.io/badge/rails-%3E%3D%207.1-red) ![License](https://img.shields.io/badge/license-MIT-green)
8
+
9
+ ## Quick Start
10
+
11
+ Get SolidOps running in a fresh Rails app in under two minutes:
12
+
13
+ ```bash
14
+ # 1. Add the gem
15
+ bundle add solid_ops
16
+
17
+ # 2. Run the installer (installs Solid Queue/Cache/Cable if missing)
18
+ bin/rails generate solid_ops:install --all
19
+
20
+ # 3. Update config/database.yml for multi-database (see "Database setup" below)
21
+
22
+ # 4. Create databases and run migrations (safe for existing apps — never drops data)
23
+ bin/rails db:prepare
24
+
25
+ # 5. Start your app and visit the dashboard
26
+ bin/rails server
27
+ # → http://localhost:3000/solid_ops
28
+ ```
29
+
30
+ The dashboard works immediately — enqueue a job, write to cache, or broadcast on Cable, then refresh the SolidOps dashboard to see events flowing in.
31
+
32
+ ## Features
33
+
34
+ **Observability** — automatic event capture via ActiveSupport instrumentation:
35
+ - Job lifecycle tracking (enqueue, perform_start, perform)
36
+ - Cache operation monitoring (read, write, delete with hit/miss rates)
37
+ - Cable broadcast tracking
38
+ - Correlation IDs across request → job → cache flows
39
+ - Configurable sampling, redaction, and retention
40
+
41
+ **Queue Management** (Solid Queue):
42
+ - Queue overview with pause/resume controls
43
+ - Job inspection, retry, and discard
44
+ - Failed jobs dashboard with bulk retry/discard
45
+ - Process monitoring (workers & supervisors)
46
+ - Recurring task browser
47
+
48
+ **Cache Management** (Solid Cache):
49
+ - Browse and search cache entries
50
+ - Inspect individual entries
51
+ - Delete entries or clear all
52
+
53
+ **Channel Management** (Solid Cable):
54
+ - Channel overview with message counts
55
+ - Message inspection per channel
56
+ - Trim old messages
57
+
58
+ ## Installation
59
+
60
+ Add to your Gemfile:
61
+
62
+ ```ruby
63
+ gem "solid_ops"
64
+ ```
65
+
66
+ Run the install generator:
67
+
68
+ ```bash
69
+ bundle install
70
+ bin/rails generate solid_ops:install
71
+ ```
72
+
73
+ The generator will ask if you want to install all Solid components. Say yes and it handles everything — adds the gems, runs their installers, and migrates all databases.
74
+
75
+ ### Install options
76
+
77
+ ```bash
78
+ # Interactive — asks what you want
79
+ bin/rails generate solid_ops:install
80
+
81
+ # Install everything at once (no prompts)
82
+ bin/rails generate solid_ops:install --all
83
+
84
+ # Pick specific components
85
+ bin/rails generate solid_ops:install --queue # just Solid Queue
86
+ bin/rails generate solid_ops:install --queue --cache # Queue + Cache
87
+ ```
88
+
89
+ The generator will:
90
+ 1. Create `config/initializers/solid_ops.rb` with all configuration options
91
+ 2. Mount the engine at `/solid_ops` in your routes
92
+ 3. Add selected Solid gems to your `Gemfile` and run their installers
93
+ 4. Configure `development.rb` and `test.rb` with `connects_to` for Solid Queue & Cache
94
+ 5. Configure `cable.yml` to use `solid_cable` adapter in development/test
95
+ 6. Print `database.yml` changes you need to apply (see below)
96
+
97
+ ### Database setup
98
+
99
+ The Solid gem installers only configure `database.yml` for **production**. You need to update
100
+ your `development` and `test` sections to use multi-database so the Solid tables have their
101
+ own SQLite files.
102
+
103
+ Replace your `development:` and `test:` sections in `config/database.yml`.
104
+
105
+ **SQLite:**
106
+
107
+ ```yaml
108
+ development:
109
+ primary:
110
+ <<: *default
111
+ database: storage/development.sqlite3
112
+ queue:
113
+ <<: *default
114
+ database: storage/development_queue.sqlite3
115
+ migrations_paths: db/queue_migrate
116
+ cache:
117
+ <<: *default
118
+ database: storage/development_cache.sqlite3
119
+ migrations_paths: db/cache_migrate
120
+ cable:
121
+ <<: *default
122
+ database: storage/development_cable.sqlite3
123
+ migrations_paths: db/cable_migrate
124
+ ```
125
+
126
+ **PostgreSQL:**
127
+
128
+ ```yaml
129
+ development:
130
+ primary:
131
+ <<: *default
132
+ database: myapp_development
133
+ queue:
134
+ <<: *default
135
+ database: myapp_development_queue
136
+ migrations_paths: db/queue_migrate
137
+ cache:
138
+ <<: *default
139
+ database: myapp_development_cache
140
+ migrations_paths: db/cache_migrate
141
+ cable:
142
+ <<: *default
143
+ database: myapp_development_cable
144
+ migrations_paths: db/cable_migrate
145
+ ```
146
+
147
+ **MySQL:**
148
+
149
+ ```yaml
150
+ development:
151
+ primary:
152
+ <<: *default
153
+ database: myapp_development
154
+ queue:
155
+ <<: *default
156
+ database: myapp_development_queue
157
+ migrations_paths: db/queue_migrate
158
+ cache:
159
+ <<: *default
160
+ database: myapp_development_cache
161
+ migrations_paths: db/cache_migrate
162
+ cable:
163
+ <<: *default
164
+ database: myapp_development_cable
165
+ migrations_paths: db/cable_migrate
166
+ ```
167
+
168
+ Apply the same pattern for `test:`. Only include the `queue:`, `cache:`, and/or `cable:` entries for the components you installed.
169
+
170
+ Then create and prepare all databases:
171
+
172
+ ```bash
173
+ bin/rails db:prepare
174
+ ```
175
+
176
+ ## Configuration
177
+
178
+ All options are documented in the generated initializer (`config/initializers/solid_ops.rb`):
179
+
180
+ ```ruby
181
+ SolidOps.configure do |config|
182
+ # Enable/disable event capture
183
+ config.enabled = true
184
+
185
+ # Sampling rate: 1.0 = everything, 0.1 = 10%
186
+ config.sample_rate = 1.0
187
+
188
+ # Auto-purge events older than this
189
+ config.retention_period = 7.days
190
+
191
+ # Maximum metadata payload size before truncation
192
+ config.max_payload_bytes = 10_000
193
+
194
+ # Strip sensitive data from metadata before storage
195
+ config.redactor = ->(meta) { meta.except(:password, :token, :secret) }
196
+
197
+ # Multi-tenant support
198
+ config.tenant_resolver = ->(request) { request.subdomain }
199
+
200
+ # Track which user triggered each event
201
+ config.actor_resolver = ->(request) { request.env["warden"]&.user&.id }
202
+
203
+ # Restrict access to the dashboard (nil = open to all)
204
+ config.auth_check = ->(controller) { controller.current_user&.admin? }
205
+ end
206
+ ```
207
+
208
+ ### Authentication
209
+
210
+ **Important:** If no `auth_check` is configured, SolidOps logs a prominent warning at boot:
211
+
212
+ ```
213
+ [SolidOps] WARNING: No auth_check configured — the dashboard is publicly accessible.
214
+ ```
215
+
216
+ Use `auth_check` to restrict access:
217
+
218
+ ```ruby
219
+ # Devise admin check
220
+ config.auth_check = ->(controller) { controller.current_user&.admin? }
221
+
222
+ # Basic HTTP auth
223
+ config.auth_check = ->(controller) {
224
+ controller.authenticate_or_request_with_http_basic do |user, pass|
225
+ user == "admin" && pass == Rails.application.credentials.solid_ops_password
226
+ end
227
+ }
228
+ ```
229
+
230
+ ### Automatic Purging
231
+
232
+ Old events are not purged automatically. Set up a recurring job or cron:
233
+
234
+ ```ruby
235
+ # In config/recurring.yml (Solid Queue)
236
+ solid_ops_purge:
237
+ class: SolidOps::PurgeJob
238
+ schedule: every day at 3am
239
+
240
+ # Or via rake
241
+ # crontab: 0 3 * * * cd /app && bin/rails solid_ops:purge
242
+ ```
243
+
244
+ ## Production Notes
245
+
246
+ - **Authentication** — configure `auth_check` in your initializer. Without it the dashboard is open to anyone who can reach the mount path. A boot-time warning is logged if unconfigured.
247
+ - **Running jobs** — the Running Jobs page uses `COUNT(*)` for the total and caps the displayed list at 500 rows to avoid loading thousands of records into memory.
248
+ - **Bulk operations** — `Retry All` processes failed jobs in batches of 100. `Clear All` (cache) deletes in batches of 1,000. Neither locks the table for the full duration.
249
+ - **Availability checks** — `solid_queue_available?`, `solid_cache_available?`, and `solid_cable_available?` are memoized per-process (one schema query at boot, not per request).
250
+ - **Event recording** — all `record_event!` calls are wrapped in `rescue` and will never crash your application. A warning is logged on failure.
251
+ - **CSS isolation** — all styles are scoped to `.solid-ops` via Tailwind's `important` selector strategy with Preflight disabled. No global CSS leaks into your host app.
252
+
253
+ ## Requirements
254
+
255
+ - **Ruby** >= 3.2
256
+ - **Rails** >= 7.1
257
+ - At least one of: `solid_queue`, `solid_cache`, `solid_cable`
258
+
259
+ SolidOps gracefully handles missing Solid components — pages for unconfigured components show a clear message instead of erroring.
260
+
261
+ ## Routes
262
+
263
+ The engine mounts at `/solid_ops` by default. Available pages:
264
+
265
+ | Path | Description |
266
+ |------|-------------|
267
+ | `/solid_ops` | Main dashboard with event breakdown |
268
+ | `/solid_ops/dashboard/jobs` | Job event analytics |
269
+ | `/solid_ops/dashboard/cache` | Cache hit/miss analytics |
270
+ | `/solid_ops/dashboard/cable` | Cable broadcast analytics |
271
+ | `/solid_ops/events` | Event explorer with filtering |
272
+ | `/solid_ops/queues` | Queue management (pause/resume) |
273
+ | `/solid_ops/jobs/running` | Currently executing jobs |
274
+ | `/solid_ops/jobs/failed` | Failed jobs (retry/discard) |
275
+ | `/solid_ops/processes` | Active workers & supervisors |
276
+ | `/solid_ops/recurring-tasks` | Recurring task browser |
277
+ | `/solid_ops/cache` | Cache entry browser |
278
+ | `/solid_ops/channels` | Cable channel browser |
279
+
280
+ ## Development
281
+
282
+ ```bash
283
+ git clone https://github.com/h0m1c1de/solid_ops.git
284
+ cd solid_ops
285
+ bin/setup
286
+ rake spec
287
+ ```
288
+
289
+ ### Rebuilding CSS
290
+
291
+ The dashboard UI is styled with Tailwind CSS, compiled at release time into a single static stylesheet. The gem ships pre-built CSS — **no Node.js, Tailwind, or build step is needed at deploy time**.
292
+
293
+ If you modify any view templates, rebuild the CSS before committing:
294
+
295
+ ```bash
296
+ npm install # first time only
297
+ ./bin/build_css # compiles app/assets/stylesheets/solid_ops/application.css
298
+ ```
299
+
300
+ Requires Node.js (for `npx tailwindcss@3`). The compiled CSS is checked into git so consumers of the gem never need Node.
301
+
302
+ ## Contributing
303
+
304
+ Bug reports and pull requests are welcome on GitHub at https://github.com/h0m1c1de/solid_ops. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
305
+
306
+ ## License
307
+
308
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +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: }input:where(:not([type])),input:where([type=date]),input:where([type=datetime-local]),input:where([type=email]),input:where([type=month]),input:where([type=number]),input:where([type=password]),input:where([type=search]),input:where([type=tel]),input:where([type=text]),input:where([type=time]),input:where([type=url]),input:where([type=week]),select,select:where([multiple]),textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}input:where(:not([type])):focus,input:where([type=date]):focus,input:where([type=datetime-local]):focus,input:where([type=email]):focus,input:where([type=month]):focus,input:where([type=number]):focus,input:where([type=password]):focus,input:where([type=search]):focus,input:where([type=tel]):focus,input:where([type=text]):focus,input:where([type=time]):focus,input:where([type=url]):focus,input:where([type=week]):focus,select:focus,select:where([multiple]):focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--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(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}select:where([multiple]),select:where([size]:not([size="1"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}input:where([type=checkbox]),input:where([type=radio]){-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}input:where([type=checkbox]){border-radius:0}input:where([type=radio]){border-radius:100%}input:where([type=checkbox]):focus,input:where([type=radio]):focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--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)}input:where([type=checkbox]):checked,input:where([type=radio]):checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}input:where([type=checkbox]):checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0'/%3E%3C/svg%3E")}@media (forced-colors:active) {input:where([type=checkbox]):checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}input:where([type=radio]):checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}@media (forced-colors:active) {input:where([type=radio]):checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}input:where([type=checkbox]):checked:focus,input:where([type=checkbox]):checked:hover,input:where([type=radio]):checked:focus,input:where([type=radio]):checked:hover{border-color:transparent;background-color:currentColor}input:where([type=checkbox]):indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active) {input:where([type=checkbox]):indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}input:where([type=checkbox]):indeterminate:focus,input:where([type=checkbox]):indeterminate:hover{border-color:transparent;background-color:currentColor}input:where([type=file]){background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}input:where([type=file]):focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}.solid-ops .absolute{position:absolute}.solid-ops .relative{position:relative}.solid-ops .sticky{position:sticky}.solid-ops .inset-x-0{left:0;right:0}.solid-ops .-left-\[25px\]{left:-25px}.solid-ops .left-0{left:0}.solid-ops .right-0{right:0}.solid-ops .top-0{top:0}.solid-ops .top-1{top:.25rem}.solid-ops .z-50{z-index:50}.solid-ops .-mx-3{margin-left:-.75rem;margin-right:-.75rem}.solid-ops .mx-auto{margin-left:auto;margin-right:auto}.solid-ops .mb-1{margin-bottom:.25rem}.solid-ops .mb-1\.5{margin-bottom:.375rem}.solid-ops .mb-2{margin-bottom:.5rem}.solid-ops .mb-3{margin-bottom:.75rem}.solid-ops .mb-4{margin-bottom:1rem}.solid-ops .mb-6{margin-bottom:1.5rem}.solid-ops .mb-8{margin-bottom:2rem}.solid-ops .ml-1{margin-left:.25rem}.solid-ops .mt-0\.5{margin-top:.125rem}.solid-ops .mt-1{margin-top:.25rem}.solid-ops .mt-1\.5{margin-top:.375rem}.solid-ops .mt-12{margin-top:3rem}.solid-ops .mt-2{margin-top:.5rem}.solid-ops .mt-4{margin-top:1rem}.solid-ops .mt-6{margin-top:1.5rem}.solid-ops .mt-8{margin-top:2rem}.solid-ops .block{display:block}.solid-ops .inline-block{display:inline-block}.solid-ops .inline{display:inline}.solid-ops .flex{display:flex}.solid-ops .inline-flex{display:inline-flex}.solid-ops .table{display:table}.solid-ops .grid{display:grid}.solid-ops .hidden{display:none}.solid-ops .h-0\.5{height:.125rem}.solid-ops .h-1{height:.25rem}.solid-ops .h-10{height:2.5rem}.solid-ops .h-14{height:3.5rem}.solid-ops .h-16{height:4rem}.solid-ops .h-2{height:.5rem}.solid-ops .h-3{height:.75rem}.solid-ops .h-3\.5{height:.875rem}.solid-ops .h-4{height:1rem}.solid-ops .h-5{height:1.25rem}.solid-ops .h-6{height:1.5rem}.solid-ops .h-7{height:1.75rem}.solid-ops .h-8{height:2rem}.solid-ops .h-full{height:100%}.solid-ops .max-h-64{max-height:16rem}.solid-ops .max-h-80{max-height:20rem}.solid-ops .min-h-\[calc\(100vh-8rem\)\]{min-height:calc(100vh - 8rem)}.solid-ops .w-10{width:2.5rem}.solid-ops .w-14{width:3.5rem}.solid-ops .w-16{width:4rem}.solid-ops .w-2{width:.5rem}.solid-ops .w-20{width:5rem}.solid-ops .w-24{width:6rem}.solid-ops .w-3{width:.75rem}.solid-ops .w-3\.5{width:.875rem}.solid-ops .w-32{width:8rem}.solid-ops .w-4{width:1rem}.solid-ops .w-5{width:1.25rem}.solid-ops .w-6{width:1.5rem}.solid-ops .w-7{width:1.75rem}.solid-ops .w-8{width:2rem}.solid-ops .w-full{width:100%}.solid-ops .min-w-\[120px\]{min-width:120px}.solid-ops .min-w-\[160px\]{min-width:160px}.solid-ops .min-w-full{min-width:100%}.solid-ops .max-w-2xl{max-width:42rem}.solid-ops .max-w-7xl{max-width:80rem}.solid-ops .max-w-lg{max-width:32rem}.solid-ops .max-w-md{max-width:28rem}.solid-ops .max-w-xs{max-width:20rem}.solid-ops .flex-1{flex:1 1 0%}.solid-ops .flex-shrink-0{flex-shrink:0}.solid-ops .transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.solid-ops .animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}.solid-ops .grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.solid-ops .grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.solid-ops .flex-wrap{flex-wrap:wrap}.solid-ops .items-start{align-items:flex-start}.solid-ops .items-end{align-items:flex-end}.solid-ops .items-center{align-items:center}.solid-ops .justify-center{justify-content:center}.solid-ops .justify-between{justify-content:space-between}.solid-ops .gap-1{gap:.25rem}.solid-ops .gap-1\.5{gap:.375rem}.solid-ops .gap-2{gap:.5rem}.solid-ops .gap-2\.5{gap:.625rem}.solid-ops .gap-3{gap:.75rem}.solid-ops .gap-4{gap:1rem}.solid-ops .gap-6{gap:1.5rem}.solid-ops :is(.space-y-3>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.solid-ops :is(.space-y-4>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.solid-ops :is(.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))}.solid-ops :is(.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))}.solid-ops :is(.divide-gray-100>:not([hidden])~:not([hidden])){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity,1))}.solid-ops :is(.divide-gray-50>:not([hidden])~:not([hidden])){--tw-divide-opacity:1;border-color:rgb(249 250 251/var(--tw-divide-opacity,1))}.solid-ops .overflow-hidden{overflow:hidden}.solid-ops .overflow-x-auto{overflow-x:auto}.solid-ops .overflow-y-auto{overflow-y:auto}.solid-ops .truncate{overflow:hidden;text-overflow:ellipsis}.solid-ops .truncate,.solid-ops .whitespace-nowrap{white-space:nowrap}.solid-ops .break-all{word-break:break-all}.solid-ops .rounded{border-radius:.25rem}.solid-ops .rounded-2xl{border-radius:1rem}.solid-ops .rounded-full{border-radius:9999px}.solid-ops .rounded-lg{border-radius:.5rem}.solid-ops .rounded-md{border-radius:.375rem}.solid-ops .rounded-xl{border-radius:.75rem}.solid-ops .border{border-width:1px}.solid-ops .border-2{border-width:2px}.solid-ops .border-b{border-bottom-width:1px}.solid-ops .border-l-2{border-left-width:2px}.solid-ops .border-t{border-top-width:1px}.solid-ops .border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.solid-ops .border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.solid-ops .border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity,1))}.solid-ops .border-emerald-200{--tw-border-opacity:1;border-color:rgb(167 243 208/var(--tw-border-opacity,1))}.solid-ops .border-emerald-400{--tw-border-opacity:1;border-color:rgb(52 211 153/var(--tw-border-opacity,1))}.solid-ops .border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.solid-ops .border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.solid-ops .border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.solid-ops .border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity,1))}.solid-ops .border-purple-400{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity,1))}.solid-ops .border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.solid-ops .border-red-200\/50{border-color:hsla(0,96%,89%,.5)}.solid-ops .border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.solid-ops .bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.solid-ops .bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-50\/50{background-color:rgba(239,246,255,.5)}.solid-ops .bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.solid-ops .bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.solid-ops .bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.solid-ops .bg-emerald-200{--tw-bg-opacity:1;background-color:rgb(167 243 208/var(--tw-bg-opacity,1))}.solid-ops .bg-emerald-50{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.solid-ops .bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.solid-ops .bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.solid-ops .bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.solid-ops .bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.solid-ops .bg-gray-50\/50{background-color:rgba(249,250,251,.5)}.solid-ops .bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.solid-ops .bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.solid-ops .bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity,1))}.solid-ops .bg-purple-200{--tw-bg-opacity:1;background-color:rgb(233 213 255/var(--tw-bg-opacity,1))}.solid-ops .bg-purple-50{--tw-bg-opacity:1;background-color:rgb(250 245 255/var(--tw-bg-opacity,1))}.solid-ops .bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.solid-ops .bg-red-100\/50{background-color:hsla(0,93%,94%,.5)}.solid-ops .bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.solid-ops .bg-slate-800{--tw-bg-opacity:1;background-color:rgb(30 41 59/var(--tw-bg-opacity,1))}.solid-ops .bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.solid-ops .bg-white\/15{background-color:hsla(0,0%,100%,.15)}.solid-ops .bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.solid-ops .bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.solid-ops .bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.solid-ops .from-amber-400{--tw-gradient-from:#fbbf24 var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,191,36,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-amber-50{--tw-gradient-from:#fffbeb var(--tw-gradient-from-position);--tw-gradient-to:rgba(255,251,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-amber-500{--tw-gradient-from:#f59e0b var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-blue-400{--tw-gradient-from:#60a5fa var(--tw-gradient-from-position);--tw-gradient-to:rgba(96,165,250,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:rgba(59,130,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-emerald-400{--tw-gradient-from:#34d399 var(--tw-gradient-from-position);--tw-gradient-to:rgba(52,211,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-emerald-50{--tw-gradient-from:#ecfdf5 var(--tw-gradient-from-position);--tw-gradient-to:rgba(236,253,245,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-emerald-500{--tw-gradient-from:#10b981 var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-gray-200{--tw-gradient-from:#e5e7eb var(--tw-gradient-from-position);--tw-gradient-to:rgba(229,231,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-gray-300{--tw-gradient-from:#d1d5db var(--tw-gradient-from-position);--tw-gradient-to:rgba(209,213,219,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-gray-400{--tw-gradient-from:#9ca3af var(--tw-gradient-from-position);--tw-gradient-to:rgba(156,163,175,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-indigo-400{--tw-gradient-from:#818cf8 var(--tw-gradient-from-position);--tw-gradient-to:rgba(129,140,248,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-indigo-500{--tw-gradient-from:#6366f1 var(--tw-gradient-from-position);--tw-gradient-to:rgba(99,102,241,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-orange-400{--tw-gradient-from:#fb923c var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,146,60,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-purple-400{--tw-gradient-from:#c084fc var(--tw-gradient-from-position);--tw-gradient-to:rgba(192,132,252,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-red-400{--tw-gradient-from:#f87171 var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,91%,71%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-slate-900{--tw-gradient-from:#0f172a var(--tw-gradient-from-position);--tw-gradient-to:rgba(15,23,42,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:rgba(250,204,21,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.solid-ops .via-slate-800{--tw-gradient-to:rgba(30,41,59,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#1e293b var(--tw-gradient-via-position),var(--tw-gradient-to)}.solid-ops .to-amber-400{--tw-gradient-to:#fbbf24 var(--tw-gradient-to-position)}.solid-ops .to-amber-500{--tw-gradient-to:#f59e0b var(--tw-gradient-to-position)}.solid-ops .to-amber-600{--tw-gradient-to:#d97706 var(--tw-gradient-to-position)}.solid-ops .to-blue-400{--tw-gradient-to:#60a5fa var(--tw-gradient-to-position)}.solid-ops .to-blue-600{--tw-gradient-to:#2563eb var(--tw-gradient-to-position)}.solid-ops .to-emerald-400{--tw-gradient-to:#34d399 var(--tw-gradient-to-position)}.solid-ops .to-emerald-600{--tw-gradient-to:#059669 var(--tw-gradient-to-position)}.solid-ops .to-gray-300{--tw-gradient-to:#d1d5db var(--tw-gradient-to-position)}.solid-ops .to-gray-400{--tw-gradient-to:#9ca3af var(--tw-gradient-to-position)}.solid-ops .to-gray-500{--tw-gradient-to:#6b7280 var(--tw-gradient-to-position)}.solid-ops .to-gray-600{--tw-gradient-to:#4b5563 var(--tw-gradient-to-position)}.solid-ops .to-green-50{--tw-gradient-to:#f0fdf4 var(--tw-gradient-to-position)}.solid-ops .to-indigo-600{--tw-gradient-to:#4f46e5 var(--tw-gradient-to-position)}.solid-ops .to-orange-500{--tw-gradient-to:#f97316 var(--tw-gradient-to-position)}.solid-ops .to-purple-600{--tw-gradient-to:#9333ea var(--tw-gradient-to-position)}.solid-ops .to-red-600{--tw-gradient-to:#dc2626 var(--tw-gradient-to-position)}.solid-ops .to-slate-900{--tw-gradient-to:#0f172a var(--tw-gradient-to-position)}.solid-ops .to-yellow-50{--tw-gradient-to:#fefce8 var(--tw-gradient-to-position)}.solid-ops .to-yellow-500{--tw-gradient-to:#eab308 var(--tw-gradient-to-position)}.solid-ops .p-1{padding:.25rem}.solid-ops .p-4{padding:1rem}.solid-ops .p-5{padding:1.25rem}.solid-ops .p-6{padding:1.5rem}.solid-ops .px-2{padding-left:.5rem;padding-right:.5rem}.solid-ops .px-2\.5{padding-left:.625rem;padding-right:.625rem}.solid-ops .px-3{padding-left:.75rem;padding-right:.75rem}.solid-ops .px-4{padding-left:1rem;padding-right:1rem}.solid-ops .px-5{padding-left:1.25rem;padding-right:1.25rem}.solid-ops .px-6{padding-left:1.5rem;padding-right:1.5rem}.solid-ops .py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.solid-ops .py-1{padding-top:.25rem;padding-bottom:.25rem}.solid-ops .py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.solid-ops .py-16{padding-top:4rem;padding-bottom:4rem}.solid-ops .py-2{padding-top:.5rem;padding-bottom:.5rem}.solid-ops .py-20{padding-top:5rem;padding-bottom:5rem}.solid-ops .py-3{padding-top:.75rem;padding-bottom:.75rem}.solid-ops .py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.solid-ops .py-4{padding-top:1rem;padding-bottom:1rem}.solid-ops .py-5{padding-top:1.25rem;padding-bottom:1.25rem}.solid-ops .py-6{padding-top:1.5rem;padding-bottom:1.5rem}.solid-ops .py-8{padding-top:2rem;padding-bottom:2rem}.solid-ops .pl-6{padding-left:1.5rem}.solid-ops .pt-0\.5{padding-top:.125rem}.solid-ops .pt-5{padding-top:1.25rem}.solid-ops .pt-6{padding-top:1.5rem}.solid-ops .text-left{text-align:left}.solid-ops .text-center{text-align:center}.solid-ops .text-right{text-align:right}.solid-ops .font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}.solid-ops .font-sans{font-family:Inter,system-ui,-apple-system,sans-serif}.solid-ops .text-2xl{font-size:1.5rem;line-height:2rem}.solid-ops .text-3xl{font-size:1.875rem;line-height:2.25rem}.solid-ops .text-\[10px\]{font-size:10px}.solid-ops .text-\[11px\]{font-size:11px}.solid-ops .text-base{font-size:1rem;line-height:1.5rem}.solid-ops .text-sm{font-size:.875rem;line-height:1.25rem}.solid-ops .text-xl{font-size:1.25rem;line-height:1.75rem}.solid-ops .text-xs{font-size:.75rem;line-height:1rem}.solid-ops .font-bold{font-weight:700}.solid-ops .font-extrabold{font-weight:800}.solid-ops .font-medium{font-weight:500}.solid-ops .font-semibold{font-weight:600}.solid-ops .uppercase{text-transform:uppercase}.solid-ops .capitalize{text-transform:capitalize}.solid-ops .leading-relaxed{line-height:1.625}.solid-ops .tracking-tight{letter-spacing:-.025em}.solid-ops .tracking-wide{letter-spacing:.025em}.solid-ops .tracking-wider{letter-spacing:.05em}.solid-ops .text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.solid-ops .text-amber-600{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.solid-ops .text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.solid-ops .text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.solid-ops .text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.solid-ops .text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.solid-ops .text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.solid-ops .text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.solid-ops .text-emerald-500{--tw-text-opacity:1;color:rgb(16 185 129/var(--tw-text-opacity,1))}.solid-ops .text-emerald-600{--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.solid-ops .text-emerald-600\/70{color:rgba(5,150,105,.7)}.solid-ops .text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.solid-ops .text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity,1))}.solid-ops .text-emerald-900{--tw-text-opacity:1;color:rgb(6 78 59/var(--tw-text-opacity,1))}.solid-ops .text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.solid-ops .text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.solid-ops .text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.solid-ops .text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.solid-ops .text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.solid-ops .text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.solid-ops .text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.solid-ops .text-indigo-700{--tw-text-opacity:1;color:rgb(67 56 202/var(--tw-text-opacity,1))}.solid-ops .text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity,1))}.solid-ops .text-orange-700{--tw-text-opacity:1;color:rgb(194 65 12/var(--tw-text-opacity,1))}.solid-ops .text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity,1))}.solid-ops .text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity,1))}.solid-ops .text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.solid-ops .text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.solid-ops .text-red-500\/80{color:rgba(239,68,68,.8)}.solid-ops .text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.solid-ops .text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.solid-ops .text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.solid-ops .text-red-900{--tw-text-opacity:1;color:rgb(127 29 29/var(--tw-text-opacity,1))}.solid-ops .text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.solid-ops .text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.solid-ops .text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.solid-ops .text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.solid-ops .text-yellow-800{--tw-text-opacity:1;color:rgb(133 77 14/var(--tw-text-opacity,1))}.solid-ops .antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.solid-ops .opacity-0{opacity:0}.solid-ops .opacity-75{opacity:.75}.solid-ops .shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.solid-ops .shadow-lg,.solid-ops .shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.solid-ops .shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.solid-ops .shadow-blue-500\/25{--tw-shadow-color:rgba(59,130,246,.25);--tw-shadow:var(--tw-shadow-colored)}.solid-ops .shadow-slate-900\/10{--tw-shadow-color:rgba(15,23,42,.1);--tw-shadow:var(--tw-shadow-colored)}.solid-ops .ring-1{--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(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.solid-ops .ring-1,.solid-ops .ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.solid-ops .ring-4{--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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.solid-ops .ring-inset{--tw-ring-inset:inset}.solid-ops .ring-amber-500\/20{--tw-ring-color:rgba(245,158,11,.2)}.solid-ops .ring-amber-600\/10{--tw-ring-color:rgba(217,119,6,.1)}.solid-ops .ring-amber-600\/20{--tw-ring-color:rgba(217,119,6,.2)}.solid-ops .ring-blue-100{--tw-ring-opacity:1;--tw-ring-color:rgb(219 234 254/var(--tw-ring-opacity,1))}.solid-ops .ring-blue-50{--tw-ring-opacity:1;--tw-ring-color:rgb(239 246 255/var(--tw-ring-opacity,1))}.solid-ops .ring-blue-500\/10{--tw-ring-color:rgba(59,130,246,.1)}.solid-ops .ring-blue-700\/10{--tw-ring-color:rgba(29,78,216,.1)}.solid-ops .ring-emerald-500\/10{--tw-ring-color:rgba(16,185,129,.1)}.solid-ops .ring-emerald-600\/20{--tw-ring-color:rgba(5,150,105,.2)}.solid-ops .ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity,1))}.solid-ops .ring-gray-500\/10{--tw-ring-color:hsla(220,9%,46%,.1)}.solid-ops .ring-gray-900\/5{--tw-ring-color:rgba(17,24,39,.05)}.solid-ops .ring-indigo-500\/10{--tw-ring-color:rgba(99,102,241,.1)}.solid-ops .ring-indigo-700\/10{--tw-ring-color:rgba(67,56,202,.1)}.solid-ops .ring-orange-600\/20{--tw-ring-color:rgba(234,88,12,.2)}.solid-ops .ring-purple-600\/20{--tw-ring-color:rgba(147,51,234,.2)}.solid-ops .ring-purple-700\/10{--tw-ring-color:rgba(126,34,206,.1)}.solid-ops .ring-red-600\/10{--tw-ring-color:rgba(220,38,38,.1)}.solid-ops .ring-yellow-600\/20{--tw-ring-color:rgba(202,138,4,.2)}.solid-ops .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)}.solid-ops .transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.solid-ops .transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.solid-ops .transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.solid-ops .transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.solid-ops .duration-150{transition-duration:.15s}.solid-ops .animate-fade-in{animation:fadeIn .2s ease-out}@keyframes fadeIn{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.solid-ops .animate-slide-in{animation:slideIn .25s ease-out}@keyframes slideIn{0%{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}.solid-ops{*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-size:.875rem;color:#111827;background-color:#f9fafb;min-height:100vh;h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}canvas,img,svg,video{display:block;vertical-align:middle;max-width:100%}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}[role=button],button{cursor:pointer}button,select{text-transform:none}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:1em}hr{height:0;color:inherit;border-top-width:1px}}.solid-ops ::-webkit-scrollbar{width:6px;height:6px}.solid-ops ::-webkit-scrollbar-track{background:transparent}.solid-ops ::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:3px}.solid-ops ::-webkit-scrollbar-thumb:hover{background:#9ca3af}.solid-ops .hover\:border-blue-200:hover{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.solid-ops .hover\:border-emerald-200:hover{--tw-border-opacity:1;border-color:rgb(167 243 208/var(--tw-border-opacity,1))}.solid-ops .hover\:border-indigo-200:hover{--tw-border-opacity:1;border-color:rgb(199 210 254/var(--tw-border-opacity,1))}.solid-ops .hover\:bg-amber-100:hover{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-blue-100:hover{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-blue-50\/30:hover{background-color:rgba(239,246,255,.3)}.solid-ops .hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-emerald-100:hover{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-gray-50\/80:hover{background-color:rgba(249,250,251,.8)}.solid-ops .hover\:bg-red-100:hover{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.solid-ops .hover\:bg-red-50\/30:hover{background-color:hsla(0,86%,97%,.3)}.solid-ops .hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.solid-ops .hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.solid-ops .hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.solid-ops .hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.solid-ops .hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.solid-ops .hover\:text-purple-700:hover{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity,1))}.solid-ops .hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.solid-ops .hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.solid-ops .hover\:shadow-md:hover,.solid-ops .hover\:shadow-sm:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.solid-ops .hover\:shadow-sm:hover{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.solid-ops .focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.solid-ops .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)}.solid-ops .focus\:ring-blue-500\/20:focus{--tw-ring-color:rgba(59,130,246,.2)}.solid-ops :is(.group:hover .group-hover\:text-blue-500){--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.solid-ops :is(.group:hover .group-hover\:text-blue-600){--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.solid-ops :is(.group:hover .group-hover\:text-emerald-600){--tw-text-opacity:1;color:rgb(5 150 105/var(--tw-text-opacity,1))}.solid-ops :is(.group:hover .group-hover\:text-indigo-600){--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.solid-ops :is(.group:hover .group-hover\:text-purple-500){--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.solid-ops :is(.group:hover .group-hover\:opacity-100){opacity:1}.solid-ops :is(.group:hover .group-hover\:shadow-blue-500\/40){--tw-shadow-color:rgba(59,130,246,.4);--tw-shadow:var(--tw-shadow-colored)}@media (min-width:640px){.solid-ops .sm\:flex{display:flex}.solid-ops .sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.solid-ops .sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.solid-ops .md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.solid-ops .md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1024px){.solid-ops .lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.solid-ops .lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.solid-ops .lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.solid-ops .lg\:px-8{padding-left:2rem;padding-right:2rem}}
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidOps
4
+ class ApplicationController < ActionController::Base
5
+ layout "solid_ops/application"
6
+ helper SolidOps::ApplicationHelper
7
+ helper_method :solid_queue_available?, :solid_cache_available?, :solid_cable_available?,
8
+ :component_diagnostics
9
+
10
+ before_action :authenticate_solid_ops!
11
+
12
+ private
13
+
14
+ def authenticate_solid_ops!
15
+ check = SolidOps.configuration.auth_check
16
+ return unless check.respond_to?(:call)
17
+
18
+ return if check.call(self)
19
+
20
+ head :unauthorized
21
+ end
22
+
23
+ # Server-side pagination helper — returns the paginated scope
24
+ # and sets @current_page, @total_pages, @total_count, @per_page
25
+ def paginate(scope, per_page: 25)
26
+ @per_page = per_page
27
+ @total_count = scope.count
28
+ @total_pages = [(@total_count.to_f / @per_page).ceil, 1].max
29
+ @current_page = params[:page].to_i.clamp(1, @total_pages)
30
+ scope.offset((@current_page - 1) * @per_page).limit(@per_page)
31
+ end
32
+
33
+ def solid_queue_available?
34
+ return false unless defined?(SolidQueue)
35
+
36
+ @@_sq_available = SolidQueue::Job.table_exists? unless defined?(@@_sq_available)
37
+ @@_sq_available
38
+ rescue StandardError
39
+ false
40
+ end
41
+
42
+ def solid_cache_available?
43
+ return false unless defined?(SolidCache)
44
+
45
+ @@_sc_available = SolidCache::Entry.table_exists? unless defined?(@@_sc_available)
46
+ @@_sc_available
47
+ rescue StandardError
48
+ false
49
+ end
50
+
51
+ def solid_cable_available?
52
+ return false unless defined?(SolidCable)
53
+
54
+ @@_scb_available = SolidCable::Message.table_exists? unless defined?(@@_scb_available)
55
+ @@_scb_available
56
+ rescue StandardError
57
+ false
58
+ end
59
+
60
+ def require_solid_queue!
61
+ return if solid_queue_available?
62
+
63
+ render_component_unavailable(
64
+ name: "Solid Queue",
65
+ gem: "solid_queue",
66
+ install_command: "bin/rails solid_queue:install",
67
+ description: "background job processing"
68
+ )
69
+ end
70
+
71
+ def require_solid_cache!
72
+ return if solid_cache_available?
73
+
74
+ render_component_unavailable(
75
+ name: "Solid Cache",
76
+ gem: "solid_cache",
77
+ install_command: "bin/rails solid_cache:install",
78
+ description: "database-backed caching"
79
+ )
80
+ end
81
+
82
+ def require_solid_cable!
83
+ return if solid_cable_available?
84
+
85
+ render_component_unavailable(
86
+ name: "Solid Cable",
87
+ gem: "solid_cable",
88
+ install_command: "bin/rails solid_cable:install",
89
+ description: "database-backed Action Cable"
90
+ )
91
+ end
92
+
93
+ def render_component_unavailable(name:, gem:, install_command:, description:)
94
+ @component_name = name
95
+ @component_gem = gem
96
+ @install_command = install_command
97
+ @component_description = description
98
+ render "solid_ops/shared/component_unavailable", status: :service_unavailable
99
+ end
100
+
101
+ def component_diagnostics
102
+ @component_diagnostics ||= {
103
+ queue: check_component("SolidQueue", "SolidQueue::Job"),
104
+ cache: check_component("SolidCache", "SolidCache::Entry"),
105
+ cable: check_component("SolidCable", "SolidCable::Message")
106
+ }
107
+ end
108
+
109
+ def check_component(mod_name, model_name)
110
+ unless Object.const_defined?(mod_name)
111
+ return { available: false, reason: "Gem not loaded — #{mod_name.underscore} is not in Gemfile or not required" }
112
+ end
113
+
114
+ model = model_name.constantize
115
+ return { available: false, reason: "No database connection for #{model_name}" } unless model.connection
116
+
117
+ db_config = model.connection_db_config
118
+ db_info = "#{db_config.adapter}://#{db_config.database}"
119
+
120
+ return { available: false, reason: "Table '#{model.table_name}' not found in #{db_info} — run db:migrate" } unless model.table_exists?
121
+
122
+ { available: true, reason: "Connected to #{db_info}, table '#{model.table_name}' exists" }
123
+ rescue StandardError => e
124
+ { available: false, reason: "#{e.class}: #{e.message}" }
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidOps
4
+ class CacheEntriesController < ApplicationController
5
+ before_action :require_solid_cache!
6
+
7
+ def index
8
+ @total_entries = SolidCache::Entry.count
9
+ @total_bytes = begin
10
+ SolidCache::Entry.sum(:byte_size)
11
+ rescue ActiveRecord::StatementInvalid
12
+ nil
13
+ end
14
+
15
+ @entries = paginate(SolidCache::Entry.order(created_at: :desc))
16
+ end
17
+
18
+ def show
19
+ @entry = SolidCache::Entry.find(params[:id])
20
+ end
21
+
22
+ def destroy
23
+ entry = SolidCache::Entry.find(params[:id])
24
+ key = entry.key
25
+ entry.destroy
26
+ redirect_to cache_entries_path, notice: "Cache key '#{key}' deleted."
27
+ end
28
+
29
+ def clear_all
30
+ count = SolidCache::Entry.count
31
+ loop do
32
+ deleted = SolidCache::Entry.limit(1_000).delete_all
33
+ break if deleted.zero?
34
+ end
35
+ redirect_to cache_entries_path, notice: "#{count} cache entries cleared."
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidOps
4
+ class ChannelsController < ApplicationController
5
+ before_action :require_solid_cable!
6
+
7
+ def index
8
+ @total_messages = SolidCable::Message.count
9
+ @channels = SolidCable::Message
10
+ .group(:channel)
11
+ .select("channel, COUNT(*) as message_count, MAX(created_at) as last_message_at")
12
+ .order(Arel.sql("COUNT(*) DESC"))
13
+ .limit(100)
14
+ end
15
+
16
+ def show
17
+ @channel = params[:id]
18
+ @messages = SolidCable::Message
19
+ .where(channel: @channel)
20
+ .order(created_at: :desc)
21
+ .limit(100)
22
+ end
23
+
24
+ def trim
25
+ count = SolidCable::Message.where("created_at < ?", 1.hour.ago).count
26
+ SolidCable::Message.where("created_at < ?", 1.hour.ago).delete_all
27
+ redirect_to channels_path, notice: "#{count} old messages trimmed."
28
+ end
29
+ end
30
+ end