Dates are saved as UTC and shown in local time
-
What you'll see
Times on a page are two hours early (one hour in winter), and a date is one day behind for anything that happened between midnight and 02:00. Nothing throws, and older records look right while new ones do not. The code takes the date apart as text:
let time = o.LastModified.split("T")[1]; // "18:14:57.000Z" - UTC, not local let day = record.Sent.substring(0, 10); // the UTC date let when = ("" + e.time).replace("T", " ").substring(0, 16);Or a date built from its parts is a day early once it has been through
toISOString:new Date(2026, 9, 1).toISOString().slice(0, 10); // "2026-09-30" - midnight local is 22:00 UTC the day beforeMeasured on the server, local time +02:00:
Expression Before 23 October 2026 Now (= JavaScript) JSON.stringify(new Date()), saved and API dates"2026-10-23T20:14:57.0643134+02:00""2026-10-23T18:14:57.064Z"new Date("2026-10-23T18:14:57Z").getHours()1820docly.format("2026-10-23T18:14:57Z", "HH:mm"),#value:HH:mm#18:1420:14new Date(2026, 9, 23, 20, 14).toISOString()"2026-10-23T20:14:00.000Z""2026-10-23T18:14:00.000Z"isNaN(new Date())truefalse -
What's actually happening
The text of a date is UTC now. JavaScript writes a date as
toISOString: UTC, three decimals, aZat the end. HashJS used to write its own format in local time with an offset. Since 23 October 2026 it writes the JavaScript format everywhere a date leaves the runtime as text —JSON.stringify,docly.saveJson,docly.saveFile, API responses,docly.http*and the console — so a date written in one place reads the same in another. The browser sees it too: a document'sLastModifiedin an API response ends inZ.Code that takes the text apart —
split("T"),substring(0, 10),substring(11, 16)— never looked at the zone. It only worked because the text used to be local time. Now it reads the UTC clock, and the UTC date, which is yesterday from midnight to 02:00 local time.Old files are not rewritten. A record saved before 23 October still holds
+02:00and still looks right, so the fault shows up on new records first. Both forms parse correctly withnew Date(...).Showing a date uses local time, as in JavaScript. The getters (
getHours,getDate,getDay,getMonth,getFullYear…),toLocaleDateString,docly.formatand#value:format#convert to the server's local time. Before, they used the zone the date was parsed with, so a date read back fromZshowed the UTC clock. That includes dates with a foreign offset:"09:28-05:00"is shown in local time, not as 09:28.new Date(year, month, …)reads its parts as local time, as JavaScript does. It used to build a UTC date from them, which looked right ingetHoursbut was two hours ahead intoISOString— and so in every browser that read the JSON.Unchanged:
toISOString, thegetUTC*methods andgetTime, strings without a zone (read as local time), and the CLR members.Year,.Month,.Day,.Hour, which keep the zone the date was parsed with — see Date objects carry both CLR and JavaScript members. -
What to do
1. Never cut a date out of its text. Parse it, then format it. On the server:
docly.format(new Date(o.LastModified), "yyyy-MM-dd HH:mm"); // local time, old and new records alikeIn the browser, let the browser do it:
new Date(o.LastModified).toLocaleString("nb-NO", { dateStyle: "short", timeStyle: "short" });2. Keep text that has no zone as it is. A value your own code wrote with
docly.format(new Date(), "yyyy-MM-dd HH:mm")is already local time. If one field can hold both kinds, convert only text that ends inZor an offset:function localIso(v) { const s = "" + (v ?? ""); if (!/T\d\d:\d\d.*(Z|[+-]\d\d:?\d\d)$/.test(s)) return s; // no zone: already local const d = new Date(s); if (isNaN(d)) return s; const p = n => String(n).padStart(2, "0"); return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + "T" + p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds()); } let time = localIso(o.LastModified).split("T")[1]; // the old code, now in local time3. A date string from a date you built: use
docly.format, nottoISOString.toISOStringis UTC by definition.docly.format(new Date(2026, 9, 1), "yyyy-MM-dd"); // "2026-10-01" new Date(2026, 9, 1).toISOString().slice(0, 10); // "2026-09-30" - wrong4. Compare dates as numbers, not as text.
"2026-10-23T18:14Z" < "2026-10-23T19:00+02:00"compares characters, and old and new records sort differently. Usenew Date(v).getTime().5. Guard the input.
new Date("")throws in HashJS, andnew Date(null)is the current time — see Date.parse and Date.UTC return an object not milliseconds for a small wrapper.6. Look for the patterns in your own code:
split("T"),substring(0, 10),substring(0, 16),substring(11,slice(0, 10),slice(0, 16)on anything that holds a date, and.toISOString().slice(0, 10)on a date built from its parts.new Date().toISOString().split("T")[0]was UTC before too, and is not affected.7. Verify on the instance you run on — one throwaway page with
new Date("2026-10-23T18:14:57Z").getHours()settles it. See Use a scratch hash file to test Docly functions.
Last reviewed 23 Oct 2026