solid_queue_heroku_autoscaler 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +79 -0
- data/lib/generators/solid_queue_heroku_autoscaler/dashboard_generator.rb +54 -0
- data/lib/generators/solid_queue_heroku_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb +24 -0
- data/lib/generators/solid_queue_heroku_autoscaler/templates/initializer.rb +6 -0
- data/lib/solid_queue_heroku_autoscaler/configuration.rb +24 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/engine.rb +136 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/views/layouts/solid_queue_heroku_autoscaler/dashboard/application.html.erb +206 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/dashboard/index.html.erb +138 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/events/index.html.erb +102 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/index.html.erb +106 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/show.html.erb +209 -0
- data/lib/solid_queue_heroku_autoscaler/dashboard.rb +99 -0
- data/lib/solid_queue_heroku_autoscaler/railtie.rb +31 -1
- data/lib/solid_queue_heroku_autoscaler/scale_event.rb +292 -0
- data/lib/solid_queue_heroku_autoscaler/scaler.rb +67 -0
- data/lib/solid_queue_heroku_autoscaler/version.rb +1 -1
- data/lib/solid_queue_heroku_autoscaler.rb +2 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7b31454d3f2b02f066a5ebf62b70e758a572f5a0ab406b6d090ffdb12e819c8
|
|
4
|
+
data.tar.gz: 90567a30026883b659cfac5b4fe3b1204e330781da31b1dfe4db0960f2336531
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c28ebe76d4529f674d91405bd2307e513ad6de767faa1a34bfc5ae64c600a95d49da1fe0b5dfb05067bfad64a651cc33816576b2d13efc341911332679af1d7
|
|
7
|
+
data.tar.gz: f7a12818f46e209751ede56e5d9cca3b8e36960690c166ef0104c09992de63a767ed03a29c0a9b921d98502c0e252e436476b985afbd12dd626873b09e2d3297
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Dashboard UI**: Web-based dashboard for monitoring autoscaler events and status
|
|
13
|
+
- Overview page with real-time metrics, worker status, and recent events
|
|
14
|
+
- Workers page with detailed status, configuration, and cooldown info
|
|
15
|
+
- Events log with filtering by worker type
|
|
16
|
+
- Manual scale trigger from the UI
|
|
17
|
+
- **ScaleEvent model**: Tracks all scaling decisions in the database
|
|
18
|
+
- **Event recording configuration**: `record_events` and `record_all_events` options
|
|
19
|
+
- **New rake tasks**: `events`, `cleanup_events` for managing scale event history
|
|
20
|
+
- **Dashboard generator**: `rails generate solid_queue_heroku_autoscaler:dashboard`
|
|
21
|
+
|
|
8
22
|
## [0.1.0] - 2025-01-XX
|
|
9
23
|
|
|
10
24
|
### Added
|
data/README.md
CHANGED
|
@@ -41,6 +41,27 @@ rails db:migrate
|
|
|
41
41
|
|
|
42
42
|
This creates a `solid_queue_autoscaler_state` table to store cooldown timestamps.
|
|
43
43
|
|
|
44
|
+
### Dashboard Setup (Optional)
|
|
45
|
+
|
|
46
|
+
For a web UI to monitor autoscaler events and status:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
rails generate solid_queue_heroku_autoscaler:dashboard
|
|
50
|
+
rails db:migrate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then mount the dashboard in `config/routes.rb`:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# With authentication (recommended)
|
|
57
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
58
|
+
mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/autoscaler"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Or without authentication
|
|
62
|
+
mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/autoscaler"
|
|
63
|
+
```
|
|
64
|
+
|
|
44
65
|
## Quick Start
|
|
45
66
|
|
|
46
67
|
### Basic Configuration (Single Worker)
|
|
@@ -417,6 +438,64 @@ end
|
|
|
417
438
|
|
|
418
439
|
In dry-run mode, all decisions are logged but no platform API calls are made.
|
|
419
440
|
|
|
441
|
+
## Dashboard
|
|
442
|
+
|
|
443
|
+
The optional dashboard provides a web UI for monitoring the autoscaler:
|
|
444
|
+
|
|
445
|
+
### Features
|
|
446
|
+
|
|
447
|
+
- **Overview Dashboard**: Real-time metrics, worker status, and recent events
|
|
448
|
+
- **Workers View**: Detailed status for each worker type with configuration and cooldowns
|
|
449
|
+
- **Events Log**: Historical record of all scaling decisions with filtering
|
|
450
|
+
- **Manual Scaling**: Trigger scale operations directly from the UI
|
|
451
|
+
|
|
452
|
+
### Setup
|
|
453
|
+
|
|
454
|
+
1. Generate the dashboard migration:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
rails generate solid_queue_heroku_autoscaler:dashboard
|
|
458
|
+
rails db:migrate
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
2. Mount the engine in `config/routes.rb`:
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
authenticate :user, ->(u) { u.admin? } do
|
|
465
|
+
mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/autoscaler"
|
|
466
|
+
end
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
3. Visit `/autoscaler` in your browser
|
|
470
|
+
|
|
471
|
+
### Event Recording
|
|
472
|
+
|
|
473
|
+
By default, all scaling events are recorded to the database. Configure in your initializer:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
SolidQueueHerokuAutoscaler.configure do |config|
|
|
477
|
+
# Record scale_up, scale_down, skipped, and error events (default: true)
|
|
478
|
+
config.record_events = true
|
|
479
|
+
|
|
480
|
+
# Also record no_change events (verbose, default: false)
|
|
481
|
+
config.record_all_events = false
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Rake Tasks for Events
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
# View recent scale events
|
|
489
|
+
bundle exec rake solid_queue_autoscaler:events
|
|
490
|
+
|
|
491
|
+
# View events for a specific worker
|
|
492
|
+
WORKER=critical_worker bundle exec rake solid_queue_autoscaler:events
|
|
493
|
+
|
|
494
|
+
# Cleanup old events (default: keep 30 days)
|
|
495
|
+
bundle exec rake solid_queue_autoscaler:cleanup_events
|
|
496
|
+
KEEP_DAYS=7 bundle exec rake solid_queue_autoscaler:cleanup_events
|
|
497
|
+
```
|
|
498
|
+
|
|
420
499
|
## Troubleshooting
|
|
421
500
|
|
|
422
501
|
### "Could not acquire advisory lock"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
|
|
6
|
+
module SolidQueueHerokuAutoscaler
|
|
7
|
+
module Generators
|
|
8
|
+
# Generator for the dashboard migrations.
|
|
9
|
+
# Creates the scale events table for tracking autoscaler history.
|
|
10
|
+
#
|
|
11
|
+
# @example Run the generator
|
|
12
|
+
# rails generate solid_queue_heroku_autoscaler:dashboard
|
|
13
|
+
class DashboardGenerator < Rails::Generators::Base
|
|
14
|
+
include ActiveRecord::Generators::Migration
|
|
15
|
+
|
|
16
|
+
source_root File.expand_path('templates', __dir__)
|
|
17
|
+
|
|
18
|
+
desc 'Creates migrations for SolidQueueHerokuAutoscaler dashboard (events table)'
|
|
19
|
+
|
|
20
|
+
def create_migration_file
|
|
21
|
+
migration_template 'create_solid_queue_autoscaler_events.rb.erb',
|
|
22
|
+
'db/migrate/create_solid_queue_autoscaler_events.rb'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def show_post_install
|
|
26
|
+
say ''
|
|
27
|
+
say '=== Solid Queue Autoscaler Dashboard Setup ==='
|
|
28
|
+
say ''
|
|
29
|
+
say 'Next steps:'
|
|
30
|
+
say ' 1. Run migrations: rails db:migrate'
|
|
31
|
+
say ' 2. Mount the dashboard in config/routes.rb:'
|
|
32
|
+
say ''
|
|
33
|
+
say ' mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/autoscaler"'
|
|
34
|
+
say ''
|
|
35
|
+
say ' 3. For authentication, wrap in a constraint:'
|
|
36
|
+
say ''
|
|
37
|
+
say ' authenticate :user, ->(u) { u.admin? } do'
|
|
38
|
+
say ' mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/autoscaler"'
|
|
39
|
+
say ' end'
|
|
40
|
+
say ''
|
|
41
|
+
say 'View the dashboard at: /autoscaler'
|
|
42
|
+
say ''
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def migration_version
|
|
48
|
+
return unless defined?(ActiveRecord::VERSION)
|
|
49
|
+
|
|
50
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateSolidQueueAutoscalerEvents < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :solid_queue_autoscaler_events do |t|
|
|
6
|
+
t.string :worker_name, null: false
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.integer :from_workers, null: false, default: 0
|
|
9
|
+
t.integer :to_workers, null: false, default: 0
|
|
10
|
+
t.text :reason
|
|
11
|
+
t.integer :queue_depth, default: 0
|
|
12
|
+
t.float :latency_seconds, default: 0.0
|
|
13
|
+
t.jsonb :metrics_json
|
|
14
|
+
t.boolean :dry_run, default: false
|
|
15
|
+
|
|
16
|
+
t.datetime :created_at, null: false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :solid_queue_autoscaler_events, :worker_name
|
|
20
|
+
add_index :solid_queue_autoscaler_events, :action
|
|
21
|
+
add_index :solid_queue_autoscaler_events, :created_at
|
|
22
|
+
add_index :solid_queue_autoscaler_events, %i[worker_name created_at]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -49,4 +49,10 @@ SolidQueueHerokuAutoscaler.configure do |config|
|
|
|
49
49
|
|
|
50
50
|
# Optional: Custom logger
|
|
51
51
|
# config.logger = Rails.logger
|
|
52
|
+
|
|
53
|
+
# Dashboard & Event Recording
|
|
54
|
+
# Record scale events to database for dashboard (requires dashboard migration)
|
|
55
|
+
config.record_events = true
|
|
56
|
+
# Also record no_change events (verbose, generates many records)
|
|
57
|
+
# config.record_all_events = false
|
|
52
58
|
end
|
|
@@ -60,6 +60,9 @@ module SolidQueueHerokuAutoscaler
|
|
|
60
60
|
attr_accessor :enabled, :logger
|
|
61
61
|
attr_writer :lock_key
|
|
62
62
|
|
|
63
|
+
# Dashboard/event recording settings
|
|
64
|
+
attr_accessor :record_events, :record_all_events
|
|
65
|
+
|
|
63
66
|
def initialize
|
|
64
67
|
# Configuration name (auto-set when using named configurations)
|
|
65
68
|
@name = :default
|
|
@@ -121,6 +124,10 @@ module SolidQueueHerokuAutoscaler
|
|
|
121
124
|
@kubernetes_namespace = ENV['K8S_NAMESPACE'] || 'default'
|
|
122
125
|
@kubernetes_context = ENV.fetch('K8S_CONTEXT', nil)
|
|
123
126
|
@kubernetes_kubeconfig = ENV.fetch('KUBECONFIG', nil)
|
|
127
|
+
|
|
128
|
+
# Dashboard/event recording settings
|
|
129
|
+
@record_events = true # Record scale events to database
|
|
130
|
+
@record_all_events = false # Also record no_change events (verbose)
|
|
124
131
|
end
|
|
125
132
|
|
|
126
133
|
# Returns the lock key, auto-generating based on name if not explicitly set
|
|
@@ -186,6 +193,23 @@ module SolidQueueHerokuAutoscaler
|
|
|
186
193
|
enabled
|
|
187
194
|
end
|
|
188
195
|
|
|
196
|
+
def record_events?
|
|
197
|
+
record_events && connection_available?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def record_all_events?
|
|
201
|
+
record_all_events && record_events?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def connection_available?
|
|
205
|
+
return true if database_connection
|
|
206
|
+
return false unless defined?(ActiveRecord::Base)
|
|
207
|
+
|
|
208
|
+
ActiveRecord::Base.connected?
|
|
209
|
+
rescue StandardError
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
189
213
|
# Returns the configured adapter instance.
|
|
190
214
|
# Creates a new instance from adapter_class if not set.
|
|
191
215
|
# Defaults to Heroku adapter.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'action_controller/railtie'
|
|
4
|
+
require 'action_view/railtie'
|
|
5
|
+
|
|
6
|
+
module SolidQueueHerokuAutoscaler
|
|
7
|
+
module Dashboard
|
|
8
|
+
# Rails engine that provides the autoscaler dashboard.
|
|
9
|
+
# Mount at /solid_queue_autoscaler or integrate with Mission Control.
|
|
10
|
+
#
|
|
11
|
+
# @example Mount in routes.rb
|
|
12
|
+
# mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/solid_queue_autoscaler"
|
|
13
|
+
#
|
|
14
|
+
# @example With authentication
|
|
15
|
+
# authenticate :user, ->(u) { u.admin? } do
|
|
16
|
+
# mount SolidQueueHerokuAutoscaler::Dashboard::Engine => "/solid_queue_autoscaler"
|
|
17
|
+
# end
|
|
18
|
+
class Engine < ::Rails::Engine
|
|
19
|
+
isolate_namespace SolidQueueHerokuAutoscaler::Dashboard
|
|
20
|
+
|
|
21
|
+
# Engine configuration
|
|
22
|
+
config.solid_queue_autoscaler_dashboard = ActiveSupport::OrderedOptions.new
|
|
23
|
+
config.solid_queue_autoscaler_dashboard.title = 'Solid Queue Autoscaler'
|
|
24
|
+
|
|
25
|
+
# Configure view paths
|
|
26
|
+
config.paths['app/views'] = File.expand_path('views', __dir__)
|
|
27
|
+
|
|
28
|
+
initializer 'solid_queue_autoscaler.dashboard.view_paths' do
|
|
29
|
+
ActiveSupport.on_load(:action_controller) do
|
|
30
|
+
append_view_path File.expand_path('views', __dir__)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
initializer 'solid_queue_autoscaler.dashboard.integration' do
|
|
35
|
+
# Auto-integrate with Mission Control if available
|
|
36
|
+
ActiveSupport.on_load(:mission_control) do
|
|
37
|
+
# Register with Mission Control's tab system if available
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Application controller for dashboard
|
|
43
|
+
class ApplicationController < ActionController::Base
|
|
44
|
+
protect_from_forgery with: :exception
|
|
45
|
+
|
|
46
|
+
layout 'solid_queue_heroku_autoscaler/dashboard/application'
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def autoscaler_status
|
|
51
|
+
@autoscaler_status ||= SolidQueueHerokuAutoscaler::Dashboard.status
|
|
52
|
+
end
|
|
53
|
+
helper_method :autoscaler_status
|
|
54
|
+
|
|
55
|
+
def events_available?
|
|
56
|
+
@events_available ||= SolidQueueHerokuAutoscaler::Dashboard.events_table_available?
|
|
57
|
+
end
|
|
58
|
+
helper_method :events_available?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Main dashboard controller
|
|
62
|
+
class DashboardController < ApplicationController
|
|
63
|
+
def index
|
|
64
|
+
@status = autoscaler_status
|
|
65
|
+
@stats = SolidQueueHerokuAutoscaler::Dashboard.event_stats(since: 24.hours.ago)
|
|
66
|
+
@recent_events = SolidQueueHerokuAutoscaler::Dashboard.recent_events(limit: 10)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Events controller
|
|
71
|
+
class EventsController < ApplicationController
|
|
72
|
+
def index
|
|
73
|
+
@worker_filter = params[:worker]
|
|
74
|
+
@events = SolidQueueHerokuAutoscaler::Dashboard.recent_events(
|
|
75
|
+
limit: params.fetch(:limit, 100).to_i,
|
|
76
|
+
worker_name: @worker_filter
|
|
77
|
+
)
|
|
78
|
+
@stats = SolidQueueHerokuAutoscaler::Dashboard.event_stats(
|
|
79
|
+
since: 24.hours.ago,
|
|
80
|
+
worker_name: @worker_filter
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Workers controller
|
|
86
|
+
class WorkersController < ApplicationController
|
|
87
|
+
def index
|
|
88
|
+
@workers = autoscaler_status
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show
|
|
92
|
+
worker_name = params[:id].to_sym
|
|
93
|
+
@worker = SolidQueueHerokuAutoscaler::Dashboard.worker_status(worker_name)
|
|
94
|
+
@events = SolidQueueHerokuAutoscaler::Dashboard.recent_events(
|
|
95
|
+
limit: 20,
|
|
96
|
+
worker_name: worker_name.to_s
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def scale
|
|
101
|
+
worker_name = params[:id].to_sym
|
|
102
|
+
@result = SolidQueueHerokuAutoscaler.scale!(worker_name)
|
|
103
|
+
redirect_to worker_path(worker_name), notice: scale_notice(@result)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def scale_notice(result)
|
|
109
|
+
if result.success?
|
|
110
|
+
if result.scaled?
|
|
111
|
+
"Scaled from #{result.decision.from} to #{result.decision.to} workers"
|
|
112
|
+
elsif result.skipped?
|
|
113
|
+
"Skipped: #{result.skipped_reason}"
|
|
114
|
+
else
|
|
115
|
+
"No change needed: #{result.decision&.reason}"
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
"Error: #{result.error}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Define routes for the engine
|
|
126
|
+
SolidQueueHerokuAutoscaler::Dashboard::Engine.routes.draw do
|
|
127
|
+
root to: 'dashboard#index'
|
|
128
|
+
|
|
129
|
+
resources :events, only: [:index]
|
|
130
|
+
|
|
131
|
+
resources :workers, only: %i[index show] do
|
|
132
|
+
member do
|
|
133
|
+
post :scale
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Solid Queue Autoscaler</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-primary: #1a1a2e;
|
|
10
|
+
--bg-secondary: #16213e;
|
|
11
|
+
--bg-card: #0f3460;
|
|
12
|
+
--text-primary: #eee;
|
|
13
|
+
--text-secondary: #aaa;
|
|
14
|
+
--accent: #e94560;
|
|
15
|
+
--success: #00d26a;
|
|
16
|
+
--warning: #ffbe0b;
|
|
17
|
+
--info: #00b4d8;
|
|
18
|
+
--border: #333;
|
|
19
|
+
}
|
|
20
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
23
|
+
background: var(--bg-primary);
|
|
24
|
+
color: var(--text-primary);
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
}
|
|
27
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
|
28
|
+
header {
|
|
29
|
+
background: var(--bg-secondary);
|
|
30
|
+
padding: 15px 0;
|
|
31
|
+
border-bottom: 1px solid var(--border);
|
|
32
|
+
margin-bottom: 30px;
|
|
33
|
+
}
|
|
34
|
+
header .container {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
header h1 {
|
|
40
|
+
font-size: 1.5rem;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 10px;
|
|
44
|
+
}
|
|
45
|
+
header h1 span { color: var(--accent); }
|
|
46
|
+
nav { display: flex; gap: 20px; }
|
|
47
|
+
nav a {
|
|
48
|
+
color: var(--text-secondary);
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
padding: 8px 16px;
|
|
51
|
+
border-radius: 6px;
|
|
52
|
+
transition: all 0.2s;
|
|
53
|
+
}
|
|
54
|
+
nav a:hover, nav a.active {
|
|
55
|
+
color: var(--text-primary);
|
|
56
|
+
background: var(--bg-card);
|
|
57
|
+
}
|
|
58
|
+
.flash {
|
|
59
|
+
padding: 12px 20px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
}
|
|
63
|
+
.flash.notice { background: rgba(0, 210, 106, 0.2); border: 1px solid var(--success); }
|
|
64
|
+
.flash.alert { background: rgba(233, 69, 96, 0.2); border: 1px solid var(--accent); }
|
|
65
|
+
.grid { display: grid; gap: 20px; }
|
|
66
|
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
|
67
|
+
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
|
|
68
|
+
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
|
|
69
|
+
.card {
|
|
70
|
+
background: var(--bg-card);
|
|
71
|
+
border-radius: 12px;
|
|
72
|
+
padding: 20px;
|
|
73
|
+
border: 1px solid var(--border);
|
|
74
|
+
}
|
|
75
|
+
.card h2, .card h3 {
|
|
76
|
+
margin-bottom: 15px;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 10px;
|
|
80
|
+
}
|
|
81
|
+
.stat-card {
|
|
82
|
+
text-align: center;
|
|
83
|
+
padding: 25px;
|
|
84
|
+
}
|
|
85
|
+
.stat-card .value {
|
|
86
|
+
font-size: 2.5rem;
|
|
87
|
+
font-weight: bold;
|
|
88
|
+
color: var(--accent);
|
|
89
|
+
}
|
|
90
|
+
.stat-card .label {
|
|
91
|
+
color: var(--text-secondary);
|
|
92
|
+
font-size: 0.9rem;
|
|
93
|
+
margin-top: 5px;
|
|
94
|
+
}
|
|
95
|
+
.stat-card.success .value { color: var(--success); }
|
|
96
|
+
.stat-card.warning .value { color: var(--warning); }
|
|
97
|
+
.stat-card.info .value { color: var(--info); }
|
|
98
|
+
table {
|
|
99
|
+
width: 100%;
|
|
100
|
+
border-collapse: collapse;
|
|
101
|
+
}
|
|
102
|
+
th, td {
|
|
103
|
+
padding: 12px;
|
|
104
|
+
text-align: left;
|
|
105
|
+
border-bottom: 1px solid var(--border);
|
|
106
|
+
}
|
|
107
|
+
th {
|
|
108
|
+
color: var(--text-secondary);
|
|
109
|
+
font-weight: 500;
|
|
110
|
+
font-size: 0.85rem;
|
|
111
|
+
text-transform: uppercase;
|
|
112
|
+
}
|
|
113
|
+
tr:hover { background: rgba(255,255,255,0.02); }
|
|
114
|
+
.badge {
|
|
115
|
+
display: inline-block;
|
|
116
|
+
padding: 4px 10px;
|
|
117
|
+
border-radius: 20px;
|
|
118
|
+
font-size: 0.8rem;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
}
|
|
121
|
+
.badge-success { background: rgba(0,210,106,0.2); color: var(--success); }
|
|
122
|
+
.badge-warning { background: rgba(255,190,11,0.2); color: var(--warning); }
|
|
123
|
+
.badge-danger { background: rgba(233,69,96,0.2); color: var(--accent); }
|
|
124
|
+
.badge-info { background: rgba(0,180,216,0.2); color: var(--info); }
|
|
125
|
+
.badge-neutral { background: rgba(170,170,170,0.2); color: var(--text-secondary); }
|
|
126
|
+
.btn {
|
|
127
|
+
display: inline-block;
|
|
128
|
+
padding: 10px 20px;
|
|
129
|
+
border-radius: 6px;
|
|
130
|
+
border: none;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
font-size: 0.9rem;
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
transition: all 0.2s;
|
|
135
|
+
}
|
|
136
|
+
.btn-primary {
|
|
137
|
+
background: var(--accent);
|
|
138
|
+
color: white;
|
|
139
|
+
}
|
|
140
|
+
.btn-primary:hover { background: #d63654; }
|
|
141
|
+
.btn-secondary {
|
|
142
|
+
background: var(--bg-secondary);
|
|
143
|
+
color: var(--text-primary);
|
|
144
|
+
border: 1px solid var(--border);
|
|
145
|
+
}
|
|
146
|
+
.btn-secondary:hover { background: var(--bg-card); }
|
|
147
|
+
.btn-sm { padding: 6px 12px; font-size: 0.8rem; }
|
|
148
|
+
.progress-bar {
|
|
149
|
+
height: 8px;
|
|
150
|
+
background: var(--bg-secondary);
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
margin-top: 8px;
|
|
154
|
+
}
|
|
155
|
+
.progress-bar .fill {
|
|
156
|
+
height: 100%;
|
|
157
|
+
border-radius: 4px;
|
|
158
|
+
transition: width 0.3s;
|
|
159
|
+
}
|
|
160
|
+
.text-muted { color: var(--text-secondary); }
|
|
161
|
+
.text-success { color: var(--success); }
|
|
162
|
+
.text-warning { color: var(--warning); }
|
|
163
|
+
.text-danger { color: var(--accent); }
|
|
164
|
+
.text-info { color: var(--info); }
|
|
165
|
+
.mb-1 { margin-bottom: 0.5rem; }
|
|
166
|
+
.mb-2 { margin-bottom: 1rem; }
|
|
167
|
+
.mb-3 { margin-bottom: 1.5rem; }
|
|
168
|
+
.mt-2 { margin-top: 1rem; }
|
|
169
|
+
.d-flex { display: flex; }
|
|
170
|
+
.justify-between { justify-content: space-between; }
|
|
171
|
+
.align-center { align-items: center; }
|
|
172
|
+
.gap-2 { gap: 1rem; }
|
|
173
|
+
.time-ago { font-size: 0.85rem; color: var(--text-secondary); }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<header>
|
|
178
|
+
<div class="container">
|
|
179
|
+
<h1>
|
|
180
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
181
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
182
|
+
<path d="M2 17l10 5 10-5"/>
|
|
183
|
+
<path d="M2 12l10 5 10-5"/>
|
|
184
|
+
</svg>
|
|
185
|
+
<span>Solid Queue</span> Autoscaler
|
|
186
|
+
</h1>
|
|
187
|
+
<nav>
|
|
188
|
+
<%= link_to 'Dashboard', main_app.respond_to?(:solid_queue_autoscaler_path) ? main_app.solid_queue_autoscaler_path : root_path, class: request.path == root_path ? 'active' : '' %>
|
|
189
|
+
<%= link_to 'Workers', workers_path, class: request.path.include?('/workers') ? 'active' : '' %>
|
|
190
|
+
<%= link_to 'Events', events_path, class: request.path.include?('/events') ? 'active' : '' %>
|
|
191
|
+
</nav>
|
|
192
|
+
</div>
|
|
193
|
+
</header>
|
|
194
|
+
|
|
195
|
+
<main class="container">
|
|
196
|
+
<% if notice %>
|
|
197
|
+
<div class="flash notice"><%= notice %></div>
|
|
198
|
+
<% end %>
|
|
199
|
+
<% if alert %>
|
|
200
|
+
<div class="flash alert"><%= alert %></div>
|
|
201
|
+
<% end %>
|
|
202
|
+
|
|
203
|
+
<%= yield %>
|
|
204
|
+
</main>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|