Commit 1265b058af938c1083c9214a7bfa9687be060df3

Authored by Nathan Broadbent
2 parents 79cbe005 e5498764
Exists in master and in 1 other branch production

Merge remote branch 'boblail/ui-updates' into ui-updates

Conflicts:
	app/controllers/apps_controller.rb
	app/models/app.rb
	app/models/deploy.rb
	app/models/problem.rb
	app/views/notices/_backtrace.html.haml
	app/views/notices/_summary.html.haml
	public/stylesheets/application.css
	spec/models/problem_spec.rb
app/controllers/apps_controller.rb
@@ -3,15 +3,19 @@ class AppsController < InheritedResources::Base @@ -3,15 +3,19 @@ class AppsController < InheritedResources::Base
3 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update] 3 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
4 respond_to :html 4 respond_to :html
5 5
6 -  
7 def show 6 def show
8 respond_to do |format| 7 respond_to do |format|
9 format.html do 8 format.html do
10 @all_errs = !!params[:all_errs] 9 @all_errs = !!params[:all_errs]
11 10
  11 + @sort = params[:sort]
  12 + @order = params[:order]
  13 + @sort = "app" unless %w{message last_notice_at last_deploy_at count}.member?(@sort)
  14 + @order = "asc" unless (@order == "desc")
  15 +
12 @problems = resource.problems 16 @problems = resource.problems
13 @problems = @problems.unresolved unless @all_errs 17 @problems = @problems.unresolved unless @all_errs
14 - @problems = @problems.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) 18 + @problems = @problems.in_env(params[:environment]).ordered_by(@sort, @order).paginate(:page => params[:page], :per_page => current_user.per_page)
15 19
16 @selected_problems = params[:problems] || [] 20 @selected_problems = params[:problems] || []
17 @deploys = @app.deploys.order_by(:created_at.desc).limit(5) 21 @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
app/controllers/errs_controller.rb
@@ -9,7 +9,13 @@ class ErrsController < ApplicationController @@ -9,7 +9,13 @@ class ErrsController < ApplicationController
9 9
10 def index 10 def index
11 app_scope = current_user.admin? ? App.all : current_user.apps 11 app_scope = current_user.admin? ? App.all : current_user.apps
12 - @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered 12 +
  13 + @sort = params[:sort]
  14 + @order = params[:order]
  15 + @sort = "app" unless %w{message last_notice_at last_deploy_at count}.member?(@sort)
  16 + @order = "asc" unless (@order == "desc")
  17 +
  18 + @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered_by(@sort, @order)
13 @selected_problems = params[:problems] || [] 19 @selected_problems = params[:problems] || []
14 respond_to do |format| 20 respond_to do |format|
15 format.html do 21 format.html do
app/helpers/application_helper.rb
1 module ApplicationHelper 1 module ApplicationHelper
2 -  
3 - def user_agent_graph(error)  
4 - tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)}  
5 - create_percentage_table(tallies, :total => error.notices.count) 2 +
  3 +
  4 +
  5 + def message_graph(problem)
  6 + create_percentage_table_for(problem) {|notice| notice.message}
6 end 7 end
7 - 8 +
  9 +
  10 +
  11 + def user_agent_graph(problem)
  12 + create_percentage_table_for(problem) {|notice| pretty_user_agent(notice.user_agent)}
  13 + end
  14 +
