Skip to content

Bug: period parameter off-by-one in non-UTC timezones (date range uses UTC instead of local) #14

@jack-arturo

Description

@jack-arturo

Summary

All period values (today, yesterday, week, lastWeek, month, lastMonth) return entries shifted back by exactly one day for users in any timezone east of UTC (e.g. CET, IST, JST). The bug is in the date → string conversion, not in the period boundary math.

Reproduction (today = 2026-04-19, observer in UTC+1+)

Call Expected Got
period: "today" Apr 19 entries 4 entries, all Apr 18
period: "yesterday" Apr 18 entries 7 entries, all Apr 17
start_date=end_date=2026-04-19 (explicit) Apr 19 entries 0 entries
start_date=2026-04-18, end_date=2026-04-20 (explicit) Apr 18 + Apr 19 8 entries, correct

The explicit-range path works correctly (and includes the currently-running entry), so the underlying fetch is fine.

Root cause

src/toggl-api.ts#L234-L238:

```ts
async getTimeEntriesForDateRange(startDate: Date, endDate: Date): Promise<TimeEntry[]> {
const params: TimeEntriesRequest = {
start_date: startDate.toISOString().split('T')[0],
end_date: endDate.toISOString().split('T')[0]
};
```

toISOString() always serializes in UTC. getDateRange() in src/utils.ts correctly builds today = new Date(); today.setHours(0,0,0,0) in local time, but toISOString() then shifts it. Example for UTC+1:

  • Local today = 2026-04-19 00:00 CET → UTC 2026-04-18T23:00:00Z.split('T')[0] = "2026-04-18"

Fix

Format the date in local time instead of via UTC. One liner:

```ts
function toLocalYMD(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
```

Then in getTimeEntriesForDateRange:

```ts
start_date: toLocalYMD(startDate),
end_date: toLocalYMD(endDate),
```

The same fix should be applied wherever a Date is serialized for the Toggl API (toISOString().split('T')[0] appears multiple places — getTimeEntriesForDateRange, the weekly report formatting in src/utils.ts#L286-L287, etc.).

Related issues uncovered while triaging

  1. end_date is exclusive in Toggl API v9 but the tool schema description (`"End date (YYYY-MM-DD format)"`) reads as inclusive. start_date=end_date=2026-04-19 returns 0 results — surprising for callers passing explicit ranges. Worth either documenting clearly in the tool schema or normalizing inclusive-end semantics in the wrapper.
  2. getTimeEntriesForWeek / getTimeEntriesForMonth pass a sunday/lastDay set to 23:59:59.999 as end_date. After .split('T')[0] that becomes Sunday's date, but since end_date is exclusive in the API, Sunday's entries are silently dropped in some timezones. Should bump end forward by one day before formatting (after fixing the local-date bug above).
  3. After the fix, double-check that today includes the currently-running entry — the explicit-range path does (verified), and a daily report that excludes the timer you're currently running would be confusing.

Tests

Suggest adding unit tests for getDateRange + the new toLocalYMD formatter that mock Date and process.env.TZ to cover at least UTC-8, UTC, UTC+1, and UTC+9.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions