logstats 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+