8 def pretty_user_agent(user_agent) 15 def pretty_user_agent(user_agent)
9 (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" 16 (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}"
10 end 17 end
11 - 18 +
  19 +
  20 +
  21 + def tenant_graph(problem)
  22 + create_percentage_table_for(problem) {|notice| get_host(notice.request['url'])}
  23 + end
  24 +
  25 + def get_host(url)
  26 + uri = url && URI.parse(url)
  27 + uri.blank? ? "N/A" : uri.host
  28 + end
  29 +
  30 +
  31 +
  32 + def create_percentage_table_for(problem, &block)
  33 + tallies = tally(problem.notices, &block)
  34 + create_percentage_table_from_tallies(tallies, :total => problem.notices.count)
  35 + end
  36 +
12 def tally(collection, &block) 37 def tally(collection, &block)
13 collection.inject({}) do |tallies, item| 38 collection.inject({}) do |tallies, item|
14 value = yield item 39 value = yield item
@@ -16,18 +41,21 @@ module ApplicationHelper @@ -16,18 +41,21 @@ module ApplicationHelper
16 tallies 41 tallies
17 end 42 end
18 end 43 end
19 -  
20 - def create_percentage_table(tallies, options={}) 44 +
  45 + def create_percentage_table_from_tallies(tallies, options={})
21 total = (options[:total] || total_from_tallies(tallies)) 46 total = (options[:total] || total_from_tallies(tallies))
22 percent = 100.0 / total.to_f 47 percent = 100.0 / total.to_f
23 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ 48 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \
24 .sort {|a, b| a[0] <=> b[0]} 49 .sort {|a, b| a[0] <=> b[0]}
25 render :partial => "errs/tally_table", :locals => {:rows => rows} 50 render :partial => "errs/tally_table", :locals => {:rows => rows}
26 end 51 end
27 - 52 +
  53 +
28 def total_from_tallies(tallies) 54 def total_from_tallies(tallies)
29 tallies.values.inject(0) {|sum, n| sum + n} 55 tallies.values.inject(0) {|sum, n| sum + n}
30 end 56 end
31 private :total_from_tallies 57 private :total_from_tallies
  58 +
  59 +
32 end 60 end
33 61
app/helpers/sort_helper.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +# encoding: utf-8
  2 +module SortHelper
  3 +
  4 + def link_for_sort(name, field=nil)
  5 + field ||= name.underscore
  6 + current = (@sort == field)
  7 + order = (current && (@order == "asc")) ? "desc" : "asc"
  8 + url = request.path + "?sort=#{field}&order=#{order}"
  9 + options = {}
  10 + options.merge(:class => "current") if current
  11 + link_to(name, url, options)
  12 + end
  13 +
  14 +end
app/models/app.rb
@@ -28,6 +28,7 @@ class App @@ -28,6 +28,7 @@ class App
28 28
29 before_validation :generate_api_key, :on => :create 29 before_validation :generate_api_key, :on => :create
30 before_save :normalize_github_url 30 before_save :normalize_github_url
  31 + after_update :store_cached_attributes_on_problems
31 32
32 validates_presence_of :name, :api_key 33 validates_presence_of :name, :api_key
33 validates_uniqueness_of :name, :allow_blank => true 34 validates_uniqueness_of :name, :allow_blank => true
@@ -143,6 +144,10 @@ class App @@ -143,6 +144,10 @@ class App
143 144
144 protected 145 protected
145 146
  147 + def store_cached_attributes_on_problems
  148 + problems.each(&:cache_app_attributes)
  149 + end
  150 +
146 def generate_api_key 151 def generate_api_key
147 self.api_key ||= ActiveSupport::SecureRandom.hex 152 self.api_key ||= ActiveSupport::SecureRandom.hex
148 end 153 end
@@ -162,6 +167,5 @@ class App @@ -162,6 +167,5 @@ class App
162 self.github_url.gsub!(/github\.com:/, 'github.com/') 167 self.github_url.gsub!(/github\.com:/, 'github.com/')
163 self.github_url.gsub!(/\.git$/, '') 168 self.github_url.gsub!(/\.git$/, '')
164 end 169 end
165 -  
166 end 170 end
167 171
app/models/deploy.rb
@@ -14,6 +14,7 @@ class Deploy @@ -14,6 +14,7 @@ class Deploy
14 14
15 after_create :deliver_notification, :if => :should_notify? 15 after_create :deliver_notification, :if => :should_notify?
16 after_create :resolve_app_errs, :if => :should_resolve_app_errs? 16 after_create :resolve_app_errs, :if => :should_resolve_app_errs?
  17 + after_create :store_cached_attributes_on_problems
17 18
18 validates_presence_of :username, :environment 19 validates_presence_of :username, :environment
19 20
@@ -39,5 +40,8 @@ class Deploy @@ -39,5 +40,8 @@ class Deploy
39 app.resolve_errs_on_deploy? 40 app.resolve_errs_on_deploy?
40 end 41 end
41 42
  43 + def store_cached_attributes_on_problems
  44 + Problem.where(:app_id => app.id).each(&:cache_app_attributes)
  45 + end
42 end 46 end
43 47
app/models/problem.rb
@@ -7,40 +7,45 @@ class Problem @@ -7,40 +7,45 @@ class Problem
7 include Mongoid::Timestamps 7 include Mongoid::Timestamps
8 8
9 field :last_notice_at, :type => DateTime 9 field :last_notice_at, :type => DateTime
  10 + field :last_deploy_at, :type => Time
10 field :resolved, :type => Boolean, :default => false 11 field :resolved, :type => Boolean, :default => false
11 field :issue_link, :type => String 12 field :issue_link, :type => String
12 13
13 # Cached fields 14 # Cached fields
  15 + field :app_name, :type => String
14 field :notices_count, :type => Integer, :default => 0 16 field :notices_count, :type => Integer, :default => 0
15 field :message 17 field :message
16 field :environment 18 field :environment
17 field :klass 19 field :klass
18 field :where 20 field :where
19 21
20 - index :last_notice_at  
21 index :app_id 22 index :app_id
  23 + index :app_name
  24 + index :message
  25 + index :last_notice_at
  26 + index :last_deploy_at
  27 + index :notices_count
22 28
23 belongs_to :app 29 belongs_to :app
24 has_many :errs, :inverse_of => :problem, :dependent => :destroy 30 has_many :errs, :inverse_of => :problem, :dependent => :destroy
25 has_many :comments, :inverse_of => :err, :dependent => :destroy 31 has_many :comments, :inverse_of => :err, :dependent => :destroy
26 32
  33 + before_create :cache_app_attributes
  34 +
27 scope :resolved, where(:resolved => true) 35 scope :resolved, where(:resolved => true)
28 scope :unresolved, where(:resolved => false) 36 scope :unresolved, where(:resolved => false)
29 scope :ordered, order_by(:last_notice_at.desc) 37 scope :ordered, order_by(:last_notice_at.desc)
30 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} 38 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
31 39
  40 +
32 def self.in_env(env) 41 def self.in_env(env)
33 env.present? ? where(:environment => env) : scoped 42 env.present? ? where(:environment => env) : scoped
34 end 43 end
35 44
36 -  
37 -  
38 def notices 45 def notices
39 Notice.for_errs(errs).ordered 46 Notice.for_errs(errs).ordered
40 end 47 end
41 48
42 -  
43 -  
44 def resolve! 49 def resolve!
45 self.update_attributes!(:resolved => true) 50 self.update_attributes!(:resolved => true)
46 end 51 end
@@ -54,7 +59,6 @@ class Problem @@ -54,7 +59,6 @@ class Problem
54 end 59 end
55 60
56 61
57 -  
58 def self.merge!(*problems) 62 def self.merge!(*problems)
59 problems = problems.flatten.uniq 63 problems = problems.flatten.uniq
60 merged_problem = problems.shift 64 merged_problem = problems.shift
@@ -82,12 +86,32 @@ class Problem @@ -82,12 +86,32 @@ class Problem
82 end 86 end
83 87
84 88
  89 + def self.ordered_by(sort, order)
  90 + case sort
  91 + when "app"; order_by(["app_name", order])
  92 + when "message"; order_by(["message", order])
  93 + when "last_notice_at"; order_by(["last_notice_at", order])
  94 + when "last_deploy_at"; order_by(["last_deploy_at", order])
  95 + when "count"; order_by(["notices_count", order])
  96 + else raise("\"#{sort}\" is not a recognized sort")
  97 + end
  98 + end
  99 +
85 100
86 def reset_cached_attributes 101 def reset_cached_attributes
87 update_attribute(:notices_count, notices.count) 102 update_attribute(:notices_count, notices.count)
  103 + cache_app_attributes
88 cache_notice_attributes 104 cache_notice_attributes
89 end 105 end
90 106
  107 + def cache_app_attributes
  108 + if app
  109 + self.app_name = app.name
  110 + self.last_deploy_at = app.last_deploy_at
  111 + self.save if persisted?
  112 + end
  113 + end
  114 +
91 def cache_notice_attributes(notice=nil) 115 def cache_notice_attributes(notice=nil)
92 notice ||= notices.first 116 notice ||= notices.first
93 attrs = {:last_notice_at => notices.max(:created_at)} 117 attrs = {:last_notice_at => notices.max(:created_at)}
@@ -98,6 +122,5 @@ class Problem @@ -98,6 +122,5 @@ class Problem
98 :where => notice.where) if notice 122 :where => notice.where) if notice
99 update_attributes!(attrs) 123 update_attributes!(attrs)
100 end 124 end
101 -  
102 -  
103 end 125 end
  126 +
app/views/apps/index.html.haml
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 %tr 7 %tr
8 %th Name 8 %th Name
9 %th Last Deploy 9 %th Last Deploy
10 - %th Errs 10 + %th Errors
11 %tbody 11 %tbody
12 - @apps.each do |app| 12 - @apps.each do |app|
13 %tr 13 %tr
app/views/errs/_table.html.haml
@@ -3,11 +3,11 @@ @@ -3,11 +3,11 @@
3 %thead 3 %thead
4 %tr 4 %tr
5 %th 5 %th
6 - %th App  
7 - %th What &amp; Where  
8 - %th Latest  
9 - %th Deploy  
10 - %th Count 6 + %th= link_for_sort "App"
  7 + %th= link_for_sort "What &amp; Where".html_safe, "message"
  8 + %th= link_for_sort "Latest", "last_notice_at"
  9 + %th= link_for_sort "Deploy", "last_deploy_at"
  10 + %th= link_for_sort "Count"
11 %th Resolve 11 %th Resolve
12 %tbody 12 %tbody
13 - errs.each do |problem| 13 - errs.each do |problem|
@@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
24 = link_to problem.message, app_err_path(problem.app, problem) 24 = link_to problem.message, app_err_path(problem.app, problem)
25 %em= problem.where 25 %em= problem.where
26 %td.latest #{time_ago_in_words(last_notice_at problem)} ago 26 %td.latest #{time_ago_in_words(last_notice_at problem)} ago
27 - %td.deploy= problem.app.last_deploy_at ? problem.app.last_deploy_at.to_s(:micro) : 'n/a' 27 + %td.deploy= problem.last_deploy_at ? problem.last_deploy_at.to_s(:micro) : 'n/a'
28 %td.count= link_to problem.notices.count, app_err_path(problem.app, problem) 28 %td.count= link_to problem.notices.count, app_err_path(problem.app, problem)
29 %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(problem.app, problem), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if problem.unresolved? 29 %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(problem.app, problem), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if problem.unresolved?
30 - if errs.none? 30 - if errs.none?
app/views/errs/all.html.haml
1 -- content_for :title, 'All Errs' 1 +- content_for :title, 'All Errors'
2 - content_for :action_bar do 2 - content_for :action_bar do
3 = link_to 'hide resolved', errs_path, :class => 'button' 3 = link_to 'hide resolved', errs_path, :class => 'button'
4 -= render 'table', :errs => @problems  
5 \ No newline at end of file 4 \ No newline at end of file
  5 += render 'table', :errs => @problems
