@agent-native/core 0.172.3 → 0.172.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/get-settings.ts +2 -5
  3. package/corpus/templates/calendar/actions/list-events.ts +21 -80
  4. package/corpus/templates/calendar/actions/update-settings.ts +11 -16
  5. package/corpus/templates/calendar/actions/view-screen.ts +25 -10
  6. package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
  7. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
  8. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
  9. package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
  10. package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
  11. package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
  12. package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
  13. package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
  14. package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
  15. package/corpus/templates/calendar/shared/settings.ts +15 -9
  16. package/corpus/templates/calendar/shared/timezone.ts +127 -0
  17. package/corpus/templates/slides/actions/list-decks.ts +79 -5
  18. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
  19. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
  20. package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
  21. package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
  22. package/corpus/templates/slides/app/pages/Index.tsx +11 -13
  23. package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
  24. package/dist/agent/production-agent.js +5 -1
  25. package/dist/agent/types.d.ts +5 -0
  26. package/dist/collab/awareness.d.ts +2 -2
  27. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  28. package/dist/observability/posthog-ai.d.ts +36 -11
  29. package/dist/observability/posthog-ai.js +99 -30
  30. package/dist/observability/routes.d.ts +3 -3
  31. package/dist/observability/routes.js +9 -2
  32. package/dist/observability/traces.js +330 -142
  33. package/dist/progress/routes.d.ts +1 -1
  34. package/dist/resources/handlers.d.ts +1 -1
  35. package/dist/server/realtime-token.d.ts +1 -1
  36. package/dist/server/transcribe-voice.d.ts +1 -1
  37. package/dist/tracking/providers.js +9 -3
  38. package/docs/content/observability.mdx +27 -10
  39. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8587
34
+ - template files: 8592
@@ -1,9 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getRequestUserEmail } from "@agent-native/core/server";
3
- import { getUserSetting } from "@agent-native/core/settings";
4
3
  import { z } from "zod";
5
4
 
6
- import { normalizeCalendarSettings } from "../shared/settings.js";
5
+ import { readCalendarSettings } from "../server/lib/calendar-settings.js";
7
6
 
