merge

Andrew Cantino 11 年之前
父節點
當前提交
08701a25ac
共有 53 個文件被更改,包括 745 次插入171 次删除
  1. 3 0
      .env.example
  2. 5 5
      Gemfile.lock
  3. 62 9
      app/assets/javascripts/application.js.coffee.erb
  4. 19 0
      app/assets/javascripts/worker-checker.js.coffee
  5. 41 4
      app/assets/stylesheets/application.css.scss.erb
  6. 7 2
      app/controllers/agents_controller.rb
  7. 19 0
      app/controllers/logs_controller.rb
  8. 1 2
      app/controllers/worker_status_controller.rb
  9. 1 1
      app/helpers/application_helper.rb
  10. 2 0
      app/helpers/logs_helper.rb
  11. 56 11
      app/models/agent.rb
  12. 23 0
      app/models/agent_log.rb
  13. 1 1
      app/models/agents/adioso_agent.rb
  14. 5 3
      app/models/agents/digest_email_agent.rb
  15. 3 2
      app/models/agents/event_formatting_agent.rb
  16. 1 1
      app/models/agents/peak_detector_agent.rb
  17. 2 1
      app/models/agents/post_agent.rb
  18. 1 1
      app/models/agents/sentiment_agent.rb
  19. 1 1
      app/models/agents/translation_agent.rb
  20. 1 1
      app/models/agents/trigger_agent.rb
  21. 2 1
      app/models/agents/twilio_agent.rb
  22. 1 1
      app/models/agents/twitter_publish_agent.rb
  23. 1 1
      app/models/agents/twitter_stream_agent.rb
  24. 1 1
      app/models/agents/twitter_user_agent.rb
  25. 1 1
      app/models/agents/user_location_agent.rb
  26. 1 1
      app/models/agents/weather_agent.rb
  27. 26 17
      app/models/agents/website_agent.rb
  28. 1 1
      app/models/agents/weibo_publish_agent.rb
  29. 1 1
      app/models/agents/weibo_user_agent.rb
  30. 1 0
      app/models/user.rb
  31. 3 2
      app/views/agents/_form.html.erb
  32. 1 1
      app/views/agents/edit.html.erb
  33. 55 37
      app/views/agents/index.html.erb
  34. 1 1
      app/views/agents/new.html.erb
  35. 72 30
      app/views/agents/show.html.erb
  36. 8 4
      app/views/layouts/_messages.html.erb
  37. 17 0
      app/views/layouts/_navigation.html.erb
  38. 10 0
      app/views/layouts/application.html.erb
  39. 30 0
      app/views/logs/index.html.erb
  40. 6 0
      config/routes.rb
  41. 13 0
      db/migrate/20130819160603_create_agent_logs.rb
  42. 11 1
      db/schema.rb
  43. 4 0
      lib/utils.rb
  44. 12 1
      spec/controllers/events_controller_spec.rb
  45. 37 0
      spec/controllers/logs_controller_spec.rb
  46. 15 0
      spec/fixtures/agent_logs.yml
  47. 77 0
      spec/models/agent_log_spec.rb
  48. 55 17
      spec/models/agent_spec.rb
  49. 1 1
      spec/models/agents/adioso_agent_spec.rb
  50. 22 5
      spec/models/agents/website_agent_spec.rb
  51. 0 0
      vendor/assets/javascripts/.gitkeep
  52. 0 0
      vendor/assets/stylesheets/.gitkeep
  53. 5 1
      vendor/assets/stylesheets/jquery.json-editor.css.scss

+ 3 - 0
.env.example

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

+ 5 - 5
Gemfile.lock

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

+ 62 - 9
app/assets/javascripts/application.js.coffee.erb

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

+ 19 - 0
app/assets/javascripts/worker-checker.js.coffee

@@ -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()

+ 41 - 4
app/assets/stylesheets/application.css.scss.erb

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

+ 7 - 2
app/controllers/agents_controller.rb

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

+ 19 - 0
app/controllers/logs_controller.rb

@@ -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 - 2
app/controllers/worker_status_controller.rb

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

+ 1 - 1
app/helpers/application_helper.rb

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

+ 2 - 0
app/helpers/logs_helper.rb

@@ -0,0 +1,2 @@
1
+module LogsHelper
2
+end

+ 56 - 11
app/models/agent.rb

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

+ 23 - 0
app/models/agent_log.rb

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

+ 1 - 1
app/models/agents/adioso_agent.rb

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

+ 5 - 3
app/models/agents/digest_email_agent.rb

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

+ 3 - 2
app/models/agents/event_formatting_agent.rb

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

+ 1 - 1
app/models/agents/peak_detector_agent.rb

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

+ 2 - 1
app/models/agents/post_agent.rb

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

+ 1 - 1
app/models/agents/sentiment_agent.rb

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

+ 1 - 1
app/models/agents/translation_agent.rb

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

+ 1 - 1
app/models/agents/trigger_agent.rb

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

+ 2 - 1
app/models/agents/twilio_agent.rb

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

+ 1 - 1
app/models/agents/twitter_publish_agent.rb

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

+ 1 - 1
app/models/agents/twitter_stream_agent.rb

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

+ 1 - 1
app/models/agents/twitter_user_agent.rb

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

+ 1 - 1
app/models/agents/user_location_agent.rb

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

+ 1 - 1
app/models/agents/weather_agent.rb

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

+ 26 - 17
app/models/agents/website_agent.rb

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

+ 1 - 1
app/models/agents/weibo_publish_agent.rb

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

+ 1 - 1
app/models/agents/weibo_user_agent.rb

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

+ 1 - 0
app/models/user.rb

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

+ 3 - 2
app/views/agents/_form.html.erb

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

+ 1 - 1
app/views/agents/edit.html.erb

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

+ 55 - 37
app/views/agents/index.html.erb

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

+ 1 - 1
app/views/agents/new.html.erb

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

+ 72 - 30
app/views/agents/show.html.erb

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

+ 8 - 4
app/views/layouts/_messages.html.erb

@@ -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">&#215;</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">&#215;</a>
6
+        <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
7
+      </div>
8
+    <% end %>
5 9
   </div>
6 10
 <% end %>

+ 17 - 0
app/views/layouts/_navigation.html.erb

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

+ 10 - 0
app/views/layouts/application.html.erb

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

+ 30 - 0
app/views/logs/index.html.erb

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

+ 6 - 0
config/routes.rb

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

+ 13 - 0
db/migrate/20130819160603_create_agent_logs.rb

@@ -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 - 1
db/schema.rb

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

+ 4 - 0
lib/utils.rb

@@ -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 - 1
spec/controllers/events_controller_spec.rb

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

+ 37 - 0
spec/controllers/logs_controller_spec.rb

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

+ 15 - 0
spec/fixtures/agent_logs.yml

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

+ 77 - 0
spec/models/agent_log_spec.rb

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

+ 55 - 17
spec/models/agent_spec.rb

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

+ 1 - 1
spec/models/agents/adioso_agent_spec.rb

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

+ 22 - 5
spec/models/agents/website_agent_spec.rb

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

+ 0 - 0
vendor/assets/javascripts/.gitkeep


+ 0 - 0
vendor/assets/stylesheets/.gitkeep


+ 5 - 1
vendor/assets/stylesheets/jquery.json-editor.css.scss

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