├── .gitignore ├── Capfile ├── MIT-LICENSE ├── README.markdown ├── Rakefile ├── config.ru ├── config ├── boot.rb ├── config.yml.example ├── database.rb ├── deploy.rb └── evoke.god ├── db └── migrations │ ├── 001_create_callbacks.rb │ ├── 002_create_delayed_jobs.rb │ ├── 003_add_guid_to_callback.rb │ ├── 004_add_method_to_callback.rb │ ├── 005_add_error_message_to_callback.rb │ ├── 006_add_delayed_job_id_to_callback.rb │ └── 007_rename_method_to_httpmethod.rb ├── evoke.rb ├── evoke_consumer.rb ├── models ├── .gitkeep ├── callback.rb ├── callback_runner.rb └── system_status.rb ├── public ├── .DS_Store ├── .gitkeep ├── images │ ├── header-logo.gif │ └── tm-block.gif └── stylesheets │ └── reset-min.css ├── test ├── callback_runner_test.rb ├── callback_test.rb ├── evoke_api_test.rb ├── evoke_status_test.rb ├── model_factory.rb ├── shoulda │ └── rest_client.rb └── test_helper.rb └── views ├── application.haml ├── application.sass ├── not_found.haml └── status.haml /.gitignore: -------------------------------------------------------------------------------- 1 | db/*.db 2 | log/* 3 | config/config.yml -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 2 | Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } 3 | load 'config/deploy' -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Justin Knowlden, Gabriel Gironda, Dan Hodos, Thumble Monks 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Evoke 2 | 3 | Evoke, a service application that provides your own HTTP-based application with a mechanism for storing and executing a delayed HTTP request. Basically, Evoke is triggers and makes a request to a fully-qualified URL at a specific date and time of your choosing. You can think of Evoke as a service for invoking delayed triggers or jobs your application needs to execute at a specific time, but you don't want to write special code for. 4 | 5 | ### For example ... 6 | 7 | Let's say you have an app that needs to send custom reminders to users (like a calendar application). Normally, you might: 8 | 9 | 1. record the date and time the reminder should be sent (easy) 10 | 2. implement the email itself (easy) 11 | 3. write some other utility, daemon, cron-job thinga-ma-bob that looks at your database repeatedly to figure out when to send the reminder (yuck and annoying). 12 | 13 | So, with Evoke, you could do 1 and 2, but then just send a request to Evoke for it to store a URL and a date/time the URL should be called. When invoked at the date/time you set, this URL will theoretically trigger the email you want to send. You don't need to run any other services that you are afraid will crash and you not know about it, which would just lead to headaches and custom service code on your application servers. Thumble Monks and Evoke will take care of it. 14 | 15 | ### How about another example ... 16 | 17 | What? Ok, fine. Let's say you have a system that allows trial memberships. Let's also say these trial memberships expire after 30 days. Well, whenever a new account is created, you could send Evoke a specific URL that could be called in 30 days (to the second) and would expire the trial membership. You wouldn't need to write any special reapers or scrubbers or anything. You would simply have to implement an action that would update the membership when called with the data you told it to be called with. 18 | 19 | If you wanted, you could even tell evoke to call a renewal reminder one year from now. It's crazy what you could do. 20 | 21 | ### Cool things 22 | 23 | * Any HTTP method you want 24 | * Gets 25 | * Updates 26 | * Ruby helper 27 | * GUID 28 | 29 | ### Coming soon ... 30 | 31 | * A JavaScript library for you to use so that you don't need to do anything special or even need a real web-app. Just HTML and JavaScript, baby! 32 | 33 | ## CAVEATS 34 | 35 | **This code is intended for a system the Thumble Monks team is putting together. We're just so nice we are sharing the code for the base service publicly ;)** 36 | 37 | ## Why would someone want to do this? 38 | 39 | Because it's in the vein of supporting a service-oriented architecture. At Thumble Monks, we believe that some things you just don't need to keep implementing. We want to be done with all of this repetitive service stress. Managing delayed triggers or jobs is just one of those stresses. 40 | 41 | Stop re-inventing the services wheel. You wouldn't intentionally write the same code in two classes would you? No. You'd abstract into a super-class or a module or something. 42 | 43 | You wouldn't intentionally write the same class in two code-bases would you? No. You'd abstract the class into a library or plug-in. 44 | 45 | So, why implement the same services between projects. We're talking a level of abstraction that is much higher than a class or a library. We're talking system abstraction and we're talking about doing it for everything in your application. 46 | 47 | ## Why is it called Evoke? 48 | 49 | Because. 50 | 51 | I guess probably because I was looking for words like invoke, trigger, remember, et al and hit upon evoke. Just seemed to make sense. 52 | 53 | ## How does it work? 54 | 55 | Like this ... 56 | 57 | rest-client 58 | delayed_job 59 | 60 | # Using App 61 | 62 | ## Requirements 63 | 64 | * [Sinatra](http://github.com/bmizerany/sinatra/tree/master) 65 | * [Chicago](http://github.com/thumblemonks/chicago/tree/master) 66 | * HAML/SASS 67 | * Ruby JSON 68 | * [Delayed Job](http://github.com/tobi/delayed_job/tree/master) 69 | * Rest Client 70 | * ActiveRecord & Sqlite3-ruby 71 | * Sinatra Authorization (sinatra-authorization): for the status page 72 | 73 | ## Setting up an app 74 | 75 | We're still working this out. 76 | 77 | ### Installation 78 | 79 | $ TBD 80 | 81 | ### Configuration 82 | 83 | TBD 84 | 85 | Includes some stuff for running a God task 86 | 87 | ### Running 88 | 89 | $ TBD 90 | 91 | # Todo 92 | 93 | ## 0.1.0 94 | 95 | * TBD 96 | 97 | ## Backlog 98 | 99 | * Aren't we clever 100 | 101 | # Acknowledgements 102 | 103 | Someone for sure. Probably our wives. 104 | 105 | # Legal 106 | 107 | Copyright © 2008 Justin Knowlden, Gabriel Gironda, Dan Hodos, Thumble Monks, released under the MIT license 108 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/testtask' 4 | 5 | desc 'Default task: run all tests' 6 | task :default => [:test] 7 | 8 | task(:set_test_env) { ENV['APP_ENV'] ||= 'test' } 9 | 10 | task(:environment) { } 11 | 12 | task :test => [:set_test_env] 13 | desc 'Run all tests' 14 | Rake::TestTask.new do |t| 15 | t.test_files = FileList['test/*_test.rb'] 16 | t.verbose = true 17 | end 18 | 19 | desc "Open an irb session preloaded with this library" 20 | task :console do 21 | exec "irb -rubygems -r ./config/boot.rb" 22 | end 23 | 24 | namespace :db do 25 | desc "Migrates the DB" 26 | task :migrate => :environment do 27 | ThumbleMonks::Database.migrate 28 | end 29 | end 30 | 31 | namespace :log do 32 | desc "Clear contents from log files" 33 | task :clear => :environment do 34 | Dir["#{File.dirname(__FILE__)}/log/*.log"].each { |file| File.truncate(file, 0) } 35 | end 36 | end # log 37 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | ENV['APP_ENV'] ||= (ENV['RACK_ENV'] || 'production') 2 | 3 | require 'rubygems' 4 | require 'evoke' 5 | 6 | use Rack::CommonLogger, File.new("#{File.dirname(__FILE__)}/log/#{ENV['APP_ENV']}.log", 'a+') 7 | 8 | use Evoke::Api 9 | use Evoke::Status 10 | run Sinatra::Base 11 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['APP_ENV'] ||= 'development' 2 | def require_local_lib(path) 3 | Dir["#{File.dirname(__FILE__)}/#{path}/*.rb"].each {|f| require f } 4 | end 5 | 6 | require 'yaml' 7 | Configuration = YAML.load_file(File.join(File.dirname(__FILE__), 'config.yml')) 8 | 9 | LIBS = %w[rubygems logger config/database rest_client delayed_job haml sass 10 | sinatra/base chicago sinatra/authorization] 11 | LIBS.each { |lib| require lib } 12 | require_local_lib('../models') 13 | 14 | Sinatra::Base.set :environment => ENV['APP_ENV'].to_sym, 15 | # :root => File.join(File.dirname(__FILE__), '..'), 16 | :raise_errors => true, 17 | :dump_errors => true, 18 | :static => true, 19 | :app_file => File.join(File.dirname(__FILE__), '..', 'evoke.rb'), 20 | :authorization_realm => "Evoke Internals" 21 | -------------------------------------------------------------------------------- /config/config.yml.example: -------------------------------------------------------------------------------- 1 | # htaccess login 2 | authorization: 3 | username: foo 4 | password: bar 5 | 6 | # database configuration 7 | database: 8 | test: 9 | adapter: sqlite3 10 | database: :memory: 11 | development: 12 | adapter: sqlite3 13 | database: db/development.db 14 | production: 15 | adapter: sqlite3 16 | database: db/production.db 17 | -------------------------------------------------------------------------------- /config/database.rb: -------------------------------------------------------------------------------- 1 | require 'activerecord' 2 | gem 'sqlite3-ruby' 3 | 4 | module ThumbleMonks 5 | module Database 6 | def self.fire_me_up(env) 7 | puts "Connecting to #{env} database" 8 | ActiveRecord::Base.configurations = Configuration["database"] 9 | ActiveRecord::Base.logger = Logger.new("log/#{env}.log") 10 | ActiveRecord::Base.establish_connection(env.to_s) 11 | end 12 | 13 | def self.migrate 14 | puts "Migrating the database" 15 | ActiveRecord::Migrator.migrate("#{File.dirname(__FILE__)}/../db/migrations") 16 | end 17 | end # Database 18 | end # ThumbleMonks 19 | 20 | ThumbleMonks::Database.fire_me_up(ENV['APP_ENV']) 21 | ThumbleMonks::Database.migrate 22 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :application, "evoke" 2 | set :scm, :git 3 | set :branch, "master" 4 | set :deploy_via, :remote_cache 5 | set :git_enable_submodules, 1 6 | set :repository, "git://github.com/thumblemonks/evoke.git" 7 | 8 | set :deploy_to, "/var/app/#{application}" 9 | set :user, "deploy" 10 | set :use_sudo, false 11 | set :runner, nil 12 | 13 | role :app, "evoke.thumblemonks.com" 14 | role :web, "evoke.thumblemonks.com" 15 | role :db, "evoke.thumblemonks.com", :primary => true 16 | 17 | namespace :deploy do 18 | desc "Restart Application" 19 | task :restart do 20 | run "touch #{current_path}/tmp/restart.txt" 21 | puts "love." 22 | end 23 | end 24 | 25 | namespace :god do 26 | desc "Terminate the God server to stop the evoke consumer" 27 | task :terminate do 28 | run "god terminate" 29 | puts "love." 30 | end 31 | 32 | desc "Start the God server with the evoke consumer recipe" 33 | task :start do 34 | run "god -c #{current_path}/config/evoke.god" 35 | puts "love." 36 | end 37 | end 38 | 39 | set :cold_deploy, false 40 | before("deploy:cold") { set :cold_deploy, true } 41 | 42 | # 43 | # CONFIG.YML support 44 | 45 | task :after_update_code, :roles => :app, :except => {:no_symlink => true} do 46 | run <<-CMD 47 | cd #{release_path} && 48 | ln -nfs #{shared_path}/config/config.yml #{release_path}/config/config.yml 49 | CMD 50 | end 51 | -------------------------------------------------------------------------------- /config/evoke.god: -------------------------------------------------------------------------------- 1 | God.pid_file_directory = '/var/tmp' 2 | 3 | APP_ENV = ENV['APP_ENV'] || 'production' 4 | NAME = 'evoke_consumer' 5 | SCRIPT = "#{NAME}.rb" 6 | 7 | PATH = ['/var/app/evoke/current', Dir.pwd].detect { |path| File.exists?("#{path}/#{SCRIPT}") } 8 | 9 | unless PATH 10 | $stderr.puts "ERROR Exiting" 11 | $stderr.puts "ERROR Cannot find script #{SCRIPT}" 12 | exit 13 | end 14 | 15 | God.watch do |watcher| 16 | watcher.name = NAME 17 | watcher.interval = 15.seconds 18 | watcher.start = "cd #{PATH} && APP_ENV=#{APP_ENV} ruby #{SCRIPT}" 19 | watcher.restart = "cd #{PATH} && APP_ENV=#{APP_ENV} ruby #{SCRIPT}" 20 | watcher.restart_grace = 5.seconds 21 | watcher.log = "/var/tmp/#{NAME}.log" 22 | 23 | watcher.behavior(:clean_pid_file) 24 | 25 | watcher.start_if do |start| 26 | start.condition(:process_running) do |c| 27 | c.interval = 30.seconds 28 | c.running = false 29 | end 30 | end 31 | 32 | watcher.lifecycle do |on| 33 | on.condition(:flapping) do |c| 34 | c.to_state = [:start, :restart] 35 | c.times = 5 36 | c.within = 5.minutes 37 | c.transition = :unmonitored 38 | c.retry_in = 10.minutes 39 | c.retry_times = 5 40 | c.retry_within = 2.hours 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /db/migrations/001_create_callbacks.rb: -------------------------------------------------------------------------------- 1 | class CreateCallbacks < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :callbacks do |t| 5 | t.text :url, :null => false 6 | t.datetime :callback_at, :null => false 7 | t.text :data 8 | t.boolean :called_back 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :callbacks 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /db/migrations/002_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | 3 | def self.up 4 | create_table :delayed_jobs, :force => true do |table| 5 | table.integer :priority, :default => 0 6 | table.integer :attempts, :default => 0 7 | table.text :handler 8 | table.string :last_error 9 | table.datetime :run_at 10 | table.datetime :locked_at 11 | table.datetime :failed_at 12 | table.string :locked_by 13 | table.timestamps 14 | end 15 | end 16 | 17 | def self.down 18 | drop_table :delayed_jobs 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /db/migrations/003_add_guid_to_callback.rb: -------------------------------------------------------------------------------- 1 | class AddGuidToCallback < ActiveRecord::Migration 2 | 3 | def self.up 4 | change_table :callbacks do |table| 5 | table.string :guid 6 | end 7 | end 8 | 9 | def self.down 10 | change_table :callbacks do |table| 11 | table.remove :guid 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /db/migrations/004_add_method_to_callback.rb: -------------------------------------------------------------------------------- 1 | class AddMethodToCallback < ActiveRecord::Migration 2 | 3 | def self.up 4 | change_table :callbacks do |table| 5 | table.string :method, :limit => 10 6 | end 7 | end 8 | 9 | def self.down 10 | change_table :callbacks do |table| 11 | table.remove :method 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /db/migrations/005_add_error_message_to_callback.rb: -------------------------------------------------------------------------------- 1 | class AddErrorMessageToCallback < ActiveRecord::Migration 2 | 3 | def self.up 4 | change_table :callbacks do |table| 5 | table.text :error_message 6 | end 7 | end 8 | 9 | def self.down 10 | change_table :callbacks do |table| 11 | table.remove :error_message 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /db/migrations/006_add_delayed_job_id_to_callback.rb: -------------------------------------------------------------------------------- 1 | class AddDelayedJobIdToCallback < ActiveRecord::Migration 2 | 3 | def self.up 4 | change_table :callbacks do |table| 5 | table.references :delayed_job 6 | end 7 | end 8 | 9 | def self.down 10 | change_table :callbacks do |table| 11 | table.remove :delayed_job_id 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /db/migrations/007_rename_method_to_httpmethod.rb: -------------------------------------------------------------------------------- 1 | class RenameMethodToHttpmethod < ActiveRecord::Migration 2 | 3 | def self.up 4 | change_table :callbacks do |table| 5 | table.rename :method, :http_method 6 | end 7 | end 8 | 9 | def self.down 10 | change_table :callbacks do |table| 11 | table.rename :http_method, :method 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /evoke.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'config', 'boot') 2 | 3 | module Evoke 4 | class Api < Sinatra::Base 5 | register Sinatra::Chicago 6 | helpers Sinatra::Chicago::Helpers 7 | helpers Sinatra::Chicago::Responders 8 | 9 | error do 10 | $stdout.puts "Sorry there was a nasty error - #{request.env['sinatra.error'].inspect}" 11 | end 12 | 13 | # Resource management 14 | 15 | not_found { throw :halt, [404, json_response('')] } 16 | 17 | def valid_record(record, options={}) 18 | options = {:status => 200, :response => record}.merge(options) 19 | status(options[:status]) 20 | json_response(options[:response]) 21 | end 22 | 23 | def invalid_record(record) 24 | # Need a simple logging facility 25 | # $stdout.puts "ERROR: record #{record.inspect} says #{record.errors.full_messages.inspect}" 26 | throw :halt, [422, json_response(:errors => record.errors.full_messages)] 27 | end 28 | 29 | def manage_resource(resource, options={}) 30 | raise Sinatra::NotFound unless resource 31 | yield(resource) if block_given? 32 | valid_record(resource, options) 33 | rescue ActiveRecord::RecordInvalid => e 34 | invalid_record(e.record) 35 | end 36 | 37 | # Actions 38 | 39 | get "/callbacks/:guid" do 40 | manage_resource(Callback.by_guid(params['guid'])) 41 | end 42 | 43 | post "/callbacks" do 44 | manage_resource(Callback.new(params), :status => 201) do |callback| 45 | callback.save! 46 | CallbackRunner.make_job_from_callback!(callback) 47 | end 48 | end 49 | 50 | put "/callbacks/:guid" do 51 | manage_resource(Callback.by_guid(params['guid'])) do |callback| 52 | attributes = params.reject {|k,v| k == "guid"} 53 | callback.update_attributes!(attributes) 54 | CallbackRunner.replace_job_for_callback!(callback) 55 | end 56 | end 57 | 58 | delete "/callbacks/:guid" do 59 | manage_resource(Callback.by_guid(params['guid']), :response => nil) do |callback| 60 | callback.destroy 61 | end 62 | end 63 | end # Api 64 | 65 | class Status < Sinatra::Base 66 | register Sinatra::Chicago 67 | helpers Sinatra::Chicago::Helpers 68 | helpers Sinatra::Authorization 69 | 70 | error do 71 | $stdout.puts "Sorry there was a nasty error - #{request.env['sinatra.error'].inspect}" 72 | end 73 | 74 | # 75 | # Status and stuff 76 | 77 | catch_all_css 78 | 79 | helpers do 80 | def authorize(username, password) 81 | [username, password] == Configuration["authorization"].values_at("username", "password") 82 | end 83 | 84 | def truncate(str, n) 85 | str.length > n ? "#{str[0..n]}..." : str 86 | end 87 | 88 | def verbal_status_message(callback) 89 | if callback.called_back? 90 | haml '.okay Already evoked callback', :layout => false 91 | elsif callback.should_have_been_called_back? && !callback.called_back? 92 | haml '.uhoh This callback should have been evoked but has not yet', :layout => false 93 | else 94 | haml '.okay Just waiting for callback time', :layout => false 95 | end 96 | end 97 | end 98 | 99 | get "/status" do 100 | login_required 101 | @status = SystemStatus.new 102 | haml :status, :layout => :application 103 | end 104 | end # Status 105 | end # Evoke 106 | -------------------------------------------------------------------------------- /evoke_consumer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['APP_ENV'] ||= ENV['RACK_ENV'] || 'production' 3 | require File.join(File.dirname(__FILE__), 'config', 'boot') 4 | 5 | Delayed::Worker.logger = Logger.new("log/#{ENV['APP_ENV']}-consumer.log") 6 | Delayed::Worker.new.start 7 | -------------------------------------------------------------------------------- /models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thumblemonks/evoke/c252aafff97f819a5e90cfd315493cdcec6a89ef/models/.gitkeep -------------------------------------------------------------------------------- /models/callback.rb: -------------------------------------------------------------------------------- 1 | class Callback < ActiveRecord::Base 2 | belongs_to :delayed_job, :class_name => 'Delayed::Job', :dependent => :destroy 3 | 4 | validates_presence_of :url, :callback_at 5 | validates_uniqueness_of :guid, :allow_nil => true 6 | 7 | before_save :data_cannot_be_nil, :http_method_cannot_be_nil, :guid_cannot_be_blank 8 | 9 | named_scope :recent, :order => 'created_at desc', :limit => 10 10 | named_scope :pending, :conditions => {:called_back => false} 11 | 12 | def self.by_guid(guid) 13 | first(:conditions => {:guid => guid}) 14 | end 15 | 16 | def should_have_been_called_back? 17 | callback_at < Time.now 18 | end 19 | 20 | private 21 | def data_cannot_be_nil 22 | write_attribute(:data, '') if data.nil? 23 | end 24 | 25 | def guid_cannot_be_blank 26 | write_attribute(:guid, nil) if guid && guid.strip == '' 27 | end 28 | 29 | def http_method_cannot_be_nil 30 | write_attribute(:http_method, 'get') if http_method.nil? 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /models/callback_runner.rb: -------------------------------------------------------------------------------- 1 | class CallbackRunner 2 | 3 | def self.make_job_from_callback!(callback) 4 | runner = CallbackRunner.new(callback) 5 | job = Delayed::Job.enqueue(runner, 0, callback.callback_at) 6 | callback.update_attributes!(:delayed_job => job, :called_back => false) 7 | end 8 | 9 | def self.replace_job_for_callback!(callback) 10 | callback.delayed_job.destroy if callback.delayed_job 11 | make_job_from_callback!(callback) 12 | end 13 | 14 | def initialize(callback) 15 | raise(ArgumentError, "Callback cannot be nil") unless callback 16 | @callback = callback 17 | end 18 | 19 | def perform 20 | @callback.reload 21 | http_method = @callback.http_method 22 | request_args = [@callback.url] 23 | request_args << @callback.data if requires_payload?(http_method) 24 | RestClient.send(http_method, *request_args) 25 | @callback.update_attributes!(:called_back => true) 26 | rescue RestClient::Exception => e 27 | @callback.update_attributes!(:error_message => e.message) 28 | end 29 | 30 | private 31 | 32 | def requires_payload?(http_method) 33 | %w[post put].include?(http_method) 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /models/system_status.rb: -------------------------------------------------------------------------------- 1 | require 'drb' 2 | require 'god' 3 | 4 | class SystemStatus 5 | def recent_callbacks; Callback.recent; end 6 | def total_callback_count; Callback.count; end 7 | def jobs_in_queue_count; Delayed::Job.count; end 8 | def pending_callback_count; Callback.pending.count; end 9 | 10 | def consumer_running? 11 | # This is how we ask God what's going on from the API 12 | DRb.start_service("druby://127.0.0.1:0") 13 | server = DRbObject.new(nil, God::Socket.socket(17165)) 14 | 15 | server.status["evoke_consumer"][:state] == :up 16 | rescue DRb::DRbConnError 17 | false 18 | ensure 19 | DRb.stop_service 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thumblemonks/evoke/c252aafff97f819a5e90cfd315493cdcec6a89ef/public/.DS_Store -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thumblemonks/evoke/c252aafff97f819a5e90cfd315493cdcec6a89ef/public/.gitkeep -------------------------------------------------------------------------------- /public/images/header-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thumblemonks/evoke/c252aafff97f819a5e90cfd315493cdcec6a89ef/public/images/header-logo.gif -------------------------------------------------------------------------------- /public/images/tm-block.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thumblemonks/evoke/c252aafff97f819a5e90cfd315493cdcec6a89ef/public/images/tm-block.gif -------------------------------------------------------------------------------- /public/stylesheets/reset-min.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.6.0 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}del,ins{text-decoration:none;} -------------------------------------------------------------------------------- /test/callback_runner_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | class CallbackRunnerTest < Test::Unit::TestCase 4 | def callback_with_runner(attributes={}) 5 | callback = Factory(:callback, attributes) 6 | runner = CallbackRunner.new(callback) 7 | [callback, runner] 8 | end 9 | 10 | should "barf if nil callback given to initializer" do 11 | assert_raise(ArgumentError) { CallbackRunner.new(nil) } 12 | end 13 | 14 | context "making job from callback" do 15 | setup do 16 | @callback = Factory(:callback) 17 | end 18 | 19 | should "enqueue a new delayed job" do 20 | runner = stub('runner') 21 | CallbackRunner.expects(:new).with(@callback).returns(runner) 22 | Delayed::Job.expects(:enqueue).with(runner, 0, @callback.callback_at) 23 | CallbackRunner.make_job_from_callback!(@callback) 24 | end 25 | 26 | should "update callback with the new job" do 27 | job = stub('job') 28 | Delayed::Job.expects(:enqueue).returns(job) 29 | @callback.expects(:update_attributes!).with({:delayed_job => job, :called_back => false}) 30 | CallbackRunner.make_job_from_callback!(@callback) 31 | end 32 | end 33 | 34 | context "replacing job for callback" do 35 | setup do 36 | @callback = Factory(:callback, :called_back => true) 37 | end 38 | 39 | context "when callback has a job" do 40 | setup do 41 | @job = stub('job') 42 | @job.expects(:destroy) 43 | @callback.stubs(:delayed_job).returns(@job) 44 | end 45 | 46 | should "delete old job tied to callback" do 47 | CallbackRunner.replace_job_for_callback!(@callback) 48 | end 49 | end 50 | 51 | should "not delete any jobs when callback has no job" do 52 | Delayed::Job.any_instance.expects(:destroy).never 53 | CallbackRunner.replace_job_for_callback!(@callback) 54 | end 55 | 56 | should "always update callback with a new job" do 57 | @runner = stub('runner') 58 | CallbackRunner.expects(:new).with(@callback).returns(@runner) 59 | job = stub('job') 60 | Delayed::Job.expects(:enqueue).with(@runner, 0, @callback.callback_at).returns(job) 61 | @callback.expects(:update_attributes!).with({:delayed_job => job, :called_back => false}) 62 | CallbackRunner.replace_job_for_callback!(@callback) 63 | end 64 | 65 | should "set called back to false" do 66 | CallbackRunner.replace_job_for_callback!(@callback) 67 | @callback.reload 68 | deny @callback.called_back? 69 | end 70 | end 71 | 72 | context "inducing perform" do 73 | 74 | should "reload callback from database before running anything" do 75 | stub_restful_requests 76 | @callback, @runner = callback_with_runner 77 | @callback.expects(:reload) 78 | @runner.perform 79 | end 80 | 81 | context "with url but no http_method" do 82 | setup { @callback, @runner = callback_with_runner(:data => nil) } 83 | 84 | should "send a get request with no payload" do 85 | expect_restful_request(:get, @callback.url) 86 | @runner.perform 87 | end 88 | 89 | should "note that the url was called back" do 90 | stub_restful_requests 91 | @runner.perform 92 | assert @callback.called_back? 93 | end 94 | end # with url but no http_method 95 | 96 | context "with get" do 97 | context "and no data" do 98 | setup do 99 | @callback, @runner = callback_with_runner(:http_method => 'get', :data => nil) 100 | end 101 | 102 | should "still not provide a payload" do 103 | expect_restful_request(:get, @callback.url) 104 | @runner.perform 105 | end 106 | end 107 | 108 | context "and data" do 109 | setup do 110 | @callback, @runner = callback_with_runner(:http_method => 'get', :data => 'abc') 111 | end 112 | 113 | should "still not inlcude a payload" do 114 | expect_restful_request(:get, @callback.url) 115 | @runner.perform 116 | end 117 | end 118 | end # with get 119 | 120 | context "with put" do 121 | context "and no data" do 122 | setup do 123 | @callback, @runner = callback_with_runner(:http_method => 'put', :data => nil) 124 | end 125 | 126 | should "still pass a payload of empty string" do 127 | expect_restful_request(:put, @callback.url, '') 128 | @runner.perform 129 | end 130 | end 131 | 132 | context "and data" do 133 | setup do 134 | @callback, @runner = callback_with_runner(:http_method => 'put', :data => 'abc') 135 | end 136 | 137 | should "include the data as the payload" do 138 | expect_restful_request(:put, @callback.url, 'abc') 139 | @runner.perform 140 | end 141 | end 142 | end # with put 143 | 144 | context "with post" do 145 | context "and no data" do 146 | setup do 147 | @callback, @runner = callback_with_runner(:http_method => 'post', :data => nil) 148 | end 149 | 150 | should "still pass a payload of empty string" do 151 | expect_restful_request(:post, @callback.url, '') 152 | @runner.perform 153 | end 154 | end 155 | 156 | context "and data" do 157 | setup do 158 | @callback, @runner = callback_with_runner(:http_method => 'post', :data => 'abc') 159 | end 160 | 161 | should "include the data as the payload" do 162 | expect_restful_request(:post, @callback.url, 'abc') 163 | @runner.perform 164 | end 165 | end 166 | end # with post 167 | 168 | context "with delete" do 169 | context "and no data" do 170 | setup do 171 | @callback, @runner = callback_with_runner(:http_method => 'delete', :data => nil) 172 | end 173 | 174 | should "still not provide a payload" do 175 | expect_restful_request(:delete, @callback.url) 176 | @runner.perform 177 | end 178 | end 179 | 180 | context "and data" do 181 | setup do 182 | @callback, @runner = callback_with_runner(:http_method => 'delete', :data => 'abc') 183 | end 184 | 185 | should "still not inlcude a payload" do 186 | expect_restful_request(:delete, @callback.url) 187 | @runner.perform 188 | end 189 | end 190 | end # with delete 191 | 192 | context "which then fails" do 193 | setup do 194 | @callback, @runner = callback_with_runner 195 | expect_restful_request_failure(:get, ::RestClient::ResourceNotFound) 196 | @runner.perform 197 | end 198 | 199 | should "note that the url was not called back" do 200 | deny @callback.called_back? 201 | end 202 | 203 | should "record error message in callback" do 204 | assert_equal "Resource not found", @callback.error_message 205 | end 206 | end # which then fails 207 | end # inducing perform 208 | 209 | end 210 | -------------------------------------------------------------------------------- /test/callback_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | class CallbackTest < Test::Unit::TestCase 4 | should_validate_presence_of :url, :callback_at 5 | should_have_db_column :data 6 | should_have_db_column :called_back 7 | should_have_db_column :http_method 8 | should_have_db_column :error_message 9 | should_belong_to :delayed_job 10 | should_allow_values_for :guid, nil 11 | 12 | context "before save" do 13 | setup do 14 | @callback = Factory(:callback, :data => nil, :guid => ' ') 15 | end 16 | 17 | should "set nil data to empty string" do 18 | assert_equal '', @callback.data 19 | end 20 | 21 | should "turn blank guid into nil" do 22 | assert_nil @callback.guid 23 | end 24 | end 25 | 26 | context "by guid" do 27 | setup do 28 | @callback = Factory(:callback, :guid => 'luscious-jackson') 29 | end 30 | 31 | should_validate_uniqueness_of :guid 32 | 33 | should "return a single callback if guid is exists" do 34 | assert_equal @callback, Callback.by_guid('luscious-jackson') 35 | end 36 | 37 | should "return nil if guid does not exist" do 38 | assert_nil Callback.by_guid('with-my-naked-eye-i-saw') 39 | end 40 | end 41 | 42 | context "should have been called back" do 43 | should "return true if callback at is in the past" do 44 | @callback = Factory(:callback, :callback_at => Time.now - 1.day) 45 | assert @callback.should_have_been_called_back? 46 | end 47 | 48 | should "return false if callback at is in the future" do 49 | @callback = Factory(:callback, :callback_at => Time.now + 1.day) 50 | deny @callback.should_have_been_called_back? 51 | end 52 | end 53 | 54 | context "recent callbacks" do 55 | setup do 56 | @callbacks = (0...20).map {|i| Factory(:callback, :callback_at => i.days.ago)} 57 | end 58 | 59 | should "return most recent 10 callbacks" do 60 | assert_equal @callbacks[0...10], Callback.recent 61 | end 62 | end 63 | 64 | context "pending callbacks" do 65 | setup do 66 | @callbacks = (0...20).map do |i| 67 | Factory(:callback, :callback_at => i.days.ago, :called_back => (i % 2 == 0)) 68 | end 69 | end 70 | 71 | should "return those not called back" do 72 | assert_equal 10, Callback.pending.count 73 | end 74 | end 75 | 76 | context "http_method" do 77 | should "return get if nil" do 78 | assert_equal 'get', Factory(:callback).http_method 79 | end 80 | 81 | should "return whatever it was provided if not nil" do 82 | assert_equal 'bologna', Factory(:callback, :http_method => 'bologna').http_method 83 | end 84 | end 85 | 86 | context "destroying a callback" do 87 | setup do 88 | callback = Factory(:callback) 89 | CallbackRunner.make_job_from_callback!(callback) 90 | callback.reload 91 | @job = callback.delayed_job 92 | callback.destroy 93 | end 94 | 95 | should "delete the job tied to the callback" do 96 | assert_nil Delayed::Job.find_by_id(@job.id) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/evoke_api_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | class EvokeApiTest < Test::Unit::TestCase 4 | def app 5 | @app = Evoke::Api 6 | end 7 | 8 | context "adding a callback" do 9 | context "when missing url" do 10 | setup { post "/callbacks", Factory.attributes_for(:callback, :url => "") } 11 | should_have_response_status 422 12 | should_have_json_response :errors => ["Url can't be blank"] 13 | end 14 | 15 | context "when missing call back at" do 16 | setup { post "/callbacks", Factory.attributes_for(:callback, :callback_at => "") } 17 | should_have_response_status 422 18 | should_have_json_response :errors => ["Callback at can't be blank"] 19 | end 20 | 21 | context "with valid data" do 22 | setup do 23 | @guid = Factory.next(:guid) 24 | CallbackRunner.expects(:make_job_from_callback!).with(anything) 25 | post "/callbacks", Factory.attributes_for(:callback, :guid => @guid) 26 | end 27 | should_have_response_status 201 28 | should_have_json_response { Callback.first } 29 | should_change "Callback.count", :by => 1 30 | end 31 | end 32 | 33 | context "updating a callback" do 34 | setup do 35 | @callback = Factory(:callback_with_job, :guid => 'lulu-rouge') 36 | end 37 | 38 | context "without a guid" do 39 | setup { put "/callbacks/" } 40 | should_have_response_status 404 41 | end 42 | 43 | context "without an existing guid" do 44 | setup { put "/callbacks/mustard-pimp" } 45 | should_have_response_status 404 46 | should_have_json_response "" 47 | end 48 | 49 | context "when no data provided" do 50 | setup do 51 | put "/callbacks/#{@callback.guid}" 52 | @callback.reload 53 | end 54 | should_have_response_status 200 55 | should_have_json_response { @callback } 56 | should_change "Callback.count", :by => 0 57 | end 58 | 59 | context "when only url provided" do 60 | setup do 61 | put "/callbacks/#{@callback.guid}", :url => "http://bar.baz" 62 | @callback.reload 63 | end 64 | should_have_response_status 200 65 | should_have_json_response %r[\"url\":\"http:\\/\\/bar\.baz\"] 66 | should_change "Callback.count", :by => 0 67 | end 68 | 69 | context "when only callback at provided" do 70 | setup do 71 | @callback_at = (Time.now - 86400) 72 | put "/callbacks/#{@callback.guid}", :callback_at => @callback_at 73 | @callback.reload 74 | end 75 | should_have_response_status 200 76 | should_have_json_response do 77 | time = @callback_at.strftime("%Y/%m/%d %H:%M:%S %z") 78 | %r[\"callback_at\":\"#{time}\"] 79 | end 80 | should_change "Callback.count", :by => 0 81 | end 82 | 83 | context "when bad data provided" do 84 | setup { put "/callbacks/#{@callback.guid}", :url => "" } 85 | should_have_response_status 422 86 | should_have_json_response :errors => ["Url can't be blank"] 87 | end 88 | 89 | context "when updating job" do 90 | setup do 91 | CallbackRunner.expects(:replace_job_for_callback!).with(@callback) 92 | end 93 | 94 | should "replace job for same callback" do 95 | put "/callbacks/#{@callback.guid}", :url => "http://bar.baz" 96 | end 97 | end 98 | end 99 | 100 | context "retrieving a callback" do 101 | context "without a guid" do 102 | setup { get '/callbacks' } 103 | should_have_response_status 404 104 | end 105 | 106 | context "without an existing guid" do 107 | setup { get '/callbacks/franken-furter' } 108 | should_have_response_status 404 109 | end 110 | 111 | context "without a blank guid" do 112 | setup do 113 | Factory(:callback) 114 | get '/callbacks/' 115 | end 116 | should_have_response_status 404 117 | end 118 | 119 | context "with a guid" do 120 | setup do 121 | @callback = Factory(:callback, :guid => 'kids-with-guns') 122 | get '/callbacks/kids-with-guns' 123 | end 124 | should_have_response_status 200 125 | should_have_json_response { @callback } 126 | end 127 | end # retrieving a callback 128 | 129 | context "deleting a callback" do 130 | context "without a guid" do 131 | setup { delete '/callbacks' } 132 | should_have_response_status 404 133 | end 134 | 135 | context "without an existing guid" do 136 | setup { delete '/callbacks/franken-furter' } 137 | should_have_response_status 404 138 | end 139 | 140 | context "with a valid guid" do 141 | setup do 142 | @callback = Factory(:callback, :guid => 'kids-with-guns') 143 | Callback.any_instance.expects(:destroy) 144 | delete '/callbacks/kids-with-guns' 145 | end 146 | should_have_response_status 200 147 | should_have_json_response(/^$/) 148 | end 149 | end # deleting a callback 150 | 151 | end 152 | -------------------------------------------------------------------------------- /test/evoke_status_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | class EvokeStatusTest < Test::Unit::TestCase 4 | def app 5 | @app = Evoke::Status 6 | end 7 | 8 | context "displaying status" do 9 | context "when logged in" do 10 | setup do 11 | 10.times { |n| Factory(:callback, :guid => "aphex-analord-#{n}") } 12 | credentials = ["foo:bar"].pack("m*") 13 | get '/status', {}, { "HTTP_AUTHORIZATION" => "Basic #{credentials}" } 14 | end 15 | 16 | should_have_response_body(/(li class='callback')/) 17 | should_have_response_status 200 18 | end 19 | 20 | context "when not logged in" do 21 | setup { get '/status' } 22 | 23 | should_have_response_status 401 24 | end 25 | end # displaying status 26 | 27 | end 28 | -------------------------------------------------------------------------------- /test/model_factory.rb: -------------------------------------------------------------------------------- 1 | require 'factory_girl' 2 | 3 | Factory.define(:delayed_job, :class => Delayed::Job) do |job| 4 | job.payload_object {{:foo => 'bar'}.to_yaml} 5 | job.priority 0 6 | job.run_at Time.now + 3600 7 | end 8 | 9 | Factory.sequence :guid do |n| 10 | "foo-#{n}-bar-#{n}" 11 | end 12 | 13 | Factory.define :callback do |callback| 14 | callback.url 'http://foo.bar/person/1/events/2' 15 | callback.data 'key=12345' 16 | callback.callback_at Time.now + 86400 17 | callback.called_back false 18 | end 19 | 20 | Factory.define :callback_with_job, :class => Callback do |callback| 21 | callback.url 'http://foo.bar/person/3/events/5' 22 | callback.data 'key=lulu' 23 | callback.callback_at Time.now + 3600 24 | callback.called_back false 25 | callback.association :delayed_job 26 | end 27 | -------------------------------------------------------------------------------- /test/shoulda/rest_client.rb: -------------------------------------------------------------------------------- 1 | module ThumbleMonks 2 | module RestClient 3 | module Shoulda 4 | 5 | def restful_methods(except=nil) 6 | [:get, :post, :put, :delete].reject { |m| m == except } 7 | end 8 | 9 | def expect_restful_request(method, *args) 10 | stub_restful_requests( restful_methods(method.to_sym) ) 11 | ::RestClient.expects(method).with(*args) 12 | end 13 | 14 | def expect_restful_request_failure(method, *raises) 15 | stub_restful_requests( restful_methods(method.to_sym) ) 16 | ::RestClient.expects(method).raises(*raises) 17 | end 18 | 19 | def stub_restful_requests(methods=nil) 20 | (methods || restful_methods).each { |method| ::RestClient.stubs(method) } 21 | end 22 | 23 | end # Shoulda 24 | end # RestClient 25 | end # ThumbleMonks 26 | 27 | Test::Unit::TestCase.send(:include, ThumbleMonks::RestClient::Shoulda) 28 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['APP_ENV'] = 'test' 2 | 3 | require 'rubygems' 4 | 5 | require 'test/unit' 6 | require 'rack/test' 7 | require File.join(File.dirname(__FILE__), '..', 'evoke') 8 | 9 | require 'ostruct' 10 | require 'shoulda' 11 | require 'shoulda/active_record' 12 | require 'mocha' 13 | require File.join(File.dirname(__FILE__), 'model_factory') 14 | 15 | require 'chicago/shoulda' 16 | require_local_lib('../test/shoulda') 17 | 18 | class Test::Unit::TestCase 19 | include Rack::Test::Methods 20 | 21 | alias_method :old_run, :run 22 | def run(*args, &block) 23 | exception_thrown = false 24 | ActiveRecord::Base.transaction do 25 | exception_thrown = false 26 | begin 27 | old_run(*args, &block) 28 | rescue Exception => e 29 | exception_thrown = true 30 | raise e 31 | ensure 32 | raise ActiveRecord::Rollback unless exception_thrown 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /views/application.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | !!! 3 | %html 4 | %head 5 | %meta{"http-equiv" => 'X-UA-Compatible', :content => 'GROW UP'} 6 | %meta{:name => 'viewport', :content => 'width=850'} 7 | %title 8 | Evoke - Status 9 | %link{:href => "/images/tm-block.gif", :rel => "icon"} 10 | = stylesheet_include 'reset-min' 11 | = stylesheet_include 'application' 12 | %body 13 | #header 14 | .container 15 | %a.logo{:href => "http://thumblemonks.com"} 16 | %img{:src => "/images/header-logo.gif"} 17 | %ul.nav 18 | %li{:class => 'current'} 19 | %a{:href => '/evoke'} Evoke 20 | .clear 21 | 22 | #content 23 | = yield 24 | .clear 25 | 26 | #footer 27 | .container 28 | %a{:href => "http://creativecommons.org/licenses/by-nc-nd/3.0/"} Copyright © 29 | 2007 - 30 | = Time.now.year 31 | %a{:href => "http://thumblemonks.com/"} Thumble Monks 32 | -------------------------------------------------------------------------------- /views/application.sass: -------------------------------------------------------------------------------- 1 | body 2 | :width 100% 3 | :text-align center 4 | :margin .5ex 0ex 5 | :padding 0em 6 | :background-color white 7 | :font normal 11pt times, serif 8 | 9 | .clear 10 | :clear both 11 | 12 | a, a:visited 13 | :color #845381 14 | 15 | .container 16 | :width 850px 17 | :margin 0 auto 18 | :text-align left 19 | :padding 0 20 | 21 | #header 22 | :background-color #231f20 23 | a.logo 24 | :float left 25 | :padding 1ex 0 .5ex 0 26 | :margin 0 2em 0 0 27 | ul.nav 28 | :display block 29 | li 30 | :float left 31 | :padding 1ex 1ex 1ex 1ex 32 | :margin 1.15em 1ex 0 1ex 33 | a 34 | :color #ddd 35 | :text-decoration none 36 | :text-transform uppercase 37 | :letter-spacing 1px 38 | :font normal 8pt "Lucida Sans Unicode", Arial, sans-serif 39 | &.current 40 | :background-color #fff9d8 41 | a 42 | :color #a483a1 43 | #footer 44 | :margin 2em 0 0 0 45 | :color #ddd 46 | :font normal 9pt "Lucida Sans Unicode", Arial, sans-serif 47 | :background-color #231f20 48 | a, a:visited 49 | :color #a483a1 50 | .container 51 | :padding 2em 0 1ex 0 52 | 53 | #content 54 | :margin 0 0 1em 0 55 | #title 56 | :background-color #fff9d8 57 | :border-bottom 1px solid #ffe8cd 58 | :margin 0 0 1em 0 59 | 60 | h1 61 | :font normal 24pt Georgia, times, serif 62 | :margin 0 0 0 0 63 | :padding 1ex 0 .5ex 0 64 | :color #433f40 65 | 66 | h2 67 | :font normal 16pt Georgia, times, serif 68 | :margin 1em 0 69 | :color #231f20 70 | :padding 0 0 .5ex 0 71 | :border-bottom 1px dotted #ccc 72 | 73 | h3 74 | :font normal 13pt "Lucida Sans Unicode", Arial, sans-serif 75 | :margin 1em 0 76 | :letter-spacing -1px 77 | :color #444 78 | 79 | h4 80 | :font bold 11pt Arial, sans-serif 81 | :margin 1ex 0 82 | :color #555 83 | 84 | #columns 85 | .left 86 | :float left 87 | :width 50% 88 | .sidebar 89 | :float right 90 | :width 40% 91 | 92 | .running 93 | :color green 94 | 95 | li.callback 96 | :border-bottom 1px solid #ccc 97 | :padding 1ex 0 98 | 99 | .okay 100 | :color green 101 | .uhoh 102 | :color red 103 | 104 | .info 105 | :font normal 10pt Arial, sans-serif 106 | :padding 0 0 .5ex 0 107 | span 108 | :padding 0 1em 0 0 109 | .label 110 | :color #888 111 | :font-weight bold 112 | .data 113 | :color #222 114 | .okay, .uhoh 115 | :padding 0 0 1ex 0 116 | -------------------------------------------------------------------------------- /views/not_found.haml: -------------------------------------------------------------------------------- 1 | What were you looking for? Because ... this ain't it. -------------------------------------------------------------------------------- /views/status.haml: -------------------------------------------------------------------------------- 1 | #title 2 | .container 3 | %h1 4 | Consumer: 5 | - if @status.consumer_running? 6 | %span.okay running 7 | - else 8 | %span.uhoh not running 9 | 10 | .container 11 | #columns 12 | .left 13 | %h3 Last 10 Callbacks 14 | %ul 15 | - @status.recent_callbacks.each do |callback| 16 | %li.callback 17 | .info 18 | = verbal_status_message(callback) 19 | .info 20 | %span.label Callback 21 | %span.data 22 | = callback.http_method.upcase 23 | = truncate(callback.url, 50) 24 | .info 25 | %span.label Evoke on 26 | %span.data= callback.callback_at.strftime("%b %d %H:%M:%S") 27 | .info 28 | %span.label Created on 29 | %span.data= callback.created_at.strftime("%b %d %H:%M:%S") 30 | - if callback.guid 31 | .info 32 | %span.label GUID 33 | %span.data= callback.guid 34 | - if callback.delayed_job 35 | .info 36 | %span.label Attempts 37 | %span.data= callback.delayed_job.attempts 38 | - if callback.error_message 39 | .info 40 | %span.label Errors 41 | .data= callback.error_message 42 | .sidebar 43 | %h3 Stats 44 | .info 45 | %span.label Total callbacks 46 | %span.data= @status.total_callback_count 47 | .info 48 | %span.label Jobs in queue 49 | %span.data= @status.jobs_in_queue_count 50 | .info 51 | %span.label Pending callbacks 52 | %span.data= @status.pending_callback_count 53 | --------------------------------------------------------------------------------