How Kaneo's Time Tracking Feature Records and Calculates Entries
Kaneo stores each work interval as a time-entry record with startTime, endTime, and a pre-computed duration in seconds, calculating the total by summing cached duration values on the client.
Kaneo's open-source project management platform includes a robust time tracking system that logs work intervals as structured database records. The feature is built around a time-entry abstraction that separates active timers from completed entries, enabling both real-time tracking and accurate historical reporting. This article examines exactly how the system records timestamps, computes durations, and aggregates totals for task-level reporting.
Database Schema for Time Entries
The foundation of Kaneo's time tracking is defined in apps/api/src/database/schema.ts. Each entry maps to a PostgreSQL row with the following columns:
id— unique identifiertaskId— foreign key to the associated taskuserId— foreign key to the user who logged timedescription— optional free-form notesstartTime— timestamp when the timer beganendTime— timestamp when stopped; null for running entriesduration— cached duration in seconds; null until entry is closedcreatedAt/updatedAt— audit timestamps
The duration field is intentionally nullable to support live timers. A running entry maintains endTime = null and duration = null, signaling the UI to compute elapsed time dynamically.
Recording a Time Entry
Creating a Running Entry
The entry point is POST /api/time-entry, handled by apps/api/src/time-entry/controllers/create-time-entry.ts. The controller accepts:
{
"taskId": "uuid",
"description": "optional notes",
"startTime": "2025-01-15T09:00:00.000Z"
}
If the client omits endTime, the controller stores endTime = null and duration = null. This state marks the entry as actively running.
Stopping the Timer and Calculating Duration
When a user stops tracking, the client calls PUT /api/time-entry/:id, processed by apps/api/src/time-entry/controllers/update-time-entry.ts. The controller:
- Records the supplied
endTime - Computes
duration = (endTime - startTime) / 1000(converting milliseconds to seconds) - Persists both values to the database
This pre-computation ensures consistent duration reporting regardless of subsequent clock adjustments or timezone changes.
Retrieving and Displaying Time Entries
The GET /api/time-entry/task/:taskId endpoint, implemented in apps/api/src/time-entry/controllers/get-time-entries.ts, retrieves all entries for a task:
// apps/api/src/time-entry/controllers/get-time-entries.ts
const timeEntries = await db
.select({
id: timeEntryTable.id,
taskId: timeEntryTable.taskId,
userId: timeEntryTable.userId,
userName: userTable.name,
description: timeEntryTable.description,
startTime: timeEntryTable.startTime,
endTime: timeEntryTable.endTime,
duration: timeEntryTable.duration,
createdAt: timeEntryTable.createdAt,
updatedAt: timeEntryTable.updatedAt,
})
.from(timeEntryTable)
.leftJoin(userTable, eq(timeEntryTable.userId, userTable.id))
.where(eq(timeEntryTable.taskId, taskId))
.orderBy(timeEntryTable.startTime);
The query joins the user table to include human-readable attribution and orders results chronologically by startTime.
Aggregating Total Time for Tasks
Because duration is pre-computed server-side, clients perform simple aggregation. The React hook in apps/web/src/hooks/queries/time-entry/use-get-time-entries.ts fetches entries, then components sum the values:
import { useGetTimeEntries } from '@kaneo/web/hooks/queries/time-entry';
import { sumBy } from 'lodash';
function TotalTime({ taskId }: { taskId: string }) {
const { data: entries } = useGetTimeEntries(taskId);
const totalSeconds = sumBy(entries ?? [], (e) => e.duration ?? 0);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
return <span>{hours}h {minutes}m</span>;
}
Running entries contribute 0 to the sum since their duration is null. The UI can optionally display live elapsed time by computing Date.now() - startTime locally.
Key Implementation Files
| Purpose | File Path |
|---|---|
| Database schema | apps/api/src/database/schema.ts |
| Create entry controller | apps/api/src/time-entry/controllers/create-time-entry.ts |
| Update/stop controller | apps/api/src/time-entry/controllers/update-time-entry.ts |
| List entries controller | apps/api/src/time-entry/controllers/get-time-entries.ts |
| Fetch hook (React) | apps/web/src/hooks/queries/time-entry/use-get-time-entries.ts |
| Create hook (React) | apps/web/src/hooks/mutations/time-entry/use-create-time-entry.ts |
| Update hook (React) | apps/web/src/hooks/mutations/time-entry/use-update-time-entry.ts |
Summary
- Kaneo's time tracking uses a time-entry table with
startTime,endTime, and cacheddurationfields - Running entries have
endTime = nullandduration = null, enabling live timer displays - Duration is calculated server-side in
update-time-entry.tsas(endTime - startTime) / 1000seconds - Clients aggregate pre-computed durations; no complex date math required for historical totals
- The React frontend uses
useGetTimeEntrieswithsumByfrom lodash for efficient summation
Frequently Asked Questions
How does Kaneo handle timezone differences in time tracking?
All timestamps are stored as UTC in PostgreSQL. The duration calculation uses raw millisecond differences, eliminating timezone conversion errors. Client-side display formatting handles localization.
What happens if a user forgets to stop a timer?
Entries remain in the running state indefinitely with endTime = null and duration = null. The UI can surface these for manual closure. No automatic timeout logic exists in the current implementation.
Can time entries be edited after stopping?
Yes. The PUT /api/time-entry/:id endpoint accepts modified startTime, endTime, or description values. The controller recalculates duration whenever endTime changes, ensuring consistency.
Does Kaneo support multiple concurrent timers per user?
The schema permits multiple running entries with different taskId values. No application-level constraint enforces a single active timer; this would require additional validation in the create controller if desired.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →