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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +308 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/solid_ops/application.css +1 -0
- data/app/controllers/solid_ops/application_controller.rb +127 -0
- data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
- data/app/controllers/solid_ops/channels_controller.rb +30 -0
- data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
- data/app/controllers/solid_ops/events_controller.rb +37 -0
- data/app/controllers/solid_ops/jobs_controller.rb +64 -0
- data/app/controllers/solid_ops/processes_controller.rb +11 -0
- data/app/controllers/solid_ops/queues_controller.rb +75 -0
- data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
- data/app/helpers/solid_ops/application_helper.rb +112 -0
- data/app/jobs/solid_ops/purge_job.rb +16 -0
- data/app/models/solid_ops/event.rb +34 -0
- data/app/views/layouts/solid_ops/application.html.erb +118 -0
- data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
- data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
- data/app/views/solid_ops/channels/index.html.erb +81 -0
- data/app/views/solid_ops/channels/show.html.erb +66 -0
- data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
- data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
- data/app/views/solid_ops/dashboard/index.html.erb +169 -0
- data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
- data/app/views/solid_ops/events/index.html.erb +98 -0
- data/app/views/solid_ops/events/show.html.erb +108 -0
- data/app/views/solid_ops/jobs/failed.html.erb +89 -0
- data/app/views/solid_ops/jobs/running.html.erb +134 -0
- data/app/views/solid_ops/jobs/show.html.erb +116 -0
- data/app/views/solid_ops/processes/index.html.erb +69 -0
- data/app/views/solid_ops/queues/index.html.erb +182 -0
- data/app/views/solid_ops/queues/show.html.erb +121 -0
- data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
- data/app/views/solid_ops/shared/_nav.html.erb +50 -0
- data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
- data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
- data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
- data/config/routes.rb +49 -0
- data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/install_generator.rb +348 -0
- data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
- data/lib/solid_ops/configuration.rb +28 -0
- data/lib/solid_ops/context.rb +34 -0
- data/lib/solid_ops/current.rb +10 -0
- data/lib/solid_ops/engine.rb +60 -0
- data/lib/solid_ops/job_extension.rb +50 -0
- data/lib/solid_ops/middleware.rb +52 -0
- data/lib/solid_ops/subscribers.rb +215 -0
- data/lib/solid_ops/version.rb +5 -0
- data/lib/solid_ops.rb +25 -0
- data/lib/tasks/solid_ops.rake +32 -0
- data/log/test.log +2 -0
- data/sig/solid_ops.rbs +4 -0
- 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
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
+
  
|
|
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 @@
|
|
|
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
|