app/views/errs/index.html.haml
1 -- content_for :title, 'Unresolved Errs' 1 +- content_for :title, 'Unresolved Errors'
2 - content_for :head do 2 - content_for :head do
3 = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}" 3 = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}"
4 - content_for :action_bar do 4 - content_for :action_bar do
5 = link_to 'show resolved', all_errs_path, :class => 'button' 5 = link_to 'show resolved', all_errs_path, :class => 'button'
6 -= render 'table', :errs => @problems  
7 \ No newline at end of file 6 \ No newline at end of file
  7 += render 'table', :errs => @problems
app/views/layouts/application.html.haml
@@ -17,10 +17,8 @@ @@ -17,10 +17,8 @@
17 #header 17 #header
18 %div 18 %div
19 = link_to 'Errbit', root_path, :id => 'site-name' 19 = link_to 'Errbit', root_path, :id => 'site-name'
  20 + = render 'shared/navigation' if current_user
20 = render 'shared/session' 21 = render 'shared/session'
21 -  
22 - = render 'shared/navigation' if current_user  
23 -  
24 #content-wrapper 22 #content-wrapper
25 #content-title 23 #content-title
26 %h1= yield :title 24 %h1= yield :title
app/views/notices/_backtrace.html.haml
1 .window 1 .window
2 %table.backtrace 2 %table.backtrace
3 - %tr.padding  
4 - %th  
5 - %td  
6 - lines.each do |line| 3 - lines.each do |line|
7 - %tr  
8 - %th.line-numbers= line_number_with_link(@app, line)  
9 - %td.lines{:class => (Notice.in_app_backtrace_line?(line) ? 'in-app' : nil)}  
10 - = line['file'] 4 + - in_app = line['file'].gsub!('[PROJECT_ROOT]','') && !line['file'].match(/^\/vendor\//)
  5 + - path = File.dirname(line['file']) + '/'
  6 + - file = File.basename(line['file'])
  7 + %tr{:class => (in_app ? 'in-app' : nil)}
  8 + %td.line
  9 + %span.path>= path
  10 + %span.file= "#{file}:" << line_number_with_link(@app, line)
11 &rarr; 11 &rarr;
12 - %strong= line['method']  
13 - %tr.padding  
14 - %th  
15 - %td 12 + %span.method= line['method']
16 13
app/views/notices/_summary.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 %table.summary 2 %table.summary
3 %tr 3 %tr
4 %th Message 4 %th Message
5 - %td.main.nowrap= notice.message 5 + %td.main.nowrap= message_graph(notice.problem)
6 - if notice.request['url'].present? 6 - if notice.request['url'].present?
7 %tr 7 %tr
8 %th URL 8 %th URL
@@ -20,6 +20,9 @@ @@ -20,6 +20,9 @@
20 %th Browser 20 %th Browser
21 %td= user_agent_graph(notice.problem) 21 %td= user_agent_graph(notice.problem)
22 %tr 22 %tr
  23 + %th Tenant
  24 + %td= tenant_graph(notice.problem)
  25 + %tr
23 %th App Server 26 %th App Server
24 %td= notice.server_environment && notice.server_environment["hostname"] 27 %td= notice.server_environment && notice.server_environment["hostname"]
25 %tr 28 %tr
app/views/shared/_navigation.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 %ul 2 %ul
3 //%li= link_to 'Dashboard', admin_dashboard_path, :class => active_if_here(:dashboards) 3 //%li= link_to 'Dashboard', admin_dashboard_path, :class => active_if_here(:dashboards)
4 %li.apps{:class => active_if_here(:apps)}= link_to 'Apps', apps_path 4 %li.apps{:class => active_if_here(:apps)}= link_to 'Apps', apps_path
5 - %li.errs{:class => active_if_here(:errs)}= link_to 'Errs', errs_path 5 + %li.errs{:class => active_if_here(:errs)}= link_to 'Errors', errs_path
6 - if user_signed_in? && current_user.admin? 6 - if user_signed_in? && current_user.admin?
7 %li.users{:class => active_if_here(:users)}= link_to 'Users', users_path 7 %li.users{:class => active_if_here(:users)}= link_to 'Users', users_path
8 %div.clear 8 %div.clear
9 \ No newline at end of file 9 \ No newline at end of file
public/stylesheets/application.css
1 html { 1 html {
2 margin: 0; padding: 0; 2 margin: 0; padding: 0;
3 - color: #585858; background-color: #E2E2E2; 3 + color: #585858; background-color: #d0d0d0;
4 font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif; 4 font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif;
5 } 5 }
6 body { 6 body {
@@ -17,7 +17,7 @@ body { @@ -17,7 +17,7 @@ body {
17 .nowrap { white-space: nowrap; } 17 .nowrap { white-space: nowrap; }
18 18
19 /* Headings */ 19 /* Headings */
20 -h1, h2, h3, h4, h5, h6 { padding: 0.2em 0; margin-bottom: 1em; border-bottom: 1px solid #E2E2E2;} 20 +h1, h2, h3, h4, h5, h6 { padding: 0.2em 0; margin-bottom: 1em; border-bottom: 1px solid #dedede;}
21 h1 { font-size: 2.0em; line-height: 1.2em; text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;} 21 h1 { font-size: 2.0em; line-height: 1.2em; text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;}
22 h2 { font-size: 1.7em; line-height: 1.2em; } 22 h2 { font-size: 1.7em; line-height: 1.2em; }
23 h3 { font-size: 1.5em; line-height: 1.2em; } 23 h3 { font-size: 1.5em; line-height: 1.2em; }
@@ -42,20 +42,24 @@ a.action { float: right; font-size: 0.9em;} @@ -42,20 +42,24 @@ a.action { float: right; font-size: 0.9em;}
42 42
43 /* Header */ 43 /* Header */
44 #header { 44 #header {
45 - height: 75px;  
46 margin-bottom: 24px; 45 margin-bottom: 24px;
47 border-bottom: 1px solid #fff; 46 border-bottom: 1px solid #fff;
48 - background: #000 url(images/header.png) 0 0 repeat-x; 47 + background: black;
  48 + position:relative;
  49 +}
  50 +#header > div {
  51 + height:64px;
49 } 52 }
50 #header #site-name { 53 #header #site-name {
51 display: block; 54 display: block;
52 width: 88px; 55 width: 88px;
53 height: 31px; 56 height: 31px;
54 position: absolute; 57 position: absolute;
55 - top: 22px; 58 + top: 26px;
56 left: 2px; 59 left: 2px;
57 - background: transparent url(images/logo.png) 0 0 no-repeat;  
58 - text-indent: -5000em; 60 + font-size: 2.5em;
  61 + font-weight:bold;
  62 + color: white !important;
59 } 63 }
60 #header #session-links { 64 #header #session-links {
61 position: absolute; top: 18px; right: 0; 65 position: absolute; top: 18px; right: 0;
@@ -64,48 +68,49 @@ a.action { float: right; font-size: 0.9em;} @@ -64,48 +68,49 @@ a.action { float: right; font-size: 0.9em;}
64 #header #session-links li { 68 #header #session-links li {
65 float: right; 69 float: right;
66 margin-left: 10px; 70 margin-left: 10px;
67 - color: #FFF; 71 + color: #ccc;
68 background-color: #000; 72 background-color: #000;
69 - border-radius: 6px;  
70 - -moz-border-radius: 6px;  
71 - -webkit-border-radius: 6px; 73 + border-radius: 30px;
  74 + -moz-border-radius: 30px;
  75 + -webkit-border-radius: 30px;
72 border: 1px solid #484B4F; 76 border: 1px solid #484B4F;
73 font-size: 14px; 77 font-size: 14px;
74 - font-weight: bold;  
75 } 78 }
76 #header #session-links li:hover { 79 #header #session-links li:hover {
77 box-shadow: 0 0 3px #69c; 80 box-shadow: 0 0 3px #69c;
78 -moz-box-shadow: 0 0 3px #69c; 81 -moz-box-shadow: 0 0 3px #69c;
79 -webkit-box-shadow: 0 0 3px #69c; 82 -webkit-box-shadow: 0 0 3px #69c;
80 } 83 }
  84 +#header #session-links li:hover a {
  85 + color: white;
  86 +}