8
7
  export default defineAction({
9
8
  description: "Get calendar settings",
@@ -12,8 +11,6 @@ export default defineAction({
12
11
  run: async () => {
13
12
  const email = getRequestUserEmail();
14
13
  if (!email) throw new Error("no authenticated user");
15
- return normalizeCalendarSettings(
16
- await getUserSetting(email, "calendar-settings"),
17
- );
14
+ return readCalendarSettings(email);
18
15
  },
19
16
  });
@@ -13,9 +13,16 @@ import { and, gte, inArray, lte, ne } from "drizzle-orm";
13
13
  import { z } from "zod";
14
14
 
15
15
  import { getDb, schema } from "../server/db/index.js";
16
+ import { getCalendarTimezone } from "../server/lib/calendar-settings.js";
16
17
  import * as googleCalendar from "../server/lib/google-calendar.js";
17
18
  import { fetchICalEvents } from "../server/lib/ical-fetcher.js";
18
19
  import type { CalendarEvent, ExternalCalendar } from "../shared/api.js";
20
+ import {
21
+ addDaysToDateKey,
22
+ dateKeyInTimezone,
23
+ dateTimeInTimezoneToIso,
24
+ isCalendarTimezone,
25
+ } from "../shared/timezone.js";
19
26
  import { calendarEventMatchesQuery } from "./event-search.js";
20
27
 
21
28
  const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -74,6 +81,7 @@ interface ListCalendarEventsArgs {
74
81
  interface ListCalendarEventsOptions {
75
82
  ownedAccounts?: string[];
76
83
  range?: CalendarEventRange;
84
+ timezone?: string;
77
85
  }
78
86
 
79
87
  type CalendarInventorySource = "google" | "bookings" | "ics" | "overlays";
@@ -346,84 +354,9 @@ function compactInventoryEvent(event: CalendarEvent): CalendarInventoryItem {
346
354
  };
347
355
  }
348
356
 
349
- function normalizeTimezone(timezone?: string): string {
350
- if (!timezone) return "UTC";
351
- try {
352
- new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
353
- return timezone;
354
- } catch {
355
- return "UTC";
356
- }
357
- }
358
-
359
- function datePartsInTimezone(date: Date, timezone: string) {
360
- const parts = new Intl.DateTimeFormat("en-US", {
361
- timeZone: timezone,
362
- hourCycle: "h23",
363
- year: "numeric",
364
- month: "2-digit",
365
- day: "2-digit",
366
- hour: "2-digit",
367
- minute: "2-digit",
368
- second: "2-digit",
369
- }).formatToParts(date);
370
- const get = (type: string) =>
371
- Number(parts.find((part) => part.type === type)?.value ?? "0");
372
- return {
373
- year: get("year"),
374
- month: get("month"),
375
- day: get("day"),
376
- hour: get("hour"),
377
- minute: get("minute"),
378
- second: get("second"),
379
- };
380
- }
381
-
382
- function dateOnlyInTimezone(date: Date, timezone: string): string {
383
- const parts = datePartsInTimezone(date, timezone);
384
- return [
385
- String(parts.year).padStart(4, "0"),
386
- String(parts.month).padStart(2, "0"),
387
- String(parts.day).padStart(2, "0"),
388
- ].join("-");
389
- }
390
-
391
- function addDaysToDateOnly(dateOnly: string, days: number): string {
392
- const [year, month, day] = dateOnly.split("-").map(Number);
393
- const date = new Date(Date.UTC(year, month - 1, day + days));
394
- return [
395
- String(date.getUTCFullYear()).padStart(4, "0"),
396
- String(date.getUTCMonth() + 1).padStart(2, "0"),
397
- String(date.getUTCDate()).padStart(2, "0"),
398
- ].join("-");
399
- }
400
-
401
- function offsetMsForTimezone(date: Date, timezone: string): number {
402
- const parts = datePartsInTimezone(date, timezone);
403
- const asUtc = Date.UTC(
404
- parts.year,
405
- parts.month - 1,
406
- parts.day,
407
- parts.hour,
408
- parts.minute,
409
- parts.second,
410
- );
411
- return asUtc - date.getTime();
412
- }
413
-
414
- function zonedDateOnlyToUtcIso(dateOnly: string, timezone: string): string {
415
- const [year, month, day] = dateOnly.split("-").map(Number);
416
- const wallClockUtc = Date.UTC(year, month - 1, day, 0, 0, 0);
417
- const firstGuess = new Date(wallClockUtc);
418
- const firstOffset = offsetMsForTimezone(firstGuess, timezone);
419
- const secondGuess = new Date(wallClockUtc - firstOffset);
420
- const secondOffset = offsetMsForTimezone(secondGuess, timezone);
421
- return new Date(wallClockUtc - secondOffset).toISOString();
422
- }
423
-
424
357
  function normalizeDateBound(value: string, timezone: string): string {
425
358
  if (DATE_ONLY_RE.test(value)) {
426
- return zonedDateOnlyToUtcIso(value, timezone);
359
+ return dateTimeInTimezoneToIso(value, "00:00", timezone);
427
360
  }
428
361
  const parsed = new Date(value);
429
362
  if (Number.isNaN(parsed.getTime())) {
@@ -437,19 +370,20 @@ export function resolveCalendarEventRange(args: {
437
370
  to?: string;
438
371
  timezone?: string;
439
372
  }): CalendarEventRange {
440
- const timezone = normalizeTimezone(args.timezone ?? getRequestTimezone());
441
- const today = dateOnlyInTimezone(new Date(), timezone);
373
+ const requested = args.timezone ?? getRequestTimezone();
374
+ const timezone = isCalendarTimezone(requested) ? requested : "UTC";
375
+ const today = dateKeyInTimezone(new Date(), timezone);
442
376
  let from = args.from?.trim();
443
377
  let to = args.to?.trim();
444
378
  let defaulted = false;
445
379
 
446
380
  if (!from && !to) {
447
381
  from = today;
448
- to = addDaysToDateOnly(today, 1);
382
+ to = addDaysToDateKey(today, 1);
449
383
  defaulted = true;
450
384
  } else if (from && !to) {
451
385
  if (DATE_ONLY_RE.test(from)) {
452
- to = addDaysToDateOnly(from, 1);
386
+ to = addDaysToDateKey(from, 1);
453
387
  } else {
454
388
  const start = new Date(from);
455
389
  if (Number.isNaN(start.getTime())) {
@@ -631,11 +565,13 @@ export async function listCalendarEvents(
631
565
  ): Promise<CalendarEventsResult> {
632
566
  const email = getRequestUserEmail();
633
567
  if (!email) throw new Error("no authenticated user");
568
+ const timezone = options.timezone ?? (await getCalendarTimezone(email));
634
569
  const range =
635
570
  options.range ??
636
571
  resolveCalendarEventRange({
637
572
  from: args.from,
638
573
  to: args.to,
574
+ timezone,
639
575
  });
640
576
 
641
577
  const sources = resolveInventorySources(args.sources);
@@ -882,6 +818,9 @@ export default defineAction({
882
818
  args.format === "inventory" || (ctx?.caller === "mcp" && !args.format);
883
819
  const owner = inventory ? getRequestUserEmail() : undefined;
884
820
  if (inventory && !owner) throw new Error("no authenticated user");
821
+ const calendarTimezone = inventory
822
+ ? await getCalendarTimezone(owner!)
823
+ : undefined;
885
824
 
886
825
  // Reject invalid, expired, owner-bound, and query-bound cursors before any
887
826
  // provider call. Omitted account filters require the cheap owned-account
@@ -895,6 +834,7 @@ export default defineAction({
895
834
  preparedRange = resolveCalendarEventRange({
896
835
  from: args.from,
897
836
  to: args.to,
837
+ timezone: calendarTimezone,
898
838
  });
899
839
  preparedOwnedAccounts = args.accountEmails
900
840
  ? undefined
@@ -921,6 +861,7 @@ export default defineAction({
921
861
  {
922
862
  ownedAccounts: preparedOwnedAccounts,
923
863
  range: preparedRange,
864
+ timezone: calendarTimezone,
924
865
  },
925
866
  );
926
867
 
@@ -1,18 +1,21 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getRequestUserEmail } from "@agent-native/core/server";
3
- import {
4
- getUserSetting,
5
- putUserSetting,
6
- putSetting,
7
- } from "@agent-native/core/settings";
8
3
  import { z } from "zod";
9
4
 
10
- import { normalizeCalendarSettings } from "../shared/settings.js";
5
+ import { saveCalendarSettings } from "../server/lib/calendar-settings.js";
6
+ import { isCalendarTimezone } from "../shared/timezone.js";
11
7
 
12
8
  export default defineAction({
13
9
  description: "Update calendar settings",
14
10
  schema: z.object({
15
- timezone: z.string().optional().describe("Timezone"),
11
+ timezone: z
12
+ .string()
13
+ .trim()
14
+ .refine(isCalendarTimezone, {
15
+ message: "Timezone must be a valid IANA timezone.",
16
+ })
17
+ .optional()
18
+ .describe("IANA timezone, e.g. Europe/Warsaw"),
16
19
  bookingPageTitle: z.string().optional().describe("Booking page title"),
17
20
  bookingPageDescription: z
18
21
  .string()
@@ -32,14 +35,6 @@ export default defineAction({
32
35
  run: async (args) => {
33
36
  const email = getRequestUserEmail();
34
37
  if (!email) throw new Error("no authenticated user");
35
- const currentSettings = await getUserSetting(email, "calendar-settings");
36
- const settings = normalizeCalendarSettings({
37
- ...normalizeCalendarSettings(currentSettings),
38
- ...args,
39
- });
40
- const settingsRecord = settings as unknown as Record<string, unknown>;
41
- await putUserSetting(email, "calendar-settings", settingsRecord);
42
- await putSetting("calendar-settings", settingsRecord);
43
- return settings;
38
+ return saveCalendarSettings(email, args);
44
39
  },
45
40
  });
@@ -6,11 +6,17 @@ import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import { rowToBookingLink } from "../server/lib/booking-link-utils.js";
9
+ import { getCalendarTimezone } from "../server/lib/calendar-settings.js";
9
10
  import type { CalendarEvent, CalendarEventDraft } from "../shared/api.js";
10
11
  import {
11
12
  CALENDAR_VIEW_PREFERENCES_KEY,
12
13
  normalizeCalendarViewPreferences,
13
14
  } from "../shared/calendar-view-preferences.js";
15
+ import {
16
+ addDaysToDateKey,
17
+ dateKeyInTimezone,
18
+ dateTimeInTimezoneToIso,
19
+ } from "../shared/timezone.js";
14
20
  import { extractVideoLink } from "./event-action-helpers.js";
15
21
  import { listCalendarEvents } from "./list-events.js";
16
22
 
@@ -49,6 +55,11 @@ async function fetchEventsForRange(
49
55
  }
50
56
  }
51
57
 
58
+ function dateKeyFromParts(date: Date): string {
59
+ const pad = (value: number) => String(value).padStart(2, "0");
60
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
61
+ }
62
+
52
63
  export default defineAction({
53
64
  description:
54
65
  "See what the user is currently looking at on screen. Returns the current view, date range, and visible events. Always call this first before taking any action.",
@@ -67,18 +78,22 @@ export default defineAction({
67
78
  const nav = navigation as any;
68
79
 
69
80
  if (nav?.view === "calendar" || !nav?.view) {
70
- const now = new Date();
71
- const viewDate = nav?.date ? new Date(nav.date) : now;
72
-
73
- const from = new Date(viewDate);
74
- from.setDate(from.getDate() - from.getDay());
75
- from.setHours(0, 0, 0, 0);
76
- const to = new Date(from);
77
- to.setDate(to.getDate() + 7);
81
+ const email = getRequestUserEmail();
82
+ if (!email) throw new Error("no authenticated user");
83
+ const timezone = await getCalendarTimezone(email);
84
+ // Work in calendar days, then resolve the two edges to instants once.
85
+ const viewDay = nav?.date ?? dateKeyInTimezone(new Date(), timezone);
86
+ // Noon UTC so the weekday can never be shifted by an offset.
87
+ const weekday = new Date(`${viewDay}T12:00:00Z`).getUTCDay();
88
+ const weekStart = addDaysToDateKey(viewDay, -weekday);
78
89
 
79
90
  const eventResult = await fetchEventsForRange(
80
- from.toISOString(),
81
- to.toISOString(),
91
+ dateTimeInTimezoneToIso(weekStart, "00:00", timezone),
92
+ dateTimeInTimezoneToIso(
93
+ addDaysToDateKey(weekStart, 7),
94
+ "00:00",
95
+ timezone,
96
+ ),
82
97
  );
83
98
  const { events } = eventResult;
84
99
 
@@ -1,6 +1,7 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import { CommandMenu } from "@agent-native/core/client/navigation";
3
3
  import type { CalendarEvent } from "@shared/api";
4
+ import { timezoneFormatter } from "@shared/timezone";
4
5
  import {
5
6
  IconCalendar,
6
7
  IconClock,
@@ -28,6 +29,7 @@ interface CommandPaletteProps {
28
29
  open: boolean;
29
30
  onClose: () => void;
30
31
  events: CalendarEvent[];
32
+ timezone?: string;
31
33
  onGoToDate: (date: Date) => void;
32
34
  onEventClick: (event: CalendarEvent) => void;
33
35
  onCreateEvent: () => void;
@@ -80,6 +82,7 @@ export function CommandPalette({
80
82
  open,
81
83
  onClose,
82
84
  events,
85
+ timezone,
83
86
  onGoToDate,
84
87
  onEventClick,
85
88
  onCreateEvent,
@@ -192,7 +195,10 @@ export function CommandPalette({
192
195
  />
193
196
  <span className="flex-1 truncate">{event.title}</span>
194
197
  <span className="ml-2 text-xs text-muted-foreground">
195
- {format(parseISO(event.start), "MMM d")}
198
+ {timezoneFormatter(event.allDay ? "UTC" : timezone, {
199
+ month: "short",
200
+ day: "numeric",
201
+ }).format(new Date(event.start))}
196
202
  </span>
197
203
  </CommandMenu.Item>
198
204
  ))}
@@ -1,5 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
2
  import type { CalendarEvent } from "@shared/api";
3
+ import { timezoneFormatter } from "@shared/timezone";
3
4
  import { IconAlertTriangleFilled, IconCalendarOff } from "@tabler/icons-react";
4
5
 
5
6
  import {
@@ -26,6 +27,7 @@ interface EventCardProps {
26
27
  onDragEnd?: () => void;
27
28
  dimmed?: boolean;
28
29
  colorPreferences?: CalendarColorPreferences;
30
+ timezone?: string;
29
31
  }
30
32
 
31
33
  export function EventCard({
@@ -37,6 +39,7 @@ export function EventCard({
37
39
  onDragEnd,
38
40
  dimmed = false,
39
41
  colorPreferences,
42
+ timezone,
40
43
  }: EventCardProps) {
41
44
  const t = useT();
42
45
  const workingLocationLabels = createWorkingLocationDisplayLabels(t);
@@ -164,10 +167,10 @@ export function EventCard({
164
167
  )}
165
168
  {!event.allDay && (
166
169
  <span className="text-foreground/70">
167
- {new Date(event.start).toLocaleTimeString([], {
170
+ {timezoneFormatter(timezone, {
168
171
  hour: "numeric",
169
172
  minute: "2-digit",
170
- })}
173
+ }).format(new Date(event.start))}
171
174
  </span>
172
175
  )}
173
176
  {event.ownerColor && (
@@ -1,4 +1,5 @@
1
1
  import type { CalendarEvent } from "@shared/api";
2
+ import { addDaysToDateKey, isCalendarTimezone } from "@shared/timezone";
2
3
  import {
3
4
  addDays,
4
5
  endOfMonth,
@@ -42,20 +43,8 @@ export function getBrowserTimezone(): string {
42
43
  }
43
44
  }
44
45
 
45
- export function isValidTimezone(timezone: string): boolean {
46
- try {
47
- new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
48
- return true;
49
- } catch (error) {
50
- if (error instanceof RangeError) return false;
51
- throw error;
52
- }
53
- }
54
-
55
46
  export function normalizeTimezone(timezone?: string): string {
56
- return timezone && isValidTimezone(timezone)
57
- ? timezone
58
- : getBrowserTimezone();
47
+ return isCalendarTimezone(timezone) ? timezone : getBrowserTimezone();
59
48
  }
60
49
 
61
50
  /** Date carriers are kept at local noon so browser DST never changes their date. */
@@ -68,11 +57,7 @@ export function dateToCalendarDateKey(date: Date): string {
68
57
  return format(date, "yyyy-MM-dd");
69
58
  }
70
59
 
71
- export function addCalendarDays(date: string, amount: number): string {
72
- const [year, month, day] = date.split("-").map(Number);
73
- const next = new Date(Date.UTC(year, month - 1, day + amount));
74
- return next.toISOString().slice(0, 10);
75
- }
60
+ export const addCalendarDays = addDaysToDateKey;
76
61
 
77
62
  function dateTimeParts(value: Date | string, timezone: string) {
78
63
  const parsed = value instanceof Date ? value : new Date(value);
@@ -1,4 +1,7 @@
1
1
  import type { CalendarEvent, UpdateEventScope } from "@shared/api";
2
+ import { dateTimeInTimezoneToIso } from "@shared/timezone";
3
+
4
+ export { dateTimeInTimezoneToIso };
2
5
 
3
6
  export type ReminderMethod = "popup" | "email";
4
7
  export type ReminderMode = "default" | "none" | "custom";
@@ -247,68 +250,6 @@ export function resolveEventTimezone(timezone?: string | null) {
247
250
  return timezone?.trim() || getLocalTimezone();
248
251
  }
249
252
 
250
- function getTimezoneOffsetMs(date: Date, timezone: string) {
251
- const parts = new Intl.DateTimeFormat("en-US", {
252
- timeZone: timezone,
253
- hourCycle: "h23",
254
- year: "numeric",
255
- month: "2-digit",
256
- day: "2-digit",
257
- hour: "2-digit",
258
- minute: "2-digit",
259
- second: "2-digit",
260
- }).formatToParts(date);
261
- const values = new Map(parts.map((part) => [part.type, part.value]));
262
- const asUtc = Date.UTC(
263
- Number(values.get("year")),
264
- Number(values.get("month")) - 1,
265
- Number(values.get("day")),
266
- Number(values.get("hour")),
267
- Number(values.get("minute")),
268
- Number(values.get("second")),
269
- );
270
- return asUtc - date.getTime();
271
- }
272
-
273
- export function dateTimeInTimezoneToIso(
274
- date: string,
275
- time: string,
276
- timezone: string,
277
- ) {
278
- const [year, month, day] = date.split("-").map(Number);
279
- const [hour, minute] = time.split(":").map(Number);
280
- const wallClockUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
281
- const offsets = new Set<number>();
282
- for (let hours = -36; hours <= 36; hours += 6) {
283
- offsets.add(
284
- getTimezoneOffsetMs(
285
- new Date(wallClockUtc + hours * 60 * 60 * 1000),
286
- timezone,
287
- ),
288
- );
289
- }
290
-
291
- const candidates = [...offsets]
292
- .map((offset) => new Date(wallClockUtc - offset))
293
- .map((candidate) => ({
294
- candidate,
295
- localWallClock:
296
- candidate.getTime() + getTimezoneOffsetMs(candidate, timezone),
297
- }))
298
- .sort((a, b) => {
299
- const aDelta = a.localWallClock - wallClockUtc;
300
- const bDelta = b.localWallClock - wallClockUtc;
301
- if (aDelta === 0 && bDelta === 0) {
302
- return a.candidate.getTime() - b.candidate.getTime();
303
- }
304
- if (aDelta >= 0 && bDelta < 0) return -1;
305
- if (aDelta < 0 && bDelta >= 0) return 1;
306
- return Math.abs(aDelta) - Math.abs(bDelta);
307
- });
308
-
309
- return candidates[0].candidate.toISOString();
310
- }
311
-
312
253
  export function formatTimezoneLabel(timezone: string) {
313
254
  const city = timezone.split("/").pop()?.replace(/_/g, " ") || timezone;
314
255
  return `${city} (${timezone})`;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-22
4
+ ---
5
+
6
+ Calendar views now render, navigate, and create events in the timezone selected in Calendar settings.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-19
4
+ ---
5
+
6
+ The calendar grid and settings load again for accounts whose saved timezone was stored in a format the calendar no longer understands
@@ -1,13 +1,11 @@
1
1
  import { readBody, getSession } from "@agent-native/core/server";
2
- import {
3
- getSetting,
4
- getUserSetting,
5
- putUserSetting,
6
- putSetting,
7
- } from "@agent-native/core/settings";
8
2
  import { defineEventHandler, setResponseStatus, type H3Event } from "h3";
9
3
 
10
- import { normalizeCalendarSettings } from "../../shared/settings.js";
4
+ import {
5
+ readCalendarSettings,
6
+ readPublicCalendarSettings,
7
+ saveCalendarSettings,
8
+ } from "../lib/calendar-settings.js";
11
9
 
12
10
  async function uEmail(event: H3Event): Promise<string> {
13
11
  const session = await getSession(event);
@@ -20,10 +18,7 @@ async function uEmail(event: H3Event): Promise<string> {
20
18
 
21
19
  export const getSettings = defineEventHandler(async (event: H3Event) => {
22
20
  try {
23
- const email = await uEmail(event);
24
- return normalizeCalendarSettings(
25
- await getUserSetting(email, "calendar-settings"),
26
- );
21
+ return await readCalendarSettings(await uEmail(event));
27
22
  } catch (error: any) {
28
23
  setResponseStatus(event, 500);
29
24
  return { error: error.message };
@@ -31,24 +26,13 @@ export const getSettings = defineEventHandler(async (event: H3Event) => {
31
26
  });
32
27
 
33
28
  export const getPublicSettings = defineEventHandler(async (_event: H3Event) => {
34
- return normalizeCalendarSettings(await getSetting("calendar-settings"));
29
+ return readPublicCalendarSettings();
35
30
  });
36
31
 
37
32
  export const updateSettings = defineEventHandler(async (event: H3Event) => {
38
33
  try {
39
34
  const email = await uEmail(event);
40
- const body = await readBody<unknown>(event);
41
- const settings = normalizeCalendarSettings({
42
- ...normalizeCalendarSettings(
43
- await getUserSetting(email, "calendar-settings"),
44
- ),
45
- ...(body && typeof body === "object" ? body : {}),
46
- });
47
- const settingsRecord = settings as unknown as Record<string, unknown>;
48
- await putUserSetting(email, "calendar-settings", settingsRecord);
49
- // Also write to global key so the public booking/settings page can read it
50
- await putSetting("calendar-settings", settingsRecord);
51
- return settings;
35
+ return await saveCalendarSettings(email, await readBody<unknown>(event));
52
36
  } catch (error: any) {
53
37
  setResponseStatus(event, 500);
54
38
  return { error: error.message };
@@ -0,0 +1,58 @@
1
+ import { getRequestTimezone } from "@agent-native/core/server";
2
+ import {
3
+ getSetting,
4
+ getUserSetting,
5
+ putSetting,
6
+ putUserSetting,
7
+ } from "@agent-native/core/settings";
8
+
9
+ import type { Settings } from "../../shared/api.js";
10
+ import {
11
+ DEFAULT_SETTINGS,
12
+ normalizeCalendarSettings,
13
+ } from "../../shared/settings.js";
14
+ import { isCalendarTimezone } from "../../shared/timezone.js";
15
+
16
+ const SETTINGS_KEY = "calendar-settings";
17
+
18
+ function callerTimezone(): string {
19
+ const timezone = getRequestTimezone();
20
+ return isCalendarTimezone(timezone) ? timezone : DEFAULT_SETTINGS.timezone;
21
+ }
22
+
23
+ export async function readCalendarSettings(email: string): Promise<Settings> {
24
+ return normalizeCalendarSettings(await getUserSetting(email, SETTINGS_KEY), {
25
+ timezone: callerTimezone(),
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Settings for the public booking page. The fixed default applies here rather
31
+ * than the caller's zone: a visitor must not shift the owner's booking times.
32
+ */
33
+ export async function readPublicCalendarSettings(): Promise<Settings> {
34
+ return normalizeCalendarSettings(await getSetting(SETTINGS_KEY));
35
+ }
36
+
37
+ /** Merge a patch over the stored settings and persist the whole record. */
38
+ export async function saveCalendarSettings(
39
+ email: string,
40
+ patch: unknown,
41
+ ): Promise<Settings> {
42
+ const settings = normalizeCalendarSettings({
43
+ ...(await readCalendarSettings(email)),
44
+ ...(patch && typeof patch === "object" ? patch : {}),
45
+ });
46
+ const record = settings as unknown as Record<string, unknown>;
47
+ await Promise.all([
48
+ putUserSetting(email, SETTINGS_KEY, record),
49
+ // Also write the global key so the public booking page can read it.
50
+ putSetting(SETTINGS_KEY, record),
51
+ ]);
52
+ return settings;
53
+ }
54
+
55
+ /** The timezone to compute event ranges in — always a valid IANA zone. */
56
+ export async function getCalendarTimezone(email: string): Promise<string> {
57
+ return (await readCalendarSettings(email)).timezone;
58
+ }