@agent-native/core 0.172.3 → 0.172.6
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/get-settings.ts +2 -5
- package/corpus/templates/calendar/actions/list-events.ts +21 -80
- package/corpus/templates/calendar/actions/update-settings.ts +11 -16
- package/corpus/templates/calendar/actions/view-screen.ts +25 -10
- package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
- package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
- package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
- package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
- package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
- package/corpus/templates/calendar/shared/settings.ts +15 -9
- package/corpus/templates/calendar/shared/timezone.ts +127 -0
- package/corpus/templates/factory/AGENTS.md +5 -3
- package/corpus/templates/factory/actions/babysit-pull-request.ts +6 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +0 -5
- package/corpus/templates/factory/changelog/2026-08-24-slack-checking-works-for-teammate-created-factories.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-24-teammates-can-edit-and-run-factory-jobs.md +6 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +8 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +1 -9
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -1
- package/corpus/templates/slides/actions/list-decks.ts +79 -5
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
- package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
- package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
- package/corpus/templates/slides/app/pages/Index.tsx +11 -13
- package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
- package/dist/agent/production-agent.js +5 -1
- package/dist/agent/types.d.ts +5 -0
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +20 -0
- package/dist/client/settings/SettingsPanel.js +80 -33
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/run-now.js +2 -2
- package/dist/observability/posthog-ai.d.ts +36 -11
- package/dist/observability/posthog-ai.js +99 -30
- package/dist/observability/routes.js +9 -2
- package/dist/observability/traces.js +330 -142
- package/dist/tracking/providers.js +9 -3
- package/dist/vite/client.js +5 -2
- package/docs/content/docs-components.mdx +20 -1
- package/docs/content/observability.mdx +27 -10
- package/docs/content/template-calendar-agent.mdx +63 -26
- package/docs/content/template-calendar-developers.mdx +165 -40
- package/docs/content/template-calendar-features.mdx +276 -0
- package/docs/content/template-calendar-integrations.mdx +78 -0
- package/docs/content/template-calendar.mdx +56 -123
- package/package.json +1 -1
- package/docs/content/template-calendar-booking-links.mdx +0 -73
- package/docs/content/template-calendar-scheduling.mdx +0 -128
package/corpus/README.md
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
441
|
-
const
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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})`;
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|