81 #header #session-links a { 87 #header #session-links a {
82 - color: #FFF;  
83 - padding: 0 10px 0 30px;  
84 - line-height: 37px; 88 + color: #ccc;
  89 + padding: 0 14px;
  90 + line-height: 30px;
85 } 91 }
86 #header #session-links a:hover { 92 #header #session-links a:hover {
87 text-decoration: none; 93 text-decoration: none;
88 } 94 }
89 -#header #session-links #sign-out {  
90 - background: transparent url(images/icons/bullet-red-sm.png) 12px 50% no-repeat;  
91 -}  
92 -#header #session-links #edit-profile {  
93 - padding-left: 10px;  
94 -}  
95 95
96 /* Navigation */ 96 /* Navigation */
97 #nav-bar { 97 #nav-bar {
98 - margin-bottom: 24px;  
99 - height: 41px; 98 + position: absolute;
  99 + bottom: 0;
  100 + left: 128px;
100 } 101 }
101 #nav-bar li { 102 #nav-bar li {
102 float: left; 103 float: left;
103 - margin-right: 18px; 104 + height: 38px;
  105 + margin-right: 12px;
104 color: #666; 106 color: #666;
105 background: #FFF url(images/button-bg.png) 0 bottom repeat-x; 107 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
106 - border-radius: 50px;  
107 - -moz-border-radius: 50px;  
108 - -webkit-border-radius: 50px; 108 + border-top-left-radius: 12px;
  109 + border-top-right-radius: 12px;
  110 + -moz-border-top-left-radius: 12px;
  111 + -moz-border-top-right-radius: 12px;
  112 + -webkit-border-top-left-radius: 12px;
  113 + -webkit-border-top-right-radius: 12px;
