├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── controllers │ └── clockwork_web │ │ └── home_controller.rb ├── helpers │ └── clockwork_web │ │ └── home_helper.rb └── views │ └── clockwork_web │ └── home │ └── index.html.erb ├── clockwork_web.gemspec ├── config └── routes.rb ├── lib ├── clockwork_web.rb └── clockwork_web │ ├── engine.rb │ └── version.rb └── test ├── controller_test.rb ├── internal ├── app │ └── assets │ │ └── config │ │ └── manifest.js ├── clock.rb └── config │ └── routes.rb └── test_helper.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: ruby/setup-ruby@v1 9 | with: 10 | ruby-version: 3.4 11 | bundler-cache: true 12 | - run: bundle exec rake test 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | *.log 15 | /test/internal/tmp/ 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 (2025-04-03) 2 | 3 | - Dropped support for Ruby < 3.2 and Rails < 7.1 4 | 5 | ## 0.3.1 (2024-09-04) 6 | 7 | - Improved CSP support 8 | 9 | ## 0.3.0 (2024-06-24) 10 | 11 | - Dropped support for Clockwork < 3 12 | - Dropped support for Ruby < 3.1 and Rails < 6.1 13 | 14 | ## 0.2.0 (2023-02-01) 15 | 16 | - Dropped support for Ruby < 2.7 and Rails < 6 17 | 18 | ## 0.1.2 (2023-02-01) 19 | 20 | - Fixed CSRF vulnerability with Rails < 5.2 - [more info](https://github.com/ankane/clockwork_web/issues/4) 21 | 22 | ## 0.1.1 (2020-03-19) 23 | 24 | - Fixed load error 25 | 26 | ## 0.1.0 (2019-10-28) 27 | 28 | - Added `on_job_update` hook 29 | 30 | ## 0.0.5 (2015-05-13) 31 | 32 | - Added `running_threshold` option 33 | 34 | ## 0.0.4 (2015-03-15) 35 | 36 | - Better monitoring for multiple processes 37 | 38 | ## 0.0.3 (2015-02-14) 39 | 40 | - Added `running?` method 41 | - Added `multiple?` method 42 | - Added `monitor` option 43 | 44 | ## 0.0.2 (2015-02-13) 45 | 46 | - Added `clock_path` option 47 | 48 | ## 0.0.1 (2015-02-13) 49 | 50 | - First release 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest" 7 | gem "combustion" 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2025 Andrew Kane 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Web 2 | 3 | A web interface for [Clockwork](https://github.com/Rykian/clockwork) 4 | 5 | [View the demo](https://clockwork.dokkuapp.com/) 6 | 7 | - see list of jobs 8 | - monitor jobs 9 | - disable jobs 10 | 11 | :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) 12 | 13 | [](https://github.com/ankane/clockwork_web/actions) 14 | 15 | ## Installation 16 | 17 | Add this line to your application’s Gemfile: 18 | 19 | ```ruby 20 | gem "clockwork_web" 21 | ``` 22 | 23 | And add it to your `config/routes.rb`. 24 | 25 | ```ruby 26 | mount ClockworkWeb::Engine, at: "clockwork" 27 | ``` 28 | 29 | Be sure to secure the dashboard in production. 30 | 31 | To monitor and disable jobs, hook up Redis in an initializer. 32 | 33 | ```ruby 34 | ClockworkWeb.redis = Redis.new 35 | ``` 36 | 37 | #### Basic Authentication 38 | 39 | Set the following variables in your environment or an initializer. 40 | 41 | ```ruby 42 | ENV["CLOCKWORK_USERNAME"] = "andrew" 43 | ENV["CLOCKWORK_PASSWORD"] = "secret" 44 | ``` 45 | 46 | #### Devise 47 | 48 | ```ruby 49 | authenticate :user, ->(user) { user.admin? } do 50 | mount ClockworkWeb::Engine, at: "clockwork" 51 | end 52 | ``` 53 | 54 | ## Monitoring 55 | 56 | ```ruby 57 | ClockworkWeb.running? 58 | ClockworkWeb.multiple? 59 | ``` 60 | 61 | ## Customize 62 | 63 | Change clock path 64 | 65 | ```ruby 66 | ClockworkWeb.clock_path = Rails.root.join("clock") # default 67 | ``` 68 | 69 | Turn off monitoring 70 | 71 | ```ruby 72 | ClockworkWeb.monitor = false 73 | ``` 74 | 75 | ## History 76 | 77 | View the [changelog](CHANGELOG.md) 78 | 79 | ## Contributing 80 | 81 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 82 | 83 | - [Report bugs](https://github.com/ankane/clockwork_web/issues) 84 | - Fix bugs and [submit pull requests](https://github.com/ankane/clockwork_web/pulls) 85 | - Write, clarify, or fix documentation 86 | - Suggest or add new features 87 | 88 | To get started with development: 89 | 90 | ```sh 91 | git clone https://github.com/ankane/clockwork_web.git 92 | cd clockwork_web 93 | bundle install 94 | bundle exec rake test 95 | ``` 96 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | task default: :test 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/clockwork_web/home_controller.rb: -------------------------------------------------------------------------------- 1 | module ClockworkWeb 2 | class HomeController < ActionController::Base 3 | layout false 4 | helper ClockworkWeb::HomeHelper 5 | 6 | protect_from_forgery with: :exception 7 | 8 | http_basic_authenticate_with name: ENV["CLOCKWORK_USERNAME"], password: ENV["CLOCKWORK_PASSWORD"] if ENV["CLOCKWORK_PASSWORD"] 9 | 10 | def index 11 | @events = 12 | Clockwork.manager.instance_variable_get(:@events).sort_by do |e| 13 | at = e.instance_variable_get(:@at) 14 | [ 15 | e.instance_variable_get(:@period), 16 | (at && at.instance_variable_get(:@hour)) || -1, 17 | (at && at.instance_variable_get(:@min)) || -1, 18 | e.job.to_s 19 | ] 20 | end 21 | 22 | @last_runs = ClockworkWeb.last_runs 23 | @disabled = ClockworkWeb.disabled_jobs 24 | @last_heartbeat = ClockworkWeb.last_heartbeat 25 | end 26 | 27 | def job 28 | job = params[:job] 29 | enable = params[:enable] == "true" 30 | if enable 31 | ClockworkWeb.enable(job) 32 | else 33 | ClockworkWeb.disable(job) 34 | end 35 | ClockworkWeb.on_job_update.call(job: job, enable: enable, user: try(ClockworkWeb.user_method)) if ClockworkWeb.on_job_update 36 | redirect_to root_path 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/helpers/clockwork_web/home_helper.rb: -------------------------------------------------------------------------------- 1 | module ClockworkWeb 2 | module HomeHelper 3 | def friendly_period(period) 4 | if period % 1.day == 0 5 | pluralize(period / 1.day, "day") 6 | elsif period % 1.hour == 0 7 | pluralize(period / 1.hour, "hour") 8 | elsif period % 1.minute == 0 9 | "#{period / 1.minute} min" 10 | else 11 | "#{period} sec" 12 | end 13 | end 14 | 15 | def last_run(time) 16 | if time 17 | "#{time_ago_in_words(time, include_seconds: true)} ago" 18 | end 19 | end 20 | 21 | def friendly_time_part(time_part) 22 | if time_part 23 | time_part.to_s.rjust(2, "0") 24 | else 25 | "**" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/views/clockwork_web/home/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Multiple clockwork processes detected
62 | <% elsif ClockworkWeb.running? %> 63 |Clockwork is running
64 | <% else %> 65 |66 | Clockwork is not running 67 | <% if @last_heartbeat %> 68 | - last heartbeat was <%= time_ago_in_words(@last_heartbeat) %> ago 69 | <% end %> 70 |
71 | <% end %> 72 | <% end %> 73 | <% else %> 74 |Add Redis for monitoring and disabling jobs
75 | <% end %> 76 | 77 |Job | 81 |Period | 82 |Last Run | 83 |Action | 84 |
---|---|---|---|
<%= event.job %> | 91 |92 | <%= friendly_period(event.instance_variable_get(:@period)) %> 93 | <% at = event.instance_variable_get(:@at) %> 94 | <% if at %> 95 | at <%= friendly_time_part(at.instance_variable_get(:@hour)) %>:<%= friendly_time_part(at.instance_variable_get(:@min)) %> 96 | <% end %> 97 | <% if event.instance_variable_get(:@if) %> 98 | if __ 99 | <% end %> 100 | | 101 |<%= last_run(@last_runs[event.job]) %> | 102 |<%= button_to enabled ? "Disable" : "Enable", home_job_path(job: event.job, enable: !enabled), disabled: !ClockworkWeb.redis %> | 103 |