[{"data":1,"prerenderedAt":1629},["ShallowReactive",2],{"blog-post:\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp":3,"blog-navigation:\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp":1608,"i-lucide:share-2":1614,"related-articles:\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp":1619},{"path":4,"title":5,"description":6,"date":7,"tags":8,"body":14,"image":1606,"imageAlt":1607},"\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp","Debugging remote timezone issues with Chrome DevTools MCP","How a timezone bug turned a calendar date into a wrong day, and how Chrome DevTools MCP helped debug it without leaving VS Code.","2026-03-14",[9,10,11,12,13],"mcp","debugging","ai","devtools","vitest",{"type":15,"value":16,"toc":1596},"minimark",[17,30,47,50,55,58,103,106,108,112,115,118,344,397,400,404,407,528,535,539,542,647,657,660,671,673,677,680,840,843,906,920,923,925,929,939,1249,1257,1270,1493,1506,1508,1512,1527,1538,1541,1543,1547,1567,1573,1579,1581,1592],[18,19,20,21,25,26,29],"p",{},"A US-based client opened a ticket: the travel itinerary showed ",[22,23,24],"strong",{},"February 26th"," instead of ",[22,27,28],{},"February 27th"," for a Rome-to-Milan transfer. One day off. In travel, that's a stranded passenger.",[18,31,32,33,40,41,46],{},"I debugged the whole thing from VS Code using ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Fgithub.com\u002FChromeDevTools\u002Fchrome-devtools-mcp",[38],"nofollow","Chrome DevTools MCP",", which lets an AI agent control Chrome's debugging tools (network inspection, console, DOM) through the ",[34,42,45],{"href":43,"rel":44},"https:\u002F\u002Fmodelcontextprotocol.io\u002F",[38],"Model Context Protocol",". Instead of manually toggling Chrome's timezone settings and reloading four times, I had the agent run timezone simulations in the browser console, inspect the network payload, and verify the fix. One editor session, start to finish.",[48,49],"hr",{},[51,52,54],"h2",{"id":53},"the-bug","The bug",[18,56,57],{},"The client in New York saw this:",[59,60,61,77],"table",{},[62,63,64],"thead",{},[65,66,67,71,74],"tr",{},[68,69,70],"th",{},"Field",[68,72,73],{},"Expected",[68,75,76],{},"What they saw",[78,79,80,92],"tbody",{},[65,81,82,86,89],{},[83,84,85],"td",{},"Pick up",[83,87,88],{},"27\u002F02\u002F2026, Rome",[83,90,91],{},"26\u002F02\u002F2026, Rome",[65,93,94,97,100],{},[83,95,96],{},"Drop off",[83,98,99],{},"27\u002F02\u002F2026, Milan",[83,101,102],{},"26\u002F02\u002F2026, Milan",[18,104,105],{},"The dates were exactly one day off, both of them. Not random corruption, not a calculation error. A one-day shift that's consistent across fields smells like a timezone conversion that shouldn't be there. From my machine in Rome (UTC+1), everything looked correct, which made it worse. I needed to see what the client was actually seeing.",[48,107],{},[51,109,111],{"id":110},"reproducing-the-bug-with-mcp","Reproducing the bug with MCP",[18,113,114],{},"The traditional approach to timezone bugs: open Chrome DevTools, change the timezone in sensor overrides, reload, inspect. Repeat for each timezone. Slow, tedious, easy to mess up.",[18,116,117],{},"Instead, I had the MCP agent run a simulation directly in the browser console, testing how the frontend's date code behaves across four timezones at once:",[119,120,125],"pre",{"className":121,"code":122,"language":123,"meta":124,"style":124},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const serverDate = \"2026-02-27\"\nconst parsed = new Date(serverDate)\n\nconst timezones = [\"Europe\u002FRome\", \"America\u002FNew_York\", \"America\u002FLos_Angeles\", \"Asia\u002FTokyo\"]\n\nconst results = {}\ntimezones.forEach((tz) => {\n  results[tz] = parsed.toLocaleDateString(\"it-IT\", { timeZone: tz })\n})\n","typescript","",[126,127,128,155,175,182,234,239,252,282,336],"code",{"__ignoreMap":124},[129,130,133,137,141,145,148,152],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"spNyl","const",[129,138,140],{"class":139},"sTEyZ"," serverDate ",[129,142,144],{"class":143},"sMK4o","=",[129,146,147],{"class":143}," \"",[129,149,151],{"class":150},"sfazB","2026-02-27",[129,153,154],{"class":143},"\"\n",[129,156,158,160,163,165,168,172],{"class":131,"line":157},2,[129,159,136],{"class":135},[129,161,162],{"class":139}," parsed ",[129,164,144],{"class":143},[129,166,167],{"class":143}," new",[129,169,171],{"class":170},"s2Zo4"," Date",[129,173,174],{"class":139},"(serverDate)\n",[129,176,178],{"class":131,"line":177},3,[129,179,181],{"emptyLinePlaceholder":180},true,"\n",[129,183,185,187,190,192,195,198,201,203,206,208,211,213,215,217,220,222,224,226,229,231],{"class":131,"line":184},4,[129,186,136],{"class":135},[129,188,189],{"class":139}," timezones ",[129,191,144],{"class":143},[129,193,194],{"class":139}," [",[129,196,197],{"class":143},"\"",[129,199,200],{"class":150},"Europe\u002FRome",[129,202,197],{"class":143},[129,204,205],{"class":143},",",[129,207,147],{"class":143},[129,209,210],{"class":150},"America\u002FNew_York",[129,212,197],{"class":143},[129,214,205],{"class":143},[129,216,147],{"class":143},[129,218,219],{"class":150},"America\u002FLos_Angeles",[129,221,197],{"class":143},[129,223,205],{"class":143},[129,225,147],{"class":143},[129,227,228],{"class":150},"Asia\u002FTokyo",[129,230,197],{"class":143},[129,232,233],{"class":139},"]\n",[129,235,237],{"class":131,"line":236},5,[129,238,181],{"emptyLinePlaceholder":180},[129,240,242,244,247,249],{"class":131,"line":241},6,[129,243,136],{"class":135},[129,245,246],{"class":139}," results ",[129,248,144],{"class":143},[129,250,251],{"class":143}," {}\n",[129,253,255,258,261,264,267,269,273,276,279],{"class":131,"line":254},7,[129,256,257],{"class":139},"timezones",[129,259,260],{"class":143},".",[129,262,263],{"class":170},"forEach",[129,265,266],{"class":139},"(",[129,268,266],{"class":143},[129,270,272],{"class":271},"sHdIc","tz",[129,274,275],{"class":143},")",[129,277,278],{"class":135}," =>",[129,280,281],{"class":143}," {\n",[129,283,285,288,292,294,297,299,302,304,307,309,311,314,316,318,321,324,327,330,333],{"class":131,"line":284},8,[129,286,287],{"class":139},"  results",[129,289,291],{"class":290},"swJcz","[",[129,293,272],{"class":139},[129,295,296],{"class":290},"] ",[129,298,144],{"class":143},[129,300,301],{"class":139}," parsed",[129,303,260],{"class":143},[129,305,306],{"class":170},"toLocaleDateString",[129,308,266],{"class":290},[129,310,197],{"class":143},[129,312,313],{"class":150},"it-IT",[129,315,197],{"class":143},[129,317,205],{"class":143},[129,319,320],{"class":143}," {",[129,322,323],{"class":290}," timeZone",[129,325,326],{"class":143},":",[129,328,329],{"class":139}," tz",[129,331,332],{"class":143}," }",[129,334,335],{"class":290},")\n",[129,337,339,342],{"class":131,"line":338},9,[129,340,341],{"class":143},"}",[129,343,335],{"class":139},[59,345,346,359],{},[62,347,348],{},[65,349,350,353,356],{},[68,351,352],{},"Timezone",[68,354,355],{},"Displayed date",[68,357,358],{},"Correct?",[78,360,361,371,381,389],{},[65,362,363,365,368],{},[83,364,200],{},[83,366,367],{},"27\u002F02\u002F2026",[83,369,370],{},"✓",[65,372,373,375,378],{},[83,374,210],{},[83,376,377],{},"26\u002F02\u002F2026",[83,379,380],{},"✗",[65,382,383,385,387],{},[83,384,219],{},[83,386,377],{},[83,388,380],{},[65,390,391,393,395],{},[83,392,228],{},[83,394,367],{},[83,396,370],{},[18,398,399],{},"Bug confirmed. Any negative UTC offset shows the previous day.",[51,401,403],{"id":402},"tracing-the-data","Tracing the data",[18,405,406],{},"MCP's network inspection showed what the server was sending:",[119,408,412],{"className":409,"code":410,"language":411,"meta":124,"style":124},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"start_date\": \"2026-02-27\",\n  \"end_date\": \"2026-02-27\",\n  \"origin\": { \"name\": \"Rome\" },\n  \"destination\": { \"name\": \"Milan\" }\n}\n","json",[126,413,414,419,440,459,492,523],{"__ignoreMap":124},[129,415,416],{"class":131,"line":132},[129,417,418],{"class":143},"{\n",[129,420,421,424,427,429,431,433,435,437],{"class":131,"line":157},[129,422,423],{"class":143},"  \"",[129,425,426],{"class":135},"start_date",[129,428,197],{"class":143},[129,430,326],{"class":143},[129,432,147],{"class":143},[129,434,151],{"class":150},[129,436,197],{"class":143},[129,438,439],{"class":143},",\n",[129,441,442,444,447,449,451,453,455,457],{"class":131,"line":177},[129,443,423],{"class":143},[129,445,446],{"class":135},"end_date",[129,448,197],{"class":143},[129,450,326],{"class":143},[129,452,147],{"class":143},[129,454,151],{"class":150},[129,456,197],{"class":143},[129,458,439],{"class":143},[129,460,461,463,466,468,470,472,474,478,480,482,484,487,489],{"class":131,"line":184},[129,462,423],{"class":143},[129,464,465],{"class":135},"origin",[129,467,197],{"class":143},[129,469,326],{"class":143},[129,471,320],{"class":143},[129,473,147],{"class":143},[129,475,477],{"class":476},"sBMFI","name",[129,479,197],{"class":143},[129,481,326],{"class":143},[129,483,147],{"class":143},[129,485,486],{"class":150},"Rome",[129,488,197],{"class":143},[129,490,491],{"class":143}," },\n",[129,493,494,496,499,501,503,505,507,509,511,513,515,518,520],{"class":131,"line":236},[129,495,423],{"class":143},[129,497,498],{"class":135},"destination",[129,500,197],{"class":143},[129,502,326],{"class":143},[129,504,320],{"class":143},[129,506,147],{"class":143},[129,508,477],{"class":476},[129,510,197],{"class":143},[129,512,326],{"class":143},[129,514,147],{"class":143},[129,516,517],{"class":150},"Milan",[129,519,197],{"class":143},[129,521,522],{"class":143}," }\n",[129,524,525],{"class":131,"line":241},[129,526,527],{"class":143},"}\n",[18,529,530,531,534],{},"Plain ",[126,532,533],{},"YYYY-MM-DD"," strings. No time component, no timezone offset. Just a calendar date. The server was fine.",[51,536,538],{"id":537},"the-root-cause","The root cause",[18,540,541],{},"The Vue component that renders the date:",[119,543,545],{"className":121,"code":544,"language":123,"meta":124,"style":124},"const formattedPickupDateTime = computed(() => {\n  const date = new Date(props.pickupDate)\n  const formatted = date.toLocaleDateString(\"it-IT\")\n  return { date: formatted, time }\n})\n",[126,546,547,568,595,620,641],{"__ignoreMap":124},[129,548,549,551,554,556,559,561,564,566],{"class":131,"line":132},[129,550,136],{"class":135},[129,552,553],{"class":139}," formattedPickupDateTime ",[129,555,144],{"class":143},[129,557,558],{"class":170}," computed",[129,560,266],{"class":139},[129,562,563],{"class":143},"()",[129,565,278],{"class":135},[129,567,281],{"class":143},[129,569,570,573,576,579,581,583,585,588,590,593],{"class":131,"line":157},[129,571,572],{"class":135},"  const",[129,574,575],{"class":139}," date",[129,577,578],{"class":143}," =",[129,580,167],{"class":143},[129,582,171],{"class":170},[129,584,266],{"class":290},[129,586,587],{"class":139},"props",[129,589,260],{"class":143},[129,591,592],{"class":139},"pickupDate",[129,594,335],{"class":290},[129,596,597,599,602,604,606,608,610,612,614,616,618],{"class":131,"line":177},[129,598,572],{"class":135},[129,600,601],{"class":139}," formatted",[129,603,578],{"class":143},[129,605,575],{"class":139},[129,607,260],{"class":143},[129,609,306],{"class":170},[129,611,266],{"class":290},[129,613,197],{"class":143},[129,615,313],{"class":150},[129,617,197],{"class":143},[129,619,335],{"class":290},[129,621,622,626,628,630,632,634,636,639],{"class":131,"line":184},[129,623,625],{"class":624},"s7zQu","  return",[129,627,320],{"class":143},[129,629,575],{"class":290},[129,631,326],{"class":143},[129,633,601],{"class":139},[129,635,205],{"class":143},[129,637,638],{"class":139}," time",[129,640,522],{"class":143},[129,642,643,645],{"class":131,"line":236},[129,644,341],{"class":143},[129,646,335],{"class":139},[18,648,649,650,653,654,656],{},"There it is. ",[126,651,652],{},"new Date('2026-02-27')"," parses the string as UTC midnight. Then ",[126,655,306],{}," converts to the user's local timezone.",[18,658,659],{},"In Rome (UTC+1): midnight UTC becomes 1:00 AM on the 27th. Correct.\nIn New York (UTC-5): midnight UTC becomes 7:00 PM on the 26th. Wrong.",[18,661,662,663,666,667,670],{},"The ",[126,664,665],{},"Date"," constructor was the trap. The string ",[126,668,669],{},"\"2026-02-27\""," looks harmless, but once it enters JavaScript's Date system, it picks up timezone semantics nobody intended.",[48,672],{},[51,674,676],{"id":675},"the-fix","The fix",[18,678,679],{},"The server sends calendar dates: a day on a calendar, not a moment in time. The fix was to stop treating them as moments:",[119,681,683],{"className":121,"code":682,"language":123,"meta":124,"style":124},"function dateToString(dateString: string): string {\n  if (!dateString) return \"\"\n  const cleanDateString = dateString.substring(0, 10)\n  const [year, month, day] = cleanDateString.split(\"-\")\n  return `${day}\u002F${month}\u002F${year}`\n}\n",[126,684,685,710,732,762,804,836],{"__ignoreMap":124},[129,686,687,690,693,695,698,700,703,706,708],{"class":131,"line":132},[129,688,689],{"class":135},"function",[129,691,692],{"class":170}," dateToString",[129,694,266],{"class":143},[129,696,697],{"class":271},"dateString",[129,699,326],{"class":143},[129,701,702],{"class":476}," string",[129,704,705],{"class":143},"):",[129,707,702],{"class":476},[129,709,281],{"class":143},[129,711,712,715,718,721,723,726,729],{"class":131,"line":157},[129,713,714],{"class":624},"  if",[129,716,717],{"class":290}," (",[129,719,720],{"class":143},"!",[129,722,697],{"class":139},[129,724,725],{"class":290},") ",[129,727,728],{"class":624},"return",[129,730,731],{"class":143}," \"\"\n",[129,733,734,736,739,741,744,746,749,751,755,757,760],{"class":131,"line":177},[129,735,572],{"class":135},[129,737,738],{"class":139}," cleanDateString",[129,740,578],{"class":143},[129,742,743],{"class":139}," dateString",[129,745,260],{"class":143},[129,747,748],{"class":170},"substring",[129,750,266],{"class":290},[129,752,754],{"class":753},"sbssI","0",[129,756,205],{"class":143},[129,758,759],{"class":753}," 10",[129,761,335],{"class":290},[129,763,764,766,768,771,773,776,778,781,784,786,788,790,793,795,797,800,802],{"class":131,"line":184},[129,765,572],{"class":135},[129,767,194],{"class":143},[129,769,770],{"class":139},"year",[129,772,205],{"class":143},[129,774,775],{"class":139}," month",[129,777,205],{"class":143},[129,779,780],{"class":139}," day",[129,782,783],{"class":143},"]",[129,785,578],{"class":143},[129,787,738],{"class":139},[129,789,260],{"class":143},[129,791,792],{"class":170},"split",[129,794,266],{"class":290},[129,796,197],{"class":143},[129,798,799],{"class":150},"-",[129,801,197],{"class":143},[129,803,335],{"class":290},[129,805,806,808,811,814,816,819,822,825,827,829,831,833],{"class":131,"line":236},[129,807,625],{"class":624},[129,809,810],{"class":143}," `${",[129,812,813],{"class":139},"day",[129,815,341],{"class":143},[129,817,818],{"class":150},"\u002F",[129,820,821],{"class":143},"${",[129,823,824],{"class":139},"month",[129,826,341],{"class":143},[129,828,818],{"class":150},[129,830,821],{"class":143},[129,832,770],{"class":139},[129,834,835],{"class":143},"}`\n",[129,837,838],{"class":131,"line":241},[129,839,527],{"class":143},[18,841,842],{},"Updated component:",[119,844,846],{"className":121,"code":845,"language":123,"meta":124,"style":124},"const formattedPickupDateTime = computed(() => {\n  const date = dateToString(props.pickupDate)\n  return { date, time }\n})\n",[126,847,848,866,886,900],{"__ignoreMap":124},[129,849,850,852,854,856,858,860,862,864],{"class":131,"line":132},[129,851,136],{"class":135},[129,853,553],{"class":139},[129,855,144],{"class":143},[129,857,558],{"class":170},[129,859,266],{"class":139},[129,861,563],{"class":143},[129,863,278],{"class":135},[129,865,281],{"class":143},[129,867,868,870,872,874,876,878,880,882,884],{"class":131,"line":157},[129,869,572],{"class":135},[129,871,575],{"class":139},[129,873,578],{"class":143},[129,875,692],{"class":170},[129,877,266],{"class":290},[129,879,587],{"class":139},[129,881,260],{"class":143},[129,883,592],{"class":139},[129,885,335],{"class":290},[129,887,888,890,892,894,896,898],{"class":131,"line":177},[129,889,625],{"class":624},[129,891,320],{"class":143},[129,893,575],{"class":139},[129,895,205],{"class":143},[129,897,638],{"class":139},[129,899,522],{"class":143},[129,901,902,904],{"class":131,"line":184},[129,903,341],{"class":143},[129,905,335],{"class":139},[18,907,908,909,912,913,915,916,919],{},"No ",[126,910,911],{},"new Date()",". The string ",[126,914,669],{}," gets split on hyphens and rearranged into ",[126,917,918],{},"\"27\u002F02\u002F2026\"",". Same output in Tokyo, Rome, and New York.",[18,921,922],{},"After deploying, I used MCP again: the same simulation that found the bug now confirmed the fix. Four timezones, all showing 27\u002F02\u002F2026, no Chrome settings changed.",[48,924],{},[51,926,928],{"id":927},"locking-it-down-with-tests","Locking it down with tests",[18,930,931,932,935,936,938],{},"If anyone refactors ",[126,933,934],{},"dateToString"," to use ",[126,937,665],{}," objects later, these tests should catch it:",[119,940,942],{"className":121,"code":941,"language":123,"meta":124,"style":124},"import { describe, it, expect } from \"vitest\"\nimport dateToString from \"@\u002Futils\u002FdateToString\"\n\ndescribe(\"dateToString\", () => {\n  it(\"reformats YYYY-MM-DD to DD\u002FMM\u002FYYYY\", () => {\n    expect(dateToString(\"2026-02-27\")).toBe(\"27\u002F02\u002F2026\")\n  })\n\n  it(\"returns empty string for falsy input\", () => {\n    expect(dateToString(\"\")).toBe(\"\")\n  })\n\n  it(\"handles date strings with time components\", () => {\n    expect(dateToString(\"2026-02-27T23:00:00Z\")).toBe(\"27\u002F02\u002F2026\")\n    expect(dateToString(\"2026-02-27T00:00:00+01:00\")).toBe(\"27\u002F02\u002F2026\")\n  })\n})\n",[126,943,944,975,992,996,1018,1040,1075,1082,1086,1107,1133,1140,1145,1167,1201,1235,1242],{"__ignoreMap":124},[129,945,946,949,951,954,956,959,961,964,966,969,971,973],{"class":131,"line":132},[129,947,948],{"class":624},"import",[129,950,320],{"class":143},[129,952,953],{"class":139}," describe",[129,955,205],{"class":143},[129,957,958],{"class":139}," it",[129,960,205],{"class":143},[129,962,963],{"class":139}," expect",[129,965,332],{"class":143},[129,967,968],{"class":624}," from",[129,970,147],{"class":143},[129,972,13],{"class":150},[129,974,154],{"class":143},[129,976,977,979,982,985,987,990],{"class":131,"line":157},[129,978,948],{"class":624},[129,980,981],{"class":139}," dateToString ",[129,983,984],{"class":624},"from",[129,986,147],{"class":143},[129,988,989],{"class":150},"@\u002Futils\u002FdateToString",[129,991,154],{"class":143},[129,993,994],{"class":131,"line":177},[129,995,181],{"emptyLinePlaceholder":180},[129,997,998,1001,1003,1005,1007,1009,1011,1014,1016],{"class":131,"line":184},[129,999,1000],{"class":170},"describe",[129,1002,266],{"class":139},[129,1004,197],{"class":143},[129,1006,934],{"class":150},[129,1008,197],{"class":143},[129,1010,205],{"class":143},[129,1012,1013],{"class":143}," ()",[129,1015,278],{"class":135},[129,1017,281],{"class":143},[129,1019,1020,1023,1025,1027,1030,1032,1034,1036,1038],{"class":131,"line":236},[129,1021,1022],{"class":170},"  it",[129,1024,266],{"class":290},[129,1026,197],{"class":143},[129,1028,1029],{"class":150},"reformats YYYY-MM-DD to DD\u002FMM\u002FYYYY",[129,1031,197],{"class":143},[129,1033,205],{"class":143},[129,1035,1013],{"class":143},[129,1037,278],{"class":135},[129,1039,281],{"class":143},[129,1041,1042,1045,1047,1049,1051,1053,1055,1057,1060,1062,1065,1067,1069,1071,1073],{"class":131,"line":241},[129,1043,1044],{"class":170},"    expect",[129,1046,266],{"class":290},[129,1048,934],{"class":170},[129,1050,266],{"class":290},[129,1052,197],{"class":143},[129,1054,151],{"class":150},[129,1056,197],{"class":143},[129,1058,1059],{"class":290},"))",[129,1061,260],{"class":143},[129,1063,1064],{"class":170},"toBe",[129,1066,266],{"class":290},[129,1068,197],{"class":143},[129,1070,367],{"class":150},[129,1072,197],{"class":143},[129,1074,335],{"class":290},[129,1076,1077,1080],{"class":131,"line":254},[129,1078,1079],{"class":143},"  }",[129,1081,335],{"class":290},[129,1083,1084],{"class":131,"line":284},[129,1085,181],{"emptyLinePlaceholder":180},[129,1087,1088,1090,1092,1094,1097,1099,1101,1103,1105],{"class":131,"line":338},[129,1089,1022],{"class":170},[129,1091,266],{"class":290},[129,1093,197],{"class":143},[129,1095,1096],{"class":150},"returns empty string for falsy input",[129,1098,197],{"class":143},[129,1100,205],{"class":143},[129,1102,1013],{"class":143},[129,1104,278],{"class":135},[129,1106,281],{"class":143},[129,1108,1110,1112,1114,1116,1118,1121,1123,1125,1127,1129,1131],{"class":131,"line":1109},10,[129,1111,1044],{"class":170},[129,1113,266],{"class":290},[129,1115,934],{"class":170},[129,1117,266],{"class":290},[129,1119,1120],{"class":143},"\"\"",[129,1122,1059],{"class":290},[129,1124,260],{"class":143},[129,1126,1064],{"class":170},[129,1128,266],{"class":290},[129,1130,1120],{"class":143},[129,1132,335],{"class":290},[129,1134,1136,1138],{"class":131,"line":1135},11,[129,1137,1079],{"class":143},[129,1139,335],{"class":290},[129,1141,1143],{"class":131,"line":1142},12,[129,1144,181],{"emptyLinePlaceholder":180},[129,1146,1148,1150,1152,1154,1157,1159,1161,1163,1165],{"class":131,"line":1147},13,[129,1149,1022],{"class":170},[129,1151,266],{"class":290},[129,1153,197],{"class":143},[129,1155,1156],{"class":150},"handles date strings with time components",[129,1158,197],{"class":143},[129,1160,205],{"class":143},[129,1162,1013],{"class":143},[129,1164,278],{"class":135},[129,1166,281],{"class":143},[129,1168,1170,1172,1174,1176,1178,1180,1183,1185,1187,1189,1191,1193,1195,1197,1199],{"class":131,"line":1169},14,[129,1171,1044],{"class":170},[129,1173,266],{"class":290},[129,1175,934],{"class":170},[129,1177,266],{"class":290},[129,1179,197],{"class":143},[129,1181,1182],{"class":150},"2026-02-27T23:00:00Z",[129,1184,197],{"class":143},[129,1186,1059],{"class":290},[129,1188,260],{"class":143},[129,1190,1064],{"class":170},[129,1192,266],{"class":290},[129,1194,197],{"class":143},[129,1196,367],{"class":150},[129,1198,197],{"class":143},[129,1200,335],{"class":290},[129,1202,1204,1206,1208,1210,1212,1214,1217,1219,1221,1223,1225,1227,1229,1231,1233],{"class":131,"line":1203},15,[129,1205,1044],{"class":170},[129,1207,266],{"class":290},[129,1209,934],{"class":170},[129,1211,266],{"class":290},[129,1213,197],{"class":143},[129,1215,1216],{"class":150},"2026-02-27T00:00:00+01:00",[129,1218,197],{"class":143},[129,1220,1059],{"class":290},[129,1222,260],{"class":143},[129,1224,1064],{"class":170},[129,1226,266],{"class":290},[129,1228,197],{"class":143},[129,1230,367],{"class":150},[129,1232,197],{"class":143},[129,1234,335],{"class":290},[129,1236,1238,1240],{"class":131,"line":1237},16,[129,1239,1079],{"class":143},[129,1241,335],{"class":290},[129,1243,1245,1247],{"class":131,"line":1244},17,[129,1246,341],{"class":143},[129,1248,335],{"class":139},[18,1250,1251,1252,935,1254,1256],{},"The tests above verify correctness, but they won't catch a timezone regression. If someone refactors ",[126,1253,934],{},[126,1255,911],{},", the tests still pass on any machine where the local timezone has a positive UTC offset. They'd only fail in CI if the runner happens to be in a negative-offset timezone.",[18,1258,1259,1260,1265,1266,1269],{},"To actually catch that, the standard way in a Node.js environment like ",[34,1261,1264],{"href":1262,"rel":1263},"https:\u002F\u002Fvitest.dev\u002Fapi\u002Fvi.html#vi-setsystemtime",[38],"Vitest"," is to set the ",[126,1267,1268],{},"TZ"," environment variable. In modern Node.js versions, you can even switch timezones dynamically between tests:",[119,1271,1273],{"className":121,"code":1272,"language":123,"meta":124,"style":124},"import { it, expect } from \"vitest\"\n\nit.each([\n  { tz: \"America\u002FNew_York\", expected: \"26\u002F02\u002F2026\" },\n  { tz: \"Europe\u002FRome\", expected: \"27\u002F02\u002F2026\" },\n  { tz: \"Asia\u002FTokyo\", expected: \"27\u002F02\u002F2026\" },\n] as const)(\"produces $expected when TZ=$tz\", ({ tz, expected }) => {\n  process.env.TZ = tz\n  expect(dateToString(\"2026-02-27\")).toBe(expected)\n})\n",[126,1274,1275,1297,1301,1314,1344,1372,1400,1438,1457,1487],{"__ignoreMap":124},[129,1276,1277,1279,1281,1283,1285,1287,1289,1291,1293,1295],{"class":131,"line":132},[129,1278,948],{"class":624},[129,1280,320],{"class":143},[129,1282,958],{"class":139},[129,1284,205],{"class":143},[129,1286,963],{"class":139},[129,1288,332],{"class":143},[129,1290,968],{"class":624},[129,1292,147],{"class":143},[129,1294,13],{"class":150},[129,1296,154],{"class":143},[129,1298,1299],{"class":131,"line":157},[129,1300,181],{"emptyLinePlaceholder":180},[129,1302,1303,1306,1308,1311],{"class":131,"line":177},[129,1304,1305],{"class":139},"it",[129,1307,260],{"class":143},[129,1309,1310],{"class":170},"each",[129,1312,1313],{"class":139},"([\n",[129,1315,1316,1319,1321,1323,1325,1327,1329,1331,1334,1336,1338,1340,1342],{"class":131,"line":184},[129,1317,1318],{"class":143},"  {",[129,1320,329],{"class":290},[129,1322,326],{"class":143},[129,1324,147],{"class":143},[129,1326,210],{"class":150},[129,1328,197],{"class":143},[129,1330,205],{"class":143},[129,1332,1333],{"class":290}," expected",[129,1335,326],{"class":143},[129,1337,147],{"class":143},[129,1339,377],{"class":150},[129,1341,197],{"class":143},[129,1343,491],{"class":143},[129,1345,1346,1348,1350,1352,1354,1356,1358,1360,1362,1364,1366,1368,1370],{"class":131,"line":236},[129,1347,1318],{"class":143},[129,1349,329],{"class":290},[129,1351,326],{"class":143},[129,1353,147],{"class":143},[129,1355,200],{"class":150},[129,1357,197],{"class":143},[129,1359,205],{"class":143},[129,1361,1333],{"class":290},[129,1363,326],{"class":143},[129,1365,147],{"class":143},[129,1367,367],{"class":150},[129,1369,197],{"class":143},[129,1371,491],{"class":143},[129,1373,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392,1394,1396,1398],{"class":131,"line":241},[129,1375,1318],{"class":143},[129,1377,329],{"class":290},[129,1379,326],{"class":143},[129,1381,147],{"class":143},[129,1383,228],{"class":150},[129,1385,197],{"class":143},[129,1387,205],{"class":143},[129,1389,1333],{"class":290},[129,1391,326],{"class":143},[129,1393,147],{"class":143},[129,1395,367],{"class":150},[129,1397,197],{"class":143},[129,1399,491],{"class":143},[129,1401,1402,1404,1407,1410,1413,1415,1418,1420,1422,1425,1427,1429,1431,1434,1436],{"class":131,"line":254},[129,1403,296],{"class":139},[129,1405,1406],{"class":624},"as",[129,1408,1409],{"class":135}," const",[129,1411,1412],{"class":139},")(",[129,1414,197],{"class":143},[129,1416,1417],{"class":150},"produces $expected when TZ=$tz",[129,1419,197],{"class":143},[129,1421,205],{"class":143},[129,1423,1424],{"class":143}," ({",[129,1426,329],{"class":271},[129,1428,205],{"class":143},[129,1430,1333],{"class":271},[129,1432,1433],{"class":143}," })",[129,1435,278],{"class":135},[129,1437,281],{"class":143},[129,1439,1440,1443,1445,1448,1450,1452,1454],{"class":131,"line":284},[129,1441,1442],{"class":139},"  process",[129,1444,260],{"class":143},[129,1446,1447],{"class":139},"env",[129,1449,260],{"class":143},[129,1451,1268],{"class":139},[129,1453,578],{"class":143},[129,1455,1456],{"class":139}," tz\n",[129,1458,1459,1462,1464,1466,1468,1470,1472,1474,1476,1478,1480,1482,1485],{"class":131,"line":338},[129,1460,1461],{"class":170},"  expect",[129,1463,266],{"class":290},[129,1465,934],{"class":170},[129,1467,266],{"class":290},[129,1469,197],{"class":143},[129,1471,151],{"class":150},[129,1473,197],{"class":143},[129,1475,1059],{"class":290},[129,1477,260],{"class":143},[129,1479,1064],{"class":170},[129,1481,266],{"class":290},[129,1483,1484],{"class":139},"expected",[129,1486,335],{"class":290},[129,1488,1489,1491],{"class":131,"line":1109},[129,1490,341],{"class":143},[129,1492,335],{"class":139},[18,1494,1495],{},[1496,1497,1498,1499,1502,1503,1505],"em",{},"(Note: While Node.js 22+ handles ",[126,1500,1501],{},"process.env.TZ"," changes dynamically, in older versions the timezone was cached after the first ",[126,1504,665],{}," was created. If you are on an older stack, you might need to run separate test processes or set the timezone at the very top of the file.)",[48,1507],{},[51,1509,1511],{"id":1510},"why-this-catches-everyone","Why this catches everyone",[18,1513,1514,1516,1517,1520,1521,1526],{},[126,1515,652],{}," parses as UTC midnight: ",[126,1518,1519],{},"2026-02-27T00:00:00.000Z",". This is per ",[34,1522,1525],{"href":1523,"rel":1524},"https:\u002F\u002Ftc39.es\u002Fecma262\u002F#sec-date",[38],"ECMA-262 §21.4.3.2",": date-only strings are interpreted as UTC.",[18,1528,1529,1530,1533,1534,1537],{},"Meanwhile, ",[126,1531,1532],{},"new Date(2026, 1, 27)"," creates a date in the ",[1496,1535,1536],{},"local"," timezone. Two constructors, two different timezone behaviors. I've known this for years and still almost missed it.",[18,1539,1540],{},"The original code was written and tested in Rome. UTC midnight is 1:00 AM in Rome, still on the 27th, so the bug was invisible during development. It only surfaces in negative-UTC timezones, where midnight UTC falls on the previous calendar day. I develop in Rome. Of course I didn't see it.",[48,1542],{},[51,1544,1546],{"id":1545},"what-i-took-away","What I took away",[18,1548,1549,1554,1555,1557,1558,1560,1561,1564,1565,260],{},[22,1550,1551,1552,260],{},"If you're displaying a calendar date, don't put it through ",[126,1553,665],{}," Split the string, rearrange, render. But document why. The next developer will look at ",[126,1556,934],{}," and think \"why didn't they just use ",[126,1559,306],{},"?\" The comment is part of the answer. The ",[126,1562,1563],{},"timezone-mock"," tests are the rest: they show exactly what breaks if someone swaps in ",[126,1566,911],{},[18,1568,1569,1572],{},[22,1570,1571],{},"Timezone bugs are invisible from positive-UTC locations."," UTC midnight is still the right calendar day in Western Europe and East Asia. The bug only shows up in the Americas.",[18,1574,1575,1578],{},[22,1576,1577],{},"Chrome DevTools MCP collapses the debugging loop."," The traditional timezone-toggle routine takes minutes of clicking around per cycle. The agent tested four timezones in one console evaluation, then the same simulation verified the fix. The debugging context stayed in one place instead of splitting across browser tabs, terminals, and my head.",[48,1580],{},[18,1582,1583],{},[1496,1584,1585,1586,1589,1590,260],{},"Debugged with ",[34,1587,39],{"href":36,"rel":1588},[38],", Vue 3 and Nuxt 4, and a healthy suspicion of ",[126,1591,911],{},[1593,1594,1595],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":124,"searchDepth":157,"depth":157,"links":1597},[1598,1599,1600,1601,1602,1603,1604,1605],{"id":53,"depth":157,"text":54},{"id":110,"depth":157,"text":111},{"id":402,"depth":157,"text":403},{"id":537,"depth":157,"text":538},{"id":675,"depth":157,"text":676},{"id":927,"depth":157,"text":928},{"id":1510,"depth":157,"text":1511},{"id":1545,"depth":157,"text":1546},"\u002Fimages\u002Fblog\u002Fdebugging-timezone-chrome-devtools-mcp.png","An editorial cartoon of a developer caught between two giant glowing clocks showing different times, surrounded by neon-lit debugging tools and timezone labels.",[1609,1613],{"path":1610,"title":1611,"date":1612},"\u002Fblog\u002Fbuilding-local-first-browser-tts-studio-kokoro","Building a local-first browser TTS studio with Kokoro","2026-03-28",{"path":4,"title":5,"date":7},{"left":1615,"top":1615,"width":1616,"height":1616,"rotate":1615,"vFlip":1617,"hFlip":1617,"body":1618},0,24,false,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"18\" cy=\"5\" r=\"3\"\u002F>\u003Ccircle cx=\"6\" cy=\"12\" r=\"3\"\u002F>\u003Ccircle cx=\"18\" cy=\"19\" r=\"3\"\u002F>\u003Cpath d=\"m8.59 13.51l6.83 3.98m-.01-10.98l-6.82 3.98\"\u002F>\u003C\u002Fg>",[1620],{"path":1610,"title":1611,"description":1621,"date":1612,"tags":1622,"image":1627,"imageAlt":1628},"How I built LocalVoice Studio to generate speech in the browser, and what AI-assisted development still needed to make it shippable.",[11,1623,1624,1625,1626],"tts","local-first","accessibility","testing","\u002Fimages\u002Fblog\u002Fbuilding-local-first-browser-tts-studio-kokoro.png","A cyberpunk illustration of Martin in a neon-lit recording studio operating a futuristic audio console. Glowing soundwaves flow from a 'Kokoro ONNX' hopper, surrounded by Vue.js holograms and an 'AI Speed' rocket anchored by chains labeled 'Linting' and 'Accessibility'",1778941875078]