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
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.
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).
- 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.
Summary
All
periodvalues (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+)
period: "today"period: "yesterday"start_date=end_date=2026-04-19(explicit)start_date=2026-04-18, end_date=2026-04-20(explicit)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()insrc/utils.tscorrectly buildstoday = new Date(); today.setHours(0,0,0,0)in local time, buttoISOString()then shifts it. Example for UTC+1:today=2026-04-19 00:00 CET→ UTC2026-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
Dateis serialized for the Toggl API (toISOString().split('T')[0]appears multiple places —getTimeEntriesForDateRange, the weekly report formatting insrc/utils.ts#L286-L287, etc.).Related issues uncovered while triaging
end_dateis 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-19returns 0 results — surprising for callers passing explicit ranges. Worth either documenting clearly in the tool schema or normalizing inclusive-end semantics in the wrapper.getTimeEntriesForWeek/getTimeEntriesForMonthpass asunday/lastDayset to23:59:59.999asend_date. After.split('T')[0]that becomes Sunday's date, but sinceend_dateis 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).todayincludes 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 newtoLocalYMDformatter that mockDateandprocess.env.TZto cover at least UTC-8, UTC, UTC+1, and UTC+9.