Time tracking is hell (I just spend less time there now)

Time tracking is hell. Especially for developers. At the end of the day Jira wants to know exactly how many hours I spent on each ticket, and my memory wants to know why I’m asking it.

Comic: a panicked developer with glasses and beard drags a huge alarm clock across the floor
Time tracking can be hell.

The irony is that my tools already know most of it. GitLab knows when I pushed to which repository and which commits mention which ticket. It knows which merge requests I was assigned to or reviewing. Jira knows which tickets I commented on. The whole day is there as an activity trail. I just never looked at it when filling in hours. So one Monday in July I asked Claude Code to turn that trail into a draft I only have to correct.

The before

I’m lazy about admin routines. I rarely log time daily. Sometimes weekly, sometimes much later. That morning I had set my team a goal of logging something on every working day, and our internal team dashboard (a small Next.js app with Jira-based evaluations) was about to show everybody’s missing days. Including mine. The first prompt of the session was harmless:

This repo contains a tool in the "evaluations" route that allows to check the time tracking.

I set my team a goal to track all workdays of the year

So we need to introduce to the table in "hours tracking"

1) a calculation of workdays in the time-frame (taken from a clever api that can tell you holidays in Bavaria Germany). Workdays are only Mo, Tu, We, Th, Fr in our company.
2) Show how many unlogged work-days there were. I want my co-workers to log at least on one ticket per day

Claude picked a free public-holiday API and filtered it to holidays that are either nationwide or list the Bavarian region code. Then it counted. A few prompts later, after a „My Calendar“ tab with my GitLab pushes per day, I saw my own number, and the real project started:

ok. now that you see that. I have my gitlab pat in <env file>

Could you do me a favour and make a little proposal

So I have 102 missed days this year. I basically need a tool to show

1  - headline day, subheadline proposed ticket id
2 - proposed ticket summary
3 - commits (number, max time diff between commits)
4 - slider - hours to log on ticket

That way, I could have a view with all my unlogged days and create an output file, that allows me to log

each day row should have a (+) icon that allows to add a row by selecting a ticket via dropdown. can we make this at a seperate route "/ai-time-track"

102 days. In July. That is the number that makes you build tools. The prompt worked well because it describes the screen, not the algorithm: a row per day, what’s in the headline, what’s in the subheadline, a slider. Point 3, „max time diff between commits“, turned out to be the seed of the whole estimation heuristic.

Tightening the rules

The first version proposed one ticket per day with an hour value nobody could actually book. Two short corrections fixed that:

single commit is at least 30min. slider should be at the proposed time rounded up to 30min slots. we can only do 30min
right now we only have one line per day. But, I need one line per day + ticket. Of course each day can have 1 or 10 sub-rows.

These are the kind of domain rules an agent cannot know: our time tracking only accepts half-hour slots, and a day is rarely one ticket. Then I got suspicious about where the tickets came from at all and asked a question instead of giving an order:

How do you determine all issues in the timeframe? My guess is - check all the user was assigned to + check all the user interacted with + all that were mentioned in commits ids...or what did you do?

Asking „what did you do?“ is underrated. The answer showed it only read branch names and the last commit of each push, which missed a lot. The result is the „scan depth“ switch in the final tool.

The heuristic

Here is how the estimate actually works. It is deliberately dumb, which is why I trust it.

  • Collect tickets. GitLab’s user events API returns my push events. Ticket keys are pulled out of branch names and commit messages with a plain regex, [A-Z][A-Z0-9]+-\d+. „Fast“ mode reads the branch and the last commit title, „Deep git“ reads every commit message of a push via the compare endpoint (in parallel, cached), „Deep git + Jira“ also pulls my Jira comments and changelog entries. Merge requests where I’m assignee or reviewer are always scanned; their title and description count for the day the MR was last updated.
  • Estimate the day. Take the time of the first and the last push of the day. The span between them, rounded up to 30-minute slots, is the day’s total. Any day with at least one commit gets at least 30 minutes.
  • Split across tickets. Each ticket gets a weight: its number of commits that day. Tickets that only show up in Jira or in a merge request get weight 1. The day’s hours are split by weight, rounded to half hours, minimum 30 minutes per ticket.
  • Skip what’s done. Days that already have a worklog get no proposal, so nothing is booked twice.