109 border: 1px solid #bbb; 114 border: 1px solid #bbb;
110 } 115 }
111 #nav-bar li a { 116 #nav-bar li a {
@@ -120,18 +125,18 @@ a.action { float: right; font-size: 0.9em;} @@ -120,18 +125,18 @@ a.action { float: right; font-size: 0.9em;}
120 #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); } 125 #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); }
121 #nav-bar li.errs a { background-image: url(images/icons/error.png); } 126 #nav-bar li.errs a { background-image: url(images/icons/error.png); }
122 #nav-bar li.users a { background-image: url(images/icons/user.png); } 127 #nav-bar li.users a { background-image: url(images/icons/user.png); }
123 -#nav-bar li:hover { 128 +#nav-bar li:not(.active):hover {
124 box-shadow: 0 0 3px #69c; 129 box-shadow: 0 0 3px #69c;
125 -moz-box-shadow: 0 0 3px #69c; 130 -moz-box-shadow: 0 0 3px #69c;
126 -webkit-box-shadow: 0 0 3px #69c; 131 -webkit-box-shadow: 0 0 3px #69c;
127 } 132 }
128 #nav-bar li.active { 133 #nav-bar li.active {
129 border-color: #fff; 134 border-color: #fff;
130 - background-color: #CCC; 135 + background-color: #d0d0d0;
131 background-image: none; 136 background-image: none;
132 - box-shadow: inset 0 0 5px #999;  
133 - -moz-box-shadow: inset 0 0 5px #999;  
134 - -webkit-box-shadow: inset 0 0 5px #999; 137 + border-width:1px 1px 0;
  138 + margin-bottom:-2px;
  139 + height:40px;
135 } 140 }
136 141
137 /* Content Wrapper */ 142 /* Content Wrapper */
@@ -144,7 +149,7 @@ a.action { float: right; font-size: 0.9em;} @@ -144,7 +149,7 @@ a.action { float: right; font-size: 0.9em;}
144 padding: 30px 20px; 149 padding: 30px 20px;
145 border-top: 1px solid #FFF; 150 border-top: 1px solid #FFF;
146 border-bottom: 1px solid #FFF; 151 border-bottom: 1px solid #FFF;
147 - background-color: #e2e2e2; 152 + background-color: #ececec;
148 } 153 }
149 #content-comments { 154 #content-comments {
150 background-color: #ffffff; 155 background-color: #ffffff;
@@ -296,7 +301,7 @@ form input[type=submit].button { @@ -296,7 +301,7 @@ form input[type=submit].button {
296 font-size: 1em; 301 font-size: 1em;
297 text-transform: none; 302 text-transform: none;
298 } 303 }
299 -form div.buttons { 304 +form div.buttons {
300 color: #666; 305 color: #666;
301 background: #FFF url(images/button-bg.png) 0 bottom repeat-x; 306 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
302 border-radius: 50px; 307 border-radius: 50px;
@@ -327,11 +332,6 @@ form div.buttons button.sign_in { @@ -327,11 +332,6 @@ form div.buttons button.sign_in {
327 padding-left: 40px; 332 padding-left: 40px;
328 background: transparent url(images/icons/right-arrow.png) 3px 3px no-repeat; 333 background: transparent url(images/icons/right-arrow.png) 3px 3px no-repeat;
329 } 334 }
330 -form > div > span {  
331 - display: block; margin-top: 0.5em;  
332 - font-size: 0.85em;  
333 - color: #787878;  
334 -}  
335 form strong.option { 335 form strong.option {
336 display: block; 336 display: block;
337 margin: 0.7em 0; 337 margin: 0.7em 0;
@@ -386,15 +386,18 @@ table thead th { @@ -386,15 +386,18 @@ table thead th {
386 border-top: 1px solid #FFF; 386 border-top: 1px solid #FFF;
387 border-bottom: 1px solid #FFF; 387 border-bottom: 1px solid #FFF;
388 } 388 }
389 -table tbody tr:first-child td {  
390 - border-top: 1px solid #C6C6C6;  
391 -}  
392 table th, table td { 389 table th, table td {
393 border-top: 1px solid #C6C6C6; 390 border-top: 1px solid #C6C6C6;
394 padding: 10px 8px; 391 padding: 10px 8px;
395 text-align: left; 392 text-align: left;
396 } 393 }
397 -table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; } 394 +table tbody tr:first-child th, table tbody tr:first-child td {
  395 + border-top: none;
  396 +}
  397 +table thead + tbody tr:first-child td {
  398 + border-top: 1px solid #C6C6C6;
  399 +}
  400 +table th { background-color: #ececec; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
398 table tbody tr:nth-child(odd) td { background-color: #F9F9F9; } 401 table tbody tr:nth-child(odd) td { background-color: #F9F9F9; }
399 table .main { width: 100%; } 402 table .main { width: 100%; }
400 403
@@ -431,56 +434,9 @@ pre { @@ -431,56 +434,9 @@ pre {
431 margin: 24px 0; 434 margin: 24px 0;
432 text-align: center; 435 text-align: center;
433 } 436 }
434 -.pagination a, .pagination em, .pagination span {  
435 - display: inline-block;  
436 - padding: 0 0.8em;  
437 - margin: 0 0.2em;  
438 - line-height: 30px;  
439 - color: #666;  
440 - background: #FFF url(images/button-bg.png) 0 50% repeat-x;  
441 - border: 1px solid #BBB;  
442 - border-radius: 5px;  
443 - -moz-border-radius: 5px;  
444 - -webkit-border-radius: 5px;  
445 -}  
446 -.pagination a:hover {  
447 - text-decoration: none !important;  
448 - box-shadow: 0px 0px 4px #69C;  
449 - -moz-box-shadow: 0px 0px 4px #69C;  
450 - -webkit-box-shadow: 0px 0px 4px #69C  
451 -}  
452 -.pagination .previous_page, .pagination .next_page {  
453 - padding: 0 1em;  
454 - margin: 0 2.5em 0 0;  
455 - line-height: 39px;  
456 - border-radius: 39px;  
457 - -moz-border-radius: 39px;  
458 - -webkit-border-radius: 39px;  
459 -}  
460 -.pagination .next_page {  
461 - margin: 0 0 0 2.5em;  
462 -}  
463 -.pagination .disabled {  
464 - opacity: 0.5;  
465 - -moz-opacity: 0.5;  
466 - -webkit-opacity: 0.5;  
467 - cursor: no-drop;  
468 -}  
469 .pagination em { 437 .pagination em {
470 - padding: 0em 0.6em;  
471 - line-height: 26px;  
472 - background-color: #CCC;  
473 - background-image: none;  
474 - border-color: #FFF;  
475 - box-shadow: inset 0 0 5px #999;  
476 - -moz-box-shadow: inset 0 0 5px #999;  
477 - -webkit-box-shadow: inset 0 0 5px #999;  
478 font-style: normal; 438 font-style: normal;
479 -}  
480 -.pagination .gap {  
481 - padding: 0 0.4em;  
482 - border: none;  
483 - background: none; 439 + font-weight: bold;
484 } 440 }
485 441
486 /* Buttons */ 442 /* Buttons */
@@ -495,10 +451,9 @@ a.button { @@ -495,10 +451,9 @@ a.button {
495 border-radius: 30px; 451 border-radius: 30px;
496 -moz-border-radius: 30px; 452 -moz-border-radius: 30px;
497 -webkit-border-radius: 30px; 453 -webkit-border-radius: 30px;
498 - box-shadow: inset 0px 0px 4px #FFF;  
499 - -moz-box-shadow: inset 0px 0px 4px #FFF;  
500 - -webkit-box-shadow: inset 0px 0px 4px #FFF;  
501 line-height: 30px; 454 line-height: 30px;
  455 + min-width: 54px;
  456 + text-align: center;
502 } 457 }
503 input[type="submit"]:hover.button, 458 input[type="submit"]:hover.button,
504 a:hover.button { 459 a:hover.button {
@@ -512,26 +467,36 @@ a.button.active { @@ -512,26 +467,36 @@ a.button.active {
512 border-color: #fff; 467 border-color: #fff;
513 background-color: #CCC; 468 background-color: #CCC;
514 background-image: none; 469 background-image: none;
515 - box-shadow: inset 0 0 5px #999;  
516 - -moz-box-shadow: inset 0 0 5px #999;  
517 - -webkit-box-shadow: inset 0 0 5px #999;  
518 } 470 }
519 471
520 472
521 /* Tab Bar */ 473 /* Tab Bar */
522 .tab-bar { 474 .tab-bar {
523 - margin-bottom: 24px;  
524 - background-color: #E2E2E2;  
525 - border: 1px solid #BBB; 475 + margin-top: 12px;
  476 +}
  477 +#content .tab-bar a.button {
  478 + border-bottom:0;
  479 + margin-bottom:0;
  480 + border-top-left-radius:12px;
  481 + border-top-right-radius:12px;
  482 + border-bottom-left-radius:0;
  483 + border-bottom-right-radius:0;
  484 + height:30px;
  485 +}
  486 +#content .tab-bar a.button.active {
  487 + background-color:white;
  488 + border-color:#ccc;
  489 + border-style:solid;
  490 + border-width:1px 1px 0;
  491 + margin-bottom:-1px;
  492 + height:31px;
526 } 493 }
527 .tab-bar ul { 494 .tab-bar ul {
528 - padding: 9px 12px;  
529 - border-top: 1px solid #FFF;  
530 - border-bottom: 1px solid #FFF; 495 + padding: 9px 0 0;
  496 + line-height:0;
531 } 497 }
532 .tab-bar li { 498 .tab-bar li {
533 display: inline-block; 499 display: inline-block;
534 - margin-right: 14px;  
535 } 500 }
536 501
537 /* Watchers and Issue Tracker Forms */ 502 /* Watchers and Issue Tracker Forms */
@@ -586,6 +551,10 @@ table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: # @@ -586,6 +551,10 @@ table.apps tbody tr:hover td ,table.errs tbody tr:hover td { background-color: #
586 table.apps td.name, table.errs td.message { 551 table.apps td.name, table.errs td.message {
587 width: 100%; 552 width: 100%;
588 } 553 }
  554 +td.message .line {
  555 + display:inline-block;
  556 + margin-left:1em;
  557 +}
589 td.deploy { 558 td.deploy {
590 white-space: nowrap; 559 white-space: nowrap;
591 } 560 }
@@ -687,44 +656,26 @@ table.deploys td.when { @@ -687,44 +656,26 @@ table.deploys td.when {
687 width: 100%; 656 width: 100%;
688 margin-bottom: 1em; 657 margin-bottom: 1em;
689 overflow: auto; 658 overflow: auto;
  659 + border:1px solid #ccc;
  660 + padding:1px;
690 } 661 }
691 662
692 .window table { 663 .window table {
693 margin: 0; 664 margin: 0;
694 } 665 }
695 666
696 -table.backtrace td, table.backtrace th {  
697 - border-top: none;  
698 -}  
699 -table.backtrace td {  
700 - width: 100%;  
701 - padding: 0;  
702 - margin: 0;  
703 - color: #C7C7C7;  
704 - background-color: #222; 667 +table.backtrace td.line {
  668 + color: #777; /* 47% */
  669 + background-color: #222 !important;
  670 + border: none;
  671 + padding: 1px 8px;
705 } 672 }
706 -  
707 -/* remove alternating color rules */  
708 -table.backtrace tr:nth-child(2n+1) td { background-color: #222; }  
709 -table.backtrace tr:first-child td { border-top: 0; }  
710 -  
711 -table.backtrace th.line-numbers {  
712 - border-bottom: 1px solid #F0F0F0;  
713 - font-size: 13px;  
714 - text-align: right;  
715 - vertical-align: top;  
716 - padding: 1px 6px 1px 7px; 673 +table.backtrace td.line .file {
  674 + color:#d2d2d2; /* 82% */
717 } 675 }
718 -  
719 -table.backtrace td.lines {  
720 - font-size: 13px;  
721 - padding-left: 8px;  
722 - vertical-align: top;  
723 - white-space: nowrap;  
724 -}  
725 -table.backtrace td.lines.in-app {  
726 - color: #2adb2e;  
727 - background-color: #2f2f2f; 676 +table.backtrace td.line .method {
  677 + color:#ececec; /* 93% */
  678 + font-weight:bold;
728 } 679 }
729 680
730 /* Extra empty rows at top and bottom of table */ 681 /* Extra empty rows at top and bottom of table */
@@ -789,3 +740,7 @@ table.errs tr td.message .inline_comment em.commenter { @@ -789,3 +740,7 @@ table.errs tr td.message .inline_comment em.commenter {
789 color: #888888; 740 color: #888888;
790 } 741 }
791 742
  743 +table.backtrace tr.in-app td.line { color:#156E16; /* 43% */}
  744 +table.backtrace tr.in-app td.line .file { color:#25C227; /* 76% */}
  745 +table.backtrace tr.in-app td.line .method { color:#2adb2e; /* 86% */}
  746 +
spec/models/problem_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe Problem do 3 describe Problem do
4 -  
5 -  
6 context '#last_notice_at' do 4 context '#last_notice_at' do
7 it "returns the created_at timestamp of the latest notice" do 5 it "returns the created_at timestamp of the latest notice" do
8 err = Factory(:err) 6 err = Factory(:err)
@@ -131,7 +129,6 @@ describe Problem do @@ -131,7 +129,6 @@ describe Problem do
131 129
132 130
133 context "notice counter cache" do 131 context "notice counter cache" do
134 -  
135 before do 132 before do
136 @app = Factory(:app) 133 @app = Factory(:app)
137 @problem = Factory(:problem, :app => @app) 134 @problem = Factory(:problem, :app => @app)
@@ -158,5 +155,46 @@ describe Problem do @@ -158,5 +155,46 @@ describe Problem do
158 end 155 end
159 156
160 157
  158 + context "#app_name" do
  159 + before do
  160 + @app = Factory(:app)
  161 + end
  162 +
  163 + it "is set when a problem is created" do
  164 + problem = Factory(:problem, :app => @app)
  165 + assert_equal @app.name, problem.app_name
  166 + end
  167 +
  168 + it "is updated when an app is updated" do
  169 + problem = Factory(:problem, :app => @app)
  170 + lambda {
  171 + @app.update_attributes!(:name => "Bar App")
  172 + problem.reload
  173 + }.should change(problem, :app_name).to("Bar App")
  174 + end
  175 + end
  176 +
  177 +
  178 + context "#last_deploy_at" do
  179 + before do
  180 + @app = Factory(:app)
  181 + @last_deploy = 10.days.ago.localtime.round(0)
  182 + deploy = Factory(:deploy, :app => @app, :created_at => @last_deploy)
  183 + end
  184 +
  185 + it "is set when a problem is created" do
  186 + problem = Factory(:problem, :app => @app)
  187 + assert_equal @last_deploy, problem.last_deploy_at
  188 + end
  189 +
  190 + it "is updated when a deploy is created" do
  191 + problem = Factory(:problem, :app => @app)
  192 + next_deploy = 5.minutes.ago.localtime.round(0)
  193 + lambda {
  194 + @deploy = Factory(:deploy, :app => @app, :created_at => next_deploy)
  195 + problem.reload
  196 + }.should change(problem, :last_deploy_at).from(@last_deploy).to(next_deploy)
  197 + end
  198 + end
161 end 199 end
162 200