@@ -40,3 +40,6 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com |
||
| 40 | 40 |
# This invitation code will be required for users to signup with your Huginn installation. |
| 41 | 41 |
# You can see its use in user.rb. |
| 42 | 42 |
INVITATION_CODE=try-huginn |
| 43 |
+ |
|
| 44 |
+# Number of lines of log messages to keep per Agent |
|
| 45 |
+AGENT_LOG_LENGTH=100 |
@@ -120,7 +120,7 @@ GEM |
||
| 120 | 120 |
method_source (0.8.1) |
| 121 | 121 |
mime-types (1.23) |
| 122 | 122 |
mini_portile (0.5.1) |
| 123 |
- multi_json (1.7.8) |
|
| 123 |
+ multi_json (1.7.9) |
|
| 124 | 124 |
multi_xml (0.5.5) |
| 125 | 125 |
multipart-post (1.2.0) |
| 126 | 126 |
mysql2 (0.3.13) |
@@ -166,15 +166,15 @@ GEM |
||
| 166 | 166 |
json (~> 1.4) |
| 167 | 167 |
rest-client (1.6.7) |
| 168 | 168 |
mime-types (>= 1.16) |
| 169 |
- rr (1.1.1) |
|
| 169 |
+ rr (1.1.2) |
|
| 170 | 170 |
rspec (2.14.1) |
| 171 | 171 |
rspec-core (~> 2.14.0) |
| 172 | 172 |
rspec-expectations (~> 2.14.0) |
| 173 | 173 |
rspec-mocks (~> 2.14.0) |
| 174 |
- rspec-core (2.14.3) |
|
| 175 |
- rspec-expectations (2.14.0) |
|
| 174 |
+ rspec-core (2.14.5) |
|
| 175 |
+ rspec-expectations (2.14.2) |
|
| 176 | 176 |
diff-lcs (>= 1.1.3, < 2.0) |
| 177 |
- rspec-mocks (2.14.1) |
|
| 177 |
+ rspec-mocks (2.14.3) |
|
| 178 | 178 |
rspec-rails (2.14.0) |
| 179 | 179 |
actionpack (>= 3.0) |
| 180 | 180 |
activesupport (>= 3.0) |
@@ -8,10 +8,6 @@ |
||
| 8 | 8 |
#= require ./worker-checker |
| 9 | 9 |
#= require_self |
| 10 | 10 |
|
| 11 |
-# Place all the behaviors and hooks related to the matching controller here. |
|
| 12 |
-# All this logic will automatically be available in application.js. |
|
| 13 |
-# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ |
|
| 14 |
- |
|
| 15 | 11 |
setupJsonEditor = -> |
| 16 | 12 |
JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
| 17 | 13 |
JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
@@ -48,17 +44,74 @@ showEventDescriptions = -> |
||
| 48 | 44 |
$(".event-descriptions").html("").hide()
|
| 49 | 45 |
|
| 50 | 46 |
$(document).ready -> |
| 47 |
+ # JSON Editor |
|
| 51 | 48 |
setupJsonEditor() |
| 52 |
- $(".select2").select2(width: 'resolve')
|
|
| 53 | 49 |
|
| 54 |
- if $(".top-flash").length
|
|
| 55 |
- setTimeout((-> $(".top-flash").slideUp(-> $(".top-flash").remove())), 5000)
|
|
| 50 |
+ # Select2 Selects |
|
| 51 |
+ $(".select2").select2(width: 'resolve')
|
|
| 56 | 52 |
|
| 53 |
+ # Flash |
|
| 54 |
+ if $(".flash").length
|
|
| 55 |
+ setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
|
|
| 56 |
+ |
|
| 57 |
+ # Agent Navigation |
|
| 58 |
+ $agentNavigate = $('#agent-navigate')
|
|
| 59 |
+ $agentNavigate.typeahead( |
|
| 60 |
+ minLength: 0, |
|
| 61 |
+ items: 15, |
|
| 62 |
+ source: agentNames |
|
| 63 |
+ ).on("change", (e) ->
|
|
| 64 |
+ if agentPaths[$agentNavigate.val()] |
|
| 65 |
+ $('#agent-navigate').closest(".navbar-search").find(".spinner").show();
|
|
| 66 |
+ window.location = agentPaths[$agentNavigate.val()] |
|
| 67 |
+ ).on("focus", (e) ->
|
|
| 68 |
+ $agentNavigate.val '' |
|
| 69 |
+ ).on("blur", (e) ->
|
|
| 70 |
+ $agentNavigate.val '' |
|
| 71 |
+ ) |
|
| 72 |
+ |
|
| 73 |
+ # Pressing '/' selects the search box. |
|
| 74 |
+ $("body").on "keypress", (e) ->
|
|
| 75 |
+ if e.keyCode == 47 # The '/' key |
|
| 76 |
+ if e.target.nodeName == "BODY" |
|
| 77 |
+ e.preventDefault() |
|
| 78 |
+ $agentNavigate.focus() |
|
| 79 |
+ |
|
| 80 |
+# Agent Show |
|
| 81 |
+ fetchLogs = (e) -> |
|
| 82 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 83 |
+ e.preventDefault() |
|
| 84 |
+ $("#logs .spinner").show()
|
|
| 85 |
+ $("#logs .refresh, #logs .clear").hide()
|
|
| 86 |
+ $.get "/agents/#{agentId}/logs", (html) =>
|
|
| 87 |
+ $("#logs .logs").html html
|
|
| 88 |
+ $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 89 |
+ $("#logs .refresh, #logs .clear").show()
|
|
| 90 |
+ |
|
| 91 |
+ clearLogs = (e) -> |
|
| 92 |
+ if confirm("Are you sure you want to clear all logs for this Agent?")
|
|
| 93 |
+ agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
|
|
| 94 |
+ e.preventDefault() |
|
| 95 |
+ $("#logs .spinner").show()
|
|
| 96 |
+ $("#logs .refresh, #logs .clear").hide()
|
|
| 97 |
+ $.post "/agents/#{agentId}/logs/clear", { "_method": "DELETE" }, (html) =>
|
|
| 98 |
+ $("#logs .logs").html html
|
|
| 99 |
+ $("#logs .spinner").stop(true, true).fadeOut ->
|
|
| 100 |
+ $("#logs .refresh, #logs .clear").show()
|
|
| 101 |
+ |
|
| 102 |
+ $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", fetchLogs
|
|
| 103 |
+ $(".agent-show #logs .clear").on "click", clearLogs
|
|
| 104 |
+ |
|
| 105 |
+ if tab = window.location.href.match(/tab=(\w+)\b/i)?[1] |
|
| 106 |
+ if tab in ["details", "logs"] |
|
| 107 |
+ $(".agent-show .nav-tabs li a[href='##{tab}']").click()
|
|
| 108 |
+ |
|
| 109 |
+ # Editing Agents |
|
| 57 | 110 |
$("#agent_source_ids").on "change", showEventDescriptions
|
| 58 | 111 |
|
| 59 | 112 |
$("#agent_type").on "change", ->
|
| 60 | 113 |
if window.jsonEditor? |
| 61 |
- $(".spinner").fadeIn();
|
|
| 114 |
+ $("#agent-spinner").fadeIn();
|
|
| 62 | 115 |
$("#agent_source_ids").select2("val", {});
|
| 63 | 116 |
$(".event-descriptions").html("").hide()
|
| 64 | 117 |
$.getJSON "/agents/type_details", { type: $(@).val() }, (json) =>
|
@@ -77,7 +130,7 @@ $(document).ready -> |
||
| 77 | 130 |
window.jsonEditor.json = json.options |
| 78 | 131 |
window.jsonEditor.rebuild() |
| 79 | 132 |
|
| 80 |
- $(".spinner").stop(true, true).fadeOut();
|
|
| 133 |
+ $("#agent-spinner").stop(true, true).fadeOut();
|
|
| 81 | 134 |
|
| 82 | 135 |
$("#agent_type").change() if $("#agent_type").length
|
| 83 | 136 |
|
@@ -1,7 +1,11 @@ |
||
| 1 | 1 |
$ -> |
| 2 |
+ firstEventCount = null |
|
| 3 |
+ |
|
| 2 | 4 |
if $("#job-indicator").length
|
| 3 | 5 |
check = -> |
| 4 | 6 |
$.getJSON "/worker_status", (json) -> |
| 7 |
+ firstEventCount = json.event_count unless firstEventCount? |
|
| 8 |
+ |
|
| 5 | 9 |
if json.pending? && json.pending > 0 |
| 6 | 10 |
tooltipOptions = {
|
| 7 | 11 |
title: "#{json.pending} pending, #{json.awaiting_retry} awaiting retry, and #{json.recent_failures} recent failures"
|
@@ -12,5 +16,20 @@ $ -> |
||
| 12 | 16 |
$("#job-indicator").tooltip('destroy').tooltip(tooltipOptions).fadeIn().find(".number").text(json.pending)
|
| 13 | 17 |
else |
| 14 | 18 |
$("#job-indicator:visible").tooltip('destroy').fadeOut()
|
| 19 |
+ |
|
| 20 |
+ if firstEventCount? && json.event_count > firstEventCount |
|
| 21 |
+ $("#event-indicator").tooltip('destroy').
|
|
| 22 |
+ tooltip(title: "Click to reload", delay: 0, placement: "bottom", trigger: "hover"). |
|
| 23 |
+ fadeIn(). |
|
| 24 |
+ find(".number").
|
|
| 25 |
+ text(json.event_count - firstEventCount) |
|
| 26 |
+ else |
|
| 27 |
+ $("#event-indicator").tooltip('destroy').fadeOut()
|
|
| 28 |
+ |
|
| 15 | 29 |
window.workerCheckTimeout = setTimeout check, 2000 |
| 30 |
+ |
|
| 16 | 31 |
check() |
| 32 |
+ |
|
| 33 |
+ $("#event-indicator a").on "click", (e) ->
|
|
| 34 |
+ e.preventDefault() |
|
| 35 |
+ window.location.reload() |
@@ -51,10 +51,6 @@ table.events {
|
||
| 51 | 51 |
margin-left: 0 !important; |
| 52 | 52 |
} |
| 53 | 53 |
|
| 54 |
-#job-indicator {
|
|
| 55 |
- display: none; |
|
| 56 |
-} |
|
| 57 |
- |
|
| 58 | 54 |
img.odin {
|
| 59 | 55 |
position: relative; |
| 60 | 56 |
top: -32px; |
@@ -85,3 +81,44 @@ img.spinner {
|
||
| 85 | 81 |
.show-view {
|
| 86 | 82 |
overflow: hidden; |
| 87 | 83 |
} |
| 84 |
+ |
|
| 85 |
+span.not-applicable:after {
|
|
| 86 |
+ color: #bbbbbb; |
|
| 87 |
+ content: "n/a"; |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+// Navbar |
|
| 91 |
+ |
|
| 92 |
+#job-indicator, #event-indicator {
|
|
| 93 |
+ display: none; |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 96 |
+.navbar-search > .spinner {
|
|
| 97 |
+ position: absolute; |
|
| 98 |
+ top: -1px; |
|
| 99 |
+ right: 1px; |
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+// Flash |
|
| 103 |
+ |
|
| 104 |
+.flash {
|
|
| 105 |
+ position: fixed; |
|
| 106 |
+ width: 210px; |
|
| 107 |
+ z-index: 99999; |
|
| 108 |
+ right: 20px; |
|
| 109 |
+ |
|
| 110 |
+ .alert {
|
|
| 111 |
+ } |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+// Logs |
|
| 115 |
+ |
|
| 116 |
+#logs .action-icon {
|
|
| 117 |
+ height: 16px; |
|
| 118 |
+ display: inline-block; |
|
| 119 |
+ vertical-align: inherit; |
|
| 120 |
+ |
|
| 121 |
+ &.refresh {
|
|
| 122 |
+ margin: 0 10px; |
|
| 123 |
+ } |
|
| 124 |
+} |
@@ -9,8 +9,13 @@ class AgentsController < ApplicationController |
||
| 9 | 9 |
end |
| 10 | 10 |
|
| 11 | 11 |
def run |
| 12 |
- Agent.async_check(current_user.agents.find(params[:id]).id) |
|
| 13 |
- redirect_to agents_path, notice: "Agent run queued" |
|
| 12 |
+ agent = current_user.agents.find(params[:id]) |
|
| 13 |
+ Agent.async_check(agent.id) |
|
| 14 |
+ if params[:return] == "show" |
|
| 15 |
+ redirect_to agent_path(agent), notice: "Agent run queued" |
|
| 16 |
+ else |
|
| 17 |
+ redirect_to agents_path, notice: "Agent run queued" |
|
| 18 |
+ end |
|
| 14 | 19 |
end |
| 15 | 20 |
|
| 16 | 21 |
def type_details |
@@ -0,0 +1,19 @@ |
||
| 1 |
+class LogsController < ApplicationController |
|
| 2 |
+ before_filter :load_agent |
|
| 3 |
+ |
|
| 4 |
+ def index |
|
| 5 |
+ @logs = @agent.logs.all |
|
| 6 |
+ render :action => :index, :layout => false |
|
| 7 |
+ end |
|
| 8 |
+ |
|
| 9 |
+ def clear |
|
| 10 |
+ @agent.logs.delete_all |
|
| 11 |
+ index |
|
| 12 |
+ end |
|
| 13 |
+ |
|
| 14 |
+ protected |
|
| 15 |
+ |
|
| 16 |
+ def load_agent |
|
| 17 |
+ @agent = current_user.agents.find(params[:agent_id]) |
|
| 18 |
+ end |
|
| 19 |
+end |
@@ -1,12 +1,11 @@ |
||
| 1 | 1 |
class WorkerStatusController < ApplicationController |
| 2 |
- skip_before_filter :authenticate_user! |
|
| 3 |
- |
|
| 4 | 2 |
def show |
| 5 | 3 |
start = Time.now.to_f |
| 6 | 4 |
render :json => {
|
| 7 | 5 |
:pending => Delayed::Job.where("run_at <= ? AND locked_at IS NULL AND attempts = 0", Time.now).count,
|
| 8 | 6 |
:awaiting_retry => Delayed::Job.where("failed_at IS NULL AND attempts > 0").count,
|
| 9 | 7 |
:recent_failures => Delayed::Job.where("failed_at IS NOT NULL AND failed_at > ?", 5.days.ago).count,
|
| 8 |
+ :event_count => current_user.events.count, |
|
| 10 | 9 |
:compute_time => Time.now.to_f - start |
| 11 | 10 |
} |
| 12 | 11 |
end |
@@ -11,7 +11,7 @@ module ApplicationHelper |
||
| 11 | 11 |
if agent.working? |
| 12 | 12 |
'<span class="label label-success">Yes</span>'.html_safe |
| 13 | 13 |
else |
| 14 |
- '<span class="label label-warning">No</span>'.html_safe |
|
| 14 |
+ link_to '<span class="label label-warning">No</span>'.html_safe, agent_path(agent, :tab => (agent.recent_error_logs? ? 'logs' : 'details')) |
|
| 15 | 15 |
end |
| 16 | 16 |
end |
| 17 | 17 |
end |
@@ -0,0 +1,2 @@ |
||
| 1 |
+module LogsHelper |
|
| 2 |
+end |
@@ -30,6 +30,9 @@ class Agent < ActiveRecord::Base |
||
| 30 | 30 |
|
| 31 | 31 |
belongs_to :user, :inverse_of => :agents |
| 32 | 32 |
has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" |
| 33 |
+ has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" |
|
| 34 |
+ has_many :logs, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" |
|
| 35 |
+ has_one :most_recent_log, :inverse_of => :agent, :class_name => "AgentLog", :order => "agent_logs.id desc" |
|
| 33 | 36 |
has_many :received_events, :through => :sources, :class_name => "Event", :source => :events, :order => "events.id desc" |
| 34 | 37 |
has_many :links_as_source, :dependent => :delete_all, :foreign_key => "source_id", :class_name => "Link", :inverse_of => :source |
| 35 | 38 |
has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver |
@@ -71,9 +74,13 @@ class Agent < ActiveRecord::Base |
||
| 71 | 74 |
raise "Implement me in your subclass" |
| 72 | 75 |
end |
| 73 | 76 |
|
| 74 |
- def event_created_within(seconds) |
|
| 75 |
- last_event = events.first |
|
| 76 |
- last_event && last_event.created_at > seconds.ago && last_event |
|
| 77 |
+ def event_created_within(days) |
|
| 78 |
+ event = most_recent_event |
|
| 79 |
+ event && event.created_at > days.to_i.days.ago && event.payload.present? && event |
|
| 80 |
+ end |
|
| 81 |
+ |
|
| 82 |
+ def recent_error_logs? |
|
| 83 |
+ most_recent_log.try(:level) == 4 |
|
| 77 | 84 |
end |
| 78 | 85 |
|
| 79 | 86 |
def sources_are_owned |
@@ -81,7 +88,11 @@ class Agent < ActiveRecord::Base |
||
| 81 | 88 |
end |
| 82 | 89 |
|
| 83 | 90 |
def create_event(attrs) |
| 84 |
- events.create!({ :user => user }.merge(attrs))
|
|
| 91 |
+ if can_create_events? |
|
| 92 |
+ events.create!({ :user => user }.merge(attrs))
|
|
| 93 |
+ else |
|
| 94 |
+ error "This Agent cannot create events!" |
|
| 95 |
+ end |
|
| 85 | 96 |
end |
| 86 | 97 |
|
| 87 | 98 |
def validate_schedule |
@@ -110,7 +121,7 @@ class Agent < ActiveRecord::Base |
||
| 110 | 121 |
end |
| 111 | 122 |
|
| 112 | 123 |
def last_event_at |
| 113 |
- @memoized_last_event_at ||= events.select(:created_at).first.try(:created_at) |
|
| 124 |
+ @memoized_last_event_at ||= most_recent_event.try(:created_at) |
|
| 114 | 125 |
end |
| 115 | 126 |
|
| 116 | 127 |
def default_schedule |
@@ -133,12 +144,28 @@ class Agent < ActiveRecord::Base |
||
| 133 | 144 |
!cannot_receive_events? |
| 134 | 145 |
end |
| 135 | 146 |
|
| 147 |
+ def cannot_create_events? |
|
| 148 |
+ self.class.cannot_create_events? |
|
| 149 |
+ end |
|
| 150 |
+ |
|
| 151 |
+ def can_create_events? |
|
| 152 |
+ !cannot_create_events? |
|
| 153 |
+ end |
|
| 154 |
+ |
|
| 136 | 155 |
def set_last_checked_event_id |
| 137 | 156 |
if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first
|
| 138 | 157 |
self.last_checked_event_id = newest_event_id |
| 139 | 158 |
end |
| 140 | 159 |
end |
| 141 | 160 |
|
| 161 |
+ def log(message, options = {})
|
|
| 162 |
+ AgentLog.log_for_agent(self, message, options) |
|
| 163 |
+ end |
|
| 164 |
+ |
|
| 165 |
+ def error(message, options = {})
|
|
| 166 |
+ log(message, options.merge(:level => 4)) |
|
| 167 |
+ end |
|
| 168 |
+ |
|
| 142 | 169 |
# Class Methods |
| 143 | 170 |
class << self |
| 144 | 171 |
def cannot_be_scheduled! |
@@ -154,6 +181,14 @@ class Agent < ActiveRecord::Base |
||
| 154 | 181 |
@default_schedule |
| 155 | 182 |
end |
| 156 | 183 |
|
| 184 |
+ def cannot_create_events! |
|
| 185 |
+ @cannot_create_events = true |
|
| 186 |
+ end |
|
| 187 |
+ |
|
| 188 |
+ def cannot_create_events? |
|
| 189 |
+ !!@cannot_create_events |
|
| 190 |
+ end |
|
| 191 |
+ |
|
| 157 | 192 |
def cannot_receive_events! |
| 158 | 193 |
@cannot_receive_events = true |
| 159 | 194 |
end |
@@ -196,9 +231,14 @@ class Agent < ActiveRecord::Base |
||
| 196 | 231 |
# and Event ids instead of a literal ActiveRecord models because it is preferable to serialize delayed_jobs with ids. |
| 197 | 232 |
def async_receive(agent_id, event_ids) |
| 198 | 233 |
agent = Agent.find(agent_id) |
| 199 |
- agent.receive(Event.where(:id => event_ids)) |
|
| 200 |
- agent.last_receive_at = Time.now |
|
| 201 |
- agent.save! |
|
| 234 |
+ begin |
|
| 235 |
+ agent.receive(Event.where(:id => event_ids)) |
|
| 236 |
+ agent.last_receive_at = Time.now |
|
| 237 |
+ agent.save! |
|
| 238 |
+ rescue => e |
|
| 239 |
+ agent.error "Exception during receive: #{e.message} -- #{e.backtrace}"
|
|
| 240 |
+ raise |
|
| 241 |
+ end |
|
| 202 | 242 |
end |
| 203 | 243 |
handle_asynchronously :async_receive |
| 204 | 244 |
|
@@ -223,9 +263,14 @@ class Agent < ActiveRecord::Base |
||
| 223 | 263 |
# id instead of a literal Agent because it is preferable to serialize delayed_jobs with ids. |
| 224 | 264 |
def async_check(agent_id) |
| 225 | 265 |
agent = Agent.find(agent_id) |
| 226 |
- agent.check |
|
| 227 |
- agent.last_check_at = Time.now |
|
| 228 |
- agent.save! |
|
| 266 |
+ begin |
|
| 267 |
+ agent.check |
|
| 268 |
+ agent.last_check_at = Time.now |
|
| 269 |
+ agent.save! |
|
| 270 |
+ rescue => e |
|
| 271 |
+ agent.error "Exception during check: #{e.message} -- #{e.backtrace}"
|
|
| 272 |
+ raise |
|
| 273 |
+ end |
|
| 229 | 274 |
end |
| 230 | 275 |
handle_asynchronously :async_check |
| 231 | 276 |
end |
@@ -0,0 +1,23 @@ |
||
| 1 |
+class AgentLog < ActiveRecord::Base |
|
| 2 |
+ attr_accessible :agent, :inbound_event, :level, :message, :outbound_event |
|
| 3 |
+ |
|
| 4 |
+ belongs_to :agent |
|
| 5 |
+ belongs_to :inbound_event, :class_name => "Event" |
|
| 6 |
+ belongs_to :outbound_event, :class_name => "Event" |
|
| 7 |
+ |
|
| 8 |
+ validates_presence_of :agent, :message |
|
| 9 |
+ validates_numericality_of :level, :only_integer => true, :greater_than_or_equal_to => 0, :less_than => 5 |
|
| 10 |
+ |
|
| 11 |
+ def self.log_for_agent(agent, message, options = {})
|
|
| 12 |
+ log = agent.logs.create! options.merge(:message => message) |
|
| 13 |
+ if agent.logs.count > log_length |
|
| 14 |
+ oldest_id_to_keep = agent.logs.limit(1).offset(log_length - 1).pluck("agent_logs.id")
|
|
| 15 |
+ agent.logs.where("agent_logs.id < ?", oldest_id_to_keep).delete_all
|
|
| 16 |
+ end |
|
| 17 |
+ log |
|
| 18 |
+ end |
|
| 19 |
+ |
|
| 20 |
+ def self.log_length |
|
| 21 |
+ ENV['AGENT_LOG_LENGTH'].present? ? ENV['AGENT_LOG_LENGTH'].to_i : 100 |
|
| 22 |
+ end |
|
| 23 |
+end |
@@ -40,7 +40,7 @@ module Agents |
||
| 40 | 40 |
end |
| 41 | 41 |
|
| 42 | 42 |
def working? |
| 43 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? |
|
| 43 |
+ event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
| 44 | 44 |
end |
| 45 | 45 |
|
| 46 | 46 |
def validate_options |
@@ -3,6 +3,8 @@ module Agents |
||
| 3 | 3 |
MAIN_KEYS = %w[title message text main value].map(&:to_sym) |
| 4 | 4 |
default_schedule "5am" |
| 5 | 5 |
|
| 6 |
+ cannot_create_events! |
|
| 7 |
+ |
|
| 6 | 8 |
description <<-MD |
| 7 | 9 |
The DigestEmailAgent collects any Events sent to it and sends them all via email when run. |
| 8 | 10 |
The email will be sent to your account's address and will have a `subject` and an optional `headline` before |
@@ -21,7 +23,7 @@ module Agents |
||
| 21 | 23 |
end |
| 22 | 24 |
|
| 23 | 25 |
def working? |
| 24 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 26 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 25 | 27 |
end |
| 26 | 28 |
|
| 27 | 29 |
def validate_options |
@@ -38,7 +40,7 @@ module Agents |
||
| 38 | 40 |
def check |
| 39 | 41 |
if self.memory[:queue] && self.memory[:queue].length > 0 |
| 40 | 42 |
groups = self.memory[:queue].map { |payload| present(payload) }
|
| 41 |
- puts "Sending mail to #{user.email}..." unless Rails.env.test?
|
|
| 43 |
+ log "Sending digest mail to #{user.email}"
|
|
| 42 | 44 |
SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => groups) |
| 43 | 45 |
self.memory[:queue] = [] |
| 44 | 46 |
end |
@@ -61,4 +63,4 @@ module Agents |
||
| 61 | 63 |
hash.to_a.sort_by {|a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless k.to_s == skip_key.to_s }.compact
|
| 62 | 64 |
end |
| 63 | 65 |
end |
| 64 |
-end |
|
| 66 |
+end |
@@ -54,7 +54,8 @@ module Agents |
||
| 54 | 54 |
{
|
| 55 | 55 |
:instructions => {
|
| 56 | 56 |
:message => "You received a text <$.text> from <$.fields.from>", |
| 57 |
- :content => "Looks like the weather is going to be <$.fields.weather>"}, |
|
| 57 |
+ :some_other_field => "Looks like the weather is going to be <$.fields.weather>" |
|
| 58 |
+ }, |
|
| 58 | 59 |
:mode => "clean", |
| 59 | 60 |
:skip_agent => "false", |
| 60 | 61 |
:skip_created_at => "false" |
@@ -62,7 +63,7 @@ module Agents |
||
| 62 | 63 |
end |
| 63 | 64 |
|
| 64 | 65 |
def working? |
| 65 |
- true |
|
| 66 |
+ !recent_error_logs? |
|
| 66 | 67 |
end |
| 67 | 68 |
|
| 68 | 69 |
def value_constructor(value, payload) |
@@ -43,7 +43,7 @@ module Agents |
||
| 43 | 43 |
end |
| 44 | 44 |
|
| 45 | 45 |
def working? |
| 46 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 46 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 47 | 47 |
end |
| 48 | 48 |
|
| 49 | 49 |
def receive(incoming_events) |
@@ -1,6 +1,7 @@ |
||
| 1 | 1 |
module Agents |
| 2 | 2 |
class PostAgent < Agent |
| 3 | 3 |
cannot_be_scheduled! |
| 4 |
+ cannot_create_events! |
|
| 4 | 5 |
|
| 5 | 6 |
description <<-MD |
| 6 | 7 |
Post Agent receives events from other agents and send those events as the contents of a post request to a specified url. `post_url` field must specify where you would like to receive post requests and do not forget to include URI scheme (`http` or `https`) |
@@ -16,7 +17,7 @@ module Agents |
||
| 16 | 17 |
end |
| 17 | 18 |
|
| 18 | 19 |
def working? |
| 19 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 20 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 20 | 21 |
end |
| 21 | 22 |
|
| 22 | 23 |
def validate_options |
@@ -34,7 +34,7 @@ module Agents |
||
| 34 | 34 |
end |
| 35 | 35 |
|
| 36 | 36 |
def working? |
| 37 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 37 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 38 | 38 |
end |
| 39 | 39 |
|
| 40 | 40 |
def receive(incoming_events) |
@@ -29,7 +29,7 @@ module Agents |
||
| 29 | 29 |
end |
| 30 | 30 |
|
| 31 | 31 |
def working? |
| 32 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 32 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 33 | 33 |
end |
| 34 | 34 |
|
| 35 | 35 |
def translate(text, to, access_token) |
@@ -42,7 +42,7 @@ module Agents |
||
| 42 | 42 |
end |
| 43 | 43 |
|
| 44 | 44 |
def working? |
| 45 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 45 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 46 | 46 |
end |
| 47 | 47 |
|
| 48 | 48 |
def receive(incoming_events) |
@@ -4,6 +4,7 @@ require 'securerandom' |
||
| 4 | 4 |
module Agents |
| 5 | 5 |
class TwilioAgent < Agent |
| 6 | 6 |
cannot_be_scheduled! |
| 7 |
+ cannot_create_events! |
|
| 7 | 8 |
|
| 8 | 9 |
description <<-MD |
| 9 | 10 |
The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled. |
@@ -58,7 +59,7 @@ module Agents |
||
| 58 | 59 |
end |
| 59 | 60 |
|
| 60 | 61 |
def working? |
| 61 |
- last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago |
|
| 62 |
+ last_receive_at && last_receive_at > options[:expected_receive_period_in_days].to_i.days.ago && !recent_error_logs? |
|
| 62 | 63 |
end |
| 63 | 64 |
|
| 64 | 65 |
def send_message(message) |
@@ -29,7 +29,7 @@ module Agents |
||
| 29 | 29 |
end |
| 30 | 30 |
|
| 31 | 31 |
def working? |
| 32 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? && event.payload[:success] == true |
|
| 32 |
+ (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? |
|
| 33 | 33 |
end |
| 34 | 34 |
|
| 35 | 35 |
def default_options |
@@ -63,7 +63,7 @@ module Agents |
||
| 63 | 63 |
end |
| 64 | 64 |
|
| 65 | 65 |
def working? |
| 66 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? |
|
| 66 |
+ event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
| 67 | 67 |
end |
| 68 | 68 |
|
| 69 | 69 |
def default_options |
@@ -45,7 +45,7 @@ module Agents |
||
| 45 | 45 |
end |
| 46 | 46 |
|
| 47 | 47 |
def working? |
| 48 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? |
|
| 48 |
+ event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
| 49 | 49 |
end |
| 50 | 50 |
|
| 51 | 51 |
def default_options |
@@ -30,7 +30,7 @@ module Agents |
||
| 30 | 30 |
MD |
| 31 | 31 |
|
| 32 | 32 |
def working? |
| 33 |
- (event = event_created_within(2.days)) && event.payload.present? |
|
| 33 |
+ event_created_within(2) && !recent_error_logs? |
|
| 34 | 34 |
end |
| 35 | 35 |
|
| 36 | 36 |
def default_options |
@@ -41,7 +41,7 @@ module Agents |
||
| 41 | 41 |
default_schedule "8pm" |
| 42 | 42 |
|
| 43 | 43 |
def working? |
| 44 |
- (event = event_created_within(2.days)) && event.payload.present? |
|
| 44 |
+ event_created_within(2) && !recent_error_logs? |
|
| 45 | 45 |
end |
| 46 | 46 |
|
| 47 | 47 |
def wunderground |
@@ -44,7 +44,7 @@ module Agents |
||
| 44 | 44 |
UNIQUENESS_LOOK_BACK = 30 |
| 45 | 45 |
|
| 46 | 46 |
def working? |
| 47 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? |
|
| 47 |
+ event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
| 48 | 48 |
end |
| 49 | 49 |
|
| 50 | 50 |
def default_options |
@@ -66,29 +66,38 @@ module Agents |
||
| 66 | 66 |
|
| 67 | 67 |
def check |
| 68 | 68 |
hydra = Typhoeus::Hydra.new |
| 69 |
+ log "Fetching #{options[:url]}"
|
|
| 69 | 70 |
request = Typhoeus::Request.new(options[:url], :followlocation => true) |
| 70 |
- request.on_complete do |response| |
|
| 71 |
+ request.on_failure do |response| |
|
| 72 |
+ error "Failed: #{response.inspect}"
|
|
| 73 |
+ end |
|
| 74 |
+ request.on_success do |response| |
|
| 71 | 75 |
doc = parse(response.body) |
| 72 | 76 |
output = {}
|
| 73 | 77 |
options[:extract].each do |name, extraction_details| |
| 74 |
- if extraction_type == "json" |
|
| 75 |
- output[name] = Utils.values_at(doc, extraction_details[:path]) |
|
| 76 |
- else |
|
| 77 |
- output[name] = doc.css(extraction_details[:css]).map { |node|
|
|
| 78 |
- if extraction_details[:attr] |
|
| 79 |
- node.attr(extraction_details[:attr]) |
|
| 80 |
- elsif extraction_details[:text] |
|
| 81 |
- node.text() |
|
| 82 |
- else |
|
| 83 |
- raise StandardError, ":attr or :text is required on HTML or XML extraction patterns" |
|
| 84 |
- end |
|
| 85 |
- } |
|
| 86 |
- end |
|
| 78 |
+ result = if extraction_type == "json" |
|
| 79 |
+ output[name] = Utils.values_at(doc, extraction_details[:path]) |
|
| 80 |
+ else |
|
| 81 |
+ output[name] = doc.css(extraction_details[:css]).map { |node|
|
|
| 82 |
+ if extraction_details[:attr] |
|
| 83 |
+ node.attr(extraction_details[:attr]) |
|
| 84 |
+ elsif extraction_details[:text] |
|
| 85 |
+ node.text() |
|
| 86 |
+ else |
|
| 87 |
+ error ":attr or :text is required on HTML or XML extraction patterns" |
|
| 88 |
+ return |
|
| 89 |
+ end |
|
| 90 |
+ } |
|
| 91 |
+ end |
|
| 92 |
+ log "Extracting #{extraction_type} at #{extraction_details[:path] || extraction_details[:css]}: #{result}"
|
|
| 87 | 93 |
end |
| 88 | 94 |
|
| 89 | 95 |
num_unique_lengths = options[:extract].keys.map { |name| output[name].length }.uniq
|
| 90 | 96 |
|
| 91 |
- raise StandardError, "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}" unless num_unique_lengths.length == 1
|
|
| 97 |
+ if num_unique_lengths.length != 1 |
|
| 98 |
+ error "Got an uneven number of matches for #{options[:name]}: #{options[:extract].inspect}"
|
|
| 99 |
+ return |
|
| 100 |
+ end |
|
| 92 | 101 |
|
| 93 | 102 |
previous_payloads = events.order("id desc").limit(UNIQUENESS_LOOK_BACK).pluck(:payload).map(&:to_json) if options[:mode].to_s == "on_change"
|
| 94 | 103 |
num_unique_lengths.first.times do |index| |
@@ -101,7 +110,7 @@ module Agents |
||
| 101 | 110 |
end |
| 102 | 111 |
|
| 103 | 112 |
if !options[:mode] || options[:mode].to_s == "all" || (options[:mode].to_s == "on_change" && !previous_payloads.include?(result.to_json)) |
| 104 |
- Rails.logger.info "Storing new result for '#{name}': #{result.inspect}"
|
|
| 113 |
+ log "Storing new result for '#{name}': #{result.inspect}"
|
|
| 105 | 114 |
create_event :payload => result |
| 106 | 115 |
end |
| 107 | 116 |
end |
@@ -28,7 +28,7 @@ module Agents |
||
| 28 | 28 |
end |
| 29 | 29 |
|
| 30 | 30 |
def working? |
| 31 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? && event.payload[:success] == true |
|
| 31 |
+ (event = event_created_within(options[:expected_update_period_in_days])) && event.payload[:success] == true && !recent_error_logs? |
|
| 32 | 32 |
end |
| 33 | 33 |
|
| 34 | 34 |
def default_options |
@@ -78,7 +78,7 @@ module Agents |
||
| 78 | 78 |
end |
| 79 | 79 |
|
| 80 | 80 |
def working? |
| 81 |
- (event = event_created_within(options[:expected_update_period_in_days].to_i.days)) && event.payload.present? |
|
| 81 |
+ event_created_within(options[:expected_update_period_in_days]) && !recent_error_logs? |
|
| 82 | 82 |
end |
| 83 | 83 |
|
| 84 | 84 |
def default_options |
@@ -23,6 +23,7 @@ class User < ActiveRecord::Base |
||
| 23 | 23 |
|
| 24 | 24 |
has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user |
| 25 | 25 |
has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user |
| 26 |
+ has_many :logs, :through => :agents, :class_name => "AgentLog" |
|
| 26 | 27 |
|
| 27 | 28 |
# Allow users to login via either email or username. |
| 28 | 29 |
def self.find_first_by_auth_conditions(warden_conditions) |
@@ -24,7 +24,7 @@ |
||
| 24 | 24 |
<div class="control-group type-select"> |
| 25 | 25 |
<%= f.label :type, :class => 'control-label' %> |
| 26 | 26 |
<div class="controls"> |
| 27 |
- <%= f.select :type, options_for_select(Agent.types.map {|type| [type.to_s.gsub(/^.*::/, ''), type.to_s] }, @agent.type), {}, :class => 'span4 select2' %>
|
|
| 27 |
+ <%= f.select :type, options_for_select(Agent.types.map(&:to_s).sort.map {|type| [type.gsub(/^.*::/, ''), type] }, @agent.type), {}, :class => 'span4 select2' %>
|
|
| 28 | 28 |
</div> |
| 29 | 29 |
</div> |
| 30 | 30 |
<% end %> |
@@ -47,8 +47,9 @@ |
||
| 47 | 47 |
<div class="control-group"> |
| 48 | 48 |
<%= f.label :sources, :class => 'control-label' %> |
| 49 | 49 |
<div class="controls link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
| 50 |
+ <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %>
|
|
| 50 | 51 |
<%= f.select(:source_ids, |
| 51 |
- options_for_select((current_user.agents - [@agent]).map {|s| [s.name, s.id] },
|
|
| 52 |
+ options_for_select(eventSources.map {|s| [s.name, s.id] },
|
|
| 52 | 53 |
@agent.source_ids), |
| 53 | 54 |
{}, { :multiple => true, :size => 5, :class => 'span4 select2' }) %>
|
| 54 | 55 |
<span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span> |
@@ -4,7 +4,7 @@ |
||
| 4 | 4 |
<div class="page-header"> |
| 5 | 5 |
<h2> |
| 6 | 6 |
Editing your <%= @agent.short_type %> |
| 7 |
- <%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
|
| 7 |
+ <%= image_tag "spinner-arrows.gif", :class => "spinner", :id => 'agent-spinner' %> |
|
| 8 | 8 |
</h2> |
| 9 | 9 |
</div> |
| 10 | 10 |
|
@@ -8,53 +8,71 @@ |
||
| 8 | 8 |
<table class='table table-striped'> |
| 9 | 9 |
<tr> |
| 10 | 10 |
<th>Name</th> |
| 11 |
+ <th>Schedule</th> |
|
| 11 | 12 |
<th>Last Check</th> |
| 12 | 13 |
<th>Last Event Out</th> |
| 13 | 14 |
<th>Last Event In</th> |
| 14 |
- <th>Events</th> |
|
| 15 |
- <th>Schedule</th> |
|
| 15 |
+ <th>Events Created</th> |
|
| 16 | 16 |
<th>Working?</th> |
| 17 | 17 |
<th></th> |
| 18 | 18 |
</tr> |
| 19 | 19 |
|
| 20 | 20 |
<% @agents.each do |agent| %> |
| 21 |
- <tr> |
|
| 22 |
- <td> |
|
| 23 |
- <%= agent.name %> |
|
| 24 |
- <br/> |
|
| 25 |
- <span class='muted'><%= agent.short_type.titleize %></span> |
|
| 26 |
- </td> |
|
| 27 |
- <td> |
|
| 28 |
- <% if agent.cannot_be_scheduled? %> |
|
| 29 |
- N/A |
|
| 30 |
- <% else %> |
|
| 31 |
- <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
| 32 |
- <% end %> |
|
| 33 |
- </td> |
|
| 34 |
- <td><%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %></td> |
|
| 35 |
- <td> |
|
| 36 |
- <% if agent.cannot_receive_events? %> |
|
| 37 |
- N/A |
|
| 21 |
+ <tr> |
|
| 22 |
+ <td> |
|
| 23 |
+ <%= agent.name %> |
|
| 24 |
+ <br/> |
|
| 25 |
+ <span class='muted'><%= agent.short_type.titleize %></span> |
|
| 26 |
+ </td> |
|
| 27 |
+ <td> |
|
| 28 |
+ <% if agent.can_be_scheduled? %> |
|
| 29 |
+ <%= agent.schedule.to_s.humanize.titleize %> |
|
| 30 |
+ <% else %> |
|
| 31 |
+ <span class='not-applicable'></span> |
|
| 32 |
+ <% end %> |
|
| 33 |
+ </td> |
|
| 34 |
+ <td> |
|
| 35 |
+ <% if agent.can_be_scheduled? %> |
|
| 36 |
+ <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
| 37 |
+ <% else %> |
|
| 38 |
+ <span class='not-applicable'></span> |
|
| 39 |
+ <% end %> |
|
| 40 |
+ </td> |
|
| 41 |
+ <td> |
|
| 42 |
+ <% if agent.can_create_events? %> |
|
| 43 |
+ <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
|
| 44 |
+ <% else %> |
|
| 45 |
+ <span class='not-applicable'></span> |
|
| 46 |
+ <% end %> |
|
| 47 |
+ </td> |
|
| 48 |
+ <td> |
|
| 49 |
+ <% if agent.can_receive_events? %> |
|
| 50 |
+ <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
| 51 |
+ <% else %> |
|
| 52 |
+ <span class='not-applicable'></span> |
|
| 53 |
+ <% end %> |
|
| 54 |
+ </td> |
|
| 55 |
+ <td> |
|
| 56 |
+ <% if agent.can_create_events? %> |
|
| 57 |
+ <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
| 58 |
+ <% else %> |
|
| 59 |
+ <span class='not-applicable'></span> |
|
| 60 |
+ <% end %> |
|
| 61 |
+ </td> |
|
| 62 |
+ <td><%= working(agent) %></td> |
|
| 63 |
+ <td> |
|
| 64 |
+ <div class="btn-group"> |
|
| 65 |
+ <%= link_to 'Show', agent_path(agent), class: "btn btn-mini" %> |
|
| 66 |
+ <%= link_to 'Edit', edit_agent_path(agent), class: "btn btn-mini" %> |
|
| 67 |
+ <%= link_to 'Delete', agent_path(agent), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-mini" %>
|
|
| 68 |
+ <% if agent.can_be_scheduled? %> |
|
| 69 |
+ <%= link_to 'Run', run_agent_path(agent, :return => "index"), method: :post, class: "btn btn-mini" %> |
|
| 38 | 70 |
<% else %> |
| 39 |
- <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
| 71 |
+ <%= link_to 'Run', "#", class: "btn btn-mini disabled" %> |
|
| 40 | 72 |
<% end %> |
| 41 |
- </td> |
|
| 42 |
- <td><%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %></td> |
|
| 43 |
- <td><%= (agent.schedule || "n/a").to_s.humanize.titleize %></td> |
|
| 44 |
- <td><%= working(agent) %></td> |
|
| 45 |
- <td> |
|
| 46 |
- <div class="btn-group"> |
|
| 47 |
- <%= link_to 'Show', agent_path(agent), class: "btn btn-mini" %> |
|
| 48 |
- <%= link_to 'Edit', edit_agent_path(agent), class: "btn btn-mini" %> |
|
| 49 |
- <%= link_to 'Delete', agent_path(agent), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %>
|
|
| 50 |
- <% if agent.can_be_scheduled? %> |
|
| 51 |
- <%= link_to 'Run', run_agent_path(agent), method: :post, class: "btn btn-mini" %> |
|
| 52 |
- <% else %> |
|
| 53 |
- <%= link_to 'Run', "#", class: "btn btn-mini disabled" %> |
|
| 54 |
- <% end %> |
|
| 55 |
- </div> |
|
| 56 |
- </td> |
|
| 57 |
- </tr> |
|
| 73 |
+ </div> |
|
| 74 |
+ </td> |
|
| 75 |
+ </tr> |
|
| 58 | 76 |
<% end %> |
| 59 | 77 |
</table> |
| 60 | 78 |
|
@@ -4,7 +4,7 @@ |
||
| 4 | 4 |
<div class="page-header"> |
| 5 | 5 |
<h2> |
| 6 | 6 |
Create a new Agent |
| 7 |
- <%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
|
| 7 |
+ <%= image_tag "spinner-arrows.gif", :class => "spinner", :id => 'agent-spinner' %> |
|
| 8 | 8 |
</h2> |
| 9 | 9 |
</div> |
| 10 | 10 |
|
@@ -1,4 +1,4 @@ |
||
| 1 |
-<div class='container'> |
|
| 1 |
+<div class='container agent-show'> |
|
| 2 | 2 |
<div class='row'> |
| 3 | 3 |
<div class='span12'> |
| 4 | 4 |
|
@@ -11,24 +11,32 @@ |
||
| 11 | 11 |
<li class='disabled'><a><i class='icon-picture'></i> Summary</a></li> |
| 12 | 12 |
<li class='active'><a href="#details" data-toggle="tab"><i class='icon-indent-left'></i> Details</a></li> |
| 13 | 13 |
<% end %> |
| 14 |
+ <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>"><i class='icon-list-alt'></i> Logs</a></li> |
|
| 14 | 15 |
|
| 15 |
- <% if @agent.events.count > 0 %> |
|
| 16 |
+ <% if @agent.can_create_events? && @agent.events.count > 0 %> |
|
| 16 | 17 |
<li><%= link_to '<i class="icon-random"></i> Events'.html_safe, events_path(:agent => @agent.to_param) %></li> |
| 17 | 18 |
<% end %> |
| 18 | 19 |
<li><%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, agents_path %></li> |
| 19 | 20 |
<li><%= link_to '<i class="icon-pencil"></i> Edit'.html_safe, edit_agent_path(@agent) %></li> |
| 20 | 21 |
|
| 21 |
- <% if @agent.events.count > 0 %> |
|
| 22 |
+ <% if @agent.can_be_scheduled? || @agent.events.count > 0 %> |
|
| 22 | 23 |
<li class="dropdown"> |
| 23 | 24 |
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Actions <b class="caret"></b></a> |
| 24 | 25 |
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> |
| 26 |
+ <% if @agent.can_be_scheduled? %> |
|
| 27 |
+ <li> |
|
| 28 |
+ <%= link_to '<i class="icon-refresh"></i> Run'.html_safe, run_agent_path(@agent, :return => "show"), method: :post, :tabindex => "-1" %> |
|
| 29 |
+ </li> |
|
| 30 |
+ <% end %> |
|
| 31 |
+ |
|
| 32 |
+ <% if @agent.can_create_events? && @agent.events.count > 0 %> |
|
| 25 | 33 |
<li> |
| 26 | 34 |
<%= link_to '<i class="icon-trash"></i> Delete all events'.html_safe, remove_events_agent_path(@agent), method: :delete, data: {confirm: 'Are you sure you want to delete ALL events for this Agent?'}, :tabindex => "-1" %>
|
| 27 | 35 |
</li> |
| 36 |
+ <% end %> |
|
| 28 | 37 |
</ul> |
| 29 | 38 |
</li> |
| 30 | 39 |
<% end %> |
| 31 |
- |
|
| 32 | 40 |
</ul> |
| 33 | 41 |
|
| 34 | 42 |
<div class="tab-content"> |
@@ -42,6 +50,18 @@ |
||
| 42 | 50 |
<% end %> |
| 43 | 51 |
</div> |
| 44 | 52 |
|
| 53 |
+ <div class="tab-pane" id="logs" data-agent-id="<%= @agent.id %>"> |
|
| 54 |
+ <h2> |
|
| 55 |
+ <%= @agent.name %> Logs |
|
| 56 |
+ <%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
|
| 57 |
+ <i class="icon-refresh action-icon refresh"></i> |
|
| 58 |
+ <i class="icon-trash action-icon clear"></i> |
|
| 59 |
+ </h2> |
|
| 60 |
+ <div class='logs'> |
|
| 61 |
+ Just a moment... |
|
| 62 |
+ </div> |
|
| 63 |
+ </div> |
|
| 64 |
+ |
|
| 45 | 65 |
<div class="tab-pane <%= agent_show_view(@agent).present? ? "" : "active" %>" id="details"> |
| 46 | 66 |
<h2><%= @agent.name %> Details</h2> |
| 47 | 67 |
|
@@ -50,38 +70,60 @@ |
||
| 50 | 70 |
<%= @agent.short_type.titleize %> |
| 51 | 71 |
</p> |
| 52 | 72 |
|
| 53 |
- <p> |
|
| 54 |
- <b>Schedule:</b> |
|
| 55 |
- <%= (@agent.schedule || "n/a").humanize.titleize %> |
|
| 56 |
- </p> |
|
| 73 |
+ <% if @agent.can_be_scheduled? %> |
|
| 74 |
+ <p> |
|
| 75 |
+ <b>Schedule:</b> |
|
| 76 |
+ <%= (@agent.schedule || "n/a").humanize.titleize %> |
|
| 77 |
+ </p> |
|
| 57 | 78 |
|
| 58 |
- <p> |
|
| 59 |
- <b>Last checked:</b> |
|
| 60 |
- <% if @agent.cannot_be_scheduled? %> |
|
| 61 |
- N/A |
|
| 62 |
- <% else %> |
|
| 79 |
+ <p> |
|
| 80 |
+ <b>Last checked:</b> |
|
| 63 | 81 |
<%= @agent.last_check_at ? time_ago_in_words(@agent.last_check_at) + " ago" : "never" %> |
| 64 |
- <% end %> |
|
| 65 |
- </p> |
|
| 82 |
+ </p> |
|
| 83 |
+ <% end %> |
|
| 66 | 84 |
|
| 67 |
- <p> |
|
| 68 |
- <b>Last event created:</b> |
|
| 69 |
- <%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %> |
|
| 70 |
- </p> |
|
| 85 |
+ <% if @agent.can_create_events? %> |
|
| 86 |
+ <p> |
|
| 87 |
+ <b>Last event created:</b> |
|
| 88 |
+ <%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %> |
|
| 89 |
+ </p> |
|
| 90 |
+ <% end %> |
|
| 71 | 91 |
|
| 72 |
- <p> |
|
| 73 |
- <b>Last received event:</b> |
|
| 74 |
- <% if @agent.cannot_receive_events? %> |
|
| 75 |
- N/A |
|
| 76 |
- <% else %> |
|
| 92 |
+ <% if @agent.can_receive_events? %> |
|
| 93 |
+ <p> |
|
| 94 |
+ <b>Last received event:</b> |
|
| 77 | 95 |
<%= @agent.last_receive_at ? time_ago_in_words(@agent.last_receive_at) + " ago" : "never" %> |
| 78 |
- <% end %> |
|
| 79 |
- </p> |
|
| 96 |
+ </p> |
|
| 97 |
+ <% end %> |
|
| 80 | 98 |
|
| 81 |
- <p> |
|
| 82 |
- <b>Event count:</b> |
|
| 83 |
- <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %> |
|
| 84 |
- </p> |
|
| 99 |
+ <% if @agent.can_create_events? %> |
|
| 100 |
+ <p> |
|
| 101 |
+ <b>Events created:</b> |
|
| 102 |
+ <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %> |
|
| 103 |
+ </p> |
|
| 104 |
+ <% end %> |
|
| 105 |
+ |
|
| 106 |
+ <% if @agent.can_receive_events? %> |
|
| 107 |
+ <p> |
|
| 108 |
+ <b>Event sources:</b> |
|
| 109 |
+ <% if @agent.sources.length %> |
|
| 110 |
+ <%= @agent.sources.map { |source_agent| link_to(source_agent.name, agent_path(source_agent)) }.to_sentence.html_safe %>
|
|
| 111 |
+ <% else %> |
|
| 112 |
+ None |
|
| 113 |
+ <% end %> |
|
| 114 |
+ </p> |
|
| 115 |
+ <% end %> |
|
| 116 |
+ |
|
| 117 |
+ <% if @agent.can_create_events? %> |
|
| 118 |
+ <p> |
|
| 119 |
+ <b>Event receivers:</b> |
|
| 120 |
+ <% if @agent.receivers.length %> |
|
| 121 |
+ <%= @agent.receivers.map { |receiver_agent| link_to(receiver_agent.name, agent_path(receiver_agent)) }.to_sentence.html_safe %>
|
|
| 122 |
+ <% else %> |
|
| 123 |
+ None |
|
| 124 |
+ <% end %> |
|
| 125 |
+ </p> |
|
| 126 |
+ <% end %> |
|
| 85 | 127 |
|
| 86 | 128 |
<p> |
| 87 | 129 |
<b>Working:</b> |
@@ -1,6 +1,10 @@ |
||
| 1 |
-<% flash.each do |name, msg| %> |
|
| 2 |
- <div class="top-flash alert alert-<%= name == :notice ? "success" : "error" %>"> |
|
| 3 |
- <a class="close" data-dismiss="alert">×</a> |
|
| 4 |
- <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
|
|
| 1 |
+<% if flash.keys.length > 0 %> |
|
| 2 |
+ <div class="flash"> |
|
| 3 |
+ <% flash.each do |name, msg| %> |
|
| 4 |
+ <div class="alert alert-<%= name == :notice ? "success" : "error" %>"> |
|
| 5 |
+ <a class="close" data-dismiss="alert">×</a> |
|
| 6 |
+ <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
|
|
| 7 |
+ </div> |
|
| 8 |
+ <% end %> |
|
| 5 | 9 |
</div> |
| 6 | 10 |
<% end %> |
@@ -9,11 +9,23 @@ |
||
| 9 | 9 |
|
| 10 | 10 |
<ul class="nav pull-right"> |
| 11 | 11 |
<% if user_signed_in? %> |
| 12 |
+ |
|
| 13 |
+ <form class="navbar-search pull-left"> |
|
| 14 |
+ <input type="text" class="search-query" id='agent-navigate' autocomplete="off" placeholder="Search"> |
|
| 15 |
+ <%= image_tag "spinner-arrows.gif", :class => "spinner" %> |
|
| 16 |
+ </form> |
|
| 17 |
+ |
|
| 12 | 18 |
<li id='job-indicator'> |
| 13 | 19 |
<a href="/delayed_job"> |
| 14 | 20 |
<span class="badge"><i class="icon-refresh icon-white"></i> <span class='number'>0</span></span> |
| 15 | 21 |
</a> |
| 16 | 22 |
</li> |
| 23 |
+ |
|
| 24 |
+ <li id='event-indicator'> |
|
| 25 |
+ <a href="#"> |
|
| 26 |
+ <span class="badge"><i class="icon-random icon-white"></i> <span class='number'>0</span> new events</span> |
|
| 27 |
+ </a> |
|
| 28 |
+ </li> |
|
| 17 | 29 |
<% end %> |
| 18 | 30 |
|
| 19 | 31 |
<li class="dropdown"> |
@@ -31,6 +43,10 @@ |
||
| 31 | 43 |
</li> |
| 32 | 44 |
|
| 33 | 45 |
<li> |
| 46 |
+ <%= link_to 'About', 'https://github.com/cantino/huginn', :tabindex => "-1" %> |
|
| 47 |
+ </li> |
|
| 48 |
+ |
|
| 49 |
+ <li> |
|
| 34 | 50 |
<% if user_signed_in? %> |
| 35 | 51 |
<%= link_to 'Logout', destroy_user_session_path, :method => :delete, :tabindex => "-1" %> |
| 36 | 52 |
<% else %> |
@@ -40,3 +56,4 @@ |
||
| 40 | 56 |
</ul> |
| 41 | 57 |
</li> |
| 42 | 58 |
</ul> |
| 59 |
+ |
@@ -32,5 +32,15 @@ |
||
| 32 | 32 |
</div> |
| 33 | 33 |
</div> |
| 34 | 34 |
</div> |
| 35 |
+ |
|
| 36 |
+ <script> |
|
| 37 |
+ var agentPaths = <%= Utils.jsonify(current_user.agents.inject({}) {|m, a| m[a.name] = agent_path(a) unless a.new_record?; m }) %>;
|
|
| 38 |
+ agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
| 39 |
+ agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
| 40 |
+ agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
| 41 |
+ agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
| 42 |
+ var agentNames = []; |
|
| 43 |
+ $.each(agentPaths, function(name, v) { agentNames.push(name); });
|
|
| 44 |
+ </script> |
|
| 35 | 45 |
</body> |
| 36 | 46 |
</html> |
@@ -0,0 +1,30 @@ |
||
| 1 |
+<table class='table table-striped logs'> |
|
| 2 |
+ <tr> |
|
| 3 |
+ <th>Message</th> |
|
| 4 |
+ <th>When</th> |
|
| 5 |
+ <th></th> |
|
| 6 |
+ </tr> |
|
| 7 |
+ |
|
| 8 |
+ <% @logs.each do |log| %> |
|
| 9 |
+ <tr> |
|
| 10 |
+ <td><%= truncate log.message, :length => 200, :omission => "..." %></td> |
|
| 11 |
+ <td><%= time_ago_in_words log.created_at %> ago</td> |
|
| 12 |
+ |
|
| 13 |
+ <td> |
|
| 14 |
+ <div class="btn-group"> |
|
| 15 |
+ <% if log.inbound_event_id.present? %> |
|
| 16 |
+ <%= link_to 'Event In', event_path(log.inbound_event), class: "btn btn-mini" %> |
|
| 17 |
+ <% else %> |
|
| 18 |
+ <%= link_to 'Event In', '#', class: "btn btn-mini disabled" %> |
|
| 19 |
+ <% end %> |
|
| 20 |
+ |
|
| 21 |
+ <% if log.outbound_event_id.present? %> |
|
| 22 |
+ <%= link_to 'Event Out', event_path(log.outbound_event), class: "btn btn-mini" %> |
|
| 23 |
+ <% else %> |
|
| 24 |
+ <%= link_to 'Event Out', '#', class: "btn btn-mini disabled" %> |
|
| 25 |
+ <% end %> |
|
| 26 |
+ </div> |
|
| 27 |
+ </td> |
|
| 28 |
+ </tr> |
|
| 29 |
+ <% end %> |
|
| 30 |
+</table> |
@@ -11,6 +11,12 @@ Huginn::Application.routes.draw do |
||
| 11 | 11 |
get :event_descriptions |
| 12 | 12 |
get :diagram |
| 13 | 13 |
end |
| 14 |
+ |
|
| 15 |
+ resources :logs, :only => [:index] do |
|
| 16 |
+ collection do |
|
| 17 |
+ delete :clear |
|
| 18 |
+ end |
|
| 19 |
+ end |
|
| 14 | 20 |
end |
| 15 | 21 |
resources :events, :only => [:index, :show, :destroy] |
| 16 | 22 |
match "/worker_status" => "worker_status#show" |
@@ -0,0 +1,13 @@ |
||
| 1 |
+class CreateAgentLogs < ActiveRecord::Migration |
|
| 2 |
+ def change |
|
| 3 |
+ create_table :agent_logs do |t| |
|
| 4 |
+ t.integer :agent_id, :null => false |
|
| 5 |
+ t.text :message, :null => false |
|
| 6 |
+ t.integer :level, :default => 3, :null => false |
|
| 7 |
+ t.integer :inbound_event_id |
|
| 8 |
+ t.integer :outbound_event_id |
|
| 9 |
+ |
|
| 10 |
+ t.timestamps |
|
| 11 |
+ end |
|
| 12 |
+ end |
|
| 13 |
+end |
@@ -11,7 +11,17 @@ |
||
| 11 | 11 |
# |
| 12 | 12 |
# It's strongly recommended to check this file into your version control system. |
| 13 | 13 |
|
| 14 |
-ActiveRecord::Schema.define(:version => 20130509053743) do |
|
| 14 |
+ActiveRecord::Schema.define(:version => 20130819160603) do |
|
| 15 |
+ |
|
| 16 |
+ create_table "agent_logs", :force => true do |t| |
|
| 17 |
+ t.integer "agent_id", :null => false |
|
| 18 |
+ t.text "message", :null => false |
|
| 19 |
+ t.integer "level", :default => 3, :null => false |
|
| 20 |
+ t.integer "inbound_event_id" |
|
| 21 |
+ t.integer "outbound_event_id" |
|
| 22 |
+ t.datetime "created_at", :null => false |
|
| 23 |
+ t.datetime "updated_at", :null => false |
|
| 24 |
+ end |
|
| 15 | 25 |
|
| 16 | 26 |
create_table "agents", :force => true do |t| |
| 17 | 27 |
t.integer "user_id" |
@@ -51,4 +51,8 @@ module Utils |
||
| 51 | 51 |
result |
| 52 | 52 |
end |
| 53 | 53 |
end |
| 54 |
+ |
|
| 55 |
+ def self.jsonify(thing) |
|
| 56 |
+ thing.to_json.gsub('</', '<\/').html_safe
|
|
| 57 |
+ end |
|
| 54 | 58 |
end |
@@ -12,10 +12,21 @@ describe EventsController do |
||
| 12 | 12 |
get :index |
| 13 | 13 |
assigns(:events).all? {|i| i.user.should == users(:bob) }.should be_true
|
| 14 | 14 |
end |
| 15 |
+ |
|
| 16 |
+ it "can filter by Agent" do |
|
| 17 |
+ sign_in users(:bob) |
|
| 18 |
+ get :index, :agent => agents(:bob_website_agent) |
|
| 19 |
+ assigns(:events).length.should == agents(:bob_website_agent).events.length |
|
| 20 |
+ assigns(:events).all? {|i| i.agent.should == agents(:bob_website_agent) }.should be_true
|
|
| 21 |
+ |
|
| 22 |
+ lambda {
|
|
| 23 |
+ get :index, :agent => agents(:jane_website_agent) |
|
| 24 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
| 25 |
+ end |
|
| 15 | 26 |
end |
| 16 | 27 |
|
| 17 | 28 |
describe "GET show" do |
| 18 |
- it "only shows Agents for the current user" do |
|
| 29 |
+ it "only shows Events for the current user" do |
|
| 19 | 30 |
sign_in users(:bob) |
| 20 | 31 |
get :show, :id => events(:bob_website_agent_event).to_param |
| 21 | 32 |
assigns(:event).should eq(events(:bob_website_agent_event)) |
@@ -0,0 +1,37 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe LogsController do |
|
| 4 |
+ describe "GET index" do |
|
| 5 |
+ it "can filter by Agent" do |
|
| 6 |
+ sign_in users(:bob) |
|
| 7 |
+ get :index, :agent_id => agents(:bob_weather_agent).id |
|
| 8 |
+ assigns(:logs).length.should == agents(:bob_weather_agent).logs.length |
|
| 9 |
+ assigns(:logs).all? {|i| i.agent.should == agents(:bob_weather_agent) }.should be_true
|
|
| 10 |
+ end |
|
| 11 |
+ |
|
| 12 |
+ it "only loads Agents owned by the current user" do |
|
| 13 |
+ sign_in users(:bob) |
|
| 14 |
+ lambda {
|
|
| 15 |
+ get :index, :agent_id => agents(:jane_weather_agent).id |
|
| 16 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
| 17 |
+ end |
|
| 18 |
+ end |
|
| 19 |
+ |
|
| 20 |
+ describe "DELETE clear" do |
|
| 21 |
+ it "deletes all logs for a specific Agent" do |
|
| 22 |
+ sign_in users(:bob) |
|
| 23 |
+ lambda {
|
|
| 24 |
+ delete :clear, :agent_id => agents(:bob_weather_agent).id |
|
| 25 |
+ }.should change { AgentLog.count }.by(-1 * agents(:bob_weather_agent).logs.count)
|
|
| 26 |
+ assigns(:logs).length.should == 0 |
|
| 27 |
+ agents(:bob_weather_agent).logs.count.should == 0 |
|
| 28 |
+ end |
|
| 29 |
+ |
|
| 30 |
+ it "only deletes logs for an Agent owned by the current user" do |
|
| 31 |
+ sign_in users(:bob) |
|
| 32 |
+ lambda {
|
|
| 33 |
+ delete :clear, :agent_id => agents(:jane_weather_agent).id |
|
| 34 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
| 35 |
+ end |
|
| 36 |
+ end |
|
| 37 |
+end |
@@ -0,0 +1,15 @@ |
||
| 1 |
+log_for_jane_website_agent: |
|
| 2 |
+ agent: jane_website_agent |
|
| 3 |
+ message: "fetching some website data" |
|
| 4 |
+ |
|
| 5 |
+log_for_bob_website_agent: |
|
| 6 |
+ agent: bob_website_agent |
|
| 7 |
+ message: "fetching some other website data" |
|
| 8 |
+ |
|
| 9 |
+first_log_for_bob_weather_agent: |
|
| 10 |
+ agent: bob_weather_agent |
|
| 11 |
+ message: "checking the weather" |
|
| 12 |
+ |
|
| 13 |
+second_log_for_bob_weather_agent: |
|
| 14 |
+ agent: bob_weather_agent |
|
| 15 |
+ message: "checking the weather again" |
@@ -0,0 +1,77 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe AgentLog do |
|
| 4 |
+ describe "validations" do |
|
| 5 |
+ before do |
|
| 6 |
+ @log = AgentLog.new(:agent => agents(:jane_website_agent), :message => "The agent did something", :level => 3) |
|
| 7 |
+ @log.should be_valid |
|
| 8 |
+ end |
|
| 9 |
+ |
|
| 10 |
+ it "requires an agent" do |
|
| 11 |
+ @log.agent = nil |
|
| 12 |
+ @log.should_not be_valid |
|
| 13 |
+ @log.should have(1).error_on(:agent) |
|
| 14 |
+ end |
|
| 15 |
+ |
|
| 16 |
+ it "requires a message" do |
|
| 17 |
+ @log.message = "" |
|
| 18 |
+ @log.should_not be_valid |
|
| 19 |
+ @log.message = nil |
|
| 20 |
+ @log.should_not be_valid |
|
| 21 |
+ @log.should have(1).error_on(:message) |
|
| 22 |
+ end |
|
| 23 |
+ |
|
| 24 |
+ it "requires a valid log level" do |
|
| 25 |
+ @log.level = nil |
|
| 26 |
+ @log.should_not be_valid |
|
| 27 |
+ @log.should have(1).error_on(:level) |
|
| 28 |
+ |
|
| 29 |
+ @log.level = -1 |
|
| 30 |
+ @log.should_not be_valid |
|
| 31 |
+ @log.should have(1).error_on(:level) |
|
| 32 |
+ |
|
| 33 |
+ @log.level = 5 |
|
| 34 |
+ @log.should_not be_valid |
|
| 35 |
+ @log.should have(1).error_on(:level) |
|
| 36 |
+ |
|
| 37 |
+ @log.level = 4 |
|
| 38 |
+ @log.should be_valid |
|
| 39 |
+ |
|
| 40 |
+ @log.level = 0 |
|
| 41 |
+ @log.should be_valid |
|
| 42 |
+ end |
|
| 43 |
+ end |
|
| 44 |
+ |
|
| 45 |
+ describe "#log_for_agent" do |
|
| 46 |
+ it "creates AgentLogs" do |
|
| 47 |
+ log = AgentLog.log_for_agent(agents(:jane_website_agent), "some message", :level => 4, :outbound_event => events(:jane_website_agent_event)) |
|
| 48 |
+ log.should_not be_new_record |
|
| 49 |
+ log.agent.should == agents(:jane_website_agent) |
|
| 50 |
+ log.outbound_event.should == events(:jane_website_agent_event) |
|
| 51 |
+ log.message.should == "some message" |
|
| 52 |
+ log.level.should == 4 |
|
| 53 |
+ end |
|
| 54 |
+ |
|
| 55 |
+ it "cleans up old logs when there are more than log_length" do |
|
| 56 |
+ stub(AgentLog).log_length { 4 }
|
|
| 57 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 1") |
|
| 58 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 2") |
|
| 59 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 3") |
|
| 60 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 4") |
|
| 61 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 4"
|
|
| 62 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 1"
|
|
| 63 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 5") |
|
| 64 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 5"
|
|
| 65 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 2"
|
|
| 66 |
+ AgentLog.log_for_agent(agents(:jane_website_agent), "message 6") |
|
| 67 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").first.message.should == "message 6"
|
|
| 68 |
+ agents(:jane_website_agent).logs.order("agent_logs.id desc").last.message.should == "message 3"
|
|
| 69 |
+ end |
|
| 70 |
+ end |
|
| 71 |
+ |
|
| 72 |
+ describe "#log_length" do |
|
| 73 |
+ it "defaults to 100" do |
|
| 74 |
+ AgentLog.log_length.should == 100 |
|
| 75 |
+ end |
|
| 76 |
+ end |
|
| 77 |
+end |
@@ -114,36 +114,61 @@ describe Agent do |
||
| 114 | 114 |
end |
| 115 | 115 |
|
| 116 | 116 |
describe "#create_event" do |
| 117 |
+ before do |
|
| 118 |
+ @checker = Agents::SomethingSource.new(:name => "something") |
|
| 119 |
+ @checker.user = users(:bob) |
|
| 120 |
+ @checker.save! |
|
| 121 |
+ end |
|
| 122 |
+ |
|
| 117 | 123 |
it "should use the checker's user" do |
| 118 |
- checker = Agents::SomethingSource.new(:name => "something") |
|
| 119 |
- checker.user = users(:bob) |
|
| 120 |
- checker.save! |
|
| 124 |
+ @checker.check |
|
| 125 |
+ Event.last.user.should == @checker.user |
|
| 126 |
+ end |
|
| 121 | 127 |
|
| 122 |
- checker.check |
|
| 123 |
- Event.last.user.should == checker.user |
|
| 128 |
+ it "should log an error if the Agent has been marked with 'cannot_create_events!'" do |
|
| 129 |
+ mock(@checker).can_create_events? { false }
|
|
| 130 |
+ lambda {
|
|
| 131 |
+ @checker.check |
|
| 132 |
+ }.should_not change { Event.count }
|
|
| 133 |
+ @checker.logs.first.message.should =~ /cannot create events/i |
|
| 124 | 134 |
end |
| 125 | 135 |
end |
| 126 | 136 |
|
| 127 | 137 |
describe ".async_check" do |
| 128 |
- it "records last_check_at and calls check on the given Agent" do |
|
| 129 |
- checker = Agents::SomethingSource.new(:name => "something") |
|
| 130 |
- checker.user = users(:bob) |
|
| 131 |
- checker.save! |
|
| 138 |
+ before do |
|
| 139 |
+ @checker = Agents::SomethingSource.new(:name => "something") |
|
| 140 |
+ @checker.user = users(:bob) |
|
| 141 |
+ @checker.save! |
|
| 142 |
+ end |
|
| 132 | 143 |
|
| 133 |
- mock(checker).check.once {
|
|
| 134 |
- checker.options[:new] = true |
|
| 144 |
+ it "records last_check_at and calls check on the given Agent" do |
|
| 145 |
+ mock(@checker).check.once {
|
|
| 146 |
+ @checker.options[:new] = true |
|
| 135 | 147 |
} |
| 136 | 148 |
|
| 137 |
- mock(Agent).find(checker.id) { checker }
|
|
| 149 |
+ mock(Agent).find(@checker.id) { @checker }
|
|
| 150 |
+ |
|
| 151 |
+ @checker.last_check_at.should be_nil |
|
| 152 |
+ Agents::SomethingSource.async_check(@checker.id) |
|
| 153 |
+ @checker.reload.last_check_at.should be_within(2).of(Time.now) |
|
| 154 |
+ @checker.reload.options[:new].should be_true # Show that we save options |
|
| 155 |
+ end |
|
| 138 | 156 |
|
| 139 |
- checker.last_check_at.should be_nil |
|
| 140 |
- Agents::SomethingSource.async_check(checker.id) |
|
| 141 |
- checker.reload.last_check_at.should be_within(2).of(Time.now) |
|
| 142 |
- checker.reload.options[:new].should be_true # Show that we save options |
|
| 157 |
+ it "should log exceptions" do |
|
| 158 |
+ mock(@checker).check.once {
|
|
| 159 |
+ raise "foo" |
|
| 160 |
+ } |
|
| 161 |
+ mock(Agent).find(@checker.id) { @checker }
|
|
| 162 |
+ lambda {
|
|
| 163 |
+ Agents::SomethingSource.async_check(@checker.id) |
|
| 164 |
+ }.should raise_error |
|
| 165 |
+ log = @checker.logs.first |
|
| 166 |
+ log.message.should =~ /Exception/ |
|
| 167 |
+ log.level.should == 4 |
|
| 143 | 168 |
end |
| 144 | 169 |
end |
| 145 | 170 |
|
| 146 |
- describe ".receive!" do |
|
| 171 |
+ describe ".receive! and .async_receive" do |
|
| 147 | 172 |
before do |
| 148 | 173 |
stub_request(:any, /wunderground/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
|
| 149 | 174 |
stub.any_instance_of(Agents::WeatherAgent).is_tomorrow?(anything) { true }
|
@@ -155,6 +180,19 @@ describe Agent do |
||
| 155 | 180 |
Agent.receive! |
| 156 | 181 |
end |
| 157 | 182 |
|
| 183 |
+ it "should log exceptions" do |
|
| 184 |
+ mock.any_instance_of(Agents::TriggerAgent).receive(anything).once {
|
|
| 185 |
+ raise "foo" |
|
| 186 |
+ } |
|
| 187 |
+ Agent.async_check(agents(:bob_weather_agent).id) |
|
| 188 |
+ lambda {
|
|
| 189 |
+ Agent.receive! |
|
| 190 |
+ }.should raise_error |
|
| 191 |
+ log = agents(:bob_rain_notifier_agent).logs.first |
|
| 192 |
+ log.message.should =~ /Exception/ |
|
| 193 |
+ log.level.should == 4 |
|
| 194 |
+ end |
|
| 195 |
+ |
|
| 158 | 196 |
it "should track when events have been seen and not received them again" do |
| 159 | 197 |
mock.any_instance_of(Agents::TriggerAgent).receive(anything).once |
| 160 | 198 |
Agent.async_check(agents(:bob_weather_agent).id) |
@@ -29,7 +29,7 @@ describe Agents::AdiosoAgent do |
||
| 29 | 29 |
it "checks if its generating events as scheduled" do |
| 30 | 30 |
@checker.should_not be_working |
| 31 | 31 |
@checker.check |
| 32 |
- @checker.should be_working |
|
| 32 |
+ @checker.reload.should be_working |
|
| 33 | 33 |
three_days_from_now = 3.days.from_now |
| 34 | 34 |
stub(Time).now { three_days_from_now }
|
| 35 | 35 |
@checker.should_not be_working |
@@ -35,11 +35,28 @@ describe Agents::WebsiteAgent do |
||
| 35 | 35 |
end |
| 36 | 36 |
|
| 37 | 37 |
it "should log an error if the number of results for a set of extraction patterns differs" do |
| 38 |
- lambda {
|
|
| 39 |
- @site[:extract][:url][:css] = "div" |
|
| 40 |
- @checker.options = @site |
|
| 41 |
- @checker.check |
|
| 42 |
- }.should raise_error(StandardError, /Got an uneven number of matches/) |
|
| 38 |
+ @site[:extract][:url][:css] = "div" |
|
| 39 |
+ @checker.options = @site |
|
| 40 |
+ @checker.check |
|
| 41 |
+ @checker.logs.first.message.should =~ /Got an uneven number of matches/ |
|
| 42 |
+ end |
|
| 43 |
+ end |
|
| 44 |
+ |
|
| 45 |
+ describe '#working?' do |
|
| 46 |
+ it 'checks if events have been received within the expected receive period' do |
|
| 47 |
+ @checker.should_not be_working # No events created |
|
| 48 |
+ @checker.check |
|
| 49 |
+ @checker.reload.should be_working # Just created events |
|
| 50 |
+ |
|
| 51 |
+ @checker.error "oh no!" |
|
| 52 |
+ @checker.reload.should_not be_working # The most recent log is an error |
|
| 53 |
+ |
|
| 54 |
+ @checker.log "ok now" |
|
| 55 |
+ @checker.reload.should be_working # The most recent log is no longer an error |
|
| 56 |
+ |
|
| 57 |
+ two_days_from_now = 2.days.from_now |
|
| 58 |
+ stub(Time).now { two_days_from_now }
|
|
| 59 |
+ @checker.reload.should_not be_working # Two days have passed without a new event having been created |
|
| 43 | 60 |
end |
| 44 | 61 |
end |
| 45 | 62 |
|
@@ -52,8 +52,12 @@ |
||
| 52 | 52 |
display: block; |
| 53 | 53 |
float: right; |
| 54 | 54 |
text-decoration: none; |
| 55 |
- padding-left: 5px; |
|
| 55 |
+ padding: 0 5px; |
|
| 56 | 56 |
border: 0 !important; |
| 57 | 57 |
color: blue; |
| 58 |
+ |
|
| 59 |
+ &:hover {
|
|
| 60 |
+ background-color: #bbb; |
|
| 61 |
+ } |
|
| 58 | 62 |
} |
| 59 | 63 |
} |