In code, the core is just two small functions:

// The day's total = push span rounded UP to 30-min slots, at least 30 min if there was a commit.
function proposeDayHours(spanMinutes, commitCount) {
  if (commitCount === 0) return 0;
  return Math.max(0.5, Math.ceil(spanMinutes / 30) * 0.5);
}

// Split the day across its tickets, weighted by commit count, every ticket at least 30 min.
function proposePerTicket(weights, dayHours) {
  const entries = [...weights.entries()].sort((a, b) => b[1] - a[1]);
  const total = entries.reduce((s, [, w]) => s + w, 0) || entries.length;
  return entries.map(([key, w]) => ({
    key,
    proposedHours: Math.max(0.5, Math.round((dayHours * w) / total / 0.5) * 0.5),
  }));
}

Yes, this underestimates days full of meetings and overestimates days where I pushed at 9 and fixed a typo at 18:00. That’s what the sliders are for. The point is not to know the truth, it’s to give me a draft that’s 80 % right so I only touch the other 20 %.

Midjourney original: the same cartoon developer in full figure, wrestling the oversized clock
The Midjourney original, before the title box went on. The clock always wins.

One click to the worklog, and two errors on the way

Downloading a CSV was nice. Not having to upload it anywhere was nicer:

cool can we now push this to the Tempo API? But this needs to happen per currently selected time band. And I need a modal that shows 2 tabs for checking

1. Calendar view with little icon how many hours per each day
2. Issue view - sum of hours per issue that will get submitted

Submission should of course happen properly with hours per day

The review modal is the important part. Before anything is booked I see the calendar with hours per day and a per-ticket sum. Then the first real submit failed, and then the second one:

I wanted to submit tempo_log.json and get back "400 {"errors":{"worker":"User is invalid"}, ... "reasons":["VALIDATION_FAILED"]}"
"error": "400 {"errors":{"TT Activity":"Work attribute 'TT Activity' (_TTActivity_) is required"}, ... }"

Polish, colleagues and the cloud

The next morning I found the tool dropped days without any activity, which defeats the purpose:

the tool sometimes looses days. if I have zero activity. can it please render all days

in case it was public holiday in bavaria put "public holiday" under <X> hours total

Allow to have a "None" row with 0h. this then needs to get removed from logs

Now every weekday shows up: holidays marked, already-logged days shown with their hours but no proposal, empty days with a „None“ row I can fill in from a ticket dropdown.

The last surprise came when we wanted to run it as a shared web app: worklogs must be written with the user’s own Jira token, not the service token from the server’s env file. So the tool now asks each user for a Jira and a GitLab token in a modal and keeps them in the browser, while the team-wide evaluations still use the server token.

From the first prompt to the merge request with per-user tokens took about a day and a half, in between other work. Six commits, roughly 1,300 lines in the page component, which is too many, but it’s an internal tool.

Results and takeaways

My time tracking went from about ten minutes a day to about one. The bigger win is that I no longer depend on memory: when I catch up a week later, the history is already there. Could it book everything automatically? Probably. I don’t want it to. An AI-generated proposal plus a quick human review feels like the right balance, and the sliders keep me honest.

  • Describe the screen you want, not the algorithm. The heuristic emerged from „max time diff between commits“.
  • State the domain rules early: booking granularity, one row per day and ticket, workdays, holidays.
  • Ask the agent how it computed something. It will tell you, including what it skipped.
  • Paste raw API errors. They are precise prompts.
  • Keep a review step before anything writes to a system of record.

Time tracking is still hell. I just spend much less time there.