@@ -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 |
} |