I am trying to closely monitor my productivity while working. For this reason I have a couple of metrics that I use to show on a dashboard on my iPad.
The metrics I am most interested in are:
- Rescuetime productivity for the week
- Top apps today (with time, and productivity type)
- Rescuetime productivity pulse for today, and for the week
- Keypresses this week
- Clicks this week
- Git commits per day
- Ratio between keypresses and clicks
What does it look like
This is how my dashboard looked last night. Productivity this week has been high in general. The productivity pulse was relatively high yesterday, however the keypress to click ratio is low enough to tell me, that I have been doing a lot of non-coding work yesterday. (Usually on a very productive day, this metric is between 20 and 30, on the graph it is around 12 on average).
How is it done
There are three parts to this dashboard; collection of data, a place to display them, and the actual widgets themselves.
Metrics
The metrics on the dashboard come from only two places (well actually more, keep reading). Rescuetime and Stathat.
Rescuetime metrics are rather simple to understand. The interessting part is how stathat gets the metrics for keypresses, clicks, ratio and commits.
Keypresses and clicks
For keypresses, clicks and the ratio between them, another service comes into play. I use WhatPulse to monitor my computer usage, and it stores its metrics in a sqlite database. The api of stathat is very simple, so it is really just a bash oneliner to add data to any metric:
curl -d "stat=[STATNAME]&ezkey=[STATHAT_KEY]&value=`sqlite3 "[PATH_TO_DB]" "[SELECT_STATEMENT_TO_GET_ONE_VALUE]" http://api.stathat.com/ez
I have made a shell script for each of the three metrics that come from WhatPulse, and each of them runs at a regular interval.
Commits
This proved to be just as easy. As above the call to stathat is just as simple (except this is a counter, not an absolute value)
The interesting part is that it is run as a git hook after each commit. I have placed a post-commit hook in my .git_template/hooks directory
#!/usr/local/bin/zsh
curl -d "stat=commit&ezkey=[STATHAT_KEY]&count=1" http://api.stathat.com/ez
and placed a init template in my git config
[init]
templatedir = ~/.git_template
This makes git init (and git clone too) use this template dir for projects. This means that the hooks in this template dir are added to all new projects. (And you can always git init an existing project, to get the hook scripts added there as well – so off course I did a shell script for doing just that, on all projects I allready had on my computer).
Thats about it.
Dashboard
The actual dashboard was the easiest part. I worked for some time on a NodeWebkit application for my desktop which could be my dashboard – very inspired by the Telepath mac app that Nich Winter did and showed off in his video The 120-hour Workweek.
However I never really got it working quite like I wanted, and when I learned about the Status Board App by Panic, I knew it would solve the problem more elegantly.
Widgets
RescueTime allready had plugins setup so that was easy. Stathat also had integrations directly. So only the top apps widget required some work.
I decided to create a little ruby (Sinatra) service to run and show the widget on a webserver. Statusboard can show either different graphs from data formatted in a special way – or it can show a simple html page (complete with css and javascript).
The sinatra app is only 44 lines of code:
require 'uri'
require 'net/https'
require 'json'
class Personal < Sinatra::Base
set :root, File.dirname(__FILE__)
set :views, Proc.new { File.join(root, "views") }
def get_data
uri = URI.parse("https://www.rescuetime.com/anapi/data?format=json&key=#{ENV['RESCUETIME_KEY']}&resolution_time=day&rk=activity&by=interval")
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 30
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
request = Net::HTTP::Get.new(uri.request_uri)
response = http.request(request)
JSON.parse(response.body)["rows"]
end
def get_class_name productivity_score
["very_unproductive", "unproductive", "neutral", "productive", "very_productive"][productivity_score+2]
end
def format_time time
hours = time/3600.to_i
minutes = (time/60 - hours * 60).to_i
seconds = (time - (minutes * 60 + hours * 3600))
"%02d:%02d:%02d" % [hours, minutes, seconds]
end
get '/widget' do
erb :widget
end
get '/content' do
<a href='https://github.com/data' class='user-mention'>@data</a> = get_data
erb :content
end
end
The widget view contains both the javascript and css directly, since it is very simple, and is only loaded once a day, and probably never changes.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
<meta http-equiv="Cache-control" content="no-cache" />
<style>
table {
width: 100%;
font-size: 12px;
}
tr {
background-color: #ccc;
color: #000;
padding: 1px;
}
tr.very_unproductive {
background-color: #FF0000;
}
tr.unproductive {
background-color: #F78181;
}
tr.neutral {
background-color: #F7F8E0;
}
tr.productive {
background-color: #BCF5A9;
}
tr.very_productive {
background-color: #2EFE64;
}
table tr td {
padding: 4px;
color: #000;
}
</style>
<script type="text/javascript">
function refresh()
{
var req = new XMLHttpRequest();
console.log("Refreshing Count...");
req.onreadystatechange=function() {
if (req.readyState==4 && req.status==200) {
document.getElementById('topAppsContainer').innerHTML = req.responseText;
}
}
req.open("GET", '/content', true);
req.send(null);
}
function init() {
if (document.location.href.indexOf('desktop') > -1)
{
document.getElementById('topAppsContainer').style.backgroundColor = 'black';
}
refresh()
var int=self.setInterval(function(){refresh()},60000);
}
</script>
</head>
<body onload="init();">
<div id="topAppsContainer">
</div>
</body>
</html>
And finally the content view contains the actual table rows
<table>
<% @data.take(9).each do |row| %>
<tr class="<%= get_class_name(row[5]) %>">
<td><%= row[3] %></td>
<td><%= row[4] %></td>
<td><%=format_time row[1].to_i %></td>
</tr>
<% end %>
</table>
… why take(9)? Thats what can be showed without overflowing!
Thats it.