-
Notifications
You must be signed in to change notification settings - Fork 17
Issue 116 Time Logged From The Right Click Menu
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.
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.
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.
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.
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 editingThe 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.
| 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. |
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. |
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.
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.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)