@luckydye/calendar 1.3.1 → 1.4.0
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/dist/calendar.js +2777 -2010
- package/package.json +7 -1
- package/src/ActiveCalendarStore.ts +88 -88
- package/src/CalDAVConfig.ts +611 -514
- package/src/CalDAVSource.ts +561 -433
- package/src/CalendarIntegration.ts +64 -47
- package/src/CalendarInternal.ts +645 -613
- package/src/CalendarLayer.ts +1 -0
- package/src/CalendarStorage.ts +51 -48
- package/src/CalendarView.ts +1085 -505
- package/src/Color.ts +48 -54
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +758 -661
- package/src/ICal.ts +420 -343
- package/src/InMemorySource.ts +56 -48
- package/src/IndexedDBStorage.ts +444 -395
- package/src/InhouseBookingSource.ts +614 -522
- package/src/Keybinds.ts +6 -1
- package/src/NotificationScheduler.ts +11 -8
- package/src/StatusBar.ts +12 -8
- package/src/StatusMessage.ts +2 -2
- package/src/Theme.ts +21 -7
- package/src/TimeseriesJson.ts +98 -98
- package/src/app.ts +301 -115
- package/src/layers/EventsLayer.ts +530 -400
- package/src/layers/GridLayer.ts +45 -125
- package/src/layers/TimeseriesHeatmapLayer.ts +123 -120
- package/src/service-worker.js +3 -2
package/src/app.ts
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
2
2
|
import "./CalDAVConfig.ts";
|
|
3
3
|
import type { CalDAVConfigElement } from "./CalDAVConfig.ts";
|
|
4
|
+
import { CalDAVSource } from "./CalDAVSource.js";
|
|
4
5
|
import {
|
|
5
|
-
CalendarIntegration,
|
|
6
6
|
type Calendar,
|
|
7
|
+
CalendarIntegration,
|
|
7
8
|
type CalendarSource,
|
|
8
9
|
} from "./CalendarIntegration.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { initializeTheme } from "./Theme.js";
|
|
12
|
-
import { InMemorySource } from "./InMemorySource.js";
|
|
10
|
+
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
11
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
13
12
|
import { GoogleCalendarSource } from "./GoogleCalendarSource.js";
|
|
13
|
+
import {
|
|
14
|
+
fetchICalEventsWithNotifications,
|
|
15
|
+
parseICalEvents,
|
|
16
|
+
parseICalEventsWithNotifications,
|
|
17
|
+
} from "./ICal.js";
|
|
18
|
+
import { InMemorySource } from "./InMemorySource.js";
|
|
14
19
|
import { InhouseBookingSource } from "./InhouseBookingSource.js";
|
|
15
|
-
import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
|
|
16
|
-
import { queueStatus } from "./StatusMessage.js";
|
|
17
|
-
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
18
|
-
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
19
20
|
import { registerKeybinds } from "./Keybinds.js";
|
|
20
|
-
import "./StatusBar.js";
|
|
21
21
|
import { NotificationScheduler } from "./NotificationScheduler.js";
|
|
22
|
-
import
|
|
22
|
+
import "./StatusBar.js";
|
|
23
23
|
import type { StatusBarElement } from "./StatusBar.js";
|
|
24
|
+
import { queueStatus } from "./StatusMessage.js";
|
|
25
|
+
import { initializeTheme } from "./Theme.js";
|
|
26
|
+
import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
|
|
27
|
+
import { CalendarViewElement } from "./lib.ts";
|
|
28
|
+
import serviceWorkerUrl from "./service-worker.js?url";
|
|
24
29
|
|
|
25
30
|
try {
|
|
26
|
-
|
|
27
|
-
} catch(err) {
|
|
28
|
-
|
|
31
|
+
customElements.define("calendar-view", CalendarViewElement);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err);
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
// Initialize theme on app start
|
|
@@ -36,16 +41,158 @@ const calendarElement = document.querySelector(
|
|
|
36
41
|
) as CalendarViewElement;
|
|
37
42
|
const calendar = calendarElement.internal;
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
type PromptApiLanguageModel = {
|
|
45
|
+
availability?: () => Promise<string>;
|
|
46
|
+
create: (options?: unknown) => Promise<{
|
|
47
|
+
promptStreaming: (
|
|
48
|
+
prompt: string,
|
|
49
|
+
options?: unknown,
|
|
50
|
+
) => Promise<ReadableStream<string>> | ReadableStream<string>;
|
|
51
|
+
destroy?: () => void;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function getPromptApiLanguageModel(): PromptApiLanguageModel | null {
|
|
56
|
+
const fromGlobal = (globalThis as { LanguageModel?: unknown }).LanguageModel;
|
|
57
|
+
if (fromGlobal) return fromGlobal as PromptApiLanguageModel;
|
|
58
|
+
|
|
59
|
+
const fromWindow = (window as { ai?: { languageModel?: unknown } }).ai
|
|
60
|
+
?.languageModel;
|
|
61
|
+
if (!fromWindow) return null;
|
|
62
|
+
return fromWindow as PromptApiLanguageModel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let summaryAbortController: AbortController | null = null;
|
|
66
|
+
let summarySession: { destroy?: () => void } | null = null;
|
|
67
|
+
let summaryRequestToken = 0;
|
|
68
|
+
let activeSummaryKey: string | null = null;
|
|
69
|
+
|
|
70
|
+
function abortActiveDescriptionSummary(): void {
|
|
71
|
+
if (summaryAbortController) {
|
|
72
|
+
summaryAbortController.abort();
|
|
73
|
+
summaryAbortController = null;
|
|
74
|
+
}
|
|
75
|
+
if (summarySession) {
|
|
76
|
+
summarySession.destroy?.();
|
|
77
|
+
summarySession = null;
|
|
78
|
+
}
|
|
79
|
+
if (activeSummaryKey) {
|
|
80
|
+
calendarElement.cancelDescriptionSummary(activeSummaryKey);
|
|
81
|
+
activeSummaryKey = null;
|
|
82
|
+
}
|
|
83
|
+
summaryRequestToken++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function streamDescriptionSummary(
|
|
87
|
+
key: string,
|
|
88
|
+
description: string,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const sanitizedDescription = sanitizeEventDescription(description);
|
|
91
|
+
abortActiveDescriptionSummary();
|
|
92
|
+
activeSummaryKey = key;
|
|
93
|
+
const token = summaryRequestToken;
|
|
94
|
+
calendarElement.startDescriptionSummary(key);
|
|
95
|
+
|
|
96
|
+
const languageModel = getPromptApiLanguageModel();
|
|
97
|
+
if (!languageModel) {
|
|
98
|
+
calendarElement.failDescriptionSummary(
|
|
99
|
+
key,
|
|
100
|
+
"Prompt API not available in this browser.",
|
|
101
|
+
);
|
|
102
|
+
activeSummaryKey = null;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
summaryAbortController = controller;
|
|
108
|
+
let session: { destroy?: () => void } | null = null;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const availability = await languageModel.availability?.();
|
|
112
|
+
if (token !== summaryRequestToken || key !== activeSummaryKey) return;
|
|
113
|
+
if (availability === "unavailable") {
|
|
114
|
+
throw new Error("Prompt API model is unavailable.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
session = await languageModel.create();
|
|
118
|
+
summarySession = session;
|
|
119
|
+
const prompt = [
|
|
120
|
+
"Summarize this calendar event description in 2-3 concise sentences.",
|
|
121
|
+
"Keep critical details like dates, times, links, and action items.",
|
|
122
|
+
"Return plain text only.",
|
|
123
|
+
"",
|
|
124
|
+
sanitizedDescription,
|
|
125
|
+
].join("\n");
|
|
126
|
+
|
|
127
|
+
const stream = await Promise.resolve(
|
|
128
|
+
session.promptStreaming(prompt, { signal: controller.signal }),
|
|
129
|
+
);
|
|
130
|
+
const reader = stream.getReader();
|
|
131
|
+
try {
|
|
132
|
+
while (true) {
|
|
133
|
+
const { value, done } = await reader.read();
|
|
134
|
+
if (done) break;
|
|
135
|
+
if (
|
|
136
|
+
token !== summaryRequestToken ||
|
|
137
|
+
key !== activeSummaryKey ||
|
|
138
|
+
controller.signal.aborted
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (value) {
|
|
143
|
+
calendarElement.appendDescriptionSummaryChunk(key, value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
reader.releaseLock();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (token !== summaryRequestToken || key !== activeSummaryKey) return;
|
|
151
|
+
calendarElement.finishDescriptionSummary(key);
|
|
152
|
+
activeSummaryKey = null;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (
|
|
155
|
+
controller.signal.aborted ||
|
|
156
|
+
token !== summaryRequestToken ||
|
|
157
|
+
key !== activeSummaryKey
|
|
158
|
+
) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
calendarElement.failDescriptionSummary(
|
|
162
|
+
key,
|
|
163
|
+
error instanceof Error
|
|
164
|
+
? error.message
|
|
165
|
+
: "Failed to generate description summary.",
|
|
166
|
+
);
|
|
167
|
+
activeSummaryKey = null;
|
|
168
|
+
} finally {
|
|
169
|
+
if (summaryAbortController === controller) {
|
|
170
|
+
summaryAbortController = null;
|
|
171
|
+
}
|
|
172
|
+
if (summarySession === session) {
|
|
173
|
+
summarySession = null;
|
|
174
|
+
}
|
|
175
|
+
session?.destroy?.();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
registerKeybinds([
|
|
180
|
+
{
|
|
181
|
+
key: "r",
|
|
182
|
+
cmdOrCtrl: true,
|
|
183
|
+
shift: true,
|
|
184
|
+
action: () => calendarElement.forceSync(),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "c",
|
|
188
|
+
cmdOrCtrl: true,
|
|
189
|
+
action: () => calendarElement.copySelectedEvents(),
|
|
190
|
+
},
|
|
191
|
+
{ key: "Backspace", action: () => calendarElement.deleteSelectedEvents() },
|
|
192
|
+
{ key: "Delete", action: () => calendarElement.deleteSelectedEvents() },
|
|
193
|
+
{ key: "Escape", action: () => calendarElement.escape() },
|
|
194
|
+
{ key: "t", action: () => calendarElement.scrollToToday() },
|
|
195
|
+
]);
|
|
49
196
|
|
|
50
197
|
let workerPromise: Promise<ServiceWorkerRegistration> | undefined;
|
|
51
198
|
|
|
@@ -61,10 +208,14 @@ if ("serviceWorker" in navigator) {
|
|
|
61
208
|
// Initialize notification scheduler
|
|
62
209
|
const notificationScheduler = new NotificationScheduler(() => workerPromise);
|
|
63
210
|
|
|
64
|
-
async function scheduleNotificationsForEvents(
|
|
211
|
+
async function scheduleNotificationsForEvents(
|
|
212
|
+
events: CalendarEvent[],
|
|
213
|
+
): Promise<void> {
|
|
65
214
|
const now = new Date();
|
|
66
215
|
const next24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
67
|
-
const upcoming = events.filter(
|
|
216
|
+
const upcoming = events.filter(
|
|
217
|
+
(e) => e.start >= now && e.start <= next24Hours && e.reminders?.length,
|
|
218
|
+
);
|
|
68
219
|
for (const event of upcoming) {
|
|
69
220
|
await notificationScheduler.scheduleEventNotifications(event);
|
|
70
221
|
}
|
|
@@ -105,14 +256,36 @@ async function requestNotificationPermission() {
|
|
|
105
256
|
}
|
|
106
257
|
}
|
|
107
258
|
|
|
108
|
-
calendarElement.addEventListener(
|
|
109
|
-
|
|
110
|
-
|
|
259
|
+
calendarElement.addEventListener(
|
|
260
|
+
"event-click",
|
|
261
|
+
() => {
|
|
262
|
+
requestNotificationPermission();
|
|
263
|
+
},
|
|
264
|
+
{ once: true },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
calendarElement.addEventListener("request-description-summary", (e: Event) => {
|
|
268
|
+
const { key, description } = (e as CustomEvent).detail as {
|
|
269
|
+
key: string;
|
|
270
|
+
description: string;
|
|
271
|
+
};
|
|
272
|
+
void streamDescriptionSummary(key, description);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
calendarElement.addEventListener("cancel-description-summary", (e: Event) => {
|
|
276
|
+
const { key } = (e as CustomEvent).detail as { key: string };
|
|
277
|
+
if (key === activeSummaryKey) {
|
|
278
|
+
abortActiveDescriptionSummary();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
111
281
|
|
|
112
282
|
// Load iCal files
|
|
113
283
|
const integration = new CalendarIntegration(calendar);
|
|
114
284
|
|
|
115
|
-
async function sync(
|
|
285
|
+
async function sync(
|
|
286
|
+
cal: Calendar,
|
|
287
|
+
options?: { force?: boolean },
|
|
288
|
+
): Promise<void> {
|
|
116
289
|
const events = await integration.sync(cal, options);
|
|
117
290
|
await scheduleNotificationsForEvents(events);
|
|
118
291
|
}
|
|
@@ -124,9 +297,14 @@ let mutationsSettledResolve: (() => void) | null = null;
|
|
|
124
297
|
let mutationsSettledPromise: Promise<void> | null = null;
|
|
125
298
|
const dirtyCalendars = new Set<Calendar>();
|
|
126
299
|
|
|
127
|
-
async function withMutation(
|
|
300
|
+
async function withMutation(
|
|
301
|
+
cal: Calendar,
|
|
302
|
+
fn: () => Promise<void>,
|
|
303
|
+
): Promise<void> {
|
|
128
304
|
if (pendingMutations === 0) {
|
|
129
|
-
mutationsSettledPromise = new Promise(r => {
|
|
305
|
+
mutationsSettledPromise = new Promise((r) => {
|
|
306
|
+
mutationsSettledResolve = r;
|
|
307
|
+
});
|
|
130
308
|
}
|
|
131
309
|
pendingMutations++;
|
|
132
310
|
try {
|
|
@@ -158,11 +336,11 @@ async function waitForMutations(): Promise<void> {
|
|
|
158
336
|
await calendar.initPromise;
|
|
159
337
|
|
|
160
338
|
// Register sample source
|
|
161
|
-
const sampleSource = new InMemorySource(
|
|
339
|
+
const sampleSource = new InMemorySource("sample", "Sample Events", "#10b981");
|
|
162
340
|
const sampleCalendar = createInMemoryCalendar(sampleSource);
|
|
163
341
|
loadedCalendars.set(sampleCalendar.id, sampleCalendar);
|
|
164
342
|
activeCalendarStore.registerCalendar(sampleCalendar);
|
|
165
|
-
|
|
343
|
+
|
|
166
344
|
// Create sidebar with CalDAV config
|
|
167
345
|
const sidebar = document.createElement("div");
|
|
168
346
|
sidebar.slot = "sidebar";
|
|
@@ -198,7 +376,6 @@ async function waitForMutations(): Promise<void> {
|
|
|
198
376
|
updateActiveCalendarColor();
|
|
199
377
|
})();
|
|
200
378
|
|
|
201
|
-
|
|
202
379
|
// Store for loaded calendars
|
|
203
380
|
const loadedCalendars: Map<string, Calendar> = new Map();
|
|
204
381
|
let config: CalDAVConfigElement;
|
|
@@ -243,7 +420,7 @@ async function createCalDAVCalendars(
|
|
|
243
420
|
username,
|
|
244
421
|
password,
|
|
245
422
|
},
|
|
246
|
-
source.enabled
|
|
423
|
+
source.enabled,
|
|
247
424
|
);
|
|
248
425
|
|
|
249
426
|
// Fetch all calendars and the current user's email from the CalDAV server
|
|
@@ -267,7 +444,11 @@ async function createCalDAVCalendars(
|
|
|
267
444
|
calendarUrl: calInfo.url,
|
|
268
445
|
|
|
269
446
|
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
270
|
-
return caldavSource.fetchEventsForCalendar(
|
|
447
|
+
return caldavSource.fetchEventsForCalendar(
|
|
448
|
+
calInfo.url,
|
|
449
|
+
calInfo.displayName,
|
|
450
|
+
calInfo.color,
|
|
451
|
+
);
|
|
271
452
|
},
|
|
272
453
|
|
|
273
454
|
async createEvent(
|
|
@@ -277,15 +458,18 @@ async function createCalDAVCalendars(
|
|
|
277
458
|
},
|
|
278
459
|
|
|
279
460
|
async updateEvent(
|
|
280
|
-
|
|
461
|
+
event: CalendarEvent,
|
|
281
462
|
updates: Partial<CalendarEvent>,
|
|
282
463
|
): Promise<CalendarEvent> {
|
|
283
|
-
return caldavSource.updateEvent(
|
|
464
|
+
return caldavSource.updateEvent(event, {
|
|
465
|
+
...updates,
|
|
466
|
+
calendarId: calInfo.url,
|
|
467
|
+
});
|
|
284
468
|
},
|
|
285
469
|
|
|
286
470
|
async deleteEvent(id: string): Promise<void> {
|
|
287
|
-
const eventUrl = `${calInfo.url.replace(/\/$/,
|
|
288
|
-
await caldavSource.request(eventUrl,
|
|
471
|
+
const eventUrl = `${calInfo.url.replace(/\/$/, "")}/${id}.ics`;
|
|
472
|
+
await caldavSource.request(eventUrl, "DELETE", undefined, {});
|
|
289
473
|
},
|
|
290
474
|
});
|
|
291
475
|
}
|
|
@@ -305,7 +489,11 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
305
489
|
sourceType: "ical",
|
|
306
490
|
|
|
307
491
|
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
308
|
-
const result = await fetchICalEventsWithNotifications(
|
|
492
|
+
const result = await fetchICalEventsWithNotifications(
|
|
493
|
+
source.credentials,
|
|
494
|
+
source.color,
|
|
495
|
+
source.name,
|
|
496
|
+
);
|
|
309
497
|
return result.events;
|
|
310
498
|
},
|
|
311
499
|
// iCal calendars are read-only by default
|
|
@@ -325,7 +513,11 @@ function createTimeseriesJsonCalendar(source: CalendarSource): Calendar {
|
|
|
325
513
|
|
|
326
514
|
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
327
515
|
return fetchTimeseriesJsonEvents(
|
|
328
|
-
source.credentials as {
|
|
516
|
+
source.credentials as {
|
|
517
|
+
url: string;
|
|
518
|
+
timestampField?: string;
|
|
519
|
+
titleField?: string;
|
|
520
|
+
},
|
|
329
521
|
source.color,
|
|
330
522
|
source.name,
|
|
331
523
|
source.id,
|
|
@@ -348,7 +540,7 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
348
540
|
tokenExpiry: source.credentials.tokenExpiry,
|
|
349
541
|
calendarId: calendarId,
|
|
350
542
|
},
|
|
351
|
-
source.enabled
|
|
543
|
+
source.enabled,
|
|
352
544
|
);
|
|
353
545
|
|
|
354
546
|
return {
|
|
@@ -372,10 +564,10 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
372
564
|
},
|
|
373
565
|
|
|
374
566
|
async updateEvent(
|
|
375
|
-
|
|
567
|
+
event: CalendarEvent,
|
|
376
568
|
updates: Partial<CalendarEvent>,
|
|
377
569
|
): Promise<CalendarEvent> {
|
|
378
|
-
return googleSource.updateEvent(
|
|
570
|
+
return googleSource.updateEvent(event, updates);
|
|
379
571
|
},
|
|
380
572
|
|
|
381
573
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -396,7 +588,7 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
396
588
|
unitId: source.credentials.unitId,
|
|
397
589
|
startHour: source.credentials.startHour,
|
|
398
590
|
},
|
|
399
|
-
source.enabled
|
|
591
|
+
source.enabled,
|
|
400
592
|
);
|
|
401
593
|
|
|
402
594
|
return {
|
|
@@ -419,10 +611,10 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
419
611
|
},
|
|
420
612
|
|
|
421
613
|
async updateEvent(
|
|
422
|
-
|
|
614
|
+
event: CalendarEvent,
|
|
423
615
|
updates: Partial<CalendarEvent>,
|
|
424
616
|
): Promise<CalendarEvent> {
|
|
425
|
-
return inhouseSource.updateEvent(
|
|
617
|
+
return inhouseSource.updateEvent(event, updates);
|
|
426
618
|
},
|
|
427
619
|
|
|
428
620
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -458,12 +650,12 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
|
|
|
458
650
|
},
|
|
459
651
|
|
|
460
652
|
async updateEvent(
|
|
461
|
-
|
|
653
|
+
event: CalendarEvent,
|
|
462
654
|
updates: Partial<CalendarEvent>,
|
|
463
655
|
): Promise<CalendarEvent> {
|
|
464
656
|
if (!source.updateEvent)
|
|
465
657
|
throw new Error("Source does not support updateEvent");
|
|
466
|
-
return source.updateEvent(
|
|
658
|
+
return source.updateEvent(event, updates);
|
|
467
659
|
},
|
|
468
660
|
|
|
469
661
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -564,7 +756,9 @@ async function syncCalDAV(force = false) {
|
|
|
564
756
|
}
|
|
565
757
|
} else if (source.type === "timeseries-json") {
|
|
566
758
|
if (!source.credentials?.url) {
|
|
567
|
-
console.warn(
|
|
759
|
+
console.warn(
|
|
760
|
+
`Skipping Timeseries JSON source ${source.name}: missing URL`,
|
|
761
|
+
);
|
|
568
762
|
continue;
|
|
569
763
|
}
|
|
570
764
|
|
|
@@ -575,11 +769,16 @@ async function syncCalDAV(force = false) {
|
|
|
575
769
|
try {
|
|
576
770
|
await sync(calendar, { force });
|
|
577
771
|
} catch (error) {
|
|
578
|
-
console.error(
|
|
772
|
+
console.error(
|
|
773
|
+
`Sync error for Timeseries JSON source ${source.name}:`,
|
|
774
|
+
error,
|
|
775
|
+
);
|
|
579
776
|
}
|
|
580
777
|
} else if (source.type === "google") {
|
|
581
778
|
if (!source.credentials?.accessToken) {
|
|
582
|
-
console.warn(
|
|
779
|
+
console.warn(
|
|
780
|
+
`Skipping Google Calendar source ${source.name}: missing access token`,
|
|
781
|
+
);
|
|
583
782
|
continue;
|
|
584
783
|
}
|
|
585
784
|
|
|
@@ -591,11 +790,19 @@ async function syncCalDAV(force = false) {
|
|
|
591
790
|
try {
|
|
592
791
|
await sync(calendar, { force });
|
|
593
792
|
} catch (error) {
|
|
594
|
-
console.error(
|
|
793
|
+
console.error(
|
|
794
|
+
`Sync error for Google Calendar source ${source.name}:`,
|
|
795
|
+
error,
|
|
796
|
+
);
|
|
595
797
|
}
|
|
596
798
|
} else if (source.type === "inhouse") {
|
|
597
|
-
if (
|
|
598
|
-
|
|
799
|
+
if (
|
|
800
|
+
!source.credentials?.sessionCookie ||
|
|
801
|
+
!source.credentials?.employeeId
|
|
802
|
+
) {
|
|
803
|
+
console.warn(
|
|
804
|
+
`Skipping Inhouse Booking source ${source.name}: missing session cookie or employee ID`,
|
|
805
|
+
);
|
|
599
806
|
continue;
|
|
600
807
|
}
|
|
601
808
|
|
|
@@ -606,7 +813,10 @@ async function syncCalDAV(force = false) {
|
|
|
606
813
|
try {
|
|
607
814
|
await sync(calendar, { force });
|
|
608
815
|
} catch (error) {
|
|
609
|
-
console.error(
|
|
816
|
+
console.error(
|
|
817
|
+
`Sync error for Inhouse Booking source ${source.name}:`,
|
|
818
|
+
error,
|
|
819
|
+
);
|
|
610
820
|
}
|
|
611
821
|
} else {
|
|
612
822
|
console.warn(`Unknown source type for ${source.name}: ${source.type}`);
|
|
@@ -618,7 +828,7 @@ async function syncCalDAV(force = false) {
|
|
|
618
828
|
updateLockedCalendars();
|
|
619
829
|
|
|
620
830
|
// Push calendar list to sidebar
|
|
621
|
-
config.calendars = Array.from(loadedCalendars.values()).map(cal => ({
|
|
831
|
+
config.calendars = Array.from(loadedCalendars.values()).map((cal) => ({
|
|
622
832
|
id: cal.id,
|
|
623
833
|
name: cal.name,
|
|
624
834
|
color: cal.color,
|
|
@@ -721,7 +931,7 @@ function showProjectPickerDialog(
|
|
|
721
931
|
const filtered = filter
|
|
722
932
|
? projects.filter((p) =>
|
|
723
933
|
p.name.toLowerCase().includes(filter.toLowerCase()),
|
|
724
|
-
|
|
934
|
+
)
|
|
725
935
|
: projects;
|
|
726
936
|
for (const project of filtered) {
|
|
727
937
|
const item = document.createElement("button");
|
|
@@ -798,46 +1008,17 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
798
1008
|
lastSynced: new Date(),
|
|
799
1009
|
});
|
|
800
1010
|
try {
|
|
801
|
-
await withMutation(calendar, () =>
|
|
1011
|
+
await withMutation(calendar, () =>
|
|
1012
|
+
calendar.createEvent({ id, title: "New Event", start, end, isAllDay }),
|
|
1013
|
+
);
|
|
802
1014
|
} catch (error) {
|
|
803
1015
|
calendarElement.internal.removeEventOptimistically(id);
|
|
804
1016
|
console.error("Failed to create event:", error);
|
|
805
|
-
queueStatus(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const { event, start, end } = e.detail;
|
|
811
|
-
|
|
812
|
-
const calendar = findCalendarForEvent(event);
|
|
813
|
-
|
|
814
|
-
if (!calendar) {
|
|
815
|
-
queueStatus("Cannot move event: calendar not found.");
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (!calendar.updateEvent) {
|
|
820
|
-
queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
console.log(
|
|
825
|
-
"Moving event:",
|
|
826
|
-
event.id,
|
|
827
|
-
"from",
|
|
828
|
-
event.start,
|
|
829
|
-
"to",
|
|
830
|
-
start,
|
|
831
|
-
"on calendar:",
|
|
832
|
-
calendar.name,
|
|
833
|
-
);
|
|
834
|
-
try {
|
|
835
|
-
calendarElement.internal.applyEventOptimistically({ ...event, start, end });
|
|
836
|
-
await withMutation(calendar, () => calendar.updateEvent(event.id, { start, end }));
|
|
837
|
-
} catch (error) {
|
|
838
|
-
calendarElement.internal.applyEventOptimistically(event);
|
|
839
|
-
console.error("Failed to move event:", error);
|
|
840
|
-
queueStatus(`Failed to move event: ${error instanceof Error ? error.message : String(error)}`);
|
|
1017
|
+
queueStatus(
|
|
1018
|
+
`Failed to create event: ${
|
|
1019
|
+
error instanceof Error ? error.message : String(error)
|
|
1020
|
+
}`,
|
|
1021
|
+
);
|
|
841
1022
|
}
|
|
842
1023
|
});
|
|
843
1024
|
|
|
@@ -858,17 +1039,16 @@ calendarElement.addEventListener("update-event", async (e) => {
|
|
|
858
1039
|
return;
|
|
859
1040
|
}
|
|
860
1041
|
|
|
861
|
-
console.log(
|
|
862
|
-
"Updating event:",
|
|
863
|
-
event
|
|
864
|
-
);
|
|
1042
|
+
console.log("Updating event:", event);
|
|
865
1043
|
try {
|
|
866
1044
|
calendarElement.internal.applyEventOptimistically({ ...event, ...updates });
|
|
867
|
-
await withMutation(calendar, () => calendar.updateEvent(event
|
|
1045
|
+
await withMutation(calendar, () => calendar.updateEvent(event, updates));
|
|
868
1046
|
|
|
869
1047
|
if ("reminders" in updates) {
|
|
870
1048
|
const eventWithReminders = { ...event, ...updates };
|
|
871
|
-
await notificationScheduler.scheduleEventNotifications(
|
|
1049
|
+
await notificationScheduler.scheduleEventNotifications(
|
|
1050
|
+
eventWithReminders,
|
|
1051
|
+
);
|
|
872
1052
|
|
|
873
1053
|
if (calendarElement.selectedEventForDetail?.id === event.id) {
|
|
874
1054
|
calendarElement.selectedEventForDetail = eventWithReminders;
|
|
@@ -877,7 +1057,11 @@ calendarElement.addEventListener("update-event", async (e) => {
|
|
|
877
1057
|
} catch (error) {
|
|
878
1058
|
calendarElement.internal.applyEventOptimistically(event);
|
|
879
1059
|
console.error("Failed to update event:", error);
|
|
880
|
-
queueStatus(
|
|
1060
|
+
queueStatus(
|
|
1061
|
+
`Failed to update event: ${
|
|
1062
|
+
error instanceof Error ? error.message : String(error)
|
|
1063
|
+
}`,
|
|
1064
|
+
);
|
|
881
1065
|
}
|
|
882
1066
|
});
|
|
883
1067
|
|
|
@@ -909,9 +1093,7 @@ calendarElement.addEventListener("delete-events", async (e) => {
|
|
|
909
1093
|
const calendar = findCalendarForEvent(calendarEvents[0]);
|
|
910
1094
|
|
|
911
1095
|
if (!calendar) {
|
|
912
|
-
queueStatus(
|
|
913
|
-
`Cannot delete events: calendar "${calendarId}" not found.`,
|
|
914
|
-
);
|
|
1096
|
+
queueStatus(`Cannot delete events: calendar "${calendarId}" not found.`);
|
|
915
1097
|
continue;
|
|
916
1098
|
}
|
|
917
1099
|
|
|
@@ -939,7 +1121,11 @@ calendarElement.addEventListener("delete-events", async (e) => {
|
|
|
939
1121
|
calendarElement.internal.applyEventOptimistically(event);
|
|
940
1122
|
}
|
|
941
1123
|
console.error("Failed to delete event:", error);
|
|
942
|
-
queueStatus(
|
|
1124
|
+
queueStatus(
|
|
1125
|
+
`Failed to delete event: ${
|
|
1126
|
+
error instanceof Error ? error.message : String(error)
|
|
1127
|
+
}`,
|
|
1128
|
+
);
|
|
943
1129
|
}
|
|
944
1130
|
}
|
|
945
1131
|
});
|
|
@@ -957,7 +1143,8 @@ calendarElement.addEventListener("force-sync", async () => {
|
|
|
957
1143
|
|
|
958
1144
|
calendarElement.addEventListener("load-notifications", async () => {
|
|
959
1145
|
try {
|
|
960
|
-
const notifications =
|
|
1146
|
+
const notifications =
|
|
1147
|
+
await notificationScheduler.getScheduledNotifications();
|
|
961
1148
|
calendarElement.setScheduledNotifications(notifications);
|
|
962
1149
|
} catch (error) {
|
|
963
1150
|
console.error("Failed to load scheduled notifications:", error);
|
|
@@ -1028,7 +1215,6 @@ calendarElement.addEventListener("import-ical", async (e) => {
|
|
|
1028
1215
|
}
|
|
1029
1216
|
});
|
|
1030
1217
|
|
|
1031
|
-
|
|
1032
1218
|
function updateActiveCalendarColor() {
|
|
1033
1219
|
const calendar = activeCalendarStore.getActiveCalendar();
|
|
1034
1220
|
calendarElement.activeCalendarColor = calendar?.color ?? null;
|
|
@@ -1037,8 +1223,8 @@ function updateActiveCalendarColor() {
|
|
|
1037
1223
|
|
|
1038
1224
|
function updateEnabledCalendars() {
|
|
1039
1225
|
const enabledCalendarIdentifiers = Array.from(loadedCalendars.values())
|
|
1040
|
-
.filter(cal => cal.enabled)
|
|
1041
|
-
.flatMap(cal => {
|
|
1226
|
+
.filter((cal) => cal.enabled)
|
|
1227
|
+
.flatMap((cal) => {
|
|
1042
1228
|
// For CalDAV, use the calendar URL; for others, use the source ID
|
|
1043
1229
|
const identifiers = [];
|
|
1044
1230
|
if (cal.calendarUrl) {
|
|
@@ -1054,8 +1240,8 @@ function updateEnabledCalendars() {
|
|
|
1054
1240
|
|
|
1055
1241
|
function updateLockedCalendars() {
|
|
1056
1242
|
const lockedCalendarIdentifiers = Array.from(loadedCalendars.values())
|
|
1057
|
-
.filter(cal => (cal as { locked?: boolean }).locked)
|
|
1058
|
-
.flatMap(cal => {
|
|
1243
|
+
.filter((cal) => (cal as { locked?: boolean }).locked)
|
|
1244
|
+
.flatMap((cal) => {
|
|
1059
1245
|
// For CalDAV, use the calendar URL; for others, use the source ID
|
|
1060
1246
|
const identifiers = [];
|
|
1061
1247
|
if (cal.calendarUrl) {
|