Skip to content

Issue 116 Time Logged From The Right Click Menu

Ed Mozley edited this page Aug 28, 2026 · 1 revision

Time logged from the right-click menu was two hours out (issue #116)

Logging time on a ticket from the right-click menu recorded it at the wrong time of day. Logging the same time from inside the ticket was correct. On a system set to Vienna, every entry made the first way arrived two hours late.

Reported in issue #116 by mbsouth, who supplied both screenshots side by side and correctly identified the cause as UTC.

Fixed in 5d1038b0, released as update #1257.

There were two faults, one on each side of the same small window, and they are worth separating because only one of them was visible to the reporter.


1. What you saw

Right-click a ticket in the list, choose Record time. The window opens with the date and time already filled in. Enter the minutes, save.

The entry appears in the ticket's time list showing a time two hours later than the one the window had offered you a moment earlier.

Do the same thing from the time form inside the ticket and it is right.


2. Why the two disagreed

The difference between the two paths is the explanation.

The form inside the ticket has no date field. It sends the minutes and the note, and the server stamps the time itself - in UTC, which is what every stored datetime in FreeITSM is.

The right-click window offers you a time you can change, which is the point of it: you log time after the fact. It sent back exactly the characters on screen:

// before
entry_datetime: when || null      // "2026-08-28T20:00"

That string carries no timezone. Nothing in it distinguishes 20:00 in Vienna from 20:00 in London or 20:00 UTC, and the server can only read it one way:

// includes/services/tickets.php
$dt = new DateTimeImmutable($v, new DateTimeZone('UTC'));

So 20:00 was stored as 20:00 UTC. Reading it back converts it into the reader's display zone, which in Vienna adds two hours, and the entry reads 22:00.

The offset is therefore not a fixed two hours - it is your own UTC offset. In the UK in winter it is zero, which is a large part of why this survived.


3. The second fault: the window read the wrong clock

The field prefilled itself from the browser:

// before
const now = new Date();
... pad(now.getHours()) ...

getHours() is the clock on the computer you are sitting at. But FreeITSM shows every date in the display timezone you have chosen in Settings β†’ Preferences, which need not be the same thing.

Set your display timezone to Vienna while sitting in London and the window opened offering 18:03 - your laptop's time - while every entry in the list directly beneath it was written in Vienna time, where it was 19:03.

This one produces no visible error whenever the two zones happen to agree, which is the case for almost everybody. It needs a reader whose machine and whose settings differ to show itself at all. The reporter's did not, so what they saw was the first fault alone.


4. The fix

Both halves are conversions between a picked wall clock and a UTC instant, and FreeITSM had no helper for that.

Its date handling sorts every stored date into one of three kinds, and has helpers for reading each of them:

Kind Example Helper
1. A UTC instant a ticket's received time parseUTCDate - converts on display
2. A naive wall clock, stored without a zone a change window, scheduled work parseNaiveDate - never converts
3. A bare date a contract renewal rendered as-is

A time entry is kind 1. But the description of kind 1 assumed the server writes it, which is true of every other one - a received time, a created time, an audit stamp. This is the one kind-1 field a person picks a value for, and nothing covered the writing of it.

That is what makes it awkward: on screen it is a datetime-local box, identical to the kind-2 boxes used for change windows, where posting the value as typed is exactly right. The two look the same and want opposite treatment.

Three helpers now cover it in assets/js/tz.js, alongside the others rather than in the ticket screen - the same trap waits for the next screen that asks somebody to pick a date and time.

nowForInput()        // "now", in the analyst's DISPLAY zone -> for the input
inputToUTC(value)    // picked wall clock -> ISO-8601 UTC, for sending
utcToInput(dbValue)  // stored UTC -> the input's value, for editing

The named zone is resolved through Intl, so daylight saving is handled by the browser's own timezone database rather than a stored offset. inputToUTC runs two passes: the first guesses the offset by reading the wall clock as if it were UTC, the second re-reads it at the instant that guess produced. That second pass is what makes the hour either side of a clock change land on the right instant. Where no display zone has been chosen, all three fall back to the browser's own zone - which is then the same thing.

Existing entries are not altered. There is no way to tell, after the event, which rows were typed in a different zone and which were stamped by the server, and a migration that guessed would corrupt the ones that were already right.


5. πŸ“ The files involved

File What changed
assets/js/tz.js nowForInput(), inputToUTC(), utcToInput() and the Intl offset resolver behind them.
assets/js/inbox.js openContextRecordTime() prefills from nowForInput(); saveContextTimeEntry() sends inputToUTC(when).
150 pages tz.js?v=4 to v5, so a cached copy without the new helpers cannot be served. inbox.js?v=109 to v110.

πŸ—ƒ Already correct

includes/services/tickets.php parseDate() reads a zone-less string as UTC. That is right for the REST API, which documents ISO 8601. The browser was sending the wrong thing.
The time form inside the ticket Sends no time at all, so the server stamps gmdate(). It was never affected.
formatDateTime() Always converted correctly on display.
Change windows, scheduled work, PIR actuals Deliberately naive: 2pm reads 2pm for everybody. These must not use the new helpers.

6. How it was verified

Reproduced from the database, not the screen. The reporter's symptom was matched against a real row: an entry logged at 18:00 UK time was stored as 2026-08-28 18:00:00 - the browser's wall clock, verbatim - while the correct entry made a minute earlier from inside the ticket was stored as 17:00:17, an hour behind it, as a genuine UTC stamp should be.

16 assertions in headless Chrome, on a machine whose own zone is Europe/London with the display zone set to Europe/Vienna - the split that separates the two faults:

Check Result
Summer and winter conversions 20:00 Vienna to 18:00Z; 09:00 to 08:00Z
Both Vienna daylight-saving boundaries correct on each side, +02:00 and +01:00
Round trip - type it, store it, read it back identical for four values
Rendered by the entry list's own formatter 20:00 in, 20:00 on screen
nowForInput() against the display zone 19:06 Vienna, while the browser clock read 18:06
Negative control - the old naive value differs, so the test can tell the two apart

Then live, against the running application, because a green test can only prove the arithmetic. Both payloads were posted to api/tickets/save_time_entry.php for a 20:00 Vienna entry:

Posted Stored
2026-08-28T18:00:00.000Z (the new code) 2026-08-28 18:00:00 - correct
2026-08-28T20:00 (the old code) 2026-08-28 20:00:00 - two hours out

That also settled the one thing the unit tests could not: that PHP accepts the milliseconds in the ISO string the browser produces. Both test rows were then removed.


7. What this means for you

Record time from the right-click menu and the time it stores is the time you chose.

If your display timezone in Settings β†’ Preferences differs from the clock on your computer, the window now opens at the current time in your chosen zone, so it agrees with the list of entries beneath it rather than with your laptop.

Entries logged before this fix are unchanged. If any of them matter, the stored value is your own UTC offset ahead of the time that was meant - delete and re-enter them.


Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally