logstats 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ scratch/
5
+ logstats.html
data/.rvmrc ADDED
@@ -0,0 +1,7 @@
1
+
2
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
3
+ && -s "${rvm_path:-$HOME/.rvm}/environments/ruby-1.8.7-p299@logstats" ]] ; then
4
+ \. "${rvm_path:-$HOME/.rvm}/environments/ruby-1.8.7-p299@logstats"
5
+ else
6
+ rvm --create use "ruby-1.8.7-p299@logstats"
7
+ fi
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in logstats.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,21 @@
1
+ GIT
2
+ remote: git://github.com/jstirk/tail_from_sentinel.git
3
+ revision: def3003aa61a46dfd239cad938700d8ad26c2988
4
+ specs:
5
+ tail_from_sentinel (0.0.1)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ haml (3.0.25)
11
+ ruby-fsevent (0.2.1)
12
+ watchr (0.7)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ haml
19
+ ruby-fsevent
20
+ tail_from_sentinel!
21
+ watchr
data/README ADDED
@@ -0,0 +1,71 @@
1
+ LogStats
2
+ ========
3
+
4
+ Generates a simple HTML file based upon my custom timesheet format.
5
+
6
+ I can then embed this on my OSX Dashboard to be able to see how I'm
7
+ progressing throughout the day/week/month.
8
+
9
+ It's probably not so useful to you, unless you happen to like my
10
+ worklog format.
11
+
12
+ I run the script every 5 minutes with cron so as that the HTML stays
13
+ up-to-date. The page is set to auto-refresh every 2.5 minutes.
14
+
15
+ The colours change when I have done at least 5 hours of work per day,
16
+ and 25 hours per week.
17
+
18
+ Requirements
19
+ ============
20
+ * HAML
21
+ * tail_from_sentinel
22
+
23
+ Usage
24
+ =====
25
+
26
+ logstats INPUTFILE.txt OUTPUTFILE.html
27
+
28
+
29
+ Worklog Format
30
+ ==============
31
+
32
+ I use a text based worklog format for all my timekeeping.
33
+
34
+ This is a simple text file that sits in my Dropbox, and it symlinked to
35
+ my home. I keep it open in an editor all day so as that I can quickly jot
36
+ down when my task changes.
37
+
38
+ It works great for unexpected interruptions, as I can quickly update it
39
+ when I am back, or even while I am on the phone (only need to type the time
40
+ I answer the call).
41
+
42
+ The format looks like this :
43
+
44
+ 01Jan2011
45
+ 0900 ABC 1234567 1012
46
+ 1015 ABC 9876543 1100
47
+ 1100 ABC Deploy 1110
48
+
49
+ 03Jan
50
+ 1200 XY1 2345678 1230
51
+
52
+ Each day has a simple header in DDMMMYYYY format, where YYYY is
53
+ optional where it is the same as the record before.
54
+
55
+ Within the day, each line follows a simple format :
56
+ * the start time (in 24hr)
57
+
58
+ * a message, which starts with a 3-character project code (if billable).
59
+ I usually follow this with a Pivotal Tracker Story ID, or a note
60
+ about what I was doing.
61
+ If it's not billable work, I might use this space to log an
62
+ unexpected call for instance.
63
+ This field is freeform, with the project picked up if it's there.
64
+
65
+ * the end time (in 24hr). It's fine for this to be in the next day.
66
+ Eg: 2330 ABC allnighter 0230
67
+
68
+ TODO
69
+ ====
70
+ * Tests!
71
+ * Make it more customizable. Not everyone has the same thresholds that I keep.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/logstats ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logstats'
4
+
5
+ ls=LogStats::Base.new(ARGV[0], ARGV[1])
6
+ ls.generate!
@@ -0,0 +1,128 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title Log Stats @ 2011-01-18 07:09
5
+ %style
6
+ :sass
7
+ $background_color_light: #454342
8
+ $background_color_dark: #333335
9
+ $text_color: white
10
+ $link_color: #a9c743
11
+ $border_color: #272b3b
12
+
13
+ body
14
+ :font-size 62.5%
15
+ :font-family Helvetica, sans-serif
16
+ :background $background_color_light
17
+ :color $text_color
18
+
19
+ #logstats
20
+ :width 400px
21
+ :height 241px
22
+ :background $background_color_dark
23
+ :border 1px solid $border_color
24
+ :text-align center
25
+
26
+ h1
27
+ :font-size 120%
28
+ :text-decoration underline
29
+ :font-variant small-caps
30
+ :text-align center
31
+ :margin 0 0 10px 0
32
+ :padding 0
33
+
34
+ ul
35
+ :list-style-type none
36
+ :margin 0
37
+ :padding 0
38
+ :text-align left
39
+
40
+ li
41
+ :margin 0
42
+ :padding 0
43
+
44
+ .duration
45
+ :font-size 200%
46
+ :color #cacaca
47
+
48
+ span
49
+ :color $text_color
50
+
51
+ .remaining, .average
52
+ :margin-top 5px
53
+
54
+ .recent, .history
55
+ :width 200px
56
+ :float left
57
+
58
+ .recent
59
+ .current, .today
60
+ :padding 5px
61
+
62
+ .current
63
+ :height 50px
64
+ :border-bottom 1px solid $border-color
65
+
66
+ .history
67
+ .today, .week, .month
68
+ :height 70px
69
+ :padding 5px
70
+ :border-left 1px solid $border_color
71
+
72
+ .today, .week
73
+ :border-bottom 1px solid $border-color
74
+
75
+
76
+ %body
77
+ #logstats
78
+ .recent
79
+ .current
80
+ %h1 Current
81
+ %span.duration
82
+ %span.minute 45
83
+ min
84
+ .today
85
+ %h1 Projects
86
+ %ul.projects
87
+ %li
88
+ %span.project MB1
89
+ %span.duration
90
+ %span.hour 1
91
+ hr
92
+ %span.minute 57
93
+ min
94
+ %li
95
+ %span.project MY1
96
+ %span.duration
97
+ %span.hour 2
98
+ hr
99
+ %span.minute 30
100
+ min
101
+ .history
102
+ .today
103
+ %h1 Today
104
+ %span.duration
105
+ %span.hour 5
106
+ hr
107
+ %span.minute 57
108
+ min
109
+ .remaining
110
+ (target met!)
111
+ .week
112
+ %h1 Week
113
+ %span.duration
114
+ %span.hour 5
115
+ hr
116
+ %span.minute 57
117
+ min
118
+ .remaining
119
+ (20 hr 3 min remaining)
120
+ .month
121
+ %h1 Month
122
+ %span.duration
123
+ %span.hour 35
124
+ hr
125
+ %span.minute 57
126
+ min
127
+ .average
128
+ (6 hr 12 min average)
@@ -0,0 +1,46 @@
1
+ module LogStats
2
+ module Helpers
3
+ # NOTE: Because of the way I'm calling this (as the HAML context) these all need to be class methods
4
+
5
+ # Turns a number of seconds into a pretty HTML string
6
+ def self.time_to_html(seconds, options={})
7
+ hrs=(seconds / 3600).floor
8
+ min=(seconds % 3600).floor / 60
9
+ o=[]
10
+ o << "<span class=\"hour\">#{hrs}</span> hr" if hrs.to_i > 0
11
+ o << "<span class=\"minute\">#{min}</span> min" if min.to_i > 0
12
+ o.join(' ')
13
+ end
14
+
15
+ # Options:
16
+ # :class : The CSS class to apply to the top element (default: duration)
17
+ def self.duration_tag(seconds, options={})
18
+ options[:class]='duration' if options[:class].nil?
19
+ "<span class=\"#{options[:class]}\">#{self.time_to_html(seconds)}</span>"
20
+ end
21
+
22
+ def self.remaining_tag(seconds_so_far, period)
23
+ seconds_required=case period
24
+ when :day
25
+ # 5 hrs per day
26
+ 5 * 3600
27
+ when :week
28
+ # 5 hrs per day, 5 days per week
29
+ 5 * 5 * 3600
30
+ else
31
+ raise "Unknown period: #{period}"
32
+ end
33
+
34
+ seconds=seconds_required - seconds_so_far
35
+
36
+ css_class=[ 'remaining ']
37
+ if seconds.nil? || seconds < 0 then
38
+ content=self.time_to_html(seconds.abs) + ' over!'
39
+ css_class << 'met'
40
+ else
41
+ content=self.time_to_html(seconds) + ' remaining'
42
+ end
43
+ "<div class=\"#{css_class.join(' ')}\">(#{content})</div>"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,123 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title Log Stats @ #{Time.now.strftime('%a, %e %b %Y %H:%M')}
5
+ %meta{ "http-equiv" => "refresh", :content => "150" }
6
+ %style
7
+ :sass
8
+ $background_color_light: #454342
9
+ $background_color_dark: #333335
10
+ $text_color: white
11
+ $text_faded_color: #cacaca
12
+ $link_color: #a9c743
13
+ $border_color: #272b3b
14
+ $success_color: #a9c743
15
+ $success_faded_color: #7D9231
16
+
17
+ body
18
+ :font-size 62.5%
19
+ :font-family Helvetica, sans-serif
20
+ :background $background_color_light
21
+ :color $text_color
22
+
23
+ #logstats
24
+ :width 400px
25
+ :height 241px
26
+ :background $background_color_dark
27
+ :border 1px solid $border_color
28
+ :text-align center
29
+
30
+ h1
31
+ :font-size 120%
32
+ :text-decoration underline
33
+ :font-variant small-caps
34
+ :text-align center
35
+ :margin 0
36
+ :padding 0
37
+
38
+ ul
39
+ :list-style-type none
40
+ :margin 0
41
+ :padding 0
42
+ :text-align left
43
+
44
+ li
45
+ :margin 0
46
+ :padding 0
47
+
48
+ .duration
49
+ :font-size 200%
50
+ :color $text_faded_color
51
+ :line-height 40px
52
+
53
+ span
54
+ :color $text_color
55
+
56
+ .met
57
+ :color $success_color
58
+
59
+ .duration
60
+ :color $success_faded_color
61
+
62
+ span
63
+ :color $success_color
64
+
65
+ //.remaining, .average
66
+
67
+ .recent, .history
68
+ :width 200px
69
+ :float left
70
+
71
+ .recent
72
+ .current, .today
73
+ :padding 5px
74
+
75
+ .current
76
+ :height 50px
77
+ :border-bottom 1px solid $border-color
78
+
79
+ .today ul
80
+ .duration
81
+ :line-height 30px
82
+
83
+ .history
84
+ .today, .week, .month
85
+ :height 70px
86
+ :padding 5px
87
+ :border-left 1px solid $border_color
88
+
89
+ .today, .week
90
+ :border-bottom 1px solid $border-color
91
+
92
+
93
+ %body
94
+ #logstats
95
+ .recent
96
+ .current
97
+ %h1 Current Task
98
+ - if current then
99
+ = duration_tag(current)
100
+ - else
101
+ N/A
102
+ .today
103
+ %h1 Projects
104
+ %ul.projects
105
+ - today[:projects].each do |project, time|
106
+ %li
107
+ %span.project= project
108
+ = duration_tag(time)
109
+ .history
110
+ .today{ :class => (today[:remaining].nil? ? 'met' : nil )}
111
+ %h1 Today
112
+ = duration_tag(today[:total])
113
+ = remaining_tag(today[:total], :day)
114
+
115
+ .week{ :class => (week[:remaining].nil? ? 'met' : nil )}
116
+ %h1 Week
117
+ = duration_tag(week[:total])
118
+ = remaining_tag(week[:total], :week)
119
+ .month
120
+ %h1 Month
121
+ = duration_tag(month[:total])
122
+ .average
123
+ (#{time_to_html(month[:average])} average)
@@ -0,0 +1,3 @@
1
+ module Logstats
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,154 @@
1
+ require 'tail_from_sentinel'
2
+ require 'time'
3
+ require 'date'
4
+
5
+ # Parses my own WorkLog file format and extracts the stats for the past month
6
+ module LogStats
7
+ class WorkLog < TailFromSentinel::Base
8
+ DATE_SENTINEL_REGEX=/(\d+)([A-Z]+)(\d{4})?$/i
9
+ WORKLOG_RECORD_REGEX=/(\d{2})(\d{2}) (.*) (\d{2})(\d{2})$/i
10
+ OPEN_WORKLOG_RECORD_REGEX=/(\d{2})(\d{2}) (.*)$/i
11
+
12
+ DATE_SENTINEL_PROC=Proc.new do |line|
13
+ if line =~ DATE_SENTINEL_REGEX then
14
+ # It's a date marker - parse it to see whether it's close enough to when we want
15
+ day,month,year=self.parse_sentinel($1, $2, $3)
16
+ (month.downcase == self.now.strftime('%b').downcase) && (year.to_i == self.now.year)
17
+ end
18
+ end
19
+
20
+ def self.now
21
+ @@now ||= Time.now
22
+ end
23
+
24
+ def self.last_seen_year
25
+ @@last_seen_year
26
+ end
27
+
28
+ def self.last_seen_year=(year)
29
+ @@last_seen_year=year
30
+ end
31
+
32
+ def self.parse_sentinel(d,m,y)
33
+ year=y || self.last_seen_year
34
+ self.last_seen_year=year
35
+ [ d, m, year ]
36
+ end
37
+
38
+ def initialize(filename)
39
+ @last_seen_year=nil
40
+ @stas=nil
41
+ super(File.open(filename, 'r'), &DATE_SENTINEL_PROC)
42
+ end
43
+
44
+ # TODO: Document this
45
+ def stats
46
+ return @stats unless @stats.nil?
47
+
48
+ days={}
49
+ now=WorkLog.now
50
+ current_time=nil
51
+
52
+ data.each do |line|
53
+ line=line.strip
54
+ case line
55
+ when DATE_SENTINEL_REGEX
56
+ # It's a date marker - set current_time
57
+ current_time=Time.parse(WorkLog.parse_sentinel($1,$2,$3).reverse.join('-'))
58
+ days[current_time]={ :total => 0, :projects => {} }
59
+
60
+ when WORKLOG_RECORD_REGEX
61
+ # It's a worklog record - parse it, and add it to the relevant buckets
62
+ duration, project=parse_worklog_record(current_time, $1, $2, $3, $4, $5)
63
+
64
+ days[current_time][:total] += duration
65
+ days[current_time][:projects][project] ||= 0
66
+ days[current_time][:projects][project] += duration
67
+
68
+ when OPEN_WORKLOG_RECORD_REGEX
69
+ duration, project=parse_worklog_record(current_time, $1, $2, $3)
70
+ days[:current]=duration
71
+
72
+ when /^\s*$/
73
+ # Blank line - ignore
74
+ else
75
+ puts "Warning: Unknown format of \"#{line}\""
76
+ end
77
+ end
78
+ @stats=compile_day_data(days)
79
+ end
80
+
81
+ def parse_worklog_record(current_time, sh,sm,msg,eh=nil,em=nil)
82
+ start_time=current_time + (sh.to_i * 3600) + (sm.to_i * 60)
83
+
84
+ if eh && em then
85
+ end_time=current_time + (eh.to_i * 3600) + (em.to_i * 60)
86
+ end_time += (24 * 3600) if end_time < start_time
87
+ else
88
+ end_time=Time.now
89
+ end
90
+ duration=end_time - start_time
91
+
92
+ if msg.match(/^([A-Z0-9]{3})/) then
93
+ project=$1
94
+ else
95
+ project='MISC'
96
+ end
97
+ [ duration, project ]
98
+ end
99
+
100
+ def compile_day_data(days)
101
+ stats={ :current => days[:current],
102
+ :today => { :total => 0,
103
+ :projects => { }
104
+ },
105
+ :week => { :total => 0,
106
+ :average => 0,
107
+ :projects => { }
108
+ },
109
+ :month => { :total => 0,
110
+ :average => 0,
111
+ :days_logged => 0,
112
+ :projects => { }
113
+ }
114
+ }
115
+
116
+ now=WorkLog.now
117
+ today=Date.today
118
+
119
+ days.each do |time, data|
120
+ next if time == :current
121
+
122
+ # It's data for today - use it
123
+ stats[:today]=data if time.day == now.day
124
+
125
+ if time_to_date(time).cweek == today.cweek then
126
+ stats[:week][:total] += data[:total]
127
+ stats[:week][:average] = (stats[:week][:total] / time_to_date(time).wday.to_f)
128
+ data[:projects].each do |project, duration|
129
+ stats[:week][:projects][project] ||= 0
130
+ stats[:week][:projects][project] += duration
131
+ end
132
+ end
133
+
134
+ # Everything is included in this month
135
+ stats[:month][:total] += data[:total]
136
+ stats[:month][:days_logged] += 1
137
+ stats[:month][:average] = (stats[:month][:total] / stats[:month][:days_logged])
138
+ data[:projects].each do |project, duration|
139
+ stats[:month][:projects][project] ||= 0
140
+ stats[:month][:projects][project] += duration
141
+ end
142
+ end
143
+
144
+ stats[:today][:total] += days[:current]
145
+ #stats[:today][:projects]['WIP'] = days[:current]
146
+
147
+ return stats
148
+ end
149
+
150
+ def time_to_date(time)
151
+ Date.new(time.year, time.month, time.day)
152
+ end
153
+ end
154
+ end
data/lib/logstats.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'haml'
2
+ require 'logstats/haml/helpers'
3
+ require 'logstats/worklog'
4
+ module LogStats
5
+ class Base
6
+ attr_reader :worklog
7
+
8
+ def initialize(source_file, output_path)
9
+ @worklog=WorkLog.new(source_file)
10
+ @output_path=output_path
11
+ @base_path=File.dirname(__FILE__)
12
+ end
13
+
14
+ def generate!
15
+ # Calculate the stats from the file
16
+ locals={}
17
+ @worklog.stats.each { |key, data| locals[key]=data }
18
+ process_haml(locals)
19
+ return true
20
+ end
21
+
22
+ private
23
+
24
+ def process_haml(locals)
25
+ # Inject them into the HAML layout
26
+ haml=nil
27
+ File.open(File.join(@base_path, 'logstats', 'template.haml'), 'r') do |f|
28
+ haml=f.read
29
+ end
30
+ engine = Haml::Engine.new(haml)
31
+ html=engine.render(LogStats::Helpers, locals)
32
+
33
+ # Save the HAML to a file
34
+ File.open(@output_path, 'w') do |f|
35
+ f << html
36
+ end
37
+ return true
38
+ end
39
+ end
40
+ end
data/logstats.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "logstats/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "logstats"
7
+ s.version = Logstats::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jason Stirk"]
10
+ s.email = ["jstirk@oobleyboo.com"]
11
+ s.homepage = "http://github.com/jstirk/logstats"
12
+ s.summary = "Generates a simple HTML file based upon my custom timesheet format."
13
+ s.description = "Generates a simple HTML file based upon my custom timesheet format."
14
+
15
+ s.rubyforge_project = "logstats"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency('tail_from_sentinel', '>= 0.0.1')
23
+ s.add_dependency('haml', '~> 3.0.25')
24
+ end
data/watchr/all.watchr ADDED
@@ -0,0 +1 @@
1
+ watch( 'examples/.*\.haml' ) {|md| system("haml #{md[0]} #{md[0].gsub(/haml$/,'html')}") }
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstats
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Jason Stirk
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-08 00:00:00 +11:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: tail_from_sentinel
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 29
30
+ segments:
31
+ - 0
32
+ - 0
33
+ - 1
34
+ version: 0.0.1
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: haml
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 53
46
+ segments:
47
+ - 3
48
+ - 0
49
+ - 25
50
+ version: 3.0.25
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ description: Generates a simple HTML file based upon my custom timesheet format.
54
+ email:
55
+ - jstirk@oobleyboo.com
56
+ executables:
57
+ - logstats
58
+ extensions: []
59
+
60
+ extra_rdoc_files: []
61
+
62
+ files:
63
+ - .gitignore
64
+ - .rvmrc
65
+ - Gemfile
66
+ - Gemfile.lock
67
+ - README
68
+ - Rakefile
69
+ - bin/logstats
70
+ - examples/example.haml
71
+ - lib/logstats.rb
72
+ - lib/logstats/haml/helpers.rb
73
+ - lib/logstats/template.haml
74
+ - lib/logstats/version.rb
75
+ - lib/logstats/worklog.rb
76
+ - logstats.gemspec
77
+ - watchr/all.watchr
78
+ has_rdoc: true
79
+ homepage: http://github.com/jstirk/logstats
80
+ licenses: []
81
+
82
+ post_install_message:
83
+ rdoc_options: []
84
+
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ hash: 3
102
+ segments:
103
+ - 0
104
+ version: "0"
105
+ requirements: []
106
+
107
+ rubyforge_project: logstats
108
+ rubygems_version: 1.3.7
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Generates a simple HTML file based upon my custom timesheet format.
112
+ test_files: []
113
+